学校老师留了个作业,让用剩下一半的寒假学学ESP32,做蓝牙透传+STA&AP模式下工作的http服务器,但是不准用Arduino
当场就傻了:ESP32我刚刚好就会一手Arduino;乐鑫那套ESPIDF太难啃,之前点了个灯就去快乐stm32了;micropython……刷完固件发现蓝牙支持跟【数据删除】一样,还不如用c写——一咬牙一跺脚,回头肝ESPIDF吧
总体思路:资源少,跟着官方走准没错,硬啃就完事了
这个系列笔记可以供只接触过单片机开发(STM32、51基础)和硬件相关知识但没有接触过网络相关知识的同学翻阅学习
项目文件夹构建
ESP-IDF项目由各种“组件”构成,你需要什么功能就要往里扔进去什么组件
如果你的代码里用了一堆WiFi的库函数,但是没把WiFi组件加入进去,你是没办法用WiFi功能的
项目保存在项目文件夹下,它的根目录如下所示:
1 | ├── CMakeLists.txt Cmake使用的文件 |
需要注意的是:ESP-IDF并不是项目文件夹的一部分,它更像是一个自助编译器,项目文件夹通过idf.py esptools等工具和${IDF_PATH}与ESP-IDF目录建立联系;同样,esp的开发工具链也独立于项目存在,通过${PATH}对项目进行作用
项目建立前,esp-idf会通过idf.py menuconfig配置出Makefile,这些配置保存在sdkconfig中。sdkconfig会被保存在项目文件夹的根目录
CMakeLists.txt通过idf_component_register将项目文件夹下面的组件进行注册,如下所示
1 | idf_component_register(SRCS "foo.c" "bar.c" |
SRCS给出了源文件清单,能支持的源文件后缀名为.c .cpp .cc .S
INCLUDE_DIRS给出了组件中文件的搜索路径
REQUIRES不是必须的,它声明了其他需要加入的组件
通过这个txt文档,esp-idf就能知道你往里扔了什么组件,然后在编译的时候就会把这些组件编译链接进去(可以理解成操作系统的静态链接)
当编译完成后,文件根目录下会多出build文件夹和sdkconfig文件,build文件夹用来存放编译过程中的文件和生成的文件,sdkconfig文件是在menuconfig的过程中产生的,如果曾经多次重新设置过menuconfig,还会发现多出了以.old结尾的config文件
另外组件也可以自制,详细内容参考官方教程;而idf.py 的底层是用Cmake、make工具实现的,所以也可以直接用这些工具进行编译(不过应该没人这么干)
CMake与component组件
【摘自官方文档】一个ESP-IDF项目可以看作是多个不同组件(component)的集合,组件是模块化且独立的代码,会被编译成静态库并链接到应用程序。ESP-IDF自带一些组件,也可以去找开源项目已有的组件来用
ESP-IDF的组件其实是对CMake的封装,如果使用纯CMake风格的构建方式也可行(说到底还是交叉编译的那套流程,只是乐鑫针对ESP32进行了优化),如下所示
1 | cmake_minimum_required(VERSION 3.5) |
示例项目的目录树结构可能如下所示:
1 | - myProject/ #主目录 |
main
目录是一个特殊的“伪组件”,包含项目本身的源代码。main
是默认名称,CMake 变量 COMPONENT_DIRS
默认包含此组件,但您可以修改此变量。或者,您也可以在顶层 CMakeLists.txt 中设置 EXTRA_COMPONENT_DIRS
变量以查找其他指定位置处的组件。如果项目中源文件较多,建议将其归于组件中,而不是全部放在 main
中。
全局CMake编写
全局CMake文档应该至少包含如下三个部分:
1 | cmake_minimum_required(VERSION 3.5) #必须放在第一行,设置构建该项目所需CMake的最小版本号 |
每个 CMakeLists 文件只能定义一个项目
还可以包含以下可选部分
1 | COMPONENT_DIRS #组件搜索目录,默认为${IDF_PATH}/components、${PROJECT_PATH}/components和EXTRA_COMPONENT_DIRS |
使用set命令来设置以上变量,如下所示
1 | set(COMPONENTS "COMPONENTx") |
注意:set命令需要放在include之前,cmake_minimum_required之后
特别地,可以重命名main组件,分为两种情况
main组件处于正常位置
${PROJECT_PATH}/main
,则会被自动添加到构建系统中,其他组件自动成为main的依赖项,方便处理依赖关系main组件被重命名为xxx,需要在全局CMake设定中设置EXTRA_COMPONENT_DIRS=${PROJECT_PATH}/xxx,并在组件CMake目录中设置COMPONENT_REQUIRES或COMPONENT_PRIV_REQUIRES以指定依赖项
组件CMake编写
每个项目都包含一个或多个组件,这些组件可以是 ESP-IDF 的一部分,可以是项目自身组件目录的一部分,也可以从自定义组件目录添加
组件是COMPONENT_DIRS列表中包含CMakeLists.txt文件的任何目录
ESP-IDF会搜索COMPONENT_DIRS中的目录列表来查找项目的组件此列表中的目录可以是组件自身(即包含CMakeLists.txt文件的目录),也可以是子目录为组件的顶级目录;搜索顺序:【ESP-IDF内部组件】-【项目组件】-【EXTRA_COMPONENT_DIRS】中的组件,如果这些目录中的两个或者多个包含具有相同名字的组件,则使用搜索到的最后一个位置的组件,允许将组件复制到项目目录中再修改以覆盖ESP-IDF组件
最小的组件CMakeLists如下
1 | set(COMPONENT_SRCS "foo.c" "k.c") #用空格分隔的源文件列表 |
有以下预设变量,不建议修改
1 | COMPONENT_PATH #组件目录,是包含CMakeLists.txt文件的绝对路径,注意路径中不能包含空格 |
有以下项目级别的变量,不建议修改,但可以在组件CMake文档中使用
1 | PROJECT_NAME #项目名,在全局CMake文档中设置 |
【摘自官网】如果一个组件仅需要额外组件的头文件来编译其源文件(而不是全局引入它们的头文件),则这些被依赖的组件需要在 COMPONENT_PRIV_REQUIRES
中指出
有以下可选的组件特定变量,用于控制某组件的行为
1 | COMPONENT_SRCS #要编译进当前组件的源文件的路径,推荐使用此方法向构建系统中添加源文件 |
组件配置文件Kconfig
每个组件都可以包含一个Kconfig文件,和CMakeLists.txt放在同一目录下
Kconfig文件中包含要添加到该组件配置菜单中的一些配置设置信息,运行menuconfig时,可以在Component Settings菜单栏下找到这些设置
有手就行的入门
1 |
|
以下内容头文件包含部分会省略这些
一般程序的入口是app_main()函数
1 | void app_main(void) |
点灯
1 |
|
UART
官方给出的配置步骤为:
- 设置uart_config_t配置结构体
- 通过ESP_ERROR_CHECK(uart_param_config(uart_num, &uart_config));应用设置
- 设置引脚
- 安装驱动,设置buffer和事件处理函数等
- 配置FSM并运行UART
1 |
|
console控制台
ESP提供了一个console用于串口调试,可以实现类似shell的操作
在固件中加入console相关组件后烧录,在串口中打出help就可以查看相关帮助
1 | void register_system(void)//系统相关指令 |
组件中两个目录 :cmd_nvs用于指令的识别;cmd_system用于系统指令的实现(这部分功能需要与RTOS配合才行)
NVS FLASH
NVS即Non-volatile storage非易失性存储
它相当于把ESP32的关键数据以键值格式存储在FLASH里,NVS通过spi_flash_{read|write|erase}
三个API进行操作,NVS使用主flash的一部分。管理方式类似数据库的表,在NVS里面可以存储很多个不同的表,每个表下面有不同的键值,每个键值可以存储8位、16位、32位等等不同的数据类型,但不能是浮点数
- 使用接口函数nvs_flash_init();进行初始化,如果失败可以使用nvs_flash_erase();先擦除再初始化
- 应用程序可以使用nvs_open();选用NVS表中的分区或通过nvs_open_from_part()指定其名称后使用其他分区
注意:NVS分区被截断时,其内容应该被擦除
读写操作
1 | nvs_get_i8(my_handle,//表的句柄 |
表操作
1 | nvs_open("List",//表名 |
NVS初始化示例程序
官方给出的示例程序中一般以以下形式初始化NVS
1 | //Initialize NVS |
ESPIDF提供的常用库函数
ESP_LOG打印系统日志到串口
1 |
|
- ESP_LOGE - 错误日志 (最高优先级)
- ESP_LOGW - 警告日志
- ESP_LOGI - 信息级别的日志
- ESP_LOGD - 用于调试的日志
- ESP_LOGV - 仅仅用于提示的日志{最低优先级)
这些日志可以在menuconfig设置中打开或关闭,也可以在代码中手动设置关闭
RTOS操作
- vTaskDelay将任务置为阻塞状态,期间CPU继续运行其它任务
持续时间由参数xTicksToDelay指定,单位是系统节拍时钟周期
1 | void vTaskDelay(portTickTypexTicksToDelay) |
常量portTickTypexTicksToDelay用来辅助计算真实时间,此值是系统节拍时钟中断的周期,单位是ms
在文件FreeRTOSConfig.h中,宏INCLUDE_vTaskDelay 必须设置成1,此函数才能有效
- xTaskCreate创建新的任务并添加到任务队列
注意:所有任务应当为死循环且永远不会返回,即嵌套在while(1)内
1 | xTaskCreate(pdTASK_CODE pvTaskCode,//指向任务的入口函数 |
注意,任务优先级0为最低,数字越大优先级越高
- FreeRTOS的神奇之处
一句话概论:RTOS就是彳亍,FreeRTOS可以实现任务之间的时间片轮转调度,两个任务可以你执行一会我执行一会,高优先级任务还能抢占低优先级任务,让它马上爪巴,高优先级任务先运行
- FreeRTOS的底层实现还没看明白,过一阵子再学,反正效果和RTThread差不多,先把作业肝完再说 =)
这种神奇的操作靠的就是上面两个API
需要注意的是:所有任务应当为死循环且永远不会返回(两次强调)
不过如果实在不想写死循环,可以在任务末尾加上
1 | vTaskDelete();//用于删除执行结束的任务 |
不过只执行一次的任务大多是在初始化阶段完成的,用的时候尽量小心些
- 事件event
事件是一种实现任务间通信的机制,主要用于实现多任务间的同步,但事件通信只能是事件类型的通信,无数据传输。事件可以实现一对多和多对多的传输:一个任务可以等待多个事件的发生:可以是任意一个事件发生时唤醒任务进行事件处理;也可以是几个事件都发生后才唤醒任务进行事件处理
1 |
|
事件使用事件循环来管理,事件循环分别为默认事件循环和自定义事件循环
默认事件循环不需要传入事件循环句柄;但自定义循环需要
1 | esp_event_loop_create(const esp_event_loop_args_t *event_loop_args,//事件循环参数 |
事件需要注册到事件循环
1 | /* 注册事件到事件循环 */ |
默认事件循环default event loop是系统的基础事件循环,用于传递系统事件(如WiFi等),但是也可以注册用户事件,一般的蓝牙+WiFi用这一个循环就足够了
1 | esp_event_loop_create_default(void)//创建默认事件循环 |
1 | esp_event_loop_run(esp_event_loop_handle_t event_loop,//事件循环句柄 |
默认事件和自定义事件之间可以进行发送操作
1 | esp_event_post(esp_event_base_t event_base, int32_t event_id, void *event_data, size_t event_data_size, TickType_t ticks_to_wait) |
使用宏
1 | ESP_EVENT_DECLARE_BASE() |
来声明和定义事件,同时事件的ID应该用enum枚举变量来指出,如下所示
1 | /* 头文件 */ |
可使用API esp_event_loop_create_default()来创建事件
1 | esp_event_loop_create_default() |