从点亮一颗 LED 开始:ESP32-S3 GPIO 输出的正确学习方式
在嵌入式开发中,GPIO 永远是第一个绕不开的外设 。
但很多人学 GPIO,往往一上来就搜"ESP32 GPIO 点灯代码",复制、烧录、亮了就结束了。
这样的问题在于:
代码能跑,但你并不知道它为什么能跑。
本文将以 ESP32-S3 控制 LED 灯亮灭 为例,完整演示一条可复用的外设学习路径,让你之后面对 UART、I2C、SPI 时,也知道该从哪里下手。
一、明确需求:我们到底要做什么?
本节的目标非常明确:
通过 ESP32-S3 的 GPIO 输出功能,控制板载 LED 周期性亮灭。
这是一个最基础、但也最具代表性的外设使用场景。
围绕这个目标,我们把学习过程拆解为四步:
- 看芯片手册,确认 GPIO 能不能用、怎么用
- 查板卡原理图,确认 LED 接在哪个 GPIO
- 跑官方 GPIO 示例,理解"标准用法"
- 回到 API 文档和源码,提炼最小可用代码
二、第一步:从芯片手册确认 GPIO 能力边界
datasheet:
https://documentation.espressif.com/esp32-s3_datasheet_cn.pdf
api说明书:
在写任何代码之前,一定要先确认硬件前提。
ESP32-S3 官方数据手册中明确说明:
- 芯片共 45 个物理 GPIO
- 编号范围为 GPIO0 ~ GPIO21、GPIO26 ~ GPIO48
- 每个 GPIO 都可以配置为通用输入或输出


但这里有几个必须提前注意的特殊点:
1. 启动配置(Strapping)管脚
GPIO0、GPIO3、GPIO45、GPIO46 是启动配置管脚,
上电或复位时会参与启动模式判断,不建议随意使用。
2. SPI Flash / PSRAM 占用
GPIO26 ~ GPIO32(以及部分 GPIO33 ~ GPIO37)通常被 SPI Flash 或 PSRAM 占用,
在大多数开发板上不适合作普通 GPIO 使用。
3. USB-JTAG 默认占用
GPIO19、GPIO20 默认用于 USB-JTAG,
若复用为 GPIO,会导致 USB-JTAG 功能失效。
这一阶段的目的不是记住所有细节,而是形成一个意识:
GPIO 能不能用,不是"编号对就行",而是要结合芯片角色来看。

三、第二步:通过原理图确认 LED 对应的 GPIO
确认芯片能力后,下一步一定是看板卡原理图。

在开发板原理图中,找到 LED 模块,可以清楚看到:
板载 LED 连接在 TX(GPIO43) RX(GPIO44) 上
这里先占用TX引脚,后期串口
这一步非常关键,因为:
- 代码里写 GPIO43 不是"拍脑袋"
- 是由 硬件连接关系决定的
到这里,我们已经得到了一个确定结论:
后续所有 GPIO 输出代码,目标引脚都是 GPIO43
四、第三步:先跑一遍官方 GPIO 示例
在 ESP-IDF 中,官方示例就是"最权威的参考实现"。
GPIO 属于外设模块,对应示例路径为:
esp-idf/examples/peripherals/gpio/generic_gpio
为什么一定要先看 README?
很多人直接打开 .c 文件开始看代码,这是一个非常低效的习惯。
README 文件里已经帮你回答了几个关键问题:
- 这个示例支持哪些芯片
- 示例里 哪些 GPIO 是输入,哪些是输出
- 默认 GPIO 号是多少
- 如果不是官方开发板,该如何修改配置
bash
| Supported Targets | ESP32 | ESP32-C2 | ESP32-C3 | ESP32-C5 | ESP32-C6 | ESP32-H2 | ESP32-P4 | ESP32-S2 | ESP32-S3 |
| ----------------- | ----- | -------- | -------- | -------- | -------- | -------- | -------- | -------- | -------- |
# Example: GPIO
(See the README.md file in the upper level 'examples' directory for more information about examples.)
This test code shows how to configure GPIO and how to use it with interruption.
## GPIO functions:
| GPIO | Direction | Configuration |
| ---------------------------- | --------- | ------------------------------------------------------ |
| CONFIG_GPIO_OUTPUT_0 | output | |
| CONFIG_GPIO_OUTPUT_1 | output | |
| CONFIG_GPIO_INPUT_0 | input | pulled up, interrupt from rising edge and falling edge |
| CONFIG_GPIO_INPUT_1 | input | pulled up, interrupt from rising edge |
## Test:
1. Connect CONFIG_GPIO_OUTPUT_0 with CONFIG_GPIO_INPUT_0
2. Connect CONFIG_GPIO_OUTPUT_1 with CONFIG_GPIO_INPUT_1
3. Generate pulses on CONFIG_GPIO_OUTPUT_0/1, that triggers interrupt on CONFIG_GPIO_INPUT_0/1
**Note:** The following pin assignments are used by default, you can change them by `idf.py menuconfig` > `Example Configuration`.
| | CONFIG_GPIO_OUTPUT_0 | CONFIG_GPIO_OUTPUT_1 | CONFIG_GPIO_INPUT_0 | CONFIG_GPIO_INPUT_1 |
| ---------------------- | -------------------- | -------------------- | ------------------- | ------------------- |
| ESP32C2/ESP32H2 | 8 | 9 | 4 | 5 |
| All other chips | 18 | 19 | 4 | 5 |
## How to use example
Before project configuration and build, be sure to set the correct chip target using `idf.py set-target <chip_name>`.
### Hardware Required
* A development board with any Espressif SoC (e.g., ESP32-DevKitC, ESP-WROVER-KIT, etc.)
* A USB cable for Power supply and programming
* Some jumper wires to connect GPIOs.
### Configure the project
### Build and Flash
Build the project and flash it to the board, then run the monitor tool to view the serial output:
Run `idf.py -p PORT flash monitor` to build, flash and monitor the project.
(To exit the serial monitor, type ``Ctrl-]``.)
See the [Getting Started Guide](https://docs.espressif.com/projects/esp-idf/en/latest/get-started/index.html) for full steps to configure and use ESP-IDF to build projects.
## Example Output
As you run the example, you will see the following log:
I (317) gpio: GPIO[18]| InputEn: 0| OutputEn: 1| OpenDrain: 0| Pullup: 0| Pulldown: 0| Intr:0
I (327) gpio: GPIO[19]| InputEn: 0| OutputEn: 1| OpenDrain: 0| Pullup: 0| Pulldown: 0| Intr:0
I (337) gpio: GPIO[4]| InputEn: 1| OutputEn: 0| OpenDrain: 0| Pullup: 1| Pulldown: 0| Intr:1
I (347) gpio: GPIO[5]| InputEn: 1| OutputEn: 0| OpenDrain: 0| Pullup: 1| Pulldown: 0| Intr:1
Minimum free heap size: 289892 bytes
cnt: 0
cnt: 1
GPIO[4] intr, val: 1
GPIO[5] intr, val: 1
cnt: 2
GPIO[4] intr, val: 0
cnt: 3
GPIO[4] intr, val: 1
GPIO[5] intr, val: 1
cnt: 4
GPIO[4] intr, val: 0
cnt: 5
GPIO[4] intr, val: 1
GPIO[5] intr, val: 1
cnt: 6
GPIO[4] intr, val: 0
cnt: 7
GPIO[4] intr, val: 1
GPIO[5] intr, val: 1
cnt: 8
GPIO[4] intr, val: 0
cnt: 9
GPIO[4] intr, val: 1
GPIO[5] intr, val: 1
cnt: 10
## Troubleshooting
For any technical queries, please open an [issue](https://github.com/espressif/esp-idf/issues) on GitHub. We will get back to you soon.
例如,该示例明确说明:
- 使用两个 GPIO 作为输出
- 使用两个 GPIO 作为输入并触发中断
- GPIO 号可通过
menuconfig修改
配置项目:可通过 idf.py menuconfig > Example Configuration 修改引脚配置。
这意味着:
GPIO 的选择,本身就是一个"可配置项",而不是写死在代码里。
五、第四步:从示例中提取"最小可用 GPIO 输出代码"
GPIO 示例功能较多,但我们当前只关心一件事:
如何把一个 GPIO 配置为输出,并拉高 / 拉低电平
从官方示例中抽取并简化后,得到如下核心代码:
c
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "driver/gpio.h"
#define LED_GPIO_IO 43
#define LED_GPIO_PIN_SEL (1ULL << LED_GPIO_IO)
void app_main(void)
{
gpio_config_t io_conf={
.pin_bit_mask=LED_GPIO_PIN_SEL,
.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);
while (1) {
gpio_set_level(LED_GPIO_IO, 1);
vTaskDelay(1000 / portTICK_PERIOD_MS);
gpio_set_level(LED_GPIO_IO, 0);
vTaskDelay(1000 / portTICK_PERIOD_MS);
}
}
这段代码只做了两件事:
- 使用
gpio_config()将 GPIO43配置为输出模式 - 使用
gpio_set_level()控制电平高低
六、理解两个核心 API 就够了
api说明书:
1. gpio_config()
这是 GPIO 初始化的唯一入口函数。
关键参数包括:
pin_bit_mask:指定要配置的 GPIO(支持多个)mode:输入 / 输出 / 开漏pull_up_en/pull_down_en:上下拉配置intr_type:中断类型(本例不使用)
c
pin_bit_mask:
一个64位的掩码,用于指定要配置的引脚。每个位对应一个 GPIO 引脚,例如,若要配置 GPIO 2 和 GPIO 3,则可以设置为 (1ULL << 2) | (1ULL << 3)。
/*
* Let's say, GPIO_INPUT_IO_0=2, GPIO_INPUT_IO_1=3
* In binary representation,
* 1ULL<<GPIO_INPUT_IO_0 is equal to 0000000000000000000000000000000000000100 and
* 1ULL<<GPIO_INPUT_IO_1 is equal to 0000000000000000000000000000000000001000
* GPIO_INPUT_PIN_SEL 0000000000000000000000000000000000001100
* */
mode: 引脚的工作模式,可以是以下值之一:
GPIO_MODE_DISABLE: 禁用引脚。
GPIO_MODE_INPUT: 输入模式。
GPIO_MODE_OUTPUT: 输出模式。
GPIO_MODE_OUTPUT_OD: 开漏输出模式。
GPIO_MODE_INPUT_OUTPUT_OD: 开漏输入输出模式。
GPIO_MODE_INPUT_OUTPUT: 输入输出模式。
pull_up_en: 上拉电阻使能,可以是以下值之一:
GPIO_PULLUP_DISABLE: 禁用上拉电阻。
GPIO_PULLUP_ENABLE: 启用上拉电阻。
pull_down_en: 下拉电阻使能,可以是以下值之一:
GPIO_PULLDOWN_DISABLE: 禁用下拉电阻。
GPIO_PULLDOWN_ENABLE: 启用下拉电阻。
intr_type: 中断类型,可以是以下值之一:
GPIO_INTR_DISABLE: 禁用中断。
GPIO_INTR_POSEDGE: 上升沿触发中断。
GPIO_INTR_NEGEDGE: 下降沿触发中断。
GPIO_INTR_ANYEDGE: 任意边沿触发中断。
GPIO_INTR_LOW_LEVEL: 低电平触发中断。
GPIO_INTR_HIGH_LEVEL: 高电平触发中断。
一句话总结 :
gpio_config() 决定了"这个 GPIO 是什么身份"。
2. gpio_set_level()
c
gpio_set_level(gpio_num, level);
gpio_num:GPIO 编号level:0(低电平)或 1(高电平)
一句话总结 :
gpio_set_level() 决定了"这个 GPIO 此刻输出什么电平"。
七、编译、烧录与现象验证
最后,将开发板插到底板上,执行:
bash
idf.py set-target esp32s3
idf.py build
idf.py -p 端口号 flash monitor
烧录完成后,可以观察到:

LED 亮 1 秒,灭 1 秒,循环执行
至此,一个完整的 GPIO 输出控制流程就完成了。
八、写在最后:这套方法可以复制到所有外设
通过这次 GPIO 点灯,你真正学到的并不是"怎么点亮 LED",而是:
- 如何从 芯片 → 原理图 → 示例 → API
- 一步步拆解一个 ESP-IDF 外设功能
后续无论是 UART、I2C、SPI,学习路径完全一致。
只要你坚持用这种方式,
ESP32 的外设并不会"越来越难",只会"越来越熟"。