第4章 GPIO实战:按键输入检测、LED流水灯、位带操作实现原子化GPIO控制

GPIO,说白了就是芯片的“手脚”。你让它输出高电平,它就推个1出去;你读它的电平,就知道外部是按下还是松开。这一章我们直接上手,把CMS32的GPIO玩透。

我个人习惯,学一个新MCU,先点灯,再读按键。这两个动作能验证寄存器配置、时钟使能、引脚复用这些基础环节有没有搞对。搞不对?灯不亮,按键没反应,问题出在哪一目了然。

4.1 GPIO模块整体结构

先看一张总图,把GPIO的“骨架”搭起来。

CMS32 GPIO 模块结构图 APB 总线接口 配置寄存器组 (MODER / OTYPER / OSPEEDR / PUPDR / IDR / ODR / BSRR / LCKR) 输出驱动 (推挽/开漏) 输入采样 (施密特/上拉/下拉) 复用功能 (AF0~AF7) GPIO 引脚 Pad (PA0~PA15, PB0~PB15, ...) 每个引脚独立配置,支持位带操作 外部设备:LED / 按键 / 传感器 / 执行器

这张图里,寄存器组是“大脑”,功能模块是“肌肉”,引脚Pad是“手指”。你写寄存器,就是在指挥大脑,让手指去干活。

4.2 按键输入检测实战

按键检测,看起来简单,坑其实不少。我刚开始做嵌入式那会儿,以为读个IO电平就完事了。结果产品一上电,按键乱跳,按一下触发十次。后来才明白,机械按键有抖动,必须做消抖处理。

4.2.1 硬件连接

引脚方向外部电路默认电平
PA0输入按键 → GND高电平 (VDD)
PA1输入按键 → GND高电平 (VDD)

按键按下,引脚被拉到GND,读回来是0。松开,内部上拉电阻把它拉回1。嗯,这里要注意:CMS32内部上拉电阻默认是关闭的,必须手动使能。

4.2.2 寄存器配置步骤

  1. 使能GPIO时钟:RCC->AHBENR 置位对应位
  2. 配置模式:MODER 设为输入模式 (00)
  3. 使能内部上拉:PUPDR 设为上拉 (01)
  4. 读取电平:读 IDR 寄存器对应位

关键寄存器速查

  • RCC->AHBENR:GPIOA使能位在bit 0
  • GPIOA->MODER:每2位控制一个引脚,00=输入
  • GPIOA->PUPDR:每2位控制一个引脚,01=上拉
  • GPIOA->IDR:每1位对应一个引脚的电平

4.2.3 代码实现:带消抖的按键检测

/* 按键初始化 */
void Key_Init(void)
{
    RCC->AHBENR |= (1 << 0);          /* 使能GPIOA时钟 */
    GPIOA->MODER &= ~(0x03 << 0);    /* PA0: 输入模式 */
    GPIOA->MODER &= ~(0x03 << 2);    /* PA1: 输入模式 */
    GPIOA->PUPDR |= (0x01 << 0);     /* PA0: 上拉 */
    GPIOA->PUPDR |= (0x01 << 2);     /* PA1: 上拉 */
}

/* 带软件消抖的按键读取 */
uint8_t Key_Read(GPIO_TypeDef *GPIOx, uint16_t pin)
{
    if (GPIOx->IDR & pin) {
        return 1;  /* 未按下 */
    }
    /* 延时10ms消抖 */
    Delay_ms(10);
    if (GPIOx->IDR & pin) {
        return 1;  /* 抖动后恢复,视为未按下 */
    }
    return 0;      /* 确实按下了 */
}

我的经验:消抖延时10ms是个经验值。按键品质好,5ms也够;品质差的,20ms才稳。我一般留个宏定义,方便调试时改。

4.3 LED流水灯实战

流水灯,GPIO输出的“Hello World”。但别小看它,它能帮你验证引脚映射、时钟树、输出驱动能力这些基础环节。

4.3.1 硬件连接

引脚方向外部电路驱动方式
PB0输出LED → 电阻 → GND推挽输出
PB1输出LED → 电阻 → GND推挽输出
PB2输出LED → 电阻 → GND推挽输出
PB3输出LED → 电阻 → GND推挽输出

4.3.2 寄存器配置步骤

  1. 使能GPIOB时钟:RCC->AHBENR bit 1
  2. 配置模式:MODER 设为输出模式 (01)
  3. 配置输出类型:OTYPER 设为推挽 (0)
  4. 配置输出速度:OSPEEDR 设为低速 (00)
  5. 写ODR寄存器:控制LED亮灭

4.3.3 代码实现:流水灯

/* LED初始化 */
void LED_Init(void)
{
    RCC->AHBENR |= (1 << 1);          /* 使能GPIOB时钟 */
    GPIOB->MODER &= ~(0xFF << 0);    /* 清除PB0~PB3模式位 */
    GPIOB->MODER |= (0x55 << 0);     /* PB0~PB3: 输出模式 */
    GPIOB->OTYPER &= ~(0x0F << 0);  /* 推挽输出 */
    GPIOB->OSPEEDR &= ~(0xFF << 0); /* 低速 */
}

/* 流水灯主循环 */
void LED_Flow(void)
{
    uint8_t i;
    uint16_t led_pattern = 0x01;  /* 从PB0开始 */

    while (1) {
        GPIOB->ODR = (GPIOB->ODR & ~0x0F) | led_pattern;
        Delay_ms(200);            /* 每200ms切换一次 */

        led_pattern <<= 1;        /* 左移一位 */
        if (led_pattern > 0x08) {
            led_pattern = 0x01;   /* 回到起点 */
        }
    }
}

注意:写ODR时,一定要用“读-改-写”操作,不要直接赋值覆盖其他引脚的状态。否则,其他引脚的电平会被意外改变。

4.4 位带操作:原子化GPIO控制

为什么需要位带操作?你想想看,在多任务环境下,一个“读-改-写”操作可能被中断打断。比如你在读ODR,准备改bit 3,结果中断里改了bit 5,等你写回去,bit 5的状态就被覆盖了。这就是竞态条件。

位带操作,说白了就是把一个32位的寄存器,映射到一段“位带别名区”。你往别名区写一个字,硬件自动完成“读-改-写”,而且这个过程是原子的,不会被中断打断。

4.4.1 位带映射公式

CMS32的位带映射和Cortex-M内核一致:

别名区地址 = 位带基址 + (字节偏移 × 32) + (位号 × 4)

具体到GPIO:

区域位带区基址别名区基址
SRAM0x2000_00000x2200_0000
外设 (含GPIO)0x4000_00000x4200_0000

4.4.2 位带操作宏定义

/* 位带操作宏:将外设地址的某个位映射到别名区 */
#define BITBAND_PERIPH(addr, bit) \
    (*(volatile uint32_t *)(0x42000000 + ((uint32_t)(addr) - 0x40000000) * 32 + (bit) * 4))

/* GPIO ODR 位带别名 */
#define PAout(n)  BITBAND_PERIPH(&GPIOA->ODR, n)
#define PBout(n)  BITBAND_PERIPH(&GPIOB->ODR, n)

/* GPIO IDR 位带别名 */
#define PAin(n)   BITBAND_PERIPH(&GPIOA->IDR, n)
#define PBin(n)   BITBAND_PERIPH(&GPIOB->IDR, n)

4.4.3 使用位带操作改写流水灯

void LED_Flow_Bitband(void)
{
    uint8_t i;

    while (1) {
        /* 逐个点亮,原子操作,无需读-改-写 */
        for (i = 0; i < 4; i++) {
            PBout(i) = 1;      /* 点亮 */
            Delay_ms(200);
            PBout(i) = 0;      /* 熄灭 */
        }
    }
}

位带操作的优势

  • 原子性:单次总线访问,不会被中断打断
  • 代码简洁:PBout(3) = 1,比 GPIOB->ODR |= (1<<3) 直观得多
  • 性能:省去了读-改-写的三条指令,只需一条STR

我的习惯:只要项目里用到了RTOS或者有多个中断源,GPIO输出我一定用位带操作。曾经在一个电机驱动项目里,就是因为中断里改了另一个引脚的状态,导致电机抖动。换成位带操作后,问题立刻消失。

4.5 本章核心知识速查表

知识点关键寄存器/操作注意事项
按键输入MODER=00, PUPDR=01, 读IDR必须消抖,10ms延时
LED输出MODER=01, OTYPER=0, 写ODR读-改-写,避免覆盖
位带操作别名区地址 = 基址 + 偏移×32 + 位×4仅适用于SRAM和外设区
原子性单次总线写操作多任务/中断环境必用