目录
1 消息队列的概念和作用
2 应用
2.1功能需求
2.2接口函数API
2.3 功能实现
3 消息队列源码分析
3.1消息队列控制块
3.2消息队列创建
3.3消息队列删除
3.4消息队列在任务中发送
3.5消息队列在中断中发送
3.6消息队列在任务中接收
3.7消息队列在中断中接收
消息队列(queue),可以在任务与任务间、中断和任务间传递消息实现任务接收来自其他任务或中断的不固定长度的消息。后面的二值信号量、互斥信号量等都是基于消息队列衍生出来的。
队列是什么?
解决三个问题:
FreeRTOS程序中的消息队列
中断和任务不断的发送消息
在固定时间内等待(Timeout,相当于osdelay挂起)消息,没有消息的时候把cpu交给其他任务。
“深度”。在队列创建时需要设定其深度和每个单元的大小。
通常情况下,队列被作为 FIFO(先进先出)使用,即数据由队列尾写入,从队列首读出。当然,由队列首写人也是有可能的。
向队列写入数据是通过字节拷贝把数据存储到队列中,从队列读出数据使得把队列中的数据拷贝删除。
在FreeROTS中,操作消息队列控制块,只要对头有消息,就会取,直到去完为止。
一般在调度器开启之前创建
可以发送在队头、队尾,一般先入先出,有紧急消息可以队头插队,常用的是Send和SendtoBack
启动调度器之前是不能调用此函数的,因为是在中断中触发的,在中断中不能阻塞。第三个参数NULL,已经不用了。
CubeMX功能配置
led端口配置、usart1中断配置、创建消息队列
消息队列接收和发送功能开发
串口中断使能
void HAL_UART_MspInit(UART_HandleTypeDef* uartHandle)
{
GPIO_InitTypeDef GPIO_InitStruct = {0};
if(uartHandle->Instance==USART1)
{
/* USER CODE BEGIN USART1_MspInit 0 */
/* USER CODE END USART1_MspInit 0 */
/* USART1 clock enable */
__HAL_RCC_USART1_CLK_ENABLE();
__HAL_RCC_GPIOA_CLK_ENABLE();
/**USART1 GPIO Configuration
PA9 ------> USART1_TX
PA10 ------> USART1_RX
*/
GPIO_InitStruct.Pin = GPIO_PIN_9|GPIO_PIN_10;
GPIO_InitStruct.Mode = GPIO_MODE_AF_PP;
GPIO_InitStruct.Pull = GPIO_PULLUP;
GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_VERY_HIGH;
GPIO_InitStruct.Alternate = GPIO_AF7_USART1;
HAL_GPIO_Init(GPIOA, &GPIO_InitStruct);
/* USART1 interrupt Init */
HAL_NVIC_SetPriority(USART1_IRQn, 5, 0);
HAL_NVIC_EnableIRQ(USART1_IRQn);
/* USER CODE BEGIN USART1_MspInit 1 */
//开启接收中断
__HAL_UART_ENABLE_IT(uartHandle,UART_IT_RXNE);
/* USER CODE END USART1_MspInit 1 */
}
}
串口中断服务函数入队操作
void USART1_IRQHandler(void)
{
uint8_t u8Data;
//接收中断标志位
if(__HAL_UART_GET_FLAG(&huart1,UART_FLAG_RXNE) == SET){
//读取接收寄存器
u8Data = huart1.Instance->DR;
//进行入队操作
xQueueSendFromISR(CmdQueueHandle,&u8Data,NULL);
}
HAL_UART_IRQHandler(&huart1);
}
从消息队列出队一直等待,当接收到第一个消息循环从消息队列出队,阻塞等待50ms,完成消息接收
uint8_t u8CmdBuff[20]; //全局变量
void Usart_Task(void const * argument)
{
uint8_t u8Index;
for(;;)
{
//每次读取消息之前,把索引初始化为0
u8Index = 0;
//1、一直等待接收消息,第一个消息应该放在消息缓冲区的第一个元素上
if(xQueueReceive(CmdQueueHandle,&u8CmdBuff[u8Index++],portMAX_DELAY)==pdPASS){
while(xQueueReceive(CmdQueueHandle,&u8CmdBuff[u8Index++],50)){}
u8CmdBuff[u8Index] = '\0';//保证一包完成字符串信息
vParseString(u8CmdBuff);
//完成解析以后,要清空接收缓冲区
memset(u8CmdBuff,0,20);
}
}
}
消息解析控制功能开发
根据板载LED端口数量循环遍历,openledX字符串进行比较,如果字符串比较成功打开相关led端口
根据板载LED端口数量循环遍历,closeledX字符串进行比较,如果字符串比较成功打开相关led端口
uint8_t *OpenString[LED_NUM] = {
"openled6",
"openled7",
"openled8",
"openled9",
};
uint8_t *CloseString[LED_NUM] = {
"closeled6",
"closeled7",
"closeled8",
"closeled9",
};
void vParseString(uint8_t *buff){
uint8_t i;
for(i=0;i
//queue.h
//队列实现,实际是xQueueGenericCreate实现的
#define xQueueCreate( uxQueueLength, uxItemSize ) xQueueGenericCreate( ( uxQueueLength ), ( uxItemSize ), ( queueQUEUE_TYPE_BASE ) )
//队列的创建类型
#define queueQUEUE_TYPE_BASE ( ( uint8_t ) 0U ) //基本类型
#define queueQUEUE_TYPE_SET ( ( uint8_t ) 0U )
#define queueQUEUE_TYPE_MUTEX ( ( uint8_t ) 1U ) //互斥锁
#define queueQUEUE_TYPE_COUNTING_SEMAPHORE ( ( uint8_t ) 2U ) //计数信号量
#define queueQUEUE_TYPE_BINARY_SEMAPHORE ( ( uint8_t ) 3U ) //二值信号量
#define queueQUEUE_TYPE_RECURSIVE_MUTEX ( ( uint8_t ) 4U ) //递归互斥锁
/*
参数:
uxQueueLength 队列长度
uxItemSize 队列项大小
ucQueueType 队列类型
返回值:
QueueHandle_t 队列的句柄 其实就是队列控制块地址
*/
QueueHandle_t xQueueGenericCreate( const UBaseType_t uxQueueLength, const UBaseType_t uxItemSize, const uint8_t ucQueueType )
{
Queue_t *pxNewQueue;
size_t xQueueSizeInBytes;
uint8_t *pucQueueStorage;
//队列内存空间为空
if( uxItemSize == ( UBaseType_t ) 0 )
{
/*队列字节大小赋值为0 */
xQueueSizeInBytes = ( size_t ) 0;
}
else
{
/* 队列字节大小赋值为 长度*每个队列项大小 */
xQueueSizeInBytes = ( size_t ) ( uxQueueLength * uxItemSize );
}
// 申请内存空间 消息队列控制块大小+消息队列空间大小
pxNewQueue = ( Queue_t * ) pvPortMalloc( sizeof( Queue_t ) + xQueueSizeInBytes );
if( pxNewQueue != NULL )
{
/* 找到消息队列操作空间的首地址 */
pucQueueStorage = ( ( uint8_t * ) pxNewQueue ) + sizeof( Queue_t );
//初始化一个新的消息队列
prvInitialiseNewQueue( uxQueueLength, uxItemSize, pucQueueStorage, ucQueueType, pxNewQueue );
}
return pxNewQueue;
}
/*
参数:
uxQueueLength
uxItemSize
pucQueueStorage:队列操作空间的首地址
ucQueueType:队列类型
pxNewQueue:队列的句柄
*/
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 )//是否有队列空间
{
/* 把队列控制块首地址赋值到队列头指针 ??这是互斥信号使用,后面分析*/
pxNewQueue->pcHead = ( int8_t * ) pxNewQueue;
}
else
{
/* 把队列空间的首地址赋值给队列头指针 */
pxNewQueue->pcHead = ( int8_t * ) pucQueueStorage;
}
/* 长度、单元大小 */
pxNewQueue->uxLength = uxQueueLength;
pxNewQueue->uxItemSize = uxItemSize;
//队列重置函数
( void ) xQueueGenericReset( pxNewQueue, pdTRUE );
}
/*
参数:
xQueue 队列句柄
xNewQueue 操作队列的状态是什么,新建pdTRUE还是已经创建好了
返回值:
*/
BaseType_t xQueueGenericReset( QueueHandle_t xQueue, BaseType_t xNewQueue )
{
Queue_t * const pxQueue = ( Queue_t * ) xQueue;
//进入临界段,这时候操作队列控制块,不允许打断
taskENTER_CRITICAL();
{
/*
1、头地址赋值
2、等待处理的消息个数为0
3、写入指针赋值为队列头指针
4、读出指针写入最后一个可用消息
5、赋值队列锁位解锁状态
*/
pxQueue->pcTail = pxQueue->pcHead + ( pxQueue->uxLength * pxQueue->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 )//不是新任务
{
/* 判断发送等待列表里面是否有任务 */
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//新建队列,直接初始化 发送和接收 列表项
{
/* Ensure the event queues start in the correct state. */
vListInitialise( &( pxQueue->xTasksWaitingToSend ) );
vListInitialise( &( pxQueue->xTasksWaitingToReceive ) );
}
}
taskEXIT_CRITICAL();
/* A value is returned for calling semantic consistency with previous
versions. */
return pdPASS;
}
void vQueueDelete( QueueHandle_t xQueue )
{
Queue_t * const pxQueue = ( Queue_t * ) xQueue;
#if ( configQUEUE_REGISTRY_SIZE > 0 )
{
vQueueUnregisterQueue( pxQueue );
}
#endif
#if( ( configSUPPORT_DYNAMIC_ALLOCATION == 1 ) && ( configSUPPORT_STATIC_ALLOCATION == 0 ) )
{
/* 释放消息队列的内存空间 */
vPortFree( pxQueue );
}
#elif( ( configSUPPORT_DYNAMIC_ALLOCATION == 1 ) && ( configSUPPORT_STATIC_ALLOCATION == 1 ) )
{
/* 释放消息队列的内存空间 */
if( pxQueue->ucStaticallyAllocated == ( uint8_t ) pdFALSE )
{
vPortFree( pxQueue );
}
else
{
mtCOVERAGE_TEST_MARKER();
}
}
#else
{
( void ) pxQueue;
}
#endif /* configSUPPORT_DYNAMIC_ALLOCATION */
}
分支一:
分支二:
问:为什么在进入临界段?
因为操作的消息是共享资源,可以被多个任务或中断接收和发送,那么操作的时候是不希望被别的任务打断的。
问:为什么要挂起调度器?
不让内核调度,不让其他任务打断下面程序的处理。
问:为什么要锁定队列?
因为消息队列是可以从任务中发送的,锁定队列是告诉任务不要打断下面程序的查询队列和插入队列,否则整个程序会打乱。
/*
queueSEND_TO_BACK ?
由于信号量都是有消息队列实现的,这个时候,操作系统,定义了消息队列的类型
类型:
#define queueSEND_TO_BACK ( ( BaseType_t ) 0 ) 1从队尾加入
#define queueSEND_TO_FRONT ( ( BaseType_t ) 1 ) 2从对头加入
#define queueOVERWRITE ( ( BaseType_t ) 2 ) 3覆盖入队
*/
#define xQueueSend( xQueue, pvItemToQueue, xTicksToWait ) xQueueGenericSend( ( xQueue ), ( pvItemToQueue ), ( xTicksToWait ), queueSEND_TO_BACK )
/*
最终实现消息队列发送的是xQueueGenericSend接口
参数:
xQueue 消息队列句柄
pvItemToQueue 要发送的消息的地址
xTicksToWait 超时时间
xCopyPosition 队列操作类型
返回值:BaseTyp_t
*/
BaseType_t xQueueGenericSend( QueueHandle_t xQueue, const void * const pvItemToQueue, TickType_t xTicksToWait, const BaseType_t xCopyPosition )
{
BaseType_t xEntryTimeSet = pdFALSE, xYieldRequired;
TimeOut_t xTimeOut;
Queue_t * const pxQueue = ( Queue_t * ) xQueue;
/*
使用for循环的目的:为了快速处理消息拷贝,消息处理功能
*/
for( ;; )
{
//进入临界段
taskENTER_CRITICAL();
{
/*
1、判断消息队列是否满了
2、判断是否允许覆盖入队
任何一个成立,执行入队操作
*/
if( ( pxQueue->uxMessagesWaiting < pxQueue->uxLength ) || ( xCopyPosition == queueOVERWRITE ) )
{
//拷贝数据到队列操作空间内
xYieldRequired = prvCopyDataToQueue( pxQueue, pvItemToQueue, xCopyPosition );
#if ( configUSE_QUEUE_SETS == 1 )
{
//宏定义不看
}
#else /* configUSE_QUEUE_SETS */
{
/* 判断等待接收的列表是否为空. */
if( listLIST_IS_EMPTY( &( pxQueue->xTasksWaitingToReceive ) ) == pdFALSE )
{
//移除等待接收任务的列表,改变等待接收任务的状态为就绪态
if( xTaskRemoveFromEventList( &( pxQueue->xTasksWaitingToReceive ) ) != pdFALSE )
{
/* 操作成功,进行上下文切换,让任务赶紧处理 */
queueYIELD_IF_USING_PREEMPTION();
}
else
{
mtCOVERAGE_TEST_MARKER();
}
}
//如果在拷贝数据的时候提示需要调度
else if( xYieldRequired != pdFALSE )
{
/* 再次调度,进行上下文切换*/
queueYIELD_IF_USING_PREEMPTION();
}
else
{
mtCOVERAGE_TEST_MARKER();
}
}
#endif /* configUSE_QUEUE_SETS */
//退出临界段,返回成功
taskEXIT_CRITICAL();
return pdPASS;
}
else //如果不允许入队
{
//是否需要阻塞
if( xTicksToWait == ( TickType_t ) 0 )
{
/* 不需要阻塞,退出临界段,之后返回队列队满 */
taskEXIT_CRITICAL();
traceQUEUE_SEND_FAILED( pxQueue );
return errQUEUE_FULL;
}
//超时结构体是否操作过
else if( xEntryTimeSet == pdFALSE )
{
/* 超时结构体初始化 */
vTaskSetTimeOutState( &xTimeOut );
xEntryTimeSet = pdTRUE;
}
else
{
/* Entry time was already set. */
mtCOVERAGE_TEST_MARKER();
}
}
}
//退出临界段
taskEXIT_CRITICAL();
/* 后面的代码都是允许阻塞处理 */
/*
1、挂起了调度器 ----不让其他任务打断
2、队列上锁------不让中断打断 (因为之前已经退出临界段了)
*/
vTaskSuspendAll();
prvLockQueue( pxQueue );
/* 判断阻塞时间是否超时了 */
if( xTaskCheckForTimeOut( &xTimeOut, &xTicksToWait ) == pdFALSE )
{
//判断队列是否满
if( prvIsQueueFull( pxQueue ) != pdFALSE )
{
//队满,把当前任务,添加到等待发送的事件列表中,内部还把任务添加到延时列表中去
traceBLOCKING_ON_QUEUE_SEND( pxQueue );
vTaskPlaceOnEventList( &( pxQueue->xTasksWaitingToSend ), xTicksToWait );
/* 解锁*/
prvUnlockQueue( pxQueue );
/* 恢复调度器 */
if( xTaskResumeAll() == pdFALSE )
{
//进行上下文切换
portYIELD_WITHIN_API();
}
}
else
{
//队列未满 解锁,恢复调度器,重新进行入队操作
/* Try again. */
prvUnlockQueue( pxQueue );
( void ) xTaskResumeAll();
}
}//
//已经超时了,解锁,开始调度器 返回队满
else
{
/* The timeout has expired. */
prvUnlockQueue( pxQueue );
( void ) xTaskResumeAll();
traceQUEUE_SEND_FAILED( pxQueue );
return errQUEUE_FULL;
}
}
}
问:上锁的目的是什么?
如果上锁了,在中断中不会处理,上面3.5在任务中发送的功能。如果中断能打断,那么中断也能操作队列、中断也能操作队列,那么优先级会出现混乱。没有上锁,那么可以操作。
/*
最终调用发送消息接口是xQueueGenericSendFromISR???为什么
由于信号量都是有消息队列实现的,这个时候,操作系统,定义了消息队列的类型
类型:
#define queueSEND_TO_BACK ( ( BaseType_t ) 0 ) 1、从队尾加入
#define queueSEND_TO_FRONT ( ( BaseType_t ) 1 ) 2、从队头加入
#define queueOVERWRITE ( ( BaseType_t ) 2 ) 3、覆盖入队
*/
#define xQueueSendFromISR( xQueue, pvItemToQueue, pxHigherPriorityTaskWoken )
xQueueGenericSendFromISR( ( xQueue ), ( pvItemToQueue ), ( pxHigherPriorityTaskWoken ), queueSEND_TO_BACK )
/*
参数:
xQueue
pvItemToQueue
NULL
queueSEND_TO_BACK
返回值:BaseType_t
*/
BaseType_t xQueueGenericSendFromISR( QueueHandle_t xQueue, const void * const pvItemToQueue, BaseType_t * const pxHigherPriorityTaskWoken, const BaseType_t xCopyPosition )
{
BaseType_t xReturn;
UBaseType_t uxSavedInterruptStatus;
Queue_t * const pxQueue = ( Queue_t * ) xQueue;
/*带返回值的关闭中断,需要保存上次关闭中断的状态值,恢复时候,写入 */
uxSavedInterruptStatus = portSET_INTERRUPT_MASK_FROM_ISR();
{
//队满?覆盖入队?
if( ( pxQueue->uxMessagesWaiting < pxQueue->uxLength ) || ( xCopyPosition == queueOVERWRITE ) )
{
//获取了队列发送锁的状态值
const int8_t cTxLock = pxQueue->cTxLock;
//拷贝数据到队列操作空间内
( void ) prvCopyDataToQueue( pxQueue, pvItemToQueue, xCopyPosition );
/* 判断队列是否上锁 */
/*
#define queueUNLOCKED ( ( int8_t ) -1 ) 解锁状态
#define queueLOCKED_UNMODIFIED ( ( int8_t ) 0 ) 上锁状态初值
*/
if( cTxLock == queueUNLOCKED )
{
#if ( configUSE_QUEUE_SETS == 1 )
#else /* configUSE_QUEUE_SETS */
{
//恢复等待消息任务,中断内没有进行上下文切换,会在开启调度器的时候进行
if( listLIST_IS_EMPTY( &( pxQueue->xTasksWaitingToReceive ) ) == pdFALSE )
{
if( xTaskRemoveFromEventList( &( pxQueue->xTasksWaitingToReceive ) ) != pdFALSE )
{
/* The task waiting has a higher priority so record that a
context switch is required. */
if( pxHigherPriorityTaskWoken != NULL )
{
*pxHigherPriorityTaskWoken = pdTRUE;
}
else
{
mtCOVERAGE_TEST_MARKER();
}
}
else
{
mtCOVERAGE_TEST_MARKER();
}
}
else
{
mtCOVERAGE_TEST_MARKER();
}
}
#endif /* configUSE_QUEUE_SETS */
}
else //队列已经上锁
{
/* 发送锁加一 */
pxQueue->cTxLock = ( int8_t ) ( cTxLock + 1 );
}
//返回成功
xReturn = pdPASS;
}
else
{
//返回队满
traceQUEUE_SEND_FROM_ISR_FAILED( pxQueue );
xReturn = errQUEUE_FULL;
}
}
//开启中断,保存上次状态值
portCLEAR_INTERRUPT_MASK_FROM_ISR( uxSavedInterruptStatus );
return xReturn;
}
//队列上锁
把发送和接受锁都赋值为上锁的初始值
#define prvLockQueue( pxQueue ) \
taskENTER_CRITICAL(); \
{ \
if( ( pxQueue )->cRxLock == queueUNLOCKED ) \
{ \
( pxQueue )->cRxLock = queueLOCKED_UNMODIFIED; \
} \
if( ( pxQueue )->cTxLock == queueUNLOCKED ) \
{ \
( pxQueue )->cTxLock = queueLOCKED_UNMODIFIED; \
} \
} \
taskEXIT_CRITICAL()
/*
队列解锁
参数:消息队列句柄
*/
static void prvUnlockQueue( Queue_t * const pxQueue )
{
/* THIS FUNCTION MUST BE CALLED WITH THE SCHEDULER SUSPENDED. */
/* 进入临界段 */
taskENTER_CRITICAL();
{
//获取发送锁的状态值
int8_t cTxLock = pxQueue->cTxLock;
/* 遍历解锁 直到解锁为止 */
while( cTxLock > queueLOCKED_UNMODIFIED )
{
/* Data was posted while the queue was locked. Are any tasks
blocked waiting for data to become available? */
#if ( configUSE_QUEUE_SETS == 1 )
#else /* configUSE_QUEUE_SETS */
{
/* 解除等待消息任务,进行上下文切换 */
if( listLIST_IS_EMPTY( &( pxQueue->xTasksWaitingToReceive ) ) == pdFALSE )
{
if( xTaskRemoveFromEventList( &( pxQueue->xTasksWaitingToReceive ) ) != pdFALSE )
{
/* The task waiting has a higher priority so record that
a context switch is required. */
vTaskMissedYield();
}
else
{
mtCOVERAGE_TEST_MARKER();
}
}
else
{
break;
}
}
#endif /* configUSE_QUEUE_SETS */
//队列发送锁减一
--cTxLock;
}
//最后,解除发送锁
pxQueue->cTxLock = queueUNLOCKED;
}
//退出临界段
taskEXIT_CRITICAL();
/* Do the same for the Rx lock. 接收锁也是一样的 */
taskENTER_CRITICAL();
{
int8_t cRxLock = pxQueue->cRxLock;
while( cRxLock > queueLOCKED_UNMODIFIED )
{
if( listLIST_IS_EMPTY( &( pxQueue->xTasksWaitingToSend ) ) == pdFALSE )
{
if( xTaskRemoveFromEventList( &( pxQueue->xTasksWaitingToSend ) ) != pdFALSE )
{
vTaskMissedYield();
}
else
{
mtCOVERAGE_TEST_MARKER();
}
--cRxLock;
}
else
{
break;
}
}
pxQueue->cRxLock = queueUNLOCKED;
}
taskEXIT_CRITICAL();
}
分支一:
是出队删除,还是出队不删除,不删除是留个多个任务读取一个消息的情况使用的。
分支二:
接收消息为空,需要判断是否允许阻塞
/*
最终调用发送消息接口是xQueueGenericReceive???为什么
队列出队,有两种模式
一种是:出队后,删除已经读取到队列项或者消息空间
另一种是:出队后,不删除,然后恢复出队记录地址,让其他任务或者中断,继续读取使用
类型:
pdFALSE 删除
pdTRUE 不删除
*/
#define xQueueReceive( xQueue, pvBuffer, xTicksToWait )
xQueueGenericReceive( ( xQueue ), ( pvBuffer ), ( xTicksToWait ), pdFALSE )
BaseType_t xQueueGenericReceive( QueueHandle_t xQueue, void * const pvBuffer, TickType_t xTicksToWait, const BaseType_t xJustPeeking )
//重点分析这一个,其他流程和发送流程差不多
//判断是否删除已经接收到的消息空间
if( xJustPeeking == pdFALSE )
{
traceQUEUE_RECEIVE( pxQueue );
//更新消息等待的记录值,让它减一
pxQueue->uxMessagesWaiting = uxMessagesWaiting - 1;
}
else
{
//不删除 就重新赋值未读取消息之前的地址,到出队指针
pxQueue->u.pcReadFrom = pcOriginalReadPosition;
}
源码分析,参考3.5中断中发送最后一部分。