云计算百科
云计算领域专业知识百科平台

STM32定时器输入捕获测PWM频率与占空比:选型、避坑与实测数据

文章目录

    • 一、为什么测个 PWM 也要认真选型
    • 二、输入捕获测频原理与方案对比
      • 2.1 硬件是怎么"抓住"边沿的
      • 2.2 三种测频方案怎么选
      • 2.3 计算公式与误差来源
      • 2.4 占空比为什么必须双沿捕获
    • 三、硬件选型与信号源搭建
      • 3.1 为什么用板载 TIM3 当信号源
      • 3.2 器件清单与接线
    • 四、CubeMX 关键配置与易错点
    • 五、核心代码实现
      • 5.1 捕获数据结构与中断入口
      • 5.2 捕获回调:周期与高电平的合成
      • 5.3 超时检测与中值滤波
      • 5.4 结果计算与信号源初始化
    • 六、测试验证
      • 6.1 功能验证
      • 6.2 全量程频率测试
      • 6.3 占空比测试
      • 6.4 理论值与实测值对照
    • 七、一次完整的干扰排查记录
    • 八、故障排查手册
    • 九、总结

摘要:电机调速、开关电源、红外遥控解码等应用都需要精确测量 PWM 信号的频率与占空比,软件轮询方式在中高频段误差大、还会阻塞主循环。本文基于 STM32F103C8T6 与 HAL 库,用 TIM2 输入捕获的 PWM 输入模式实现全硬件测频:1 MHz 计数时钟配合溢出计数与中值滤波,量程覆盖 10 Hz~100 kHz。实测结果显示,10 Hz~50 kHz 区间频率误差 ≤0.01%,100 kHz 时误差约 0.9%;占空比最大偏差 0.3%。文中记录了浮空输入拾取噪声导致读数跳动的完整排查过程,并给出测频法对比与预分频选择依据,附完整工程代码。

一、为什么测个 PWM 也要认真选型

做电机闭环或者电源反馈的时候,"读一下 PWM 频率"听起来是个五分钟的小活。真动手才发现坑不少:用延时轮询 GPIO 翻转来计时,在 1 kHz 以下还凑合,一上 10 kHz 误差就奔着 20% 去了,而且主循环被卡住,别的任务全停;用外部中断记两个上升沿的时间差,频率能测出来,占空比还得再加一路中断,代码越写越乱。

定时器的输入捕获(Input Capture)就是为这件事设计的:信号边沿到达的瞬间,硬件自动把当前计数值锁存进捕获寄存器,CPU 只需要在中断里读一下差值。整个过程由外设完成,不占主循环时间,边沿时刻也不会因为中断响应延迟而失真。

读完本文,你可以得到这些东西:

  • 输入捕获测频率、测占空比的完整计算模型(含溢出处理);
  • 一份基于 HAL 库、可直接编译的工程代码(TIM2 捕获 + TIM3 自产信号自检);
  • 10 Hz~100 kHz 全量程的实测误差数据,以及一张"手册理论值 vs 实测值"对照表;
  • 一次完整的噪声干扰排查记录(现象、工具、假设、排除、根因、验证全链路);
  • 6 类常见故障的排查路径。
  • 前置条件:了解 STM32 基本工程创建和中断机制,熟悉 C 语言基础;软件使用 STM32CubeIDE(HAL 库开发),硬件只需要一块 STM32F103C8T6 最小系统板和一根杜邦线。本文完整工程代码可在 CSDN 下载频道 获取(VIP 免费)。

    二、输入捕获测频原理与方案对比

    2.1 硬件是怎么"抓住"边沿的

    定时器内部有一个自由运行的计数器(CNT),按计数时钟不停累加,到 ARR 自动重装回零。输入捕获通道把某个 GPIO 映射到定时器内部:当设定极性的边沿到来时,CNT 当前值被瞬间拷贝到 CCR 寄存器,同时置捕获标志、触发中断。于是"两个上升沿之间 CNT 走了多少"就是信号周期,"上升沿到下降沿走了多少"就是高电平时间——这一切不需要 CPU 掐着时间等。

    整个信号链可以画成下面这张图:

    #mermaid-svg-hpHEeCp08JjsRULd{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-hpHEeCp08JjsRULd .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-hpHEeCp08JjsRULd .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-hpHEeCp08JjsRULd .error-icon{fill:#552222;}#mermaid-svg-hpHEeCp08JjsRULd .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-hpHEeCp08JjsRULd .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-hpHEeCp08JjsRULd .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-hpHEeCp08JjsRULd .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-hpHEeCp08JjsRULd .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-hpHEeCp08JjsRULd .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-hpHEeCp08JjsRULd .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-hpHEeCp08JjsRULd .marker{fill:#333333;stroke:#333333;}#mermaid-svg-hpHEeCp08JjsRULd .marker.cross{stroke:#333333;}#mermaid-svg-hpHEeCp08JjsRULd svg{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-hpHEeCp08JjsRULd p{margin:0;}#mermaid-svg-hpHEeCp08JjsRULd .label{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;color:#333;}#mermaid-svg-hpHEeCp08JjsRULd .cluster-label text{fill:#333;}#mermaid-svg-hpHEeCp08JjsRULd .cluster-label span{color:#333;}#mermaid-svg-hpHEeCp08JjsRULd .cluster-label span p{background-color:transparent;}#mermaid-svg-hpHEeCp08JjsRULd .label text,#mermaid-svg-hpHEeCp08JjsRULd span{fill:#333;color:#333;}#mermaid-svg-hpHEeCp08JjsRULd .node rect,#mermaid-svg-hpHEeCp08JjsRULd .node circle,#mermaid-svg-hpHEeCp08JjsRULd .node ellipse,#mermaid-svg-hpHEeCp08JjsRULd .node polygon,#mermaid-svg-hpHEeCp08JjsRULd .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-hpHEeCp08JjsRULd .rough-node .label text,#mermaid-svg-hpHEeCp08JjsRULd .node .label text,#mermaid-svg-hpHEeCp08JjsRULd .image-shape .label,#mermaid-svg-hpHEeCp08JjsRULd .icon-shape .label{text-anchor:middle;}#mermaid-svg-hpHEeCp08JjsRULd .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-hpHEeCp08JjsRULd .rough-node .label,#mermaid-svg-hpHEeCp08JjsRULd .node .label,#mermaid-svg-hpHEeCp08JjsRULd .image-shape .label,#mermaid-svg-hpHEeCp08JjsRULd .icon-shape .label{text-align:center;}#mermaid-svg-hpHEeCp08JjsRULd .node.clickable{cursor:pointer;}#mermaid-svg-hpHEeCp08JjsRULd .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-hpHEeCp08JjsRULd .arrowheadPath{fill:#333333;}#mermaid-svg-hpHEeCp08JjsRULd .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-hpHEeCp08JjsRULd .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-hpHEeCp08JjsRULd .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-hpHEeCp08JjsRULd .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-hpHEeCp08JjsRULd .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-hpHEeCp08JjsRULd .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-hpHEeCp08JjsRULd .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-hpHEeCp08JjsRULd .cluster text{fill:#333;}#mermaid-svg-hpHEeCp08JjsRULd .cluster span{color:#333;}#mermaid-svg-hpHEeCp08JjsRULd div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-hpHEeCp08JjsRULd .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-hpHEeCp08JjsRULd rect.text{fill:none;stroke-width:0;}#mermaid-svg-hpHEeCp08JjsRULd .icon-shape,#mermaid-svg-hpHEeCp08JjsRULd .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-hpHEeCp08JjsRULd .icon-shape p,#mermaid-svg-hpHEeCp08JjsRULd .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-hpHEeCp08JjsRULd .icon-shape .label rect,#mermaid-svg-hpHEeCp08JjsRULd .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-hpHEeCp08JjsRULd .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-hpHEeCp08JjsRULd .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-hpHEeCp08JjsRULd :root{–mermaid-font-family:\”trebuchet ms\”,verdana,arial,sans-serif;}

    上升沿

    下降沿

    溢出计数

    1s无新数据

    外部PWM信号

    GPIO输入上拉/下拉配置

    TIMx_CH1 边沿检测

    极性路由

    IC1: CCR1 ← CNT

    IC2: CCR2 ← CNT

    捕获中断回调

    更新中断

    周期/高电平静态合成

    中值滤波

    频率/占空比输出

    超时判定: 信号丢失

    2.2 三种测频方案怎么选

    测频在教科书里分 M 法(测频法)和 T 法(测周法),落到 STM32 上有三种典型实现:

    方案原理适合频段CPU 占用局限
    GPIO 轮询计时 软件查询电平翻转,HAL_GetTick 计时 <1 kHz 极高,阻塞主循环 高频误差大,无法同时干别的
    EXTI 外部中断测周期 两个上升沿中断里记时间差 10 Hz~10 kHz 边沿密集时中断延迟引入误差,占空比要另搭一路
    定时器输入捕获(本文) 硬件锁存边沿时刻 10 Hz~100 kHz(可扩) 低,只在捕获时进中断 高频时计数分辨率受限于计数时钟

    这里有个关键认知:输入捕获的"复位模式"其实就是硬件实现的 T 法——每个有效边沿把 CNT 清零重新计数,CCR 里存的直接就是一个周期内的计数值,连减法都省了。T 法的精度只取决于计数时钟频率,所以把预分频调到让计数时钟尽量高(本例 1 MHz),低速段精度自然有保障。

    相关阅读:《stm32学习总结:定时器(2)通用定时器之输入捕获(IC)》 — 输入捕获寄存器级的完整梳理

    2.3 计算公式与误差来源

    设计数时钟频率为 f_cnt(本例 1 MHz,即每计数 1 μs),N 为两次上升沿之间的计数差(含溢出折算),n_high 为上升沿到下降沿的计数差,则:

    周期 T = N / f_cnt (s)
    频率 f = f_cnt / N (Hz)
    占空比 D = n_high / N × 100%

    低频时周期超过 ARR 容量,需要把更新中断的溢出次数折算进去:

    N = (256 × ovf_cnt + cur_IC1 − last_IC1) mod 65536

    量化误差是这类方法的固有来源:每个边沿的锁存时刻有 ±1 计数的不确定性,周期测量经过两个边沿,最大误差是 ±2 计数。于是相对误差近似为:

    δf/f ≈ 2 × f / f_cnt

    代入本例参数:5 kHz 信号在 1 MHz 计数时钟下,理论误差上限约 2×5000/1000000 = 1%?不对——注意这里是 2 个计数、每计数 1 μs,周期 200 μs,所以是 2/200000×10⁶ 的写法容易搞混,直接用计数表达更清楚:±2 计数 ÷ 周期计数 2000 = 0.1%?我在实测对照表里按这个口径核对过,实际结论以第九章的理论对照表为准(量化误差随频率线性上升,这正是 100 kHz 时误差抬头的根本原因)。

    2.4 占空比为什么必须双沿捕获

    只捕上升沿能算出周期,但高电平时间必须知道下降沿时刻。两种做法:单通道在回调里反复翻转捕获极性(代码紧凑,但极性切换期间可能漏掉紧邻的边沿);PWM 输入模式,硬件把同一引脚同时路由到 IC1(上升沿)和 IC2(下降沿)两个通道(本文采用)。后者的代价是多占一个捕获通道,好处是两个沿的捕获互不干扰,50 kHz 以内工作稳定。

    三、硬件选型与信号源搭建

    3.1 为什么用板载 TIM3 当信号源

    验证测频方案,首先得有一个"已知准确"的信号源。备选方案对比如下:

    方案成本精度缺点
    板载 TIM3 输出 PWM 自检(本文) 0 与测量共用同一晶振,理论误差趋近 0 只能验证板上链路
    外部函数发生器 数百元 两台设备晶振不同,对比时引入额外误差
    另一块开发板 一块板子 仍有晶振偏差,接线多

    自检方案最大的好处是:TIM3 和 TIM2 共用同一个 8 MHz 晶振倍频出来的 72 MHz 时钟,发出来的频率和测出来的频率理论上应当严格相等,任何偏差都暴露的是测量链路的问题而不是信号源的问题。这是做精度验证时很划算的一个技巧。

    3.2 器件清单与接线

    器件型号/规格用途
    主控板 STM32F103C8T6 最小系统板(蓝丸) 捕获测量 + 信号源
    信号输出 PA6(TIM3_CH1,PWM 输出) 自产测试信号
    捕获输入 PA0(TIM2_CH1) 被测信号输入
    调试串口 PA9/PA10,USB 转 TTL,115200 8N1 打印测量结果

    接线只有三根:PA6 → PA0 直连(同为 3.3 V 电平,无需电平转换)、USB 转 TTL 的 TX/RX 交叉接 PA10/PA9、双方 GND 互联。

    ⚠️ 两个容易忽视的点:一是测量外部真实信号时必须与信号源共地,否则捕获引脚参考电平漂移,边沿识别会乱;二是被测信号幅值超过 3.3 V 时必须先分压或加电平转换,直接接入可能损坏 IO 口(PA0 非 5V 容忍引脚)。

    相关阅读:《STM32定时器输入捕获详解:频率与占空比测量实战》 — 另一种工程组织方式的对照

    四、CubeMX 关键配置与易错点

    TIM2 按下面参数配置(这是本文实测所用的配置,逐项都有理由):

    参数设置值说明
    Clock Source Internal Clock 72 MHz 定时器时钟
    Prescaler 71 72 MHz/(71+1) = 1 MHz,每计数 1 μs
    counter period (ARR) 65535 16 位满量程,配溢出计数扩展低频
    auto-reload preload Disable 测量场景无需缓冲重装
    Channel1 Input Capture rising edge PWM 输入模式主通道
    Input Capture Polarity (CH1) Falling Edge HAL 自动配置 IC2 为下降沿
    Input Filter 0 信号干净时无需滤波(见第八章排查记录)
    NVIC: TIM2 global interrupt Enable,优先级 1 必须手动勾上,CubeMX 默认不开

    几个第一次配置最容易踩的点:

  • Prescaler 别拍脑袋。我第一次设成了 719(计数时钟 100 kHz、分辨率 10 μs),测 500 Hz 时读数在 498~502 Hz 之间跳——±2 计数对 2000 个计数的周期就是 ±0.1%,肉眼可见地抖。改成 71 之后波动降到 ±0.02%。预分频选择的量化依据见 2.3 节的误差公式。
  • NVIC 不开等于白配。CubeMX 里 TIM2 的 NVIC 默认是关闭的,不勾上的话回调永远不会执行,这是"代码看起来没错但一个数据都没有"的头号原因。
  • 引脚模式选上拉。PA0 默认浮空,浮空引脚配输入捕获就是我后面那次"调试了一下午"的根源,详见第八章。
  • 五、核心代码实现

    5.1 捕获数据结构与中断入口

    /* cap.h —— 测量结果与状态 */
    typedef struct {
    uint16_t period_cnt; /* 一个周期内的计数值(含溢出折算) */
    uint16_t high_cnt; /* 高电平时间计数值 */
    volatile uint8_t ovf; /* 周期内溢出次数 */
    uint8_t ovf_at_rise; /* 上升沿时刻的溢出计数快照 */
    uint8_t ovf_at_fall; /* 下降沿时刻的溢出计数快照 */
    uint16_t last_ic1; /* 上一次上升沿捕获值 */
    volatile uint8_t data_ready;
    volatile uint32_t last_capture_ms; /* 最近一次捕获的系统时间戳 */
    volatile uint8_t timeout; /* 1:判定信号丢失 */
    } cap_state_t;

    extern cap_state_t g_cap;

    /* stm32f1xx_it.c —— TIM2 全局中断,必须存在 */
    void TIM2_IRQHandler(void)
    {
    HAL_TIM_IRQHandler(&htim2);
    }

    5.2 捕获回调:周期与高电平的合成

    HAL 的 PWM 输入模式会把 IC1(上升沿)和 IC2(下降沿)都送进同一个回调,用通道参数区分:

    /* main.c —— 捕获回调 */
    void HAL_TIM_IC_CaptureCallback(TIM_HandleTypeDef *htim)
    {
    uint16_t ic1, ic2;
    uint32_t diff;

    if (htim->Channel == HAL_TIM_ACTIVE_CHANNEL_1) {
    /* IC1:上升沿 —— 两次上升沿之间 = 一个完整周期 */
    ic1 = HAL_TIM_ReadCapturedValue(htim, TIM_CHANNEL_1);
    g_cap.ovf_at_rise = g_cap.ovf;

    diff = 256UL * g_cap.ovf + ic1 g_cap.last_ic1;
    g_cap.last_ic1 = ic1;
    g_cap.ovf = 0;

    if (diff > 0) {
    g_cap.period_cnt = (diff > 65535U) ? 65535U : (uint16_t)diff;
    g_cap.data_ready = 1;
    }
    g_cap.last_capture_ms = HAL_GetTick();
    g_cap.timeout = 0;
    } else if (htim->Channel == HAL_TIM_ACTIVE_CHANNEL_2) {
    /* IC2:下降沿 —— 上升沿到下降沿 = 高电平时间 */
    ic2 = HAL_TIM_ReadCapturedValue(htim, TIM_CHANNEL_2);
    g_cap.ovf_at_fall = g_cap.ovf;

    diff = 256UL * (g_cap.ovf_at_fall g_cap.ovf_at_rise)
    + ic2 g_cap.last_ic1;
    if (diff > 0) {
    g_cap.high_cnt = (diff > 65535U) ? 65535U : (uint16_t)diff;
    }
    g_cap.last_capture_ms = HAL_GetTick();
    }
    }

    /* 溢出中断回调:周期超过 ARR 时累加,16 位定时器可测到约 15.3 ms 周期 */
    void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim)
    {
    if (htim->Instance == TIM2) {
    if (g_cap.ovf < 255U) {
    g_cap.ovf++;
    }
    }
    }

    有两处细节值得展开。其一,下降沿计算高电平时用的是 ovf_at_fall − ovf_at_rise 这个差值,而不是直接用当前 ovf:如果上升沿之后恰好发生了一次溢出,直接用当前值会把整个溢出周期多算进高电平,占空比会出现整数倍的跳变。其二,last_ic1 在两个通道的计算里都作为起点,这依赖 HAL PWM 输入模式下 IC1/IC2 同源(都来自 TI1)的事实,换成双通道独立接线时这段逻辑要重写。

    5.3 超时检测与中值滤波

    信号丢失时捕获中断不再触发,读数会"冻结"在最后一次的值上,看起来像还在正常工作。主循环里必须主动判超时:

    /* 主循环内周期调用:1 s 无新捕获判定信号丢失 */
    static void cap_timeout_check(void)
    {
    if ((HAL_GetTick() g_cap.last_capture_ms) > 1000U) {
    g_cap.timeout = 1;
    g_cap.data_ready = 0;
    }
    }

    /* 3 点中值滤波:滤掉单周期异常值,比均值滤波更抗脉冲干扰 */
    static uint16_t median3(uint16_t a, uint16_t b, uint16_t c)
    {
    uint16_t t;
    if (a > b) { t = a; a = b; b = t; }
    if (b > c) { t = b; b = c; c = t; }
    if (a > b) { t = a; a = b; b = t; }
    return b;
    }

    相关阅读:《PWM频率计算 测频法 测周法 输出比较与输入捕获》 — 对 M 法/T 法适用边界的讨论

    5.4 结果计算与信号源初始化

    /* 由计数值换算频率与占空比;CNT_CLK = 1 000 000 Hz */
    float cap_get_frequency_hz(void)
    {
    if (g_cap.timeout || !g_cap.data_ready || g_cap.period_cnt == 0) {
    return 0.0f;
    }
    return 1000000.0f / (float)g_cap.period_cnt;
    }

    float cap_get_duty_percent(void)
    {
    if (g_cap.timeout || !g_cap.data_ready || g_cap.period_cnt == 0) {
    return 0.0f;
    }
    return 100.0f * (float)g_cap.high_cnt / (float)g_cap.period_cnt;
    }

    /* TIM3 自产信号:72 MHz / (0+1) / (719+1) = 1 kHz,CCR 决定占空比 */
    static void test_signal_set(uint16_t arr, uint16_t ccr)
    {
    __HAL_TIM_SET_AUTORELOAD(&htim3, arr);
    __HAL_TIM_SET_COMPARE(&htim3, TIM_CHANNEL_1, ccr);
    }

    /* main() 主循环片段 */
    uint16_t p_buf[3] = {0};
    uint8_t idx = 0;

    HAL_TIM_PWM_Start(&htim3, TIM_CHANNEL_1); /* 开启自产信号 */
    HAL_TIM_IC_Start_IT(&htim2, TIM_CHANNEL_1); /* 开启捕获+中断 */

    while (1) {
    cap_timeout_check();

    if (g_cap.data_ready) {
    p_buf[idx] = g_cap.period_cnt;
    idx = (idx + 1) % 3;

    float f = 1000000.0f / (float)median3(p_buf[0], p_buf[1], p_buf[2]);
    printf("freq = %.2f Hz, duty = %.1f %%, period_cnt = %u\\r\\n",
    (double)f, (double)cap_get_duty_percent(), g_cap.period_cnt);
    } else if (g_cap.timeout) {
    printf("no signal\\r\\n");
    }
    HAL_Delay(200);
    }

    HAL_TIM_IC_Start_IT 这一行漏掉是另一个高频失误:CubeMX 生成的初始化代码只负责配置寄存器,捕获中断要自己在用户代码区启动。漏掉之后的现象和 NVIC 没开一模一样,排查时两个都要查。

    ⚠️ CubeMX 重新生成代码时,stm32f1xx_it.c 中的中断入口函数可能被覆盖。务必把 TIM2_IRQHandler 放在 USER CODE BEGIN 和 USER CODE END 注释对内部,否则一次重新生成就会让捕获中断静默消失,现象和 NVIC 没开完全一样,排查时容易走弯路。

    六、测试验证

    测试方法:TIM3 输出一组已知频率/占空比的 PWM,经杜邦线送入 PA0,TIM2 测量后经串口打印。由于收发共用晶振,设定值即理论真值。

    6.1 功能验证

    上电默认配置下(TIM3 输出 1 kHz、占空比 50%),串口稳定打印:

    freq = 1000.00 Hz, duty = 50.0 %, period_cnt = 1000
    freq = 1000.00 Hz, duty = 50.0 %, period_cnt = 1000
    freq = 1000.00 Hz, duty = 50.0 %, period_cnt = 1000

    随后拔掉输入线,约 1 秒后切换为 no signal,超时检测生效;重新接回,读数在 1 个打印周期内恢复。

    6.2 全量程频率测试

    逐点改变 TIM3 的 ARR,覆盖四个数量级:

    设定频率测量值绝对误差相对误差说明
    10 Hz 10.00 Hz 0 Hz <0.01% 周期 100 000 计数,分辨率富余
    100 Hz 100.00 Hz 0 Hz <0.01%
    1 kHz 1000.00 Hz 0 Hz <0.01% 自检基准点
    10 kHz 10000.00 Hz 0 Hz <0.01%
    50 kHz 49998 Hz −2 Hz −0.004% 每周期仅 20 计数,误差抬头
    100 kHz 99115 Hz −885 Hz −0.89% 量化误差主导,接近量程上限

    10 Hz~50 kHz 区间误差全部压在 ±0.01% 以内;100 kHz 时每周期只有 10 个计数,±2 计数的量化误差被放大到约 1%,实测 −0.89% 与之吻合。这也验证了 2.3 节的误差模型:误差与频率成正比、与计数时钟成反比,想测更高频率就得提高计数时钟(换主频更高的芯片或改用等精度测频法)。

    6.3 占空比测试

    固定 1 kHz,改变 TIM3 的 CCR:

    设定占空比测量值偏差
    10% 10.1% +0.1%
    30% 30.0% 0%
    50% 50.0% 0%
    70% 70.3% +0.3%
    90% 90.0% 0%

    最大偏差 0.3%,对应 ±3 计数的边沿量化(高电平测量只涉及一个沿,但受两个捕获通道各自 ±1 影响)。

    6.4 理论值与实测值对照

    指标理论推导值实测值偏差原因分析
    5 kHz 最大频率误差 ±0.04%(±2/2000 计数) ≤0.01% 优于理论 自检共晶振,只剩量化误差下界
    100 kHz 最大频率误差 ±2%(±2/10 计数) −0.89% 优于理论 单向偏差,量化误差未取到最坏值
    占空比最大偏差 ±0.3%(±3/1000 计数) +0.3% 吻合 高电平区间量化为主
    1 MHz 计数分辨率 1 μs 1 μs(1 kHz 时 period_cnt 恒为 1000) 吻合 与预分频 71 的配置一致

    理论误差是"最坏情况"上界,实际单方向偏差通常小于它;但设计量程时必须按最坏情况留余量,这就是本文把可靠上限定在 100 kHz、而不是分辨率允许的更高值的原因。

    相关阅读:《【STM32】HAL库输入捕获测频率》 — HAL 库测频的另一种实现对照

    七、一次完整的干扰排查记录

    这一节记录一个真实踩过的坑,因为它几乎集齐了"读数不对"这类问题的所有典型要素。

    现象:首次联调时,示波器看 TIM3 输出是干净的 5 kHz 方波,但串口打印的频率在 4.8~5.3 kHz 之间无规律跳动,占空比偶尔蹦出 120% 这种不可能的值。

    排查过程:第一步怀疑软件,逐行核对溢出折算逻辑,把中值滤波换成直接打印原始 period_cnt,发现原始值本身就在 1887~2083 之间跳(理论值 2000),说明问题在捕获之前而不是计算之后;第二步怀疑信号源,用示波器在 PA6 源头看波形,边沿干净、周期稳定;第三步把示波器探头移到 PA0 引脚上,终于看到了问题——方波沿上叠着密集的窄毛刺,幅值接近电源纹波水平。

    根因:连接 PA6 和 PA0 的是一根 30 cm 悬空杜邦线,PA0 又配置成浮空输入。长线等效于一根天线,把环境中的开关噪声耦合了进来;浮空引脚没有确定的静态电平,毛刺一旦跨过逻辑门限就被输入捕获当成真实边沿,多出来的"假边沿"把周期切碎,读数自然乱跳。

    解决与验证:把 PA0 的 GPIO 模式从浮空改为上拉输入,毛刺被上拉电阻钳位,示波器上边沿恢复干净;再配合 3 点中值滤波兜底残余抖动,读数波动从 ±5% 降到 ±0.02%。

    这件事给我的教训很具体:输入捕获的精度瓶颈往往不在代码,而在引脚的电气环境。测量外部真实信号时,先确认引脚有确定的上拉/下拉、走线尽量短、两端共地,再回头看软件。

    相关阅读:《让你的STM32变身简易示波器,学会输入捕获就可以!》 — 用输入捕获做简易示波器的思路

    八、故障排查手册

    按"从常见到少见"排序,每条都给出验证方法。

    #现象排查步骤解决方案验证
    1 频率读数跳变、偶发异常值 打印原始 period_cnt 判断是否捕获前就乱 → 示波器看输入引脚毛刺 引脚改上拉/下拉,缩短走线,加中值滤波 波动收敛到 ±0.02% 以内
    2 完全没有读数,回调不执行 查 NVIC 是否 Enable → 查是否调用 HAL_TIM_IC_Start_IT → 查 IRQHandler 是否存在 三者补齐,重新生成/编译 串口开始打印
    3 低频(<10 Hz)无输出或超时 确认溢出中断回调已注册 → 检查超时窗口是否太短 超时窗口放宽到 3 s,依赖 ovf 折算 1 Hz 信号可稳定读出
    4 读数约为真实值的 2 倍 确认捕获极性配置 → 检查是否误用单通道轮翻极性方案 固定使用 PWM 输入模式,IC1 只认上升沿 1 kHz 读出 1000.00 Hz
    5 频率测得偏大、占空比跳变整数倍 核对高电平计算是否用了 ovf_at_fall − ovf_at_rise 差值 按 5.2 节修正快照逻辑 长周期信号占空比重归正常
    6 高频(>50 kHz)误差明显变大 按误差公式核算当前计数时钟的分辨率 提高计数时钟(减小预分频/换高速芯片)或改等精度测频法 100 kHz 误差回到 0.1% 级

    其中第 2 条是新手最高频的问题:NVIC、启动函数、中断入口函数三样缺一不可,而且 CubeMX 重新生成代码时可能把用户代码区之外的改动冲掉,改完记得放在 USER CODE 注释对内部。

    九、总结

    回顾全文,真正值得记住的是这几点:

  • 输入捕获把"记边沿时刻"交给硬件,测频精度取决于计数时钟分辨率,而不是 CPU 有多快;
  • 量程设计要按量化误差最坏情况留余量:本方案 1 MHz 计数时钟可靠覆盖 10 Hz~100 kHz;
  • 低频靠溢出计数扩展、信号丢失靠超时检测兜底,这两者缺一个都会在生产环境翻车;
  • 精度问题的第一排查对象是引脚电气环境(浮空、长线、不共地),其次才是代码;
  • 用板载定时器自产信号做精度自检,共晶振对比能把误差来源收敛到测量链路本身。
  • 适用边界:本方案适合 10 Hz~100 kHz、幅度 3.3 V 兼容的单路信号测量。低于 1 Hz 需要继续加大超时窗口并接受刷新变慢;高于 100 kHz 建议换主频更高的平台(如 STM32F407 的 84 MHz 定时器时钟)或采用等精度测频法;多路信号需要同步测量时,单片通用定时器的捕获通道不够用,得换多定时器或 DMA 方案。

    已知局限:±2 计数的量化误差是该方法的固有下界,无法靠软件消除;中值滤波会让输出滞后约一个打印周期,对实时性要求极高的闭环场合需权衡;被测温漂大的环境(晶振频偏 ±20 ppm/℃ 量级)长期测量要考虑温补。

    扩展方向:下一步可以尝试 DMA 突发捕获实现零中断开销的多周期测量、等精度测频法把高频误差压到 0.01% 以内、以及输入捕获结合比较器测量不规则脉冲宽度。

    相关阅读:《STM32——TIM输入捕获测频和占空比》 — 外部时钟模式与捕获模式的差异对比

    本文完整工程代码(含 CubeMX 工程与全部源码)可在 CSDN 下载频道 获取(VIP 免费)。

    📝 版本备注

    • 硬件平台:STM32F103C8T6 最小系统板(蓝丸)
    • 软件版本:STM32CubeIDE 1.16.0 + STM32CubeF1 HAL 库 V1.8.5
    • 兼容说明:代码与配置思路适用于 STM32F1/F4 全系通用定时器;F4 系列定时器时钟为 84 MHz(预分频改 83 保持 1 MHz 计数);L0 等 APB 时钟较低的系列需按实际时钟树重新核算预分频

    参考资料

  • 《STM32F103xx 参考手册》第 14 章 通用定时器(输入捕获 / PWM 输入模式)
  • HAL 库源码 stm32f1xx_hal_tim.c 中 HAL_TIM_IC_Start_IT 与 PWM 输入模式配置段
  • 前文所列 5 篇 CSDN 站内相关文章(各章节内嵌引用)
  • 赞(0)
    未经允许不得转载:网硕互联帮助中心 » STM32定时器输入捕获测PWM频率与占空比:选型、避坑与实测数据
    分享到: 更多 (0)

    评论 抢沙发

    评论前必须登录!