3. 系统时钟与定时器:时钟树详解与实战

时钟,是微控制器的「心跳」。没有时钟,芯片就是一块死硅。CMS32 的时钟树不算复杂,但坑不少。我刚开始用的时候,就因为在时钟配置上偷懒,导致串口波特率怎么都对不上。嗯,今天咱们把这部分彻底理清楚。

3.1 时钟树概览

CMS32 的时钟源主要有三个:HSI(高速内部振荡器)、HSE(高速外部晶振)和 PLL(锁相环倍频)。它们的关系,我画了张图,你一看就明白。

CMS32 时钟树结构图 HSI 8 MHz HSE 4~16 MHz PLL 最高 72 MHz SYSCLK 系统主时钟 APB 总线时钟 PCLK / 定时器 💡 说明: HSI 内部集成,上电即用,但精度一般(±2%)。 HSE 需要外部晶振,精度高(±50ppm),适合通信场景。 PLL 可对 HSI 或 HSE 倍频,最高输出 72 MHz。

3.2 HSI、HSE 与 PLL 配置

说白了,选哪个时钟源,取决于你的应用场景。如果只是跑个流水灯,HSI 够用。但要做串口通信,我建议你老老实实用 HSE 加 PLL。

3.2.1 HSI 内部振荡器

HSI 上电默认开启,频率 8 MHz。优点是快——芯片一通电就能跑。缺点是温度漂移大。我在做一款工业传感器时,环境温度从 25°C 升到 85°C,HSI 频率漂了将近 3%,串口直接乱码。从那以后,只要涉及通信,我必用 HSE。

3.2.2 HSE 外部晶振

HSE 需要接 4~16 MHz 的晶振。我个人习惯用 8 MHz,因为这样 PLL 倍频到 72 MHz 刚好是整数倍(×9)。配置时要注意:晶振的两个引脚要加 20pF 左右的负载电容,别省。

⚠️ 我曾经踩过的坑: HSE 起振需要时间,大约几毫秒。如果你在代码里配置完时钟立刻就去操作外设,很可能因为时钟还没稳定而出错。记得加超时检测。

3.2.3 PLL 锁相环倍频

PLL 可以把 HSI 或 HSE 倍频到更高频率。CMS32 最高支持 72 MHz。配置时注意分频系数,别超了。我见过有人把 PLL 配到 80 MHz,芯片直接不工作——嗯,超频不是这么玩的。

// 时钟配置示例:HSE 8 MHz → PLL → 72 MHz
void SystemClock_Config(void)
{
    // 1. 使能 HSE
    RCC->CR |= RCC_CR_HSEON;
    while(!(RCC->CR & RCC_CR_HSERDY));  // 等待稳定

    // 2. 配置 PLL:8 MHz × 9 = 72 MHz
    RCC->CFGR |= RCC_CFGR_PLLSRC_HSE;   // PLL 源选 HSE
    RCC->CFGR |= RCC_CFGR_PLLMUL_9;     // 9 倍频

    // 3. 使能 PLL
    RCC->CR |= RCC_CR_PLLON;
    while(!(RCC->CR & RCC_CR_PLLRDY));

    // 4. 切换系统时钟为 PLL
    RCC->CFGR |= RCC_CFGR_SW_PLL;
    while((RCC->CFGR & RCC_CFGR_SWS) != RCC_CFGR_SWS_PLL);
}

3.3 SysTick 滴答定时器

SysTick 是 Cortex-M 内核自带的 24 位递减计数器。它不占用外设定时器资源,非常适合做系统心跳。说白了,它就是给你提供一个「每隔 1 毫秒干点啥」的机制。

3.3.1 SysTick 配置

配置很简单:设置重装载值,使能中断。重装载值 = 系统时钟频率 / 1000。比如 72 MHz 下,就是 72000。

// SysTick 1ms 中断配置
void SysTick_Init(void)
{
    // 重装载值 = 72000000 / 1000 = 72000
    SysTick->LOAD = 72000 - 1;
    SysTick->VAL  = 0;
    SysTick->CTRL = SysTick_CTRL_CLKSOURCE_Msk |
                    SysTick_CTRL_TICKINT_Msk   |
                    SysTick_CTRL_ENABLE_Msk;
}

// 中断服务函数
void SysTick_Handler(void)
{
    static uint32_t ms_counter = 0;
    ms_counter++;
    // 这里可以放你的 1ms 周期性任务
}
💡 我的习惯: 用 SysTick 做软件定时器的基础时钟。比如你要一个 100ms 的定时,就在 SysTick 中断里计数 100 次。这样省外设定时器,留给 PWM 和输入捕获用。

3.4 通用定时器 PWM 输出

通用定时器(TIM2~TIM5)可以输出 PWM 波形。PWM 有两个关键参数:频率和占空比。频率由定时器的时钟和自动重装载值决定,占空比由比较值决定。

3.4.1 PWM 配置步骤

  1. 使能定时器时钟
  2. 配置 GPIO 为复用推挽输出
  3. 设置定时器的 PSC(预分频器)和 ARR(自动重装载值)
  4. 设置 CCR(捕获比较寄存器)决定占空比
  5. 配置输出比较模式为 PWM1 或 PWM2
  6. 使能定时器
// PWM 输出配置:1 kHz,50% 占空比
void PWM_Init(void)
{
    // 假设 TIM2 时钟为 72 MHz
    TIM2->PSC = 72 - 1;      // 预分频:72 MHz / 72 = 1 MHz
    TIM2->ARR = 1000 - 1;    // 自动重装载:1 MHz / 1000 = 1 kHz
    TIM2->CCR1 = 500;        // 比较值:50% 占空比

    TIM2->CCMR1 |= TIM_CCMR1_OC1M_1 | TIM_CCMR1_OC1M_2;  // PWM1 模式
    TIM2->CCER  |= TIM_CCER_CC1E;                         // 使能输出

    TIM2->CR1   |= TIM_CR1_CEN;                           // 启动定时器
}
🔑 关键点: PWM 频率 = 定时器时钟 / (PSC + 1) / (ARR + 1)。占空比 = CCR / (ARR + 1) × 100%。调整 CCR 就能平滑改变占空比,比如控制 LED 亮度或电机速度。

3.5 输入捕获测量脉宽

输入捕获可以测量外部信号的脉宽或频率。原理很简单:定时器捕获到上升沿时记录当前计数值,捕获到下降沿时再记录一次。两次的差值就是高电平脉宽。

3.5.1 配置与实现

我做过一个遥控器解码的项目,就是用输入捕获测量每个脉冲的宽度。配置时注意:捕获通道的 GPIO 要配置为输入,且开启滤波,防止毛刺干扰。

// 输入捕获配置:测量高电平脉宽
void InputCapture_Init(void)
{
    TIM3->PSC = 72 - 1;      // 1 MHz 计数频率,每个 tick = 1 us
    TIM3->ARR = 0xFFFF;      // 最大计数值

    // 配置通道 1 为输入捕获,上升沿触发
    TIM3->CCMR1 |= TIM_CCMR1_CC1S_0;          // 通道 1 映射到 TI1
    TIM3->CCER  |= TIM_CCER_CC1P;             // 上升沿捕获
    TIM3->CCER  |= TIM_CCER_CC1E;             // 使能捕获

    TIM3->CR1   |= TIM_CR1_CEN;               // 启动定时器
}

// 中断中读取捕获值
void TIM3_IRQHandler(void)
{
    if(TIM3->SR & TIM_SR_CC1IF)
    {
        uint16_t capture_val = TIM3->CCR1;     // 读取捕获值
        // 计算脉宽 = capture_val × 1 us
        TIM3->SR &= ~TIM_SR_CC1IF;
    }
}
⚠️ 注意: 输入捕获容易受噪声干扰。我建议在 GPIO 配置时开启施密特触发器和数字滤波。CMS32 的定时器有输入滤波寄存器(ICF),可以设置采样频率和次数,能有效滤除短脉冲干扰。

3.6 定时器参数速查表

定时器类型 位数 主要功能 典型应用
SysTick 24 位 系统滴答中断 操作系统心跳、软件定时
TIM2~TIM5 16 位 PWM、输入捕获、输出比较 电机控制、信号测量
TIM6/TIM7 16 位 基本定时器 定时中断、DMA 触发

好了,时钟和定时器这部分就聊到这儿。记住一句话:时钟是基础,定时器是工具。基础打牢了,后面做串口 Bootloader 才稳当。