4. 定时器模块(TMR):基础定时器、PWM生成、输入捕获与输出比较的原理与代码实现
定时器,说白了就是单片机的“心跳”。没有它,很多时序相关的功能都玩不转。CMS32 的定时器模块功能挺全的,基础定时、PWM、输入捕获、输出比较,一个模块全搞定。我刚开始接触 CMS32 时,觉得定时器不就是计个数嘛,后来才发现,用好它,能省掉不少外部硬件。
4.1 定时器模块整体架构
CMS32 的定时器模块,内部有好几个通道。每个通道都可以独立配置。我个人习惯把定时器分成两大类:
- 基础定时器:就是单纯的计数,产生中断。用来做延时、任务调度。
- 功能定时器:带比较/捕获功能的通道。用来生成 PWM、测量脉宽、输出精确波形。
你想想看,一个芯片里塞这么多定时器通道,就是为了让你能同时处理多个时序任务。比如,一个通道跑 PWM 驱动电机,另一个通道捕获遥控器信号,互不干扰。
核心知识点:定时器的本质是计数器 + 比较器。计数器不停累加,比较器在计数值达到设定值时触发动作(中断、翻转引脚、更新占空比)。
4.2 基础定时器:延时与周期性任务
基础定时器,就是让计数器从0跑到65535(16位),溢出时产生中断。我在项目中常用它来做“非阻塞延时”。比如,按键消抖、LED闪烁,用定时器中断比用 delay() 死等靠谱多了。
4.2.1 工作原理
- 计数器从 0 开始,每个时钟脉冲加1。
- 当计数值等于 自动重装载值(ARR) 时,计数器清零,并产生更新事件(中断)。
- 中断频率 = 时钟频率 / (预分频系数 + 1) / (ARR + 1)。
我的经验:预分频系数和ARR的值,最好算成整数。我曾经为了凑一个精确的1ms中断,调了半天分频系数,最后发现直接用系统时钟的1/1000分频,ARR设成1000,简单又准确。
4.2.2 代码实现:1ms定时中断
// 假设系统时钟为 48MHz
// 目标:1ms 中断一次
// 预分频系数:48-1(即 48分频),计数器时钟 = 48MHz / 48 = 1MHz
// ARR = 1000-1,溢出时间 = 1000 * 1us = 1ms
void Timer_Init(void) {
// 1. 开启定时器时钟(以 TIM2 为例)
RCC->APB1ENR |= (1 << 0); // TIM2 时钟使能
// 2. 配置预分频器
TIM2->PSC = 48 - 1; // 预分频值
// 3. 配置自动重装载值
TIM2->ARR = 1000 - 1; // 自动重装载值
// 4. 清除更新标志位
TIM2->SR &= ~(1 << 0); // 清除 UIF 标志
// 5. 使能更新中断
TIM2->DIER |= (1 << 0); // 使能更新中断
// 6. 使能定时器
TIM2->CR1 |= (1 << 0); // 使能计数器
// 7. 配置 NVIC(中断优先级)
NVIC_EnableIRQ(TIM2_IRQn);
NVIC_SetPriority(TIM2_IRQn, 1);
}
// 中断服务函数
void TIM2_IRQHandler(void) {
if (TIM2->SR & (1 << 0)) { // 检查更新中断标志
TIM2->SR &= ~(1 << 0); // 清除标志
// 用户代码:每1ms执行一次
// 比如:按键扫描、LED翻转
}
}
注意:中断服务函数里不要做耗时操作。我曾经在中断里调了一个 printf 打印调试信息,结果导致定时器中断响应延迟,整个系统时序乱套了。中断里只做标志位设置,具体处理放到主循环里。
4.3 PWM生成:呼吸灯与电机控制
PWM,就是通过调节高低电平的占空比,来模拟模拟量输出。CMS32 的定时器做 PWM 特别方便,只需要配置好比较值,硬件自动翻转引脚。
4.3.1 原理简述
- 计数器从0累加到ARR。
- 当计数值小于 比较值(CCR) 时,输出高电平。
- 当计数值大于等于 CCR 时,输出低电平。
- 占空比 = CCR / (ARR + 1)。
说白了,ARR 决定周期,CCR 决定占空比。你想让 LED 呼吸,就不断改变 CCR 的值。
4.3.2 代码实现:PWM输出(1kHz,50%占空比)
// 配置 TIM2 通道1 输出 PWM
// 频率 = 48MHz / (48-1+1) / (1000-1+1) = 1kHz
// 占空比 = 500 / 1000 = 50%
void PWM_Init(void) {
// 1. 时钟配置(同上)
RCC->APB1ENR |= (1 << 0);
// 2. 配置 GPIO 为复用功能(PA0 为 TIM2_CH1)
GPIOA->MODER &= ~(3 << 0); // 清除模式
GPIOA->MODER |= (2 << 0); // 复用功能模式
GPIOA->AFR[0] |= (1 << 0); // AF1 对应 TIM2
// 3. 配置定时器
TIM2->PSC = 48 - 1; // 预分频
TIM2->ARR = 1000 - 1; // 周期
// 4. 配置输出比较模式
TIM2->CCMR1 |= (6 << 4); // 模式1:PWM模式1(向上计数时,CNT<CCR时有效)
TIM2->CCMR1 |= (1 << 3); // 预装载使能
// 5. 设置比较值(占空比)
TIM2->CCR1 = 500; // 50% 占空比
// 6. 使能输出
TIM2->CCER |= (1 << 0); // 使能通道1输出
// 7. 使能定时器
TIM2->CR1 |= (1 << 0);
}
// 动态修改占空比
void PWM_SetDuty(uint16_t duty) {
TIM2->CCR1 = duty; // 直接修改比较值
}
避坑指南:修改 CCR 时,如果定时器正在运行,建议先使能预装载功能(CCMRx 的 OCxPE 位),这样 CCR 会在下一个周期开始时才生效,避免产生不完整的脉冲。我曾经没开预装载,结果在修改占空比时,输出了一个极窄的毛刺,把后级的 MOS 管驱动给烧了。
4.4 输入捕获:测量脉宽与频率
输入捕获,就是测量外部信号的脉宽或频率。比如,测量遥控器信号的脉宽,或者测速编码器的脉冲频率。
4.4.1 工作原理
- 外部信号从定时器的捕获引脚输入。
- 当检测到指定边沿(上升沿或下降沿)时,硬件自动将当前计数器的值锁存到捕获寄存器。
- 通过两次捕获值的差值,计算出脉宽或周期。
4.4.2 代码实现:测量高电平脉宽
// 配置 TIM2 通道1 输入捕获
// 测量 PA0 引脚上的高电平脉宽
volatile uint16_t capture_value_1 = 0;
volatile uint16_t capture_value_2 = 0;
volatile uint8_t capture_done = 0;
void Capture_Init(void) {
// 1. 时钟配置
RCC->APB1ENR |= (1 << 0);
// 2. GPIO 配置(输入模式)
GPIOA->MODER &= ~(3 << 0); // 输入模式
// 3. 定时器基础配置
TIM2->PSC = 48 - 1; // 1MHz 计数频率(1us 分辨率)
TIM2->ARR = 0xFFFF; // 最大计数值
// 4. 配置输入捕获
TIM2->CCMR1 |= (1 << 0); // 通道1 映射到 TI1
TIM2->CCMR1 |= (0 << 2); // 每次捕获都更新
// 5. 配置捕获边沿:先捕获上升沿
TIM2->CCER |= (1 << 0); // 上升沿捕获
// 6. 使能捕获中断
TIM2->DIER |= (1 << 1); // 使能 CC1 中断
// 7. 使能定时器
TIM2->CR1 |= (1 << 0);
NVIC_EnableIRQ(TIM2_IRQn);
}
void TIM2_IRQHandler(void) {
if (TIM2->SR & (1 << 1)) { // CC1 中断
TIM2->SR &= ~(1 << 1);
if (!capture_done) {
// 第一次捕获(上升沿)
capture_value_1 = TIM2->CCR1;
// 切换为下降沿捕获
TIM2->CCER |= (1 << 1); // 设置 CC1P 为下降沿
capture_done = 1;
} else {
// 第二次捕获(下降沿)
capture_value_2 = TIM2->CCR1;
// 计算脉宽(单位:us)
uint16_t pulse_width;
if (capture_value_2 >= capture_value_1) {
pulse_width = capture_value_2 - capture_value_1;
} else {
// 处理溢出情况
pulse_width = (0xFFFF - capture_value_1) + capture_value_2 + 1;
}
// 用户代码:使用 pulse_width 值
// 切换回上升沿捕获,准备下一次测量
TIM2->CCER &= ~(1 << 1);
capture_done = 0;
}
}
}
我的经验:输入捕获时,一定要考虑计数器溢出的情况。如果被测信号周期很长,可能两次捕获之间计数器已经溢出好几次了。我一般会在中断里再加一个溢出计数器,或者把预分频调大,降低计数频率,确保不会溢出。
4.5 输出比较:精确波形控制
输出比较,和 PWM 有点像,但更灵活。它可以在计数值达到比较值时,执行多种动作:翻转引脚、输出高电平、输出低电平、触发中断。
4.5.1 应用场景
- 产生单脉冲:比如,触发一个外部设备,只输出一个特定宽度的脉冲。
- 产生任意波形:通过多个比较通道,组合出复杂的时序。
- 软件定时:在指定时间点触发中断,执行特定任务。
4.5.2 代码实现:单脉冲输出
// 配置 TIM2 通道1 输出比较
// 在计数到 500 时,翻转 PA0 引脚
void OC_Init(void) {
// 1. 时钟配置
RCC->APB1ENR |= (1 << 0);
// 2. GPIO 配置(复用功能)
GPIOA->MODER &= ~(3 << 0);
GPIOA->MODER |= (2 << 0);
GPIOA->AFR[0] |= (1 << 0);
// 3. 定时器基础配置
TIM2->PSC = 48 - 1; // 1MHz
TIM2->ARR = 1000 - 1; // 1ms 周期
// 4. 配置输出比较模式
TIM2->CCMR1 |= (3 << 4); // 模式3:翻转(Toggle)
TIM2->CCMR1 |= (1 << 3); // 预装载使能
// 5. 设置比较值
TIM2->CCR1 = 500; // 在 500us 时翻转
// 6. 使能输出
TIM2->CCER |= (1 << 0);
// 7. 使能定时器
TIM2->CR1 |= (1 << 0);
}
4.6 四种模式对比
| 模式 | 核心功能 | 典型应用 | 关键寄存器 |
|---|---|---|---|
| 基础定时 | 计数溢出产生中断 | 延时、任务调度 | PSC, ARR, SR |
| PWM生成 | 自动输出可调占空比方波 | LED调光、电机调速 | PSC, ARR, CCR, CCMR |
| 输入捕获 | 捕获外部信号边沿时的计数值 | 脉宽测量、频率测量 | CCR, CCER, CCMR |
| 输出比较 | 计数值匹配时执行指定动作 | 单脉冲、任意波形 | CCR, CCMR, CCER |
总结一下:定时器模块是 CMS32 的“瑞士军刀”。基础定时是基本功,PWM 是输出利器,输入捕获是测量神器,输出比较是波形艺术家。把这四种模式吃透,大部分时序问题都能搞定。