第4章 定时器中断实战:基本定时器配置、更新中断、PWM中断与捕获中断
定时器中断,是嵌入式开发里最常用的外设中断之一。说实话,我做了这么多年固件,几乎每个项目都离不开它。从简单的延时、到复杂的波形测量,定时器都是核心角色。
这一章,我们聚焦STM32的定时器中断。我会从基本定时器讲起,然后逐步深入到PWM中断和输入捕获中断。嗯,咱们一步步来。
4.1 基本定时器配置与更新中断
基本定时器(TIM6、TIM7)结构最简单。它只有一个功能:计数,然后产生更新事件。说白了,就是内部计数器从0数到自动重装载值,然后溢出,触发中断。
我个人习惯,把基本定时器当作“软件心跳”。比如做系统调度、周期性数据采集,用它最省心。
4.1.1 基本定时器的核心寄存器
| 寄存器 | 作用 | 说明 |
|---|---|---|
| PSC | 预分频器 | 对时钟源分频,决定计数频率 |
| ARR | 自动重装载寄存器 | 计数值达到ARR时产生更新事件 |
| CNT | 计数器当前值 | 只读,反映当前计数值 |
| SR | 状态寄存器 | UIF位指示更新中断是否发生 |
配置流程其实很固定。我刚开始学的时候,总觉得寄存器太多记不住。后来发现,只要抓住PSC和ARR这两个关键参数,其他都是套路。
4.1.2 配置步骤(HAL库方式)
- 使能定时器时钟:__HAL_RCC_TIM6_CLK_ENABLE()
- 初始化定时器句柄:htim6.Instance = TIM6
- 设置PSC和ARR:决定中断频率
- 使能更新中断:__HAL_TIM_ENABLE_IT(&htim6, TIM_IT_UPDATE)
- 使能定时器:HAL_TIM_Base_Start_IT(&htim6)
- 编写中断回调:HAL_TIM_PeriodElapsedCallback()
关键公式:中断频率 = 定时器时钟 / (PSC + 1) / (ARR + 1)
举个例子:时钟72MHz,PSC=7199,ARR=999,中断频率 = 72M / 7200 / 1000 = 10Hz,也就是100ms触发一次。
4.1.3 代码示例:1秒定时器中断
// 定时器句柄定义
TIM_HandleTypeDef htim6;
// 定时器初始化函数
void MX_TIM6_Init(void)
{
htim6.Instance = TIM6;
htim6.Init.Prescaler = 7199; // 72MHz / 7200 = 10kHz
htim6.Init.Period = 9999; // 10kHz / 10000 = 1Hz
htim6.Init.AutoReloadPreload = TIM_AUTORELOAD_PRELOAD_ENABLE;
HAL_TIM_Base_Init(&htim6);
// 使能更新中断
HAL_NVIC_SetPriority(TIM6_DAC_IRQn, 0, 0);
HAL_NVIC_EnableIRQ(TIM6_DAC_IRQn);
}
// 启动定时器
HAL_TIM_Base_Start_IT(&htim6);
// 中断回调函数
void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim)
{
if (htim->Instance == TIM6)
{
// 每1秒执行一次
HAL_GPIO_TogglePin(LED_GPIO_Port, LED_Pin);
}
}
我的经验:回调函数里不要做耗时操作。我曾经在一个项目里,把LCD刷新放在定时器回调里,结果中断执行时间超过了定时周期,导致系统卡死。后来改成用标志位,在主循环里处理,问题就解决了。
4.2 PWM中断:不只是输出波形
很多人以为PWM只是用来输出波形。其实PWM中断也很有用。比如,你想在PWM的每个周期开始或结束时做点事情,就可以用PWM中断。
STM32的通用定时器和高级定时器都支持PWM模式。PWM中断主要有两种:更新中断(周期结束)和比较中断(占空比匹配点)。
4.2.1 PWM中断的应用场景
- 逐周期调整占空比:比如软件实现呼吸灯,每个周期更新一次CCR值
- PWM周期同步采样:在PWM周期的特定时刻触发ADC采样
- 故障检测:在PWM中断里检测过流、过温等异常
4.2.2 配置PWM中断
配置PWM中断,和基本定时器类似,但多了一步:设置比较值(CCR)。
// 定时器2 PWM初始化
TIM_HandleTypeDef htim2;
TIM_OC_InitTypeDef sConfigOC;
void MX_TIM2_PWM_Init(void)
{
htim2.Instance = TIM2;
htim2.Init.Prescaler = 71; // 72MHz / 72 = 1MHz
htim2.Init.Period = 999; // 1MHz / 1000 = 1kHz PWM
htim2.Init.CounterMode = TIM_COUNTERMODE_UP;
HAL_TIM_PWM_Init(&htim2);
sConfigOC.OCMode = TIM_OCMODE_PWM1;
sConfigOC.Pulse = 500; // 50% 占空比
sConfigOC.OCPolarity = TIM_OCPOLARITY_HIGH;
sConfigOC.OCFastMode = TIM_OCFAST_DISABLE;
HAL_TIM_PWM_ConfigChannel(&htim2, &sConfigOC, TIM_CHANNEL_1);
// 使能更新中断和比较中断
__HAL_TIM_ENABLE_IT(&htim2, TIM_IT_UPDATE | TIM_IT_CC1);
HAL_NVIC_SetPriority(TIM2_IRQn, 0, 1);
HAL_NVIC_EnableIRQ(TIM2_IRQn);
}
// 启动PWM输出并开启中断
HAL_TIM_PWM_Start_IT(&htim2, TIM_CHANNEL_1);
注意:PWM中断频率等于PWM频率。如果PWM频率是20kHz,那中断每秒触发20000次。中断服务函数必须非常精简,否则会拖累系统。
我曾经在一个电机控制项目里,PWM频率设到50kHz,中断里还做了浮点运算,结果CPU占用率直接爆表。后来把运算挪到主循环,中断里只做标志位设置,才稳定下来。
4.2.3 中断回调中的占空比调整
void HAL_TIM_PWM_PulseFinishedCallback(TIM_HandleTypeDef *htim)
{
if (htim->Instance == TIM2)
{
static uint16_t pulse = 0;
static uint8_t dir = 1;
// 呼吸灯效果:逐周期调整占空比
if (dir) pulse += 10;
else pulse -= 10;
if (pulse >= 1000) dir = 0;
if (pulse == 0) dir = 1;
__HAL_TIM_SET_COMPARE(&htim2, TIM_CHANNEL_1, pulse);
}
}
4.3 输入捕获中断:测量外部信号
输入捕获,说白了就是测量外部信号的频率、周期、脉宽。它的原理很简单:当外部信号发生跳变时,定时器会把当前CNT值“捕获”到CCR寄存器里,同时触发中断。
我在做遥控器解码时,就靠输入捕获来测量红外信号的脉冲宽度。嗯,这个功能真的很实用。
4.3.1 输入捕获的两种模式
| 模式 | 触发边沿 | 典型应用 |
|---|---|---|
| 上升沿捕获 | 信号从低到高 | 测量频率、周期开始 |
| 下降沿捕获 | 信号从高到低 | 测量脉宽、周期结束 |
| 双边沿捕获 | 上升和下降都触发 | 同时测量高电平和低电平时间 |
4.3.2 配置输入捕获中断
// 定时器3输入捕获初始化
TIM_HandleTypeDef htim3;
TIM_IC_InitTypeDef sConfigIC;
void MX_TIM3_IC_Init(void)
{
htim3.Instance = TIM3;
htim3.Init.Prescaler = 71; // 1MHz计数频率
htim3.Init.Period = 0xFFFF; // 最大计数值
htim3.Init.CounterMode = TIM_COUNTERMODE_UP;
HAL_TIM_IC_Init(&htim3);
// 配置通道1:上升沿捕获
sConfigIC.ICPolarity = TIM_ICPOLARITY_RISING;
sConfigIC.ICSelection = TIM_ICSELECTION_DIRECTTI;
sConfigIC.ICPrescaler = TIM_ICPSC_DIV1;
sConfigIC.ICFilter = 0;
HAL_TIM_IC_ConfigChannel(&htim3, &sConfigIC, TIM_CHANNEL_1);
// 使能捕获中断
__HAL_TIM_ENABLE_IT(&htim3, TIM_IT_CC1);
HAL_NVIC_SetPriority(TIM3_IRQn, 0, 2);
HAL_NVIC_EnableIRQ(TIM3_IRQn);
}
// 启动输入捕获
HAL_TIM_IC_Start_IT(&htim3, TIM_CHANNEL_1);
4.3.3 测量信号频率
测量频率的经典方法:连续两次上升沿捕获,计算时间差。
volatile uint32_t cap1 = 0, cap2 = 0;
volatile uint8_t cap_flag = 0;
void HAL_TIM_IC_CaptureCallback(TIM_HandleTypeDef *htim)
{
if (htim->Instance == TIM3 && htim->Channel == HAL_TIM_ACTIVE_CHANNEL_1)
{
if (cap_flag == 0)
{
cap1 = HAL_TIM_ReadCapturedValue(&htim3, TIM_CHANNEL_1);
cap_flag = 1;
}
else
{
cap2 = HAL_TIM_ReadCapturedValue(&htim3, TIM_CHANNEL_1);
cap_flag = 2;
}
}
}
// 在主循环中计算频率
if (cap_flag == 2)
{
uint32_t period_ticks;
if (cap2 > cap1)
period_ticks = cap2 - cap1;
else
period_ticks = (0xFFFF - cap1) + cap2 + 1; // 处理溢出
// 频率 = 计数频率 / 周期计数值
float frequency = 1000000.0f / period_ticks; // 1MHz计数频率
cap_flag = 0;
}
避坑指南:处理计数器溢出是输入捕获的难点。当两次捕获之间计数器发生了溢出,直接相减会得到错误结果。我建议用两种方法解决:
- 方法一:使能更新中断,在更新中断里记录溢出次数
- 方法二:把ARR设到最大(0xFFFF),用公式处理溢出情况
我个人更推荐方法一,虽然代码复杂点,但精度更高。
4.4 本章知识体系
下面这张图,把三种定时器中断的关系和配置要点串起来了。你可以把它当作一个快速参考。
4.5 小结与避坑清单
这一章的内容,说白了就是三个核心点:
- 基本定时器:PSC+ARR决定中断频率,适合周期性任务
- PWM中断:更新中断+比较中断,适合逐周期控制
- 输入捕获:边沿触发捕获,适合信号测量
避坑清单(我踩过的坑):
- 中断频率过高:别把中断频率设到几十kHz以上,除非你确认CPU扛得住
- 回调函数里做延时:HAL_Delay依赖SysTick中断,在定时器中断里调用会导致死锁
- 忘记清除中断标志:HAL库会自动清除,但如果你操作寄存器,别忘了清UIF或CCxIF位
- 输入捕获溢出处理:两次捕获之间计数器可能溢出,一定要处理这个情况
- PWM启动顺序:先配置好所有参数,再启动PWM输出,否则第一个周期可能异常
嗯,定时器中断的内容就到这里。这些代码和配置方法,都是我在实际项目中反复验证过的。你照着做,基本不会出问题。如果遇到奇怪的现象,先检查时钟配置,再检查中断优先级——这两个地方最容易翻车。