FreeRTOS内核详解—-Queue 1

来源:互联网 发布:2017年流行什么网络语? 编辑:程序博客网 时间:2024/06/07 06:28

FreeRTOS内核详解—-Queue


队列的主要作用是任务间或者任务与中断之间的通信或者同步,对于FreeRTOS内核而言,队列的主要用在信号量的实现上。因为供外部使用的函数基本都是对几个特定函数,使用宏进行包装实现的,这里只讲述主要的函数。同时FreeRTOS给每个队列基本操作函数提供了一个可选简化版本,简化版本中所有的操作都需要关中断,以此来达到简化操作,提高速度的目的。简化版本的函数中带有Alt,例如 QueueGenericSend 简化版本 QueueAltGenericSend。这里只讲述通用版本的函数。

  • 队列的结构
  • 队列的创建
  • 向队列中添加数据
  • 从队列中获取数据
  • 队列的锁定与解锁
  • 中断中队列数据的处理
  • 队列集

1.队列的结构

typedef struct QueueDefinition{    signed char *pcHead;        // 指向队列存储区的起始位置    signed char *pcTail;        // 指向队列存储区的结尾位置    signed char *pcWriteTo;     //指向队列下次写数据的位置    signed char *pcReadFrom;    //指向队列上次读数据的位置    xList xTasksWaitingToSend;  //等待向队列发送数据的任务链表(简称等待发送链表)    xList xTasksWaitingToReceive;//等待从队列接收数据的任务链表(简称等待接收链表)    volatile unsigned portBASE_TYPE uxMessagesWaiting;//当前队列中的数据项个数    unsigned portBASE_TYPE uxLength;//数据项个数 数据项可以是结构体    unsigned portBASE_TYPE uxItemSize;//数据项大小    volatile signed portBASE_TYPE xRxLock;//存储当队列LOCK时,接收到队列的数据项个数    volatile signed portBASE_TYPE xTxLock;//存储当队列LOCK时,发送到队列的数据项个数    #if ( configUSE_TRACE_FACILITY == 1 )        unsigned char ucQueueNumber;        unsigned char ucQueueType;    #endif    #if ( configUSE_QUEUE_SETS == 1 )        struct QueueDefinition *pxQueueSetContainer;    #endif} xQUEUE;
  • 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

xTasksWaitingToSend:如果队列存储区中尚有空间时,向队列中加入数据,可以立刻存入,但是如果队列已经满了,而任务还想向队列中存储数据,就会将队列挂接到 xTasksWaitingToSend上(前提是任务愿意等待),然后等待队列有空间时再将数据添加到队列中。

xTasksWaitingToReceive:如果队列存储区有数据时,从队列中读取数据,可以立刻获取,但是如果队列为空,而任务还想从队列中获取数据,就会将队列挂接到 xTasksWaitingToReceive上(前提是任务愿意等待),然后等待队列有数据时再将从队列中获取数据。

同时请注意 pcWriteTo 和 pcReadFrom 所指位置的差异。

2.队列的创建

在讲述队列的创建之前,先讲述一个队列复位函数

portBASE_TYPE xQueueGenericReset( xQueueHandle xQueue, portBASE_TYPE xNewQueue ){xQUEUE *pxQueue;    pxQueue = ( xQUEUE * ) xQueue;    configASSERT( pxQueue );    taskENTER_CRITICAL();    {        pxQueue->pcTail = pxQueue->pcHead + ( pxQueue->uxLength * pxQueue->uxItemSize );        pxQueue->uxMessagesWaiting = ( unsigned portBASE_TYPE ) 0U;        pxQueue->pcWriteTo = pxQueue->pcHead;        pxQueue->pcReadFrom = pxQueue->pcHead + ( ( pxQueue->uxLength - ( unsigned portBASE_TYPE ) 1U ) * pxQueue->uxItemSize );        pxQueue->xRxLock = queueUNLOCKED;        pxQueue->xTxLock = queueUNLOCKED;        if( xNewQueue == pdFALSE )        {    //如果不是新创建的队列,需要更新队列            if( listLIST_IS_EMPTY( &( pxQueue->xTasksWaitingToSend ) ) == pdFALSE )            {                //如果之前有任务等待向队列中发送数据,现在队列有空间了,将任务从等待队列中移除 并进行一次调度                if( xTaskRemoveFromEventList( &( pxQueue->xTasksWaitingToSend ) ) == pdTRUE )                {                    portYIELD_WITHIN_API();                }            }        }        else        {            // 初始化队列            vListInitialise( &( pxQueue->xTasksWaitingToSend ) );            vListInitialise( &( pxQueue->xTasksWaitingToReceive ) );        }    }    taskEXIT_CRITICAL();    //为了兼容之前版本    return pdPASS;}
  • 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

此函数的两个参数,第一个是需要复位的队列,第二个参数是当前队列是否是新队列,如果是新队列则需要初始化队列中的链表。如果是对已有队列进行复位则需要检查是否有任务正在等待向队列中发送数据,如果有则将将任务从等待链表中移除,同时进行任务调度。

xQueueHandle xQueueGenericCreate( unsigned portBASE_TYPE uxQueueLength, unsigned portBASE_TYPE uxItemSize, unsigned char ucQueueType ){    xQUEUE *pxNewQueue;    size_t xQueueSizeInBytes;    xQueueHandle xReturn = NULL;    ( void ) ucQueueType;// 防止编译器报警 有些编译器函数参数没有使用会报警    /* 判断队列项的个数,因为有些队列不需要数据空间只需要记录当前队列中的队列项个数,    例如二值信号量*/    if( uxQueueLength > ( unsigned portBASE_TYPE ) 0 )    {        pxNewQueue = ( xQUEUE * ) pvPortMalloc( sizeof( xQUEUE ) );//队列结构体存储空间的动态申请        if( pxNewQueue != NULL )        {            //队列数据存储空间的大小计算 比正常存储空间大1个字节 //原因make wrap checking easier/faster.            xQueueSizeInBytes = ( size_t ) ( uxQueueLength * uxItemSize ) + ( size_t ) 1;            //给队列数据动态申请存储空间            pxNewQueue->pcHead = ( signed char * ) pvPortMalloc( xQueueSizeInBytes );            if( pxNewQueue->pcHead != NULL )            {                pxNewQueue->uxLength = uxQueueLength;       //初始化队列数据项的个数                pxNewQueue->uxItemSize = uxItemSize;        //初始化队列数据项的大小                xQueueGenericReset( pxNewQueue, pdTRUE );   //复位队列                #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 );                xReturn = pxNewQueue;            }            else            {                traceQUEUE_CREATE_FAILED( ucQueueType );                vPortFree( pxNewQueue );    //给队列数据区申请空间失败,释放队列结构体所占空间            }        }    }    configASSERT( xReturn );    return xReturn;}
  • 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

此函数包含3个参数: 
uxQueueLength : 队列项的个数 
uxItemSize : 队列项的尺寸 
ucQueueType : 队列的类型,没有用到

FreeRTOS的队列不止可以插入基本普通数据类型的数据(例如int char),还可以存储高级数据类型数据(例如:结构体、队列指针)。 
队列创建过程中既需要给存储队列的数据区申请空间 uxQueueLength * uxItemSize +1),还需要给队列结构体本身申请空间,如果空间申请成功,则返回队列指针,如果不成功,则返回 NULL 。 
创建和完成初始化的队列如下图:

这里写图片描述

3.向队列中添加数据

将数据添加到队列的代码如下:

static void prvCopyDataToQueue( xQUEUE *pxQueue, const void *pvItemToQueue, portBASE_TYPE xPosition ){    if( pxQueue->uxItemSize == ( unsigned portBASE_TYPE ) 0 )    {   //二值信号量 计数信号量等uxItemSize值为0        #if ( configUSE_MUTEXES == 1 )        {            if( pxQueue->uxQueueType == queueQUEUE_IS_MUTEX )            {                /* The mutex is no longer being held. */                vTaskPriorityDisinherit( ( void * ) pxQueue->pxMutexHolder );                pxQueue->pxMutexHolder = NULL;            }        }        #endif    }    else if( xPosition == queueSEND_TO_BACK )    {   //插入队列的结尾        memcpy( ( void * ) pxQueue->pcWriteTo, pvItemToQueue, ( unsigned ) pxQueue->uxItemSize );        pxQueue->pcWriteTo += pxQueue->uxItemSize;  //更新下次写数据的位置指针        if( pxQueue->pcWriteTo >= pxQueue->pcTail )        {            pxQueue->pcWriteTo = pxQueue->pcHead;   //如果写到了队列结尾则从头开始写        }    }    else    {   //插入到队列开头        memcpy( ( void * ) pxQueue->pcReadFrom, pvItemToQueue, ( unsigned ) pxQueue->uxItemSize );        pxQueue->pcReadFrom -= pxQueue->uxItemSize; //更新上次读数据位置        if( pxQueue->pcReadFrom < pxQueue->pcHead )             {            pxQueue->pcReadFrom = ( pxQueue->pcTail - pxQueue->uxItemSize );//如果读到了队列结尾则从头开始读        }    }    ++( pxQueue->uxMessagesWaiting );   //更新当前队列数据个数}
  • 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

此函数包含3个参数: 
pxQueue : 拷入数据的目的队列 
pvItemToQueue : 需要拷入队列的数据 
xPosition : 插入队列的位置 结尾或者开头。

上面函数中用到一个函数 memcpy ,作用是拷贝数据,它的第一个参数是目的存储区,这里我们就可以理解为什么 pcWriteTo 要指向下次数据写入位置了,因为当写入数据到队列结尾时, pcWriteTo 直接带入函数就可以。当需要插入队列Head位置时,就是插入到上次读取数据的位置 pcReadFrom 。这样就会方便很多。 
下图展示了向队列开头和结尾添加数据的不同: 
插入数据头部(插入前) 
这里写图片描述

插入数据头部(插入后) 
这里写图片描述

插入数据尾部(插入前) 
这里写图片描述

插入数据尾部(插入后) 
这里写图片描述

向队列中添加数据不只是向数据区拷贝数据这样简单,还需要考虑队列满时,该如何添加数据,还要考虑如果有任务正在等待从队列中读取数据等情况,FreeRTOS使用下面的函数来处理向队列中添加数据过程中牵扯到的问题,如下:

signed portBASE_TYPE xQueueGenericSend( xQueueHandle xQueue, const void * const pvItemToQueue, portTickType xTicksToWait, portBASE_TYPE xCopyPosition ){    signed portBASE_TYPE xEntryTimeSet = pdFALSE;    xTimeOutType xTimeOut;    xQUEUE *pxQueue;    pxQueue = ( xQUEUE * ) xQueue;    configASSERT( pxQueue );    configASSERT( !( ( pvItemToQueue == NULL ) && ( pxQueue->uxItemSize != ( unsigned portBASE_TYPE ) 0U ) ) );    for( ;; )    {        taskENTER_CRITICAL();        {            if( pxQueue->uxMessagesWaiting < pxQueue->uxLength )            {   //队列中还有空间                traceQUEUE_SEND( pxQueue );                //将数据拷贝到队列中                prvCopyDataToQueue( pxQueue, pvItemToQueue, xCopyPosition );                #if ( configUSE_QUEUE_SETS == 1 )                {                    if( pxQueue->pxQueueSetContainer != NULL )                    {                        if( prvNotifyQueueSetContainer( pxQueue, xCopyPosition ) == pdTRUE )                        {                            /* The queue is a member of a queue set, and posting                            to the queue set caused a higher priority task to                            unblock. A context switch is required. */                            portYIELD_WITHIN_API();                        }                    }                    else                    {                        if( listLIST_IS_EMPTY( &( pxQueue->xTasksWaitingToReceive ) ) == pdFALSE )                        {   //如果有队列正在等待数据                            if( xTaskRemoveFromEventList( &( pxQueue->xTasksWaitingToReceive ) ) == pdTRUE )                            {   //如果等待接收数据任务优先级高于当前任务则进行调度                                portYIELD_WITHIN_API();                            }                        }                    }                }                #else /* configUSE_QUEUE_SETS */                {                    if( listLIST_IS_EMPTY( &( pxQueue->xTasksWaitingToReceive ) ) == pdFALSE )                    {//如果有队列正在等待数据                        if( xTaskRemoveFromEventList( &( pxQueue->xTasksWaitingToReceive ) ) == pdTRUE )                        {                            //如果等待接收数据任务优先级高于当前任务则进行调度                            portYIELD_WITHIN_API();                        }                    }                }                #endif /* configUSE_QUEUE_SETS */                taskEXIT_CRITICAL();                // 退出循环 继续当前任务执行                return pdPASS;            }            else            {                if( xTicksToWait == ( portTickType ) 0 )                {   //不进行等待 直接返回加入队列错误 继续当前任务执行                    taskEXIT_CRITICAL();                    traceQUEUE_SEND_FAILED( pxQueue );                    return errQUEUE_FULL;                }                else if( xEntryTimeSet == pdFALSE )                {   //记录申请加入数据的时间                    vTaskSetTimeOutState( &xTimeOut );                    xEntryTimeSet = pdTRUE;                }            }        }        taskEXIT_CRITICAL();        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            {                //再次尝试将数据加入队列                prvUnlockQueue( pxQueue );                ( void ) xTaskResumeAll();            }        }        else        {            //阻塞时间到            prvUnlockQueue( pxQueue );            ( void ) xTaskResumeAll();            traceQUEUE_SEND_FAILED( pxQueue );            return errQUEUE_FULL;        }    }}
  • 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

这个函数的总体目的就是如果队列有空间,将数据加入队列,如果没有空间且有阻塞时间,则加入等待发送链表。数据直接加入队列看一下注释就不讲述了,这里解释一下如果队列满了之后,向队列添加数据的处理方法。

我们以一个向满列中添加数据,解释一下上述过程:

  • 调用下面函数函数 vTaskSetTimeOutState( &xTimeOut ) 记录尝试向队列加入数据的时间TICK,代码如下:
    else if( xEntryTimeSet == pdFALSE )    {   //记录申请加入数据的时间         vTaskSetTimeOutState( &xTimeOut );         xEntryTimeSet = pdTRUE;    }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 因为队列是满列,则会调用 vTaskPlaceOnEventList将任务从就绪列表中移除,添加到等待发送列表中,同时会进行一次任务调度,此时此函数不再继续执行。代码如下:

    if( prvIsQueueFull( pxQueue ) != pdFALSE ){    traceBLOCKING_ON_QUEUE_SEND( pxQueue );    //队列已满将任务加入到等待发送数据链表    vTaskPlaceOnEventList( &( pxQueue->xTasksWaitingToSend ), xTicksToWait );    prvUnlockQueue( pxQueue );      //解锁队列    if( xTaskResumeAll() == pdFALSE )//起悬任务挂起    {        portYIELD_WITHIN_API();     }}
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11

    这段代码重新能够得到执行的条件是阻塞时间到或者有其他任务从队列中取出了数据,当前队列不为空,假如阻塞时间到,会执行下面代码返回一个向队列满错误 errQUEUE_FULL 。

    else    {        //阻塞时间到        prvUnlockQueue( pxQueue );        ( void ) xTaskResumeAll();        traceQUEUE_SEND_FAILED( pxQueue );        return errQUEUE_FULL;    }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9

如果是因为队列有数据移除,当前已经不是满列,则执行下面代码。

    else    {        //再次尝试将数据加入队列        prvUnlockQueue( pxQueue );        ( void ) xTaskResumeAll();    }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6

函数会执行一个死循环 for(;;) ,我们可以看到除非阻塞时间到或者将数据成功加入到队列,这个函数会一直执行,于是在上面代码执行完成之后又会从循环开始的地方重新执行,因为此时队列不再是满列,于是数据顺利的通过如下代码添加到队列中,同时退出循环:

if( pxQueue->uxMessagesWaiting < pxQueue->uxLength )            {   //队列中还有空间                traceQUEUE_SEND( pxQueue );                //将数据拷贝到队列中                prvCopyDataToQueue( pxQueue, pvItemToQueue, xCopyPosition );                ...                ...                #else /* configUSE_QUEUE_SETS */                {                    if( listLIST_IS_EMPTY( &( pxQueue->xTasksWaitingToReceive ) ) == pdFALSE )                    {//如果有队列正在等待数据                        if( xTaskRemoveFromEventList( &( pxQueue->xTasksWaitingToReceive ) ) == pdTRUE )                        {                            //如果等待接收数据任务优先级高于当前任务则进行调度                            portYIELD_WITHIN_API();                        }                    }                }                #endif /* configUSE_QUEUE_SETS */                taskEXIT_CRITICAL();                // 退出循环 继续当前任务执行                return pdPASS;            }
  • 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

通过上面代码的分析,我们可以看到,无论是满列还是非满列,数据入列的过程都是通过这个函数执行,只不过,如果队列有空间,则直接添加数据,如果队列没有空间,则先将队列加入一个等待列表,待队列有空间时,在由其他任务或者中断将此任务从等待列表中移除,接着运行,继续向队列中添加数据。

4.从队列中获取数据

signed portBASE_TYPE xQueueGenericReceive( xQueueHandle xQueue, void * const pvBuffer, portTickType xTicksToWait, portBASE_TYPE xJustPeeking ){    signed portBASE_TYPE xEntryTimeSet = pdFALSE;    xTimeOutType xTimeOut;    signed char *pcOriginalReadPosition;    xQUEUE *pxQueue;    pxQueue = ( xQUEUE * ) xQueue;    configASSERT( pxQueue );    configASSERT( !( ( pvBuffer == NULL ) && ( pxQueue->uxItemSize != ( unsigned portBASE_TYPE ) 0U ) ) );    for( ;; )    {        taskENTER_CRITICAL();        {            if( pxQueue->uxMessagesWaiting > ( unsigned portBASE_TYPE ) 0 )            {   // 队列中有数据                pcOriginalReadPosition = pxQueue->pcReadFrom;//记录读数据位置,防止仅仅是读数据,然而并不出列                prvCopyDataFromQueue( pxQueue, pvBuffer );                if( xJustPeeking == pdFALSE )                {   //不是仅仅读数据 需要出列                    traceQUEUE_RECEIVE( pxQueue );                    --( pxQueue->uxMessagesWaiting );//更新数据个数                    #if ( configUSE_MUTEXES == 1 )                    {                        if( pxQueue->uxQueueType == queueQUEUE_IS_MUTEX )                        {                            /* Record the information required to implement                            priority inheritance should it become necessary. */                            pxQueue->pxMutexHolder = xTaskGetCurrentTaskHandle();                        }                    }                    #endif                    if( listLIST_IS_EMPTY( &( pxQueue->xTasksWaitingToSend ) ) == pdFALSE )                    {   // 有任务等待向队列中发送数据 则将任务从事件链表中移除                        if( xTaskRemoveFromEventList( &( pxQueue->xTasksWaitingToSend ) ) == pdTRUE )                        {                            portYIELD_WITHIN_API();                        }                    }                }                else                {                    traceQUEUE_PEEK( pxQueue );                    // 因为是只读数据 所以还要还原读数据位置指针                    pxQueue->pcReadFrom = pcOriginalReadPosition;                    // 因为当前队列中有数据 如果有任务等待从队列读取数据 将任务从队列中移除                    if( listLIST_IS_EMPTY( &( pxQueue->xTasksWaitingToReceive ) ) == pdFALSE )                    {                        if( xTaskRemoveFromEventList( &( pxQueue->xTasksWaitingToReceive ) ) != pdFALSE )                        {                            // 调度                            portYIELD_WITHIN_API();                        }                    }                }                taskEXIT_CRITICAL();                return pdPASS;//从队列中获取数据成功,退出函数            }            else            {                if( xTicksToWait == ( portTickType ) 0 )                {/                    taskEXIT_CRITICAL();                    traceQUEUE_RECEIVE_FAILED( pxQueue );                    return errQUEUE_EMPTY;//返回队列空错误,退出函数                }                else if( xEntryTimeSet == pdFALSE )                {                    // 记录申请读取数据时间                    vTaskSetTimeOutState( &xTimeOut );                    xEntryTimeSet = pdTRUE;                }            }        }        taskEXIT_CRITICAL();        vTaskSuspendAll();        prvLockQueue( pxQueue );        if( xTaskCheckForTimeOut( &xTimeOut, &xTicksToWait ) == pdFALSE )        {   // 检查阻塞时间没有到            if( prvIsQueueEmpty( pxQueue ) != pdFALSE )            {                traceBLOCKING_ON_QUEUE_RECEIVE( pxQueue );                #if ( configUSE_MUTEXES == 1 )                {                    if( pxQueue->uxQueueType == queueQUEUE_IS_MUTEX )                    {                        portENTER_CRITICAL();                        {                            vTaskPriorityInherit( ( void * ) pxQueue->pxMutexHolder );                        }                        portEXIT_CRITICAL();                    }                }                #endif                // 队列满 将当前任务放入等待接收链表中                vTaskPlaceOnEventList( &( pxQueue->xTasksWaitingToReceive ), xTicksToWait );                prvUnlockQueue( pxQueue );                if( xTaskResumeAll() == pdFALSE )                {                    portYIELD_WITHIN_API();                }            }            else            {                // 当前队列已经有数据 再次尝试读取                prvUnlockQueue( pxQueue );                ( void ) xTaskResumeAll();            }        }        else        {            prvUnlockQueue( pxQueue );            ( void ) xTaskResumeAll();            traceQUEUE_RECEIVE_FAILED( pxQueue );            return errQUEUE_EMPTY;//阻塞时间到,返回队列空错误,退出函数        }    }}
  • 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

从上面代码可以看到,整体思路与向队列中写数据是一致的,这里就不再讲述了,看上述代码注释即可。 
从队列中获取数据涉及到的出列过程如下图:

读取数据前 
这里写图片描述

读取数据后 
这里写图片描述

5.中断中队列数据的处理

  • 中断中发送数据
signed portBASE_TYPE xQueueGenericSendFromISR( xQueueHandle xQueue, const void * const pvItemToQueue, signed portBASE_TYPE *pxHigherPriorityTaskWoken, portBASE_TYPE xCopyPosition ){signed portBASE_TYPE xReturn;unsigned portBASE_TYPE uxSavedInterruptStatus;xQUEUE *pxQueue;    pxQueue = ( xQUEUE * ) xQueue;    configASSERT( pxQueue );    configASSERT( !( ( pvItemToQueue == NULL ) && ( pxQueue->uxItemSize != ( unsigned portBASE_TYPE ) 0U ) ) );    uxSavedInterruptStatus = portSET_INTERRUPT_MASK_FROM_ISR();//屏蔽更高优先级中断    {        if( pxQueue->uxMessagesWaiting < pxQueue->uxLength )        {//有空间            traceQUEUE_SEND_FROM_ISR( pxQueue );            // 直接将数据放到队列存储区中            prvCopyDataToQueue( pxQueue, pvItemToQueue, xCopyPosition );            if( pxQueue->xTxLock == queueUNLOCKED )            {                #if ( configUSE_QUEUE_SETS == 1 )                {                    if( pxQueue->pxQueueSetContainer != NULL )                    {                        if( prvNotifyQueueSetContainer( pxQueue, xCopyPosition ) == pdTRUE )                        {                            /* The queue is a member of a queue set, and posting                            to the queue set caused a higher priority task to                            unblock.  A context switch is required. */                            if( pxHigherPriorityTaskWoken != NULL )                            {                                *pxHigherPriorityTaskWoken = pdTRUE;                            }                        }                    }                    else                    {                        // 将等待队列数据任务从等待接收链表中移除                        if( listLIST_IS_EMPTY( &( pxQueue->xTasksWaitingToReceive ) ) == pdFALSE )                        {                            if( xTaskRemoveFromEventList( &( pxQueue->xTasksWaitingToReceive ) ) != pdFALSE )                            {                                //记录有更高优先级任务 以便推出之后可以进行调度                                if( pxHigherPriorityTaskWoken != NULL )                                {                                    *pxHigherPriorityTaskWoken = pdTRUE;                                }                            }                        }                    }                }                #else /* configUSE_QUEUE_SETS */                {                    // 将等待队列数据任务从等待接收链表中移除                    if( listLIST_IS_EMPTY( &( pxQueue->xTasksWaitingToReceive ) ) == pdFALSE )                    {                        if( xTaskRemoveFromEventList( &( pxQueue->xTasksWaitingToReceive ) ) != pdFALSE )                        {                            //记录有更高优先级任务 以便推出之后可以进行调度                            if( pxHigherPriorityTaskWoken != NULL )                            {                                *pxHigherPriorityTaskWoken = pdTRUE;                            }                        }                    }                }                #endif /* configUSE_QUEUE_SETS */            }            else            {                //队列存储如果锁住了 更新新加入数据个数                ++( pxQueue->xTxLock );            }            xReturn = pdPASS;        }        else        {            traceQUEUE_SEND_FROM_ISR_FAILED( pxQueue );            xReturn = errQUEUE_FULL;        }    }    portCLEAR_INTERRUPT_MASK_FROM_ISR( uxSavedInterruptStatus );    return xReturn;}
  • 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

从上面代码我们可以看到,与通用添加函数不同,中断中添加数据不能进入阻塞状态,也就是如果队列有空间,则立刻添加进去,如果没有空间,则返回队列满错误。同时如果队列处于发送数据锁定状态,则需要进行记录,以便退出中断后,能够对等待发送链表进行更新。如果队列没有锁定,则立刻更新等待发送链表,但是即使移除的任务比当前任务优先级高,也不会进行立刻调度,而是更新· *pxHigherPriorityTaskWoken = pdTRUE;记录有需要调度, 然后在中断中调用 taskYIELD_YIELD_FROM_ISR() 进行调度。

  • 中断中读取数据
signed portBASE_TYPE xQueueReceiveFromISR( xQueueHandle xQueue, void * const pvBuffer, signed portBASE_TYPE *pxHigherPriorityTaskWoken ){signed portBASE_TYPE xReturn;unsigned portBASE_TYPE uxSavedInterruptStatus;xQUEUE *pxQueue;    pxQueue = ( xQUEUE * ) xQueue;    configASSERT( pxQueue );    configASSERT( !( ( pvBuffer == NULL ) && ( pxQueue->uxItemSize != ( unsigned portBASE_TYPE ) 0U ) ) );    uxSavedInterruptStatus = portSET_INTERRUPT_MASK_FROM_ISR();    {        if( pxQueue->uxMessagesWaiting > ( unsigned portBASE_TYPE ) 0 )        {   //队列中有数据            traceQUEUE_RECEIVE_FROM_ISR( pxQueue );            //从队列中读取数据            prvCopyDataFromQueue( pxQueue, pvBuffer );            --( pxQueue->uxMessagesWaiting );            if( pxQueue->xRxLock == queueUNLOCKED )            {                //没有锁定任务读取 移除等待发送链表上的任务                if( listLIST_IS_EMPTY( &( pxQueue->xTasksWaitingToSend ) ) == pdFALSE )                {                    if( xTaskRemoveFromEventList( &( pxQueue->xTasksWaitingToSend ) ) != pdFALSE )                    {                        //有高优先级的任务 记录 待退出中断后调度                        if( pxHigherPriorityTaskWoken != NULL )                        {                            *pxHigherPriorityTaskWoken = pdTRUE;                        }                    }                }            }            else            {                //记录读取数据的次数                ++( pxQueue->xRxLock );            }            xReturn = pdPASS;        }        else        {            xReturn = pdFAIL;            traceQUEUE_RECEIVE_FROM_ISR_FAILED( pxQueue );        }    }    portCLEAR_INTERRUPT_MASK_FROM_ISR( uxSavedInterruptStatus );    return xReturn;}
  • 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

这个函数与通用函数的差异,与从中断接收数据函数类似,这里就不讲述了。

6.队列的锁定与解锁

  • 队列锁定
 #define prvLockQueue( pxQueue )                                \    taskENTER_CRITICAL();                                   \    {                                                       \        if( ( pxQueue )->xRxLock == queueUNLOCKED )         \        {                                                   \            ( pxQueue )->xRxLock = queueLOCKED_UNMODIFIED;  \        }                                                   \        if( ( pxQueue )->xTxLock == queueUNLOCKED )         \        {                                                   \            ( pxQueue )->xTxLock = queueLOCKED_UNMODIFIED;  \        }                                                   \    }                                                       \    taskEXIT_CRITICAL()
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13

队列锁定使用一个宏来实现,很简单,就是将 xTxLock 和 xRxLock 都设定为锁定状态,也就是值由-1改成0

队列锁定之后,需要使用队列解锁函数来进行,队列锁定期间,可能发生的数据入列和出列事件进行处理。函数如下:

static void prvUnlockQueue( xQUEUE *pxQueue ){    /* THIS FUNCTION MUST BE CALLED WITH THE SCHEDULER SUSPENDED. */    taskENTER_CRITICAL();    {        /* See if data was added to the queue while it was locked. */        while( pxQueue->xTxLock > queueLOCKED_UNMODIFIED )        {            //xTxLock记录了锁定期间入列数据个数            #if ( configUSE_QUEUE_SETS == 1 )            {                if( pxQueue->pxQueueSetContainer != NULL )                {                    if( prvNotifyQueueSetContainer( pxQueue, queueSEND_TO_BACK ) == pdTRUE )                    {                        /* The queue is a member of a queue set, and posting to                        the queue set caused a higher priority task to unblock.                        A context switch is required. */                        vTaskMissedYield();                    }                }                else                {                    //移除等待接收链表的任务                    if( listLIST_IS_EMPTY( &( pxQueue->xTasksWaitingToReceive ) ) == pdFALSE )                    {                        if( xTaskRemoveFromEventList( &( pxQueue->xTasksWaitingToReceive ) ) != pdFALSE )                        {                            //记录有高优先级任务需要调度                            vTaskMissedYield();                        }                    }                    else                    {                        break;                    }                }            }            #else /* configUSE_QUEUE_SETS */            {                //移除等待接收链表上的任务                if( listLIST_IS_EMPTY( &( pxQueue->xTasksWaitingToReceive ) ) == pdFALSE )                {                    if( xTaskRemoveFromEventList( &( pxQueue->xTasksWaitingToReceive ) ) != pdFALSE )                    {                        //记录有高优先级任务需要调度                        vTaskMissedYield();                    }                }                else                {                    break;                }            }            #endif /* configUSE_QUEUE_SETS */            --( pxQueue->xTxLock );        }        pxQueue->xTxLock = queueUNLOCKED;    }    taskEXIT_CRITICAL();    /* Do the same for the Rx lock. */    taskENTER_CRITICAL();    {        while( pxQueue->xRxLock > queueLOCKED_UNMODIFIED )        {            if( listLIST_IS_EMPTY( &( pxQueue->xTasksWaitingToSend ) ) == pdFALSE )            {                //移除等待向队列发送数据的链表上的任务                if( xTaskRemoveFromEventList( &( pxQueue->xTasksWaitingToSend ) ) != pdFALSE )                {                    //记录有高优先级任务需要调度                    vTaskMissedYield();                }                --( pxQueue->xRxLock );            }            else            {                break;            }        }        pxQueue->xRxLock = queueUNLOCKED;    }    taskEXIT_CRITICAL();}
  • 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

解锁队列函数除了去除任务锁定状态外,还有一个任务就是看看是否有中断向队列中发送或者读取数据,如果有,则更新两个链表,并且如果需要调度,更新一个变量 xMissedYield 。从之前的代码我们可以看到,通用的发送和接收数据代码,并不关注队列的锁定,队列锁定是专为中断中发送和接收数据而设的,因此在任务进行操作之前先要挂起任务调度,然后锁定队列,在解除队列锁定之后,必须调用 xTaskResumeAll() 解除任务调度的锁定,同时 xTaskResumeAll() 函数还会根据 xMissedYield 来决定是否需要进行调度。

7.队列集

首先队列集也是一个队列,只是其存储区放置的是队列的指针,队列集的作用是让一个任务可以同时从几个队列中获取数据或者阻塞在其中等待数据。队列集是很少使用,这里也讲述一下。

  • 队列的创建

队列的创建 我们可以看到,队列集与普通队列创建方法是一样的,只是队列是队列指针,注意队列集的数据个数 uxEventQueueLength 必须设置为为所有被包含队列的数据个数总和,这一点后面讲述

xQueueSetHandle xQueueCreateSet( unsigned portBASE_TYPE uxEventQueueLength )    {    xQueueSetHandle pxQueue;        pxQueue = xQueueGenericCreate( uxEventQueueLength, sizeof( xQUEUE * ), queueQUEUE_TYPE_SET );        return pxQueue;    }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 将队列添加到队列集中

向队列集中添加队列 仅仅是将队列的 pxQueueSetContainer 设置为当前队列集,这样后面就可以通过 pxQueueSetContainer 来判断和获取队列集了。

portBASE_TYPE xQueueAddToSet( xQueueSetMemberHandle xQueueOrSemaphore, xQueueSetHandle xQueueSet )    {    portBASE_TYPE xReturn;        if( ( ( xQUEUE * ) xQueueOrSemaphore )->pxQueueSetContainer != NULL )        {            xReturn = pdFAIL;        }        else        {            taskENTER_CRITICAL();            {                ( ( xQUEUE * ) xQueueOrSemaphore )->pxQueueSetContainer = xQueueSet;            }            taskEXIT_CRITICAL();            xReturn = pdPASS;        }        return xReturn;    }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 将队列添加到队列集数据存储区中

并没有一个专门函数处理向队列集中发送数据,而是当任务使用 xQueueGenericSend 向属于队列集中队列发送数据时会调用 prvNotifyQueueSetContainer 函数向队列集发送通知消息、以便将队列放入到队列集存储区中、检查是否有任务阻塞在队列集等待接收数据链表,如果有则移除。我们可以看到如果一个属于队列集的队列每次收到数据都会将其存储到队列集中。我们可能会注意到一个现象,并没有对队列集满情况进行处理进行处理,这就要求我们将队列集长度设置为所有被包含队列的数据长度总和.

static portBASE_TYPE prvNotifyQueueSetContainer( xQUEUE *pxQueue, portBASE_TYPE xCopyPosition )    {    xQUEUE *pxQueueSetContainer = pxQueue->pxQueueSetContainer;    portBASE_TYPE xReturn = pdFALSE;        configASSERT( pxQueueSetContainer );        configASSERT( pxQueueSetContainer->uxMessagesWaiting < pxQueueSetContainer->uxLength );        if( pxQueueSetContainer->uxMessagesWaiting < pxQueueSetContainer->uxLength )        {            traceQUEUE_SEND( pxQueueSetContainer );            // 将当前队列指针复制到队列集数据存储区中            prvCopyDataToQueue( pxQueueSetContainer, &pxQueue, xCopyPosition );            if( listLIST_IS_EMPTY( &( pxQueueSetContainer->xTasksWaitingToReceive ) ) == pdFALSE )            {                if( xTaskRemoveFromEventList( &( pxQueueSetContainer->xTasksWaitingToReceive ) ) != pdFALSE )                {                    //如果有优先级更改的任务从等待接收链表中移除,返回pdTRUE                    xReturn = pdTRUE;                }            }        }        return xReturn;    }
  • 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
  • 获取队列集数据存储区中队列

从队列集中获取一个队列,使用的是通用的接收方法,我们可以看到它返回的是一个队列,而并不返回相应队列的值,也就是还需要使用 xQueueReceive 从返回的队列中读取数据,而且我们不能使用 xQueueReceive从一个属于队列集的队列中直接获取数据 ,必须先调用 xQueueSelectFromSet,然后再调用 xQueueReceive 获取队列中的数据。

xQueueSetMemberHandle xQueueSelectFromSet( xQueueSetHandle xQueueSet, portTickType xBlockTimeTicks ){xQueueSetMemberHandle xReturn = NULL;    xQueueGenericReceive( ( xQueueHandle ) xQueueSet, &xReturn, xBlockTimeTicks, pdFALSE );    return xReturn;}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 队列集的使用流程
1、创建队列集2、将队列加入队列集3、当队列集中的队列有数据存入时,就会将队列存入队列集的数据存储区中,并且更新等待接收链表4、需要从任务集中获取数据的任务调用xQueueSelectFromSet,查看和获取队列集的数据存储区中的队列数据5、再使用xQueueReceive从上步获取的队列中获取数据
  • 队列集的使用例子

例子来自官网,代码如下:

/* Define the lengths of the queues that will be added to the queue set. */#define QUEUE_LENGTH_1      10#define QUEUE_LENGTH_2      10/* Binary semaphores have an effective length of 1. */#define BINARY_SEMAPHORE_LENGTH 1/* Define the size of the item to be held by queue 1 and queue 2 respectively.The values used here are just for demonstration purposes. */#define ITEM_SIZE_QUEUE_1   sizeof( uint32_t )#define ITEM_SIZE_QUEUE_2   sizeof( something_else_t )/* The combined length of the two queues and binary semaphore that will beadded to the queue set. */#define COMBINED_LENGTH ( QUEUE_LENGTH_1 +                          QUEUE_LENGTH_2 +                          BINARY_SEMAPHORE_LENGTH )void vAFunction( void ){static QueueSetHandle_t xQueueSet;QueueHandle_t xQueue1, xQueue2, xSemaphore;QueueSetMemberHandle_t xActivatedMember;uint32_t xReceivedFromQueue1;something_else_t xReceivedFromQueue2;    /* Create the queue set large enough to hold an event for every space in    every queue and semaphore that is to be added to the set. */    xQueueSet = xQueueCreateSet( COMBINED_LENGTH );    /* Create the queues and semaphores that will be contained in the set. */    xQueue1 = xQueueCreate( QUEUE_LENGTH_1, ITEM_SIZE_QUEUE_1 );    xQueue2 = xQueueCreate( QUEUE_LENGTH_2, ITEM_SIZE_QUEUE_2 );    /* Create the semaphore that is being added to the set. */    xSemaphore = xSemaphoreCreateBinary();    /* Check everything was created. */    configASSERT( xQueueSet );    configASSERT( xQueue1 );    configASSERT( xQueue2 );    configASSERT( xSemaphore );    /* Add the queues and semaphores to the set.  Reading from these queues and    semaphore can only be performed after a call to xQueueSelectFromSet() has    returned the queue or semaphore handle from this point on. */    xQueueAddToSet( xQueue1, xQueueSet );    xQueueAddToSet( xQueue2, xQueueSet );    xQueueAddToSet( xSemaphore, xQueueSet );    for( ;; )    {        /* Block to wait for something to be available from the queues or        semaphore that have been added to the set.  Don't block longer than        200ms. */        xActivatedMember = xQueueSelectFromSet( xQueueSet,                                                200 / portTICK_PERIOD_MS );        /* Which set member was selected?  Receives/takes can use a block time        of zero as they are guaranteed to pass because xQueueSelectFromSet()        would not have returned the handle unless something was available. */        if( xActivatedMember == xQueue1 )        {            xQueueReceive( xActivatedMember, &xReceivedFromQueue1, 0 );            vProcessValueFromQueue1( xReceivedFromQueue1 );        }        else if( xActivatedMember == xQueue2 )        {            xQueueReceive( xActivatedMember, &xReceivedFromQueue2, 0 );            vProcessValueFromQueue2( &xReceivedFromQueue2 );        }        else if( xActivatedMember == xSemaphore )        {            /* Take the semaphore to make sure it can be "given" again. */            xSemaphoreTake( xActivatedMember, 0 );            vProcessEventNotifiedBySemaphore();            break;        }        else        {            /* The 200ms block time expired without an RTOS queue or semaphore            being ready to process. */        }    }}
  • 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

 

 
 
FreeRTOS内核详解----LIST
FreeRTOS内核详解----信号
0 0
原创粉丝点击