3、系统时钟配置:STM32时钟树、HAL库时钟配置、SysTick定时器详解

说到嵌入式系统,时钟就是心脏。没有时钟,芯片就是一坨硅。我刚开始用STM32那会儿,总觉得时钟配置是个麻烦事——不就是给个晶振让它跑吗?后来踩了坑才明白,时钟树这玩意儿,搞不好能让你的系统直接罢工。

今天咱们就好好聊聊STM32的时钟系统。我会把时钟树、HAL库配置、还有那个无处不在的SysTick定时器,掰开了揉碎了讲清楚。

3.1 STM32时钟树:别被它吓到

第一次看STM32时钟树图,我差点劝退。密密麻麻的线,各种分频器、倍频器、选择器……但说白了,它就是个「水源分配系统」。

时钟源就是水源,PLL就是增压泵,分频器就是阀门。你要给CPU、外设、总线分别供水,压力还不能太大也不能太小。

STM32常用的时钟源有这几个:

  • HSI(高速内部振荡器):8MHz,上电就用,精度一般。我习惯在调试阶段先用它,省事。
  • HSE(高速外部振荡器):4-16MHz,精度高。量产项目我建议用这个,稳定。
  • LSI(低速内部振荡器):32KHz,给RTC和独立看门狗用。
  • LSE(低速外部振荡器):32.768KHz,RTC专用,走时准。
  • PLL(锁相环):倍频用的,能把8MHz变成72MHz甚至更高。

核心要点:系统时钟(SYSCLK)最高能跑到多少,取决于你的芯片型号。F103系列最高72MHz,F4系列能到168MHz甚至180MHz。别贪心,跑太高容易翻车。

时钟树里还有几个关键总线:

  • AHB总线:给CPU、内存、DMA用的,跑得最快。
  • APB1总线:低速外设,比如USART、I2C、SPI。我遇到过APB1时钟配太高,USART乱码的情况。
  • APB2总线:高速外设,比如GPIO、ADC、定时器。

嗯,这里要注意:APB1的时钟不能超过36MHz(F103),APB2可以到72MHz。这是硬性规定,超了外设就不干活了。

3.2 HAL库时钟配置:HAL_RCC_Config

HAL库把时钟配置封装成了几个函数。我个人习惯用CubeMX生成初始化代码,但有时候需要手动调,那就得懂底层。

典型的时钟配置流程是这样的:

  1. 复位RCC寄存器,回到默认状态。
  2. 使能HSE或HSI,等待稳定。
  3. 配置PLL的倍频系数和分频系数。
  4. 使能PLL,等待锁定。
  5. 配置AHB、APB1、APB2的分频器。
  6. 选择系统时钟源(PLL/HSE/HSI)。
  7. 更新系统时钟变量(SystemCoreClock)。

来看看代码,我习惯这么写:

void SystemClock_Config(void)
{
    RCC_OscInitTypeDef RCC_OscInitStruct = {0};
    RCC_ClkInitTypeDef RCC_ClkInitStruct = {0};

    // 第一步:配置HSE和PLL
    RCC_OscInitStruct.OscillatorType = RCC_OSCILLATORTYPE_HSE;
    RCC_OscInitStruct.HSEState = RCC_HSE_ON;
    RCC_OscInitStruct.PLL.PLLState = RCC_PLL_ON;
    RCC_OscInitStruct.PLL.PLLSource = RCC_PLLSOURCE_HSE;
    RCC_OscInitStruct.PLL.PLLMUL = RCC_PLL_MUL9;  // 8MHz * 9 = 72MHz
    if (HAL_RCC_OscConfig(&RCC_OscInitStruct) != HAL_OK)
    {
        Error_Handler();
    }

    // 第二步:配置总线分频和系统时钟源
    RCC_ClkInitStruct.ClockType = RCC_CLOCKTYPE_HCLK | RCC_CLOCKTYPE_SYSCLK
                                | RCC_CLOCKTYPE_PCLK1 | RCC_CLOCKTYPE_PCLK2;
    RCC_ClkInitStruct.SYSCLKSource = RCC_SYSCLKSOURCE_PLLCLK;
    RCC_ClkInitStruct.AHBCLKDivider = RCC_SYSCLK_DIV1;     // AHB = 72MHz
    RCC_ClkInitStruct.APB1CLKDivider = RCC_HCLK_DIV2;      // APB1 = 36MHz
    RCC_ClkInitStruct.APB2CLKDivider = RCC_HCLK_DIV1;      // APB2 = 72MHz
    if (HAL_RCC_ClockConfig(&RCC_ClkInitStruct, FLASH_LATENCY_2) != HAL_OK)
    {
        Error_Handler();
    }
}

我的小技巧:配置完时钟后,记得读一下SystemCoreClock这个全局变量。它由HAL库自动更新,但有时候会不准。我习惯在调试时打印出来确认一下。

你可能会问:为什么要给APB1分频?直接跑72MHz不行吗?

不行。APB1的硬件设计最高只支持36MHz。我曾经在项目里偷懒没分频,结果I2C通信时好时坏,查了两天才发现是时钟超了。嗯,从那以后我再也不敢马虎了。

3.3 SysTick定时器:RTOS的心跳

SysTick是Cortex-M内核自带的24位递减计数器。说白了,它就是个简单的定时器,但地位极高——它是FreeRTOS、uC/OS这些RTOS的节拍来源。

SysTick的工作原理:

  • 从你设定的值开始递减。
  • 减到0时,触发一次中断。
  • 自动重装载,继续下一轮。

HAL库已经帮我们初始化好了SysTick,默认1ms中断一次。代码在HAL_Init()里就调用了:

__weak HAL_StatusTypeDef HAL_InitTick(uint32_t TickPriority)
{
    // 配置SysTick为1ms中断
    if (HAL_SYSTICK_Config(SystemCoreClock / 1000) != HAL_OK)
    {
        return HAL_ERROR;
    }
    // 设置中断优先级
    HAL_NVIC_SetPriority(SysTick_IRQn, TickPriority, 0);
    return HAL_OK;
}

你看,SystemCoreClock / 1000就是重装载值。如果系统时钟是72MHz,那重装载值就是72000。72MHz的时钟,每72000个周期就是1ms。

注意:SysTick的中断优先级默认是15(最低)。在RTOS里,这个优先级不能随意改。FreeRTOS要求SysTick的优先级必须是最低的,否则会导致任务调度异常。我见过有人把SysTick优先级设高了,结果系统直接死机。

SysTick还有一个妙用——做软件延时。HAL库的HAL_Delay()就是基于SysTick实现的:

__weak void HAL_Delay(uint32_t Delay)
{
    uint32_t tickstart = HAL_GetTick();
    while ((HAL_GetTick() - tickstart) < Delay)
    {
        // 空转等待
    }
}

但说实话,这种延时很粗糙。如果你在中断里调用HAL_Delay(),系统直接卡死。为什么?因为SysTick中断优先级比你的外设中断低,你永远等不到tick更新。

我曾经在项目里犯过这个错——在UART中断里调了HAL_Delay(),结果串口收一个字节就卡住。查了半天才发现是优先级的问题。从那以后,我只要看到HAL_Delay()出现在中断里,心里就咯噔一下。

3.4 实战经验:时钟配置的坑

说了这么多,我总结几个实际项目中容易踩的坑:

  • HSE起振失败:晶振匹配电容不对,或者PCB走线太长。我建议用示波器量一下晶振引脚,看波形对不对。
  • PLL锁定超时:倍频系数设太高,或者输入频率不对。F103的PLL输入范围是4-16MHz,别超了。
  • Flash等待周期:系统时钟超过24MHz就要设等待周期。72MHz需要2个等待周期,设少了程序直接跑飞。
  • SysTick中断冲突:RTOS里别乱改SysTick优先级,否则调度器罢工。

一句话总结:时钟配置是嵌入式系统的地基。地基没打好,上面盖的楼再漂亮也得塌。花10分钟把时钟配好,能省你后面10小时的调试时间。

下一章咱们聊聊GPIO的编程。GPIO看似简单,但里面的门道也不少——什么推挽输出、开漏输出、上拉下拉、复用功能……到时候我把我踩过的坑也一并告诉你。