4. GPIO进阶:从按键消抖到中断响应

各位同学,欢迎来到GPIO的进阶篇。上一章我们学会了怎么让LED灯亮起来,说白了就是GPIO输出。但DSP如果只能输出,那跟一个普通灯泡有什么区别?真正有意思的是输入——让芯片感知外部世界的变化。

这一章,我会带着大家搞定三个核心技能:按键输入检测、按键消抖处理、GPIO中断配置。这三板斧学会了,你的C2000就算是真正入门了。

4.1 GPIO输入模式配置

先说说输入模式。其实配置输入比输出还简单——你只需要告诉DSP:「这个引脚我要用来读数据,不是写数据」。

我个人习惯把GPIO输入配置写成这样:

// 配置GPIO12为输入模式
GpioCtrlRegs.GPAMUX1.bit.GPIO12 = 0;    // 设置为普通GPIO
GpioCtrlRegs.GPADIR.bit.GPIO12 = 0;     // 0=输入,1=输出
GpioCtrlRegs.GPAQSEL1.bit.GPIO12 = 0;   // 采样方式,0=仅同步,3=6次采样

这里有个细节我想强调一下:GPAQSEL这个寄存器。它控制输入信号的采样方式。我在项目中遇到过一个问题——按键明明按下了,程序就是检测不到。后来发现是采样次数设得太高,按键按下时间太短,DSP还没来得及采样就错过了。

我的建议:对于普通按键,采样次数设为0(仅同步)或1(3次采样)就够了。只有环境干扰特别严重的地方,才需要设成6次采样。

4.2 按键消抖原理与实现

说到按键,就不得不提消抖。你想想看,一个机械按键按下时,金属触点会弹跳几次才稳定。这个弹跳时间大概5-20ms。如果不做处理,DSP可能会认为你按了十几次。

消抖说白了就两种方法:硬件消抖软件消抖

硬件消抖简单粗暴——加个RC滤波电路。但咱们做嵌入式开发的,能省则省,一个电阻一个电容也是成本啊。所以我一般用软件消抖。

软件消抖的核心思想就一句话:等信号稳定了再读取。具体实现有两种:

  • 延时消抖:检测到电平变化后,延时20ms再读一次
  • 连续采样消抖:连续读取N次,如果结果一致才认为有效

我个人更推荐第二种,因为它不会阻塞程序运行。来看代码:

// 连续采样消抖实现
#define DEBOUNCE_COUNT   5   // 连续采样5次

Uint16 ReadKeyDebounced(void)
{
    Uint16 keyState = 0;
    Uint16 stableCount = 0;
    Uint16 i;
    
    for(i = 0; i < DEBOUNCE_COUNT; i++)
    {
        if(GpioDataRegs.GPADAT.bit.GPIO12 == 0)  // 按键按下(低电平有效)
        {
            stableCount++;
        }
        DELAY_US(1);  // 每次采样间隔1us
    }
    
    if(stableCount == DEBOUNCE_COUNT)
    {
        keyState = 1;  // 确认按下
    }
    
    return keyState;
}
注意:我曾经犯过一个错误——在消抖延时里用了for(i=0;i<100000;i++)这种空循环。结果按键响应慢得像蜗牛。后来改用定时器或者SysTick来做延时,程序流畅多了。

4.3 GPIO中断配置

好了,按键检测学会了。但有个问题——你总不能一直轮询按键吧?DSP还要干别的活呢。这时候就需要中断出场了。

中断的原理很简单:让硬件主动通知CPU,而不是CPU去轮询硬件。C2000的GPIO中断支持上升沿触发、下降沿触发、或者两者都触发。

配置GPIO中断的步骤,我总结为四步走:

  1. 配置GPIO为输入(刚才已经做了)
  2. 选择中断触发方式(上升沿/下降沿)
  3. 使能GPIO中断(打开对应的中断屏蔽位)
  4. 编写中断服务函数(ISR)

来看具体代码:

// 配置GPIO12为下降沿触发中断
// 步骤1:GPIO输入配置(同上,略)

// 步骤2:选择触发方式
GpioCtrlRegs.GPIOINTPRIMSEL.bit.GPIO12 = 0;  // 0=使用GPIOINT1A
GpioIntRegs.GPIOINT1SEL.bit.GPIO12 = 12;     // 选择GPIO12作为中断源

// 步骤3:配置触发边沿
GpioCtrlRegs.GPIOINTPOL.bit.GPIO12 = 0;      // 0=下降沿触发,1=上升沿触发

// 步骤4:使能中断
GpioIntRegs.GPIOINT1EN.bit.GPIO12 = 1;       // 使能GPIO12中断
PieCtrlRegs.PIEIER1.bit.INTx1 = 1;           // 使能PIE第1组第1个中断
IER |= M_INT1;                                // 使能CPU中断1

这里有个容易搞混的地方——GPIO中断号和PIE中断号的关系。C2000的GPIO中断先映射到GPIOINT1~GPIOINT6这6个中断线上,然后再映射到PIE的对应组。说白了就是两级映射。

核心要点:GPIO12的下降沿触发中断,最终会触发PIE第1组第1个中断(INT1.1)。这个对应关系在TI的手册里有详细表格,我建议你打印出来贴在工位上。

4.4 中断服务函数编写

中断配置好了,最后一步就是写中断服务函数。这个函数就是中断发生时要执行的代码。

写ISR有几个铁律:

  • 快进快出——不要在ISR里做复杂运算
  • 清除中断标志——否则会一直进中断
  • 保护现场——保存和恢复用到的寄存器

来看一个完整的按键中断服务函数:

// 中断服务函数
interrupt void GPIO_ISR(void)
{
    // 清除中断标志
    GpioIntRegs.GPIOINT1CLR.bit.GPIO12 = 1;
    PieCtrlRegs.PIEACK.all = PIEACK_GROUP1;
    
    // 读取按键状态(已经消抖过的)
    if(ReadKeyDebounced() == 1)
    {
        // 按键按下,执行对应操作
        GpioDataRegs.GPATOGGLE.bit.GPIO0 = 1;  // 翻转LED
    }
}

嗯,这里要注意一点:中断服务函数里不要做延时消抖。为什么?因为中断讲究的就是快。你想想看,如果在ISR里延时20ms,其他中断就得排队等着,系统实时性就没了。

我的做法是:中断里只设置一个标志位,主循环里检测这个标志位再做消抖处理。这样既利用了中断的实时性,又避免了阻塞。

4.5 本章知识体系

说了这么多,我画了一张图帮你理清思路:

GPIO进阶知识体系 GPIO进阶 GPIO输入配置 GPADIR = 0(输入) GPAQSEL(采样配置) 按键消抖 硬件消抖(RC滤波) 软件消抖(连续采样) GPIO中断 上升沿/下降沿触发 中断服务函数(ISR) 核心原则 中断里只设标志位,主循环里做消抖处理

这张图把本章的三个核心内容串起来了。你从左到右看:先配好输入模式,再搞定按键消抖,最后用中断来提升效率。每一步都是下一步的基础。

4.6 避坑指南

最后,我把自己踩过的坑总结一下,你遇到了可以直接拿来用:

常见问题 现象 解决方案
中断触发多次 按一次按键,LED闪了好几下 检查是否在ISR里清除了中断标志
按键不响应 按了没反应 检查GPIO方向寄存器是否设为了输入
程序卡死 进中断后出不来 检查PIEACK是否清除,以及中断优先级配置
误触发 没按键也会进中断 增加硬件滤波或提高采样次数

我曾经在一个项目里,因为忘了清除PIEACK,导致中断只触发了一次就再也不响应了。查了两天才找到原因。所以你现在看到我写中断代码,第一件事就是清标志——这已经成肌肉记忆了。

好了,这一章的内容就到这里。GPIO输入和中断是嵌入式开发的基石,你花时间把它吃透了,后面学PWM、ADC、CAN这些外设会轻松很多。动手试试吧,把代码烧进去,按按键看看LED会不会听话地翻转。


公众号:蓝海资料掘金营,微信deep3321