3. Bootloader阶段的外设初始化:时钟系统与GPIO配置

各位好,我是老周。今天咱们聊聊Bootloader阶段最基础、也最容易出问题的一个环节——外设初始化。说白了,就是让芯片先“活”过来。

你想想看,芯片上电后,默认状态是什么?大部分MCU出厂时,内部RC振荡器在工作,主频可能只有几兆赫兹。这时候你要做Bootloader,要跑通信协议,要擦写Flash……这点速度根本不够用。所以,第一步就是先把时钟系统整明白。

3.1 时钟系统初始化——芯片的“心跳”

时钟系统,我习惯叫它芯片的“心跳”。心跳不对,全身都不对。Bootloader阶段时钟初始化的核心任务,就是把PLL锁相环配置好,让系统跑在目标频率上。

3.1.1 时钟树的基本结构

大部分ARM Cortex-M系列芯片,时钟树都长这样:

核心路径: 外部晶振 → PLL输入 → PLL倍频 → 系统时钟总线 → 各外设时钟分频

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

Bootloader阶段时钟初始化流程 外部晶振 8MHz / 16MHz PLL输入选择 HSE / HSI PLL倍频 N/M倍频 SYSCLK AHB分频 (HCLK) APB1分频 (PCLK1) APB2分频 (PCLK2) ⚠ 关键点: • 先配PLL源,再配倍频系数,最后切换系统时钟源 • 切换时钟源前,必须等待新时钟稳定(一般用HSI过渡)

3.1.2 PLL配置的典型步骤

我个人习惯把PLL配置拆成三步走:

  1. 选择时钟源——外部晶振(HSE)还是内部振荡器(HSI)?Bootloader阶段我建议用HSE,精度高,通信协议不容易出问题。
  2. 配置倍频系数——根据目标频率反推。比如外部8MHz晶振,目标72MHz,倍频系数就是9。
  3. 切换系统时钟——先把PLL使能,等它稳定,再把系统时钟源切到PLL输出。

我的经验: 切换时钟源之前,一定要先读一下时钟就绪标志位。我曾经在一个项目里,没等PLL稳定就切过去了,结果系统直接跑飞。后来加了超时判断,再也没出过事。

下面是一个典型的PLL初始化代码,以STM32F1为例:

void SystemClock_Init(void)
{
    uint32_t timeout = 0;
    
    /* 1. 使能外部晶振 */
    RCC->CR |= RCC_CR_HSEON;
    
    /* 2. 等待HSE就绪,加超时保护 */
    timeout = 0xFFFFFF;
    while (!(RCC->CR & RCC_CR_HSERDY))
    {
        if (--timeout == 0)
        {
            /* HSE起振失败,回退到HSI */
            RCC->CR &= ~RCC_CR_HSEON;
            return;
        }
    }
    
    /* 3. 配置Flash等待周期(重要!频率高了必须加) */
    FLASH->ACR = FLASH_ACR_LATENCY_2;
    
    /* 4. 配置PLL:HSE 8MHz * 9 = 72MHz */
    RCC->CFGR |= RCC_CFGR_PLLSRC_HSE | RCC_CFGR_PLLMULL9;
    
    /* 5. 使能PLL */
    RCC->CR |= RCC_CR_PLLON;
    
    /* 6. 等待PLL就绪 */
    while (!(RCC->CR & RCC_CR_PLLRDY));
    
    /* 7. 切换系统时钟到PLL */
    RCC->CFGR |= RCC_CFGR_SW_PLL;
    while ((RCC->CFGR & RCC_CFGR_SWS) != RCC_CFGR_SWS_PLL);
}

⚠ 注意: Flash等待周期(Latency)很多人会忘。频率超过48MHz时,必须增加等待周期,否则程序会随机跑飞。我见过好几个工程师在这上面栽跟头。

3.2 GPIO初始化与复用功能配置

时钟配好了,接下来就是GPIO。Bootloader阶段的GPIO初始化,跟应用层不太一样。你想想看,Bootloader要跟外界通信——串口下载、SPI Flash、USB DFU……这些都需要特定的引脚。

3.2.1 GPIO模式选择

GPIO的模式,说白了就是告诉芯片:这个引脚是输入还是输出?要不要上拉?速度多快?

模式 用途 Bootloader常见场景
推挽输出 驱动LED、控制信号 状态指示、复位控制
开漏输出 I2C总线、电平转换 I2C通信
浮空输入 读取外部电平 按键检测、启动模式选择
复用功能 外设信号引出 UART TX/RX、SPI SCK/MOSI

3.2.2 复用功能配置——最容易踩坑的地方

复用功能,就是把GPIO引脚“借”给外设用。比如你要用UART1,PA9就是TX,PA10就是RX。但这里有个坑——不同芯片的复用映射不一样。

我记得有一次做项目,用的是GD32F103,UART2的TX在PD5。我按STM32的习惯配了PA2,结果死活不通。查了半天手册才发现,GD32的复用映射跟ST不完全一样。从那以后,我每次配复用功能,都会先看一眼数据手册的“Alternate Function Mapping”表。

复用功能配置三步法:

  1. 使能对应GPIO端口的时钟
  2. 使能对应外设的时钟
  3. 配置GPIO为复用功能模式,并选择正确的复用编号(AF号)

以STM32L4系列配置UART1为例:

void GPIO_UART_Init(void)
{
    /* 1. 使能时钟 */
    RCC->AHB2ENR |= RCC_AHB2ENR_GPIOAEN;
    RCC->APB2ENR |= RCC_APB2ENR_USART1EN;
    
    /* 2. 配置PA9为复用功能,AF7(USART1_TX) */
    GPIOA->MODER &= ~(GPIO_MODER_MODE9);
    GPIOA->MODER |= GPIO_MODER_MODE9_1;  // 复用功能
    
    /* 3. 选择复用功能编号 */
    GPIOA->AFR[1] |= (7 << 4);  // AF7
    
    /* 4. 配置输出类型、速度、上拉 */
    GPIOA->OTYPER &= ~GPIO_OTYPER_OT9;   // 推挽
    GPIOA->OSPEEDR |= GPIO_OSPEEDR_OSPEED9; // 高速
    GPIOA->PUPDR &= ~GPIO_PUPDR_PUPD9;    // 无上拉
    
    /* 5. 同样配置PA10为RX */
    GPIOA->MODER &= ~(GPIO_MODER_MODE10);
    GPIOA->MODER |= GPIO_MODER_MODE10_1;
    GPIOA->AFR[1] |= (7 << 8);  // AF7
}

3.2.3 Bootloader特有的GPIO注意事项

Bootloader阶段的GPIO配置,跟应用层有几个关键区别:

  • 引脚状态要保持——Bootloader退出后,GPIO状态不能乱。我习惯在跳转到应用前,把用过的GPIO恢复到默认状态。
  • 启动引脚检测——很多Bootloader通过检测某个引脚的电平来决定是否进入升级模式。这个引脚必须在时钟初始化之前就能读。
  • 功耗考虑——Bootloader阶段虽然短,但也要注意。没用的外设时钟关掉,没用的GPIO设成模拟输入,省电。

⚠ 我曾经踩过的坑: 有一次做OTA升级,Bootloader里配了UART的GPIO,跳转到应用后,应用也配了同样的引脚。结果两个配置冲突,串口乱码。后来我在Bootloader跳转前,把GPIO复位到默认状态,问题解决。

3.3 时钟与GPIO的联动——一个实际案例

咱们来看一个完整的例子。假设你要做一个Bootloader,用UART1下载固件,波特率115200,系统时钟72MHz。

初始化顺序是这样的:

  1. 先配时钟——PLL输出72MHz
  2. 再配GPIO——PA9/PA10复用为UART1
  3. 最后配UART——设置波特率、数据格式

为什么是这个顺序?因为UART的波特率发生器需要系统时钟来计算分频系数。时钟没配好,波特率就是错的。

小技巧: 调试的时候,可以在GPIO初始化后,先点个LED。如果LED亮了,说明时钟和GPIO都对了。然后再去调UART。这样能快速定位问题。