繼續學習freertos消息隊列

  • 2019 年 10 月 15 日
  • 筆記

寫在前面:傑傑這個月很忙~所以並沒有時間更新,現在健身房閉館裝修,晚上有空就更新一下!其實在公眾號沒更新的這段日子,每天都有兄弟在來關注我的公眾號,這讓我受寵若驚,在這裡謝謝大家的支援啦!!謝謝^

在這裡我們就跟著火哥的書來學習一下FreeRTOS的消息隊列,這本書我覺得寫得很好,基本都講解到了,關於什麼是消息隊列,就請大家去看書,基礎知識我暫時不說了。

聲明:本書絕大部分內容來自《FreeRTOS 內核實現與應用開發實戰指南—基於野火 STM32 全系列(M3/4/7)開發板》,如涉及侵權請聯繫傑傑刪除

FreeRTOS的消息隊列支援

  • FreeRTOS 中使用隊列數據結構實現任務非同步通訊工作,具有如下特性:
  • 消息支援先進先出方式排隊,支援非同步讀寫工作方式。
  • 讀寫隊列均支援超時機制。
  • 消息支援後進先出方式排隊, 往隊首發送消息(LIFO) 。
  • 可以允許不同長度(不超過隊列節點最大值)的任意類型消息。
  • 一個任務能夠從任意一個消息隊列接收和發送消息。
  • 多個任務能夠從同一個消息隊列接收和發送消息。
  • 當隊列使用結束後,可以通過刪除隊列函數進行刪除。

FreeRTOS隊列的特點

一般來說,魚與熊掌不可兼得,如果數據太多,那數據傳輸的速度必然是會慢下來,而如果採用引用傳遞的方式,當原始數據被修改的時候,數據有變得不安全,但是FreeRTOS支援拷貝與引用的方式進行數據的傳輸,變得更加靈活。
隊列是通過拷貝傳遞數據的,但這並不妨礙隊列通過引用來傳遞數據。當資訊的大小到達一個臨界點後,逐位元組拷貝整個資訊是不實際的,可以定義一個指向數據區域的指針,將指針傳遞即可。這種方法在物聯網中是非常常用的。

消息隊列控制塊

其實消息隊列不僅僅是用於當做消息隊列,FreeRTOS還把他當做訊號量的數據結構來使用

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 BaseType_t xRxLock;   /* 隊列上鎖後,存儲從隊列收到的列表項數目,如果隊列沒有上鎖,設置為queueUNLOCKED */      volatile BaseType_t xTxLock;   /* 隊列上鎖後,存儲發送到隊列的列表項數目,如果隊列沒有上鎖,設置為queueUNLOCKED */        /* 刪除部分源碼 */    } xQUEUE;    typedef xQUEUE Queue_t;

先過一遍消息隊列的數據結構,其實沒啥東西的,記不住也沒啥大問題,下面會用到就行了。

創建消息隊列

FreeRTOS創建隊列API函數是xQueueCreate(),但其實這是一個宏。真正被執行的函數是xQueueGenericCreate(),我們稱這個函數為通用隊列創建函數。

    QueueHandle_t xQueueGenericCreate( const UBaseType_t uxQueueLength, const UBaseType_t uxItemSize, const uint8_t ucQueueType )      {      Queue_t *pxNewQueue;      size_t xQueueSizeInBytes;      uint8_t *pucQueueStorage;            configASSERT( uxQueueLength > ( UBaseType_t ) 0 );            if( uxItemSize == ( UBaseType_t ) 0 )          {              /* 如果 uxItemSize 為 0,也就是單個消息空間大小為 0,這樣子就不  需要申請記憶體了,那麼 xQueueSizeInBytes 也設置為 0 即可,設置為 0 是可以的,用作訊號  量的時候這個就可以設置為 0。*/              xQueueSizeInBytes = ( size_t ) 0;          }          else          {              /* 分配足夠消息存儲空間,空間的大小為隊列長度*單個消息大小 */              xQueueSizeInBytes = ( size_t ) ( uxQueueLength * uxItemSize ); /*lint !e961 MISRA exception as the casts are only redundant for some ports. */          }          /* FreeRTOS 調用 pvPortMalloc()函數向系統申請記憶體空間,記憶體大  小為消息隊列控制塊大小加上消息存儲空間大小,因為這段記憶體空間是需要保證連續的 */          pxNewQueue = ( Queue_t * ) pvPortMalloc( sizeof( Queue_t ) + xQueueSizeInBytes );            if( pxNewQueue != NULL )          {              /* 計算出消息存儲空間的起始地址 */              pucQueueStorage = ( ( uint8_t * ) pxNewQueue ) + sizeof( Queue_t );                #if( configSUPPORT_STATIC_ALLOCATION == 1 )              {                  pxNewQueue->ucStaticallyAllocated = pdFALSE;              }              #endif /* configSUPPORT_STATIC_ALLOCATION */                prvInitialiseNewQueue( uxQueueLength, uxItemSize, pucQueueStorage, ucQueueType, pxNewQueue );          }            return pxNewQueue;      }

真正的初始化在下面這個函數中:

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->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();        return pdPASS;  }

初始化完成之後,為了讓大家理解,消息隊列是怎麼樣的,就給出一個示意圖,黃色部分是消息隊列的控制塊,而綠色部分則是消息隊列的存放消息的地方,在創建的時候,我們知道的消息隊列長度與單個消息空間大小。
此圖截自野火FreeRTOS書籍

消息隊列發送

任務或者中斷服務程式都可以給消息隊列發送消息,當發送消息時,如果隊列未滿或者允許覆蓋入隊, FreeRTOS 會將消息拷貝到消息隊列隊尾,否則,會根據用戶指定的阻塞超時時間進行阻塞,在這段時間中,如果隊列一直不允許入隊,該任務將保持阻塞狀態以等待隊列允許入隊。當其它任務從其等待的隊列中讀取入了數據(隊列未滿),該任務將自動由阻塞態轉為就緒態。當任務等待的時間超過了指定的阻塞時間,即使隊列中還不允許入隊,任務也會自動從阻塞態轉移為就緒態,此時發送消息的任務或者中斷程式會收到一個錯誤碼 errQUEUE_FULL。
發送緊急消息的過程與發送消息幾乎一樣,唯一的不同是,當發送緊急消息時,發送的位置是消息隊列隊頭而非隊尾,這樣,接收者就能夠優先接收到緊急消息,從而及時進行消息處理。
下面是消息隊列的發送API介面,函數中有FromISR則表明在中斷中使用的。
消息隊列入隊(發送)的API介面

1 /*-----------------------------------------------------------*/   2 BaseType_t xQueueGenericSend( QueueHandle_t xQueue,      (1)   3                               const void * const pvItemToQueue,  (2)   4                               TickType_t xTicksToWait,       (3)   5                               const BaseType_t xCopyPosition )   (4)   6 {   7     BaseType_t xEntryTimeSet = pdFALSE, xYieldRequired;   8     TimeOut_t xTimeOut;   9     Queue_t * const pxQueue = ( Queue_t * ) xQueue;  10  11     /* 已刪除一些斷言操作 */  12  13     for ( ;; ) {  14         taskENTER_CRITICAL();                    (5)  15         {  16             /* 隊列未滿 */  17             if ( ( pxQueue->uxMessagesWaiting < pxQueue->uxLength )  18                  || ( xCopyPosition == queueOVERWRITE ) ) {  (6)  19                 traceQUEUE_SEND( pxQueue );  20                 xYieldRequired =  21           prvCopyDataToQueue( pxQueue, pvItemToQueue, xCopyPosition ); (7)  22  23                 /* 已刪除使用隊列集部分程式碼 */  24                 /* 如果有任務在等待獲取此消息隊列 */  25       if ( listLIST_IS_EMPTY(&(pxQueue->xTasksWaitingToReceive))==pdFALSE){ (8)  26                     /* 將任務從阻塞中恢復 */  27             if ( xTaskRemoveFromEventList(  28                   &( pxQueue->xTasksWaitingToReceive ) )!=pdFALSE) { (9)  29                         /* 如果恢復的任務優先順序比當前運行任務優先順序還高,  30                         那麼需要進行一次任務切換 */  31                         queueYIELD_IF_USING_PREEMPTION();    (10)  32                     } else {  33                         mtCOVERAGE_TEST_MARKER();  34                     }  35                 } else if ( xYieldRequired != pdFALSE ) {  36                     /* 如果沒有等待的任務,拷貝成功也需要任務切換 */  37                     queueYIELD_IF_USING_PREEMPTION();        (11)  38                 } else {  39                     mtCOVERAGE_TEST_MARKER();  40                 }  41  42                 taskEXIT_CRITICAL();             (12)  43                 return pdPASS;  44             }  45             /* 隊列已滿 */  46             else {                       (13)  47                 if ( xTicksToWait == ( TickType_t ) 0 ) {  48                     /* 如果用戶不指定阻塞超時時間,退出 */  49                     taskEXIT_CRITICAL();         (14)  50                     traceQUEUE_SEND_FAILED( pxQueue );  51                     return errQUEUE_FULL;  52                 } else if ( xEntryTimeSet == pdFALSE ) {  53                  /* 初始化阻塞超時結構體變數,初始化進入  54              阻塞的時間xTickCount和溢出次數xNumOfOverflows */  55                     vTaskSetTimeOutState( &xTimeOut );       (15)  56                     xEntryTimeSet = pdTRUE;  57                 } else {  58                     mtCOVERAGE_TEST_MARKER();  59                 }  60             }  61         }  62         taskEXIT_CRITICAL();                 (16)  63         /* 掛起調度器 */  64         vTaskSuspendAll();  65         /* 隊列上鎖 */  66         prvLockQueue( pxQueue );  67  68         /* 檢查超時時間是否已經過去了 */  69         if (xTaskCheckForTimeOut(&xTimeOut, &xTicksToWait)==pdFALSE){ (17)  70             /* 如果隊列還是滿的 */  71             if ( prvIsQueueFull( pxQueue ) != pdFALSE ) {    (18)  72                 traceBLOCKING_ON_QUEUE_SEND( pxQueue );  73                 /* 將當前任務添加到隊列的等待發送列表中  74                    以及阻塞延時列表,延時時間為用戶指定的超時時間xTicksToWait */  75                 vTaskPlaceOnEventList(  76                    &( pxQueue->xTasksWaitingToSend ), xTicksToWait );(19)  77                 /* 隊列解鎖 */  78                 prvUnlockQueue( pxQueue );           (20)  79  80                 /* 恢復調度器 */  81                 if ( xTaskResumeAll() == pdFALSE ) {  82                     portYIELD_WITHIN_API();  83                 }  84             } else {  85                 /* 隊列有空閑消息空間,允許入隊 */  86                 prvUnlockQueue( pxQueue );           (21)  87                 ( void ) xTaskResumeAll();  88             }  89         } else {  90             /* 超時時間已過,退出 */  91             prvUnlockQueue( pxQueue );               (22)  92             ( void ) xTaskResumeAll();  93  94             traceQUEUE_SEND_FAILED( pxQueue );  95             return errQUEUE_FULL;  96         }  97     }  98 }  99 /*-----------------------------------------------------------*/  

如果阻塞時間不為 0,任務會因為等待入隊而進入阻塞, 在將任務設置為阻塞的過程中, 系統不希望有其它任務和中斷操作這個隊列的 xTasksWaitingToReceive 列表和 xTasksWaitingToSend 列表,因為可能引起其它任務解除阻塞,這可能會發生優先順序翻轉。比如任務 A 的優先順序低於當前任務,但是在當前任務進入阻塞的過程中,任務 A 卻因為其它原因解除阻塞了,這顯然是要絕對禁止的。因此FreeRTOS 使用掛起調度器禁止其它任務操作隊列,因為掛起調度器意味著任務不能切換並且不準調用可能引起任務切換的 API 函數。但掛起調度器並不會禁止中斷,中斷服務函數仍然可以操作隊列事件列表,可能會解除任務阻塞、可能會進行上下文切換,這也是不允許的。於是,解決辦法是不但掛起調度器,還要給隊列上鎖,禁止任何中斷來操作隊列。
再借用朱工精心製作的流程圖加以理解:圖片出自:https://blog.csdn.net/zhzht19861011/article/details/51510384
消息隊列入隊流程

消息隊列出隊的API函數介面:
在這裡插入圖片描述
消息隊列出隊過程分析,其實跟入隊差不多,請看注釋:

 1 /*-----------------------------------------------------------*/   2 BaseType_t xQueueGenericReceive( QueueHandle_t xQueue,       (1)   3                                  void * const pvBuffer,      (2)   4                                  TickType_t xTicksToWait,    (3)   5                                  const BaseType_t xJustPeeking ) (4)   6 {   7     BaseType_t xEntryTimeSet = pdFALSE;   8     TimeOut_t xTimeOut;   9     int8_t *pcOriginalReadPosition;  10     Queue_t * const pxQueue = ( Queue_t * ) xQueue;  11  12     /* 已刪除一些斷言 */  13     for ( ;; ) {  14         taskENTER_CRITICAL();                    (5)  15         {  16             const UBaseType_t uxMessagesWaiting = pxQueue->uxMessagesWaiting;  17  18             /* 看看隊列中有沒有消息 */  19             if ( uxMessagesWaiting > ( UBaseType_t ) 0 ) {   (6)  20                 /*防止僅僅是讀取消息,而不進行消息出隊操作*/  21                 pcOriginalReadPosition = pxQueue->u.pcReadFrom;  (7)  22                 /* 拷貝消息到用戶指定存放區域pvBuffer */  23                 prvCopyDataFromQueue( pxQueue, pvBuffer );   (8)  24  25                 if ( xJustPeeking == pdFALSE ) {     (9)  26                     /* 讀取消息並且消息出隊 */  27                     traceQUEUE_RECEIVE( pxQueue );  28  29                     /* 獲取了消息,當前消息隊列的消息個數需要減一 */  30                     pxQueue->uxMessagesWaiting = uxMessagesWaiting - 1;  (10)  31                     /* 判斷一下消息隊列中是否有等待發送消息的任務 */  32                     if ( listLIST_IS_EMPTY(          (11)  33                              &( pxQueue->xTasksWaitingToSend ) ) == pdFALSE ) {  34                         /* 將任務從阻塞中恢復 */  35                         if ( xTaskRemoveFromEventList(       (12)  36                                  &( pxQueue->xTasksWaitingToSend ) ) != pdFALSE ) {  37                             /* 如果被恢復的任務優先順序比當前任務高,會進行一次任務切換 */  38                             queueYIELD_IF_USING_PREEMPTION();    (13)  39                         } else {  40                             mtCOVERAGE_TEST_MARKER();  41                         }  42                     } else {  43                         mtCOVERAGE_TEST_MARKER();  44                     }  45                 } else {                 (14)  46                     /* 任務只是看一下消息(peek),並不出隊 */  47                     traceQUEUE_PEEK( pxQueue );  48  49                     /* 因為是只讀消息 所以還要還原讀消息位置指針 */  50                     pxQueue->u.pcReadFrom = pcOriginalReadPosition; (15)  51  52                     /* 判斷一下消息隊列中是否還有等待獲取消息的任務 */  53                     if ( listLIST_IS_EMPTY(          (16)  54                              &( pxQueue->xTasksWaitingToReceive ) ) == pdFALSE ) {  55                         /* 將任務從阻塞中恢復 */  56                         if ( xTaskRemoveFromEventList(  57                               &( pxQueue->xTasksWaitingToReceive ) ) != pdFALSE ) {  58                             /* 如果被恢復的任務優先順序比當前任務高,會進行一次任務切換 */  59                             queueYIELD_IF_USING_PREEMPTION();  60                         } else {  61                             mtCOVERAGE_TEST_MARKER();  62                         }  63                     } else {  64                         mtCOVERAGE_TEST_MARKER();  65                     }  66                 }  67  68                 taskEXIT_CRITICAL();             (17)  69                 return pdPASS;  70             } else {                     (18)  71                 /* 消息隊列中沒有消息可讀 */  72                 if ( xTicksToWait == ( TickType_t ) 0 ) {    (19)  73                     /* 不等待,直接返回 */  74                     taskEXIT_CRITICAL();  75                     traceQUEUE_RECEIVE_FAILED( pxQueue );  76                     return errQUEUE_EMPTY;  77                 } else if ( xEntryTimeSet == pdFALSE ) {  78                     /* 初始化阻塞超時結構體變數,初始化進入  79                     阻塞的時間xTickCount和溢出次數xNumOfOverflows */  80                     vTaskSetTimeOutState( &xTimeOut );       (20)  81                     xEntryTimeSet = pdTRUE;  82                 } else {  83                     mtCOVERAGE_TEST_MARKER();  84                 }  85             }  86         }  87         taskEXIT_CRITICAL();  88  89         vTaskSuspendAll();  90         prvLockQueue( pxQueue );             (21)  91  92         /* 檢查超時時間是否已經過去了*/  93         if ( xTaskCheckForTimeOut( &xTimeOut, &xTicksToWait ) == pdFALSE ) {(22)  94             /* 如果隊列還是空的 */  95             if ( prvIsQueueEmpty( pxQueue ) != pdFALSE ) {  96                 traceBLOCKING_ON_QUEUE_RECEIVE( pxQueue );   (23)  97                 /* 將當前任務添加到隊列的等待接收列表中  98                    以及阻塞延時列表,阻塞時間為用戶指定的超時時間xTicksToWait */  99                 vTaskPlaceOnEventList(  100                     &( pxQueue->xTasksWaitingToReceive ), xTicksToWait );  101                 prvUnlockQueue( pxQueue );  102                 if ( xTaskResumeAll() == pdFALSE ) {  103                     /* 如果有任務優先順序比當前任務高,會進行一次任務切換 */  104                     portYIELD_WITHIN_API();  105                 } else {  106                     mtCOVERAGE_TEST_MARKER();  107                 }  108             } else {  109                 /* 如果隊列有消息了,就再試一次獲取消息 */  110                 prvUnlockQueue( pxQueue );          (24)  111                 ( void ) xTaskResumeAll();  112             }  113         } else {  114             /* 超時時間已過,退出 */  115             prvUnlockQueue( pxQueue );              (25)  116             ( void ) xTaskResumeAll();  117  118             if ( prvIsQueueEmpty( pxQueue ) != pdFALSE ) {  119                 /* 如果隊列還是空的,返回錯誤程式碼errQUEUE_EMPTY */  120                 traceQUEUE_RECEIVE_FAILED( pxQueue );  121                 return errQUEUE_EMPTY;              (26)  122             } else {  123                 mtCOVERAGE_TEST_MARKER();  124             }  125         }  126     }  127 }  128 /*-----------------------------------------------------------*/

關注我

歡迎關注我公眾號

更多資料歡迎關注「物聯網IoT開發」公眾號!