从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 PWMInput Capture direct mode 是两种截然不同的路径,选错了就无法工作。

2. 实战配置:在CubeMX中构建PWM与输入捕获共存的系统

理论说再多,不如动手配一遍。我们假设一个常见场景:使用TIM4的通道1(CH1)产生一路PWM驱动LED或电机,同时使用TIM4的通道2(CH2)测量另一路外部信号的频率。

2.1 时钟与引脚分配

首先,在CubeMX的Pinout & Configuration视图,完成系统时钟树(RCC)的配置,确保定时器有正确的时钟源。例如,如果TIM4挂载在APB1总线上,需要确认APB1的时钟频率。

接着分配引脚:

  1. 找到TIM4_CH1对应的引脚(例如PD12),将其功能设置为TIM4_CH1
  2. 找到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输出

    1. Channel1下拉菜单中选择PWM Generation CH1
    2. 下方参数窗口会出现PWM专属配置。Mode保持PWM mode 1Pulse是初始占空比对应的比较值(可后续动态修改),Fast ModeCH Polarity根据实际硬件需求选择(例如,驱动共阳极LED可能需要低电平有效)。
  • 通道2 (CH2) - 输入捕获

    1. Channel2下拉菜单中选择Input Capture direct mode
    2. 下方参数窗口变为输入捕获配置:
      • 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输出的稳定性)。解决方案:

  1. 使用DMA:如前所述,这是最优解。
  2. 提高滤波器(ICFilter)值:滤掉一些毛刺,但也会降低响应速度。
  3. 使用分频器(ICPrescaler):设置为DIV2DIV4,每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_periodoverflow_count,这是定位问题最快的方法。STM32的定时器是个强大的工具,理解其模块化设计,你就能像搭积木一样,灵活构建出满足复杂需求的高效控制系统。

Logo

智能硬件社区聚焦AI智能硬件技术生态,汇聚嵌入式AI、物联网硬件开发者,打造交流分享平台,同步全国赛事资讯、开展 OPC 核心人才招募,助力技术落地与开发者成长。

更多推荐