MCU 定时器脉宽捕获:双边沿读引脚与 PWM 输入模式

使用定时器测量外部信号的脉宽是嵌入式开发中的常见需求。以下场景都会遇到:

这篇文章整理三种基于定时器捕获通道的脉宽测量方法,并讨论一个问题:硬件能自动过滤不合格的脉宽吗?哪些必须在软件里做?

PY32F002A 的定时器资源

先看一眼手里的硬件。PY32F002A(TSSOP-20 封装)有 2 个定时器:

定时器 类型 捕获通道 能测脉宽?
TIM1 高级定时器 4 个(CH1~CH4)
TIM16 通用定时器 0 个——没有 CCMR、CCER、CCR ❌ 只能做定时中断

只有 TIM1 能做输入捕获。 不过它有 4 个通道,每个通道是完全独立的硬件:独立的 CCR 寄存器、独立的输入滤波器(ICxF)、独立的预分频器(ICxPSC)、独立的中断标志。唯一共享的是一颗 16 位计数器 CNT。

硬件滤波:能做的和不能做的

每个捕获通道都有一个 4 位数字滤波器(ICxF)和一个 2 位事件预分频器(ICxPSC),在信号进入边沿检测之前工作。

数字滤波器去毛刺

滤波器用 fDTS(死区时间采样时钟,等于 CK_INT 或其 2/4 分频)对输入信号连续采样 N 次。只有 N 次采样结果一致,信号电平才被传递到边沿检测电路。

ICxF fDTS N 48MHz 下的最小脉冲
0x0 无滤波 - -
0x3 fCK_INT 8 167 ns
0x7 fCK_INT/4 8 667 ns
0xB fCK_INT/16 8 2.67 μs
0xF fCK_INT/32 8 5.33 μs

宽度小于上述值的毛刺会被硬件自动吞掉,不会触发捕获。这是纯硬件行为,你的 ISR 根本感知不到这些毛刺。

事件预分频器跳脉冲

ICxPSC 让捕获通道每 N 个边沿才响应一次:

ICPSC 效果
DIV1 每个边沿都捕获
DIV2 每 2 个边沿捕获 1 次
DIV4 每 4 个边沿捕获 1 次
DIV8 每 8 个边沿捕获 1 次

预分频器适合周期性信号——比如你只关心每第 4 次上升沿的时间戳,中间 3 次直接忽略。

硬件不能做的:阈值判定

ICxF 滤毛刺,ICxPSC 跳脉冲。但没有寄存器可以设"脉宽小于 100 个 tick 就自动丢弃"。脉冲是否合格,必须在 ISR 里用软件判断。

好在这不难:中断里读到 CCR 值,跟前一次捕获值做差,大于最小阈值、小于最大阈值就算合格,不满足的直接丢弃。

三种捕获触发模式

每个捕获通道可以独立选择边沿触发方式:

模式 HAL 宏 行为
上升沿 TIM_ICPOLARITY_RISING 只在上升沿锁存 CNT → CCR,触发中断
下降沿 TIM_ICPOLARITY_FALLING 只在下降沿锁存 CNT → CCR,触发中断
双边沿 TIM_ICPOLARITY_BOTHEDGE 两个边沿都锁存 CNT → CCR,都触发中断

触发条件满足时,硬件自动把当前 CNT 值锁存到对应的 CCR 寄存器里,置中断标志。整个过程不需要软件参与——你在 ISR 里读 CCR 拿到的就是边沿到达那一刻的计数器快照。

下面三种方法本质上是在这三种触发模式上做组合。

方法一:单通道双边沿 + 读引脚判方向

一个通道配成双边沿捕获,上升沿和下降沿都会触发中断。中断里读对应 GPIO 的输入电平:高说明刚才是上升沿,低说明刚才是下降沿。

// 配置:CH1 双边沿捕获 + 硬件滤波
TIM_IC_InitTypeDef sIC = {0};
sIC.ICPolarity  = TIM_ICPOLARITY_BOTHEDGE;   // 双边沿都触发
sIC.ICSelection = TIM_ICSELECTION_DIRECTTI;   // 直接连 TI1
sIC.ICPrescaler = TIM_ICPSC_DIV1;             // 每个边沿都捕获
sIC.ICFilter    = 0x0F;                       // 最强硬件滤波
HAL_TIM_IC_ConfigChannel(&htim1, &sIC, TIM_CHANNEL_1);
HAL_TIM_IC_Start_IT(&htim1, TIM_CHANNEL_1);

// ISR
void HAL_TIM_IC_CaptureCallback(TIM_HandleTypeDef *htim)
{
    static uint32_t last = 0;

    if (htim->Channel == HAL_TIM_ACTIVE_CHANNEL_1) {
        uint32_t now = HAL_TIM_ReadCapturedValue(htim, TIM_CHANNEL_1);
        uint32_t diff = (now >= last) ? (now - last) : (0xFFFF - last + now);

        if (HAL_GPIO_ReadPin(GPIOA, GPIO_PIN_8) == GPIO_PIN_SET) {
            // 引脚高 → 刚才上升沿 → diff = 低电平宽度
            if (diff > MIN_TICKS && diff < MAX_TICKS) {
                g_pulse_ticks = diff;   // 合格
            }
            // 不合格的静默丢弃
        }
        // 下降沿只记录时间戳,不计算宽度
        last = now;
    }
}

时序:

信号 ──┐        ┌──────────┐        ┌──────
       └────────┘          └────────┘
        ↓        ↑          ↓        ↑
    下降沿     上升沿      下降沿    上升沿
    CCR=100   CCR=400    CCR=650  CCR=900
    PIN=0     PIN=1      PIN=0    PIN=1
    只记录    低脉宽      只记录    低脉宽
              =400-100             =900-650
              =300                =250

优点:一个通道搞定一路信号。TIM1 有 4 个通道,够测 4 路。

代价:ISR 里要读一次 GPIO 来判断方向。对于高频脉冲,GPIO 读操作有 1~2 个 APB 周期的延迟,但绝大多数应用场景完全够用。

方法二:PWM 输入模式——两个通道绑一个信号

PWM 输入模式用两个捕获通道(CH1+CH2 或 CH3+CH4)绑到同一个输入引脚。CH1 只抓上升沿,CH2 只抓下降沿,两个通道各自独立触发,互不干扰。ISR 里不需要读 GPIO——哪个通道触发,就知道是哪个边沿。

同一个信号物理上只接一根引脚。CH2 通过 CCMR 的 INDIRECT 模式"内部布线"连到同一路 TI1 输入。

// CH1: 上升沿, 直接连 TI1
TIM_IC_InitTypeDef sIC1 = {0};
sIC1.ICPolarity  = TIM_ICPOLARITY_RISING;
sIC1.ICSelection = TIM_ICSELECTION_DIRECTTI;
sIC1.ICPrescaler = TIM_ICPSC_DIV1;
sIC1.ICFilter    = 0x0F;
HAL_TIM_IC_ConfigChannel(&htim1, &sIC1, TIM_CHANNEL_1);

// CH2: 下降沿, 间接连到同一个 TI1(不需要额外引脚)
TIM_IC_InitTypeDef sIC2 = {0};
sIC2.ICPolarity  = TIM_ICPOLARITY_FALLING;
sIC2.ICSelection = TIM_ICSELECTION_INDIRECTTI;  // 内部连 TI1
sIC2.ICPrescaler = TIM_ICPSC_DIV1;
sIC2.ICFilter    = 0x0F;
HAL_TIM_IC_ConfigChannel(&htim1, &sIC2, TIM_CHANNEL_2);

HAL_TIM_IC_Start_IT(&htim1, TIM_CHANNEL_1);
HAL_TIM_IC_Start_IT(&htim1, TIM_CHANNEL_2);

// ISR
void HAL_TIM_IC_CaptureCallback(TIM_HandleTypeDef *htim)
{
    if (htim->Channel == HAL_TIM_ACTIVE_CHANNEL_1) {
        // CH1 中断 → 一定是上升沿 → 读 CCR1(周期值)
        uint32_t period = HAL_TIM_ReadCapturedValue(htim, TIM_CHANNEL_1);
        g_period_ticks = period;
    }
    if (htim->Channel == HAL_TIM_ACTIVE_CHANNEL_2) {
        // CH2 中断 → 一定是下降沿 → 读 CCR2(高电平宽度)
        uint32_t high = HAL_TIM_ReadCapturedValue(htim, TIM_CHANNEL_2);
        g_high_ticks = high;
        g_low_ticks = g_period_ticks - high;   // 低脉宽 = 周期 - 高脉宽
    }
}

优点:ISR 不需要读 GPIO,不需要记上次值,边沿方向由通道号本身保证。代码干净。

代价:一路信号吃掉两个捕获通道。TIM1 最多测两路(CH1+CH2 一路,CH3+CH4 一路)。CH3+CH4 在 CCMR2 中没有 INDIRECT 模式——CH4 不能间接映射到 TI3,第二路只能退回方法一。

方法三:纯 GPIO 双边沿中断

前两种方法都需要占用定时器捕获通道。如果引脚不满足 TIM1 通道映射、或者捕获通道已挪作他用,可以用 GPIO 双边沿中断加软件状态机来测脉宽。

PY32F002A 的 EXTI 原生支持双边沿触发:

GPIO_InitStruct.Mode = GPIO_MODE_IT_RISING_FALLING;

配置之后,上升沿和下降沿都会进同一个 EXTI 中断,不需要像 51 单片机那样在 ISR 里来回切换触发边沿。ISR 里读一次引脚电平即可判断方向,再用一个自由运行的定时器(比如 SysTick 或只做时基的 TIM16)拿时间戳:

// 两路信号各用一个小的状态机
typedef struct {
    uint32_t last_tick;
    uint8_t  state;       // 0=等下降沿, 1=等上升沿
    uint32_t low_width;
} pulse_detect_t;

pulse_detect_t g_ch1, g_ch2;

void EXTI_IRQHandler(void)
{
    uint32_t now = TIM16->CNT;  // 自由运行的时间基准

    if (__HAL_GPIO_EXTI_GET_IT(GPIO_PIN_2) != RESET) {
        if (HAL_GPIO_ReadPin(GPIOA, GPIO_PIN_2) == GPIO_PIN_SET) {
            // 上升沿 → 低电平结束
            uint32_t diff = (now >= g_ch1.last_tick) ? (now - g_ch1.last_tick)
                                                     : (0xFFFF - g_ch1.last_tick + now);
            if (diff > MIN_TICKS && diff < MAX_TICKS) {
                g_ch1.low_width = diff;  // 合格
            }
        } else {
            // 下降沿 → 低电平开始,只记录起点
            g_ch1.last_tick = now;
        }
        __HAL_GPIO_EXTI_CLEAR_IT(GPIO_PIN_2);
    }
    // 通道2 同理...
}

精度能接受吗

软件中断的延迟不是随机乱跳——Cortex-M0+ 的中断响应是确定性的 12~16 个周期。跑 48MHz 时约 0.3μs。真正引入抖动的是被其他中断或临界区短暂挡住。

以 250μs 最小脉宽为例:

中断延迟:~0.3μs
SysTick 挡住(典型):~2μs
最坏情况 jitter:~2μs
相对误差:2 / 250 = 0.8%

对于电机霍尔、红外遥控、超声波测距这类场景完全够用。但如果脉宽是几十微秒级别,或者系统里有长时间关中断的代码(Flash 擦写、I2C 阻塞等待),误差就会明显增大。

延伸阅读在 51 单片机上用中断捕获 12 路微秒级脉宽,软件双边沿会碰到实时性边界 的情况不同——51 主频低、路数多、脉宽窄,软件方案才会成为瓶颈。

三种方法对比

方法一
双边沿+读引脚
方法二
PWM输入模式
方法三
GPIO中断
通道/信号数 1 个 2 个 0(不用定时器)
TIM1 最大信号数 4 路 2 路 不限
ISR 读 GPIO 需要 不需要 需要
ISR 复杂度 中等
精度 硬件时间戳 硬件时间戳 软件时间戳,~2μs jitter
适用场景 多路低频脉冲 1~2 路高频 PWM 脉宽 ≥100μs,无长时间关中断
额外要求 引脚映射到 TIM1 CHx 引脚映射到 TIM1 CHx EXTI 可用即可

软件阈值判定的最终形态

不论用方法一还是方法二,硬件只能去毛刺,不能判断脉宽是否合法。ISR 里代码形态是相同的:

// 拿到 diff(两次捕获的 tick 差值)
if (diff < MIN_VALID_TICKS)  return;  // 太窄,丢弃
if (diff > MAX_VALID_TICKS)  return;  // 太宽,丢弃
g_valid_count++;                        // 合格
// 然后该干啥干啥

MIN 和 MAX 的取值取决于你的信号协议。红外遥控 NEC 协议逻辑 0 是 560μs,逻辑 1 是 1690μs;超声波测距 HC-SR04 高电平约 150μs~25ms。把这些物理时间换算成定时器 tick 数作为阈值,不合规的直接 return 丢弃。

延伸阅读一种基于脉宽判定的电机霍尔反馈抗干扰方法 在电机霍尔信号上实际应用了脉宽阈值校验——用定时器捕获霍尔边沿,通过脉宽范围过滤干扰脉冲。

小结

定时器捕获通道自带两级硬件预处理:数字滤波器去毛刺,预分频器跳脉冲。这两项是纯硬件行为,不需要软件参与。

但"判定脉宽范围"必须软件做——没有硬件阈值比较器。

怎么选:

具体到 PY32F002A:只有 TIM1 能做捕获,TSSOP-20 能引出的 TIM1 通道取决于封装。查数据手册的引脚复用表确认 CH1~CH4 对应的 GPIO 即可。TIM1 通道不够用时,TIM16 配成自由运行计数器给方法三当时间基准。