3. GPIO 与基础外设:从按键到中断的实战之路

GPIO 是嵌入式系统最基础、也最容易被轻视的外设。说白了,它就是芯片和外界打交道的「手脚」——拉高拉低、读个电平、触发个中断。但就是这看似简单的操作,我在医疗项目中踩过的坑可不少。

记得有一次,心电采集设备的按键在 ICU 里频繁误触发,护士按一次启动,设备却重启了三次。查到最后,就是 GPIO 消抖没做好。所以这一章,咱们把 GPIO 的输入输出、按键消抖、LED 指示、中断优先级这些基本功,一次性讲透。

本章核心知识点:GPIO 工作模式、硬件消抖 vs 软件消抖、中断优先级分组与配置、实战中的抗干扰设计。
GPIO 与基础外设知识体系 GPIO 核心 输入模式 输出模式 外部中断 复用功能 上拉/下拉 消抖处理 优先级配置 嵌套与抢占 LED 驱动 实战要点:输入防抖 + 输出限流 + 中断优先级分组 = 可靠的外设控制

3.1 GPIO 工作模式:不只是输入输出

很多新手以为 GPIO 就是「读」和「写」。其实在 STM32 这类 MCU 上,每个 GPIO 引脚可以配置成 8 种模式。我习惯把它们分成四大类:

模式类别 典型应用 注意事项
推挽输出 LED 驱动、蜂鸣器 注意灌电流限制,一般不超过 20mA
开漏输出 I²C 总线、电平转换 必须外接上拉电阻
浮空输入 外部按键(需外接上拉) 电平不确定,易受干扰
上拉/下拉输入 按键检测、传感器数字输入 内部上拉约 40kΩ,弱驱动场景够用
模拟输入 ADC 采样 关闭数字功能,降低功耗
复用推挽/开漏 USART、SPI、定时器输出 由外设控制,GPIO 寄存器失效
我的习惯:医疗设备中,按键输入我优先用内部上拉输入模式,省掉一颗外部电阻。但如果是长线缆传输的信号,我会外加 4.7kΩ 上拉到 VCC,抗干扰能力更强。

3.2 按键消抖:硬件与软件的双保险

机械按键按下时,触点会弹跳 5-20ms。如果不处理,一次按下可能被识别成多次。我在心电监护仪的「开始/停止」按键上就吃过这个亏——护士按一次,设备认为按了三次,直接死机。

消抖有两种思路:

3.2.1 硬件消抖

在按键两端并联一个 0.1μF 电容,利用电容的充放电特性滤除毛刺。优点是零 CPU 开销,缺点是增加 BOM 成本,且电容值需要根据按键机械特性调整。

3.2.2 软件消抖

这是最常用的方法。检测到电平变化后,延时 10-20ms 再读一次。如果电平一致,才确认按键动作。

// 软件消抖示例(STM32 HAL 库风格)
uint8_t Debounce_ReadPin(GPIO_TypeDef* GPIOx, uint16_t GPIO_Pin)
{
    if (HAL_GPIO_ReadPin(GPIOx, GPIO_Pin) == GPIO_PIN_RESET)  // 第一次检测到按下
    {
        HAL_Delay(15);  // 延时 15ms,避开抖动区间
        if (HAL_GPIO_ReadPin(GPIOx, GPIO_Pin) == GPIO_PIN_RESET)  // 再次确认
        {
            return 1;  // 确认按下
        }
    }
    return 0;  // 抖动或未按下
}
避坑指南:我曾经在 RTOS 环境下用 HAL_Delay() 做消抖,结果导致高优先级任务被阻塞。后来改用定时器轮询 + 状态机的方式,既不影响实时性,又保证了消抖效果。记住:中断服务函数里绝对不要用延时消抖!

3.3 LED 指示:驱动与限流

LED 看起来简单,但医疗设备对指示灯的亮度、颜色、闪烁频率都有严格要求。比如报警指示灯必须是红色,闪烁频率 1-2Hz,亮度不低于 100mcd。

驱动 LED 时,限流电阻的计算公式:

R = (VCC - VF) / IF

其中 VF 是 LED 正向压降(红色约 2V,蓝色约 3.3V),IF 是工作电流(通常 5-10mA)。

举个例子:VCC=3.3V,红色 LED,IF=5mA,则 R = (3.3 - 2) / 0.005 = 260Ω,取标称值 270Ω。

经验之谈:我习惯在 LED 驱动引脚上加一个 100Ω 的串联电阻做限流保护,即使软件配置错误,也不会烧坏 GPIO。另外,高亮度 LED 不要用 GPIO 直驱,加一个三极管或 MOSFET 更稳妥。

3.4 中断优先级配置:别让紧急事件排队

医疗系统中,中断优先级配置直接决定系统的实时响应能力。比如心率检测中断必须比串口通信中断优先级高——心跳丢了可是要出人命的。

以 ARM Cortex-M 系列为例,中断优先级分为抢占优先级和子优先级。配置时遵循两个原则:

  1. 时间敏感的中断优先级最高:比如定时器采样、外部触发信号。
  2. 非关键中断让路:比如按键中断、LED 刷新,可以放在低优先级。

优先级分组由 SCB->AIRCR 寄存器控制,常见分组方式:

分组 抢占优先级位数 子优先级位数 适用场景
NVIC_PriorityGroup_0 0 位 4 位 所有中断同级,无嵌套
NVIC_PriorityGroup_2 2 位 2 位 4 级抢占,每级 4 个子优先级
NVIC_PriorityGroup_4 4 位 0 位 16 级抢占,无子优先级

我个人习惯在医疗项目中使用 NVIC_PriorityGroup_2,既保留了足够的抢占层级,又能在同一优先级内做子优先级排序。

// 中断优先级配置示例
HAL_NVIC_SetPriorityGrouping(NVIC_PriorityGroup_2);

// 心率检测中断:抢占优先级 0,子优先级 0(最高)
HAL_NVIC_SetPriority(EXTI0_IRQn, 0, 0);
HAL_NVIC_EnableIRQ(EXTI0_IRQn);

// 按键中断:抢占优先级 2,子优先级 1(较低)
HAL_NVIC_SetPriority(EXTI1_IRQn, 2, 1);
HAL_NVIC_EnableIRQ(EXTI1_IRQn);
注意:中断优先级一旦配置,整个系统运行期间不要修改。我曾经在调试时动态改过优先级,结果导致中断嵌套死锁,设备直接挂死。配置写在系统初始化阶段,之后只读。

3.5 实战中的抗干扰设计

医疗设备经常面临强电磁干扰环境——电刀、除颤仪、MRI 设备。GPIO 作为最外层的接口,必须做好防护。

  • 输入引脚加 TVS 管:防止静电放电损坏 GPIO。
  • 长线缆使用差分信号:比如 RS-485 或 CAN,避免单端 GPIO 直接驱动长线。
  • 未使用的 GPIO 引脚:配置为模拟输入或下拉输出,不要悬空。悬空引脚会像天线一样引入噪声。
  • 软件滤波:在中断服务函数中连续采样 3 次,取多数值,避免单次误触发。
总结一下:GPIO 看似简单,但输入要防抖、输出要限流、中断要分优先级、整体要抗干扰。把这些基本功练扎实了,后面的传感器采集和生理信号处理才能站得住脚。