第3章 定时器详解:SysTick、基本定时器、PWM输出、输入捕获
定时器这东西,说白了就是单片机的「心跳」。我刚开始做嵌入式那会儿,总觉得定时器不就是计个数嘛,有啥好研究的?直到有一次做电机控制项目,PWM频率算错了,电机嗡嗡响就是不转……嗯,从那以后我老老实实把STM32的定时器家族捋了一遍。
这一章,咱们把四种最常用的定时器模式讲透:SysTick、基本定时器、PWM输出、输入捕获。你学完就能自己搭一个精准的定时框架。
3.1 SysTick:系统的心脏起搏器
SysTick是Cortex-M内核自带的24位递减计数器。它不依赖任何外设时钟树,内核上电它就存在。我个人习惯把SysTick叫做「系统的心跳」——FreeRTOS的任务调度、HAL_Delay()、各种超时检测,全指着它。
配置SysTick其实就三步:
- 设置重装载值(LOAD寄存器)
- 清除当前计数值(VAL寄存器写0)
- 使能中断并启动计数器
重装载值怎么算?公式很简单:
重装载值 = (系统时钟频率 / 目标中断频率) - 1
// 举例:HCLK=72MHz,想要1ms中断一次
// 72,000,000 / 1000 - 1 = 71999
SysTick->LOAD = 71999;
SysTick->VAL = 0;
SysTick->CTRL = 0x07; // 使能 + 中断使能 + 使用HCLK
为什么减1?因为计数器从LOAD值减到0,一共是LOAD+1个时钟周期。这个坑我踩过——第一次写的时候没减1,结果中断频率快了0.0014%,虽然不大,但做精准时间同步时就露馅了。
3.2 基本定时器:精准延时的底气
基本定时器(TIM6/TIM7)是STM32定时器家族里最「单纯」的成员——只有向上计数,没有外部引脚,没有PWM,没有捕获。但它有一个绝活:16位自动重装载+16位可编程预分频器。
预分频器是个好东西。它可以把计数器时钟再分一次,让你在低频场景下也能用上定时器。举个例子:
// 配置TIM6,产生1Hz中断
// 假设APB1时钟=36MHz
TIM6->PSC = 35999; // 预分频:36MHz / (35999+1) = 1kHz
TIM6->ARR = 999; // 自动重装载:1kHz / (999+1) = 1Hz
TIM6->CR1 |= TIM_CR1_CEN; // 启动
这里有个细节:预分频器是缓冲型的。你写PSC寄存器后,它不会立刻生效,要等到当前计数周期结束才更新。我刚开始不知道这个,调试时发现频率不对,查了半天手册……
| 定时器类型 | 计数器位数 | 预分频器 | 外部引脚 | 典型用途 |
|---|---|---|---|---|
| SysTick | 24位 | 无(固定HCLK) | 无 | RTOS心跳、HAL_Delay |
| 基本定时器 | 16位 | 16位可编程 | 无 | 精准延时、时基生成 |
| 通用定时器 | 16位 | 16位可编程 | 有(4通道) | PWM、输入捕获、编码器 |
| 高级定时器 | 16位 | 16位可编程 | 有(带互补输出) | 电机控制、三相PWM |
3.3 PWM输出:让硬件帮你干活
PWM输出,说白了就是让定时器在某个比较值处翻转电平。你想想看,如果让CPU用GPIO翻转来做PWM,频率稍微高一点CPU就啥也干不了了。而硬件PWM——配置好之后,CPU可以睡觉,PWM自己跑。
配置PWM的核心是三个寄存器:
- ARR(自动重装载):决定PWM周期
- CCR(捕获/比较):决定占空比
- CCER(捕获/比较使能):决定输出极性
占空比的计算:占空比 = CCR / (ARR + 1) × 100%
// 配置TIM2_CH1输出50Hz PWM,占空比50%
// APB1时钟=36MHz,预分频后得到1kHz的计数器时钟
TIM2->PSC = 35999; // 36MHz / 36000 = 1kHz
TIM2->ARR = 19; // 1kHz / 20 = 50Hz
TIM2->CCR1 = 9; // 占空比 = 10/20 = 50%
// 配置通道1为PWM1模式(向上计数时,CNT<CCR输出有效)
TIM2->CCMR1 |= (6 << 4); // OC1M = 110 (PWM模式1)
TIM2->CCER |= TIM_CCER_CC1E; // 使能输出
TIM2->CR1 |= TIM_CR1_CEN; // 启动定时器
PWM频率和分辨率是矛盾的。举个例子:
- 想要20kHz的PWM(电机控制常用),ARR=1800(72MHz/40000-1),分辨率约11位
- 想要1kHz的PWM(LED调光常用),ARR=71999,分辨率约16位
分辨率越高,调光越细腻。但电机控制需要高频率来降低噪音——这就是个取舍。
3.4 输入捕获:测量外部信号的脉搏
输入捕获,就是定时器「偷看」外部信号的能力。当检测到指定的边沿(上升沿或下降沿)时,定时器会把当前CNT的值「咔嚓」一下锁存到CCR寄存器里。你连续捕获两次,一减,就知道信号的周期了。
测量频率的标准流程:
- 配置通道为输入捕获模式,选择上升沿触发
- 使能捕获中断
- 第一次捕获:记录CCR1值
- 第二次捕获:记录CCR2值,计算差值
- 频率 = 定时器时钟 / 差值
// 配置TIM3_CH1为输入捕获,上升沿触发
TIM3->CCMR1 |= TIM_CCMR1_CC1S_0; // CC1通道配置为输入
TIM3->CCMR1 &= ~TIM_CCMR1_IC1F; // 不滤波
TIM3->CCER |= TIM_CCER_CC1P; // 上升沿触发(注意:CC1P=0是上升沿)
TIM3->DIER |= TIM_DIER_CC1IE; // 使能捕获中断
TIM3->CR1 |= TIM_CR1_CEN; // 启动
// 中断服务函数中
void TIM3_IRQHandler(void) {
if(TIM3->SR & TIM_SR_CC1IF) {
uint16_t capture = TIM3->CCR1;
// 计算与上一次捕获的差值
// 注意处理计数器溢出
TIM3->SR = ~TIM_SR_CC1IF; // 清除标志
}
}
测量脉宽(占空比)需要两个边沿:
- 配置通道捕获上升沿,记录T1
- 切换边沿极性,捕获下降沿,记录T2
- 高电平时间 = T2 - T1
或者用两个通道:一个捕获上升沿,一个捕获下降沿。我个人喜欢用后者,省去切换极性的麻烦。
3.5 定时器中断优先级:别让RTOS饿死
最后聊一个容易被忽略的问题:中断优先级。SysTick的优先级在FreeRTOS里是固定的——configKERNEL_INTERRUPT_PRIORITY。但其他定时器的优先级你得自己配。
我的原则是:
- SysTick:最高优先级(FreeRTOS调度依赖它)
- PWM相关中断:中等优先级(占空比更新不及时最多波形抖一下)
- 输入捕获:较高优先级(丢失边沿等于丢失数据)
配置优先级用HAL库的HAL_NVIC_SetPriority(),或者直接操作NVIC寄存器。记住:STM32只使用优先级寄存器的高4位,低4位是保留的。
// 设置TIM3中断优先级为5(0最高,15最低)
HAL_NVIC_SetPriority(TIM3_IRQn, 5, 0);
HAL_NVIC_EnableIRQ(TIM3_IRQn);