第 4 節

Tutorial

0瀏覽次數0訪問次數--跳出率--平均停留

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

alt text

Copy here esp-idf/examples/get-started/sample_projectalt text

Create a new project

Click on New Projectsalt text

alt text

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.) alt text

alt text

Created successfully

alt text

alt text

  1. Configure the main frequency Search for CPU, find the CPU frequency setting, and set the CPU frequency to 240MHz (maximum). alt text
  2. 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.

alt text

Search for PSRAM and check the box.

alt text

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

alt text

alt text

alt text

  1. Configure FreeRTOS

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

  1. Configure the partition table Search for partition, find partition table, and configure it as 自定义分区表CSV. alt text
  2. Save alt text

Old backups can be deleted. alt text

  1. Edit the partition table Press ctrl shift P together, type Partition Table, and find Open Partition Table Eidtors UI.

alt text

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

You can see that everything has been generated. alt text

  1. Compile and test

alt text

As shown in the figure, the compilation was successful.

alt text

Introduction to Partition Tables

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

alt text

alt text

alt text

alt text

Custom project architecture and adding components

Introduces the engineering architecture.

Here is the official project structure from Espressif:

alt text

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.

alt text

Create the project architecture.

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

alt text

Open the newly created folder with VSCode.

cd ~/UserFolder/MySource/ESP32_Projects/N01_LED
code .

Create the following files

alt text

alt text

Open the top-level CMakeLists

alt text

# 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.

alt text

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. alt text

Successfully compiled

alt text

Add component

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

Select the model alt text

For example, to install OpenAI, search for OpenAI.

alt text

Click Install.

alt text

Installation successful

alt text

The image below shows a successful installation. alt text

Clean and rebuild to see. alt text

Successfully compiled

alt text

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

  1. Find device
    1. Windows If you are on Windows, right-click 此电脑, then click 管理.

    alt text

You can see that it has been detected normally below.

![alt text](https://cdn.tungchiahui.cn/tungwebsite/assets/images/2026/01/07/1768375466417.webp)

2. Linux

If you are using Linux, open the terminal.

ls /dev | grep USB
# 或者
 ls /dev | grep ACM
```
![alt text](https://cdn.tungchiahui.cn/tungwebsite/assets/images/2026/01/07/1768449184448.webp)

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 `烧录`.

![alt text](https://cdn.tungchiahui.cn/tungwebsite/assets/images/2026/01/07/1768375800452.webp)

Then select yes.

![alt text](https://cdn.tungchiahui.cn/tungwebsite/assets/images/2026/01/07/1768375960461.webp)


As shown, the flashing is complete.

![alt text](https://cdn.tungchiahui.cn/tungwebsite/assets/images/2026/01/07/1768376077047.webp)

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 `清理`, `构建`, `烧录`.

![alt text](https://cdn.tungchiahui.cn/tungwebsite/assets/images/2026/01/07/1768449239886.webp)

![alt text](https://cdn.tungchiahui.cn/tungwebsite/assets/images/2026/01/07/1768449318480.webp)

Select yes here.
![alt text](https://cdn.tungchiahui.cn/tungwebsite/assets/images/2026/01/07/1768449382096.webp)

The image below shows a successful flash (if you are unable to download normally, please refer to `常见问题` below).
![alt text](https://cdn.tungchiahui.cn/tungwebsite/assets/images/2026/01/07/1768539121456.webp)

#### 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. alt text

This means you've successfully entered debug mode. alt text

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.

alt text

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

alt text

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 ialt text

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

alt text

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

alt text

Frequently Asked Questions
  1. 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

alt text

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. alt text

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

alt text

alt text

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.

alt text

alt text

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).

alt text

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.

alt text

Here is the ESP32's IO:

alt text

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.

  1. 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.

  1. 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;
  1. 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: alt text SMD LED alt text

In the circuit below, the LED only lights up when the IO port is at a high level. alt text In the circuit below, the LED only lights up when the IO port is at a low level. alt text 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.

alt text

KEY Practical

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

alt text

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

alt text

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

alt text

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

alt text

Actual wiring

alt text

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.

alt text

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.

alt text

alt text

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. alt text

alt text

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.

alt text

alt text

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 Functions
Introduction 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.

alt text

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.

alt text

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.

  1. Interrupt number: The unique identifier for each interrupt, used to reference and configure a specific interrupt in a program.
  2. 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.
  3. 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.
  4. 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

alt text

音乐页