4. GPIO 驱动实战:点亮 LED 与读取按键状态

好,我们正式开始动手写代码。前面几章讲了寄存器、时钟、GPIO 模式,都是理论。这一章,咱们直接拿两块最基础的外设——LED 和按键——来练手。你想想看,一个嵌入式系统最朴素的“输出”和“输入”就是这两样东西了。

我个人习惯,学一个新 MCU 平台,第一个工程永远是点灯。灯亮了,说明你的时钟配置对了、GPIO 模式设对了、代码下载进去了。这一步走通,后面才有信心。

4.1 硬件连接与原理回顾

先看硬件。我假设你手头的板子上,LED 接在 GPIOA Pin 5,按键接在 GPIOC Pin 13。当然,不同开发板引脚不一样,但原理完全一样。

外设 GPIO 端口 Pin 默认电平 有效电平
LED GPIOA 5 低电平(灭) 高电平(亮)
按键 GPIOC 13 高电平(通过上拉电阻) 低电平(按下)

这里有个细节:按键按下时为什么是低电平?因为外部电路把按键一端接地了,另一端通过上拉电阻接到 VCC。没按时,引脚被拉到高电平;按下去,引脚直接接地,变成低电平。嗯,这个电路很常见,但新手容易搞反。

小提示: 如果你板子上的按键按下后读到的电平是反的,别慌。检查一下外部电路是上拉还是下拉,或者直接在代码里把逻辑取反就行。我刚开始做项目时,就因为这个“电平极性”问题折腾了半小时。

4.2 GPIO 输出模式配置(点亮 LED)

点亮 LED 需要把 GPIO 设为推挽输出模式。为什么是推挽?因为推挽输出既能灌电流也能拉电流,驱动 LED 最合适。开漏输出一般用于 I2C 等总线,驱动 LED 反而麻烦。

配置步骤其实就三步:

  1. 开启 GPIO 时钟 —— 不开启时钟,寄存器写不进去。
  2. 配置 GPIO 模式寄存器 —— 设为输出模式,推挽结构。
  3. 写输出数据寄存器 —— 置 1 点亮,清 0 熄灭。

直接上代码。我用的是寄存器操作,不依赖 HAL 库。为什么?因为寄存器操作让你真正理解硬件在干什么。

/* 步骤1:开启 GPIOA 时钟(挂在 AHB1 总线上) */
RCC->AHB1ENR |= (1 << 0);   // GPIOA 时钟使能

/* 步骤2:配置 PA5 为输出模式,推挽结构 */
GPIOA->MODER &= ~(0x3 << (5 * 2));  // 先清空 PA5 的模式位
GPIOA->MODER |= (0x1 << (5 * 2));   // 设为 01:通用输出模式

GPIOA->OTYPER &= ~(1 << 5);         // 推挽输出(0 表示推挽)

/* 步骤3:点亮 LED */
GPIOA->ODR |= (1 << 5);             // PA5 输出高电平,LED 亮

你看,代码就这么几行。但每一行都有讲究。比如 MODER 寄存器每两位控制一个引脚,所以清位时要左移 5 * 2 位。这个计算方式我建议你记牢,后面所有 GPIO 配置都会用到。

关键点: 配置 GPIO 前一定要先开时钟。我曾经在调试一个 I2C 设备时,死活写不进寄存器,查了半天才发现是时钟没开。这个坑,踩过一次就再也不会忘了。

4.3 GPIO 输入模式配置(读取按键)

按键读取需要把 GPIO 设为输入模式。输入模式也有讲究:要不要上拉?要不要下拉?要不要用施密特触发器?

对于按键这种机械开关,我建议:

  • 模式: 输入模式(MODER = 00)
  • 上拉/下拉: 如果外部电路已经有上拉电阻,内部上拉可以不开。但为了保险,我习惯开启内部上拉。
  • 施密特触发器: 默认开启,不用管。

配置代码:

/* 步骤1:开启 GPIOC 时钟 */
RCC->AHB1ENR |= (1 << 2);   // GPIOC 时钟使能

/* 步骤2:配置 PC13 为输入模式 */
GPIOC->MODER &= ~(0x3 << (13 * 2));  // 清位,设为 00:输入模式

/* 步骤3:开启内部上拉电阻 */
GPIOC->PUPDR &= ~(0x3 << (13 * 2));  // 先清位
GPIOC->PUPDR |= (0x1 << (13 * 2));   // 设为 01:上拉

/* 步骤4:读取按键状态 */
uint8_t key_state = (GPIOC->IDR >> 13) & 0x1;  // 读 IDR 寄存器

读按键时,读的是 IDR(输入数据寄存器),不是 ODR。这个别搞混了。ODR 是输出用的,IDR 是输入用的。

注意: 按键按下时,我们读到的是低电平(0)。所以判断按键按下的条件应该是 key_state == 0。如果你发现按键没反应,先检查一下是不是逻辑写反了。

4.4 按键消抖处理

机械按键有个讨厌的特性:抖动。按下和松开的一瞬间,电平会来回跳变几次,持续 5~20ms。如果不做消抖,一次按下可能被误判成多次。

消抖方法有两种:

方法 原理 优点 缺点
硬件消抖 RC 低通滤波器 + 施密特触发器 不占 CPU 增加 BOM 成本
软件消抖 检测到电平变化后,延时 10~20ms 再读一次 零成本,灵活 占用 CPU 时间

我一般用软件消抖,简单可靠。代码实现:

uint8_t Read_Key_GPIO(void)
{
    if ((GPIOC->IDR >> 13) & 0x1) {
        return 1;  // 按键未按下
    }

    /* 延时 10ms(简单忙等,实际项目建议用定时器) */
    for (volatile uint32_t i = 0; i < 100000; i++);

    /* 再次读取确认 */
    if ((GPIOC->IDR >> 13) & 0x1) {
        return 1;  // 抖动,不是真的按下
    }

    return 0;  // 确认按下
}

这个函数的核心逻辑是:第一次读到低电平不急着信,等 10ms 再读一次。如果还是低电平,那就是真的按下了。如果变成高电平,说明是抖动,忽略。

经验之谈: 延时时间别太长,10~20ms 就够了。我曾经见过有人延时 100ms,结果按键响应特别迟钝,用户体验很差。另外,实际产品中不要用这种忙等延时,要用定时器或者状态机来做,不然 CPU 啥也干不了。

4.5 综合实战:按键控制 LED 亮灭

好了,把上面两部分合起来。按下按键,LED 亮;松开按键,LED 灭。这就是一个最简单的输入输出闭环。

int main(void)
{
    /* 初始化 */
    RCC->AHB1ENR |= (1 << 0) | (1 << 2);  // 开启 GPIOA 和 GPIOC 时钟

    /* PA5 输出模式 */
    GPIOA->MODER &= ~(0x3 << (5 * 2));
    GPIOA->MODER |= (0x1 << (5 * 2));
    GPIOA->OTYPER &= ~(1 << 5);

    /* PC13 输入模式 + 上拉 */
    GPIOC->MODER &= ~(0x3 << (13 * 2));
    GPIOC->PUPDR &= ~(0x3 << (13 * 2));
    GPIOC->PUPDR |= (0x1 << (13 * 2));

    while (1)
    {
        if (Read_Key_GPIO() == 0)  // 按键按下
        {
            GPIOA->ODR |= (1 << 5);   // LED 亮
        }
        else
        {
            GPIOA->ODR &= ~(1 << 5);  // LED 灭
        }
    }
}

这段代码跑起来,你按下按键,灯就亮;松开,灯就灭。是不是很简单?但别小看它,这个模式是所有 GPIO 驱动的基础。后面你要驱动继电器、蜂鸣器、传感器,本质上都是这套逻辑。

4.6 知识体系总览

为了让你更清楚地看到本章的知识结构,我画了一张图。你可以把它当作一个“思维导图”来看。

GPIO 驱动实战知识体系 GPIO 驱动实战 输出模式(LED) 推挽输出 · ODR 寄存器 MODER 配置 · 时钟使能 输入模式(按键) 上拉/下拉 · IDR 寄存器 软件消抖 · 电平确认 综合实战 按键控制 LED 亮灭 输入 → 处理 → 输出闭环 核心:时钟 → 模式 → 数据寄存器

这张图把本章内容分成了三块:输出、输入、综合。你看,所有 GPIO 驱动都绕不开“时钟 → 模式 → 数据寄存器”这个核心链路。把这个链路刻在脑子里,后面不管接什么外设,你都知道从哪下手。

4.7 避坑指南

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

  • 时钟没开就配寄存器: 写进去的值根本不会生效。调试时先检查时钟寄存器有没有置位。
  • MODER 清位不彻底: 多个位共用同一个寄存器,不清干净就设新值,结果模式乱掉。我习惯先 &= 清位,再 |= 设值。
  • 按键电平极性搞反: 按下是高还是低?先看原理图,别想当然。实在不确定,用万用表量一下。
  • 消抖延时太长: 10~20ms 足够,别超过 50ms。否则按键响应慢,用户会骂娘。

好了,这一章的内容就到这。代码不多,但都是基本功。你最好自己动手敲一遍,别复制粘贴。敲完跑起来,看到灯亮了、按键有反应了,那种成就感是看一百遍书都比不了的。