第1章 BootROM初始化流程:上电复位与时钟初始化
各位同学,咱们今天聊聊BootROM最开始的那些事。很多人觉得上电复位就是按个开关,CPU就开始跑了。其实没那么简单。我做过这么多项目,每次看到示波器上电源波形慢慢爬升,心里都会默念:别抖、别抖、稳住。
BootROM的初始化,说白了就是给芯片“接生”。从电源稳定那一刻起,到第一条指令执行,中间有大量硬件和软件的配合。咱们一步步拆开看。
1.1 上电复位(POR)——芯片的“第一口气”
上电复位,英文叫Power-On Reset,简称POR。它的作用很简单:确保芯片从已知状态开始跑。
你想想看,如果电源刚上来时电压不稳,CPU内部寄存器全是随机值,那第一条指令可能就飞到天上去了。所以硬件上会有一个POR电路,专门监测VDD电压。当电压低于某个阈值(比如0.9V),芯片就一直保持在复位状态。等电压稳定到正常工作范围(比如1.8V或3.3V),POR电路才会释放复位信号。
关键点:POR不是瞬间完成的。从电源上电到POR释放,通常需要几百微秒到几毫秒。这个时间由外部电容和内部迟滞电路决定。
我在项目中遇到过一个问题:某款MCU在低温下启动失败。查了半天,发现是POR阈值随温度漂移了。低温时阈值变高,电源刚上来时电压爬升慢,POR一直不释放。后来换了宽温范围的POR芯片才解决。
1.2 时钟初始化——给芯片“定心跳”
复位释放后,CPU开始执行第一条指令。但这时候时钟系统还没完全配置好。大多数芯片上电后默认使用内部低速振荡器(比如32kHz或几MHz的RC振荡器)。为什么?因为内部RC振荡器不需要外部晶振,上电就能跑,虽然精度差了点,但胜在稳定可靠。
时钟初始化的典型流程是这样的:
- 等待主振荡器稳定——外部晶振或PLL需要起振时间,通常几毫秒到几十毫秒
- 配置PLL倍频——把低频时钟倍频到CPU需要的高频(比如从8MHz倍频到400MHz)
- 切换系统时钟源——从内部RC切换到PLL输出
- 配置外设时钟分频——给UART、Timer等外设分配时钟
我的习惯:在切换时钟源之前,先确认PLL锁定标志位。有些芯片的PLL锁定需要几百个时钟周期,如果没等锁定就切换,CPU直接跑飞。我曾经因为这个bug调了三天。
代码示例(ARM Cortex-M系列典型写法):
// 1. 等待外部晶振稳定
while(!(RCC->CR & RCC_CR_HSERDY));
// 2. 配置PLL:8MHz输入,倍频到72MHz
RCC->CFGR |= RCC_CFGR_PLLMULL9; // 8MHz * 9 = 72MHz
RCC->CR |= RCC_CR_PLLON;
// 3. 等待PLL锁定
while(!(RCC->CR & RCC_CR_PLLRDY));
// 4. 切换系统时钟到PLL
RCC->CFGR |= RCC_CFGR_SW_PLL;
while((RCC->CFGR & RCC_CFGR_SWS) != RCC_CFGR_SWS_PLL);
1.3 CPU模式与异常向量表设置
时钟跑起来之后,CPU就要进入正常工作模式了。但这里有个细节:上电后CPU默认处于什么模式?
以ARM Cortex-M为例,上电后CPU处于线程模式(Thread Mode),使用主堆栈指针(MSP)。异常向量表默认放在Flash起始地址(0x00000000)。但很多芯片会把BootROM映射到0x00000000,而用户程序在别的地址。所以BootROM的第一步,往往是把异常向量表重定位到SRAM或者BootROM自己的地址空间。
注意:异常向量表必须按4字节对齐。如果放错位置,一旦发生中断,CPU会从错误地址取向量,直接HardFault。我见过有人把向量表放在奇数地址上,结果中断一进来就死机。
设置异常向量表的典型代码:
// 将向量表基址设置为BootROM起始地址
SCB->VTOR = BOOTROM_BASE_ADDRESS;
// 确认写入成功
__DSB();
__ISB();
为什么要加DSB和ISB?因为VTOR寄存器写入后,CPU需要立即刷新指令流水线。如果不加这两个屏障,下一条指令可能还是从旧向量表取数据。嗯,这里要注意,很多新手会忽略这个细节。
1.4 堆栈指针(SP)与链接寄存器(LR)初始化
堆栈指针SP,说白了就是CPU的“临时仓库管理员”。函数调用、局部变量、中断现场,全都要靠SP来管理。上电后SP的值从哪里来?
ARM Cortex-M的启动流程是这样的:CPU复位后,自动从地址0x00000000加载SP初始值,从地址0x00000004加载PC初始值(即Reset_Handler入口)。所以BootROM的链接脚本里,第一个字必须是SRAM顶端的地址,第二个字是启动函数地址。
链接脚本示例:
ENTRY(Reset_Handler)
MEMORY
{
FLASH (rx) : ORIGIN = 0x08000000, LENGTH = 512K
SRAM (rwx): ORIGIN = 0x20000000, LENGTH = 128K
}
SECTIONS
{
.vectors :
{
LONG(ORIGIN(SRAM) + LENGTH(SRAM)); /* SP初始值 */
LONG(Reset_Handler); /* PC初始值 */
/* 其他异常向量 */
} > FLASH
}
链接寄存器LR呢?它保存函数返回地址。在BootROM初始化阶段,LR其实没什么用,因为还没调用任何函数。但我会习惯性地把LR初始化为一个特殊值(比如0xFFFFFFFF),这样万一程序跑飞了,调试时能一眼看出来LR被污染了。
避坑指南:我曾经在移植BootROM时,发现SP初始值设错了。链接脚本里SRAM大小是128KB,但实际芯片只有64KB。结果SP指向了不存在的地址,一调用函数就死机。所以一定要确认芯片的SRAM实际大小,别光看数据手册上的“最大支持”。
1.5 关键外设的早期初始化——Timer和UART
BootROM里为什么要初始化外设?因为后面要下载程序、要打印调试信息、要做超时判断。Timer和UART是最常用的两个外设。
Timer的早期初始化:
Timer用来做超时保护。比如等待外部晶振稳定,如果等了100ms还没稳定,说明晶振坏了或者没焊好,BootROM应该报错而不是死等。我习惯用一个硬件Timer来做看门狗,防止BootROM卡死在某个循环里。
// 初始化SysTick定时器,1ms中断一次
SysTick_Config(SystemCoreClock / 1000);
// 或者用通用定时器做超时
TIM_TimeBaseInitTypeDef tim;
tim.TIM_Prescaler = 72 - 1; // 72MHz / 72 = 1MHz
tim.TIM_Period = 1000 - 1; // 1ms
TIM_TimeBaseInit(TIM2, &tim);
TIM_Cmd(TIM2, ENABLE);
UART的早期初始化:
UART用来输出调试信息。BootROM启动过程中,每做一步就打印一条日志,这样出了问题能快速定位。但要注意:UART初始化时,GPIO引脚要先配置成复用功能,否则数据发不出去。
// 配置UART引脚
GPIO_InitTypeDef gpio;
gpio.GPIO_Pin = GPIO_Pin_9 | GPIO_Pin_10; // TX, RX
gpio.GPIO_Mode = GPIO_Mode_AF_PP;
gpio.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(GPIOA, &gpio);
// 配置UART参数
USART_InitTypeDef uart;
uart.USART_BaudRate = 115200;
uart.USART_WordLength = USART_WordLength_8b;
uart.USART_StopBits = USART_StopBits_1;
uart.USART_Parity = USART_Parity_No;
USART_Init(USART1, &uart);
USART_Cmd(USART1, ENABLE);
注意:UART初始化时,波特率误差要控制在2%以内。如果时钟源不准(比如内部RC振荡器),115200波特率可能误差达到5%,导致通信失败。我建议在BootROM里先校准内部RC振荡器,或者直接用外部晶振给UART提供时钟。
1.6 本章知识体系总览
下面这张图把BootROM初始化的核心流程串起来了。你可以把它当作一个检查清单,做项目时对着走一遍,基本不会漏东西。
这张图里,步骤1到4是串行执行的,缺一不可。步骤5里的Timer和UART可以并行初始化,但要注意先后顺序——先配时钟,再配GPIO,最后配外设模块。这个顺序搞反了,外设寄存器写不进去。
好了,BootROM的初始化流程就讲到这里。下一节咱们聊聊BootROM怎么检测启动介质、怎么加载用户程序。到时候我会拿一个实际项目里的启动流程图出来,带大家一步步分析。
课后思考:如果你的芯片上电后UART打印不出任何信息,你会从哪几个方向排查?我建议你从电源、时钟、GPIO配置、UART寄存器这四个维度入手。每个维度列一个检查清单,下次遇到问题直接对着查。