4. 定时器:系统滴答定时器SysTick、通用定时器TIM、PWM输出、输入捕获、定时器中断
定时器这东西,在嵌入式开发里就像呼吸一样自然。你几乎找不到一个项目不用它的。做点灯延时要用,做通信协议要用,做电机控制更要用。CH32系列基于RISC-V内核,它的定时器模块设计得挺灵活,但坑也不少。这一章,我把SysTick、通用定时器、PWM、输入捕获和中断串起来讲,都是实战里高频使用的功能。
4.1 SysTick:系统的心跳
SysTick,说白了就是内核自带的一个24位递减计数器。它挂在系统时钟上,每来一个时钟脉冲就减1,减到0就触发中断,然后自动重装。我习惯拿它做操作系统的心跳,或者简单的延时函数。
在CH32上配置SysTick很简单。核心寄存器就三个:
| 寄存器 | 功能 | 关键位 |
|---|---|---|
| CSR (0xE000E010) | 控制与状态 | ENABLE(0)、TICKINT(1)、COUNTFLAG(16) |
| RVR (0xE000E014) | 重装载值 | 24位有效,写入后自动加载 |
| CVR (0xE000E018) | 当前计数值 | 读返回当前值,写清零 |
举个例子,系统时钟144MHz,想要1ms中断一次:
// SysTick 1ms 中断配置
SysTick->RVR = 144000 - 1; // 144MHz / 1000 = 144000
SysTick->CVR = 0; // 清空计数器
SysTick->CSR = 0x07; // 使能 + 中断使能 + 使用系统时钟
4.2 通用定时器TIM:多才多艺的计时员
CH32的通用定时器比SysTick灵活得多。它有16位自动重装载计数器,支持向上、向下、中央对齐三种计数模式。每个定时器还有4个独立通道,可以配置成PWM输出、输入捕获或者比较输出。
我个人觉得,通用定时器最常用的场景就是产生周期性的中断。比如控制一个LED以500ms的频率闪烁:
// 使用TIM2,1秒中断一次
RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM2, ENABLE);
TIM_TimeBaseInitTypeDef tim;
tim.TIM_Prescaler = 144 - 1; // 144MHz / 144 = 1MHz
tim.TIM_CounterMode = TIM_CounterMode_Up;
tim.TIM_Period = 1000000 - 1; // 1MHz / 1000000 = 1Hz
tim.TIM_ClockDivision = 0;
TIM_TimeBaseInit(TIM2, &tim);
TIM_ITConfig(TIM2, TIM_IT_Update, ENABLE);
TIM_Cmd(TIM2, ENABLE);
这里有个细节要注意:预分频器是计数器+1的关系。你写144-1,实际分频系数是144。我刚开始做的时候经常在这里算错,导致定时时间翻倍或者减半。
4.3 PWM输出:让LED呼吸起来
PWM,脉冲宽度调制。说白了就是通过调节占空比来控制输出功率。在CH32上,通用定时器的每个通道都可以独立输出PWM。
配置PWM的步骤其实就四步:
- 设置定时器的时基(频率)
- 配置通道为PWM模式
- 设置占空比(比较值)
- 使能输出和定时器
看个呼吸灯的例子,频率1kHz,占空比从0到100%渐变:
// TIM2_CH1 PWM输出,PA0引脚
TIM_OCInitTypeDef oc;
oc.TIM_OCMode = TIM_OCMode_PWM1;
oc.TIM_OutputState = TIM_OutputState_Enable;
oc.TIM_Pulse = 0; // 初始占空比0
oc.TIM_OCPolarity = TIM_OCPolarity_High;
TIM_OC1Init(TIM2, &oc);
// 主循环中渐变
for(uint16_t duty = 0; duty < 1000; duty++) {
TIM_SetCompare1(TIM2, duty);
Delay_Ms(2);
}
4.4 输入捕获:测量外部信号的脉搏
输入捕获,就是测量外部信号的频率或脉宽。原理很简单:当检测到指定的边沿(上升沿或下降沿)时,定时器会把当前的计数值锁存到捕获寄存器里。两次捕获的差值,就是信号的周期或脉宽。
配置输入捕获的要点:
- 选择捕获通道和对应的GPIO
- 设置触发边沿(上升沿、下降沿或双边沿)
- 使能捕获中断(如果需要)
- 读取捕获值并计算
测量一个PWM信号的频率,代码骨架:
// TIM2_CH1 输入捕获配置
TIM_ICInitTypeDef ic;
ic.TIM_Channel = TIM_Channel_1;
ic.TIM_ICPolarity = TIM_ICPolarity_Rising;
ic.TIM_ICSelection = TIM_ICSelection_DirectTI;
ic.TIM_ICPrescaler = TIM_ICPSC_DIV1;
ic.TIM_ICFilter = 0;
TIM_ICInit(TIM2, &ic);
// 中断中读取捕获值
void TIM2_IRQHandler(void) {
if(TIM_GetITStatus(TIM2, TIM_IT_CC1)) {
uint16_t cap = TIM_GetCapture1(TIM2);
// 计算频率:定时器时钟 / (两次捕获差值)
TIM_ClearITPendingBit(TIM2, TIM_IT_CC1);
}
}
4.5 定时器中断:让CPU从轮询中解放
定时器中断,是定时器最核心的能力。没有中断,CPU就得一直查标志位,啥也干不了。有了中断,定时器自己跑,到了时间通知CPU,CPU处理完继续干别的活。
CH32的定时器中断类型主要有:
| 中断类型 | 触发条件 | 典型应用 |
|---|---|---|
| 更新中断 | 计数器溢出或软件更新 | 周期性任务、系统时基 |
| 捕获/比较中断 | 输入捕获事件或比较匹配 | PWM占空比调整、信号测量 |
| 触发中断 | 外部触发事件 | 同步启动多个定时器 |
中断服务函数里,记得做三件事:
- 读取中断状态寄存器,确认中断源
- 执行用户逻辑(比如翻转LED、更新数据)
- 清除中断标志位
我见过不少新手在中断里做复杂运算,导致中断响应时间过长。记住,中断服务函数要短小精悍。复杂计算放到主循环里做,中断只负责设标志位或搬数据。
好了,定时器的核心内容就这些。从SysTick到通用定时器,从PWM到输入捕获,再到中断机制,这些东西在物联网项目里几乎天天用。你动手试试,把每个例程跑一遍,遇到问题再回头看这一章,理解会更深刻。