ESP32
基础
发展历程
ESP32
集成 2.4 GHz Wi-Fi 和蓝牙双模的单芯片方案,具有超高的射频性能、稳定性、通用性和可靠性,以及超低的功耗,满足不同的功耗需求,适用子各种应用场景。是ESP8266的后继产品,具有比ESP8266更出色的性能以及更优秀的能力
ESP32-S3
搭载 Xtensa 32位LX7双核处理器:主频高达240M,内置512KB SRAM,384KB ROM
- SRAM 随机存储器-掉电丢失?
- ROM 只读存储器
- PSRAM 内存?
- 特点
- WiFi + BLE5:2.4GHz WiFi
- 强大的AI运算能力:加速神经网络计算和信号处理等工作的向量指令
- 安全加密机制
开发环境
支持三种开发方式:ESP-IDF[推荐] Arduino MicorPython
ESP-IDF 【专业】【官方主推】
- 环境搭建教程
- 特点
- 支持C/C++语言,并提供一套完整的API,可控制ESP32各种功能和外设
- 配置灵活、性能更高、功能更多、适合商用
### 可以忽略以下内容,尝试直接使用VSCode插件完成!!!
# MAC OS
brew install cmake ninja dfu-util # 安装 CMake 和 Ninja 编译工具
brew install ccache # 安装 ccache 以获得更快的编译速度
python3 --version # 检查电脑上是否已经安装过 Python 3
brew install python3 # 安装 Python 3
git clone -b v5.4.1 --recursive https://github.com/espressif/esp-idf.git # 获取 ESP-IDF
./install.sh esp32s3 # 为目标芯片安装所需工具
./install.sh esp32,esp32s2 # 【一次性指定多个目标芯片】为目标芯片安装所需工具
./install.sh all # 一次性为所有支持的目标芯片安装工具
# 设置环境变量
# 不建议直接将 export.sh 添加到 shell 的配置文件
. $HOME/esp/esp-idf/export.sh # 在需要运行 ESP-IDF 的终端窗口运行
# 使用 ESP-IDF
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
Arduino【爱好】
- 基于C/C++的语言,让开发者更容易控制和编程ESP32
- 代码简单、入门容易、快速开发、社区资源多
- 编译下载耗时长、开发效率低
MicorPython【爱好】
- 精简的Python3语言,可运行在ESP32和其他一些微控制器
- 执行效率低
ESP-IDF
目录结构
- main # 必要,工程源码目录
- main.c # 必要,源码入口文件
- CMakeLists.txt # 用于指示CMake构建系统,对main目录进行构建
- CMakeLists.txt # 用于指示CMake构建系统,对整个项目进行构建
- components # 公共代码存放
- sdkconfig # 必要,工程组件配置文件
- build # 编译后,生成的bin和中间文件,自动生成
2
3
4
5
6
7
程序存储结构【了解】
- DRAM (数据RAM)
- 非常量静态数据(.data段)和零初始化数据(.bss段,一般是数值为0的全局变量)由链接器放入内部SRAM作为数据存储,也可配置放入外部RAM
- 例:static变量、初始化为0的全局变量
- "noinit" DRAM
- 未初始化的全局变量数据,由链接器放入内部 SRAM 作为数据存储,也可配置放入外部 RAM
- 在启动时不会被初始化
- IRAM ( 指令RAM )
- ESP-IDF将内部SRAMO (SRAM分为3个存储块SRAMO, SRAM1和SRAM2)的部分区域(SRAMO和SRAM1)分配为指令RAM中断处理程序,一般放入指令RAM中
- 例:中断处理程序、通过 IRAM_ATTR 宏在源代码也可中指定需放入 IRAM 的代码
- IROM(代码从flash中运行)
- 如果一个函数没有被显式地声明放在 IRAM 或者 RTC 存储器中,则它会放在 flash 中
- 例:一般程序的代码、函数
- DROM(数据存储在flash中):用于存放只读数据
- RTC Slow memory (RTC慢速存储器)
- RTC_NOINIT_ATTR 属性宏可以用来将数据放入 RTC Slow memory。
- 放入此类型存储器的值从深度睡眠模式中醒来后会保持值不变
- RTC FAST memory (RTC快速存储器)
- RTC FAST memory 的同一区域既可以作为指令存储器也可以作为数据存储器进行访问。
- 从深度睡眠模式唤醒后必须要运行的代码要放在RTC 存储器中
启动流程【了解】
- 一级引导程序,固化在ROM中,不可修改,加载二级引导程序至RAM中运行,检查IOO引脚,选择程序模式,当芯片上电检测到100引脚是低电平时,就会进入下载模式,否则就会继续执行二级引导程序。
- 二级引导程序,bootloader程序,从Ox8000处读取分区表,处理各种段,加载应用程序
- 应用程序,硬件外设和基本C语言运行环境的初始化,freertos初始化,最后执行app_main()函数
前置知识
portTICK_PERIOD_MS
portTICK_PERIOD_MS
是 FreeRTOS 实时操作系统中的一个核心宏定义,用于表示一个系统时钟节拍(tick)对应的毫秒数。它是连接 FreeRTOS 时间相关 API 与实际物理时间的关键桥梁,在 ESP32-S3 开发中频繁用于任务延时、超时设置等场景
- 本质:一个常量值,单位为 毫秒(ms),表示 FreeRTOS 系统时钟的最小时间单位(1 个 tick 的时长)。
- 作用:将 FreeRTOS API 中的 “节拍数” 转换为实际 “毫秒数”,或反之。例如:
- 当需要延时 100 毫秒时,需计算对应的节拍数:
100 / portTICK_PERIOD_MS
。 - 当 API 返回节拍数时,可通过
节拍数 * portTICK_PERIOD_MS
得到实际毫秒数。
- 当需要延时 100 毫秒时,需计算对应的节拍数:
- 在 ESP-IDF 框架(基于 FreeRTOS)中,
portTICK_PERIOD_MS
的默认值为 10 毫秒(ms)- 即
#define portTICK_PERIOD_MS 10
- 1 个 FreeRTOS 时钟节拍(tick) = 10 毫秒。
- 系统时钟频率为 100Hz(1 秒 = 100 个 tick,100 * 10ms = 1000ms)
portTICK_PERIOD_MS
由 FreeRTOS 的系统时钟频率决定,可通过修改configTICK_RATE_HZ
(在sdkconfig
中配置)间接调整
- 即
- 常见误区
- 误区 1:直接用毫秒数作为
vTaskDelay()
的参数(如vTaskDelay(100)
)。 错误原因:vTaskDelay()
的参数是 tick 数,而非毫秒数。若默认portTICK_PERIOD_MS=10
,vTaskDelay(100)
实际延时 100 * 10ms = 1000ms(1 秒),而非 100ms。 - 误区 2:认为
portTICK_PERIOD_MS
是固定值,不会变化。 实际:它由configTICK_RATE_HZ
决定,若修改配置,其值会变化。因此,必须用x / portTICK_PERIOD_MS
而非硬编码数值(如10
),确保代码兼容性。
- 误区 1:直接用毫秒数作为
pdMS_TO_TICKS()
pdMS_TO_TICKS(ms)
: 这是 FreeRTOS 提供的宏函数,专门用于将「毫秒」转换为「时钟节拍数(ticks)」,内部已经处理了与portTICK_PERIOD_MS
的计算关系。 公式等价于:(ms + portTICK_PERIOD_MS - 1) / portTICK_PERIOD_MS
(带向上取整,更精确)。ms / portTICK_PERIOD_MS
: 这是直接通过「毫秒数除以时钟节拍周期(毫秒 / 节拍)」来计算节拍数,是更原始的写法。 其中portTICK_PERIOD_MS
是一个宏,代表每个时钟节拍的毫秒数(ESP32 中默认是 10ms,即 1 个 tick = 10ms)。- 推荐 优先使用
pdMS_TO_TICKS(ms)
- 可读性更好:明确表达「将毫秒转换为 ticks」的意图,代码更易理解
- 兼容性更强:当
portTICK_PERIOD_MS
配置改变(例如从 10ms 改为 5ms)时,无需修改代码 - 计算更精确:内部带向上取整逻辑,确保延迟时间不小于预期的毫秒数(避免因整数除法截断导致的不足)
vTaskDelay(pdMS_TO_TICKS(1000)); // 延迟 1 秒,推荐写法
其他时间表述
/* ets_delay_us 推荐!!
特点:精度高(微秒级),适用于短时间精确延时(如硬件时序控制)
注意:此函数会阻塞当前任务,不宜在中断服务程序(ISR)中使用,也不适合长时间延时(可能影响系统调度)
- ESP-IDF 封装的 ROM 层延时函数,与 ets_delay_us() 本质上调用相同的底层实现
同样是阻塞式延时,功能和精度与 ets_delay_us() 一致,ESP-IDF v4.4及以上兼容性更好,推荐
*/
#include "esp_timer.h" // 包含必要的头文件
esp_rom_delay_us(20);
/*
- ESP 芯片 ROM 中实现的底层延时函数,属于早期常用的延时接口
精度较高,适用于短时间(微秒级)精确延时
会阻塞当前任务执行,直到延时结束
可在大多数场景使用,包括初始化阶段
*/
#include "esp_rom/ets_sys.h"
ets_delay_us(1); // 延时 1 微秒
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
ESP_LOG
ESP-IDF 中核心的日志输出系统,专为嵌入式设备设计,提供了分级控制、模块化标签、低资源消耗等特性
宏 | 级别 | 用途 | 颜色(终端) |
---|---|---|---|
ESP_LOGV | 详细 | 开发调试细节(默认关闭) | 灰色 |
ESP_LOGD | 调试 | 函数调用、变量值等调试信息 | 蓝色 |
ESP_LOGI | 信息 | 系统状态变化、关键流程 | 绿色 |
ESP_LOGW | 警告 | 非致命错误、潜在问题 | 黄色 |
ESP_LOGE | 错误 | 功能失效、无法恢复的错误 | 红色 |
#define TAG "wifi" // 模块标签
#include "esp_log.h"
#define TAG "TAG"
void connect_wifi(void) {
ESP_LOGI(TAG, "开始连接 Wi-Fi..."); // 信息日志
esp_err_t err = esp_wifi_connect();
if (err != ESP_OK) {
ESP_LOGE(TAG, "连接失败: %s", esp_err_to_name(err)); // 错误日志
return;
}
ESP_LOGD(TAG, "Wi-Fi 连接请求已发送"); // 调试日志
}
// 支持 printf 风格的格式化参数
int voltage = 3300;
ESP_LOGI(TAG, "电池电压: %d mV", voltage); // 输出: I (123) wifi: 电池电压: 3300 mV
// 日志自动包含时间戳(毫秒)和任务名称
// 输出格式: [级别] (时间戳) [标签]: 消息
I (1234) wifi: 连接成功
// 低功耗日志使用:进入 Light Sleep 前设置
esp_light_sleep_set_wakeup_cb(log_wakeup_callback);
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
优化日志
- 避免在高频中断或临界区使用日志,减少中断延迟
- 对性能敏感的代码段临时提升日志级别
- 日志体积控制:大数组或结构体日志使用
ESP_LOG_BUFFER_HEX
输出十六进制 - 条件日志:使用编译时条件减少日志开销
// 临时禁用 DEBUG 日志
#define ESP_LOGD(...) ((void)0)
// 性能敏感代码
#undef ESP_LOGD
// 日志体积控制
uint8_t data[16] = {0};
ESP_LOG_BUFFER_HEX(TAG, data, sizeof(data)); // 以十六进制输出缓冲区
// 条件日志
#if CONFIG_DEBUG_MODE
ESP_LOGD(TAG, "调试信息: %d", value);
#endif
2
3
4
5
6
7
8
9
10
11
12
13
14
15
常见问题
- 日志丢失或乱序
- 可能原因:日志缓冲区溢出或波特率不足
- 方法:提高波特率、增大日志缓冲区(
menuconfig → Log output → Log buffer size
)
- 未显示日志
- 原因:全局日志级别设置过高(如
INFO
会过滤DEBUG
)- 通过
esp_log_level_set("*", ESP_LOG_DEBUG)
临时启用所有调试日志。 - 或在
menuconfig
中调整默认级别
- 通过
- 原因:全局日志级别设置过高(如
- 固件体积过大
- 原因:调试日志未被过滤。
- 在生产版本中设置
CONFIG_LOG_DEFAULT_LEVEL_ERROR
。 - 使用
esp_log_level_set
为不同模块单独设置级别
- 在生产版本中设置
- 原因:调试日志未被过滤。
ESP_ERROR_CHECK
ESP-IDF 中用于错误处理的核心宏,通过编译时断言和运行时错误检查机制,帮助开发者快速定位问题
- 功能定位
- 错误码检查:自动验证 ESP-IDF API 的返回值。
- 即时反馈:发现错误时立即终止程序并输出详细错误信息。
- 编译时优化:在 Release 版本中可通过配置移除,减少代码体积。
- 设计哲学
- 防御式编程:假设所有 API 调用都可能失败。
- 零容忍原则:任何非预期错误都应被视为严重问题。
- 错误透明化:不隐藏错误,而是提供明确的定位信息。
- 性能参数
- 代码体积:每个检查点增加约 20~30 字节(取决于架构)
- 运行时开销:正常路径(无错误)约 5~10 个 CPU 周期
- 注意
- 第三方库错误码无法被 ESP_ERROR_CHECK 处理
// 核心宏定义(位于 esp_err.h)
#define ESP_ERROR_CHECK(x) do {
esp_err_t __err_rc = (x);
if (__err_rc != ESP_OK) {
esp_log_level_t __level = ESP_LOG_ERROR;
esp_system_abort(__err_rc);
}
} while(0)
// 自定义错误处理:使用 esp_err_to_name() 转换错误码
esp_err_t err = i2c_master_init();
if (err != ESP_OK) {
ESP_LOGE(TAG, "I2C 初始化失败: %s", esp_err_to_name(err));
// 执行恢复操作或返回
}
/* 优化策略 */
// 1.仅在关键节点检查错误
// 2.临时禁用检查(用于性能敏感代码)
#define ESP_ERROR_CHECK(x) (void)(x)
// ...性能敏感代码...
#undef ESP_ERROR_CHECK
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
GPIO操作
对比项 | 51 单片机(以 STC89C52 为例) | ESP32-S3 |
---|---|---|
操作方式 | 直接操作寄存器(如 P0、P1 寄存器) | 调用 ESP-IDF 封装的 API 函数(如 gpio_config ()) |
引脚功能 | 固定功能(如 P3.0 默认 UART_RX) | 多功能复用(同一引脚可配置为 GPIO/SPI/I2C 等) |
输入配置 | 需手动外接上拉电阻(弱上拉需特殊配置) | 内置可编程上拉 / 下拉电阻 |
中断支持 | 仅特定引脚支持外部中断(如 P3.2/INT0) | 所有 GPIO 均可配置中断,支持多触发方式 |
输出能力 | 灌电流强(拉电流弱,需外接上拉) | 推挽 / 开漏输出可选,驱动能力更强(最大 20mA) |
ESP32-S3 的 GPIO 操作通过 ESP-IDF 框架的driver/gpio.h
库实现,核心函数只有两个:配置函数和电平操作函数,比 51 的寄存器操作更直观。
/*
gpio_config()
GPIO 初始化配置
一次性完成 GPIO 的方向(输入 / 输出)、上拉 / 下拉、中断配置等所有参数设置
原型:esp_err_t gpio_config(const gpio_config_t *pGPIOConfig);
关键参数(gpio_config_t 结构体):
typedef struct {
uint64_t pin_bit_mask; // 引脚掩码(需操作的GPIO编号,如GPIO_NUM_2则设为(1ULL << 2))
gpio_mode_t mode; // 模式:GPIO_MODE_INPUT(输入)/ GPIO_MODE_OUTPUT(输出-默认为推挽输出)/ GPIO_MODE_OUTPUT_OD (开漏输出)
gpio_pullup_t pull_up_en; // 上拉使能:GPIO_PULLUP_ENABLE / GPIO_PULLUP_DISABLE
gpio_pulldown_t pull_down_en; // 下拉使能:GPIO_PULLDOWN_ENABLE / GPIO_PULLDOWN_DISABLE
gpio_int_type_t intr_type; // 中断类型:GPIO_INTR_DISABLE(禁用)/ 上升沿/下降沿/双边沿/低电平/高电平
} gpio_config_t;
*/
/*
gpio_set_level ()
输出控制,设置 GPIO 输出高 / 低电平,替代 51 中 "P1_0 = 1;" 的直接赋值
原型:esp_err_t gpio_set_level(gpio_num_t gpio_num, uint32_t level);
- gpio_num 引脚编号(如 GPIO_NUM_2,范围 0-45,注意开发板实际引出的引脚)
- level 0-低电平 1-高电平
*/
/*
gpio_get_level()
- 获取引脚的电平
*/
// 例1:LED 接 GPIO_NUM_2,实现 1 秒闪烁
#include "driver/gpio.h"
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#define LED_PIN GPIO_NUM_2 // 定义LED引脚
void app_main(void) {
// 1. 配置GPIO参数
gpio_config_t io_conf = {
.pin_bit_mask = (1ULL << LED_PIN), // 选中LED_PIN
.mode = GPIO_MODE_OUTPUT, // 输出模式
.pull_up_en = GPIO_PULLUP_DISABLE, // 禁用上拉
.pull_down_en = GPIO_PULLDOWN_DISABLE, // 禁用下拉
.intr_type = GPIO_INTR_DISABLE // 禁用中断
};
gpio_config(&io_conf); // 应用配置
// 2. 循环控制LED闪烁
while(1) {
gpio_set_level(LED_PIN, 0); // 低电平点亮
// portTICK_PERIOD_MS默认是 10ms(即 1 个 tick=10ms),因此1000 / portTICK_PERIOD_MS等价于 100 个 tick=1 秒
vTaskDelay(1000 / portTICK_PERIOD_MS); // 延时1秒(FreeRTOS延时函数)
gpio_set_level(LED_PIN, 1); // 高电平熄灭
vTaskDelay(1000 / portTICK_PERIOD_MS);
}
}
// 例2:按键接 GPIO_NUM_0,按下时接地(低电平),松开时通过内部上拉为高电平;实现 "按键按下时触发中断,在中断服务函数中翻转 LED 状态"
#include "driver/gpio.h"
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#define LED_PIN GPIO_NUM_2
#define KEY_PIN GPIO_NUM_0
// 中断服务函数(ISR):必须快速执行,不能调用阻塞函数
static void IRAM_ATTR key_isr_handler(void* arg) {
// 翻转LED状态(读取当前电平后取反)
uint32_t current_level = gpio_get_level(LED_PIN);
gpio_set_level(LED_PIN, !current_level);
}
void app_main(void) {
// 1. 配置LED引脚(输出)
gpio_config_t led_conf = {
.pin_bit_mask = (1ULL << LED_PIN),
.mode = GPIO_MODE_OUTPUT,
.pull_up_en = GPIO_PULLUP_DISABLE,
.pull_down_en = GPIO_PULLDOWN_DISABLE,
.intr_type = GPIO_INTR_DISABLE
};
gpio_config(&led_conf);
// 2. 配置按键引脚(输入+中断)
gpio_config_t key_conf = {
.pin_bit_mask = (1ULL << KEY_PIN),
.mode = GPIO_MODE_INPUT, // 输入模式
.pull_up_en = GPIO_PULLUP_ENABLE, // 使能上拉(按键松开时为高电平)
.pull_down_en = GPIO_PULLDOWN_DISABLE,
.intr_type = GPIO_INTR_NEGEDGE // 下降沿触发(按键按下时电平从高变低)
};
gpio_config(&key_conf);
// 3. 安装中断服务
gpio_install_isr_service(0); // 初始化中断服务,参数为中断优先级掩码(0表示默认)
// 绑定中断处理函数到按键引脚
gpio_isr_handler_add(KEY_PIN, key_isr_handler, NULL);
// 主任务:空循环(中断在后台触发)
while(1) {
vTaskDelay(1000 / portTICK_PERIOD_MS); // 避免任务退出
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
1ULL
1ULL
是关键的类型修饰符1ULL << GPIO_NUM_20
是嵌入式开发中用于配置 GPIO 引脚掩码的常见写法- 作用是生成一个仅第
GPIO_NUM_20
位为1
、其余位为0
的二进制数,用于告诉 ESP32-S3 的 GPIO 驱动:“我要操作的是第 20 号 GPIO 引脚”。 - 1ULL << 20
表示二进制数
00000000 00000000 00000100 00000000 00000000(仅第 20 位为 1),对应十进制值为
1048576
- 作用是生成一个仅第
1ULL
的作用:指定数据类型为 “无符号长 long”1
:默认是int
类型(在 ESP32 中通常为 32 位)ULL
:是 C 语言中的类型后缀,表示将1
强制转换为unsigned long long
类型(64 位无符号整数)U
:表示unsigned
(无符号,确保数值非负)LL
:表示long long
(长 long,64 位宽度)
- 为什么必须用
1ULL
而不是1
- ESP32-S3 的 GPIO 引脚编号最高可达 45(如 GPIO45),若用普通的
1
(32 位 int)左移 45 位,会出现数据溢出:- 32 位 int 的最大值是
2^31 - 1
,左移超过 31 位后,高位会被截断,导致结果错误 - 64 位
unsigned long long
可支持左移 0~63 位,完全覆盖 ESP32-S3 的所有 GPIO 引脚(0~45)
- 32 位 int 的最大值是
- ESP32-S3 的 GPIO 引脚编号最高可达 45(如 GPIO45),若用普通的
中断说明
- 中断触发方式: ESP32 支持 5 种中断触发类型(通过
intr_type
配置):GPIO_INTR_POSEDGE
:上升沿触发(低→高)GPIO_INTR_NEGEDGE
:下降沿触发(高→低)GPIO_INTR_ANYEDGE
:双边沿触发(高低电平变化均触发)GPIO_INTR_LOW_LEVEL
:低电平触发(电平保持低时持续触发)GPIO_INTR_HIGH_LEVEL
:高电平触发(电平保持高时持续触发) 按键场景常用GPIO_INTR_NEGEDGE
(避免按键抖动导致的多次触发,后续可通过软件消抖优化)。
- 中断服务函数(ISR)注意事项:
- 必须添加
IRAM_ATTR
属性,确保函数被加载到 IRAM(快速访问内存),避免因 Flash 访问延迟导致中断响应慢。 - 不能调用
vTaskDelay()
、printf()
等阻塞或耗时函数(中断需快速完成)。 - 若需要在中断中传递数据到主任务,应使用 FreeRTOS 的队列(xQueueSendFromISR ()),而非全局变量(可能导致竞态问题)。
- 必须添加
- 与 51 中断的对比: 51 的外部中断(如 INT0)只能固定引脚、固定触发方式(低电平或下降沿),而 ESP32 的所有 GPIO 均可配置中断,且触发方式更灵活,无需硬件上拉电阻,电路更简单。
GPIO 复用与功能切换
ESP32 的 GPIO 支持多功能复用(如 GPIO_NUM_18 可配置为 SPI_CLK),通过gpio_set_direction()
可临时切换引脚功能:
// 将GPIO_NUM_18从输出切换为输入
gpio_set_direction(GPIO_NUM_18, GPIO_MODE_INPUT);
// 切换回输出并设置电平
gpio_set_direction(GPIO_NUM_18, GPIO_MODE_OUTPUT);
gpio_set_level(GPIO_NUM_18, 1);
2
3
4
5
6
这一特性在资源紧张时非常有用(如通过同一引脚分时复用为 UART 和 GPIO)。
常见问题与避坑指南
- 引脚号混淆: ESP32-S3 的引脚编号(如 GPIO_NUM_0)是芯片的硬件编号,与开发板上的丝印(如 "D0")可能不一致,需对照开发板原理图确认对应关系。
- 中断不触发:
- 检查
gpio_install_isr_service()
是否被调用(必须先初始化中断服务)。 - 确认中断触发方式与实际电平变化匹配(如按键按下是低电平,却配置了上升沿触发)。
- 检查
- 输出电平异常:
- 若引脚配置为输入模式,
gpio_set_level()
会失效(需先切换为输出模式)。 - 超过引脚最大驱动电流(20mA)会导致电平不稳定,需外接驱动电路。
- 若引脚配置为输入模式,
- 按键抖动问题: 机械按键会有 10-50ms 的抖动,中断可能触发多次。解决方法:在中断服务函数中通过
vTaskDelayFromISR()
添加软件延时,或在主任务中通过定时器消抖。
通信
协议 | 核心优势 | 典型应用 | 不适用场景 |
---|---|---|---|
SPI | 高速、全双工、多设备独立控制 | 显示屏、SD 卡、高速传感器 | 设备数量多(需大量 CS 引脚)、低速场景 |
I2C | 引脚少(2 线)、多设备总线型 | 温湿度传感器、OLED(小尺寸) | 高速数据传输(如视频流) |
UART | 点对点异步通信 | 模块通信(如 GPS、蓝牙模块) | 多设备通信、同步数据传输 |
UART串口
对比项 | 51 单片机(以 STC89C52 为例) | ESP32-S3 |
---|---|---|
硬件资源 | 1 个 UART(仅 UART0,对应 P3.0/RXD、P3.1/TXD) | 3 个 UART(UART0/UART1/UART2),支持引脚复用 |
操作方式 | 直接操作SBUF (发送 / 接收缓冲寄存器)、SCON (控制寄存器)等 | 调用 ESP-IDF 封装的 API(如uart_driver_install() 、uart_write_bytes() ) |
波特率配置 | 需通过定时器 1(或专用波特率发生器)计算装载值,手动配置TH1 /TL1 | 直接传入波特率参数(如 115200),由框架自动配置时钟分频 |
中断处理 | 仅支持 "接收完成" 和 "发送完成" 单种中断,需手动判断中断源 | 支持接收 / 发送 / 错误等多种中断类型,可通过回调函数区分 |
缓冲区 | 仅 1 字节硬件缓冲(无软件缓冲区,需快速处理避免数据丢失) | 可配置软件缓冲区(大小自定义),支持中断 + DMA 模式,抗干扰能力更强 |
ESP32-S3 的 UART 操作主要依赖driver/uart.h
头文件中的函数,核心流程分为初始化→发送→接收→中断处理四步,比 51 的寄存器配置更清晰。
核心数据结构与函数
- UART 配置结构体:
uart_config_t
- 用于定义 UART 的基本参数(波特率、数据位、校验位等),替代 51 中
SCON
寄存器的位配置
- 用于定义 UART 的基本参数(波特率、数据位、校验位等),替代 51 中
- 应用参数配置:
uart_param_config()
- 必须在 uart_driver_install() 之前调用
- 将在
uart_config_t
结构体中设置的参数(如波特率、数据位、校验位等)应用到指定的 UART 控制器上
- 初始化函数:
uart_driver_install()
- 用于安装 UART 驱动、配置缓冲区大小,替代 51 中 "初始化波特率发生器 + 使能串口" 的步骤
- 引脚映射函数:
uart_set_pin()
- ESP32 的 UART 引脚可灵活复用,需通过该函数绑定物理引脚(51 的 UART 引脚固定,无此步骤)
- 发送函数:
uart_write_bytes()
- 用于发送数据,替代 51 中 " 写入
SBUF
寄存器 " 操作 - 开发建议
- 若需处理不定长数据(如以换行符
\n
结尾的字符串),可在读取后遍历data
数组,查找结束标志。 - 若需处理固定长度的协议帧(如每次发 10 字节),可循环调用
uart_read_bytes()
,直到累计读取到 10 字节。 - 软件缓冲区
rx_buffer_size
建议根据实际通信速率设置(如波特率 115200 时,1 秒最多传 11520 字节,缓冲区设为 2048 即可满足大部分场景)
- 若需处理不定长数据(如以换行符
- 用于发送数据,替代 51 中 " 写入
- 接收函数:
uart_read_bytes()
- 用于读取接收缓冲区的数据,替代 51 中 " 读取
SBUF
寄存器 " 操作
- 用于读取接收缓冲区的数据,替代 51 中 " 读取
发送场景 | 接收缓冲区数据量 | uart_read_bytes() 行为 | len 返回值 |
---|---|---|---|
发送方一次性发 300 字节,在调用函数前已接收完毕 | 300 字节 | 立即读取,无需等待超时 | 300 |
调用函数时无数据,100ms 后发送方发 600 字节 | 600 字节(在超时前) | 收到数据后立即读取,但受 BUF_SIZE=500 限制 | 500(剩余 100 字节留在缓冲区,下次可读取) |
调用函数后,200ms 内陆续收到 1200 字节 | 1200 字节(超过软件缓冲区上限 1024) | 最多读取软件缓冲区能存的 1024 字节,但受 BUF_SIZE=500 限制 | 500(剩余 524 字节留在缓冲区) |
200ms 内未收到任何数据 | 0 字节 | 超时后返回 | 0 |
// UART 配置 uart_config_t
typedef struct {
int baud_rate; // 波特率(如9600、115200)
uart_word_length_t data_bits; // 数据位(UART_DATA_5_BITS到UART_DATA_8_BITS)
uart_parity_t parity; // 校验位(UART_PARITY_DISABLE、UART_PARITY_EVEN、UART_PARITY_ODD)
uart_stop_bits_t stop_bits; // 停止位(UART_STOP_BITS_1、UART_STOP_BITS_1_5、UART_STOP_BITS_2)
uart_flow_ctrl_t flow_ctrl; // 流控(UART_HW_FLOWCTRL_DISABLE、UART_HW_FLOWCTRL_RTS等)
uint8_t source_clk; // 时钟源(默认UART_SCLK_APB)
} uart_config_t;
/*
应用参数配置(关键步骤)
uart_num: UART 端口号(UART_NUM_0、UART_NUM_1 或 UART_NUM_2)
uart_config:指向 uart_config_t 结构体的指针,包含具体配置参数
*/
esp_err_t uart_param_config(uart_port_t uart_num, const uart_config_t *uart_config);
// 初始化函数(分配资源、创建缓冲区)
esp_err_t uart_driver_install(
uart_port_t uart_num, // UART端口号(UART_NUM_0、UART_NUM_1、UART_NUM_2)
int rx_buffer_size, // 接收缓冲区大小(字节,如1024)
int tx_buffer_size, // 发送缓冲区大小(字节,如1024)
int queue_size, // 事件队列大小(0表示不使用事件队列)
QueueHandle_t *uart_queue,// 事件队列句柄(NULL表示不使用)
int intr_alloc_flags // 中断优先级配置(默认0即可)
);
// 引脚映射(可选,若使用默认引脚可省略)
esp_err_t uart_set_pin(
uart_port_t uart_num, // UART端口号
int tx_io_num, // 发送引脚(如GPIO_NUM_4)
int rx_io_num, // 接收引脚(如GPIO_NUM_5)
int rts_io_num, // RTS流控引脚(-1表示禁用)
int cts_io_num, // CTS流控引脚(-1表示禁用)
);
// 发送函数
int uart_write_bytes(
uart_port_t uart_num, // UART端口号
const char *src, // 待发送数据的缓冲区
size_t size // 发送数据长度(字节)
);
// 接收函数
// 在超时时间内能一次性返回接收缓冲区中积累的所有数据
int uart_read_bytes(
uart_port_t uart_num, // UART端口号
uint8_t *buf, // 接收数据的缓冲区
uint32_t length, // 期望读取的长度(字节)
TickType_t ticks_to_wait // 超时时间(如100/portTICK_PERIOD_MS表示100ms)
);
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
常见问题
- 波特率不匹配:
- 51 需严格匹配晶振频率(如 11.0592MHz 对应 9600 波特率),ESP32 默认使用 APB 时钟(40MHz),波特率误差极小,无需手动校准。
- 若通信乱码,优先检查
uart_config_t
中的baud_rate
是否与 PC 端一致。
- 引脚复用冲突:
- ESP32 的 UART 引脚可能与其他外设(如 SPI、I2C)复用,需查阅开发板原理图,避免引脚功能冲突(如 UART0 默认对应 GPIO1/3,可能与 USB-JTAG 共用)。
- 发送数据丢失:
- 51 因无软件缓冲区,连续发送时需等待
TI
标志位;ESP32 的uart_write_bytes()
会返回实际发送的字节数,若返回值小于预期,可能是发送缓冲区满,需延时后重试。
- 51 因无软件缓冲区,连续发送时需等待
- 接收数据不完整:
- 51 需在中断中及时读取
SBUF
,否则新数据会覆盖旧数据;ESP32 需确保uart_read_bytes()
的超时时间足够长(或使用事件驱动),避免数据未读完就退出。
- 51 需在中断中及时读取
流控引脚
RTS(Request To Send)是 UART 通信中用于硬件流控的引脚,主要作用是协调发送方和接收方的数据传输节奏,防止接收方因缓冲区溢出而丢失数据
- 在高速或不稳定的通信环境中,接收方可能因处理速度慢或缓冲区已满,无法及时处理新数据。此时,若发送方继续发送,会导致数据丢失。硬件流控通过两根专用引脚(RTS 和 CTS)实现实时协调:
- RTS(Request To Send):由接收方控制,用于向发送方发出 “是否可以发送数据” 的请求。
- CTS(Clear To Send):由发送方控制,用于向接收方确认 “是否已准备好接收数据”
- 具体过程
- 当接收方的缓冲区快满时,会主动拉低 RTS 引脚(逻辑 0),向发送方表明:“我快处理不过来了,请暂停发送!” 发送方检测到 RTS 为低电平时,会暂停发送数据,直到 RTS 重新变为高电平(接收方缓冲区有空间)。
- 初始状态:接收方缓冲区有空间,RTS 保持高电平(允许发送)。
- 接收方缓冲区接近满:接收方将 RTS 拉低(禁止发送)。
- 发送方检测到 RTS 为低:立即停止发送数据。
- 接收方处理数据后腾出空间:将 RTS 拉高(允许发送)。
- 发送方检测到 RTS 为高:继续发送剩余数据。
- 当接收方的缓冲区快满时,会主动拉低 RTS 引脚(逻辑 0),向发送方表明:“我快处理不过来了,请暂停发送!” 发送方检测到 RTS 为低电平时,会暂停发送数据,直到 RTS 重新变为高电平(接收方缓冲区有空间)。
- 何时需要?
- 通信速率极高(如波特率≥230400),接收方处理速度跟不上。
- 接收缓冲区较小,容易满(如传感器连续高速发送数据)。
- 通信链路不稳定,需要精确控制数据流。
- 场景
- ESP32 作为接收方,从高速传感器(如激光雷达)接收大量点云数据。
- 与工业设备通信(如 PLC),需确保数据不丢失
- 注意事项
- 双向流控:若需完全可靠的通信,建议同时启用 RTS 和 CTS(
UART_HW_FLOWCTRL_RTS_CTS
)。 - 引脚选择:RTS/CTS 引脚必须选择支持外设复用的 GPIO(如 GPIO18、GPIO19 等),否则流控无效。
- 兼容性:若对端设备不支持硬件流控,需禁用此功能,改用软件流控或无流控。
- 双向流控:若需完全可靠的通信,建议同时启用 RTS 和 CTS(
对比项 | 硬件流控(RTS/CTS) | 软件流控(XON/XOFF) |
---|---|---|
实现方式 | 通过专用引脚(物理信号) | 通过发送特殊字符(如 ASCII 17/XON、ASCII 19/XOFF) |
适用场景 | 高速通信(如波特率 > 115200) | 低速通信(波特率较低时) |
优点 | 不占用数据位,响应速度快 | 无需额外引脚,节省硬件资源 |
缺点 | 需要额外两根引脚 | 可能与传输的实际数据冲突(如数据中恰好包含 XON/XOFF 字符) |
示例
- 每秒向 PC 发送一次字符串
- ESP32-S3 接收 PC 发送的字符串,原样回传;类似51的回传
- 通过事件队列处理 UART 中断(如接收完成、发送完成、错误等)
- 事件驱动模式下,主任务无需轮询接收,仅在有事件时处理,CPU 利用率更高(尤其适合多任务场景)
// 以 "每秒向 PC 发送一次字符串" 为例
#include <string.h>
#include "driver/uart.h"
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
// 定义UART参数
#define UART_NUM UART_NUM_1 // 使用UART1
#define TX_PIN GPIO_NUM_4 // 发送引脚:GPIO4
#define RX_PIN GPIO_NUM_5 // 接收引脚:GPIO5(本次仅发送,可随意定义)
#define BAUD_RATE 115200 // 波特率
#define TX_BUF_SIZE 1024 // 发送缓冲区大小
#define RX_BUF_SIZE 1024 // 接收缓冲区大小
void UART_Init() {
// 1. 配置UART参数
uart_config_t uart_config = {
.baud_rate = BAUD_RATE,
.data_bits = UART_DATA_8_BITS,
.parity = UART_PARITY_DISABLE,
.stop_bits = UART_STOP_BITS_1,
.flow_ctrl = UART_HW_FLOWCTRL_DISABLE
};
uart_param_config(UART_NUM, &uart_config); // 应用配置
// 2. 绑定引脚
uart_set_pin(UART_NUM, TX_PIN, RX_PIN, -1, -1, UART_PIN_CONFIG_DEFAULT);
// 3. 安装UART驱动
uart_driver_install(UART_NUM, RX_BUF_SIZE, TX_BUF_SIZE, 0, NULL, 0);
}
void app_main(void) {
UART_Init();
while(1) {
// 发送字符串
const char *str = "Hello from ESP32-S3!\r\n";
uart_write_bytes(UART_NUM, str, strlen(str));
vTaskDelay(1000 / portTICK_PERIOD_MS); // 延时1秒
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
// "ESP32-S3 接收 PC 发送的字符串,原样回传" 为例,演示双向通信(类似 51 的 "回声" 功能)
#include "driver/uart.h"
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include <string.h>
#define UART_NUM UART_NUM_1
#define TX_PIN GPIO_NUM_4
#define RX_PIN GPIO_NUM_5
#define BAUD_RATE 115200
#define BUF_SIZE 1024
void UART_Init() {
uart_config_t uart_config = {
.baud_rate = BAUD_RATE,
.data_bits = UART_DATA_8_BITS,
.parity = UART_PARITY_DISABLE,
.stop_bits = UART_STOP_BITS_1,
.flow_ctrl = UART_HW_FLOWCTRL_DISABLE
};
uart_param_config(UART_NUM, &uart_config);
uart_set_pin(UART_NUM, TX_PIN, RX_PIN, -1, -1, UART_PIN_CONFIG_DEFAULT);
uart_driver_install(UART_NUM, BUF_SIZE, BUF_SIZE, 0, NULL, 0);
}
void app_main(void) {
UART_Init();
uint8_t data[BUF_SIZE]; // 接收缓冲区
while(1) {
// 读取接收数据(超时100ms)
int len = uart_read_bytes(UART_NUM, data, BUF_SIZE, 100 / portTICK_PERIOD_MS);
if(len > 0) {
// 回传接收到的数据
uart_write_bytes(UART_NUM, (const char*)data, len);
}
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
#include "driver/uart.h"
#include "freertos/FreeRTOS.h"
#include "freertos/queue.h"
#include <string.h>
#define UART_NUM UART_NUM_1
#define TX_PIN GPIO_NUM_4
#define RX_PIN GPIO_NUM_5
#define BUF_SIZE 1024
#define QUEUE_SIZE 10 // 事件队列大小
QueueHandle_t uart_queue; // 事件队列句柄
void UART_Init() {
uart_config_t uart_config = {
.baud_rate = 115200,
.data_bits = UART_DATA_8_BITS,
.parity = UART_PARITY_DISABLE,
.stop_bits = UART_STOP_BITS_1,
.flow_ctrl = UART_HW_FLOWCTRL_DISABLE,
.source_clk = UART_SCLK_APB
};
uart_param_config(UART_NUM, &uart_config);
uart_set_pin(UART_NUM, TX_PIN, RX_PIN, -1, -1, UART_PIN_CONFIG_DEFAULT);
// 安装驱动时创建事件队列
uart_driver_install(UART_NUM, BUF_SIZE * 2, BUF_SIZE * 2, QUEUE_SIZE, &uart_queue, 0);
}
void app_main(void) {
UART_Init();
uart_event_t event; // 事件结构体
while(1) {
// 等待事件队列消息(阻塞等待)
if(xQueueReceive(uart_queue, (void *)&event, portMAX_DELAY)) {
switch(event.type) {
case UART_DATA: // 接收数据事件
{
uint8_t data[BUF_SIZE];
// 读取接收数据
uart_read_bytes(UART_NUM, data, event.size, 0);
// 回传数据
uart_write_bytes(UART_NUM, (const char*)data, event.size);
}
break;
case UART_FIFO_OVF: // FIFO溢出事件
uart_flush_input(UART_NUM); // 清空输入缓冲区
xQueueReset(uart_queue); // 重置事件队列
break;
// 可处理其他事件(如UART_PARITY_ERR、UART_FRAME_ERR等)
default:
break;
}
}
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
SPI串行外设接口
高速、全双工、同步的串行通信协议,因其传输速率高、时序简单、支持全双工等特性;传输速率远高于 I2C(ESP32-S3 的硬件 SPI 最高可达 80MHz),广泛应用于快速传输大量数据的场景
对比项 | 51 单片机(以 STC89C52 为例) | ESP32-S3 |
---|---|---|
硬件资源 | 无原生 SPI 接口,需通过 IO 模拟(软件 SPI) | 3 个硬件 SPI 控制器(SPI0/1/2),支持 DMA |
操作方式 | 手动控制 IO 引脚时序(如 P1.0~P1.3 模拟 SPI) | 调用 ESP-IDF 的 SPI 驱动 API(如spi_device_... ) |
通信速率 | 软件模拟最高约 100kHz(受晶振和代码影响) | 硬件 SPI 最高 80MHz(实际常用 40MHz 以内) |
参数配置 | 手动计算时钟周期,无 CPOL/CPHA 等概念 | 需精确配置 CPOL/CPHA、位宽、字节序等参数 |
多设备支持 | 通过软件切换片选(CS)引脚 | 驱动自动管理片选,支持多设备注册 |
- 适用场景
- 高速数据传输场景
- 显示屏 / 触摸屏:SPI 接口的 LCD/OLED 显示屏、电容触摸屏
- SD 卡、Flash 芯片(如 W25Q 系列)
- 高速传感器:惯性测量单元(如 MPU9250)、激光雷达(如 TFmini-S)
- 多设备独立控制场景
- SPI 通过片选(CS)引脚区分不同从设备(每个从机独占一个 CS 引脚),主设备可通过切换 CS 引脚独立控制任意从机,避免总线冲突。这种 “一对一” 的控制方式适合需要精准控制多个独立设备的场景
- 外设扩展模块:SPI 接口的 ADC(如 MCP3208)、DAC(如 MCP4921)、GPIO 扩展芯片(如 MCP23S17)
- 工业控制设备
- SPI 的差分信号(SCK、MOSI 等)抗干扰能力强,适合工业环境的噪声场景
- SPI 的 CS 引脚可确保指令仅发送到目标设备,避免误操作
- 高速数据传输场景
基本组成
引脚名称 | 作用(新手通俗理解) |
---|---|
SCK | 时钟线,主设备发 "节拍",告诉从设备什么时候读 / 写数据(像打拍子指挥节奏) |
MOSI | 主设备输出线(Master Out Slave In),主设备通过这根线给从设备发数据 |
MISO | 主设备输入线(Master In Slave Out),从设备通过这根线给主设备发数据 |
CS/SS | 片选线(Chip Select/Slave Select),主设备通过这根线选择要通信的从设备(比如总线上有 3 个设备,拉低哪个的 CS,就和哪个说话) |
核心参数【数据手册里一定会写它的模式,照抄即可】
- 时钟极性(CPOL):SCK 线空闲时的电平
- CPOL=0:SCK 线没数据传输时(空闲)是低电平(0V)
- CPOL=1:SCK 线空闲时是高电平(3.3V)
- 时钟相位(CPHA):数据采样的时机-在 SCK 的 "上升沿" 还是 "下降沿" 被读取
- CPHA=0:数据在 SCK 的第一个跳变沿(从低到高,或从高到低)被读取
- CPHA=1:数据在 SCK 的第二个跳变沿被读取
模式 CPOL CPHA 通俗理解(以 0V 为低,3.3V 为高) SPI_MODE0 0 0 空闲时 SCK 低电平,第一个上升沿读数据 SPI_MODE1 0 1 空闲时 SCK 低电平,第一个下降沿读数据 SPI_MODE2 1 0 空闲时 SCK 高电平,第一个下降沿读数据 SPI_MODE3 1 1 空闲时 SCK 高电平,第一个上升沿读数据 - 时钟极性(CPOL):SCK 线空闲时的电平
使用引导
初始化 SPI(代码配置)
c#include "driver/spi_master.h" // 1.接线 #define PIN_NUM_MOSI 11 // MOSI 发数据引脚 #define PIN_NUM_CLK 12 // 时钟线 #define PIN_NUM_CS 10 // 片选 // 2. 配置SPI总线(SCK、MOSI等通用设置) spi_bus_config_t bus_config = { .mosi_io_num = PIN_NUM_MOSI, // MOSI引脚 .miso_io_num = -1, // 这里只发数据,不接收,所以MISO设为-1 .sclk_io_num = PIN_NUM_CLK, // SCK引脚 .quadwp_io_num = -1, // 不用的引脚设为-1【QSPI时使用】 .quadhd_io_num = -1, .max_transfer_sz = 0, // 最大传输字节数,0表示默认(4096字节) }; // 3.初始化总线(用SPI2_HOST这个控制器,你也可以用SPI1_HOST) // 在 ESP32-S3 中,SPI 控制器支持通过 DMA 进行高速数据传输。SPI_DMA_CH_AUTO 是一个特殊值,告诉驱动自动选择合适的 DMA 通道,减少CPU的占用 spi_bus_initialize(SPI2_HOST, &bus_config, SPI_DMA_CH_AUTO); // 4. 配置具体设备(OLED屏幕的SPI参数) spi_device_interface_config_t dev_config = { .clock_speed_hz = 10 * 1000 * 1000, // 时钟频率:10MHz(屏幕支持的最大频率) .mode = 0, // SPI_MODE0(和屏幕手册一致) .spics_io_num = PIN_NUM_CS, // CS引脚 .queue_size = 7, // 传输队列大小,7足够用 }; // 把设备添加到总线上,得到一个设备句柄(后面用这个句柄通信) spi_device_handle_t spi_handle; spi_bus_add_device(SPI2_HOST, &dev_config, &spi_handle);
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31发送数据(与设备通信)
- 用
spi_device_transmit
函数给设备发数据,比如给 OLED 屏幕发一个命令 spi_transaction_t
是传输结构体,告诉 SPI 控制器要发什么数据、发多少。tx_buffer
是要发送的数据地址,这里发的是一个字节的命令。
c// 给OLED发送一个命令(比如初始化命令) void oled_send_cmd(spi_device_handle_t handle, uint8_t cmd) { // 准备传输的数据 spi_transaction_t t; memset(&t, 0, sizeof(t)); // 清空结构体 memset-c标准函数 t.length = 8; // 传输8位(1个字节) t.tx_buffer = &cmd; // 要发送的数据(命令) t.user = (void*)0; // 自定义标记,0表示是命令(屏幕需要区分命令和数据) // 发送数据 spi_device_transmit(handle, &t); } // 在main里调用 void app_main() { // 前面的初始化代码... // 发送初始化命令(具体命令看屏幕手册) oled_send_cmd(spi_handle, 0xAE); // 关闭屏幕 oled_send_cmd(spi_handle, 0xAF); // 打开屏幕 }
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22- 用
接收数据(如果需要读设备数据)
- 从设备中读数据(比如读传感器的值),只需要在
spi_transaction_t
里设置rx_buffer
c// 从SPI设备读取数据 void spi_read_data(spi_device_handle_t handle, uint8_t *rx_data, int len) { spi_transaction_t t; memset(&t, 0, sizeof(t)); t.length = len * 8; // 传输长度(单位是位,所以×8) t.rx_buffer = rx_data; // 接收数据的缓冲区 t.tx_buffer = NULL; // 不发送数据 spi_device_transmit(handle, &t); // 发送(同时接收) } // 调用示例:读2个字节 uint8_t data[2]; spi_read_data(spi_handle, data, 2);
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15- 从设备中读数据(比如读传感器的值),只需要在
传输队列
- queue_size
- 好处:提高程序效率,避免 CPU 等待 SPI 传输完成
- SPI 通信不是瞬间完成的,发送一个字节需要几个时钟周期。如果在发送过程中,程序又想发送新的数据,有两种处理方式:
- 无队列:必须等待当前传输完成,才能发送新数据。
- 有队列:把新数据放入队列,然后继续执行其他任务,完成当前传输后,自动从队列中取出下一个数据发送
- 队列满时的行为:如果队列已经有 n 个请求,再调用
spi_device_transmit()
会阻塞(程序暂停),直到队列有空闲位置
QSPI(四线 SPI)
- 标准SPI - 双线传输
- MOSI:主设备→从设备(单向车道)
- MISO:从设备→主设备(单向车道)
- 同一时间只能单向传输数据(要么发,要么收)
- QSPI - 四线传输
- IO0/IO1:双向数据传输(主→从 和 从→主 同时进行)
- IO2/IO3:额外的双向数据线(用于高速传输)
- 同一时间可以双向、多线并行传输数据,速度是标准 SPI 的 2~4 倍!
quadwp_io_num
和quadhd_io_num
- 用于配置 QSPI 模式下的额外数据线;
quadwp_io_num
:配置 WP(Write Protect)引脚,对应 QSPI 的 IO2 线;用于写保护功能,防止误操作修改存储设备(如 Flash)quadhd_io_num
:配置 HD(Hold)引脚,对应 QSPI 的 IO3 线;用于暂停通信,允许设备在忙时暂时停止接收数据
- 注意
- 支持 QSPI 的设备才需配置(如四线 Flash、四线 SD 卡、某些高速传感器)。对于普通 SPI 设备(如 OLED 屏幕、SD 卡),这两个参数永远是
-1
- ESP32-S3 的 SPI 控制器支持标准 SPI、DSPI(双线 SPI)、QSPI 三种模式,但同一时间只能选一种。配置错误会导致通信失败
- 支持 QSPI 的设备才需配置(如四线 Flash、四线 SD 卡、某些高速传感器)。对于普通 SPI 设备(如 OLED 屏幕、SD 卡),这两个参数永远是
spi_bus_config_t bus_config = {
.mosi_io_num = 11, // QSPI模式下变为IO0
.miso_io_num = 13, // QSPI模式下变为IO1
.sclk_io_num = 12, // 时钟线
.quadwp_io_num = 14, // QSPI的IO2线
.quadhd_io_num = 15, // QSPI的IO3线
.max_transfer_sz = 4096,
};
// 同时需要在设备配置中启用QSPI模式
spi_device_interface_config_t dev_config = {
.clock_speed_hz = 40 * 1000 * 1000, // QSPI通常用更高频率
.mode = 0,
.spics_io_num = 10,
.flags = SPI_DEVICE_QUAD_TX | SPI_DEVICE_QUAD_RX, // 启用四线发送和接收
};
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
常见问题
- 通信失败,设备没反应?
- 检查接线:SCK、MOSI、CS 引脚是否接错,GND 是否共地
- 检查模式:
mode
参数是否和设备手册一致(比如设备是 Mode 3,你写成了 Mode 0) - 检查频率:
clock_speed_hz
是否超过设备支持的最大频率(降慢试试,比如 1MHz)
- 数据乱码?
- CPOL/CPHA 错误:模式不对,数据采样的时机错了,导致读出来的是错误的 bit
- 接线太长:线太长会导致信号延迟,尽量缩短接线(<20cm)
- 怎么知道设备用哪种模式?
- 看设备的数据手册(Datasheet),搜索 "SPI Mode" 或 "CPOL CPHA",比如: "SPI Interface: Mode 0 (CPOL=0, CPHA=0), 10MHz max"
IIC
- 双线通信:只需要两根线就能连接多个设备
- SDA(Serial Data):数据线,用于传输数据
- SCL(Serial Clock):时钟线,控制数据传输的节奏
- 半双工:同一时间只能单向传输(要么主发从收,要么主收从发)
- 多设备总线:通过 7 位或 10 位地址区分不同设备(最多支持 127 个 7 位地址设备)
- 低速:标准模式 100kHz,快速模式 400kHz,高速模式 1MHz
特性 | SPI | I2C |
---|---|---|
引脚数量 | 至少 4 根(SCK、MOSI、MISO、CS) | 2 根(SDA、SCL) |
寻址方式 | 通过 CS 引脚选择从设备 | 通过设备地址(如 0x50)选择从设备 |
通信速率 | 高速(可达 80MHz) | 低速(最高 1MHz) |
拓扑结构 | 星形(每个从设备独立 CS) | 总线型(所有设备共享 SDA/SCL) |
应用场景 | 高速数据传输(如屏幕、SD 卡) | 低速多设备通信(如传感器网络) |
通信过程
- 起始信号:主设备拉低 SDA,再拉低 SCL,表示开始通信
- 发送从设备地址:主设备发送 7 位地址 + 1 位读写位(0 = 写,1 = 读)
- 等待从设备应答:从设备收到地址后,拉低 SDA 表示应答
- 数据传输:
- 写操作:主设备发送数据,从设备接收
- 读操作:从设备发送数据,主设备接收
- 应答机制:每传输一个字节后,接收方需要拉低 SDA 应答
- 停止信号:主设备拉高 SCL,再拉高 SDA,表示通信结束
前置知识
i2c_cmd_link_create()
ESP-IDF 框架提供的 I2C 驱动函数- 创建一个命令链接(Command Link),用于将一系列 I2C 操作(如发送地址、读写数据)按顺序添加进去,最后一次性执行
#include "driver/i2c.h"
必须包含这个头文件- 把多个 I2C 操作组合成一个 "命令包",减少 CPU 干预
- 支持复杂的 I2C 事务(如先写寄存器地址,再读寄存器值)
常用的命令链接 函数 | 作用 |
---|---|
i2c_master_start() | 添加起始信号(SDA 下降沿,SCL 高电平) |
i2c_master_stop() | 添加停止信号(SDA 上升沿,SCL 高电平) |
i2c_master_write_byte() | 添加写一个字节操作 |
i2c_master_write() | 添加写多个字节操作 |
i2c_master_read_byte() | 添加读一个字节操作 |
i2c_master_read() | 添加读多个字节操作 |
// i2c_config_t 配置I2C控制器
typedef struct {
i2c_mode_t mode; // 模式:主模式或从模式
gpio_num_t sda_io_num; // SDA 引脚号(如 GPIO_NUM_15)
gpio_num_t scl_io_num; // SCL 引脚号(如 GPIO_NUM_14)
bool sda_pullup_en; // 是否启用 SDA 上拉电阻
bool scl_pullup_en; // 是否启用 SCL 上拉电阻
union {
struct {
uint32_t clk_speed; // 主模式下的时钟频率(如 400000 = 400kHz)
} master;
struct {
uint32_t clk_speed; // 从模式下的时钟频率
uint8_t slave_addr; // 从模式下的自身地址
bool slave_addr_10bit; // 是否使用 10 位地址
} slave;
};
int clk_flags; // 时钟标志(通常设为 0)
} i2c_config_t;
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
#include "driver/i2c.h"
#define I2C_MASTER_SCL_IO GPIO_NUM_14 // SCL 引脚
#define I2C_MASTER_SDA_IO GPIO_NUM_15 // SDA 引脚
#define I2C_MASTER_FREQ_HZ 400000 // 400kHz(快速模式)
#define I2C_MASTER_NUM I2C_NUM_0 // I2C 控制器编号
void i2c_master_init(void) {
// 配置 I2C 参数
i2c_config_t conf = {
.mode = I2C_MODE_MASTER, // 主模式
.sda_io_num = I2C_MASTER_SDA_IO, // SDA 引脚
.scl_io_num = I2C_MASTER_SCL_IO, // SCL 引脚
.sda_pullup_en = GPIO_PULLUP_ENABLE, // 启用 SDA 上拉
.scl_pullup_en = GPIO_PULLUP_ENABLE, // 启用 SCL 上拉
.master.clk_speed = I2C_MASTER_FREQ_HZ, // 时钟频率
};
// 应用配置
i2c_param_config(I2C_MASTER_NUM, &conf);
// 安装驱动(队列长度设为 10,表示可排队 10 个传输请求)
i2c_driver_install(I2C_MASTER_NUM, conf.mode, 0, 0, 0);
}
// 写操作:主设备向从设备发送数据(如向 I2C 传感器写命令)
esp_err_t i2c_master_write_to_device(
uint8_t device_addr,
const uint8_t *data,
size_t data_len,
TickType_t timeout
) {
i2c_cmd_handle_t cmd = i2c_cmd_link_create(); // 创建 I2C 命令链接
i2c_master_start(cmd); // 开始 I2C 事务
i2c_master_write_byte(cmd, (device_addr << 1) | I2C_MASTER_WRITE, true); // 发送从设备地址(写操作)
i2c_master_write(cmd, data, data_len, true); // 发送数据
i2c_master_stop(cmd); // 结束 I2C 事务
esp_err_t ret = i2c_master_cmd_begin(I2C_MASTER_NUM, cmd, timeout); // 执行命令
i2c_cmd_link_delete(cmd); // 释放命令链接资源
return ret;
}
// 读操作:主设备从从设备接收数据(如读取传感器数据)
esp_err_t i2c_master_read_from_device(
uint8_t device_addr,
uint8_t *data,
size_t data_len,
TickType_t timeout
) {
i2c_cmd_handle_t cmd = i2c_cmd_link_create(); // 创建 I2C 命令链接
i2c_master_start(cmd); // 开始 I2C 事务
i2c_master_write_byte(cmd, (device_addr << 1) | I2C_MASTER_READ, true); // 发送从设备地址(读操作)
// 读取数据(最后一个字节发送 NACK,表示不再接收)
if (data_len > 1) {
i2c_master_read(cmd, data, data_len - 1, I2C_MASTER_ACK);
}
i2c_master_read_byte(cmd, data + data_len - 1, I2C_MASTER_NACK);
i2c_master_stop(cmd); // 结束 I2C 事务
esp_err_t ret = i2c_master_cmd_begin(I2C_MASTER_NUM, cmd, timeout); // 执行命令
i2c_cmd_link_delete(cmd); // 释放命令链接资源
return ret;
}
// 先写寄存器地址,再读寄存器值(很多I2C 设备需要先指定寄存器地址,再读取该寄存器的值)
// 读取指定寄存器的值
esp_err_t i2c_master_read_reg(
uint8_t device_addr,
uint8_t reg_addr,
uint8_t *data,
size_t data_len,
TickType_t timeout
) {
// 创建 I2C 命令链接
i2c_cmd_handle_t cmd = i2c_cmd_link_create();
// 第1步:发送寄存器地址(写操作)
i2c_master_start(cmd);
i2c_master_write_byte(cmd, (device_addr << 1) | I2C_MASTER_WRITE, true);
i2c_master_write_byte(cmd, reg_addr, true);
i2c_master_stop(cmd);
// 执行第1步命令
esp_err_t ret = i2c_master_cmd_begin(I2C_MASTER_NUM, cmd, timeout);
i2c_cmd_link_delete(cmd);
if (ret != ESP_OK) {
return ret;
}
// 第2步:读取寄存器数据(读操作)
cmd = i2c_cmd_link_create();
i2c_master_start(cmd);
i2c_master_write_byte(cmd, (device_addr << 1) | I2C_MASTER_READ, true);
if (data_len > 1) {
i2c_master_read(cmd, data, data_len - 1, I2C_MASTER_ACK);
}
i2c_master_read_byte(cmd, data + data_len - 1, I2C_MASTER_NACK);
i2c_master_stop(cmd);
// 执行第2步命令
ret = i2c_master_cmd_begin(I2C_MASTER_NUM, cmd, timeout);
i2c_cmd_link_delete(cmd);
return ret;
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
其他
地址在代码中的表示【在 ESP-IDF 中,设备地址需要左移 1 位,再加上读写位】
- 写操作:
(device_addr << 1) | 0
- 读操作:
(device_addr << 1) | 1
c// 写操作地址:0x50 << 1 = 0xA0 i2c_master_write_byte(cmd, 0xA0, true); // 读操作地址:(0x50 << 1) | 1 = 0xA1 i2c_master_write_byte(cmd, 0xA1, true);
1
2
3
4
5- 写操作:
常见问题 | 可能原因 | 解决方法 |
---|---|---|
通信失败(返回 ESP_FAIL) | 1. 设备地址错误 2. 引脚配置错误 3. 上拉电阻缺失 | 1. 检查设备数据手册地址 2. 确认 SDA/SCL 引脚 3. 确保上拉电阻(4.7kΩ~10kΩ) |
数据读取错误 | 1. 时序不匹配 2. 寄存器地址错误 3. 数据格式错误 | 1. 降低时钟频率 2. 确认寄存器地址 3. 检查数据手册数据格式 |
总线锁定 | 1. 设备未释放 SDA/SCL 2. 通信超时 | 1. 硬件复位设备 2. 增加超时时间 3. 使用 i2c_reset_bus () 复位总线 |
存储
非易失性存储NVS
NVS 是基于 Flash 的键值对存储系统,专为嵌入式设备设计,轻松实现设备配置保存、状态记录等功能,为复杂嵌入式系统提供数据持久化支持
- 典型应用场景
- 保存 Wi-Fi 连接信息(SSID / 密码)
- 存储设备校准参数(如传感器零点偏移)
- 记录用户配置(工作模式、亮度设置等)
- 持久化系统状态(如设备唯一 ID、运行计数器)
/*
esp_err_t nvs_flash_init() 初始化nvs
返回值:
ESP_OK:初始化成功。
ESP_ERR_NVS_NO_FREE_PAGES:分区已满或损坏。
ESP_ERR_NVS_NEW_VERSION_FOUND:NVS 版本不兼容。
其他错误码(如 Flash 操作失败)
底层逻辑:
1.分区存在 n-返回错误 y-2
2.验证分区格式 n-返回错误 y-3
3.格式有效 n-擦出分区并创建新格式 y-挂载分区并加载命名空间索引
4.返回 ESP_Ok
其他:
默认使用 nvs 分区(大小通常为 24KB),可通过 partitions.csv 自定义
数据按命名空间(Namespace)组织,不同命名空间可存储相同键名
*/
esp_err_t nvs_flash_init(void);
// 错误处理:分区损坏或已满
esp_err_t err = nvs_flash_init();
if (err == ESP_ERR_NVS_NO_FREE_PAGES || err == ESP_ERR_NVS_NEW_VERSION_FOUND) {
// 擦除分区并重试
ESP_ERROR_CHECK(nvs_flash_erase());
err = nvs_flash_init();
}
ESP_ERROR_CHECK(err); // 再次检查是否成功
// 自定义分区:指定分区标签初始化
esp_err_t nvs_flash_init_partition(const char* partition_label);
// 示例:使用 "my_nvs" 分区
ESP_ERROR_CHECK(nvs_flash_init_partition("my_nvs"));
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
/* 操作命名空间 */
nvs_handle_t handle;
esp_err_t err;
err = nvs_open("storage", NVS_READWRITE, &handle); // 打开命名空间(读写模式)
if (err != ESP_OK) {
// 处理错误
}
nvs_close(handle); // 关闭命名空间
/* 读写操作 */
// 写入数据
err = nvs_set_str(handle, "wifi_ssid", "MyWiFi"); // 字符串
err = nvs_set_i32(handle, "counter", 123); // 32位整数
err = nvs_set_blob(handle, "data", buffer, size); // 二进制数据
// 提交更改到 Flash
err = nvs_commit(handle);
// 读取数据
char ssid[32];
size_t len = sizeof(ssid);
err = nvs_get_str(handle, "wifi_ssid", ssid, &len);
int32_t counter;
err = nvs_get_i32(handle, "counter", &counter);
/* 枚举命名空间中的所有键 */
nvs_iterator_t it = NULL;
esp_err_t err = nvs_entry_find("storage", NVS_TYPE_ANY, &it);
while (it != NULL) {
nvs_entry_info_t info;
nvs_entry_info(it, &info);
printf("键: %s, 类型: %d\n", info.key, info.type);
it = nvs_entry_next(it);
}
nvs_release_iterator(it);
// ESP32-S3加密支持:启用 NVS 加密(需提前配置加密密钥)
esp_err_t nvs_flash_init_encrypted(void);
// 检查分区是否已加密
bool nvs_flash_is_encrypted(void);
// 获取分区统计信息
esp_err_t nvs_flash_get_security_info(nvs_flash_security_info_t *info);
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
性能优化建议
- 批量提交:避免频繁调用
nvs_commit()
,合并多次写操作- 写操作:约 10~20ms(包含 Flash 擦除和写入)
- 读操作:约 1~2ms(从缓存读取)
- 减少擦写次数
- 对频繁更新的数据(如计数器),使用 RAM 缓存,定时写入
- 预估数据增长,适当增大分区大小
- 数据压缩:对大尺寸数据(如配置文件),考虑压缩后存储
注意
- 数据类型匹配:读取时必须使用与写入相同的数据类型(如
nvs_get_i32()
对应nvs_set_i32()
) - 字符串长度:读取字符串时需预先分配足够空间,并通过
len
参数获取实际长度 - 内存泄漏:
nvs_open()
后必须调用nvs_close()
,避免句柄泄漏 - 断电保护:重要数据写入后及时调用
nvs_commit()
,确保持久化
PWM与定时器
PWM 是一种通过调节信号的占空比来控制平均电压的技术
特性 | 51 定时器实现 PWM | ESP32 LEDC 外设 |
---|---|---|
实现方式 | 软件定时器 + IO 翻转(依赖 CPU 中断) | 硬件 PWM 控制器(不依赖 CPU) |
分辨率 | 通常 8 位(0~255) | 最高 13 位(0~8191) |
频率范围 | 受晶振和定时器位数限制 | 支持 1Hz~40MHz 宽范围 |
多路 PWM | 每路需要独立定时器 | 16 路独立通道,可共享定时器 |
渐变功能 | 需要软件不断修改占空比 | 硬件自动渐变,无需 CPU 干预 |
资源占用 | 高(频繁中断) | 低(硬件自动运行) |
- 应用场景
- 调光控制:LED 呼吸灯、显示器亮度调节
- 电机控制:直流电机转速、舵机角度控制
- 电源管理:开关电源的输出电压调节
- 音频合成:通过 PWM 生成模拟音频信号
LEDC
- (LED Control)是 ESP32 专门设计的 PWM 控制器,也称
LED PWM
- 硬件级实现:不依赖 CPU 中断,PWM 生成过程完全由硬件完成
- 高精度:支持 13 位分辨率(8192 级)的占空比调节
- 多路输出:最多支持 16 路独立 PWM 通道(8 个高速通道 + 8 个低速通道)
- 自动渐变:支持占空比自动线性变化(无需软件干预)
- 低功耗:特别适合 LED 调光、电机控制等场景
函数 | 特点 | 适用场景 |
---|---|---|
ledc_set_duty() | 直接设置占空比,无渐变 | 静态亮度控制 |
ledc_set_fade_with_time() | 指定时间,自动计算渐变步长 | 平滑呼吸灯、定时调光 |
ledc_set_fade_with_step() | 指定总步数和每步间隔时间 | 精确控制渐变过程 |
ledc_set_fade() | 手动控制每一步的占空比值和持续时间 | 自定义非线性渐变(如指数曲线) |
// ledc_timer_config_t 配置 PWM 定时器的基本参数
typedef struct {
ledc_mode_t speed_mode; // 速度模式:高速或低速 LEDC_HIGH_SPEED_MODE | LEDC_LOW_SPEED_MODE
ledc_timer_t timer_num; // 定时器编号(0~3)
ledc_clk_cfg_t clk_cfg; // 时钟配置
uint32_t duty_resolution; // 占空比分辨率(位)
uint32_t freq_hz; // PWM 频率(Hz)
} ledc_timer_config_t;
// ledc_channel_config_t 配置 PWM 通道的输出参数
// 注:一个定时器可被多个通道关联共享,但一个通道只能关联一个定时器
typedef struct {
ledc_mode_t speed_mode; // 速度模式
ledc_channel_t channel; // 通道编号(0~7) LEDC_CHANNEL_0 ~ LEDC_CHANNEL_7
gpio_num_t gpio_num; // 输出 GPIO 引脚
ledc_timer_t timer_sel; // 关联的定时器
int duty; // 初始占空比值
int hpoint; // 硬件点(通常设为 0)
} ledc_channel_config_t;
// 用于配置 LEDC 通道占空比渐变的函数
// 注:调用 ledc_set_fade_with_time() 后,需调用 ledc_fade_start() 触发实际渐变
esp_err_t ledc_set_fade_with_time(
ledc_mode_t speed_mode, // 速度模式(高速或低速)
ledc_channel_t channel, // 通道编号
uint32_t target_duty, // 目标占空比值
uint32_t max_fade_time_ms // 渐变持续时间(毫秒)
);
// 用于启动已配置的占空比渐变的函数
esp_err_t ledc_fade_start(
ledc_mode_t speed_mode, // 速度模式(高速或低速)
ledc_channel_t channel, // 通道编号
ledc_fade_mode_t fade_mode // 渐变模式 LEDC_FADE_NO_WAIT-直接返回 LEDC_FADE_WAIT_DONE-阻塞到渐变完成
);
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
/* 实现呼吸灯 demo */
#include "driver/ledc.h"
#include "esp_err.h"
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
// 定义 LEDC 参数
#define LEDC_TIMER LEDC_TIMER_0 // 使用定时器0
#define LEDC_MODE LEDC_LOW_SPEED_MODE // 低速模式
#define LEDC_OUTPUT_IO (5) // GPIO5 连接 LED
#define LEDC_CHANNEL LEDC_CHANNEL_0 // 使用通道0
#define LEDC_DUTY_RES LEDC_TIMER_13_BIT // 13 位分辨率(0~8191),精度极高
#define LEDC_DUTY_MAX (8191) // 13 位最大占空比:2^13-1=8191
#define LEDC_FREQUENCY (5000) // PWM 频率(Hz)
void app_main(void)
{
// 1. 配置定时器
ledc_timer_config_t ledc_timer = {
.speed_mode = LEDC_MODE,
.timer_num = LEDC_TIMER,
.duty_resolution = LEDC_DUTY_RES,
.freq_hz = LEDC_FREQUENCY, // 频率 5kHz
.clk_cfg = LEDC_AUTO_CLK, // 自动选择时钟源
};
ledc_timer_config(&ledc_timer);
// 2. 配置通道
ledc_channel_config_t ledc_channel = {
.speed_mode = LEDC_MODE,
.channel = LEDC_CHANNEL,
.timer_sel = LEDC_TIMER,
.gpio_num = LEDC_OUTPUT_IO,
.duty = 0, // 初始占空比为 0(LED熄灭)
.hpoint = 0,
};
ledc_channel_config(&ledc_channel);
// 3. 设置占空比渐变功能
ledc_fade_func_install(0); // 安装渐变功能
// 4. 循环实现呼吸灯效果
while (1) {
// 亮度渐亮(占空比从 0 到最大值)
ledc_set_fade_with_time(LEDC_MODE, LEDC_CHANNEL, LEDC_DUTY_MAX, 1000); // 1秒内完成渐变
ledc_fade_start(LEDC_MODE, LEDC_CHANNEL, LEDC_FADE_NO_WAIT);
vTaskDelay(1000 / portTICK_PERIOD_MS); // 等待渐变完成
// 亮度渐暗(占空比从最大值到 0)
ledc_set_fade_with_time(LEDC_MODE, LEDC_CHANNEL, 0, 1000); // 1秒内完成渐变
ledc_fade_start(LEDC_MODE, LEDC_CHANNEL, LEDC_FADE_NO_WAIT);
vTaskDelay(1000 / portTICK_PERIOD_MS); // 等待渐变完成
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
Wi-Fi
- 工作模式
- STA(Station)模式:作为客户端连接到现有 Wi-Fi 路由器(如家里的路由器)
- AP(Access Point)模式:作为热点,允许其他设备连接到 ESP32
- STA+AP 混合模式:同时作为客户端和热点工作
WIFI 事件 ID | 数据结构 | 含义 |
---|---|---|
WIFI_EVENT_STA_START | 无数据 | STA 模式启动 |
WIFI_EVENT_STA_CONNECTED | wifi_event_sta_connected_t | 已连接到 AP(未获取 IP) |
WIFI_EVENT_STA_DISCONNECTED | wifi_event_sta_disconnected_t | 与 AP 断开连接 |
WIFI_EVENT_SCAN_DONE | wifi_event_sta_scan_done_t | 扫描可用 AP 完成 |
IP 事件 ID | 数据结构 | 含义 |
---|---|---|
IP_EVENT_STA_GOT_IP | ip_event_got_ip_t | STA 模式获取到 IP 地址 |
IP_EVENT_STA_LOST_IP | 无数据 | STA 模式失去 IP 地址 |
STA 模式联网
- 初始化 LwIP 协议栈(处理 TCP/IP 协议)
- 初始化 Wi-Fi 驱动
- 配置 Wi-Fi 参数(如 SSID、密码)
- 启动 Wi-Fi
- 调用
esp_wifi_connect()
连接到指定 AP - 等待连接成功,获取 IP 地址
关键结构体 / 函数 | 作用 |
---|---|
wifi_config_t | 存储 Wi-Fi 配置(SSID、密码、认证模式等) |
esp_netif_init() | 初始化网络接口框架,为 Wi-Fi 提供底层支持 |
esp_netif_create_default_wifi_sta() | 创建默认 STA 模式网络接口 |
esp_wifi_set_mode() | 设置 Wi-Fi 工作模式(STA/AP/ 混合) |
esp_wifi_connect() | 触发 STA 模式连接到指定 AP |
esp_event_handler_register() | 注册事件回调,处理连接状态 / IP 获取等事件 |
// wifi_config_t 配置 Wi-Fi 参数,定义在 esp_wifi_types.h
typedef struct {
union {
struct {
uint8_t ssid[32]; // AP 的 SSID(Wi-Fi 名称)
uint8_t password[64]; // AP 的密码
uint8_t bssid_set; // 是否指定 BSSID(MAC 地址)
uint8_t bssid[6]; // AP 的 BSSID(MAC 地址)
uint8_t channel; // 连接的通道(0 表示自动)
} sta; // STA 模式配置
struct {
uint8_t ssid[32]; // AP 的 SSID
uint8_t password[64]; // AP 的密码
uint8_t ssid_len; // SSID 长度(0 表示自动计算)
uint8_t channel; // AP 工作的通道
wifi_auth_mode_t authmode; // 认证模式
uint8_t ssid_hidden; // 是否隐藏 SSID
uint8_t max_connection; // 最大连接数
uint16_t beacon_interval; // 信标间隔(毫秒)
} ap; // AP 模式配置
};
} wifi_config_t;
/* ESP-IDF 中用于快速配置 Wi-Fi 的STA模式
功能定位【封装了网络接口(ESP-NETIF)与 Wi-Fi 驱动的初始化流程】
1.初始化 LwIP 协议栈并创建默认的 STA 网络接口,绑定到 Wi-Fi 驱动
2.设置 DHCP 客户端、默认事件处理函数,以及与 Wi-Fi 驱动的通信通道
3.通过 esp_event 系统监听网络状态变化(如连接成功、IP 分配)
返回值
成功:返回创建的 esp_netif_t 网络接口句柄(通常无需手动操作)。
失败:返回 NULL(内存不足或初始化错误)
*/
esp_netif_t* esp_netif_create_default_wifi_sta(void);
/* 注册 Wi-Fi 事件监听器的关键操作
event_base
- WIFI_EVENT :Wi-Fi 驱动产生的事件(如连接状态变化)
- IP_EVENT :IP 协议栈产生的事件(如 IP 地址分配)
event_id:
- ESP_EVENT_ANY_ID:监听该 event_base 下的所有事件。
- 具体 ID(如 WIFI_EVENT_STA_CONNECTED):监听特定事件
*/
esp_err_t esp_event_handler_register(
esp_event_base_t event_base, // 事件基础类型(如 WIFI_EVENT、IP_EVENT)
int32_t event_id, // 事件 ID(如 ESP_EVENT_ANY_ID 表示所有事件)
esp_event_handler_t event_handler,// 回调函数指针
void* handler_arg // 传递给回调函数的参数(通常为 NULL)
);
void event_handler(
void* arg,
esp_event_base_t event_base,
int32_t event_id,
void* event_data
)
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
// demo 联网实现
#include "nvs_flash.h"
#include "esp_event.h"
#include "esp_wifi.h"
#include "esp_netif.h"
#include "esp_log.h"
#include "freertos/event_groups.h"
// 定义 Wi-Fi 配置
#define WIFI_SSID "你的 Wi-Fi 名称"
#define WIFI_PASS "你的 Wi-Fi 密码"
// 事件组用于等待 Wi-Fi 连接结果
static EventGroupHandle_t s_wifi_event_group;
// 定义事件标志位
#define WIFI_CONNECTED_BIT BIT0
#define WIFI_DISCONNECTED_BIT BIT1
// 日志标签
static const char *TAG = "wifi";
// 事件处理回调函数
static void event_handler(
void* arg,
esp_event_base_t event_base,
int32_t event_id,
void* event_data
) {
if (event_base == WIFI_EVENT && event_id == WIFI_EVENT_STA_START) {
// Wi-Fi 启动后,主动连接 AP
esp_wifi_connect();
ESP_LOGI(TAG, "正在连接到 AP...");
} else if (event_base == WIFI_EVENT && event_id == WIFI_EVENT_STA_DISCONNECTED) {
// 连接断开,设置标志位
xEventGroupSetBits(s_wifi_event_group, WIFI_DISCONNECTED_BIT);
ESP_LOGI(TAG, "与 AP 断开连接");
} else if (event_base == IP_EVENT && event_id == IP_EVENT_STA_GOT_IP) {
// 获取到 IP 地址,设置标志位
ip_event_got_ip_t* event = (ip_event_got_ip_t*) event_data;
ESP_LOGI(TAG, "获取到 IP: " IPSTR, IP2STR(&event->ip_info.ip));
xEventGroupSetBits(s_wifi_event_group, WIFI_CONNECTED_BIT);
}
}
void wifi_init_sta(void) {
// 创建事件组
s_wifi_event_group = xEventGroupCreate();
// 初始化 NVS(非易失性存储)
esp_err_t ret = nvs_flash_init();
if (ret == ESP_ERR_NVS_NO_FREE_PAGES || ret == ESP_ERR_NVS_NEW_VERSION_FOUND) {
ESP_ERROR_CHECK(nvs_flash_erase());
ret = nvs_flash_init();
}
ESP_ERROR_CHECK(ret);
ESP_ERROR_CHECK(esp_netif_init()); // 初始化网络接口
ESP_ERROR_CHECK(esp_event_loop_create_default()); // 创建默认事件循环
esp_netif_create_default_wifi_sta(); // 创建默认的 STA 网络接口
// 初始化 Wi-Fi 驱动
wifi_init_config_t cfg = WIFI_INIT_CONFIG_DEFAULT();
ESP_ERROR_CHECK(esp_wifi_init(&cfg));
// 注册事件处理函数
//
ESP_ERROR_CHECK(esp_event_handler_register(WIFI_EVENT, ESP_EVENT_ANY_ID, &event_handler, NULL));
ESP_ERROR_CHECK(esp_event_handler_register(IP_EVENT, IP_EVENT_STA_GOT_IP, &event_handler, NULL));
// 配置 Wi-Fi 参数
wifi_config_t wifi_config = {
.sta = {
.ssid = WIFI_SSID,
.password = WIFI_PASS,
.threshold.authmode = WIFI_AUTH_WPA2_PSK, // 常见认证模式
},
};
// 设置 Wi-Fi 工作模式为 STA
ESP_ERROR_CHECK(esp_wifi_set_mode(WIFI_MODE_STA));
// 应用 Wi-Fi 配置
ESP_ERROR_CHECK(esp_wifi_set_config(ESP_IF_WIFI_STA, &wifi_config));
// 启动 Wi-Fi
ESP_ERROR_CHECK(esp_wifi_start());
ESP_LOGI(TAG, "Wi-Fi STA 初始化完成");
}
void app_main(void) {
// 初始化 Wi-Fi
wifi_init_sta();
// 等待连接结果
EventBits_t bits = xEventGroupWaitBits(
s_wifi_event_group,
WIFI_CONNECTED_BIT | WIFI_DISCONNECTED_BIT,
pdTRUE, // 等待后清除标志位
pdFALSE, // 不等待所有标志位
portMAX_DELAY // 无限等待
);
// 处理连接结果
if (bits & WIFI_CONNECTED_BIT) {
ESP_LOGI(TAG, "Wi-Fi 连接成功");
} else if (bits & WIFI_DISCONNECTED_BIT) {
ESP_LOGI(TAG, "Wi-Fi 连接失败");
} else {
ESP_LOGE(TAG, "等待 Wi-Fi 事件超时");
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
补充
- 硬件特性:ESP32-S3 集成了 2.4GHz、802.11 b/g/n Wi-Fi,支持 40MHz 的带宽,可提供高达 150Mbps 的数据传输速率。其内置了 RF(射频)前端、基带处理器和 MAC(媒体访问控制)层,构成完整的 Wi-Fi 通信系统。芯片还具备 45 个可编程 GPIOs,其中 14 个 GPIOs 可配置为电容式触摸输入,这在一些需要结合 Wi-Fi 控制和触摸交互的应用中非常有用。
- 安全机制:ESP32-S3 支持硬件加速的 Wi-Fi 加密算法,包括 WPA/WPA2-PSK 和 WPA3-SAE 加密,使得加密和解密数据的速度更快,提高了系统的整体性能和安全性。
- 低功耗模式:ESP32-S3 提供多种 Wi-Fi 省电模式,例如可以使用
WiFi.setSleep(true)
启用自动睡眠模式,在不活跃时降低功耗,这对于电池供电的物联网设备来说非常重要,可以延长设备的续航时间。 - Wi-Fi 直连(Wi-Fi Direct)模式:除了 STA 模式和 AP 模式外,ESP32-S3 还支持 Wi-Fi 直连模式,该模式下设备可以直接与其他支持 Wi-Fi 直连的设备进行连接,无需通过中间的路由器,可用于设备间的快速数据传输,如手机与 ESP32-S3 设备之间直接传输文件或控制指令。
- 网络扫描与信号强度获取:可以使用
WiFi.scanNetworks()
函数扫描周围可用的 Wi-Fi 网络,该函数返回扫描到的网络数量。通过WiFi.SSID(i)
和WiFi.RSSI(i)
函数可以获取指定索引的扫描到的 Wi-Fi 网络的 SSID 和信号强度(RSSI),这在需要选择最优网络进行连接或者实时监测网络信号质量的场景中很有帮助。 - ESP-MESH 组网技术:ESP32-S3 基于 Wi-Fi 协议研发了 ESP-MESH 组网技术,以满足物联网应用场景下智能设备对互连功能的需求。通过该技术,多个 ESP32-S3 设备可以组成一个 Mesh 网络,实现信号的扩展和多设备之间的通信,适用于大规模的物联网部署,如智能楼宇、智慧工厂等
蓝牙
// 蓝牙控制器初始化【指定蓝牙模式、时钟等配置】
esp_err_t esp_bt_controller_init(const esp_bt_controller_config_t *cfg);
typedef struct {
esp_bt_mode_t mode; // 蓝牙模式(BLE/经典蓝牙/双模)
esp_bt_controller_clock_source_t clk_src; // 时钟源选择
uint32_t controller_task_stack_size; // 控制器任务栈大小
uint32_t controller_task_prio; // 控制器任务优先级
esp_bt_controller_sleep_mode_t sleep_mode; // 休眠模式
uint8_t ble_max_conn; // BLE 最大连接数
uint8_t bt_max_acl_conn; // 经典蓝牙最大 ACL 连接数
uint8_t bt_sco_datapath; // SCO 数据路径(硬件/软件)
bool enable_log; // 是否启用控制器日志
// 其他高级参数(如射频功率、缓存大小等)
} esp_bt_controller_config_t;
/*
BT_CONTROLLER_INIT_CONFIG_DEFAULT()
ESP-IDF 中预定义的宏,用于初始化 esp_bt_controller_config_t 结构体的默认值
#define BT_CONTROLLER_INIT_CONFIG_DEFAULT() { \
.mode = ESP_BT_MODE_IDLE, \
.clk_src = ESP_BT_CTRL_CLK_SRC_DEFAULT, \
.controller_task_stack_size = 3072, \
.controller_task_prio = 23, \
.sleep_mode = ESP_BT_CONTROLLER_LIGHT_SLEEP, \
.ble_max_conn = 3, \
.bt_max_acl_conn = 1, \
.bt_sco_datapath = ESP_BT_SCO_DATA_PATH_DEFAULT, \
.enable_log = false, \
}
*/
// 设置设备名称,最长 248 字节
esp_err_t esp_gap_ble_set_device_name(const char *name);
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
#include "esp_bt.h"
#include "esp_bt_main.h"
#include "esp_gap_ble_api.h"
#define DEVICE_NAME "ESP32-S3-BLE" // 自定义设备名称
// 初始化蓝牙控制器
void ble_controller_init(void) {
// 1. 禁用蓝牙休眠(可选,提升响应速度)
esp_bt_controller_config_t bt_cfg = BT_CONTROLLER_INIT_CONFIG_DEFAULT();
bt_cfg.sleep_mode = ESP_BT_CONTROLLER_NO_SLEEP; // 不休眠
// 2. 初始化控制器
esp_err_t err = esp_bt_controller_init(&bt_cfg);
if (err != ESP_OK) {
ESP_LOGE(TAG, "蓝牙控制器初始化失败: %s", esp_err_to_name(err));
return;
}
// 3. 启动控制器
err = esp_bt_controller_enable(ESP_BT_MODE_BLE); // 仅启用 BLE 模式
if (err != ESP_OK) {
ESP_LOGE(TAG, "启动蓝牙控制器失败: %s", esp_err_to_name(err));
return;
}
// 4. 初始化 Bluedroid 协议栈(必须在控制器启动后)
err = esp_bluedroid_init();
if (err != ESP_OK) {
ESP_LOGE(TAG, "初始化 Bluedroid 失败: %s", esp_err_to_name(err));
return;
}
// 5. 启用 Bluedroid 协议栈
err = esp_bluedroid_enable();
if (err != ESP_OK) {
ESP_LOGE(TAG, "启用 Bluedroid 失败: %s", esp_err_to_name(err));
return;
}
ESP_LOGI(TAG, "蓝牙控制器初始化完成");
}
// GAP 事件回调(处理设备发现、连接等事件)
static void gap_event_handler(esp_gap_ble_cb_event_t event, esp_ble_gap_cb_param_t *param) {
switch (event) {
case ESP_GAP_BLE_SET_DEVICE_NAME_COMPLETE_EVT:
ESP_LOGI(TAG, "设备名称设置完成: %s", DEVICE_NAME);
// 名称设置后可继续初始化广播
break;
// 其他事件(如连接请求、断开等)处理...
default:
break;
}
}
// 初始化 GAP 层并设置设备名称
void gap_init(void) {
// 注册 GAP 回调函数
esp_err_t err = esp_ble_gap_register_callback(gap_event_handler);
if (err != ESP_OK) {
ESP_LOGE(TAG, "注册 GAP 回调失败: %s", esp_err_to_name(err));
return;
}
// 设置设备名称
err = esp_gap_ble_set_device_name(DEVICE_NAME);
if (err != ESP_OK) {
ESP_LOGE(TAG, "设置设备名称失败: %s", esp_err_to_name(err));
}
}
// 配置 BLE 广播数据【让其他设备可查找到】
void ble_advertise_init(void) {
esp_ble_adv_data_t adv_data = {
.set_scan_rsp = false, // 非扫描响应包
.include_name = true, // 广播中包含设备名称
.include_txpower = true, // 包含发射功率
.min_interval = 0x0800, // 最小连接间隔(单位:1.25ms)
.max_interval = 0x1000, // 最大连接间隔
.appearance = 0x00, // 设备外观(0 表示未知)
.manufacturer_len = 0, // 厂商数据长度
.p_manufacturer_data = NULL,
.service_data_len = 0,
.p_service_data = NULL,
.service_uuid_len = 0,
.p_service_uuid = NULL,
.flag = (ESP_BLE_ADV_FLAG_GEN_DISC | ESP_BLE_ADV_FLAG_BREDR_NOT_SUPPORTED)
};
// 设置广播数据
esp_err_t err = esp_ble_gap_config_adv_data(&adv_data);
if (err != ESP_OK) {
ESP_LOGE(TAG, "配置广播数据失败: %s", esp_err_to_name(err));
return;
}
// 启动广播
esp_ble_adv_params_t adv_params = {
.adv_int_min = 0x20, // 最小广播间隔(32 * 0.625ms = 20ms)
.adv_int_max = 0x40, // 最大广播间隔(64 * 0.625ms = 40ms)
.adv_type = ADV_TYPE_IND, // 非定向可连接广播
.own_bda = NULL, // 使用默认蓝牙地址
.channel_map = ADV_CHNL_ALL, // 全通道广播(37、38、39)
.adv_filter_policy = ADV_FILTER_ALLOW_SCAN_ANY_CON_ANY // 允许任何设备扫描和连接
};
err = esp_ble_gap_start_advertising(&adv_params);
if (err != ESP_OK) {
ESP_LOGE(TAG, "启动广播失败: %s", esp_err_to_name(err));
} else {
ESP_LOGI(TAG, "BLE 广播已启动,设备名称: %s", DEVICE_NAME);
}
}
void app_main(void) {
// 初始化 NVS(蓝牙需要 NVS 存储配对信息)
ESP_ERROR_CHECK(nvs_flash_init());
// 初始化蓝牙控制器
ble_controller_init();
// 初始化 GAP 层并设置设备名称
gap_init();
// 初始化 GATT 服务
gatts_init();
// 启动广播
ble_advertise_init();
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
// 通过 GATT 服务交换数据。以下是简单的 “回声服务” 示例:
// 自定义 UUID(可通过在线工具生成)
#define ECHO_SERVICE_UUID 0x00FF
#define ECHO_CHARACTERISTIC_UUID 0xFF01
// GATT 回调(处理数据读写请求)
static void gatts_profile_event_handler(esp_gatts_cb_event_t event, esp_gatt_if_t gatts_if, esp_ble_gatts_cb_param_t *param) {
switch (event) {
case ESP_GATTS_CONNECT_EVT:
ESP_LOGI(TAG, "手机已连接,设备地址: " ESP_BD_ADDR_STR, ESP_BD_ADDR_HEX(param->connect.remote_bda));
break;
case ESP_GATTS_WRITE_EVT: {
// 收到手机发送的数据
esp_ble_gatts_write_evt_param_t *write_param = ¶m->write;
ESP_LOGI(TAG, "收到数据: %.*s", write_param->len, write_param->value);
// 回声:将收到的数据原样返回
esp_gatt_rsp_t rsp;
memset(&rsp, 0, sizeof(esp_gatt_rsp_t));
rsp.attr_value.handle = write_param->handle;
rsp.attr_value.len = write_param->len;
memcpy(rsp.attr_value.value, write_param->value, write_param->len);
esp_ble_gatts_send_response(gatts_if, param->write.trans_id, ESP_GATT_OK, &rsp);
break;
}
case ESP_GATTS_DISCONNECT_EVT:
ESP_LOGI(TAG, "手机已断开连接");
// 断开后重新启动广播
esp_ble_gap_start_advertising(&adv_params);
break;
// 其他事件(如服务初始化、特征读取等)处理...
default:
break;
}
}
// 初始化 GATT 服务
void gatts_init(void) {
// 注册 GATT 回调
esp_err_t err = esp_ble_gatts_register_callback(gatts_profile_event_handler);
if (err != ESP_OK) {
ESP_LOGE(TAG, "注册 GATT 回调失败: %s", esp_err_to_name(err));
return;
}
// 创建 GATT 服务(简化示例,完整实现需定义服务和特征)
// ...(详细代码见实战案例)
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
手机配对与通信步骤
手机端操作:
打开手机蓝牙,进入 “设置 → 蓝牙” 页面。
扫描设备,找到名称为
ESP32-S3-BLE
的设备并点击连接。使用 BLE 调试工具(如nRF Connect):
- 连接设备后,找到自定义服务(UUID: 0x00FF)。
向特征(UUID: 0xFF01)写入数据,ESP32-S3 会回声返回。
常见问题:
- 无法发现设备:检查广播配置或手机蓝牙权限。
- 连接失败:确认
menuconfig
中蓝牙模式与代码一致(如仅启用 BLE)。 - 数据传输失败:检查 GATT 服务 UUID 和特征定义是否正确。
实战扩展
- 安全配对:启用 BLE 加密(
esp_ble_gap_set_security_param
),防止数据泄露。 - 低功耗优化:配置蓝牙休眠模式(
esp_bt_controller_config_t.sleep_mode
),延长电池寿命。 - 经典蓝牙(BR/EDR):若需支持传统蓝牙(如串口透传),需启用双模并使用
esp_bt_gap_set_device_name
(注意与 BLE 名称函数区分)。
通过以上步骤,可实现 ESP32-S3 与手机的蓝牙双向通信,适用于遥控器、传感器数据传输等场景。如需具体功能(如蓝牙串口、OTA 升级),可进一步扩展 GATT 服务逻辑。
FreeRTOS支持
基于ESP32乐鑫,轻量级实时操作系统(RTOS),专为微控制器和小型微处理器设计。它提供任务调度、内存管理、同步机制等核心功能,是 ESP32-S3 开发中实现多任务处理、复杂逻辑和资源管理的关键。
任务
任务就是一段独立运行的函数代码,有自己的 "状态"、"优先级"、"专属内存栈",能被操作系统(FreeRTOS)主动调度执行。因为 ESP32-S3 是双核 CPU,最多能真正同时运行 2 个任务,其他任务在 "排队等待"
优先级
- 抢占式优先级调度:高优先级任务一旦进入 “就绪” 状态,会立即抢占低优先级任务的 CPU
- 同优先级任务分时运行:采用 “时间片轮转”,同优先级的就绪任务,看起来像同时运行
- 状态优先:只有 “就绪” 状态的任务才可能被调度
- 高优先级任务 执行vTaskDelay 等阻塞时,会进行任务切换执行
/* 获取任务优先级
- 传入要查询的任务句柄
- 若传入NULL,则查询当前正在运行的任务的优先级
- 使用场景:若某任务长期无法执行,可通过uxTaskPriorityGet()检查是否有更高优先级的任务持续占用 CPU(未进入阻塞状态)
*/
UBaseType_t uxTaskPriorityGet( const TaskHandle_t xTask );
/* 修改任务优先级 */
void vTaskPrioritySet( TaskHandle_t xTask, UBaseType_t uxNewPriority );
/* 在中断中获取优先级 */
UBaseType_t uxTaskPriorityGetFromISR( TaskHandle_t xTask );
// 示例:low_handle为TaskHandle_t
UBaseType_t self_prio = uxTaskPriorityGet(low_handle);
ESP_LOGI("LowTask", "自身优先级:%u", self_prio);
vTaskPrioritySet(low_handle, 3); // 将低优先级任务的优先级提升到 3
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
注意
任务函数必须无限循环:如果任务函数执行完(没有
while(1)
),会导致栈溢出(因为 FreeRTOS 会试图回收不存在的任务)- FreeRTOS 将程序分解为多个独立 “任务”(类似线程),每个任务是一个无限循环函数(如
void myTask(void *pvParameters)
)
- FreeRTOS 将程序分解为多个独立 “任务”(类似线程),每个任务是一个无限循环函数(如
栈大小要合适:栈太小会崩溃(比如函数调用层级深、局部变量大时),可以用
uxTaskGetStackHighWaterMark()
查看栈的剩余空间(返回 0 表示溢出)避免在任务中使用
malloc/free
:动态内存分配可能导致内存碎片,长期运行会崩溃(优先用静态内存或 FreeRTOS 的内存池)任务间通信要用 FreeRTOS 提供的机制(队列、信号量等),不要直接操作全局变量(可能导致数据混乱)
核心绑定*:ESP32-S3 是双核芯片(Core 0 和 Core 1),部分任务需要固定在某个核心运行(如避免核心切换开销、独占硬件资源),此时需要 “核心绑定” 功能。
核心绑定和静态分配都是 “优化手段”,而非必须。先实现功能,再根据需求优化性能和内存
/* 任务控制块 TCB,Task Control Block */
typedef struct tskTaskControlBlock {
// 1. 任务栈相关(任务运行时的内存空间)
StackType_t *pxTopOfStack; // 栈顶指针(当前任务使用的栈的顶部地址)
StackType_t *pxStack; // 栈的起始地址(任务创建时分配的栈内存起点)
configSTACK_DEPTH_TYPE usStackDepth; // 栈大小(创建任务时指定的栈深度)
// 2. 任务状态与调度相关
UBaseType_t uxPriority; // 任务优先级(0~configMAX_PRIORITIES-1,数值越大优先级越高,S3中最大32)
eTaskState eCurrentState; // 当前任务状态(就绪/运行/阻塞/挂起,枚举类型)
TickType_t xTicksToDelay; // 阻塞延迟的滴答数(vTaskDelay()时设置,倒计时结束后进入就绪态)
// 3. 任务链表指针(FreeRTOS用链表管理所有任务)
struct tskTaskControlBlock *pxNext; // 指向下一个任务(用于就绪链表、阻塞链表等)
struct tskTaskControlBlock *pxPrevious;// 指向前一个任务(双向链表,方便插入/删除)
// 4. 任务名称与标识
const char *pcTaskName; // 任务名称(创建时指定,调试用)
TaskHandle_t xHandle; // 任务句柄(对外暴露的"任务ID",用于操作任务)
// 开发者无需直接操作 TCB,而是通过句柄调用 API(如vTaskDelete(handle)删除任务)。
// 5. 其他辅助信息(简化后)
BaseType_t xCoreID; // ESP32-S3双核专用:任务绑定的核心(0或1,-1表示未绑定-由调度器自动分配)
} tskTCB;
// 任务状态枚举(eTaskState)
typedef enum {
eRunning, // 正在运行(当前占用CPU)
eReady, // 就绪(可以运行,等CPU空闲)
eBlocked, // 阻塞(等待事件,比如延时结束、队列有数据)
eSuspended // 挂起(被手动暂停,需调用vTaskResume恢复)
} eTaskState;
// 任务句柄本质是TCB的指针
typedef struct tskTaskControlBlock *TaskHandle_t;
/* 任务创建
- 任务必须是void (*)(void*)类型的函数,且必须包含无限循环(while (1)),如果函数执行完退出,会导致系统崩溃
- 任务运行需要的内存(栈)大小。比如填 1024,实际内存是 1024×4=4096 字节(ESP32 上 1 字 = 4 字节)。栈太小会导致 “栈溢出”(任务崩溃),新手建议先设 2048(8KB),调试时再减小
- 任务句柄-输出参数,用于获取 “任务句柄”(TaskHandle_t 类型)
- 返回值:创建成功-返回pdPASS,否则返回相应的错误码
*/
// xTaskCreate 自动核心,内存动态分配(最常用)
BaseType_t xTaskCreate(
TaskFunction_t pxTaskCode, // 任务要执行的函数(核心) void (*)(void*)类型
const char *pcName, // 任务名称(仅调试用,最长16字符,如"SensorTask")
configSTACK_DEPTH_TYPE usStackDepth, // 栈大小(单位:字,1字=4字节)
void *pvParameters, // 传给任务的参数(可选,NULL表示无参数)
UBaseType_t uxPriority, // 优先级(0~31,数值越大越优先)
TaskHandle_t *pxCreatedTask // 任务句柄(类似ID,后续操作任务用)
);
// 动态分配、强制绑定核心,避免核心切换带来的延迟
// - 实时性要求高的任务(如高频数据处理),避免被其他核心的任务干扰;
// - 需独占某核心硬件资源的任务(如特定外设中断处理)
BaseType_t xTaskCreatePinnedToCore(
TaskFunction_t pvTaskCode, // 任务函数指针,原型是 void fun(void *param)
const char *constpcName, // 任务名称,打印调试可能会有用
const uint32_t usStackDepth, // 指定的任务堆栈空间大小(字节)
void *constpvParameters, // 任务参数 UBaseType_t uxPriority,
UBaseType_t uxPriority, //优先级,数字越大,优先级越大,0到(configMAX_PRIORITIES-1),默认0-24
TaskHandle_t*constpvCreatedTask, //传回来的任务句柄
const BaseType_t xCorelD // 分配在哪个内核上运行(0=Core0,1=Core1,tskNO_AFFINITY=不绑定)
);
/*
静态分配、强制绑定核心(内存地址固定,无动态分配碎片)
- 长期运行、不删除的任务(如设备主循环),避免动态内存碎片;
- 资源受限的系统(如低内存设备),精确控制内存使用;
- 对安全性要求高的场景(内存地址固定,便于调试和审计)
*/
TaskHandle_t xTaskCreateStaticPinnedToCore(
TaskFunction_t pxTaskCode, // 任务函数
const char *pcName, // 任务名称
uint32_t ulStackDepth, // 栈大小(单位:字)
void *pvParameters, // 传递参数
UBaseType_t uxPriority, // 优先级
StackType_t *pxStackBuffer, // 手动分配的栈内存(用户提供)
StaticTask_t *pxTaskBuffer, // 手动分配的TCB内存(用户提供)
BaseType_t xCoreID // 绑定的核心
);
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
动态 vs 静态
- 动态分配:任务的栈和控制块(TCB)由 FreeRTOS 从堆中自动分配,无需手动管理内存,但可能产生内存碎片。
- 静态分配:任务的栈和 TCB 需提前手动定义(如全局数组),内存地址固定,无碎片风险,但需要手动计算内存大小。*
函数 | 核心绑定能力 | 内存分配方式 | 适用场景 |
---|---|---|---|
xTaskCreate | 不绑定,由系统自动分配核心 | 动态(栈和 TCB 从堆分配) | 大多数通用场景,对核心和内存分配无特殊要求,频繁创建 / 删除可能导致内存碎片 |
xTaskCreatePinnedToCore | 强制绑定到指定核心(0 或 1) | 动态(同xTaskCreate ) | 需要固定核心运行的场景(如实时性要求高的任务) |
xTaskCreateStaticPinnedToCore | 强制绑定到指定核心(0 或 1) | 静态(栈和 TCB 需手动提前分配) | 对内存分配有严格控制的场景(如避免碎片、资源受限系统) |
// 示例:
#include <stdio.h>
#include "freertos/FreeRTOS.h" // 引入FreeRTOS
#include "freertos/task.h" // 使用到任务相关 task.h
#include "esp_log.h" // 日志打印需求
#define STACK_SIZE 2048 // 栈大小(字)
StackType_t static_stack[STACK_SIZE]; // 栈内存(全局数组)
StaticTask_t static_tcb; // TCB内存(全局变量)
// 任务函数
void network_task(void *pvParam) {
while (1) {
// 处理网络数据(长期运行,适合静态分配)
process_network_data();
vTaskDelay(pdMS_TO_TICKS(100));
}
}
// 定义任务A
void taskA(void* param){
while(1){
// 打印日志
ESP_LOGI('main标记','hello word 打印的内容');
// 延时500ms,vTaskDelay的参数为系统节拍
// pdMS_TO_TICKS()函数,由FreeRTOS提供,将任意ms转化为系统节拍数
vTaskDelay(pdMS_TO_TICKS(500));
}
}
void sensor_task(void *pvParam) {
while (1) {
// 高频读取传感器数据(需要固定核心保证实时性)
read_sensor_data();
vTaskDelay(pdMS_TO_TICKS(10)); // 每10ms读取一次
}
}
void app_main(void)
{
TaskHandle_t sensor_handle;
// 1.创建任务,强制绑定到Core 1,动态分配内存
xTaskCreatePinnedToCore(
sensor_task, // 任务函数
"Sensor_Task", // 名称
4096, // 栈大小(16KB)
NULL, // 无参数
3, // 高优先级(3)
&sensor_handle, // 任务句柄
1 // 绑定到Core 1
);
// 2. 创建任务:静态分配内存,绑定到Core 0
TaskHandle_t net_handle = xTaskCreateStaticPinnedToCore(
network_task, // 任务函数
"Network_Task", // 名称
STACK_SIZE, // 栈大小(使用提前定义的2048字)
NULL, // 无参数
2, // 优先级2
static_stack, // 手动分配的栈内存
&static_tcb, // 手动分配的TCB内存
0 // 绑定到Core 0
);
}
// demo2
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "driver/gpio.h"
#define LED_PIN GPIO_NUM_4 // 假设LED接在GPIO4
// 任务函数:LED闪烁(必须是无限循环)
void led_task(void *pvParam) {
// 初始化GPIO为输出
gpio_reset_pin(LED_PIN);
gpio_set_direction(LED_PIN, GPIO_MODE_OUTPUT);
while (1) { // 无限循环,任务核心
gpio_set_level(LED_PIN, 1); // 点亮LED
vTaskDelay(pdMS_TO_TICKS(500)); // 延时500ms(释放CPU给其他任务)
gpio_set_level(LED_PIN, 0); // 熄灭LED
vTaskDelay(pdMS_TO_TICKS(500));
}
}
void app_main() { // ESP32程序入口(类似main函数)
TaskHandle_t led_handle; // 定义任务句柄
// 创建任务
BaseType_t ret = xTaskCreate(
led_task, // 任务函数
"LED_Blink", // 任务名称
2048, // 栈大小(2048字=8KB)
NULL, // 无参数
1, // 优先级1(较低)
&led_handle // 获取句柄
);
if (ret == pdPASS) {
printf("LED任务创建成功,句柄:%p\n", led_handle);
} else {
printf("任务创建失败!\n");
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
阻塞
// 延时 xTicksToDelay 个周期,
// 调用后立刻进入阻塞状态,但不一定会在xTicksToDelay 个周期后解除阻塞,需要等待cpu调用
void vTicksToDelay(const TickType_t xTicksToDelay)
// 用于表示精确的解除阻塞时间
void vTaskDelayUntil( TickType_t *pxPreviousWakeTime, const TickType_t xTimelncrement );
cnt = xTaskGetTickCount(); //获取当前系统节拍数
while(1){
vTaskDelayUntil(&cnt,100);
......
}
2
3
4
5
6
7
8
9
10
11
任务通讯与同步
在多任务系统中,多个任务往往需要协作(如传递数据、共享资源、同步执行步骤),如果直接操作全局变量或硬件资源,会导致竞态条件(多个任务同时读写同一资源,导致数据混乱或硬件异常)
队列
队列是 FreeRTOS 中最常用的通信机制,用于在任务间传递数据(如传感器值、命令、事件等),类似 “管道”:一个任务往管道里放数据,另一个任务从管道里取数据
- 核心特征
- 先进先出(FIFO):先放入的数据先被取出(默认,也可配置为后进先出)。
- 异步通信:发送方和接收方可独立运行,无需等待对方(支持阻塞等待)。
- 多发送方 / 多接收方:多个任务可向同一队列发送数据,多个任务也可从同一队列接收数据
- 注意
- 队列发送数据时会拷贝完整数据到队列缓冲区(而非传递指针),因此发送方的数据可以是局部变量
- 队列满 / 空的阻塞:
xQueueSend
在队列满时会阻塞xTicksToWait
时间,xQueueReceive
在队列空时会阻塞,避免 CPU 空转 - 中断中使用:必须用
xQueueSendFromISR
/xQueueReceiveFromISR
,且xTicksToWait
必须为 0(中断中不能阻塞)
操作 | 函数原型 | 功能 |
---|---|---|
创建队列 | QueueHandle_t xQueueCreate(UBaseType_t uxQueueLength, UBaseType_t uxItemSize); | 创建一个队列,指定队列长度(最多存多少个数据)和每个数据的大小(字节) |
发送数据(任务中) | BaseType_t xQueueSend(QueueHandle_t xQueue, const void *pvItemToQueue, TickType_t xTicksToWait); | 向队列发送数据,xTicksToWait 为阻塞等待时间(0 = 不等待,portMAX_DELAY = 永久等待) |
接收数据(任务中) | BaseType_t xQueueReceive(QueueHandle_t xQueue, void *pvBuffer, TickType_t xTicksToWait); | 从队列接收数据,数据存入pvBuffer ,xTicksToWait 为阻塞等待时间 |
中断中发送 | BaseType_t xQueueSendFromISR(QueueHandle_t xQueue, const void *pvItemToQueue, BaseType_t *pxHigherPriorityTaskWoken); | 中断服务程序(ISR)中发送数据 |
中断中接收 | BaseType_t xQueueReceiveFromISR(QueueHandle_t xQueue, void *pvBuffer, BaseType_t *pxHigherPriorityTaskWoken); | 中断中接收数据。 |
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "freertos/queue.h"
#include "esp_log.h"
// 定义数据结构:温湿度
typedef struct {
float temp; // 温度
float humi; // 湿度
} SensorData_t;
QueueHandle_t sensor_queue; // 全局队列句柄
// 任务A:采集传感器数据,发送到队列
void sensor_task(void *pvParam) {
SensorData_t data;
while (1) {
// 模拟读取传感器(实际开发中替换为硬件读取代码)
data.temp = 25.5 + (rand() % 10) / 10.0; // 25.5~26.4℃
data.humi = 60.0 + (rand() % 10) / 10.0; // 60.0~60.9%
// 发送数据到队列,最多等待100ms(队列满时阻塞)
BaseType_t ret = xQueueSend(sensor_queue, &data, pdMS_TO_TICKS(100));
if (ret != pdPASS) {
ESP_LOGE("SensorTask", "队列满,发送失败!");
}
vTaskDelay(pdMS_TO_TICKS(1000)); // 每1秒采集一次
}
}
// 任务B:从队列接收数据,打印
void print_task(void *pvParam) {
SensorData_t data;
while (1) {
// 从队列接收数据,永久等待(队列空时阻塞)
xQueueReceive(sensor_queue, &data, portMAX_DELAY);
ESP_LOGI("PrintTask", "温度:%.1f℃,湿度:%.1f%%", data.temp, data.humi);
}
}
void app_main() {
// 创建队列:长度5(最多存5组数据),每个数据大小为SensorData_t的大小
sensor_queue = xQueueCreate(5, sizeof(SensorData_t));
if (sensor_queue == NULL) {
ESP_LOGE("Main", "队列创建失败(内存不足)!");
return;
}
// 创建两个任务
xTaskCreate(sensor_task, "SensorTask", 2048, NULL, 1, NULL);
xTaskCreate(print_task, "PrintTask", 2048, NULL, 1, NULL);
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
信号量
信号量是一种 “计数器”,用于控制对共享资源的访问(同步)或协调任务执行顺序(同步)
- 二进制信号量(任务同步)
- 本质:只能为 0 或 1,类似开关
- 用途:协调两个任务的执行顺序(如 “任务 A 完成后,任务 B 才能执行”)
- 计数信号量(资源管理)
- 本质:可以是0~N(N为最大整数)
- 用途:管理有限数量的共享资源(如 “2 个 UART 端口,最多允许 2 个任务同时使用”)
- 互斥信号量(解决优先级反转)
- 本质:特殊的二进制信号量,支持优先级继承,解决优先级反转问题
- 何为优先级反转?:低优先级任务持有资源,高优先级任务等待该资源,导致中等优先级任务抢占低优先级任务,高优先级任务长期等待(示例:任务 C(高)等任务 A(低)的资源,任务 B(中)抢占 A,C 一直等)
- 注意:
- 互斥信号量只能由获取它的任务释放(避免其他任务误释放),且不能在中断中使用(中断不能阻塞)
/*
互斥信号量
- 创建:SemaphoreHandle_t xSemaphoreCreateMutex();(初始值 1)
- 释放 / 获取:同二进制信号量(xSemaphoreGive/xSemaphoreTake)
*/
/*
二进制信号量
- 创建:SemaphoreHandle_t xSemaphoreCreateBinary();(初始值为 0)
- 释放(发送信号):BaseType_t xSemaphoreGive(SemaphoreHandle_t xSemaphore);(计数器从 0→1)
- 获取(等待信号):BaseType_t xSemaphoreTake(SemaphoreHandle_t xSemaphore, TickType_t xTicksToWait);(计数器从 1→0,若为 0 则阻塞等待)
- xSemaphore 信号量句柄
- xTicksToWait 超时时间
返回值:pdPASS信号量可用,其它表示获取失败
*/
// demo:任务 A 初始化传感器,完成后通知任务 B 开始采
SemaphoreHandle_t init_sem; // 二进制信号量
// 任务A:初始化传感器,完成后释放信号量
void init_task(void *pvParam) {
ESP_LOGI("InitTask", "开始初始化传感器...");
vTaskDelay(pdMS_TO_TICKS(2000)); // 模拟初始化耗时
ESP_LOGI("InitTask", "初始化完成!");
xSemaphoreGive(init_sem); // 释放信号量(0→1)
vTaskDelete(NULL); // 初始化完成,删除自身
}
// 任务B:等待初始化完成后开始采集
void collect_task(void *pvParam) {
ESP_LOGI("CollectTask", "等待初始化...");
// 等待信号量(最多等5秒,超时则报错)
if (xSemaphoreTake(init_sem, pdMS_TO_TICKS(5000)) == pdPASS) {
ESP_LOGI("CollectTask", "开始采集数据!");
while (1) {
vTaskDelay(pdMS_TO_TICKS(1000)); // 模拟采集
}
} else {
ESP_LOGE("CollectTask", "初始化超时,退出!");
vTaskDelete(NULL);
}
}
void app_main() {
init_sem = xSemaphoreCreateBinary(); // 初始值0
xTaskCreate(init_task, "InitTask", 2048, NULL, 2, NULL);
xTaskCreate(collect_task, "CollectTask", 2048, NULL, 1, NULL);
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
/*
计数信号量
- 创建:SemaphoreHandle_t xSemaphoreCreateCounting(UBaseType_t uxMaxCount, UBaseType_t uxInitialCount);(uxMaxCount为最大计数,uxInitialCount为初始计数)
- 释放:xSemaphoreGive()(计数 + 1)
- 获取:xSemaphoreTake()(计数 - 1,若为 0 则阻塞)
*/
// demo:3 个任务竞争 2 个设备资源,每次最多 2 个任务同时使用
SemaphoreHandle_t device_sem; // 计数信号量
// 任务函数:尝试获取设备资源
void device_task(void *pvParam) {
int task_id = *(int*)pvParam; // 任务ID(1/2/3)
while (1) {
// 尝试获取资源(最多等1秒)
if (xSemaphoreTake(device_sem, pdMS_TO_TICKS(1000)) == pdPASS) {
ESP_LOGI("Task%d", task_id, "获取设备成功,开始使用...");
vTaskDelay(pdMS_TO_TICKS(2000)); // 模拟使用设备
ESP_LOGI("Task%d", task_id, "释放设备");
xSemaphoreGive(device_sem); // 释放资源(计数+1)
} else {
ESP_LOGW("Task%d", task_id, "获取设备失败,重试...");
}
vTaskDelay(pdMS_TO_TICKS(500));
}
}
void app_main() {
// 创建计数信号量:最大2个资源,初始2个可用
device_sem = xSemaphoreCreateCounting(2, 2);
int id1=1, id2=2, id3=3;
xTaskCreate(device_task, "Task1", 2048, &id1, 1, NULL);
xTaskCreate(device_task, "Task2", 2048, &id2, 1, NULL);
xTaskCreate(device_task, "Task3", 2048, &id3, 1, NULL);
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
事件组
事件组是一种同步机制,替代传统的信号量或轮询机制,允许任务或中断通过设置、清除或等待特定标志位来实现协作,处理复杂的多条件同步场景,(如 “任务 C 等待任务 A 完成且任务 B 完成”,或 “任务 C 等待任务 A 完成或任务 B 完成”)
特性 | 事件组 | 信号量 | 互斥锁/互斥信号 |
---|---|---|---|
数据结构 | 32 位标志位集合 | 计数器 | 特殊的二进制信号量 |
适用场景 | 多条件同步 | 资源计数或单条件同步 | 保护临界资源 |
多标志位支持 | ✅ 支持多个标志位 | ❌ 仅单标志 | ❌ 仅单标志 |
等待多个条件 | ✅ 可等待任意 / 所有标志位 | ❌ 只能等待单个信号量 | ❌ 只能等待单个锁 |
自动清除标志 | ✅ 支持退出时自动清除 | ❌ 需要手动操作 | ❌ 需要手动操作 |
任务优先级反转 | ❌ 不存在 | ❌ 普通信号量存在 | ✅ 支持优先级继承 |
- 双核优化:事件组操作(设置 / 清除 / 等待)均为原子操作,ESP32-S3 的双核架构下无需额外同步。
- 低功耗适配:事件组可与 ESP32-S3 的 Light Sleep 模式结合,实现条件唤醒(如等待 Wi-Fi 连接后唤醒)
- 概念
- 事件组是一个 32 位的无符号整数变量
EventBits_t
(某些架构下是 16 位),每一位代表一个事件标志(如 bit0 = 任务 A 完成,bit1 = 任务 B 完成) - 等待事件时可指定 “与”(所有位都置位)或 “或”(至少一位置位)
- 标志位
- 设置标志位(表示某个事件发生)
- 清除标志位(表示事件处理完毕)
- 等待标志位(阻塞直到满足特定条件)
- 优势
- 替代复杂的信号量:当需要多个条件同时满足时,事件组比信号量更灵活。
- 高效的多任务同步:多个任务可以监听同一个事件组的不同标志位。
- 减少任务唤醒次数:通过等待多个标志位,避免任务频繁被唤醒
- 事件组是一个 32 位的无符号整数变量
// 创建事件组(返回句柄指针,失败返回 NULL)
EventGroupHandle_t xEventGroupCreate(void);
// 删除事件组(释放内存) 使用完事件组后
void vEventGroupDelete(EventGroupHandle_t xEventGroup);
// 任务中设置标志位(返回设置前的标志值)
EventBits_t xEventGroupSetBits(
EventGroupHandle_t xEventGroup, // 事件组句柄
const EventBits_t uxBitsToSet // 要设置的位(如 BIT0 | BIT1)
);
// 中断中安全设置标志位(需传递 pxHigherPriorityTaskWoken 用于任务切换)
BaseType_t xEventGroupSetBitsFromISR(
EventGroupHandle_t xEventGroup,
const EventBits_t uxBitsToSet,
BaseType_t *pxHigherPriorityTaskWoken
);
// 任务中清除标志位(返回清除前的标志值)
EventBits_t xEventGroupClearBits(
EventGroupHandle_t xEventGroup,
const EventBits_t uxBitsToClear
);
// 中断中安全清除标志位(不常用,因等待时可自动清除)
/* 等待指定事件标志位
- xClearOnExit=pdTRUE 表示退出时清除位,
- xWaitForAllBits=pdTRUE 表示等待所有位(与),否则等待任一(或)
*/
EventBits_t xEventGroupWaitBits(
EventGroupHandle_t xEventGroup, // 事件组句柄
const EventBits_t uxBitsToWaitFor, // 等待的标志位(如 BIT0 | BIT1)
const BaseType_t xClearOnExit, // 退出时是否清除标志(pdTRUE 自动清除)
const BaseType_t xWaitForAllBits, // pdTRUE=等待所有位,pdFALSE=等待任意位
TickType_t xTicksToWait // 超时时间(portMAX_DELAY 为永久等待)
);
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
// demo:任务 C 等待任务 A(bit0)和任务 B(bit1)都完成后,执行下一步
EventGroupHandle_t event_group;
#define EVENT_A_BIT (1 << 0) // bit0:任务A完成
#define EVENT_B_BIT (1 << 1) // bit1:任务B完成
// 任务A:完成后设置bit0
void task_a(void *pvParam) {
vTaskDelay(pdMS_TO_TICKS(1000)); // 模拟工作
ESP_LOGI("TaskA", "完成,设置事件位");
xEventGroupSetBits(event_group, EVENT_A_BIT); // 置位bit0
vTaskDelete(NULL);
}
// 任务B:完成后设置bit1
void task_b(void *pvParam) {
vTaskDelay(pdMS_TO_TICKS(2000)); // 模拟工作
ESP_LOGI("TaskB", "完成,设置事件位");
xEventGroupSetBits(event_group, EVENT_B_BIT); // 置位bit1
vTaskDelete(NULL);
}
// 任务C:等待bit0和bit1都置位
void task_c(void *pvParam) {
ESP_LOGI("TaskC", "等待任务A和B完成...");
// 等待bit0和bit1(与关系),超时10秒,退出时清除位
EventBits_t bits = xEventGroupWaitBits(
event_group,
EVENT_A_BIT | EVENT_B_BIT, // 等待的位
pdTRUE, // 退出时清除位
pdTRUE, // 等待所有位(与)
pdMS_TO_TICKS(10000) // 超时时间
);
if ((bits & (EVENT_A_BIT | EVENT_B_BIT)) == (EVENT_A_BIT | EVENT_B_BIT)) {
ESP_LOGI("TaskC", "A和B都完成,开始工作!");
} else {
ESP_LOGE("TaskC", "等待超时!");
}
vTaskDelete(NULL);
}
void app_main() {
event_group = xEventGroupCreate();
xTaskCreate(task_a, "TaskA", 2048, NULL, 1, NULL);
xTaskCreate(task_b, "TaskB", 2048, NULL, 1, NULL);
xTaskCreate(task_c, "TaskC", 2048, NULL, 1, NULL);
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
ESP32-s3
从任务调度到内存管理
任务间通信与同步
FreeRTOS 提供多种机制协调任务协作,避免竞态条件:
队列(Queue):用于传递数据或事件通知。例如:传感器数据采集任务 → 数据处理任务。 核心函数:
xQueueCreate()
创建队列,xQueueSend()
发送数据,xQueueReceive()
阻塞接收。信号量(Semaphore):
- 二进制信号量:控制共享资源访问(如互斥锁保护临界区)。
计数信号量:管理有限资源(如多个任务共享打印机设备)。 示例:用互斥信号量保护全局变量访问:
cSemaphoreHandle_t xMutex; // 声明互斥锁 void init_mutex() { xMutex = xSemaphoreCreateMutex(); // 创建互斥锁 } void access_shared_data(int value) { xSemaphoreTake(xMutex, portMAX_DELAY); // 等待锁(阻塞直到获得) // 临界区操作(修改共享变量) xSemaphoreGive(xMutex); // 释放锁 }
1
2
3
4
5
6
7
8
9
10
11事件组(Event Groups):用于多任务同步复杂事件组合(如 “连接 Wi-Fi + 获取传感器校准数据” 后启动主循环)。
4. 中断处理与 RTOS 兼容
ESP32-S3 的硬件中断(如 GPIO 边沿触发、定时器溢出)需与 FreeRTOS 协同:
- 中断安全 API:使用
xQueueSendFromISR()
、xSemaphoreGiveFromISR()
等函数在中断服务程序(ISR)中安全操作队列或信号量。 - 临界区保护:用
taskENTER_CRITICAL()
/taskEXIT_CRITICAL()
临时关闭调度器,避免中断嵌套时任务抢占异常。 - 注意:ISR 应尽可能简短,复杂处理通过队列传递给后台任务完成(中断下半部机制)。
5. 定时器与延迟机制
- 系统节拍(Tick):FreeRTOS 依赖系统定时器(通常是 RTC 或 APB 时钟分频)产生周期性中断驱动调度。ESP-IDF 默认节拍频率为 1000Hz(1ms / 滴答)。
- 软件定时器:创建独立定时器任务(
xTimerCreate
)实现定时回调,可动态启停。 - 任务延迟:
vTaskDelay(pdMS_TO_TICKS(100))
让当前任务阻塞等待 N 个滴答,期间 CPU 可调度其他任务。
四、基于 ESP32-S3 的 FreeRTOS 实战:从基础到项目
项目 1:多任务 LED 闪烁与按键检测
目标:创建两个任务(LED 闪烁、按键检测),通过队列传递按键事件。
步骤:
- 初始化 GPIO(LED 输出、按键输入 + 上拉电阻)。
- 创建 LED 任务(周期性翻转电平)。
- 创建按键扫描任务(阻塞读取 GPIO,检测下降沿后发送队列消息)。
- 主任务接收队列消息并处理(如打印日志或改变 LED 模式)。
代码关键点:
- 使用
xQueueCreate(1, sizeof(int))
创建容量为 1 的按键事件队列。
- 使用
在按键 ISR 中用
xQueueSendFromISR()
安全发送消息。
项目 2:Wi-Fi 连接与数据上报(结合 FreeRTOS 调度)
目标:同时管理网络连接任务和传感器数据采集任务。
实现:
- 创建
wifi_task
处理 STA 模式连接、重连逻辑。 - 创建
sensor_task
周期性读取传感器(如加速度计)并存储数据。 - 主任务定时检查网络状态,当连接成功时通过队列发送数据到云端。
- 创建
线程安全注意:共享网络状态变量(如
is_connected
标志)需用互斥锁保护。
项目 3:低功耗优化(ESP32-S3 深度睡眠 + RTOS 唤醒)
目标:在 FreeRTOS 调度下实现超低功耗模式(如传感器唤醒→处理数据→休眠循环)。
关键操作:
- 使用
vTaskDelayUntil()
实现精确休眠间隔(比vTaskDelay
更节能)。 - 配置 ESP32-S3 进入深度睡眠时关闭未使用外设,依赖 RTC 定时器或 GPIO 中断唤醒任务。
- 使用
参考 API:
esp_sleep_enable_timer_wakeup()
、esp_deep_sleep_start()
。
五、调试与性能优化
监控工具:
- 使用 ESP-IDF 的
vTaskList()
和vTaskGetRunTimeStats()
打印任务状态、栈使用情况及运行时间统计。 - 通过串口日志分析任务调度冲突或资源泄漏。
- 使用 ESP-IDF 的
栈溢出预防: 任务栈大小必须足够容纳函数调用深度和局部变量。建议初始分配比估计大 20%,调试时逐步缩减。
内存碎片化管理:优先使用静态内存分配(
xTaskCreateStatic
)或池化内存(如预分配缓冲区队列)。
六、进阶与扩展学习
多核处理:ESP32-S3 双核支持任务绑定(
xTaskCreatePinnedToCore
指定运行核心),深入理解任务亲和性与负载均衡。FreeRTOS+TCP/IP:学习 LwIP 协议栈集成,实现 HTTP 服务器、MQTT 客户端等网络任务(ESP-IDF 已集成)。
高级同步机制:事件组、任务通知(替代轻量级队列)等进阶通信方法。
社区与资源:
- 官方文档:FreeRTOS 官网 + ESP-IDF 编程指南。
- GitHub 仓库:搜索 ESP32-S3 FreeRTOS 示例项目(如多任务传感器采集、IoT 网关)。
- 论坛:乐鑫社区、FreeRTOS 论坛获取实践问题解答。
七、实习准备与实践建议
理解企业需求:嵌入式实习生常需参与:
- 基于 FreeRTOS 的任务拆分与线程安全设计。
- 调试内存 / 栈溢出问题,优化低功耗场景。
- 使用队列 / 信号量实现模块间解耦。
构建作品集:完成 2-3 个典型项目(如智能环境监测节点、简易智能家居终端),记录关键代码和调试过程。
面试准备:
- 解释 FreeRTOS 调度原理与任务通信机制。
- 分析 ESP32-S3 裸机开发 vs RTOS 开发的优缺点。
- 举例说明如何用队列优化中断处理流程。
总结学习路径
- 基础阶段(1-2 周):掌握环境搭建、任务创建与调度、队列 / 信号量基础使用。
- 强化阶段(2-3 周):通过实战项目理解中断处理、低功耗适配及多核协同。
- 实战阶段(持续):结合具体需求(如 IoT、工业控制)开发小型系统,逐步优化代码质量。
2. ESP-IDF 框架深度解析(10-14 天)
组件化开发:
- 分析官方
examples/get-started/hello_world
工程,理解main
组件、esp_system
库和CMakeLists.txt
构建脚本。 - 尝试新建自定义组件(如
my_gpio
),通过idf.py menuconfig
配置编译选项。
- 分析官方
内存管理:
- 使用
heap_caps_malloc()
分配特定类型内存(如外部 PSRAM),调用heap_caps_free()
释放。 - 通过
esp_get_free_heap_size()
监控内存使用,避免 51 开发中常见的内存泄漏问题。
- 使用
低功耗设计:
- 配置
esp_sleep_enable_timer_wakeup()
实现定时唤醒,对比 51 的掉电模式,理解 ULP 协处理器的应用场景。
- 配置
3. 高级调试技巧(3-5 天)
OpenOCD 调试:
- 连接 JTAG 接口,通过 VS Code 的
launch.json
配置 OpenOCD,实现断点调试、变量监控7。 - 调试案例:在 FreeRTOS 任务中设置断点,观察任务寄存器状态。
- 连接 JTAG 接口,通过 VS Code 的
SystemView 分析:
- 开启
Component config > Application Level Tracing
,通过esp sysview start
命令录制多任务运行轨迹,使用 SystemView 工具分析任务切换耗时。
- 开启
三、实战进阶阶段(4-6 周)
1. 项目实战(2-3 周)
基础项目:
- 温湿度采集系统:使用 DHT11 传感器(I2C 通信)+ OLED 显示屏(SPI 通信),通过 Wi-Fi 上传数据到云端(如 Blynk 平台)。
- 蓝牙遥控器:开发手机 APP 通过 BLE 控制 ESP32 的 GPIO 输出,替代传统红外遥控。
进阶项目:
- AI 语音识别:基于 TensorFlow Lite Micro,在 ESP32-S3 上部署轻量级模型,实现 “开灯”“关灯” 语音指令识别8。
Mesh 网络:使用
esp_wifi_mesh
组件构建多节点温湿度监测网络,学习无线中继与数据路由策略。
2. 官方文档与社区资源(持续学习)
必看文档:
- 《ESP32-S3 技术参考手册》:深入理解外设寄存器映射和时序要求10。
- 《ESP-IDF 编程指南》:掌握 Wi-Fi、蓝牙协议栈的 API 调用逻辑。
社区支持:
- 在乐鑫论坛提问,参考 GitHub 开源项目(如
espressif/esp-idf
)。
- 在乐鑫论坛提问,参考 GitHub 开源项目(如
关注电子工程专辑等技术媒体,学习前沿应用案例1。
四、学习资源推荐
开发板选择:
- 乐鑫官方板:ESP32-S3-DevKitC-1(基础功能齐全,适合入门)。
- 扩展板:搭配乐鑫 ESP32-S3-USB-Bridge 实现 JTAG 调试。
工具链:
- ESP-IDF 离线安装包:乐鑫下载中心。
- 串口调试工具:PuTTY(Windows)或 Minicom(Linux)。
五、避坑指南
内存问题:
- 避免在中断服务函数中调用
malloc
,改用静态缓冲区或队列传递数据。 - 优先使用内部 SRAM(TCM),外部 PSRAM 访问速度较慢。
- 避免在中断服务函数中调用
时钟配置:
- 使用
esp_clk_init()
初始化系统时钟,避免手动设置寄存器导致的时序错误。
- 使用
电源管理:
- 外设供电不足可能导致通信异常,建议为传感器模块单独供电。
通过以上路线,你可在 2-3 个月内从 51 开发者转型为 ESP32-S3 熟练工程师。建议每天投入 2-3 小时,边学边练,重点突破 FreeRTOS 和 ESP-IDF 框架,这两个模块是掌握 ESP32 开发的关键。遇到问题时,优先查阅官方文档和社区资源,逐步培养独立解决问题的能力。
FreeRTOS
二、FreeRTOS 内核核心原理
任务管理
- 任务本质:独立的执行单元(代码 + 堆栈 + 上下文)
- 任务状态机:运行态、就绪态、阻塞态、挂起态(转换逻辑与触发条件)
- 调度器原理:
- 优先级抢占式调度(高优先级任务可打断低优先级任务)
- 时间片轮转调度(同优先级任务的切换机制)
- 调度器钩子函数(
vApplicationTickHook
等扩展点)
- 任务优先级设计原则(避免优先级反转、合理划分任务粒度)
任务间通信与同步
- 队列(Queue):
- 底层实现(环形缓冲区、线程安全机制)
- 应用场景:数据传递(结构体、指针)、异步通知
- 高级特性:队列集(Queue Set)、覆盖写入(Overwrite)
- 信号量(Semaphore):
- 二值信号量(同步事件)、计数信号量(资源计数)
- 与队列的区别(无数据载体,仅做状态标记)
- 互斥锁(Mutex):
- 优先级继承协议(解决优先级反转的核心机制)
- 递归互斥锁(同一任务多次获取不死锁)
- 事件组(Event Group):
- 多事件触发机制(任意事件满足、所有事件满足)
- 与信号量的组合使用(复杂条件同步)
- 队列(Queue):
内存管理
- FreeRTOS 堆管理策略:
- 5 种堆实现(
heap_1
到heap_5
)的适用场景 - 动态内存分配(
pvPortMalloc
)vs 静态分配(xTaskCreateStatic
)
- 5 种堆实现(
- 内存碎片问题:成因、检测(
vPortGetHeapStats
)与优化方案 - 栈管理:
- 任务栈大小计算(静态分析、运行时监控
uxTaskGetStackHighWaterMark
) - 栈溢出防护(
configCHECK_FOR_STACK_OVERFLOW
配置与原理)
- 任务栈大小计算(静态分析、运行时监控
- FreeRTOS 堆管理策略:
定时器
- 软件定时器原理(基于系统滴答定时器,精度与系统 tick 相关)
- 单次定时器 vs 周期定时器(适用场景与资源消耗)
- 定时器服务任务(
Timer Service Task
)的优先级设计
中断管理
- 中断与任务的协作模型:
- 中断服务函数(ISR)的特性(快速执行、禁止阻塞操作)
- 中断安全 API(
xQueueSendFromISR
等带FromISR
后缀的函数)
- 中断优先级:
- FreeRTOS 临界区(
taskENTER_CRITICAL
)的实现原理 - ESP32 中断优先级分组(与 FreeRTOS 系统调用的兼容性)
- FreeRTOS 临界区(
- 中断与任务的协作模型:
内核配置与裁剪
FreeRTOSConfig.h
1
核心参数:
- 任务最大优先级(`configMAX_PRIORITIES`)、系统 tick 频率(`configTICK_RATE_HZ`)
- 低功耗配置(`configUSE_TICKLESS_IDLE` 与 ESP32 睡眠模式协同)
- 功能裁剪原则(按需关闭未使用的模块,减少资源占用)
### **三、ESP32 硬件深度掌握**
1. **芯片架构与核心外设**
- 双核 Xtensa LX6 处理器:架构特点、Cache 机制、指令集(与 RTOS 任务绑定的关联性)
- 存储器系统:
- 内部 RAM(IRAM、DRAM)与外部 Flash 的布局(程序存放、数据分配)
- 存储器保护单元(MPU)的作用(内存访问权限控制)
- 时钟与电源管理:
- 时钟树(PLL 配置、外设时钟源选择)
- 低功耗模式(Light Sleep、Deep Sleep)与 FreeRTOS 协作
2. **关键外设驱动原理**
- **GPIO**:复用功能、中断配置(边沿 / 电平触发)、内部上拉 / 下拉
- 通信接口
:
- SPI(硬件 FIFO、DMA 传输、QSPI 扩展)
- I2C(总线仲裁、超时处理、多设备共存)
- UART(硬件流控 RTS/CTS、DMA 大数据传输)
- 定时器与 PWM
:
- 通用定时器(`timer_group`)与 LEDC(PWM 精度控制)
- 捕获比较模式(输入捕获测频率、输出比较生成波形)
- Wi-Fi / 蓝牙
:
- 射频原理(信道、功率控制)
- 协议栈架构(与 FreeRTOS 任务的交互方式)
3. **ESP32 特有功能**
- 双核任务绑定(`xTaskCreatePinnedToCore`):负载均衡策略
- 硬件加速模块(加密引擎、ADC/DAC 校准)
- 看门狗定时器(WDT):任务喂狗机制(防止系统卡死)
### **四、工程开发与调试能力**
1. **ESP-IDF 开发框架**
- 构建系统:CMake 构建流程(`CMakeLists.txt` 配置、组件管理)
- 组件化开发:自定义组件设计、依赖管理
- menuconfig 配置系统(内核裁剪、外设参数调整)
2. **调试工具与技巧**
- 基础调试:GDB 远程调试、OpenOCD 与 JTAG 接口
- 高级调试:
- 任务状态分析(`vTaskList`、`vTaskGetRunTimeStats`)
- 内存监控(`heap_trace` 检测泄漏、`malloc_stats`)
- 中断跟踪(`esp_intr_dump`)
- 硬件工具:逻辑分析仪(SPI/I2C 时序验证)、示波器(信号完整性分析)
3. **系统性能优化**
- CPU 利用率优化:任务阻塞时间最大化、减少空循环
- 功耗优化:
- Tickless 模式(`configUSE_TICKLESS_IDLE`)配置与原理
- 外设低功耗策略(Wi-Fi / 蓝牙休眠、外设时钟关闭)
- 代码优化:指令缓存(ICache)、数据缓存(DCache)的利用
### **五、ESP32 外设与通信协议实战**
1. **有线通信**
- UART:高速通信(1Mbps+)、中断 / DMA 模式选择
- SPI:
- 硬件 SPI 与软件模拟的对比(速度、资源占用)
- 多设备共享总线(片选信号管理、时序匹配)
- I2C:
- 总线拉电阻设计(4.7kΩ 选择依据)
- 从机地址冲突解决(地址重映射、软件过滤)
2. **无线通信**
- Wi-Fi:
- STA/AP/STA+AP 模式配置(`wifi_config_t` 深入解析)
- 连接管理:自动重连、信号强度检测、漫游(Roaming)
- 高级特性:802.11n 协议(MIMO 技术)、功耗控制(`wifi_set_sleep_type`)
- 蓝牙(BLE):
- GATT 协议栈(Service、Characteristic 设计)
- 广播与扫描(低功耗策略、数据长度限制)
- 与手机 App 通信( Nordic nRF Connect 调试)
3. **定时器与 PWM 高级应用**
- LEDC 外设:多级调光、呼吸灯(硬件渐变原理)
- 通用定时器:输入捕获(脉冲计数、频率测量)
### **六、项目实战与架构设计**
1. **典型应用场景**
- 物联网(IoT)设备:
- MQTT 协议对接(`esp-mqtt` 组件)
- OTA 升级(分区表设计、安全校验)
- 传感器数据采集:
- 多传感器并发采集(任务划分与同步)
- 数据滤波与校准(滑动平均、中位数滤波)
- 人机交互:按键(消抖、长按 / 短按识别)、显示屏(SPI/OLED 驱动)
2. **系统架构设计**
- 分层架构:应用层、业务逻辑层、驱动层、硬件抽象层(HAL)
- 状态机设计:复杂流程的状态管理(避免 goto 与深层嵌套)
- 模块化原则:高内聚、低耦合(接口设计、依赖注入)
3. **可靠性设计**
- 异常处理:断言(`assert`)、错误码机制、故障恢复( watchdog 复位)
- 数据安全:Flash 数据加密(`nvs_flash` 加密)、通信加密(TLS/DTLS)
- 抗干扰设计:电源滤波、信号完整性(布线原则)
### **七、扩展能力与行业视野**
1. **嵌入式 Linux 与 RTOS 结合**
- ESP32-S3 等高端型号的 Linux 支持(Buildroot 构建)
- 混合架构:RTOS 处理实时任务 + Linux 处理复杂应用(通信桥接)
2. **工具链与自动化**
- 持续集成(CI/CD):GitLab CI 自动编译、单元测试
- 代码质量:静态代码分析(`cppcheck`)、代码覆盖率(`gcov`)
3. **行业知识**
- 消费电子:低功耗设计、用户体验优化
- 工业控制:实时性保障、抗干扰设计
- 物联网:云平台对接(阿里云、AWS IoT)、边缘计算
## 开发思路
- UART 串口
- 结合前端网站/app 实现硬件连接后,修改设备配置信息【类似 key按键的配置】
- 网页版串口调试工具
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
实操记录
DHT11
- 数字温湿度传感器,含已校准数字信号输出的温湿度复合传感器
- 包含一个电容式感湿元件、一个NTC测温元件、一个高性能8位单片机
- 工作电流约:0.5mA
- 测量范围:温度 -20-60 ±2°c 湿度 5-95 ±5%RH
- 分辨率:温度 0.1°c 湿度 1%
- 过程回顾
- 对于时序要求较严格的内容,任何一个无关的内容都将影响最终结果【特别注意额外的日志,排查很久】
- gpio_get_level 函数使用前,要求引脚为 输入 模式,否则获取一直为0
- 处处应严谨,特别是对错误的预处理控制/日志输出,否则一旦失败,再排查将是巨大工作量!
#include "esp_log.h"
#include "esp_timer.h"
#include "driver/gpio.h"
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
// 定义DHT11引脚
#define DHT11_PIN GPIO_NUM_4 // DHT11数据引脚:GPIO6
#define DHT11_TAG "DHT11"
// 初始化
void dht11_init(gpio_num_t pin)
{
// 1. 配置GPIO
gpio_config_t io_conf = {
.pin_bit_mask = (1ULL << pin), // 配置目标引脚
.mode = GPIO_MODE_OUTPUT_OD, // 输出模式-开漏输出
.pull_up_en = GPIO_PULLUP_ENABLE, // 上拉
.pull_down_en = GPIO_PULLDOWN_DISABLE, // 禁用下拉
.intr_type = GPIO_INTR_DISABLE // 禁用中断
};
gpio_config(&io_conf); // 应用配置
}
// 发送开始信号
void dht11_start(gpio_num_t pin)
{
gpio_set_level(pin, 0);
vTaskDelay(pdMS_TO_TICKS(20)); // 1. 起始信号低电平持续时间
gpio_set_level(pin, 1);
esp_rom_delay_us(20);
gpio_set_direction(pin, GPIO_MODE_INPUT);
}
// 监测从机响应信号
int8_t dht11_check(gpio_num_t pin)
{
// 等待DHT11响应(80us低电平)
int16_t timeout = 100;
while (gpio_get_level(pin) == 1 && timeout--)
{
esp_rom_delay_us(1);
}
if (timeout == 0)
{
ESP_LOGE(DHT11_TAG, "等待低电平响应超时");
return 0;
}
// 等待DHT11拉高(80us高电平)
timeout = 100;
while (gpio_get_level(pin) == 0 && timeout--)
{
esp_rom_delay_us(1);
}
if (timeout <= 0)
{
ESP_LOGE(DHT11_TAG, "等待高电平响应超时");
return 0;
}
// ESP_LOGI(DHT11_TAG, "从机响应成功,%d", gpio_get_level(pin)); 影响时序,导致结果失败,留作教训!
timeout = 200;
while (gpio_get_level(pin) == 1 && timeout--)
{
esp_rom_delay_us(1);
}
if (timeout <= 0)
{
ESP_LOGE(DHT11_TAG, "等待低电平响应超时");
return 0;
}
return 1; // 成功响应
}
// 读取从机发送的1 bit数据
uint8_t dht11_read_bit(gpio_num_t pin)
{
int16_t time = 0;
// 计时高电平持续时间
while (gpio_get_level(pin) == 0 && time < 100)
{
time++;
esp_rom_delay_us(1);
}
if (time >= 100){
ESP_LOGE(DHT11_TAG, "等待高电平响应超时,%d", time);
return 0; // 超时
}
time = 0;
while (gpio_get_level(pin) == 1 && time < 100)
{
time++;
esp_rom_delay_us(1);
}
if (time >= 100)
{
ESP_LOGE(DHT11_TAG, "等待低电平响应超时,%d", time);
return 0;
}
// DHT11定义:高电平26-28us为0,70us为1
return (time > 40) ? 1 : 0; // 40us作为阈值
}
// 读取从机发送的1字节数据
uint8_t dht11_read_data(gpio_num_t pin)
{
uint8_t i, data = 0;
for (i = 0; i < 8; i++)
{
data <<= 1;
data |= dht11_read_bit(pin); // 读取每一位数据
}
return data;
}
uint8_t dht11_check_data(gpio_num_t pin)
{
uint8_t i, buf[5] = {0};
dht11_init(pin);
dht11_start(pin);
if (!dht11_check(pin))
{
ESP_LOGE(DHT11_TAG, "连接失败: DHT11传感器未响应"); // 错误日志
return 0;
}
for (i = 0; i < 5; i++)
{
buf[i] = dht11_read_data(pin);
}
if (buf[0] + buf[1] + buf[2] + buf[3] == buf[4])
{
ESP_LOGI(DHT11_TAG, "温度: %d.%d, 湿度: %d.%d", buf[2], buf[3], buf[0], buf[1]);
return 1; // 返回1表示数据读取成功
}
ESP_LOGE(DHT11_TAG, "数据校验失败:%d, %d, %d, %d, %d", buf[0], buf[1], buf[2], buf[3], buf[4]);
return 0;
}
void app_main(void)
{
while (1)
{
vTaskDelay(pdMS_TO_TICKS(1000)); // 上电后等待1秒稳定
dht11_check_data(DHT11_PIN);
vTaskDelay(pdMS_TO_TICKS(5000)); // 添加5秒延时
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151