3、Bootloader定制:U-Boot的移植流程、启动参数配置、安全启动链(Secure Boot)的实现、双系统启动方案设计

Bootloader这东西,说白了就是系统上电后跑的第一段代码。它负责把内核从Flash里捞出来,放到内存里,然后跳过去执行。在POS机这种对安全性和稳定性要求极高的场景下,Bootloader的定制工作就显得格外重要。我个人习惯把U-Boot比作「看门大爷」——它得先确认来的人是不是自己人,再决定要不要开门。

3.1 U-Boot的移植流程

移植U-Boot,说白了就是让它在你的板子上跑起来。我刚开始做移植时,总觉得这活儿很玄学,后来发现其实就是三板斧:板级配置、驱动适配、编译调试。

第一步:选择基础版本
U-Boot官方主线支持大量SoC。我建议你从接近自己硬件的参考板开始。比如你的POS机用的是全志V3s,那就找sunxi家族的配置做模板。

第二步:创建板级目录

cd u-boot
mkdir board/mycompany/mypos
cp -r board/sunxi/sun8i/* board/mycompany/mypos/

嗯,这里要注意:目录名别太长,否则编译时路径截断会出怪问题。我曾经因为目录名超过32个字符,折腾了两天才发现是这原因。

第三步:修改Kconfig和Makefile
你需要告诉U-Boot:我的板子用哪个CPU、哪个DRAM、哪个串口。在board/mycompany/mypos/Kconfig里加上:

config TARGET_MYPOS
    bool "Support mypos board"
    select CPU_V7
    select DRAM_TYPE_DDR3

第四步:配置设备树
设备树是U-Boot和内核沟通的「翻译官」。你得把板子上的GPIO、I2C、SPI等外设信息写清楚。我习惯先拿参考板的dts改,删掉不需要的外设,加上POS机特有的——比如打印机串口、密码键盘I2C地址。

第五步:编译与烧录

make ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf- mypos_defconfig
make -j4
# 得到 u-boot-sunxi-with-spl.bin

你想想看,如果这一步报错,八成是交叉编译器版本不对。我建议用Linaro的gcc 7.5以上版本,太老的编译器会缺某些头文件。

我的小技巧: 第一次移植时,先别管NAND、网络这些复杂驱动。只让U-Boot能从SD卡启动、串口能打印信息就行。这叫「最小可启动系统」——先跑起来,再慢慢加功能。

3.2 启动参数配置

U-Boot的启动参数,其实就是一组环境变量。它们告诉内核:根文件系统在哪、控制台用哪个串口、内存大小是多少。我见过不少同事,内核都编好了,结果启动参数写错,系统死活起不来。

核心参数解析:

参数名 示例值 说明
bootargs console=ttyS0,115200 root=/dev/mmcblk0p2 rw 内核命令行参数
bootcmd mmc dev 0; fatload mmc 0:1 0x42000000 uImage; bootm 0x42000000 自动启动时执行的命令
fdt_addr 0x43000000 设备树在内存中的加载地址
loadaddr 0x42000000 内核镜像加载地址

配置方法:

# 在U-Boot命令行中设置
setenv bootargs 'console=ttyS0,115200 root=/dev/mmcblk0p2 rw rootwait'
setenv bootcmd 'mmc dev 0; fatload mmc 0:1 ${loadaddr} uImage; bootm ${loadaddr}'
saveenv

这里有个坑:POS机通常用NAND Flash存系统,但NAND有坏块问题。我建议在bootargs里加上mtdparts参数,明确分区表,避免内核把坏块当成好块用。

注意: 千万别把saveenv忘了!我有个同事调试了一下午,每次重启参数都变回默认值,最后发现是没保存环境变量到Flash里。

3.3 安全启动链(Secure Boot)的实现

安全启动,说白了就是「验明正身」。从BootROM开始,每一级代码都要验证下一级的签名,确保没有被篡改。POS机涉及金融交易,这步是强制要求——银联认证里明确写了。

实现流程:

  1. 生成密钥对: 用OpenSSL生成RSA 2048位密钥
  2. 签名镜像: 对U-Boot、内核、设备树分别签名
  3. 烧录公钥: 把公钥哈希烧到SoC的efuse里(一次性,不可更改)
  4. 验证链: BootROM验证SPL → SPL验证U-Boot → U-Boot验证内核

代码层面怎么做? 以全志平台为例,在U-Boot配置中开启:

CONFIG_SECURE_BOOT=y
CONFIG_RSA=y
CONFIG_FIT_SIGNATURE=y

然后使用mkimage工具签名:

mkimage -f fit.its -k keys/ -r u-boot.itb

这里fit.its是镜像描述文件,里面指定了哪些镜像需要验证、用哪把公钥。我建议把公钥放在U-Boot的DTS里,编译时直接嵌进去,省得运行时再去读efuse——efuse读多了会磨损。

我曾经踩过的坑: 第一次做Secure Boot时,我把私钥和公钥放在同一个目录下,结果编译脚本把私钥也打包进了固件。测试人员一分析固件,直接拿到了私钥...嗯,从那以后我严格区分了keys/privatekeys/public目录,并且把私钥目录加了.gitignore

3.4 双系统启动方案设计

双系统启动,说白了就是「A/B分区」——一个系统坏了,另一个顶上。POS机不能因为系统升级失败就变砖,这方案是行业标配。

分区布局:

mtd0: Bootloader (U-Boot)
mtd1: 系统A (内核+根文件系统)
mtd2: 系统B (内核+根文件系统)
mtd3: 数据分区 (交易记录、日志)
mtd4: 标志分区 (记录当前启动哪个系统)

U-Boot中的实现逻辑:

# 读取标志分区,判断启动哪个系统
if mmc read 0x43000000 0x100 0x10; then
    # 检查标志位
    if itest.b *0x43000000 == 0x41; then
        # 启动系统A
        setenv bootpart 1
    else
        # 启动系统B
        setenv bootpart 2
    fi
fi

# 加载对应分区的内核
mmc dev 0
fatload mmc 0:${bootpart} ${loadaddr} uImage
bootm ${loadaddr}

升级流程:

  1. 当前运行系统A,下载新固件到系统B分区
  2. 验证系统B的完整性(校验哈希或签名)
  3. 修改标志分区,指向系统B
  4. 重启,U-Boot从系统B启动
  5. 如果系统B启动失败,U-Boot超时后自动切回系统A

我的建议: 标志分区别用单个bit,用「计数器+校验」的方式。比如写0xA5A5A5A5表示系统A,写0x5A5A5A5A表示系统B。万一写标志时掉电,至少不会出现「两个系统都认为自己是当前系统」的混乱局面。

你想想看,双系统方案最怕什么?最怕「两个系统都坏了」。所以我在实际项目中,还会在U-Boot里保留一个「救援模式」——按住某个按键开机,直接从SD卡或USB加载一个最小系统,用于恢复固件。这个功能平时用不到,但真遇到变砖时,它就是救命稻草。

嗯,关于Bootloader定制,今天就聊这么多。记住一句话:Bootloader是系统的「第一道防线」,它稳了,整个系统就稳了一半。