第 4 節

教程

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

下面教程我以ESP32S3和Fedora Linux爲例.

本教程特點是,雖然大部分基於正點原子教程,但是正點原子教程用了非常多的宏定義,這並不適合新手進行閱讀,本教程會盡量少用宏定義,儘量能夠把最原本的代碼教給你.

基礎工程創建

準備工作

(一些老版本必須移動這個文件夾) 複製一下這個文件夾esp-idf/tools/templates/sample_project

alt text

複製到這裏esp-idf/examples/get-started/sample_projectalt text

創建新工程

點擊New Projectsalt text

alt text

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

alt text

創建成功

alt text

alt text

  1. 配置主頻 搜索CPU,找到CPU主頻的設置,設置CPU主頻爲240MHz(最大) alt text
  2. 配置Flash和RAM

Flash,設置Flash SPI mode爲QIO,這種模式下速度最快.

查看淘寶自己買的ESP32S3型號,ESP32-S3 N16R8得知我的Flash爲16M,RAM爲8M

alt text

PSRAM並打勾

alt text

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

alt text

alt text

alt text

  1. 配置FreeRTOS

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

  1. 配置分區表 搜partition,找到partition table,並配置爲自定义分区表CSValt text
  2. 保存 alt text

舊的備份可以刪掉 alt text

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

alt text

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

可以看到都生成完畢了 alt text

  1. 編譯測試

alt text

如圖則爲編譯成功

alt text

分區表簡介

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

alt text

alt text

alt text

alt text

自定義工程架構及添加組件

介紹工程架構

以下是樂鑫官方的工程結構:

alt text

這種明顯是很雜糅的

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

alt text

創建工程架構

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

alt text

用VScode打開新創建的文件夾

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

創建以下文件

alt text

alt text

打開頂層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)

修改BSP裏的CMakeLists

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)

清理再編譯看看 alt text

成功編譯

alt text

添加組件

ctrl shift P搜索esp component找到esp component registryalt text

選擇型號 alt text

例如要安裝OpenAI,則搜索OpenAI

alt text

點安裝

alt text

安裝成功

alt text

下圖所示就是安裝成功 alt text

清理再編譯看看 alt text

成功編譯

alt text

注意,在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接口.

如圖

  1. 查找設備
    1. Windows 如果你是windows,則右鍵此电脑,點管理

    alt text
    看到下面正常檢測出來了
    alt text
    1. Linux 如果你是linux,則打開終端
    ls /dev | grep USB
    # 或者
     ls /dev | grep ACM
    

    alt text
    看是否有設備被檢測出來.如上圖,我的爲/dev/ttyACM0.
  2. 下載
    1. Windows 如果你是Windows,則要先選模式爲JTAG,端口爲檢測出來的端口COM4,芯片型號爲esp32s3,然後點擊清理,构建,烧录.

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

    alt text
    alt text
    這裏選yes alt text
    如下圖就是燒錄成功了(如果你不能正常下載,請往下面找常见问题) alt text

調試

先寫一個程序

//包含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
    }
}

直接點調試, alt text

這樣就是正常進入debug了 alt text

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

alt text

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

alt text

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

點擊這個添加到监视(Add to Watch)

alt text

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

alt text

常見問題
  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

alt text

找到這個後面是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的時鐘樹

時鐘

時鐘是一個週期性翻轉的信號 alt text

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

時鐘樹

alt text

alt text

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

alt text

alt text

GPIO

GPIO理論

實際上就是通用輸入輸出端口的意思,可以輸出高低電平,也可以讀取高低電平.

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

alt text

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

alt text

以下是ESP32的IO:

alt text

他們是高度複用的,每一個都可以複用成其他外設的接口.

但有些特例,有些引腳不可以當輸出輸入,他們只能用來連接模塊上的FLASH或PSRAM. 可以看正點原子的esp32-s3_datasheet_cn.pdf裏的詳細說明.

GPIO相關函數

  1. 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是是否啓用中斷類型.

  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是選擇哪一個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;
  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是選擇哪一個IO口,可以go to definition看一看,如下面的代碼可知,他可以填GPIO_NUM_x,這裏的x就是選擇初始化IOx(也就是初始化哪一個引腳IO)

return返回的int的數據是讀取到的電平是高還是低,0就是低,1就是高.

LED燈實戰

兩種LED實物: alt text 貼片LED alt text

下面這個電路中IO口端是高電平的時候LED才亮. alt text 下面這個電路中IO口端是低電平的時候LED才亮. alt text 上面這倆缺點是電流直接走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.
但上面的寫法更加正規,如果其他人想用你的代碼,別人也不會讓延遲跑錯單位.還是推薦不要簡化的辦法,而正點原子是簡化後的,如果別人遷移複製你的代碼,會出大問題的.

老三樣,依次摁,然後可以看到板子上的燈會閃爍.

alt text

KEY實戰

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

alt text

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

alt text

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

alt text

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

alt text

實際接線

alt text

首先先修改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的按鍵當按鍵.

alt text

如上圖,這個按鍵一邊接的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中,首先要初始化gpio0gpio.

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

編譯後無報錯

alt text

alt text

可以測試下,當按下按鍵的時候,LED燈會翻轉電平.

還有一個測試方法,可以用debug來監視key_value的值,但要求你把按鍵掃描模式改成支持長按,上面的代碼只需要改key_value = key_scan(1);這一行.

你可以進入debug測試一下, 在下圖打斷點,然後當你摁住按鍵點繼續運行的時候,他的監視值會變成1,當你鬆開的時候按繼續運行,會變成0. alt text

alt text

外部中斷

中斷簡介

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

alt text

alt text

中斷運行的過程到底是什麼樣的,請看下面這個鏈接裏的介紹,看完中斷運行過程即可,其他的都是STM32的東西不用看.

中斷服務函數的介紹
外部中斷簡介

由上面這個鏈接你會明白,外部中斷EXIT只是中斷IT裏的其中一種,我們這一節詳細講講外部中斷.

alt text

ESP32的EXIT比STM32要多一個電平觸發的模式.

電平觸發:高、低電平觸發,要求保持中斷的電平狀態直到 CPU 響應。
邊沿觸發:上升沿和下降沿觸發,這類型的中斷一旦觸發,CPU 即可響應。

ESP32S3 的外部中斷功能能夠以非常精確的方式捕捉外部事件的觸發。開發者可以通過配 置中斷觸發方式(如上升沿、下降沿、任意電平、低電平保持、高電平保持等)來適應不同的 外部事件,並在事件發生時立即中斷當前程序的執行,轉而執行中斷服務函數。

中斷優先級

當多箇中斷同時觸發的時候,CPU執行中斷也是有順序的,先執行優先級高的中斷,再執行優先級低的中斷.

ESP32-S3 支持六級中斷,同時支持中斷嵌套,也就是優先級中斷可以被高優先級中斷打斷。 如下表中的優先級一欄,數字越大表明該中斷的優先級越高。其中,NMI 中斷擁有最高 優先級,此類中斷已經觸發,CPU 必須處理。

alt text

在 ESP32S3 中,中斷系統被用於響應各種內部和外部事件。這些中斷按照其觸發方式和優 先級進行分類。上表詳細列出了 ESP32S3 的中斷號、類別、種類以及相應的優先級。通過配置 這些中斷,開發者可以實現對各種事件的及時響應和處理,提高系統的效率和實時性。

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

alt text

音乐页