4. GPIO实战:按键输入检测、LED流水灯、蜂鸣器驱动、GPIO中断配置

GPIO是MCU最基础、最常用的外设。说白了,它就是芯片和外界打交道的「手脚」。你想想看,一个单片机系统,无论多复杂,最终都要通过GPIO去读按键、点灯、驱动蜂鸣器、控制继电器。

我个人习惯,拿到一款新MCU,第一件事就是先把GPIO玩透。因为GPIO的初始化流程、寄存器配置、中断响应机制,几乎代表了这颗芯片的设计哲学。CMS32的GPIO模块设计得挺清爽,但有几个坑,我当年第一次用的时候也踩过。

本章核心目标:

  • 掌握CMS32 GPIO的输入/输出模式配置
  • 实现按键消抖与状态检测
  • 完成LED流水灯与蜂鸣器驱动
  • 理解并配置GPIO外部中断
GPIO实战知识体系 GPIO 核心配置 按键输入检测 LED流水灯 蜂鸣器驱动 GPIO中断配置 按键消抖 硬件消抖 / 软件延时消抖 边沿检测 vs 电平检测 流水灯模式 左移 / 右移 / 呼吸 定时器驱动时序 有源 vs 无源 PWM驱动方式 占空比与音量控制 中断触发方式 上升沿 / 下降沿 / 双边沿 中断优先级与嵌套 💡 实战要点 先配置时钟 → 再配置GPIO模式 → 最后配置中断(如需)

4.1 GPIO模式配置:输入与输出

CMS32的GPIO端口,每个引脚都可以独立配置。我习惯把GPIO配置分成三步走:开时钟、设模式、写数据。

配置步骤 寄存器/操作 说明
1. 使能端口时钟 PMCx / PFCx 先要让GPIO模块「通电」
2. 设置方向 PMx 寄存器 0=输出,1=输入
3. 读/写数据 Px 寄存器 输出写Px,输入读Px

我的习惯:初始化GPIO时,先把所有用到的引脚配置成输入(PMx=1),再逐个改成输出。这样可以防止上电瞬间引脚电平不确定导致误动作。我在一个车载项目中,就因为没注意这个顺序,上电时蜂鸣器「嘀」了一声,被客户投诉了。

4.2 按键输入检测与消抖

按键检测看起来简单,但实际项目中坑不少。机械按键按下和释放时,触点会抖动,持续时间通常5~20ms。如果不做消抖,一次按键可能被误判成多次。

我常用的消抖方案有两种:

  • 硬件消抖:RC低通滤波 + 施密特触发器。适合对成本不敏感的场景。
  • 软件消抖:检测到电平变化后,延时10~20ms再读一次。这是最通用的做法。
// 按键消抖示例代码
uint8_t Key_Scan(void)
{
    static uint8_t key_last = 1;  // 上次状态
    uint8_t key_now;
    uint8_t key_val = 0;

    key_now = (P1 & 0x01);  // 读取P1.0引脚

    if (key_last != key_now) {
        // 检测到变化,启动延时消抖
        Delay_ms(10);       // 延时10ms
        key_now = (P1 & 0x01);

        if (key_last == 1 && key_now == 0) {
            key_val = 1;    // 下降沿有效,按键按下
        }
        key_last = key_now;
    }
    return key_val;
}

注意:延时消抖期间,MCU会阻塞。如果系统有实时性要求,建议用定时器+状态机的方式做非阻塞消抖。我曾经在一个电机控制项目中,因为延时消抖导致PWM响应延迟,电机出现了抖动。

4.3 LED流水灯实现

流水灯是GPIO输出的经典练习。CMS32的GPIO驱动能力不错,直接推LED没问题,但记得串限流电阻,一般330Ω~1kΩ。

我个人喜欢用查表法做流水灯,代码简洁,扩展性好:

// 流水灯模式表
const uint8_t led_pattern[] = {
    0x01, 0x02, 0x04, 0x08,  // 左移
    0x08, 0x04, 0x02, 0x01   // 右移
};

void LED_Flow(void)
{
    static uint8_t index = 0;

    P0 = led_pattern[index];  // 直接赋值到P0口
    index++;
    if (index >= 8) {
        index = 0;
    }
    Delay_ms(200);  // 每200ms切换一次
}

小技巧:如果想让流水灯更平滑,可以用PWM控制每个LED的亮度,实现「呼吸」效果。CMS32的定时器可以输出多路PWM,配合DMA简直绝配。

4.4 蜂鸣器驱动

蜂鸣器分有源和无源两种。有源蜂鸣器给电就响,无源蜂鸣器需要PWM驱动。CMS32的GPIO可以直接驱动有源蜂鸣器,但要注意电流限制。

类型 驱动方式 频率控制 适用场景
有源蜂鸣器 GPIO高低电平 不可控 报警、提示音
无源蜂鸣器 PWM输出 可控 音乐、变调
// 有源蜂鸣器驱动
#define BUZZER_PIN   P2_0

void Buzzer_On(void)
{
    BUZZER_PIN = 1;  // 高电平驱动
}

void Buzzer_Off(void)
{
    BUZZER_PIN = 0;
}

// 无源蜂鸣器PWM驱动(使用定时器)
void Buzzer_PWM_Init(void)
{
    // 配置定时器输出2kHz PWM
    // 占空比50%
    TMR_Config(TMR0, 2000, 50);
    TMR_Start(TMR0);
}

我曾经踩过的坑:有源蜂鸣器内部有振荡电路,驱动电压不能太高。有一次我直接用5V驱动一个3V的蜂鸣器,结果烧了。一定要看数据手册的额定电压。

4.5 GPIO中断配置

GPIO中断是嵌入式系统的「事件驱动」核心。CMS32支持外部中断,每个端口都可以配置为中断源。

中断配置流程:

  1. 使能GPIO端口时钟
  2. 配置引脚为输入模式
  3. 设置中断触发方式(上升沿/下降沿/双边沿)
  4. 清除中断标志位
  5. 使能中断(全局中断 + 外设中断)
  6. 编写中断服务函数
// GPIO中断配置示例
void GPIO_Interrupt_Init(void)
{
    // 1. 使能时钟
    PMC1 |= 0x01;   // P1.0 使能

    // 2. 配置为输入
    PM1 |= 0x01;    // P1.0 输入模式

    // 3. 设置下降沿触发
    EGP1 |= 0x01;   // 下降沿使能
    // 或者上升沿:EGP1 &= ~0x01; 然后 PNP1 |= 0x01;

    // 4. 清除中断标志
    PIF1 &= ~0x01;

    // 5. 使能中断
    INTEN |= 0x01;  // 使能外部中断
    __enable_irq(); // 使能全局中断
}

// 中断服务函数
void INT_P1_IRQHandler(void)
{
    if (PIF1 & 0x01) {
        // 按键中断处理
        // 注意:中断里不要做耗时操作
        PIF1 &= ~0x01;  // 清除中断标志
    }
}

中断服务函数注意事项:

  • 尽量短小精悍,只做标志位设置
  • 必须清除中断标志,否则会一直触发
  • 不要在中断里调用延时函数
  • 注意中断优先级,避免嵌套过深

4.6 综合实战:按键控制流水灯模式

把前面几个知识点串起来,做一个完整的例子:按键切换流水灯模式,蜂鸣器给出提示音。

// 主循环
void main(void)
{
    uint8_t mode = 0;

    System_Init();      // 系统时钟初始化
    GPIO_Init();        // GPIO初始化
    Buzzer_Init();      // 蜂鸣器初始化
    GPIO_Interrupt_Init();  // 中断初始化

    while(1) {
        if (key_flag) {     // 中断中置位
            key_flag = 0;
            mode++;
            if (mode > 3) mode = 0;
            Buzzer_Beep(100);  // 蜂鸣器提示
        }

        switch(mode) {
            case 0: LED_LeftShift(); break;
            case 1: LED_RightShift(); break;
            case 2: LED_Breath(); break;
            case 3: LED_AllBlink(); break;
        }
        Delay_ms(100);
    }
}

我的建议:实际项目中,按键检测和LED控制最好用状态机实现,不要在主循环里用Delay。CMS32的定时器资源丰富,用定时器中断做时间基准,主循环只处理状态切换,这样系统响应更快,也更省电。

嗯,GPIO这部分内容就这些。看似基础,但每个点展开都有不少细节。你把这些代码跑通了,CMS32的GPIO就算入门了。后面章节我们会用到这些基础操作,到时候再深入。