3. 硬件初始化实战:让芯片先“活过来”

硬件初始化,是Bootloader最底层的功夫。说白了,就是给芯片“通电唤醒”的过程。我见过不少新手,一上来就急着写Flash驱动、搬代码,结果串口打印死活不出来,最后发现是PLL根本没锁住。

这一章,我们聚焦四个核心模块:时钟系统、DDR内存、串口调试、GPIO与关键外设。顺序不能乱,因为后面的模块依赖前面的时钟和内存。

核心原则:硬件初始化必须遵循“先时钟,后外设;先内存,后搬移”的依赖链。跳过任何一步,后面都是死路。
硬件初始化依赖链 1. 时钟系统 PLL / 分频 / 使能 2. DDR内存 时序 / 校准 / 测试 3. 串口 波特率 / 引脚 / 打印 4. GPIO & 外设 LED / 看门狗 / 电源 5. 跳转主程序 搬运 / 清BSS / 跳转 依赖方向:时钟 → 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初始化通常包含以下步骤:

  1. 电源与复位:确保DDR供电稳定,释放复位信号
  2. 时钟使能:给DDR控制器和DDR颗粒提供时钟
  3. 时序参数配置:CAS延迟、突发长度、刷新周期等
  4. ZQ校准:阻抗匹配,保证信号完整性
  5. 训练与测试:写读校验,确认内存可用

时序参数是DDR初始化的核心。每个参数都对应DDR颗粒的电气特性,不能随便填。我一般直接从芯片厂商提供的DDR初始化脚本里抄参数,然后根据实际板卡微调。

避坑指南:我曾经在一块四层板上调试DDR,怎么都通不过训练。折腾了两天,最后发现是DDR的VREF电压走线太长,噪声太大。加了个电容滤波就好了。所以,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 串口初始化步骤

串口初始化相对简单,但顺序不能错:

  1. 引脚复用配置:把GPIO引脚切换到UART功能
  2. 时钟使能:打开UART模块时钟
  3. 波特率配置:根据时钟频率计算波特率寄存器值
  4. 数据格式设置:8N1、8E1等
  5. 使能UART:开启发送器和接收器

以常见的115200波特率为例:

/* 假设UART时钟为64MHz,目标波特率115200 */
/* 波特率 = 时钟 / (16 * USARTDIV) */
/* USARTDIV = 64000000 / (16 * 115200) = 34.72 */
USART1->BRR = (34 << 4) | (0x72 >> 4);  // 整数部分34,小数部分0.72
小技巧:波特率计算有误差是正常的,只要误差在±2%以内,一般都能正常通信。我习惯用整数除法加四舍五入,避免浮点运算。

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就成功了一半。

实战建议:每次移植到一个新板卡,先只跑时钟和串口,看到打印信息后再逐步添加DDR和其他外设。这样出了问题,你能快速定位是哪个环节的锅。