第4章 定时器(TIM)基础:从工作原理到呼吸灯实战
定时器这东西,可以说是STM32里最常用的外设之一了。我刚开始做嵌入式那会儿,总觉得定时器就是用来计个时、定个闹钟,后来才发现它的玩法远比想象中丰富。PWM输出、输入捕获、编码器接口……很多高级功能都离不开它。
这一章,咱们就把定时器的底裤扒干净。从工作原理讲起,再到HAL库的封装,最后用两个实战案例——定时中断和呼吸灯,让你彻底搞懂它。
本章核心目标:
- 理解STM32定时器的分层结构与计数模式
- 掌握HAL库定时器相关函数的使用套路
- 独立实现定时中断与PWM呼吸灯两个项目
4.1 定时器的工作原理——其实没那么玄乎
说白了,定时器就是一个会自己加1的计数器。它内部有一个CNT寄存器,每来一个时钟脉冲就加1。当CNT的值达到我们设定的ARR(自动重装载寄存器)时,就会触发一个事件——要么产生中断,要么翻转输出电平。
那这个时钟脉冲从哪来?两个途径:
- 内部时钟(CK_INT):来自APB总线,通常是几十MHz
- 外部时钟(TIx/ETR):从GPIO引脚输入,可以接编码器或外部信号
这里有个关键器件叫预分频器(PSC)。你想想看,如果直接拿72MHz去计数,那1秒钟要数7200万次,ARR得设到7200万才能得到1秒的周期——这显然不现实。PSC的作用就是把时钟频率降下来。
经验公式:
定时器溢出频率 = 时钟源频率 / (PSC + 1) / (ARR + 1)
举个例子:72MHz时钟,PSC=7199,ARR=999,那么溢出频率 = 72M / 7200 / 1000 = 10Hz,也就是100ms中断一次。
我在项目中遇到过一个问题:用TIM2做1ms定时,算出来的PSC和ARR明明没错,但实际跑起来就是不对。后来查了半天,发现是APB1的时钟树配置问题——TIM2挂在APB1上,如果APB1分频系数不是1,那么定时器的时钟会翻倍。嗯,这里要注意,查数据手册的时钟树是基本功。
4.2 三种计数模式,你都得会
| 计数模式 | 工作方式 | 典型应用 |
|---|---|---|
| 向上计数 | CNT从0递增到ARR,然后归零重新开始 | 普通定时中断、周期性任务 |
| 向下计数 | CNT从ARR递减到0,然后跳回ARR重新开始 | 某些特殊时序控制 |
| 中央对齐 | CNT先递增到ARR,再递减到0,如此反复 | 对称PWM、电机控制 |
我个人习惯,90%的场景都用向上计数模式。向下计数用得少,中央对齐模式主要是在做电机FOC控制时才会用到。初学者先把向上计数玩熟就够了。
4.3 HAL库定时器函数——三板斧
HAL库把定时器的操作封装得很规整。我总结了一套"三板斧"套路,你照着来就行:
- 初始化:配置PSC、ARR、计数模式
- 启动:启动计数,可选是否开启中断
- 回调:在中断回调函数里写你的业务逻辑
核心函数就这几个:
// 1. 初始化定时器句柄
TIM_HandleTypeDef htim2;
// 2. 配置时基单元
htim2.Instance = TIM2;
htim2.Init.Prescaler = 7199; // PSC预分频
htim2.Init.CounterMode = TIM_COUNTERMODE_UP; // 向上计数
htim2.Init.Period = 999; // ARR自动重装载值
htim2.Init.ClockDivision = TIM_CLOCKDIVISION_DIV1;
HAL_TIM_Base_Init(&htim2);
// 3. 启动定时器(中断模式)
HAL_TIM_Base_Start_IT(&htim2);
// 4. 中断回调函数
void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim)
{
if(htim->Instance == TIM2)
{
// 你的业务代码,比如翻转LED
HAL_GPIO_TogglePin(LED_GPIO_Port, LED_Pin);
}
}
我曾经踩过的坑:
回调函数的名字不能写错!HAL_TIM_PeriodElapsedCallback 是弱定义,你重写的时候必须一字不差。我见过有人写成 PeriodElapsed_Callback 或者 PeriodElapsedCallback_TIM2,结果中断触发了但回调不执行——因为编译器找不到匹配的函数。
另外,启动函数有三种变体,区别在于是否开启中断和DMA:
| 函数 | 说明 |
|---|---|
| HAL_TIM_Base_Start | 纯软件启动,不产生中断 |
| HAL_TIM_Base_Start_IT | 启动并开启中断 |
| HAL_TIM_Base_Start_DMA | 启动并配合DMA传输 |
4.4 实战一:定时中断——让LED以精确间隔闪烁
咱们来写一个完整的例子。目标:用TIM2产生1Hz的中断,让板载LED以1秒为周期闪烁。
步骤1:CubeMX配置
- 选择TIM2,时钟源选Internal Clock
- Prescaler设为7199(72MHz / 7200 = 10kHz)
- Counter Period设为4999(10kHz / 5000 = 2Hz)
- NVIC Settings里使能TIM2全局中断
步骤2:生成代码,添加用户逻辑
// main.c 中的 MX_TIM2_Init 函数由CubeMX自动生成
// 我们只需要在 main() 中启动定时器
int main(void)
{
HAL_Init();
SystemClock_Config();
MX_GPIO_Init();
MX_TIM2_Init();
// 启动定时器中断
HAL_TIM_Base_Start_IT(&htim2);
while (1)
{
// 主循环可以干别的事
// 中断里已经处理了LED翻转
}
}
// 在 stm32f1xx_it.c 或用户文件中添加回调
void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim)
{
if(htim->Instance == TIM2)
{
HAL_GPIO_TogglePin(GPIOB, GPIO_PIN_0); // 假设LED在PB0
}
}
小技巧:
如果你想让中断频率更精确,可以用定时器时钟预分频。比如APB1时钟是36MHz,但TIM2的时钟可能是72MHz(因为APB1预分频系数不为1时,定时器时钟会翻倍)。我一般会在初始化前用 HAL_RCC_GetPCLK1Freq() 确认一下实际时钟频率。
4.5 实战二:PWM输出与呼吸灯——让LED学会呼吸
PWM的全称是脉冲宽度调制。说白了,就是在一个周期内,让高电平的时间(占空比)可调。人眼有视觉暂留效应,只要频率够高(通常1kHz以上),看到的就不是闪烁,而是亮度变化。
呼吸灯的原理就更简单了:让占空比从0%逐渐增加到100%,再从100%逐渐减小到0%,如此循环。LED就会像呼吸一样,由暗变亮再变暗。
步骤1:CubeMX配置PWM通道
- 选择TIM3,通道1(假设对应PA6)
- 时钟源Internal Clock,PSC=71,ARR=999(得到1kHz的PWM频率)
- Channel1模式选PWM Generation CH1
- NVIC里不用开中断(PWM不需要中断)
步骤2:代码实现呼吸效果
// 启动PWM输出
HAL_TIM_PWM_Start(&htim3, TIM_CHANNEL_1);
// 呼吸灯主循环
uint16_t duty = 0;
int8_t direction = 1;
while (1)
{
// 设置占空比
__HAL_TIM_SET_COMPARE(&htim3, TIM_CHANNEL_1, duty);
// 延时10ms(可以用HAL_Delay,但实际项目中建议用定时器)
HAL_Delay(10);
// 更新占空比方向
duty += direction * 5;
if(duty >= 1000) direction = -1;
if(duty == 0) direction = 1;
}
关键点解析:
- ARR=999,所以占空比范围是0~999,对应0%~100%
- 每次步进5,步进间隔10ms,一个完整呼吸周期约4秒
- __HAL_TIM_SET_COMPARE 是宏,直接操作CCR寄存器,效率很高
我在项目中用过这个思路做呼吸式状态指示灯——设备正常运行时LED缓慢呼吸,故障时快速闪烁。用户一看就明白设备状态,连说明书都不用看。
注意:
PWM频率不能太低。低于50Hz人眼就能看到闪烁,低于20Hz甚至会让人不舒服。我一般设1kHz~10kHz,既平滑又听不到线圈啸叫(如果用蜂鸣器或电机的话)。
4.6 定时器配置速查表
为了方便你快速上手,我把常用配置整理成了一张表。假设系统时钟72MHz:
| 目标功能 | PSC | ARR | 实际频率/周期 |
|---|---|---|---|
| 1ms定时中断 | 71 | 999 | 1kHz / 1ms |
| 10ms定时中断 | 719 | 999 | 100Hz / 10ms |
| 1s定时中断 | 7199 | 9999 | 1Hz / 1s |
| 1kHz PWM(呼吸灯) | 71 | 999 | 1kHz |
| 10kHz PWM(电机) | 71 | 99 | 10kHz |
你可能会问:为什么PSC和ARR要这么搭配?其实核心就一句话——先降频到合适范围,再精确计数。PSC负责粗调,ARR负责细调。两者配合,就能得到你想要的任何频率。
好了,定时器的基础内容就这些。从工作原理到HAL库函数,再到两个实战项目,这一套走下来,你应该能自己上手写定时器相关的代码了。记住我那句老话:先算频率,再配参数,最后写回调。顺序别搞反,能省不少调试时间。