3. Bootloader 分区打包:U-Boot 镜像结构、编译与打包、校验头添加(CRC32)

Bootloader 是整个系统的「看门人」。它负责初始化硬件、加载内核,然后交出控制权。在嵌入式产品中,Bootloader 一旦损坏,设备基本就变砖了。所以,给 Bootloader 分区做打包和校验,是量产前必须搞定的一步。

我个人习惯把 Bootloader 分区打包拆成三个环节:理解镜像结构 → 编译出正确格式 → 添加校验头。下面我们一个一个来过。

3.1 U-Boot 镜像结构:它到底长什么样?

U-Boot 编译完成后,生成的不是一个裸的二进制文件,而是一个带有头部的镜像包。这个头部叫 U-Boot Image Header,它告诉加载器:这个镜像是给哪个 CPU 架构用的、加载到哪个地址、校验值是多少。

标准的 U-Boot 镜像头(ihdr)结构如下:

偏移(字节) 字段 大小 说明
0x00 ih_magic 4 魔数,固定为 0x27051956
0x04 ih_hcrc 4 头部 CRC32 校验值
0x08 ih_time 4 时间戳(Unix 时间戳)
0x0C ih_size 4 镜像数据大小(不含头部)
0x10 ih_load 4 加载地址
0x14 ih_ep 4 入口地址
0x18 ih_dcrc 4 数据区 CRC32 校验值
0x1C ih_os 1 操作系统类型(0x05 = Linux)
0x1D ih_arch 1 CPU 架构(0x02 = ARM)
0x1E ih_type 1 镜像类型(0x02 = 内核,0x05 = 固件)
0x1F ih_comp 1 压缩类型(0x00 = 未压缩)
0x20 ih_name 32 镜像名称(ASCII 字符串)

头部总共 64 字节。后面紧跟着实际的数据区。加载器拿到镜像后,先读头部,校验 ih_hcrc 确保头部没被篡改,再校验 ih_dcrc 确保数据完整。两关都过了,才敢把数据加载到内存里执行。

关键点:ih_hcrc 校验的是头部自身(从 ih_magic 到 ih_name 共 64 字节,但 ih_hcrc 字段本身在计算时填 0)。ih_dcrc 校验的是头部之后的数据区。这两个 CRC 缺一不可。

3.2 编译与打包:从源码到可烧录镜像

U-Boot 的编译流程,说白了就是三步:配置、编译、打包。但这里有个坑——很多人以为编译完的 u-boot.bin 就能直接烧,其实不对。

标准流程是这样的:

  1. 配置:make <board>_defconfig,选择目标板级配置。
  2. 编译:make,生成 u-boot、u-boot.bin、u-boot.map 等文件。
  3. 打包:tools/mkimage 工具将 u-boot.bin 包装成 U-Boot 镜像格式。

我记得第一次做量产打包时,直接拿 u-boot.bin 烧进去,结果板子死活起不来。后来才发现,BootROM 只认带头部格式的镜像,裸的 bin 文件它根本不认。

正确的打包命令:

# 将 u-boot.bin 打包为 u-boot.img
tools/mkimage -A arm -O linux -T firmware -C none \
  -a 0x20000000 -e 0x20000000 \
  -n "U-Boot 2024.01 for MyBoard" \
  -d u-boot.bin u-boot.img

参数说明:

  • -A arm:指定 ARM 架构
  • -O linux:操作系统类型
  • -T firmware:镜像类型为固件
  • -C none:不压缩
  • -a 0x20000000:加载地址
  • -e 0x20000000:入口地址(通常与加载地址相同)
  • -n "...":镜像名称,会写入 ih_name 字段
  • -d u-boot.bin:输入文件
  • u-boot.img:输出文件

小技巧:如果你的板子有 SPL(Secondary Program Loader),那流程会多一层。SPL 编译出来是 u-boot-spl.bin,它也需要用 mkimage 打包,但类型通常设为 -T standalone。我习惯在 Makefile 里加一个 pack 目标,把这两步自动化。

3.3 校验头添加:CRC32 的计算与写入

mkimage 工具在打包时,会自动计算并写入 CRC32 校验值。但如果你需要自己实现打包脚本(比如在量产工具里动态生成镜像),就得手动算 CRC32。

计算流程:

  1. 准备 64 字节头部缓冲区,所有字段先填 0,尤其是 ih_hcrc 和 ih_dcrc。
  2. 填充魔数、时间戳、大小、加载地址、入口地址等字段。
  3. 计算数据区的 CRC32,写入 ih_dcrc。
  4. 将 ih_hcrc 字段置 0,计算整个头部(64 字节)的 CRC32,写入 ih_hcrc。

为什么 ih_hcrc 计算时要先置 0?因为校验值本身不能参与自己的校验计算,否则就成了「先有鸡还是先有蛋」的问题。你想想看,如果 ih_hcrc 里已经有一个值,再拿整个头部算 CRC,结果肯定对不上。

下面是一个 C 语言示例,演示如何手动构建 U-Boot 镜像头并计算 CRC32:

#include <stdio.h>
#include <stdint.h>
#include <string.h>
#include <zlib.h>  // 使用 zlib 的 crc32 函数

#define UBOOT_MAGIC         0x27051956
#define IH_OS_LINUX         0x05
#define IH_ARCH_ARM         0x02
#define IH_TYPE_FIRMWARE    0x05
#define IH_COMP_NONE        0x00
#define HEADER_SIZE         64

typedef struct {
    uint32_t ih_magic;
    uint32_t ih_hcrc;
    uint32_t ih_time;
    uint32_t ih_size;
    uint32_t ih_load;
    uint32_t ih_ep;
    uint32_t ih_dcrc;
    uint8_t  ih_os;
    uint8_t  ih_arch;
    uint8_t  ih_type;
    uint8_t  ih_comp;
    char     ih_name[32];
} uboot_image_header_t;

int build_uboot_image(const char *data_file, const char *output_file,
                      uint32_t load_addr, uint32_t entry_addr) {
    FILE *fin = fopen(data_file, "rb");
    if (!fin) return -1;

    // 读取数据区
    fseek(fin, 0, SEEK_END);
    uint32_t data_size = ftell(fin);
    fseek(fin, 0, SEEK_SET);

    uint8_t *data = malloc(data_size);
    fread(data, 1, data_size, fin);
    fclose(fin);

    // 准备头部
    uboot_image_header_t hdr;
    memset(&hdr, 0, sizeof(hdr));
    hdr.ih_magic = UBOOT_MAGIC;
    hdr.ih_time  = (uint32_t)time(NULL);
    hdr.ih_size  = data_size;
    hdr.ih_load  = load_addr;
    hdr.ih_ep    = entry_addr;
    hdr.ih_os    = IH_OS_LINUX;
    hdr.ih_arch  = IH_ARCH_ARM;
    hdr.ih_type  = IH_TYPE_FIRMWARE;
    hdr.ih_comp  = IH_COMP_NONE;
    strncpy(hdr.ih_name, "U-Boot for MyBoard", 31);

    // 计算数据区 CRC
    hdr.ih_dcrc = crc32(0, data, data_size);

    // 计算头部 CRC(ih_hcrc 先置 0)
    hdr.ih_hcrc = 0;
    hdr.ih_hcrc = crc32(0, (uint8_t *)&hdr, HEADER_SIZE);

    // 写入输出文件
    FILE *fout = fopen(output_file, "wb");
    if (!fout) { free(data); return -1; }
    fwrite(&hdr, 1, HEADER_SIZE, fout);
    fwrite(data, 1, data_size, fout);
    fclose(fout);

    free(data);
    printf("Done: %s (size=%u, hcrc=0x%08X, dcrc=0x%08X)\n",
           output_file, HEADER_SIZE + data_size,
           hdr.ih_hcrc, hdr.ih_dcrc);
    return 0;
}

曾经踩过的坑:我遇到过一个问题——用 zlib 的 crc32 算出来的值,跟 mkimage 算的不一样。后来发现是字节序的问题。U-Boot 头部字段都是大端(Big-Endian)存储,而我的 x86 开发机是小端。所以计算 CRC 前,必须把多字节字段转换成大端字节序。上面示例为了清晰没做转换,实际代码里记得用 htonl() 处理。

3.4 校验流程:烧录前和启动时的双重保障

打包完成后,镜像里已经嵌入了两个 CRC32 值。那校验发生在什么时候?

  • 烧录前校验:量产工具读取 u-boot.img,重新计算 ih_hcrc 和 ih_dcrc,跟头部里的值比对。不一致就报错,防止烧录坏镜像。
  • 启动时校验:BootROM 或 SPL 加载 u-boot.img 时,先校验头部 CRC,再校验数据 CRC。任何一步失败,就进入恢复模式或打印错误信息。

我个人习惯在量产脚本里加一个 verify 步骤,用 mkimage 的 -l 参数检查镜像:

tools/mkimage -l u-boot.img
# 输出示例:
# Image Name:   U-Boot 2024.01 for MyBoard
# Created:      Thu Jan 18 10:30:00 2024
# Image Type:   ARM U-Boot Firmware (uncompressed)
# Data Size:    524288 Bytes = 512.00 KiB = 0.50 MiB
# Load Address: 20000000
# Entry Point:  20000000
# CRC Check:    OK

看到 CRC Check: OK,心里就踏实了。

3.5 本章小结:一张图看懂 Bootloader 分区打包

下面这张图把整个流程串起来了。从源码编译到最终烧录,每一步的输入输出和校验点都标清楚了。

Bootloader 分区打包与校验流程 Bootloader 分区打包与校验流程 U-Boot 源码编译 u-boot.bin mkimage 打包 u-boot.img 烧录前 CRC 校验 烧录到 Flash BootROM 校验 校验通过 → 启动 校验失败 → 恢复模式 校验内容: • ih_hcrc:头部 CRC • ih_dcrc:数据区 CRC • 两者都通过才算 OK

嗯,到这里 Bootloader 分区打包的核心内容就讲完了。从镜像结构到编译打包,再到 CRC32 校验头的添加,每一步都有它的道理。下次你拿到一块新板子,按这个流程走一遍,基本不会出大问题。