3. 硬件初始化实战:让芯片先“活过来”
硬件初始化,是Bootloader最底层的功夫。说白了,就是给芯片“通电唤醒”的过程。我见过不少新手,一上来就急着写Flash驱动、搬代码,结果串口打印死活不出来,最后发现是PLL根本没锁住。
这一章,我们聚焦四个核心模块:时钟系统、DDR内存、串口调试、GPIO与关键外设。顺序不能乱,因为后面的模块依赖前面的时钟和内存。
3.1 时钟系统配置:一切性能的起点
时钟是芯片的心脏。没有时钟,CPU不跑,外设不动。我习惯把时钟配置拆成三步:选源、锁相、分发。
3.1.1 时钟源的选择
大部分SoC支持多种时钟源:外部晶振(HSE)、内部RC振荡器(HSI)、低速晶振(LSE)等。Bootloader阶段,我建议优先用外部晶振,精度高、温漂小。内部RC虽然启动快,但频率不准,跑串口容易丢数据。
3.1.2 PLL配置:锁相环的坑
PLL配置是时钟系统里最容易出问题的地方。参数不对,要么锁不住,要么输出频率超限。我曾经在一个项目里,因为PLL的VCO频率设高了50MHz,芯片直接过热保护。
配置PLL时,记住这个公式:
PLL输出频率 = 输入频率 × N / M / P
其中N是倍频系数,M是预分频,P是后分频。不同芯片的N、M、P范围不同,一定要看数据手册。
以STM32MP1为例,典型配置:
/* 配置PLL1为800MHz */
RCC->PLL1CFGR = (25 << 8) | // N = 25
(2 << 0) | // M = 2
(1 << 16); // P = 1
/* 等待PLL1锁定 */
while(!(RCC->CR & RCC_CR_PLL1RDY));
/* 切换系统时钟 */
RCC->CFGR = (3 << 0); // 选择PLL1作为系统时钟
3.1.3 时钟分发与使能
系统时钟配置好后,还要使能各个外设的时钟。这一步容易被忽略——你配置了UART寄存器,但没开UART时钟,它根本不会工作。
我习惯在时钟初始化函数里,统一使能所有Bootloader阶段需要用到的外设时钟:
/* 使能GPIOA、USART1、DDR控制器时钟 */
RCC->AHB1ENR |= (1 << 0); // GPIOA
RCC->APB2ENR |= (1 << 4); // USART1
RCC->AHB3ENR |= (1 << 0); // DDR控制器
3.2 DDR内存初始化:让程序有地方住
DDR初始化是Bootloader里最复杂、最敏感的环节。参数错了,内存读写就会出错,程序跑飞是常事。
3.2.1 DDR初始化流程
DDR初始化通常包含以下步骤:
- 电源与复位:确保DDR供电稳定,释放复位信号
- 时钟使能:给DDR控制器和DDR颗粒提供时钟
- 时序参数配置:CAS延迟、突发长度、刷新周期等
- ZQ校准:阻抗匹配,保证信号完整性
- 训练与测试:写读校验,确认内存可用
时序参数是DDR初始化的核心。每个参数都对应DDR颗粒的电气特性,不能随便填。我一般直接从芯片厂商提供的DDR初始化脚本里抄参数,然后根据实际板卡微调。
3.2.2 DDR测试:确认内存可靠
初始化完成后,一定要做内存测试。我常用的测试方法:
- 走马灯测试:写0x55、0xAA等模式,检查数据线是否短路
- 地址线测试:写不同地址,检查地址线是否粘连
- 压力测试:全地址范围写随机数,再读回校验
测试代码示例:
/* 简单的DDR测试:写0x55AA55AA,读回校验 */
uint32_t *ddr_base = (uint32_t *)0xC0000000;
uint32_t test_pattern = 0x55AA55AA;
int i;
for(i = 0; i < 1024; i++) {
ddr_base[i] = test_pattern;
}
for(i = 0; i < 1024; i++) {
if(ddr_base[i] != test_pattern) {
/* 错误处理:点亮错误LED或串口打印 */
while(1);
}
}
3.3 串口初始化与调试输出:Bootloader的眼睛
串口是Bootloader阶段最重要的调试手段。没有串口打印,你就像在黑暗中摸索。我常说,串口就是Bootloader的“眼睛”。
3.3.1 串口初始化步骤
串口初始化相对简单,但顺序不能错:
- 引脚复用配置:把GPIO引脚切换到UART功能
- 时钟使能:打开UART模块时钟
- 波特率配置:根据时钟频率计算波特率寄存器值
- 数据格式设置:8N1、8E1等
- 使能UART:开启发送器和接收器
以常见的115200波特率为例:
/* 假设UART时钟为64MHz,目标波特率115200 */
/* 波特率 = 时钟 / (16 * USARTDIV) */
/* USARTDIV = 64000000 / (16 * 115200) = 34.72 */
USART1->BRR = (34 << 4) | (0x72 >> 4); // 整数部分34,小数部分0.72
3.3.2 调试输出函数
初始化串口后,写一个简单的字符输出函数:
void uart_putc(char c) {
/* 等待发送缓冲区空 */
while(!(USART1->SR & (1 << 7)));
/* 发送字符 */
USART1->DR = c;
}
void uart_puts(const char *str) {
while(*str) {
uart_putc(*str++);
}
}
有了这两个函数,你就可以在Bootloader里到处加打印了。我习惯在关键步骤前后都加打印,方便定位问题。
3.4 GPIO与关键外设的早期配置
GPIO配置是硬件初始化的最后一步,但同样重要。Bootloader阶段,GPIO主要用于:状态指示、硬件检测、外设控制。
3.4.1 状态指示LED
我习惯在Bootloader里用LED指示当前状态:
- 常亮:初始化完成,等待命令
- 闪烁:正在加载或升级
- 快闪:错误状态
配置GPIO为推挽输出:
/* 配置PA5为推挽输出,用于LED */
GPIOA->MODER |= (1 << 10); // 输出模式
GPIOA->OTYPER &= ~(1 << 5); // 推挽
GPIOA->OSPEEDR |= (3 << 10);// 高速
GPIOA->PUPDR &= ~(3 << 10); // 无上下拉
3.4.2 看门狗处理
很多SoC上电后看门狗默认是开启的。如果不及时喂狗,芯片会不断复位。我建议在Bootloader入口处就关闭看门狗,或者至少配置一个较长的超时时间。
3.4.3 电源管理外设
有些板卡需要GPIO控制电源芯片的使能脚,或者控制DDR的VTT电压。这些外设必须在DDR初始化之前配置好,否则DDR无法正常工作。
我习惯把这类“关键外设”的配置放在一个单独的函数里:
void early_periph_init(void) {
/* 使能DDR电源 */
GPIOB->BSRR = (1 << 0); // PB0置高,使能DDR电源
/* 等待电源稳定 */
delay_ms(10);
/* 配置复位引脚 */
GPIOC->MODER |= (1 << 6); // PC3输出
GPIOC->BSRR = (1 << 3); // 释放复位
}
3.5 本章小结
硬件初始化是Bootloader的基石。时钟、DDR、串口、GPIO,这四个模块环环相扣,缺一不可。我见过太多人因为跳过某个步骤,或者参数配置错误,导致整个Bootloader无法运行。
记住:调试输出是Bootloader的眼睛,时钟是心脏,DDR是骨架,GPIO是手脚。把这四个模块配置好,你的Bootloader就成功了一半。