4. 定时器中断:SysTick、通用定时器与PWM中断实战
定时器中断,可以说是嵌入式开发里最常用的外设中断之一。我个人做项目时,几乎每个工程都会用到至少一个定时器——要么做系统心跳,要么做精确延时,要么生成PWM波。CMS32的定时器资源挺丰富的,咱们今天就把SysTick、通用定时器和PWM中断这三个核心应用捋清楚。
4.1 SysTick定时器:系统的“心跳”
SysTick是Cortex-M0内核自带的24位递减计数器。说白了,它就是给操作系统或者裸机程序提供一个固定的时间基准。我习惯把它配置成1ms中断一次,这样整个系统就有了一个“心跳”。
核心要点:SysTick是内核级外设,不依赖芯片厂商的定时器模块。所有Cortex-M系列单片机都有,代码可移植性极强。
4.1.1 SysTick寄存器配置
SysTick只有4个寄存器,操作起来非常简单:
| 寄存器 | 地址 | 功能 |
|---|---|---|
| SYST_CSR | 0xE000E010 | 控制与状态寄存器(使能、中断使能、标志位) |
| SYST_RVR | 0xE000E014 | 重装载值寄存器(24位有效) |
| SYST_CVR | 0xE000E018 | 当前值寄存器(读清零) |
| SYST_CALIB | 0xE000E01C | 校准寄存器(一般不用) |
配置SysTick的步骤其实就三步:
- 设置重装载值(SYST_RVR)
- 清除当前值(写SYST_CVR任意值)
- 使能SysTick并开启中断(写SYST_CSR)
重装载值的计算公式:
重装载值 = 系统时钟频率 / 目标中断频率 - 1
// 例如:系统时钟48MHz,目标1ms中断
// 重装载值 = 48000000 / 1000 - 1 = 47999
我的习惯:SysTick中断函数里只做“标记”操作,比如置一个全局标志位。千万别在里面做复杂计算或者延时,否则整个系统的实时性就毁了。
4.1.2 代码示例:1ms SysTick中断
/* SysTick初始化函数 */
void SysTick_Init(void)
{
/* 1. 关闭SysTick,确保配置期间不产生中断 */
SYST_CSR = 0x00;
/* 2. 设置重装载值:48MHz / 1000Hz = 48000 */
SYST_RVR = 48000 - 1;
/* 3. 清除当前值 */
SYST_CVR = 0;
/* 4. 配置并使能:使用处理器时钟,使能中断,使能计数器 */
SYST_CSR = 0x07; // Bit0: ENABLE, Bit1: TICKINT, Bit2: CLKSOURCE
}
/* SysTick中断服务函数 */
void SysTick_Handler(void)
{
static uint32_t ms_counter = 0;
ms_counter++;
/* 每1ms置一次标志 */
g_systick_flag = 1;
/* 每1000ms(1秒)翻转一次LED */
if(ms_counter >= 1000)
{
ms_counter = 0;
LED_TOGGLE();
}
}
嗯,这里要注意:SysTick_Handler这个函数名是固定的,不能自己随便起。CMS32的启动文件里已经帮你把中断向量表写好了,你只需要实现这个函数就行。
4.2 通用定时器中断:更灵活的计时方案
SysTick虽然好用,但毕竟只有一个。如果你需要多个不同频率的定时中断,那就得用通用定时器了。CMS32系列通常有2-4个通用定时器模块,每个都可以独立配置。
我在项目中遇到过这样一个场景:一个传感器需要10ms读取一次,另一个通信协议需要50ms超时判断,还有一个按键消抖需要20ms扫描。三个不同周期,一个SysTick根本不够用。这时候通用定时器就派上用场了。
4.2.1 通用定时器结构
4.2.2 定时时间计算
通用定时器的定时时间由三个参数决定:
定时时间 = (预分频值 + 1) × (自动重装载值 + 1) / 定时器时钟频率
// 例如:定时器时钟48MHz,预分频值47999,自动重装载值999
// 定时时间 = 48000 × 1000 / 48000000 = 1秒
我曾经踩过的坑:预分频器和自动重装载值都是16位的,最大值65535。如果你需要很长的定时时间,记得先算一下会不会溢出。我有个项目就因为这个原因,定时器死活不按预期工作,查了半天才发现是重装载值设超了。
4.2.3 代码示例:通用定时器1秒中断
/* 通用定时器0初始化:1秒中断 */
void TIMER0_Init(void)
{
/* 1. 使能定时器时钟 */
SYS->CLKEN |= (1 << 6); // 假设TIMER0时钟使能位在bit6
/* 2. 配置定时器模式:向上计数,自动重装载 */
TIMER0->CTRL = 0x00; // 先停止定时器
TIMER0->CTRL |= (1 << 4); // 自动重装载模式
/* 3. 设置预分频值:48000000 / 48000 = 1000Hz */
TIMER0->PSC = 48000 - 1;
/* 4. 设置自动重装载值:1000 / 1000 = 1秒 */
TIMER0->ARR = 1000 - 1;
/* 5. 使能更新中断 */
TIMER0->DIER |= (1 << 0); // UIE位
/* 6. 使能NVIC中断 */
NVIC_EnableIRQ(TIMER0_IRQn);
/* 7. 启动定时器 */
TIMER0->CTRL |= (1 << 0); // CEN位
}
/* 定时器0中断服务函数 */
void TIMER0_IRQHandler(void)
{
/* 检查更新中断标志 */
if(TIMER0->SR & (1 << 0))
{
/* 清除中断标志 */
TIMER0->SR = ~(1 << 0);
/* 用户代码:每1秒执行一次 */
g_timer0_1s_flag = 1;
}
}
4.3 PWM中断:精准控制与波形生成
PWM(脉冲宽度调制)在电机控制、LED调光、音频输出等场景中非常常见。CMS32的通用定时器通常都支持PWM输出模式,而且可以同时产生中断。
我个人觉得PWM中断最有用的场景是“逐周期控制”——比如电机控制中,每个PWM周期结束时就读取一次电流值,然后调整下一个周期的占空比。这种闭环控制在工业驱动里非常普遍。
4.3.1 PWM中断类型
| 中断类型 | 触发时机 | 典型应用 |
|---|---|---|
| 更新中断(UEV) | 计数器溢出/重装载时 | 周期同步、数据更新 |
| 捕获/比较中断(CCx) | 计数器值与比较值匹配时 | 占空比调整、脉宽测量 |
| 刹车中断(BRK) | 刹车信号输入时 | 紧急停止、故障保护 |
4.3.2 代码示例:PWM中断调整占空比
/* PWM初始化:50Hz,初始占空比50% */
void PWM_Init(void)
{
/* 1. 配置GPIO为PWM输出 */
GPIO->AFR[0] |= (2 << 4); // 假设PA2为TIMER0_CH2
/* 2. 配置定时器PWM模式 */
TIMER0->CTRL = 0x00;
TIMER0->CTRL |= (1 << 4); // 自动重装载
/* 3. 设置PWM频率:48MHz / 48000 = 1000Hz */
TIMER0->PSC = 48 - 1; // 预分频48
TIMER0->ARR = 1000 - 1; // 重装载值1000
/* 4. 配置通道2为PWM模式1 */
TIMER0->CCMR2 |= (6 << 4); // OC2M = 110 (PWM模式1)
TIMER0->CCMR2 |= (1 << 3); // OC2PE = 1 (预装载使能)
/* 5. 设置初始占空比50% */
TIMER0->CCR2 = 500;
/* 6. 使能捕获/比较中断 */
TIMER0->DIER |= (1 << 4); // CC2IE
/* 7. 使能输出 */
TIMER0->CCER |= (1 << 4); // CC2E
/* 8. 使能NVIC */
NVIC_EnableIRQ(TIMER0_IRQn);
/* 9. 启动定时器 */
TIMER0->CTRL |= (1 << 0);
}
/* PWM中断:每个周期调整占空比 */
void TIMER0_IRQHandler(void)
{
static uint8_t direction = 0;
/* 检查CC2中断标志 */
if(TIMER0->SR & (1 << 4))
{
/* 清除标志 */
TIMER0->SR = ~(1 << 4);
/* 呼吸灯效果:逐渐改变占空比 */
if(direction == 0)
{
TIMER0->CCR2 += 10; // 增加占空比
if(TIMER0->CCR2 >= 990)
direction = 1;
}
else
{
TIMER0->CCR2 -= 10; // 减少占空比
if(TIMER0->CCR2 <= 10)
direction = 0;
}
}
}
避坑指南:我曾经在PWM中断里直接修改CCR寄存器,结果波形出现了毛刺。后来才发现,必须使能预装载功能(OCxPE位),然后在中断里写CCR,硬件会在下一个周期开始时自动加载新值。这样波形才平滑。
4.4 三种定时器中断对比
咱们把三种定时器中断放在一起对比一下,方便你选型:
| 特性 | SysTick | 通用定时器 | PWM中断 |
|---|---|---|---|
| 精度 | 24位,较高 | 16位,可调 | 16位,可调 |
| 数量 | 1个(内核级) | 2-4个(外设级) | 每个定时器多个通道 |
| 主要用途 | 系统心跳、延时基准 | 多周期定时、计数 | 波形生成、闭环控制 |
| 中断频率 | 固定(通常1ms) | 可任意配置 | 与PWM频率相同 |
| 资源占用 | 极少 | 中等 | 中等(含GPIO) |
选型建议:
- 只需要一个固定周期中断?用SysTick就够了。
- 需要多个不同周期的中断?用通用定时器。
- 需要输出波形并且逐周期控制?用PWM中断。
总结一下:定时器中断是嵌入式系统的“时间管理者”。SysTick负责系统心跳,通用定时器负责多路计时,PWM中断负责波形控制。三者配合使用,基本能覆盖99%的定时需求。我建议你在项目初期就把定时器资源规划好,哪个定时器做什么事,中断优先级怎么分配,这些想清楚了,后面开发会顺畅很多。