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,芯片可能直接卡死在时钟就绪检测里。

⚠ 我曾经踩过的坑: 某次调试一块 i.MX6ULL 板子,死活起不来。查了两天,最后发现是外部晶振的负载电容焊错了。25 MHz 晶振配了 22 pF 电容,结果起振时间从 2 ms 拖到了 50 ms,Bootloader 在超时前没等到时钟稳定标志位。换回 12 pF 后一次通过。

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));
💡 小技巧: 配置完 PLL 后,一定要等 PLL 锁定标志位。我见过有人直接跳过等待,结果系统时钟频率不对,串口打印全是乱码。

3.2 SDRAM/DDR 初始化——内存系统的基石

Bootloader 通常要加载到 DDR 里运行,所以 DDR 初始化必须在跳转到主程序之前完成。这块是最容易出问题的,因为 DDR 的时序参数极其敏感。

3.2.1 DDR 初始化流程

标准的 DDR 初始化步骤是这样的:

  1. 上电等待:VDD 和 VDDQ 稳定后,等待至少 200 μs
  2. 发送 NOP 命令:让 DDR 进入空闲状态
  3. 预充电所有 Bank:确保所有 Bank 处于空闲
  4. 发送 2 个自动刷新命令:唤醒内部刷新逻辑
  5. 设置模式寄存器 (MR):配置 CAS 延迟、突发长度、突发类型
  6. 设置扩展模式寄存器 (EMR):配置 DLL 使能、输出驱动强度
  7. 执行 ZQ 校准:DDR3/DDR4 需要,校准输出驱动阻抗
  8. 进入就绪状态:可以正常读写

我画了一张流程图,帮你理清这个顺序:

DDR 初始化标准流程 上电等待 ≥200 μs 发送 NOP 命令 预充电所有 Bank 2 次自动刷新 配置模式寄存器 MR 配置扩展模式寄存器 EMR ZQ 校准 (DDR3/4) 进入就绪状态 ⚠ 自动刷新间隔必须 严格遵循 tRFC 参数 💡 CAS 延迟必须与 硬件设计匹配

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 串口初始化步骤

  1. 使能 GPIO 时钟和 USART 时钟
  2. 配置 TX/RX 引脚为复用功能
  3. 设置波特率、数据位、停止位、校验位
  4. 使能 USART 模块
  5. 发送一个测试字符(验证配置是否正确)

代码示例(基于 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';
⚠ 我曾经踩过的坑: 有一次串口死活不输出,查了半天发现是 GPIO 复用功能号配错了。STM32F4 的 USART1 是 AF7,但我配成了 AF8。这种错误用逻辑分析仪一看就知道——TX 引脚上根本没有信号翻转。

3.3.2 波特率误差

串口通信要求双方的波特率误差在 ±2% 以内。计算公式:

USARTDIV = 外设时钟 / (16 × 目标波特率)

举个例子,APB2 = 84 MHz,目标 115200:

USARTDIV = 84000000 / (16 × 115200) = 45.5729

取整后实际波特率 = 84000000 / (16 × 45) = 116666,误差 = 1.27%,在允许范围内。

💡 小技巧: 如果误差超过 2%,可以调整外设时钟频率,或者选一个更接近的整数分频值。我一般会准备一个波特率计算表,把常用频率和波特率的组合都算好。

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 初始化顺序的坑

这四个初始化模块之间有依赖关系,顺序不能乱:

  1. 时钟系统 → 必须先配好,因为其他所有外设都依赖时钟
  2. GPIO → 配好基本 GPIO,至少让 LED 能亮
  3. 串口 → 有了串口才能打印调试信息
  4. DDR → 最后初始化,因为 DDR 初始化过程中可能需要串口打印状态

但这里有个例外:如果你的 Bootloader 本身就放在 DDR 里运行(比如从 NAND Flash 加载),那 DDR 初始化必须放在最前面。这种情况下,串口打印只能放在 DDR 初始化之后。

⚠ 我曾经踩过的坑: 某次我把 DDR 初始化放在了串口之前,结果 DDR 初始化失败,没有任何输出,完全不知道卡在哪一步。后来我加了一个 GPIO 脉冲指示,才发现是 DDR 的 ZQ 校准没通过——板子上的 ZQ 电阻焊错了,标称 240 Ω 实际焊了 100 Ω。

嗯,硬件初始化这块,说白了就是“慢工出细活”。每个参数都要核对数据手册,每个时序都要用示波器验证。别嫌麻烦,这一步做好了,后面的调试会顺畅很多。