FreeRTOS 对此提供了 ⼀个叫做“队列”的机制来完成任务与任务、任务与中断之间的消息传递,由于队列⽤来传递消息的,所以也称 为消息队列。
数据存储
数据发送到队列中会导致数据拷贝,也就是将要发送的数据拷贝到队列中,这就意味着在 队列中存储的是数据的原始值,而不是原数据的引用(即只传递数据的指针),这个也叫做值传递。
采用值传递的话虽然会导致数据拷贝,会浪费一点时间,但是一旦将消息发送到队列中原始的数据缓冲区就可以删除掉或者覆写,这样的话这些缓冲区就可以被重复的使用。
FreeRTOS 中使用队列传递消息的话虽然使用的是数据拷贝,但是也可以使用引用来传递消息,可以直接往队列中发送指向这个消息的地址指针。
多任务访问
队列不是属于某个特别指定的任务的,任何任务都可以向队列中发送消息,或者从队列中 提取消息。
出队阻塞
当任务尝试从一个队列中读取消息的时候可以指定一个阻塞时间,这个阻塞时间就是当任 务从队列中读取消息无效的时候任务阻塞的时间。
入队阻塞
入队说的是向队列中发送消息,将消息加入到队列中。和出队阻塞一样,当一个任务向队 列发送消息的话也可以设置阻塞时间。
队列结构体
有一个结构体用于描述队列,叫做 Queue_t,这个结构体在文件 queue.c 中
typedef struct QueueDefinition {
int8_t *pcHead; //指向队列存储区开始地址。
int8_t *pcTail; //指向队列存储区最后一个字节。
int8_t *pcWriteTo; //指向存储区中下一个空闲区域。
union {
int8_t *pcReadFrom; //当用作队列的时候指向最后一个出队的队列项首地址
UBaseType_t uxRecursiveCallCount;//当用作递归互斥量的时候用来记录递归互斥量被
//调用的次数。
} u;
List_t xTasksWaitingToSend; //等待发送任务列表,那些因为队列满导致入队失败而进
//入阻塞态的任务就会挂到此列表上。
List_t xTasksWaitingToReceive; //等待接收任务列表,那些因为队列空导致出队失败而进
//入阻塞态的任务就会挂到此列表上。
volatile UBaseType_t uxMessagesWaiting; //队列中当前队列项数量,也就是消息数
UBaseType_t uxLength; //创建队列时指定的队列长度,也就是队列中最大允许的
//队列项(消息)数量
UBaseType_t uxItemSize; //创建队列时指定的每个队列项(消息)最大长度,单位字节
volatile int8_t cRxLock; //当队列上锁以后用来统计从队列中接收到的队列项数
//量,也就是出队的队列项数量,当队列没有上锁的话此字
//段为 queueUNLOCKED
volatile int8_t cTxLock; //当队列上锁以后用来统计发送到队列中的队列项数量,
//也就是入队的队列项数量,当队列没有上锁的话此字
//段为 queueUNLOCKED
#if( ( configSUPPORT_STATIC_ALLOCATION == 1 ) &&\
( configSUPPORT_DYNAMIC_ALLOCATION == 1 ) )
uint8_t ucStaticallyAllocated;//如果使用静态存储的话此字段设置为 pdTURE。
#endif
#if ( configUSE_QUEUE_SETS == 1 ) //队列集相关宏
struct QueueDefinition *pxQueueSetContainer;
#endif
#if ( configUSE_TRACE_FACILITY == 1 ) //跟踪调试相关宏
UBaseType_t uxQueueNumber;
uint8_t ucQueueType;
#endif
} xQUEUE;
typedef xQUEUE Queue_t;队列创建
在使用队列之前必须先创建队列,有两种创建队列的方法,
一种是静态的,使用函数 xQueueCreateStatic();
另一个是动态的,使用函数 xQueueCreate()。
这两个函数本质上都是宏, 真正完成队列创建的函数是
xQueueGenericCreate() 和 xQueueGenericCreateStatic(),
这两个函数 在文件 queue.c 中有定义
创建函数
函数 xQueueCreate()
/*
uxQueueLength: 要创建的队列的队列长度,这里是队列的项目数。
uxItemSize: 队列中每个项目(消息)的长度,单位为字节
*/
QueueHandle_t xQueueCreate(UBaseType_t uxQueueLength,
UBaseType_t uxItemSize)函数 xQueueCreateStatic()
/*
uxQueueLength: 要创建的队列的队列长度,这里是队列的项目数。
uxItemSize: 队列中每个项目(消息)的长度,单位为字节
pucQueueStorage: 指向队列项目的存储区,也就是消息的存储区,这个存储区需要用户自
行分配。此参数必须指向一个 uint8_t 类型的数组。这个存储区要大于等
于(uxQueueLength * uxItemsSize)字节。
pxQueueBuffer: 此参数指向一个 StaticQueue_t 类型的变量,用来保存队列结构体。
*/
QueueHandle_t xQueueCreateStatic(UBaseType_t uxQueueLength,
UBaseType_t uxItemSize,
uint8_t * pucQueueStorageBuffer,
StaticQueue_t * pxQueueBuffer)函数 xQueueGenericCreate()
/*
uxQueueLength:要创建的队列的队列长度,这里是队列的项目数。
uxItemSize: 队列中每个项目(消息)的长度,单位为字节。
ucQueueType: 队列类型,由于 FreeRTOS 中的信号量等也是通过队列来实现的,创建信号
量的函数最终也是使用此函数的,因此在创建的时候需要指定此队列的用途,
也就是队列类型,一共有六种类型:
queueQUEUE_TYPE_BASE 普通的消息队列
queueQUEUE_TYPE_SET 队列集
queueQUEUE_TYPE_MUTEX 互斥信号量
queueQUEUE_TYPE_COUNTING_SEMAPHORE 计数型信号量
queueQUEUE_TYPE_BINARY_SEMAPHORE 二值信号量
queueQUEUE_TYPE_RECURSIVE_MUTEX 递归互斥信号量
函 数 xQueueCreate() 创建队列的时候此参数默认选择的就是
queueQUEUE_TYPE_BASE。
*/
QueueHandle_t xQueueGenericCreate( const UBaseType_t uxQueueLength,
const UBaseType_t uxItemSize,
const uint8_t ucQueueType )函数 xQueueGenericCreateStatic()
/*
uxQueueLength: 要创建的队列的队列长度,这里是队列的项目数。
uxItemSize: 队列中每个项目(消息)的长度,单位为字节
pucQueueStorage: 指向队列项目的存储区,也就是消息的存储区,这个存储区需要用户自
行分配。此参数必须指向一个 uint8_t 类型的数组。这个存储区要大于等
于(uxQueueLength * uxItemsSize)字节。
pxStaticQueue: 此参数指向一个 StaticQueue_t 类型的变量,用来保存队列结构体。
ucQueueType: 队列类型。
*/
QueueHandle_t xQueueGenericCreateStatic( const UBaseType_t uxQueueLength,
const UBaseType_t uxItemSize,
uint8_t * pucQueueStorage,
StaticQueue_t * pxStaticQueue,
const uint8_t ucQueueType )队列初始化函数
队列创建后会调用初始化函数
static void prvInitialiseNewQueue( const UBaseType_t uxQueueLength, //队列长度
const UBaseType_t uxItemSize, //队列项目长度
uint8_t * pucQueueStorage, //队列项目存储区
const uint8_t ucQueueType, //队列类型
Queue_t * pxNewQueue ) //队列结构体 {
//防止编译器报错
( void ) ucQueueType;
if( uxItemSize == ( UBaseType_t ) 0 ) {
//队列项(消息)长度为 0,说明没有队列存储区,这里将 pcHead 指向队列开始地址
pxNewQueue->pcHead = ( int8_t * ) pxNewQueue;
}
else {
//设置 pcHead 指向队列项存储区首地址
pxNewQueue->pcHead = ( int8_t * ) pucQueueStorage; (1)
}
//初始化队列结构体相关成员变量
pxNewQueue->uxLength = uxQueueLength; (2)
pxNewQueue->uxItemSize = uxItemSize;
( void ) xQueueGenericReset( pxNewQueue, pdTRUE ); (3)
#if ( configUSE_TRACE_FACILITY == 1 ) //跟踪调试相关字段初始化
{
pxNewQueue->ucQueueType = ucQueueType;
}
#endif /* configUSE_TRACE_FACILITY */
#if( configUSE_QUEUE_SETS == 1 ) //队列集相关字段初始化
{
pxNewQueue->pxQueueSetContainer = NULL;
}
#endif /* configUSE_QUEUE_SETS */
traceQUEUE_CREATE( pxNewQueue );
}(1)、队列结构体中的成员变量 pcHead 指向队列存储区中首地址。
(2)、初始化队列结构体中的成员变量 uxQueueLength 和 uxItemSize,这两个成员变量保存 队列的最大队列项目和每个队列项大小。
(3)、调用函数 xQueueGenericReset()复位队列。
队列复位函数
队列初始化函数 prvInitialiseNewQueue()中调用了函数 xQueueGenericReset()来复位队列
BaseType_t xQueueGenericReset( QueueHandle_t xQueue, BaseType_t xNewQueue ) {
Queue_t * const pxQueue = ( Queue_t * ) xQueue;
configASSERT( pxQueue );
taskENTER_CRITICAL();
{
//初始化队列相关成员变量
pxQueue->pcTail = pxQueue->pcHead + ( pxQueue->uxLength * pxQueue->\ (1)
uxItemSize );
pxQueue->uxMessagesWaiting = ( UBaseType_t ) 0U;
pxQueue->pcWriteTo = pxQueue->pcHead;
pxQueue->u.pcReadFrom = pxQueue->pcHead + ( ( pxQueue->uxLength - \
( UBaseType_t ) 1U ) * pxQueue->uxItemSize );
pxQueue->cRxLock = queueUNLOCKED;
pxQueue->cTxLock = queueUNLOCKED;
if( xNewQueue == pdFALSE ) (2)
{
//由于复位队列以后队列依旧是空的,所以对于那些由于出队(从队列中读取消
//息)而阻塞的任务就依旧保持阻塞壮态。但是对于那些由于入队(向队列中发送
//消息)而阻塞的任务就不同了,这些任务要解除阻塞壮态,从队列的相应列表中
//移除。
if( listLIST_IS_EMPTY( &( pxQueue->xTasksWaitingToSend ) ) == pdFALSE )
{
if( xTaskRemoveFromEventList( &( pxQueue->\
xTasksWaitingToSend ) ) != pdFALSE ) {
queueYIELD_IF_USING_PREEMPTION();
} else {
mtCOVERAGE_TEST_MARKER();
}
} else {
mtCOVERAGE_TEST_MARKER();
}
} else {
//初始化队列中的列表
vListInitialise( &( pxQueue->xTasksWaitingToSend ) ); (3)
vListInitialise( &( pxQueue->xTasksWaitingToReceive ) );
}
}
taskEXIT_CRITICAL();
return pdPASS;
}(1)、初始化队列中的相关成员变量。
(2)、根据参数 xNewQueue 确定要复位的队列是否是新创建的队列,如果不是的话还需要 做其他的处理
(3)、初始化队列中的列表 xTasksWaitingToSend 和 xTasksWaitingToReceive。
向队列发送消息
函数 xQueueSend()、xQueueSendToBack()和 xQueueSendToFront()
这三个函数都是用于向队列中发送消息的,这三个函数本质都是宏,其中函数 xQueueSend() 和 xQueueSendToBack()是一样的,都是后向入队,即将新的消息插入到队列的后面。函数 xQueueSendToToFront()是前向入队,即将新消息插入到队列的前面。然而!这三个函数最后都 是调用的同一个函数:xQueueGenericSend()。
/*
xQueue: 队列句柄,指明要向哪个队列发送数据,创建队列成功以后会返回此队列的
队列句柄。
pvItemToQueue:指向要发送的消息,发送时候会将这个消息拷贝到队列中。
xTicksToWait: 阻塞时间,此参数指示当队列满的时候任务进入阻塞态等待队列空闲的最大
时间。如果为 0 的话当队列满的时候就立即返回;当为 portMAX_DELAY 的
话就会一直等待,直到队列有空闲的队列项,也就是死等,但是宏
INCLUDE_vTaskSuspend 必须为 1。
*/
BaseType_t xQueueSend( QueueHandle_t xQueue,
const void * pvItemToQueue,
TickType_t xTicksToWait);
BaseType_t xQueueSendToBack(QueueHandle_t xQueue,
const void* pvItemToQueue,
TickType_t xTicksToWait);
BaseType_t xQueueSendToToFront(QueueHandle_t xQueue,
const void *pvItemToQueue,
TickType_t xTicksToWait);返回值: pdPASS: 向队列发送消息成功!
errQUEUE_FULL: 队列已经满了,消息发送失败。
函数 xQueueOverwrite()
此函数也是用于向队列发送数据的,当队列满了以后会覆写掉旧的数据,不管这个旧数据 有没有被其他任务或中断取走。这个函数常用于向那些长度为 1 的队列发送消息,此函数也是 一个宏,最终调用的也是函数 xQueueGenericSend()
/*
xQueue: 队列句柄,指明要向哪个队列发送数据,创建队列成功以后会返回此队列的
队列句柄。
pvItemToQueue:指向要发送的消息,发送的时候会将这个消息拷贝到队列中。
*/
BaseType_t xQueueOverwrite(QueueHandle_t xQueue,
const void * pvItemToQueue);函数 xQueueGenericSend()
此函数才是真正干活的,上面讲的所有的任务级入队函数最终都是调用的此函数
/*
xQueue: 队列句柄,指明要向哪个队列发送数据,创建队列成功以后会返回此队列的
队列句柄。
pvItemToQueue:指向要发送的消息,发送的过程中会将这个消息拷贝到队列中。
xTicksToWait: 阻塞时间。
xCopyPosition: 入队方式,有三种入队方式:
queueSEND_TO_BACK: 后向入队
queueSEND_TO_FRONT: 前向入队
queueOVERWRITE: 覆写入队。
上面讲解的入队 API 函数就是通过此参数来决定采用哪种入队方式的。
*/
BaseType_t xQueueGenericSend( QueueHandle_t xQueue,
const void * const pvItemToQueue,
TickType_t xTicksToWait,
const BaseType_t xCopyPosition )函数 xQueueSendFromISR()、xQueueSendToBackFromISR()、 xQueueSendToFrontFromISR()
这三个函数也是向队列中发送消息的,这三个函数用于中断服务函数中。这三个函数本质 也宏,其中函数 xQueueSendFromISR ()和 xQueueSendToBackFromISR ()是一样的,都是后向入 队,即将新的消息插入到队列的后面。函数 xQueueSendToFrontFromISR ()是前向入队,即将新 消息插入到队列的前面。这三个函数同样调用同一个函数 xQueueGenericSendFromISR ()。
/*
xQueue: 队列句柄,指明要向哪个队列发送数据,创建队列成功以后会返回此队列的
队列句柄。
pvItemToQueue:指向要发送的消息,发送的时候会将这个消息拷贝到队列中。
pxHigherPriorityTaskWoken: 标记退出此函数以后是否进行任务切换,这个变量的值由这
三个函数来设置的,用户不用进行设置,用户只需要提供一
个变量来保存这个值就行了。当此值为 pdTRUE 的时候在退
出中断服务函数之前一定要进行一次任务切换。
*/
BaseType_t xQueueSendFromISR(QueueHandle_t xQueue,
const void * pvItemToQueue,
BaseType_t * pxHigherPriorityTaskWoken);
BaseType_t xQueueSendToBackFromISR(QueueHandle_t xQueue,
const void * pvItemToQueue,
BaseType_t * pxHigherPriorityTaskWoken);
BaseType_t xQueueSendToFrontFromISR(QueueHandle_t xQueue,
const void * pvItemToQueue,
BaseType_t * pxHigherPriorityTaskWoken);函数 xQueueOverwriteFromISR()
此函数是 xQueueOverwrite()的中断级版本,用在中断服务函数中,在队列满的时候自动覆 写掉旧的数据,此函数也是一个宏,实际调用的也是函数 xQueueGenericSendFromISR(),
BaseType_t xQueueOverwriteFromISR(QueueHandle_t xQueue,
const void * pvItemToQueue,
BaseType_t * pxHigherPriorityTaskWoken);从队列读取消息
函数 xQueueReceive()
此函数用于在任务中从队列中读取一条(请求)消息,读取成功以后就会将队列中的这条数据 删除,此函数的本质是一个宏,真正执行的函数是 xQueueGenericReceive()。此函数在读取消 息的时候是采用拷贝方式的,所以用户需要提供一个 数组或缓冲区 来保存读取到的数据,所读 取的数据长度是创建队列的时候所设定的每个队列项目的长度
/*
xQueue: 队列句柄,指明要读取哪个队列的数据,创建队列成功以后会返回此队列的
队列句柄。
pvBuffer: 保存数据的缓冲区,读取队列的过程中会将读取到的数据拷贝到这个缓冲区
中。
xTicksToWait: 阻塞时间,此参数指示当队列空的时候任务进入阻塞态等待队列有数据的最
大时间。如果为 0 的话当队列空的时候就立即返回;当为 portMAX_DELAY
的 话 就 会 一 直 等 待 , 直 到 队 列 有 数 据 , 也 就 是 死 等 , 但 是 宏
INCLUDE_vTaskSuspend 必须为 1。
*/
BaseType_t xQueueReceive(QueueHandle_t xQueue,
void * pvBuffer,
TickType_t xTicksToWait);函数 xQueuePeek()
此函数用于从队列读取一条(请求)消息,只能用在任务中!此函数在读取成功以后 不会将消息删除,此函数是一个宏,真正执行的函数是 xQueueGenericReceive()。此函数在读取消息的 时候是采用拷贝方式的,所以用户需要提供一个 数组或缓冲区 来保存读取到的数据,所读取的 数据长度是创建队列的时候所设定的每个队列项目的长度
/*
xQueue: 队列句柄,指明要读取哪个队列的数据,创建队列成功以后会返回此队列的
队列句柄。
pvBuffer: 保存数据的缓冲区,读取队列的过程中会将读取到的数据拷贝到这个缓冲区
中。
xTicksToWait: 阻塞时间,此参数指示当队列空的时候任务进入阻塞态等待队列有数据的最
大时间。如果为 0 的话当队列空的时候就立即返回;当为 portMAX_DELAY
的 话 就 会 一 直 等 待 , 直 到 队 列 有 数 据 , 也 就 是 死 等 , 但 是 宏
INCLUDE_vTaskSuspend 必须为 1。
*/
BaseType_t xQueuePeek(QueueHandle_t xQueue,
void * pvBuffer,
TickType_t xTicksToWait);函数 xQueueGenericReceive()
不管是函数 xQueueReceive() 还 是 xQueuePeek() ,最终都是调用的函数 xQueueGenericReceive(),此函数是真正干事的
BaseType_t xQueueGenericReceive(QueueHandle_t xQueue,
void* pvBuffer,
TickType_t xTicksToWait
BaseType_t xJustPeek)函数 xQueueReceiveFromISR()
此函数是 xQueueReceive()的中断版本,用于在中断服务函数中从队列中读取(请求)一条消 息,读取成功以后就会将队列中的这条数据删除。此函数在读取消息的时候是采用拷贝方式的, 所以需要用户提供一个数组或缓冲区来保存读取到的数据,所读取的数据长度是创建队列的时 候所设定的每个队列项目的长度
/*
xQueue: 队列句柄,指明要读取哪个队列的数据,创建队列成功以后会返回此队列的
队列句柄。
pvBuffer: 保存数据的缓冲区,读取队列的过程中会将读取到的数据拷贝到这个缓冲区
中。
pxTaskWoken: 标记退出此函数以后是否进行任务切换,这个变量的值是由函数来设置的,
用户不用进行设置,用户只需要提供一个变量来保存这个值就行了。当此值
为 pdTRUE 的时候在退出中断服务函数之前一定要进行一次任务切换。
*/
BaseType_t xQueueReceiveFromISR(QueueHandle_t xQueue,
void* pvBuffer,
BaseType_t * pxTaskWoken);xQueuePeekFromISR()
此函数是 xQueuePeek()的中断版本,此函数在读取成功以后不会将消息删除
/*
xQueue: 队列句柄,指明要读取哪个队列的数据,创建队列成功以后会返回此队列的
队列句柄。
pvBuffer: 保存数据的缓冲区,读取队列的过程中会将读取到的数据拷贝到这个缓冲区
中。
*/
BaseType_t xQueuePeekFromISR(QueueHandle_t xQueue,
void * pvBuffer)队列集
如果有多个队列,逻辑需要同时获取队列的数据,这样就需要一个个等待来获取数据,如果队列中没有数据任务就会阻塞,非常浪费资源和时间,所以需要队列集来管理
创建
//队列handle
static QueueHandle_t xQueueHandle1;
static QueueHandle_t xQueueHandle2;
//队列集的handle
QueueSetHandle_t xQueueHandle_set;
//选择读队列集的handle
QueueSetMemberHandle_t xQueueMember_handle;
/************1.创建两个队列*************/
//并且判断是否创建成功
xQueueHandle1=xQueueCreate(2, sizeof(int));
if (!xQueueHandle1) {
printf("create queue1 is error");
}
xQueueHandle2=xQueueCreate(2, sizeof(int));
if (!xQueueHandle2) {
printf("create queue2 is error");
}
/************2.创建队列集*************/
xQueueHandle_set=xQueueCreateSet(4);//传入的参数是队列的长度,就是有几个数据
/*因为队列1有两个数据,队列2有两个数据;队列集的长度=队列1的长度+队列2的长度=4*/
/************3.把队列1和队列2加到队列集中*************/
xQueueAddToSet(xQueueHandle1, xQueueHandle_set);
xQueueAddToSet( xQueueHandle2, xQueueHandle_set);
/*第一个参数是要加入队列集的队列的handle;第二个参数是队列集的handle*/读取
// 数据读取
// 一直等待有数据可读
xQueueMember_handle=xQueueSelectFromSet(xQueueHandle_set,portMAX_DELAY);
// 判断是那个队列有数据可读
if (xQueueMember_handle==xQueueHandle1) {
// 从队列一中接收数据,并打印
// 此时有数据不需要等待,直接是0
xQueueReceive(xQueueHandle1, &date, 0);
}
if(xQueueMember_handle==xQueueHandle2) {
// 从队列一中接收数据,并打印
// 此时有数据不需要等待,直接是0
xQueueReceive(xQueueHandle2, &date, 0);
}任务函数示例
以使用结构体为例
创建数据结构
typedef struct {
uint32_t address;
uint8_t *data;
uint16_t size;
TaskHandle_t callback_task;
} flash_write_cmd_t;
/********************************************************************************
* @brief: 分区读写
********************************************************************************/
static QueueHandle_t flash_write_queue;初始化
// 创建 flash 写入队列
flash_write_queue = xQueueCreate(10, sizeof(flash_write_cmd_t));写入与读取队列
/********************************************************************************
* @brief: 写入配置任务
* @param {config_part_t} block
* @param {void} *data
* @param {size_t} size
* @return {*}
********************************************************************************/
void flash_write_task(void *pvParameters) {
flash_write_cmd_t *cmd;
while (1) {
// 等待写入命令
if (xQueueReceive(flash_write_queue, &cmd, portMAX_DELAY)) {
ESP_LOGI(TAG, "Invalid address or size: address=%ld, size=%d",
cmd->address, cmd->size);
// 扇区索引
uint32_t sector_num =
(cmd->size + CONFIG_SECTOR_SIZE - 1) / CONFIG_SECTOR_SIZE;
if (cmd->address >= config_partition->size || cmd->size == 0 ||
cmd->address + cmd->size > config_partition->size) {
ESP_LOGE(TAG, "Invalid address or size: address=%ld, size=%d",
cmd->address, cmd->size);
// 释放内存
heap_caps_free(cmd->data);
continue;
}
// portDISABLE_INTERRUPTS();
// 擦除目标扇区 (必须擦除后才能写入)
esp_err_t err = esp_partition_erase_range(
config_partition, cmd->address, sector_num * CONFIG_SECTOR_SIZE);
if (err != ESP_OK) {
ESP_LOGE(TAG, "Flash 擦除失败: %s", esp_err_to_name(err));
}
// 执行 Flash 写入
err = esp_partition_write(config_partition, cmd->address, cmd->data,
cmd->size);
if (err != ESP_OK) {
ESP_LOGE(TAG, "Flash 写入失败: %s", esp_err_to_name(err));
}
// 释放内存
heap_caps_free(cmd->data);
}
vTaskDelay(pdMS_TO_TICKS(10));
}
}
/********************************************************************************
* @brief: 发送数据到队列
* @param {flash_write_cmd_t} *cmd
* @return {*}
********************************************************************************/
esp_err_t send_xqueue_data(flash_write_cmd_t *cmd) {
// 发送到写入队列
if (xQueueSend(flash_write_queue, &cmd, pdMS_TO_TICKS(1000)) == pdTRUE) {
// 等待写入完成
return ESP_OK;
} else {
// 处理发送失败
return ESP_ERR_INVALID_STATE;
}
}