3. Bootloader阶段的外设初始化:时钟系统与GPIO配置
各位好,我是老周。今天咱们聊聊Bootloader阶段最基础、也最容易出问题的一个环节——外设初始化。说白了,就是让芯片先“活”过来。
你想想看,芯片上电后,默认状态是什么?大部分MCU出厂时,内部RC振荡器在工作,主频可能只有几兆赫兹。这时候你要做Bootloader,要跑通信协议,要擦写Flash……这点速度根本不够用。所以,第一步就是先把时钟系统整明白。
3.1 时钟系统初始化——芯片的“心跳”
时钟系统,我习惯叫它芯片的“心跳”。心跳不对,全身都不对。Bootloader阶段时钟初始化的核心任务,就是把PLL锁相环配置好,让系统跑在目标频率上。
3.1.1 时钟树的基本结构
大部分ARM Cortex-M系列芯片,时钟树都长这样:
核心路径: 外部晶振 → PLL输入 → PLL倍频 → 系统时钟总线 → 各外设时钟分频
我画了一张图,帮你理清这个流程:
3.1.2 PLL配置的典型步骤
我个人习惯把PLL配置拆成三步走:
- 选择时钟源——外部晶振(HSE)还是内部振荡器(HSI)?Bootloader阶段我建议用HSE,精度高,通信协议不容易出问题。
- 配置倍频系数——根据目标频率反推。比如外部8MHz晶振,目标72MHz,倍频系数就是9。
- 切换系统时钟——先把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”表。
复用功能配置三步法:
- 使能对应GPIO端口的时钟
- 使能对应外设的时钟
- 配置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。
初始化顺序是这样的:
- 先配时钟——PLL输出72MHz
- 再配GPIO——PA9/PA10复用为UART1
- 最后配UART——设置波特率、数据格式
为什么是这个顺序?因为UART的波特率发生器需要系统时钟来计算分频系数。时钟没配好,波特率就是错的。
小技巧: 调试的时候,可以在GPIO初始化后,先点个LED。如果LED亮了,说明时钟和GPIO都对了。然后再去调UART。这样能快速定位问题。