ESP32教程
简介
ESP32环境搭建
Linux
打开下面的网站, https://docs.espressif.com/projects/esp-idf/zh_CN/stable/esp32/get-started/linux-macos-setup.html
安装依赖

第一步按照这个网站所示,如果你是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

获取 ESP-IDF

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

如上图所示,为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

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

设置工具

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


上图就是成功的样子,这里要复制一下我红色圈起来的东西(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'

source ~/.bashrc

配置VScode
https://docs.espressif.com/projects/vscode-esp-idf-extension/zh_CN/latest/index.html
- 安装ESP-IDF插件

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

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

右下角这么显示则为成功


紧接着要配置 openOCD :
复制上面这行,并开终端(任意终端都行,不用管路径)运行
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

结束!
Windows
打开下面的网站, https://docs.espressif.com/projects/esp-idf/zh_CN/stable/esp32/get-started/windows-setup.html
下载安装程序
查看当前最新版,比如我这里最新版是v5.5.2

点击工具下载

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

安装ESP-IDF
点击下载好的esp-idf



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

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

然后点完成

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

配置环境
随便打开一个文件夹,右键This PC(此电脑),点属性
这里有个高级系统设置

点环境变量

点击新建


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


配置VScode
安装下面这个插件

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

点击第一个

按我这么来,然后点安装

这个界面就是安装成功

下载串口驱动
- CP210x驱动


解压刚才下载的压缩包

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

- FTDI驱动


同样解压下载好的压缩包

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


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




参考视频
https://www.bilibili.com/video/BV1EPisBWEUX
教程
下面教程我以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 的中断号、类别、种类以及相应的优先级。通过配置 这些中断,开发者可以实现对各种事件的及时响应和处理,提高系统的效率和实时性。
- 中断号:每个中断的唯一标识符,用于在程序中引用和配置特定的中断。
- 类别:中断的来源类型,分为外部中断和内部中断。外部中断由外部设备或信号触发, 如按键、传感器等;内部中断则由微控制器内部的硬件事件触发,如定时器溢出、软件中断等。
- 种类:中断的触发方式,包括电平触发和边沿触发。电平触发是在输入信号达到特定 电平(如高电平或低电平)时触发中断;边沿触发则是在输入信号从一种电平状态变化到另一 种状态时触发中断。
- 优先级:中断的响应优先级。当多个中断同时发生时,微控制器会根据中断的优先级 来决定先处理哪个中断。较高的优先级意味着中断将优先得到处理。 在开发过程中,开发者可以根据实际需求配置中断的触发方式、优先级等参数,以实现高 效、可靠的事件处理机制。
外部中断实战
