Notes on “Xilinx Zynq-7000 Embedded System Design and Implementation”111 U-Boot File Structure
Unlocking the U-Boot Toolbox: A Code Architecture Guide Essential for FPGA Developers
When you receive a U-Boot source code package, facing hundreds of folders and files, do you feel like standing at the entrance of a massive mechanical workshop filled with various tools and parts, unsure of where to start?
Don’t worry! As FPGA developers, our strength lies in understanding structured design. The code structure of U-Boot is actually like a super factory providing “boot solutions” for different embedded boards. Today, we will delve into this factory from the perspective of FPGA and see how it customizes services for our boards (such as a Zynq or MicroBlaze development board).
1. Overview: Functional Partitioning of Core Directories
When you extract a U-Boot source package (for example, <span><span>u-boot-xilinx-v2022.01.tar.gz</span></span>), you will see dozens of folders at the top level. We don’t need to memorize them all, but we must recognize several “core functional areas”.
Imagine this factory has the following departments:
-
<span><span>arch/</span></span>– CPU Design Department:
- Function This is the lowest-level department, closely related to CPU architecture. It contains the code for how U-Boot initializes specific CPU cores.
- FPGA Instance If your FPGA uses an ARM Cortex-A53 core (like Zynq UltraScale+), then the relevant code is under
<span><span>arch/arm/</span></span>. If your FPGA uses a RISC-V soft core, it will be under<span><span>arch/riscv/</span></span>. If it’s a MicroBlaze soft core, it will be under<span><span>arch/microblaze/</span></span>. This directory determines how U-Boot directly communicates with the “brain” of the CPU.
<span><span>board/</span></span> – Board Customization Workshop:
- Function This is the directory most closely related to us FPGA developers! It is responsible for hardware initialization for specific development boards or custom boards. Each board has its own “sub-workshop” here.
- FPGA Instance For example, if you are using Avnet’s ZedBoard, then in the
<span><span>board/xilinx/zynq/</span></span>directory, you will likely find a<span><span>zedboard</span></span>related directory. This contains the board-specific code: for example, GPIO initialization (to control an LED), QSPI Flash read/write, SD card detection pin settings, etc. When you port U-Boot for your custom FPGA board, most of the work is done by creating new board-level directories and files here.
<span><span>cmd/</span></span> – Command Set Toolbox:
- Function This is where all the source code for the commands we can use in the U-Boot command line is stored. For example,
<span><span>bootm</span></span>,<span><span>tftpboot</span></span>,<span><span>mmc read</span></span>,<span><span>setenv</span></span>,<span><span>ping</span></span>, etc. - FPGA Instance When you are debugging and enter
<span><span>mmc list</span></span>to check if the SD card is recognized, the code for this “list” command is located in<span><span>cmd/mmc.c</span></span>. If you want to add a custom command to U-Boot (for example, a command specifically for configuring a certain IP core in your FPGA), you will need to create a new<span><span>.c</span></span>file here.
<span><span>common/</span></span> – Common Resource Library:
- Function Contains some common functions that are architecture and board independent. For example, handling environment variables, command line parsing, CRC checks, U-Boot’s main loop, etc.
<span><span>drivers/</span></span> – Peripheral Driver Repository:
- Function This is the department with the largest amount of code, containing almost all common peripheral driver models and specific drivers, such as serial ports, network cards, USB, SD/MMC, I2C, SPI, GPIO, etc.
- FPGA Instance The driver for the Cadence e1000 Gigabit Ethernet card on your Zynq board is located under
<span><span>drivers/net/</span></span>. Your QSPI Flash driver is in<span><span>drivers/mtd/spi/</span></span>. When you add a new Ethernet PHY chip to your board, you may need to find or write the corresponding driver here.
<span><span>dts/</span></span> – Hardware Description File Archive:
- Function Stores device tree source files (
<span><span>.dts</span></span>and<span><span>.dtsi</span></span>).<span><span>.dts</span></span><code><span><span> describes specific boards, while </span></span><code><span><span>.dtsi</span></span><span><span> is a SoC-level general description that can be reused across multiple boards.</span></span> - FPGA Instance For Xilinx’s Zynq-7000, the general definition of the SoC is in
<span><span>dts/arm/zynq-7000.dtsi</span></span>, while the specific definition for the ZedBoard may be in<span><span>dts/arm/zynq-zed.dts</span></span>. This file describes the CPU, memory addresses, peripheral connections, etc., and serves as a bridge connecting FPGA hardware design with Linux software.
<span><span>include/</span></span> – Header Files and Configuration Center:
- Function Contains a large number of header files (
<span><span>.h</span></span>). One of the most important is the<span><span>include/configs/</span></span>directory, which stores the core configuration files for each board. - FPGA Instance
<span><span>include/configs/zedboard.h</span></span>. This header file uses a large number of<span><span>CONFIG_*</span></span>macros to trim U-Boot’s functionality. For example, by using<span><span>#define</span><span> CONFIG_SYS_MMC_ENV_DEV 0</span></span>to define which MMC device the environment variables are stored on. Configuring U-Boot mainly involves modifying or creating header files in this directory.
2. Delving Deeper: How is a Board “Assembled”?
Let’s track how the code is “picked” and “assembled” from these directories when you compile U-Boot for a custom Zynq FPGA board.
Step 1: Select the Blueprint – Configuration File
Compiling U-Boot usually starts with <span><span>make <board_name>_defconfig</span></span>. This command finds the corresponding configuration file (such as <span><span>zynq_zed_defconfig</span></span>) from the <span><span>configs/</span></span> directory and copies its content into the top-level <span><span>.config</span></span> file. This <span><span>.config</span></span> file serves as the “overall blueprint” for this compilation, defining:
<span><span>CONFIG_ARM=y</span></span>-> Compiles code for the ARM architecture.<span><span>CONFIG_TARGET_ZYNQ_ZED=y</span></span>-> Compiles board-level code for the ZedBoard.<span><span>CONFIG_CMD_MMC=y</span></span>-> Includes the MMC command toolbox in the compilation.<span><span>CONFIG_E1000=y</span></span>-> Includes the driver from the e1000 Ethernet card driver repository.
Step 2: Boot Process – Code Execution Path
The boot process of U-Boot follows a fixed sequence, which is reflected in the order of code calls. Let’s look at the file structure:
-
<span><span>arch/arm/cpu/armv7/start.S</span></span>: This is the entry point of the entire U-Boot! After the CPU powers on, it jumps here. This is written in assembly and is responsible for setting the CPU to SVC mode, disabling interrupts, setting the exception vector table, and other fundamental tasks. It will eventually call the C function<span><span>_main</span></span>. -
<span><span>arch/arm/lib/crt0.S</span></span>: This file contains the<span><span>_main</span></span>function, which is responsible for setting up the initial stack space before calling<span><span>board_init_f</span></span>. -
<span><span>common/board_f.c</span></span>: The<span><span>board_init_f</span></span>function here is the core of the first stage initialization. It performs a series of initializations that do not require relocation, such as:
- Calling functions in
<span><span>arch/arm/mach-zynq/spl.c</span></span>to initialize the Zynq clock and DDR3 controller (This is crucial for FPGA system startup!). - Initializing the serial port (so we can see output in the terminal).
- Preparing for U-Boot’s relocation.
<span><span>common/board_r.c</span></span>: After completing the relocation, the <span><span>board_init_r</span></span> function begins executing the second stage initialization. It will call the “initialization sequence” to initialize all enabled drivers and devices in turn:
<span><span>drivers/mmc/zynq_sdhci.c</span></span>-> Initializes the SD card controller.<span><span>drivers/net/zynq_gem.c</span></span>-> Initializes the Ethernet controller.<span><span>cmd/nvedit.c</span></span>function -> Reads environment variables from Flash.
Entering the Main Loop: After all initializations are complete, it enters the <span><span>common/main.c</span></span> function’s <span><span>main_loop</span></span> function. Here, it checks if there is a key interrupt; if not, it executes the command in the <span><span>bootcmd</span></span> environment variable, ultimately loading and starting the Linux kernel.
3. Practical Experience for FPGA Developers: Adding an LED Driver as an Example
Suppose we want to light up an LED connected to the PS MIO when U-Boot starts on our custom board.
- Modify the Device Tree In your board’s
<span><span>dts/arm/</span></span>directory, add a node for this LED, defining its GPIO controller and pin number. - Modify the Board Code In your board directory under
<span><span>board/xilinx/zynq/</span></span>(for example,<span><span>mymodule.c</span></span>), find a suitable initialization function (such as<span><span>board_early_init_f</span></span>), and add the GPIO initialization and code to set high/low levels. - Check Configuration Ensure that the GPIO-related configuration macros (such as
<span><span>CONFIG_DM_GPIO=y</span></span>) are defined in<span><span>include/configs/your_board.h</span></span>. - Compile and Test Recompile U-Boot and flash it to the board. After powering on, you should see the LED lit up at the expected stage.
This process perfectly demonstrates the collaborative work of the U-Boot file structure: The device tree (<span><span>dts/</span></span>) provides hardware information, the board code (<span><span>board/</span></span>) performs specific operations, the driver model (<span><span>drivers/</span></span>) provides GPIO operation interfaces, while the configuration header files (<span><span>include/configs/</span></span>) control the entire functionality.
Conclusion: From Chaos to Clarity
Through this “archaeological” exploration of the U-Boot file structure, we find that it is not a tangled mess, but rather a highly modular and well-defined design.
<span><span>arch/</span></span>manages the CPU.<span><span>board/</span></span>manages the board.<span><span>drivers/</span></span>manages peripherals.<span><span>dts/</span></span>manages descriptions.<span><span>include/configs/</span></span>manages functionality trimming.
As an FPGA developer, when your system fails to boot, you can trace the problem like a detective based on the logs printed to the serial port:
- Stuck at “DRAM initialization”? -> Check
<span><span>arch/.../spl.c</span></span>and your Vivado DDR configuration. - SD card not recognized? -> Check the drivers under
<span><span>drivers/mmc/</span></span>and the board-level pin configurations in<span><span>board/...</span></span>. - Network
<span><span>ping</span></span><span><span> not working? -> Check the drivers under </span></span><code><span><span>drivers/net/</span></span>and PHY configurations. - Environment variables not saving? -> Check
<span><span>common/env_...</span></span>and the implementation of storage drivers.
Now, when you open the U-Boot source code again, do you feel it has transformed from a strange behemoth into a clearly structured treasure trove ready for exploration? Understanding its structure is a necessary path to becoming a true embedded systems expert.
