Tutorial
Below, I'll use the ESP32S3 and Fedora Linux as an example for this tutorial.
This tutorial is unique in that, while it is largely based on the Zhengdian Atom tutorial, that tutorial uses a lot of macro definitions, which is not suitable for beginners to read. This tutorial will minimize the use of macro definitions and try to teach you the most original code as much as possible.
Basic project creation
Preparation Work
(Some older versions must move this folder)
Copy this folder esp-idf/tools/templates/sample_project

Copy here esp-idf/examples/get-started/sample_project
Create a new project
Click on New Projects

Either of the following two options works. Some older versions don't have the bottom option. (For the latest version, just select the bottom one.)


Created successfully

menuconfig configuration

- Configure the main frequency
Search for
CPU, find the CPU frequency setting, and set the CPU frequency to 240MHz (maximum).
- Configure Flash and RAM
Search for Flash, set Flash SPI mode to QIO. This mode provides the fastest speed.
Check the ESP32S3 model purchased on Taobao. ESP32-S3 N16R8 I found that my Flash is 16M and RAM is 8M.

Search for PSRAM and check the box.

Looking at the official PSRAM introduction, we should select Octal SPI.



- Configure FreeRTOS
Configure configTICK_RATE_HZ as 1000, so the period is 1ms, and the unit of the vTaskDelay() function becomes ms.

- Configure the partition table
Search for
partition, findpartition table, and configure it as自定义分区表CSV.
- Save

Old backups can be deleted.

- Edit the partition table
Press ctrl shift P together, type
Partition Table, and findOpen Partition Table Eidtors UI.

按照下面一点都别抄错的抄下来

You can see that everything has been generated.

- Compile and test

As shown in the figure, the compilation was successful.

Introduction to Partition Tables
The partition table divides Flash into multiple storage areas, recording the specific functions and purposes of each area.




Custom project architecture and adding components
Introduces the engineering architecture.
Here is the official project structure from Espressif:

This is clearly very mixed.
The following is the project structure of Zhengdian Atom, which is more modular, offers greater scalability, and has clearer layering.

Create the project architecture.
Copy the basic project, paste it into the folder where you store your code, and rename it to N01_LED.

Open the newly created folder with VSCode.
cd ~/UserFolder/MySource/ESP32_Projects/N01_LED
code .
Create the following files


Open the top-level CMakeLists

# Set the extra component directories
set(EXTRA_COMPONENT_DIRS components/Middlewares)
# Add compile options,warning has color.
add_compile_options(-fdiagnostics-color=always)
Modify the CMakeLists in the BSP.

set(src_dirs
LED)
set(include_dirs
LED)
# GPIO的一些组件在driver里
set(requires
driver)
idf_component_register(SRC_DIRS ${src_dirs}
INCLUDE_DIRS ${include_dirs} REQUIRES ${requires})
component_compile_options(-ffast-math -O3 -Wno-error=format=-Wno-format)
Clean and rebuild to see.

Successfully compiled

Add component
Search for esp component using ctrl shift P, then find esp component registry.

Select the model

For example, to install OpenAI, search for OpenAI.

Click Install.

Installation successful

The image below shows a successful installation.

Clean and rebuild to see.

Successfully compiled

Note: In main.c, you can directly call the added components, but in the files under the components folder, you cannot use them directly; you need to modify CMakeLists.txt.
Downloading and debugging the ESP32
Download
First, write a program. Add a hello world in main.c.
#include <stdio.h>
void app_main(void)
{
printf("Hello World!\n");
}
You use a USB-A to USB-C cable, plugging one end into the computer and the other end into the development board's USB port, not the UART port.
As shown in the figure
- Find device
- Windows
If you are on Windows, right-click
此电脑, then click管理.

- Windows
If you are on Windows, right-click
You can see that it has been detected normally below.

2. Linux
If you are using Linux, open the terminal.
ls /dev | grep USB
# 或者
ls /dev | grep ACM
```

Check if any device is detected. As shown in the image above, mine is `/dev/ttyACM0`.
2. Download
1. Windows
If you are on Windows, first select the mode as `JTAG`, the port as the detected port `COM4`, and the chip model as `esp32s3`. Then click `清理`, `构建`, and `烧录`.

Then select yes.

As shown, the flashing is complete.

2. Linux
If you are on Linux, first select the mode as `JTAG`, the port as the detected port `/dev/ttyACM0`, and the chip model as `esp32s3`. Then click `清理`, `构建`, `烧录`.


Select yes here.

The image below shows a successful flash (if you are unable to download normally, please refer to `常见问题` below).

#### Debugging
First, write a program.
```c
//包含FreeRTOS头文件(为了用vTaskDelay)
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
int32_t i = 0;
void app_main(void)
{
//死循环,等同于while(1),但效率比while(1)更高
for(;;)
{
i++;
vTaskDelay(500 / portTICK_PERIOD_MS); //延时500ms
}
}
Just click Debug.

This means you've successfully entered debug mode.

The image below, The first one is to start running. The second is step over, running one function at a time. The third one is single-step debugging, which executes by stepping into the function. The fourth one is step out, which will exit this function. The fifth one is the restart program, but there's a bug in the ESP32 where it doesn't seem to restart. Not sure if this will be fixed in a future update. The sixth is to disconnect the link and exit debugging.

You can also set breakpoints, so the program will stop running when it reaches a breakpoint. As shown in the figure

You can also right-click a variable to add it to Watch, set a breakpoint at a certain line in the program, and then check the value of that variable before the program reaches that line (note: the breakpoint is placed at the line where execution has just arrived, meaning that line hasn't run yet).
As shown in the figure
For example, right-click this i
Click this 添加到监视(Add to Watch)

Every time you 开始运行(F5) to a breakpoint, this i increments by 1.

Frequently Asked Questions
- If you are using Linux and encounter the following issue
Open On-Chip Debugger v0.12.0-esp32-20250707 (2025-07-06-17:37) Licensed under GNU GPL v2 For bug reports, read http://openocd.org/doc/doxygen/bugs.html [OpenOCD] Open On-Chip Debugger v0.12.0-esp32-20250707 (2025-07-06-17:37) Licensed under GNU GPL v2 For bug reports, read http://openocd.org/doc/doxygen/bugs.html debug_level: 2 Info : esp_usb_jtag: VID set to 0x303a and PID to 0x1001 Info : esp_usb_jtag: capabilities descriptor set to 0x2000 Info : Listening on port 6666 for tcl connections Info : Listening on port 4444 for telnet connections ❌ Error: libusb_open() failed with LIBUSB_ERROR_ACCESS Error: libusb_open() failed with LIBUSB_ERROR_ACCESS ❌ Error: esp_usb_jtag: could not find or open device! /home/tungchiahui/.espressif/tools/openocd-esp32/v0.12.0-esp32-20250707/openocd-esp32/share/openocd/scripts/target/esp_common.cfg:9: Error: Traceback (most recent call last): File "/home/tungchiahui/.espressif/tools/openocd-esp32/v0.12.0-esp32-20250707/openocd-esp32/share/openocd/scripts/target/esp_common.cfg", line 9, in script Error: esp_usb_jtag: could not find or open device! /home/tungchiahui/.espressif/tools/openocd-esp32/v0.12.0-esp32-20250707/openocd-esp32/share/openocd/scripts/target/esp_common.cfg:9: Error: Traceback (most recent call last): File "/home/tungchiahui/.espressif/tools/openocd-esp32/v0.12.0-esp32-20250707/openocd-esp32/share/openocd/scripts/target/esp_common.cfg", line 9, in script For assistance with OpenOCD errors, please refer to our Troubleshooting FAQ: https://github.com/espressif/openocd-esp32/wiki/Troubleshooting-FAQ OpenOCD Exit with non-zero error code 1 [Stopped] : OpenOCD Server [/OpenOCD] [Flash] Can't perform JTAG flash, because OpenOCD server is not running! Flash has finished. You can monitor your device with 'ESP-IDF: Monitor command'然后tungchiahui@Dell-G15-5511:~/UserFolder/MySource/ESP32_Projects/N01_LED$ ls /dev | grep ACM ttyACM0 tungchiahui@Dell-G15-5511:~/UserFolder/MySource/ESP32_Projects/N01_LED$ lsusb Bus 001 Device 001: ID 1d6b:0002 Linux Foundation 2.0 root hub Bus 002 Device 001: ID 1d6b:0003 Linux Foundation 3.0 root hub Bus 003 Device 001: ID 1d6b:0002 Linux Foundation 2.0 root hub Bus 003 Device 002: ID 0416:b23c Winbond Electronics Corp. Gaming Keyboard Bus 003 Device 003: ID 046d:c539 Logitech, Inc. Lightspeed Receiver Bus 003 Device 004: ID 0c45:6720 Microdia Integrated_Webcam_HD Bus 003 Device 005: ID 8087:0026 Intel Corp. AX201 Bluetooth Bus 003 Device 006: ID 303a:1001 Espressif USB JTAG/serial debug unit Bus 004 Device 001: ID 1d6b:0003 Linux Foundation 3.0 root hub tungchiahui@Dell-G15-5511:~/UserFolder/MySource/ESP32_Projects/N01_LED
Most likely it's a permission issue.
lsusb

Find the device that follows 1001, and remember the device ID before it. For example, mine is 303a.
Next, edit udev.
sudo vim /etc/udev/rules.d/99-esp32-usb-jtag.rules
Please provide the content you'd like me to translate, and I'll fill in the idVendor placeholder as needed.
SUBSYSTEM=="usb", ATTR{idVendor}=="303a", MODE="0666"
After saving, first type the following command
sudo udevadm control --reload-rules
sudo udevadm trigger
Then plug and unplug the USB, and the programming will succeed.
The clock tree of the ESP32-S3
clock
A clock is a signal that toggles periodically.

Each time a clock edge arrives, the entire circuit undergoes a complete state update. This way, the entire system will move forward one step.
Clock tree


OSC is a high-speed crystal oscillator, CLK is the clock, PLL is a phase-locked loop, DIV in the diagram is a frequency divider, and MUX is a multiplexer. The OSC needs to be connected to a crystal oscillator. CLK is an available clock signal. PLL is used for the clock frequency that is to be multiplied or divided. To save power and conserve resources. MUX selects which clock source.


GPIO
GPIO Theory
Actually, it refers to General Purpose Input/Output ports, which can output high or low levels and also read high or low levels.
The input principle is as shown in the figure below: if pressing KEY causes the circuit level to go low, a pull-up resistor is needed, and the falling edge is used to determine whether the button is pressed. Due to the pull-up resistor, when the KEY is not pressed, the IO port in the lower circuit is at a high level. Once the KEY is pressed, it becomes low, creating a falling edge (the level transitions from high to low).

Output, as shown in the figure below: when the IO is at a high level, the LED (light-emitting diode) conducts and turns on; when the IO is at a low level, the LED does not conduct and turns off.

Here is the ESP32's IO:

They are highly reusable, and each one can be reused as an interface for other peripherals.
But there are some exceptions: certain pins cannot be used as inputs or outputs — they can only be used to connect to the FLASH or PSRAM on the module.
You can refer to the detailed explanation in the esp32-s3_datasheet_cn.pdf from Zhengdian Atom.
GPIO-related functions
- gpio_config
This is the GPIO initialization function.
esp_err_t gpio_config(const gpio_config_t *pGPIOConfig)
Then right-click to select gpio_config_t, click go to definition, and the following struct will appear — this is the entry parameter struct inside the GPIO initialization function.
/**
* @brief Configuration parameters of GPIO pad for gpio_config function
*/
typedef struct {
uint64_t pin_bit_mask; /*!< GPIO pin: set with bit mask, each bit maps to a GPIO */
gpio_mode_t mode; /*!< GPIO mode: set input/output mode */
gpio_pullup_t pull_up_en; /*!< GPIO pull-up */
gpio_pulldown_t pull_down_en; /*!< GPIO pull-down */
gpio_int_type_t intr_type; /*!< GPIO interrupt type */
#if SOC_GPIO_SUPPORT_PIN_HYS_FILTER
gpio_hys_ctrl_mode_t hys_ctrl_mode; /*!< GPIO hysteresis: hysteresis filter on slope input */
#endif
} gpio_config_t;
pin_bit_mask is used to set the GPIO pin you want to configure, typically 1ull << x. Here, x is the IOx number of the GPIO you want to set.
mode is the mode for configuring input or output. You can check go to definition by looking at gpio_mode_t.
typedef enum {
GPIO_MODE_DISABLE = GPIO_MODE_DEF_DISABLE, /*!< GPIO mode : disable input and output */
GPIO_MODE_INPUT = GPIO_MODE_DEF_INPUT, /*!< GPIO mode : input only */
GPIO_MODE_OUTPUT = GPIO_MODE_DEF_OUTPUT, /*!< GPIO mode : output only mode */
GPIO_MODE_OUTPUT_OD = ((GPIO_MODE_DEF_OUTPUT) | (GPIO_MODE_DEF_OD)), /*!< GPIO mode : output only with open-drain mode */
GPIO_MODE_INPUT_OUTPUT_OD = ((GPIO_MODE_DEF_INPUT) | (GPIO_MODE_DEF_OUTPUT) | (GPIO_MODE_DEF_OD)), /*!< GPIO mode : output and input with open-drain mode*/
GPIO_MODE_INPUT_OUTPUT = ((GPIO_MODE_DEF_INPUT) | (GPIO_MODE_DEF_OUTPUT)), /*!< GPIO mode : output and input mode */
} gpio_mode_t;
The same goes for the rest — you can all go to definition to see what this struct can actually hold.
pull_up_en indicates whether to enable the pull-up resistor, meaning it adds a circuit in parallel to the IO line that has a resistor connected to VCC.
pull_down_en is whether to enable the pull-down resistor, meaning it adds a circuit in parallel to the IO line that has a resistor and GND.
intr_type is whether to enable the interrupt type.
- gpio_set_level
esp_err_t gpio_set_level(gpio_num_t gpio_num, uint32_t level)
{
GPIO_CHECK(GPIO_IS_VALID_OUTPUT_GPIO(gpio_num), "GPIO output gpio_num error", ESP_ERR_INVALID_ARG);
gpio_hal_set_level(gpio_context.gpio_hal, gpio_num, level);
return ESP_OK;
}
gpio_num is which IO port to select. You can take a look at go to definition. As shown in the code below, it can be filled with GPIO_NUM_x. Here, x is used to select which IOx to initialize (i.e., which pin/IO to initialize).
level determines whether the output level is high or low; fill in 0 for low and 1 for high.
/**
* @brief GPIO number
*/
typedef enum {
GPIO_NUM_NC = -1, /*!< Use to signal not connected to S/W */
GPIO_NUM_0 = 0, /*!< GPIO0, input and output */
GPIO_NUM_1 = 1, /*!< GPIO1, input and output */
GPIO_NUM_2 = 2, /*!< GPIO2, input and output */
GPIO_NUM_3 = 3, /*!< GPIO3, input and output */
GPIO_NUM_4 = 4, /*!< GPIO4, input and output */
GPIO_NUM_5 = 5, /*!< GPIO5, input and output */
GPIO_NUM_6 = 6, /*!< GPIO6, input and output */
GPIO_NUM_7 = 7, /*!< GPIO7, input and output */
GPIO_NUM_8 = 8, /*!< GPIO8, input and output */
GPIO_NUM_9 = 9, /*!< GPIO9, input and output */
GPIO_NUM_10 = 10, /*!< GPIO10, input and output */
GPIO_NUM_11 = 11, /*!< GPIO11, input and output */
GPIO_NUM_12 = 12, /*!< GPIO12, input and output */
GPIO_NUM_MAX,
} gpio_num_t;
- gpio_get_level
int gpio_get_level(gpio_num_t gpio_num)
{
return gpio_hal_get_level(gpio_context.gpio_hal, gpio_num);
}
gpio_num is which IO port to select. You can take a look at go to definition. As shown in the code below, it can be filled with GPIO_NUM_x. Here, x is used to select which IOx to initialize (i.e., which pin/IO to initialize).
The returned int data indicates whether the read level is high or low: 0 means low, 1 means high.
LED light practical
Two types of physical LEDs:
SMD LED

In the circuit below, the LED only lights up when the IO port is at a high level.
In the circuit below, the LED only lights up when the IO port is at a low level.
The two drawbacks above are that the current flows directly through the MCU. There is another method using a transistor that is better—you can search for it yourself, or take a look at the schematic of the DJI STM32C board (STM32F407IGH6).
Let's start by writing the code for led.h. This is the minimal framework. You should have learned about conditional compilation in C — it's used to prevent the header file from being included multiple times.
#ifndef __LED_H_
#define __LED_H_
#endif
Next, continue to improve led.h. The following enum is designed to make the code more readable by giving names to high and low logic levels. PIN_RESET represents a low level, and PIN_SET represents a high level.
#ifndef __LED_H_
#define __LED_H_
//包含ESP32的gpio组件的头文件
#include "driver/gpio.h"
typedef enum {
PIN_RESET = 0,
PIN_SET
} gpio_output_state_t;
void led_init(void);
#endif
Next, write the content for led.c:
Since I looked at the schematic of my board, IO43 is the control pin for the LED, so I initialize IO43 below. (I only have IO43 here; IO44 is connected to a regular light, so I have no choice but to first replace Channel for console output in menuconfig with none, but you don't need to change this for your light.)
#include "led.h"
void led_init(void)
{
//给结构体清零(C语言知识,要给局部变量清零,防止未被初始化的地方出现很奇怪的事情)
gpio_config_t gpio_init_struct = {0};
gpio_init_struct.pin_bit_mask = (1ULL << GPIO_NUM_43); //初始化IO43
gpio_init_struct.mode = GPIO_MODE_OUTPUT; //设置为输出模式
gpio_init_struct.pull_up_en = GPIO_PULLUP_DISABLE; //禁用上拉电阻
gpio_init_struct.pull_down_en = GPIO_PULLDOWN_DISABLE; //禁用下拉电阻
gpio_init_struct.intr_type = GPIO_INTR_DISABLE; //禁用中断
gpio_config(&gpio_init_struct); //配置GPIO
}
You can find out what values to fill in for the above parameters by querying the struct type through go to definition.
If you don't understand, carefully watch the Zhengdian Atom video on how to look up structs.
Then edit main.c:
//包含FreeRTOS头文件(为了用vTaskDelay)
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
//包含LED头文件,为了初始化LED和使用GPIO
#include "led.h"
void app_main(void)
{
//初始化led的GPIO
led_init();
//死循环,等同于while(1),但效率比while(1)更高
for(;;)
{
gpio_set_level(GPIO_NUM_43, PIN_SET); //设置为高电平
vTaskDelay(500 / portTICK_PERIOD_MS); //延时500ms
gpio_set_level(GPIO_NUM_43, PIN_RESET); //设置为低电平
vTaskDelay(500 / portTICK_PERIOD_MS); //延时500ms
}
}
The code above makes the LED blink once every 1 second.
Here
vTaskDelay(500 / portTICK_PERIOD_MS); can actually be simplified to vTaskDelay(500);, because we previously configured configTICK_RATE_HZ to 1000, which will cause the value of portTICK_PERIOD_MS to be 1.
However, the above approach is more standardized. If others want to use your code, they won't run into unit errors with delays. It's still recommended to avoid the simplified method. Zhengdian Atom uses the simplified version, but if someone copies and migrates your code, it could cause major issues.
The usual three steps, press them in order, and then you'll see the lights on the board flash.

KEY Practical
Below is a circuit for a KEY. When the KEY is pressed, it conducts.

Actually, pressing a button can cause bouncing, which is typically caused by mechanical materials, structure, or the environment.

However, the middle section is normal, as shown in the figure below.

For everyday use, the delay method in software debouncing is sufficient.

Actual wiring

First, modify CMakeLists.txt in the BSP folder:
set(src_dirs
LED
KEY)
set(include_dirs
LED
KEY)
# GPIO的一些组件在driver里
set(requires
driver)
idf_component_register(SRC_DIRS ${src_dirs}
INCLUDE_DIRS ${include_dirs} REQUIRES ${requires})
component_compile_options(-ffast-math -O3 -Wno-error=format=-Wno-format)
Let's open the schematic and see which pin the button is connected to.
Since my board doesn't have custom buttons, I chose to use the BOOT button as a button.

As shown in the image above, one side of this button is connected to GND and the other side is connected to GPIO0. So initially, the GPIO0 side must be in a high-level state. When the button is pressed, the entire line will be pulled to a low level by GND. Therefore, GPIO0 detects the level changing from high to low, which is called detecting a falling edge, and the opposite is called a rising edge.
How do we make GPIO0 start at a high level? We need to connect GPIO0 to a pull-up resistor.
Conversely, if BOOT has VCC on its left side, then GPIO0 needs a pull-down resistor and should detect the rising edge.
First, create the content of key.h.
#ifndef __KEY_H_
#define __KEY_H_
//包含ESP32的gpio组件的头文件
#include "driver/gpio.h"
#define BOOT_PRES 1
#endif
Here, BOOT_PRES is an identifier. When the BOOT button is pressed, the value BOOT_PRES will be detected, which is 1.
Then in key.c, first initialize the gpio of gpio0.
It's similar to led. However, note that the mode here is input mode, and the pull-up resistor must be enabled.
void key_init(void)
{
//给结构体清零(C语言知识,要给局部变量清零,防止未被初始化的地方出现很奇怪的事情)
gpio_config_t gpio_init_struct = {0};
gpio_init_struct.pin_bit_mask = (1ULL << GPIO_NUM_0); //初始化IO0
gpio_init_struct.mode = GPIO_MODE_INPUT; //设置为输入模式
gpio_init_struct.pull_up_en = GPIO_PULLUP_ENABLE; //启用上拉电阻
gpio_init_struct.pull_down_en = GPIO_PULLDOWN_DISABLE; //禁用下拉电阻
gpio_init_struct.intr_type = GPIO_INTR_DISABLE; //禁用中断
gpio_config(&gpio_init_struct); //配置GPIO
}
Now for the main event: the key scanning function. The comments below explain it in great detail, so you can analyze the code carefully on your own.
/**
* @brief 按键扫描函数
* @note 无
* @param mode:0 / 1, 具体含义如下:
* @arg 0, 不支持连续按(当按键按下不放时, 只有第一次调用会返回键值,
* 必须松开以后, 再次按下才会返回其他键值)
* @arg 1, 支持连续按(当按键按下不放时, 每次调用该函数都会返回键值)
* @retval 键值, 定义如下:
* BOOT_PRES, 1, BOOT按键按下
*/
uint8_t key_scan(uint8_t mode)
{
static uint8_t key_up = 1; /* 按键按松开标志 */
uint8_t keyval = 0;
if (mode) /* 支持连按 */
{
key_up = 1;
}
if (key_up && (gpio_get_level(GPIO_NUM_0) == 0)) /* 按键松开标志为1, 且有任意一个按键按下了 */
{
vTaskDelay(10 / portTICK_PERIOD_MS); /* 去抖动 */
key_up = 0;
if (gpio_get_level(GPIO_NUM_0) == 0) keyval = BOOT_PRES;
}
else if (gpio_get_level(GPIO_NUM_0) == 1) /* 没有任何按键按下, 标记按键松开 */
{
key_up = 1;
}
return keyval; /* 返回键值 */
}
Mainly using the function int gpio_get_level(gpio_num_t gpio_num), which we covered in the first section.
int gpio_get_level(gpio_num_t gpio_num)
{
return gpio_hal_get_level(gpio_context.gpio_hal, gpio_num);
}
Then also define a global variable to serve as the key variable, storing the key result.
uint8_t key_value = 0;
Write the main logic in main.c, which is to toggle the level when the button is pressed.
//包含FreeRTOS头文件(为了用vTaskDelay)
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
//包含LED头文件,为了初始化LED和使用GPIO
#include "led.h"
//包含KEY头文件,为了初始化KEY和使用GPIO
#include "key.h"
//按键外部变量声明;
extern uint8_t key_value;
void app_main(void)
{
//初始化led的GPIO
led_init();
//初始化key的GPIO
key_init();
//死循环,等同于while(1),但效率比while(1)更高
for(;;)
{
key_value = key_scan(0);
switch (key_value)
{
case BOOT_PRES:
gpio_set_level(GPIO_NUM_43, !gpio_get_level(GPIO_NUM_43)); //翻转电平
break;
default:
break;
}
vTaskDelay(10 / portTICK_PERIOD_MS); //延时10ms
}
}
Below is the complete content of key.h and key.c:
#include "key.h"
//包含FreeRTOS头文件(为了用vTaskDelay)
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
uint8_t key_value = 0;
void key_init(void)
{
//给结构体清零(C语言知识,要给局部变量清零,防止未被初始化的地方出现很奇怪的事情)
gpio_config_t gpio_init_struct = {0};
gpio_init_struct.pin_bit_mask = (1ULL << GPIO_NUM_0); //初始化IO0
gpio_init_struct.mode = GPIO_MODE_INPUT; //设置为输入模式
gpio_init_struct.pull_up_en = GPIO_PULLUP_ENABLE; //启用上拉电阻
gpio_init_struct.pull_down_en = GPIO_PULLDOWN_DISABLE; //禁用下拉电阻
gpio_init_struct.intr_type = GPIO_INTR_DISABLE; //禁用中断
gpio_config(&gpio_init_struct); //配置GPIO
}
/**
* @brief 按键扫描函数
* @note 无
* @param mode:0 / 1, 具体含义如下:
* @arg 0, 不支持连续按(当按键按下不放时, 只有第一次调用会返回键值,
* 必须松开以后, 再次按下才会返回其他键值)
* @arg 1, 支持连续按(当按键按下不放时, 每次调用该函数都会返回键值)
* @retval 键值, 定义如下:
* BOOT_PRES, 1, BOOT按键按下
*/
uint8_t key_scan(uint8_t mode)
{
static uint8_t key_up = 1; /* 按键按松开标志 */
uint8_t keyval = 0;
if (mode) /* 支持连按 */
{
key_up = 1;
}
if (key_up && (gpio_get_level(GPIO_NUM_0) == 0)) /* 按键松开标志为1, 且有任意一个按键按下了 */
{
vTaskDelay(10 / portTICK_PERIOD_MS); /* 去抖动 */
key_up = 0;
if (gpio_get_level(GPIO_NUM_0) == 0) keyval = BOOT_PRES;
}
else if (gpio_get_level(GPIO_NUM_0) == 1) /* 没有任何按键按下, 标记按键松开 */
{
key_up = 1;
}
return keyval; /* 返回键值 */
}
#ifndef __KEY_H_
#define __KEY_H_
//包含ESP32的gpio组件的头文件
#include "driver/gpio.h"
#define BOOT_PRES 1
void key_init(void);
uint8_t key_scan(uint8_t mode);
#endif
Compiled without errors.


You can test it: when the button is pressed, the LED toggles its level.
Another testing method is to use debug to monitor the value of key_value, but it requires you to change the button scanning mode to support long presses. In the code above, you only need to modify the key_value = key_scan(1); line.
You can enter debug mode to test it.
In the image below, set a breakpoint. Then, when you hold down the button and click "Continue," the monitored value will change to 1. When you release the button and click "Continue," it will change to 0.


External interrupt
Introduction to Interrupts
In the previous section, the method of continuously running the key scanning function inside the infinite loop in app_main is actually called 轮询, and this type of function is called 阻塞式函数. If you've studied STM32, you definitely know what 阻塞式 and 中断式 are.


What exactly does the interrupt execution process look like? Please refer to the introduction in the link below. You only need to read about the interrupt execution process; everything else is STM32-specific and doesn't need to be reviewed.
Introduction to Interrupt Service FunctionsIntroduction to External Interrupts
From the link above, you'll understand that external interrupt EXIT is just one type of interrupt within IT. In this section, we'll discuss external interrupts in detail.

The ESP32's EXIT has one more level-triggered mode than the STM32.
Level-triggered: High and low level triggering require maintaining the interrupt level state until the CPU responds.
Edge-triggered: rising edge and falling edge triggering. Once this type of interrupt is triggered, the CPU can respond immediately.
The external interrupt function of the ESP32S3 can capture external event triggers with very high precision. Developers can configure Set the interrupt trigger mode (e.g., rising edge, falling edge, any level, low-level hold, high-level hold, etc.) to accommodate different External events, which immediately interrupt the current program execution when they occur, and instead execute the interrupt service routine.
interrupt priority
When multiple interrupts are triggered simultaneously, the CPU executes them in a specific order, handling higher-priority interrupts first and lower-priority ones afterward.
ESP32-S3 supports six levels of interrupts and also supports interrupt nesting, meaning a lower-priority interrupt can be preempted by a higher-priority interrupt. As shown in the table below, in the Priority column, a larger number indicates a higher priority for that interrupt. Among them, the NMI interrupt has the highest Priority: Once this type of interrupt is triggered, the CPU must handle it.

In the ESP32S3, the interrupt system is used to respond to various internal and external events. These interrupts, based on their trigger method and priority, Priorities are classified. The table above lists the interrupt numbers, categories, types, and corresponding priorities for the ESP32S3 in detail. By configuring These interrupts allow developers to achieve timely response and processing of various events, improving system efficiency and real-time performance.
- Interrupt number: The unique identifier for each interrupt, used to reference and configure a specific interrupt in a program.
- Category: Source type of interrupts, divided into external interrupts and internal interrupts. External interrupts are triggered by external devices or signals. such as buttons and sensors; internal interrupts are triggered by hardware events inside the microcontroller, such as timer overflows and software interrupts.
- Types: Interrupt trigger methods include level-triggered and edge-triggered. Level-triggered occurs when the input signal reaches a specific Interrupts are triggered by signal levels (such as high or low levels); edge triggering occurs when the input signal changes from one level state to another. An interrupt is triggered when in a certain state.
- Priority: Interrupt response priority. When multiple interrupts occur simultaneously, the microcontroller prioritizes them based on their interrupt priority. to determine which interrupt to handle first. A higher priority means the interrupt will be processed first. During development, developers can configure interrupt trigger modes, priorities, and other parameters based on actual needs to achieve high Efficient and reliable event handling mechanism.
External Interrupt in Practice
