4、定时器基础:SysTick定时器、通用定时器配置、PWM输出原理、呼吸灯实现
定时器这东西,说白了就是单片机的“心跳”。没有它,你的程序就像没戴手表的打工人——完全不知道时间过了多久。我刚开始学STM32时,总觉得定时器很神秘,后来才发现,它其实就是个会数数的计数器,数到某个值就干点什么事。
这一节,咱们把定时器家族里最常用的三个角色捋清楚:SysTick、通用定时器,还有PWM。最后用呼吸灯把知识串起来,你做完就知道定时器到底怎么玩了。
核心知识点速览
- SysTick:系统滴答定时器,RTOS的节拍器
- 通用定时器:多功能定时器,输入捕获、输出比较、PWM都靠它
- PWM:脉冲宽度调制,说白了就是“假装输出模拟电压”
- 呼吸灯:PWM的经典应用,让LED像呼吸一样渐变
4.1 SysTick定时器:系统的心跳
SysTick是Cortex-M内核自带的定时器,24位向下计数。你想想看,每个STM32芯片里都有它,不用额外配置外设时钟,省事得很。
我习惯用SysTick做系统时钟基准,比如延时函数、超时判断。在RTOS里,它就是任务调度的节拍器。裸机开发时,我经常用它来产生1ms的中断,然后累加计数。
配置SysTick其实就三步:
- 设置重装载值(决定定时周期)
- 选择时钟源(一般是AHB/8)
- 使能中断(如果需要)
// SysTick配置示例:产生1ms中断
void SysTick_Init(void)
{
// 系统时钟72MHz,AHB/8 = 9MHz
// 1ms = 9000个时钟周期
SysTick->LOAD = 9000 - 1; // 重装载值
SysTick->VAL = 0; // 清空计数器
SysTick->CTRL = 0x07; // 使能、中断、使用内核时钟
}
我的小技巧:SysTick的LOAD值最好用宏定义,方便移植。比如#define SYSTICK_LOAD (SystemCoreClock / 1000 - 1),这样换芯片时改SystemCoreClock就行。
嗯,这里要注意:SysTick是24位的,最大计数值是16777215。如果你的时钟频率很高,又想产生很长的延时,就得用中断里累加的方式。我曾经在项目里直接用SysTick做1秒延时,结果LOAD值溢出了,程序直接跑飞……后来查了半天才发现是这个问题。
4.2 通用定时器:多功能选手
通用定时器比SysTick复杂多了。STM32的通用定时器(TIM2-TIM5)有16位自动重装载计数器,支持向上、向下、中央对齐三种计数模式。
我个人觉得,通用定时器最常用的功能就三个:
- 定时中断:和SysTick类似,但可以配置更灵活的分频
- PWM输出:控制电机、LED亮度、舵机角度
- 输入捕获:测量脉冲宽度、频率
配置通用定时器的步骤,我总结了一个口诀:开时钟、配分频、设重装、配模式、使能中断(如果需要)。
// 通用定时器配置示例:TIM3产生1Hz中断
void TIM3_Init(void)
{
RCC->APB1ENR |= RCC_APB1ENR_TIM3EN; // 开启TIM3时钟
TIM3->PSC = 7200 - 1; // 预分频:72MHz / 7200 = 10kHz
TIM3->ARR = 10000 - 1; // 自动重装载:10kHz / 10000 = 1Hz
TIM3->DIER |= TIM_DIER_UIE; // 使能更新中断
NVIC_EnableIRQ(TIM3_IRQn); // 使能NVIC中断
TIM3->CR1 |= TIM_CR1_CEN; // 使能定时器
}
避坑指南:我曾经在配置定时器时忘了设置预分频器(PSC),结果定时器跑得飞快,中断频率完全不对。记住:PSC和ARR都要配置,缺一不可。另外,APB1和APB2的时钟频率可能不同,查数据手册确认。
4.3 PWM输出原理:假装输出模拟电压
PWM,全称Pulse Width Modulation,脉冲宽度调制。说白了,就是通过调节高电平占一个周期的时间比例,来“假装”输出一个模拟电压。
你想想看,LED灯接在GPIO上,要么亮要么灭。但如果让它以极快的速度闪烁,人眼就反应不过来了,看到的亮度就是平均亮度。占空比越大,灯越亮;占空比越小,灯越暗。
PWM的两个关键参数:
- 频率:周期时间的倒数,决定了PWM波的快慢
- 占空比:高电平时间占整个周期的比例
在STM32里,PWM是通过定时器的输出比较模式实现的。定时器计数到比较值时,输出电平翻转。我画了个图帮你理解:
配置PWM输出,我一般用定时器的PWM模式1或模式2。模式1是向上计数时,CNT < CCR时输出有效电平;模式2则相反。
// PWM配置示例:TIM2通道1输出PWM
void PWM_Init(void)
{
RCC->APB1ENR |= RCC_APB1ENR_TIM2EN; // 开启TIM2时钟
RCC->AHB1ENR |= RCC_AHB1ENR_GPIOAEN; // 开启GPIOA时钟
// 配置PA0为复用功能(TIM2_CH1)
GPIOA->MODER |= GPIO_MODER_MODER0_1; // 复用功能
GPIOA->AFR[0] |= 0x01 << (0 * 4); // AF1:TIM2
// 定时器配置
TIM2->PSC = 72 - 1; // 72MHz / 72 = 1MHz
TIM2->ARR = 1000 - 1; // 1MHz / 1000 = 1kHz PWM频率
// PWM模式配置
TIM2->CCMR1 |= TIM_CCMR1_OC1M_1 | TIM_CCMR1_OC1M_2; // PWM模式1
TIM2->CCMR1 |= TIM_CCMR1_OC1PE; // 预装载使能
TIM2->CCER |= TIM_CCER_CC1E; // 使能输出
TIM2->CR1 |= TIM_CR1_CEN; // 使能定时器
}
// 设置占空比
void PWM_SetDuty(uint16_t duty)
{
TIM2->CCR1 = duty; // 0~999
}
我的经验:PWM频率的选择有讲究。控制LED,1kHz就够了,太高了反而增加功耗。控制电机,20kHz左右比较合适,避免人耳听到啸叫。舵机一般是50Hz,占空比5%~10%对应0°~180°。
4.4 呼吸灯实现:把知识串起来
呼吸灯,就是让LED的亮度像呼吸一样慢慢变亮再慢慢变暗。说白了,就是PWM的占空比从0逐渐增加到100%,再从100%逐渐减小到0。
实现思路很简单:
- 配置一个定时器产生PWM
- 在定时器中断或主循环里,每隔一段时间改变CCR的值
- CCR从0递增到ARR,再从ARR递减到0
// 呼吸灯实现代码
uint16_t duty = 0;
uint8_t direction = 1; // 1:递增,0:递减
void TIM3_IRQHandler(void)
{
if (TIM3->SR & TIM_SR_UIF)
{
TIM3->SR = ~TIM_SR_UIF; // 清除中断标志
// 改变占空比
if (direction)
{
duty++;
if (duty >= 999) direction = 0;
}
else
{
duty--;
if (duty <= 0) direction = 1;
}
TIM2->CCR1 = duty; // 更新占空比
}
}
效果优化小技巧:人眼对亮度的感知不是线性的。如果你觉得呼吸灯变化不均匀,可以试试指数曲线。比如duty = (uint16_t)(1000 * sin(angle)),这样亮度变化会更自然。我在做智能台灯项目时就用过这个技巧,效果比线性变化好很多。
嗯,这里还要提醒一下:呼吸灯的周期不要太快,一般2~3秒一个完整呼吸比较舒服。太快了像闪烁,太慢了像没反应。你可以通过调整定时器中断的频率或者每次改变占空比的步长来控制呼吸速度。
我曾经在客户项目里做呼吸灯,一开始用的步长是1,结果呼吸一次要10秒,客户说“这灯是不是坏了”。后来把步长改成5,呼吸周期缩到2秒,效果就对了。所以调试时多试试不同参数,找到最舒服的节奏。
小结
定时器是STM32里最常用的外设之一。SysTick负责系统心跳,通用定时器负责各种定时和PWM任务。PWM的原理不复杂,就是快速开关信号来模拟模拟电压。呼吸灯是PWM的经典应用,把定时器和PWM的知识串在一起,做完你就掌握了定时器的核心用法。
实际项目中,定时器的应用远不止这些。电机控制、传感器采样、通信协议时序……几乎每个项目都离不开它。把这一节的内容吃透,后面遇到定时器相关的问题,你心里就有底了。
公众号:蓝海资料掘金营,微信deep3321