ESP32-S3 学习笔记:ESP-IDF、FreeRTOS 与常用外设开发
ESP32-S3 学习笔记:ESP-IDF、FreeRTOS 与常用外设开发
本文整理自个人 ESP32-S3 学习笔记,覆盖开发环境搭建、程序结构、FreeRTOS 基础、GPIO/中断/定时器/PWM/ADC/UART/I2C/SPI/SD 卡、RAM 与 Flash、Wi-Fi STA/AP、SNTP 时间同步以及网页图传等内容。适合作为 ESP-IDF 入门到常用外设开发的速查笔记。
适用环境
- 开发框架:ESP-IDF v5.2
- 开发板/芯片:ESP32-S3
- 主要系统:Windows、Ubuntu 20.04
- Python 环境:Python 3.8
前言
ESP32-S3 集成 Wi-Fi、Bluetooth LE、丰富的 GPIO 矩阵和常用外设,非常适合物联网终端、传感器采集、网页控制、图传和轻量级边缘计算场景。本文按学习路径从基础环境、RTOS 并发模型到外设驱动逐步展开,重点记录常用 API、配置项和实践注意点。
原始环境备注:# 1 需要在vs;- 选用的Ubuntu 20.04;- 默认python环境3.8;- 选用的ESP32为v5.2
1.1 前言——概述
1 ESP32-S3内部结构讲解
1.1 芯片
1.1.1 介绍

- RAM(随机存取存储器),临时工作台,用来跑程序的
- 如果RAM不够用了,可以加装PSRAM
- ROM(只读存储器),用来存程序的
- 如果ROM不够用了,那就加装ROM,叫做FLASH
1.1.2 芯片命名规则

1.2 模组
1.2.1 模组命名规则

1.2.2 模组引脚定义


1.2.3 GPIO交换矩阵

- GPIO交换矩阵可以让任意外设与任意引脚相连,缺点是速度较慢
- IO MUX可以让速度较快的串口与引脚相连
2 ESP32-S3 核心板原理图
2.1 复位按键
-
高电平时芯片开启,低电平时芯片关闭

-
模组复位引脚触发的复位为芯片复位也就是全部重启
-

2.2 BOOT电路
- 用于在模组启动时,引导模组进入不同的模式
2.2.1 strapping引脚
- Strapping引脚是特殊的引脚,芯片在上电启动时,会先检测Strapping引脚的电平状态,依据其电平状态,进行不同的配置操作。



2.2.2 BOOT模式的工作原理
-
GPIO0和GPIO46共同决定模组上电时的启动模式

-
SPI Boot模式下,芯片正常读取程序并启动
-
Download Boot模式下,用户可以通过USB或UART0将编译好的二进制文件下载到Flash中。
2.2.3 BOOT模式的工作原理图

)
2.3 JTAG电路
- JTAG接口是一种国际标准测试协议,主要用于芯片内部测试。
- ESP32-S3内部集成JTAG接口,但是需要对JTAG接口的信号源(输入信号)进行选择。
- JATG信号源:
- 正常情况下:USB串口/内部JTAG控制器
- 异常情况:外部JTAG调试器


2.4 SDCARD电路
- 核心板预留了SD卡的接口,SD卡通过SPI接口与模组进行通信

2.5 LED指示灯

2.6 电源电路

2.7 串口电路
- 左边是type-c接口,用于连接电脑和核心板,右边是usb转串口芯片

2.8 USB电路

1.2 前言——搭建环境(Windows)
1 安装ESP-IDF底层配置
- 将espressif-ide以管理员身份运行
- 如果报错则选择修复
- 其他正常点下一步即可
2 定义编译程序时所需要的环境变量(IDF_PATH 、IDF_TOOLS_PATH)
-下载完软件后自动下载了环境变量
-
新建IDF_PATH环境变量

-
找到刚才ESP-IDF的下载路径

3 安装VSCode
4 安装必要的VSCODE插件(CC++插件/CHINESE插件)
- c/c++
- c/c++ Extension Pack
5 安装ESP-IDF插件
- 安装特定版本1.10.2版本
6 添加ESP-IDF底层配置
- 配置ESP-IDF


- 修改两个地址到对应路径后点击安装

7 安装CH340驱动
8 新建一个项目

1.3 前言——搭建环境(linux)
1 搭建Ubuntu 20.04环境(SSH连接)
# 下载网络工具,方便ssh控制
sudo apt-get install net-tools
# 用ifconfig来看ip地址
ifconfig

# 安装各种必要的工具
sudo apt-get install git wget flex bison gperf python3-pip python3-venv cmake ninja-build ccache libffi-dev libssl-dev dfu-util libusb-1.0-0 net-tools
# 新建esp32目录
mkdir esp32
cd esp32
# 拉取gitee工具
git clone https://gitee.com/EspressifSystems/esp-gitee-tools.git
# 执行gitee工具切换镜像脚本
cd esp-gitee-tools
./jihu-mirror.sh set
# 拉取ESP-IDF源码
cd ..
git clone --recursive https://github.com/espressif/ESP-IDF.git
ESP32所有的大版本是不兼容的,这里是基于v5.2进行开发的,所以要切换到5.2
# 切换ESP-IDF版本分支到v5.2
cd ESP-IDF
git checkout v5.2
# 相应的子模块也切换到版本上
# 如果提示失败或有错误试下这句:../esp-gitee-tools/submodule-update.sh
git submodule update --init --recursive
# 更换pip源
pip config set global.index-url http://mirrors.aliyun.com/pypi/simple
pip config set global.trusted-host mirrors.aliyun.com
现在还需要一些编译工具
# 安装编译工具
../esp-gitee-tools/install.sh
# 设置环境变量并将环境变量放到.bashrc中
source export.sh
echo "source ~/esp32/ESP-IDF/export.sh" >> ~/.bashrc
拉取课程源码
# 下载课程配套源码
cd ~/esp32
git clone --recursive https://gitee.com/vi-iot/esp32-board.git
编译
# 编译
cd esp32-board/helloworld
idf.py build
插线后将设备连接到虚拟机,执行烧录

# 烧录
idf.py flash

如果提示没有权限,那就增加权限
# 改设备文件为最大权限
sudo chmod 777 /dev/ttyUSB0
# 重新烧录
idef.py flash
显示done,则烧录成功
通过下面代码看调试
# 看调试
idf.py monitor # 该程序为每隔一秒打印hello world,按ctrl+】退出
2 vscode 设置
2.1安装插件
1、 Chinese(Simplified)(简体中文)
2、 Remote - SSH # 用于远程登录到Ubuntu的插件
3、 C/C++(安装在虚拟机里的)
4、 ESP-IDF(安装在虚拟机里的)
2.2 如何远程连接Ubuntu的SSH







2.3 打开进行操作




2.4 使用ESP-IDF的插件

2.5 减少c/c++ cpu占用

3 永久环境变量搭建
cd ~
ls -al
# 找到.profile,这个是终端执行后默认执行的语句
# 开始编辑
vim .profile
# 在文件末端加入以下代码
source ~/esp32/ESP-IDF/export.sh
# 保存退出
wq
# 重新启动
exit
r
4 设置永久的串口权限
# 设置USB串口权限
sudo usermod -aG dialout username #username换成自己的使用用户名
5 目录介绍

6 工程配置
cd ~/esp32/esp32-board/helloworld/
idf.py menuconfig
# 工作主频->默认 160Mhz,ESP32 最高支持 240Mhz
Component config
ESP System Settings
CPU frequency (240 MHz)
# Flash 大小->默认 2M,开发板上的模组是 4M Flash
Serial flasher config
Flash size (4 MB)
# PSRAM->默认没有,开发板上的模组是 2M PSRAM
Component config
ESP PSRAM
启用 Support for external, SPI-connected RAM
# 修改完成后,按 s,回车保持,q 退出,输入 idf.py build 重新编译
sq
1.4 前言——程序架构与执行
1 基础工程目录

2 程序存储于结构和启动流程
2.1 存储结构
- 在使用idf.py build 编译后,编译器会根据我们的代码区分出指令总线和数据总线
- 指令总线是可以执行的,比如定义的部分函数
- 数据总线是不可以执行的,只能通过字节操作访问比如我们的全局变量

2.2 启动流程

3 Linux下运行
3.1 创建文件
# 运用idf.py 创建工程,后面跟的是工程的名字 ,确认建立的芯片是s3的芯片否则不用这个选项
idf.py create-project --target esp32s3 test_task
# 用build进行编译
idf.py build
3.2 调试和执行
idf.py flash monitor
4 运行helloworld
4.1 打印函数介绍
// 导入打印函数头文件
#include "esp_log.h"
// I 代表information,信息,打印的信息是绿色的
ESP_LOGI("main","HelloWorld!\r\n");
// E 代表error,错误,打印的信息是红色的
ESP_LOGE("main","HelloWorld!\r\n");
// 打印的是灰色的
printf("HelloWorld!\r\n")
4.2 时钟频率设置
4.2.1 Serial Flasher config – 配置封装外Flash,此处与所选用的芯片对应,因为用的是Quid SPI所以用QIO
- 设置flash的速度和大小
4.2.2 ESP PSRAM – 配置封装内PSRAM
4.2.3 ESP System Setting – ESP32系统配置(主频)
4.2.4 FreeRTOS – 配置时钟节拍
4.3 示例
#include <stdio.h>
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "esp_log.h"
void app_main(void)
{
while (1)
{
ESP_LOGI("APP_MAIN", "Hello world!");
ESP_LOGE("APP_MAIN", "Hello world!");
printf("Hello world!\n");
vTaskDelay(500 );
}
}
1.5 前言——模块化配置
1 创建components 文件夹
2 编写.c文件
3 编写.h 文件


4 编写CMakelsits文件

2.1 FreeRTOS学习——任务
1. 任务创建
ESP-IDF 是基于 FreeRTOS 的框架,里面用到的组件,以及我们的应用程序都是基于FreeRTOS来开发的,因此我们必须掌握 FreeRTOS 的用法。如果我们不深究原理,只关注于 FreeRTOS的接口使用,我们很快就能掌握。另外,因为 FreeRTOS 开源免费的特性,目前大部分芯片产商做的 SDK 都是基于 FreeRTOS 系统开发的,因此我们就更有理由要学习 RTOS 了。
1.1 概述
使用 FreeRTOS 的实时应用程序可以被构建为一组独立的任务。每个任务在自己的上下文中执行,不依赖于系统内的其他任务或 RTOS 调度器本身。任务分为四个状态:运行、准备就绪、阻塞、挂起,状态转换如下
1.2 任务存在的状态
运行:
当任务实际执行时,它被称为处于运行状态。 任务当前正在使用处理器。 如果运行RTOS 的处理器只有一个内核, 那么在任何给定时间内都只能有一个任务处于运行状态。
准备就绪:
准备就绪任务指那些能够执行(它们不处于阻塞或挂起状态),但目前没有执行的任务,因为同等或更高优先级的不同任务已经处于运行状态。
阻塞:
如果任务当前正在等待时间或外部事件,则该任务被认为处于阻塞状态。例如,如果一个任务调用 vTaskDelay(),它将被阻塞(被置于阻塞状态),直到延迟结束-一个时间事件。任务也可以通过阻塞来等待队列、信号量、事件组、通知或信号量事件。处于阻塞状态任务通常有一个"超时"期, 超时后任务将被超时,并被解除阻塞,即使该任务所等待的事件没有发生。“阻塞”状态下的任务不使用任何处理时间,不能被选择进入运行状态。
挂起:
与“阻塞”状态下的任务一样, “挂起”状态下的任务不能被选择进入运行状态,但处于挂起状态的任务没有超时。相反,任务只有在分别通过 vTaskSuspend() 和 xTaskResume()API 调用明确命令时 才会进入或退出挂起状态。
优先级:每个任务均被分配了从 0 到 ( configMAX_PRIORITIES - 1 ) 的优先级,其中的configMAX_PRIORITIES 在 FreeRTOSConfig.h 中定义,低优先级数字表示低优先级任务。 空闲任务的优先级为零。
1.3 调用的API
1.3.1 任务创建
这个是乐鑫自己创建的,原生freertos是没有的
BaseType_t xTaskCreatePinnedToCore(
TaskFunction_t pvTaskCode, //任务函数指针,原型是 void fun(void *param)
const char *constpcName, //任务的名称,打印调试可能会有用
const uint32_t usStackDepth,//指定的任务堆栈空间大小(字节)(最小2048个字节)
void *constpvParameters, //任务参数
UBaseType_t uxPriority, // 优 先 级 , 数 字 越 大 , 优 先 级 越 大 , 0 到(configMAX_PRIORITIES - 1) 前面一个是宏的意思,为25.也就是最大可以到24
TaskHandle_t *constpvCreatedTask, //传回来的任务句柄
const BaseType_t xCoreID) //分配在哪个内核上运行,因为ESP32是双核,所以这里可以是0或者1
// 如果创建成功,则返回pdPASS,否则传回相应错误码
1.3.2 阻塞
// 延时xTicksToDelay个周期
void vTaskDelay( const TickType_t xTicksToDelay )
// 用于表示精确的接触阻塞时间
void vTaskDelayUntil(TickType_t*pxPreviousWakeTime,const TickType_t xTimeincrement);
cnt = xTaskGetTickCount();//获取当前系统节拍数
while(1)
{
vTaskDelayUntil(&cnt,100);
……
}
1.3.3 示例
#include <stdio.h>
// 调用FreeRTOS接口
#include "freertos/FreeRTOS.h"
// 调用任务的头文件
#include "freertos/task.h"
// 用来打印提示
#include "esp_log.h"
void taskA(void* param)
{
while(1)
{
// 打印的函数,方便查找打印日志,后面是打印的内容
ESP_LOGI("main","Hello world!");
// vTaskDelay()里的值是系统节拍
// FreeRTOS提供的pdMs_TO_TICKS(),可以把ms转化为系统节拍
vTaskDelay(pdMs_TO_TICKS(500));
}
}
void app_main(void)
{
xTaskCreatePinnedToCore(taskA,"helloworld",2048,NULL,3,NULL,1)
}
2.2 FreeRTOS学习——队列操作
1 任务间同步
- RTOS中的同步,是指是不同任务之间或者任务与外部事件之间的协同工作方式确保多个并发执行的任务按照预期的顺序或时机执行。它涉及到线程或任务间的通信和协调机制,目的是为了避免数据竞争、解决竞态条件,并确保系统的正确行为。
- 互斥是指某一资源同时只允许一个访问者对其进行访问,具有唯一性和排它性。
1.1 队列
队列是任务间通信的主要形式。 它们可以用于在任务之间以及中断和任务之间发送消息。 在大多数情况下,它们作为线程安全的 FIFO(先进先出)缓冲区使用,新数据被发送到队列的后面, 尽管数据也可以发送到前面。
1.1.1 API
// 创建一个队列
QueueHandle_t xQueueCreate(
UBaseType_t uxQueueLength, // 队列容量
UBaseType_t uxItemSize // 每个队列项所占内存的大小(单位是字节)
);
// 向队列头部发送一个消息
BaseType_t xQueueSend(
QueueHandle_t xQueue, // 队列句柄
const void * pvItemToQueue, // 要发送的消息指针,是拷贝不是发送
TickType_t xTicksToWait // 等待时间,如果在等待时间内没有写入就会返回一个FALSE
);
// 向队列尾部发送一个消息
BaseType_t xQueueSendToBack(
QueueHandle_t xQueue, // 队列句柄
const void * pvItemToQueue, // 要发送的消息指针
TickType_t xTicksToWait // 等待时间
);
// 从队列接收一条消息
BaseType_t xQueueReceive(
QueueHandle_t xQueue, // 队列句柄
void * pvBuffer, // 指向接收消息缓冲区的指针。
TickType_t xTicksToWait // 等待时间
);
// xQueueSend 的中断版本
BaseType_t xQueueSendFromISR(
QueueHandle_t xQueue, // 队列句柄
const void * pvItemToQueue, // 要发送的消息指针
BaseType_t *pxHigherPriorityTaskWoken
// 如果发送到队列导致任务解除阻塞,且解除阻塞的任务的优先级于当前运的任务
// 则xQueueSendFromlSR()会将*pxHigherPriorityTaskWoken设置为pdTRUE
// 如果xQueueSendFromlSR()将此值设置为pdTRUE,则应在中断退出前要执请求上下文切换函数
);
1.1.1 示例
#include <stdio.h>
// 调用FreeRTOS接口
#include "freertos/FreeRTOS.h"
// 调用任务的头文件
#include "freertos/task.h"
// 调用队列头文件
#include "freertos/queue.h"
// 用来打印提示
#include "esp_log.h"
#include <string.h>
// 定义一个队列句柄
// FreeRTOS 给 「队列句柄」起的类型别名
// 规定这个变量是用来存「队列句柄」的
QueueHandle_t queue_handle = NULL;
// 定义一个结构体
typedef struct
{
int value;
}queue_data_t;
// 定义任务一
// 从队列里面接受数据,并打印
void taskA(void* param)
{
queue_data_t data;
while(1)
{
// 如果成功接受数据,就返回pdTRUE
// 失败了就返回pdFALSE
if(pdTRUE == xQueueReceive(queue_handle,&data,100))
{
ESP_LOGI("queue","reveive queue value:%d",data.value);
}
}
}
// 定义任务二
// 每隔一秒,向队列里发送数据
void taskB(void *param)
{
queue_data_t data;
// 对存放数据的进行初始化
/*
void *memset(
void *ptr, // 要操作的内存起始地址(黑板的位置)
int value, // 要批量写入的值(比如0,相当于“擦成空白”)
size_t num // 要操作的内存大小(单位:字节,也就是黑板的长度)
);
*/
memset(&data,0,sizeof(queue_data_t));
while(1)
{
xQueueSend(queue_handle,&data,100);
// 延时
vTaskDelay(pdMS_TO_TICKS(1000));
// 数据递增
data.value++;
}
}
void app_main(void)
{
// 新建一个队列
queue_handle = xQueueCreate(10,sizeof(queue_data_t)
xTaskCreatePinnedToCore(taskA,"taskA",2048,NULL,3,NULL,1);
xTaskCreatePinnedToCore(taskB,"taskB",2048,NULL,3,NULL,1);
);
}
2.3 FreeRTOS学习——信号量
1 概念、
信号量是用来保护共享资源不会被多个任务并发使用
- 这个图中,有一个雨伞池,里面一共有六把雨伞,上面的人是正在使用雨伞的人,当使用人数达到 6 人时雨伞已经没有了,下面是等待使用雨伞的人,因为没有雨伞了,他们只能等待,当有人还雨伞的时候,他们其中一人就可以拿雨伞来用。
- 这个图里面,雨伞池是我们的信号量句柄,雨伞是信号量,而用户就是任务,任务可以等待是否取得信号量,再去继续执行后续操作,保证资源的正确访问,当使用完信号量后,需要释放信号量。
- 信号量使用起来比较简单。因为在 FreeRTOS 中它本质上就是队列,只不过信号量只关心队列中的数量而不关心队列中的消息内容,在 FreeRTOS 中有两种常用的信号量,一是计数信号量,而是二进制信号量。
- 二进制信号量很简单,就是信号量总数只有 1,也就是这个图中总雨伞数量只有 1。
- 与二进制类似的概念有一个叫互斥锁
- 但与二进制信号量不同的是,他会发生优先级翻转。比如当任务c占用信号量的时候等级更高的任务A来了之后因为信号量不够而堵住时可能会先执行不占用任务量的任务B,那此时互斥锁会提升任务C的等级到A的等级,从而先调用任务A
- 计数信号量则可以自定义总共的信号量
- 二进制信号量很简单,就是信号量总数只有 1,也就是这个图中总雨伞数量只有 1。
2 API
//创建二值信号量,成功则返回信号量句柄(二值信号量最大只有 1 个)
SemaphoreHandle_t xSemaphoreCreateBinary(void);
//创建计数信号量,成功则返回信号量句柄
SemaphoreHandle_t xSemaphoreCreateCounting(
UBaseType_t uxMaxCount, //最大信号量数
UBaseType_t uxInitialCount//初始信号量数
);
//获取一个信号量,如果获得信号量,则返回 pdTRUE
xSemaphoreTake(SemaphoreHandle_t xSemaphore,//信号量句柄
TickType_t xTicksToWait//等待时间
);
//释放一个信号量
xSemaphoreGive(
SemaphoreHandle_t xSemaphore//信号量句柄
);
//删除信号量
void vSemaphoreDelete(SemaphoreHandle_t xSemaphore );
//创建一个互斥锁
SemaphoreHandle_t xSemaphoreCreateMutex( void )
3 示例
#include <stdio.h>
// 调用FreeRTOS接口
#include "freertos/FreeRTOS.h"
// 调用任务的头文件
#include "freertos/task.h"
// 调用队列头文件
#include "freertos/queue.h"
// 用来打印提示
#include "esp_log.h"
//信号量里与互斥锁有关的头文件
#include "freertos/semphr.h"
// ESP-IDF 框架下的 GPIO 驱动头文件
#include "driver/gpio.h"
//DHT11 温湿度传感器的驱动封装头文件
#include "dht11.h"
#include <string.h>
//二进制信号量
static SemaphoreHandle_t s_testBinSem;
//计数信号量
static SemaphoreHandle_t s_testCountSem;
//互斥信号量
static SemaphoreHandle_t s_testMuxSem;
void sem_taskA(void* param)
{
const int count_sem_num = 5;
while(1)
{
//向二值信号量释放信号
xSemaphoreGive(s_testBinSem);
//向计数信号量释放5个信号
for(int i = 0;i < count_sem_num;i++)
{
xSemaphoreGive(s_testCountSem);
}
//向互斥信号量释放信号
xSemaphoreGiveRecursive(s_testMuxSem);
vTaskDelay(pdMS_TO_TICKS(2000));
}
}
void sem_taskB(void* param)
{
BaseType_t ret = 0;
while(1)
{
//无限等待二进制信号量,直到获取成功才返回
ret = xSemaphoreTake(s_testBinSem,portMAX_DELAY);
if(ret == pdTRUE)
ESP_LOGI(TAG,"take binary semaphore");
//接收计数信号量,每次接收200ms,直到接收失败才结束循环
int sem_count = 0;
do
{
ret = xSemaphoreTake(s_testCountSem,pdMS_TO_TICKS(200));
if(ret==pdTRUE)
{
ESP_LOGI(TAG,"take count semaphore,count:%d\r\n",++sem_count);
}
}while(ret ==pdTRUE);
//无限等待互斥信号量,直到获取成功才返回,这里用法和二进制信号量极为类似
ret = xSemaphoreTakeRecursive(s_testMuxSem,portMAX_DELAY);
if(ret == pdTRUE)
ESP_LOGI(TAG,"take Mutex semaphore");
}
}
/** 初始化信号量例程
* @param 无
* @return 无
*/
void rtos_sem_sample(void)
{
s_testBinSem = xSemaphoreCreateBinary();
s_testCountSem = xSemaphoreCreateCounting(5,0);
s_testMuxSem = xSemaphoreCreateMutex();
xTaskCreatePinnedToCore(sem_taskA,"sem_taskA",2048,NULL,3,NULL,1);
xTaskCreatePinnedToCore(sem_taskB,"sem_taskB",2048,NULL,3,NULL,1);
}
2.4 FreeRTOS学习——事件组和直达任务
1. 事件组
事件位:用于指示事件是否发生,事件位通常称为事件标志
事件组:就是一组事件位。 事件组中的事件位通过位编号来引用
下图表示一个 24 位事件组, 使用 3 个位来保存前面描述的 3 个示例事件
1.1 API
//创建一个事件组,返回事件组句柄,失败返回 NULL
EventGroupHandle_t xEventGroupCreate( void );
//等待事件组中某个标志位,用返回值以确定哪些位已完成设置
EventBits_t xEventGroupWaitBits(
const EventGroupHandle_t xEventGroup, //事件组句柄
const EventBits_t uxBitsToWaitFor, //哪些位需要等待
const BaseType_t xClearOnExit, //是否自动清除标志位
const BaseType_t xWaitForAllBits, //是否等待的标志位都成功了才返回
TickType_t xTicksToWait //最大阻塞时间
);
//设置标志位
EventBits_t xEventGroupSetBits(
EventGroupHandle_t xEventGroup,//事件组句柄
const EventBits_t uxBitsToSet //设置哪个位
);
//清除标志位
EventBits_t xEventGroupClearBits(
EventGroupHandle_t xEventGroup, //事件组句柄
const EventBits_t uxBitsToClear //清除的标志位
);
1.2 示例
#include <stdio.h>
// 调用FreeRTOS接口
#include "freertos/FreeRTOS.h"
// 调用任务的头文件
#include "freertos/task.h"
// 调用⌚️组的头文件
#include "freertos/event_groups.h"
// 用来打印提示
#include "esp_log.h"
#define NUM0_BIT BIT0
#define NUM1_BIT BIT1
//事件组句柄
static EventGroupHandle_t s_testEvent;
/** 事件任务A,用于定时标记事件
* @param 无
* @return 无
*/
void event_taskA(void* param)
{
while(1)
{
xEventGroupSetBits(s_testEvent,NUM0_BIT);
vTaskDelay(pdMS_TO_TICKS(1000));
xEventGroupSetBits(s_testEvent,NUM1_BIT);
vTaskDelay(pdMS_TO_TICKS(1500));
}
}
/** 事件任务B,等待事件组中BIT0和BIT1位
* @param 无
* @return 无
*/
void event_taskB(void* param)
{
EventBits_t ev;
while(1)
{
ev = xEventGroupWaitBits(s_testEvent,NUM0_BIT|NUM1_BIT,pdTRUE,pdFALSE,portMAX_DELAY);
if(ev & BIT0)
{
ESP_LOGI(TAG,"Event BIT0 set");
}
if(ev& BIT1)
{
ESP_LOGI(TAG,"Event BIT1 set");
}
}
}
/** 事件例程初始化
* @param 无
* @return 无
*/
void rtos_event_sample(void)
{
s_testEvent = xEventGroupCreate();
xTaskCreatePinnedToCore(event_taskA,"event_taskA",2048,NULL,3,NULL,1);
xTaskCreatePinnedToCore(event_taskB,"event_taskB",2048,NULL,3,NULL,1);
}
2 直达任务通知
定义:每个 RTOS 任务都有一个任务通知数组。 每条任务通知 都有“挂起”或“非挂起”的通知状态, 以及一个 32 位通知值。
直达任务通知是直接发送至任务的事件, 而不是通过中间对象 (如队列、事件组或信号量)间接发送至任务的事件。向任务发送“直达任务通知” 会将目标任务通知设为“挂起”状态(此挂起不是挂起任务)。
2.1 API
//用于将事件直接发送到 RTOS 任务并可能取消该任务的阻塞状态
BaseType_t xTaskNotify(
TaskHandle_t xTaskToNotify, //要通知的任务句柄
uint32_t ulValue, //携带的通知值
eNotifyAction eAction //执行的操作
);
- eAction 可选用的参数

BaseType_t xTaskNotifyWait(
uint32_t ulBitsToClearOnEntry, //进入函数清除的通知值位
uint32_t ulBitsToClearOnExit,//退出函数清除的通知值位
uint32_t *pulNotificationValue, //通知值
TickType_t xTicksToWait //等待时长
);
2.2 示例
#include <stdio.h>
// 调用FreeRTOS接口
#include "freertos/FreeRTOS.h"
// 调用任务的头文件
#include "freertos/task.h"
// 调用⌚️组的头文件
#include "freertos/event_groups.h"
// 用来打印提示
#include "esp_log.h"
//要使用任务通知,需要记录任务句柄
static TaskHandle_t s_notifyTaskAHandle;
static TaskHandle_t s_notifyTaskBHandle;
/** 任务通知A,用于定时向任务通知B直接传输数据
* @param 无
* @return 无
*/
void notify_taskA(void* param)
{
uint32_t rec_val = 0;
while(1)
{
if (xTaskNotifyWait(0x00, ULONG_MAX, &rec_val, pdMS_TO_TICKS(1000)) == pdTRUE)
{
ESP_LOGI(TAG,"receive notify value:%lu",rec_val);
}
}
}
/** 任务通知B,实时接收任务通知A的数据
* @param 无
* @return 无
*/
void notify_taskB(void* param)
{
int notify_val = 0;
while(1)
{
xTaskNotify(s_notifyTaskAHandle, notify_val, eSetValueWithOverwrite);
notify_val++;
vTaskDelay(pdMS_TO_TICKS(1000));
}
}
/** 任务通知例程初始化
* @param 无
* @return 无
*/
void rtos_notify_sample(void)
{
xTaskCreatePinnedToCore(notify_taskA,"notify_taskA",2048,NULL,3,&s_notifyTaskAHandle,1);
xTaskCreatePinnedToCore(notify_taskB,"notify_taskB",2048,NULL,3,&s_notifyTaskBHandle,1);
}
//入口函数
void app_main(void)
{
/*
以下是每种FreeRTOS特性的测试例程的初始化函数,建议每次只开一个,否则有太多打印影响体验
*/
//rtos_task_sample();
//rtos_queue_sample();
rtos_sem_sample();
//rtos_event_sample();
//rtos_notify_sample();
}
2.5 原生 FreeRTOS 和 ESP-IDF 中 FreeRTOS 的区别
1 主要问题
- 优先级问题,多核情况并不适用,因为多个任务可同时运行
- ESP-IDF自动创建空闲、定时器、app_main、IPC、ESP定时器
- ESP-IDF不使用原生FreeRTOS的内存堆管理
- 实现了自己的堆
- 创建任务使用xTaskCreatePinnedToCore()
- 删除任务避免删除另外一个核的任务
- 临界区使用自旋锁确保同步
- 如果任务中用到浮点运算,则创建任务的时候必须指定具体运行在哪个核上,不能由系统自动安排
2 概括
- 程序应用开发创建任务指定内核,建议不要使用tskNO_AFFINITY。
- 通常,负责处理无线网络的任务(例如,WiFi或蓝牙)将被固定到CPUO(因此名称PRO_CPU),而处理应用程序其余部分的任务将被固定到CPU1(因此名称APP_CPU)
3.1 外设学习——GPIO
1 概述
1.1 GPIO介绍
- GPIO(General Purpose Input Output)通用输入输出口,可配置成6种输入输出模式
- 引脚电平:0V、3.3V
- 输出模式下可控制端口输出高低电平,用以驱动LED、控制蜂鸣器、模拟通信协议时序等
- 输入模式下可读取端口的高低电平或电压,用于读取按键输入、外接模块电平信号输入、模拟通信协议接收数据等
1.2 输入输出模式

1.3 GPIO配置流程

1.4 输出模式

2 API
2.1 使用gpio_config()函数来配置GPIO
#include "driver/gpio.h"
esp_err_t gpio_config(const gpio_config_t *pGPIOConfig);
2.1.1 esp_err_t 返回值类型
- 当调用成功的时候,返回ESP_OK
- 当调用失败的时候,返回ESP_FAIL
// 可对返回值进行判断,来得知成功与否
esp_err_t err;
err = gpio_config(const gpio_config_t *pGPIOConfig);
if(err ! = ESP_OK)
{
printf("init error");
}
2.1.2 参数一gpio_config_t 结构体
- gpio_config_t 结构体
gpio_config_t gpio_config = {
.pin_bit_mask = (1ULL << GPIO_NUM_2), // 配置 GPIO2,用哪一个引脚就左移多少位
.mode = GPIO_MODE_OUTPUT, // 设置为输出模式
/*
GPIO_MODE_DISABLE 不输入输出
GPIO_MODE_INPUT 输入模式
GPIO_MODE_OUTPUT 推挽输出模式
GPIO_MODE_OUTPUT_OD 开漏输出模式
GPIO_MODE_INPUT_OUTPUT_OD 输入+开漏输出模式
GPIO_MODE_INPUT_OUTPUT 输入+推挽输出模式
*/
.pull_up_en = GPIO_PULLUP_DISABLE, // 禁止上拉
/*
GPIO_PULLUP_DISABLE 上拉关闭
GPIO_PULLUP_ENABLE 上拉开启
*/
.pull_down_en = GPIO_PULLDOWN_DISABLE, // 禁止下拉
/*
GPIO_PULLDOWN_DISABLE 下拉关闭
GPIO_PULLDOWN_ENABLE 下拉开启
*/
.intr_type = GPIO_INTR_DISABLE // 表示是否开始GPIO口中断
/*
GPIO_INTR_DISABLE 无中断
GPIO_INTR_POSEDGE 上升沿触发
GPIO_INTR_NEGEDGE 下降沿触发
GPIO_INTR_ANYEDGE 双边沿触发
GPIO_INTR_LOW_LEVEL 低电平触发
GPIO_INTR_HIGH_LEVEL 高电平触发
*/
};
2.2使用gpio_set_level()函数来控制IO口输出高电平/低电平
esp_err_t gpio_set_level(gpio_num_t gpio_num, uint32_t level);
- 参数一,GPIO_NUM_NUM,NUM表示数字来显示用的哪一个io口
- 参数二,level,0表示低电平,1表示高电平
2.3 使用gpio_get_level()函数来读取当前IO口的电平状态
int gpio_get_level(gpio_num_t gpio_num);
- 返回值,如果是1则为高电平,反之反之
- 参数一,GPIO_NUM_NUM,最后一个NUM表示数字来显示用的哪一个io口
3.2 外设学习——中断
1 概述
1.1 定义
- 中断:在主程序运行过程中,出现了特定的中断触发条件(中断源),使得CPU暂停当前正在运行的程序,转而去处理中断程序,处理完成后又返回原来被暂停的位置继续运行
- 中断优先级:当有多个中断源同时申请中断时,优先响应更加紧急的中断源
- 中断嵌套:当一个中断程序正在运行时,又有新的更高优先级的中断源申请中断,CPU再次暂停当前中断程序,转而去处理新的中断程序,处理完成后依次进行返回
1.2 中断源
- ESP32有99个外部中断源,包含EXTI、ADC、UART、SPI等多个外设
- 而每个CPU只有32个中断通道,因此使用“中断矩阵”管理中断源和CPU的连接关系。

2 EXTI外部中断
2.1 概念
- EXTI(Extern Interrupt)外部中断
- EXTI可以监测指定IO口的电平信号,当其指定的IO口产生电平变化时,EXTI将立即向中断矩阵发出中断申请,经过中断矩阵裁决后即可中断CPU主程序,使CPU执行EXTI对应的中断程序
- 支持的触发方式:上升沿/下降沿/双边沿/高电平/低电平
- 支持的IO口:所有可正常配置的IO口
2.2 API
2.2.1 使用 “gpio_config” 函数配置GPIO
2.2.2 使用 “gpio_install_isr_service” 函数向中断矩阵注册中断
esp_err_t gpio_install_isr_service(int intr_alloc_flags);
/*
可以选择的参数如下
#define ESP_INTR_FLAG_LEVEL1 (1<<1) // 最低优先级
#define ESP_INTR_FLAG_LEVEL2 (1<<2)
#define ESP_INTR_FLAG_LEVEL3 (1<<3)
#define ESP_INTR_FLAG_LEVEL4 (1<<4)
#define ESP_INTR_FLAG_LEVEL5 (1<<5)
#define ESP_INTR_FLAG_LEVEL6 (1<<6)
#define ESP_INTR_FLAG_NMI (1<<7) // 最高优先级,不可屏蔽中断
#define ESP_INTR_FLAG_EDGE (1<<9) // 边沿触发中断
#define ESP_INTR_FLAG_SHARED (1<<8) // 中断可以在多个ISR之间共享,开启这个标志后,多个 GPIO 可以共享同一个中断向量,中断控制器会自动遍历所有注册了这个向量的中断服务函数,找到对应的那个执行。
#define ESP_INTR_FLAG_IRAM (1<<10) // 当cache禁用时,ISR仍然可以被调用,开启这个标志后,中断服务函数会被加载到内部 RAM (IRAM) 中,即使 Flash 禁用也能正常运行。
#define ESP_INTR_FLAG_INTRDISABLED (1<<11) // 返回时禁用该中断
*/
2.2.3 使用 “gpio_isr_handler_add” 函数将IO口与中断服务函数匹配
esp_err_t gpio_isr_handler_add(gpio_num_t gpio_num, gpio_isr_t isr_handler, void *args);
- 参数一,配置的中断GPIO口,GPIO_NUM_NUM,NUM表示数字来显示用的哪一个io口
- 参数二,中断服务函数句柄,定义一个函数typedef void (*gpio_isr_t)(void *arg);
- 参数三,向中断服务函数传入的值
2.2.4 使用 “gpio_intr_enable” 函数开启中断
esp_err_t gpio_intr_enable(gpio_num_t gpio_num);
- 参数一,上面配置的io口
2.2.5 编写前文定义的中断函数
3.3 外设学习——通用定时器
1 ESP32-S3时钟系统
ESP32-S3的时钟主要用来给CPU、外设或其他功能电路提供脉冲,在芯片内部,所有功能模块的工作均按照时钟节拍进行。相当于芯片的“心脏”。
ESP32-S3的时钟主要来源于振荡器、RC震荡电路和PLL时钟生成电路。产生的时钟,经过各种分频器、选择器处理之后,使得大部分功能模块可以根据不同功耗和性能需求来获取对应频率的工作时钟。
1.1 时钟架构系统


1.2 外设时钟选择

2 通用定时器工作流程
- ESP32-S3包含两组通用定时器,即定时器组0和定时器组1
- 每个定时器组都有两个通用定时器和一个主系统看门狗定时器


3 API
3.1 使用gptimer_handle_t定义定时器句柄(给定时器起名)
#include "driver/gptimer.h"
// 定义一个定时器句柄
gptimer_handle_t gptim;
3.2 使用 “gptimer_new_timer” 函数配置时钟源和计数器
gptimer_config_t timer_config = {
.clk_src = GPTIMER_CLK_SRC_DEFAULT, // 使用默认时钟源
/*
GPTIMER_CLK_SRC_DEFAULT 使用默认值 APB_CLK
GPTIMER_CLK_SRC_APB 使用 APB_CLK
GPTIMER_CLK_SRC_XTAL 使用 XTAL_CLK
*/
.direction = GPTIMER_COUNT_UP, // 计数方向为向上计数
/*
GPTIMER_COUNT_DOWN, 向下计数
GPTIMER_COUNT_UP, 向上计数
*/
.resolution_hz = 1000, // 计数器分辨率为 1 kHz (每毫秒递增一次)
/*
*/
.intr_priority = 0, // 中断优先级,如果设置为0则会从1,2,3中自动设置一个优先级
.flags.intr_shared = false // 不共享中断
};
esp_err_t gptimer_new_timer(const gptimer_config_t *config, gptimer_handle_t *ret_timer);
- 参数一,结构体
- 参数二,句柄
3.3 使用 “gptimer_set_alarm_action” 函数配置比较器的动作
gptimer_alarm_config_t alarm_config = {
.alarm_count = 1000000, // 1秒钟产生中断
.reload_count = 0, // 设置自动重装的值
.flags.auto_reload_on_alarm = 1//配置为1时自动重装,配置为0时不重新计数
};
esp_err_t gptimer_set_alarm_action();
- 参数一, 句柄
- 参数二,结构体
3.4 使用 “gptimer_register_event_callbacks” 函数配置报警事件
typedef bool (*gptimer_alarm_cb_t) (gptimer_handle_t timer, const gptimer_alarm_event_data_t *edata, void *user_ctx);
-
可以设置中断函数代码在IRAM中运行,这样运行响应的速度会更快

-
如果要在IRAM中运行,那就需要进行宏声明
//需要引用宏定义
#include "esp_attr.h"
//需要在调用的函数前标准声明,例:
bool IRAM_ATTR TimerCallback(gptimer_handle_t timer, const gptimer_alarm_event_data_t *edata, void *user_ctx)
gptimer_event_callbacks_t cbs = {
.on_alarm = timer_alarm_callback, // 定义一个回调函数,当定时器到达设定的计数值时会调用这个函数
};
esp_err_t gptimer_register_event_callbacks(gptimer_handle_t timer, const gptimer_event_callbacks_t *cbs, void *user_data);
- 参数一,句柄
- 参数二,结构体指针
- 参数三,传入的参数,如果没有就填NULL
3.5 使用 “gptimer_enable” 函数使能通用定时器
- 参数一,句柄
3.6 使用 “gptimer_start” 函数开启通用定时器
- 参数二,句柄
3.4 外设学习——系统定时器
1 概念

2 API
// 包含的头文件
#include "esp_timer.h"
2.1 使用 esp_timer_handle_t定义定时器句柄
2.2 使用 “esp_timer_create” 函数创建一个定时器并配置报警事件
void (*esp_timer_cb_t)(void* arg)
esp_timer_create_args_t timer_args = {
.callback = timer_callback, // 定义一个回调函数,当定时器到达设定的时间时会调用这个函数
.arg = NULL, // 传递给回调函数的参数,可以是任何类型的数据
.dispatch_method = ESP_TIMER_TASK, // 定时器事件的分发方式,这里选择在任务中处理
/*
ESP_TIMER_TASK,
ESP_TIMER_ISR,
ESP_TIMER_MAX,
*/
.name = "my_timer" // 定时器的名称,便于调试和识别
.skip_unhandled_events = true//用于定义是否跳过中间未处理的警报事件
};
- 要不要iram中运行,要的话需要给出宏定义

esp_err_t esp_timer_create(const esp_timer_create_args_t* args,
esp_timer_handle_t* out_handle)
- 参数一,结构体的地址
- 参数二,定时器句柄
2.3 使用 “esp_timer_start_periodic” 函数配置比较器值,并开启定时器
esp_err_t esp_timer_start_periodic(esp_timer_handle_t timer, uint64_t period);
- 参数一,定时器句柄
- 参数二,配置定时周期,默认时间是1微妙
3.5 外设学习——PWM
1 概念
PWM(Pulse Width Modulation)脉冲宽度调制
在具有惯性的系统中,可以通过对一系列脉冲的宽度进行调制,来等效地获得所需要的模拟参量,常应用于电机控速等领域
PWM参数:
频率 = 1 / TS 占空比 = TON / TS 分辨率 = 占空比变化步距
1.1 PWM 控制器
- ESP32S3中有两种PWM控制器:LED PWM 和 MC PWM
- LED PWM控制器用于生成控制LED的PWM,具有占空比自动渐变等专门功能。该外设也可生成PWM信号用作其他用途。
- ESP32S3具有八个独立的PWM生成器(即八个PWM输出通道)和四个独立定时器给PWM生成器提供时钟。
- 四个定时器可独立配置,但是共用一个时钟源。
- 每个PWM生成器会在四个定时器中选择一个,以该定时器为基准生成PWM信号。
1.2 PWM 控制器配置流程

2 API
2.1使用 ledc_timer_config() 函数配置时钟源和定时器参数
// 调用的头文件
#include "driver/ledc.
typedef struct {
.ledc_mode_t speed_mode; // 设置输出模式
/*
LEDC_HIGH_SPEED_MODE 高速模式
LEDC_LOW_SPEED_MODE 低速模式,ESP32S3仅支持低速模式
*/
.ledc_timer_bit_t duty_resolution;// 设置分辨率
/*
LEDC_TIMER_1_BIT
LEDC_TIMER_2_BIT,
LEDC_TIMER_3_BIT,
LEDC_TIMER_4_BIT,
LEDC_TIMER_5_BIT,
LEDC_TIMER_6_BIT,
LEDC_TIMER_7_BIT,
LEDC_TIMER_8_BIT,
LEDC_TIMER_9_BIT,
LEDC_TIMER_10_BIT,
LEDC_TIMER_11_BIT,
LEDC_TIMER_12_BIT,
LEDC_TIMER_13_BIT,
LEDC_TIMER_14_BIT,
*/
.ledc_timer_t timer_num;// PWM要使用的定时器
/*
LEDC_TIMER_0
LEDC_TIMER_1,
LEDC_TIMER_2,
LEDC_TIMER_3
*/
.uint32_t freq_hz; // 设置频率,单位HZ
.ledc_clk_cfg_t clk_cfg; // 配置时钟器的时钟源
/*
LEDC_AUTO_CLK // 自动选择时钟源
LEDC_USE_APB_CLK // 设置未APB_CLK
LEDC_USE_RC_FAST_CLK // 设置为RC_FAST_CLK
LEDC_USE_REF_TICK // 设置未REF_TICK,是XTAL_CLK经过分频后产生的
*/
}
- 频率和分辨率的配置逻辑

esp_err_t
ledc_timer_config(const·ledc_timer_config_t *timer_conf);
- 参数一,结构体
2.2 使用 ledc_channel_config() 函数配置PWM控制器参数
typedef struct {
.gpio_num // 电平输出的IO口,例GPIO_NUM_38
.speed_mode; // 用于PWM的输出模式
/*
LEDC_HIGH_SPEED_MODE 高速模式
LEDC_LOW_SPEED_MODE 低速模式,ESP32S3仅支持低速模式
*/
.channel; // 用于设置PWM通道
/*
LEDC_CHANNEL_0
LEDC_CHANNEL_1
……
LEDC_CHANNEL_7
*/
.intr_type; // 是否开启中断
/*
LEDC_INTR_DISABLE 不开启中断
LEDC_INTR_FADE_END 开启中断
*/
.timer_sel; // pwm要连接的定时器
/*
LEDC_TIMER_0
LEDC_TIMER_1,
LEDC_TIMER_2,
LEDC_TIMER_3
*/
.duty; // 高电平所占用的数量,最小值为0最大值为上文设置的分辨率
.hpoint; // 相位偏移,与前一个计数值进行比较
.flags.output_invert//前面生产的脉冲电平是否翻转,为1则翻转,为0则不翻转
} ledc_channel_config_t;
esp_err_t ledc_channel_config(const ledc_channel_config_t *ledc_conf);
2.3 使用 ledc_set_duty()函数修改占空比
esp_err_t ledc_set_duty(ledc_mode_t speed_mode,-ledc_channel_t-channel,·uint32_t duty);
- 参数一,输出模式
- 参数二,修改的输出通道
- 参数三,修改的占空比数值
2.4 使用 ledc_update_duty() 函数更新占空比
esp_err_tiedc_update_duty(ledc_mode_t speed_mode, ledc_channel_t channel);
- 参数一,输出模式
- 参数二,PWM输出通道
3.6 外设学习——电机
1 概念
电机分类:直流有刷电机、直流无刷电机、步进电机等
2 舵机
- 舵机是一种根据输入PWM信号占空比来控制输出角度的电机
- 常用舵机分类:180°舵机(方向、角度) ; 360°舵机(方向、转速)
- 角度控制方式:PWM信号占空比控制
- 输入PWM信号要求:周期为20ms(50Hz),高电平宽度为0.5ms~2.5ms

3.7 外设学习——ADC连续转换模式
1 概念
- ADC(Analog-Digital Converter)模拟-数字转换器
- ADC可以将引脚上连续变化的模拟电压转换为内存中存储的数字变量,建立模拟电路到数字电路的桥梁
- 连续转换模式用的是数字 ADC 控制器,为了高速采样,驱动已经帮你选好了最优的 160MHz PLL 时钟源,你只需要告诉它采样频率就行
- 2个12位逐次逼近型ADC
- 输入电压范围:03.3V,转换结果范围:04095
- ESP32S3 ADC资源:SAR ADC1、SAR ADC2,共20路输入通道


1.1 架构图

1.2 工作流程图

1.3 内存中的存储形式

2 API
2.1 使用 “adc_continuous_new_handle” 函数配置转换结果存储单元大小、转换帧存储单元大小
// cmakelists中加入官方要用的文件
set(
requires
esp_adcdriver
)
// 加入的头文件
#include "myadc.h"
#include "hal/adc_types.h"
#include "esp_adc/adc_continuous.h"
adc_continuous_handle_t adc_continuous_handle;
adc_continuous_handle_cfg_t adc_contunuous_structure ={
.conv_frame_size = // 配置转换帧的大小,单位是字节,一个通道四个字节计算要几个通道
.max_store_buf_size = // 配置转换结果的最大值,示例1024
}
esp_err_t adc_continuous_new_handle(const adc_continuous_handle_cfg_t *hdl_config, adc_continuous_handle_t *ret_handle);
- 参数一,结构体
- 参数二,句柄
2.2 使用 “adc_digi_pattern_config_t” 结构体配置转换表
adc_digi_pattern_config_t adc_digi_arr ={
.atiten = // 配置ADC的衰减系数
/*
ADC ATTEN DB 0 0-1.1V
ADC_ATTENDB_2_5 0-1.5V
ADC ATTEN DB 6 0-2.2V
ADC ATTEN DB 11 0-3.3V
*/
.bit_width = // ADC输出的分辨率位宽
/*
ADC_BITWIDTH_DEFAULT
ADC_BITWIDTH_9
ADC_BITWIDTH_10
ADC_BITWIDTH_11
ADC_BITWIDTH_12 一般选择12位
ADC_BITWIDTH_13
*/
.channel = // 要配置的ADC通道,注意通道与IO口是一一对应的,要选择好
/*
ADC_CHANNEL_0,
ADC_CHANNEL_1,
ADC_CHANNEL_2,
ADC_CHANNEL_3,
ADC_CHANNEL_4,
ADC_CHANNEL_5,
ADC_CHANNEL_6,
ADC_CHANNEL_7,
ADC_CHANNEL_8,
ADC_CHANNEL_9,
*/
.unit = //要使用的ADC单元,由模组引脚定义决定
/*
ADC_UNIT_1,
ADC_UNIT_2,
*/
}
2.3 使用 “adc_continuous_config” 函数配置ADC总控制器需要下发和执行的参数
adc_continuous_config_t continuous_config_structure =
{
.adc_pattern =adc_digi_arr// 总控制器要定义的转换表名称,直接复制前面的转换表名字即可
.conv_mode = // ADC的转换表模式
/*
ADC_CONV_SINGLE_UNIT_1 // 仅转换ADC1
ADC_CONV_SINGLE_UNIT_2// 仅转换ADC2
ADC_CONV_BOTH_UNIT// 同时转换ADC2和ADC1
ADC_CONV_ALTER_UNIT// ADC1和ADC2循环转换
*/
.format = //ADC和DMA的输出模式
/*
ADC_DIGI_OUTPUT_FORMAT_TYPE1 包含通道和数据位
ADCDIGI_OUTPUT_FORMAT_TYPE2 包含ADC,通道和数据位,数据位少一位
*/
.pattern_num = // 转换表中共有几个要转换的ADC的通道
.sample_freq_hz = // 采样频率,最大频率(2*1000*1000),最小频率(20*1000)
}
esp_err_t adc_continuous_config(adc_continuous_handle_t handle, const adc_continuous_config_t *config);
- 参数一,句柄
- 参数二,结构体指针
2.4 使用 “adc_continuous_register_event_callbacks” 函数注册回调函数,用于数据读取
// 定义一个指针存放数据的地址
uint8_t *data_value;
// 定义一个全局变量用来存放r1的数据信息
uint16_t adc_value_rl;
// 定义一个全局变量用来存放rp的数据信息
uint16_t adc_value_rp;
// 回调函数
typedef bool (*adc_continuous_callback_t)(adc_continuous_handle_t handle, const adc_continuous_evt_data_t *edata, void *user_data)
{
// 取出结构体edata里的buffer东西
data_value = edata->conv_frame_buffer;if(edata->size == 8)
// 如果信号帧大小是8字节
if(edata->size == 8)
{
// 取出r1的信息
adc_value_rl = ((data_value[1]& exeF) << 8) | data_value[0];
// 取出rp的信息
adc_value_rp =((data_value[5]& 0xeF)<< 8) | data_value[4];
return true;
}
return false;
}
- 参数一,句柄
- 参数二,结构体
- 参数三,传入的数据
adc_continuous_evt_cbs_t evt_structure =
{
.on_conv_done = // 当生成一个转换帧的时候,触发一次中断
.on_pool_ovf = // 当缓冲区满的时候触发中断
// 两个参数配置一个即可,配置好了删掉另一个,参数填写回调函数的名称
}
esp_err_t adc_continuous_register_event_callbacks(adc_continuous_handle_t handle, const adc_continuous_evt_cbs_t *cbs, void *user_data);
- 参数一,ADC句柄
- 参数二,结构体指针
- 参数三,要传入回调函数的参数
2.5 使用 “adc_continuous_start” 函数开启连续转换
esp_err_t
adc_continuous_start(adc_continuous_handle_t·handle);
- 参数一,句柄
2.6 头文件额外变量声明
// 调用要声明的头文件
#include <stdint.h>
// 全局变量声明
extern uint16_t adc_value_rl;
extern uint16_t adc_value_rp;
3.8 外设学习——ADC单次转换模式
1 原理图

- 单次转换模式用的是RTC ADC 控制器,为了适应各种低功耗场景,需要你手动选择时钟源
2 API
2.1调用头文件
#include "esp_adc/adc_oneshot.h"
2.2 使用 “adc_oneshot_new_unit” 函数配置时钟源、ADC参数
adc_oneshot_unit_init_cfg_t oneshot_structure =
{
.clk_src = // 配置ADC时钟源
/*
ADC_RTC_CLK_SRC_RC_FAST // RC_FAST
ADC_RTC_CLK_SRC_DEFAULT // 默认
*/
.ulp_mode = // 是否开启低功耗控制器
/*
ADC_ULP_MODE_DISABLE 不使用
ADC_ULP_MODE_FSM 用FSM处理
ADC ULP MODE RISCV 用RISCV处理
*/
.unit_id = // 选用哪一个ADC单元
/*
ADC_UNIT_1,
ADC_UNIT_2,
*/
}
esp_err_t adc_oneshot_new_unit(const adc_oneshot_unit_init_cfg_t *init_config, adc_oneshot_unit_handle_t *ret_unit);
- 参数一,结构体指针
- 参数二,ADC句柄
2.3 使用 “adc_oneshot_config_channel” 函数配置总控制器要下发和执行的参数
adc_oneshot_chan_cfg_t oneshot_chan_structure ={
.atten = // 电压衰减系数
/*
ADC ATTEN DB 0 0-1.1V
ADC_ATTENDB_2_5 0-1.5V
ADC ATTEN DB 6 0-2.2V
ADC ATTEN DB 11 0-3.3V
*/
.bitwidth = // ADC输出的分辨率位宽
/*
ADC_BITWIDTH_DEFAULT
ADC_BITWIDTH_9
ADC_BITWIDTH_10
ADC_BITWIDTH_11
ADC_BITWIDTH_12 一般选择12位
ADC_BITWIDTH_13
*/
esp_err_t adc_oneshot_config_channel(
adc_oneshot_unit_handle_t handle,
adc_channel_t channel,
/*
ADC_CHANNEL_0,
ADC_CHANNEL_1,
ADC_CHANNEL_2,
ADC_CHANNEL_3,
ADC_CHANNEL_4,
ADC_CHANNEL_5,
ADC_CHANNEL_6,
ADC_CHANNEL_7,
ADC_CHANNEL_8,
ADC_CHANNEL_9,
*/
const adc_oneshot_chan_cfg_t *config);
- 参数一,ADC句柄
- 参数二,ADC指定通道
- 参数三,结构体指针
4 使用 “adc_oneshot_read” 函数开启一次转换并获取转换数据
esp_err_t adc_oneshot_read(adc_oneshot_unit_handle_t handle, adc_channel_t chan, int *out_raw);
- 参数一,句柄
- 参数二,要使能的ADC的通道
- 参数三,存放输入电压的转换结果
5 局外声明
#include "esp_adc/adc_oneshot.h"
extern adc_oneshot_unit_handle_t adc_unit_handle;
void adc_init(void);
3.9 外设学习——串口通信
1 概念
- 通信的目的:将一个设备的数据传送到另一个设备
- 通信协议:制定通信的规则,通信双方按照协议规则进行数据收发
- 通信分类:
- 依据传输线数量:串行通信(UART、I2C、SPI)、并行通信
- 依据传输方向:单工通信、半双工通信、全双工通信
- 依据同步方式:同步、异步
1.1 通信类型

1.2 电平标准
- 电平标准是数据1和数据0的表达方式,是传输线缆中人为规定的电压与数据的对应关系,串口常用的电平标准有如下三种:
- TTL电平:+3.3V(电源电压)表示1,0V表示0
- RS232电平:-3-15V表示1,+3+15V表示0
- RS485电平:两线压差+2+6V表示1,-2-6V表示0(差分信号)
1.3 串口的参数及时序
- 波特率:串口通信的速率
- 起始位:标志一个数据帧的开始,固定为低电平
- 停止位:用于数据帧间隔,固定为高电平
- 数据位:数据帧的有效载荷,1为高电平,0为低电平,低位先行
- 校验位:用于数据验证,根据数据位计算得来
1.4 UART
- UART(Universal Asynchronous Receiver/Transmitter)通用异步收发器
- UART是ESP32S3内部集成的硬件外设,可以将数据缓冲区中的一个字节数据自动生成数据帧时序,从TX引脚发送出去,也可自动接收RX引脚的数据帧时序,拼接为一个字节数据,存放在数据缓冲区里
- 串口通信速度最高达5Mbits/s
- 可配置数据位长度(5/6/7/8)、停止位长度(1/1.5/2/3)
- 可选校验位(无校验/奇校验/偶校验)
- ESP32S3 UART资源: UART0、 UART1、 UART2
2 内部架构图


3 API
3.1 头文件
#include "driver/uart.h"
#include "driver/gpio.h"
3.2 使用 “uart_param_config” 配置时钟源和UART控制器
uart_config_t uart_structure = {
.baud_rate = // 串口的波特率
.data_bits = // 配置数据位的长度
/*
UART_DATA_5_BITS
UART DATA 6 BITS
UART_DATA_7_BITS
UART_DATA_8_BITS
*/
.flow_ctrl // 用于设置串口的硬件流控模式,意思是增加一个线来判断是否需要接受数据,如果准备好了就发送没有的话不发送
/*
UART_HW_FLOWCTRL_DISABLE 不开启
UART_HW_FLOWCTRL_RTS 使能接受
UART_HW_FLOWCTRL_CTS 使能发送
UART_HW_FLOWCTRL_CTS_RTS 同时发送和接受
*/
.parity = // 用于配置校验模式
/*
UART_PARITY_DISABLE 不开启校验
UART_PARITY_EVEN 开启奇校验
UART_PARITY_ODD 开启偶校验
*/
.rx_flow_ctrl_thresh = // 用于配置硬件流控的阈值,单位是字节,当缓冲区的数据超过阈值的时候会触发硬件流控告诉外部设备我处理不过来了不传了,如果没开启硬件流控那么久随便填写一个值
.source_clk = // 配置串口的时钟源
/*
UART_SCLK_APB
UART SCLK REF TICK
UART_SCLK_DEFAULT 默认时钟源
*/
.stop_bits = // 停止位
/*
UART_STOP_BITS_1
UART_STOP_BITS_1_5
UART_STOP_BITS_2
*/
esp_err_t uart_param_config(uart_port_t·uart_num,const·uart_config_t-*uart_config);
- 参数一,调用哪一个UART串口,分别为UART_NUM_0(默认用来下载程序)、UART_NUM_1、UART_NUM_2
- 参数二,结构体
3.2 使用 “uart_set_pin” 配置输入输出引脚
esp_err_t uart_set_pin(uart_port_t uart_num, int tx_io_num, int rx_io_num, int rts_io_num, int cts_io_num);
- 参数一,调用哪一个UART串口,分别为UART_NUM_0(默认用来下载程序)、UART_NUM_1、UART_NUM_2
- 参数二,用哪一个IO引脚作为发送引脚,例:GPIO_NUM_17
- 参数三,用哪一个IO引脚作为接受引脚,例:GPIO_NUM_18
- 参数四,发送硬件流控引脚,不使用填写-1
- 参数五,接受硬件流控引脚,不使用填写-1
3.3 使用 “uart_driver_install” 配置发送/接收缓冲区
esp_err_t uart_driver_install(uart_port_t uart_num, int rx_buffer_size, int tx_buffer_size, int queue_size, QueueHandle_t* uart_queue, int intr_alloc_flags);
- 参数一,调用哪一个UART串口,分别为UART_NUM_0(默认用来下载程序)、UART_NUM_1、UART_NUM_2
- 参数二,用于配置接受缓冲区大小,字节,例:1024
- 参数三,用于配置发送缓冲区大小,字节,例:1024
- 参数四,配置串口驱动程序内部缓冲队列的大小,不使用队列输入0
- 参数五,队列句柄
- 参数六,分贝串口中断标志,不开启中断输入0
3.4 使用 “uart_write_bytes” 向发送缓冲区中写入数据
int uart_write_bytes(uart_port_t uart_num, const void* src, size_t size);
- 参数一,UART串口,分别为UART_NUM_0(默认用来下载程序)、UART_NUM_1、UART_NUM_2
- 参数二,接受到的数据存放地址
- 参数三,从接收缓冲区中读取的字节长度
3.5 使用 “uart_read_bytes” 从接收缓冲区中读取数据
int uart_read_bytes(uart_port_t uart_num, void* buf, uint32_t length, TickType_t ticks_to_wait);
- 返回值一,int类型,当返回值为-1时,说明返回值异常,当大于等于0时表示接收缓冲区接受到的数据长度
- 参数一,UART串口,分别为UART_NUM_0(默认用来下载程序)、UART_NUM_1、UART_NUM_2
- 参数二,接受到的数据存放地址
- 参数三,从接收缓冲区中读取的字节长度
- 参数四,等待时间
3.6 用uart_flush函数来清空缓存区
esp_err_t uart_flush(uart_port_t uart_num);
- 参数一,uart编号
3.10 外设学习——I2C
1 概述
- I2C/IIC(Inter IC Bus)是由Philips公司开发的一种通用数据总线
- 两根通信线:SCL(Serial Clock)、SDA(Serial Data)
- 同步,半双工
- 带数据应答
- 总线挂载多设备(一主多从,多主多从)
1.1 地址字节的构成

1.2 时序单元
- 起始条件:SCL高电平期间,SDA从高电平切换到低电平
- 终止条件:SCL高电平期间,SDA从低电平切换到高电平

1.2.1 发送
- 发送一个字节:SCL低电平期间,主机将数据位依次放到SDA线上(高位先行),然后释放SCL,从机将在SCL高电平期间读取数据位,依次循环上述过程8次,即可发送一个字节


1.2.2 接受
- 接收一个字节:SCL低电平期间,从机将数据位依次放到SDA线上(高位先行),然后释放SCL,主机将在SCL高电平期间读取数据位,依次循环上述过程8次,即可接收一个字节(主机在接收之前,需要释放SDA)


- 应答信号发送的是0,非应答信号发送的是1
1.2.3 应答
- 发送应答:主机在接收完一个字节之后,在下一个时钟发送一位数据,数据0表示应答,数据1表示非应答
- 接收应答:主机在发送完一个字节之后,在下一个时钟接收一位数据,判断从机是否应答,数据0表示应答,数据1表示非应答(主机在接收之前,需要释放SDA)


1.3 I2C外设介绍
- ESP32-S3内部集成2个I2C控制器,负责处理I2C总线上的通信
- 支持主机模式、从机模式
- 支持7位/10位地址模式
- 支持不同的通讯速度,标准速度(100 kHz),快速(400 kHz)
- ESP32-S3硬件I2C资源:I2C0、I2C1
1.4 时序介绍


1.5 通信参数
- 从机模块的工作流程:确定指令发送顺序
- 从机模块的指令集:确保能够正确操作从机
- 从机模块的I2C通信速度:确保IIC通信正常进行
- 从机模块的地址:确保主机能够正确呼唤从机
- 从机模块的数据帧结构:确保主机和从机能够正确解析数据
2 API
2.1 头文件
#include "driver/i2c.h"
2.2 使用 “i2c_param_config” 函数配置时钟源、GPIO交换矩阵引脚、I2C参数
i2c_config_t i2c_structure =
{
.clk_flags = // 用于设置I2C外设时钟源,默认为0,按照通信速度设置时钟源
.master.clk_speed = // 用于设置通信速度,要与从机模块相匹配
.mode = // 设置通信模式
/*
I2C_MODE_SLAVE 从机模式
I2C_MODE_MASTER 主机模式
*/
.scl_io_num = //设置scl引脚
.scl_pullup_en= // scl是否开启内部上拉
/*
GPIO_PULLDOWN_DISABLE 下拉关闭
GPIO_PULLDOWN_ENABLE 下拉开启
*/
.sda_io_num= // 设置sda引脚
.sda_pullup_en= // sda是否开启内部上拉
.slave = // 设置为从机的时候才需要配置,直接删掉即可
}
esp_err_t i2c_param_config(i2c_port_t i2c_num, const i2c_config_t *i2c_conf);
- 参数一,配置使用的i2c的端口号,可以为I2C_NUM_0和I2C_NUM_1
- 参数二,为结构体指针
2.3 使用 “i2c_driver_install” 向CPU注册I2C
esp_err_t i2c_driver_install(i2c_port_t i2c_num, i2c_mode_t mode, size_t slv_rx_buf_len, size_t slv_tx_buf_len, int intr_alloc_flags);
- 参数一,I2C的端口号
- 参数二,I2C的模式
- 参数三,接收区缓存大小(仅用于从机模式,不用的话设置为0)
- 参数四,发送区缓存大小(仅用于从机模式,不用的话设置为0)
- 参数五,分配中断标志位,不用的话设置0
2.4 使用 “i2c_cmd_link_create” 函数创建命令寄存器中的命令链(存放命令的数组)
i2c_cmd_handle_t cmd = i2c_cmd_link_create(void);//cmd 是你要创建的命令链的名字
2.5 使用 “i2c_master_start” 函数向命令链中存入I2C_START命令
esp_err_t i2c_master_start(i2c_cmd_handle_t cmd_handle);
- 参数一,句柄
2.6 使用 “i2c_master_write_byte” 函数向命令链中存入I2C_ WRITE命令
esp_err_t i2c_master_write_byte(i2c_cmd_handle_t cmd_handle, uint8_t data, bool ack_en);
- 参数一,句柄
- 参数二,发送的数据,从机写入地址或者参数
- 参数三,是否开启应答,开启则填true
2.7 使用 “i2c_master_read_byte” 函数向命令链中存入I2C_ READ命令
esp_err_t i2c_master_read_byte(i2c_cmd_handle_t cmd_handle, uint8_t *data, i2c_ack_type_t ack);
- 参数一,句柄
- 参数二,数据存放地址
- 参数三,应答类型,I2C_MASTER_ACK(应答,用于任意比特位) 、I2C_MASTER_NACK(不应答,用于任意比特位)、I2C_MASTER_LAST_NACK(不应答,用于最后一个比特位)
2.8 使用 “i2c_master_stop” 函数向命令链中存入I2C_ STOP命令
esp_err_t i2c_master_stop(i2c_cmd_handle_t cmd_handle);
参数一,句柄
2.9 使用 “i2c_master_cmd_begin” 函数开启命令控制器,开始IIC通信
esp_err_t i2c_master_cmd_begin(i2c_port_t i2c_num, i2c_cmd_handle_t cmd_handle, TickType_t ticks_to_wait);
- 参数一,i2c端口号
- 参数二,命令链句柄
- 参数三,最大通信时间
2.10 使用 “i2c_cmd_link_delete” 函数删除命令链,释放内存空间
void i2c_cmd_1ink_delete(i2c_cmd_handle_t cmd_handle)
- 参数一,命令链句柄
3.11 外设学习——SPI
1 概念
SPI(Serial Peripheral Interface)是由Motorola公司开发的一种通用数据总线
支持总线挂载多设备(一主多从)
四根通信线:SCK(Serial Clock)、MOSI(Master Output Slave Input)、MISO(Master Input Slave Output)、SS(Slave Select)
同步,全双工
1.1 硬件电路
所有SPI设备的SCK、MOSI、MISO分别连在一起
主机另外引出多条SS控制线,分别接到各从机的SS引脚
1.2 通信示例图
SPI通信过程:数据交换
1.3 SPI模式真值表

1.4 SPI外设
- ESP32-S3内部集成了4个SPI控制器:
- SPI0、SPI1、通用SPI2(GP-SPI2)、通用SPI3(GP-SPI3)
- GP-SPI2最多可挂载6个从设备,GP-SPI3最多可挂载3个从设备
- 支持主机模式和从机模式
- 主机模式最大时钟速度:80MHz
- 支持半双工通信和全双工通信
- 支持多种数据格式:
- 1-bit SPI:1个时钟周期传输1bit
- 2-bit Dual SPI:1个时钟周期传输2bit
- 4-bit Quad SPI :1个时钟周期传输4bit
- 8-bit Octal SPI :1个时钟周期传输8bit
1.5 内部架构图


2 API
2.1使用 “spi_bus_initialize” 函数配置SPI控制器参数和GPIO交换矩阵
2.2 使用 “spi_bus_add_device” 函数向SPI总线添加从设备信息
2.3 使用 “spi_device_polling_transmit” 函数进行数据交换
4 SD卡
1 概述
- SD卡是一种小型非易失性闪存存储设备,由松下、东芝、闪迪联合制定标准,是目前嵌入式系统外接大容量存储的最主流方案。
- 常用SD卡类型:SD卡、Micro SD卡(TF卡)
- TF卡按照容量分类:SDSC、SDHC、SDXC
- TF卡通信接口:SDIO接口(需要专门协议)、SPI接口

1.1 引脚定义

1.2 工作模式
TF卡工作模式:空闲模式、卡识别模式、数据交互模式
1.3 存储方式
- 常见的数据交互方式:
- 直接向芯片的物理地址中,写入数据并存储(适用小容量存储设备)
- 以文件的形式,写入数据并存储(适用大容量存储设备)

2 FATFS 库
- FATFS 是一个开源、轻量级、专为嵌入式单片机开发使用的 FAT 文件系统驱动库。本质:一套C语言代码
- VFS(Virtual File System),虚拟文件系统,将C函数转换成FATFS函数。
- FATFS库函数:f_open()、f_read()、f_write()、f_close()
- C语言函数:fopen()、fread()、fwrite()、fclose()
- FATFS驱动库对应的是FAT16和FAT32文件系统
3 配置流程
- 格式化TF卡(文件系统类型:FAT32 分配单元大小:4096)
- 使用 “esp_vfs_fat_sdspi_mount” 函数向SPI总线添加从设备信息、配置SD卡的物理参数(电压、时钟速度等)、配置SD卡的SPI总线信息(使用哪一个SPI、SS引脚)、建立VFS与FATFS与SD卡的匹配关系
4 总线连接示例图

5 API
5.1 使用 “esp_vfs_fat_sdspi_mount” 函数向SPI总线添加从设备信息、配置SD卡的物理参数(电压、时钟速度等)、配置SD卡的SPI总线信息(使用哪一个SPI、SS引脚)、建立VFS与FATFS与SD卡的匹配关系
5 下载官方库


6 RAM和FLASH
1 概述
- RAM(Random Access Memory),中文名:随机存取存储器。读写速度极快、容量小、掉电数据丢失。(内存条)
- FLASH(Flash Memory),中文名:闪存存储器。读写速度较RAM慢、容量大、掉电数据不丢失。(硬盘)
1.1 分区表
- 分区表:对FLASH进行分区的表格
- FLASH大小:16MB
- FLASH物理地址范围:16MB=161024KB=161024*1024字节= 16,777,216 = 0x01000000

1.2 分区表格式

1.3 自定义分区表

1.4 区域类型

2 API
2.1 头文件
#include "esp_spiffs.h"
#include "ffs.h"
#include "esp_vfs.h"
2.1 Sdkmenuconfig中将默认分区表改成自定义分区表

2.2 使用分区表编辑器,按照框图对FLASH进行分区


2.3 使用esp_vfs_spiffs_registerO函数初始化文件系统参数
2.4 调用C语言文件操作函数操作文件(建立文件/打开文件/写数据/读数据)
7.1 Wi-Fi功能——STA
1 概念
- WiFi 简介:WIFI是无线局域网(WLAN)的商用俗称。无线局域网(Wireless Local Area Network,WLAN),是基于 IEEE 802.11 系列标准制定的无线联网技术。
- WIFI的工作模式:STA(站点模式)、AP(接入点模式)、AP+STA(混合模式)
- 按照频段分类:
- 2.4GHz频段:国际标准频率范围2.400GHz~2.4835GHz
- 5GHz频段:国际标准频率范围5.150 GHz ~ 5.825 GHz
- 按照IEEE802.11标准号分类:
- 802.11b、 802.11g、 802.11n、 802.11ac、 802.11ax、 802.11be
1.1 IEEE802.11标准简介
- IEEE 802.11是国际电气和电子工程师协会(IEEE)制定的无线局域网技术标准集,是 WiFi 技术的核心底层规范,定义了无线局域网中设备的信号传输、数据交互、组网规则,是全球通用的无线联网技术基础。
- 所有消费级 WiFi 设备(路由器、手机、嵌入式设备)都必须遵循 802.11 标准才能实现互联互通。

1.2 网络拓扑
- 网络拓扑是指网络中设备、节点、链路及它们之间连接关系的几何结构与逻辑结构的总称。
- 网络拓扑决定了数据在网络中的传输路径、通信效率和可靠性。
- 在嵌入式领域,星型拓扑是最常用、最主流的拓扑结构。

1.3 ESP32S3-WIFI功能简介
- ESP32S3 内部集成了无线局域网(WLAN)模块;
- 工作频段:仅支持2.4GHz
- 标准:符合802.11 b/g/n 标准
- 工作模式:STA(站点模式)、AP(接入点模式)、STA+AP(混合模式)
2 工作流程

3 API
3.1 CMakelists和头文件
// 头文件中引入Wi-Fi模块的库,cmakelists
set(requires
esp_Wi-Fi
driver)
#include "esp_Wi-Fi.h"
#include "nvs_flash.h"
3.2 调用nvs_flash()函数,初始化NVS
nvs_flash_init()
3.3 调用esp_netif_init()函数,初始化LWIP协议栈
esp_err_t esp_netif_init(void);
3.4 调用esp_event_loop_create_default()函数,创建循环事件组
esp_err_t esp_event_loop_create_default(void);
3.5 调用esp_event_handler_register()函数,向循环事件组中注册要监听的事件组以及事件编号,并创建事件函数
void Wi-Fista_event_handler(void* event_handler_arg,esp_event_base_t event_base,int32_t event_id,void* event_data);
- 参数一,要传入的参数
- 参数二,事件组名称
- 参数三,具体的事件
- 参数四,存储该事件返回的全部信息
esp_err_t esp_event_handler_register(
esp_event_base_t event_base,// 事件组名称
/*
WIFI_EVENT wife事件组
IP_EVENT ip事件组
*/
int32_t event_id, // 该事件组下的具体事件
/*
ESP_EVENT_ANY_ID 表示该事件组下所有的事件,其他的转到上一个事件组里的声明中自主选择即可
*/
esp_event_handler_t event_handler,// 创建事件函数
void *event_handler_arg // 向事件函数中传入的参数
);
3.6 调用esp_netif_create_default_Wi-Fi_sta()函数,将STA模块/AP模块 与LWIP协议栈单元连接
esp_netif_t* esp_netif_create_default_Wi-Fi_sta(void);
3.7 调用esp_Wi-Fi_init()函数,初始化与WIFI相关的底层硬件
Wi-Fi_init_config_t cfg = WIFI_INIT_CONFIG_DEFAULT();
// 因为要配置的参数比较多,一般选择用默认的即可
esp_err_t esp_Wi-Fi_init(const Wi-Fi_init_config_t *config);
- 参数一,结构体指针
3.8 调用esp_Wi-Fi_set_mode()函数,设置WIFI模块工作模式(STA模式/AP模式)
esp_err_t esp_Wi-Fi_set_mode(Wi-Fi_mode_t mode);// 用于设置wife的工作模式
/*
WIFI_MODE_NULL 不设置模式
WIFI_MODE_STA, STA模式
WIFI_MODE_AP, AP模式
WIFI_MODE_APSTA, 混合模式
WIFI_MODE_NAN, NAN邻近感知模式
*/
3.9 调用esp_Wi-Fi_set_config()函数,配置STA/AP模式参数
Wi-Fi_config_t Wi-Fista_config =
{
.sta = // 对sta进行设置
{
.ssid = ,// 账号
.password = // 密码
}
//.ap = {}
//.nan = {}
}
esp_err_t esp_Wi-Fi_set_config(
Wi-Fi_interface_t interface,// 用于选择,我们要配置的哪一个模式的参数
/*
WIFI_IF_STA
WIFI_IF_AP
WIFI_IF_NAN
*/
Wi-Fi_config_t *conf // 结构体指针
);
3.10 调用esp_Wi-Fi_start()函数,启动WIFI模块
esp_err_t esp_Wi-Fi_start(void);
3.11 调用esp_Wi-Fi_connect()函数,启动WIFI连接
esp_Wi-Fi_connect()// 写在回调函数中,当检测到Wi-Fi事件就启用
3.12 调用esp_Wi-Fi_stop()函数,关闭WIFI模块
esp_Wi-Fi_stop()// 写在回调函数中,当检测到Wi-Fi事件就启用
es
4 补充
4.1 确认自己的IP地址
if(event_base == IP_EVENT)
{ if(event_id == IP_EVENT_STA_GOT_IP)
{
esp_netif_ip_info_t *event =(esp_netif_ip_info_t*)event_data;
/*
#define IPSTR "%d.%d.%d.%d"
#define esp_ip4_addr1_16(ipaddr) 识别前三位
#define esp_ip4_addr2_16(ipaddr) 识别第二个三位
#define esp_ip4_addr3_16(ipaddr) 识别第三个三位
#define esp_ip4_addr4_16(ipaddr) 识别后三位
*/
}
}
7.2 Wi-Fi功能——AP
1 CMakelists和头文件
// 头文件中引入Wi-Fi模块的库,cmakelists
set(requires
esp_Wi-Fi
driver)
#include "esp_Wi-Fi.h"
#include "nvs_flash.h"
2 调用nvs_flash()函数,初始化NVS
nvs_flash_init()
3 调用esp_netif_init()函数,初始化LWIP协议栈
esp_err_t esp_netif_init(void);
4 调用esp_event_loop_create_default()函数,创建循环事件组
esp_err_t esp_event_loop_create_default(void);
5 调用esp_event_handler_register()函数,向循环事件组中注册要监听的事件组以及事件编号,并创建事件函数
void Wi-Fista_event_handler(void* event_handler_arg,esp_event_base_t event_base,int32_t event_id,void* event_data);
- 参数一,要传入的参数
- 参数二,事件组名称
- 参数三,具体的事件
- 参数四,存储该事件返回的全部信息
esp_err_t esp_event_handler_register(
esp_event_base_t event_base,// 事件组名称
/*
WIFI_EVENT wife事件组
IP_EVENT ip事件组
*/
int32_t event_id, // 该事件组下的具体事件
/*
ESP_EVENT_ANY_BASE 表示该事件组下所有的事件,其他的转到上一个事件组里的声明中自主选择即可
*/
esp_event_handler_t event_handler,// 创建事件函数
void *event_handler_arg // 向事件函数中传入的参数
);
6 调用esp_netif_create_default_Wi-Fi_ap()函数,将STA模块/AP模块 与LWIP协议栈单元连接
esp_netif_t* esp_netif_create_default_Wi-Fi_ap(void);
7 调用esp_Wi-Fi_init()函数,初始化与WIFI相关的底层硬件
Wi-Fi_init_config_t cfg = WIFI_INIT_CONFIG_DEFAULT();
// 因为要配置的参数比较多,一般选择用默认的即可
esp_err_t esp_Wi-Fi_init(const Wi-Fi_init_config_t *config);
- 参数一,结构体指针
3.8 调用esp_Wi-Fi_set_mode()函数,设置WIFI模块工作模式(STA模式/AP模式)
esp_err_t esp_Wi-Fi_set_mode(Wi-Fi_mode_t mode);// 用于设置wife的工作模式
/*
WIFI_MODE_NULL 不设置模式
WIFI_MODE_STA, STA模式
WIFI_MODE_AP, AP模式
WIFI_MODE_APSTA, 混合模式
WIFI_MODE_NAN, NAN邻近感知模式
*/
3.9 调用esp_Wi-Fi_set_config()函数,配置STA/AP模式参数
Wi-Fi_config_t Wi-Fista_config =
{
.ap = // 对sta进行设置
{
.ssid = "123",// 账号
.ssid_len = strlen("123"),// 账号长度
.password = "12345678" ,// 密码
.max_connection = 4, // 最多连接数
.authmode = WIFI_AUTH_WPA_WPA2_PSK // 加密方式,要与手机连接时的加密方式一致
}
//.ap = {}
//.nan = {}
};
esp_err_t esp_Wi-Fi_set_config(
Wi-Fi_interface_t interface,// 用于选择,我们要配置的哪一个模式的参数
/*
WIFI_IF_STA
WIFI_IF_AP
WIFI_IF_NAN
*/
Wi-Fi_config_t *conf // 结构体指针
);
3.10 调用esp_Wi-Fi_start()函数,启动WIFI模块
esp_err_t esp_Wi-Fi_start(void);
7.3 Wi-Fi功能——时间
1 UTC/时间戳
- UTC 全称 Coordinated Universal Time(协调世界时),是全球统一的时间基准。UTC代表0时区的时间-英国伦敦格林尼治的当地时间。
- 时间戳是从1970年1月1日00:00:00的UTC时间,到当前时刻,一共经过的总秒数。简单来说,时间戳是一个纯数字整数,用一个数字来表示某一个时刻。


2 API
2.1 第一步:联网
2.1.1配置ESP32S3为STA模式并连接外网
2.2 第二步:时间同步
2.2.1 调用esp_sntp_setoperatingmode()函数,设置时间同步模式
2.2.2 调用esp_sntp_setservername()函数,设置要访问的NTP服务器
2.2.3 调用esp_sntp_init()函数,初始化SNTP
2.2.4 调用setenv()函数,设置当前时区
2.2.5 调用tzset()函数,使设置时区的操作立刻生效
2.3 第三步:读取时间
2.3.1 调用time()函数,获取时间存储单元中的时间戳
2.3.2 调用localtime()函数,将时间戳转换成日期(年\月\日\时\分\秒)
8.1 网页设置——图传
2.1 第一步:硬件连接与开发环境准备
2.1.1 将支持 UVC 标准的 Type-C 免驱摄像头插入 ESP32-S3 的原生 USB 主机接口
2.1.2 在 Arduino IDE 中安装 ESP32 开发板核心(版本≥2.0.14)
2.1.3 搜索并安装UVC_ESP32库(作者:ToBozo)
2.1.4 (可选)安装ESPAsyncWebServer库用于更高性能的异步 Web 服务
2.2 第二步:USB 主机与 UVC 摄像头初始化
2.2.1 调用USBHost.begin()函数,初始化 ESP32-S3 内置 USB 主机控制器
2.2.2 调用uvc.begin(width, height, fps, UVC_PIXEL_FORMAT_MJPEG)函数,配置摄像头参数
2.2.3 调用uvc.isConnected()函数,检查摄像头是否成功识别并连接
2.2.4 若连接失败,循环重试或重启 USB 主机控制器
2.3 第三步:WiFi 网络配置
2.3.1 调用WiFi.begin(ssid, password)函数,配置 ESP32-S3 为 STA 模式并连接外网
2.3.2 循环调用WiFi.status()函数,等待 WiFi 连接成功
2.3.3 调用WiFi.localIP()函数,获取 ESP32-S3 的局域网 IP 地址
2.3.4 (可选)调用WiFi.softAP(ssid, password)函数,配置为 AP 模式供设备直连
2.4 第四步:Web 服务器搭建与 MJPEG 流服务
2.4.1 调用WebServer server(80),创建 HTTP Web 服务器实例(监听 80 端口)
2.4.2 注册根路径"/“路由,返回包含视频播放器的 HTML 页面
2.4.3 注册流路径”/stream"路由,处理 MJPEG 视频流请求
2.4.4 在流处理函数中,发送multipart/x-mixed-replace格式的 HTTP 响应头
2.4.5 循环调用uvc.isFrameAvailable()函数,检查是否有新的摄像头帧
2.4.6 调用uvc.getFrameBuffer()和uvc.getFrameSize()获取 MJPEG 帧数据和长度
2.4.7 通过 HTTP 连接将 MJPEG 帧推送给浏览器,然后调用uvc.releaseFrameBuffer()释放缓冲区
2.5 第五步:主循环与任务调度
2.5.1 在主循环中调用server.handleClient()函数,处理所有 Web 客户端请求
2.5.2 在主循环中调用USBHost.task()函数,处理 USB 主机底层事件
2.5.3 (进阶)使用xTaskCreate()创建独立任务处理摄像头帧捕获,避免阻塞 Web 服务
2.6 第六步:测试与验证
2.6.1 打开串口监视器,查看 ESP32-S3 输出的 IP 地址
2.6.2 在同一局域网的电脑 / 手机浏览器中输入该 IP 地址
2.6.3 验证浏览器是否能正常显示实时摄像头画面
2.6.4 测试视频帧率和延迟,调整分辨率 / 帧率参数优化性能
总结
这份笔记覆盖了 ESP32-S3 开发中最常用的几类能力:开发环境、工程结构、FreeRTOS 任务通信、外设驱动、存储、Wi-Fi 与网页交互。实际项目中建议先确认 ESP-IDF 版本,再结合芯片手册、模组原理图和官方示例逐步验证外设功能,避免因为引脚复用、电源、启动模式或中断上下文使用不当导致调试困难。
更多推荐



所有评论(0)