4. 系统时钟与复位:时钟树结构、HSE/LSE/HSI/LSI配置、系统时钟切换实战、复位源分析
时钟,是MCU的心脏。没有时钟,芯片就是一块死硅片。CMS32的时钟树,说实话,我第一次看参考手册时也愣了一下——比8位单片机复杂不少。但别怕,拆开来看,其实就那几个关键节点。
4.1 时钟树整体结构
CMS32的时钟源有四个:
- HSE:外部高速晶振,4~16MHz,精度最高
- HSI:内部高速RC振荡器,8MHz,上电默认就用它
- LSE:外部低速晶振,32.768kHz,RTC专用
- LSI:内部低速RC振荡器,40kHz,看门狗和低功耗场景
这些时钟源经过PLL倍频、分频后,供给CPU核心、外设总线、定时器、ADC等模块。我画了一张图,帮你把整个脉络理清楚。
核心要点:系统上电后默认使用HSI(8MHz),所有外设时钟默认关闭。你需要手动开启并配置。
4.2 HSE与LSE配置
外部晶振,精度高,但起振慢。我习惯在系统初始化时先配好HSE,等它稳定后再切过去。
/* HSE配置示例:使用8MHz外部晶振 */
void HSE_Config(void)
{
/* 1. 复位时钟控制寄存器 */
RCC->CSR = 0x00000000;
/* 2. 开启HSE振荡器 */
RCC->CR |= RCC_CR_HSEON;
/* 3. 等待HSE就绪 */
while(!(RCC->CR & RCC_CR_HSERDY));
/* 4. 配置PLL:8MHz * 8 = 64MHz */
RCC->CFGR = (RCC->CFGR & ~RCC_CFGR_PLLMUL) | RCC_CFGR_PLLMUL_8;
RCC->CR |= RCC_CR_PLLON;
while(!(RCC->CR & RCC_CR_PLLRDY));
/* 5. 切换系统时钟为PLL输出 */
RCC->CFGR = (RCC->CFGR & ~RCC_CFGR_SW) | RCC_CFGR_SW_PLL;
while((RCC->CFGR & RCC_CFGR_SWS) != RCC_CFGR_SWS_PLL);
}
个人经验:LSE的起振时间比HSE长得多,有时需要1~2秒。我在一个低功耗项目中,RTC总是走不准,查了半天发现是LSE起振超时,代码直接跳过了等待循环。后来我加了超时处理,问题解决。
4.3 HSI与LSI配置
内部振荡器,上电即用,不需要外部元件。但精度一般,温度漂移也大。HSI出厂校准到8MHz ±1%,在全温度范围内可能漂到±3%。
/* HSI配置:其实默认就开了,但你可以重新校准 */
void HSI_Trim(void)
{
/* 读取出厂校准值 */
uint8_t trim_val = *(uint8_t*)0x1FFFF7F8;
/* 写入HSI校准寄存器 */
RCC->HSITRIM = trim_val;
/* 确认HSI就绪 */
if(RCC->CR & RCC_CR_HSIRDY)
{
/* HSI已稳定,可以用于系统时钟 */
}
}
注意:LSI主要用于独立看门狗(IWDG)和RTC校准。如果你要用RTC走时,建议用LSE,LSI的温漂太大,一天能差好几秒。
4.4 系统时钟切换实战
时钟切换,说白了就是改CFGR寄存器的SW位。但有个坑——切换过程中,如果目标时钟还没稳定,系统会直接挂掉。
我总结了一套安全切换流程:
- 开启目标时钟(比如HSE或PLL)
- 等待就绪标志(HSERDY/PLLRDY)
- 配置分频系数(AHB、APB预分频)
- 写入SW位,发起切换
- 读取SWS位,确认切换完成
- 关闭旧时钟(可选,省电)
/* 安全切换系统时钟到HSE */
void SysClock_Switch_To_HSE(void)
{
uint32_t timeout = 0;
/* 步骤1:开启HSE */
RCC->CR |= RCC_CR_HSEON;
/* 步骤2:等待就绪,带超时 */
timeout = 1000000;
while(!(RCC->CR & RCC_CR_HSERDY))
{
if(--timeout == 0)
{
/* HSE起振失败,回退到HSI */
return;
}
}
/* 步骤3:配置AHB不分频,APB不分频 */
RCC->CFGR &= ~(RCC_CFGR_HPRE | RCC_CFGR_PPRE);
/* 步骤4:切换系统时钟为HSE */
RCC->CFGR = (RCC->CFGR & ~RCC_CFGR_SW) | RCC_CFGR_SW_HSE;
/* 步骤5:确认切换完成 */
while((RCC->CFGR & RCC_CFGR_SWS) != RCC_CFGR_SWS_HSE);
/* 步骤6:关闭HSI(可选) */
RCC->CR &= ~RCC_CR_HSION;
}
避坑指南:我曾经在切换时钟时忘了等SWS标志,直接去配外设时钟。结果UART波特率完全不对,调试了一下午才发现是时钟还没切过去。记住——SWS是最终确认,不是SW。
4.5 复位源分析
CMS32的复位源,我数了数,一共7种:
| 复位源 | 触发条件 | 影响范围 | 典型场景 |
|---|---|---|---|
| POR(上电复位) | VDD低于POR阈值 | 全芯片 | 上电、掉电 |
| BOR(欠压复位) | VDD低于BOR阈值 | 全芯片 | 电源波动 |
| NRST引脚复位 | NRST引脚拉低 | 全芯片 | 外部按键、调试器 |
| WWDG复位 | 窗口看门狗超时 | 全芯片 | 程序跑飞检测 |
| IWDG复位 | 独立看门狗超时 | 全芯片 | 硬件看门狗 |
| 软件复位 | 写SYSRESETREQ位 | 全芯片 | OTA升级、故障恢复 |
| LPWR复位 | 低功耗模式异常 | 部分逻辑 | 唤醒失败 |
怎么判断是哪种复位?看RCC->CSR寄存器的复位标志位。上电后读一下,就能知道芯片是怎么活过来的。
/* 读取并清除复位源 */
void Check_Reset_Source(void)
{
uint32_t csr = RCC->CSR;
if(csr & RCC_CSR_PORRSTF)
{
/* 上电复位 */
}
if(csr & RCC_CSR_BORRSTF)
{
/* 欠压复位 */
}
if(csr & RCC_CSR_PINRSTF)
{
/* NRST引脚复位 */
}
if(csr & RCC_CSR_WWDGRSTF)
{
/* 窗口看门狗复位 */
}
if(csr & RCC_CSR_IWDGRSTF)
{
/* 独立看门狗复位 */
}
if(csr & RCC_CSR_SFTRSTF)
{
/* 软件复位 */
}
/* 清除所有复位标志 */
RCC->CSR |= RCC_CSR_RMVF;
}
我的习惯:在产品代码里,我会在main函数开头调用这个函数,把复位源通过串口打印出来。生产测试时,如果发现大量BOR复位,说明电源设计有问题。如果全是IWDG复位,那就要查查代码是不是有死循环了。
4.6 实战:从HSI切换到PLL(64MHz)
把前面讲的串起来,写一个完整的时钟初始化函数。这是CMS32上电后第一个要调用的函数。
/* 完整时钟初始化:HSI → PLL(64MHz) */
void SystemClock_Init(void)
{
/* 1. 配置Flash等待周期(64MHz需要2个等待周期) */
FLASH->ACR = (FLASH->ACR & ~FLASH_ACR_LATENCY) | FLASH_ACR_LATENCY_2;
/* 2. 开启HSE */
RCC->CR |= RCC_CR_HSEON;
while(!(RCC->CR & RCC_CR_HSERDY));
/* 3. 配置PLL:HSE 8MHz * 8 = 64MHz */
RCC->CFGR = (RCC->CFGR & ~RCC_CFGR_PLLMUL) | RCC_CFGR_PLLMUL_8;
RCC->CFGR |= RCC_CFGR_PLLSRC; /* PLL时钟源选择HSE */
/* 4. 开启PLL */
RCC->CR |= RCC_CR_PLLON;
while(!(RCC->CR & RCC_CR_PLLRDY));
/* 5. 配置AHB不分频,APB不分频 */
RCC->CFGR &= ~(RCC_CFGR_HPRE | RCC_CFGR_PPRE);
/* 6. 切换系统时钟到PLL */
RCC->CFGR = (RCC->CFGR & ~RCC_CFGR_SW) | RCC_CFGR_SW_PLL;
while((RCC->CFGR & RCC_CFGR_SWS) != RCC_CFGR_SWS_PLL);
/* 7. 关闭HSI(省电) */
RCC->CR &= ~RCC_CR_HSION;
/* 8. 更新系统时钟变量 */
SystemCoreClock = 64000000;
}
重要提醒:切换时钟前,一定要先配好Flash等待周期。频率越高,Flash读取速度跟不上,就会取指令错误。64MHz至少要2个等待周期。我见过有人忘了配这个,程序跑起来各种随机死机,查了三天。
嗯,时钟这块就讲这么多。记住一句话:时钟是一切外设的基础,配错了,后面全白搭。下节课我们开始玩GPIO,终于可以点灯了。