3、定时器中断实验:CubeMX配置定时器、HAL库定时器中断回调函数、实现1ms精准定时
定时器这东西,说白了就是单片机的“心跳计数器”。
我刚开始学STM32时,总觉得定时器中断不就是个延时嘛,有啥好研究的?直到有一次做电机控制项目,发现电机转速忽快忽慢,查了半天才发现是定时器配置出了问题——预分频算错了,导致定时周期漂了将近20%。从那以后,我对定时器的每一个寄存器都格外小心。
这一章,咱们就手把手把1ms定时中断做出来。你跟着我走一遍,以后遇到任何定时需求都能自己搞定。
3.1 定时器中断的核心逻辑
先理清一个概念:定时器中断不是让程序“等”,而是让定时器“自己数数”,数到指定值就通知CPU。
整个过程分三步:
- 时钟源:给定时器一个稳定的时钟脉冲(比如72MHz)
- 预分频:把时钟频率降到你想要的节奏(比如降到1MHz)
- 自动重装载:计数器从0数到设定值,触发中断
举个例子:你让定时器每秒数100万下,然后设定“数到1000就叫我”。那它每1ms就叫你一次。就这么简单。
关键公式:
定时周期 = (预分频值 + 1) × (自动重装载值 + 1) / 定时器时钟频率
要得到1ms中断:72MHz时钟 → 预分频71(72分频)→ 1MHz → 重装载值999 → 1ms
3.2 CubeMX配置步骤(我习惯的流程)
我个人习惯先把时钟树配好,再配定时器。顺序乱了容易漏配置。
3.2.1 时钟配置
打开CubeMX,先切到Clock Configuration标签页:
- HSE选择Crystal/Ceramic Resonator
- PLL源选HSE
- 主频拉到72MHz(STM32F103最常见配置)
- APB1定时器时钟自动变成72MHz
嗯,这里要注意:APB1上的定时器(TIM2~TIM7)时钟可能翻倍。如果APB1预分频不是1,定时器时钟 = APB1时钟 × 2。我踩过这个坑,当时配了半天中断频率都不对,查手册才发现这个细节。
3.2.2 定时器参数配置
切到Pinout & Configuration,选TIM2:
| 参数 | 值 | 说明 |
|---|---|---|
| Clock Source | Internal Clock | 使用内部时钟源 |
| Prescaler (PSC) | 71 | 72MHz ÷ (71+1) = 1MHz |
| Counter Mode | Up | 向上计数模式 |
| Counter Period (ARR) | 999 | 1MHz ÷ (999+1) = 1kHz → 1ms |
| Auto-reload Preload | Enable | 使能自动重装载预装载 |
配完这些,别忘了在NVIC Settings标签页里勾上TIM2全局中断。我见过有人配了定时器但忘了开中断,调试了半天以为代码写错了。
3.3 代码生成与HAL库回调机制
点击Generate Code,生成工程。打开后你会发现,HAL库已经把定时器初始化写好了,在MX_TIM2_Init()里。
启动定时器中断只需要一行:
HAL_TIM_Base_Start_IT(&htim2);
我建议放在main()的初始化部分,就在MX_TIM2_Init()之后。
3.3.1 中断回调函数
HAL库的中断处理分两层:
- 底层中断函数:
TIM2_IRQHandler()——由HAL库自动生成,你不用动 - 用户回调函数:
HAL_TIM_PeriodElapsedCallback()——你在这里写业务逻辑
回调函数长这样:
void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim)
{
if(htim->Instance == TIM2)
{
// 1ms到了,在这里做你想做的事
// 比如翻转一个LED、累加一个计数器
HAL_GPIO_TogglePin(LED_GPIO_Port, LED_Pin);
}
}
我的经验:回调函数里不要做耗时操作。1ms很短,如果你在里面跑个printf或者复杂计算,中断还没退出下一个中断又来了,程序就卡死了。我一般只放标志位或者简单累加。
3.4 验证1ms精度
怎么确认定时真的准?我习惯用逻辑分析仪或者示波器看波形。
在回调函数里翻转一个GPIO:
void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim)
{
static uint16_t cnt = 0;
if(htim->Instance == TIM2)
{
cnt++;
if(cnt >= 1000) // 1ms × 1000 = 1s
{
cnt = 0;
HAL_GPIO_TogglePin(TEST_PIN_GPIO_Port, TEST_PIN_Pin);
}
}
}
用示波器量TEST_PIN,周期应该是2秒(1秒高1秒低)。如果偏差超过1%,检查时钟配置和预分频值。
我曾经踩过的坑:
- 预分频值写成了71,但忘了PSC和ARR都是从0开始计数的,实际分频是72,周期是72/72MHz=1μs,不是1ms
- APB1预分频没注意,定时器时钟变成了36MHz,算出来的周期翻倍
- 中断优先级没配,被其他高优先级中断一直抢占,定时器回调迟迟进不去
3.5 知识结构总览
下面这张图把整个定时器中断的脉络串起来了,建议你保存下来对照着配:
3.6 常见问题与排查思路
配完发现定时不准?别慌,按这个顺序查:
| 现象 | 可能原因 | 排查方法 |
|---|---|---|
| 中断完全不触发 | NVIC没使能 / 没调用Start_IT | 检查CubeMX NVIC设置,确认代码中有HAL_TIM_Base_Start_IT |
| 周期偏大一倍 | APB1预分频导致定时器时钟翻倍 | 查RCC时钟树,确认APB1分频系数 |
| 周期偏小 | PSC或ARR算错了 | 重新代入公式计算,注意+1 |
| 程序卡死 | 回调函数耗时超过1ms | 回调里只放标志位,主循环处理 |
嗯,这一套走下来,1ms定时中断应该稳了。你把这个基础打牢,后面做PWM、输入捕获、ADC定时触发都会轻松很多。