Skip to content

2.2 GPIO:LED 与按键 ​

这节要搞懂:点亮一颗灯、读一个按键。这是嵌入式的"Hello World"。 需要基础:2.1

一、什么是 GPIO ​

GPIO(General Purpose Input/Output,通用输入输出)是芯片上可由程序自由配置为输入或输出的引脚。作为输出时,它可以被设定为高电平或低电平;作为输入时,可以读取外部电平的高低。

简单说:芯片边上那一排金属脚,每一个都可以:

  • 当输出用:由你决定它输出 3.3V(高)还是 0V(低)→ 点灯、控制模块
  • 当输入用:读它现在是高还是低 → 读按键、读开关

二、本课程板子用了哪些引脚 ⭐ ​

这块板子上的器件都已经焊好了,不用自己接线。要知道的只有一件事:哪个器件接在哪个引脚上。

后面每一节的代码都按这张表写。

三颗 LED ​

引脚电路
LED1GPIO18正极 → 1kΩ 电阻 → GPIO;负极 → GND
LED2GPIO39同上
LED3GPIO21同上
三颗 LED 的原理图:正极经 1kΩ 电阻接 GPIO,负极接地
三颗 LED 的原理图:正极经 1kΩ 电阻接 GPIO,负极接地

负极接地、正极接 GPIO,所以:

GPIO 输出 1(高电平)→ 有电流流过 → 灯亮
GPIO 输出 0(低电平)→ 两端都是 0V → 灯灭

电阻不能省

LED 直接接引脚会烧掉。必须串一个 220Ω ~ 1kΩ 的电阻来限制电流。

三个按键 ​

引脚电路内部要开按下读到
KEY1GPIO453.3V ──[10kΩ]── 按键 ── GPIO45下拉1
KEY2GPIO47GND ──[10kΩ]── 按键 ── GPIO47上拉0
KEY3GPIO48GND ──[10kΩ]── 按键 ── GPIO48上拉0
三个按键的原理图:注意 10kΩ 电阻是串在按键回路里的,不是并联到 GPIO 上
三个按键的原理图:注意 10kΩ 电阻是串在按键回路里的,不是并联到 GPIO 上

这块板子的接法有点特别 ⭐

网上大多数教程里,按键是这么画的:

                3.3V
                 │
               [10kΩ]        ← 电阻并联在 GPIO 上,起"上拉"作用
                 │
GPIO ────────────┤
                 │
               按键
                 │
                GND

电阻一头接电源、一头接 GPIO,松开时 GPIO 被拉到 3.3V,按下时被按键短接到 GND。这叫并联式,电阻本身就是那个"上拉电阻"。

但这块板子画的是串联式:

GND ──[10kΩ]── 按键 ──── GPIO47

电阻串在按键的回路里,和 GPIO 不是并联关系。这带来一个后果:

  • 按下时:GPIO 经 10kΩ 接到 GND → 读到 0
  • 松开时:回路断开,GPIO 什么都没接上 —— 悬空!读到的是随机值

所以这块板子上,内部上拉/下拉不是"可选项",是必须开。 它负责的是"松开时把引脚拉到一个确定电平"这件事。

对应到代码:

电阻另一头接内部要开松开读到按下读到
KEY13.3VGPIO_PULLDOWN_ENABLE01
KEY2 / KEY3GNDGPIO_PULLUP_ENABLE10

KEY1 和另外两个是反的,写代码时别搞混。

本节只用一颗灯 + 一个按键 ​

一颗灯加一个按键,就足够把原理、代码和用法讲完整了。

GPIO 要讲的就那么几件事 —— 怎么输出高低电平、怎么读引脚电平、上拉下拉怎么配、怎么消抖。这些用一路输出加一路输入全都能演示到,再多接几路也讲不出新东西。

所以这一节只用:

引脚说明
LEDGPIO39输出 1 亮
按键GPIO47开内部上拉,按下读 0

剩下两颗灯、两个按键电路完全一样,想用的时候改个引脚号就行 —— 只有 KEY1(GPIO45)要记得把内部上拉换成下拉、判断条件从 0 改成 1。

三、点亮一颗 LED ​

用上面表里的 GPIO39,输出高电平就亮。

c
#include <stdio.h>
#include "driver/gpio.h"
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"

void led_task(void *arg)
{
    (void)arg;

    while (1)
    {
        gpio_set_level(GPIO_NUM_39, 1);
        vTaskDelay(pdMS_TO_TICKS(500));
        }
}

void app_main(void)
{
    gpio_config_t led = {
        .pin_bit_mask = (1ULL << GPIO_NUM_39),
        .mode = GPIO_MODE_OUTPUT,
        .pull_up_en = GPIO_PULLUP_DISABLE,
        .pull_down_en = GPIO_PULLDOWN_DISABLE,
        .intr_type = GPIO_INTR_DISABLE,
    };
    gpio_config(&led);
    xTaskCreate(led_task,"LED",2048,NULL,5,NULL);
}

灯不亮怎么办

  1. LED 有方向,反了不亮 —— 掉个头试试
  2. 电阻接了吗
  3. 换个引脚试试(有些脚有特定功能,不能随便用)

四、读一个按键 ​

用上面表里的 KEY2(GPIO47),接法是 GND ──[10kΩ]── 按键 ── GPIO47,内部上拉必须打开。

五、什么是上拉和下拉 ​

上拉电阻(Pull-up Resistor)是连接在信号线与电源之间的电阻,没有其他信号驱动时把引脚拉到高电平。

下拉电阻(Pull-down Resistor)则接在信号线与地之间,把引脚拉到低电平。

为什么必须要它:引脚既没接电源也没接地的时候叫"悬空",读出来是随机值 —— 周围一有干扰就乱跳。

ESP32-S3 内部就自带上拉和下拉电阻,写一行配置就能打开,不用外接。

KEY2(GPIO47)打开内部上拉之后:

松开时:回路断开,内部上拉把引脚拉到 3.3V  → 读到 1
按下时:引脚经 10kΩ 接到 GND               → 读到 0

所以:按下读到的是 0,不是 1。

KEY1 要开的是下拉,不是上拉

KEY1 的电阻另一头接的是 3.3V,方向反过来了:

c
.pull_up_en   = GPIO_PULLUP_DISABLE,
.pull_down_en = GPIO_PULLDOWN_ENABLE,   // ⭐ KEY1 开下拉
松开时:回路断开,内部下拉把引脚拉到 0V    → 读到 0
按下时:引脚经 10kΩ 接到 3.3V             → 读到 1

同一块板子上,KEY1 按下读 1,KEY2/KEY3 按下读 0。 判断条件写反了,现象是"不按的时候一直触发"。

c
#define KEY_GPIO GPIO_NUM_47

static void key_init(void)
{
    gpio_config_t config = {
        .pin_bit_mask = 1ULL << KEY_GPIO,
        .mode         = GPIO_MODE_INPUT,        // 设为输入
        .pull_up_en   = GPIO_PULLUP_ENABLE,     // ⭐ 打开内部上拉,这行很关键
        .pull_down_en = GPIO_PULLDOWN_DISABLE,
        .intr_type    = GPIO_INTR_DISABLE,
    };
    gpio_config(&config);
}

// 读取
if (gpio_get_level(KEY_GPIO) == 0) {
    printf("key pressed\n");                    // 注意:按下是 0
}

六、什么是按键抖动 ​

抖动(Bounce)指的是机械按键在按下或松开的瞬间,触点会快速通断多次,持续约 5 ~ 20 毫秒,导致一次按压可能会被识别成多次。

理想:  ─────┐
             └──────────
实际:  ─────┐ ┌┐ ┌┐
             └─┘└─┘└────    ← 这段抖动持续 5~20 毫秒

消抖的最简单办法:按下后等 20 毫秒再确认一次。

c
void key_task(void *arg)
{
    (void)arg;

    while (1)
    {
        if(gpio_get_level(KEY_GPIO) == 0)
        {
            vTaskDelay(pdMS_TO_TICKS(20));
            if(gpio_get_level(KEY_GPIO) == 0)
            {
                gpio_set_level(LED_GPIO, 1);
            }
        }
        else
        {
            gpio_set_level(LED_GPIO, 0);
        }
        vTaskDelay(pdMS_TO_TICKS(10));//没有按下按键就要让出CPU运行空闲任务
    }
}

看,多任务的好处

闪灯和读按键是两个互不干扰的任务。

灯该闪闪它的,按键该等等它的 —— 这就是第一章 1.4 说的"拆成独立的小程序"。

七、ESP32-S3 引脚使用禁忌 ⭐ ​

这张表能帮你省下几个小时的调试时间。

注意:S3 和老款 ESP32 完全不同

网上大量教程写的是老款 ESP32("别用 6-11"、"34-39 只能输入")——那些规则在 S3 上是错的,照抄会踩坑。

以下是 ESP32-S3 的实际情况。

引脚能用吗说明
GPIO 26 – 32❌ 绝对不能用接内部 SPI Flash,动了就启动不了
GPIO 22 – 25❌ 根本不存在S3 上没有这几个编号
GPIO 33 – 37⚠️ 看模组带 8 线 PSRAM 的模组(如 N16R8)会占用它们
GPIO 19 / 20⚠️ 慎用板载 USB 接口用的
GPIO 43 / 44⚠️ 慎用UART0,占了会看不到日志
GPIO 0 / 3 / 45 / 46⚠️ 慎用Strapping 脚,影响开机模式
GPIO 39 – 42⚠️ 一般可用JTAG 调试用,不接调试器就能用
其他引脚✅ 放心用推荐:GPIO 1 – 18、21

记一句就够

S3 上挑 GPIO 1 到 18,再加一个 21,闭着眼用都不会错。

S3 的一个重要改进:没有"只能输入"的引脚

老款 ESP32 的 GPIO34-39 只能读不能写,还没有内部上拉,接按键很麻烦。

ESP32-S3 取消了这个限制 —— 所有可用的 GPIO 既能输入也能输出,都带内部上下拉。画板子时自由得多。

八、别直接驱动大功率负载 ​

一个引脚最多输出 20 毫安左右,够点一颗 LED。

想控制电机、继电器、大功率灯?必须加三极管或 MOS 管,直接接会烧芯片。

本节要点 ​

  • GPIO = 可配置为输入或输出的通用引脚
  • LED 必须串电阻
  • ⭐ 这块板子的按键是串联式接法,松手时引脚悬空,内部上拉/下拉必须打开
  • KEY1(GPIO45)开下拉、按下读 1;KEY2/KEY3(GPIO47/48)开上拉、按下读 0
  • 抖动会让一次按压变多次,必须消抖(等 20ms 再确认)
  • ⭐ S3 上用 GPIO 1-18 和 21 最安全;GPIO 26-32 接 Flash 绝对不能碰
  • ⭐ S3 所有 GPIO 都能输入输出,不像老款 ESP32 有"只能输入"的脚

操作过程 ​

目标:LED 每秒闪一次,按一下按键串口打印一行。

第 1 步:建工程 ⭐ 通用流程 ​

这一步后面每节都一样

第二章每个实验都是独立的小工程,建法完全相同 —— 只有工程名和目标芯片要填对。

后面 2.3 ~ 2.8 会直接引用这里,不再重复。

我们用 VS Code 的 ESP-IDF 插件,点四下就建好,不用敲命令。

① 打开新项目向导 ​

左侧点 ESP-IDF 图标 → 在命令列表里找到最下面的 Advanced → 点 新项目向导。

顶部会弹出"选择要使用的框架",选你装的那个 ESP-IDF 版本。

① 侧边栏 Advanced → 新项目向导,然后选 ESP-IDF 版本
① 侧边栏 Advanced → 新项目向导,然后选 ESP-IDF 版本

装了多个版本怎么办

列表里可能有好几个路径。选你平时用的那个就行,本课程用 v5.4.3。

版本差一点问题不大,但至少要 v5.x —— v4 的 API 和课程里写的不一样。

② 选模板 ​

左侧展开 ESP-IDF Templates → 点 sample_project(最简单的空工程)。

右上角点 Create project using template sample_project。

② 选 ESP-IDF Templates 里的 sample_project
② 选 ESP-IDF Templates 里的 sample_project

上面那个 ESP-IDF Examples 是什么

是官方例子库,里面有几百个现成的例子。

想直接改现成的就从那里挑 —— 比如 2.8 蓝牙那节用的 bleprph,就是从 Examples 里找的。

本章前几节从空工程开始,代码自己写,学得更扎实。

③ 填工程信息 ⭐ 最关键的一步 ​

③ 填工程名、路径,选目标芯片 esp32s3
③ 填工程名、路径,选目标芯片 esp32s3
字段填什么
Project Name工程名,本节填 LED(后面每节换成对应的名字)
Enter Project directory工程存哪,路径里别有中文和空格
Choose ESP-IDF Target⭐ esp32s3 —— 千万别选错
Choose ESP-IDF BoardESP32-S3 chip (via builtin USB-JTAG)
Choose serial portdetect(自动检测)
Add your ESP-IDF Component directory留空

填完点右边的 Create Project。

Target 一定要选 esp32s3

默认可能是 esp32(老款)。选错的话编译出来的固件烧进 S3 根本跑不起来,而且现象很迷惑 —— 串口只会不停重启,不会告诉你"芯片选错了"。

这是新手最常踩的第一个坑。

路径里别有中文和空格

G:\项目\我的 工程\ 这种路径,编译时大概率报一堆莫名其妙的错。

用纯英文路径,比如 G:\PROJECT\ESP32\。

④ 打开工程 ​

看到 Project has been created! 就成功了,点 Open Project。

④ 创建成功,点 Open Project 打开它
④ 创建成功,点 Open Project 打开它

打开之后长这样

LED/
├── CMakeLists.txt
├── main/
│   ├── CMakeLists.txt
│   └── LED.c          ← ⭐ 代码写在这里
└── sdkconfig          ← 配置文件,向导已经按 esp32s3 生成好了

代码就写在 main/ 下那个和工程同名的 .c 文件里。

第 2 步:写代码 ​

把 main/gpio_demo.c 的内容整个替换成:

c
#include <stdio.h>
#include "driver/gpio.h"
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"

#define LED_GPIO GPIO_NUM_39
#define KEY_GPIO GPIO_NUM_47

void key_task(void *arg)
{
    (void)arg;

    while (1)
    {
        if(gpio_get_level(KEY_GPIO) == 0)
        {
            vTaskDelay(pdMS_TO_TICKS(20));
            if(gpio_get_level(KEY_GPIO) == 0)
            {
                gpio_set_level(LED_GPIO, 1);
            }
        }
        else
        {
            gpio_set_level(LED_GPIO, 0);
        }
        vTaskDelay(pdMS_TO_TICKS(10));//没有按下按键就要让出CPU运行空闲任务
    }
}

void app_main(void)
{
    gpio_config_t led = {
        .pin_bit_mask = (1ULL << LED_GPIO),
        .mode = GPIO_MODE_OUTPUT,
        .pull_up_en = GPIO_PULLUP_DISABLE,
        .pull_down_en = GPIO_PULLDOWN_DISABLE,
        .intr_type = GPIO_INTR_DISABLE,
    };
        gpio_config(&led);
        gpio_set_level(LED_GPIO, 0); 
    gpio_config_t key = {
        .pin_bit_mask = (1ULL << KEY_GPIO),
        .mode = GPIO_MODE_INPUT,
        .pull_up_en = GPIO_PULLUP_ENABLE,
        .pull_down_en = GPIO_PULLDOWN_DISABLE,
        .intr_type = GPIO_INTR_DISABLE,
    };
        gpio_config(&key);
    xTaskCreate(key_task,"key",2048,NULL,6,NULL);

}

第 3 步:编译烧录 ⭐ 通用流程 ​

这一步后面每节也都一样

和建工程一样,编译烧录的操作每节完全相同,后面 2.3 ~ 2.8 直接引用这里。

所有操作都在 VS Code 最底部那一条状态栏上,从左到右排着一排小图标。

找不到按钮?

鼠标悬停在图标上会显示名称。 下面每一步都会说明对应哪个。

如果整条状态栏都没有,说明工程没被识别 —— 确认你打开的是工程根目录(里面有 CMakeLists.txt 那一层),不是它的上级目录。

前三步是设置,一个工程只需要做一次;之后每次改完代码,只要重复第 ④ ⑤ 两步。

① 选烧录方式 ​

点状态栏上显示 UART 的那个按钮,在弹出的列表里选 UART。

① 选烧录方式:JTAG / UART / DFU,选 UART
① 选烧录方式:JTAG / UART / DFU,选 UART

三种方式的区别

方式说明
UART⭐ 最常用,就是平时插的那根 USB 线
JTAG需要调试器,能单步调试
DFU特定场景用,新手用不到

选 UART 就对了。

② 选串口 ​

点状态栏上显示 COMxx 的按钮,列表里选认得出是你板子的那个。

② 选串口:认准括号里写着 ESP32-S3 的那个
② 选串口:认准括号里写着 ESP32-S3 的那个

怎么认出哪个是板子

图里 COM13 后面跟着 ESP32-S3 (QFN56) —— 插件直接告诉你这是块 S3,选它准没错。

其他几个标着 Microsoft 或"标准端口类型"的是电脑自带的,不是板子。

也可以选 detect 让它自动找。

串口被占用

注意看图里终端的报错:

Error: GetOverlappedResult failed (PermissionError(13, ...))
--- Waiting for the device to reconnect...

说明串口被别的程序占着了 —— 最常见的是上一次的串口监视器还开着,或者你另外开了个串口助手。

关掉那个再烧。

③ 选单片机型号 ​

点状态栏上显示芯片型号的按钮,列表里选 esp32s3。

③ 选目标芯片:一长串里认准 esp32s3
③ 选目标芯片:一长串里认准 esp32s3

列表里 esp32 和 esp32s3 挨得很近

上面第二行是 esp32s2、第三行是 esp32c3…… 看清楚再点,选错了编译出来的固件烧进去跑不起来。

改型号会重建配置

看图里输出窗口那几行:

Set Target to: esp32s3, new sdkconfig will be created.
Existing sdkconfig renamed to sdkconfig.old

改目标芯片会重新生成 sdkconfig,你之前在 menuconfig 里改过的东西会被清掉(旧的备份成 sdkconfig.old)。

所以型号要一开始就选对,别等配置改了一堆再回来改型号。

④ 编译 ​

点状态栏上的编译按钮(悬停显示"编译")。

④ 点编译,输出窗口会滚动显示编译过程
④ 点编译,输出窗口会滚动显示编译过程

第一次编译要几分钟 —— 它要把整个 ESP-IDF 框架编一遍。之后再编只编改动的部分,几秒就好。

编译成功的标志是输出窗口最后出现:

Build files have been written to: .../LED/build

⑤ 烧录 ​

点状态栏上的烧录按钮(悬停显示"烧录/编译后烧录")。

⑤ 点烧录,把固件写进板子
⑤ 点烧录,把固件写进板子

其实可以一步到位

状态栏上还有个**"编译、烧录并监视"的按钮(火焰图标),点它会依次做完编译 → 烧录 → 打开串口监视**。

平时调试用它最省事,这里拆成三步是为了讲清楚每步在干什么。

烧录连不上

如果卡在 Connecting...... 一直连不上:

  1. 按住板子上的 BOOT 键,再按一下 RST,然后松开 BOOT —— 手动让它进下载模式
  2. 换一根 能传数据的 USB 线(很多充电线只有电源线芯)
  3. 确认串口选对了,而且没被别的程序占用

第 4 步:应该看到什么 ​

板子上:LED 亮 0.5 秒、灭 0.5 秒,一直循环。

串口(每按一次键出现一行):

key pressed, count = 1
key pressed, count = 2
key pressed, count = 3

看不到就别往下走

LED 不闪,或者按键没打印,先回头检查代码和引脚号,不要接着往下做。

动手试试 ​

  1. 让 LED 每秒闪一次。
  2. 加个按键任务:按一下 LED 亮,再按一下灭(用第一章学的队列传消息)。
  3. 故意把消抖代码去掉,打印按键次数,看看按一下会被识别成几次。
  4. 查一下你手上开发板的原理图,确认板载 LED 接在哪个引脚。