3、GPIO与LED控制:GPIO初始化与配置、LED闪烁实验、GPIO中断、按键输入检测

好,咱们今天来聊聊GPIO。这是嵌入式开发里最基础、也最绕不开的东西。说白了,芯片跟外界打交道,全靠这些引脚。你想想看,一个单片机如果连IO口都不会用,那基本等于废了。

我个人习惯把GPIO比作芯片的「手脚」。你要让它动起来,就得先告诉它:你是想往外推信号(输出),还是想听外面的动静(输入)。这个配置过程,就是初始化。

3.1 GPIO初始化与配置

在ESP32上,GPIO的初始化其实不复杂。但有几个坑,我当年刚上手时踩过不少。先看一段最基础的代码,让一个引脚输出高电平。

#include "driver/gpio.h"

#define LED_GPIO_PIN  2  // 开发板上通常接GPIO2

void app_main(void)
{
    // 1. 配置GPIO为推挽输出模式
    gpio_config_t io_conf = {
        .pin_bit_mask = (1ULL << LED_GPIO_PIN),  // 选中GPIO2
        .mode = GPIO_MODE_OUTPUT,                  // 输出模式
        .pull_up_en = GPIO_PULLUP_DISABLE,         // 不上拉
        .pull_down_en = GPIO_PULLDOWN_DISABLE,     // 不下拉
        .intr_type = GPIO_INTR_DISABLE             // 禁用中断
    };
    gpio_config(&io_conf);

    // 2. 设置初始电平为高
    gpio_set_level(LED_GPIO_PIN, 1);
}

嗯,这里要注意。gpio_config_t结构体里的pin_bit_mask,用的是位掩码。如果你要配置多个引脚,可以用或运算:(1ULL << GPIO_NUM_2) | (1ULL << GPIO_NUM_4)。我刚开始写的时候,老忘记加ULL后缀,结果在32位系统上移位出问题,灯死活不亮。排查了半天,最后发现是类型溢出。

避坑指南: 我曾经在配置GPIO时,同时设置了上拉和下拉为使能。结果引脚电平一直不稳定,测量电压只有1.2V左右。后来查手册才发现,上下拉同时使能会导致内部电阻分压,输出逻辑混乱。切记:上拉和下拉只能选一个,或者都不选。

3.2 LED闪烁实验

学会了配置,咱们来让LED闪起来。这是嵌入式界的「Hello World」。你想想看,一个灯能按你的节奏亮灭,说明你的代码真的在控制硬件了。

#include "driver/gpio.h"
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"

#define LED_GPIO_PIN  2

void app_main(void)
{
    gpio_config_t io_conf = {
        .pin_bit_mask = (1ULL << LED_GPIO_PIN),
        .mode = GPIO_MODE_OUTPUT,
        .pull_up_en = GPIO_PULLUP_DISABLE,
        .pull_down_en = GPIO_PULLDOWN_DISABLE,
        .intr_type = GPIO_INTR_DISABLE
    };
    gpio_config(&io_conf);

    int level = 0;
    while (1) {
        level = !level;                     // 翻转电平
        gpio_set_level(LED_GPIO_PIN, level);
        vTaskDelay(pdMS_TO_TICKS(500));     // 延时500ms
    }
}

这里用了FreeRTOS的vTaskDelay,而不是普通的delay。为什么?因为vTaskDelay会让出CPU,让其他任务有机会运行。如果你用死循环delay,整个系统就卡死了。我在做多任务项目时,一开始图省事用了for(i=0;i<1000000;i++);这种空转延时,结果WiFi任务一直得不到调度,连接都连不上。后来全部改成vTaskDelay,问题迎刃而解。

小技巧: pdMS_TO_TICKS(500) 是把毫秒转换成系统Tick数。如果你的FreeRTOS Tick频率是1000Hz,那500ms就是500个Tick。这个宏帮你自动计算,不用自己操心。

3.3 GPIO中断

接下来是重点——中断。说白了,就是让芯片「耳朵竖起来」,一旦引脚电平变化,立刻停下手里的事去处理。这比轮询高效得多。

ESP32的GPIO中断支持几种触发方式:上升沿、下降沿、任意沿、低电平、高电平。我个人最常用的是上升沿和下降沿。

#include "driver/gpio.h"

#define BUTTON_GPIO_PIN  0   // 开发板上的BOOT按键,通常接GPIO0

// 中断服务函数
static void IRAM_ATTR gpio_isr_handler(void* arg)
{
    uint32_t gpio_num = (uint32_t) arg;
    // 注意:ISR里不能调用printf等阻塞函数
    // 通常的做法是发一个信号量给任务
    gpio_set_level(2, !gpio_get_level(2));  // 简单演示:翻转LED
}

void app_main(void)
{
    // 1. 配置按键引脚为输入模式,并使能中断
    gpio_config_t io_conf = {
        .pin_bit_mask = (1ULL << BUTTON_GPIO_PIN),
        .mode = GPIO_MODE_INPUT,
        .pull_up_en = GPIO_PULLUP_ENABLE,    // 内部上拉,默认高电平
        .pull_down_en = GPIO_PULLDOWN_DISABLE,
        .intr_type = GPIO_INTR_NEGEDGE       // 下降沿触发(按键按下)
    };
    gpio_config(&io_conf);

    // 2. 安装GPIO中断服务
    gpio_install_isr_service(0);

    // 3. 为指定引脚注册中断处理函数
    gpio_isr_handler_add(BUTTON_GPIO_PIN, gpio_isr_handler, (void*) BUTTON_GPIO_PIN);
}
关键点: 中断服务函数(ISR)必须用 IRAM_ATTR 修饰,确保代码放在IRAM中运行。否则如果Flash缓存未命中,会导致系统崩溃。我有个同事没加这个修饰,调试了整整两天,最后发现是ISR代码在Flash里,中断来了读不到指令。

为什么会这样?因为Flash读取速度慢,而且可能被其他操作占用。IRAM是片内高速RAM,CPU可以直接访问,没有延迟。所以中断处理函数必须放在IRAM里。

3.4 按键输入检测

按键检测看起来简单,但有个老生常谈的问题——抖动。你按一下按键,实际上电平会跳变好几次,持续几毫秒到几十毫秒。如果不做处理,一次按键会被误判成多次。

我常用的消抖方法有两种:硬件消抖(加RC滤波)和软件消抖(延时采样)。在ESP32上,软件消抖更灵活。

#include "driver/gpio.h"
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"

#define BUTTON_GPIO_PIN  0
#define DEBOUNCE_TIME_MS 20   // 消抖时间

void app_main(void)
{
    gpio_config_t io_conf = {
        .pin_bit_mask = (1ULL << BUTTON_GPIO_PIN),
        .mode = GPIO_MODE_INPUT,
        .pull_up_en = GPIO_PULLUP_ENABLE,
        .pull_down_en = GPIO_PULLDOWN_DISABLE,
        .intr_type = GPIO_INTR_DISABLE
    };
    gpio_config(&io_conf);

    int last_state = 1;  // 上一次稳定状态,默认高电平
    int current_state;

    while (1) {
        current_state = gpio_get_level(BUTTON_GPIO_PIN);

        if (current_state != last_state) {
            // 检测到电平变化,延时再采样一次
            vTaskDelay(pdMS_TO_TICKS(DEBOUNCE_TIME_MS));
            current_state = gpio_get_level(BUTTON_GPIO_PIN);

            if (current_state != last_state) {
                // 确认状态改变,执行按键动作
                if (current_state == 0) {
                    printf("按键按下!\n");
                    // 这里可以触发你的业务逻辑
                }
                last_state = current_state;
            }
        }
        vTaskDelay(pdMS_TO_TICKS(10));  // 每10ms检测一次
    }
}
避坑指南: 我曾经在按键检测里用了太短的消抖时间(5ms),结果在强电磁干扰环境下,按键频繁误触发。后来把消抖时间加到30ms,问题就解决了。不同按键的抖动时间不一样,建议实际测试一下你的按键,用示波器看看抖动波形,再确定消抖时间。

另外,如果你用中断方式检测按键,消抖逻辑要放在任务里做,不能在ISR里延时。正确的做法是:ISR里只记录一个标志位,然后唤醒一个任务,由任务去执行延时采样和业务逻辑。

好了,GPIO这块就聊到这儿。从初始化到LED闪烁,再到中断和按键检测,这些是ESP32开发的基本功。你把这些吃透了,后面操作外设、驱动传感器,思路都是一样的。下一章咱们聊聊定时器,那个东西用好了,能帮你省不少CPU资源。