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),所有外设时钟默认关闭。你需要手动开启并配置。

CMS32 时钟树结构 HSE 4~16MHz HSI 8MHz LSE 32.768kHz LSI 40kHz PLL (x4~x16) AHB 分频 APB 分频 系统时钟 SYSCLK HCLK (AHB总线) PCLK (APB总线) LSE/LSI 低速时钟 CPU DMA UART RTC 说明: • 上电默认:HSI 8MHz → 系统时钟,AHB/APB不分频 • 切换时钟前,必须先确认目标时钟已稳定(等待就绪标志) • PLL输出频率 = 输入频率 × 倍频系数,不能超过芯片最大规格

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位。但有个坑——切换过程中,如果目标时钟还没稳定,系统会直接挂掉。

我总结了一套安全切换流程:

  1. 开启目标时钟(比如HSE或PLL)
  2. 等待就绪标志(HSERDY/PLLRDY)
  3. 配置分频系数(AHB、APB预分频)
  4. 写入SW位,发起切换
  5. 读取SWS位,确认切换完成
  6. 关闭旧时钟(可选,省电)
/* 安全切换系统时钟到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,终于可以点灯了。