4. 开发环境搭建与工程模板
定时器,说白了就是单片机的“心跳计数器”。
我刚开始学STM32时,觉得定时器不就是个计时器嘛,有啥好讲的?后来做项目才发现,这玩意儿的水深得很。从简单的延时,到复杂的电机控制、波形生成,全都离不开它。
今天咱们就把定时器家族捋清楚。你想想看,STM32的定时器分三类:基本、通用、高级。它们之间到底差在哪?怎么用HAL库快速上手?呼吸灯又是怎么实现的?
嗯,一个一个来。
三类定时器的核心区别
先看一张对比表,心里有个底:
| 特性 | 基本定时器 | 通用定时器 | 高级定时器 |
|---|---|---|---|
| 典型型号 | TIM6、TIM7 | TIM2~TIM5 | TIM1、TIM8 |
| 计数方向 | 仅向上 | 向上/向下/双向 | 向上/向下/双向 |
| PWM输出 | ❌ 无 | ✅ 有(4通道) | ✅ 有(6通道+互补) |
| 输入捕获 | ❌ 无 | ✅ 有 | ✅ 有 |
| 死区插入 | ❌ 无 | ❌ 无 | ✅ 有 |
| 刹车功能 | ❌ 无 | ❌ 无 | ✅ 有 |
| 主要用途 | 基础定时、触发DAC | PWM、捕获、编码器 | 电机控制、逆变器 |
看到没?基本定时器就是个“光杆司令”,只能计时。通用定时器是“多面手”,PWM、捕获都能干。高级定时器则是“特种兵”,专门伺候电机和电源这类高要求的活儿。
我的经验:做普通LED呼吸灯、舵机控制,用通用定时器就够了。别一上来就开高级定时器,配置复杂不说,还容易踩坑。我曾经在一个项目中误用了高级定时器的互补通道,结果输出波形一直不对,查了两天才发现是死区设置的问题。
定时器的核心工作原理
不管哪类定时器,核心就三样东西:
- 预分频器(PSC):把时钟源分频,降低计数频率
- 计数器(CNT):真正干活的,每来一个时钟脉冲就加1或减1
- 自动重装载寄存器(ARR):计数值到了这个数,就重置或翻转
举个例子:时钟是72MHz,PSC设为71,那计数频率就是72M / (71+1) = 1MHz,也就是1微秒计一次。如果ARR设为999,那定时周期就是1000微秒,也就是1毫秒。
公式很简单:定时时间 = (PSC+1) × (ARR+1) / 时钟频率
小技巧:我习惯先定PSC让计数频率变成1MHz或1kHz,这样ARR直接就是毫秒或微秒数,调试时脑子不用转。比如要50ms中断,PSC=71(72MHz下),ARR=49999,搞定。
HAL_TIM_Base_Start_IT:开启定时器中断
HAL库把定时器封装得挺舒服。用基本定时器做个1ms中断,代码就这几步:
// 1. 初始化定时器句柄
TIM_HandleTypeDef htim6;
void MX_TIM6_Init(void)
{
htim6.Instance = TIM6;
htim6.Init.Prescaler = 71; // 72MHz / (71+1) = 1MHz
htim6.Init.Period = 999; // 1MHz / (999+1) = 1kHz = 1ms
HAL_TIM_Base_Init(&htim6);
}
// 2. 启动中断模式
HAL_TIM_Base_Start_IT(&htim6);
// 3. 中断回调函数
void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim)
{
if (htim->Instance == TIM6)
{
// 每1ms执行一次
// 比如翻转一个LED
HAL_GPIO_TogglePin(LED_GPIO_Port, LED_Pin);
}
}
这里有个细节:HAL_TIM_Base_Start_IT 启动的是中断模式,还有 HAL_TIM_Base_Start(轮询)和 HAL_TIM_Base_Start_DMA(DMA)。我一般只在需要精确时序控制时才用中断,简单延时用HAL_Delay就够了。
注意:中断回调里别做耗时操作!我曾经在回调里加了个HAL_Delay(10),结果系统直接卡死。中断服务函数要短平快,复杂逻辑扔到主循环或任务里处理。
PWM输出:让LED呼吸起来
PWM,说白了就是快速开关信号,通过调节占空比来模拟模拟量。LED呼吸灯就是最经典的例子——占空比从0慢慢变到100%,再变回来。
用通用定时器的PWM模式,配置如下:
// 使用TIM2的通道1(PA0)
TIM_HandleTypeDef htim2;
TIM_OC_InitTypeDef sConfigOC;
void MX_TIM2_PWM_Init(void)
{
htim2.Instance = TIM2;
htim2.Init.Prescaler = 71; // 1MHz
htim2.Init.Period = 999; // PWM频率 = 1kHz
htim2.Init.CounterMode = TIM_COUNTERMODE_UP;
HAL_TIM_PWM_Init(&htim2);
sConfigOC.OCMode = TIM_OCMODE_PWM1;
sConfigOC.Pulse = 0; // 初始占空比0%
sConfigOC.OCPolarity = TIM_OCPOLARITY_HIGH;
sConfigOC.OCFastMode = TIM_OCFAST_DISABLE;
HAL_TIM_PWM_ConfigChannel(&htim2, &sConfigOC, TIM_CHANNEL_1);
}
// 启动PWM
HAL_TIM_PWM_Start(&htim2, TIM_CHANNEL_1);
// 动态调节占空比
__HAL_TIM_SET_COMPARE(&htim2, TIM_CHANNEL_1, 500); // 50%占空比
呼吸灯的逻辑就更简单了:
uint16_t pulse = 0;
int8_t direction = 1;
while (1)
{
__HAL_TIM_SET_COMPARE(&htim2, TIM_CHANNEL_1, pulse);
HAL_Delay(5); // 每5ms调整一次
pulse += direction * 5;
if (pulse >= 999) direction = -1;
if (pulse == 0) direction = 1;
}
你看,就这么几行代码,LED就能像呼吸一样渐亮渐暗。
避坑指南:我曾经把PWM频率设成了10Hz,结果LED一闪一闪的,跟呼吸完全不像。人眼对50Hz以上的闪烁就不敏感了,建议PWM频率至少100Hz以上。另外,占空比变化步长别太大,否则会看到明显的跳变。
知识结构图:定时器体系一览
下面这张图帮你理清三类定时器的定位和关系:
总结一下关键点
- 基本定时器:只有向上计数,不能输出PWM,适合做系统心跳或触发DAC
- 通用定时器:功能最均衡,PWM、捕获、编码器都能干,日常项目的主力
- 高级定时器:带死区、刹车、互补输出,专为电机和电源设计
- HAL_TIM_Base_Start_IT:启动定时器中断,回调里别做耗时操作
- PWM呼吸灯:调节占空比从0到100%循环,频率100Hz以上,步长别太大
最后说一句:定时器是STM32里最常用的外设之一,也是最容易出问题的。我见过太多人把PWM频率算错、中断优先级配乱、占空比寄存器写错位置。我的建议是——先拿基本定时器跑通中断,再上PWM,最后再碰高级功能。一步一步来,稳得很。