3. STM32启动文件与系统时钟配置

好,咱们进入第三章。这一章要解决一个很实在的问题——怎么让STM32一上电就按我们的想法跑起来,并且给FreeRTOS提供一个稳定的心跳。

启动文件和时钟配置,说白了就是芯片的「开机流程」。我刚开始学的时候也觉得这部分挺枯燥的,不就是复制粘贴嘛。但后来踩过几次坑才发现,这里要是搞错了,后面RTOS跑起来各种诡异问题,你根本不知道从哪查起。

3.1 启动文件在干什么

STM32的启动文件(startup_stm32fxxx.s)是汇编写的。它主要干三件事:

  • 设置堆栈指针(SP)
  • 初始化中断向量表
  • 调用SystemInit(),然后跳转到main()

嗯,这里要注意。启动文件里默认调用了SystemInit(),这个函数在system_stm32fxxx.c里。我们的时钟配置,就是从这里开始的。

个人习惯:我一般不会去改启动文件本身,除非要调整堆栈大小。堆栈大小默认是0x400(1KB),如果你任务里用了大数组,记得改大点。我在一个项目中就吃过这个亏,任务栈溢出了,系统莫名其妙重启,查了两天才发现是启动文件里堆栈设小了。

3.2 系统时钟配置——HSE + PLL

FreeRTOS需要系统节拍,一般是1ms一次。这个节拍来自SysTick定时器,而SysTick的时钟源又来自系统时钟。所以,系统时钟准不准,直接决定了RTOS的节拍准不准。

我常用的配置是:外部8MHz晶振(HSE) → PLL倍频 → 72MHz系统时钟。这是STM32F103最经典的配置。

来看看代码:

void SystemInit(void)
{
    // 1. 开启HSE,等待就绪
    RCC->CR |= RCC_CR_HSEON;
    while(!(RCC->CR & RCC_CR_HSERDY));

    // 2. 配置FLASH预取指、等待周期
    // 72MHz下,等待周期设为2
    FLASH->ACR = FLASH_ACR_PRFTBE | FLASH_ACR_LATENCY_2;

    // 3. 配置PLL:HSE 8MHz * 9 = 72MHz
    RCC->CFGR |= RCC_CFGR_PLLSRC_HSE;
    RCC->CFGR |= RCC_CFGR_PLLMULL9;

    // 4. 配置AHB、APB1、APB2分频
    // AHB = 72MHz, APB1 = 36MHz, APB2 = 72MHz
    RCC->CFGR |= RCC_CFGR_HPRE_DIV1;   // AHB不分频
    RCC->CFGR |= RCC_CFGR_PPRE1_DIV2;  // APB1 2分频
    RCC->CFGR |= RCC_CFGR_PPRE2_DIV1;  // APB2不分频

    // 5. 开启PLL,等待锁定
    RCC->CR |= RCC_CR_PLLON;
    while(!(RCC->CR & RCC_CR_PLLRDY));

    // 6. 切换系统时钟为PLL输出
    RCC->CFGR &= ~RCC_CFGR_SW;
    RCC->CFGR |= RCC_CFGR_SW_PLL;
    while((RCC->CFGR & RCC_CFGR_SWS) != RCC_CFGR_SWS_PLL);
}

这段代码看起来不长,但每一步都有讲究。我一个个说。

3.3 关键细节与避坑

第一个坑:HSE起振时间。

HSE起振需要时间,尤其是外部晶振质量一般的时候。我遇到过一块板子,HSE死活起不来,后来发现是晶振负载电容焊错了。所以while循环等待HSE就绪这一步,不能省略,也不能用超时替代——除非你做了容错处理。

第二个坑:FLASH等待周期。

很多人会忽略这一步。STM32F103在72MHz下,FLASH等待周期必须设为2,否则程序会跑飞。你想想看,CPU比FLASH快那么多,不等一下怎么行?

我曾经:在一个项目中为了省电,把系统时钟降到36MHz,但忘了改FLASH等待周期。结果程序在36MHz下跑得好好的,一升到72MHz就死机。查了半天才发现是等待周期没跟着改。所以建议你养成习惯:改时钟频率的同时,检查FLASH配置。

第三个坑:APB1和APB2的分频。

APB1最高只能到36MHz,APB2可以到72MHz。如果你把APB1设成72MHz,外设(比如USART、I2C)会工作异常。我见过有人把APB1设成72MHz,然后USART波特率怎么算都不对,其实就是这里的问题。

3.4 配置SysTick为FreeRTOS提供节拍

系统时钟配好了,接下来要让SysTick产生1ms中断。FreeRTOS的vPortSetupTimerInterrupt()会帮我们做这件事,但前提是configCPU_CLOCK_HZconfigTICK_RATE_HZ要配对。

在FreeRTOSConfig.h里:

#define configCPU_CLOCK_HZ      ( ( unsigned long ) 72000000 )
#define configTICK_RATE_HZ      ( ( TickType_t ) 1000 )

SysTick的重装载值 = 系统时钟频率 / 节拍频率 = 72MHz / 1000Hz = 72000。

这个值会在vPortSetupTimerInterrupt()里自动计算。你只要确保configCPU_CLOCK_HZ和实际系统时钟一致就行。

我建议:在main()一开始就调用SystemInit(),然后再初始化FreeRTOS。顺序不要搞反。我曾经试过先创建任务再配时钟,结果SysTick中断来了,系统时钟还没稳定,直接进HardFault。

3.5 验证时钟是否配置正确

配完了怎么验证?最简单的方法:用示波器看MCO引脚。STM32的PA8可以输出系统时钟。

// 使能MCO输出,选择系统时钟
RCC->CFGR |= RCC_CFGR_MCO_SYSCLK;
// 配置PA8为复用推挽输出
GPIOA->CRH &= ~0x0000000F;
GPIOA->CRH |= 0x0000000B;

用示波器量PA8,如果看到72MHz的方波,说明时钟配对了。如果没有波形或者频率不对,回头检查PLL倍频系数和晶振频率。

嗯,这里有个小技巧。如果你没有示波器,也可以用定时器来间接验证。比如配置一个定时器,让它1秒中断一次,然后在中断里翻转一个GPIO,用逻辑分析仪看周期。虽然精度差点,但够用。

3.6 总结一下

这一章的核心就三件事:

  1. 启动文件负责初始化,我们一般不动它
  2. 系统时钟用HSE+PLL配到72MHz,注意FLASH等待周期和APB分频
  3. SysTick的配置依赖configCPU_CLOCK_HZ,一定要和实际时钟一致

时钟配好了,FreeRTOS才能有个准的节拍。下一章我们会开始写第一个任务,让LED灯闪烁起来。到时候你就知道,时钟不准的话,那个闪烁频率会多离谱。

一句话记住:系统时钟是FreeRTOS的命根子,配错了,后面全是白搭。