第2章:GPIO基础与LED控制

各位同学,欢迎来到第二章。

上一章我们搭好了开发环境,点亮了第一颗LED。但说实话,那只是“Hello World”级别的操作。这一章,我们要真正搞懂GPIO是怎么工作的。你想想看,一个按摩仪上那么多按键、指示灯、电机控制,归根结底都是GPIO在干活。

2.1 GPIO工作原理

GPIO,全称General Purpose Input Output,通用输入输出口。说白了,它就是芯片和外界打交道的“手脚”。

每个GPIO引脚,内部其实是一堆MOS管和寄存器。我刚开始学的时候,总觉得这东西很神秘。后来拆了几个芯片的datasheet才发现,原理其实很简单:

  • 输出模式:芯片内部写寄存器,引脚就输出高电平或低电平
  • 输入模式:读取引脚上的电平状态,判断是0还是1
  • 复用功能:比如当作串口、I2C、PWM等专用接口

嗯,这里要注意:不同芯片的GPIO结构略有差异,但核心思想是一样的。我在项目中遇到过一个问题——某次用STM32驱动一个继电器,结果死活拉不低电平。查了半天,原来是GPIO没配置成推挽输出模式。所以,配置模式这一步,千万别偷懒。

核心概念:GPIO的三种基本模式——输入、输出、复用。输出又分推挽和开漏,输入又分浮空和上拉/下拉。

2.2 寄存器操作

操作GPIO,本质就是读写寄存器。每个芯片都有对应的寄存器映射表。以STM32为例,GPIO相关的寄存器主要有:

寄存器 功能 位宽
GPIOx_MODER 模式选择(输入、输出、复用、模拟) 32位
GPIOx_OTYPER 输出类型(推挽、开漏) 32位
GPIOx_OSPEEDR 输出速度 32位
GPIOx_PUPDR 上拉/下拉配置 32位
GPIOx_IDR 输入数据寄存器(只读) 32位
GPIOx_ODR 输出数据寄存器 32位
GPIOx_BSRR 位设置/复位寄存器 32位

我个人习惯用BSRR寄存器来操作输出。为什么?因为它可以原子操作,不会被打断。比如你要同时拉高PA0和拉低PA1,用ODR需要两步,中间可能被中断干扰。用BSRR一步搞定。

来看一个实际代码:

// 配置PA5为推挽输出,速度50MHz
GPIOA->MODER &= ~(0x3 << (5 * 2));   // 先清零
GPIOA->MODER |= (0x1 << (5 * 2));    // 设置为输出模式

GPIOA->OTYPER &= ~(0x1 << 5);        // 推挽输出
GPIOA->OSPEEDR |= (0x2 << (5 * 2));  // 50MHz速度

// 点亮LED
GPIOA->BSRR = (1 << 5);              // 置位BS5,输出高电平

// 熄灭LED
GPIOA->BSRR = (1 << (5 + 16));      // 置位BR5,输出低电平

小技巧:操作寄存器时,尽量用“读-改-写”模式,不要直接赋值。否则会影响到其他引脚配置。我曾经见过一个同事直接写MODER = 0xFFFFFFFF,结果所有引脚都变成模拟模式了……

2.3 点亮板载LED

大多数开发板都有一颗板载LED,通常连接在某个GPIO上。以STM32F103C8T6的“蓝色 pill”为例,LED接在PC13上,低电平点亮。

点亮它的步骤很简单:

  1. 使能GPIOC的时钟
  2. 配置PC13为推挽输出
  3. 输出低电平
// 使能GPIOC时钟
RCC->APB2ENR |= (1 << 4);  // APB2外设时钟使能寄存器,第4位对应GPIOC

// 配置PC13为推挽输出
GPIOC->CRH &= ~(0xF << ((13 - 8) * 4));  // 先清零
GPIOC->CRH |= (0x3 << ((13 - 8) * 4));   // 设置为推挽输出,50MHz

// 点亮LED(低电平有效)
GPIOC->BSRR = (1 << (13 + 16));  // BR13置位,输出低电平

嗯,这里要注意:不同开发板的LED连接方式不同。有的高电平点亮,有的低电平点亮。我建议你拿到板子后,先看原理图确认一下。我曾经因为没看原理图,直接按高电平点亮写代码,结果LED死活不亮,折腾了半小时才发现是反的。

2.4 实现呼吸灯效果

呼吸灯,说白了就是让LED的亮度从暗到亮、再从亮到暗,循环变化。怎么实现?用PWM(脉冲宽度调制)。

PWM的原理很简单:通过调节高电平占空比,控制LED的平均功率。占空比越大,LED越亮。

但如果我们不用硬件PWM,只用GPIO和延时,也能模拟出呼吸灯效果。方法就是:

  • 快速切换GPIO的高低电平
  • 每次切换时,改变高电平的持续时间
  • 人眼有视觉暂留效应,看起来就是亮度变化
void breath_led(void) {
    int i;
    // 渐亮
    for (i = 0; i < 100; i++) {
        GPIOA->BSRR = (1 << 5);       // 高电平
        delay_us(i * 10);              // 延时逐渐增加
        GPIOA->BSRR = (1 << (5 + 16)); // 低电平
        delay_us((100 - i) * 10);      // 延时逐渐减少
    }
    // 渐暗
    for (i = 100; i > 0; i--) {
        GPIOA->BSRR = (1 << 5);
        delay_us(i * 10);
        GPIOA->BSRR = (1 << (5 + 16));
        delay_us((100 - i) * 10);
    }
}

注意:这种软件模拟PWM的方式,会占用CPU大量时间。在按摩仪这种实时性要求不高的场景下可以用。但如果你的系统有多个任务,建议用硬件PWM模块。我做过一个项目,用软件PWM控制4个LED,结果CPU占用率飙到80%,其他任务全卡死了。

2.5 按键输入检测与消抖

按键,是按摩仪上最常用的输入设备。但按键有个烦人的问题——抖动。

为什么会抖动?因为按键是机械结构,按下和释放的瞬间,触点会弹跳几次,产生多个电平变化。如果不处理,一次按键会被误判成多次。

消抖的方法有两种:

  • 硬件消抖:加RC滤波电路,或者用施密特触发器
  • 软件消抖:检测到电平变化后,延时10-20ms再读一次

我个人更推荐软件消抖,成本低、灵活。来看代码:

// 按键检测函数,返回1表示按下,0表示未按下
uint8_t key_scan(void) {
    static uint8_t last_state = 1;  // 上一次状态,默认高电平(未按下)
    uint8_t current_state;
    
    current_state = (GPIOA->IDR >> 0) & 0x01;  // 读取PA0的电平
    
    // 检测下降沿(高->低)
    if (last_state == 1 && current_state == 0) {
        delay_ms(20);  // 延时消抖
        current_state = (GPIOA->IDR >> 0) & 0x01;  // 再读一次
        if (current_state == 0) {
            last_state = 0;
            return 1;  // 确认按下
        }
    }
    
    // 检测上升沿(低->高)
    if (last_state == 0 && current_state == 1) {
        delay_ms(20);
        current_state = (GPIOA->IDR >> 0) & 0x01;
        if (current_state == 1) {
            last_state = 1;
            // 释放检测,这里不返回
        }
    }
    
    return 0;
}

避坑指南:我曾经在一个量产项目中,按键消抖只做了10ms延时。结果有批按键质量不好,抖动时间长达30ms,导致频繁误触发。后来我把消抖时间改成了30ms,问题解决。所以,消抖时间要根据实际按键的规格来定,别死板地用20ms。

另外,按键检测还有一种更高效的方式——状态机。用状态机可以避免阻塞延时,适合在RTOS中使用。不过那是后面章节的内容了,这里先不展开。

小结

这一章我们讲了GPIO的工作原理、寄存器操作、LED控制、呼吸灯和按键消抖。这些都是嵌入式开发的基本功。你想想看,按摩仪上的模式切换按键、力度指示灯、电机启停,哪一样离得开GPIO?

下一章,我们会讲定时器。有了定时器,我们就能做更精确的时间控制,比如PWM输出、脉冲计数等。到时候,呼吸灯效果就可以用硬件PWM来实现,效果会好很多。

好了,这一章就到这里。动手试试吧,把板载LED点亮,再做个呼吸灯,最后接个按键试试消抖效果。遇到问题别怕,调试本身就是学习的过程。