3. 系统时钟与定时器:时钟树详解与实战
时钟,是微控制器的「心跳」。没有时钟,芯片就是一块死硅。CMS32 的时钟树不算复杂,但坑不少。我刚开始用的时候,就因为在时钟配置上偷懒,导致串口波特率怎么都对不上。嗯,今天咱们把这部分彻底理清楚。
3.1 时钟树概览
CMS32 的时钟源主要有三个:HSI(高速内部振荡器)、HSE(高速外部晶振)和 PLL(锁相环倍频)。它们的关系,我画了张图,你一看就明白。
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 左右的负载电容,别省。
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 周期性任务
}
3.4 通用定时器 PWM 输出
通用定时器(TIM2~TIM5)可以输出 PWM 波形。PWM 有两个关键参数:频率和占空比。频率由定时器的时钟和自动重装载值决定,占空比由比较值决定。
3.4.1 PWM 配置步骤
- 使能定时器时钟
- 配置 GPIO 为复用推挽输出
- 设置定时器的 PSC(预分频器)和 ARR(自动重装载值)
- 设置 CCR(捕获比较寄存器)决定占空比
- 配置输出比较模式为 PWM1 或 PWM2
- 使能定时器
// 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; // 启动定时器
}
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;
}
}
3.6 定时器参数速查表
| 定时器类型 | 位数 | 主要功能 | 典型应用 |
|---|---|---|---|
| SysTick | 24 位 | 系统滴答中断 | 操作系统心跳、软件定时 |
| TIM2~TIM5 | 16 位 | PWM、输入捕获、输出比较 | 电机控制、信号测量 |
| TIM6/TIM7 | 16 位 | 基本定时器 | 定时中断、DMA 触发 |
好了,时钟和定时器这部分就聊到这儿。记住一句话:时钟是基础,定时器是工具。基础打牢了,后面做串口 Bootloader 才稳当。