教程
下面教程我以ESP32S3和Fedora Linux爲例.
本教程特點是,雖然大部分基於正點原子教程,但是正點原子教程用了非常多的宏定義,這並不適合新手進行閱讀,本教程會盡量少用宏定義,儘量能夠把最原本的代碼教給你.
基礎工程創建
準備工作
(一些老版本必須移動這個文件夾)
複製一下這個文件夾esp-idf/tools/templates/sample_project

複製到這裏esp-idf/examples/get-started/sample_project
創建新工程
點擊New Projects

下面這倆選哪個都行,一些老版本沒有最底下那個選項.(正常最新版選最底下那個就行)


創建成功

menuconfig配置

- 配置主頻
搜索
CPU,找到CPU主頻的設置,設置CPU主頻爲240MHz(最大)
- 配置Flash和RAM
搜Flash,設置Flash SPI mode爲QIO,這種模式下速度最快.
查看淘寶自己買的ESP32S3型號,ESP32-S3 N16R8得知我的Flash爲16M,RAM爲8M

搜PSRAM並打勾

查看官方給的PSRAM介紹,我們應該選Octal SPI



- 配置FreeRTOS
將configTICK_RATE_HZ配置爲1000,這樣週期爲1ms,vTaskDelay()函數的單位也就變成了ms.

- 配置分區表
搜
partition,找到partition table,並配置爲自定义分区表CSV
- 保存

舊的備份可以刪掉

- 編輯分區表
ctrl shift P一起摁,輸入
Partition Table,找到Open Partition Table Eidtors UI

按照下面一點都別抄錯的抄下來

可以看到都生成完畢了

- 編譯測試

如圖則爲編譯成功

分區表簡介
分區表作用是將Flash分爲多個存儲區域,記錄每個區域的特定功能和用途.




自定義工程架構及添加組件
介紹工程架構
以下是樂鑫官方的工程結構:

這種明顯是很雜糅的
以下是正點原子的工程結構,更加模塊化,擴展性更靈活,分層更清晰.

創建工程架構
複製basic工程,並粘貼到你存放代碼的文件夾,然後重命名爲N01_LED.

用VScode打開新創建的文件夾
cd ~/UserFolder/MySource/ESP32_Projects/N01_LED
code .
創建以下文件


打開頂層CMakeLists

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

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)
清理再編譯看看

成功編譯

添加組件
ctrl shift P搜索esp component找到esp component registry
選擇型號

例如要安裝OpenAI,則搜索OpenAI

點安裝

安裝成功

下圖所示就是安裝成功

清理再編譯看看

成功編譯

注意,在main.c裏可以直接調用添加的組件,但在components文件夾下的文件裏,不可以直接用,要修改CMakeLists.txt
ESP32的下載與調試
下載
先寫一個程序,在main.c裏寫個helloworld.
#include <stdio.h>
void app_main(void)
{
printf("Hello World!\n");
}
你用一根USB-A轉USB-C的數據線,一頭在電腦上,一頭插到開發板的USB接口,而不是UART接口.
如圖
- 查找設備
- Windows
如果你是windows,則右鍵
此电脑,點管理

看到下面正常檢測出來了
- Linux 如果你是linux,則打開終端
ls /dev | grep USB # 或者 ls /dev | grep ACM
看是否有設備被檢測出來.如上圖,我的爲/dev/ttyACM0. - Windows
如果你是windows,則右鍵
- 下載
- Windows
如果你是Windows,則要先選模式爲
JTAG,端口爲檢測出來的端口COM4,芯片型號爲esp32s3,然後點擊清理,构建,烧录.

然後選擇yes
如圖燒錄好了
- Linux
如果你是Linux,則要先選模式爲
JTAG,端口爲檢測出來的端口/dev/ttyACM0,芯片型號爲esp32s3,然後點擊清理,构建,烧录.


這裏選yes
如下圖就是燒錄成功了(如果你不能正常下載,請往下面找常见问题)
- Windows
如果你是Windows,則要先選模式爲
調試
先寫一個程序
//包含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
}
}
直接點調試,

這樣就是正常進入debug了

下圖, 第一個是開始運行. 第二個是逐過程,一個函數一個函數的運行. 第三個是單步調試,他會進入函數內部執行. 第四個是單步跳出,會跳出這個函數. 第五個是重啓程序,但在esp32裏有bug,貌似不會重啓,不知道後續是否會修. 第六個是斷開鏈接,退出調試.

也可以打斷點,這樣程序運行到斷點處就不會接着運行了. 如圖

然後也可以右鍵一個變量,把他添加到Watch,在程序某行打斷點,來看程序運行到這一行前(注意,這裏是在哪行打斷點,就是剛運行到哪行,那一個還沒有運行呢),這個變量的值爲多少.
如圖
比如右鍵這個i
點擊這個添加到监视(Add to Watch)

你每次开始运行(F5)到斷點處,這個i都會加1.

常見問題
- 如果你是linux且遇到下面的這個問題
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
多半是因爲權限問題.
lsusb

找到這個後面是1001的這個設備,記住它前面這個設備id,比如我的是303a
接下來,編輯udev
sudo vim /etc/udev/rules.d/99-esp32-usb-jtag.rules
輸入以下內容(注意,這裏的idVendor要填上你對應的)
SUBSYSTEM=="usb", ATTR{idVendor}=="303a", MODE="0666"
保存後,先敲下面的命令
sudo udevadm control --reload-rules
sudo udevadm trigger
然後插拔USB,再燒錄就成功了.
ESP32-S3的時鐘樹
時鐘
時鐘是一個週期性翻轉的信號

每來一次時鐘邊沿,整個電路就會完整一次狀態的更新, 這樣,整個系統就會往前一步.
時鐘樹


OSC是高速晶振,CLK是時鐘,PLL是鎖相環,圖裏的DIV是分頻器,MUX是選擇器. OSC要接晶振. CLK是可用的時鐘信號. PLL爲了被倍頻或者分頻的時鐘頻率. DIV爲了省電,節約資源. MUX是選擇哪一個時鐘源.


GPIO
GPIO理論
實際上就是通用輸入輸出端口的意思,可以輸出高低電平,也可以讀取高低電平.
輸入的原理,就是如下圖,如果KEY按下會讓電路電平變爲低,那就要上拉電阻,用下降沿來判斷按鍵是否按下. 由於上拉電阻,所以剛開始KEY沒按下的時候下方電路的IO口那邊是高電平,一旦KEY按下,將會變成低電平,這樣就會有一個下降沿(電平由高變低).

輸出,如下圖,當IO爲高電平的時候,LED發光二極管導通,然後LED亮,當IO爲低電平的時候,LED發光二極管不導通,LED滅.

以下是ESP32的IO:

他們是高度複用的,每一個都可以複用成其他外設的接口.
但有些特例,有些引腳不可以當輸出輸入,他們只能用來連接模塊上的FLASH或PSRAM.
可以看正點原子的esp32-s3_datasheet_cn.pdf裏的詳細說明.
GPIO相關函數
- gpio_config
這個是GPIO初始化函數
esp_err_t gpio_config(const gpio_config_t *pGPIOConfig)
然後你右鍵選中gpio_config_t,點go to definition,就會出現下面這個是GPIO初始化函數里的入口參數的結構體
/**
* @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號.
mode是配置輸入還是輸出的模式,你可以go to definition看看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;
其他的也一樣,你都可以go to definition來看這個結構體到底能填什麼.
pull_up_en是是否使能上拉電阻,就是講IO那條電路再並聯一條上面有一個電阻和VCC的電路.pull_down_en是是否使能下拉電阻,就是講IO那條電路再並聯一條上面有一個電阻和GND的電路.
intr_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是選擇哪一個IO口,可以go to definition看一看,如下面的代碼可知,他可以填GPIO_NUM_x,這裏的x就是選擇初始化IOx(也就是初始化哪一個引腳IO)
level是輸出的電平是高還是低,填0就是低,填1就是高.
/**
* @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是選擇哪一個IO口,可以go to definition看一看,如下面的代碼可知,他可以填GPIO_NUM_x,這裏的x就是選擇初始化IOx(也就是初始化哪一個引腳IO)
return返回的int的數據是讀取到的電平是高還是低,0就是低,1就是高.
LED燈實戰
兩種LED實物:
貼片LED

下面這個電路中IO口端是高電平的時候LED才亮.
下面這個電路中IO口端是低電平的時候LED才亮.
上面這倆缺點是電流直接走MCU,有另一種三極管的方法更加好,可以自己搜搜,或者看一下大疆STM32C板(STM32F407IGH6)的原理圖.
先來編寫led.h的代碼,這個是最小的框架,你應該學過C語言的條件編譯,這是爲了讓頭文件不會重複.
#ifndef __LED_H_
#define __LED_H_
#endif
接下來繼續完善led.h,下面這個枚舉是爲了讓代碼可讀性更高,給高低電平起了個名字,PIN_RESET爲低電平,PIN_SET爲高電平.
#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
接下來再編寫led.c的內容:
由於看我自己板子的原理圖,我的板子的IO43是LED燈的控制引腳,所以下面我初始化IO43.(我這裏只有IO43,IO44接了個普通燈,沒辦法,只能先把menuconfig裏的Channel for console output換成none,而你的燈不用改這個)
#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
}
上面這些參數你該填什麼,都可以通過go to definition查詢結構體類型來得知.
不懂的詳細看看正點原子視頻怎麼查詢結構體.
然後再編輯一下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
}
}
以上代碼會讓LED每隔1s閃爍一次.
這裏的
vTaskDelay(500 / portTICK_PERIOD_MS);其實可以簡化爲vTaskDelay(500);,因爲咱們之前配置過configTICK_RATE_HZ爲1000,這樣會導致portTICK_PERIOD_MS的值爲1.
但上面的寫法更加正規,如果其他人想用你的代碼,別人也不會讓延遲跑錯單位.還是推薦不要簡化的辦法,而正點原子是簡化後的,如果別人遷移複製你的代碼,會出大問題的.
老三樣,依次摁,然後可以看到板子上的燈會閃爍.

KEY實戰
如下是一個KEY的電路,KEY被按下就導通了.

實際上按鍵按下會導致抖動,一般是機械材料,結構,環境導致.

但是中間這段是正常的,如下圖

日常用軟件消抖的延時法就夠了.

實際接線

首先先修改BSP文件夾裏的CMakeLists.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)
我們打開原理圖,看看按鍵是哪個引腳?,
由於我的板子沒有自定義的按鍵,所以我選擇用BOOT的按鍵當按鍵.

如上圖,這個按鍵一邊接的GND一邊接的GPIO0,所以剛開始GPIO0這邊必須是一個高電平狀態,等按鍵按下,整條線都會被GND變成低電平,故GPIO0檢測到電平由高電平變低電平,這種叫做檢測到下降沿,反之叫上升沿.
我們如何讓GPIO0剛上來是高電平呢?就是要讓GPIO0要接上拉電阻.
反之,如果BOOT左邊是VCC,那GPIO0就要下拉電阻,並檢測上升沿.
首先先創建key.h的內容
#ifndef __KEY_H_
#define __KEY_H_
//包含ESP32的gpio组件的头文件
#include "driver/gpio.h"
#define BOOT_PRES 1
#endif
這裏的BOOT_PRES是一個標識,當BOOT按鍵被按下後,就會檢測到BOOT_PRES這個值,也就是1.
然後在key.c中,首先要初始化gpio0的gpio.
與led的差不多.但是注意,這裏的模式爲輸入模式,然後要啓用上拉電阻.
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; /* 返回键值 */
}
主要是用了int gpio_get_level(gpio_num_t gpio_num)這麼一個函數,在第一節裏我們講過這個api了.
int gpio_get_level(gpio_num_t gpio_num)
{
return gpio_hal_get_level(gpio_context.gpio_hal, gpio_num);
}
然後還要在定義一個全局變量當作按鍵的變量,存儲按鍵的結果.
uint8_t key_value = 0;
在main.c裏寫主要邏輯,就是當按鍵被按下,則翻轉電平.
//包含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
}
}
下面是key.h和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
編譯後無報錯


可以測試下,當按下按鍵的時候,LED燈會翻轉電平.
還有一個測試方法,可以用debug來監視key_value的值,但要求你把按鍵掃描模式改成支持長按,上面的代碼只需要改key_value = key_scan(1);這一行.
你可以進入debug測試一下,
在下圖打斷點,然後當你摁住按鍵點繼續運行的時候,他的監視值會變成1,當你鬆開的時候按繼續運行,會變成0.


外部中斷
中斷簡介
在上一節裏,我們在app_main裏的死循環裏一直跑按鍵掃描函數的方式其實叫做轮询,這類函數叫做阻塞式函数,如果你學過STM32,你肯定深有體會什麼是阻塞式,什麼是中断式.


中斷運行的過程到底是什麼樣的,請看下面這個鏈接裏的介紹,看完中斷運行過程即可,其他的都是STM32的東西不用看.
中斷服務函數的介紹外部中斷簡介
由上面這個鏈接你會明白,外部中斷EXIT只是中斷IT裏的其中一種,我們這一節詳細講講外部中斷.

ESP32的EXIT比STM32要多一個電平觸發的模式.
電平觸發:高、低電平觸發,要求保持中斷的電平狀態直到 CPU 響應。
邊沿觸發:上升沿和下降沿觸發,這類型的中斷一旦觸發,CPU 即可響應。
ESP32S3 的外部中斷功能能夠以非常精確的方式捕捉外部事件的觸發。開發者可以通過配 置中斷觸發方式(如上升沿、下降沿、任意電平、低電平保持、高電平保持等)來適應不同的 外部事件,並在事件發生時立即中斷當前程序的執行,轉而執行中斷服務函數。
中斷優先級
當多箇中斷同時觸發的時候,CPU執行中斷也是有順序的,先執行優先級高的中斷,再執行優先級低的中斷.
ESP32-S3 支持六級中斷,同時支持中斷嵌套,也就是優先級中斷可以被高優先級中斷打斷。 如下表中的優先級一欄,數字越大表明該中斷的優先級越高。其中,NMI 中斷擁有最高 優先級,此類中斷已經觸發,CPU 必須處理。

在 ESP32S3 中,中斷系統被用於響應各種內部和外部事件。這些中斷按照其觸發方式和優 先級進行分類。上表詳細列出了 ESP32S3 的中斷號、類別、種類以及相應的優先級。通過配置 這些中斷,開發者可以實現對各種事件的及時響應和處理,提高系統的效率和實時性。
- 中斷號:每個中斷的唯一標識符,用於在程序中引用和配置特定的中斷。
- 類別:中斷的來源類型,分爲外部中斷和內部中斷。外部中斷由外部設備或信號觸發, 如按鍵、傳感器等;內部中斷則由微控制器內部的硬件事件觸發,如定時器溢出、軟件中斷等。
- 種類:中斷的觸發方式,包括電平觸發和邊沿觸發。電平觸發是在輸入信號達到特定 電平(如高電平或低電平)時觸發中斷;邊沿觸發則是在輸入信號從一種電平狀態變化到另一 種狀態時觸發中斷。
- 優先級:中斷的響應優先級。當多箇中斷同時發生時,微控制器會根據中斷的優先級 來決定先處理哪個中斷。較高的優先級意味着中斷將優先得到處理。 在開發過程中,開發者可以根據實際需求配置中斷的觸發方式、優先級等參數,以實現高 效、可靠的事件處理機制。
外部中斷實戰
