第4章 时钟系统:CMS32的“心跳”你调对了吗?
时钟,是单片机的“心跳”。没有它,CPU、外设、定时器全都停摆。CMS32的时钟树不算复杂,但坑不少。我见过不少新手,程序下载进去跑不起来,查半天发现是时钟配置错了。
这一章,咱们就把时钟树掰开揉碎,从振荡器到PLL,从分频到输出监测,全过一遍。
4.1 时钟树架构概览
CMS32的时钟源有四个:内部高速振荡器(HSI)、内部低速振荡器(LSI)、外部高速晶振(HSE)、外部低速晶振(LSE)。它们通过一个多路选择器,最终供给系统时钟(SYSCLK)、外设总线时钟(HCLK、PCLK)和实时时钟(RTC)。
下面这张图,是我画的时钟树结构,你看一眼就明白了。
从图里你能看到,四个时钟源先经过MUX选择,然后可以走PLL倍频,再经过分频器,最后输出SYSCLK。外设时钟HCLK和PCLK,是从SYSCLK分出来的。
4.2 内部振荡器:HSI与LSI
内部振荡器,说白了就是芯片内部自带的RC振荡电路。上电默认就是HSI,频率8MHz,精度±2%左右。LSI是32kHz,专给看门狗和RTC用。
HSI的特点:
- 上电即用,无需等待起振
- 精度一般,温度变化会漂移
- 适合对时序要求不高的场景
LSI的特点:
- 超低功耗,电流只有微安级
- 频率不准,典型值32kHz,实际可能差到±10%
- 只能用于独立看门狗(IWDG)和RTC校准
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,那系统会卡死在那。所以一定要等时钟就绪标志位拉高。
4.4 PLL锁相环配置
PLL的作用,就是把低频时钟倍频上去。CMS32的PLL输入范围是4~16MHz,输出最高能到72MHz。倍频系数从4到16,步进是2。
PLL配置步骤:
- 选择PLL输入源(HSI或HSE)
- 设置倍频系数(PLLMUL)
- 等待PLL锁定(PLLRDY标志位)
- 切换系统时钟到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)。
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配置步骤:
- 选择MCO输出源(HSI、LSI、HSE、PLL/2、SYSCLK)
- 设置分频系数(1~5)
- 配置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,就能看到时钟波形。如果波形不对,说明时钟配置有问题。
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保底。