第四章 GPIO实战:按键控制LED、外部中断触发、中断优先级配置、去抖处理

好,咱们进入第四章。这一章是真正动手的环节了。

前面几章我们把AURIX的架构、开发环境、时钟系统都捋了一遍。说实话,那些都是基础,但真正让单片机“活”起来的,就是GPIO和中断。你想想看,一个单片机如果连按键都读不了,灯都点不亮,那跟一块石头有什么区别?

这一章,我会带着你从最简单的按键控制LED开始,一步步深入到外部中断、优先级配置,最后再解决一个让很多新手头疼的问题——按键抖动。嗯,这些都是我当年踩过的坑,咱们一个一个填平。

4.1 按键控制LED:最朴素的输入输出

先来个最简单的。一个按键,一个LED。按下按键,灯亮;松开,灯灭。这听起来像单片机界的“Hello World”,对吧?

但你别小看它。我在项目中遇到过,有些工程师连这个基础逻辑都写不对——不是因为不会,而是因为忽略了端口配置的细节。

4.1.1 硬件连接与端口配置

假设我们用的是AURIX TC275开发板。LED接在P00.5,按键接在P00.6。注意,按键通常是低电平有效——按下时引脚被拉到GND,松开时被内部上拉电阻拉到高电平。

配置端口,我习惯分三步走:

  1. 使能端口模块时钟——别笑,真有人忘了这一步,折腾半天发现寄存器写不进去。
  2. 配置引脚功能——设置为GPIO模式,而不是复用功能。
  3. 设置方向——LED引脚输出,按键引脚输入。

核心要点:按键引脚一定要配置上拉电阻。否则引脚悬空时电平不确定,读到的值会乱跳。

代码怎么写?看下面:

#include "IfxPort.h"
#include "IfxPort_PinMap.h"

#define LED_PIN     &IfxPort_P00_5  // LED引脚
#define KEY_PIN     &IfxPort_P00_6  // 按键引脚

void GPIO_Init(void)
{
    // 1. 使能端口模块(通常由系统初始化完成,但显式调用更安全)
    IfxPort_enableModule(&MODULE_P00);

    // 2. 配置LED引脚为推挽输出
    IfxPort_setPinMode(LED_PIN, IfxPort_Mode_outputPushPullGeneral);

    // 3. 配置按键引脚为输入,带上拉电阻
    IfxPort_setPinMode(KEY_PIN, IfxPort_Mode_inputPullUp);
}

void Key_Control_LED(void)
{
    // 读取按键状态
    uint8 keyState = IfxPort_getPinState(KEY_PIN);

    // 按键按下(低电平)则点亮LED
    if(keyState == 0)
    {
        IfxPort_setPinLow(LED_PIN);  // 低电平点亮
    }
    else
    {
        IfxPort_setPinHigh(LED_PIN); // 高电平熄灭
    }
}

这段代码逻辑上没问题。但实际跑起来,你可能会发现一个问题——灯的反应有点“神经质”。按下时偶尔会闪一下,或者松开时灯会抖一下。这就是抖动在作怪。

4.2 按键抖动:一个被忽视的“敌人”

为什么会抖动?说白了,按键是机械结构。按下和松开的瞬间,金属触点会弹跳几次,每次持续几毫秒到十几毫秒。在这段时间里,单片机读到的是“0-1-0-1-0”这样的乱序信号。

我刚开始做项目时,就因为这个被坑过。一个简单的按键计数程序,按一次能跳好几个数。当时我还以为是芯片坏了,查了半天……后来才知道,是没做去抖处理。

警告:千万不要在中断服务函数里做延时去抖!中断服务函数要求快速执行,延时会阻塞其他中断,导致系统响应变慢甚至崩溃。

4.2.1 软件去抖的两种常用方法

去抖的方法有很多,我挑两个最实用的讲:

  • 延时去抖法:检测到按键状态变化后,延时10-20ms,再读一次。如果状态一致,就认为是有效动作。简单粗暴,适合对实时性要求不高的场景。
  • 状态机去抖法:通过连续多次采样,判断状态是否稳定。不阻塞CPU,适合在定时中断中执行。

我个人更推荐状态机法。为什么呢?因为延时去抖虽然简单,但如果你在主循环里用delay_ms(20),整个系统都会被卡住。你想想看,如果这时候有更紧急的中断来了,怎么办?

状态机去抖的代码示例:

#define DEBOUNCE_TIME_MS    20  // 去抖时间
#define SAMPLE_INTERVAL_MS  5   // 采样间隔

typedef enum
{
    STATE_IDLE,      // 空闲状态
    STATE_PRESSED,   // 按下检测中
    STATE_RELEASED   // 松开检测中
} DebounceState;

DebounceState debounceState = STATE_IDLE;
uint8 keyStableState = 1;  // 稳定后的按键状态,默认高电平(松开)
uint16 debounceCounter = 0;

void Debounce_Task(void)  // 每5ms调用一次
{
    uint8 currentLevel = IfxPort_getPinState(KEY_PIN);

    switch(debounceState)
    {
        case STATE_IDLE:
            if(currentLevel == 0)  // 检测到按下
            {
                debounceState = STATE_PRESSED;
                debounceCounter = 0;
            }
            break;

        case STATE_PRESSED:
            if(currentLevel == 0)
            {
                debounceCounter++;
                if(debounceCounter >= (DEBOUNCE_TIME_MS / SAMPLE_INTERVAL_MS))
                {
                    // 确认按下
                    keyStableState = 0;
                    debounceState = STATE_RELEASED;
                    debounceCounter = 0;
                    // 在这里触发按键按下事件
                    Key_Pressed_Callback();
                }
            }
            else
            {
                // 抖动,回到空闲状态
                debounceState = STATE_IDLE;
            }
            break;

        case STATE_RELEASED:
            if(currentLevel == 1)  // 检测到松开
            {
                debounceCounter++;
                if(debounceCounter >= (DEBOUNCE_TIME_MS / SAMPLE_INTERVAL_MS))
                {
                    keyStableState = 1;
                    debounceState = STATE_IDLE;
                    // 在这里触发按键松开事件
                    Key_Released_Callback();
                }
            }
            else
            {
                debounceState = STATE_PRESSED;
            }
            break;
    }
}

这段代码看着长,但逻辑很清晰。它用了一个简单的状态机,配合定时采样,既实现了去抖,又不阻塞CPU。嗯,这才是工程上能用的写法。

4.3 外部中断:让CPU“主动”响应按键

前面我们用的是轮询方式——主循环不断去读按键状态。但你想过没有,如果CPU正在忙别的事,比如处理通信数据,按键按下去可能要等几十毫秒才能被响应。这在一些实时性要求高的场合是不能接受的。

这时候就该外部中断出场了。外部中断让CPU在按键按下时“主动”跳出来处理,而不是一遍遍去问“你按了吗?你按了吗?”

4.3.1 配置外部中断

AURIX的外部中断配置,我习惯按这个顺序来:

  1. 配置引脚为输入——和前面一样,带上拉电阻。
  2. 选择中断触发方式——下降沿触发(按下时)、上升沿触发(松开时),或者双边沿触发。
  3. 配置中断优先级——这个后面细讲。
  4. 使能中断——别忘了全局中断也要打开。

小技巧:我一般用下降沿触发检测按键按下。因为按下动作是用户主动触发的,响应要快。松开动作通常不需要立即响应。

配置代码:

#include "IfxPort.h"
#include "IfxSrc.h"
#include "IfxCpu_Irq.h"

// 中断服务函数声明
IFX_INTERRUPT(ISR_KeyPressed, 0, ISR_PRIORITY_KEY);

void ExtInterrupt_Init(void)
{
    // 1. 配置引脚为输入带上拉
    IfxPort_setPinMode(KEY_PIN, IfxPort_Mode_inputPullUp);

    // 2. 使能引脚的中断功能
    IfxPort_enablePinInterrupt(KEY_PIN, IfxPort_InputInterruptType_descedingEdge);

    // 3. 配置中断优先级
    volatile Ifx_SRC_SRCR *src = IfxPort_getSrcPointer(KEY_PIN);
    IfxSrc_init(src, ISR_PROVIDER_CPU0, ISR_PRIORITY_KEY);
    IfxSrc_enable(src);

    // 4. 使能CPU全局中断
    IfxCpu_enableInterrupts();
}

// 中断服务函数
IFX_INTERRUPT(ISR_KeyPressed, 0, ISR_PRIORITY_KEY)
{
    // 注意:这里不要做耗时操作!
    // 只设置一个标志位,具体处理放到主循环或任务中
    g_keyPressedFlag = 1;

    // 如果需要,可以在这里做简单的去抖
    // 但更推荐用定时器+状态机的方式
}

看到没?中断服务函数里我只做了一件事——设置标志位。为什么?因为中断服务函数执行时间越短越好。我曾经见过有人在中断里做延时、做printf打印,结果系统直接卡死。嗯,这都是血的教训。

4.4 中断优先级配置:谁更紧急谁先来

AURIX的中断系统很强大,支持多级优先级。但强大也意味着复杂。配置不好,就会出现“低优先级中断永远得不到响应”或者“中断嵌套把自己搞死”的情况。

4.4.1 优先级配置原则

我个人总结了几条原则,你记一下:

  • 时间敏感的中断优先级高——比如电机控制、PWM周期中断。
  • 数据量大的中断优先级低——比如串口接收,丢一帧数据还能重发,但电机控制丢一个脉冲可能就飞车了。
  • 按键中断优先级适中——比通信高一点,比实时控制低一点。

在AURIX中,优先级配置通过IfxSrc_init()函数完成。第一个参数是中断源指针,第二个是服务提供者(CPU核),第三个就是优先级数值。数值越大,优先级越高。

// 假设系统中有三个中断
#define ISR_PRIORITY_KEY    10   // 按键中断
#define ISR_PRIORITY_UART   8    // 串口中断
#define ISR_PRIORITY_PWM    12   // PWM中断(最高)

// 配置时按优先级从高到低配置,避免冲突
IfxSrc_init(srcPwm,  ISR_PROVIDER_CPU0, ISR_PRIORITY_PWM);
IfxSrc_init(srcKey,  ISR_PROVIDER_CPU0, ISR_PRIORITY_KEY);
IfxSrc_init(srcUart, ISR_PROVIDER_CPU0, ISR_PRIORITY_UART);

注意:不要给两个中断设置相同的优先级!虽然AURIX支持同优先级轮询,但容易引入不确定性。我建议每个中断一个独立优先级,方便调试。

4.5 综合实战:带去抖的按键中断控制LED

好了,我们把前面所有知识点串起来。做一个完整的例子:按键按下时,用中断响应,配合定时器去抖,然后翻转LED状态。

这个例子的架构是这样的:

  • 外部中断检测按键下降沿,触发后启动一个定时器。
  • 定时器每5ms中断一次,在定时中断里执行去抖状态机。
  • 去抖确认后,设置一个事件标志。
  • 主循环检测到事件标志,翻转LED。

为什么要这么绕?因为中断服务函数里不能做延时,也不能做复杂逻辑。把去抖放到定时中断里,既保证了实时性,又不阻塞系统。这是工程上最稳妥的做法。

// 全局变量
volatile uint8 g_keyEvent = 0;  // 按键事件标志

// 外部中断服务函数(按键按下触发)
IFX_INTERRUPT(ISR_KeyPressed, 0, ISR_PRIORITY_KEY)
{
    // 启动去抖定时器(假设定时器已配置好)
    IfxStm_enableTimer(&stmTimer, DEBOUNCE_TIMEOUT);
}

// 定时器中断服务函数(每5ms触发)
IFX_INTERRUPT(ISR_TimerTick, 0, ISR_PRIORITY_TIMER)
{
    // 执行去抖状态机
    Debounce_Task();

    // 如果去抖完成,设置事件标志
    if(debounceState == STATE_IDLE && keyStableState == 0)
    {
        g_keyEvent = 1;
        keyStableState = 1;  // 复位,等待下次按下
    }
}

// 主循环
void main(void)
{
    // 初始化
    GPIO_Init();
    ExtInterrupt_Init();
    Timer_Init();

    while(1)
    {
        if(g_keyEvent)
        {
            g_keyEvent = 0;
            // 翻转LED
            IfxPort_togglePin(LED_PIN);
        }

        // 其他任务...
    }
}

总结一下:这个例子虽然简单,但体现了嵌入式系统设计的核心思想——分层、解耦、不阻塞。外部中断只负责“通知”,定时中断负责“确认”,主循环负责“执行”。各司其职,互不干扰。

4.6 避坑指南:我踩过的那些坑

最后,分享几个我实际项目中遇到的坑,你遇到了可以少走弯路:

  • 坑一:中断里调用printf——printf是阻塞的,而且重入不安全。我在调试时习惯用GPIO翻转来指示中断是否触发,简单可靠。
  • 坑二:忘记清中断标志——AURIX有些外设的中断标志需要软件清除。不清除的话,中断会一直触发,CPU直接卡死在中断里。
  • 坑三:优先级配置冲突——我曾经把两个中断配成相同优先级,结果一个中断触发后,另一个永远响应不了。查了两天才发现是优先级冲突。
  • 坑四:去抖时间太短——有些按键质量差,抖动时间可能长达20ms。我建议去抖时间设置在15-30ms之间,太短去不干净,太长影响手感。

嗯,这一章的内容就到这里。从最简单的按键控制LED,到外部中断,再到去抖处理和优先级配置,我们一步步把GPIO和中断的核心知识点都过了一遍。下一章我们会进入更高级的外设——定时器,到时候你会发现,定时器和中断配合起来,能玩出很多花样。

记住,嵌入式开发没有捷径。多动手、多踩坑、多总结,你也能成为高手。