2、时钟配置实战:以STM32为例,配置HSE、HSI、PLL输出72MHz系统时钟

时钟,是单片机的“心跳”。

我见过不少新手,代码逻辑写得飞起,结果板子一跑就死机。查了半天,最后发现是时钟没配好。说白了,时钟树要是乱了,整个系统就别想正常工作。

这一章,咱们就拿STM32F103来开刀。手把手教你配出72MHz的系统时钟。HSE、HSI、PLL,一个一个来。

2.1 先搞清楚:你要用哪个时钟源?

STM32的时钟源,说白了就四种。但咱们实战最常用的,就是HSE和HSI。

时钟源 频率 精度 特点
HSI (内部高速) 8 MHz ±1% 上电就用,不用外部晶振。但温漂大,精度一般。
HSE (外部高速) 4~16 MHz (常用8M) ±50 ppm 精度高,稳定。我一般都用它做PLL的源头。
LSI (内部低速) 40 kHz 一般 给独立看门狗、RTC用。
LSE (外部低速) 32.768 kHz 高 给RTC用,走时准。
我的习惯: 只要板子上有8M晶振,我肯定用HSE。HSI我一般只用在快速验证或者低成本的简易板子上。你想想看,如果产品要过温漂测试,用HSI心里真没底。

2.2 配置流程:三步走,稳得很

配置72MHz,其实就三步。别被手册里那一大堆寄存器吓到。

  1. 打开HSE,等它稳定。 这是源头,源不稳,后面全白搭。
  2. 配置PLL,选好分频和倍频。 这是核心,算错了就出不了72M。
  3. 切换系统时钟,验证结果。 最后一步,确认跑起来了。

嗯,这里要注意:每一步都要检查标志位。别一股脑往下写,硬件没准备好,你写了也白写。

2.3 代码实战:从寄存器到库函数

我早期做项目,都是直接操作寄存器。现在库函数封装得好了,但底层逻辑你得懂。不然出了问题,你连debug都不知道从哪下手。

2.3.1 直接操作寄存器版(硬核)

这段代码,我建议你收藏。它能帮你理解时钟树的本质。

// 假设外部晶振是8MHz
// 目标:SYSCLK = 72MHz, HCLK = 72MHz, PCLK1 = 36MHz, PCLK2 = 72MHz

void HSE_SetSysClk(uint32_t RCC_PLLMul_x) // RCC_PLLMul_x 是倍频系数
{
    __IO uint32_t HSEStartUpStatus = 0;

    // 1. 复位RCC配置
    RCC->CR |= (uint32_t)0x00000001; // 开启HSI
    while(!(RCC->CR & RCC_CR_HSIRDY)); // 等HSI稳定
    RCC->CFGR = 0x00000000;           // 清空配置
    RCC->CR = 0x00000089;             // 关闭HSE、PLL、CSS

    // 2. 打开HSE
    RCC->CR |= RCC_CR_HSEON;
    HSEStartUpStatus = RCC->CR & RCC_CR_HSERDY;

    if(HSEStartUpStatus != RCC_CR_HSERDY)
    {
        // 这里可以加HSE启动失败的异常处理
        // 我曾经遇到过晶振虚焊,卡在这里半天
        return;
    }

    // 3. 配置AHB、APB1、APB2预分频
    // HCLK = SYSCLK / 1 = 72MHz
    // PCLK2 = HCLK / 1 = 72MHz
    // PCLK1 = HCLK / 2 = 36MHz (APB1最高36MHz,别超了)
    RCC->CFGR |= RCC_CFGR_HPRE_DIV1;  // AHB不分频
    RCC->CFGR |= RCC_CFGR_PPRE2_DIV1; // APB2不分频
    RCC->CFGR |= RCC_CFGR_PPRE1_DIV2; // APB1二分频

    // 4. 配置PLL
    // PLLCLK = HSE / PLL_X * PLL_N
    // 这里用8M晶振,倍频到72M: 8MHz * 9 = 72MHz
    RCC->CFGR |= RCC_CFGR_PLLSRC_HSE; // PLL源选HSE
    RCC->CFGR |= RCC_PLLMul_x;        // 设置倍频系数

    // 5. 使能PLL,等它锁定
    RCC->CR |= RCC_CR_PLLON;
    while((RCC->CR & RCC_CR_PLLRDY) == 0);

    // 6. 切换系统时钟为PLL输出
    RCC->CFGR &= ~RCC_CFGR_SW;
    RCC->CFGR |= RCC_CFGR_SW_PLL;
    while((RCC->CFGR & RCC_CFGR_SWS) != RCC_CFGR_SWS_PLL);
}
避坑指南: 我曾经在切换时钟源后,忘了等SWS标志位。结果代码直接往下跑,外设配置全乱套。记住:切换后一定要等SWS确认!

2.3.2 标准库函数版(推荐)

现在项目里,我一般用标准库。封装得好,可读性强。但核心逻辑和上面寄存器版一模一样。

void SystemClock_Config(void)
{
    ErrorStatus HSEStartUpStatus;

    // 1. 复位RCC
    RCC_DeInit();

    // 2. 使能HSE
    RCC_HSEConfig(RCC_HSE_ON);
    HSEStartUpStatus = RCC_WaitForHSEStartUp();

    if(HSEStartUpStatus == SUCCESS)
    {
        // 3. 配置FLASH预取指
        // 72MHz下,FLASH需要2个等待周期
        FLASH_PrefetchBufferCmd(FLASH_PrefetchBuffer_Enable);
        FLASH_SetLatency(FLASH_Latency_2);

        // 4. 配置AHB、APB1、APB2
        RCC_HCLKConfig(RCC_SYSCLK_Div1);
        RCC_PCLK2Config(RCC_HCLK_Div1);
        RCC_PCLK1Config(RCC_HCLK_Div2);

        // 5. 配置PLL
        RCC_PLLConfig(RCC_PLLSource_HSE_Div1, RCC_PLLMul_9);

        // 6. 使能PLL
        RCC_PLLCmd(ENABLE);
        while(RCC_GetFlagStatus(RCC_FLAG_PLLRDY) == RESET);

        // 7. 切换系统时钟
        RCC_SYSCLKConfig(RCC_SYSCLKSource_PLLCLK);
        while(RCC_GetSYSCLKSource() != 0x08); // 0x08表示PLL作为系统时钟
    }
    else
    {
        // HSE启动失败,可以在这里做异常处理
        // 比如切换到HSI,保证系统能跑
        while(1);
    }
}
小技巧: 我习惯在HSE启动失败的分支里,自动切换到HSI并降频运行。这样板子至少还能亮灯,方便你排查问题。别一失败就死循环,那debug太痛苦了。

2.4 时钟树结构图:一图胜千言

下面这张图,是我自己画的。把STM32F1的时钟树主干画出来了。你对照着看代码,会清晰很多。

STM32F1时钟树主干配置图 STM32F1 时钟树主干配置图 (72MHz) HSE 8 MHz HSI 8 MHz PLLSRC 选择器 PLL ×9 倍频 输出 72 MHz SYSCLK 72 MHz 系统时钟 HCLK = 72 MHz PCLK1 = 36 MHz PCLK2 = 72 MHz ⚠️ 关键检查点 1. HSE是否稳定 2. PLL是否锁定

2.5 验证与调试:别信感觉,信示波器

代码写完了,怎么知道配对了没?

我见过有人用延时函数闪灯,觉得灯闪得够快就是72M。千万别这么干!

  • 最靠谱:用示波器测MCO引脚。 把PA8配置为MCO功能,输出系统时钟。一看波形,频率对不对一目了然。
  • 次靠谱:读RCC寄存器。 读RCC->CFGR的SWS位,确认当前时钟源。读RCC->CFGR的HPRE、PPRE1、PPRE2,确认分频系数。
  • 不靠谱:凭感觉。 别问我为什么知道,我曾经就吃过这个亏。
核心要点: 时钟配置,本质上就是“选源 -> 分频/倍频 -> 切换 -> 验证”这四个动作。不管你是用F1、F4还是H7,底层逻辑都一样。把这个流程刻在脑子里,以后换芯片也就是换个寄存器地址的事。