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位系统上移位出问题,灯死活不亮。排查了半天,最后发现是类型溢出。
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,问题迎刃而解。
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);
}
为什么会这样?因为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检测一次
}
}
另外,如果你用中断方式检测按键,消抖逻辑要放在任务里做,不能在ISR里延时。正确的做法是:ISR里只记录一个标志位,然后唤醒一个任务,由任务去执行延时采样和业务逻辑。
好了,GPIO这块就聊到这儿。从初始化到LED闪烁,再到中断和按键检测,这些是ESP32开发的基本功。你把这些吃透了,后面操作外设、驱动传感器,思路都是一样的。下一章咱们聊聊定时器,那个东西用好了,能帮你省不少CPU资源。