4、启动模式与硬件配置:Boot Pin配置、启动设备枚举、启动设备初始化(时钟、PLL、DDR控制器)
好,咱们接着聊。上一章我们把SoC上电后的“第一口呼吸”——复位向量和异常向量表讲清楚了。但芯片复位之后,它怎么知道自己该从哪儿启动呢?是读SD卡?还是从NAND Flash?还是从USB下载?
答案就在这一章:启动模式与硬件配置。说白了,就是芯片通过几个管脚的电平,来决定自己的“人生路线”。
4.1 Boot Pin配置:芯片的“人生选择题”
每个SoC都会预留一组专用的GPIO,叫做Boot Pin。芯片复位后,硬件逻辑会在极短的时间内(通常是几十个时钟周期)锁存这些管脚的电平状态。然后根据这个状态,决定从哪个设备加载启动代码。
我个人习惯在设计板子时,把这几个Boot Pin用排针引出来,或者用拨码开关控制。为什么?因为调试阶段太方便了。你想从SD卡启动,拨一下;想从串口下载,再拨一下。不用改代码,不用重新烧录。
典型Boot Pin配置示例(以某款ARM Cortex-A7 SoC为例):
// Boot Pin映射表
// BOOT[2:0] 对应 GPIO0_2, GPIO0_3, GPIO0_4
// 电平采样发生在复位释放后的第10个时钟周期
// 配置定义
#define BOOT_MODE_SD_CARD 0b000
#define BOOT_MODE_NAND_FLASH 0b001
#define BOOT_MODE_SPI_NOR 0b010
#define BOOT_MODE_UART 0b011
#define BOOT_MODE_USB_DFU 0b100
#define BOOT_MODE_RESERVED 0b101
嗯,这里要注意:Boot Pin的采样窗口非常短。如果外部电路的电平还没稳定,芯片就读到了错误的值。我曾经遇到过一块板子,每次上电启动设备都不一样,查了两天才发现是Boot Pin的上拉电阻焊错了位置。所以,Boot Pin的硬件设计一定要加RC滤波,确保电平在采样窗口内稳定。
4.2 启动设备枚举:芯片的“寻路之旅”
Boot Pin确定了启动设备类型,但具体是哪个设备呢?比如你选了SD卡,但板子上可能有SD卡0和SD卡1。这时候就需要启动设备枚举。
启动ROM(也就是固化在芯片内部的那一小段代码)会按照一个预定义的优先级顺序,去尝试每个可能的启动设备。它怎么试?很简单:发送一个读命令,如果能收到有效回应,就认为这个设备存在。
我举个例子,某款SoC的枚举顺序是这样的:
| 优先级 | 启动设备 | 枚举方式 |
|---|---|---|
| 1 | eMMC (内部) | 发送CMD0,等待回应 |
| 2 | SD卡 (外部插槽) | 检测CD管脚电平,发送CMD0 |
| 3 | SPI NOR Flash | 发送0x9F (读ID命令),检查返回的厂商ID |
| 4 | NAND Flash | 发送复位命令,读取ID字节 |
| 5 | USB DFU | 等待USB主机枚举 |
你想想看,如果SD卡没插,ROM就会跳过它,继续尝试下一个。直到找到一个能用的设备,然后从该设备的固定偏移地址(比如第1个扇区)读取启动代码。
避坑指南:我曾经在一个项目里,因为SD卡插槽的CD(Card Detect)管脚没接,导致ROM始终检测不到SD卡存在,直接跳过了。最后只能飞线解决。所以,如果板子上有可插拔的启动设备,CD管脚一定要接,而且要配置正确的上拉/下拉。
4.3 启动设备初始化:时钟、PLL、DDR控制器
找到启动设备之后,ROM代码就要开始干正事了——初始化硬件。这一步非常关键,因为后续的启动代码(比如SPL或U-Boot)需要在一个稳定的硬件环境下运行。
4.3.1 时钟与PLL初始化
芯片复位后,默认工作在低速时钟下,可能是外部晶振的24MHz,也可能是内部RC振荡器的几MHz。这个速度太慢了,加载代码会慢得让人抓狂。所以ROM要做的第一件事,就是配置PLL,把CPU和总线频率提上去。
我记得在某款SoC上,ROM代码会先读取一个固定的OTP(一次性可编程)区域,里面存着推荐的PLL配置参数。如果没有,就用默认值。流程大概是:
// 伪代码:PLL初始化流程
void pll_init(void) {
// 1. 等待外部晶振稳定
while(!(OSC_STATUS & OSC_READY_MASK));
// 2. 配置PLL倍频系数
PLL_CTRL = (MULTIPLIER << 8) | (DIVIDER << 0);
// 3. 等待PLL锁定
while(!(PLL_STATUS & PLL_LOCK_MASK));
// 4. 切换系统时钟到PLL输出
CLK_SWITCH = CLK_SOURCE_PLL;
}
嗯,这里有个坑:PLL锁定需要时间,通常是几百微秒。如果ROM代码不等锁定就切换时钟,系统直接死给你看。我见过一个案例,某工程师为了省那几百微秒,没等PLL锁定就往下走了,结果芯片在低温下死活起不来。所以,PLL锁定标志一定要等到,别偷懒。
4.3.2 DDR控制器初始化
DDR初始化是启动流程中最复杂、也最容易出问题的一步。为什么?因为DDR的时序参数太多了,而且和PCB布局、颗粒型号强相关。
ROM代码通常不会内置所有DDR颗粒的配置,而是提供一个“训练”机制。它会根据Boot Pin或OTP区域的信息,加载一组DDR参数,然后进行读写测试,自动调整时序。
我给大家看一个典型的DDR初始化流程:
// 伪代码:DDR控制器初始化
void ddr_init(void) {
// 1. 配置DDR控制器基础参数
DDR_CTRL = (ROW_BITS << 24) | (COL_BITS << 16) | (BANK_BITS << 8);
// 2. 设置时序参数(tRCD, tRP, tRAS等)
DDR_TIMING = (TRCD_VAL << 0) | (TRP_VAL << 8) | (TRAS_VAL << 16);
// 3. 发送DDR初始化序列(NOP, 预充电, 刷新等)
ddr_send_command(DDR_CMD_NOP);
ddr_send_command(DDR_CMD_PRECHARGE_ALL);
ddr_send_command(DDR_CMD_AUTO_REFRESH);
// 4. 设置模式寄存器(MR0, MR1, MR2, MR3)
ddr_set_mode_register(MR0, CAS_LATENCY | BURST_LENGTH);
ddr_set_mode_register(MR1, DLL_ENABLE | DRIVE_STRENGTH);
// 5. 执行ZQ校准
ddr_send_command(DDR_CMD_ZQ_CALIB);
// 6. 等待DLL锁定
while(!(DDR_STATUS & DLL_LOCK_MASK));
}
警告:DDR初始化失败是启动阶段最常见的故障之一。我曾经在一个项目中,因为DDR的VREF电压偏了50mV,导致系统在高温下频繁崩溃。排查了整整一周,最后用示波器量了DDR的参考电压才发现问题。所以,DDR的硬件设计一定要严格按照芯片手册的布局要求来,VREF的走线要单独拉,不能和电源共用。
4.3.3 启动设备控制器初始化
时钟和DDR都准备好了,接下来就是初始化具体的启动设备控制器。比如你从SD卡启动,就要初始化SD/MMC控制器;从NAND启动,就要初始化NAND控制器。
这一步相对简单,主要是配置控制器的时钟、引脚复用、中断等。ROM代码会从Boot Pin获取设备类型,然后调用对应的初始化函数。
// 伪代码:根据启动模式初始化设备控制器
void boot_device_init(void) {
switch(boot_mode) {
case BOOT_MODE_SD_CARD:
// 配置SD控制器时钟
SD_CLK_CTRL = SD_CLK_DIV_4;
// 配置引脚复用
PIN_MUX[SD_PINS] = ALT_FUNC_SD;
// 发送初始化命令
sd_send_command(CMD0, 0);
sd_send_command(CMD8, 0x1AA);
break;
case BOOT_MODE_NAND_FLASH:
// 配置NAND控制器时序
NAND_TIMING = (TCLS_VAL << 0) | (TALS_VAL << 8);
// 发送复位命令
nand_send_command(0xFF);
break;
// 其他设备类似...
}
}
好了,到这里,启动设备已经初始化完毕,DDR也已经可用。ROM代码接下来就会从启动设备读取下一级启动代码(比如SPL或U-Boot),加载到DDR中,然后跳转过去执行。
下一章,我们就来讲讲这个“跳转”的过程,以及如何编写一个最小化的SPL(Secondary Program Loader)。