嵌入式物理输入采集与消抖

Babel36acl 嵌入式实战 无~ 2 次阅读 预计阅读时间: 15 分钟 发布于 1 天前 最后更新于 1 小时前 3233 字


1. 问题背景

工业现场的数字量输入信号——行程开关、光电传感器、接近开关——在物理触点切换的瞬间会持续产生 机械抖动(bouncing):信号在高低电平之间来回跳变数十毫秒后才稳定。如果不做任何处理,一次按压可能被解释为几十次触发。

flowchart TB
    subgraph HARD[硬件层]
        H1[GPIO 输入]
        H2[机械按键/编码器]
    end
    subgraph FILTER[消抖层]
        F1[延时采样]
        F2[边沿检测]
        F3[长按/短按识别]
    end
    subgraph EVENT[事件层]
        E1[按键事件队列]
        E2[协议帧封装]
    end
    HARD --> FILTER --> EVENT
    style HARD fill:transparent,stroke:#8dc7ff,color:#eaf4ff
    style FILTER fill:transparent,stroke:#8dc7ff,color:#eaf4ff
    style EVENT fill:transparent,stroke:#8dc7ff,color:#eaf4ff

常见的应对方案有两种:硬件 RC 滤波(在 GPIO 引脚上加电容电阻构成低通滤波器)和 软件消抖(在固件代码中通过时间窗口判断信号的稳定状态)。

本文讨论的是一个纯软件消抖的实现方案,运行在 STM32F103 平台上,对应的抽象层名为 GENERIC_INPUT_CONTROLLER。该模块负责管理最多 4 路物理数字输入,提供带消抖的稳定状态接口、原始电平旁路接口以及状态重同步机制。

2. 整体架构

GENERIC_INPUT_CONTROLLER 位于 Service 层,不直接调用 HAL 的 GPIO 接口,而是通过 bsp_gpio_read_pin() 间接读取电平。每一路输入的管脚映射、有效电平极性、消抖时间等配置均以 编译期常量表 的形式定义在 app_define.h 中。

模块的核心 API 接口如下:

/* 模块初始化 — 根据配置表建立输入通道 */
void generic_input_init(void);

/* 周期性更新 — 必须在主循环/任务中以固定周期调用 */
void generic_input_update(uint32_t now_ms);

/* 获取消抖后的稳定状态 */
bool generic_input_is_active(uint8_t ch);

/* 获取原始电平(绕过消抖,用于急停等场景) */
bool generic_input_is_raw_active(uint8_t ch);

/* 强制重同步 — 将稳定态与候选态对齐到当前硬件电平 */
void generic_input_resync(uint8_t ch);

3. 消抖数据结构

每个物理输入通道维护 4 个状态变量:

typedef struct {
    uint8_t channel_id;              /* 通道编号 */
    uint8_t  raw_level;              /* 本次采样到的原始电平 */
    uint8_t  candidate_level;        /* 候选稳定电平 */
    uint32_t candidate_start_tick;   /* 候选电平首次出现的时间戳 */
    uint8_t  stable_level;           /* 经过消抖确认的稳定电平 */
    uint8_t  active_level;           /* 有效电平极性(ACTIVE_HIGH / ACTIVE_LOW) */
    uint16_t debounce_ms;            /* 消抖时间窗口,默认 40 ms */
} generic_input_ch_t;

关键设计思路:不依赖硬件定时器,所有时间窗口计算都基于外部传入的 now_ms 时间戳。这意味着模块本身不关心时间基从哪来,只要调用方在每个周期传入单调递增的 ms 级时间即可。典型场景下由 RTOS 的 Task 循环或 Systick 计数器提供。

4. 消抖算法实现

算法本质上是一个 两级状态机

  1. 原始电平变化 → 立即刷新 candidate_level 并重置 candidate_start_tick
  2. 候选电平保持超过 debounce_ms → 将 stable_level 更新为 candidate_level

核心更新逻辑如下:

void generic_input_update(uint32_t now_ms) {
    for (int i = 0; i < GENERIC_INPUT_CH_MAX; i++) {
        generic_input_ch_t *ch = &channels[i];

        /* 跳过未配置的通道 */
        if (ch->port == NULL) continue;

        /* 1. 读取原始电平 */
        uint8_t new_raw = bsp_gpio_read_pin(ch->port, ch->pin);
        ch->raw_level = new_raw;

        /* 2. 检测到跳变 — 立即刷新候选 */
        if (new_raw != ch->candidate_level) {
            ch->candidate_level = new_raw;
            ch->candidate_start_tick = now_ms;
            continue;   /* 等待下一次更新再判断 */
        }

        /* 3. 候选已稳定且超过消抖阈值 — 更新稳定态 */
        if (now_ms - ch->candidate_start_tick >= ch->debounce_ms) {
            ch->stable_level = ch->candidate_level;
        }
    }
}

这里有一个容易被忽视的细节:在步骤 2 检测到跳变后,立即 continue,不进入时间窗口判断。这意味着一个通道从跳变开始至少等待一个完整的 debounce_ms 窗口才能改变稳定态。这是防止「刚跳变就在同一周期内被判为稳定」的关键。

5. 状态查询 API

稳定态读取需要结合 active_level 进行极性转换:

bool generic_input_is_active(uint8_t ch_idx) {
    generic_input_ch_t *ch = &channels[ch_idx];
    if (ch->port == NULL) return false;  /* 未配置通道始终返回 inactive */
    return (ch->stable_level == ch->active_level);
}

bool generic_input_is_raw_active(uint8_t ch_idx) {
    generic_input_ch_t *ch = &channels[ch_idx];
    if (ch->port == NULL) return false;
    return (ch->raw_level == ch->active_level);
}

is_raw_active() 的存在是为了应对 急停(E-Stop) 等需要零延迟响应的高优先级输入。它绕过消抖层,直接返回当前物理电平。对于常规的限位开关、到位传感器等信号,则应始终使用 is_active() 以避免误触发。

6. 重同步机制

在实际项目中还发现一个边界场景:假设设备正在执行某个动作(如气缸伸出),动作的完成信号由 IN1 提供。在动作启动之前,IN1 可能处于任意状态(过去残留的稳定态可能仍然是「激活」)。如果直接用 is_active() 判断动作是否完成,就会因为历史状态错误而立即认为完成了。

解决方案是在动作开始前对相关输入做 一次强制重同步

void generic_input_resync(uint8_t ch_idx) {
    generic_input_ch_t *ch = &channels[ch_idx];
    if (ch->port == NULL) return;

    uint8_t hw_level = bsp_gpio_read_pin(ch->port, ch->pin);

    /* 将三个层级全部对齐到当前硬件电平 */
    ch->raw_level        = hw_level;
    ch->candidate_level  = hw_level;
    ch->stable_level     = hw_level;
    ch->candidate_start_tick = 0;  /* 强制候选为「已稳定」状态 */
}

重同步的典型调用时机:进入一个新的工艺步骤之前,对步骤相关的所有传感器输入做一次 resync(),确保消抖状态与物理世界之间没有历史残留。

7. 通道配置表

所有通道参数在编译期通过宏展开生成实例:

/* app_define.h — 传感器映射表 */
#define GENERIC_INPUT_CFG_TABLE \
    ENTRY(GPIOA, GPIO_PIN_0, ACTIVE_HIGH, 40),   /* CH0: 气缸到位传感器 */  \
    ENTRY(GPIOA, GPIO_PIN_1, ACTIVE_HIGH, 40),   /* CH1: 物料在位传感器 */  \
    ENTRY(GPIOA, GPIO_PIN_2, ACTIVE_LOW,  50),   /* CH2: 校准光电传感器 */  \
    ENTRY(GPIOA, GPIO_PIN_3, ACTIVE_HIGH, 40),   /* CH3: 料斗光电传感器 */

/* 初始化时通过 X-Macro 展开为结构体数组 */
static const generic_input_cfg_t input_cfg[GENERIC_INPUT_CH_MAX] = {
    #define ENTRY(p, n, a, d) { .port = p, .pin = n, .active_level = a, .debounce_ms = d },
    GENERIC_INPUT_CFG_TABLE
    #undef ENTRY
};

对于 未使用的通道,将配置设为 {NULL, 0},模块在 update() 循环中会跳过这些端口。此时 is_active()is_raw_active() 均返回 false(inactive)。

8. 禁用通道的处理

如果某个物理输入被禁用(比如硬件上没有连接或者被功能裁剪),只需要在配置表中将其项注释或设置为无效:

#define GENERIC_INPUT_CFG_TABLE \
    ENTRY(GPIOA, GPIO_PIN_0, ACTIVE_HIGH, 40),   /* CH0 */  \
    /* ENTRY(GPIOA, GPIO_PIN_1, ACTIVE_HIGH, 40), */        \
    ENTRY(NULL,  0,            ACTIVE_HIGH, 40),   /* CH1 — 禁用,占位 */ \
    ENTRY(GPIOA, GPIO_PIN_2, ACTIVE_LOW,  50),   /* CH2 */  \
    ENTRY(NULL,  0,            ACTIVE_HIGH, 40),   /* CH3 — 禁用,占位 */

这种做法使得模块在硬件裁剪时不需要修改任何逻辑代码,只需修改配置表即可。对于被禁用的通道,bsp_gpio_read_pin(NULL, 0) 的实现直接返回非激活电平值。

9. 与 RTOS 的集成方式

模块本身 不依赖 RTOS——它是一组纯计算函数,无阻塞、无临界区。唯一的外部输入是 now_ms 时间戳。典型的调用模式:

/* PackerTask 主循环 */
void packer_task(void *param) {
    generic_input_init();

    while (1) {
        uint32_t now = get_system_tick_ms();

        /* 步骤 1: 更新所有输入状态 */
        generic_input_update(now);

        /* 步骤 2: 读取稳定后的输入做工艺逻辑 */
        if (generic_input_is_active(CH_CYLINDER_OUT)) {
            /* 气缸已伸出到位,执行下一步 */
        }

        /* 步骤 3: 特殊信号走原始电平 */
        if (generic_input_is_raw_active(CH_ESTOP)) {
            emergency_stop();
        }

        osDelay(10);  /* 10 ms 周期 */
    }
}

消抖周期设为 40 ms,而 Task 调度周期为 10 ms,这意味着稳定态切换最多需要 4 个更新周期 + 40 ms 候选窗口才能反映最新物理状态——这在绝大多数工业场景下完全可以接受。

10. 总结

纯软件消抖方案在 STM32F103 这类资源受限的 MCU 上是可靠且经济的选择:

  • 零 BOM 成本:不需要电容电阻,省 PCB 面积也省物料
  • 参数可调:每个通道独立配置消抖时间,适应不同传感器特性(机械开关通常 20–50 ms,光电传感器可以更短)
  • 无阻塞设计:所有函数都是 O(1) 纯计算,不会干扰 RTOS 的调度延迟
  • 重同步机制:解决了状态机切换过程中的历史状态残留问题,是生产环境中一个非常实用的技巧
  • Service 层隔离:通过 BSP 抽象层屏蔽 HAL 细节,换 MCU 平台时只需重写 BSP 层,消抖逻辑完全不变

这套设计已经在实际产线上运行,稳定处理了累计数百万次的数字输入采样。如果你正在设计一个同样需要多路数字量输入的嵌入式系统,纯软件消抖 + 重同步的架构值得参考。


STM32F103 | DIGITAL INPUT | DEBOUNCE | GENERIC_INPUT_CONTROLLER | EMBEDDED

此作者没有提供个人介绍。
最后更新于 2026-05-30