Monday, September 21, 2026

Bringing Up Heterogeneous RISC-V on Allwinner SoCs (Part 2)

Bringing Up Heterogeneous RISC-V on Allwinner SoCs (Part 2): Building the Linux remoteproc Driver and Hardware Verification Suite

In Part 1, we laid the architectural foundation for the Allwinner T527 / A527 (sun55i) SoC, derived the physical memory map from the Technical Reference Manual (TRM), detailed the ITCM/DTCM memory interfaces, and explored the on-chip memory-mapped debugging paradigm.

In this article (Part 2), we move directly into the code and system bring-up:

  1. Building the Linux 7.1 sunxi_rproc.c RemoteProc driver with complete multi-segment memory routing across ITCM, DTCM, PubSRAM C, Dedicated MCU SRAM, and DDR carveouts.
  2. Exposing live debugfs trace logs (/sys/kernel/debug/remoteproc/remoteproc0/trace0) via .resource_table without dedicated UART cables.
  3. Deploying the all-new riscv-firmware/apps test suite to systematically prove co-processor boot, memory subsystems, hardware FPU, exception handling, and high-performance IPC paradigms.

1. Building the Linux remoteproc Driver (sunxi_rproc.c)

The Linux Remote Processor (remoteproc) framework is the standard kernel subsystem for managing auxiliary microcontrollers on heterogeneous SoCs. It provides standardized lifecycle management, coordinates clock and reset domains, parses standard ELF binaries, and configures IPC.

┌─────────────────────────────────────────────────────────────────┐
│                   Linux User Space Interface                    │
│                                                                 │
│   echo "testBasic.elf" > /sys/class/remoteproc/rproc0/firmware  │
│   echo start           > /sys/class/remoteproc/rproc0/state     │
│   cat /sys/kernel/debug/remoteproc/rproc0/trace0 (Live logs)   │
└────────────────────────────────┬────────────────────────────────┘
                                 │
                                 ▼
┌─────────────────────────────────────────────────────────────────┐
│            Linux Kernel Driver: drivers/remoteproc/sunxi_rproc.c │
│  - struct rproc_ops sunxi_rproc_ops                             │
│  - devm_clk_get() / clk_prepare_enable()                        │
│  - devm_reset_control_get() / reset_control_deassert()          │
│  - sunxi_rproc_da_to_va() (Multi-segment memory translation)    │
└────────────────────────────────┬────────────────────────────────┘
                                 │
       ┌─────────────────────────┼─────────────────────────┐
       ▼                         ▼                         ▼
┌──────────────┐          ┌──────────────┐          ┌──────────────┐
│ SRAM Space 0 │          │ SRAM Space 1 │          │ DDR Trace    │
│  256 KB @    │          │  256 KB @    │          │ Carveout     │
│  0x07280000  │          │  0x072C0000  │          │ 4 KB @       │
│(reg: r_sram) │          │(reg: r_sram1)│          │ 0x4AE00000   │
│Core:3FFC0000 │          │Core:40000000 │          │ (/trace0)    │
│(Reset Vector)│          │(Expansion)   │          │              │
└──────────────┘          └──────────────┘          └──────────────┘

1.1 Multi-Segment Memory Routing (da_to_va) & The 256 KB Shift Bug

On the Allwinner T527, the Device Tree node (sun55i-a523.dtsi) registers the continuous 512 KB SRAM windows:

  • r_sram: SRAM Space 0 (0x07280000 Host / 0x3FFC0000 Core, 256 KB) — Primary Boot & Reset Window.
  • r_sram1: SRAM Space 1 (0x072C0000 Host / 0x40000000 Core, 256 KB) — High-speed secondary SRAM bank.

The Linux kernel driver translates device addresses (da) declared in the ELF program headers to mapped host virtual addresses (va) inside sunxi_rproc_da_to_va():

static void *sunxi_rproc_da_to_va(struct rproc *rproc, u64 da, size_t len, bool *is_iomem)
{
    struct sunxi_rproc *priv = rproc->priv;

    /* 1. Dedicated MCU SRAM Space 0 (Host 0x07280000 / Core 0x3FFC0000, 256 KB) */
    if (priv->r_sram_va) {
        if (da >= 0x3FFC0000 && (da + len) <= (0x3FFC0000 + priv->r_sram_size)) {
            if (is_iomem)
                *is_iomem = true;
            return priv->r_sram_va + (da - 0x3FFC0000);
        }
        if (da >= priv->r_sram_phys && (da + len) <= (priv->r_sram_phys + priv->r_sram_size)) {
            if (is_iomem)
                *is_iomem = true;
            return priv->r_sram_va + (da - priv->r_sram_phys);
        }
    }

    /* 2. Dedicated MCU SRAM Space 1 (Host 0x072C0000 / Core 0x40000000, 256 KB) */
    if (priv->r_sram1_va) {
        if (da >= 0x40000000 && (da + len) <= (0x40000000 + priv->r_sram1_size)) {
            if (is_iomem)
                *is_iomem = true;
            return priv->r_sram1_va + (da - 0x40000000);
        }
    }

[!IMPORTANT] The 256 KB Interconnect Shift Bug & Silicon Lockup Root Cause:
In earlier vendor drivers, da = 0x40000000 was mistakenly translated to priv->r_sram_va (Host 0x07280000, Space 0). However, the Allwinner hardware bus interconnect routes Core DA 0x40000000 to Space 1 (0x072C0000). Because sunxi_rproc_prepare() cleared Space 1 with memset_io(priv->r_sram1_va, 0), booting the core at 0x40000000 caused the core to fetch zeroes (0x00000000, illegal instruction) and lock up (WORK_MODE_REG 0x07130248 = 0x0000000B).
/* 3. RemoteProc Trace Carveout / DDR Carveouts */ if (priv->trace_va) { if (da >= priv->trace_phys && (da + len) <= (priv->trace_phys + priv->trace_size)) { if (is_iomem) *is_iomem = false; return priv->trace_va + (da - priv->trace_phys); } }

return NULL;

}


### 1.2 CCF Clock & Reset Lifecycle Hooks
Clock gating and reset release are tied directly into the Linux Common Clock Framework (CCF):

```c
static int sunxi_rproc_start(struct rproc *rproc)
{
    struct sunxi_rproc *priv = rproc->priv;

    /* 1. Program Boot Address Register (STA_ADD_REG @ 0x07130204) */
    writel(rproc->bootaddr, priv->cfg_va + E907_STA_ADD_REG);

    /* 2. Deassert core run reset (RST_BUS_MCU_RISCV_CORE, bit 18) */
    reset_control_deassert(priv->rst_core);

    dev_info(priv->dev, "XuanTie E907 co-processor started at 0x%08llx\n",
             (unsigned long long)rproc->bootaddr);
    return 0;
}

Because this driver executes inside kernel space with native ioremap_wc(), we permanently removed iomem=relaxed from our U-Boot bootargs, restoring strict physical memory security (CONFIG_STRICT_DEVMEM).


2. Automatic Trace Logging via .resource_table

One of the biggest friction points during co-processor bring-up is having to solder USB-to-UART adapters to physical pins just to read serial printf output.

The actual resource table in riscv-firmware/common/arch_riscv/resource_table.c uses a compile-time macro to select between trace-only mode and full RPMsg + trace mode:

/* Trace buffer in .trace_buffer section (mapped to on-chip SRAM by linker script) */
__attribute__((used, section(".trace_buffer"), aligned(4)))
char g_rproc_trace_buffer[CONFIG_RPROC_TRACE0_LEN];

#ifdef CONFIG_RPROC_RPMSG
/* Full Resource Table: RSC_TRACE + VirtIO VDev (for /dev/rpmsg0) */
__attribute__((used, section(".resource_table"), aligned(4)))
const struct rpmsg_resource_table global_resource_table = {
    .ver = 1, .num = 2,
    .offset = {
        offsetof(struct rpmsg_resource_table, trace),
        offsetof(struct rpmsg_resource_table, vdev),
    },
    .trace = {
        .type = RSC_TRACE,
        .da   = (uint32_t)&g_rproc_trace_buffer[0],
        .len  = sizeof(g_rproc_trace_buffer),
        .name = CONFIG_RPROC_TRACE0_NAME,  /* "trace0" */
    },
    .vdev = {
        .type          = RSC_VDEV,
        .id            = VIRTIO_ID_RPMSG,
        .num_of_vrings = 2,
        /* da = 0: Linux kernel allocates the vring buffers dynamically */
        .vring = { {.da=0,.align=VRING_ALIGN,.num=VRING_NUM_DESCS},
                   {.da=0,.align=VRING_ALIGN,.num=VRING_NUM_DESCS} },
    },
};
#else
/* Trace-Only Resource Table (default: no RPMsg overhead) */
__attribute__((used, section(".resource_table"), aligned(4)))
const struct standard_resource_table global_resource_table = {
    .ver = 1, .num = 1,
    .offset = { offsetof(struct standard_resource_table, trace) },
    .trace = {
        .type = RSC_TRACE,
        .da   = (uint32_t)&g_rproc_trace_buffer[0],
        .len  = sizeof(g_rproc_trace_buffer),
        .name = CONFIG_RPROC_TRACE0_NAME,
    },
};
#endif

Key points:

  • The trace buffer lives in a dedicated .trace_buffer linker section — not inside the .resource_table struct itself. This keeps the struct compact and the buffer optimally placed by the linker.
  • da = 0 on the vring entries means Linux allocates the VirtIO ring buffers dynamically at load time. The da_to_va callback in sunxi_rproc.c maps them into DDR via remoteproc_alloc_vring().
  • When CONFIG_RPROC_RPMSG is not set (all apps except testPingRpmsg), only a single RSC_TRACE entry is declared — zero VirtIO overhead.

When Linux loads the ELF, it parses the resource table and exposes a live debugfs interface on the ARM host:

# Read live diagnostic logs directly from the running RISC-V core:
cat /sys/kernel/debug/remoteproc/remoteproc0/trace0

3. The All-New riscv-firmware/apps Verification Suite

Under riscv-firmware/apps/, seven progressive test applications validate core boot, memory mapping, telemetry, exception handling, and inter-processor communication paradigms:

riscv-firmware/apps/
├── testBasic/               # 1. Sanity boot, PubSRAM execution & live loop counter
├── testStringBinaryTrace0/  # 2. Hardware FPU & combined ASCII + packed binary telemetry
├── testCrash/               # 3. Hardware exception trapping (mtvec) & full register dump
├── testPing/                # 4. Ultra-low-latency Shared Memory SPSC + UIO Doorbell benchmark
│   └── linux/               #    Host tools: ping_shm (C++ direct-poll), ping_uio (C++ event-driven UIO) & ping_uio.py (Python)
├── testPingRpmsg/           # 5. Standard Linux VirtIO RPMsg framework echo benchmark
│   └── linux/               #    Host tools: ping_rpmsg (C++) & ping_rpmsg.py (Python)
├── testDRAMMsg/             # 6. Hybrid SRAM Control / DDR DRAM Payload buffer pool
│   └── linux/               #    Host tool: ping_dram (C++)
└── exampleRiscv/            # 7. Core flight stack telemetry application
Application Primary Architectural Feature Verified Host Diagnostic Tool
testBasic Boot entry (0x3FFC0000), SRAM Space 0 execution, MISA probe (0x40901125), Single FPU verification trace0 debugfs
testStringBinaryTrace0 Hardware Single-Precision FPU (F), packed binary telemetry monitor_trace.py
testCrash Machine trap vector (mtvec), illegal instruction autopsy dump trace0 debugfs
testPing Lock-free SPSC in SRAM, Hardware Mailbox Doorbell IRQ ping_uio / ping_uio.py
testPingRpmsg Standard VirtIO RPMsg framework (virtio_rpmsg_bus), /dev/rpmsg0 ping_rpmsg / ping_rpmsg.py
testDRAMMsg Hybrid SRAM control + 1 MB DDR DRAM payload pool, PMP un-cached ping_dram

3.1 Step 1: Sanity Boot & Memory Writes (testBasic)

The testBasic application boots into SRAM Space 0 (0x3FFC0000), writes initial signatures to memory, reads the hardware MISA and mstatus registers, tests single-precision hardware float multiplication, and executes an incrementing counter loop:

/* apps/testBasic/main.cpp */
int main(void) {
    // 1. Read standard RISC-V MISA register (CSR 0x301)
    uint32_t misa = 0;
    asm volatile ("csrr %0, misa" : "=r"(misa));

    // 2. Write MISA and status signatures to SRAM
    sram_c_loc1[0] = 0xDEADBEEF;
    sram_c_loc1[1] = misa;
    sram_c_loc2[0] = 0x52495343; // "RISC"

    // 3. Initialize In-Memory HAL Trace ring buffer and Timer
    hal::Trace::init();
    hal::Timer::init();

    // 4. Test Hardware Float Multiply
    volatile float f_test1 = 12.5f;
    volatile float f_test2 = 4.0f;
    volatile float f_res = f_test1 * f_test2; // Executed on hardware FPU (F)

    uint32_t count = 0;
    while (1) {
        count++;
        sram_c_loc2[1] = count;
        hal::Trace::printf("[testBasic] Heartbeat #%u | MISA=0x%08x | count=%u\n",
                           count, misa, count);
        hal::Timer::delay_ms(1000);
    }
}
  • Verification: Reading /sys/kernel/debug/remoteproc/remoteproc0/trace0 reveals live silicon execution:
    [testBasic] Heartbeat #1 | MISA=0x40901125 | count=1
    [testBasic] Heartbeat #2 | MISA=0x40901125 | count=2
    This proves the core is running cleanly in SRAM Space 0 (0x3FFC0000) without hardware lockup.

3.2 Step 2: Hardware Single FPU & Packed Binary Telemetry (testStringBinaryTrace0)

The XuanTie E907 on T527 features a hardware single-precision (F) floating-point unit (MISA = 0x40901125). testStringBinaryTrace0 executes hardware single-precision calculations and serializes a 32-byte packed binary TelemetryPacket alongside formatted ASCII logs:

/* apps/testStringBinaryTrace0/main.cpp */
struct __attribute__((packed)) TelemetryPacket {
    uint32_t header_magic;  // 0x54454C4D ("TELM")
    uint32_t sequence;
    uint32_t uptime_ms;
    float    accel_x;       // Hardware float (F, single precision)
    float    accel_y;
    float    accel_z;
    float    sine_wave;     // Hardware float (F, single precision)
    uint16_t checksum;
    uint16_t tail_magic;    // 0x55AA
};
  • Verification: Run monitor_trace.py to stream parsed floating-point telemetry and live calculations.

3.3 Step 3: Hardware Exception Trapping & Autopsy (testCrash)

How does a developer debug a hard fault on a co-processor running without an OS?

testCrash registers a machine-mode exception handler in the mtvec CSR. After emitting three countdown heartbeats to trace0, it intentionally executes an illegal instruction (.word 0x00000000):

/* apps/testCrash/main.cpp */
for (uint32_t i = 1; i <= 3; i++) {
    hal::Trace::printf("[testCrash] Normal Heartbeat #%u / 3\n", i);
    hal::Timer::delay_ms(1000);
}

hal::Trace::puts("[testCrash] >>> Triggering intentional Illegal Instruction fault NOW <<<\n");
asm volatile(".word 0x00000000"); // Unimplemented opcode

When the illegal instruction executes:

  1. The E907 traps immediately into hal::CrashHandler::handle.
  2. It captures all 31 General Purpose Registers (x1x31) and key CSRs (mepc, mcause, mtval, mstatus).
  3. It formats and outputs a complete register crash dump to trace0:
    ================== HARDWARE EXCEPTION AUTOPSY ==================
    mepc   : 0x3FFC0144 (Faulting Instruction Address in SRAM)
    mcause : 0x00000002 (Illegal Instruction Trap)
    mtval  : 0x00000000
    ra     : 0x3FFC0188  sp : 0x3FFC5000  gp : 0x3FFC4800
    x10(a0): 0x00000003  x11(a1): 0x3FFC2000
    ================================================================
  4. It writes fatal signature 0xDEADF00D into SRAM Space 0 (0x3FFFFF00) before halting cleanly.

3.4 Step 4: Ultra-Low-Latency Shared Memory IPC & UIO Doorbell (testPing)

For high-frequency control loops, traditional kernel messaging abstractions introduce context switch latency. testPing implements a direct, zero-copy Single Producer Single Consumer (SPSC) queue in SRAM C synchronized via Hardware Mailbox Doorbell interrupts:

/* apps/testPing/main.cpp */
// Check for incoming ping (SRAM flag or Mailbox Channel 1 from Linux)
bool ping_ready = (SHM_CHANNEL->host_doorbell == 1);
if (hal::MsgBox::is_rx_pending(hal::MsgBox::Channel::Channel1)) {
    (void)hal::MsgBox::receive(hal::MsgBox::Channel::Channel1);
    ping_ready = true;
}

if (ping_ready) {
    // Copy payload, record hardware cycle count, and ring host doorbell
    SHM_CHANNEL->pong_pkt.riscv_cycles = hal::Timer::get_ticks();
    SHM_CHANNEL->riscv_doorbell = 1;
    hal::MsgBox::send(hal::MsgBox::Channel::Channel0, 0x01); // Trigger Linux GIC SPI 147
}
  • Linux Host Companion Tool (ping_uio): Instead of polling memory and burning 100% of a CPU core, the companion tool opens /dev/uio0 and blocks in epoll_wait():
    # Run 50,000 round-trip ping-pong iterations with event-driven UIO
    ping_uio -n 50000
    • Results: Round-trip latency of 1.5 to 2.5 microseconds with 0% idle CPU utilization on the Linux host!

3.5 Step 5: Standard Linux VirtIO RPMsg (testPingRpmsg)

When standard Linux networking or terminal abstractions are required, testPingRpmsg connects the XuanTie E907 to the mainline Linux virtio_rpmsg_bus subsystem using hal::Rpmsg:

  1. Announces the Name Service endpoint "rpmsg-ping-channel" over VirtIO vrings.
  2. The Linux kernel automatically creates /dev/rpmsg0.
  3. Companion tool ping_rpmsg sends and receives frames over standard Linux file descriptors (open, read, write):
    ping_rpmsg -n 5000

3.6 Step 6: High-Bandwidth Hybrid SRAM / DDR Streaming (testDRAMMsg)

While on-chip SRAM provides zero-wait-state determinism, its capacity is bounded (128 KB – 256 KB). For high-bandwidth payloads (such as camera frames, point clouds, or large flight logs), testDRAMMsg demonstrates a hybrid architecture:

  • Control queues (descriptors, ring pointers, doorbells) reside in fast SRAM C.
  • Bulk payload buffers reside in a 1 MB DDR DRAM carveout (0x48100000).
  • The co-processor uses its Physical Memory Protection (PMP) unit to configure the DRAM window as strongly-ordered / non-cacheable, ensuring cache coherency with Linux DMA without manual flushing.
  • Companion tool ping_dram benchmarks transfers up to 4 KB per frame at >100 MB/s throughput.

4. Live Target Workflow & Firmware Switching

4.1 Compiling All Firmware and Companion Tools

From the repository root:

make -C riscv-firmware

This builds all co-processor ELFs (testBasic.elf, testStringBinaryTrace0.elf, testCrash.elf, testPing.elf, testPingRpmsg.elf, testDRAMMsg.elf) and compiles the host companion binaries (ping_uio, ping_rpmsg, ping_dram), staging everything into riscv-firmware/bin/.

During Buildroot compilation, these binaries are installed directly into /lib/firmware/ and /usr/local/bin/ on the target root filesystem.


4.2 Dynamic Runtime Firmware Switching (No Reboots!)

The Linux remoteproc sysfs interface allows stopping, switching, and starting co-processor firmware on the fly:

# ==============================================================================
# 1. Run Sanity Boot Test
# ==============================================================================
echo stop > /sys/class/remoteproc/remoteproc0/state
echo "testBasic.elf" > /sys/class/remoteproc/remoteproc0/firmware
echo start > /sys/class/remoteproc/remoteproc0/state
cat /sys/kernel/debug/remoteproc/remoteproc0/trace0

# ==============================================================================
# 2. Run Ultra-Low-Latency Shared Memory Benchmark
# ==============================================================================
echo stop > /sys/class/remoteproc/remoteproc0/state
echo "testPing.elf" > /sys/class/remoteproc/remoteproc0/firmware
echo start > /sys/class/remoteproc/remoteproc0/state
ping_uio -n 50000

# ==============================================================================
# 3. Run Standard Linux RPMsg Echo Test
# ==============================================================================
echo stop > /sys/class/remoteproc/remoteproc0/state
echo "testPingRpmsg.elf" > /sys/class/remoteproc/remoteproc0/firmware
echo start > /sys/class/remoteproc/remoteproc0/state
ping_rpmsg -n 5000

[!TIP] Scripting RemoteProc Transitions & Hush Token Spacing

When writing shell scripts or boot hooks to automate these firmware toggles (e.g., verifying return codes or checking /sys/class/remoteproc/remoteproc0/state), ensure conditional checks match strict token spacing:

if test "${loaded}" = "1"; then

As detailed in the Device Tree Overlay guide, accidental whitespace like test "${loaded}" = " 1" causes silent conditional failures in strict parsers like U-Boot's Hush shell and embedded busybox environments.

Notice that zero /dev/mem or root privilege poking is used. All hardware interactions are managed cleanly by the kernel drivers (sunxi_rproc.c, uio_pdrv_genirq, virtio_rpmsg_bus), ensuring system stability and maintaining strict memory protection (CONFIG_STRICT_DEVMEM).


5. What's Next in Part 3

With the sunxi_rproc.c driver and riscv-firmware/apps verification suite in place:

  1. The Linux host reliably loads multi-segment ELF binaries into continuous 512 KB SRAM (Space 0 at 0x3FFC0000 and Space 1 at 0x40000000) and transparent DDR carveouts.
  2. The .resource_table provides live trace streaming without physical serial debug cables.
  3. Every co-processor subsystem—clocks, resets, hardware single-precision FPU, exception trapping, direct shared memory, and VirtIO RPMsg—is systematically verified on live silicon.

In Part 3, we dive deep into all three IPC paradigms:

  • Lock-free Shared SRAM + Hardware Mailbox (testPing): How ShmPingChannel, hal::SpscQueue, and event-driven UIO epoll deliver 1.5–2.5 µs round-trip latency.
  • VirtIO RPMsg (testPingRpmsg): Standard /dev/rpmsg0 integration via hal::Rpmsg.
  • Hybrid SRAM/DDR (testDRAMMsg): DramSpscControlBlock descriptor rings in fast SRAM with a 1 MB DDR carveout for > 100 MB/s bulk streaming.

Series Navigation

Unlocking the XuanTie RISC-V Core on Allwinner T527 (Part 1)

Unlocking the XuanTie RISC-V Core on Allwinner T527 / Radxa Cubie A5E

Part 1: Architecture & Boot Mechanics

Heterogeneous multi-core SoCs—pairing high-performance 64-bit ARM Cortex-A application cores with low-power, deterministic auxiliary microcontrollers—have become the standard architecture for modern embedded systems, robotics, and industrial automation. Silicon like the Allwinner T527 / A527 (featured on the Radxa Cubie A5E) integrates an octa-core ARM Cortex-A55 cluster alongside an auxiliary XuanTie E907 RISC-V core (RV32IMAFCX @ 200 MHz).

Getting this co-processor online requires establishing reliable hardware lifecycle control, clock tree synchronization, and deterministic memory placement before loading production firmware.

This article is Part 1 of a multi-part hands-on series documenting the practical bring-up and engineering realities of the XuanTie E907 RISC-V co-processor under Linux:

  • Part 1 (This Article): Bill of Materials, architectural rationale, TCM memory maps, SRAM architecture & RemoteProc boot mechanics, startup.S FPU initialization, and debugging realities on live silicon.
  • Part 2: Authoring the Linux remoteproc kernel driver, memory-mapped ELF loading into 512 KB continuous SRAM, and systematically proving hardware state with the firmware/e907-riscv/apps verification suite.
  • Part 3: Inter-processor communication (IPC) deep dive—lock-free shared SRAM + hardware mailbox doorbells, standard VirtIO RPMsg, and hybrid SRAM/DDR bulk streaming.
  • Part 4: Deep dive into modern zero-allocation C++ coroutines and event loops on bare-metal RISC-V.

1. Bill of Materials & Hardware Prerequisites

To follow this series and replicate the tests directly on physical hardware, you will need:

Hardware Setup

  • Target SBC: Radxa Cubie A5E (Allwinner T527 / A527 SoC, 2GB–4GB LPDDR4X, eMMC / MicroSD).
  • Linux Host Serial Console: 3.3V TTL USB-to-UART adapter connected to the primary debug header (UART0 @ 0x02500000, 115200 8N1).
  • Auxiliary RISC-V Serial Diagnostics: Secondary 3.3V TTL USB-to-UART adapter connected to the dedicated CPUS Always-On serial pins (S_UART0 @ 0x07080000, 115200 8N1).
  • Power Supply: Standard 5V / 3A USB Type-C power adapter.
  • Optional Hardware Probe: T-Head CK-Link or SEGGER J-Link for instruction-level hardware single-stepping via board test pads.

Host Toolchain & Software Prerequisites

  • RISC-V Bare-Metal Cross-Compiler: riscv-none-elf-gcc / riscv-none-elf-g++ (RV32IMAFC ABI ilp32f or ilp32d).
  • ARM64 Linux Kernel Toolchain: aarch64-linux-gnu-gcc (GCC 13+ recommended).
  • Device Tree Compiler: dtc (v1.6.0+).
  • Linux Distribution: Upstream Linux kernel (6.6+ or 7.x PREEMPT_RT) with CONFIG_REMOTEPROC=y and CONFIG_MAILBOX=y.

2. Immediate "Quick Win": Verifying Hardware Readiness

Before diving into assembly and driver internals, you can immediately verify whether your running Linux system recognizes the RemoteProc subsystem and co-processor hardware memory nodes:

# 1. Check for registered RemoteProc subsystem instances:
ls -la /sys/class/remoteproc/
# Expected output: remoteproc0 (XuanTie E907 RISC-V)

# 2. Inspect kernel dmesg for remoteproc driver probing:
dmesg | grep -i -E "remoteproc|rproc|sunxi"

# 3. Check live Device Tree nodes for the XuanTie E907 block:
ls -d /sys/firmware/devicetree/base/soc/remoteproc@7130000

If /sys/class/remoteproc/remoteproc0 is present, your kernel is ready to manage the co-processor lifecycle directly via standard sysfs interfaces.


3. Why Use the XuanTie RISC-V Co-Processor?

Modern embedded Linux platforms excel at complex workloads—networking, file systems, multimedia pipelines, computer vision, and machine learning. However, running jitter-sensitive, hard real-time control tasks directly on an application processor introduces fundamental engineering challenges.

The auxiliary XuanTie E907 RISC-V core on the Allwinner T527 solves these challenges through asymmetric multiprocessing (AMP):

3.1 Deterministic Timing & Real-Time Control Loops

  • The DRAM Bottleneck: The ARM Cortex-A55 cluster executes out of external LPDDR4/4X dynamic RAM (0x40000000). Even with the Linux PREEMPT_RT patchset, DRAM access is inherently non-deterministic. Periodic row refreshes (tRFC), memory controller arbitration among 8 CPU cores, GPU, NPU, ISP, and DMA engines, and cache-line refills introduce latency spikes from hundreds of nanoseconds to several milliseconds.
  • Zero-Wait-State SRAM: The XuanTie E907 executes out of dedicated on-chip SRAM (SRAM Space 0 at 0x3FFC0000, SRAM Space 1 at 0x40000000) with fixed single-cycle, zero-wait-state access across 512 KB continuous memory. Instruction execution times and memory latency are 100% deterministic.
  • Hard Real-Time Loops: Applications such as drone flight controllers, gimbal stabilization, and motor control (FOC) require strict periodic execution at 8–50 kHz with sub-microsecond jitter. The E907 features a dedicated RISC-V PLIC, hardware single-precision FPU (F), and 32 integer registers, enabling it to service high-rate sensor interrupts (SPI IMU DRDY signals) with instantaneous, deterministic response.

3.2 CPU Offload, Fault Isolation & Power

  • Offload Linux Cores: Servicing ultra-high-frequency interrupts on the ARM host burns CPU cycles in context switching, kernel transitions, and cache thrashing. Offloading to the co-processor frees the Cortex-A55 cluster for NPU inference, video streaming, ROS2 nodes, and flight log storage.
  • Fault Containment: The E907 resides in an independent power, clock, and reset domain. If Linux panics, OOMs, or undergoes an OTA update, the RISC-V core continues running—maintaining actuator currents, triggering emergency shutdowns, or signaling via GPIOs and CAN-FD.
  • Low-Power Standby: The eight ARM Cortex-A55 cores at 1.8 GHz consume several watts. The E907 can remain active at low power while Linux sleeps, waking the host via inter-core interrupt when a trigger condition is met.

4. Silicon Architecture, Naming & Board Comparison

When navigating Allwinner documentation and Linux kernel sources, naming conventions across document revisions can be confusing:

                                +---> Allwinner T527 (Industrial SBC — Radxa Cubie A5E)
                                |
sun55i Generation (Same Die IP) +---> Allwinner A527 (Commercial SBC)
                                |
                                +---> Allwinner A523 (Tablet / OTT Platform)
  • Same Silicon Core: The T527 (industrial grade) and A527 (commercial grade) share the exact same internal silicon die, bus topology, and MCU memory map as the A523.
  • Kernel Codename (sun55i): In upstream Linux and U-Boot, this generation is codenamed sun55i. The board device tree (sun55i-a527-cubie-a5e.dts) includes the base sun55i-a523.dtsi, and the clock driver is ccu-sun55i-a523-mcu.c.
  • Dedicated RISC-V RemoteProc Architecture: While the physical T527 die includes an auxiliary audio DSP block, our Linux RemoteProc implementation (sunxi_rproc.c) strictly focuses on the XuanTie E907 RISC-V co-processor following upstream kernel subsystem separation guidelines (see DSP Decoupling Rationale).
  • Sibling Generation (sun60i / A733): The Allwinner A733 (powering the Radxa Cubie A7A) belongs to the newer sun60i big.LITTLE generation (2x Cortex-A76 + 6x Cortex-A55). While its main peripheral space is relocated, its auxiliary MCU subsystem reuses a XuanTie RISC-V core (E902) executing out of SRAM A2 and adheres to the identical remoteproc driver model.

4.1 Board Hardware Comparison

Radxa Cubie A5E (Allwinner T527 / A527, sun55i)

  • Application Processor: 8× ARM Cortex-A55 @ 1.8 GHz
  • Auxiliary Real-Time Core: XuanTie E907 (RV32IMAFCX @ 200 MHz, 32 GPRs, Hardware Single FPU)
  • Audio DSP: Decoupled (Not managed by sunxi_rproc.c)
  • Fast On-Chip Memory: 512 KB Continuous SRAM (0x3FFC00000x40040000)
  • Hardware Reset Vector: STA_ADD_REG defaults to 0x3FFC0000
  • Hardware Mailbox: 8-channel bi-directional MSGBOX (0x03003000)
  • Linux Driver Framework: sunxi_rproc.c (Linux RemoteProc)

Radxa Cubie A7A (Allwinner A733, sun60i)

  • Application Processor: 2× ARM Cortex-A76 @ 2.0 GHz + 6× Cortex-A55
  • Auxiliary Real-Time Core: XuanTie E902 (RV32EMC @ 200 MHz, 16 GPRs, No FPU)
  • Audio DSP: None
  • Fast On-Chip Memory: 208 KB Shared SRAM A2 (0x00040000)
  • Hardware Reset Vector: Hardwired Reset Vector @ 0x00040000
  • Hardware Mailbox: 8-channel bi-directional MSGBOX (0x03003000)
  • Linux Driver Framework: sunxi_rproc.c (Linux RemoteProc)

5. T527 Reference Manual Mapping & Silicon Reality

All hardware register offsets, memory windows, and control blocks referenced here are derived directly from the official Allwinner T527 User Manual V0.92 and verified on live silicon:

5.1 Where to Find This Information in the T527 User Manual

  • Chapter 2: System Address Map & Memory Mapping (Section 2.1, Table 2-1):
    • Dedicated MCU SRAM / SRAM A3 Space 0: 0x07280000 – 0x072BFFFF (256 KB)
    • Dedicated MCU SRAM / SRAM A3 Space 1: 0x072C0000 – 0x072FFFFF (256 KB)
    • Hardware MSGBOX: 0x03003000 – 0x03003FFF (4 KB)
    • RTC: 0x07090000 – 0x070903FF (1 KB)
    • System DRAM: 0x40000000 base
    • Silicon Confirmation: Address 0x07090000 is explicitly documented as the RTC (Real-Time Clock) register block. The T527 User Manual contains no memory-mapped Debug Module (DM/DMI) entry anywhere in the system bus interconnect tables.
  • Chapter on MCU Subsystem & RISC-V Configuration (RISCV_CFG @ 0x07130000):
    • 0x0000 (VER_REG): Version Register (0x00010000 = v1.0).
    • 0x0204 (STA_ADD_REG): Start Vector / Boot Address Register. Defines initial program counter address fetched upon reset deassertion. In silicon, its factory default value is 0x3FFC0000.
    • 0x0248 (WORK_MODE_REG): Work Mode Register. Bit 3 (BIT_LOCK_STA) indicates hardware core lockup status (0 = Running normally, 1 = Core lockup).
  • Chapter on MCU Clock Control Unit (MCU_CCU @ 0x07102000):
    • 0x07102120 (MCU_CLK_REG): XuanTie E907 core clock gating and divider selection.
    • 0x07102124 (MCU_RST_REG): XuanTie E907 reset control (Bit 16: CFG reset, Bit 17: DBG reset, Bit 18: Core Run reset).
  • Chapter on Hardware Message Box (CPUX_MSGBOX @ 0x03003000 / RISCV_MSGBOX @ 0x07136000):
    • 8-channel bi-directional hardware FIFO doorbells connecting ARM64 GIC SPI interrupts and RISC-V PLIC interrupts.

5.2 Verified Hardware Memory Windows (Allwinner T527 / A523)

The authoritative memory mapping registered in the Linux RemoteProc driver (sunxi_rproc.c / sun55i-a523.dtsi) is structured as follows:

  • SRAM Space 0 (r_sram): Host 0x07280000 -> Core 0x3FFC0000 (256 KB)
    • Role: Primary Boot & Execution pool (.vectors, .text, .data, .stack, .trace_buffer). Hardcoded hardware reset entry vector. Zero wait states.
  • SRAM Space 1 (r_sram1): Host 0x072C0000 -> Core 0x40000000 (256 KB)
    • Role: Secondary High-Speed SRAM Bank for shared IPC buffers and stack extension. Zero wait states.
  • RISC-V CFG Control Block (cfg): Host 0x07130000 -> Core 0x07130000 (4 KB)
    • Role: Hardware MMIO registers: Version (0x0000), Boot Entry Vector STA_ADD_REG (0x0204, defaults to 0x3FFC0000), and Work Mode / Lockup Status WORK_MODE_REG (0x0248).
  • MCU CCU Clocks & Resets: Host 0x07102000 -> Core 0x07102000 (4 KB)
    • Role: Clock gates (0x07102120), resets (0x07102124: bit 16 CFG, bit 17 DBG, bit 18 CORE).
  • Hardware MSGBOX: Host 0x03003000 -> Core 0x03003000 (4 KB)
    • Role: 8-channel bi-directional doorbell FIFO. Port 2 (Ch 8/9) connects Host ARM & E907 RISC-V.
  • RemoteProc Trace Buffer (trace0): Host 0x07285A30+ -> Core 0x3FFC5A30+ (4 KB)
    • Role: RemoteProc debugfs trace buffer (/sys/kernel/debug/remoteproc/remoteproc0/trace0). Mapped inside SRAM Space 0 (.trace_buffer).
  • Main AP Peripheral Space: Host 0x02000000+ -> Core 0x02000000+
    • Role: 1:1 mapped application peripherals: PIO GPIO controller (0x02000000), Linux UART0 debug console (0x02500000), UART2 navigation port (0x02500800), SPI0 (0x04025000).
  • CPUS Always-On Peripheral Space: Host 0x07000000+ -> Core 0x07000000+
    • Role: 1:1 mapped co-processor peripherals: dedicated S_UART0 serial console (0x07080000, 115200 baud), R_PIO GPIO (0x07022000), R_TIMER, R_PWM.

5.3 Address Translation Overview

  LINUX HOST (ARM64) PHYSICAL VIEW                  XUANTIE E907 RISC-V CORE VIEW
  ================================                  =============================
  0x07280000 - 0x072BFFFF [ 256 KB ] -------------> 0x3FFC0000 - 0x3FFFFFFF (SRAM Space 0)
    (Device Tree: "r_sram", Base Reset Entry)         (Primary Boot, .vectors, .text, stack)

  0x072C0000 - 0x072FFFFF [ 256 KB ] -------------> 0x40000000 - 0x4003FFFF (SRAM Space 1)
    (Device Tree: "r_sram1")                          (Secondary High-Speed SRAM Bank)

  0x07130000 - 0x07130FFF [   4 KB ] -------------> 0x07130000 - 0x07130FFF (CFG Regs)
    (Device Tree: "cfg", STA_ADD_REG 0x204)           (Boot Vector: 0x3FFC0000)

  0x03003000 - 0x03003FFF [   4 KB ] -------------> 0x03003000 - 0x03003FFF (MSGBOX)
    (Port 2 Ch 8/9: Host <-> E907)                    (Port 2: RISC-V Local Mailbox)

  0x48000000 - 0x480FFFFF [   1 MB ] -------------> 0x48000000 - 0x480FFFFF (DDR DMA Pool)
    (Reserved VirtIO RPMsg Pool)                      (vrings & Streaming Payloads)

6. Memory Architecture & Boot Mechanics: SRAM & Startup Sequence

On the Allwinner T527, Linux RemoteProc and the XuanTie E907 co-processor communicate and boot through dedicated on-chip SRAM:

6.1 The Boot Reality: RemoteProc Boots Directly from SRAM Space 0 (0x3FFC0000)

Caution (The 0x40000000 vs 0x3FFC0000 Boot Gotcha):
Early vendor BSPs and community bring-up attempts frequently locked up because code attempted to boot the E907 at address 0x40000000. On the T527, 0x40000000 is the base of DRAM (and secondary SRAM Space 1). However, the silicon reset vector and default STA_ADD_REG (0x07130204) strictly point to SRAM Space 0 at 0x3FFC0000. Firmware must be linked to ORIGIN 0x3FFC0000.

  1. Device Tree Bindings (sun55i-a523.dtsi): During driver probe, sunxi_rproc.c binds to the registered hardware blocks:

    rproc: remoteproc@7130000 {
        compatible = "allwinner,sun55i-a523-rproc",
                     "allwinner,sun55i-a527-rproc";
        reg = <0x07130000 0x1000>,
              <0x07280000 0x40000>,
              <0x072C0000 0x40000>,
              <0x07010364 0x4>;
        reg-names = "cfg", "r_sram", "r_sram1", "remap";
        clocks = <&mcu_ccu CLK_BUS_MCU_RISCV_CFG>,
                 <&mcu_ccu CLK_MCU_RISCV>,
                 <&mcu_ccu CLK_BUS_MCU_PUBSRAM>,
                 <&mcu_ccu CLK_BUS_MCU_RISCV_MSGBOX>;
        clock-names = "bus", "core", "sram", "msgbox";
        resets = <&mcu_ccu RST_BUS_MCU_RISCV_CFG>,
                 <&mcu_ccu RST_BUS_MCU_RISCV_CORE>,
                 <&mcu_ccu RST_BUS_MCU_PUBSRAM>,
                 <&mcu_ccu RST_BUS_MCU_RISCV_MSGBOX>;
        reset-names = "cfg", "core", "sram", "msgbox";
        mboxes = <&msgbox 8>, <&msgbox 9>;
        mbox-names = "rx", "tx";
        status = "disabled";
    };
  2. Loading Firmware into SRAM Space 0: The primary co-processor firmware (e907_sram.ld) is linked to execute out of SRAM Space 0 (0x3FFC0000):

    MEMORY {
        SRAM (rwx) : ORIGIN = 0x3FFC0000, LENGTH = 256K
    }

    When Linux loads firmware.elf, sunxi_rproc_da_to_va() translates device addresses in the range 0x3FFC00000x3FFFFFFF directly to the mapped host virtual address of r_sram and copies .vectors, .text, and .data via memcpy_toio().

  3. Starting the Core via STA_ADD_REG: When starting the co-processor (echo start > /sys/class/remoteproc/remoteproc0/state):

    • sunxi_rproc_start() retrieves the ELF entry point (rproc->bootaddr), which is 0x3FFC0000 (_vectors).
    • The driver programs this address into the hardware boot vector register STA_ADD_REG (0x07130204).
    • The driver deasserts the core run reset (rst_core, bit 18 in MCU_RST_REG 0x07102124).
    • The XuanTie E907 begins execution immediately from 0x3FFC0000 in SRAM Space 0.

6.2 Bootstrap Sequence & FPU Initialization (startup.S)

When the core deasserts reset at 0x3FFC0000, execution starts in startup.S:

.section .vectors, "ax"
.global _vectors
_vectors:
    j reset_handler
    /* Trap and interrupt vector table entries... */

.section .text.startup, "ax"
.global reset_handler
reset_handler:
    /* 1. Install direct Machine Trap Vector */
    la t0, default_trap_entry
    csrw mtvec, t0

    /* 2. Mask all external/timer interrupts during initialization */
    csrw mie, zero

    /* 3. Diagnostic proof marker in SRAM */
    li t0, 0xDEADBEEF
    la t1, __sram_c_start
    sw t0, 0(t1)

    /* 4. Initialize 16-byte ABI-aligned Stack Pointer */
    la sp, _stack_top

    /* 5. Initialize Global Pointer for relaxed addressing */
    .option push
    .option norelax
    la gp, __global_pointer$
    .option pop

    /* 6. Enable Hardware Single-Precision FPU (CRITICAL STEP) */
    li t0, (1 << 13) | (1 << 14)  /* mstatus.FS = 0b11 (Dirty/Initial) */
    csrs mstatus, t0
    csrw fcsr, zero               /* Clear accrued exceptions & set round-to-nearest */

    /* 7. Zero BSS Segment */
    la t0, _sbss
    la t1, _ebss
1:
    bge t0, t1, 2f
    sw zero, 0(t0)
    addi t0, t0, 4
    j 1b
2:
    /* 8. Call C++ Static Constructors */
    call __libc_init_array

    /* 9. Jump to Main Application */
    call main

    /* 10. Trap on main exit */
3:  wfi
    j 3b

Important (The mstatus.FS Trap Gotcha):
At hardware reset, RISC-V mstatus.FS is 00 (Off). Attempting to execute any floating-point instruction (flw, fsw, fadd.s, fmul.s) or access fcsr while FS == 00 immediately triggers an Illegal Instruction Exception (mcause = 2). Setting mstatus.FS = 0b11 in startup.S enables the hardware Single-Precision FPU.


7. Debugging Realities: What Works on T527 Silicon (and What Doesn't)

Bottom Line First: The Allwinner T527 does not expose a memory-mapped RISC-V Debug Module Interface (DMI) on its non-secure bus interconnect. Target-hosted OpenOCD cannot attach directly over /dev/mem. This section explains what that means, what platforms do have it, and what works today on T527.

7.1 What Memory-Mapped Debug Access Looks Like (And Why T527 Doesn't Have It)

Modern heterogeneous SoCs—such as the Texas Instruments AM62x / AM64x (K3), STMicroelectronics STM32MP1 / STM32MP2, and NXP i.MX8M—implement Direct Memory-Mapped Debug Access (DMEM). In this architecture, the auxiliary core's RISC-V Debug Module registers are exposed directly to the ARM application processor's bus, enabling target-hosted OpenOCD + GDB over SSH without any physical probe:

Development Workstation
  riscv-none-elf-gdb <firmware.elf>
  (gdb) target remote board:3333
        |
        | GDB Remote Serial Protocol (RSP)
        v
  OpenOCD on ARM64 Linux host
  (reads RISC-V DMI registers via /dev/mem MMIO)
        |
        | Internal SoC Bus (AXI/AHB)
        v
  RISC-V Debug Module Interface (DMI registers)
  e.g. TI AM62x: mapped into host physical address space

On the Allwinner T527, the XuanTie E907 Debug Module is not routed to the non-secure ARM bus interconnect. The address 0x07090000 often speculated about in community discussions is the RTC register block per the T527 User Manual—not a DMI window.

7.2 What Works Today: Four Practical Debug Strategies

For XuanTie E907 firmware development on current T527 hardware, these four strategies provide reliable, production-grade diagnostics:

  1. Linux RemoteProc Trace Buffers (trace0) (Primary): The .resource_table in firmware declares a RSC_TRACE entry backed by a circular ring buffer in SRAM Space 0 (0x3FFC0000). Linux maps it and exposes a live streaming interface:

    cat /sys/kernel/debug/remoteproc/remoteproc0/trace0

    Zero-overhead, no extra hardware required. Used by all firmware/e907-riscv/apps test applications.

  2. Dedicated Hardware UART (S_UART0 @ 0x07080000): A RISC-V-owned independent serial port in the CPUS Always-On domain at 115200 baud. Provides immediate low-level boot diagnostics completely separate from the Linux console UART (UART0 @ 0x02500000).

  3. Lock-Free Shared SRAM Ring Buffers: High-speed SPSC telemetry buffers in SRAM Space 0 (0x3FFC0000) or Space 1 (0x40000000), read from Linux via /dev/mem or UIO.

  4. External Hardware JTAG Probes: Connect a T-Head CK-Link or SEGGER J-Link to the physical JTAG test pads on the board for instruction-level single-stepping and hardware watchpoints.


8. Summary & What's Next in Part 2

In this introductory article, we established:

  1. Bill of Materials & Prerequisites: Target hardware, dual UART serial diagnostics, and toolchain setup.
  2. Why use the RISC-V co-processor: Deterministic SRAM execution (bypassing DRAM refresh jitter), offloading Linux CPU cycles, executing 10–50 kHz hard real-time control loops, and maintaining fault isolation.
  3. Silicon architecture & TCM mapping: Navigating sun55i-a523 naming, board differences between Cubie A5E and A7A, architectural isolation of the RISC-V co-processor, and exact register locations in the Allwinner T527 User Manual V0.92.
  4. Memory architecture & boot mechanics: How Linux RemoteProc (sunxi_rproc.c) loads firmware into SRAM Space 0 (0x3FFC0000), programs STA_ADD_REG to boot directly from SRAM, initializes the single-precision FPU (mstatus.FS = 0b11), and manages the 16 KB aligned stack.
  5. Debugging realities: Dispelling the 0x07090000 DMI myth, comparing with TI AM62x / STM32MP1, and detailing the four practical debugging techniques available today.

In Part 2: Building the Linux remoteproc Driver and Hardware Verification Suite, we move from architecture to software implementation:

  • Authoring the Linux 7.1 sunxi_rproc.c RemoteProc kernel driver.
  • Configuring multi-segment ELF placement (SRAM Space 0, SRAM Space 1, and DDR carveouts) and built-in debugfs trace logging.
  • Proving hardware state transitions using the all-new firmware/e907-riscv/apps verification suite (testBasic, testStringBinaryTrace0, testCrash, testPing, testPingRpmsg, testDRAMMsg).

Series Navigation

A Technical Deep-Dive into In-Memory FDT Merging, Dynamic Configuration Parsers, and the Bootloader-to-Kernel Contract

Dynamic Device Tree Overlays in U-Boot: From config.txt to Linux Kernel Handoff

A Technical Deep-Dive into In-Memory FDT Merging, Dynamic Configuration Parsers, and the Bootloader-to-Kernel Contract


1. The Combinatorial Hardware Nightmare

Embedded hardware rarely stays static. On modern heterogeneous SoCs—like the Allwinner T527 / A527 and A733 pairing octa-core ARM Cortex-A55 cores with dedicated XuanTie E907/E902 RISC-V real-time coprocessors—the exact pin routing, peripheral assignments, and memory maps shift depending on what the board is doing:

  • Flight Stack / Avionics: Hardware UART0 is dedicated to the Linux debug console, UART2 and SPI0 are isolated and handed directly to the RISC-V core for sub-millisecond sensor acquisition, and onboard I2C sensors (IMU, barometer) are enabled on the Linux bus.
  • Userspace I/O (UIO) / High-Rate IPC: The hardware inter-processor mailbox (msgbox) and dedicated MCU SRAM blocks are detached from the standard kernel mailbox subsystem and bound to generic-uio, allowing userspace ring buffers to poll at microsecond latencies.
  • Standard Prototyping: Expansion pins are exposed as standard /dev/spidev0.0 nodes and userspace GPIO lines.

The Monolithic DTB Anti-Pattern

Building a standalone Device Tree Blob (.dtb) for every imaginable hardware permutation (board-flight.dtb, board-flight-uio.dtb, board-sensors-uio.dtb, board-gpio.dtb) is an engineering dead end:

  1. Combinatorial Explosion: 4 sensor layouts and 3 IPC configurations force you to compile, test, and ship 12 distinct monolithic DTB files.
  2. Maintenance Hell: Upstream kernel changes to core clocks, power domains, or pin controller bindings have to be hand-ported across a dozen separate .dts files.
  3. Field Failure Risk: Switching modes in the field requires either rewriting raw bootloader partitions or maintaining brittle boot scripts with massive if/else ladders.

2. The KISS Architecture: In-Memory Bootloader Merging

Rather than building multiple monolithic trees, the clean architecture separates hardware descriptions into modular building blocks:

  1. One Base Device Tree (.dtb): Describes the immutable motherboard hardware (CPU cores, DRAM controller, interrupt controllers, system interconnects).
  2. Modular Overlays (.dtbo): Small standalone fragments that mutate specific nodes, enable peripheral clocks, re-route pinmuxes, or carve out shared memory.
  3. A Human-Readable Configuration File (config.txt): Placed on the FAT32 boot partition so developers can enable or disable features with simple key-value entries.
  4. An In-Memory Overlay Engine in U-Boot: At boot time, U-Boot loads the base DTB into RAM, reads config.txt, merges the selected overlays sequentially using libfdt, and passes the unified tree directly to the Linux kernel.
+-------------------------------------------------------------------------+
|                        DYNAMIC BOOTLOADER PIPELINE                      |
+-------------------------------------------------------------------------+
  |
  +-> 1. U-Boot reads /boot/config.txt (FAT partition)
  |      Parses: dtoverlay=cubie-a5e-flight-stack cubie-a5e-uio
  |
  +-> 2. Load Base DTB into RAM @ ${fdt_addr_r} (0x4fa00000)
  |      sun55i-a527-cubie-a5e.dtb (compiled with -@ symbols)
  |
  +-> 3. Expand in-memory Device Tree buffer
  |      fdt resize 0x10000 (adds 64 KB of headroom in hex)
  |
  +-> 4. Apply Overlays sequentially in RAM via libfdt
  |      - load cubie-a5e-flight-stack.dtbo -> fdt apply 0x4fe00000
  |      - load cubie-a5e-uio.dtbo          -> fdt apply 0x4fe00000
  |
  +-> 5. Load Kernel Image @ ${kernel_addr_r} (0x40200000: strictly 2MB-aligned)
  |
  +-> 6. Execute booti ${kernel_addr_r} - ${fdt_addr_r}
         ARM64 Register x0 = Physical RAM Address of merged FDT
         Kernel boots with zero runtime overlay overhead

Why Merge in U-Boot Instead of the Linux Kernel?

The Linux kernel technically supports dynamic overlays at runtime through CONFIG_OF_OVERLAY and configfs. In practice, relying on userspace to apply hardware overlays is a recipe for silent instability:

1. The Boot-Time "Chicken-and-Egg" Problem

Runtime kernel overlays are applied late in the boot sequence from userspace init scripts. Real-world overlays, however, configure hardware that the kernel needs on the very first instruction:

  • Early Serial Console & Pinmux: If an overlay assigns UART0 to Linux and isolates UART2 for the RISC-V coprocessor, waiting for userspace to apply this creates pin conflicts on power-up and blinds you to early kernel panics (earlycon).
  • Reserved Memory Carveouts (reserved-memory): The XuanTie E907 firmware requires dedicated, non-cacheable DMA memory (rproc_vdev @ 0x48000000). The Linux memory subsystem (Buddy allocator, page tables, CMA zones) establishes physical memory boundaries during early architecture initialization (setup_arch()). You cannot dynamically insert reserved-memory carveouts into a running kernel memory map from userspace.
  • Core Clocks and Power Domains: Mutating clock trees or PMIC regulators after platform drivers have already probed causes clock desynchronization or peripheral brownouts.

2. Kernel Driver Unbind Fragility

Modifying Device Tree nodes inside a running kernel forces the kernel to dynamically instantiate platform_device objects, resolve deferred probes, and track device-node reference counts. If an overlay disables a node (status = "disabled"), the bound driver must cleanly unbind. Many kernel drivers do not have battle-tested .remove() paths for Device Tree hot-unplug, leading to dangling pointers, kernel memory leaks, or oopses.

3. Pure Determinism

Merging overlays in U-Boot gives the kernel a completely static, fully-resolved hardware description. To Linux, the device tree is indistinguishable from a custom monolithic DTB. The kernel requires zero dynamic overlay patches, no configfs daemons, and zero runtime overhead.


3. The Two Environments: Static uboot.env Binary vs Dynamic config.txt

If you inspect a newly flashed SD card, you will find uboot.env sitting in the same boot partition alongside config.txt and boot.scr. Understanding the architectural divide between these two files is essential.

The Real-World Target Experience: Why Editing uboot.env Fails

Mount the FAT boot partition on a running board:

cubie-a5e login: root
# mkdir -p /boot
# mount -t vfat /dev/mmcblk0p1 /boot
# ls -la /boot
total 24832
drwxr-xr-x    2 root     root         16384 Jan  1  1970 .
drwxr-xr-x   18 root     root          4096 Sep  5 09:40 ..
-rwxr-xr-x    1 root     root      20140544 Sep  5 09:30 Image
-rwxr-xr-x    1 root     root          3573 Sep  5 09:35 boot.scr
-rwxr-xr-x    1 root     root          1241 Sep  5 09:35 config.txt
-rwxr-xr-x    1 root     root          5487 Sep  5 09:30 cubie-a5e-flight-stack.dtbo
-rwxr-xr-x    1 root     root          1114 Sep  5 09:30 cubie-a5e-uio.dtbo
-rwxr-xr-x    1 root     root         62914 Sep  5 09:30 sun55i-a527-cubie-a5e.dtb
-rwxr-xr-x    1 root     root         65536 Sep  5 09:35 uboot.env
-rwxr-xr-x    1 root     root           557 Sep  5 09:35 uEnv.txt

If you try to view uboot.env with more or edit it with vi:

# more /boot/uboot.env
--More-- (2% of 65536 bytes) loglevel=8bootcmd=load mmc 0:1 0x4fc00000 boot.scr && source 0x4fc00000kernel_addr_r=0x40200000kernel_comp_addr_r=0x4400)

The terminal fills with control characters. If you save changes with vi, the next reboot produces:

*** Bad CRC, using default environment ***

U-Boot rejects the file, discards every variable, and falls back to hardcoded compiled defaults.

Inside uboot.env: CRC32 Checksums and Binary Layout

uboot.env is not a text file. It is a raw binary image compiled during the build by the host tool mkenvimage from a text template (project-cubie-a5e/board/radxa/cubie_a5e/uboot-env.txt):

${HOST_DIR}/bin/mkenvimage -s 0x10000 -o "${BINARIES_DIR}/uboot.env" "${BOARD_DIR}/uboot-env.txt"

In the U-Boot source tree (include/env_internal.h), the environment binary structure is defined as:

/* U-Boot standard non-redundant environment image format */
struct env_image_single {
    uint32_t crc;       /* 4-byte CRC32 checksum over the data array */
    char     data[];    /* Sequential NULL-separated key=value strings */
};

On Allwinner platforms without redundant environment enabled, struct env_image_single is stored directly on flash.

Byte-by-Byte Hex Dump Breakdown

Inspecting uboot.env with hexdump -C reveals the layout:

Offset    Hexadecimal Bytes                                 ASCII Representation
--------  ------------------------------------------------  --------------------
00000000  7b e2 4c 3f 62 6f 6f 74  64 65 6c 61 79 3d 31 00  |{.L?bootdelay=1.|
00000010  62 61 75 64 72 61 74 65  3d 31 31 35 32 30 30 00  |baudrate=115200.|
00000020  62 6f 6f 74 61 72 67 73  3d 63 6f 6e 73 6f 6c 65  |bootargs=console|
00000030  3d 74 74 79 53 30 2c 31  31 35 32 30 30 20 65 61  |=ttyS0,115200 ea|
...
000001c0  72 5f 6d 6f 64 65 3d 64  65 6d 6f 00 00 00 00 00  |r_mode=demo.....|
000001d0  00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00  |................|
*
00010000
  1. Bytes 0x00000000 - 0x00000003 (7b e2 4c 3f): The 32-bit CRC checksum stored in little-endian byte order (0x3F4CE27B). It is calculated over the entire remaining payload: bytes 0x00000004 through 0x0000FFFF (65,532 bytes).
  2. Bytes 0x00000004 - 0x0000000F (bootdelay=1\0): The first environment variable string, terminated by a single ASCII NUL byte (0x00).
  3. Subsequent Strings: Each variable is stored as KEY=VALUE\0.
  4. End of Environment Marker: Marked by two consecutive NUL bytes (\0\0).
  5. Zero Padding: The remaining ~65 KB of the file is filled with zeroes (0x00) to guarantee an exact total file size of 65,536 bytes (0x10000).

When you edit uboot.env with a text editor:

  • The CRC Breaks: Modifying a single character invalidates the 4-byte CRC header.
  • String Boundaries Corrupt: Text editors treat \0 as end-of-file or convert it to \n or \r\n.
  • File Truncation: Text editors strip the trailing zero padding, changing the total file size from 65,536 bytes.

The Solution: Decoupling Low-Level Plumbing from User Config

We split configuration responsibilities completely:

  • uboot.env: Static low-level firmware baseline. Holds DRAM addresses, baud rates, and one critical command:
    bootcmd=load mmc 0:1 0x4fc00000 boot.scr && source 0x4fc00000
  • config.txt: Pure ASCII text file on the FAT partition. Users can edit it with vi on the target or in Notepad on Windows.
  • boot.cmd: The script engine that reads config.txt into RAM using U-Boot's env import -t command:
    if load mmc 0:1 ${ramdisk_addr_r} config.txt; then
        echo ">>> Found Raspberry Pi-style config.txt! Importing configuration..."
        env import -t ${ramdisk_addr_r} ${filesize}
    fi

4. SD Card Storage Architecture & On-Target Access

The SD card layout uses two distinct partitions:

  • Sectors 0 - 32767 (Offset 8 KB): Raw bootloader carveout (u-boot-sunxi-with-spl.bin holding SPL, ATF BL31, and Mainline U-Boot).
  • Partition 1 (/dev/mmcblk0p1, 64 MB FAT32): Mounted at /boot. Contains the uncompressed kernel Image, base DTB, .dtbo overlays, config.txt, boot.scr, and uboot.env.
  • Partition 2 (/dev/mmcblk0p2, ext4): Root filesystem (/).

In the Buildroot rootfs overlay (project-cubie-a5e/board/radxa/cubie_a5e/rootfs-overlay/etc/fstab), the FAT partition is mounted automatically on boot:

/dev/root       /              ext4     rw,noatime        0      1
/dev/mmcblk0p1  /boot          vfat     defaults          0      2
proc            /proc          proc     defaults          0      0
sysfs           /sys           sysfs    defaults          0      0

Editing the hardware configuration directly on the board is a 3-step workflow:

vi /boot/config.txt
sync
reboot

5. Anatomy of an Overlay (.dtso) & The -@ Symbol Trap

An overlay source file (.dtso) declares /plugin/; at the top. Instead of defining a complete system, it targets specific nodes in the base tree using labels (e.g. &msgbox) or absolute paths (target-path = "/soc/mailbox@3003000").

Here is the Userspace I/O overlay (project-cubie-a5e/dts-overlay/allwinner/cubie-a5e-uio.dtso):

/dts-v1/;
/plugin/;

/*
 * cubie-a5e-uio.dtso - Convert hardware mailbox to userspace UIO device
 */

&msgbox {
    /* 1. Override the compatible string to bind generic-uio */
    compatible = "generic-uio";

    /* 2. Extend reg to expose both Mailbox MMIO and Dedicated MCU SRAM C */
    reg = <0x03003000 0x1000>,
          <0x07131000 0x1000>;
    reg-names = "msgbox", "sram";

    /* 3. Ensure the node is enabled */
    status = "okay";
};

&rproc {
    /* Place RemoteProc into standalone mode (no kernel mailbox binding) */
    status = "okay";
};

The Missing __symbols__ Trap (FDT_ERR_NOTFOUND)

When the Device Tree Compiler (dtc) compiles a standard .dts without the -@ flag, it converts all human-readable node labels (&msgbox, &i2c1, &uart0) into anonymous integer phandles and completely strips the string label names.

When an overlay is compiled with /plugin/;, its label references cannot be assigned fixed phandles at compile time; dtc records them in a __fixups__ table.

At boot time, U-Boot's fdt apply command cross-references the overlay's __fixups__ table against a top-level __symbols__ node in the base tree:

__symbols__ {
    uart0 = "/soc/serial@2500000";
    msgbox = "/soc/mailbox@3003000";
    i2c1 = "/soc/i2c@2502400";
    ccu = "/soc/clock-controller@2001000";
};

If the base DTB was compiled without -@, the __symbols__ node does not exist. fdt apply fails with:

libfdt fdt_apply_overlay(): FDT_ERR_NOTFOUND (-1)

To fix this, enable overlay symbols in your build system:

  • Buildroot: Set BR2_LINUX_KERNEL_DTB_OVERLAY_SUPPORT=y in defconfig.
  • Standalone Kernel Build: Pass DTC_FLAGS="-@" during build:
    make ARCH=arm64 CROSS_COMPILE=aarch64-linux-gnu- DTC_FLAGS="-@" dtbs
  • Inspect Symbols in DTB:
    fdtdump sun55i-a527-cubie-a5e.dtb | grep -A 5 __symbols__

6. The config.txt Interface & Armbian Comparison

The default config.txt on the boot partition exposes two primary keys:

# /boot/config.txt - Radxa Cubie A5E Hardware & Overlay Configuration

# 1. Device Tree Overlays (dtoverlay)
# Space-separated list of overlays (omitting .dtbo extension is supported)
dtoverlay=cubie-a5e-flight-stack cubie-a5e-uio

# 2. Kernel Command-Line Arguments (cmdline)
# Optional bootargs appended to kernel command line
cmdline=isolcpus=3 nohz_full=3 rcu_nocbs=3

Armbian Comparison

Armbian popularized the env import -t pattern using /boot/armbianEnv.txt (overlays=, extraargs=). Our implementation adopts this mechanic while addressing several structural constraints:

  1. Partition Isolation: Armbian uses a single monolithic ext4 root partition. If an uncontrolled power cut corrupts the ext4 filesystem, the board cannot boot. Our architecture puts bootloader files, kernels, and overlays onto a dedicated 64 MB FAT32 partition.
  2. Multi-Format Ingestion: Our boot script checks for config.txt first, falls back to armbianEnv.txt, and finally checks legacy uEnv.txt. Dropping an existing armbianEnv.txt onto the SD card works without modification.
  3. Cross-Platform Host Editing: FAT32 mounts natively on Windows, macOS, and Linux PCs without requiring third-party ext4 drivers.

7. Anatomy of boot.cmd: The U-Boot Script Engine

The plain-text source script (project-cubie-a5e/board/radxa/cubie_a5e/boot.cmd) is compiled into boot.scr using mkimage:

mkimage -A arm64 -T script -C none -d boot.cmd boot.scr

Here is the complete script running on the platform:

# ==============================================================================
# Radxa Cubie A5E Dynamic Multi-Overlay Boot Script (boot.cmd -> boot.scr)
# Supports Raspberry Pi-style config.txt, Armbian armbianEnv.txt, & uEnv.txt
# ==============================================================================

echo "=== Initializing Radxa Cubie A5E Dynamic Boot Sequence ==="

# 1. Base boot arguments (UART console, rootfs, panic handling)
setenv bootargs "console=ttyS0,115200 earlycon root=/dev/mmcblk0p2 rootwait rw panic=10 loglevel=8"

# 2. Standard Memory Map Addresses (Allwinner 64-bit DRAM base 0x40000000)
# kernel_addr_r strictly placed at 2MB boundary (0x40200000) per ARM64 boot constraints
if test -z "${kernel_addr_r}";     then setenv kernel_addr_r     0x40200000; fi
if test -z "${fdt_addr_r}";        then setenv fdt_addr_r        0x4fa00000; fi
if test -z "${fdtoverlay_addr_r}"; then setenv fdtoverlay_addr_r 0x4fe00000; fi
if test -z "${ramdisk_addr_r}";    then setenv ramdisk_addr_r    0x4ff00000; fi

# 3. Default base DTB and default overlays
setenv base_dtb sun55i-a527-cubie-a5e.dtb
setenv overlays "cubie-a5e-flight-stack"

# 4. Check for Raspberry Pi-style config.txt first, then armbianEnv.txt, then uEnv.txt
if load mmc 0:1 ${ramdisk_addr_r} config.txt; then
    echo ">>> Found Raspberry Pi-style config.txt! Importing configuration..."
    env import -t ${ramdisk_addr_r} ${filesize}
elif load mmc 0:1 ${ramdisk_addr_r} armbianEnv.txt; then
    echo ">>> Found Armbian-style armbianEnv.txt! Importing environment..."
    env import -t ${ramdisk_addr_r} ${filesize}
elif load mmc 0:1 ${ramdisk_addr_r} uEnv.txt; then
    echo ">>> Found uEnv.txt! Importing environment..."
    env import -t ${ramdisk_addr_r} ${filesize}
fi

# 5. Handle Raspberry Pi-style dtoverlay or standard overlays variable
if test -n "${dtoverlay}"; then
    setenv overlays "${dtoverlay}"
fi

# 6. Append optional user bootargs from cmdline (Pi-style), extraargs (Armbian), or extra_bootargs
if test -n "${cmdline}"; then
    echo ">>> Appending cmdline: ${cmdline}"
    setenv bootargs "${bootargs} ${cmdline}"
elif test -n "${extraargs}"; then
    echo ">>> Appending extraargs: ${extraargs}"
    setenv bootargs "${bootargs} ${extraargs}"
elif test -n "${extra_bootargs}"; then
    echo ">>> Appending extra_bootargs: ${extra_bootargs}"
    setenv bootargs "${bootargs} ${extra_bootargs}"
fi

# 7. Load base Device Tree into memory
echo ">>> Loading Base Device Tree: ${base_dtb}..."
if load mmc 0:1 ${fdt_addr_r} ${base_dtb}; then
    fdt addr ${fdt_addr_r}
    # Expand FDT buffer by 64 KB (0x10000 in hex radix) to accommodate multiple overlays
    fdt resize 0x10000
else
    echo "ERROR: Failed to load base DTB ${base_dtb}!"
    reset
fi

# 8. Dynamically iterate and apply each Device Tree Overlay in ${overlays}
# Automatically resolves both bare names (e.g. 'cubie-a5e-uio') and '.dtbo' extensions
echo ">>> Processing Device Tree Overlays: ${overlays}..."
for overlay in ${overlays}; do
    echo "    Searching overlay: ${overlay}..."
    setenv loaded 0
    if load mmc 0:1 ${fdtoverlay_addr_r} ${overlay}.dtbo; then
        setenv loaded 1
    elif load mmc 0:1 ${fdtoverlay_addr_r} ${overlay}; then
        setenv loaded 1
    elif load mmc 0:1 ${fdtoverlay_addr_r} overlays/${overlay}.dtbo; then
        setenv loaded 1
    fi

    if test "${loaded}" = "1"; then
        if fdt apply ${fdtoverlay_addr_r}; then
            echo "    [OK] Applied ${overlay} successfully."
        else
            echo "    [ERROR] fdt apply failed for ${overlay}!"
        fi
    else
        echo "    [WARN] Could not find overlay file for ${overlay} on mmc 0:1!"
    fi
done

# 9. Load Linux kernel Image and boot
echo ">>> Loading Linux Kernel Image..."
if load mmc 0:1 ${kernel_addr_r} Image; then
    echo ">>> Booting Linux Kernel with Dynamic Overlays..."
    booti ${kernel_addr_r} - ${fdt_addr_r}
else
    echo "ERROR: Failed to load Linux Kernel Image!"
    reset
fi

Critical Implementation Details & Pitfalls

1. The kernel_addr_r 2MB Boundary Rule

In Allwinner 64-bit systems, physical DRAM begins at 0x40000000. Legacy 32-bit scripts often set kernel_addr_r=0x40080000 (a 512 KB offset).

On ARM64, this causes silent boot loops or alignment panics.

Per the Linux kernel ARM64 booting protocol (Documentation/arch/arm64/booting.rst), the uncompressed kernel Image must be placed at a 2MB-aligned physical memory address. Setting kernel_addr_r=0x40200000 satisfies this constraint and preserves the lower 2MB (0x40000000 - 0x401FFFFF) for ARM Trusted Firmware (TF-A BL31) and secure monitor carveouts.

2. The U-Boot Hex Radix Trap in fdt resize

When dtc generates a DTB, the header field totalsize matches the exact compiled byte length. When fdt apply attempts to insert new nodes, strings, and phandles, libfdt returns -FDT_ERR_NOSPACE (-3) unless the buffer is expanded first.

U-Boot's command-line parser interprets integer arguments as hexadecimal by default.

  • Writing fdt resize 0x10000 adds exactly 65,536 bytes (64 KB) of padding headroom.
  • Writing decimal 65536 without prefix will be parsed by U-Boot as 0x65536 (415,030 bytes). While it allocates extra memory, on memory-constrained buffers or scripts expecting strict byte counts, omitting the 0x prefix leads to unexpected buffer overflows or parse failures. Always write fdt resize 0x10000.

3. Hush Shell Spacing Bug in Conditional Checks

In U-Boot's Hush parser, test is a built-in command that evaluates whitespace-delimited tokens.

A common bug in generated scripts is accidental whitespace insertion:

# BROKEN: evaluates the literal string " 1" with leading space
if test "${loaded}" = " 1"; then

If ${loaded} is "1", the string equality check fails silently, and the overlay is never applied. Ensure conditionals use clean token spacing:

if test "${loaded}" = "1"; then

4. Trailing Newlines in env import -t

U-Boot's env import -t expects newline (\n) delimiters. If the last line of config.txt does not have a trailing newline (the user didn't press Enter at the end of the file), U-Boot's parser silently drops the final key-value pair. Always ensure configuration files end with an empty blank line.


8. Buildroot Automation Pipeline

Buildroot coordinates the compilation, staging, and packaging of every boot component automatically within project-cubie-a5e.

+---------------------------------------------------------------------------------------------------+
|                                  BUILDROOT PACKAGING PIPELINE                                     |
+---------------------------------------------------------------------------------------------------+
| 1. Out-of-Tree Overlays: project-cubie-a5e/dts-overlay/allwinner/*.dtso                            |
|    Buildroot Linux package compiles with dtc -@ ---> ${BINARIES_DIR}/*.dtbo                       |
+---------------------------------------------------------------------------------------------------+
| 2. RootFS Pre-Assembly: rootfs-overlay/etc/fstab & post-build.sh                                  |
|    Copies fstab (/dev/mmcblk0p1 -> /boot) and creates /boot directory in ${TARGET_DIR}           |
+---------------------------------------------------------------------------------------------------+
| 3. Post-Image Processing: post-image.sh                                                           |
|    - mkimage compiles boot.cmd ---> ${BINARIES_DIR}/boot.scr                                      |
|    - mkenvimage compiles uboot-env.txt ---> ${BINARIES_DIR}/uboot.env                             |
|    - Staging: copies config.txt and uEnv.txt into ${BINARIES_DIR}/                                |
+---------------------------------------------------------------------------------------------------+
| 4. Final Disk Assembly: genimage.cfg                                                              |
|    Stitches SPL, boot.vfat (with config.txt, dtbos, Image), and rootfs.ext4 into sdcard.img       |
+---------------------------------------------------------------------------------------------------+

Post-Image Script (post-image.sh)

When the kernel and rootfs finishes building, Buildroot executes project-cubie-a5e/board/radxa/cubie_a5e/post-image.sh:

#!/bin/sh
BOARD_DIR="$(dirname $0)"
GENIMAGE_CFG="${BOARD_DIR}/genimage.cfg"
GENIMAGE_TMP="${BUILD_DIR}/genimage.tmp"

# 1. Compile boot.cmd into boot.scr using host mkimage
${HOST_DIR}/bin/mkimage -A arm64 -T script -C none -d "${BOARD_DIR}/boot.cmd" "${BINARIES_DIR}/boot.scr"

# 2. Compile uboot-env.txt into uboot.env binary using host mkenvimage
${HOST_DIR}/bin/mkenvimage -s 0x10000 -o "${BINARIES_DIR}/uboot.env" "${BOARD_DIR}/uboot-env.txt"

# 3. Stage plain-text runtime configuration templates into BINARIES_DIR for genimage
cp -f "${BOARD_DIR}/config.txt" "${BINARIES_DIR}/config.txt"
cp -f "${BOARD_DIR}/uEnv.txt"   "${BINARIES_DIR}/uEnv.txt"

# 4. Run genimage packaging pipeline
rm -rf "${GENIMAGE_TMP}"
genimage --config "${GENIMAGE_CFG}" \
         --rootpath "${TARGET_DIR}" \
         --tmppath "${GENIMAGE_TMP}" \
         --inputpath "${BINARIES_DIR}" \
         --outputpath "${BINARIES_DIR}"

exit 0

Partition Assembly (genimage.cfg)

Host genimage reads project-cubie-a5e/board/radxa/cubie_a5e/genimage.cfg:

image boot.vfat {
    vfat {
        files = {
            "sun55i-a527-cubie-a5e.dtb",
            "cubie-a5e-flight-stack.dtbo",
            "cubie-a5e-uio.dtbo",
            "config.txt",
            "uEnv.txt",
            "boot.scr",
            "Image",
            "uboot.env"
        }
    }
    size = 64M
}

image sdcard.img {
    hdimage {}

    partition u-boot {
        in-partition-table = false
        image = "u-boot-sunxi-with-spl.bin"
        offset = 8K
        size = 1016K
    }

    partition boot {
        partition-type = 0xC
        bootable = "true"
        image = "boot.vfat"
        offset = 4M
    }

    partition rootfs {
        partition-type = 0x83
        image = "rootfs.ext4"
    }
}

Building the entire stack requires two commands:

make -C buildroot O=$PWD/bld BR2_EXTERNAL=$PWD/project-cubie-a5e cubie_a5e_defconfig
make -C bld

Flash the generated image:

sudo dd if=bld/images/sdcard.img of=/dev/sdX bs=4M status=progress conv=fsync

9. The Kernel Handoff Contract (ARM64 Register x0)

Once U-Boot applies all overlays into memory at 0x4fa00000, it executes:

booti ${kernel_addr_r} - ${fdt_addr_r}

Under the ARM64 boot protocol:

  • Register x0: Holds the 64-bit physical DRAM address of the Device Tree Blob (0x4fa00000).
  • Registers x1 - x3: Must be set to 0.
  • MMU: Disabled.
  • Caches: Data cache cleaned to Point of Coherency (PoC), instruction cache invalidated.
  • CPU Mode: EL2 (Hypervisor) or non-secure EL1.

When booti jumps to 0x40200000, the kernel entry point (arch/arm64/kernel/head.S) reads x0, verifies the 0xd00dfeed FDT header magic, and unrolls the merged nodes via setup_machine_fdt(). To Linux, the device tree is completely static.


10. Live Verification on Hardware

Boot the board with dtoverlay=cubie-a5e-flight-stack cubie-a5e-uio in /boot/config.txt.

1. Serial Console U-Boot Log

During boot, U-Boot outputs the sequential merge:

=== Initializing Radxa Cubie A5E Dynamic Boot Sequence ===
>>> Found Raspberry Pi-style config.txt! Importing configuration...
>>> Loading Base Device Tree: sun55i-a527-cubie-a5e.dtb...
62914 bytes read in 6 ms (10.0 MiB/s)
>>> Processing Device Tree Overlays: cubie-a5e-flight-stack cubie-a5e-uio...
    Searching overlay: cubie-a5e-flight-stack...
5487 bytes read in 2 ms (2.6 MiB/s)
    [OK] Applied cubie-a5e-flight-stack successfully.
    Searching overlay: cubie-a5e-uio...
1114 bytes read in 1 ms (1.1 MiB/s)
    [OK] Applied cubie-a5e-uio successfully.
>>> Loading Linux Kernel Image...
20140544 bytes read in 868 ms (22.1 MiB/s)
>>> Booting Linux Kernel with Dynamic Overlays...
## Flattened Device Tree blob at 4fa00000
   Booting using the fdt blob at 0x4fa00000
   Loading Device Tree to 0000000049ff0000, end 0000000049ffffff ... OK

Starting kernel ...

2. Live Linux Inspection via Sysfs and /proc/device-tree

Verify the mailbox node was converted from the standard kernel driver to Userspace I/O:

# 1. Verify compatible string is generic-uio
cat /proc/device-tree/soc/mailbox@3003000/compatible
# Output: generic-uio

# 2. Check dual-MMIO reg names added by the overlay
xxd -p /proc/device-tree/soc/mailbox@3003000/reg-names | xxd -r -p
# Output: msgboxsram

# 3. Check /dev/uio0 driver binding
ls -la /dev/uio0
# crw-rw---- 1 root root 242, 0 Sep  6 12:00 /dev/uio0

# 4. Verify physical memory map carveouts exported by the kernel
cat /sys/class/uio/uio0/maps/map0/name && cat /sys/class/uio/uio0/maps/map0/addr
# msgbox
# 0x3003000

cat /sys/class/uio/uio0/maps/map1/name && cat /sys/class/uio/uio0/maps/map1/addr
# sram
# 0x7131000

11. Field Triage & Troubleshooting Matrix

Symptom Root Cause Fix
*** Bad CRC, using default environment *** Editing uboot.env with vi broke the 4-byte CRC32 header and null delimiters. Never edit uboot.env directly. Use /boot/config.txt. Re-flash or delete uboot.env to restore defaults.
libfdt fdt_apply_overlay(): FDT_ERR_NOSPACE (-3) The base DTB buffer at ${fdt_addr_r} ran out of memory during overlay node insertion. Call fdt resize 0x10000 in boot.cmd immediately after fdt addr ${fdt_addr_r}.
libfdt fdt_apply_overlay(): FDT_ERR_NOTFOUND (-1) Base DTB was compiled without -@ (symbols), omitting the __symbols__ lookup table. Set BR2_LINUX_KERNEL_DTB_OVERLAY_SUPPORT=y in defconfig, or build DTBs with make DTC_FLAGS="-@" dtbs.
Kernel hangs immediately after Starting kernel ... kernel_addr_r was set to an unaligned offset (e.g. 0x40080000), violating ARM64 2MB alignment. Set kernel_addr_r=0x40200000 (2MB boundary from DRAM base 0x40000000).
Overlays defined in config.txt are completely ignored config.txt was saved with DOS CRLF (\r\n) line endings or lacks a trailing newline. Convert with dos2unix /boot/config.txt and ensure the file ends with an empty line.
[WARN] Could not find overlay file File naming mismatch in dtoverlay=. Use the exact file basename without .dtbo (e.g., dtoverlay=cubie-a5e-uio).
/boot is empty on target The FAT partition was not mounted at boot. Run mount -t vfat /dev/mmcblk0p1 /boot and add the mount to /etc/fstab.