4、定时器与PWM应用:SysTick系统滴答定时器、通用定时器中断、PWM输出控制电磁锁蜂鸣器、输入捕获测量刷卡时长
定时器,说白了就是单片机的“心跳”。没有它,很多时序相关的功能都玩不转。我刚开始做门禁系统时,觉得定时器不就是计个数嘛,有啥难的?结果真到项目里,才发现这里面的门道不少。今天咱们就把定时器这块彻底捋清楚。
4.1 SysTick系统滴答定时器:操作系统的“节拍器”
SysTick,也叫系统滴答定时器。它是 Cortex-M 内核自带的,24位向下计数。你想想看,它最大的好处是什么?——不占用通用定时器资源。
我个人习惯,只要跑 RTOS,SysTick 必开。它负责给操作系统提供时间基准。比如 FreeRTOS 里的 vTaskDelay(),底层就是靠 SysTick 来计时的。
核心配置要点:
- 时钟源:一般选内核时钟(HCLK/8 或直接 HCLK)
- 重装载值:决定中断频率。比如 72MHz 主频,8分频后是 9MHz,要产生 1ms 中断,重装载值就是 9000-1
- 中断优先级:建议设成最低,别影响关键任务
// SysTick 配置示例(1ms 中断)
void SysTick_Init(void)
{
// 72MHz / 8 = 9MHz, 9MHz / 9000 = 1KHz = 1ms
if (SysTick_Config(SystemCoreClock / 8 / 1000))
{
// 配置失败,死循环
while(1);
}
}
// 中断服务函数
void SysTick_Handler(void)
{
static uint32_t tick = 0;
tick++;
// 这里可以放系统心跳任务
// 比如:按键扫描、LED闪烁计时等
}
避坑指南:我曾经在 SysTick 中断里放了一个延时函数,结果系统直接卡死。记住,中断服务函数里千万别放阻塞操作!SysTick 中断频率高,稍微卡一下,整个系统就崩了。
4.2 通用定时器中断:精准定时的“瑞士军刀”
通用定时器比 SysTick 灵活得多。它有多种工作模式:向上计数、向下计数、中央对齐。我项目中用得最多的是向上计数模式。
为什么需要通用定时器?因为 SysTick 只有一个,而且精度有限。比如你要产生一个 10us 的中断,SysTick 就搞不定了。这时候就得请出通用定时器。
| 参数 | SysTick | 通用定时器 |
|---|---|---|
| 位数 | 24位 | 16位/32位 |
| 精度 | ms级 | us级甚至ns级 |
| 通道数 | 1个 | 4个(可配置PWM/捕获) |
| 适用场景 | 系统心跳 | PWM、输入捕获、精准延时 |
// 通用定时器中断配置(TIM3,1ms中断)
void TIM3_Init(void)
{
TIM_TimeBaseInitTypeDef TIM_TimeBaseStructure;
// 使能时钟
RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM3, ENABLE);
// 72MHz / 72 = 1MHz, 1MHz / 1000 = 1KHz = 1ms
TIM_TimeBaseStructure.TIM_Prescaler = 72 - 1; // 预分频
TIM_TimeBaseStructure.TIM_Period = 1000 - 1; // 自动重装载
TIM_TimeBaseStructure.TIM_CounterMode = TIM_CounterMode_Up;
TIM_TimeBaseInit(TIM3, &TIM_TimeBaseStructure);
// 使能中断
TIM_ITConfig(TIM3, TIM_IT_Update, ENABLE);
TIM_Cmd(TIM3, ENABLE);
}
void TIM3_IRQHandler(void)
{
if (TIM_GetITStatus(TIM3, TIM_IT_Update) != RESET)
{
// 用户代码:比如蜂鸣器状态翻转
TIM_ClearITPendingBit(TIM3, TIM_IT_Update);
}
}
注意:预分频器和自动重装载值的计算要仔细。我见过有人把预分频设成 0,结果定时器直接按 72MHz 跑,1ms 中断变成了 0.013us,CPU 全耗在中断里了。
4.3 PWM输出:控制电磁锁与蜂鸣器
PWM,脉冲宽度调制。说白了就是通过调节占空比来控制输出功率。在门禁系统里,PWM 有两个典型应用:电磁锁的电压控制和蜂鸣器的音调控制。
电磁锁这玩意儿,你想想看,它需要持续供电才能保持吸合。但长时间满功率供电,线圈发热严重。我之前的项目就遇到过,锁体温度能到 70 度,差点把外壳烤变形。
解决方案?用 PWM 控制!上电瞬间给 100% 占空比,确保锁舌吸合。稳定后降到 50% 占空比,保持吸力同时大幅降低功耗和发热。
// PWM 输出配置(TIM2_CH1,PA0)
void PWM_Init(void)
{
GPIO_InitTypeDef GPIO_InitStructure;
TIM_TimeBaseInitTypeDef TIM_TimeBaseStructure;
TIM_OCInitTypeDef TIM_OCInitStructure;
// 使能时钟
RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM2, ENABLE);
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE);
// 配置 PA0 为复用推挽输出
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_0;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP;
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(GPIOA, &GPIO_InitStructure);
// 定时器配置:72MHz / 72 = 1MHz, 周期 = 1000, PWM频率 = 1KHz
TIM_TimeBaseStructure.TIM_Prescaler = 72 - 1;
TIM_TimeBaseStructure.TIM_Period = 1000 - 1;
TIM_TimeBaseStructure.TIM_CounterMode = TIM_CounterMode_Up;
TIM_TimeBaseInit(TIM2, &TIM_TimeBaseStructure);
// PWM1 模式:CNT < CCR 时输出有效电平
TIM_OCInitStructure.TIM_OCMode = TIM_OCMode_PWM1;
TIM_OCInitStructure.TIM_OutputState = TIM_OutputState_Enable;
TIM_OCInitStructure.TIM_Pulse = 500; // 初始占空比 50%
TIM_OCInitStructure.TIM_OCPolarity = TIM_OCPolarity_High;
TIM_OC1Init(TIM2, &TIM_OCInitStructure);
TIM_Cmd(TIM2, ENABLE);
}
// 设置占空比函数
void PWM_SetDuty(uint16_t duty)
{
if (duty > 1000) duty = 1000;
TIM_SetCompare1(TIM2, duty);
}
经验之谈:蜂鸣器的 PWM 频率建议设在 2KHz-4KHz 之间。太低会有“哒哒哒”的噪音,太高人耳听不见。我一般用 3KHz,占空比 50%,声音清脆又不刺耳。
4.4 输入捕获:测量刷卡时长
输入捕获,这功能很有意思。它能精确测量外部信号的脉宽或周期。在门禁系统里,我用来测量刷卡时长。
为什么测刷卡时长?因为有些用户刷卡太快,读卡器还没反应过来就移开了。通过输入捕获,我们可以统计每次刷卡的有效时长,低于某个阈值就提示“请重新刷卡”。
工作原理很简单:定时器检测到上升沿时,把当前计数值存到捕获寄存器。检测到下降沿时,再存一次。两次的差值就是高电平脉宽。
// 输入捕获配置(TIM4_CH1,PB6)
void InputCapture_Init(void)
{
GPIO_InitTypeDef GPIO_InitStructure;
TIM_TimeBaseInitTypeDef TIM_TimeBaseStructure;
TIM_ICInitTypeDef TIM_ICInitStructure;
// 使能时钟
RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM4, ENABLE);
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB, ENABLE);
// 配置 PB6 为浮空输入
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_6;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IN_FLOATING;
GPIO_Init(GPIOB, &GPIO_InitStructure);
// 定时器配置:72MHz / 72 = 1MHz, 每个计数 = 1us
TIM_TimeBaseStructure.TIM_Prescaler = 72 - 1;
TIM_TimeBaseStructure.TIM_Period = 0xFFFF; // 最大周期
TIM_TimeBaseStructure.TIM_CounterMode = TIM_CounterMode_Up;
TIM_TimeBaseInit(TIM4, &TIM_TimeBaseStructure);
// 配置输入捕获通道1
TIM_ICInitStructure.TIM_Channel = TIM_Channel_1;
TIM_ICInitStructure.TIM_ICPolarity = TIM_ICPolarity_Rising; // 上升沿捕获
TIM_ICInitStructure.TIM_ICSelection = TIM_ICSelection_DirectTI;
TIM_ICInitStructure.TIM_ICPrescaler = TIM_ICPSC_DIV1;
TIM_ICInitStructure.TIM_ICFilter = 0x0;
TIM_ICInit(TIM4, &TIM_ICInitStructure);
// 使能捕获中断
TIM_ITConfig(TIM4, TIM_IT_CC1, ENABLE);
TIM_Cmd(TIM4, ENABLE);
}
// 中断服务函数
uint32_t capture_value = 0;
uint8_t capture_flag = 0;
void TIM4_IRQHandler(void)
{
if (TIM_GetITStatus(TIM4, TIM_IT_CC1) != RESET)
{
capture_value = TIM_GetCapture1(TIM4);
capture_flag = 1;
TIM_ClearITPendingBit(TIM4, TIM_IT_CC1);
}
}
// 主循环中判断刷卡时长
void CheckCardSwipeDuration(void)
{
if (capture_flag)
{
capture_flag = 0;
// capture_value 单位是 us
// 比如要求刷卡时长 > 50ms = 50000us
if (capture_value < 50000)
{
// 刷卡太快,提示重新刷卡
Buzzer_Beep(3); // 蜂鸣器响3声提示
}
else
{
// 刷卡有效,执行开门逻辑
Door_Unlock();
}
}
}
曾经踩过的坑:输入捕获的极性配置要特别注意。我一开始设成了下降沿捕获,结果测出来的脉宽全是 0。后来才发现,信号过来时第一个边沿是上升沿,捕获根本没触发。记住:先确定信号的有效边沿,再配置极性。
4.5 综合应用:定时器与PWM的协同工作
实际项目中,这些定时器功能往往是配合使用的。我给你说个典型场景:
- SysTick 提供系统心跳,每 1ms 扫描一次按键状态
- 通用定时器 产生 100Hz 中断,用于电磁锁的 PWM 占空比渐变控制(上电时从 0% 平滑升到 100%)
- PWM 输出 直接驱动电磁锁和蜂鸣器
- 输入捕获 监测刷卡信号,判断刷卡是否有效
你想想看,如果没有定时器,这些功能全得靠软件延时来实现。CPU 会被占得死死的,啥也干不了。有了定时器,CPU 就能腾出手来处理更复杂的逻辑,比如联网通信、数据加密等。
总结一下:
- SysTick:系统级定时,适合做 RTOS 心跳或简单延时
- 通用定时器:灵活多变,PWM、输入捕获、精准中断都能干
- PWM:控制功率输出,省电又高效
- 输入捕获:测量外部信号,精度可达微秒级
记住一句话:能用硬件定时器解决的问题,千万别用软件延时。这是嵌入式开发的黄金法则。
好了,定时器这块就聊到这儿。下一章咱们开始讲串口通信,这可是门禁系统和上位机沟通的桥梁,到时候见。