1. J6M平台U-Boot启动流程深度解析
在嵌入式系统开发中,U-Boot作为最常用的Bootloader之一,其启动流程的理解对于驱动开发和系统移植至关重要。本文将基于J6M平台,深入剖析U-Boot的完整启动过程,特别关注SPL和U-Boot Proper两个阶段的实现细节。
1.1 启动流程整体架构
J6M平台的U-Boot启动过程分为两个主要阶段:
-
SPL阶段(Secondary Program Loader)
- 负责最基本的硬件初始化
- 主要任务包括DDR初始化、启动信息读取
- 决定下一步加载路径(Flash/UART/CAN/USB)
-
U-Boot Proper阶段
- 完整的U-Boot功能
- 设置环境变量、生成bootargs
- 决定内核加载位置和启动参数
这两个阶段虽然功能不同,但共享部分关键信息,特别是通过strap_pin、bootinfo和bootcount等寄存器传递的启动上下文。
1.2 关键寄存器与数据结构
在深入分析之前,我们需要了解几个关键寄存器:
| 寄存器名称 | 地址 | 作用描述 |
|---|---|---|
| BOOTSTRAP_REG | 0x23200068 | 硬件Strap采样结果 |
| BOOTROM_BOOTINFO_REG | 0x23200028 | BootROM传递给后续阶段的启动信息 |
| BOOTROM_COUNT_REG | 0x23200024 | 启动失败计数 |
| ADC_CH0/ADC_CH1 | 0x23210430 | 板型ADC信息 |
| SECURE_LIFECYCLE | 0x23210460 | 安全生命周期信息 |
这些寄存器值在SPL阶段被读取后,会保存在一个共享数据结构sci_boot_flags中,供U-Boot Proper阶段使用:
c复制struct sci_boot_flags {
uint32_t magic; // 魔数校验
uint32_t strap_pin; // Strap采样值
uint32_t boardid_adc_ch0; // ADC通道0值
uint32_t boardid_adc_ch1; // ADC通道1值
uint32_t bootinfo; // BootROM传递的信息
uint32_t bootcount; // 启动计数
uint32_t secure_lifecycle; // 安全生命周期状态
};
2. SPL阶段深度分析
2.1 SPL启动流程概览
SPL作为上电后最先执行的代码,其核心任务不是提供完整功能,而是建立最基本的运行环境。在J6M平台上,SPL的主要执行流程如下:
- 初始化最小硬件环境
- 配置DDR控制器
- 读取并打印Strap/bootinfo/bootcount等关键信息
- 决定下一步加载路径
- 加载并跳转到U-Boot Proper
2.2 SPL代码执行链路
SPL的代码执行遵循典型的ARM64 U-Boot启动流程,但有其特殊性:
c复制_start
-> reset
-> save_boot_params()
-> set_vbar / apply_core_errata / lowlevel_init()
-> _main
_main
-> 设置早期栈
-> board_init_f_alloc_reserve()
-> board_init_f_init_reserve()
-> board_init_f()
其中board_init_f()是进入板级C代码的真正入口。在J6M平台上,SPL的board_init_f()实现位于board/hobot/common/spl.c中。
2.3 SPL关键初始化过程
SPL的board_init_f()函数完成了多项关键初始化:
c复制board_init_f()
-> spl_early_init() // OF/DM早期初始化
-> preloader_console_init() // 早期串口
-> hb_spl_set_vdsp_tcm_firewall()
-> hb_blkcache_configure()
-> hb_get_aon_blk_type()
-> spl_dram_init() // 初始化DDR
-> icache_enable()
-> spl_sram_backup()
特别值得注意的是hb_blkcache_configure()函数,它会调用hb_get_aon_blk_type()读取BOOTSTRAP_REG,并结合ADC值判断当前板子是偏向emmc还是ufs,以此调整块缓存参数。这意味着Strap在board_init_f()阶段已经开始影响存储访问策略。
2.4 SPL的board_init_r()阶段
board_init_f()完成后,SPL会进入common/spl/spl.c中的board_init_r()函数:
c复制board_init_r()
-> spl_set_bd()
-> mem_malloc_init()
-> spl_init()
-> timer_init()
-> spl_board_init()
-> hb_spl_set_dram_firewall()
-> hb_spl_print_aon_flags()
-> hb_spl_set_sci_boot_flags()
-> dram_init_banksize()
-> bootcount_inc()
-> board_boot_order()
-> boot_from_devices()
-> spl_perform_fixups()
-> spl_board_prepare_for_boot()
-> jump_to_image_no_args() / jump_to_image_linux() / spl_invoke_atf()
这个阶段有三个与Strap密切相关的函数:
hb_spl_print_aon_flags():打印AON寄存器信息hb_spl_set_sci_boot_flags():保存启动信息到共享区spl_boot_device()/hb_get_boot_mode():决定启动设备
2.5 SPL阶段的Strap处理链
SPL阶段最关键的Strap处理链路如下:
c复制spl_board_init()
-> hb_spl_print_aon_flags()
-> hb_spl_set_sci_boot_flags()
board_boot_order()
-> spl_boot_device()
-> hb_get_boot_mode()
这条链路完成了从硬件Strap读取到启动决策的全过程:
hb_spl_print_aon_flags():打印AON寄存器信息供调试hb_spl_set_sci_boot_flags():保存信息到共享区hb_get_boot_mode():解释这些值,决定启动方式
2.6 hb_get_boot_mode()深度解析
hb_get_boot_mode()是SPL阶段最核心的函数之一,它实现了完整的启动决策逻辑:
c复制int hb_get_boot_mode(void)
{
uint32_t strap_pin = boot_flags.strap_pin;
uint32_t bootcount = ROMBOOTCOUNT(boot_flags.bootcount);
uint32_t ret = BOOT_MODE_FLASH;
// 如果bootinfo有效,用其低5位覆盖strap_pin
if (PIN_AONBOOTINFO_VAILD(boot_flags.bootinfo) == PIN_BOOTINFO_IS_VAILD) {
strap_pin &= ~0x1Fu;
strap_pin |= boot_flags.bootinfo & 0x1Fu;
}
// 根据bootcount决定是否强制进入UART模式
if (bootcount > BOOTROM_BOOTCOUNT_FAILCOUNT_UART) {
ret = BOOT_MODE_UART;
goto exit;
} else if (bootcount > BOOTROM_BOOTCOUNT_FAILCOUNT_SERIAL) {
strap_pin |= (1 << 2); // 强制serial boot
}
// 根据最终strap_pin决定启动模式
if (PIN_BOOTSEC_IS_SERIAL(strap_pin)) {
switch (PIN_2ND_SERIALSRC(strap_pin)) {
case PIN_2ND_UART:
ret = BOOT_MODE_UART;
break;
case PIN_2ND_CAN:
ret = BOOT_MODE_CAN;
break;
case PIN_2ND_USB0:
case PIN_2ND_USB1:
ret = BOOT_MODE_USB;
break;
}
}
exit:
return ret;
}
这个函数的决策逻辑可以总结为:
- 先取原始Strap值
- 如果bootinfo有效,用其低5位覆盖Strap的低5位
- 检查bootcount,如果失败次数过多,强制进入UART模式
- 次一级失败阈值时,强制设置serial boot标志
- 最终根据Strap[2]和Strap[1:0]决定启动模式
2.7 启动模式到设备的映射
hb_get_boot_mode()返回的逻辑启动模式会被spl_boot_device()映射为具体的设备类型:
c复制u32 spl_boot_device(void)
{
uint32_t boot_mode;
boot_mode = hb_get_boot_mode();
switch (boot_mode) {
case BOOT_MODE_FLASH:
return BOOT_DEVICE_HB_BLK;
case BOOT_MODE_CAN:
case BOOT_MODE_UART:
return BOOT_DEVICE_UART;
case BOOT_MODE_USB:
return BOOT_DEVICE_DFU;
}
return BOOT_DEVICE_NONE;
}
映射关系如下:
| hb_get_boot_mode()结果 | spl_boot_device()返回值 | 含义 |
|---|---|---|
| BOOT_MODE_FLASH | BOOT_DEVICE_HB_BLK | 从块设备加载 |
| BOOT_MODE_UART/CAN | BOOT_DEVICE_UART | 串行下载路径 |
| BOOT_MODE_USB | BOOT_DEVICE_DFU | USB DFU/fastboot路径 |
3. U-Boot Proper阶段分析
3.1 U-Boot Proper启动流程
当SPL成功加载U-Boot Proper后,系统进入完整的U-Boot阶段。这一阶段的主要关注点不再是"能否启动",而是:
- 恢复SPL保存的启动上下文
- 设置环境变量
- 决定内核所在块设备
- 配置串口波特率
- 决定是否自动进入fastboot
- 生成Linux bootargs
3.2 关键初始化流程
U-Boot Proper的初始化主要分为两个阶段:
board_init_f():通用初始化、重定位准备board_init_r():运行环境初始化
其中与Strap相关的逻辑主要通过last_stage_init()接入:
c复制last_stage_init()
-> scmi_debug_control()
-> chip_last_stage_init()
-> hb_set_sci_boot_flags()
-> board_env_setup()
-> auto_fastboot()
-> eval_memdump()
-> fixup_addr_env()
-> hb_gpt_extend()
3.3 启动上下文恢复
hb_set_sci_boot_flags()函数负责从共享区恢复启动上下文:
c复制int32_t hb_set_sci_boot_flags(void)
{
struct sci_boot_flags *boot_flags_p =
(struct sci_boot_flags *)SCI_BOOT_FLAGS_BASE;
// 检查魔数
if ((boot_flags_p->magic & SCI_BOOT_FLAGS_MASK) != SCI_BOOT_FLAGS_MAGIC) {
printf("sci_boot_flags get failed, magic = %llx\n",
boot_flags_p->magic);
return -EIO;
}
// 拷贝到全局变量
memcpy(&boot_flags, boot_flags_p, sizeof(boot_flags));
debug("strap_pin = 0x%x bootinfo = 0x%x bootcount = 0x%x\r\n",
boot_flags.strap_pin, boot_flags.bootinfo, boot_flags.bootcount);
return 0;
}
这个函数完成了两个关键任务:
- 检查共享区数据的有效性(通过魔数校验)
- 将SPL保存的启动信息恢复到U-Boot Proper的全局变量中
3.4 环境变量设置
board_env_setup()函数将Strap派生结果转换为环境变量:
c复制static void board_env_setup(void)
{
int32_t boot_block_device = 0;
char kernel_dev_str[20] = {0};
// 设置默认bootcmd
env_set("bootcmd",
"run " ENV_AB_SELECT_CMD ";"
"run " ENV_AVB_VERIFY_BOOT_CMD ";"
"run " ENV_DISTRO_BOOTCMD ";");
// 获取内核所在设备
boot_block_device = hb_get_kernel_in_device();
printf("boot_block_device [%d]\n", boot_block_device);
// 设置相关环境变量
snprintf(kernel_dev_str, sizeof(kernel_dev_str), "%s%s",
bootinf_str[boot_block_device], bootdev_str[boot_block_device]);
env_set("kernel_in_dev", kernel_dev_str);
env_set("boot_targets", kernel_dev_str);
env_set("bootintf", bootinf_str[boot_block_device]);
env_set("bootdev", bootdev_str[boot_block_device]);
// 设置fastboot相关变量
env_set("fastboot.bootintf", bootinf_str[boot_block_device]);
env_set_ulong("fastboot.secflag", hb_check_secure());
env_set_ulong("fastboot.lifecycle", hb_get_secure_lifecycle());
hb_serial_set();
}
这个函数设置了多个关键环境变量,包括:
kernel_in_dev:内核所在设备boot_targets:启动目标设备bootintf/bootdev:启动接口和设备fastboot.*:fastboot相关参数
3.5 内核设备选择逻辑
hb_get_kernel_in_device()函数根据Strap值决定内核所在设备:
c复制static int32_t hb_get_kernel_in_device(void)
{
uint32_t strap_pin = 0;
regmap_read(hobot_sci_regmap(SCI_BOOT_FLAGS),
HOBOT_SCI_STRIP_PIN, &strap_pin);
return PIN_KERNEL_IN_SEL(strap_pin);
}
当前代码中的映射关系如下:
| Strap[13:12] | 含义 |
|---|---|
| 00 | UFS |
| 01 | eMMC |
| 10 | NVMe |
| 11 | SD |
需要注意的是,这个映射关系可能会随代码版本变化,实际开发时应以当前代码为准。
3.6 串口波特率设置
hb_get_uart_baud()函数根据Strap[9]决定串口波特率:
c复制uint32_t hb_get_uart_baud(void)
{
uint32_t strap_pin = 0;
regmap_read(hobot_sci_regmap(SCI_BOOT_FLAGS),
HOBOT_SCI_STRIP_PIN, &strap_pin);
return PIN_SERIAL_SPEED(strap_pin) ?
SERIAL_BAUD_921600 : SERIAL_BAUD_115200;
}
映射关系很简单:
- Strap[9]=0:115200
- Strap[9]=1:921600
在实际调试中,如果看不到串口输出,首先应该检查Strap[9]的设置和终端波特率是否匹配。
3.7 自动进入fastboot的逻辑
auto_fastboot()函数实现了根据启动模式自动进入fastboot的功能:
c复制static void auto_fastboot(void)
{
int32_t ret = 0;
switch (hb_get_boot_mode()) {
case BOOT_MODE_UART:
printf("uart boot\n");
break;
case BOOT_MODE_CAN:
printf("can boot\n");
break;
case BOOT_MODE_USB:
printf("usb boot auto fastboot usb ing ....\n");
ret = run_command("fastboot 0", 0);
if (ret)
printf("fastboot usb failed\n");
break;
case BOOT_MODE_FLASH:
printf("flash boot\n");
break;
default:
printf("unknown boot mode\n");
break;
}
}
当hb_get_boot_mode()返回BOOT_MODE_USB时,U-Boot会自动执行fastboot 0命令进入fastboot模式。这在调试时可能会被误认为是"死机",实际上是正常进入了下载模式。
3.8 bootargs生成过程
board_bootargs_setup()函数根据当前环境和Strap派生结果生成Linux的bootargs:
c复制void board_bootargs_setup(void)
{
char boot_args[2048] = { 0 };
char console_args[64] = { 0 };
char *cmdline = env_get("bootargs");
uint32_t uart_baud = hb_get_uart_baud();
// 设置串口参数
snprintf(console_args, sizeof(console_args), "ttyS0,%dn8", uart_baud);
// 组合完整的bootargs
snprintf(boot_args, sizeof(boot_args),
"console=%s "
"loglevel=%ld hobot.kernel_in=%s "
"hobotboot.secureboot=%d "
"hobotboot.bootcount=%d "
" %s",
console_args,
env_get_ulong("loglevel", 10, 1), env_get("kernel_in_dev"),
hb_check_secure_flags(),
ROMBOOTCOUNT(hb_get_bootcount()),
cmdline ? cmdline : "");
printf("os cmdline: %s\n", boot_args);
env_set("bootargs", boot_args);
}
这个函数将以下Strap派生信息传递给Linux内核:
- 串口参数(波特率来自Strap[9])
- 内核所在设备(来自Strap[13:12])
- 安全启动标志
- 启动计数
4. Strap Pin的深入解析
4.1 Strap位定义与用途
根据当前J6M U-Boot源码,实际被使用的Strap位及其用途如下:
| Strap位 | 宏/函数 | 当前源码含义 | 在U-Boot中的作用 |
|---|---|---|---|
| [1:0] | PIN_2ND_SERIALSRC() | 串行下载源:UART/CAN/USB0/USB1 | 决定serial boot走哪种通道 |
| [2] | PIN_BOOTSEC_IS_SERIAL() | 0=Flash,1=Serial | 决定是正常存储启动还是下载启动 |
| [4:3] | PIN_BOOTMODE_IS_N() | 1X/2X/3X/4X | 决定bootmode档位和secure相关路径 |
| [6] | PIN_IS_SECURE() | secure属性近似位 | 安全相关判断 |
| [9] | PIN_SERIAL_SPEED() | 0=115200,1=921600 | 决定串口波特率 |
| [13:12] | PIN_KERNEL_IN_SEL() | UFS/eMMC/NVMe/SD | 决定镜像所在块设备 |
| [14] | PIN_KERNEL_IN_SEL_NOR() | NOR辅助位 | 配合image location使用 |
4.2 Bootinfo对Strap的覆盖
在hb_get_boot_mode()和hb_get_bootmode()中,有一段关键逻辑:
c复制if (PIN_AONBOOTINFO_VAILD(boot_flags.bootinfo) == PIN_BOOTINFO_IS_VAILD) {
strap_pin &= ~0x1Fu;
strap_pin |= boot_flags.bootinfo & 0x1Fu;
}
这段代码意味着,如果bootinfo有效,会用其低5位覆盖strap_pin的低5位,影响:
- Strap[1:0]:串行下载源
- Strap[2]:启动源选择
- Strap[4:3]:bootmode档位
因此,物理Strap值不一定等于U-Boot最终使用的逻辑Strap值。
4.3 Bootcount的影响
hb_get_boot_mode()中还包含bootcount的处理逻辑:
c复制if (bootcount > BOOTROM_BOOTCOUNT_FAILCOUNT_UART) {
ret = BOOT_MODE_UART;
goto exit;
} else if (bootcount > BOOTROM_BOOTCOUNT_FAILCOUNT_SERIAL) {
strap_pin |= (1 << 2); // 强制serial boot
}
这意味着:
- 当失败次数超过UART阈值时,强制进入UART模式
- 次一级失败阈值时,强制设置serial boot标志
因此,即使Strap配置为Flash启动,也可能因为多次启动失败而自动进入下载模式。
5. 实际调试技巧与注意事项
5.1 关键日志解读
在SPL阶段,hb_spl_print_aon_flags()会打印如下格式的日志:
code复制aon read strap_pin 0x...
aon read boardid_adc_ch0 0x...
aon read boardid_adc_ch1 0x...
aon read bootinfo 0x...
aon read bootcount 0x...
aon read secure_lifecycle 0x...
这些日志是分析启动问题的第一手资料,应该:
- 确认所有寄存器都被成功读取
- 检查strap_pin值是否符合预期
- 关注bootcount值,判断是否触发了恢复机制
5.2 常见问题排查
-
看不到串口输出
- 检查Strap[9]设置
- 确认终端波特率设置正确(115200或921600)
- 检查串口线连接是否可靠
-
自动进入fastboot模式
- 检查
hb_get_boot_mode()返回值 - 确认Strap[2]和Strap[1:0]设置
- 检查bootcount值是否触发了恢复机制
- 检查
-
无法从预期设备启动
- 检查Strap[13:12]设置
- 确认
hb_get_kernel_in_device()返回值 - 检查bootinfo是否覆盖了Strap值
5.3 开发建议
-
保持代码与文档同步
- Strap位的定义可能随代码版本变化
- 开发时应以当前代码中的定义为准
-
充分利用调试信息
- 在关键决策点添加调试打印
- 保存完整的启动日志供分析
-
考虑所有影响因素
- 不仅关注物理Strap设置
- 还要考虑bootinfo、bootcount等的影响
-
安全相关注意事项
- 某些Strap位可能影响安全启动流程
- 修改这些位设置前应充分评估安全影响
通过深入理解J6M平台的U-Boot启动流程和Strap处理机制,开发者可以更高效地进行系统移植和调试工作,快速定位和解决启动阶段的各种问题。