从PWM到频率计:STM32定时器捕获模式的两种经典应用对比
从PWM到频率计:STM32定时器捕获模式的两种经典应用对比
如果你正在用STM32做电机控制、电源管理或者信号分析,大概率绕不开两个核心需求:生成精准的PWM波形,以及测量外部信号的频率。有意思的是,在STM32的世界里,这两件看似独立的任务,背后依赖的其实是同一个硬件模块——通用定时器(TIM)。同一个外设,通过不同的配置,就能在“输出”和“输入”两种角色间自如切换,这种设计思想本身就充满了工程师的智慧。
今天,我们不打算重复那些基础教程,而是想深入聊聊,当你手头资源紧张,需要让一个定时器同时肩负PWM输出和频率测量双重使命时,该如何思考和操作。这不仅仅是配置几个寄存器那么简单,它涉及到对定时器内部结构、中断冲突、资源复用的深度理解。我会结合实际的CubeMX配置和代码案例,拆解其中的关键点,帮你避开那些我踩过的坑。
1. 理解核心:定时器的模块化与多面手本质
STM32的通用定时器,远不止一个简单的计数器。它是一个高度模块化、可配置的信号处理单元。理解这一点,是玩转它的前提。
从结构上看,一个完整的通用定时器(如TIM2、TIM3、TIM4等)通常包含以下几个关键部分:
- 时基单元:这是心脏,由预分频器(PSC)、计数器(CNT)和自动重载寄存器(ARR)组成,负责产生最基础的计时节拍。
- 捕获/比较通道:这是实现多功能的双手。每个通道都可以独立配置为输入捕获(抓取外部信号时间点)或输出比较(产生特定波形)。
- 编码器接口:专为电机控制设计的“特化技能”。
- 触发控制器与从模式:这是实现硬件级自动化的“智能管家”,允许一个事件自动触发另一个操作,无需CPU干预。
当我们谈论PWM输出时,本质是利用了输出比较模式。定时器按照ARR设定的周期循环计数,当CNT的值与某个通道的比较寄存器(CCR)匹配时,根据配置翻转引脚电平,从而产生占空比可调的方波。
而频率测量,则切换到输入捕获模式。外部信号的边沿触发捕获事件,将此刻的CNT值锁存到CCR中。通过计算连续两个上升沿捕获到的CNT差值,就能推算出信号的周期,进而得到频率。
关键在于:一个定时器的多个通道是相互独立的。这意味着,通道1可以配置为PWM输出,同时通道2可以配置为输入捕获,它们共享同一个时基(CNT),但工作互不干扰。这种共享时基的特性,反而为一些同步测量场景提供了便利。
提示:在CubeMX中配置时,务必理清每个通道的“方向”。
Output Compare PWM和Input Capture direct mode是两种截然不同的路径,选错了就无法工作。
2. 实战配置:在CubeMX中构建PWM与输入捕获共存的系统
理论说再多,不如动手配一遍。我们假设一个常见场景:使用TIM4的通道1(CH1)产生一路PWM驱动LED或电机,同时使用TIM4的通道2(CH2)测量另一路外部信号的频率。
2.1 时钟与引脚分配
首先,在CubeMX的Pinout & Configuration视图,完成系统时钟树(RCC)的配置,确保定时器有正确的时钟源。例如,如果TIM4挂载在APB1总线上,需要确认APB1的时钟频率。
接着分配引脚:
- 找到TIM4_CH1对应的引脚(例如PD12),将其功能设置为
TIM4_CH1。 - 找到TIM4_CH2对应的引脚(例如PD13),将其功能设置为
TIM4_CH2。
2.2 定时器基础参数配置
在左侧Timers分类下点击TIM4,进行全局配置:
- 时基参数:
Prescaler (PSC - 16 bits value):预分频值。这决定了CNT的计数频率。公式为:计数频率 = 定时器时钟 / (PSC + 1)。例如,定时器时钟为84MHz,PSC设为83,则计数频率为1MHz(即每个计数代表1微秒)。Counter Mode:计数模式。通常选择Up(向上计数)。Counter Period (AutoReload Register - 16 bits value):自动重装载值ARR。这决定了PWM的周期和输入捕获的最大测量范围。对于16位定时器,最大值为65535。auto-reload preload:建议使能(Enable),确保ARR的更新在下一个更新事件生效,避免当前周期波形畸变。
这里的选择需要权衡:PWM的精度和频率测量的量程。计数频率越高,PWM分辨率越高,但ARR不变的情况下周期越短;同时,输入捕获能测量的最高频率也越高(因为CNT跑得更快),但测量低频信号时更容易溢出(CNT计满归零)。通常需要根据你的核心需求来折中。
2.3 通道独立配置
这是最关键的一步,两个通道要分开设置。
-
通道1 (CH1) - PWM输出:
- 在
Channel1下拉菜单中选择PWM Generation CH1。 - 下方参数窗口会出现PWM专属配置。
Mode保持PWM mode 1,Pulse是初始占空比对应的比较值(可后续动态修改),Fast Mode和CH Polarity根据实际硬件需求选择(例如,驱动共阳极LED可能需要低电平有效)。
- 在
-
通道2 (CH2) - 输入捕获:
- 在
Channel2下拉菜单中选择Input Capture direct mode。 - 下方参数窗口变为输入捕获配置:
IC Selection:选择Direct(直连模式),信号从本通道引脚直接进入。ICPolarity:选择Rising Edge(上升沿捕获)用于测频率。若需测占空比,则需在代码中动态切换边沿。IC Selection:与上一条的IC Selection是同一设置,确认是Direct即可。ICPrescaler:分频器。选择DIV1(每次边沿都捕获),这是最常用的测频设置。ICFilter:滤波器值。如果输入信号有毛刺,可以适当设置(如0x8)进行数字滤波,平滑信号。
- 在
2.4 中断与DMA配置
- 中断:要使输入捕获工作,必须开启捕获中断。在
NVIC Settings标签页,勾选TIM4 global interrupt使能全局中断。PWM输出通常不需要中断。 - DMA(可选的高级技巧):如果你测量的频率很高,或者想彻底解放CPU,可以考虑使用DMA来搬运捕获值。在
DMA Settings标签页,点击Add,选择TIM4_CH2(或TIM4_UP用于溢出计数),配置DMA为循环模式(Circular),将外设地址(CCR2寄存器)的数据搬运到用户定义的数组中。这样,CPU只需要定期处理数组中的数据即可,无需进入每次捕获中断。
完成以上步骤后,生成代码。CubeMX会为你初始化好这一切硬件配置。
3. 代码实现:处理冲突与实现精准测量
生成的代码搭建了舞台,真正的演出还需要我们编写。这里有几个核心函数和避坑点。
3.1 启动外设
在main.c的初始化部分(/* USER CODE BEGIN 2 */之后),启动定时器和相应通道:
/* 启动PWM输出 */
HAL_TIM_PWM_Start(&htim4, TIM_CHANNEL_1);
/* 以中断方式启动输入捕获 */
HAL_TIM_IC_Start_IT(&htim4, TIM_CHANNEL_2);
/* 如果需要处理计数器溢出,还需开启更新中断 */
__HAL_TIM_ENABLE_IT(&htim4, TIM_IT_UPDATE);
3.2 输入捕获中断回调函数
这是频率测量的核心。在main.c文件末尾的用户代码区(/* USER CODE BEGIN 4 */)重写回调函数:
// 定义全局变量用于计算
volatile uint32_t capture_prev_value = 0;
volatile uint32_t capture_period = 0;
volatile uint8_t is_first_capture = 1;
void HAL_TIM_IC_CaptureCallback(TIM_HandleTypeDef *htim)
{
if (htim->Instance == TIM4)
{
uint32_t current_capture = HAL_TIM_ReadCapturedValue(htim, TIM_CHANNEL_2);
if (is_first_capture)
{
// 第一次捕获,只记录值,不计算
capture_prev_value = current_capture;
is_first_capture = 0;
}
else
{
// 计算两次捕获的差值(周期)
if (current_capture > capture_prev_value)
{
capture_period = current_capture - capture_prev_value;
}
else
{
// 处理计数器溢出(CNT从ARR回到0)
capture_period = (htim4.Instance->ARR - capture_prev_value) + current_capture + 1;
}
// 更新前一次值,为下次计算准备
capture_prev_value = current_capture;
// 此时 capture_period 就是信号周期的计数值
// 可以在此计算频率,或设置标志位让主循环处理
}
}
}
3.3 处理溢出与提高精度
上面的代码简单处理了溢出,但只适用于溢出一次的情况。对于极低频率的信号,可能发生多次溢出。更健壮的方法是结合更新(溢出)中断:
volatile uint16_t overflow_count = 0;
// 更新中断回调(在同一个用户代码区)
void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim)
{
if (htim->Instance == TIM4)
{
overflow_count++;
}
}
然后在捕获回调中,计算周期时需要将溢出次数考虑进去:总周期 = overflow_count * (ARR + 1) + 本次捕获差值。计算完成后,记得将overflow_count清零。
为了提高测量精度,尤其是对抗信号抖动,可以采用多次测量取平均的策略。例如,连续捕获N个上升沿,计算N-1个周期的平均值。
| 策略 | 优点 | 缺点 | 适用场景 |
|---|---|---|---|
| 单次捕获 | 响应快,代码简单 | 易受单次抖动影响,精度低 | 对实时性要求高,信号干净 |
| 滑动平均 | 平滑效果好,精度提升 | 引入一定延迟,占用内存 | 大多数通用测量场景 |
| 中位值平均 | 抗突发干扰能力强 | 计算量稍大,需要排序 | 工业环境等干扰较大场合 |
3.4 动态调整与多任务复用
一个更进阶的场景是:PWM的频率需要根据测量到的外部频率进行动态调整。这就需要在主循环或定时中断中,根据计算出的capture_period,动态修改TIM4的ARR或CH1的CCR值。
// 示例:根据测量频率调整PWM占空比(简单比例控制)
void adjust_pwm_based_on_frequency(void)
{
if (capture_period != 0)
{
float measured_freq = (float)SystemCoreClock / (htim4.Init.Prescaler + 1) / capture_period;
// 假设我们想将PWM占空比设置为与测量频率成某种关系
uint32_t new_pulse = (uint32_t)(measured_freq / 100.0f); // 示例计算
if (new_pulse > htim4.Instance->ARR)
{
new_pulse = htim4.Instance->ARR;
}
__HAL_TIM_SET_COMPARE(&htim4, TIM_CHANNEL_1, new_pulse);
}
}
重要提醒:在修改ARR或PSC等时基参数时,如果PWM正在输出,为了波形连续,最好先停止定时器(HAL_TIM_PWM_Stop),修改参数,再重新启动。而修改CCR(占空比)则可以在运行时直接进行,影响的是下一个PWM周期。
4. 性能优化与陷阱规避
当PWM和输入捕获在同一个定时器上高强度工作时,有些陷阱需要警惕。
陷阱一:中断风暴。如果测量的信号频率本身很高(比如几十kHz),每次边沿都触发中断,CPU将疲于奔命,严重影响其他任务(包括你PWM输出的稳定性)。解决方案:
- 使用DMA:如前所述,这是最优解。
- 提高滤波器(ICFilter)值:滤掉一些毛刺,但也会降低响应速度。
- 使用分频器(ICPrescaler):设置为
DIV2或DIV4,每2个或4个边沿才捕获一次,牺牲分辨率换取CPU时间。
陷阱二:寄存器访问冲突。虽然通道独立,但读写同一个定时器的寄存器(如SR状态寄存器)时,在中断和主循环间可能存在竞争。确保对关键变量的访问是原子的(如使用__disable_irq()和__enable_irq()临时关中断),或者使用标志位进行异步通信。
陷阱三:测量范围与精度矛盾。这是硬件限制。一个16位定时器,在1MHz计数频率下,测量1Hz信号(周期1秒)需要CNT计100万个值,这远远超过了ARR的最大值65535,必然频繁溢出。此时,要么换用32位定时器(如TIM2/TIM5在某些系列中),要么降低计数频率(增大PSC),但这样又会降低对高频信号的分辨率。务必根据你的目标频率范围,预先计算并选择合适的PSC和ARR。
一个实用的选型参考:
| 待测信号频率范围 | 推荐定时器类型 | 关键配置思路 |
|---|---|---|
| 高频(>100kHz) | 任意16/32位定时器 | 使用高计数频率(小PSC),注意中断/DMA负担。 |
| 中频(1kHz~100kHz) | 16位定时器 | 平衡PSC设置,使ARR在中间值,兼顾高低频。 |
| 低频(<1kHz) | 32位定时器 | 使用低计数频率(大PSC),或必须启用溢出计数。 |
最后,调试阶段,善用示波器。同时观察PWM输出引脚和输入捕获引脚,验证波形是否正常,测量值是否准确。利用串口打印关键的中间变量,如capture_period、overflow_count,这是定位问题最快的方法。STM32的定时器是个强大的工具,理解其模块化设计,你就能像搭积木一样,灵活构建出满足复杂需求的高效控制系统。
更多推荐


所有评论(0)