4. 定时器与PWM:系统滴答定时器、通用定时器配置、PWM输出驱动蜂鸣器/电机
定时器,说白了就是嵌入式系统的“心跳”。没有它,你连个精准的延时都做不了。我刚开始做医疗设备时,总觉得定时器就是个计数的玩意儿,后来才发现——它才是整个系统实时性的基石。
这一章,咱们就聊聊三种最常用的定时器:系统滴答定时器、通用定时器,还有PWM输出。我会结合医疗设备里的实际场景,比如驱动蜂鸣器报警、控制微型电机,把配置思路和坑都讲清楚。
- SysTick 定时器:操作系统的“节拍器”
- 通用定时器:精准延时与事件计数
- PWM 输出:用占空比控制蜂鸣器音量和电机转速
- 实战案例:医疗输液泵报警蜂鸣器驱动
4.1 系统滴答定时器(SysTick)
SysTick 是 Cortex-M 内核自带的 24 位递减计数器。它不依赖外设时钟,所以移植性极好。我习惯把它配置成 1ms 中断一次,作为整个系统的时基。
为什么是 1ms?因为人耳能分辨的延时大约在 10ms 级别,1ms 的精度足够覆盖大部分传感器采样和通信协议的需求。你想想看,心电信号采样率也就 250Hz 到 1000Hz,1ms 的节拍完全够用。
配置代码其实很简单,以 STM32 为例:
// SysTick 配置函数,重装载值 = 系统时钟 / 1000
void SysTick_Init(uint32_t sysclk_hz)
{
// 重装载值:例如 72MHz 时钟下,72 000 000 / 1000 = 72000
SysTick->LOAD = sysclk_hz / 1000 - 1;
// 当前值清零
SysTick->VAL = 0;
// 使能 SysTick,使能中断,使用处理器时钟
SysTick->CTRL = SysTick_CTRL_ENABLE_Msk |
SysTick_CTRL_TICKINT_Msk |
SysTick_CTRL_CLKSOURCE_Msk;
}
// 1ms 中断服务函数
void SysTick_Handler(void)
{
// 只做一件事:置位 1ms 标志
systick_flag = 1;
}
我曾经在一个心电监护仪项目里,因为 SysTick 中断里做了太多浮点运算,导致系统响应变慢。后来把所有计算都挪到主循环,问题就解决了。嗯,这里要注意——中断里别干重活。
4.2 通用定时器配置
通用定时器比 SysTick 灵活得多。它可以向上计数、向下计数、或者中心对齐计数。我常用的场景有两个:
- 精准延时:用定时器做微秒级延时,比软件循环靠谱多了。
- 事件计数:比如统计单位时间内的心跳脉冲数。
配置通用定时器,核心就是三个参数:预分频器(PSC)、自动重装载值(ARR)、计数模式。它们的关系是:
| 参数 | 作用 | 计算公式 |
|---|---|---|
| PSC(预分频器) | 将时钟源分频 | 定时器时钟 = 系统时钟 / (PSC + 1) |
| ARR(自动重装载值) | 设定计数上限 | 溢出时间 = (ARR + 1) / 定时器时钟 |
| 计数模式 | 向上/向下/中心对齐 | 向上模式最常用 |
举个例子,我想产生一个 100ms 的定时中断。系统时钟 72MHz,预分频器设为 7200-1,那么定时器时钟就是 72MHz / 7200 = 10kHz。ARR 设为 1000-1,溢出时间就是 1000 / 10kHz = 100ms。
// 通用定时器 TIM3 初始化,100ms 中断
void TIM3_Init(void)
{
RCC->APB1ENR |= RCC_APB1ENR_TIM3EN; // 使能 TIM3 时钟
TIM3->PSC = 7200 - 1; // 预分频,得到 10kHz
TIM3->ARR = 1000 - 1; // 自动重装载值,100ms 溢出
TIM3->CR1 |= TIM_CR1_CEN; // 使能计数器
// 使能更新中断
TIM3->DIER |= TIM_DIER_UIE;
NVIC_EnableIRQ(TIM3_IRQn);
}
4.3 PWM 输出:驱动蜂鸣器与电机
PWM,说白了就是通过调节占空比来模拟模拟量。占空比越高,蜂鸣器越响,电机转得越快。我做过一个输液泵项目,就是用 PWM 控制微型直流电机,实现精准的输液速度调节。
配置 PWM 的关键步骤:
- 选择定时器通道:每个通用定时器有 4 个通道,可以独立输出 PWM。
- 配置 GPIO 复用功能:把引脚设为定时器输出模式。
- 设置 PWM 模式:常用模式 1 或模式 2,区别在于有效电平的极性。
- 设定频率和占空比:频率由 ARR 决定,占空比由 CCRx 决定。
来看一个驱动蜂鸣器的例子。蜂鸣器频率一般设在 2kHz 到 4kHz 之间,人耳最敏感。我习惯用 3kHz:
// TIM2 通道 1 输出 PWM,驱动蜂鸣器
// 系统时钟 72MHz,目标频率 3kHz,占空比 50%
void Buzzer_PWM_Init(void)
{
// 1. 使能 TIM2 时钟
RCC->APB1ENR |= RCC_APB1ENR_TIM2EN;
// 2. 配置 GPIOA0 为复用推挽输出
RCC->AHB1ENR |= RCC_AHB1ENR_GPIOAEN;
GPIOA->MODER |= GPIO_MODER_MODER0_1; // 复用功能
GPIOA->AFR[0] |= 1 << 0; // AF1 = TIM2_CH1
// 3. 计算参数:PSC=0, ARR=24000-1, 频率=72MHz/24000=3kHz
TIM2->PSC = 0;
TIM2->ARR = 24000 - 1;
// 4. 配置通道 1 为 PWM 模式 1,输出极性高
TIM2->CCMR1 |= (6 << TIM_CCMR1_OC1M_Pos); // PWM 模式 1
TIM2->CCMR1 |= TIM_CCMR1_OC1PE; // 预装载使能
TIM2->CCER |= TIM_CCER_CC1E; // 使能输出
// 5. 设置占空比 50%:CCR1 = ARR/2 = 12000
TIM2->CCR1 = 12000;
// 6. 使能计数器
TIM2->CR1 |= TIM_CR1_CEN;
}
// 调节蜂鸣器音量:占空比 0~100
void Buzzer_SetVolume(uint8_t percent)
{
if(percent > 100) percent = 100;
TIM2->CCR1 = (uint32_t)(TIM2->ARR) * percent / 100;
}
4.4 避坑指南
做定时器和 PWM 开发,有几个坑我踩过,分享给你:
- 预分频器更新时机:修改 PSC 后,要等下一个更新事件才会生效。我习惯在修改后手动产生一次软件更新。
- 占空比突变:电机或蜂鸣器的占空比不要从 0 直接跳到 100,容易产生电流尖峰。加一个渐变函数,比如每次增加 5%。
- 中断优先级:SysTick 中断优先级不要设得太高,否则会阻塞其他外设中断。我一般设成最低优先级。
- PWM 输出引脚:确认引脚是否支持定时器复用功能。我见过有人把 PWM 配置到普通 GPIO 上,折腾了半天才发现。
好了,定时器和 PWM 的内容就这些。记住一个原则:定时器是系统的骨架,PWM 是执行器的语言。把这两样玩熟了,医疗设备里的时序控制和驱动问题,基本就解决了一大半。