I Tried to Port Linux to an Obscure SoC. It Caught Fire.
Ported U-Boot, the Linux kernel, and Arch to an ancient SoC—then lost it to the magic smoke.
Introduction
What started as a weekend experiment to install Arch Linux turned into a full-scale resurrection: reverse-engineering legacy FEX files, manually tuning DRAM timings, enabling graphics, and even setting up JTAG debugging with an Arduino. I ported U-Boot. I got a mainline Linux kernel running. Arch booted. The framebuffer came alive. And then it caught fire.
I bought the CubieAIO-A20 years ago. Back then, it looked like the perfect little hacker machine: touchscreen, metal enclosure, SIM slot, USB ports, Wi-Fi—all in a compact form factor. It came preloaded with Android 4.2, which I immediately tried to replace with Linux. I failed. And like so many failed side projects, it went onto the shelf to gather dust.
Years later, after building UTMS—a programmable time modeling system—I started thinking about physical interfaces. Smart controller nodes. Something small, embedded, always on. And I remembered the Cubie. It had everything. All it needed was a modern OS.
What started as a weekend experiment to install Arch Linux turned into a full-scale resurrection effort: reverse engineering legacy FEX files, manually tuning DRAM timings, enabling graphics, and even setting up JTAG debugging using an Arduino Uno R3. I ported U-Boot. I got a mainline Linux kernel running. Arch booted. The framebuffer came alive.
And then the board started smoking. How did I get here?
Background and Context
The CubieAIO-A20 seemed like it should have been a serious contender. Designed as an all-in-one industrial panel PC, it packed a sturdy metal case, a touchscreen, Wi-Fi, a SIM slot, and a decent amount of I/O: multiple USB ports, audio jacks, IR, and GPIO headers. At its heart was the Allwinner A20—a dual-core ARM Cortex-A7 SoC paired with 1GB DDR3 RAM and onboard NAND storage.
It promised flexibility and durability. CubieTech even designed a modular compute core, called Einstein-A20, to make it easier for embedded projects to adopt. On paper, it was a polished, ready-to-deploy system for kiosks, dashboards, or smart controllers.
But despite this hardware promise, the board failed to gain traction. The timing was bad: by the time it launched, more powerful boards with active communities were sweeping the market. Raspberry Pi’s ecosystem was booming, and BeagleBone was solidifying its industrial foothold. The CubieAIO was stuck running ancient Android 4.2 or outdated, blob-heavy Linux images based on Linaro builds, with no modern, mainline kernel support.
If you’ve never heard of “Linaro Linux,” you’re not alone — neither had I. I assumed it was some obscure embedded distro, but it turns out it was never a real distribution at all. Linaro is (or was) an engineering consortium that provided ARM reference builds and toolchains, not end-user operating systems. Vendors like Cubietech took these experimental Ubuntu-based images — built with Linaro’s toolchains and patched kernels — and shipped them as if they were official OS releases. What I installed was an abandoned engineering demo, not a maintained platform.
The software ecosystem was its Achilles’ heel. Allwinner’s SDKs locked developers behind proprietary FEX configuration files, partial documentation, and confusing register-level details that were often missing or poorly explained. The community around CubieTech never materialized into an active developer base. No forums thrived, no upstream patches appeared, no mainline support came. It became an orphaned platform.
I first booted it years ago, briefly saw its outdated Android UI, tried and failed to get Linux running properly, and shelved it. The board gathered dust as the software landscape moved on without it.
When I decided to revisit it as a smart controller for UTMS, I knew I was diving into a forgotten ecosystem. What I didn’t expect was how deep and brutal the resurrection would be—and how the board would pay its dues with smoke.
Resurrection Begins
When I first tried flashing Armbian images made for the CubieBoard2 and CubieTruck, the screen stayed black. At first, I thought the board wasn’t booting at all—it looked dead. I had no idea what was going on, and honestly, that was where I threw in the towel the first time I bought the device. No output, no progress, nothing.
Only later did I realize the board was booting—the problem was the screen simply wasn’t working with those images. But I didn’t know that then. So, frustrated, I turned to the official CubieBoard download page for guidance.
That page was a disaster. Links to FTP servers were broken. Downloads were scattered across Mega, Baidu, and random HTTP mirrors. The English download section sent me to Mega, but navigating it felt like stepping into a digital ghost town. The comment sections were frozen in time—last active over a decade ago—with users complaining about the same lack of updates and support.
Despite the mess, I found three images labeled for the CubieAIO-A20: Android 4.2, Debian Jessie, and Linaro 14.04. I picked the Linaro image, flashed it on an SD card, and powered the board on. This time, something different happened—the board actually started booting and began installing the OS.
While it was doing its thing, I grabbed the installation guide linked alongside the image. It was painful to read. Broken English, vague instructions, cryptic warnings. A snippet read something like:
I had to guess what they meant. It felt like reading a bad high school homework assignment, not official docs.
The weirdest part: after flashing, the board shuts down automatically with no clear success signal. You have to power it back on manually to get any display. No “done” message. No progress bar. Just silence and waiting.
But I stuck with it. I waited for the “automatic shutdown,” powered it back on, and suddenly I was greeted by a working Linaro Linux. Finally, I was inside a familiar environment, able to run commands and explore the system freely.
This breakthrough was huge—it meant the board could run a modern Linux distro. But the screen issue I faced earlier hinted at deeper problems with hardware support, which I wouldn’t uncover until much later, after I had set up JTAG debugging.
Where the Boot begins: The Hunt for First Code
As exciting as it was to finally see the board boot into Linaro, the system it ran was so ancient I didn’t even want to connect it to the network. We’re talking kernel and userspace from the Snowden era—before modern mitigations, before systemd, before half the internet got TLS. The thing hadn’t seen a security patch in a decade. Whatever was running in there, it felt less like Linux and more like a digital biohazard.
Still, I wasn’t ready to give up. I wanted to understand how it booted—what made it tick—and figure out why no other OS would work. Maybe I could port something modern. So I started digging.
My attempts to boot newer images—Armbian, Arch Linux ARM, anything even vaguely modern—kept failing. Nothing worked. So I shifted gears and decided to treat the existing Linaro image as a forensic artifact. I mounted it, examined the filesystems, and started reverse engineering what I could from the live system.
The image had two partitions, but I focused on the boot partition first. That’s where the early-stage magic happens, and clearly something in there was being picky. Inside, I found three files:
-rw-r--r-- 1 root root 46808 Mar 23 2017 script.bin
-rw-r--r-- 1 root root 155 Mar 23 2017 uEnv.txt
-rw-r--r-- 1 root root 4975864 Mar 23 2017 uImage
I’d never seen a script.bin
before—at least, not in any modern system. I hadn’t installed Linux systems this old in a long time, and whatever this was, it looked... prehistoric. So I started researching it.
Turns out, script.bin
is a compiled version of a FEX file—a proprietary hardware description format used by older Allwinner SoCs before the Device Tree standard took hold. It tells the bootloader how to configure DRAM, GPIOs, display interfaces, voltages, clocks—basically everything the SoC needs to bring up the board.
If you’re dealing with Allwinner boards, you’ll run into “sunxi” sooner or later. It’s not a company, but a loose collective of hackers who basically rebuilt support for these neglected chips from scratch. The sunxi-tools were crucial in my workflow, and their wiki—linux-sunxi.org—felt like the only place where the board actually existed. While Allwinner dumped ancient blobs and vanished, the sunxi community quietly reverse-engineered the bootloaders, documented every weird register, and got many of these boards running on modern Linux. They did the vendor’s job, better than the vendor ever did.
I extracted the FEX file using the bin2fex tool, finally uncovering a human-readable hardware configuration hidden inside the binary blob. This file was essentially the board’s DNA — a detailed map of clocks, voltages, GPIO assignments, and storage parameters critical for initializing the hardware correctly.
Here’s a snippet from the extracted FEX:
[product]
version = "100"
machine = "cubietruck"
[platform]
eraseflag = 0
[target]
boot_clock = 912
dcdc2_vol = 1450
dcdc3_vol = 1300
ldo2_vol = 3000
ldo3_vol = 2800
ldo4_vol = 2800
storage_type = 0
power_start = 1
[clock]
pll3 = 297
pll4 = 300
pll6 = 600
pll7 = 297
pll8 = 336
[card_boot]
logical_start = 40960
sprite_gpio0 =
[card0_boot_para]
card_ctrl = 0
card_high_speed = 1
card_line = 4
sdc_d1 = port:PF00<2><1><default><default>
sdc_d0 = port:PF01<2><1><default><default>
sdc_clk = port:PF02<2><1><default><default>
sdc_cmd = port:PF03<2><1><default><default>
sdc_d3 = port:PF04<2><1><default><default>
sdc_d2 = port:PF05<2><1><default><default>
[...]
Interestingly, even though my board is a CubieAIO, this configuration file identified the machine as “cubietruck” — a close relative but not compatible out of the box. This mismatch explained why existing U-Boot images built for CubieTruck or CubieBoard2 failed on my device.
With this insight, I had a direction: use the FEX as a base to build a modern U-Boot configuration for the CubieAIO. Unlike ancient boot blobs, modern Allwinner boards rely on U-Boot with Device Tree to handle hardware init cleanly. If I could map the FEX data to U-Boot’s platform config and DRAM setup, I’d finally control the boot chain—and from there, chainload any OS I wanted.
This was the foundation for my resurrection effort — turning legacy blobs into actionable hardware knowledge and laying the groundwork for a usable, up-to-date system.
While doing all of this, I also had the idea to connect the board to a monitor via HDMI. Until now, I was relying solely on the built-in LCD to see anything—bad idea, in hindsight. One day, just for fun, I inserted an Armbian SD card and powered it on with HDMI attached. Boom. U-Boot output. The board was alive. It couldn't get to the kernel, but I was inside U-Boot.
That changed everything.
I spent hours poking around the U-Boot shell, trying to debug bootargs, kernel loading paths, memory layouts. But nothing worked. This board wasn’t the same as the CubieBoard2 or CubieTruck—even if it shared the same SoC. Something wasn’t lining up.
So I cloned mainline U-Boot, grabbed the Cubietruck_defconfig
as a starting point from the configs/ directory., and began hacking together a config for the AIO. It should’ve been a minor patch. It wasn’t.
The CubieTruck config was a minimal bootloader with just enough to bring up memory and storage. But the AIO needed proper GPIO, PMIC, USB PHYs, even HDMI and SATA support if I wanted to use the full board. That meant digging through the messy sprawl of Kconfig options, enabling frameworks like CONFIG_DM
, CONFIG_PINCTRL
, and power regulators, then carefully layering in just the peripherals I needed. Even now I’m not entirely sure which of those options were necessary and which not, but somehow I made it all work together eventually.
Eventually I had a build that seemed solid—U-Boot booted, gave me logs, showed signs of life. But there was a problem: USB didn’t work. No keyboard, no input, no way to interact with the shell. And that meant I needed serial.
I didn’t have a USB-UART adapter lying around—but I did have an Arduino Uno R3. I wondered if I could abuse it as a dumb serial passthrough. Turns out, I could.
First of all, I had to find the UART pins on the board, so I disassembled the device, and they weren’t this hard to find:
Here’s what my whole setup looked like:
And here’s how the wiring works:
I lobotomized the Arduino by wiring RESET to GND, so the microcontroller stayed inert. Then I hooked up its TX/RX to the Cubie’s serial pins and ran screen on my laptop. At first—nothing. No output. I shorted RX and TX on the Arduino just to test the loopback, and it echoed my keystrokes fine. So I swapped the Cubie TX/RX wires—because UART pin labeling is a mess—and boom. U-Boot logs, scrolling across my terminal. I was in.
This was the turning point. The screen came alive. I could finally see what the bootloader was doing—not through the guesswork of a blank LCD, but directly, precisely, and on my terms..
Debugging USB and Migrating to Device Tree
With serial access finally unlocked, I had visibility into the bootloader. U-Boot was alive—but USB wasn’t. No keyboard input, no device detection, just U-Boot logs complaining that it couldn’t find anything on the USB bus. That was a problem, because without USB, I couldn’t even interact with the system directly.
The cause wasn’t obvious, but I had a working theory: the old FEX config was lying to me.
Remember, I had extracted this FEX using bin2fex
, and it even proudly announced:
[product]
version = "100"
machine = "cubietruck"
But this wasn’t a Cubietruck. And despite sharing some lineage, the USB layout clearly didn’t match. I needed to port this FEX config into a proper Device Tree Source (DTS) file—the modern standard used by U-Boot and Linux to describe hardware. This wasn't a simple rename-and-go job. FEX is a quirky INI-style format; DTS is hierarchical, symbolic, and tied to the kernel’s driver model. The translation required meticulous, manual work.
This wasn’t just a rename-and-go job. FEX is a janky INI-style format used in ancient Allwinner bootflows, while DTS is hierarchical, symbolic, and tied closely to the kernel’s driver model. The translation wasn’t clean.
So I did it manually. I took the extracted FEX as my ground truth, compared it to a known-good Cubietruck DTS from upstream U-Boot, and then rebuilt a custom .dts
file line by line. I spent hours cross-referencing the FEX, a known-good Cubietruck DTS, and the A20 datasheet, matching FEX keys to their Device Tree counterparts...
I constantly cross-checked: what does the FEX say? What pins are mapped? Does this line up with what U-Boot is generating at runtime? It was meticulous, but it worked. After flashing the new U-Boot with my patched Device Tree, USB devices started responding. Still no OS yet, but now I had I/O—and, crucially, control.
The DRAM Puzzle and the Memory Test Nightmare
With USB finally cooperating, I thought I’d earned a breather. Instead, the board greeted me with two equally stubborn failure modes: either the display went dead right after the last boot.cmd
message, or it just froze there, stuck mid-boot as if taunting me.
I went into full bootloader–kernel handshake troubleshooting mode. I reused the same device tree source from my earlier bootloader work, compiled a matching kernel image, and started experimenting. Some attempts were bare-bones manual — mkimage
-wrapped zImage
and DTB, loading them into memory, and issuing the boot commands by hand in U-Boot. Others were scripted into a boot.cmd
that U-Boot would turn into a boot.scr
so the whole sequence could run on its own. I even rolled the dice with an older cross-compiler, just to see if some subtle toolchain change was sabotaging the boot process.
Nothing changed the fact that the board either stared back at me with a frozen boot log or blinked into blackness.
I crafted a memtest script inside the bootloader and watched as it churned through the RAM. And then it hit me: errors everywhere. Not just a few glitches, but waves of faults scattered across the address space. The board looked like it had a dead or defective RAM chip.
I went back through the kernel’s menuconfig
, scanning for any options that could plausibly influence my setup—DDR3 support, memory controller settings, bus widths, and so on. These had been sitting at their defaults; I hadn’t previously tuned them for this board, but I wanted to confirm nothing obvious was missing.
As another angle, I even swapped out my cross-compiler, moving from GCC 13.x down to the 12.3.rel1 release in case some subtle codegen quirk was creeping in. No dice—the behavior was unchanged.
At that point, frustration pushed me into U-Boot’s source tree, specifically the DRAM initialization code under board/sunxi. The stock generic auto-configuration driver (dram_sun4i_auto.c) clearly wasn’t ideal for my CubieAIO. Most boards with stable bring-up had tightly tuned, board-specific DRAM init code. Since there was no dram_cubieaio-a20.c
anywhere in tree, I wrote my own, using those tuned examples as a reference. This gave me a second DRAM configuration path to test—one explicitly crafted for my board instead of relying on the generic heuristics.
I wrote my own version of that file, painstakingly cross-referencing:
The Allwinner A20 SoC technical reference manual
The SK Hynix DDR3 datasheet for H5TQ4G63AFR chips soldered on the board
The legacy FEX config values dumped from the original Linaro system
This structure defined clock speed, memory type, rank count, density, bus width, CAS latency, and most importantly, the timing registers (tpr0
through tpr5
) and EMR (extended mode registers).
The single most elusive parameter was the DQS Gating Delay, a subtle calibration value needed to synchronize data strobes with the clock. The the linux-sunxi wiki described it as an experimentally discovered window, often narrow and unique to each board's physical layout.
I tried dozens of values, monitoring boot success and memory errors. To help, I leveraged ssvb’s a10-dram-tools, a set of utilities that read live DRAM controller registers from a running Linaro kernel. This gave me feedback about the timings that a known-good system was using.
The closer I got to the documented “working” window, the fewer errors appeared. But still, the memtest reported failures. Confused, I analyzed the failing addresses: they clustered suspiciously around 0x79f5xxxx
.
After a eureka moment, it dawned on me — this memory region was reserved and actively used by U-Boot itself for storing the Flattened Device Tree (FDT) blob. I was hammering on live memory in use. No wonder it failed.
Using U-Boot’s bdinfo command, I confirmed the FDT location. I then adjusted my memtest to exclude this range, and suddenly, all memory tests passed without a single error.
The RAM wasn’t broken. I was just shooting myself in the foot.
With this revelation, I finally had:
A working custom DRAM init tailored precisely for CubieAIO hardware
Verified stable memory with no errors beyond reserved regions
USB working alongside serial console for full I/O access
At last, the hardware was stable enough to boot a mainline Linux kernel — the true rebirth.
Bringing up Graphics and the Fatal Test
With the DRAM init solid and the kernel happily booting, I finally had a working shell. First on Armbian—booted on the very first try, no hacks, no debugging, just straight in. I thought, finally, I’m in familiar territory. I swapped out Armbian’s rootfs for my own Arch setup, and to my surprise, it still worked. The only hiccup was the root partition not being detected on the first go, but that was a solved problem in my book—two edits later, it was booting cleanly into Arch.
But the win wasn’t complete. I had no graphical interface at all. HDMI was dead, and the LCD panel was equally lifeless. My only interaction points were JTAG and a USB keyboard—no framebuffer output, no X, no console. That’s when I realized the graphics stack was actually disabled in my kernel config. It wasn’t even trying to light up the display. So I recompiled the kernel with the necessary graphics options enabled, put all the pieces back together, and prepared for the moment of truth—finally seeing something on HDMI or the LCD.
I powered it on. And then—smoke. A thin, unmistakable wisp, curling up from the board. My stomach dropped. I killed power instantly, heart racing. No obvious scorch marks, no melted traces—just that acrid smell.
The end of the road
Still hoping it might have been some transient glitch, I decided to boot the original Linaro image—maybe my kernel tweaks were somehow overdriving something. I powered it on again. This time, within a second, more smoke. That was it. Game over.
With no soldering and electronics repair skills, and knowing that hiring a shop to diagnose and fix it would cost more than the board’s worth, the decision was made for me. Even if I replaced it, there’d be no reason to buy an ancient, unsupported platform again. A newer board would make far more sense for the smart home controller I’d been building toward.
So the CubieAIO project ends here—not in success, but in a smoking crater of lessons learned. Still, it wasn’t a waste. The deep dive into U-Boot, DTS porting, DRAM tuning, and kernel bring-up taught me more than I expected, and every scrap of work I did will be open-sourced on my GitHub for anyone who wants to continue where I left off (after I clean up some stuff to make it open source). It might have died on my desk, but maybe it’ll live on in someone else’s hands.
I set out to bring Linux back from the dead. Instead, I gave this board the most metal funeral imaginable: death by mainline kernel. Rest in peace, CubieAIO. Your sacrifice taught me more than success ever could.
Theories on the Smoke: That first wisp from the power region suggests a voltage regulator (PMIC) or capacitor failure – possibly aged components stressed by my DRAM/GPU initialization. When smoke later poured from beneath the Einstein module, it pointed to hidden damage under the SoC or PCB layers. I see no visible burns. That’s classic for short-circuited internal traces or a fried power plane – flaws only microscopes or multimeters catch. If I had to bet: decade-old capacitors finally gave out when the mainline kernel enabled power-hungry subsystems. But I don’t know for sure – and that’s the haunting beauty of resurrecting the dead.
P.S. If you have any suggestions or ideas on what I could do next with this device or the project, please drop a comment below. For now, I’m keeping it as a trophy—a reminder of the board I literally fried while pushing its limits. But who knows? Maybe there’s still some resurrection left in it. I’d love to hear your thoughts.