2. 硬件初始化流程:从复位向量到C语言入口
硬件初始化,说白了就是给芯片“接生”。芯片上电那一刻,所有寄存器都是默认值,内存还没准备好,时钟还在乱跑。你得一步步把它扶上正轨,最后才能跳进C语言的世界。
我刚开始做Bootloader时,总觉得这步很简单——不就是配几个寄存器吗?结果有一次,板子死活跑不起来,折腾了两天才发现是PLL锁定时间没等够。嗯,从那以后,我对初始化流程再也不敢马虎了。
2.1 复位向量:芯片的第一口气
芯片上电后,PC指针会直接跳到一个固定地址——复位向量。这个地址通常放在Flash或ROM的最开头,比如0x00000000或0x08000000。
复位向量里放的是什么?一条跳转指令,直接跳到复位处理函数。比如ARM Cortex-M系列,向量表第一个字是栈顶指针,第二个字才是复位向量。
; 典型的ARM Cortex-M向量表示例
__Vectors DCD __stack_top ; 栈顶指针
DCD Reset_Handler ; 复位向量
DCD NMI_Handler ; NMI中断
DCD HardFault_Handler ; 硬错误中断
; ... 其他中断向量
我个人习惯在复位向量后面留几个字的填充,方便以后加功能。你想想看,万一要加个安全启动校验,向量表空间不够就尴尬了。
注意: 复位向量必须放在链接脚本指定的地址上。我曾经见过有人改了链接脚本但忘了更新向量表地址,结果芯片上电直接飞了。
2.2 从汇编到C语言:关键的几步
复位处理函数通常用汇编写,因为它要做几件C语言跑起来之前必须干的事:
- 关中断——防止在初始化过程中被意外打断
- 设置栈指针——C语言需要栈才能调用函数
- 清零BSS段——未初始化的全局变量必须归零
- 拷贝数据段——从Flash把初始化数据搬到RAM
- 使能FPU(如果有)——浮点运算单元要手动打开
- 跳转到main——终于可以写C代码了
Reset_Handler PROC
; 1. 关中断
CPSID I
; 2. 设置栈指针
LDR R0, =__stack_top
MOV SP, R0
; 3. 清零BSS段
LDR R0, =__bss_start
LDR R1, =__bss_end
MOV R2, #0
BL memset
; 4. 拷贝数据段
LDR R0, =__data_start
LDR R1, =__data_end
LDR R2, =__data_flash_start
BL memcpy
; 5. 使能FPU(Cortex-M4/M7)
LDR R0, =0xE000ED88
LDR R1, [R0]
ORR R1, R1, #(0xF << 20)
STR R1, [R0]
DSB
ISB
; 6. 跳转到main
BL main
ENDP
这里有个坑:BSS段清零必须在数据段拷贝之前。为什么?因为有些链接脚本会把data段的加载地址放在BSS段里,顺序搞反了数据就全乱了。我踩过这个坑,查了两天才发现。
2.3 时钟系统初始化:PLL配置
时钟是芯片的心脏。默认情况下,芯片用的是内部低速振荡器,比如8MHz的HSI。你要把它切换到PLL,倍频到几百兆赫兹。
PLL配置的典型步骤:
- 选择参考时钟源(HSE/HSI)
- 配置分频系数(M、N、P、Q)
- 使能PLL并等待锁定
- 切换系统时钟到PLL输出
| 参数 | 说明 | 典型值 |
|---|---|---|
| HSE | 外部高速晶振 | 8MHz / 25MHz |
| M | 输入分频 | 8(8MHz输入时) |
| N | 倍频系数 | 336(输出336MHz) |
| P | 系统时钟分频 | 2(得到168MHz) |
| Q | USB/SDIO分频 | 7(得到48MHz) |
void SystemClock_Config(void)
{
// 1. 使能HSE
RCC->CR |= RCC_CR_HSEON;
while(!(RCC->CR & RCC_CR_HSERDY));
// 2. 配置PLL参数
RCC->PLLCFGR = (8 << 0) | // M = 8
(336 << 6) | // N = 336
(0 << 16) | // P = 2
(7 << 24) | // Q = 7
RCC_PLLCFGR_PLLSRC_HSE;
// 3. 使能PLL并等待锁定
RCC->CR |= RCC_CR_PLLON;
while(!(RCC->CR & RCC_CR_PLLRDY));
// 4. 配置Flash等待周期(168MHz需要5个等待周期)
FLASH->ACR = FLASH_ACR_LATENCY_5WS | FLASH_ACR_PRFTEN | FLASH_ACR_ICEN | FLASH_ACR_DCEN;
// 5. 切换系统时钟到PLL
RCC->CFGR |= RCC_CFGR_SW_PLL;
while((RCC->CFGR & RCC_CFGR_SWS) != RCC_CFGR_SWS_PLL);
}
经验之谈: PLL锁定时间不是固定的。温度、电压都会影响。我一般会在等待锁定循环里加个超时保护,防止晶振坏了导致死锁。
2.4 内存控制器初始化:DDR/SDRAM
内存初始化是Bootloader里最麻烦的部分之一。DDR的时序参数多、寄存器复杂,配错了要么跑不起来,要么跑起来不稳定。
DDR初始化的核心步骤:
- 供电稳定——DDR需要1.8V/1.35V/1.2V,必须先确认电压到位
- 时钟使能——给DDR提供差分时钟
- 复位释放——拉高DDR的复位引脚
- CKE使能——时钟使能信号拉高
- 初始化序列——发送NOP、预充电、刷新、模式寄存器配置
- ZQ校准——DDR3/DDR4需要ZQ校准
void DDR_Init(void)
{
// 1. 配置DDR控制器时序参数
FMC->SDCR1 = FMC_SDCR1_SDCLK_2 | // 时钟分频
FMC_SDCR1_NBANK_4 | // 4个Bank
FMC_SDCR1_NR_13 | // 13行地址
FMC_SDCR1_NC_10; // 10列地址
// 2. 配置时序寄存器
FMC->SDTR1 = (7 << 0) | // TMRD: 7个时钟
(2 << 4) | // TXSR: 2个时钟
(7 << 8) | // TRAS: 7个时钟
(7 << 12) | // TRC: 7个时钟
(2 << 16) | // TRP: 2个时钟
(2 << 20) | // TRCD: 2个时钟
(2 << 24); // TWR: 2个时钟
// 3. 发送初始化命令序列
// 先发NOP,等待200us
FMC->SDCMR = FMC_SDCMR_CTB1 | (0 << 5); // NOP命令
delay_us(200);
// 预充电所有Bank
FMC->SDCMR = FMC_SDCMR_CTB1 | (1 << 5); // 预充电
delay_us(10);
// 自动刷新(8次)
for(int i = 0; i < 8; i++) {
FMC->SDCMR = FMC_SDCMR_CTB1 | (2 << 5); // 自动刷新
delay_us(1);
}
// 配置模式寄存器
FMC->SDCMR = FMC_SDCMR_CTB1 | (3 << 5) | (0x33 << 9);
delay_us(10);
// 4. ZQ校准(DDR3必须)
FMC->SDCMR = FMC_SDCMR_CTB1 | (4 << 5);
delay_us(1);
}
特别注意: DDR的时序参数必须和芯片数据手册完全一致。我曾经遇到过一个问题:板子上用的是DDR3L(1.35V),但代码里配的是DDR3(1.5V)的时序,结果高温下频繁死机。查了三天才发现是电压等级配错了。
2.5 初始化流程总览
把上面这些串起来,完整的硬件初始化流程就是:
这张图看起来简单,但每一步都有坑。我个人习惯在每个初始化函数里加状态打印,用UART输出关键寄存器的值。这样万一板子挂了,至少知道是卡在哪一步。
核心原则: 硬件初始化要“先慢后快”。先用低速时钟把基本功能跑通,再切换到高速PLL。内存初始化要“先稳后准”,先保证能读写,再优化时序。