ESP32教程

2026-01-07

简介

ESP32环境搭建

Linux

打开下面的网站, https://docs.espressif.com/projects/esp-idf/zh_CN/stable/esp32/get-started/linux-macos-setup.html

安装依赖

alt text

第一步按照这个网站所示,如果你是Ubuntu

sudo apt-get install git wget flex bison gperf python3 python3-pip python3-venv cmake ninja-build ccache libffi-dev libssl-dev dfu-util libusb-1.0-0

如果你是Fedora

sudo dnf -y makecache && sudo dnf install git wget flex bison gperf python3 python3-setuptools cmake ninja-build ccache dfu-util libusbx

alt text

获取 ESP-IDF

alt text

看看目前最新稳定版是哪个版本

alt text

如上图所示,为5.5.2版本. 接下来,你选择一个你要存放固件的文件夹, 比如我要放在/home/tungchiahui/UserFolder/Applications/文件夹下(其中/home/tungchiahui可用~/代替) 那么

cd ~/UserFolder/Applications
mkdir -p ./esp
cd ./esp
# 记得版本号要改成最新稳定版(网络环境一定要好)
git clone -b v5.5.2 --recursive https://github.com/espressif/esp-idf.git

alt text

如图才是成功,不是下面这样的都是没下载成功的 alt text

设置工具

alt text

cd ./esp-idf
# 选择国内服务器
export IDF_GITHUB_ASSETS="dl.espressif.cn/github_assets"
./install.sh all

alt text

alt text

上图就是成功的样子,这里要复制一下我红色圈起来的东西(export.sh的路径),下一个环境配置中要使用,比如我这里复制下来的路径是 ~/UserFolder/Applications/esp/esp-idf/export.sh

设置环境变量

此时,刚刚安装的工具尚未添加至 PATH 环境变量,无法通过“命令窗口”使用这些工具。因此,必须设置一些环境变量。这可以通过 ESP-IDF 提供的另一个脚本进行设置。

vim ~/.bashrc

添加下面这句(这里要填你具体存放的路径,也就是刚才复制的那串路径,记得把~改成$HOME,增加健壮性)

alias get_idf='. $HOME/UserFolder/Applications/esp/esp-idf/export.sh'

alt text

source ~/.bashrc

alt text

配置VScode

https://docs.espressif.com/projects/vscode-esp-idf-extension/zh_CN/latest/index.html

  1. 安装ESP-IDF插件

alt text

  1. 点击 Express 并选择下载服务器:

按下图的选,因为我们刚才配置过环境了,这里不要再选择版本了,直接去选择从我的电脑里找到ESP-IDF

alt text

他会自动帮你补全所有工具链

alt text

右下角这么显示则为成功

alt text

alt text

紧接着要配置 openOCD : alt text 复制上面这行,并开终端(任意终端都行,不用管路径)运行

sudo cp --update=none /home/tungchiahui/.espressif/tools/openocd-esp32/v0.12.0-esp32-20250707/openocd-esp32/share/openocd/contrib/60-openocd.rules /etc/udev/rules.d

alt text

结束!

Windows

打开下面的网站, https://docs.espressif.com/projects/esp-idf/zh_CN/stable/esp32/get-started/windows-setup.html

下载安装程序

查看当前最新版,比如我这里最新版是v5.5.2 alt text

点击工具下载 alt text

并点击最新版的安装,比如我这里最新版是v5.5.2

alt text

安装ESP-IDF

点击下载好的esp-idf alt text

alt text

alt text

选择你想安装的硬盘分区,比如我要安装在C:\Espressif,那就是如下图这样。 alt text 如果你想安装在D盘,就把第一个C改成D。 alt text

把这些没勾的全部勾上,点安装 alt text

然后点完成 alt text

弹出来的框都这么显示,则是成功安装了 alt text

配置环境

随便打开一个文件夹,右键This PC(此电脑),点属性alt text

这里有个高级系统设置 alt text

点环境变量 alt text

点击新建 alt text

alt text

找到刚才安装idf的目录里的这个目录点确定

alt text

alt text

配置VScode

安装下面这个插件

alt text

安装完毕后,点击打开开始向导

alt text

点击第一个 alt text

按我这么来,然后点安装 alt text

这个界面就是安装成功 alt text

下载串口驱动

打开下面这个网站 https://docs.espressif.com/projects/esp-idf/zh_CN/stable/esp32/get-started/establish-serial-connection.html

  1. CP210x驱动

alt text

alt text

解压刚才下载的压缩包 alt text

右键silabser.inf点击安装alt text 安装完毕 alt text

  1. FTDI驱动

alt text

alt text

同样解压下载好的压缩包

alt text

分别右键下面这俩,点安装

alt text

alt text

  1. CH340驱动 打开下面的网站 https://www.wch.cn/products/ch340.html

alt text

alt text

alt text

alt text

参考视频

https://www.bilibili.com/video/BV1EPisBWEUX

教程

下面教程我以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

--- END ---