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 就能直接烧,其实不对。
标准流程是这样的:
- 配置:
make <board>_defconfig,选择目标板级配置。 - 编译:
make,生成 u-boot、u-boot.bin、u-boot.map 等文件。 - 打包:
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。
计算流程:
- 准备 64 字节头部缓冲区,所有字段先填 0,尤其是 ih_hcrc 和 ih_dcrc。
- 填充魔数、时间戳、大小、加载地址、入口地址等字段。
- 计算数据区的 CRC32,写入 ih_dcrc。
- 将 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 分区打包的核心内容就讲完了。从镜像结构到编译打包,再到 CRC32 校验头的添加,每一步都有它的道理。下次你拿到一块新板子,按这个流程走一遍,基本不会出大问题。