4. 定时器基础:STM32定时器资源概览、基本定时器配置、PWM输出配置与占空比调节
定时器,说白了就是STM32的“心跳节拍器”。我刚开始学STM32那会儿,总觉得定时器就是个计数的玩意儿,没啥技术含量。直到有一次做电机控制项目,PWM频率抖了一下,电机直接啸叫起来……嗯,从那以后我再也不敢小看定时器了。
这一章,咱们把STM32的定时器家族捋一遍,重点搞定基本定时器和PWM输出。你想想看,无论是LED呼吸灯、舵机控制,还是后面I2C/SPI通信中的时钟生成,都离不开定时器。
4.1 STM32定时器资源概览
STM32的定时器分三类:基本定时器、通用定时器、高级定时器。它们的关系,我用一张图给你讲清楚。
核心观点:三类定时器是“层层叠加”的关系——高级定时器包含通用定时器的全部功能,通用定时器又包含基本定时器的全部功能。
我个人习惯这样记:基本定时器就是“纯计时器”,通用定时器是“多功能瑞士军刀”,高级定时器是“电机控制专用核弹”。
| 特性 | 基本定时器 | 通用定时器 | 高级定时器 |
|---|---|---|---|
| 计数器位数 | 16位 | 16/32位 | 16位 |
| 计数模式 | 仅向上 | 向上/向下/中央对齐 | 向上/向下/中央对齐 |
| PWM通道数 | 0 | 4 | 4(含互补输出) |
| 输入捕获 | 无 | 有 | 有 |
| 死区插入 | 无 | 无 | 有 |
| 典型应用 | 定时中断、时基 | PWM、脉冲测量 | 电机控制、逆变器 |
选型建议:做I2C/SPI通信的时钟延时,用基本定时器就够了。做LED呼吸灯或舵机控制,用通用定时器。做无刷电机FOC控制,才需要上高级定时器。别杀鸡用牛刀。
4.2 基本定时器配置实战
基本定时器,说白了就是一个“自动加1的计数器”。它从0开始,每来一个时钟脉冲就加1,加到预设值就归零,同时触发中断。我在项目中常用它来做“精确延时”和“周期性任务调度”。
4.2.1 基本定时器的工作原理
核心公式就一个:
定时时间 = (预分频值 + 1) × (自动重装载值 + 1) / 定时器时钟频率
举个例子:STM32F103的APB1时钟是36MHz,我想产生1ms的定时中断。
// 目标:1ms中断一次
// 时钟:36MHz = 36,000,000 Hz
// 预分频:36000-1 → 计数频率 = 36MHz / 36000 = 1KHz = 1ms
// 自动重装载:1000-1 → 计数1000次 = 1ms
TIM_TimeBaseInitTypeDef TIM_TimeBaseStructure;
RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM6, ENABLE);
TIM_TimeBaseStructure.TIM_Prescaler = 36000 - 1; // 预分频
TIM_TimeBaseStructure.TIM_Period = 1000 - 1; // 自动重装载
TIM_TimeBaseStructure.TIM_CounterMode = TIM_CounterMode_Up;
TIM_TimeBaseInit(TIM6, &TIM_TimeBaseStructure);
TIM_ITConfig(TIM6, TIM_IT_Update, ENABLE); // 使能更新中断
TIM_Cmd(TIM6, ENABLE); // 启动定时器
我曾经踩过的坑:预分频值和自动重装载值都要减1!因为寄存器是从0开始计数的。我第一次写的时候忘了减1,结果定时时间整整多了一个时钟周期,排查了半小时才找到原因。
4.2.2 CubeMX配置步骤
用CubeMX配置基本定时器,其实就三步:
- 选择定时器:在Pinout视图的Timers里勾选TIM6或TIM7
- 设置参数:配置Prescaler和Counter Period
- 使能中断:在NVIC Settings里勾选TIM6 global interrupt
我个人习惯把预分频值设成“时钟频率/10000”,这样计数频率就是10KHz,自动重装载值直接写“定时时间(ms)×10”,算起来特别方便。
4.3 PWM输出配置与占空比调节
PWM,说白了就是“快速开关信号”。通过调节高电平占一个周期的时间比例(占空比),就能控制LED亮度、电机转速、舵机角度。我做过一个智能灯项目,用PWM调光,效果比模拟调压好太多了。
4.3.1 PWM的工作原理
通用定时器的PWM模式,核心是“比较匹配”机制:
- 计数器从0开始向上计数
- 当计数值 < 比较值(CCR)时,输出高电平
- 当计数值 ≥ 比较值(CCR)时,输出低电平
- 计数值达到自动重装载值(ARR)时,归零重新开始
你想想看,这不就是一个“锯齿波+阈值比较”的过程吗?
4.3.2 CubeMX配置PWM输出
以TIM2的通道1(PA0)输出50Hz、50%占空比的PWM为例:
- 选择定时器通道:在Pinout视图里找到TIM2,Channel1选PWM Generation CH1
- 配置参数:
- Prescaler: 720-1(72MHz/720=100KHz)
- Counter Period: 2000-1(100KHz/2000=50Hz)
- Pulse: 1000(50%占空比)
- 生成代码:CubeMX会自动生成初始化代码
生成的HAL代码长这样:
// 初始化代码(CubeMX自动生成)
TIM_HandleTypeDef htim2;
void MX_TIM2_Init(void)
{
TIM_OC_InitTypeDef sConfigOC = {0};
htim2.Instance = TIM2;
htim2.Init.Prescaler = 720 - 1;
htim2.Init.CounterMode = TIM_COUNTERMODE_UP;
htim2.Init.Period = 2000 - 1;
htim2.Init.ClockDivision = TIM_CLOCKDIVISION_DIV1;
HAL_TIM_PWM_Init(&htim2);
sConfigOC.OCMode = TIM_OCMODE_PWM1;
sConfigOC.Pulse = 1000; // 初始占空比50%
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);
4.3.3 动态调节占空比
运行时调节占空比,其实就是修改CCR寄存器的值。HAL库提供了现成的函数:
// 设置占空比为75%
__HAL_TIM_SET_COMPARE(&htim2, TIM_CHANNEL_1, 1500);
// 或者用宏定义更清晰
#define PWM_PERIOD 2000
#define SET_DUTY(x) __HAL_TIM_SET_COMPARE(&htim2, TIM_CHANNEL_1, (x))
// 呼吸灯效果示例
void breath_led(void)
{
for(int duty = 0; duty < PWM_PERIOD; duty += 10)
{
SET_DUTY(duty);
HAL_Delay(5);
}
for(int duty = PWM_PERIOD; duty > 0; duty -= 10)
{
SET_DUTY(duty);
HAL_Delay(5);
}
}
经验之谈:调节占空比时,注意不要超过ARR值。我曾经在调试时不小心把CCR设成了2500(ARR是1999),结果PWM波形直接乱掉了。建议加个限幅:duty = (duty > PWM_PERIOD) ? PWM_PERIOD : duty;
4.4 常见问题与避坑指南
问题1:PWM输出没波形
- 检查GPIO是否配置为复用功能(AF)
- 检查定时器时钟是否使能
- 检查是否调用了HAL_TIM_PWM_Start()
问题2:定时时间不准
- 确认系统时钟配置是否正确
- 预分频值和自动重装载值是否减1
- 中断优先级是否被更高优先级中断抢占
问题3:占空比调节不线性
- 检查PWM模式是PWM1还是PWM2(两者极性相反)
- 确认OCPolarity设置是否正确
- 如果负载是LED,人眼对亮度感知是对数的,需要做Gamma校正
嗯,定时器这块内容就这么多。说白了就是三个参数:预分频器决定计数速度,自动重装载决定周期,比较值决定占空比。掌握了这三个旋钮,你就能玩转STM32的定时器了。