4. 定时器基础与PWM输出:SysTick与HAL_Delay原理、通用定时器配置、PWM输出与呼吸灯实现、封装PWM驱动模块
定时器,说白了就是单片机的“心跳”。没有它,你连个精准的延时都做不了。我刚开始用STM32时,总觉得定时器就是用来计数的,后来才发现——它才是整个系统调度的基石。
这一章,咱们从最基础的SysTick讲起,再到通用定时器,最后用PWM做个呼吸灯。嗯,我会把项目中踩过的坑也一并告诉你。
4.1 SysTick与HAL_Delay原理
SysTick是Cortex-M内核自带的一个24位递减计数器。它只有一个任务——每1ms产生一次中断。HAL_Delay()就是靠它实现的。
核心流程:
- SysTick每1ms减到0,触发中断
- 中断服务函数中,全局变量
uwTick加1 - HAL_Delay(ms) 不断读取
uwTick,直到差值达到ms
我曾经在项目里直接用HAL_Delay做按键消抖,结果系统卡死。为什么?因为HAL_Delay是阻塞的——它占着CPU不放。你想想看,如果中断里调了HAL_Delay,那整个系统就停摆了。
避坑指南:不要在中断服务函数里调用HAL_Delay()。我曾经因为这个让整个电机控制系统“死机”了3天。
4.2 通用定时器配置
通用定时器比SysTick灵活得多。它有多个通道、支持输入捕获、输出比较、PWM生成。我个人习惯用TIM2或TIM3做基础定时,因为它们挂在APB1上,时钟配置相对简单。
配置步骤其实就四步:
- 使能时钟:__HAL_RCC_TIMx_CLK_ENABLE()
- 设置分频与重装值:PSC和ARR
- 配置中断:HAL_NVIC_SetPriority() + HAL_NVIC_EnableIRQ()
- 启动定时器:HAL_TIM_Base_Start_IT()
这里有个关键公式:
定时频率 = 定时器时钟 / (PSC + 1) / (ARR + 1)
举个例子,如果APB1时钟是72MHz,想要1ms中断一次:
PSC = 71; // 72MHz / (71+1) = 1MHz
ARR = 999; // 1MHz / (999+1) = 1kHz = 1ms
个人经验:PSC和ARR的值尽量用整数,避免浮点运算。我习惯先算PSC让计数器频率为1MHz,再算ARR得到目标周期。
4.3 PWM输出与呼吸灯实现
PWM,说白了就是快速开关信号,通过改变占空比来模拟不同电压。呼吸灯就是PWM占空比从0%慢慢变到100%,再变回来。
配置PWM的步骤:
- 初始化定时器为PWM模式
- 配置通道:极性、占空比初始值
- 启动PWM输出:HAL_TIM_PWM_Start()
- 在循环中修改CCR寄存器改变占空比
代码示例:
// 初始化TIM3通道1为PWM输出
TIM_OC_InitTypeDef sConfigOC = {0};
sConfigOC.OCMode = TIM_OCMODE_PWM1;
sConfigOC.Pulse = 0; // 初始占空比0%
sConfigOC.OCPolarity = TIM_OCPOLARITY_HIGH;
HAL_TIM_PWM_ConfigChannel(&htim3, &sConfigOC, TIM_CHANNEL_1);
// 启动PWM
HAL_TIM_PWM_Start(&htim3, TIM_CHANNEL_1);
// 呼吸灯主循环
while (1) {
for (int i = 0; i <= 1000; i += 10) {
__HAL_TIM_SET_COMPARE(&htim3, TIM_CHANNEL_1, i);
HAL_Delay(5);
}
for (int i = 1000; i >= 0; i -= 10) {
__HAL_TIM_SET_COMPARE(&htim3, TIM_CHANNEL_1, i);
HAL_Delay(5);
}
}
注意:CCR的值不能超过ARR。如果ARR=999,CCR最大只能设999。我曾经设了1000,结果PWM直接变成常亮——占空比100%了。
4.4 封装PWM驱动模块
裸写PWM配置代码,每个项目都要重复一遍。我习惯把它封装成一个驱动模块,方便复用。
封装思路:
- pwm_driver.h:对外接口声明
- pwm_driver.c:具体实现
- 结构体:保存定时器句柄、通道、ARR值
头文件示例:
#ifndef __PWM_DRIVER_H
#define __PWM_DRIVER_H
#include "stm32f4xx_hal.h"
typedef struct {
TIM_HandleTypeDef *htim;
uint32_t channel;
uint32_t period; // ARR值
} PWM_Handle_t;
void PWM_Init(PWM_Handle_t *pwm, TIM_HandleTypeDef *htim,
uint32_t channel, uint32_t period);
void PWM_SetDuty(PWM_Handle_t *pwm, uint16_t duty);
void PWM_Start(PWM_Handle_t *pwm);
void PWM_Stop(PWM_Handle_t *pwm);
#endif
实现文件关键部分:
void PWM_SetDuty(PWM_Handle_t *pwm, uint16_t duty) {
if (duty > pwm->period) duty = pwm->period;
__HAL_TIM_SET_COMPARE(pwm->htim, pwm->channel, duty);
}
封装的好处:换一个定时器或通道,只需要改初始化参数,不用改业务逻辑。我在三个项目里复用了同一套PWM驱动,改的只是引脚和时钟配置。
4.5 知识结构图
下面这张图展示了本章的核心知识脉络:
4.6 常见问题与避坑
| 问题 | 原因 | 解决方法 |
|---|---|---|
| HAL_Delay不准确 | SysTick优先级被其他中断抢占 | 提高SysTick中断优先级 |
| PWM输出无波形 | GPIO未配置为复用功能 | 检查GPIO_AF配置 |
| 呼吸灯闪烁不均匀 | CCR变化步长太大 | 减小步长,增加延时 |
| 定时器中断不触发 | 未使能NVIC中断 | 调用HAL_NVIC_EnableIRQ() |
本章小结:
- SysTick是1ms心跳,HAL_Delay是阻塞的,别在中断里用
- 通用定时器配置记住公式:频率 = 时钟 / (PSC+1) / (ARR+1)
- PWM占空比由CCR控制,CCR不能超过ARR
- 封装驱动模块,让代码可复用、可维护
嗯,定时器这部分内容就到这里。我个人觉得,把SysTick和通用定时器搞明白,后面学高级定时器、输入捕获、编码器接口都会轻松很多。呼吸灯虽然简单,但它背后涉及的PWM原理和驱动封装思路,是嵌入式开发里非常实用的基本功。