4. 硬件初始化实战:时钟树配置、GPIO初始化、串口外设初始化、滴答定时器配置

硬件初始化,说白了就是给芯片“通电开窍”。

很多刚入行的朋友,拿到一块新板子,上来就写应用逻辑。结果程序跑飞了,连个打印都看不到。我早期也犯过这个错——折腾了半天,最后发现是时钟没配好,CPU 压根没跑在预期频率上。

这一章,咱们就把 Bootloader 启动阶段最核心的四个硬件模块捋清楚。你跟着我的节奏走一遍,以后移植任何芯片心里都有底。

4.1 时钟树配置:芯片的心脏起搏器

时钟是芯片的“心跳”。没有时钟,外设不工作,CPU 也不干活。我习惯把时钟树想象成一个自来水系统:晶振是水源,PLL 是增压泵,分频器是阀门,最终流向各个外设。

4.1.1 时钟源的选择

常见的时钟源有这几种:

时钟源 典型频率 特点 Bootloader 常用场景
HSI(内部高速振荡器) 8/16 MHz 上电即用,精度一般 初始启动、备份时钟
HSE(外部高速晶振) 4~25 MHz 精度高,需要起振时间 主系统时钟
LSI(内部低速振荡器) 32 kHz 低功耗,精度差 独立看门狗、RTC
LSE(外部低速晶振) 32.768 kHz 高精度,RTC 专用 精准计时
PLL(锁相环) 倍频后可达数百 MHz 高频,需配置倍频系数 高性能运行

我个人习惯在 Bootloader 里优先使用 HSE + PLL 的组合。为什么?因为 Bootloader 要快速完成初始化,然后跳转到应用。HSE 精度高,PLL 能拉到芯片最高主频,这样 Flash 读取、CRC 校验都能跑得快。

经验之谈: 有些芯片的 HSE 起振时间长达几毫秒。我建议在等待 HSE Ready 标志位时,加一个超时处理。否则晶振坏了,程序就卡死在 while 循环里。我曾经遇到过一批板子晶振虚焊,加了超时后,Bootloader 能自动切回 HSI,至少还能通过串口打印错误信息。

4.1.2 典型时钟配置流程

以 STM32F4 为例,配置 168 MHz 系统时钟的步骤大致如下:

  1. 复位 RCC 寄存器
  2. 使能 HSE,等待就绪
  3. 配置 PLL 参数(倍频、分频)
  4. 使能 PLL,等待锁定
  5. 配置 AHB、APB1、APB2 总线分频
  6. 切换系统时钟到 PLL 输出
  7. 更新系统滴答定时器的时钟源

代码示例:

void SystemClock_Config(void)
{
    // 1. 使能 HSE
    RCC->CR |= RCC_CR_HSEON;
    while(!(RCC->CR & RCC_CR_HSERDY))
    {
        // 超时处理,防止死等
        if(timeout-- == 0) break;
    }

    // 2. 配置 PLL: HSE 8MHz -> PLL_M=8 -> PLL_N=336 -> PLL_P=2 -> 168MHz
    RCC->PLLCFGR = (8 << 0)  |   // PLL_M
                    (336 << 6) |  // PLL_N
                    (0 << 16)  |  // PLL_P = 2
                    (RCC_PLLCFGR_PLLSRC_HSE);

    // 3. 使能 PLL
    RCC->CR |= RCC_CR_PLLON;
    while(!(RCC->CR & RCC_CR_PLLRDY));

    // 4. 配置总线分频
    RCC->CFGR = RCC_CFGR_HPRE_DIV1 |   // AHB = 168MHz
                RCC_CFGR_PPRE1_DIV4 |   // APB1 = 42MHz
                RCC_CFGR_PPRE2_DIV2;    // APB2 = 84MHz

    // 5. 切换系统时钟
    RCC->CFGR |= RCC_CFGR_SW_PLL;
    while((RCC->CFGR & RCC_CFGR_SWS) != RCC_CFGR_SWS_PLL);
}
注意: 不同芯片的 PLL 参数范围差异很大。比如 STM32F4 的 PLL_N 范围是 192~432,而某些国产芯片可能只有 64~240。移植时一定要查数据手册,别直接抄代码。我曾经见过有人把 F4 的配置直接搬到 F1 上,结果芯片直接锁死。

4.2 GPIO 初始化:让芯片的“手脚”动起来

GPIO 是芯片和外界交互的“手脚”。在 Bootloader 里,GPIO 主要用来控制状态指示灯、检测启动模式引脚、或者做简单的调试输出。

4.2.1 GPIO 模式选择

每个 GPIO 引脚通常有这几种模式:

  • 输入模式: 读取外部电平。用于检测按键、跳线帽。
  • 输出模式: 推挽输出(驱动能力强)或开漏输出(用于 I2C 等总线)。
  • 复用功能: 引脚交给片上外设(如串口、SPI)控制。
  • 模拟模式: 用于 ADC、DAC。

我个人习惯在 Bootloader 里把调试串口的 TX/RX 配置为复用功能推挽输出/浮空输入,把状态 LED 配置为推挽输出。其他不用的引脚,我通常会设为模拟输入或带上拉输入,这样最省电、最稳定。

4.2.2 初始化代码示例

void GPIO_Init(void)
{
    // 使能 GPIOA 和 GPIOC 时钟
    RCC->AHB1ENR |= RCC_AHB1ENR_GPIOAEN | RCC_AHB1ENR_GPIOCEN;

    // PA9: USART1_TX, 复用功能推挽输出
    GPIOA->MODER &= ~(3 << 18);  // 清除原配置
    GPIOA->MODER |=  (2 << 18);  // 复用功能
    GPIOA->AFR[1] |= (7 << 4);   // AF7 = USART1

    // PA10: USART1_RX, 复用功能浮空输入
    GPIOA->MODER &= ~(3 << 20);
    GPIOA->MODER |=  (2 << 20);
    GPIOA->AFR[1] |= (7 << 8);

    // PC13: LED, 推挽输出
    GPIOC->MODER &= ~(3 << 26);
    GPIOC->MODER |=  (1 << 26);  // 输出模式
    GPIOC->OTYPER &= ~(1 << 13); // 推挽
    GPIOC->OSPEEDR |= (3 << 26); // 高速
}
避坑指南: 我曾经在调试一块板子时,串口死活发不出数据。查了半天,发现是 GPIO 的复用功能映射表搞错了。同一个引脚,不同芯片可能对应不同的 AF 编号。一定要看数据手册的“Alternate function mapping”表格,别想当然。

4.3 串口外设初始化:Bootloader 的“嘴巴”

串口是 Bootloader 最重要的调试和通信接口。你想想看,没有串口打印,你怎么知道程序跑到哪一步了?

4.3.1 串口参数配置

标准配置一般是:115200 波特率、8 位数据、1 位停止位、无校验。但要注意,有些芯片的串口时钟来自 APB 总线,如果 APB 时钟变了,波特率也要重新算。

波特率计算公式:

USART_BRR = (APB_Clock + (Baudrate / 2)) / Baudrate

举个例子,APB2 时钟是 84 MHz,目标波特率 115200:

USART_BRR = (84000000 + 57600) / 115200 = 729.5 ≈ 730

4.3.2 串口初始化代码

void USART1_Init(void)
{
    // 使能 USART1 时钟
    RCC->APB2ENR |= RCC_APB2ENR_USART1EN;

    // 配置波特率 115200 @ 84MHz
    USART1->BRR = 730;

    // 使能发送、接收、以及整个 USART
    USART1->CR1 = USART_CR1_TE | USART_CR1_RE | USART_CR1_UE;

    // 8 位数据,1 位停止位(默认配置,无需额外设置)
}
关键点: 串口初始化顺序很重要。我建议先配 GPIO,再配串口寄存器,最后使能 USART。如果先使能了 USART,再配 GPIO,可能会产生一个错误的起始位,导致对方收到乱码。

4.4 滴答定时器配置:系统的“心跳节拍”

SysTick 是 Cortex-M 内核自带的 24 位递减定时器。它最大的用处就是提供系统滴答,用来做延时、超时判断、或者 RTOS 的时基。

4.4.1 SysTick 配置要点

  • 重装载值: 等于 (系统时钟频率 / 期望滴答频率) - 1。比如 168 MHz 时钟,想要 1 ms 滴答一次,重装载值就是 168000 - 1。
  • 时钟源: 可以选择处理器时钟(AHB)或 AHB/8。我一般选 AHB,这样精度最高。
  • 中断使能: 如果需要定时回调,就使能 SysTick 中断。

4.4.2 配置代码

void SysTick_Init(void)
{
    // 重装载值: 168MHz / 1000Hz = 168000 - 1
    SysTick->LOAD = 168000 - 1;

    // 清零当前值
    SysTick->VAL = 0;

    // 使能 SysTick,使用处理器时钟,使能中断
    SysTick->CTRL = SysTick_CTRL_CLKSOURCE_Msk |
                    SysTick_CTRL_TICKINT_Msk   |
                    SysTick_CTRL_ENABLE_Msk;
}

// 中断处理函数
void SysTick_Handler(void)
{
    static uint32_t tick = 0;
    tick++;
    // 可以在这里做 1ms 周期的任务,比如 LED 闪烁
}
注意: SysTick 是 24 位的,最大计数值是 16,777,215。如果你的系统时钟很高,又想用很低的滴答频率(比如 10 Hz),重装载值可能会溢出。这时候要么降低时钟源(选 AHB/8),要么用其他定时器。我遇到过有人把重装载值写成 16800000,结果 SysTick 直接不工作,因为溢出了。

4.5 本章知识结构图

下面这张图把四个模块的依赖关系画清楚了。时钟是基础,GPIO 和串口依赖时钟,SysTick 依赖系统时钟。你按这个顺序初始化,基本不会出问题。

硬件初始化流程与依赖关系 时钟源配置 HSE / HSI / PLL 系统时钟 AHB / APB1 / APB2 GPIO 初始化 模式 / 速度 / 复用 串口初始化 波特率 / 数据位 / 停止位 SysTick 初始化 重装载值 / 中断 初始化顺序建议 ① 配置时钟源 → ② 配置系统时钟总线分频 ③ 初始化 GPIO → ④ 初始化串口 → ⑤ 初始化 SysTick 串口和 SysTick 顺序可互换,但 GPIO 必须在串口之前

4.6 常见问题与调试技巧

硬件初始化阶段最容易出问题。我总结了几条经验,你遇到了可以直接对号入座:

现象 可能原因 排查方法
程序跑不起来,全速运行后无反应 时钟配置错误,芯片未启动 用示波器测晶振引脚,看有无波形;检查 PLL 锁定标志
串口输出乱码 波特率不对,或 GPIO 复用配置错误 核对 APB 时钟频率与 BRR 计算值;检查 AF 映射表
LED 不亮 GPIO 模式或时钟未使能 检查 RCC 外设时钟使能位;用万用表测引脚电平
延时不准 SysTick 重装载值计算错误 确认 SysTick 时钟源是 AHB 还是 AHB/8;核对系统时钟频率
进入 HardFault 外设时钟未使能就操作寄存器 检查所有外设的 RCC 时钟使能位是否已置位
我的调试习惯: 每初始化一个模块,就在串口打印一条日志。比如“Clock OK”、“GPIO OK”、“UART OK”。这样一旦出问题,马上能定位到是哪个环节挂了。别等到所有初始化都写完再调试,那会儿你根本不知道是哪里崩的。

嗯,硬件初始化这块,说白了就是细心活。时钟配错了,后面全白搭。GPIO 搞反了,串口就哑巴。你只要按我上面说的顺序来,再配合串口日志输出,基本一次过。