3. 嵌入式Linux系统启动流程与产测模式设计

STM32MP1 系列处理器的启动链路与通用 Cortex-A 应用处理器类似,但多级固件分工明确,每一级都有独立的加载地址、校验方式和可干预点。理解这条链路,是设计产测模式触发条件的前提:只有知道在哪一级可以插入判断、在哪一级可以改写启动参数,才能让设备在"正常启动"和"进入产测"之间可靠切换。

一、五级启动链路的分工

从上电到根文件系统挂载,STM32MP1 依次经历 ROM Code、FSBL(TF-A)、SSBL(U-Boot)、Linux Kernel、Rootfs 五个阶段。

ROM Code 固化在芯片内部,上电后首先运行。它读取 BOOT 引脚电平决定启动介质(eMMC、SD 卡、NAND、串口/USB),从介质固定偏移处加载 FSBL 到 SYSRAM,并校验头部签名。这一级不可修改,但 BOOT 引脚状态是产测可以借助的硬件输入。

TF-A(Trusted Firmware-A) 作为 FSBL,运行在 SYSRAM 中,负责 DDR 初始化、时钟树配置、安全域划分,然后加载 U-Boot 到 DDR。STM32MP1 的 TF-A 分为 BL2 和 BL32(OP-TEE)两部分,BL2 完成基础初始化,BL32 提供安全服务。

U-Boot 作为 SSBL,运行在 DDR 中,是启动流程中可干预程度最高的一级。它读取环境变量、加载内核镜像和设备树、拼接 bootargs,最终跳转到内核入口。产测模式的触发判断通常放在这一级。

Linux Kernel 解压后初始化子系统,解析设备树,挂载根文件系统。内核命令行中的 root=、init=、console= 等参数直接决定后续行为。

Rootfs 挂载后执行 init 进程,启动用户空间服务。产测程序通常作为 init 脚本中的一个分支被拉起。

STM32MP1 五级启动链路与产测介入点 ROM Code BOOT引脚选介质 加载FSBL到SYSRAM TF-A (FSBL) DDR初始化/时钟 加载U-Boot到DDR U-Boot (SSBL) 读环境变量 拼bootargs/加载内核 Linux Kernel 解析设备树 挂载根文件系统 Rootfs init进程 启动服务 产测触发判断点 GPIO / 按键 / 环境变量 正常启动 bootargs root=/dev/mmcblk0p5 rw 产测启动 bootargs root=/dev/mmcblk0p7 init=/sbin/factory_init ROM Code 不可改 → TF-A 可裁剪 → U-Boot 可编程判断 → Kernel 参数可切换 → Rootfs 可分流 产测模式的核心介入点集中在 U-Boot 阶段,通过环境变量与 GPIO 状态决定 bootargs 走向

二、产测模式的触发条件设计

产测模式不能依赖人工输入命令,必须在无人值守的产线环境下自动判断。常见的触发条件有三类,实际项目中通常组合使用。

GPIO 电平触发是最直接的方式。产测治具上设置一个拉高/拉低的检测引脚,例如 PA13,U-Boot 启动时读取该引脚状态。若为有效电平,则设置 factory_mode=1。这种方式响应快、不受存储介质影响,但需要硬件配合。

按键长按触发适合没有治具的场景。U-Boot 在启动延时阶段检测某个按键(如 USER1)是否持续按下超过 2 秒。实现上可在 board_late_init() 中轮询 GPIO,或利用 bootdelay 期间的 ctrlc 机制扩展。

环境变量或标志分区触发适合远程切换。在 eMMC 中划分一个 misc 分区,写入 factory_flag 字节。U-Boot 读取该分区,若标志有效则进入产测。这种方式可以在系统运行时通过 reboot factory 命令写入标志后重启生效。

建议采用"GPIO 优先、标志分区兜底"的组合策略:产线治具用 GPIO 保证确定性,售后返修用标志分区保证可远程操作。

三、U-Boot 中的实现要点

在 U-Boot 源码的板级文件中,核心逻辑集中在 board_late_init() 或自定义的 board_factory_check() 函数。以下是一个可参考的实现框架:

/* board/st/stm32mp1/factory_check.c */
static int board_factory_check(void)
{
    int gpio_val;
    int ret;

    /* 1. 读取产测检测GPIO */
    gpio_val = gpio_get_value(FACTORY_DETECT_GPIO);
    if (gpio_val == FACTORY_ACTIVE_LEVEL) {
        env_set("factory_mode", "1");
        return 0;
    }

    /* 2. 读取misc分区标志 */
    ret = read_misc_flag(&flag);
    if (ret == 0 && flag == FACTORY_FLAG_MAGIC) {
        env_set("factory_mode", "1");
        return 0;
    }

    env_set("factory_mode", "0");
    return 0;
}

随后在环境变量中定义两套 bootargs,由 factory_mode 决定引用哪一套:

# 正常启动
bootargs_normal=root=/dev/mmcblk0p5 rootwait rw console=ttySTM0,115200

# 产测启动
bootargs_factory=root=/dev/mmcblk0p7 rootwait rw console=ttySTM0,115200 init=/sbin/factory_init

# 选择逻辑
bootargs=${bootargs_normal}
if test "${factory_mode}" = "1"; then
    setenv bootargs ${bootargs_factory}
fi

这里的关键是产测根文件系统与正常根文件系统分离。产测 rootfs 通常是一个精简的 BusyBox 环境,只包含测试程序、必要的驱动模块和结果上报工具,避免正常系统的服务干扰测试。

四、启动参数与产测程序的衔接

产测 rootfs 的 init 脚本需要完成三件事:挂载必要的虚拟文件系统、加载测试所需的驱动模块、启动产测主程序。一个典型的 /sbin/factory_init 脚本结构如下:

#!/bin/sh
mount -t proc none /proc
mount -t sysfs none /sys
mount -t devtmpfs none /dev

# 加载测试所需模块
modprobe stm32_i2c
modprobe stm32_spi
modprobe dwc2

# 启动产测主程序,结果写入日志分区
/usr/bin/factory_test --log /var/log/factory_result.json

# 测试完成后根据结果决定是否重启
if [ $? -eq 0 ]; then
    echo "PASS" > /dev/mmcblk0p8
else
    echo "FAIL" > /dev/mmcblk0p8
fi
poweroff -f

产测结果写入独立分区而非 rootfs,是为了保证即使 rootfs 被重新烧录,测试记录仍然保留,便于产线追溯。

五、设计检查清单

检查项要求验证方式
GPIO 触发有效电平与硬件原理图一致万用表测量治具引脚
标志分区misc 分区可读写,magic 值唯一U-Boot 下 mmc read 验证
bootargs 切换两套参数互不干扰串口打印确认 root= 路径
产测 rootfs独立分区,不依赖正常系统拔掉正常 rootfs 仍能启动
结果持久化写入独立分区,掉电不丢失重启后读取结果分区
注意:修改 U-Boot 环境变量后必须执行 saveenv 才会写入存储介质。产测模式下如果频繁写环境变量,需评估 eMMC 寿命,建议将 factory_mode 作为运行时变量而非持久化变量。

整体来看,产测模式的设计本质是在启动链路的 U-Boot 阶段插入一个可控的分支判断。触发条件决定"是否进入",bootargs 决定"进入后加载什么",rootfs 中的 init 脚本决定"进入后做什么"。三者配合,才能形成一条从硬件信号到测试结果的完整通路。