GPIO输入与中断:从配置到精确测量
GPIO输入和中断,是嵌入式系统里最基础也最容易被忽视的环节。很多人觉得不就是读个引脚电平嘛,有什么好讲的?但我在几个量产项目中吃过亏——中断响应慢了导致数据丢包,电平抖动没处理好引发误触发,优先级配错了造成死锁。嗯,这些坑踩过一遍,你就知道这里面的门道有多深了。
上拉与下拉配置:别让引脚“悬空”
先说说上拉和下拉。说白了,就是给引脚一个确定的电平状态。如果引脚既不接高也不接低,那就是“悬空”状态——外界一点电磁干扰就能让电平乱跳。
在嵌入式HAL里,配置上拉/下拉通常就一行代码。以STM32的GPIO为例:
// 配置PA0为上拉输入
let gpioa = dp.GPIOA.split();
let button = gpioa.pa0.into_pull_up_input();
// 或者下拉输入
let button = gpioa.pa0.into_pull_down_input();
我个人习惯:按键类输入用上拉,低电平触发;传感器信号用下拉,高电平触发。这样逻辑更直观。
| 配置方式 | 默认电平 | 适用场景 |
|---|---|---|
| 上拉输入 | 高电平 | 按键、开关、机械触点 |
| 下拉输入 | 低电平 | 传感器输出、逻辑信号 |
| 浮空输入 | 不确定 | 外部已有上下拉电路 |
中断触发方式:边沿还是电平?
中断触发方式的选择,直接影响系统的可靠性。边沿触发和电平触发,各有各的脾气。
边沿触发:检测电平的跳变(上升沿或下降沿)。优点是响应快,只触发一次。缺点是容易受噪声干扰——一个毛刺就可能触发一次中断。
电平触发:只要电平保持有效状态就一直触发。优点是抗干扰强,缺点是如果中断服务程序没清掉电平状态,会反复触发。
我在项目中遇到过这样一个问题:用边沿触发检测编码器脉冲,结果电机一启动,中断频率直接飙到几十KHz,CPU啥也干不了了。后来改成电平触发+定时查询,才把问题解决。
// 配置上升沿触发中断
let exti = Exti::new(dp.EXTI);
let button_interrupt = exti.listen_gpio(&button, TriggerEdge::Rising);
// 配置下降沿触发
let button_interrupt = exti.listen_gpio(&button, TriggerEdge::Falling);
// 配置双边沿触发
let button_interrupt = exti.listen_gpio(&button, TriggerEdge::Both);
中断优先级与嵌套:别让高优先级饿死
中断优先级配置,是实时系统的命门。配置不好,轻则响应延迟,重则系统崩溃。
ARM Cortex-M系列通常支持8~16级抢占优先级。数值越小,优先级越高。我一般这样分配:
- 最高优先级(0-1):系统时钟、看门狗、紧急故障
- 高优先级(2-3):实时通信(SPI、I2C)、DMA完成
- 中优先级(4-7):GPIO中断、定时器
- 低优先级(8-15):UART接收、ADC转换完成
// 设置中断优先级(数值越小优先级越高)
NVIC::set_priority(Interrupt::EXTI0, 4);
NVIC::set_priority(Interrupt::TIM2, 2);
NVIC::enable(Interrupt::EXTI0);
NVIC::enable(Interrupt::TIM2);
中断嵌套要注意一个关键点:高优先级中断可以打断低优先级中断,但同优先级不会互相打断。我曾经见过一个案例,两个同优先级的GPIO中断互相抢CPU,结果谁都没法完整执行——这就是典型的优先级配置失误。
测量中断延迟:你到底快不快?
中断延迟,就是从硬件触发到CPU开始执行中断服务程序的时间。这个指标直接决定了系统的实时性。
测量方法其实不复杂:在中断触发引脚上接一个逻辑分析仪,同时在中断服务程序入口翻转另一个GPIO。两个信号的时间差,就是中断延迟。
// 测量中断延迟的示例代码
static mut START_TIME: u32 = 0;
#[interrupt]
fn EXTI0() {
// 记录当前时间
let now = DWT::get_cycle_count();
// 计算延迟
let latency = now - unsafe { START_TIME };
// 翻转测试引脚
test_pin.set_high().ok();
// 记录下一次触发时间
unsafe { START_TIME = now; }
}
影响中断延迟的因素主要有:
- CPU当前是否在执行更高优先级的中断
- 中断向量表的访问速度(RAM还是Flash?)
- CPU的流水线状态
- 总线仲裁延迟(特别是DMA在传输时)
使用DWT周期计数器:精确到纳秒级
DWT(Data Watchpoint and Trace)是Cortex-M内核自带的调试组件。它的周期计数器可以精确记录CPU时钟周期数,精度远高于普通的定时器。
启用DWT很简单:
// 启用DWT周期计数器
let dwt = DWT::new(dp.DWT, &mut dp.DCB);
dwt.enable_cycle_counter();
// 测量代码段执行时间
let start = DWT::get_cycle_count();
// 要测量的代码
critical_section::with(|cs| {
// 中断处理
});
let end = DWT::get_cycle_count();
let elapsed_cycles = end - start;
let elapsed_ns = elapsed_cycles * 1000 / 72; // 72MHz下
我个人习惯用DWT做三件事:
- 测量中断延迟:精确到单个时钟周期
- 分析代码热点:找出最耗时的函数
- 生成微秒级延时:比定时器中断更精准
知识结构总览
下面这张图把GPIO输入与中断的核心知识点串起来了:
实战建议:从配置到验证的完整流程
最后,我总结一套我自己在项目中用的流程:
- 先确认硬件:看原理图,引脚有没有外部上下拉?电平逻辑对不对?
- 配置GPIO:设置模式、上下拉、输出类型
- 配置中断:选择触发方式、设置优先级、使能中断
- 写中断服务程序:尽量短,只做必要操作
- 测量验证:用DWT或逻辑分析仪测中断延迟,确认满足实时性要求
- 压力测试:模拟最坏情况(多个中断同时触发),看系统是否稳定