Wiki
Tutorial
Existing English translations were reused. 5 current source blocks remain in Simplified Chinese, without triggering paid translation.
下面教程我以ESP32S3和Fedora Linux为例.
The characteristic of this tutorial is that although most of it is based on the ALIENTEK tutorial, the ALIENTEK tutorial uses a very large number 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.
1.Basic project creation
1.1.Preparation work
(Some older versions require moving this folder) Copy this folderesp-idf/tools/templates/sample_project

Copy hereesp-idf/examples/get-started/sample_project

1.2.Create New Project
ClickNew Projects


Either of the following two will work; some older versions do not have the bottommost option. (For a normal latest version, just select the bottommost one.) 

Created successfully

1.3.menuconfig Configuration

-
Configure CPU frequency Search
CPU, find the CPU frequency setting, set the CPU frequency to 240MHz (maximum)
-
Configure Flash and RAM
SearchFlash, set itFlash SPI modeas QIO; this mode has the fastest speed.
Check the ESP32S3 model I bought on Taobao,ESP32-S3 N16R8and learned that my Flash is 16M and RAM is 8M

SearchPSRAMand check

Looking at the official PSRAM introduction, we should chooseOctal SPI



- Configure FreeRTOS
SetconfigTICK_RATE_HZ configured as1000, so the period is 1 ms, and thevTaskDelay() unit of the function also becomes ms. 
-
Configure partition table Search
partition, findpartition table, and configure as自定义分区表CSV
-
Save

Old backups can be deleted 
- Edit partition table Press ctrl shift P together, enter
Partition Table, findOpen Partition Table Eidtors UI

Copy it down below without making a single mistake 
It can be seen that all have been generated. 
- Compile test

As shown in the figure, the compilation is successful.

2.Introduction to Partition Tables
The role of the partition table is to divide Flash into multiple storage areas and record the specific function and purpose of each area.




3.Custom engineering architecture and adding components
3.1.Introduce the engineering architecture
The following is Espressif's official project structure:

This is obviously quite a mishmash.
The following is ALIENTEK's project structure, which is more modular, offers more flexible scalability, and has clearer layering.

3.2.Create project architecture
Copy the basic project, paste it into your code folder, and then rename it to N01_LED.

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


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 recompile to see 
Compilation successful

3.3.Add component
ctrl shift P searchesp componentfoundesp component registry

Select model 
For example, to install OpenAI, search for OpenAI

Tap to install

Installation successful

The figure below shows that the installation was successful 
Clean and recompile to see 
Compilation successful

Note that inmain.cyou can directly call the added component, but in files under thecomponentsfolder, you cannot use it directly; you need to modifyCMakeLists.txt
4.ESP32 Download and Debug
4.1.Download
First write a program, and write a hello world inside main.c.
#include <stdio.h>
void app_main(void)
{
printf("Hello World!\n");
}
You use a USB-A to USB-C data cable, with one end on the computer and the other end plugged into the development board's USB interface, not the UART interface.
As shown in the figure
-
Device Detection
- Windows If you are on Windows, right-click
此电脑, click管理

You will see it has been detected properly as shown below

- Linux If you are on Linux, open a terminal
ls /dev | grep USB # 或者 ls /dev | grep ACM
Check whether any device is detected. As shown above, mine is
/dev/ttyACM0. - Windows If you are on Windows, right-click
-
Download
- Windows If you are on Windows, first select the mode as
JTAG, set the port to the detected portCOM4, and the chip model toesp32s3, then click清理,构建,烧录.

Then select yes

It is flashed as shown in the figure

- Linux If you are on Linux, first select the mode as
JTAG, set the port to the detected port/dev/ttyACM0, and the chip model toesp32s3, then click清理,构建,烧录.


Select yes here

The figure below indicates successful flashing (if you cannot download normally, please look below
常见问题)
- Windows If you are on Windows, first select the mode as
4.2.Debugging
First, write a program
//包含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
}
}
Directly click Debug, 
This way, it enters debug mode normally. 
Below, The first one is start running. The second one is step over, running function by function. The third one is step into, which will enter the function and execute inside. The fourth one is step out, which will exit this function. The fifth one is restart the program, but there is a bug in esp32; it seems it will not restart, not sure if it will be fixed later. The sixth one is disconnect, exiting debugging.

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

You can also right-click a variable and add it to the Watch, then set a breakpoint on a certain line of the program to see the value of the variable before the program runs to that line (note that wherever you set the breakpoint is exactly the line that has just been reached, and that line has not yet been executed). As shown in the figure. For example, right-click thisi

Click this添加到监视(Add to Watch)

Each time you开始运行(F5)reach the breakpoint, thisiwill increase by 1.

4.2.1.Frequently Asked Questions
- If you are on 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
It is most likely due to permission issues.
lsusb

Find the device after this that is1001; remember the device ID before it, for example mine is303a
Next, edit udev
sudo vim /etc/udev/rules.d/99-esp32-usb-jtag.rules
Enter the following content (note that the "idVendor" here should be filled in with your corresponding value)
SUBSYSTEM=="usb", ATTR{idVendor}=="303a", MODE="0666"
After saving, run the following command first
sudo udevadm control --reload-rules
sudo udevadm trigger
Then plug and unplug the USB, and the next flashing attempt succeeds.
5.ESP32-S3 clock tree
5.1.clock
A clock is a signal that toggles periodically 
On each clock edge, the entire circuit completes one full state update, and in this way, the whole system advances by one step.
5.2.Clock tree


OSC is a high-speed crystal oscillator, CLK is a clock, PLL is a phase-locked loop, DIV in the figure is a frequency divider, and MUX is a selector. OSC needs to be connected to a crystal oscillator. CLK is an available clock signal. PLL is for multiplying or dividing the clock frequency. DIV is for saving power and conserving resources. MUX is for selecting which clock source.


6.GPIO
6.1.GPIO Theory
实际上就是通用输入输出端口的意思,可以输出高低电平,也可以读取高低电平.
The input principle is as shown in the figure below. If pressing the KEY causes the circuit level to become low, then a pull-up resistor is required, and the falling edge is used to determine whether the key is pressed. Because of the pull-up resistor, when the KEY is not pressed at the beginning, the IO port of the circuit below is at a high level. Once the KEY is pressed, it will become a low level, so there will be a falling edge (the level changes from high to low).

Output: as shown in the figure below, when IO is at a high level, the LED light-emitting diode is conducting, and then the LED lights up; when IO is at a low level, the LED light-emitting diode is not conducting, and the LED is off.

The following are the ESP32 IOs:

They are highly reusable, and each one can be reused as an interface for other peripherals.
However, 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 description in ALIENTEK'sesp32-s3_datasheet_cn.pdf.
6.2.GPIO-related functions
- gpio_config
This is the GPIO initialization function
esp_err_t gpio_config(const gpio_config_t *pGPIOConfig)
Then you right-click to selectgpio_config_t, clickgo to definition, and the following will appear, which is the structure of the entry parameters in 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是用于设置你要配置的GPIO引脚,一般为1ull << x,这里的x就是你要设置的GPIO的IOx号.
modeIt is the mode for configuring input or output, you cango to definitiontake a lookgpio_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;
其他的也一样,你都可以go to definition来看这个结构体到底能填什么.
pull_up_enWhether to enable the pull-up resistor means connecting another circuit in parallel with the IO line, with a resistor and VCC in that circuit.
pull_down_enWhether to enable the pull-down resistor means connecting another circuit in parallel with the IO line, with a resistor and GND in that circuit.
intr_typeWhether 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_numWhich IO port is selected cango to definitionTake a look; as can be seen from the code below, it can be filled withGPIO_NUM_x, here thexis to select and initializeIOx(that is, which pin IO is initialized)
levelIt is whether the output level is high or low. Filling in 0 means low, and filling in 1 means 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_numWhich IO port is selected cango to definitionTake a look; as can be seen from the code below, it can be filled withGPIO_NUM_x, here thexis to select and initializeIOx(that is, which pin IO is initialized)
The int data returned by return indicates whether the read level is high or low; 0 means low, 1 means high.
6.3.LED Light Practical Practice
Two types of LED physical objects:
SMD LED 
In the following circuit, the LED lights up only when the IO port is at a high level.
In the following circuit, the LED lights up only when the IO port is at a low level.
The disadvantage of the above two is that the current flows directly through the MCU. Another transistor-based method is better; you can search for it yourself, or look at the schematic of DJI's STM32C board (STM32F407IGH6).
First, write the code forled.h; this is the minimal framework. You should have learned conditional compilation in C, which is used to prevent header files from being included repeatedly.
#ifndef __LED_H_
#define __LED_H_
#endif
Next, continue to improve.led.hThe following enumeration is intended to make the code more readable by giving names to high and low levels,PIN_RESETis low level,PIN_SETis 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 ofled.c Because looking at my own board's schematic, IO43 on my board is the LED control pin, so next I will initialize IO43. (Here I only have IO43; IO44 is connected to an ordinary light, so there is no way around it—I can only first replace the menuconfig in Channel for console output with none, while your light does not need to change this.)
#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
}
What you should fill in for the above parameters can all be determined bygo to definitionquerying the structure type. If you do not understand, watch the Zhengdian Atom video in detail to see how to query the structure.
Then edit it a bitmain.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 above code will make the LED blink once every 1 second. Here vTaskDelay(500 / portTICK_PERIOD_MS);Actually, it can be simplified tovTaskDelay(500);, because we previously configuredconfigTICK_RATE_HZto 1000, this will cause the value ofportTICK_PERIOD_MSto be 1. However, the above writing style is more formal; if others want to use your code, they will not cause the delay unit to run incorrectly. It is still recommended not to simplify it, whereas ALIENTEK uses the simplified version; if others migrate and copy your code, big problems will occur.
Press the same three keys in order, and then you can see the light on the board blinking.

6.4.KEY in practice
The following is a KEY circuit; when the KEY is pressed, it becomes conductive.

In reality, pressing a key causes bouncing, which is generally caused by mechanical materials, structures, or the environment.

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

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

Actual wiring

First, modifyBSPin the folderCMakeLists.txt:
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 on. Since my board does not have a custom button, I chose to use the button ofBOOTas the button.

As shown in the figure above, one side of this button is connected toGNDthe other side is connected toGPIO0, so initiallyGPIO0this side must be in a high-level state; when the button is pressed, the entire line will be pulledGNDto a low level, soGPIO0when a transition from high level to low level is detected, this is called detecting a falling edge; conversely, it is called a rising edge.
How do we makeGPIO0it high level right at power-up? That is to makeGPIO0a pull-up resistor must be connected.
Conversely, ifBOOTthe left side isVCC, thenGPIO0a pull-down resistor is required, and the rising edge must be detected.
First, createkey.hcontent
#ifndef __KEY_H_
#define __KEY_H_
//包含ESP32的gpio组件的头文件
#include "driver/gpio.h"
#define BOOT_PRES 1
#endif
Here,BOOT_PRESis an identifier; when the BOOT button is pressed, this value, that is,BOOT_PRESwill be detected,1.
Then inkey.cFirst, initialize thegpio0of thegpio.
Similar toledBut note that the mode here is input mode, and then 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
}
Next comes the main part, the key scanning function. The comments below explain it in detail, so you can carefully analyze the code yourself.
/**
* @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; /* 返回键值 */
}
The main thing used isint gpio_get_level(gpio_num_t gpio_num)such a function; we discussed this API 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, a global variable also needs to be defined as the key variable to store the key result.
uint8_t key_value = 0;
Write the main logic inmain.c; that is, when the key is pressed, toggle the level.
//包含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 thekey.handkey.cFull content:
#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
No errors after compilation


You can test it: when the button is pressed, the LED will toggle its level.
There is another testing method: you can use debug to monitorkey_valuevalue, but it requires that you change the key scan mode to support long press. In the above code, you only need to modifykey_value = key_scan(1);this line.
You can enter debug mode and give it a try. Set a breakpoint at the position shown in the image below. Then, when you hold down the key and click Continue, its watch value will become 1. When you release it and click Continue, it will become 0. 

6.5.External interrupt
6.5.1.Interrupt Introduction
In the previous section, we kept running the key scanning function in the infinite loop insideapp_main, and this approach is actually called轮询, and this type of function is called阻塞式函数. If you have studied STM32, you will surely have a deep understanding of what阻塞式, and what中断式.


What exactly is the process of interrupt execution like? Please see the introduction in the link below. Just read the interrupt execution process; the rest is all STM32 stuff and does not need to be read.
Introduction to Interrupt Service Routines
6.5.2.Introduction to External Interrupts
From the link above, you will understand that external interrupt EXIT is just one type of interrupt IT. In this section, we will explain external interrupts in detail.

The ESP32's EXIT has one more level-triggered mode than the STM32.
Level-triggered: high-level and low-level triggering, requiring the interrupt's level state to be held 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 ESP32S3's external interrupt function can capture the triggering of external events in a highly precise manner. Developers can adapt to different external events by configuring the interrupt trigger mode (such as rising edge, falling edge, any level, low-level hold, high-level hold, etc.), and when an event occurs, immediately interrupt the execution of the current program and switch to executing the interrupt service function.
6.5.3.Interrupt Priority
When multiple interrupts are triggered simultaneously, the CPU also executes interrupts in a certain order: interrupts with higher priority are executed first, followed by interrupts with lower priority.
ESP32-S3 Supports six interrupt levels and also supports interrupt nesting, meaning a lower-priority interrupt can be preempted by a higher-priority interrupt. As shown in the priority column in the following table,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.

在 ESP32S3 中,中断系统被用于响应各种内部和外部事件。这些中断按照其触发方式和优 先级进行分类。上表详细列出了 ESP32S3 的中断号、类别、种类以及相应的优先级。通过配置 这些中断,开发者可以实现对各种事件的及时响应和处理,提高系统的效率和实时性。
- Interrupt Number: A unique identifier for each interrupt, used to reference and configure a specific interrupt in a program.
- Category: The source type of the interrupt, divided into external interrupts and internal interrupts. External interrupts are triggered by external devices or signals, such as buttons, sensors, etc.; internal interrupts are triggered by hardware events inside the microcontroller, such as timer overflow, software interrupts, etc.
- Type: The trigger mode of the interrupt, including level triggering and edge triggering. Level triggering triggers an interrupt when the input signal reaches a specific level (such as high level or low level); edge triggering triggers an interrupt when the input signal changes from one level state to another.
- Priority: The response priority of the interrupt. When multiple interrupts occur simultaneously, the microcontroller determines which interrupt to handle first based on the interrupt priority. A higher priority means the interrupt will be handled first. During development, developers can configure parameters such as the interrupt trigger mode and priority according to actual requirements to implement an efficient and reliable event handling mechanism.
6.5.4.External Interrupt in Practice
