第4章 时钟系统:CMS32的“心跳”你调对了吗?

时钟,是单片机的“心跳”。没有它,CPU、外设、定时器全都停摆。CMS32的时钟树不算复杂,但坑不少。我见过不少新手,程序下载进去跑不起来,查半天发现是时钟配置错了。

这一章,咱们就把时钟树掰开揉碎,从振荡器到PLL,从分频到输出监测,全过一遍。

4.1 时钟树架构概览

CMS32的时钟源有四个:内部高速振荡器(HSI)、内部低速振荡器(LSI)、外部高速晶振(HSE)、外部低速晶振(LSE)。它们通过一个多路选择器,最终供给系统时钟(SYSCLK)、外设总线时钟(HCLK、PCLK)和实时时钟(RTC)。

下面这张图,是我画的时钟树结构,你看一眼就明白了。

CMS32时钟树架构图 CMS32 时钟树架构 HSI 8MHz LSI 32kHz HSE 4~16MHz LSE 32.768kHz MUX PLL x4~x16 分频器 /1~/512 SYSCLK 说明 HSI/LSI 内部集成,无需外部元件 HSE/LSE 需要外接晶振和电容

从图里你能看到,四个时钟源先经过MUX选择,然后可以走PLL倍频,再经过分频器,最后输出SYSCLK。外设时钟HCLK和PCLK,是从SYSCLK分出来的。

我的习惯: 开发阶段先用HSI,等硬件稳定了再切HSE+PLL。这样能快速排除晶振起振问题。

4.2 内部振荡器:HSI与LSI

内部振荡器,说白了就是芯片内部自带的RC振荡电路。上电默认就是HSI,频率8MHz,精度±2%左右。LSI是32kHz,专给看门狗和RTC用。

HSI的特点:

  • 上电即用,无需等待起振
  • 精度一般,温度变化会漂移
  • 适合对时序要求不高的场景

LSI的特点:

  • 超低功耗,电流只有微安级
  • 频率不准,典型值32kHz,实际可能差到±10%
  • 只能用于独立看门狗(IWDG)和RTC校准
注意: 千万别用LSI做精确延时!我曾经在一个低功耗项目里用LSI做定时唤醒,结果实际唤醒周期差了将近20%,电池续航直接少了一半。

4.3 外部晶振:HSE与LSE

外部晶振需要接两个引脚:OSC_IN和OSC_OUT,再加两个负载电容。HSE常用4MHz、8MHz、16MHz,LSE固定32.768kHz。

晶振选型要点:

参数 HSE LSE
频率范围 4~16MHz 32.768kHz
负载电容 10~22pF 6~12.5pF
起振时间 约1~5ms 约0.5~2s
精度 ±50ppm ±20ppm

你想想看,LSE起振要2秒,如果程序一上电就切到LSE,那系统会卡死在那。所以一定要等时钟就绪标志位拉高。

避坑指南: 我曾经在批量生产时,发现部分板子LSE不起振。查了半天,是PCB布局问题——晶振走线太长,还跟电源线并行走。后来把晶振紧贴MCU,周围包地,问题全解决了。

4.4 PLL锁相环配置

PLL的作用,就是把低频时钟倍频上去。CMS32的PLL输入范围是4~16MHz,输出最高能到72MHz。倍频系数从4到16,步进是2。

PLL配置步骤:

  1. 选择PLL输入源(HSI或HSE)
  2. 设置倍频系数(PLLMUL)
  3. 等待PLL锁定(PLLRDY标志位)
  4. 切换系统时钟到PLL输出

举个例子,如果HSE是8MHz,你想跑到72MHz,倍频系数就是9。但CMS32的PLLMUL只有偶数倍,所以8MHz×9=72MHz这条路走不通。怎么办?

我一般用8MHz×18=144MHz?不对,PLL输出上限是72MHz。所以得换个思路:用4MHz晶振,倍频到72MHz(4×18=72),或者用8MHz晶振,倍频到64MHz(8×8=64)。

我的建议: 别把PLL推到极限。72MHz是上限,我通常跑64MHz或48MHz,留点余量。温度一高,极限频率容易出问题。

4.5 系统时钟切换与分频

系统时钟切换,是通过时钟控制寄存器(RCC_CFGR)的SW位来选的。切换时要注意:新时钟源必须稳定就绪,否则切换会失败。

切换流程:

  • 使能目标时钟源(HSI/HSE/PLL)
  • 等待时钟就绪标志
  • 修改SW位选择新时钟
  • 等待SWS位确认切换完成

分频器在PLL后面,可以分1、2、4、8、16、64、128、256、512。HCLK和PCLK就是从分频器出来的。

总线 最大频率 典型分频
SYSCLK 72MHz 不分频
HCLK(AHB) 72MHz /1
PCLK(APB1) 36MHz /2
PCLK(APB2) 72MHz /1

嗯,这里要注意:APB1上的外设(比如USART2、I2C1)最高只能跑36MHz,所以PCLK1必须分频。我刚开始做项目时没注意这个,USART波特率怎么配都不对,查了半天才发现是时钟超了。

4.6 时钟输出监测

CMS32有个MCO(Microcontroller Clock Output)功能,可以把内部时钟引到某个GPIO脚上,用示波器看。调试时特别好用。

MCO配置步骤:

  1. 选择MCO输出源(HSI、LSI、HSE、PLL/2、SYSCLK)
  2. 设置分频系数(1~5)
  3. 配置GPIO为复用功能输出

代码示例:

// 配置MCO输出PLL/2时钟
RCC->CFGR |= RCC_CFGR_MCO_SYSCLK;  // 选择SYSCLK作为MCO源
RCC->CFGR |= RCC_CFGR_MCO_PRE_1;   // 不分频

// 配置PA8为MCO输出
GPIOA->MODER |= GPIO_MODER_MODE8_1; // 复用功能
GPIOA->AFR[1] |= 0x0000000F;        // AF0

用示波器量PA8,就能看到时钟波形。如果波形不对,说明时钟配置有问题。

注意: MCO输出频率不要超过50MHz,否则波形会失真。我试过输出72MHz,示波器上看全是毛刺,根本没法用。

4.7 实战:配置系统时钟到64MHz

咱们来写一个完整的时钟初始化函数。目标:HSE 8MHz + PLL ×8 = 64MHz,HCLK不分频,PCLK1二分频。

void SystemClock_Config(void)
{
    // 1. 使能HSE
    RCC->CR |= RCC_CR_HSEON;
    while(!(RCC->CR & RCC_CR_HSERDY)); // 等待就绪
    
    // 2. 配置PLL:HSE作为输入,倍频系数8
    RCC->CFGR |= RCC_CFGR_PLLSRC_HSE; // PLL源选HSE
    RCC->CFGR |= RCC_CFGR_PLLMUL_8;   // 8MHz × 8 = 64MHz
    
    // 3. 使能PLL
    RCC->CR |= RCC_CR_PLLON;
    while(!(RCC->CR & RCC_CR_PLLRDY)); // 等待锁定
    
    // 4. 配置分频
    RCC->CFGR |= RCC_CFGR_HPRE_DIV1;  // HCLK = SYSCLK / 1
    RCC->CFGR |= RCC_CFGR_PPRE1_DIV2; // PCLK1 = HCLK / 2
    RCC->CFGR |= RCC_CFGR_PPRE2_DIV1; // PCLK2 = HCLK / 1
    
    // 5. 切换系统时钟到PLL
    RCC->CFGR |= RCC_CFGR_SW_PLL;
    while((RCC->CFGR & RCC_CFGR_SWS) != RCC_CFGR_SWS_PLL);
    
    // 6. 更新系统时钟变量(用于HAL库延时)
    SystemCoreClock = 64000000;
}

这段代码我用了好几年,基本没出过问题。唯一要注意的是,如果HSE起振失败,程序会卡在while循环里。所以量产时最好加个超时处理,切回HSI保底。

总结一下: 时钟配置的核心就三步——选源、倍频、分频。MCO是调试利器,出了问题先量它。记住,时钟不稳,万事皆休。