乐鑫ESP32 S3教程_基于ESP-IDF v5.0

本文最后更新于:2023年2月22日 晚上

个人教程,第一次制作,有配套视频教程,bilibili个人空间:合集和列表,文章章节顺序和列表一致。

内容简介

本手册将由浅入深,带领大家学习 ESP32-S3-WROOM-1-N16R8模组(ESP32-S3) 的各个功能,为您开启 ESP32-S3 的学习
之旅。本手册总共分为三篇:

  1. 硬件篇:主要介绍本手册配套的硬件平台(开发板);
  2. 软件篇:主要介绍ESP32-S3常用开发软件的使用以及一些下载调试的技巧,并详细介绍了几个常用的系统文件(程序);
  3. 实战篇,主要通过 n 个实例(绝大部分都是使用 EDP-IDF 库完成的)带领大家一步步深入了解 ESP32-S3。

本手册为 ESP32-S3-WROOM-1-N16R8模组的配套教程,有详细原理图以及所有实例的完整代码,这些代码都有详细的注释,所有源码都经过我们严格测试,不会有任何警告和错误,另外,源码有我们生成好的 hex 文件,大家只需要通过仿真器下载到开发板即可看到实验现象,亲自体验实验过程。

本手册不仅非常适合广大学生和电子爱好者学习 ESP32-S3-WROOM-1-N16R8模组,其大量的实验以及详细的解说,也是公司产品开发的不二参考

前言

01-开发板

使用源地ESP32-S3核心板
也可以购买复刻版:ESP32 S3核心板

N16R8(16M 外扩flash/8M PSRAM)/双Type-C USB口/W2812 rgb/高速USB转串口
enter description here

ESP32-S3-0702 _9_

02-模组资料:模组和芯片关系

简单来说,模组包含芯片,是一个最小系统,我们只需要模组供电,并引出引脚等,就构成了开发板。
enter description here

如下图所示,单独的芯片时不能直接工作的,还需要外接晶振、Flash(基本所有的ESP32都是没有内部flash的,都需要外接flash工作)等才能正常工作

enter description here

02-资料获取

牢记:官网是最佳的学习渠道

以下是从官网提取的关于S3芯片的几个重要文档,其它文档(如硬件设计指南等)请去上面文档中心下载

另外还有一个最重要的在线文档:

推荐访问在线文档,该文档随时都会更新。另外轻易英文内容为准,中文的可能有翻译错误和更新延迟。

官方开发板资料:(产品开发参考,初学者不用看)

03-例程创建和项目文件结构介绍

  • 新建工程
    • 示例
    • 默认
    • 文件夹介绍(基本)
    • ESP IDF介绍
    • RTOS

04-GPIO引脚介绍

参考资料:

  • ESP32­S3­WROOM 技术规格书
  • ESP32-S3 系列芯片 技术规格书

我们已经知道芯片模组是在芯片基础上添加外设得来的。所以其中的部分引脚是被外设芯片占用了,是无法使用的。
参考下列芯片和模组引脚对比,我们可以发现

  • 模组少了GPIO26-32,GPIO33-34引脚
    我们从芯片引脚介绍得知,GPIO26-32用于内部falsh/PSRAM通信
    另外我们在模组的最下面介绍也得知:在带有 OSPI PSRAM(即内置芯片为 ESP32-S3xxR8)的模组中,管脚 IO35、 IO36、 IO37 用于连接至模组内部集成的 OSPI PSRAM,不可用于其他功能
  • 部分引脚只能作为单输入、输出的。GPIO39、41、42、44只能输入、无高阻态
    GPIO26、29、30、40、43、47、48只能输出,其中43无高阻态
  • 当WIFI一致保持连接时,ADC2不可用
    enter description here
    我们可以继续查看下面的关系表,可以发现
  • 对于八线SPI(内置R8 PSRAM),GPIO26-37都不可使用
  • 对于八线已下的,GPIO26-32都不可使用
    enter description here
    那么GPIO22-34引脚去哪了呢,查看模组原理图,发现
  • GPIO22-34官方并没有被引出来
    所以如果我们使用的是八线以下的falsh/PSRAM,那么这两个引脚就被浪费了,所以我们自己设计模组时,是可以使用这两个引脚的,这样就相对于官方模组额外多了两个引脚(仅限八线以下的falsh/PSRAM)
    enter description here

另外我们还要看另一组特殊的IO口,Strapping 管脚 。

  • GPIO0
  • GPIO45
  • GPIO46
  • GPIO3
    芯片每次上电或复位时,都需要一些初始配置参数,如加载芯片的启动模式、 flash 存储器的电压等。这些参数通过 strapping 管脚控制。复位放开后, strapping 管脚和普通 IO 管脚功能相同。
    芯片复位时, strapping 管脚在复位时控制以下参数:
    • 芯片启动模式 – GPIO0 和 GPIO46
    • VDD_SPI 电压 – GPIO45
    • ROM 代码日志打印 – GPIO46
    • JTAG 信号源 – GPIO3
    总之请确保这些引脚在上电时,都能处于默认配置值。
    enter description here

总结:

  • 对于八线SPI(内置R8 PSRAM),GPIO26-37都不可使用
  • 对于八线以下的,GPIO26-32都不可使用
  • Strapping 管脚上电保持默认值
  • 使用USB下载时,软件会自动复位芯片,并进入boot下载模式。GPIO0不要有外部上拉,否则无法自动下载
  • GPIO22-34在官方模组中并没有被引出来
  • EN引脚,上拉,上电使能芯片

ESP32 C3

对于内置flash版本的,以下引脚不可用
flash引脚
GPIO11不可用
enter description here
GPIO2、8、9为 strapping 管脚,GPIO9 内部默认弱上拉

  • 实测GPIO2悬空也能用,但最好外部上拉
  • GPIO8必须外部上拉,否则无法进入boot下载模式
    enter description here

05-IO MUX 和 GPIO 交换矩阵

ESP32-S3 芯片有 45 个物理通用输入输出管脚 (GPIO Pin)。每个管脚都可用作一个通用输入输出,或连接一个
内部外设信号。利用 GPIO 交换矩阵、IO MUX 和 RTC IO MUX,可配置外设模块的输入信号来源于任何的 GPIO
管脚,并且外设模块的输出信号也可连接到任意 GPIO 管脚。这些模块共同组成了芯片的输入输出控制。

注意:这 45 个物理 GPIO 管脚的编号为:0 ~ 21、26 ~ 48。这些管脚既可作为输入又可作为输出管脚。

GPIO 交换矩阵特性

  • GPIO 交换矩阵是外设输入输出信号和 GPIO 管脚之间的全交换矩阵;
  • 175 个数字外设输入信号可以选择任意一个 GPIO 管脚的输入信号;
  • 每个 GPIO 管脚的输出信号可以来自 184 个数字外设输出信号的任意一个;
  • 支持输入信号经 GPIO SYNC 模块同步至 APB 时钟总线;
  • 支持输入信号滤波;
  • 支持 Sigma Delta 调制输出 (SDM);
  • 支持 GPIO 简单输入输出。

IO MUX 特性

• 为每个 GPIO 管脚提供一个寄存器 IO_MUX_GPIOn_REG,每个管脚可配置成:
– GPIO 功能,连接 GPIO 交换矩阵;
– 直连功能,旁路 GPIO 交换矩阵。
• 支持快速信号如 SPI、JTAG、UART 等可以旁路 GPIO 交换矩阵以实现更好的高频数字特性。所以高速信号会直接通过 IO MUX 输入和输出

RTC IO MUX 特性

• 控制 22 个 RTC GPIO 管脚的低功耗特性;
• 控制 22 个 RTC GPIO 管脚的模拟功能;
• 将 22 个 RTC 输入输出信号引入 RTC 系统

enter description here

总结

  • GPIO 交换矩阵就是说所有的外设都可以映射到设疑引脚,但有的速度可能会慢些
  • IO MUX 特指GPIO功能和部分外设直连。不经过GPIO交换矩阵,速度快,性能好,但只支持部分外设(目前只有SPI和UART)
  • RTC与低功耗和模拟输入输出有关,和IO MUX平级
  • 在设计时,优先使用IO MUX引脚功能,在使用GPIO 交换矩阵
  • 在程序编程时,软件会自动匹配,无需配置IO MIX和GPIO矩阵

以下有两张表说明GPIO 交换矩阵和IO MUX 区别
IO MUX管教表格就说明引脚的一般功能,非常类似于一般STM32的引脚图,这里的功能0-4说的就是引脚的不能复用功能,不需要经过内部的GPIO矩阵,速度快,性能好
ESP32­S3 技术参考手册:
enter description here

外设管脚分配表,说明除了ADC、触摸、SPI0/1 外设,其它的外设都可以映射到任意引脚
ESP32-S3 系列芯片 技术规格书
enter description here

环境篇

Espressif-IDE简介与安装

简介

Espressif IDE 是乐鑫基于 Eclipse CDT,专为乐鑫物联网开发框架 ESP-IDF 打造的独立集成开发环境 (Integrated Development Environment, IDE),支持用户使用 ESP-IDF 实现端到端物联网应用开发。

Espressif IDE 附带最新的 ESP-IDF Eclipse 插件、基本的 Eclipse CDT 插件、OpenOCD 插件以及其他来自 Eclipse 平台的第三方插件,以支持构建 ESP-IDF 应用程序。
enter description here

ESP IDF 是乐鑫官方推出的物联网开发框架(类似标准库或HAL库)

Espressif IDE 的主要特性

  • 在 Eclipse CDT 环境下构建,易于使用
  • 专为 ESP-IDF 应用程序开发而打造
  • 集成编译所需的 ESP 工具链配置
  • 可自动配置编译环境变量
  • 提供新建项目向导以及 ESP-IDF 快速入门模板
  • 具备领先的的编辑、编译以及语法着色功能
  • 配有预建的函数头和函数定义导航
  • 支持安装一个全新的 ESP-IDF 或是配置现有的 ESP-IDF
  • 可直接从 IDE 中安装和配置 ESP-IDF 工具
  • 配有用于项目设置的 SDK 配置编辑器
  • 集成 CMake 编辑器,用于编辑 CMake 文件,如 CMakeLists.txt
  • 支持基于 CMake 的编译系统
  • 支持通过 UART 和 JTAG 烧录
  • 支持自定义的 ESP-IDF OpenOCD 调试功能,包含预配置和设置
  • 支持 GDB 硬件调试
  • 集成 ESP-IDF 串行监视器,用于查看程序的串行输出
  • 配备预置编译环境变量的 ESP-IDF 终端
  • 配备应用程序大小分析编辑器,用于分析应用程序的静态内存使用情况
  • 支持堆分析,用于进行内存分析并发现内存泄漏
  • 支持 Panic 模式下 GDB Stub 调试
  • 提供应用层跟踪,以便使用启动和停止命令,分析程序行为
  • 支持 ESP32、ESP32-S2、ESP32-S3 和 ESP32-C3 系列芯片
  • 支持中英文
  • 具备可扩展性,适用于 Eclipse 生态体系中的其他第三方插件
  • 支持 Windows、macOS 和 Linux 操作系统

Espressif-IDE 离线安装器,集成了 OpenJDK、Python、CMake、Git、ESP-IDF、Eclipse IDE、IDF Eclipse 插件及相关构建工具,类似与Keil。

下载

下载:Espressif-IDE 离线安装器
如果上述链接失效,可按下述方法找到:

  1. 进入ESP-IDF 编程指南
  2. 左侧栏,找到手动安装,单击右侧链接进入下载界面
    3.

这里选择 espressif-ide-setup-2.8.1-with-esp-idf-5.0 离线安装包,包含IDE和IDF。上面第一个选项是在线安装,会很慢,不推荐
enter description here

安装

  1. 双击下载的安装包,开始安装
    enter description here
  2. 语言默认中文,点击确定
    enter description here
  3. 同意协议,单击下一步
    enter description here
  4. 等待软件完成系统检查(如果检查有问题则无法安装,需要根据提示解决相应问题),单击下一步
    enter description here
  5. 选择安装路径。注意路径一定不要有中文、括号、空格及其它字符
    强烈建议保持默认路径及C盘不动,否则后续在编译和调试可能会出现一系列离谱错误,所以请保持默认路径不要动。光软件安装测试就用了两周时间的吐血教训!
    enter description here
  6. 选择组件,我们选择完全安装,单击下一步
    enter description here
  7. 单击 安装,软件即开始自动安装,这个过程可能又10-30min,请耐心等待,中途不要退出安装或关机(最好什么都不要操作)
    enter description here
  8. 安装完成,这里默认勾选所有,单击完成
    enter description here
  9. 之后软件会自动运行类似下面的命令行,我们等待最后一行出现 esp-idf 字段时(此时命令行已停止自动运行),右上角❌关闭命令行即可
    enter description here
  10. 此时桌面会出现三个软件图标,我们只需要使用 Espressif IDE 就行了,另两个不用关心
    enter description here

新建工程与工程结构讲解

打开并运行软件,会提示我们选择工作空间,工作空间相当于一个存档,保存了IDE的相关设置,如字体、排版、界面设置等自定义设置。如果下一次换了一个工作空间位置,软件就会恢复到默认设置。

工作空间的目的在于协作,我们将工程文件和工作空间文件夹一同发给别人,另一个人打开软件时,在下面界面就可以选择我们发送的那个工作空间文件夹,这样别人就能保持和你一样的软件设置,放置不同人使用的软件不用,导致一些位置错误。

这里我们时第一次使用,保持默认即可。你也可以自定义位置
enter description here

首次运行会显示欢迎界面,这里有两个很重要的链接,不过如果你点击链接后,会直接在软件中打开,而不是浏览器打开,你也可以通过下方的链接访问

第一个链接其实是 ESP-IDF Eclipse 插件的使用说明,但我们的Espressif-IDE就是基于Eclipse 的,所以也可以阅读参考学习IDE的使用
第二个链接就是关于ESP-IDF的教程了,类似于标准库或HAL库,这是官方教程, 是最好的学习资料,当然本教程也是基于官方资料讲解,并且更加细、易懂,欢迎大家学习参考

enter description here
关闭欢迎界面,来到主窗口,主要分为5个区域

  1. 菜单栏
  2. 项目浏览器,显示项目文件
  3. 编辑页面,编写代码的地方
  4. 信息栏,包含有信息窗口、查找、错误信息等关于工程和代码的信息
  5. 辅助栏,类似信息栏,也会显示部分信息,主要帮助理解代码结构的,必须显示一些类、函数名、全局变量等

以上窗口的详细介绍,会在后续的教程内容中逐步讲解
enter description here

大家可能也注意到,我们安装时,选择了中文,但这里的界面只有部分时中文,这个是官方还没汉化完全,所以只能这么使用了
特别提醒大家,在菜单栏是有更改语言的选项的,但请不要点击!不要点击!不要点击! 否则软件会直接崩溃,彻底无法打开,只能重装软件。这是IDE软件已知有的bug了,至今还未修复。
enter description here

新建样例工程

工程结构:
https://xie.infoq.cn/article/ddb67ebf28bfe7fecce6a2368
https://blog.csdn.net/qq_40500005/article/details/113840391

  1. 有两种方式新建工程,如下图所示
  • 通过项目浏览器中的快捷链接
  • 左上角 File - > New -> 乐鑫IDF项目
    enter description here
  1. 输入项目名 SampleProject,并选择项目存放位置,我们建议给项目新建一个文件夹 01-SampleProject,另外注意路径不要有中文、空格、括号等
    enter description here
  2. 选择模板,这里我们选择官方提供的 sample_project 项目模板(至于为什么要使用模板,待会就会讲到),单击 Finish 完成工程创建
    在这里我们也可以看到,官方提供了很多模板例程,这是一个很好的学习资料,大家可以自行尝试不同模板例程。

    使用模板创建工程,有一个问题就是,IDE会自动修改项目名称。所以你需要在这里再次手动修改项目名

enter description here
4. 以下就是我们的工程了。此时 main.c 里只有一个主函数,还没有任何功能。
同时也可以发现,程序中会有波浪线,和其它错误警告,在下面的编译完成后,就会都消失的
enter description here

这里我们可以发现,主程序并不是常见的 main,而是 app_main,这是因为esp32默认待 Free RTOS 系统,关于该操作系统后面涉及时在讲解;对于初学,只需要把它当成 main 函数就行了。项目目录

编译和下载

  1. 在编译前我们需要选择目标芯片。单击齿轮图标
    enter description here
  2. IDF目标选择芯片类型 esp32s3

    注意这里一定要先选择芯片型号,IDE有时会抽风,会自动变更这里的芯片型号,所以每次编译前最好都确认下。

enter description here
7. 单击左上角锤子图标,编译程序
编译就是将我们编写的程序变成可以在芯片上运行的文件,然后可以被我们下载到芯片中,芯片才能工作。
enter description here
8. 我们可以在控制台(Console)窗口看到编译过程,同时在其右下角看到编译进度条。
enter description here
9. 编译完成,我们可以看到0个错误、0个警告,说明我们的程序没有任何问题,可以下载了。如果这里有错误我们就需要根据提示修复错误,警告看情况修复。
我们也可以在该窗口看到程序文件大小、占用内存大小等信息。
编译完成后,程序中一开始的那些警告、波浪线也会随之消失
enter description here
同时在工程名,右键,可以打开快捷菜单,打开 应用程序内存分析
enter description here
可以更加直观的观察到内存占用情况
enter description here
10. 下载。点击芯片类型边的齿轮图标
enter description here
11. 在串口号选择开发板串口号。如果不确定是那个端口,可以插拔一下开发板,有变动的端口就是我们需要确定的端口号。
enter description here
12. 单击开始图标,开始下载
enter description here
13. 在控制台窗口,可以观看到下载进度。最后一行显示 Done 表示下载完成
由于我们程序中什么都没有,因此下载后,开发板并没有任何反应。
enter description here

如果下载时遇到如下错误提示:
说明串口被其它软件(串口调试助手等)占用了,断开该软件的连接,重新下载即可

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
A fatal error occurred: Could not open COM9, the port doesn't exist

CMake Error at run_serial_tool.cmake:55 (message):

python;;C:/Espressif/frameworks/esp-idf-v5.0/components/esptool_py/esptool/esptool.py;--chip;esp32s3
failed


FAILED: CMakeFiles/flash D:/00-learning/010-ESP32/S3/2_sources/05-Log/build/CMakeFiles/flash

cmd.exe /C "cd /D C:\Espressif\frameworks\esp-idf-v5.0\components\esptool_py && C:\Espressif\tools\cmake\3.24.0\bin\cmake.exe -D IDF_PATH=C:/Espressif/frameworks/esp-idf-v5.0 -D SERIAL_TOOL=python;;C:/Espressif/frameworks/esp-idf-v5.0/components/esptool_py/esptool/esptool.py;--chip;esp32s3 -D SERIAL_TOOL_ARGS=--before=default_reset;--after=hard_reset;write_flash;@flash_args -D WORKING_DIRECTORY=D:/00-learning/010-ESP32/S3/2_sources/05-Log/build -P C:/Espressif/frameworks/esp-idf-v5.0/components/esptool_py/run_serial_tool.cmake"

ninja: build stopped: subcommand failed.

ninja failed with exit code 1, output of the command is in the d:\00-learning\010-esp32\s3\2_sources\05-log\build\log\idf_py_stderr_output_5680 and d:\00-learning\010-esp32\s3\2_sources\05-log\build\log\idf_py_stdout_output_5680

新建默认工程

步骤和上面类似。只是在模板选择界面,我们保持默认,不选择任何模板,直接点击 Finish 完成工程创建
enter description here
可以看到,默认工程中,main函数中是有一个打印函数的,打印函数的功能就是将其中的字符串通过串口发送给电脑,我们可以通过串口助手查看到开发板发送的信息
enter description here
如下所示,串口波特率默认115200
enter description here

导入工程

之所叫导入教程,是因为IDE的bug,无法正常打开工程

  1. 按照一般如Eclipse和STM32CubeIDE使用方式,我们直接点击 从文件系统打开工程
    enter description here
  2. 选择我们的工程文件夹,不要勾选main文件
    enter description here
    否则你的mian文件夹也会出现 .project 文件。如果你的已经有了,直接删除即可
    enter description here
  3. 你会发现,无论怎么编译,你的工程都会有这个波浪线,提示错误和警告。所以该方法导入项目并不可行
    enter description here

我们选择另一种可行方案,导入工程

  1. 按以下来个那种方式,都可以,单击导入 import
    enter description here
  2. 选择 乐鑫 -> 现有IDF项目
    enter description here
  3. 单击浏览选择项目文件夹。单击Finish完成导入
    enter description here
  4. 此时编译就不会有任何错误了
    enter description here

调试

要启用调试功能,需要先配置ESP-IDF工具

如果后续编译时遇到如下报错“

1
ninja: error: loading 'build.ninja': esp32

按下面教程重新安装ESP-IDF工具即可。一般都是因为windows系统自动更新导致的,更换工作空间也会导致这个错误。

至于为什么这样做,我也不知道,只知道按下面操作才能启动调试功能。参考视频 【乐鑫开发者大会-13】在 Espressif-IDE 中使用 ESP-IDF 开发应用】,3分35秒开始

enter description here
然后选择使用文件系统现有ESP-IDF,选择软件安装目录下的文件夹,如果你的软件默认在C盘,路径应该和下图一致
enter description here
Yess
enter description here
保持默认,点击 安装工具
enter description here
等待系统下载安装完成,这个过程可能很慢或者失败,一般半小时左右。如果半小时还没动静,建议关闭软件重新安装。右下角是进度条
enter description here
显示如下,标志安装完成
enter description here
安装完成,重启电脑。一定要重启电脑。
重新打开软件,可能会弹出如下提示,点击 即可。
enter description here
在工程名下拉,选择New Launch Configuration
enter description here
单击 Debug,选择 ESP-IDF GDB OpenOCD Debugging 这里是配置调试工具,Next
enter description here
Main 标签页保持默认,注意这里要有 .elf文件
enter description here
Debugger 标签页,目标选择问哦们的芯片 S3,开发板应该翻译为调试工具,选择芯片内部 USB-JTAG,点击 Apply 保存设置,单击 Finish 退出设置JTAG调试

根据官方描述,ESP32 C3和S3都内置JTAG调试器,即我们只需要通过USB线连接到ESP32的USB引脚,就能通过IDE直接调试,不需要额外的如ST-Link或Jtag调试硬件工具,非常方便。

USB接口同时也支持虚拟串口,所以在接USB后,我们就不要再接串口RX和TX调试了,反而还省了两个口。否则像其它ESP32都需要使用串口下载程序的,因为S3和C3的USB自带虚拟串口,所以就不需要串口 引脚了。所以建议用S3和C3时,使用USB口调试、调试、虚拟串口打印。我们在使用USB连接ESP32时,电脑会自动生成一个串口,就跟普通的串口一样,使用即可。

同时注意,使用调试功能,USB引脚就不能用于其它功能了。除非后续不再使用调试功能,usb两个引脚才可以作为他用。

后面教程都会只用USB接口和虚拟串口,虚拟串口默认和串口0相连

enter description here
此时我们的目标文件名,已经自动变成 test Configuration,同时还需要将左侧的Run改为Debug
这里可以发现出现波浪线提示错误,单击最左侧 build 重新编译即可。
enter description here
检查目标芯片和串口号没有错误
enter description here
单击左侧的虫子图标,开始调试
enter description here
等待软件编译完成,出现如下弹窗,选择 Switch,切换到调试界面
enter description here
软件会自动停留在程序开始位置,可以使用工具栏的调试工具进行调试运行。
enter description here
值得注意的是,当我们停止调试并返回到Run模式,是需要我们手动操作的,如下,都需要我们手动选择。

所以这里建议,以后的项目都默认直接使用Debug模式。在Debug模式下编写程序和调试。避免来回切换。
enter description here
在调试模式下,如果不想调试,只想下载,可以单击运行图标,选择工程文件名,即可下载。
注意不要选错了,不要选第二个Configurantion(用于调试的)。
enter description here

自带终端,串口助手使用

打开终端,选择串口号,其它保持默认(串口监控)
enter description here
界面底部栏,终端标签页会显示板子发送过来的串口信息
enter description here

建议还是使用专门的串口调试软件,如正点原子的 XCOM 调试软件
默认波特率115200
串口助手

内存分析查看

ESP32 Programmers’ Memory Model

编译后,在信息里会输出内存大小,如下图所示,总内存
enter description here

  • IRAM用于代码(指令),DRAM用于数据。这里有两个是因为S3是双核
  • Iram是用于代码的,也就是函数,但只是那些需要在iram中快速执行的函数,而不是flash。 例如中断处理程序。 基于链接器脚本或手动标记具有iram属性的函数,将代码放置在iram中。 有许多menuconfig设置与iram函数放置相关。

也可以使用乐鑫的工具查看,如下图,在项目名上右键
enter description here
软件会以图形形式展现内存占用情况
enter description here
Details细节可以查看每个文件的内存占用
enter description here

新建组件

内容参考:ESP-IDF编程指南 :API 指南 » 构建系统

espressif IDE如果导入外部文件夹是一件比较麻烦的事,涉及CmakeLists文件的编写,并且IDE的图形操作见面并没有添加源文件的设置选项(原Eclipse是有的),这就意味着不能和Eclipse一样自己新建文件夹,再将文件夹包含到头文件,实现文件的导入或新建

但Espressif IDE给了我们另一个选择,组件。组件其实就是具备一个单独功能的文件夹,包含一个 .c和.h文件,保存在指定文件夹 components 下。实现模块化编程。

file -> New -> 乐鑫IDF组件

enter description here
输入组件名称
enter description here
确认OK
enter description here
软件会自动在项目文件夹下创建 components文件和test组件文件夹。

  • components是系统目录,不可更改
  • test是我们自己的组件名
  • 这里的test组件实际和IDF目录下的components下的组件是一个意思,就类似于库
    enter description here
    但这里有个小问题。就是组件依赖。比如我们要在test.h 文件中包含 #include "driver/gpio.h"是不行的,编译会显示错误。但这个问题再以前IDE版本中并没有。为了解决这个问题。
    我们需要打开test目录下的 CMakeLists.txt 文件。其中源码如下:
    1
    2
    idf_component_register(SRCS "test.c"
    INCLUDE_DIRS "include")
    我们需要修改源码。修改后如下
    1
    2
    3
    idf_component_register(SRCS "test.c"
    INCLUDE_DIRS "include"
    REQUIRES driver)
    意思就是告诉编译器我们的组件依赖driver组件(IDF目录下的components下)。这样我们再去头文件包含就不会又错了。。

FreeRTOS操作系统

参考:
https://blog.csdn.net/believe666/article/details/127205502

基础知识

我们以前在使用 51、AVR、STM32 单片机裸机(未使用系统)的时候一般都是在
main 函数里面用 while(1)做一个大循环来完成所有的处理,即应用程序是一个无限的循环,循
环中调用相应的函数完成所需的处理。有时候我们也需要中断中完成一些处理。相对于多任务
系统而言,这个就是单任务系统,也称作前后台系统,中断服务函数作为前台程序,大循环
while(1)作为后台程序,如图所示:
enter description here

前后台系统的实时性差,前后台系统各个任务(应用程序)都是排队等着轮流执行,不管你
这个程序现在有多紧急,没轮到你就只能等着!相当于所有任务(应用程序)的优先级都是一样
的。但是前后台系统简单啊,资源消耗也少啊!在稍微大一点的嵌入式应用中前后台系统就明
显力不从心了,此时就需要多任务系统出马了。

多任务系统会把一个大问题(应用)“分而治之”,把大问题划分成很多个小问题,这些小问题可以单独的作为一个小任务来处理。这些小任务是并发处理的,注意,并不是说同一时刻一起执行很多个任务,而是由于每个任务执行的时间很短,导致看起来像是同一时刻执行了很多个任务一样。多个任务带来了一个新的问题,究竟哪个任务先运行,哪个任务后运行呢?

完成这个功能的东西在 RTOS 系统中叫做任务调度器。不同的系统其任务调度器的实现方法也不同,比如 FreeRTOS 是一个抢占式的实时多任务系统,那么其任务调度器也是抢占式的,运行过程如图 5.1.2 所示:
enter description here

高优先级的任务可以打断低优先级任务的运行而取得 CPU 的使用权,这样就保证了那些紧急任务的运行。这样我们就可以为那些对实时性要求高的任务设置一个很高的优先级,比如自动驾驶中的障碍物检测任务等。高优先级的任务执行完成以后重新把 CPU 的使用权归还给低优先级的任务,这个就是抢占式多任务系统的基本原理。

任务状态切换

任务挂起函数
vTaskSuspend()

挂起指定任务,被挂起的任务绝不会得到 CPU 的使用权
vTaskSuspendAll()

将所有的任务都挂起  任务恢复函数

复制
vTaskResume()
vTaskResume()
xTaskResumeFromISR()
1.
2.
3.
任务恢复就是让挂起的任务重新进入就绪状态,恢复的任务会保留挂起前的状态信息,在恢复的时候根据挂起时的状态继续运行。xTaskResumeFromISR() 专门用在中断服务程序中。无论通过调用一次或多次vTaskSuspend()函数而被挂起的任务,也只需调用一次恢复即可解挂 。

 任务删除函数 vTaskDelete()用于删除任务。当一个任务可以删除另外一个任务,形参为要删除任 务创建时返回的任务句柄,如果是删除自身, 则形参为 NULL。

enter description here

  1. 函数 vTaskDelete()
    被删除了的任务不再存在,也就是说再也不会进入运行态。任务被删除以后就不能再使用此任务的句柄!如果此任务是使用动态方法创建的,也就是使用函数 xTaskCreate()创建的,那么在此任务被删除以后此任
    务之前申请的堆栈和控制块内存会在空闲任务中被释放掉,因此当调用函数 vTaskDelete()删除任务以后必须给空闲任务一定的运行时间。
    只有那些由内核分配给任务的内存才会在任务被删除以后自动的释放掉,用户分配给任务的内存需要用户自行释放掉,比如某个任务中用户调用函数 pvPortMalloc()分配了 500 字节的内存,那么在此任务被删除以后用户也必须调用函数 vPortFree()将这 500 字节的内存释放掉,否则会导致内存泄露。

  2. 函数 vTaskSuspend()
    此函数用于将某个任务设置为挂起态,进入挂起态的任务永远都不会进入运行态。退出挂起态的唯一方法就是调用任务恢复函数 vTaskResume()或 xTaskResumeFromISR()。,函数原型如
    注意!如果参数为 NULL 的话表示挂起任务自己。

  3. 函数 vTaskResume()
    将一个任务从挂起态恢复到就绪态,只有通过函数 vTaskSuspend()设置为挂起态的任务才可以使用 vTaskRexume()恢复!恢复的任务会保留挂起前的状态信息,在恢复的时候根据挂起时的状态继续运行。

  4. 函数 xTaskResumeFromISR()
    此函数是 vTaskResume()的中断版本,用于在中断服务函数中恢复一个任务。

任务创建

1
2
3
4
5
6
BaseType_t xTaskCreate( TaskFunction_t 			pxTaskCode,
const char * const pcName,
const uint16_t usStackDepth,
void * const pvParameters,
UBaseType_t uxPriority,
TaskHandle_t * const pxCreatedTask )

参数:

  • pxTaskCode:任务函数。
  • pcName:任务名字,一般用于追踪和调试,任务名字长度不能超过。configMAX_TASK_NAME_LEN
  • usStackDepth:任务堆栈大小,注意实际申请到的堆栈是 usStackDepth 的 4 倍。其中空闲任务的任务堆栈大小为 configMINIMAL_STACK_SIZE。
  • pvParameters: 传递给任务函数的参数。
  • uxPriotiry: 任务优先级,范围 0~ configMAX_PRIORITIES-1。
  • pxCreatedTask: 任务句柄,任务创建成功以后会返回此任务的任务句柄,这个句柄其实就是任务的任务堆栈。此参数就用来保存这个任务句柄。其他 API 函数可能会使用到这个句柄

pxCreatedTask 任务句柄才是我们创建的任务,他是任务的ID,相当于身份证,可以通过句柄就能获取任务的所有信息。
而pxTaskCode 任务函数,是任务的执行部分,就是任务要做什么。因此多个不同任务是可以有相同的任务函数的。如果我们的任务函数是唯一的,并且也不需要对任务做任何额外操作,只是作为一个任务运行。那么pxCreatedTask 可以为空。(但要注意变量共享问题)
而pcName 只是一个名字,为了在调试时,可以打印这个名字,识别是什么任务,在程序中并无实际意义

建议自行参考视频资料学习:

IDE使用常见问题

1. has no explicit encoding set警告

编译时报如下警告

1
has no explicit encoding set

解决办法

  • 在Problems视图中,选择警告,按Ctrl+1并应用提供的QuickFix(将项目编码显式设置为工作区编码)
  • 在“项目属性”中手动更改项目>:资源。Project > Properties: Resource

如果您希望忽略所有项目的此警告,则可以更改首选项:

  • General -> Workspace -> Report missing project encoding as: Ignore
    常规->工作区->将缺少的项目编码报告为:忽略
    enter description here

参考资料:

软件篇

Flash Download Tools 固件烧录工具的使用

软件下载地址:https://www.espressif.com.cn/zh-hans/support/download/other-tools
enter description here
软件安装包里有教程,这里说明几个重点

如何确认下载地址
首先你需要使用IDE个要下载的芯片下载程序,后在 Console 信息输出栏找到如下语句

  • 可以通过找到 COMx 口语句找到

enter description here
bin文件在项目文件夹下的build 文件夹下
enter description here
后按提示信息将 .bin 文件和对应地址填到选项框中。文件前后顺序无要求,只要对应的地址是对的就行

  • 这里有一个问题。就是bootloader的地址应该是0x1000,详见 引导加载程序 (Bootloader)
  • 但测试写0x0也是没问题的,之后为了安全还是改为0x1000

flash tools

ESP-IDF编译原理简述(CMakeLists/CMake)和构建自定义项目

https://blog.csdn.net/kangweijian/article/details/123283714

固件大小优化

https://blog.csdn.net/kangweijian/article/details/127497916

Partition Tables分区表

一个ESP32的闪存(Flash存储芯片)可以包含多个应用程序,以及许多不同类型的数据(校准数据、文件系统、参数存储等)。因此,分区表在闪存中被刷新到(默认偏移量)0x8000。

分区表长度为0xC00字节,因为我们允许最多95个条目。MD5校验和用于在运行时检查分区表的完整性,被附加在表数据之后。因此,分区表占据了大小为0x1000(4KB)的整个闪存扇区。因此,它后面的任何分区必须至少位于(默认偏移量)+ 0x1000。

分区表就是一张表格,是 .csv格式文件,下载时转换成bin文件存储在0x8000位置,并且占据占据了大小为0x1000(4KB)

我们在项目名右键,找到Partition Table Editor就能打开分区表,这个是默认的
Partition Tables分区表

  • NVS主要是给我们存储数据用的,类似EEPROM,比如wifi数据,用户信息、开机次数等等信息
  • PHY分区主要和射频通信配置相关,例如WIFI、RF、蓝牙,存储一些校准数据(一般用不到),参考:RF calibration射频校准什么是与ESP32相关的PHY?

以上两个分区时可以任意配置的,也可以直接删除不要。但下面的factory谨慎修改,factory里面存的就是我们的应用程序,系统在初始化后会自动跳转到该地址(0x10000) 运行程序,所以最后修改这里的偏移位置(0x10000),但我们可以修改其大小(Size),可以看到默认只有1M-Byte大小,如果我们的程序太大的话,是没办法下载运行的,就需要我们自行修改了分区表了。

系统自带几个默认的分区表配置,可以直接在 sdconfig 文件中修改,不需要在这里需改分区表。

sdconfig Partition Table
有三种默认配置

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
# 配置1:Factory app,two OTA definitions

# ESP-IDF Partition Table
# Name, Type, SubType, Offset, Size, Flags
nvs, data, nvs, 0x9000, 0x6000,
phy_init, data, phy, 0xf000, 0x1000,
factory, app, factory, 0x10000, 1M,

# 配置2:Factory app,two OTA definitions

# ESP-IDF Partition Table
# Name, Type, SubType, Offset, Size, Flags
nvs, data, nvs, 0x9000, 0x6000,
phy_init, data, phy, 0xf000, 0x1000,
factory, app, factory, 0x10000, 1.5M,

# 配置2:Factory app,two OTA definitions

# ESP-IDF Partition Table
# Name, Type, SubType, Offset, Size, Flags
nvs, data, nvs, 0x9000, 0x4000,
otadata, data, ota, 0xd000, 0x2000,
phy_init, data, phy, 0xf000, 0x1000,
factory, app, factory, 0x10000, 1M,
ota_0, app, ota_0, 0x110000, 1M,
ota_1, app, ota_1, 0x210000, 1M,

配置三主要用于OTA升级程序使用的。可以看出来如果我们的Flash大小大于4M(ESP32模组默认最小内存2MByte),那么这三个默认配置其实都没有用到Flash的全部内存位置,并不适合实际使用

我们可以自定义分区表:

  • 分区表选择 Custom
  • flash大小设置成我们芯片模组的真实大小,默认是2M,我的是16M的

partition table config

  • 项目右键、打开分区表编辑器
  • 将app内存大小增大,保存退出
  • 工程目录下会多出一个 partition 文件
  • 我们不需要单独下载,在程序下载时,IDE会自动编译下载这个分区表,替换掉默认的分区表

partition table edit

下面这张图可以帮助我们更好的理解分区

  • boot 的地址是固定的 0x1000 (ESP8266 的 boot 地址为固定的 0x0000),而且 boot 地址的加载早于分区表的加载,因此无需在分区表中表现,大小与 boot 配置项有关,可以在编译完成后查看build/bootloader/bootloader.bin 来确认当前配置项 boot 大小。
  • boot分区位置和大小是不能自定义的,但我们可以修改这里的bootloader.bin文件
    enter description here

结合之前flash下载工具的内存地址

  • boot在0x1000,分区表在0x8000,程序在0x10000
  • 0x8000-0x10000钟中间的是nvs和phy_init ,是运行时使用,存储信息的,因此不下载文件
    flash tools

静态库

参考资料:基于 ESP-IDF 将自定义的静态库制作成组件
使用 ESP-IDF 生成第三方的 .a 静态库并使用的流程

实战篇

1、printf

参考IDF官方例程:C:\Espressif\frameworks\esp-idf-v5.0\examples\get-started\hello_world\main\hello_world_main.c

新建样例工程(最简工程)。
添加如下代码:

1
2
3
4
5
6
7
#include <stdio.h>

void app_main(void)
{
printf("Hello\n");
}

编译下载程序,打开终端监视器
可以发现串口成功输出

USB虚拟串口和串口0都可以输出,应该时映射的,所以此时接串口0和USB虚拟串口都会有输出
enter description here
如果将后面换行符 \n去除,再下载

1
2
3
4
5
6
#include <stdio.h>

void app_main(void)
{
printf("Hello");
}

会发现并没有输出。这是因为串口发送会有一个缓存机制,芯片再检测到缓存满后,才会发送一次数据。而我们现在发送的数据并不能填充满缓存区,所以就没有输出。
有两种解决办法

  1. 和上面一样,再输出内容最后加上换行符,可以强制打印输出
  2. fflush(stdout) ,也是强制刷新缓冲区,冲洗掉缓存区内容
    该方法仅适用于传统串口,不适用于USB虚拟串口
    1
    2
    3
    4
    5
    6
    7
    #include <stdio.h>

    void app_main(void)
    {
    printf("Hello");
    fflush(stdout);
    }

sdkconfig项目配置

串口默认串口0,即GPIO43、44。默认波特率115200
enter description here

我们也可以修改默认串口。按下图打开sdkconfig项目配置工具,找到ESP System Setttings,找到console输出配置,这里有两个关于串口的配置,我们可以点击右侧问好查看帮助信息
enter description here
我们可以修改默认串口为用户自定义,这是后我们可以定义串口引脚(串口引脚需要查看芯片手册,按照引脚功能选择)和波特率。这里不推荐修改默认串口配置。

同时我们这里也可以看到,串口的第二通道,输出默认是USB-JTAG,这就解释了为什么我们的USB虚拟串口会和默认串口是一样可以打印输出了。如果这里我们关闭辅助输出功能,USB虚拟串口将不会打印输出,但还会保留启动输出。

如果我们的默认串口连接了串口助手,那么USB虚拟串口的输出将会关闭。

enter description here

设置完成,ctrl+s保存配置,重新编译下载才能生效

帮助信息:

配置esp_CONSOLE_uart
控制台输出通道
选择控制台输出的发送位置(通过stdout和stderr)。
默认情况是在预定义的GPIO上使用UART0。
如果选择“自定义”,则可以选择UART0或UART1,并且可以选择任何引脚。
如果选择“None”(无),则除ROM引导加载程序的初始输出外,任何UART上都不会有控制台输出。此ROM输出可以通过GPIO捆扎或EFUSE抑制,详细信息请参阅芯片数据表。
在带有USB OTG外围设备的芯片上,“USB CDC”选项将输出重定向到CDC端口。此选项使用芯片ROM中的CDC驱动程序。此选项与TinyUSB堆栈不兼容。
在带有USB串行/JTAG调试控制器的芯片上,选择将输出重定向到该设备的CDC/ACM(串行端口仿真)组件的选项。
可用选项:

  • 默认值:UART0(ESP_CONSOLE_ART_Default)
  • USB CDC(ESP_CONSOLE_USB_CDC)
  • USB串行/JTAG控制器(ESP_CONSOLE_USB_Serial_JTAG)
  • 自定义UART(ESP_CONSOLE_ART_Custom)
  • 无(ESP_CONSOLE_None)

配置sp_CONSOLE_SECONDARY
控制台二次输出通道
位于:组件配置>ESP系统设置
当选择UART0端口作为主要端口但未连接时,此辅助选项支持通过其他特定端口(如USB_SERIAL_JTAG)输出。此辅助输出当前仅支持不使用REPL的非阻塞模式。如果您想用REPL以阻塞模式输出或通过该辅助端口输入,请在控制台输出菜单的通道中将主配置更改为该端口。
可用选项:
- 无辅助控制台(ESP_console_CONDARY_NONE)
- USB_SERIAL_JTAG端口(ESP_CONSOLE_condary_USB_SERIAL_JTAG)

当UART0端口未连接时,此选项支持通过USB_SERIAL_JTAG端口输出。 输出当前仅支持非阻塞模式,而不使用控制台。如果您想使用REPL以阻塞模式输出或通过USB_SERIAL_JTAG端口输入,请将主配置更改为上面的ESP_CONSOLE_USB_SERIAL_JTAG。

2、sleep 延时

在上一节课基础上,添加延时函数,时间间隔定时输出
需要包含头文件 #include <unistd.h>,完整程序如下

1
2
3
4
5
6
7
8
9
10
11
#include <stdio.h>
#include <unistd.h>

void app_main(void)
{
while (1) {
printf("Hello from app_man!\n");
sleep(1);
}
}

我们选中sleep单词,按F3或右键单击Open Declaration,可以其具体实现
右键单击去定义
可以看到,sleep实际调用的usleepusleep则调用的vTaskDelay
vTaskDelay 实际和FreeRTOS有关,我们的EPS32 运行时离不开FreeRTOS的,相关的教程完成也有很多,这里不再讲解,大家可以参下网络教程学习,

3、Log日志输出

参考:ESP-IDF 编程指南:API参考» 系统接口» 日志库

Log日志输出其实和printf类似,也是调用的默认串口输出信息。但它多了一个等级控制,可以方便程序员在调试时使用最高等级打印调试信息,在发布时关闭信息输出,或者打开部分输出。总之是非常方便程序员调试使用的。

Log一共5个输出等级:

  • ESP_LOGE- 错误(最低)
  • ESP_LOGW- 警告
  • ESP_LOGI- 信息
  • ESP_LOGD-调试
  • ESP_LOGV- 详细(最高)

要使用log打印,需包含库:esp_log.h

源码:

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
#include <stdio.h>
#include <stdbool.h>
#include <unistd.h>
#include <esp_log.h>
// 定义一个标签,方便批量换名字
static const char* tagMyModule = "MyModule";

void app_main(void)
{
while (true) {

printf("Hello from app_main!\n");
// 错误
ESP_LOGE(tagMyModule,"error"); // 最低级别
// 警告
ESP_LOGW(tagMyModule,"warning");
// 信息
ESP_LOGI(tagMyModule,"information");
// 调试
ESP_LOGD(tagMyModule,"debug");
// 详细
ESP_LOGV(tagMyModule,"verbose"); // 最高级别
sleep(1);
}
}

串口打印输出
enter description here

可以发现只打印三个,高级别的两个没有打印。这是因为Log打印输出是由等级控制的,在sdkconfig中设置,搜索log

  • 默认等级:info。即info级别以下的都可以打印输出。可以自己更改选项
  • 最高等级:如果这里设置的级别比上面默认等级低,那么打印等级就会被限制在最高等级。这里默认和默认等级一致,也就是我们默认等级设置多少,最高等级就是多少。

修改完成后,重新编译下载即可生效

enter description here

上面的等级修改是在编译时就固定好的,我们也可以使用函数在程序中实时修改等级。

如果在程序修改等级,我们需要将sdkconfig中的最高等级改为最大(默认和默认等级一致),否则我们的等级也会被现在在最高等级
enter description here
程序中如果修改了等级,则sdkconfig中的配置将失效,以程序中的设置为准

主要程序:

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
void app_main(void)
{

// 指定tagMyModule标签的等级
esp_log_level_set(tagMyModule, ESP_LOG_WARN);
// 设置全局的等级
// 与上面指定标签,分先后顺序
// 如果最后一步设置的是全局的,则指定等级失效,以全局为准,例如现在的程序,则tagMyModule等级就等于全局等级ESP_LOG_DEBUG
// 如果是先设置的全局,则指定标签等级=指定的等级,其它的=全局等级,可以将这两两行代码调换顺序尝试。
esp_log_level_set("*", ESP_LOG_DEBUG);


while (true) {

printf("Hello from app_main!\n");
// 错误
ESP_LOGE(tagMyModule,"error"); // 最低级别
// 警告
ESP_LOGW(tagMyModule,"warning");
// 信息
ESP_LOGI(tagMyModule,"information");
// 调试
ESP_LOGD(tagMyModule,"debug");
// 详细
ESP_LOGV(tagMyModule,"verbose"); // 最高级别
sleep(1);
}
}

打印输出内容:
enter description here

4、GPIO 输出

参考:ESP-IDF 编程指南:API参考» 外设接口» GPIO & RTC GPIO
参考IDF官方例程:C:\Espressif\frameworks\esp-idf-v5.0\examples\get-started\blink\main\blink_example_main.c
要使用log打印,需包含库:#include “driver/gpio.h”

初始化配置。有两种方式

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
/**
* @brief led初始化,设置推挽输出,设置初始电平。
*
* @note 引脚会默认上拉
*/
void gpio_init()
{
// 将 gpio 重置为默认状态(选择 gpio 功能,启用上拉并禁用输入和输出)。
gpio_reset_pin(BLINK_GPIO);
// 配置GPIO方向
gpio_set_direction(BLINK_GPIO, GPIO_MODE_OUTPUT);

}

/**
* @brief led初始化,设置推挽输出,设置初始电平。推荐使用该方式
*
* @note 这里和gpio_reset_pin底层实现很相似,但可以自定义修改配置
*/
void led_init()
{
gpio_config_t io_conf;
//bit mask of the pins that you want to set,e.g.GPIO18/19 配置GPIO_OUT寄存器
io_conf.pin_bit_mask = BIT64(BLINK_GPIO) | BIT64(BLINK_GPIO2); // 使用BIT64替代1ULL<<BLINK_GPIO2
//IO模式:输入输出
io_conf.mode = GPIO_MODE_OUTPUT;
//下拉电阻:禁止
io_conf.pull_down_en = 0;
//上拉电阻:禁止
io_conf.pull_up_en = 0;
//IO中断类型:禁止中断
io_conf.intr_type = GPIO_INTR_DISABLE;
//使用给定设置配置GPIO
gpio_config(&io_conf);
}

控制输出使用 gpio_set_level,下面是主程序

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
void app_main(void)
{
gpio_init();
//led_init();
while (true) {
/* Blink off (output low) */
ESP_LOGI(tagInfo,"Turning off the LED\n");
gpio_set_level(BLINK_GPIO, 0);
sleep(1);
/* Blink on (output high) */
ESP_LOGI(tagInfo,"Turning on the LED\n");
gpio_set_level(BLINK_GPIO, 1);
sleep(1);
}
}

测量引脚输出波形,周期2S
enter description here

3、GPIO 输入

参考:ESP-IDF 编程指南:API参考» 外设接口» GPIO & RTC GPIO

引脚内部是弱上拉,很容易受到外部干扰,最好外部硬件上拉保证稳定性。

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
void key_init()
{
gpio_config_t io_conf;
//bit mask of the pins that you want to set,e.g.GPIO18/19 配置GPIO_OUT寄存器
io_conf.pin_bit_mask = BIT64(KEY_GPIO); // 使用BIT64替代1ULL<<BLINK_GPIO2
//IO模式:输入输出 如果不定义输入输出模式,将无法获取引脚电平
io_conf.mode = GPIO_MODE_INPUT;
//下拉电阻:禁止
io_conf.pull_down_en = 0;
//上拉电阻:禁止
io_conf.pull_up_en = 1;
//IO中断类型:禁止中断
io_conf.intr_type = GPIO_INTR_DISABLE;
//使用给定设置配置GPIO
gpio_config(&io_conf);

}

//按键处理函数
//返回按键值
//mode:0,不支持连续按;1,支持连续按;
//0,没有任何按键按下
//1,WKUP按下 WK_UP
//注意此函数有响应优先级,KEY0>KEY1>KEY2>WK_UP!!
uint8_t KEY_Scan(uint8_t mode)
{
if(KEY==0)
{
usleep(10000);
if(KEY==0)
return KEY_PRES;
}

return 0; //无按键按下
}

4、外部中断

参考IDF官方例程:C:\Espressif\frameworks\esp-idf-v5.0\examples\peripherals\gpio\generic_gpio

中断里面调用ESP_LOGX或printf都会导致系统重启。所以不要再中断里调用打印函数

初始化

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
#include "exti.h"
#include "esp_attr.h" // IRAM_ATTR 库
#include "driver/gpio.h"
#include <led.h>
/**
* @brief GPIO配置为中断输入模式。下降沿触发
*
* @return
* - none
*/
void exti_init()
{
gpio_config_t io_conf;
//IO中断类型:下降沿
io_conf.intr_type = GPIO_INTR_NEGEDGE;
//IO模式:输入
io_conf.mode = GPIO_MODE_INPUT;
//bit mask of the pins that you want to set,e.g.GPIO18/19 配置GPIO_OUT寄存器
io_conf.pin_bit_mask = BIT64(GPIO_NUM_4);
//下拉电阻:禁止
io_conf.pull_down_en = 0;
//上拉电阻:禁止
io_conf.pull_up_en = 1;
//使用给定设置配置GPIO
gpio_config(&io_conf);

// 安装GPIO ISR处理程序服务。开启整个gpio的中断
// ESP_INTR_FLAG_LEVEL1 优先级最低
gpio_install_isr_service(ESP_INTR_FLAG_LEVEL1);
//添加中断回调处理函数
gpio_isr_handler_add(GPIO_NUM_4, gpio_isr_handler, NULL);//gpio_isr_handler
}

中断函数

1
2
3
4
5
6
// 定义 gpio isr 中断服务处理函数。
// IRAM_ATTR 是将函数定义在iRAM区 提高中断程序加载速度
void IRAM_ATTR gpio_isr_handler()
{
led_toggle();
}

主程序

主程序只调用一个初始化程序。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// 定义一个标签,方便批量换名字
static const char* tagInfo = "tagInfo";

void app_main(void)
{
uint8_t key;
led_init();
exti_init();
while (true) {
key=KEY_Scan(0); //得到键值
if(key)
ESP_LOGI(tagInfo,"keyValue = %d\r\n",key);
//led_toggle(LED_GPIO, 0);
usleep(10000);
}
}

现象

没触发一次中断,LED反转一次

串口

参考:ESP-IDF 编程指南:API参考» 外设 API » 通用异步接收器/发送器 (UART)
参考例程:esp-idf-v5.0\examples\peripherals\uart\uart_async_rxtxtasks

功能概述

下文介绍了如何使用 UART 驱动程序的函数和数据类型在 ESP32-S3 和其他 UART 设备之间建立通信。基本编程流程分为以下几个步骤:

  1. 设置通信参数 - 设置波特率、数据位、停止位等
  2. 设置通信管脚 - 分配连接设备的管脚
  3. 安装驱动程序 - 为 UART 驱动程序分配 ESP32-S3 资源
  4. 运行 UART 通信 - 发送/接收数据
  5. 使用中断 - 触发特定通信事件的中断
  6. 删除驱动程序 - 如无需 UART 通信,则释放已分配的资源

步骤 1 到 3 为配置阶段,步骤 4 为 UART 运行阶段,步骤 5 和 6 为可选步骤。

UART 驱动程序函数通过 uart_port_t 识别不同的 UART 控制器。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
void uart_init(void) {
const 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_DEFAULT, // 时钟源
};
// We won't use a buffer for sending data.
uart_driver_install(UART_NUM_1, RX_BUF_SIZE * 2, 0, 0, NULL, 0);
uart_param_config(UART_NUM_1, &uart_config);
uart_set_pin(UART_NUM_1, TXD_PIN, RXD_PIN, UART_PIN_NO_CHANGE, UART_PIN_NO_CHANGE);

xTaskCreate(uart_rx_task, "uart_rx_task", 1024*2, NULL, configMAX_PRIORITIES, NULL);
xTaskCreate(uart_tx_task, "uart_tx_task", 1024*4, NULL, configMAX_PRIORITIES-1, NULL);
}
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
// 接受缓存大小
static const int RX_BUF_SIZE = 1024;

#define TXD_PIN (GPIO_NUM_17)
#define RXD_PIN (GPIO_NUM_18)

int uart_sendData(const char* logName, const char* data)
{
const int len = strlen(data);
const int txBytes = uart_write_bytes(UART_NUM_1, data, len);
ESP_LOGI(logName, "Wrote %d bytes", txBytes);
return txBytes;
}

static void uart_tx_task(void *arg)
{
static const char *TX_TASK_TAG = "TX_TASK";
while (1) {
uart_sendData(TX_TASK_TAG, "Hello world");
vTaskDelay(2000 / portTICK_PERIOD_MS);
}
}

static void uart_rx_task(void *arg)
{
static const char *RX_TASK_TAG = "RX_TASK";
uint8_t* data = (uint8_t*) malloc(RX_BUF_SIZE+1);
while (1) {
const int rxBytes = uart_read_bytes(UART_NUM_1, data, RX_BUF_SIZE, 1000 / portTICK_PERIOD_MS);
if (rxBytes > 0) {
data[rxBytes] = 0;
ESP_LOGI(RX_TASK_TAG, "Read %d bytes: '%s'", rxBytes, data);
//以下语句会导致系统重启,原因未知
//ESP_LOG_BUFFER_HEXDUMP(RX_TASK_TAG, data, rxBytes, ESP_LOG_INFO);
}
}
free(data);
}

串口DMA

目前并没有查询到DMA模式

LED PWM 控制器 (LEDC)

LED PWM 控制器用于生成控制 LED 的脉冲宽度调制信号 (PWM),具有占空比自动渐变等专门功能。该外设也可成生成 PWM 信号用作其他用途。

主要特性

LED PWM 控制器具有如下特性:
• 八个独立的 PWM 生成器(即八个通道)
• 四个独立定时器,可实现小数分频
• 占空比自动渐变(即 PWM 信号占空比可逐渐增加或减小,无须处理器干预),渐变完成时产生中断
• 输出 PWM 信号相位可调
• 低功耗模式 (Light-sleep mode) 下可输出 PWM 信号
• PWM 最大精度为 14 位
四个定时器具有相同的功能和运行方式,下文将四个定时器统称为定时器x(x 的范围是 0 到 3)。八个 PWM 生成器的功能和运行方式也相同,下文将统称为 PWM n(n 的范围是 0 到 7)。
enter description here

从以上描述可以得到以下信息:

  • 只有8个PWM输出
  • 结合之前讲解的GPIO矩阵,这8个PWM可以配置到任意引脚
  • 定时器决定了PWM的频率,因此可以只使用一个定时器,生成8路频率相同的PWM(占空比可不同)。如果要频率不同,就必须再使用一个定时器。

功能概览

设置 LEDC 通道分三步完成。注意,ESP32-S3 仅支持设置通道为低速模式

  • 定时器配置 指定 PWM 信号的频率和占空比分辨率。
  • 通道配置 绑定定时器和输出 PWM 信号的 GPIO。
  • 改变 PWM 信号 输出 PWM 信号来驱动 LED。可通过软件控制或使用硬件渐变功能来改变 LED 的亮度。

另一个可选步骤是可以在渐变终端设置一个中断。

enter description here

首次 LEDC 配置时,建议先配置定时器(调用函数 ledc_timer_config()),再配置通道(调用函数 ledc_channel_config())。这样可以确保 IO 脚上的 PWM 信号自有输出开始其频率就是正确的。

定时器配置

要设置定时器,可调用函数 ledc_timer_config(),并将包括如下配置参数的数据结构ledc_timer_config_t 传递给该函数:

  • 速度模式(值必须为 LEDC_LOW_SPEED_MODE)
  • 定时器索引 ledc_timer_t
  • PWM 信号频率
  • PWM 占空比分辨率
  • 时钟源 ledc_clk_cfg_t

频率和占空比分辨率相互关联。PWM 频率越高,占空比分辨率越低,反之亦然。

时钟源同样可以限制PWM频率。选择的时钟源频率越高,可以配置的PWM频率上限就越高。
ESP32-S3 LEDC 时钟源特性

备注:

  1. 如果 ESP32-S3 的定时器选用了RC_FAST_CLK作为其时钟源,驱动会通过内部校准来得知这个时钟源的实际频率。这样确保了输出PWM信号频率的精准性。
  2. ESP32-S3 的所有定时器共用一个时钟源。因此 ESP32-S3 不支持给不同的定时器配置不同的时钟源。

通道配置

定时器设置好后,请配置所需的通道(ledc_channel_t 之一)。配置通道需调用函数 ledc_channel_config()。

通道的配置与定时器设置类似,需向通道配置函数传递包括通道配置参数的结构体 ledc_channel_config_t 。

此时,通道会按照 ledc_channel_config_t 的配置开始运作,并在选定的 GPIO 上生成由定时器设置指定的频率和占空比的 PWM 信号。在通道运作过程中,可以随时通过调用函数 ledc_stop() 将其暂停。

改变 PWM 信号

通道开始运行、生成具有恒定占空比和频率的 PWM 信号之后,有几种方式可以改变该信号。驱动 LED 时,主要通过改变占空比来变化光线亮度。

以下两节介绍了如何使用软件和硬件改变占空比。如有需要,PWM 信号的频率也可更改,详见 改变 PWM 频率 一节。

备注
在 ESP32-S3 的 LED PWM 控制器中,所有的定时器和通道都只支持低速模式。对 PWM 设置的任何改变,都需要由软件显式地触发(见下文)。

使用软件改变 PWM 占空比

调用函数 ledc_set_duty() 可以设置新的占空比。之后,调用函数 ledc_update_duty() 使新配置生效。要查看当前设置的占空比,可使用 get 函数 ledc_get_duty()。

另外一种设置占空比和其他通道参数的方式是调用 通道配置 一节提到的函数 ledc_channel_config()。

传递给函数的占空比数值范围取决于选定的 duty_resolution,应为 0 至 (2 ** duty_resolution) - 1。例如,如选定的占空比分辨率为 10,则占空比的数值范围为 0 至 1023。此时分辨率为 ~0.1%。

使用硬件改变 PWM 占空比

LED PWM 控制器硬件可逐渐改变占空比的数值。要使用此功能,需用函数 ledc_fade_func_install() 使能渐变,之后用下列可用渐变函数之一配置:

  • ledc_set_fade_with_time()
  • ledc_set_fade_with_step()
  • ledc_set_fade()

最后需要调用 ledc_fade_start() 开启渐变。渐变可以在阻塞或非阻塞模式下运行,具体区别请查看 ledc_fade_mode_t。需要特别注意的是,不管在哪种模式下,下一次渐变或是单次占空比配置的指令生效都必须等到前一次渐变完成或被中止。中止一个正在运行中的渐变需要调用函数 ledc_fade_stop()。

此外,在使能渐变后,每个通道都可以额外通过调用 ledc_cb_register() 注册一个回调函数用以获得渐变完成的事件通知。回调函数的原型被定义在 ledc_cb_t。每个回调函数都应当返回一个布尔值给驱动的中断处理函数,用以表示是否有高优先级任务被其唤醒。此外,值得注意的是,由于驱动的中断处理函数被放在了 IRAM 中, 回调函数和其调用的函数也需要被放在 IRAM 中。 ledc_cb_register() 会检查回调函数及函数上下文的指针地址是否在正确的存储区域。

如不需要渐变和渐变中断,可用函数 ledc_fade_func_uninstall() 关闭。

改变 PWM 频率

LED PWM 控制器 API 有多种方式即时改变 PWM 频率:

  • 通过调用函数 ledc_set_freq() 设置频率。可用函数 ledc_get_freq() 查看当前频率。
  • 通过调用函数 ledc_bind_channel_timer() 将其他定时器绑定到该通道来改变频率和占空比分辨率。
  • 通过调用函数 ledc_channel_config() 改变通道的定时器。

控制 PWM 的更多方式

有一些较底层的定时器特定函数可用于更改 PWM 设置:

  • ledc_timer_set()
  • ledc_timer_rst()
  • ledc_timer_pause()
  • ledc_timer_resume()

前两个功能可通过函数 ledc_channel_config() 在后台运行,在定时器配置后启动。

使用中断

配置 LED PWM 控制器通道时,可在 ledc_channel_config_t 中选取参数 ledc_intr_type_t ,在渐变完成时触发中断。

要注册处理程序来处理中断,可调用函数 ledc_isr_register()。

频率和占空比分辨率支持范围

LED PWM 控制器主要用于驱动 LED。该控制器 PWM 占空比设置的分辨率范围较广。比如,PWM 频率为 5 kHz 时,占空比分辨率最大可为 13 位。这意味着占空比可为 0 至 100% 之间的任意值,分辨率为 ~0.012%(2 ^ 13 = 8192 LED 亮度的离散电平)。然而,这些参数取决于为 LED PWM 控制器定时器计时的时钟信号,LED PWM 控制器为通道提供时钟(具体可参考 定时器配置 和 ESP32-S3 技术参考手册 > LED PWM 计时器 (LEDC) [PDF])。

LED PWM 控制器可用于生成频率较高的信号,足以为数码相机模组等其他设备提供时钟。此时,最大频率可为 40 MHz,占空比分辨率为 1 位。也就是说,占空比固定为 50%,无法调整。

LED PWM 控制器 API 会在设定的频率和占空比分辨率超过 LED PWM 控制器硬件范围时报错。例如,试图将频率设置为 20 MHz、占空比分辨率设置为 3 位时,串行端口监视器上会报告如下错误:

E (196) ledc: requested frequency and duty resolution cannot be achieved, try reducing freq_hz or duty_resolution. div_param=128
此时,占空比分辨率或频率必须降低。比如,将占空比分辨率设置为 2 会解决这一问题,让占空比设置为 25% 的倍数,即 25%、50% 或 75%。

如设置的频率和占空比分辨率低于所支持的最低值,LED PWM 驱动器也会反映并报告,如:

E (196) ledc: requested frequency and duty resolution cannot be achieved, try increasing freq_hz or duty_resolution. div_param=128000000
占空比分辨率通常用 ledc_timer_bit_t 设置,范围是 10 至 15 位。如需较低的占空比分辨率(上至 10,下至 1),可直接输入相应数值。

程序编写

实验一,软件修改占空比

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
42
43
44
45
46
47
#include <stdio.h>
#include "driver/ledc.h"
#include "esp_err.h"

#define LEDC_TIMER LEDC_TIMER_0
#define LEDC_MODE LEDC_LOW_SPEED_MODE
#define LEDC_OUTPUT_IO (5) // Define the output GPIO
#define LEDC_CHANNEL LEDC_CHANNEL_0
#define LEDC_DUTY_RES LEDC_TIMER_13_BIT // Set duty resolution to 13 bits
#define LEDC_DUTY (4095) // Set duty to 50%. ((2 ** 13) - 1) * 50% = 4095
#define LEDC_FREQUENCY (5000) // Frequency in Hertz. Set frequency at 5 kHz

static void example_ledc_init(void)
{
// Prepare and then apply the LEDC PWM timer configuration
ledc_timer_config_t ledc_timer = {
.speed_mode = LEDC_MODE,
.timer_num = LEDC_TIMER,
.duty_resolution = LEDC_DUTY_RES,
.freq_hz = LEDC_FREQUENCY, // Set output frequency at 5 kHz
.clk_cfg = LEDC_AUTO_CLK
};
ESP_ERROR_CHECK(ledc_timer_config(&ledc_timer));

// Prepare and then apply the LEDC PWM channel configuration
ledc_channel_config_t ledc_channel = {
.speed_mode = LEDC_MODE,
.channel = LEDC_CHANNEL,
.timer_sel = LEDC_TIMER,
.intr_type = LEDC_INTR_DISABLE,
.gpio_num = LEDC_OUTPUT_IO,
.duty = 0, // Set duty to 0%
.hpoint = 0
};
ESP_ERROR_CHECK(ledc_channel_config(&ledc_channel));
}

void app_main(void)
{
// Set the LEDC peripheral configuration
example_ledc_init();
// Set duty to 50%
ESP_ERROR_CHECK(ledc_set_duty(LEDC_MODE, LEDC_CHANNEL, LEDC_DUTY));
// Update duty to apply the new value
ESP_ERROR_CHECK(ledc_update_duty(LEDC_MODE, LEDC_CHANNEL));
}

输出波形
enter description here
后期可以调用

  • ledc_set_duty
  • ledc_update_duty
    改变PWM占空比

实验二,硬件修改占空比

硬件修改只能实现渐变,我们设置好参数后,硬件就会自动修改PWM,不需要软件再参与

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

void ledc_init(void)
{
// Prepare and then apply the LEDC PWM timer configuration
ledc_timer_config_t ledc_timer = {
.speed_mode = LEDC_MODE,
.timer_num = LEDC_TIMER,
.duty_resolution = LEDC_DUTY_RES,
.freq_hz = LEDC_FREQUENCY, // Set output frequency at 5 kHz
.clk_cfg = LEDC_AUTO_CLK
};
ledc_timer_config(&ledc_timer);

// Prepare and then apply the LEDC PWM channel configuration
ledc_channel_config_t ledc_channel = {
.speed_mode = LEDC_MODE,
.channel = LEDC_CHANNEL,
.timer_sel = LEDC_TIMER,
.intr_type = LEDC_INTR_DISABLE,
.gpio_num = LEDC_OUTPUT_IO,
.duty = 0, // Set duty to 0%
.hpoint = 0
};
ledc_channel_config(&ledc_channel);

// 设置占空比
ledc_set_duty(LEDC_MODE, LEDC_CHANNEL, LEDC_DUTY);
// 更新,应用生效
ledc_update_duty(LEDC_MODE, LEDC_CHANNEL);
// 初始化渐变fade服务
ledc_fade_func_install(0);

// 指定ledc通道,在设定的时间time内ms,从0渐变到 期望脉宽duty(0~定时器的满分辨率,需要手动计算)
// 渐变到的期望脉宽值(与定时器Bit有关,100%占空比对应满分辨率)
ledc_set_fade_with_time(LEDC_MODE,LEDC_CHANNEL, 8192, 10000);
ledc_fade_start(LEDC_MODE,LEDC_CHANNEL, LEDC_FADE_NO_WAIT);


}

仅仅是初始化程序修改了,增加了三个函数。

  • 占空比达到期望值后,就会一直保持期望脉宽
  • 实际测试,按13bit频率算,8192才是100%占空比,8191不行(不是从0开始的吗,不解)
  • 软件和硬件修改PWM,那个最后一次使用,那个生效,同一时间两者只能有一个可用
  • 硬件PWM还可以配合中断,实现脉宽的连续递增递减,淡入淡出

输出波形,占空比自动渐变
enter description here
enter description here

ADC

  • Wi-Fi也使用ADC2,即只要WIFI工作,ADC2就不能工作。因此建议只是用ADC1
  • ADC引脚是特定的引脚,不可使用IO MUX任意指定

每个 ADC 单元支持两种工作模式,ADC 单次采样模式和ADC连续采样(DMA)模式。

  • ADC 单次采样模式适用于低频采样操作。
  • ADC 连续采样(DMA)模式适用于高频连续采样动作。
  • 最大12位,位数减少会减小内存占用

配置顺序:

  • 设置通道引脚
  • 设置精度、衰减倍数
  • 读取转换结果(数字)
  • 可选:将数字结果转换为电压(相对于参考电压引脚),需要先执行校准
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
/*  ESP32 ADC 衰减系数 与 量程 对照表

+----------+------------+--------------------------+------------------+
| SoC | attenuation| suggested range (mV) | full range (V) |
+==========+============+==========================+==================+
| | 0 | 100 ~ 800 | 0 ~ 1.1 |
| +------------+--------------------------+------------------+
| | 2.5 | 100 ~ 1100 | 0 ~ 1.5 |
| ESP32-S2 +------------+--------------------------+------------------+
| | 6 | 150 ~ 1350 | 0 ~ 2.2 |
| +------------+--------------------------+------------------+
| | 11 | 150 ~ 2600 | 0 ~ 3.9 |
+----------+------------+--------------------------+------------------+

*/

程序编写

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
#include "adc.h"
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include "soc/soc_caps.h"
#include "esp_adc/adc_oneshot.h"
#include "esp_adc/adc_cali.h"
#include "esp_adc/adc_cali_scheme.h"
#include "esp_log.h"
//-------------ADC1 Init---------------//
adc_oneshot_unit_handle_t adc1_handle;

void adc_init()
{



adc_oneshot_unit_init_cfg_t init_config1 = {
.unit_id = ADC_UNIT_1,
.ulp_mode = ADC_ULP_MODE_DISABLE,//协处理器,与低功耗有关,暂时不用
};
ESP_ERROR_CHECK(adc_oneshot_new_unit(&init_config1, &adc1_handle));

//-------------ADC1 Config---------------//
adc_oneshot_chan_cfg_t config = {
.bitwidth = ADC_BITWIDTH_DEFAULT,
.atten = ADC_ATTEN_DB_11,
};
ESP_ERROR_CHECK(adc_oneshot_config_channel(adc1_handle, ADC_CHANNEL_7, &config));
}

static int adc_raw[2][10];
const static char *TAG = "ADC_TEST";
void adc_loop()
{
ESP_ERROR_CHECK(adc_oneshot_read(adc1_handle, ADC_CHANNEL_7, &adc_raw[0][0]));
ESP_LOGI(TAG, "ADC%d Channel[%d] Raw Data: %d", ADC_UNIT_1 + 1, ADC_CHANNEL_7, adc_raw[0][0]);
}


主程序

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 定义一个标签,方便批量换名字
static const char* tagInfo = "tagInfo";

void app_main(void)
{

//uart_init();
ledc_init();
adc_init();
while (true) {
adc_loop();
ESP_LOGI(tagInfo,"running\r\n");
sleep(1);
}
}

实验现象:

将引脚8连接到3.3V和GND,串口输出

1
2
3
4
5
6
7
8
9
10
11
12
13
14
I (8317) ADC_TEST: ADC1 Channel[7] Raw Data: 4095
I (8317) tagInfo: running

I (9317) ADC_TEST: ADC1 Channel[7] Raw Data: 4095
I (9317) tagInfo: running

I (10317) ADC_TEST: ADC1 Channel[7] Raw Data: 1097
I (10317) tagInfo: running

I (11317) ADC_TEST: ADC1 Channel[7] Raw Data: 0
I (11317) tagInfo: running

I (12317) ADC_TEST: ADC1 Channel[7] Raw Data: 0
I (12317) tagInfo: running

ADC连续转换模式

连续转换模式实际使用的是DMA,我们将需要转换的通道序列传给DMA,它就会自动按顺序转换数据,并存储到我们指定的内存数组中。

  • 没完成一组序列转换,就是一帧
  • 连续转换支持开辟内存缓存多帧数据(通常转换回避读取快)

程序编写

参考例程:C:\Espressif\frameworks\esp-idf-v5.0\examples\peripherals\adc\continuous_read

  • channel[3] 就是我们的转换序列,这里有三个
  • 例程里最多只能7个通道,有一个bug,以修改
  • 例程里使用了ADC2,但实际采集为0,已修改仅使用ADC1
  • 例程中衰减倍数改为11,可以测量3.3V
    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
    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
    #include "adc.h"
    #include <stdio.h>
    #include <stdlib.h>
    #include <string.h>
    #include "sdkconfig.h"
    #include "esp_log.h"
    #include "freertos/FreeRTOS.h"
    #include "freertos/task.h"
    #include "freertos/semphr.h"
    #include "esp_adc/adc_continuous.h"

    const static char *TAG = "ADC_TEST";
    static TaskHandle_t s_task_handle;

    #define EXAMPLE_READ_LEN 256


    static adc_channel_t channel[3] = {ADC_CHANNEL_2, ADC_CHANNEL_3, ADC_CHANNEL_8};

    //-------------ADC1 Init---------------//
    adc_continuous_handle_t handle = NULL;


    static bool IRAM_ATTR s_conv_done_cb(adc_continuous_handle_t handle, const adc_continuous_evt_data_t *edata, void *user_data)
    {
    BaseType_t mustYield = pdFALSE;
    //Notify that ADC continuous driver has done enough number of conversions
    vTaskNotifyGiveFromISR(s_task_handle, &mustYield);

    return (mustYield == pdTRUE);
    }

    void adc_init(adc_channel_t *channel, uint8_t channel_num, adc_continuous_handle_t *out_handle)
    {

    adc_continuous_handle_cfg_t adc_config = {
    .max_store_buf_size = 1024, // 转换结果缓存,超出结果将丢失,字节
    .conv_frame_size = EXAMPLE_READ_LEN, // 一帧的大小,包含多个转换结果
    };
    ESP_ERROR_CHECK(adc_continuous_new_handle(&adc_config, &handle));

    adc_continuous_config_t dig_cfg = {
    .sample_freq_hz = 20 * 1000, // 最大100kSPS
    .conv_mode = ADC_CONV_SINGLE_UNIT_1, // S3仅支持ADC1。不支持ADC2
    // 参考adc_digi_output_data_t C:\Espressif\frameworks\esp-idf-v5.0\components\hal\include\hal\adc_types.h
    // 不同芯片会有不同的输出格式 每个转换结果是一个结构体,并不单单是一个adc数据
    // s3就只有一个TYPE2
    .format = ADC_DIGI_OUTPUT_FORMAT_TYPE2,
    };

    adc_digi_pattern_config_t adc_pattern[SOC_ADC_PATT_LEN_MAX] = {0};
    dig_cfg.pattern_num = channel_num;
    for (int i = 0; i < channel_num; i++) {
    uint8_t ch = channel[i];
    adc_pattern[i].atten = ADC_ATTEN_DB_11;
    adc_pattern[i].channel = ch;
    adc_pattern[i].unit = ADC_UNIT_1;
    adc_pattern[i].bit_width = ADC_BITWIDTH_12; // SOC_ADC_DIGI_MAX_BITWIDTH

    ESP_LOGI(TAG, "adc_pattern[%d].atten is :%x", i, adc_pattern[i].atten);
    ESP_LOGI(TAG, "adc_pattern[%d].channel is :%x", i, adc_pattern[i].channel);
    ESP_LOGI(TAG, "adc_pattern[%d].unit is :%x", i, adc_pattern[i].unit);
    }
    dig_cfg.adc_pattern = adc_pattern;
    ESP_ERROR_CHECK(adc_continuous_config(handle, &dig_cfg));

    *out_handle = handle;
    }

    static bool check_valid_data(const adc_digi_output_data_t *data)
    {
    const unsigned int unit = data->type2.unit;
    if (unit > 2) return false;
    if (data->type2.channel >= SOC_ADC_CHANNEL_NUM(unit)) return false;

    return true;
    }


    void adc_loop()
    {
    esp_err_t ret;
    uint32_t ret_num = 0;
    uint8_t result[EXAMPLE_READ_LEN] = {0};
    memset(result, 0xcc, EXAMPLE_READ_LEN);

    s_task_handle = xTaskGetCurrentTaskHandle();

    adc_continuous_handle_t handle = NULL;
    adc_init(channel, sizeof(channel) / sizeof(adc_channel_t), &handle);

    // 注册转换完成回调函数
    adc_continuous_evt_cbs_t cbs = {
    .on_conv_done = s_conv_done_cb,
    };
    ESP_ERROR_CHECK(adc_continuous_register_event_callbacks(handle, &cbs, NULL));
    ESP_ERROR_CHECK(adc_continuous_start(handle));

    while(1) {

    /**
    * This is to show you the way to use the ADC continuous mode driver event callback.
    * This `ulTaskNotifyTake` will block when the data processing in the task is fast.
    * However in this example, the data processing (print) is slow, so you barely block here.
    *
    * Without using this event callback (to notify this task), you can still just call
    * `adc_continuous_read()` here in a loop, with/without a certain block timeout.
    */
    ulTaskNotifyTake(pdTRUE, portMAX_DELAY);

    while (1) {
    ret = adc_continuous_read(handle, result, EXAMPLE_READ_LEN, &ret_num, 0);
    if (ret == ESP_OK) {
    ESP_LOGI("TASK", "ret is %x, ret_num is %"PRIu32, ret, ret_num);
    for (int i = 0; i < ret_num; i += SOC_ADC_DIGI_RESULT_BYTES) {
    adc_digi_output_data_t *p = (void*)&result[i];

    if (check_valid_data(p)) {
    ESP_LOGI(TAG, "Unit: %d,_Channel: %d, Value: %x", p->type2.unit+1, p->type2.channel, p->type2.data);
    } else {
    ESP_LOGI(TAG, "Invalid data [%d_%d_%x]", p->type2.unit+1, p->type2.channel, p->type2.data);
    }
    }
    /**
    * Because printing is slow, so every time you call `ulTaskNotifyTake`, it will immediately return.
    * To avoid a task watchdog timeout, add a delay here. When you replace the way you process the data,
    * usually you don't need this delay (as this task will block for a while).
    */
    vTaskDelay(1);
    } else if (ret == ESP_ERR_TIMEOUT) {
    //We try to read `EXAMPLE_READ_LEN` until API returns timeout, which means there's no available data
    break;
    }
    }
    }
    }

LCD-SPI接口

参考资料:ESP-IDF 编程指南:API 参考 » 外设 API » LCD
参考IDF官方例程:C:\Espressif\frameworks\esp-idf-v5.0\examples\get-started\blink\main\blink_example_main.c

SPI接口与引脚

SPI接口

ESP32-S3 具有以下 SPI 接口:
• SPI0,供 ESP32-S3 和加密 DMA (EDMA) 访问封装内或封装外 flash/PSRAM
• SPI1,供 CPU 访问封装内或封装外 flash/PSRAM
• SPI2,通用 SPI 控制器,具有单独的 DMA 通道,主机80MHz,从机60MHz
• SPI3,通用 SPI 控制器,和部分外设共用一个 DMA 通道,主机80MHz,从机60MHz
表 3-2
结合 前言-GPIO交换矩阵可知

  • EPS32 S3芯片的SPI0/1用于外部flash和PSRAM.所以只有SP2/3可用
  • ESP32 S3 中SPI3和部分外设公用DMA通道,因此速度受限
  • ESP32 S3推荐使用SPI2作为LCD的接口

EPS32 只有SPI2可用。SPI0/1用于flash和PSRAM
可连接 GDMA 通道。

  • 在主机模式下,时钟频率最高为 80 MHz,支持 SPI 传输的四种时钟模式。
  • 在从机模式下,时钟频率最高为 60 MHz,也支持 SPI 传输的四种时钟模式。

另外ESP32的普通GPIO最大只能30MHz,而IOMUX默认的SPI,CLK最大可以设置到80MHz。所以为了提高刷屏速度,尽量使用硬件的IOMUX端口。

ESP32的SPI默认使用DMA,在传输长度较长的数据时可以明显提高效率

SPI引脚

SPI2的IO MUX 管脚如下。SPI3可以可以映射到任意引脚
FSPISPI3 总线信号功能描述

看门狗

定时器中断

输入捕获

电容触摸

触摸

LCD SPI

参考例程:https://github.com/espressif/esp-bsp/tree/master/components/lcd

LCD LVGL

参考程序:https://github.com/espressif/esp-bsp
其中LVGL接口程序:https://github.com/espressif/esp-bsp/tree/master/components/esp_lvgl_port

一直过程中需要修改
图像 1

sdkconfig 打开颜色交换。部分LCD适用,如果你发向颜色不对
图像 3

RTC

编程指南: API 参考 » System API » 系统时间
RTC不太实用,我们使用另一个SNTP从网络获取时间,见下一章节

概述

ESP32-S3 使用两种硬件时钟源建立和保持系统时间。根据应用目的及对系统时间的精度要求,既可以仅使用其中一种时钟源,也可以同时使用两种时钟源。这两种硬件时钟源为:

  • RTC 定时器:RTC 定时器在任何睡眠模式下及在任何复位后均可保持系统时间(上电复位除外,因为上电复位会重置 RTC 定时器)。时钟频率偏差取决于 RTC 定时器时钟源,该偏差只会在睡眠模式下影响时间精度。睡眠模式下,时间分辨率为 6.667 μs。

  • 高分辨率定时器:高分辨率定时器在睡眠模式下及在复位后不可用,但其时间精度更高。该定时器使用 APB_CLK 时钟源(通常为 80 MHz),时钟频率偏差小于 ±10 ppm,时间分辨率为 1 μs。

高分辨率定时器无法获取的年月日,重启后计数归零。因此还是用RTC,另外使用RTC还需要外部电池和低功耗模式,维持RTC的运行,否则 如果整个系统彻底断电重启,则日期回到初始状态。

RTC 定时器时钟源

RTC 定时器有以下时钟源:

  • 内置 136 kHz RC 振荡器 (默认):Deep-sleep 模式下电流消耗最低,不依赖任何外部元件。但由于温度波动会影响该时钟源的频率稳定性,在 Deep-sleep 和 Light-sleep 模式下都有可能发生时间偏移。

  • 外置 32 kHz 晶振:需要将一个 32 kHz 晶振连接到 XTAL_32K_P 和 XTAL_32K_N 管脚。频率稳定性更高,但在 Deep-sleep 模式下电流消耗略高(比默认模式高 1 μA)。

  • 管脚 XTAL_32K_P 外置 32 kHz 振荡器:允许使用由外部电路产生的 32 kHz 时钟。外部时钟信号必须连接到管脚 XTAL_32K_P。正弦波信号的振幅应小于 1.2 V,方波信号的振幅应小于 1 V。正常模式下,电压范围应为 0.1 < Vcm < 0.5 xVamp,其中 Vamp 代表信号振幅。使用此时钟源时,管脚 XTAL_32K_P 无法用作 GPIO 管脚。

  • 内置 17.5 MHz 振荡器的 256 分频时钟 (~68 kHz):频率稳定性优于 内置 136 kHz RC 振荡器,同样无需外部元件,但 Deep-sleep 模式下电流消耗更高(比默认模式高 5 μA)。

时钟源的选择取决于系统时间精度要求和睡眠模式下的功耗要求。要修改 RTC 时钟源,请在项目配置中设置 CONFIG_RTC_CLK_SRC。

模组中不自带外置晶振的,所以还需要自己搭建电路,这里测试使用内部振荡器。

SNTP

参考资料:SNTP 时间同步

介绍

  • SNTP 简单网络时间协议(Simple Network Time Protocol),由 NTP 改编而来。
  • SNTP 主要用来同步因特网中的计算机时钟。在 RFC2030 中定义。
  • SNTP 基于UDP协议,存在SNTP服务器和SNTP客户端
  • SNTP 有单播和广播两种模式。
  • 单播模式下,SNTP客户端定时访问SNTP服务器获得准确的时间信息,用于同步时间。
  • 广播模式下,SNTP服务器周期性地发送消息给指定的IP广播地址或者IP多播地址,用户SNTP客户端同步时间。
  • 网络中存在多个SNTP服务器,SNTP客户端可以选择多个SNTP服务器作为外部时间源,当某个SNTP服务器故障断开连接时,可以及时切换到其他SNTP服务器。
  • ESP32的SNTP同步功能是基于lwIP SNTP库,功能实现很简单,请见下文。

知识点

3.1 时间同步状态

1
2
3
4
5
6
/// SNTP sync status
typedef enum {
SNTP_SYNC_STATUS_RESET, // Reset status.
SNTP_SYNC_STATUS_COMPLETED, // Time is synchronized.
SNTP_SYNC_STATUS_IN_PROGRESS, // Smooth time sync in progress.
} sntp_sync_status_t;

3.2 时间同步模式

1
2
3
4
5
/// SNTP time update mode
typedef enum {
SNTP_SYNC_MODE_IMMED, /* 立即更新时间 */
SNTP_SYNC_MODE_SMOOTH, /* 平滑更新,如果时间差别不大,软件会慢慢将时间调整到网络时间,而不是立即改变,这样用户就看不出来时间更改了,但如果时间差别太大(>35min),也会立即更新时间 */
} sntp_sync_mode_t;

3.3 SNTP工作模式
默认单播

1
2
3
4
/* SNTP operating modes: default is to poll using unicast.
The mode has to be set before calling sntp_init(). */
#define SNTP_OPMODE_POLL 0
#define SNTP_OPMODE_LISTENONLY 1

3.3 关键函数
设置同步模式。
void sntp_set_sync_mode(sntp_sync_mode_t sync_mode)

获取同步模式。
sntp_sync_mode_t sntp_get_sync_mode(void)

设置时间同步状态。
void sntp_set_sync_status(sntp_sync_status_t sync_status)

获取时间同步状态。
sntp_sync_status_t sntp_get_sync_status(void)

设置时间同步通知回调函数
void sntp_set_time_sync_notification_cb(sntp_sync_time_cb_t callback)

设置 SNTP 操作的同步间隔。
注意:SNTPv4 RFC 4330 强制最小同步间隔为 15 秒。此同步间隔将用于通过 SNT​​P 的下一次尝试更新时间。要应用新的同步间隔,请调用 sntp_restart() 函数,否则,它将在最后一个间隔到期后应用。
void sntp_set_sync_interval(uint32_t interval_ms)

获取 SNTP 操作的同步间隔。
uint32_t sntp_get_sync_interval(void)

设置SNTP工作模式,单播或者广播
void sntp_setoperatingmode(u8_t operating_mode)

设置SNTP服务器
void sntp_setservername(u8_t idx, const char *server)

SNTP初始化。
void sntp_init(void)

SNTP停止。
void sntp_stop(void)

重新启动 SNTP(先停止,再初始化)。
bool sntp_restart(void)

wifi配置

connect config

示例程序

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
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
#include <stdbool.h>
#include <unistd.h>
#include <esp_log.h>
#include "global.h"
#include "lwip/err.h"
#include "lwip/apps/sntp.h"
#include "protocol_examples_common.h"
#include "esp_sntp.h"
#include "freertos/event_groups.h"
#include <time.h>
#include <sys/time.h>
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include <string.h>
#include "esp_system.h"
#include "esp_event.h"
#include "esp_log.h"
#include "esp_attr.h"
#include "esp_sleep.h"
#include "nvs_flash.h"

// 定义一个标签,方便批量换名字
static const char* tagInfo = "tagInfo";
static const char* TAG = "SNTP";

void get_time(void)
{
time_t second;
struct tm *datetime;

time(&second);
datetime = localtime(&second);
ESP_LOGI(TAG, "The current time is: %04d/%02d/%02d %02d:%02d:%02d. by Lonly", datetime->tm_year+1900, datetime->tm_mon+1, datetime->tm_mday, datetime->tm_hour, datetime->tm_min, datetime->tm_sec);
}
char* get_sntp_status(void)
{
int status = sntp_get_sync_status();
switch(status){
case SNTP_SYNC_STATUS_RESET:
return "RESET";
break;
case SNTP_SYNC_STATUS_COMPLETED:
sntp_stop();
return "COMPLETED";
break;
case SNTP_SYNC_STATUS_IN_PROGRESS:
return "PROGRESS";
break;
}
return "";
}

static void initialize_sntp(void)
{
ESP_LOGI(TAG, "Initializing SNTP");
#ifdef LWIP_DHCP_GET_NTP_SRV
sntp_servermode_dhcp(1);
#endif

sntp_setoperatingmode(SNTP_OPMODE_POLL);
// sntp_setservername(0, "pool.ntp.org");
// sntp_setservername(0, "210.72.145.44"); // 国家授时中心服务器 IP 地址
sntp_setservername(0, "ntp1.aliyun.com");

// sntp_set_time_sync_notification_cb(time_sync_notification_cb);
sntp_set_sync_mode(SNTP_SYNC_MODE_IMMED);
// sntp_set_sync_interval(15*1000);
sntp_init();
// sntp_restart();
// ESP_LOGI(TAG, "SNTP interval: %d", sntp_get_sync_interval());
}

void app_main(void)
{
nvs_flash_init();
esp_netif_init();
esp_event_loop_create_default();
example_connect();

initialize_sntp();
//uart_init();
//ledc_init();
int i=0;
while (true) {
ESP_LOGI(tagInfo,"running\r\n");
sleep(1);
ESP_LOGI(TAG, "sntp sync status: %s |%d", get_sntp_status(), i++);
vTaskDelay(1000 / portTICK_PERIOD_MS);
get_time();
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
I (16603) example_connect: Wi-Fi disconnected, trying to reconnect...
I (19013) example_connect: Wi-Fi disconnected, trying to reconnect...
I (21413) wifi:new:<6,0>, old:<6,0>, ap:<255,255>, sta:<6,0>, prof:1
I (21413) wifi:state: init -> auth (b0)
I (22413) wifi:state: auth -> init (200)
I (22423) wifi:new:<6,0>, old:<6,0>, ap:<255,255>, sta:<6,0>, prof:1
I (22423) example_connect: WiFi Connect failed 7 times, stop reconnect.
I (22423) SNTP: Initializing SNTP
I (22433) tagInfo: running

I (23433) SNTP: sntp sync status: RESET |0
I (24433) SNTP: The current time is: 2023/07/16 11:06:42. by Lonly
I (24433) tagInfo: running

非易失性存储 (NVS)

参考资料:非易失性存储库
ESP-IDF storage 目录下提供了数个代码示例:

再说NVS前,请先阅读前面章节:Partition Tables分区表

1、NVS介绍

非易失性存储 (NVS) 库主要用于在 flash 中存储键值格式的数据。

NVS 库通过调用 esp_partitionAPI 使用主 flash 的部分空间,即类型为 data 且子类型为 nvs 的所有分区。

2、操作流程

读操作流程:
enter description here
写操作流程
enter description here

3、关键函数

  • 初始化默认的 NVS 分区。

  • 此 API 初始化默认 NVS 分区。默认 NVS 分区是分区表中标记为“nvs”的分区。
    esp_err_t nvs_flash_init(void)

  • 擦除默认的 NVS 分区。

  • 擦除默认 NVS 分区的所有内容(带有标签“nvs”的分区)。
    esp_err_t nvs_flash_erase(void)

  • 从默认 NVS 分区打开具有给定命名空间的非易失性存储。
    -多个内部 ESP-IDF 和第三方应用程序模块可以将它们的键值对存储在 NVS 模块中。为了减少可能的键名冲突,每个模块都可以使用自己的命名空间。默认 NVS 分区是分区表中标记为“nvs”的分区。
    esp_err_t nvs_open(const char* name, nvs_open_mode_t open_mode, nvs_handle_t *out_handle)

  • 获取给定键的 int8_t 值

  • 这些函数根据给定的名称检索键的值。如果key不存在,或者请求的变量类型与设置值时使用的类型不匹配,则返回错误。
    esp_err_t nvs_get_i8 (nvs_handle_t c_handle, const char* key, int32_t* out_value)

  • 为给定键设置 int8_t 值

  • 设置键的值,给定它的名称。nvs_commit请注意,实际存储在调用之前不会更新。
    esp_err_t nvs_get_i8 (nvs_handle_t handle, const char* key, int32_t value)

  • 将任何挂起的更改写入非易失性存储。

  • 设置任何值后,必须调用 nvs_commit() 以确保将更改写入非易失性存储。个别实现可能会在其他时间写入存储,但这不能保证。
    esp_err_t nvs_commit(nvs_handle_t c_handle)

  • 关闭handle并释放所有分配的资源。

  • handle不再使用,则应为使用 nvs_open 打开的每个handle调用此函数。

  • 关闭句柄可能不会自动将更改写入非易失性存储。这必须使用 nvs_commit 函数显式完成。
    void nvs_close(nvs_handle_t handle)

  • 擦除具有给定键名的键值对。
    esp_err_t nvs_erase_key(nvs_handle_t handle, const char *key)

  • 擦除命名空间中的所有键值对。
    esp_err_t nvs_erase_all(nvs_handle_t handle);

示例1:随机整数读取

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
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
#include <stdio.h>
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "esp_system.h"
#include "esp_log.h"

#include "nvs_flash.h"
#include "nvs.h"

static const char *TAG = "nvs_kv";
static const char *KEY = "nvs_demo_key";

void delay_ms(uint32_t millisecond)
{
vTaskDelay(millisecond / portTICK_RATE_MS);
}

int get_value()
{
esp_err_t err;
nvs_handle_t handle;
int value=0;

err = nvs_open("nvs", NVS_READWRITE, &handle);
if (err != ESP_OK) {
ESP_LOGE(TAG, "Error (%s) opening NVS handle!", esp_err_to_name(err));
}
else
{
err = nvs_get_i32(handle, KEY, &value);
if(err!=ESP_OK)
ESP_LOGE(TAG, "get_value (%s)", esp_err_to_name(err));
}
nvs_close(handle);
return value;
}

void set_value(int value)
{
esp_err_t err;
nvs_handle_t handle;

err = nvs_open("nvs", NVS_READWRITE, &handle);
if (err != ESP_OK) {
ESP_LOGE(TAG, "Error (%s) opening NVS handle!", esp_err_to_name(err));
}
else
{
ESP_LOGI(TAG, "set_value %d", value);

err = nvs_set_i32(handle, KEY, value);
if(err!=ESP_OK)
ESP_LOGE(TAG, "set_value (%s)", esp_err_to_name(err));

err = nvs_commit(handle);
if(err!=ESP_OK)
ESP_LOGE(TAG, "nvs_commit (%s)", esp_err_to_name(err));
}
nvs_close(handle);
}

void app_main(void)
{
// Initialize NVS
esp_err_t err = nvs_flash_init();
if (err == ESP_ERR_NVS_NO_FREE_PAGES || err == ESP_ERR_NVS_NEW_VERSION_FOUND) {
// NVS partition was truncated and needs to be erased
// Retry nvs_flash_init
ESP_ERROR_CHECK(nvs_flash_erase());
err = nvs_flash_init();
}
ESP_ERROR_CHECK( err );

while(1)
{
delay_ms(3000);
ESP_LOGE(TAG, "get_value = %d", get_value());
set_value(rand()%1000);
}
}

enter description here

示例2:数组读取

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
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
static const char *NVS_KEY = "nvs_demo_key3";

esp_err_t getValue()
{
esp_err_t err;
nvs_handle_t my_handle;
// Open
// Open
err = nvs_open(NVS_KEY, NVS_READWRITE, &my_handle);
if (err != ESP_OK) return err;

// Read the size of memory space required for blob
size_t required_size = 0; // value will default to 0, if not set yet in NVS
err = nvs_get_blob(my_handle, "run_time", NULL, &required_size);
if (err != ESP_OK && err != ESP_ERR_NVS_NOT_FOUND) return err;
printf("Run time:\n");
// Read previously saved blob if available
if (required_size == 0) {
printf("Nothing saved yet!\n");
} else {
uint32_t* run_time = malloc(required_size);
err = nvs_get_blob(my_handle, "run_time", run_time, &required_size);
if (err != ESP_OK) {
free(run_time);
return err;
}
for (int i = 0; i < required_size / sizeof(uint32_t); i++) {
printf("%d: %ld\n", i + 1, run_time[i]);
}
free(run_time);
}

// Close
nvs_close(my_handle);
return ESP_OK;
}

esp_err_t setValue(int value)
{
esp_err_t err;
nvs_handle_t my_handle;
static uint32_t s_countTest =0 ;
err = nvs_open(NVS_KEY, NVS_READWRITE, &my_handle);
if (err != ESP_OK) return err;

// Write
//ESP_LOGI(tagInfo, "set_value %d", value);
// Read the size of memory space required for blob
/*先使用一次nvs_get_blob函数,但是第三个参数输出地址使用的是NULL,
表示读出的数据不保存,因为这里使用只是为了看一下 "run_time" 处是否
有数据,只是先读一下数据,看一下读完以后 required_size 还是不是0
*/
size_t required_size = 0; // value will default to 0, if not set yet in NVS
err = nvs_get_blob(my_handle, "run_time", NULL, &required_size);
if (err != ESP_OK && err != ESP_ERR_NVS_NOT_FOUND) return err;

// Write value including previously saved blob if available
//

uint32_t* run_time = malloc(required_size + sizeof(uint32_t));
if (required_size > 0) {
err = nvs_get_blob(my_handle, "run_time", run_time, &required_size);
if (err != ESP_OK) {
free(run_time);
return err;
}
}

required_size += sizeof(uint32_t);
run_time[required_size / sizeof(uint32_t) - 1] = s_countTest++;
err = nvs_set_blob(my_handle, "run_time", run_time, required_size);
free(run_time);

if (err != ESP_OK) return err;

// Commit
err = nvs_commit(my_handle);
if (err != ESP_OK) return err;

// Close
nvs_close(my_handle);
return ESP_OK;
}
void app_main(void)
{
nvs_handle_t my_handle;
// Initialize NVS
esp_err_t err = nvs_flash_init();
if (err == ESP_ERR_NVS_NO_FREE_PAGES || err == ESP_ERR_NVS_NEW_VERSION_FOUND) {
// NVS partition was truncated and needs to be erased
// Retry nvs_flash_init
ESP_ERROR_CHECK(nvs_flash_erase());
err = nvs_flash_init();
}
ESP_ERROR_CHECK( err );
// Open
nvs_open(NVS_KEY, NVS_READWRITE, &my_handle);

nvs_erase_key(my_handle,"run_time");


while (true) {
vTaskDelay(2000 / portTICK_PERIOD_MS);
getValue();
setValue(rand()%1000);
}
}

运行结果
每2s追加一个数据,并读取出来
这里用到了擦除函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
 (0) cpu_start: Starting scheduler on APP CPU.
Run time:
Nothing saved yet!
Run time:
1: 0
Run time:
1: 0
2: 1
Run time:
1: 0
2: 1
3: 2
Run time:
1: 0
2: 1
3: 2
4: 3

示例3:字符串数组读取

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
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
static const char *NVS_HANDLE = "nvs_demo_handle";
static const char *NVS_KEY = "nvs_demo_key";

esp_err_t getValue()
{
esp_err_t err;
nvs_handle_t my_handle;
// Open
// Open
err = nvs_open(NVS_HANDLE, NVS_READWRITE, &my_handle);
if (err != ESP_OK) return err;

// Read the size of memory space required for blob
size_t required_size = 0; // value will default to 0, if not set yet in NVS
err = nvs_get_str(my_handle, NVS_KEY, NULL, &required_size);
if (err != ESP_OK && err != ESP_ERR_NVS_NOT_FOUND) return err;
printf("Run time:\n");
// Read previously saved blob if available
if (required_size == 0) {
printf("Nothing saved yet!\n");
} else {
char* get_char = malloc(required_size);
err = nvs_get_str(my_handle, NVS_KEY, get_char, &required_size);
if (err != ESP_OK) {
free(get_char);
return err;
}
printf("test str is: %s \nsize is %d \n",get_char,required_size);

free(get_char);
}

// Close
nvs_close(my_handle);
return ESP_OK;
}

esp_err_t setValue(int value)
{
esp_err_t err;
nvs_handle_t my_handle;
char test_str[]="this is my test str,boom!";

err = nvs_open(NVS_HANDLE, NVS_READWRITE, &my_handle);
if (err != ESP_OK) return err;

err = nvs_set_str(my_handle, NVS_KEY, test_str);
if (err != ESP_OK) return err;
// Commit
err = nvs_commit(my_handle);
if (err != ESP_OK) return err;

// Close
nvs_close(my_handle);
return ESP_OK;
}
void app_main(void)
{
nvs_handle_t my_handle;
// Initialize NVS
esp_err_t err = nvs_flash_init();
if (err == ESP_ERR_NVS_NO_FREE_PAGES || err == ESP_ERR_NVS_NEW_VERSION_FOUND) {
// NVS partition was truncated and needs to be erased
// Retry nvs_flash_init
ESP_ERROR_CHECK(nvs_flash_erase());
err = nvs_flash_init();
}
ESP_ERROR_CHECK( err );
// Open
nvs_open(NVS_KEY, NVS_READWRITE, &my_handle);

nvs_erase_key(my_handle,"run_time");


while (true) {
vTaskDelay(2000 / portTICK_PERIOD_MS);
getValue();
setValue(rand()%1000);
}
}
1
2
3
4
5
6
Run time:
test str is: this is my test str,boom!
size is 26
Run time:
test str is: this is my test str,boom!
size is 26

示例4:结构体

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
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
// 定义一个标签,方便批量换名字
static const char* tagInfo = "tagInfo";

static const char *NVS_HANDLE = "nvs_demo_handle";
static const char *NVS_KEY = "nvs_demo_key";
typedef struct {
uint8_t a, b, c;
uint32_t x, y;
} test_struct;

esp_err_t getValue()
{
esp_err_t err;
nvs_handle_t my_handle;
test_struct value;
//test_struct value;
size_t length = sizeof(value);
// Open
// Open
err = nvs_open(NVS_HANDLE, NVS_READWRITE, &my_handle);
if (err != ESP_OK) return err;

// Read the size of memory space required for blob
size_t required_size = 0; // value will default to 0, if not set yet in NVS
err = nvs_get_blob(my_handle, NVS_KEY, NULL, &required_size);
if (err != ESP_OK && err != ESP_ERR_NVS_NOT_FOUND) return err;
// Read previously saved blob if available
if (required_size == 0) {
printf("Nothing saved yet!\n");
} else {
err = nvs_get_blob(my_handle, NVS_KEY, &value, &length);
if (err != ESP_OK) {
return err;
}
ESP_LOGE(tagInfo, "get_value {a=%d, b=%d, c=%d, x=%ld, y=%ld}", value.a, value.b, value.c, value.x, value.y);
}


// Close
nvs_close(my_handle);
return ESP_OK;
}

esp_err_t setValue(test_struct value)
{
esp_err_t err;
nvs_handle_t my_handle;
size_t length = sizeof(value);


err = nvs_open(NVS_HANDLE, NVS_READWRITE, &my_handle);
if (err != ESP_OK) return err;

ESP_LOGI(tagInfo, "set_value {a=%d, b=%d, c=%d, x=%ld, y=%ld}, length=%d", value.a, value.b, value.c, value.x, value.y, length);

err = nvs_set_blob(my_handle, NVS_KEY, &value,length);

if (err != ESP_OK) return err;
// Commit
err = nvs_commit(my_handle);
if (err != ESP_OK) return err;

// Close
nvs_close(my_handle);
return ESP_OK;
}
void app_main(void)
{
nvs_handle_t my_handle;
// Initialize NVS
esp_err_t err = nvs_flash_init();
if (err == ESP_ERR_NVS_NO_FREE_PAGES || err == ESP_ERR_NVS_NEW_VERSION_FOUND) {
// NVS partition was truncated and needs to be erased
// Retry nvs_flash_init
ESP_ERROR_CHECK(nvs_flash_erase());
err = nvs_flash_init();
}
ESP_ERROR_CHECK( err );
// Open
nvs_open(NVS_KEY, NVS_READWRITE, &my_handle);

nvs_erase_key(my_handle,"run_time");


test_struct value;
while (true) {
getValue();


vTaskDelay(2000 / portTICK_PERIOD_MS);
value.a = rand()%10;
value.b = rand()%10;
value.c = rand()%10;
value.x = rand()%1000;
value.y = rand()%1000;
setValue(value);
}
}
1
2
3
4
5
6
7
E (344) tagInfo: get_value {a=8, b=2, c=6, x=256, y=119}
I (2344) tagInfo: set_value {a=3, b=3, c=2, x=529, y=700}, length=12
E (2354) tagInfo: get_value {a=3, b=3, c=2, x=529, y=700}
I (4354) tagInfo: set_value {a=8, b=2, c=6, x=256, y=119}, length=12
E (4364) tagInfo: get_value {a=8, b=2, c=6, x=256, y=119}
I (6364) tagInfo: set_value {a=1, b=1, c=3, x=705, y=108}, length=12
E (6374) tagInfo: get_value {a=1, b=1, c=3, x=705, y=108}

命名空间和键值对

  • 类比于文件夹,我们如果想要存储一个文件,esp是强制要有一个文件夹的,然后你可以在这个文件夹中存放自己的数据,这就是命名空间
  • 命名空间(handle, )类似于文件夹名字,我们只有先找到文件夹,打开文件夹,才能读取里面的各个文件数据
  • 键值对(key)类似于我们的文件名,也就是我们的变量名,可以通过键值对找到我们存储的变量,所以建议键值对名称和变量名保持一致,便于理解和阅读
  • 目前不支持浮点数存储,可以考虑将浮点数扩到整数倍(1000倍)转换成整数存储
    enter description here

参考资料:

量产烧写设备配置和序列号, NVS partition分区确认, NVS 分区生成程序, csv转bin

参考:NVS 分区生成程序

1、介绍

设备量产的时候,每个设备都有不同的序列号、配置参数等。
这就需要提供不同的固件给不同的设备,出厂前直接烧写到设备Flash中。

这一操作需要用到NVS(非易失性存储)和NVS 分区生成程序。

NVS 分区生成程序 (nvs_flash/nvs_partition_generator/nvs_partition_gen.py) 根据 CSV 文件中的键值对生成二进制文件。该二进制文件与 非易失性存储器 (NVS) 中定义的 NVS 结构兼容。NVS 分区生成程序适合用于生成二进制数据(Blob),其中包括设备生产时可从外部烧录的 ODM/OEM 数据。这也使得生产制造商在使用同一个应用固件的基础上,通过自定义参数,如序列号,为每个设备生成不同配置的二进制 NVS 分区。

2、操作流程

  • IDE打开分区表
  • 编辑分区表
  • 下载分区表
  • 编写nvs程序读取分区表数据
  • 下载主程序

3、编辑分区表

打开分区表编辑器并编辑

  • 文件大小必须大于0x3000,否则无法将csv文件转换成bin文件
  • add row添加行
  • save 保存 csv文件
  • geberate 生成bin文件
    nvs table

随后我们可以在项目文件夹下看到生成的 .csv和.bin 文件。也可以使用excel编辑csv文件(不推荐)
nvs csv

然后需要使用 flash download 软件下载 .bin 分区文件

  • 地址0x9000
    如果点击 START 没有反应,没有下载,那说明 .bin 文件有问题。一般是 .csv 中 value一栏数据错误,上图第四行的 root.pem.key 文件并不存在,所以下载就会出现错误,删除重新生成 .bin 文件就能够下载了
    download nvs

同样的你可以编辑多个命名空间
enter description here

4、示例程序

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
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
// 定义一个标签,方便批量换名字
static const char* TAG = "tagInfo";

static const char *NVS_HANDLE = "myHandle";

esp_err_t getValue()
{
esp_err_t err;
nvs_handle_t my_handle;

// Open
err = nvs_open(NVS_HANDLE, NVS_READWRITE, &my_handle);
if (err != ESP_OK) return err;

char *name, *rootCa;
uint8_t age;
uint32_t baudrate;
unsigned int nameLen, rootCaLen;

err = nvs_get_u8(my_handle, "age", &age);
if(err!=ESP_OK)
ESP_LOGE(TAG, "get_value age (%s)", esp_err_to_name(err));
else
ESP_LOGI(TAG, "get_value age = %d", age);

err = nvs_get_u32(my_handle, "baudrate", &baudrate);
if(err!=ESP_OK)
ESP_LOGE(TAG, "get_value uartBaudrate (%s)", esp_err_to_name(err));
else
ESP_LOGI(TAG, "get_value uartBaudrate = %ld", baudrate);

err = nvs_get_str(my_handle,"name", NULL, &nameLen);
if (err != ESP_OK && err != ESP_ERR_NVS_NOT_FOUND) return err;
// Read previously saved blob if available
if (nameLen == 0) {
printf("Nothing saved yet!\n");
} else {
name = malloc(nameLen);
ESP_LOGE(TAG, "nameLen = %d", nameLen);

err = nvs_get_str(my_handle, "name", name, &nameLen);
if(err!=ESP_OK)
ESP_LOGE(TAG, "get_value name (%s)", esp_err_to_name(err));
else
ESP_LOGI(TAG, "get_value name = %s", name);
free(name);
}


err = nvs_get_str(my_handle,"root", NULL, &rootCaLen);
if (err != ESP_OK && err != ESP_ERR_NVS_NOT_FOUND) return err;
// Read previously saved blob if available
if (rootCaLen == 0) {
printf("Nothing saved yet!\n");
} else {
rootCa = malloc(rootCaLen);
ESP_LOGE(TAG, "rootCaLen = %d", nameLen);

err = nvs_get_str(my_handle, "root", rootCa, &rootCaLen);
if(err!=ESP_OK)
ESP_LOGE(TAG, "get_value name (%s)", esp_err_to_name(err));
else
ESP_LOGI(TAG, "get_value rootCa = %s", rootCa);
free(rootCa);
}



nvs_close(my_handle);
return ESP_OK;
}

void app_main(void)
{
// Initialize NVS
esp_err_t err = nvs_flash_init();
if (err == ESP_ERR_NVS_NO_FREE_PAGES || err == ESP_ERR_NVS_NEW_VERSION_FOUND) {
// NVS partition was truncated and needs to be erased
// Retry nvs_flash_init
ESP_ERROR_CHECK(nvs_flash_erase());
err = nvs_flash_init();
}
ESP_ERROR_CHECK( err );
// Open

while (true) {
getValue();

vTaskDelay(2000 / portTICK_PERIOD_MS);

}
}

输出:

1
2
3
4
5
6
I (346054) tagInfo: get_value age = 28
I (346054) tagInfo: get_value uartBaudrate = 9600
E (346054) tagInfo: nameLen = 9
I (346054) tagInfo: get_value name = LonlyPan
E (346054) tagInfo: rootCaLen = 9
E (346064) tagInfo: get_value name (ESP_ERR_NVS_NOT_FOUND)

WIFI Scan

参考资料:Wi-Fi 库
应用示例:ESP-IDF 仓库的 wifi

概述

Wi-Fi 库支持配置及监控 ESP32-C3 Wi-Fi 连网功能。支持配置:

  • station 模式(即 STA 模式或 Wi-Fi 客户端模式),此时 ESP32-C3 连接到接入点 (AP)。
  • AP 模式(即 Soft-AP 模式或接入点模式),此时基站连接到 ESP32-C3。
  • station/AP 共存模式(ESP32-C3 既是接入点,同时又作为基站连接到另外一个接入点)。
  • 上述模式的各种安全模式(WPA、WPA2、WPA3 等)。
  • 扫描接入点(包括主动扫描及被动扫描)。
  • 使用混杂模式监控 IEEE802.11 Wi-Fi 数据包。

操作流程

程序流程

  1. nvs_flash_init,初始化默认 NVS 分区。
  2. esp_netif_init,初始化底层TCP/IP堆栈
  3. esp_event_loop_create_default,创建默认事件循环。
  4. esp_netif_create_default_wifi_sta,使用默认WiFi Station配置创建esp_netif 对象,将netif连接到WiFi并注册默认WiFi处理程序。
  5. esp_wifi_init,为 WiFi 驱动初始化 WiFi 分配资源,如 WiFi 控制结构、RX/TX 缓冲区、WiFi NVS 结构等,这个 WiFi 也启动 WiFi 任务。必须先调用此API,然后才能调用所有其他WiFi API
  6. esp_wifi_set_mode,设置WiFi工作模式为station、soft-AP或station+soft-AP,默认模式为soft-AP模式。本程序设置为station
  7. esp_wifi_start,根据配置,启动WiFi
  8. esp_wifi_scan_start,扫描所有有效的AP
  9. esp_wifi_scan_get_ap_records,获取上次扫描中找到的AP列表
  10. esp_wifi_scan_get_ap_num,获取上次扫描中找到的AP数

示例程序

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
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
// 扫面列表大小
#define DEFAULT_SCAN_LIST_SIZE 10

static const char *TAG = "scan";

// 打印加密模式
static void print_auth_mode(int authmode)
{
switch (authmode) {
case WIFI_AUTH_OPEN:
ESP_LOGI(TAG, "Authmode \tWIFI_AUTH_OPEN");
break;
case WIFI_AUTH_OWE:
ESP_LOGI(TAG, "Authmode \tWIFI_AUTH_OWE");
break;
case WIFI_AUTH_WEP:
ESP_LOGI(TAG, "Authmode \tWIFI_AUTH_WEP");
break;
case WIFI_AUTH_WPA_PSK:
ESP_LOGI(TAG, "Authmode \tWIFI_AUTH_WPA_PSK");
break;
case WIFI_AUTH_WPA2_PSK:
ESP_LOGI(TAG, "Authmode \tWIFI_AUTH_WPA2_PSK");
break;
case WIFI_AUTH_WPA_WPA2_PSK:
ESP_LOGI(TAG, "Authmode \tWIFI_AUTH_WPA_WPA2_PSK");
break;
case WIFI_AUTH_WPA2_ENTERPRISE:
ESP_LOGI(TAG, "Authmode \tWIFI_AUTH_WPA2_ENTERPRISE");
break;
case WIFI_AUTH_WPA3_PSK:
ESP_LOGI(TAG, "Authmode \tWIFI_AUTH_WPA3_PSK");
break;
case WIFI_AUTH_WPA2_WPA3_PSK:
ESP_LOGI(TAG, "Authmode \tWIFI_AUTH_WPA2_WPA3_PSK");
break;
default:
ESP_LOGI(TAG, "Authmode \tWIFI_AUTH_UNKNOWN");
break;
}
}

// 打印密码类型
static void print_cipher_type(int pairwise_cipher, int group_cipher)
{
switch (pairwise_cipher) {
case WIFI_CIPHER_TYPE_NONE:
ESP_LOGI(TAG, "Pairwise Cipher \tWIFI_CIPHER_TYPE_NONE");
break;
case WIFI_CIPHER_TYPE_WEP40:
ESP_LOGI(TAG, "Pairwise Cipher \tWIFI_CIPHER_TYPE_WEP40");
break;
case WIFI_CIPHER_TYPE_WEP104:
ESP_LOGI(TAG, "Pairwise Cipher \tWIFI_CIPHER_TYPE_WEP104");
break;
case WIFI_CIPHER_TYPE_TKIP:
ESP_LOGI(TAG, "Pairwise Cipher \tWIFI_CIPHER_TYPE_TKIP");
break;
case WIFI_CIPHER_TYPE_CCMP:
ESP_LOGI(TAG, "Pairwise Cipher \tWIFI_CIPHER_TYPE_CCMP");
break;
case WIFI_CIPHER_TYPE_TKIP_CCMP:
ESP_LOGI(TAG, "Pairwise Cipher \tWIFI_CIPHER_TYPE_TKIP_CCMP");
break;
default:
ESP_LOGI(TAG, "Pairwise Cipher \tWIFI_CIPHER_TYPE_UNKNOWN");
break;
}

switch (group_cipher) {
case WIFI_CIPHER_TYPE_NONE:
ESP_LOGI(TAG, "Group Cipher \tWIFI_CIPHER_TYPE_NONE");
break;
case WIFI_CIPHER_TYPE_WEP40:
ESP_LOGI(TAG, "Group Cipher \tWIFI_CIPHER_TYPE_WEP40");
break;
case WIFI_CIPHER_TYPE_WEP104:
ESP_LOGI(TAG, "Group Cipher \tWIFI_CIPHER_TYPE_WEP104");
break;
case WIFI_CIPHER_TYPE_TKIP:
ESP_LOGI(TAG, "Group Cipher \tWIFI_CIPHER_TYPE_TKIP");
break;
case WIFI_CIPHER_TYPE_CCMP:
ESP_LOGI(TAG, "Group Cipher \tWIFI_CIPHER_TYPE_CCMP");
break;
case WIFI_CIPHER_TYPE_TKIP_CCMP:
ESP_LOGI(TAG, "Group Cipher \tWIFI_CIPHER_TYPE_TKIP_CCMP");
break;
default:
ESP_LOGI(TAG, "Group Cipher \tWIFI_CIPHER_TYPE_UNKNOWN");
break;
}
}

/* Initialize Wi-Fi as sta and set scan method */
static void wifi_scan(void)
{
// 初始化底层TCP/IP堆栈
ESP_ERROR_CHECK(esp_netif_init());
// 创建默认事件循环
ESP_ERROR_CHECK(esp_event_loop_create_default());
// 使用默认WiFi Station配置创建esp_netif 对象,将netif连接到WiFi并注册默认WiFi处理程序。
esp_netif_t *sta_netif = esp_netif_create_default_wifi_sta();
assert(sta_netif);

wifi_init_config_t cfg = WIFI_INIT_CONFIG_DEFAULT();
// 为 WiFi 驱动初始化 WiFi 分配资源,如 WiFi 控制结构、RX/TX 缓冲区、WiFi NVS 结构等,这个 WiFi 也启动 WiFi 任务。必须先调用此API,然后才能调用所有其他WiFi API
ESP_ERROR_CHECK(esp_wifi_init(&cfg));

uint16_t number = DEFAULT_SCAN_LIST_SIZE;
wifi_ap_record_t ap_info[DEFAULT_SCAN_LIST_SIZE];
uint16_t ap_count = 0;
memset(ap_info, 0, sizeof(ap_info));
// 设置WiFi工作模式为station、soft-AP或station+soft-AP,默认模式为soft-AP模式。本程序设置为station
ESP_ERROR_CHECK(esp_wifi_set_mode(WIFI_MODE_STA));
// 根据配置,启动WiFi
ESP_ERROR_CHECK(esp_wifi_start());
// 扫描所有有效的AP 阻塞
esp_wifi_scan_start(NULL, true);
// 获取上次扫描中找到的AP列表
ESP_ERROR_CHECK(esp_wifi_scan_get_ap_records(&number, ap_info));
// 获取上次扫描中找到的AP数获取上次扫描中找到的AP数
ESP_ERROR_CHECK(esp_wifi_scan_get_ap_num(&ap_count));
ESP_LOGI(TAG, "Total APs scanned = %u", ap_count);
for (int i = 0; (i < DEFAULT_SCAN_LIST_SIZE) && (i < ap_count); i++) {
ESP_LOGI(TAG, "SSID \t\t%s", ap_info[i].ssid); // 名称
ESP_LOGI(TAG, "RSSI \t\t%d", ap_info[i].rssi); // 信号强度
print_auth_mode(ap_info[i].authmode); // wifi加密模式
if (ap_info[i].authmode != WIFI_AUTH_WEP) {
print_cipher_type(ap_info[i].pairwise_cipher, ap_info[i].group_cipher);
}
ESP_LOGI(TAG, "Channel \t\t%d\n", ap_info[i].primary); // 信道
}

}

void app_main(void)
{
// Initialize NVS
// 初始化默认 NVS 分区,wifi库会使用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 );

wifi_scan();
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
I (539) wifi:mode : sta (34:85:18:98:06:dc)
I (539) wifi:enable tsf
I (3039) scan: Total APs scanned = 3
I (3039) scan: SSID hannto-printer-honey1s_mibtDE14
I (3039) scan: RSSI -39
I (3039) scan: Authmode WIFI_AUTH_WPA2_PSK
I (3039) scan: Pairwise Cipher WIFI_CIPHER_TYPE_CCMP
I (3049) scan: Group Cipher WIFI_CIPHER_TYPE_CCMP
I (3049) scan: Channel 6

I (3059) scan: SSID DIRECT-9C-EPSON-A511AF
I (3059) scan: RSSI -50
I (3069) scan: Authmode WIFI_AUTH_WPA2_PSK
I (3069) scan: Pairwise Cipher WIFI_CIPHER_TYPE_CCMP
I (3079) scan: Group Cipher WIFI_CIPHER_TYPE_CCMP
I (3079) scan: Channel 6

I (3089) scan: SSID 15-501
I (3089) scan: RSSI -55
I (3089) scan: Authmode WIFI_AUTH_WPA_WPA2_PSK
I (3099) scan: Pairwise Cipher WIFI_CIPHER_TYPE_CCMP
I (3099) scan: Group Cipher WIFI_CIPHER_TYPE_CCMP
I (3109) scan: Channel 6

WIFI Station模式

本博文描述ESP32-C3作为Station基站模式接入到WiFi AP热点。

操作流程

  • nvs_flash_init,初始化默认 NVS 分区。
  • esp_netif_init,初始化底层TCP/IP堆栈。
  • esp_event_loop_create_default,创建默认事件循环。
  • esp_netif_create_default_wifi_sta,使用默认WiFi Station配置创建esp_netif对象,将netif连接到WiFi并注册默认WiFi处理程序。
  • esp_wifi_init,为 WiFi 驱动初始化 WiFi 分配资源,如 WiFi 控制结构、RX/TX 缓冲区、WiFi NVS 结构等,这个 WiFi 也启动 WiFi 任务。必须先调用此API,然后才能调用所有其他WiFi API
  • esp_event_handler_instance_register,监听WIFI_EVENTWiFi 任意事件,触发事件后,进入回调函数
  • esp_event_handler_instance_register,监听IP_EVENT从连接的AP获得IP的事件,触发事件后,进入回调函数
  • esp_wifi_set_mode,设置WiFi工作模式为station、soft-AP或station+soft-AP,默认模式为soft-AP模式。本程序设置为station
  • esp_wifi_set_config,设置 ESP32 STA 或 AP 的配置。本程序设置为STA,并包含WiFi SSID和密码等信息
  • esp_wifi_start,根据配置,启动WiFi
  • xEventGroupWaitBits,等待事件,打印连接成功信息
  • esp_event_handler_instance_unregister,取消WIFI_EVENT事件监听
  • esp_event_handler_instance_unregister,取消IP_EVENT事件监听

注册事件回调函数

  • WIFI_EVENT_STA_START:station启动时,esp_wifi_connect连接
  • WIFI_EVENT_STA_DISCONNECTED:从AP失去连接时,,连接次数小于最大连接次数时,esp_wifi_connect 连接
  • IP_EVENT_STA_GOT_IP:从连接的AP获得IP时,打印IP
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
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) {
esp_wifi_connect();
} else if (event_base == WIFI_EVENT && event_id == WIFI_EVENT_STA_DISCONNECTED) {
if (s_retry_num < EXAMPLE_ESP_MAXIMUM_RETRY) {
esp_wifi_connect();
s_retry_num++;
ESP_LOGI(TAG, "retry to connect to the AP");
} else {
xEventGroupSetBits(s_wifi_event_group, WIFI_FAIL_BIT);
}
ESP_LOGI(TAG,"connect to the AP fail");
} else if (event_base == IP_EVENT && event_id == IP_EVENT_STA_GOT_IP) {
ip_event_got_ip_t* event = (ip_event_got_ip_t*) event_data;
ESP_LOGI(TAG, "got ip:" IPSTR, IP2STR(&event->ip_info.ip));
s_retry_num = 0;
xEventGroupSetBits(s_wifi_event_group, WIFI_CONNECTED_BIT);
}
}

关键函数

将事件回调函数注册到特定循环

  • [in] event_base:要为其注册事件的基本 ID
  • [in] event_id:要为其注册事件的 ID
  • [in] event_handler:事件被调度时的回调函数
  • [in] event_handler_arg:事件被调度时的回调函数的参数
  • [out] instance:事件被调度时的回调函数的实例,取消该注册所必须的参数。如果不取消,则可以不保存
1
esp_err_t esp_event_handler_instance_register(esp_event_base_t event_base, int32_t  event_id, esp_event_handler_t event_handler, void *event_handler_arg, esp_event_handler_instance_t *instance)

示例代码

新建项目,选择example,选择WiFi—>getting_started->station
同时在sdconfig中设置wifi名和密码
下载运行

enter description here

WIFI AP

程序流程

  • nvs_flash_init,初始化默认 NVS 分区。
  • esp_netif_init,初始化底层TCP/IP堆栈
  • esp_event_loop_create_default,创建默认事件循环。
  • esp_netif_create_default_wifi_ap,使用默认WiFi AP配置创建esp_netif对象,将netif连接到WiFi并注册默认WiFi处理程序。
  • esp_wifi_init,为 WiFi 驱动初始化 WiFi 分配资源,如 WiFi 控制结构、RX/TX 缓冲区、WiFi NVS 结构等,这个 WiFi 也启动 WiFi 任务。必须先调用此API,然后才能调用所有其他WiFi API
  • esp_event_handler_instance_register,监听WIFI_EVENTWiFi 任意事件,触发事件后,进入回调函数
  • esp_wifi_set_mode,设置WiFi工作模式为station、soft-AP或station+soft-AP,默认模式为soft-AP模式。本程序设置为AP
  • esp_wifi_set_config,设置 ESP32 STA 或 AP 的配置
  • esp_wifi_start,根据配置,启动WiFi

注册事件回调函数

WIFI_EVENT_AP_STACONNECTED:一个station连接到AP时
WIFI_EVENT_AP_STADISCONNECTED:一个station断开连接时

1
2
3
4
5
6
7
8
9
10
static void wifi_event_handler(void* arg, esp_event_base_t event_base, int32_t event_id, void* event_data)
{
if (event_id == WIFI_EVENT_AP_STACONNECTED) {
wifi_event_ap_staconnected_t* event = (wifi_event_ap_staconnected_t*) event_data;
ESP_LOGI(TAG, "station "MACSTR" join, AID=%d", MAC2STR(event->mac), event->aid);
} else if (event_id == WIFI_EVENT_AP_STADISCONNECTED) {
wifi_event_ap_stadisconnected_t* event = (wifi_event_ap_stadisconnected_t*) event_data;
ESP_LOGI(TAG, "station "MACSTR" leave, AID=%d", MAC2STR(event->mac), event->aid);
}
}

关键函数

关键函数
6.1 将事件回调函数注册到特定循环

  • [in] event_base:要为其注册事件的基本 ID
  • [in] event_id:要为其注册事件的 ID
  • [in] event_handler:事件被调度时的回调函数
  • [in] event_handler_arg:事件被调度时的回调函数的参数
  • [out] instance:事件被调度时的回调函数的实例,取消该注册所必须的参数。如果不取消,则可以不保存
1
esp_err_t esp_event_handler_instance_register(esp_event_base_t event_base, int32_t  event_id, esp_event_handler_t event_handler, void *event_handler_arg, esp_event_handler_instance_t *instance)

示例代码

新建项目,选择example,选择WiFi—>getting_started->station
同时在sdconfig中设置wifi名和密码
下载运行

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
I (564) phy_init: phy_version 503,13653eb,Jun  1 2022,17:47:08
I (604) wifi:mode : softAP (34:85:18:98:06:dd)
I (604) wifi:Total power save buffer number: 16
I (614) wifi:Init max length of beacon: 752/752
I (614) wifi:Init max length of beacon: 752/752
I (614) wifi softAP: wifi_init_softap finished. SSID:lonly password:mypassword channel:1
I (16004) wifi:new:<1,1>, old:<1,1>, ap:<1,1>, sta:<255,255>, prof:1
I (16004) wifi:station: f2:bf:ee:fa:2c:4c join, AID=1, bgn, 40U
I (16034) wifi softAP: station f2:bf:ee:fa:2c:4c join, AID=1
I (16224) esp_netif_lwip: DHCP server assigned IP to a client, IP is: 192.168.4.2
W (17534) wifi:<ba-add>idx:2 (ifx:1, f2:bf:ee:fa:2c:4c), tid:0, ssn:15, winSize:64
I (31574) wifi:station: f2:bf:ee:fa:2c:4c leave, AID = 1, bss_flags is 134259, bss:0x3fcf2d54
I (31574) wifi:new:<1,0>, old:<1,1>, ap:<1,1>, sta:<255,255>, prof:1
W (31574) wifi:<ba-del>idx
I (31584) wifi softAP: station f2:bf:ee:fa:2c:4c leave, AID=1

WIFI 配网-基于EspTouchForAndroid

WiFi配网方式

WiFi配网即:用户通过App/小程序/网页等途径将WiFi的SSID和密码等信息发送给ESP32,方式有很多种:

  • SoftAP 配网:ESP32 会建立一个 Wi-Fi 热点,用户将手机连接到这个热点后将要连接的 Wi-Fi 信息发送给 ESP32。这种配网模式需要用户手动连接到 ESP32 的热点网络,这会让用户感到奇怪和不友好,不过这种方式很可靠,设备端的代码也简单。
  • Bluetooth Low Energy 配网:ESP32 会进行 Bluetooth Low Energy 广播,附近的手机收到该广播后会询问用户是否进行 Bluetooth Low Energy 连接,如选择连接,则手机即可将信息发送给 ESP32。在这个的过程中用户无需切换 Wi-Fi 网络,但是需要打开蓝牙,用户体验相对 SoftAP 配网好一些。但是,需要在设备端加入蓝牙相关代码,这会增加固件的大小,并在配网完成前占用一定内存。
  • Smartconfig 配网:这种方式不需要建立任何通信链路,手机端通过发送不同长度的 UDP 广播包来表示 Wi-Fi 信息,ESP32 在混杂模式监听信号覆盖范围内的所有数据帧,通过一定算法得到 Wi-Fi 信息。缺点是配网成功率受环境的影响较大。
    • SmartConfig有很多种,EspTouch(APP)、AirKiss(微信)、EspTouchV2(APP)等
  • WEB 配网:在 ESP32 上建立热点,使用手机连接上后在浏览器打开配置网页,在网页中完成配网,这种方式很可靠,而且允许在电脑端完成配网,缺点是需要在设备端占用空间来嵌入网页。

主程序

  • nvs_flash_init,初始化默认 NVS 分区
  • esp_netif_init,初始化底层TCP/IP堆栈
  • esp_event_loop_create_default,创建默认事件循环
  • esp_netif_create_default_wifi_sta,使用默认WiFi Station配置创建esp_netif对象,将netif连接到WiFi并注册默认WiFi处理程序
  • esp_wifi_init,为 WiFi 驱动初始化 WiFi 分配资源,如 WiFi 控制结构、RX/TX 缓冲区、WiFi NVS 结构等,这个 WiFi 也启动 WiFi 任务。必须先调用此API,然后才能调用所有其他WiFi API
  • esp_event_handler_instance_register,监听WIFI_EVENTWiFi 任意事件,触发事件后,进入回调函数
  • esp_event_handler_instance_register,监听IP_EVENT从连接的AP获得IP的事件,触发事件后,进入回调函数
  • esp_event_handler_instance_register,监听SC_EVENT从SmartConfig任意事件,触发事件后,进入回调函数
  • esp_wifi_set_mode,设置WiFi工作模式为station、soft-AP或station+soft-AP,默认模式为soft-AP模式。本程序设置为station
  • esp_wifi_start,根据配置,启动WiFi

回调函数

  • WIFI_EVENT_STA_START,WiFi station 模式启动时:
  • 创建smartconfig_example_task线程,开始SmartConfig
  • WIFI_EVENT_STA_DISCONNECTED,WiFi station 模式失去连接
  • 清除CONNECTED_BIT标志位
  • IP_EVENT_STA_GOT_IP,WiFi station 模式从连接的AP那获得IP
  • 设置CONNECTED_BIT标志位
  • SC_EVENT_SCAN_DONE,SmartConfig 扫描AP列表结束
  • SC_EVENT_FOUND_CHANNEL,SmartConfig 从目标AP找到频道
  • SC_EVENT_GOT_SSID_PSWD,SmartConfig 获得WiFi信息(SSID和密码)时:
  • 解析出WiFi的SSID和密码
  • esp_wifi_disconnect断开当前WiFi连接
  • esp_wifi_set_configWiFI配置,设置WIFI_IF_STA模式,设置WiFi的SSID和密码
  • esp_wifi_connectWiFi连接
  • SC_EVENT_SEND_ACK_DONE,SmartConfig 给App发送完成ACK
  • 设置ESPTOUCH_DONE_BIT标志位
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
42
43
44
45
46
47
48
49
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) {
xTaskCreate(smartconfig_example_task, "smartconfig_example_task", 4096, NULL, 3, NULL);
} else if (event_base == WIFI_EVENT && event_id == WIFI_EVENT_STA_DISCONNECTED) {
esp_wifi_connect();
xEventGroupClearBits(s_wifi_event_group, CONNECTED_BIT);
} else if (event_base == IP_EVENT && event_id == IP_EVENT_STA_GOT_IP) {
xEventGroupSetBits(s_wifi_event_group, CONNECTED_BIT);
} else if (event_base == SC_EVENT && event_id == SC_EVENT_SCAN_DONE) {
ESP_LOGI(TAG, "Scan done");
} else if (event_base == SC_EVENT && event_id == SC_EVENT_FOUND_CHANNEL) {
ESP_LOGI(TAG, "Found channel");
} else if (event_base == SC_EVENT && event_id == SC_EVENT_GOT_SSID_PSWD) {
ESP_LOGI(TAG, "Got SSID and password");

smartconfig_event_got_ssid_pswd_t *evt = (smartconfig_event_got_ssid_pswd_t *)event_data;
wifi_config_t wifi_config;
uint8_t ssid[33] = { 0 };
uint8_t password[65] = { 0 };
uint8_t rvd_data[33] = { 0 };

bzero(&wifi_config, sizeof(wifi_config_t));
memcpy(wifi_config.sta.ssid, evt->ssid, sizeof(wifi_config.sta.ssid));
memcpy(wifi_config.sta.password, evt->password, sizeof(wifi_config.sta.password));
wifi_config.sta.bssid_set = evt->bssid_set;
if (wifi_config.sta.bssid_set == true) {
memcpy(wifi_config.sta.bssid, evt->bssid, sizeof(wifi_config.sta.bssid));
}

memcpy(ssid, evt->ssid, sizeof(evt->ssid));
memcpy(password, evt->password, sizeof(evt->password));
ESP_LOGI(TAG, "SSID:%s", ssid);
ESP_LOGI(TAG, "PASSWORD:%s", password);
if (evt->type == SC_TYPE_ESPTOUCH_V2) {
ESP_ERROR_CHECK( esp_smartconfig_get_rvd_data(rvd_data, sizeof(rvd_data)) );
ESP_LOGI(TAG, "RVD_DATA:");
for (int i=0; i<33; i++) {
printf("%02x ", rvd_data[i]);
}
printf("\n");
}
ESP_ERROR_CHECK( esp_wifi_disconnect() );
ESP_ERROR_CHECK( esp_wifi_set_config(WIFI_IF_STA, &wifi_config) );
esp_wifi_connect();
} else if (event_base == SC_EVENT && event_id == SC_EVENT_SEND_ACK_DONE) {
xEventGroupSetBits(s_wifi_event_group, ESPTOUCH_DONE_BIT);
}
}

SmartConfig线程

  • esp_smartconfig_set_type设置SmartConfig的协议类型为SC_TYPE_ESPTOUCH。协议类型有:SC_TYPE_ESPTOUCH、SC_TYPE_AIRKISS、SC_TYPE_ESPTOUCH_AIRKISS、SC_TYPE_ESPTOUCH_V2等
  • esp_smartconfig_start启动SmartConfig
  • 等待ESPTOUCH_DONE_BIT标志位
    • esp_smartconfig_stop停止SmartConfig
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      static void smartconfig_example_task(void * parm)
      {
      EventBits_t uxBits;
      ESP_ERROR_CHECK( esp_smartconfig_set_type(SC_TYPE_ESPTOUCH) );
      smartconfig_start_config_t cfg = SMARTCONFIG_START_CONFIG_DEFAULT();
      ESP_ERROR_CHECK( esp_smartconfig_start(&cfg) );
      while (1) {
      uxBits = xEventGroupWaitBits(s_wifi_event_group, CONNECTED_BIT | ESPTOUCH_DONE_BIT, true, false, portMAX_DELAY);
      if(uxBits & CONNECTED_BIT) {
      ESP_LOGI(TAG, "WiFi Connected to ap");
      }
      if(uxBits & ESPTOUCH_DONE_BIT) {
      ESP_LOGI(TAG, "smartconfig over");
      esp_smartconfig_stop();
      vTaskDelete(NULL);
      }
      }
      }

流程图

  • 回调函数和SmartConfig线程是通过xEventGroupWaitBits、xEventGroupSetBits、xEventGroupClearBits等函数进行通信
    • 回调函数设置或清楚Bits,xEventGroupSetBits、xEventGroupClearBits
    • SmartConfig线程通过循环等待xEventGroupWaitBits

smart liucheng

示例代码

新建项目,选择example,选择WiFi—>smart_config
下载运行

  • 下载、安装、打开EspTouchForAndroid App
  • 输入WiFi密码,确认即可
  • 注意:ESP32只支持2.4GHz WiFi,还不支持5GHz WiF

enter description here

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
I (600) wifi:mode : sta (34:85:18:98:06:dc)
I (600) wifi:enable tsf
I (660) smartconfig: SC version: V3.0.1
I (5470) wifi:ic_enable_sniffer
I (5470) smartconfig: Start to find channel...
I (5470) smartconfig_example: Scan done
I (57790) smartconfig: TYPE: ESPTOUCH
I (57800) smartconfig: T|AP MAC: 58:41:20:1e:06:19
I (57800) smartconfig: Found channel on 6-0. Start to get ssid and password...
I (57800) smartconfig_example: Found channel
I (63120) smartconfig: T|pswd: 15996264842
I (63120) smartconfig: T|ssid: 15-501
I (63120) smartconfig: T|bssid: 58:41:20:1e:06:19
I (63120) wifi:ic_disable_sniffer
I (63120) smartconfig_example: Got SSID and password
I (63120) smartconfig_example: SSID:15-501
I (63130) smartconfig_example: PASSWORD:15996264842
I (63180) wifi:new:<6,0>, old:<6,0>, ap:<255,255>, sta:<6,0>, prof:1
I (64160) wifi:state: init -> auth (b0)
I (64160) wifi:state: auth -> assoc (0)
I (64220) wifi:state: assoc -> run (10)
W (64240) wifi:<ba-add>idx:0 (ifx:0, 58:41:20:1e:06:19), tid:5, ssn:0, winSize:64
I (64360) wifi:connected with 15-501, aid = 8, channel 6, BW20, bssid = 58:41:20:1e:06:19
I (64360) wifi:security: WPA2-PSK, phy: bgn, rssi: -52
I (64370) wifi:pm start, type: 1

I (64370) wifi:set rx beacon pti, rx_bcn_pti: 0, bcn_timeout: 0, mt_pti: 25000, mt_time: 10000
I (64440) wifi:BcnInt:102400, DTIM:1
I (65440) esp_netif_handlers: sta ip: 192.168.0.108, mask: 255.255.255.0, gw: 192.168.0.1
I (65440) smartconfig_example: WiFi Connected to ap
W (65830) wifi:<ba-add>idx:1 (ifx:0, 58:41:20:1e:06:19), tid:6, ssn:2, winSize:64
I (68460) smartconfig_example: smartconfig over

WIFI 配网-基于AirKiss

本文主要是基于Airkiss的SmartConfig智能配网,流程原理与EspTouch基本相同,都是通过udp广播的方式将WiFi AP的SSID和密码发送出去,只是通信协议和数据格式不同而已。

EspTouch是乐鑫的提供的WiFi配网协议,提供了Android/IOS源码
Airkiss是微信提供的WiFi配网协议,如果是基于微信生态开发的应用,如小程序、公众号等,则优先选择它

代码修改

smartconfig_main.c中函数smartconfig_example_task中的esp_smartconfig_set_type设置SmartConfig通信协议修改掉,如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
static void smartconfig_example_task(void * parm)
{
EventBits_t uxBits;
// ESP_ERROR_CHECK( esp_smartconfig_set_type(SC_TYPE_ESPTOUCH) );
// 修改为下面这一句,其它保持一致
ESP_ERROR_CHECK( esp_smartconfig_set_type(SC_TYPE_ESPTOUCH_AIRKISS) );
smartconfig_start_config_t cfg = SMARTCONFIG_START_CONFIG_DEFAULT();
ESP_ERROR_CHECK( esp_smartconfig_start(&cfg) );
while (1) {
uxBits = xEventGroupWaitBits(s_wifi_event_group, CONNECTED_BIT | ESPTOUCH_DONE_BIT, true, false, portMAX_DELAY);
if(uxBits & CONNECTED_BIT) {
ESP_LOGI(TAG, "WiFi Connected to ap");
}
if(uxBits & ESPTOUCH_DONE_BIT) {
ESP_LOGI(TAG, "smartconfig over");
esp_smartconfig_stop();
vTaskDelete(NULL);
}
}
}

配网

  • 微信搜索:乐鑫信息科技
  • 菜单商铺 —> Airkiss设备
  • 输入WiFi密码,点击连接
  • 注意:ESP32只支持2.4GHz WiFi,还不支持5GHz WiFi

或直接扫描下面二维码
官方给的工具网址为:https://iot.espressif.cn/configWXDeviceWiFi.html
可以利用网址二维码生成器将链接转二维码,像我生成的二维码:
5e4c1a97-6c05-4222-ae70-0ae6ef4ac87d

enter description here

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
I (611) wifi:mode : sta (34:85:18:98:06:dc)
I (611) wifi:enable tsf
I (661) smartconfig: SC version: V3.0.1
I (5471) wifi:ic_enable_sniffer
I (5471) smartconfig: Start to find channel...
I (5471) smartconfig_example: Scan done
I (25221) smartconfig: TYPE: AIRKISS
I (25221) smartconfig: T|AP MAC: 58:41:20:1e:06:19
I (25231) smartconfig: Found channel on 6-0. Start to get ssid and password...
I (25231) smartconfig_example: Found channel
I (28451) smartconfig: T|pswd: 15996264842
I (28451) smartconfig: T|ssid: 15-501
I (28451) wifi:ic_disable_sniffer
I (28451) smartconfig_example: Got SSID and password
I (28451) smartconfig_example: SSID:15-501
I (28461) smartconfig_example: PASSWORD:15996264842
I (28561) wifi:new:<6,0>, old:<6,0>, ap:<255,255>, sta:<6,0>, prof:1
I (29541) wifi:state: init -> auth (b0)
I (29571) wifi:state: auth -> assoc (0)
I (29591) wifi:state: assoc -> run (10)
I (32701) wifi:state: run -> init (fc0)
I (32701) wifi:new:<6,0>, old:<6,0>, ap:<255,255>, sta:<6,0>, prof:1
I (32701) wifi:new:<6,0>, old:<6,0>, ap:<255,255>, sta:<6,0>, prof:1
I (35121) wifi:new:<6,0>, old:<6,0>, ap:<255,255>, sta:<6,0>, prof:1
I (35121) wifi:state: init -> auth (b0)
I (35131) wifi:state: auth -> assoc (0)
I (35231) wifi:state: assoc -> run (10)
I (35331) wifi:connected with 15-501, aid = 8, channel 6, BW20, bssid = 58:41:20:1e:06:19
I (35331) wifi:security: WPA2-PSK, phy: bgn, rssi: -52
I (35341) wifi:pm start, type: 1

I (35341) wifi:set rx beacon pti, rx_bcn_pti: 0, bcn_timeout: 0, mt_pti: 25000, mt_time: 10000
I (35341) wifi:BcnInt:102400, DTIM:1
I (36441) esp_netif_handlers: sta ip: 192.168.0.108, mask: 255.255.255.0, gw: 192.168.0.1
I (36441) smartconfig_example: WiFi Connected to ap
W (37211) wifi:<ba-add>idx:0 (ifx:0, 58:41:20:1e:06:19), tid:5, ssn:3, winSize:64
W (37231) wifi:<ba-add>idx:1 (ifx:0, 58:41:20:1e:06:19), tid:6, ssn:1, winSize:64
I (39631) smartconfig_example: smartconfig over

http request client

ESP HTTP client介绍

包含:

  • 启用https
  • 位于: Component config > ESP HTTP client
  • 此选项将通过链接esp-tls库并初始化SSL传输来启用https协议
  • Default value: Yes (enabled)

CONFIG_ESP_HTTP_CLIENT_ENABLE_HTTPS

  • 启用HTTP基本身份验证

  • 位于:Component config > ESP HTTP client

  • 此选项将启用HTTP基本身份验证。它在默认情况下被禁用,因为基本身份验证使用未加密的编码,因此在不使用TLS时会引入漏洞

  • Default value: Yes (enabled)
    CONFIG_ESP_HTTP_CLIENT_ENABLE_BASIC_AUTH

  • 启用HTTP摘要式身份验证

  • Found in: Component config > ESP HTTP client

  • 此选项将启用HTTP摘要身份验证。默认情况下,它是启用的,但使用 不建议配置,因为密码可以从exchange中派生,因此它引入了 不使用TLS时的漏洞

  • Default value:默认值:No (disabled)否(禁用

CONFIG_ESP_HTTP_CLIENT_ENABLE_DIGEST_AUTH

HTTP简介

HTTP协议是Hyper Text Transfer Protocol超文本传输协议的缩写,基于TCP传输层协议进行通信,采用客户端-服务器模型(C/S架构),属于应用层协议
HTTP数据传输是透明的,明文传输涉及到通信安全时,传输层上可套接TLS/SSL协议进行加密,也就是HTTPS
enter description here

特点

  • HTTP默认使用80端口,HTTPS默认使用443端口
  • 无状态,协议对于事务处理没有记忆能力,每次都是一个新的连接,服务端不会记录前后的请求信息(针对这个问题,引入Cookie将记录加密存储到客户端中)
  • 无连接,限制每次连接只处理一个请求。服务器处理完客户的请求,并收到客户的应答后,即断开连接;(HTTP1.1版本支持持久连接的方法)
  • 媒体独立,允许传输任意类型的数据对象,传输类型由Content-Type定义

操作步骤

  • esp_http_client_init(),创建一个 esp_http_client_handle_t 实例,即基于给定的 esp_http_client_config_t 配置创建 HTTP 客户端句柄。此函数必须第一个被调用。若用户未明确定义参数的配置值,则使用默认值。
  • 其次调用 esp_http_client_perform(),执行 esp_http_client 的所有操作,包括打开连接、交换数据、关闭连接(如需要),同时在当前任务完成前阻塞该任务。所有相关的事件(在 esp_http_client_config_t 中指定)将通过事件处理程序被调用。
  • 最后调用 esp_http_client_cleanup() 来关闭连接(如有),并释放所有分配给 HTTP 客户端实例的内存。此函数必须在操作完成后最后一个被调用。

持久连接

持久连接是 HTTP 客户端在多次交换中重复使用同一连接的方法。如果服务器没有使用 Connection: close 头来请求关闭连接,连接就会一直保持开放,用于其他新请求。

为了使 ESP HTTP 客户端充分利用持久连接的优势,建议尽可能多地使用同一个句柄实例来发起请求,可参考应用示例中的函数 http_rest_with_url 和 http_rest_with_hostname_path。示例中,一旦创建连接,即会在连接关闭前发出多个请求(如 GET、 POST、 PUT 等)。

HTTPS 请求

ESP HTTP 客户端支持使用 mbedTLS 的 SSL 连接,需将 url 配置为以 https 开头,或将 transport_type 设置为 HTTP_TRANSPORT_OVER_SSL。可以通过 CONFIG_ESP_HTTP_CLIENT_ENABLE_HTTPS 来配置 HTTPS 支持(默认启用)。

备注

在发起 HTTPS 请求时,如需服务器验证,首先需要向 esp_http_client_config_t 配置中的 cert_pem 成员提供额外的根证书(PEM 格式)。用户还可以通过 esp_http_client_config_t 配置中的 crt_bundle_attach 成员,使用 ESP x509 Certificate Bundle 进行服务器验证。

如需了解上文备注中的实现细节,请参考应用示例中的函数 https_with_url 和 https_with_hostname_path。

HTTP 流

1
2
3
4
5
esp_http_client_config_t config = {
.url = "https://www.howsmyssl.com", // url
.cert_pem = howsmyssl_com_root_cert_pem_start,//证书
};
esp_http_client_handle_t client = esp_http_client_init(&config);

传递配置形参,url成员必填,如果跳过证书可将元素skip_cert_common_name_check改为TRUE

  • url:常见的链接,传输协议(https)+域名或IP(host)+ 端口号(port) +路径(path)+查询字符串(query)+锚点https://www.baidu.com/swd=hello&rsv_spt=1#5
    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
    /**
    * @brief HTTP configuration
    */
    typedef struct {
    const char *url; //url请求接口必须配置 /*!< HTTP URL, the information on the URL is most important, it overrides the other fields below, if any */
    const char *host; //服务器域名或ip地址 /*!< Domain or IP as string */
    int port; //端口 http默认80 https 默认443 /*!< Port to connect, default depend on esp_http_client_transport_t (80 or 443) */
    const char *username; //用户名,认证使用 /*!< Using for Http authentication */
    const char *password; //用户密码,认证使用 /*!< Using for Http authentication */
    esp_http_client_auth_type_t auth_type; //认证方式 /*!< Http authentication type, see `esp_http_client_auth_type_t` */
    const char *path; //路径 /*!< HTTP Path, if not set, default is `/` */
    const char *query; //请求参数 /*!< HTTP query */
    const char *cert_pem; //证书 /*!< SSL server certification, PEM format as string, if the client requires to verify server */
    const char *client_cert_pem; /*!< SSL client certification, PEM format as string, if the server requires to verify client */
    const char *client_key_pem; /*!< SSL client key, PEM format as string, if the server requires to verify client */
    esp_http_client_method_t method; //请求方式 post get /*!< HTTP Method */
    int timeout_ms; //请求超时 /*!< Network timeout in milliseconds */
    bool disable_auto_redirect; /*!< Disable HTTP automatic redirects */
    int max_redirection_count; /*!< Max number of redirections on receiving HTTP redirect status code, using default value if zero*/
    int max_authorization_retries; /*!< Max connection retries on receiving HTTP unauthorized status code, using default value if zero. Disables authorization retry if -1*/
    http_event_handle_cb event_handler; //可注册回调 /*!< HTTP Event Handle */
    esp_http_client_transport_t transport_type; // 传输方式 tcp ssl /*!< HTTP transport type, see `esp_http_client_transport_t` */
    int buffer_size; //接收缓存大小 /*!< HTTP receive buffer size */
    int buffer_size_tx; //发送缓存大小 /*!< HTTP transmit buffer size */
    void *user_data; //http用户数据 /*!< HTTP user_data context */
    bool is_async; //同步模式 /*!< Set asynchronous mode, only supported with HTTPS for now */
    bool use_global_ca_store; /*!< Use a global ca_store for all the connections in which this bool is set. */
    bool skip_cert_common_name_check; //跳过证书 /*!< Skip any validation of server certificate CN field */
    } esp_http_client_config_t;

其次调用 esp_http_client_perform(),执行 esp_http_client 的所有操作,包括打开连接、交换数据、关闭连接(如需要),同时在当前任务完成前阻塞该任务。所有相关的事件(在 esp_http_client_config_t 中指定)将通过事件处理程序被调用。

最后调用 esp_http_client_cleanup() 来关闭连接(如有),并释放所有分配给 HTTP 客户端实例的内存。此函数必须在操作完成后最后一个被调用。

https://www.cnblogs.com/weibanggang/p/9454581.html
https://juejin.cn/post/7106310756580196388
https://cloud.tencent.com/developer/article/2097221

待机唤醒

内部温度

DAC

PWM DAC

IIC

SPI

DMA

串口 DMA

SPI DMA

LCD DMA

SRAM

内存管理

SD

FATFS

汉字显示

图片显示

照相机

音乐播放

视频播放

手写识别

输入法

串口IAP

USB、OTA

RTOS

WIFI网络

蓝牙

温度传感器

MPU6050

Cmake学习

cmake_minimum_required(): cmake最小支持版本
include():cmake 包含
project(): cmake 工程名
idf_component_register(): idf文件注册

  • 参数:SCRS 源文件
  • INCLUDE_DIRS .h头文件
  • REQUIRES 依赖

file(): 文件操作命令

  • 参数: GLOB 通过正则表达式匹配文件名并保存到变量中

ESP32/ESP8266程序下载电路

官方一键下载电路分析

https://www.espressif.com/zh-hans/products/modules

https://zhuanlan.zhihu.com/p/145369083
https://blog.csdn.net/weixin_41975300/article/details/104834771
https://blog.csdn.net/woniulx2014/article/details/117172805
https://blog.csdn.net/wutongpro/article/details/109101063
https://www.jianshu.com/p/d7c0dbb223a0
https://www.jianshu.com/p/fe98713e40eb
https://www.cnblogs.com/cai-zi/p/13942615.html

官方下载程序的SDK代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# issue reset-to-bootloader:
# RTS = either CH_PD/EN or nRESET (both active low = chip in reset
# DTR = GPIO0 (active low = boot to flasher)
#
# DTR & RTS are active low signals,
# ie True = pin @ 0V, False = pin @ VCC.
if mode != 'no_reset':
self._setDTR(False) # IO0=HIGH
1) self._setRTS(True) # EN=LOW, chip in reset
time.sleep(0.1)
2) self._setDTR(True) # IO0=LOW
3) self._setRTS(False) # EN=HIGH, chip out of reset
time.sleep(0.05)
4) self._setDTR(False) # IO0=HIGH, done

分析代码可知,下载程序有四步:

  1. IO0=HIGH EN=LOW, chip in reset 延时 100ms
  2. IO0=LOW EN=HIGH, chip out of reset 延时 50ms
  3. IO0=HIGH done
    程序下载流程:
  4. 芯片断电,设置 IO0 = 0 EN = 0 进入下载模式
  5. 上电,

S2 硬件设计注意事项

  • IO0相当于 WAKEUP,EN相当于 RESET
  • U0TXD 需串联 499R 电阻,S2 模组已经内置。
  • S2模组的管脚 IO26 默认用于连接至模组上集成的 PSRAM 的 CS 端,不可用于其他功能
  • IO0 和 IO46 为系统启动模式功能。为0下载模式,为1从内部flash启动
    管脚 默认 SPI启动模式 下载启动模式
    IO0 上拉 1 0
    IO46 下拉 x 0
    复位放开后, 管脚和普通管脚功能相同
  • IO45 设置 VDD_SPI 电压。下拉为3.3V,上拉为1.8V。S2模组中 VDD_SPI为扩展的SPI、PSRAM电压。
  • GPIO18 作为 U1RXD,在芯片上电时是不确定状态,可能会影响芯片正常进入下载启动模式,需要在外部增加一个上拉电阻来解决

用Arduino玩ESP32(05):GPIO使用


乐鑫ESP32 S3教程_基于ESP-IDF v5.0
http://lonlypan.com/2022/05/08/乐鑫ESP32 S2教程_基于ESP-IDF v4.4/
作者
LonlyPan
发布于
2022年5月8日
许可协议