從0開始學FreeRTOS-(消息隊列)-5


問題解答

曾經有人問我,FreeRTOS那么多API,到底怎么記住呢?
我想說,其實API不難記,就是有點難找,因為FreeRTOS的API很多都是帶參宏,所以跳來跳去的比較麻煩,而且注釋也很多,要找還真不是那么容易,不過也沒啥,一般都會有API手冊的,我就告訴大家一下:
FreeRTOS Kernel: Reference Manual
FreeRTOS內核:參考手冊,大家可以在官網下載,也能在后台得到。
當然書本是英文的,如果英語像我這樣子不咋地的同學,可以用谷歌瀏覽器在官網直接看API手冊,直接翻譯一下就行了。傳送門:https://www.freertos.org/a00018.html
Reference Manual
FreeRTOS官網的API

FreeRTOS消息隊列

基於 FreeRTOS 的應用程序由一組獨立的任務構成——每個任務都是具有獨立權限的程序。這些獨立的任務之間的通訊與同步一般都是基於操作系統提供的IPC通訊機制,而FreeRTOS 中所有的通信與同步機制都是基於隊列實現的。
消息隊列是一種常用於任務間通信的數據結構,隊列可以在任務與任務間、中斷和任務間傳送信息,實現了任務接收來自其他任務或中斷的不固定長度的消息。任務能夠從隊列里面讀取消息,當隊列中的消息是空時,掛起讀取任務,用戶還可以指定掛起的任務時間;當隊列中有新消息時,掛起的讀取任務被喚醒並處理新消息,消息隊列是一種異步的通信方式。

隊列特性

1.數據存儲

隊列可以保存有限個具有確定長度的數據單元。隊列可以保存的最大單元數目被稱為隊列的“深度”。在隊列創建時需要設定其深度和每個單元的大小。
通常情況下,隊列被作為 FIFO(先進先出)緩沖區使用,即數據由隊列尾寫入,從隊列首讀出。當然,由隊列首寫入也是可能的。
往隊列寫入數據是通過字節拷貝把數據復制存儲到隊列中;從隊列讀出數據使得把隊列中的數據拷貝刪除。

2.讀阻塞

當某個任務試圖讀一個隊列時,其可以指定一個阻塞超時時間。在這段時間中,如果隊列為空,該任務將保持阻塞狀態以等待隊列數據有效。當其它任務或中斷服務例程往其等待的隊列中寫入了數據,該任務將自動由阻塞態轉移為就緒態。當等待的時間超過了指定的阻塞時間,即使隊列中尚無有效數據,任務也會自動從阻塞態轉移為就緒態。
由於隊列可以被多個任務讀取,所以對單個隊列而言,也可能有多個任務處於阻塞狀態以等待隊列數據有效。這種情況下,一旦隊列數據有效,只會有一個任務會被解除阻塞,這個任務就是所有等待任務中優先級最高的任務。而如果所有等待任務的優先級相同,那么被解除阻塞的任務將是等待最久的任務。

說些題外話,ucos中是具有廣播消息的,當有多個任務阻塞在隊列上,當發送消息的時候可以選擇廣播消息,那么這些阻塞的任務都能被解除阻塞。

3.寫阻塞

與讀阻塞想反,任務也可以在寫隊列時指定一個阻塞超時時間。這個時間是當被寫隊列已滿時,任務進入阻塞態以等待隊列空間有效的最長時間。
由於隊列可以被多個任務寫入,所以對單個隊列而言,也可能有多個任務處於阻塞狀態以等待隊列空間有效。這種情況下,一旦隊列空間有效,只會有一個任務會被解除阻塞,這個任務就是所有等待任務中優先級最高的任務。而如果所有等待任務的優先級相同,那么被解除阻塞的任務將是等待最久的任務。

消息隊列的工作流程

1.發送消息

任務或者中斷服務程序都可以給消息隊列發送消息,當發送消息時,如果隊列未滿或者允許覆蓋入隊, 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 函數。但掛起調度器並不會禁止中斷,中斷服務函數仍然可以操作隊列阻塞列表,可能會解除任務阻塞、可能會進行上下文切換,這也是不允許的。於是,FreeRTOS解決辦法是不但掛起調度器,還要給隊列上鎖,禁止任何中斷來操作隊列。
下面來看看流程圖:
消息隊列發送流程
相比在任務中調用的發送函數,在中斷中調用的函數會更加簡單一些, 沒有任務阻塞操作。
函數 xQueueGenericSend中插入數據后, 會檢查等待接收鏈表是否有任務等待,如果有會恢復就緒。如果恢復的任務優先級比當前任務高, 則會觸發任務切換;但是在中斷中調用的這個函數的做法是返回一個參數標志是否需要觸發任務切換,並不在中斷中切換任務。
在任務中調用的函數中有鎖定和解鎖隊列的操作, 鎖定隊列的時候, 隊列的事件鏈表不能被修改。 而在被中斷中發送消息的處理是: 當遇到隊列被鎖定的時候, 將新數據插入到隊列后, 並不會直接恢復因為等待接收的任務, 而是累加了計數, 當隊列解鎖的時候, 會根據這個計數, 對應恢復幾個任務。
遇到隊列滿的情況, 函數會直接返回, 而不是阻塞等待, 因為在中斷中阻塞是不允許的!!!

 1 BaseType_t xQueueGenericSendFromISR(
 2        QueueHandle_t xQueue,
 3        const void * const pvItemToQueue,
 4        /* 不在中斷函數中觸發任務切換, 而是返回一個標記 */
 5        BaseType_t * const pxHigherPriorityTaskWoken,
 6        const BaseType_t xCopyPosition )
 7{
 8    BaseType_t xReturn;
 9    UBaseType_t uxSavedInterruptStatus;
10    Queue_t * const pxQueue = ( Queue_t * ) xQueue;
11
12    uxSavedInterruptStatus = portSET_INTERRUPT_MASK_FROM_ISR();
13    {
14        // 判斷隊列是否有空間插入新內容
15        if( ( pxQueue->uxMessagesWaiting < pxQueue->uxLength ) || ( xCopyPosition == queueOVERWRITE ) )
16        {
17            const int8_t cTxLock = pxQueue->cTxLock;
18
19            // 中斷中不能使用互斥鎖, 所以拷貝函數只是拷貝數據,
20            // 沒有任務優先級繼承需要考慮
21            ( void ) prvCopyDataToQueue( pxQueue, pvItemToQueue, xCopyPosition );
22
23            // 判斷隊列是否被鎖定
24            if( cTxLock == queueUNLOCKED )
25            {
26            #if ( configUSE_QUEUE_SETS == 1 )
27                // 集合相關代碼
28            #else /* configUSE_QUEUE_SETS */
29                {
30                    // 將最高優先級的等待任務恢復到就緒鏈表
31                    if( listLIST_IS_EMPTY( &( pxQueue->xTasksWaitingToReceive ) ) == pdFALSE )
32                    {
33                        if( xTaskRemoveFromEventList( &( pxQueue->xTasksWaitingToReceive ) ) != pdFALSE)
34                        {
35                            // 如果有高優先級的任務被恢復
36                            // 此處不直接觸發任務切換, 而是返回一個標記
37                            if( pxHigherPriorityTaskWoken != NULL )
38                            {
39                                *pxHigherPriorityTaskWoken = pdTRUE;
40                            }
41                        }
42                    }
43                }
44            #endif /* configUSE_QUEUE_SETS */
45            }
46            else
47            {
48                // 隊列被鎖定, 不能修改事件鏈表
49                // 增加計數, 記錄需要接觸幾個任務到就緒
50                // 在解鎖隊列的時候會根據這個計數恢復任務
51                pxQueue->cTxLock = ( int8_t ) ( cTxLock + 1 );
52            }
53            xReturn = pdPASS;
54        }
55        else
56        {
57            // 隊列滿 直接返回 不阻塞
58            xReturn = errQUEUE_FULL;
59        }
60    }
61
62    // 恢復中斷的優先級
63    portCLEAR_INTERRUPT_MASK_FROM_ISR( uxSavedInterruptStatus );
64
65    return xReturn;
66}

消息隊列讀取

消息讀取
任務調用接收函數收取隊列消息, 函數首先判斷當前隊列是否有未讀消息, 如果沒有, 則會判斷參數 xTicksToWait, 決定直接返回函數還是阻塞等待。
如果隊列中有消息未讀, 首先會把待讀的消息復制到傳進來的指針所指內, 然后判斷函數參數 xJustPeeking == pdFALSE的時候, 符合的話, 說明這個函數讀取了數據, 需要把被讀取的數據做出隊處理, 如果不是, 則只是查看一下(peek),只是返回數據,但是不會把數據清除。
對於正常讀取數據的操作, 清除數據后隊列會空出空位, 所以查看隊列中的等待列表中是否有任務等發送數據而被掛起, 有的話恢復一個任務就緒, 並根據優先級判斷是否需要出進行任務切換。
對於只是查看數據的, 由於沒有清除數據, 所以沒有空間新空出,不需要檢查發送等待鏈表, 但是會檢查接收等待鏈表, 如果有任務掛起會切換其到就緒並判斷是否需要切換。

消息隊列出隊過程分析,其實跟入隊差不多,請看注釋:

 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 /*-----------------------------------------------------------*/

提示

如果隊列存儲的數據較大時,那最好是利用隊列來傳遞數據的指針而不是數據本身,因為傳遞數據的時候是需要CPU一字節一字節地將數據拷貝進隊列或從隊列拷貝出來。而傳遞指針無論是在處理速度上還是內存空間利用上都更有效。但是,當利用隊列傳遞指針時,一定要十分小心地做到以下兩點:

1.指針指向的內存空間的所有權必須明確

當任務間通過指針共享內存時,應該從根本上保證所不會有任意兩個任務同時修改共享內存中的數據,或是以其它行為方式使得共享內存數據無效或產生一致性問題。原則上,共享內存在其指針發送到隊列之前,其內容只允許被發送任務訪問;共享內存指針從隊列中被讀出之后,其內容亦只允許被接收任務訪問。

2.指針指向的內存空間必須有效

如果指針指向的內存空間是動態分配的,只應該有一個任務負責對其進行內存釋放。當這段內存空間被釋放之后,就不應該有任何一個任務再訪問這段空間。
並且最最最重要的是禁止使用指針訪問任務棧上的空間,也就是局部變量。因為當棧發生改變后,棧上的數據將不再有效。

關注我

歡迎關注我公眾號

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


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM