3. 硬件初始化要点:时钟系统配置、SDRAM/DDR初始化、串口初始化、GPIO配置
硬件初始化,说白了就是给芯片“通电醒脑”的过程。很多刚入行的朋友觉得这步简单,照着参考代码抄一遍就行。但我告诉你,Bootloader 启动失败,十有八九都栽在这四个环节上。
我调试过不下 50 块开发板,从 STM32 到 i.MX 再到全志,每次拿到新板子,第一件事就是把这四个初始化点逐个敲一遍。今天我把这些年踩过的坑、总结的经验,全部分享给你。
3.1 时钟系统配置——芯片的心脏起搏器
时钟不对,一切白费。芯片内部所有外设、CPU 核、总线,都靠时钟信号驱动。你想想看,如果心脏跳得忽快忽慢,人还能正常工作吗?
3.1.1 时钟源的选择
常见的时钟源有这几种:
| 时钟源 | 典型频率 | 精度 | 典型用途 |
|---|---|---|---|
| 内部高速 RC 振荡器 (HSI) | 8/16 MHz | ±1%~±3% | 上电初始运行、低功耗模式 |
| 外部高速晶振 (HSE) | 4~25 MHz | ±10~±50 ppm | 主系统时钟、PLL 参考源 |
| 内部低速 RC (LSI) | 32~40 kHz | ±5%~±10% | 独立看门狗、RTC 备用 |
| 外部低速晶振 (LSE) | 32.768 kHz | ±5~±20 ppm | 高精度 RTC |
我个人习惯:上电后先用 HSI 跑起来,等 HSE 稳定了再切过去。为什么?因为 HSE 起振需要时间,有的晶振要等几毫秒甚至几十毫秒。如果一上来就切 HSE,芯片可能直接卡死在时钟就绪检测里。
3.1.2 PLL 配置要点
PLL(锁相环)用来把低频时钟倍频到高频。配置时要注意三个参数:
- M(分频系数):把输入时钟分到 PLL 的最佳输入范围(通常 1~2 MHz)
- N(倍频系数):决定输出频率,N 越大抖动越大
- P/Q(后分频):得到最终需要的时钟频率
公式很简单:PLL输出 = (输入时钟 / M) × N / P
举个例子,STM32F4 上配置 168 MHz 系统时钟:
/* HSE = 8 MHz, 目标 SysClk = 168 MHz */
/* PLL 输入范围要求 1~2 MHz,所以 M = 8 */
/* N = 336, P = 2 → 8/8 × 336/2 = 168 MHz */
RCC->PLLCFGR = (8 << 0) | /* PLL_M = 8 */
(336 << 6) | /* PLL_N = 336 */
(0 << 16) | /* PLL_P = 2 (对应 00) */
(7 << 24); /* PLL_Q = 7 (用于 USB/SDIO) */
/* 等待 PLL 就绪 */
while(!(RCC->CR & RCC_CR_PLLRDY));
3.2 SDRAM/DDR 初始化——内存系统的基石
Bootloader 通常要加载到 DDR 里运行,所以 DDR 初始化必须在跳转到主程序之前完成。这块是最容易出问题的,因为 DDR 的时序参数极其敏感。
3.2.1 DDR 初始化流程
标准的 DDR 初始化步骤是这样的:
- 上电等待:VDD 和 VDDQ 稳定后,等待至少 200 μs
- 发送 NOP 命令:让 DDR 进入空闲状态
- 预充电所有 Bank:确保所有 Bank 处于空闲
- 发送 2 个自动刷新命令:唤醒内部刷新逻辑
- 设置模式寄存器 (MR):配置 CAS 延迟、突发长度、突发类型
- 设置扩展模式寄存器 (EMR):配置 DLL 使能、输出驱动强度
- 执行 ZQ 校准:DDR3/DDR4 需要,校准输出驱动阻抗
- 进入就绪状态:可以正常读写
我画了一张流程图,帮你理清这个顺序:
3.2.2 时序参数配置
DDR 的时序参数藏在芯片的数据手册里。你需要关注这几个关键参数:
| 参数 | 含义 | 典型值 (DDR3-1600) | 配置错误后果 |
|---|---|---|---|
| tCL (CAS Latency) | 列地址选通延迟 | 11 个时钟周期 | 数据读取错位,系统崩溃 |
| tRCD | 行到列延迟 | 11 个时钟周期 | 读写时序混乱 |
| tRP | 行预充电时间 | 11 个时钟周期 | Bank 切换失败 |
| tRAS | 行激活时间 | 28 个时钟周期 | 数据保持不足 |
| tRFC | 刷新周期时间 | 260 ns | 刷新失败,数据丢失 |
🔑 核心经验: 我建议你先把时序参数设得保守一点(比如把 tCL 加 1),等系统跑稳了再优化。我见过太多人一上来就追求极致性能,结果 DDR 初始化成功率只有 60%,温度一高就死机。
3.3 串口初始化——Bootloader 的“眼睛”
串口是调试 Bootloader 最重要的工具。没有串口打印,你根本不知道芯片跑到哪一步了。
3.3.1 串口初始化步骤
- 使能 GPIO 时钟和 USART 时钟
- 配置 TX/RX 引脚为复用功能
- 设置波特率、数据位、停止位、校验位
- 使能 USART 模块
- 发送一个测试字符(验证配置是否正确)
代码示例(基于 STM32 HAL 风格,但去掉了 HAL 依赖):
/* 假设 USART1, TX=PA9, RX=PA10, 波特率 115200 */
/* 1. 使能时钟 */
RCC->APB2ENR |= RCC_APB2ENR_USART1EN;
RCC->AHB1ENR |= RCC_AHB1ENR_GPIOAEN;
/* 2. 配置 GPIO 复用 */
GPIOA->MODER &= ~(0x3 << 18); /* PA9 清零 */
GPIOA->MODER |= (0x2 << 18); /* PA9 复用模式 */
GPIOA->AFR[1] |= (0x7 << 4); /* PA9 AF7 = USART1_TX */
GPIOA->MODER &= ~(0x3 << 20); /* PA10 清零 */
GPIOA->MODER |= (0x2 << 20); /* PA10 复用模式 */
GPIOA->AFR[1] |= (0x7 << 8); /* PA10 AF7 = USART1_RX */
/* 3. 配置 USART 参数 */
USART1->BRR = 84000000 / 115200; /* 假设 APB2 = 84 MHz */
USART1->CR1 = USART_CR1_TE | USART_CR1_RE; /* 使能发送和接收 */
USART1->CR1 |= USART_CR1_UE; /* 使能 USART */
/* 4. 发送测试字符 'U' */
while(!(USART1->SR & USART_SR_TXE));
USART1->DR = 'U';
3.3.2 波特率误差
串口通信要求双方的波特率误差在 ±2% 以内。计算公式:
USARTDIV = 外设时钟 / (16 × 目标波特率)
举个例子,APB2 = 84 MHz,目标 115200:
USARTDIV = 84000000 / (16 × 115200) = 45.5729
取整后实际波特率 = 84000000 / (16 × 45) = 116666,误差 = 1.27%,在允许范围内。
3.4 GPIO 配置——芯片的“手脚”
GPIO 看似简单,但配置错了照样翻车。Bootloader 里 GPIO 主要用来做三件事:
- 状态指示:LED 闪烁,告诉开发者 Bootloader 跑到哪一步了
- 硬件使能:控制电源芯片、复位信号、片选信号
- 调试辅助:用 GPIO 输出脉冲,配合示波器测量时序
3.4.1 GPIO 配置参数
| 参数 | 可选值 | 典型用途 |
|---|---|---|
| 模式 | 输入、输出、复用、模拟 | LED 用输出,按键用输入 |
| 输出类型 | 推挽、开漏 | 推挽驱动 LED,开漏用于 I2C |
| 上拉/下拉 | 上拉、下拉、浮空 | 按键输入用上拉,避免悬空 |
| 速度 | 低速、中速、高速、极速 | LED 用低速,SPI 用高速 |
3.4.2 一个实用的 GPIO 调试技巧
我习惯在 Bootloader 的每个关键步骤设置一个 GPIO 翻转点:
/* 定义调试 GPIO */
#define DEBUG_GPIO_PORT GPIOB
#define DEBUG_GPIO_PIN GPIO_PIN_0
/* 初始化 */
DEBUG_GPIO_PORT->MODER &= ~(0x3 << 0);
DEBUG_GPIO_PORT->MODER |= (0x1 << 0); /* 输出模式 */
DEBUG_GPIO_PORT->OSPEEDR |= (0x3 << 0); /* 高速 */
/* 在关键位置翻转 */
void debug_pulse(void) {
DEBUG_GPIO_PORT->BSRR = DEBUG_GPIO_PIN; /* 置高 */
DEBUG_GPIO_PORT->BSRR = (uint32_t)DEBUG_GPIO_PIN << 16; /* 置低 */
}
/* 使用示例 */
debug_pulse(); /* 时钟初始化完成 */
debug_pulse(); /* DDR 初始化完成 */
debug_pulse(); /* 串口初始化完成 */
用示波器看这个 GPIO 的波形,你就能精确知道每个步骤花了多少时间。如果某个步骤的脉冲没出现,问题就锁定在那一步。
🔑 核心经验: 我建议你在 Bootloader 里至少留 2 个 GPIO 做调试用。一个做“心跳”指示(周期性翻转),一个做“步骤”指示(每个关键点翻转)。这样即使串口还没初始化好,你也能通过示波器判断 Bootloader 的运行状态。
3.5 初始化顺序的坑
这四个初始化模块之间有依赖关系,顺序不能乱:
- 时钟系统 → 必须先配好,因为其他所有外设都依赖时钟
- GPIO → 配好基本 GPIO,至少让 LED 能亮
- 串口 → 有了串口才能打印调试信息
- DDR → 最后初始化,因为 DDR 初始化过程中可能需要串口打印状态
但这里有个例外:如果你的 Bootloader 本身就放在 DDR 里运行(比如从 NAND Flash 加载),那 DDR 初始化必须放在最前面。这种情况下,串口打印只能放在 DDR 初始化之后。
嗯,硬件初始化这块,说白了就是“慢工出细活”。每个参数都要核对数据手册,每个时序都要用示波器验证。别嫌麻烦,这一步做好了,后面的调试会顺畅很多。