ESP32 编程笔记

ESP32 网络接收阻塞与非阻塞

在网络数据接收实现过程中,revc函数默认是阻塞接收,有时常用于非阻塞接收模式,此时可使用第三个FLAG参数进行配置,

1
2
3
4
5
6
7
8
9
10
// 在sockets.h中 
/* Flags we can use with send and recv. */
#define MSG_PEEK 0x01 /* Peeks at an incoming message */
#define MSG_WAITALL 0x02 /* Unimplemented: Requests that the function block until the full amount of data requested can be returned */
#define MSG_OOB 0x04 /* Unimplemented: Requests out-of-band data. The significance and semantics of out-of-band data are protocol-specific */
#define MSG_DONTWAIT 0x08 /* Nonblocking i/o for this operation only */
#define MSG_MORE 0x10 /* Sender will send more */
#define MSG_NOSIGNAL 0x20 /* Uninmplemented: Requests not to send the SIGPIPE signal if an attempt to send is made on a stream-oriented socket that is no longer connected. */


MSG_DONTWAIT 可使此次接收非阻塞

1
recv(recv_sock, buf, sizeof(buf)-1, MSG_PEEK | MSG_DONTWAIT);

也可使用如下方法

1
2
uint_t non_blocking = true;
ioctlsocket(sock, FIONBIO, &non_blocking);

如果某个套接字的FIONBIO属性设置为true那么被意味着将此套接字设置为非阻塞模式,反之则为阻塞模式。

JSON解析相关

一、JSON简介

JSON (JavaScript Object Notation, JS 对象简谱) 是一种轻量级的数据交换格式。它基于 ECMAScript (欧洲计算机协会制定的js规范)的一个子集,采用完全独立于编程语言的文本格式来存储和表示数据。

1.1 JSON 语法规则

在 JS 语言中,一切都是对象。 因此,任何支持的类型都可以通过 JSON 来表示,例如字符串、数字、对象、数组等。但是对象和数组是比较特殊且常用的两种类型:

● 对象表示为键值对
● 数据由逗号分隔
● 花括号保存对象
● 方括号保存数组

1.2 JSON 键/值对

JSON 键值对是用来保存 JS 对象的一种方式,键/值对组合中的键名写在前面并用双引号 “” 包裹,使用冒号 : 分隔,然后紧接着值:

1
{"firstName": "Json"}

二、添加cJSON

文件位于 esp-idf\components\json\cJSON
使用时包含头文件 cJSON.h

1
#include "cJSON.h"

三、生成JSON数据

流程:创建JSON结构体 –> 添加数据 –> 释放内存

3.1 创建JSON结构体

1
2
cJSON *pRoot = cJSON_CreateObject();                         // 创建JSON根部结构体
cJSON *pValue = cJSON_CreateObject(); // 创建JSON子叶结构体

3.2 添加字符串类型数据

1
2
3
cJSON_AddStringToObject(pRoot,"mac","65:c6:3a:b2:33:c8");    // 添加字符串类型数据到根部结构体
cJSON_AddItemToObject(pRoot, "value",pValue);
cJSON_AddStringToObject(pValue,"day","Sunday"); // 添加字符串类型数据到子叶结构体

3.3 添加整型数据

1
cJSON_AddNumberToObject(pRoot,"number",2);                   // 添加整型数据到根部结构体

3.4 添加数组类型数据

3.4.1 整型数组
1
2
3
int hex[5]={51,15,63,22,96};
cJSON *pHex = cJSON_CreateIntArray(hex,5); // 创建整型数组类型结构体
cJSON_AddItemToObject(pRoot,"hex",pHex); // 添加整型数组到数组类型结构体
3.4.2 JSON对象数组
1
2
3
4
5
cJSON * pArray = cJSON_CreateArray();                        // 创建数组类型结构体
cJSON_AddItemToObject(pRoot,"info",pArray); // 添加数组到根部结构体
cJSON * pArray_relay = cJSON_CreateObject(); // 创建JSON子叶结构体
cJSON_AddItemToArray(pArray,pArray_relay); // 添加子叶结构体到数组结构体
cJSON_AddStringToObject(pArray_relay, "relay", "on"); // 添加字符串类型数据到子叶结构体

3.5 格式化JSON对象

1
2
char *sendData == cJSON_Print(pRoot);                        // 从cJSON对象中获取有格式的JSON对象
os_printf("data:%s\\n", sendData); // 打印数据

生成JSON格式:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
{
"mac": "65:c6:3a:b2:33:c8",
"value":
{
"day": "Sunday"
},
"number": 2,
"hex": [51,15,63,22,96],
"info":
[
{
"relay": "on"
}
]
}

3.6 释放内存

1
2
cJSON_free((void *) sendData);                             // 释放cJSON_Print ()分配出来的内存空间
cJSON_Delete(pRoot); // 释放cJSON_CreateObject ()分配出来的内存空间

这里说明一下,我们前面调用了3次cJSON_CreateObject (),最后只需要针对root调用一次释放即可,因为后面创建的对象也是挂接在root上的。

四、解析JSON数据

流程:判断JSON格式 –> 解析数据 –> 释放内存

4.1 判断是否JSON格式

1
2
3
4
5
6
7
8
// receiveData是要剖析的数据
//首先整体判断是否为一个json格式的数据
cJSON *pJsonRoot = cJSON_Parse(receiveData);
//如果是否json格式数据
if (pJsonRoot !=NULL)
{
···
}

4.2 解析字符串类型数据

1
2
3
4
5
6
7
8
9
10
char bssid[23] = {0};
cJSON *pMacAdress = cJSON_GetObjectItem(pJsonRoot, "mac"); // 解析mac字段字符串内容
if (!pMacAdress) return; // 判断mac字段是否json格式
else
{
if (cJSON_IsString(pMacAdress)) // 判断mac字段是否string类型
{
strcpy(bssid, pMacAdress->valuestring); // 拷贝内容到字符串数组
}
}

4.3 解析子叶结构体

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
char strDay[23] = {0};
cJSON *pValue = cJSON_GetObjectItem(pJsonRoot, "value"); // 解析value字段内容
if (!pValue) return; // 判断value字段是否json格式
else
{
cJSON *pDay = cJSON_GetObjectItem(pValue, "day"); // 解析子节点pValue的day字段字符串内容
if (!pDay) return; // 判断day字段是否json格式
else
{
if (cJSON_IsString(pDay)) // 判断day字段是否string类型
{
strcpy(strDay, pDay->valuestring); // 拷贝内容到字符串数组
}
}
}

4.4 解析整型数组数据

1
2
3
4
5
6
7
8
9
10
11
cJSON *pArry = cJSON_GetObjectItem(pJsonRoot, "hex");        // 解析hex字段数组内容
if (!pArry) return; // 判断hex字段是否json格式
else
{
int arryLength = cJSON_GetArraySize(pArry); // 获取数组长度
int i;
for (i = 0; i < arryLength; i++)
{ // 打印数组内容
os_printf("cJSON_GetArrayItem(pArry, %d)= %d\\n",i,cJSON_GetArrayItem(pArry, i)->valueint);
}
}

4.5 解析JSON对象数组数据

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
cJSON *pArryInfo = cJSON_GetObjectItem(pJsonRoot, "info");   // 解析info字段数组内容
cJSON *pInfoItem = NULL;
cJSON *pInfoObj = NULL;
char strRelay[23] = {0};
if (!pArryInfo) return; // 判断info字段是否json格式
else
{
int arryLength = cJSON_GetArraySize(pArryInfo); // 获取数组长度
int i;
for (i = 0; i < arryLength; i++)
{
pInfoItem = cJSON_GetArrayItem(pArryInfo, i); // 获取数组中JSON对象
if(NULL != pInfoItem)
{
pInfoObj = cJSON_GetObjectItem(pInfoItem,"relay");// 解析relay字段字符串内容
if(pInfoObj)
{
strcpy(strRelay, pInfoObj->valuestring); // 拷贝内容到字符串数组
}
}
}
}

解析JSON数据:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
{
"mac": "65:c6:3a:b2:33:c8",
"value":
{
"day": "Sunday"
},
"number": 2,
"hex": [51,15,63,22,96],
"info":
[
{
"relay": "on"
}
]
}

4.6 释放内存

1
cJSON_Delete(pJsonRoot);                                      // 释放cJSON_Parse()分配出来的内存空间

五、常用库函数

  1. 从给定的JSON字符串中得到cJSON对象
    cJSON *cJSON_Parse(const char *value)
  2. 从cJSON对象中获取有格式的JSON对象
    char *cJSON_Print(cJSON *item)
  3. 删除cJSON对象,释放链表占用的内存空间
    void cJSON_Delete(cJSON *c)
  4. 获取cJSON对象数组成员的个数
    int cJSON_GetArraySize(cJSON *array)
  5. 根据下标获取cJSON对象数组中的对象
    cJSON *cJSON_GetArrayItem(cJSON*array,int item)
  6. 根据键获取对应的值(cJSON对象)
    cJSON *cJSON_GetObjectItem(cJSON*object,const char *string)
  7. 新增一个字符串类型字段到JSON格式的数据
    cJSON_AddStringToObject(object,name,s)
  8. 新增一个新的子节点cJSON到根节点
    void cJSON_AddItemToObject(cJSON *object,const char *string,cJSON *item)

参考资料

ESP32学习笔记(12)——JSON接口使用 (136.la)

ESP32 线程创建和监控

vTaskList()

使用 ESP32/ESP8266 进行开发时,读者可通过 vTaskList() 来协助分析操作系统当前 task 状态,以帮助优化内存,帮助定位栈溢出问题,帮助理解和学习操作系统原理相关知识。

读者若想深入了解 vTaskList(), 可参考 vTaskList() 英文原版介绍 相关文档。

注意: 使用 vTaskList() 前需使能:

1
2
3
4
5
6
make menuconfig -> Component config -> FreeRTOS -> Enable FreeRTOS trace facility
make menuconfig -> Component config -> FreeRTOS -> Enable FreeRTOS trace facility -> Enable FreeRTOS stats formatting functions
make menuconfig -> Component config -> FreeRTOS -> Enable FreeRTOS trace facility -> Enable FreeRTOS stats formatting functions -> ESP32 Enable display of xCoreID in vTaskList

通过上面配置,等同于使能 FreeRTOSConfig.h 中如下两个宏:
configUSE_TRACE_FACILITY 和 configUSE_STATS_FORMATTING_FUNCTIONS

参考代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
#include <stdio.h>
#include <stdlib.h>
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "esp_system.h"
void esp_print_tasks(void)
{
char *pbuffer = (char *)calloc(1, 2048);
printf("--------------- heap:%u ---------------------\r\n", esp_get_free_heap_size());
vTaskList(pbuffer);
printf("%s", pbuffer);
printf("----------------------------------------------\r\n");
free(pbuffer);
}

void test_task(void *param)
{
while(1) {
esp_print_tasks();
vTaskDelay(3000 / portTICK_RATE_MS);
}
}

打印一次 task 情况可调用:
esp_print_tasks();

循环打印 task 情况可调用:
xTaskCreate(test_task, “test_task”, 2048, NULL, 5, NULL);
读者可以去 task.c 中查看 vTaskList() 相关实现逻辑,或更有助于读者理解。

输出结果分析

vTaskList() 输出结果如下,详细信息解释如下:

image-20220920141555367

  • 第一列:task name
    即 xTaskCreate 创建该 task 时第二个参数。

    如果名称过长,会根据 configMAX_TASK_NAME_LEN 截断。

  • 第二列:task 当前状态

    • X: running
    • B: blocked
    • R: ready
    • D: deleted
    • S: suspended
  • 第三列:task 优先级
    即 xTaskCreate 创建该 task 时第四个参数。
    数字越大,优先级越高,建议客户设置 task 优先级在 1-9 之间,慎行!

  • 第四列:最小剩余 task 栈空间,字节为单位
    在 xTaskCreate 创建 task 时,给定的第三个参数值代表该 task 调度和运行过程中,最大可用 task 栈空间,以字节为单位(读者可以修改 portSTACK_TYPE 宏来决定是否以字节还是4字节为单位);
    Task 如果 API 调用比较深,则使用的栈空间越大,也就意味着最小剩余 task 栈空间越小。

  • 第五列:task 创建顺序

  • 第六列:分配的核心ID

    0,1代表指定分配到哪个核心

    -1 没有指定要分配到哪个核心运行

优化调整建议

  • 当某个 task 最小剩余 task 栈空间比较大时,适当减小 xTaskCreate 创建该 task 时给定的第三个参数值,可节约 DRAM,以优化系统内存。
  • 当某个 task 最小剩余 task 栈空间比较小时,适当增大 xTaskCreate 创建该 task 时给定的第三个参数值,可降低 task 栈溢出风险。
  • 如果对 SDK 没有深入了解,不要修改 系统 task 优先级和分配的最大可用栈空间。
  • 不要在系统 task 的 callback 函数里,添加过多代码,不要添加阻塞操作。
    因为系统 task 通常都是经过优化配置的,如果代码深度较大,容易造成 task 栈溢出;
    如果有阻塞操作,将导致该系统 task 接下来逻辑无法执行,甚至有死锁的可能。
    例如:
    sniffer 的 callback 函数: wifi_promiscuous_cb_t cb
    WiFi callback 函数: system_event_cb_t cb
  • 减少应用代码 task 的个数,以优化内存。
    例如:
    多个 socket 数据流,可以通过 select() 放在同一个 task 里处理;
    而不是一个 socket 数据流,一个 task;
    更不要一个 socket 数据流,居然三个 task (接收 task, 发送 task, 处理 task), it is amazing!
  • 占用空间较大的变量,尽可能通过 malloc/calloc 等动态申请释放,以提高栈空间利用率。

参考资料

FreeRTOS 接口: vTaskList() - 可优化内存和 task 栈溢出定位_乐鑫科技 Espressif的博客-CSDN博客