Create your own
Lesson illustration

Tracing U-Boot Startup: Stack Setup, DDR Initialization, Relocation, and Board Initialization

Welcome back. In the previous lesson, you built a boot-artifact and address map for an ARM Linux system, including the AM62x sequence from ROM-managed firmware through tispl.bin, U-Boot, Linux, and PID 1. That map told you which components own each stage and where they can coexist in memory.

This lesson moves inside U-Boot. You will learn to trace the boundary between constrained early execution and normal DRAM-backed execution: temporary stack setup, board_init_f(), DDR initialization, self-relocation, and board_init_r(). The goal is not to memorize a single vendor tree. It is to establish a repeatable source-navigation method that works when a board hangs before the U-Boot banner, after DRAM initialization, or during relocation.


The central model: two U-Boot operating conditions

U-Boot initialization is easier to analyze when treated as two distinct operating conditions rather than one continuous C program.

PhaseMain functionMemory conditionsProgramming constraintsTypical purpose
Early phaseboard_init_f()Initial execution memory and a temporary stack; normally no usable BSSAvoid ordinary global/static state; use local variables and carefully initialized global dataEstablish serial output, detect or initialize DRAM, calculate relocation layout
Relocated phaseboard_init_r()DRAM is available; U-Boot code, stack, global data, and BSS have been relocated or establishedNormal C runtime assumptions are largely validInitialize drivers and boot services, then run the boot policy or command loop

The letters are historical: f originally meant code intended to run “from flash,” while r meant code running after relocation. Modern platforms may initially execute from SRAM, cache-backed memory, a boot package placed in DRAM, or another SoC-specific memory region. The useful distinction is therefore not flash versus RAM. It is:

  • What memory and runtime services exist before relocation?
  • What memory and runtime services exist after relocation?

On a production AM62x system, this distinction appears in more than one binary. The A53 SPL within tispl.bin has an early constrained job: get sufficient platform state and DRAM ready for a larger stage. U-Boot Proper then has its own startup path, often including a full relocation into its chosen DRAM region.

Board Initialisation Flow — Das U-Boot unknown version documentation

Read the U-Boot community’s “Board Initialisation Flow” documentation first. It defines the intended roles of the early and relocated phases across U-Boot Proper and program-loader stages such as SPL and TPL.

Begin with the “Board Initialisation Flow” introduction, then read the subsections “lowlevel_init()”, “board_init_f()”, and “board_init_r()” in order. In “lowlevel_init()”, focus on the early limitations. In “board_init_f()”, focus on the first-phase constraints. Finish with “board_init_r()”, noting what becomes available after relocation.

The documentation describes the intended common model. Board ports can have architectural exceptions, especially in a multicore SoC boot flow. Treat the model as a set of invariants to verify against the actual source and generated configuration, rather than as an assumption that every function runs at an identical address on every board.


From reset entry to the first C function

At reset, the CPU cannot simply call a normal C function. A compiler-generated C function assumes certain runtime conditions already exist: a valid stack, a usable ABI environment, and often initialized data. Early assembly establishes enough of that environment to make the first constrained C code possible.

In a U-Boot source tree, the startup path is usually distributed across four areas:

Source areaWhat you are looking for
arch/<architecture>/...Reset/vector entry, CPU mode and exception-level setup, early assembly, stack register assignment
arch/<architecture>/lib/crt0*.SCommon C-runtime bridge, calls to board_init_f(), relocation handoff, entry to board_init_r()
common/board_f.cThe early initialization-call list and common board_init_f() machinery
common/board_r.cThe post-relocation initialization-call list and common board_init_r() machinery
board/<vendor>/<board>/ and arch/arm/mach-<soc>/Board and SoC hooks: power, pin multiplexing, UART, DDR controller or PHY configuration
configs/, Kconfig, generated .configConfiguration symbols that decide which hooks and drivers are compiled into a particular image

Exact filenames evolve between U-Boot releases. On many ARM64 trees, a file resembling arch/arm/lib/crt0_64.S is a useful starting point, but the correct approach is to find the call sites in your checked-out revision, not rely on a remembered path.

Temporary stack setup is a hard dependency

The stack is a region of memory used for function calls, return addresses, local variables, and saved registers. Before a valid stack exists, even apparently harmless C code is risky: calling a function may overwrite unknown memory or return to an invalid address.

Early assembly normally performs work in this broad order:

  1. Enter from the architecture-specific reset or loader entry point.
  2. Set a temporary stack pointer to memory known to be accessible at that instant.
  3. Establish the U-Boot global_data pointer or equivalent early state.
  4. Transfer into the constrained C initialization path.
  5. Call board_init_f().

The temporary stack is deliberately not the final stack. Before DDR is usable, it commonly belongs in on-chip SRAM. Once DDR is working, U-Boot can allocate its final stack in the DRAM region selected during relocation.

There is an important AM62x nuance. The AM62 platform documentation shows an A53 SPL DDR memory layout, including a stack at 0x80b77660 through 0x80b77e60. This does not contradict the generic early-stack rule. It tells you that this particular SPL layout describes a point after DDR has become available. When inspecting code, always ask:

Is this stack address used before DDR initialization, while DDR is being initialized, or after DDR has already been validated?

AM62 Platforms — Das U-Boot unknown version documentation

Revisit the U-Boot AM62 platform documentation’s “A53 SPL DDR Memory Layout.” It gives a concrete example of how code, a stack, global data, allocator space, BSS, and blobs occupy distinct RAM regions during SPL execution.

In the “A53 SPL DDR Memory Layout” subsection, read the full table beginning with the SPL memory layout. Compare the stack, GD, malloc, and BSS rows. Do not treat listed empty gaps as free payload locations without checking board configuration, firmware reservations, and the lifetime of each object.


board_init_f(): make DRAM and diagnostics possible

board_init_f() is the main early C-phase coordinator. It executes before U-Boot has the full facilities it will later enjoy.

The exact function list is configuration dependent, but common responsibilities include:

  • establishing early global data;
  • setting up a timer or basic clock source;
  • initializing pin multiplexing needed for UART, I2C, storage, or DDR;
  • enabling an early UART console;
  • invoking or arranging DRAM discovery and initialization;
  • reserving future DRAM regions for relocated U-Boot, stacks, malloc space, FDTs, and other live data;
  • calculating the relocation destination.

The most important constraint is BSS availability. BSS is the zero-initialized memory region that backs uninitialized global and static variables. Before it is cleared or otherwise established, code such as this is unsafe:

static int retry_count;

The variable may contain arbitrary RAM contents rather than zero. In board_init_f() code, prefer stack-local variables, initialized state carried in global_data, or explicitly initialized memory whose availability you can prove.

Where DDR initialization really lives

A common mistake is searching only for dram_init() and assuming its body is the complete DDR bring-up. On an actual SoC, DDR enablement can be divided between several layers:

LayerPossible responsibility
Board codeBoard-specific RAM topology, resistor/population variant selection, PMIC sequencing
SoC support codeDDR controller register programming, clock selection, PHY setup
DDR driver or training firmwareCalibration, training, impedance tuning, delay-line adjustment, error handling
Secure or system firmwarePermission, clock, power, resource-management services
U-Boot common codeCalls the selected initialization hooks and records detected DRAM banks

For an AM62x design, DDR is not just a matter of Cortex-A53 code writing controller registers. The K3 system’s firmware and device-management architecture participates in system services. Therefore, the right investigative question is:

Which source function first makes external DRAM reliable enough for this boot stage, and what prior power, clock, pinmux, or firmware services does it depend on?

This is much more useful than asking only “Where is the DDR initialization function?”

Implementing State-of-the-Art U-Boot Port, 2018 Edition - Marek Vasut, Self-employed

Watch “Implementing State-of-the-Art U-Boot Port, 2018 Edition” from The Linux Foundation. Marek Vasut gives a compact explanation of the reset entry, crt0.S, constrained board_f work, relocation, and the transition to board_r.

Watch the startup walk-through. Pay particular attention to the distinction between architecture-specific entry code, the shared C-runtime assembly bridge, early board_f initialization, and code that executes only after U-Boot has moved into RAM.


Relocation: U-Boot moves itself without losing control

Once DRAM is usable, U-Boot Proper generally relocates itself to a final DRAM region, often near the top of available RAM after reserving regions needed by firmware and boot artifacts. This is a controlled move of a live program, not simply a file copy.

Conceptually, relocation requires U-Boot to:

  1. Determine available DRAM and account for reserved regions.
  2. Select a safe destination for the relocated monitor image.
  3. Reserve space for the future stack, global_data, malloc arena, FDT control data, and architecture-specific requirements.
  4. Copy U-Boot’s executable and initialized-data regions to the new destination.
  5. Apply relocation records so address-bearing references remain valid at the new runtime location.
  6. establish or clear BSS in the new environment.
  7. Move the stack and global-data state as required.
  8. branch into board_init_r() at the relocated runtime address.

If an image was linked at address and its final runtime base is , the relocation offset is:

For an absolute address reference that was linked as , relocation makes the runtime reference:

This is why a raw binary copy is insufficient for a relocatable image. U-Boot must also process the relocation information produced by the linker, commonly associated with sections such as .rel.dyn or .rela.dyn, depending on architecture and toolchain conventions.

Not every reference requires the same repair. PC-relative references may already remain correct after the code and target move together; absolute addresses generally need adjustment. The linker and architecture startup code determine the exact mechanics.

The relocation boundary is an excellent debug boundary

A serial log that fails around DRAM size detection, prints an early banner but never reaches normal U-Boot, or resets immediately after a “relocating” style message often points to one of these categories:

ObservationMost plausible investigation area
No serial output at allBoot ROM selection, early entry assembly, pinmux, UART clocks, initial stack
Early serial output stops before DRAM is reportedDDR clocks, PMIC rails, DRAM controller/PHY configuration, training, early stack corruption
DRAM is reported, then execution hangsRelocation destination calculation, bad RAM size, reserved-memory collision, copy or relocation fault
Full U-Boot banner appears but storage is absentPost-relocation driver-model initialization, pinmux, clocks, device tree, configuration
Command prompt appears but automatic boot failsEnvironment, boot targets, image format, addresses, DTB, kernel command line

The visible text is not itself proof. Vendor branches customize messages and may buffer early output. But the last confirmed message is valuable evidence because it brackets the stage that failed.


board_init_r(): normal U-Boot becomes operational

After relocation, board_init_r() starts the broad, feature-rich U-Boot initialization phase. BSS is available, external DRAM is available, and the final runtime stack is in place.

A U-Boot `board_r.c` source view declaring `init_sequence_r[]`; the function-pointer list represents the post-relocation initialization work that prepares U-Boot for its command loop and boot policy.

The image illustrates a source pattern you will encounter frequently: an initialization sequence declared as an array of function pointers. Functions in this list are normally executed in order by common initialization code. Their exact membership changes with U-Boot revision and Kconfig settings, so a screenshot from one version is a navigation aid, not an authoritative sequence for another build.

Typical post-relocation responsibilities include:

  • cache and memory-management adjustments appropriate to the stage;
  • full console operation;
  • heap and malloc initialization;
  • driver-model and bus probing;
  • storage and filesystem support;
  • Ethernet, USB, and other optional subsystems;
  • loading and parsing the persistent U-Boot environment;
  • boot count, boot target, and boot policy setup;
  • entry to main_loop(), which can provide the U-Boot shell or execute bootcmd.

Porting U-Boot and Linux on New ARM Boards: A Step-by-Step Guide - Quentin Schulz, Free Electrons

Watch the short “Porting U-Boot and Linux on New ARM Boards” segment from The Linux Foundation for a practical view of the early and relocated initialization-call lists and their diagnostic value.

Watch the init sequences. Focus on why the early and relocated lists are separate, why conditional compilation changes the actual sequence, and why a failing initialization callback can stop boot partway through visible serial output.


A repeatable source-tracing procedure

When you receive a U-Boot source tree from a vendor SDK, do not start by reading every board file. Trace the startup path in a disciplined order and record the evidence.

1. Identify the exact build

Capture the U-Boot commit, vendor branch, board defconfig, and whether you are examining SPL or U-Boot Proper. They are separate binaries with different configuration options and may follow different startup paths.

git rev-parse HEAD
git status --short
grep -E 'CONFIG_(SPL|TPL|VPL|SYS_INIT_SP|STACK|DRAM|OF_CONTROL)' .config

A function present in the source but excluded from the generated .config cannot explain the behavior of the binary you are debugging.

2. Locate the two phase coordinators

Use source searches to find definitions and call sites:

git grep -n "board_init_f"
git grep -n "board_init_r"
git grep -n "init_sequence_f"
git grep -n "init_sequence_r"

Start in common/board_f.c and common/board_r.c if those files exist in your revision. Then follow calls outward toward architecture assembly and inward toward board or SoC hooks.

3. Find the temporary and final stack assignments

Search for stack-related configuration and assembly references:

git grep -n -E 'SYS_INIT_SP|SPL_STACK|STACK_R|STACK'
git grep -n -E 'board_init_f|relocate_code|board_init_r' -- 'arch/*'

In the architecture startup assembly, identify the instruction that writes the stack-pointer register. On AArch64, that is the architectural sp register. Record:

  • the initial stack address or expression;
  • the memory region that backs it;
  • whether that memory is usable before DRAM initialization;
  • the point at which a final stack is installed.

Do not infer stack validity from its numerical address alone. A plausible-looking DRAM address is invalid if clocks, power, controller setup, or training have not made DRAM operational.

4. Trace DDR from caller to hardware action

Search broadly, then follow the call chain:

git grep -n -E 'dram_init|ddr.*init|ddr_init|sdram'
git grep -n -E 'board_init_f|spl_board_init|spl_dram_init'

For each relevant match, classify it as one of the following:

  • a generic framework call;
  • a board policy hook;
  • a SoC controller or PHY routine;
  • a firmware/service request;
  • a training or calibration operation;
  • a size-detection or bookkeeping routine that runs after hardware initialization.

Only the chain that reaches the active build configuration is evidence of the board’s actual DDR path.

5. Identify the relocation handoff

Search the architecture startup code and common initialization code for relocation terms:

git grep -n -E 'reloc|relocate_code|relocaddr|reloc_off'
git grep -n -E 'reserve_.*|setup_.*addr' common arch board

Your trace should answer five concrete questions:

  1. Where is the relocation destination calculated?
  2. Which memory regions are reserved before choosing it?
  3. Which code performs the copy and relocation-record processing?
  4. At what point are BSS and the final stack valid?
  5. Where does execution enter board_init_r()?

6. Produce a compact startup trace

For the AM62x private implementation repository, use a table such as this:

Evidence itemLocation in your treeWhat it proves
Initial stack assignmentArchitecture assembly file and lineWhich memory supports calls before normal runtime initialization
board_init_f() definitionCommon early-init sourceWhich early initialization calls are enabled
DDR entry pointBoard, SoC, driver, or firmware hookHow the build establishes usable external DRAM
Relocation calculationCommon or architecture codeFinal U-Boot placement and reservation policy
Relocation transferAssembly or architecture routineWhere execution changes to the final copy
board_init_r() definitionCommon post-relocation sourceWhich services are enabled before boot policy begins
Serial-log milestoneCaptured boot logWhich stage was last known to complete

For a public portfolio, publish the method, a sanitized call graph, and non-sensitive log excerpts. Keep exact production memory reservations, secure firmware details, key material, signing settings, and provisioning logic private.


Key takeaways

U-Boot startup is best understood as a transition between two runtime environments:

  • Early assembly establishes just enough CPU state and temporary stack space to reach constrained C code.
  • board_init_f() prepares the platform for normal execution while BSS and rich runtime services may still be unavailable.
  • DDR initialization can span board code, SoC code, PHY/training logic, and platform firmware; trace active callers rather than stopping at the first dram_init() symbol.
  • Relocation selects a safe DRAM destination, copies U-Boot, applies relocation records, establishes final runtime memory, and transfers execution safely.
  • board_init_r() runs with DRAM and BSS available, initializes operational subsystems, and eventually enters U-Boot’s boot policy or command loop.
  • The most reliable investigation uses the exact source revision, generated configuration, startup assembly, initialization lists, linker/runtime data, and serial logs together.

Next, you will use this knowledge from the U-Boot command prompt: configuring a boot command that loads a kernel, DTB, and initramfs from a chosen storage device while proving that their memory regions do not overlap.

Can't find a good explanation? Sign up and we'll make it for you

Sign up