韋東山freeRTOS系列教程之【第五章】佇列(queue)
需要獲取更好閱讀體驗的同學,請訪問我專門設立的站點檢視,地址:http://rtos.100ask.net/
系列教程總目錄
本教程連載中,篇章會比較多,為方便同學們閱讀,點選這裡可以檢視文章的 目錄列表,目錄列表頁面地址:
概述
佇列(queue)可以用於"任務到任務"、"任務到中斷"、"中斷到任務"直接傳輸資訊。
本章涉及如下內容:
- 怎麼建立、清除、刪除佇列
- 佇列中訊息如何儲存
- 怎麼向佇列傳送資料、怎麼從佇列讀取資料、怎麼覆蓋佇列的資料
- 在佇列上阻塞是什麼意思
- 怎麼在多個佇列上阻塞
- 讀寫佇列時如何影響任務的優先順序
5.1 佇列的特性
5.1.1 常規操作
佇列的簡化操如入下圖所示,從此圖可知:
- 佇列可以包含若干個資料:佇列中有若干項,這被稱為"長度"(length)
- 每個資料大小固定
- 建立佇列時就要指定長度、資料大小
- 資料的操作採用先進先出的方法(FIFO,First In First Out):寫資料時放到尾部,讀資料時從頭部讀
- 也可以強制寫佇列頭部:覆蓋頭部資料
更詳細的操作入下圖所示:
5.1.2 傳輸資料的兩種方法
使用佇列傳輸資料時有兩種方法:
- 拷貝:把資料、把變數的值複製進佇列裡
- 引用:把資料、把變數的地址複製進佇列裡
FreeRTOS使用拷貝值的方法,這更簡單:
-
區域性變數的值可以傳送到佇列中,後續即使函式退出、區域性變數被回收,也不會影響佇列中的資料
-
無需分配buffer來儲存資料,佇列中有buffer
-
區域性變數可以馬上再次使用
-
傳送任務、接收任務解耦:接收任務不需要知道這資料是誰的、也不需要傳送任務來釋放資料
-
如果資料實在太大,你還是可以使用佇列傳輸它的地址
-
佇列的空間有FreeRTOS核心分配,無需任務操心
-
對於有記憶體保護功能的系統,如果佇列使用引用方法,也就是使用地址,必須確保雙方任務對這個地址都有訪問許可權。使用拷貝方法時,則無此限制:核心有足夠的許可權,把資料複製進佇列、再把資料複製出佇列。
5.1.3 佇列的阻塞訪問
只要知道佇列的控制代碼,誰都可以讀、寫該佇列。任務、ISR都可讀、寫佇列。可以多個任務讀寫佇列。
任務讀寫佇列時,簡單地說:如果讀寫不成功,則阻塞;可以指定超時時間。口語化地說,就是可以定個鬧鐘:如果能讀寫了就馬上進入就緒態,否則就阻塞直到超時。
某個任務讀佇列時,如果佇列沒有資料,則該任務可以進入阻塞狀態:還可以指定阻塞的時間。如果佇列有資料了,則該阻塞的任務會變為就緒態。如果一直都沒有資料,則時間到之後它也會進入就緒態。
既然讀取佇列的任務個數沒有限制,那麼當多個任務讀取空佇列時,這些任務都會進入阻塞狀態:有多個任務在等待同一個佇列的資料。當佇列中有資料時,哪個任務會進入就緒態?
- 優先順序最高的任務
- 如果大家的優先順序相同,那等待時間最久的任務會進入就緒態
跟讀佇列類似,一個任務要寫佇列時,如果佇列滿了,該任務也可以進入阻塞狀態:還可以指定阻塞的時間。如果佇列有空間了,則該阻塞的任務會變為就緒態。如果一直都沒有空間,則時間到之後它也會進入就緒態。
既然寫佇列的任務個數沒有限制,那麼當多個任務寫"滿佇列"時,這些任務都會進入阻塞狀態:有多個任務在等待同一個佇列的空間。當佇列中有空間時,哪個任務會進入就緒態?
- 優先順序最高的任務
- 如果大家的優先順序相同,那等待時間最久的任務會進入就緒態
5.2 佇列函式
使用佇列的流程:建立佇列、寫佇列、讀佇列、刪除佇列。
5.2.1 建立
佇列的建立有兩種方法:動態分配記憶體、靜態分配記憶體,
- 動態分配記憶體:xQueueCreate,佇列的記憶體在函式內部動態分配
函式原型如下:
QueueHandle_t xQueueCreate( UBaseType_t uxQueueLength, UBaseType_t uxItemSize );
引數 | 說明 |
---|---|
uxQueueLength | 佇列長度,最多能存放多少個數據(item) |
uxItemSize | 每個資料(item)的大小:以位元組為單位 |
返回值 | 非0:成功,返回控制代碼,以後使用控制代碼來操作佇列 NULL:失敗,因為記憶體不足 |
- 靜態分配記憶體:xQueueCreateStatic,佇列的記憶體要事先分配好
函式原型如下:
QueueHandle_t xQueueCreateStatic(
UBaseType_t uxQueueLength,
UBaseType_t uxItemSize,
uint8_t *pucQueueStorageBuffer,
StaticQueue_t *pxQueueBuffer
);
引數 | 說明 |
---|---|
uxQueueLength | 佇列長度,最多能存放多少個數據(item) |
uxItemSize | 每個資料(item)的大小:以位元組為單位 |
pucQueueStorageBuffer | 如果uxItemSize非0,pucQueueStorageBuffer必須指向一個uint8_t陣列, 此陣列大小至少為"uxQueueLength * uxItemSize" |
pxQueueBuffer | 必須執行一個StaticQueue_t結構體,用來儲存佇列的資料結構 |
返回值 | 非0:成功,返回控制代碼,以後使用控制代碼來操作佇列 NULL:失敗,因為pxQueueBuffer為NULL |
示例程式碼:
// 示例程式碼
#define QUEUE_LENGTH 10
#define ITEM_SIZE sizeof( uint32_t )
// xQueueBuffer用來儲存佇列結構體
StaticQueue_t xQueueBuffer;
// ucQueueStorage 用來儲存佇列的資料
// 大小為:佇列長度 * 資料大小
uint8_t ucQueueStorage[ QUEUE_LENGTH * ITEM_SIZE ];
void vATask( void *pvParameters )
{
QueueHandle_t xQueue1;
// 建立佇列: 可以容納QUEUE_LENGTH個數據,每個資料大小是ITEM_SIZE
xQueue1 = xQueueCreateStatic( QUEUE_LENGTH,
ITEM_SIZE,
ucQueueStorage,
&xQueueBuffer );
}
5.2.2 復位
佇列剛被建立時,裡面沒有資料;使用過程中可以呼叫xQueueReset()
把佇列恢復為初始狀態,此函式原型為:
/* pxQueue : 復位哪個佇列;
* 返回值: pdPASS(必定成功)
*/
BaseType_t xQueueReset( QueueHandle_t pxQueue);
5.2.3 刪除
刪除佇列的函式為vQueueDelete()
,只能刪除使用動態方法建立的佇列,它會釋放記憶體。原型如下:
void vQueueDelete( QueueHandle_t xQueue );
5.2.4 寫佇列
可以把資料寫到佇列頭部,也可以寫到尾部,這些函式有兩個版本:在任務中使用、在ISR中使用。函式原型如下:
/* 等同於xQueueSendToBack
* 往佇列尾部寫入資料,如果沒有空間,阻塞時間為xTicksToWait
*/
BaseType_t xQueueSend(
QueueHandle_t xQueue,
const void *pvItemToQueue,
TickType_t xTicksToWait
);
/*
* 往佇列尾部寫入資料,如果沒有空間,阻塞時間為xTicksToWait
*/
BaseType_t xQueueSendToBack(
QueueHandle_t xQueue,
const void *pvItemToQueue,
TickType_t xTicksToWait
);
/*
* 往佇列尾部寫入資料,此函式可以在中斷函式中使用,不可阻塞
*/
BaseType_t xQueueSendToBackFromISR(
QueueHandle_t xQueue,
const void *pvItemToQueue,
BaseType_t *pxHigherPriorityTaskWoken
);
/*
* 往佇列頭部寫入資料,如果沒有空間,阻塞時間為xTicksToWait
*/
BaseType_t xQueueSendToFront(
QueueHandle_t xQueue,
const void *pvItemToQueue,
TickType_t xTicksToWait
);
/*
* 往佇列頭部寫入資料,此函式可以在中斷函式中使用,不可阻塞
*/
BaseType_t xQueueSendToFrontFromISR(
QueueHandle_t xQueue,
const void *pvItemToQueue,
BaseType_t *pxHigherPriorityTaskWoken
);
這些函式用到的引數是類似的,統一說明如下:
引數 | 說明 |
---|---|
xQueue | 佇列控制代碼,要寫哪個佇列 |
pvItemToQueue | 資料指標,這個資料的值會被複制進佇列, 複製多大的資料?在建立佇列時已經指定了資料大小 |
xTicksToWait | 如果佇列滿則無法寫入新資料,可以讓任務進入阻塞狀態, xTicksToWait表示阻塞的最大時間(Tick Count)。 如果被設為0,無法寫入資料時函式會立刻返回; 如果被設為portMAX_DELAY,則會一直阻塞直到有空間可寫 |
返回值 | pdPASS:資料成功寫入了佇列 errQUEUE_FULL:寫入失敗,因為佇列滿了。 |
5.2.5 讀佇列
使用xQueueReceive()
函式讀佇列,讀到一個數據後,佇列中該資料會被移除。這個函式有兩個版本:在任務中使用、在ISR中使用。函式原型如下:
BaseType_t xQueueReceive( QueueHandle_t xQueue,
void * const pvBuffer,
TickType_t xTicksToWait );
BaseType_t xQueueReceiveFromISR(
QueueHandle_t xQueue,
void *pvBuffer,
BaseType_t *pxTaskWoken
);
引數說明如下:
引數 | 說明 |
---|---|
xQueue | 佇列控制代碼,要讀哪個佇列 |
pvBuffer | bufer指標,佇列的資料會被複制到這個buffer 複製多大的資料?在建立佇列時已經指定了資料大小 |
xTicksToWait | 果佇列空則無法讀出資料,可以讓任務進入阻塞狀態, xTicksToWait表示阻塞的最大時間(Tick Count)。 如果被設為0,無法讀出資料時函式會立刻返回; 如果被設為portMAX_DELAY,則會一直阻塞直到有資料可寫 |
返回值 | pdPASS:從佇列讀出資料入 errQUEUE_EMPTY:讀取失敗,因為佇列空了。 |
5.2.6 查詢
可以查詢佇列中有多少個數據、有多少空餘空間。函式原型如下:
/*
* 返回佇列中可用資料的個數
*/
UBaseType_t uxQueueMessagesWaiting( const QueueHandle_t xQueue );
/*
* 返回佇列中可用空間的個數
*/
UBaseType_t uxQueueSpacesAvailable( const QueueHandle_t xQueue );
5.2.7 覆蓋/偷看
當佇列長度為1時,可以使用xQueueOverwrite()
或xQueueOverwriteFromISR()
來覆蓋資料。
注意,佇列長度必須為1。當佇列滿時,這些函式會覆蓋裡面的資料,這也以為著這些函式不會被阻塞。
函式原型如下:
/* 覆蓋佇列
* xQueue: 寫哪個佇列
* pvItemToQueue: 資料地址
* 返回值: pdTRUE表示成功, pdFALSE表示失敗
*/
BaseType_t xQueueOverwrite(
QueueHandle_t xQueue,
const void * pvItemToQueue
);
BaseType_t xQueueOverwriteFromISR(
QueueHandle_t xQueue,
const void * pvItemToQueue,
BaseType_t *pxHigherPriorityTaskWoken
);
如果想讓佇列中的資料供多方讀取,也就是說讀取時不要移除資料,要留給後來人。那麼可以使用"窺視",也就是xQueuePeek()
或xQueuePeekFromISR()
。這些函式會從佇列中複製出資料,但是不移除資料。這也意味著,如果佇列中沒有資料,那麼"偷看"時會導致阻塞;一旦佇列中有資料,以後每次"偷看"都會成功。
函式原型如下:
/* 偷看佇列
* xQueue: 偷看哪個佇列
* pvItemToQueue: 資料地址, 用來儲存複製出來的資料
* xTicksToWait: 沒有資料的話阻塞一會
* 返回值: pdTRUE表示成功, pdFALSE表示失敗
*/
BaseType_t xQueuePeek(
QueueHandle_t xQueue,
void * const pvBuffer,
TickType_t xTicksToWait
);
BaseType_t xQueuePeekFromISR(
QueueHandle_t xQueue,
void *pvBuffer,
);
5.3 示例8: 佇列的基本使用
本節程式碼為:FreeRTOS_08_queue
。
本程式會建立一個佇列,然後建立2個傳送任務、1個接收任務:
- 傳送任務優先順序為1,分別往佇列中寫入100、200
- 接收任務優先順序為2,讀佇列、列印數值
main函式中建立的佇列、建立了傳送任務、接收任務,程式碼如下:
/* 佇列控制代碼, 建立佇列時會設定這個變數 */
QueueHandle_t xQueue;
int main( void )
{
prvSetupHardware();
/* 建立佇列: 長度為5,資料大小為4位元組(存放一個整數) */
xQueue = xQueueCreate( 5, sizeof( int32_t ) );
if( xQueue != NULL )
{
/* 建立2個任務用於寫佇列, 傳入的引數分別是100、200
* 任務函式會連續執行,向佇列傳送數值100、200
* 優先順序為1
*/
xTaskCreate( vSenderTask, "Sender1", 1000, ( void * ) 100, 1, NULL );
xTaskCreate( vSenderTask, "Sender2", 1000, ( void * ) 200, 1, NULL );
/* 建立1個任務用於讀佇列
* 優先順序為2, 高於上面的兩個任務
* 這意味著佇列一有資料就會被讀走
*/
xTaskCreate( vReceiverTask, "Receiver", 1000, NULL, 2, NULL );
/* 啟動排程器 */
vTaskStartScheduler();
}
else
{
/* 無法建立佇列 */
}
/* 如果程式執行到了這裡就表示出錯了, 一般是記憶體不足 */
return 0;
}
傳送任務的函式中,不斷往佇列中寫入數值,程式碼如下:
static void vSenderTask( void *pvParameters )
{
int32_t lValueToSend;
BaseType_t xStatus;
/* 我們會使用這個函式建立2個任務
* 這些任務的pvParameters不一樣
*/
lValueToSend = ( int32_t ) pvParameters;
/* 無限迴圈 */
for( ;; )
{
/* 寫佇列
* xQueue: 寫哪個佇列
* &lValueToSend: 寫什麼資料? 傳入資料的地址, 會從這個地址把資料複製進佇列
* 0: 不阻塞, 如果佇列滿的話, 寫入失敗, 立刻返回
*/
xStatus = xQueueSendToBack( xQueue, &lValueToSend, 0 );
if( xStatus != pdPASS )
{
printf( "Could not send to the queue.\r\n" );
}
}
}
接收任務的函式中,讀取佇列、判斷返回值、列印,程式碼如下:
static void vReceiverTask( void *pvParameters )
{
/* 讀取佇列時, 用這個變數來存放資料 */
int32_t lReceivedValue;
BaseType_t xStatus;
const TickType_t xTicksToWait = pdMS_TO_TICKS( 100UL );
/* 無限迴圈 */
for( ;; )
{
/* 讀佇列
* xQueue: 讀哪個佇列
* &lReceivedValue: 讀到的資料複製到這個地址
* xTicksToWait: 如果佇列為空, 阻塞一會
*/
xStatus = xQueueReceive( xQueue, &lReceivedValue, xTicksToWait );
if( xStatus == pdPASS )
{
/* 讀到了資料 */
printf( "Received = %d\r\n", lReceivedValue );
}
else
{
/* 沒讀到資料 */
printf( "Could not receive from the queue.\r\n" );
}
}
}
程式執行結果如下:
任務排程情況如下圖所示:
5.4 示例9: 分辨資料來源
本節程式碼為:FreeRTOS_09_queue_datasource
。
當有多個傳送任務,通過同一個佇列發出資料,接收任務如何分辨資料來源?資料本身帶有"來源"資訊,比如寫入佇列的資料是一個結構體,結構體中的lDataSouceID用來表示資料來源:
typedef struct {
ID_t eDataID;
int32_t lDataValue;
}Data_t;
不同的傳送任務,先構造好結構體,填入自己的eDataID
,再寫佇列;接收任務讀出資料後,根據eDataID
就可以知道資料來源了,如下圖所示:
- CAN任務傳送的資料:eDataID=eMotorSpeed
- HMI任務傳送的資料:eDataID=eSpeedSetPoint
FreeRTOS_09_queue_datasource
程式會建立一個佇列,然後建立2個傳送任務、1個接收任務:
- 建立的佇列,用來發送結構體:資料大小是結構體的大小
- 傳送任務優先順序為2,分別往佇列中寫入自己的結構體,結構體中會標明資料來源
- 接收任務優先順序為1,讀佇列、根據資料來源列印資訊
main函式中建立了佇列、建立了傳送任務、接收任務,程式碼如下:
/* 定義2種資料來源(ID) */
typedef enum
{
eMotorSpeed,
eSpeedSetPoint
} ID_t;
/* 定義在佇列中傳輸的資料的格式 */
typedef struct {
ID_t eDataID;
int32_t lDataValue;
}Data_t;
/* 定義2個結構體 */
static const Data_t xStructsToSend[ 2 ] =
{
{ eMotorSpeed, 10 }, /* CAN任務傳送的資料 */
{ eSpeedSetPoint, 5 } /* HMI任務傳送的資料 */
};
/* vSenderTask被用來建立2個任務,用於寫佇列
* vReceiverTask被用來建立1個任務,用於讀佇列
*/
static void vSenderTask( void *pvParameters );
static void vReceiverTask( void *pvParameters );
/*-----------------------------------------------------------*/
/* 佇列控制代碼, 建立佇列時會設定這個變數 */
QueueHandle_t xQueue;
int main( void )
{
prvSetupHardware();
/* 建立佇列: 長度為5,資料大小為4位元組(存放一個整數) */
xQueue = xQueueCreate( 5, sizeof( Data_t ) );
if( xQueue != NULL )
{
/* 建立2個任務用於寫佇列, 傳入的引數是不同的結構體地址
* 任務函式會連續執行,向佇列傳送結構體
* 優先順序為2
*/
xTaskCreate(vSenderTask, "CAN Task", 1000, (void *) &(xStructsToSend[0]), 2, NULL);
xTaskCreate(vSenderTask, "HMI Task", 1000, (void *) &( xStructsToSend[1]), 2, NULL);
/* 建立1個任務用於讀佇列
* 優先順序為1, 低於上面的兩個任務
* 這意味著傳送任務優先寫佇列,佇列常常是滿的狀態
*/
xTaskCreate( vReceiverTask, "Receiver", 1000, NULL, 1, NULL );
/* 啟動排程器 */
vTaskStartScheduler();
}
else
{
/* 無法建立佇列 */
}
/* 如果程式執行到了這裡就表示出錯了, 一般是記憶體不足 */
return 0;
}
傳送任務的函式中,不斷往佇列中寫入數值,程式碼如下:
static void vSenderTask( void *pvParameters )
{
BaseType_t xStatus;
const TickType_t xTicksToWait = pdMS_TO_TICKS( 100UL );
/* 無限迴圈 */
for( ;; )
{
/* 寫佇列
* xQueue: 寫哪個佇列
* pvParameters: 寫什麼資料? 傳入資料的地址, 會從這個地址把資料複製進佇列
* xTicksToWait: 如果佇列滿的話, 阻塞一會
*/
xStatus = xQueueSendToBack( xQueue, pvParameters, xTicksToWait );
if( xStatus != pdPASS )
{
printf( "Could not send to the queue.\r\n" );
}
}
}
接收任務的函式中,讀取佇列、判斷返回值、列印,程式碼如下:
static void vReceiverTask( void *pvParameters )
{
/* 讀取佇列時, 用這個變數來存放資料 */
Data_t xReceivedStructure;
BaseType_t xStatus;
/* 無限迴圈 */
for( ;; )
{
/* 讀佇列
* xQueue: 讀哪個佇列
* &xReceivedStructure: 讀到的資料複製到這個地址
* 0: 沒有資料就即刻返回,不阻塞
*/
xStatus = xQueueReceive( xQueue, &xReceivedStructure, 0 );
if( xStatus == pdPASS )
{
/* 讀到了資料 */
if( xReceivedStructure.eDataID == eMotorSpeed )
{
printf( "From CAN, MotorSpeed = %d\r\n", xReceivedStructure.lDataValue );
}
else if( xReceivedStructure.eDataID == eSpeedSetPoint )
{
printf( "From HMI, SpeedSetPoint = %d\r\n", xReceivedStructure.lDataValue );
}
}
else
{
/* 沒讀到資料 */
printf( "Could not receive from the queue.\r\n" );
}
}
}
執行結果如下:
任務排程情況如下圖所示:
- t1:HMI是最後建立的最高優先順序任務,它先執行,一下子向佇列寫入5個數據,把佇列都寫滿了
- t2:佇列已經滿了,HMI任務再發起第6次寫操作時,進入阻塞狀態。這時CAN任務是最高優先順序的就緒態任務,它開始執行
- t3:CAN任務發現佇列已經滿了,進入阻塞狀態;接收任務變為最高優先順序的就緒態任務,它開始執行
- t4:現在,HMI任務、CAN任務的優先順序都比接收任務高,它們都在等待佇列有空閒的空間;一旦接收任務讀出1個數據,會馬上被搶佔。被誰搶佔?誰等待最久?HMI任務!所以在t4時刻,切換到HMI任務。
- t5:HMI任務向佇列寫入第6個數據,然後再次阻塞,這是CAN任務已經阻塞很久了。接收任務變為最高優先順序的就緒態任務,開始執行。
- t6:現在,HMI任務、CAN任務的優先順序都比接收任務高,它們都在等待佇列有空閒的空間;一旦接收任務讀出1個數據,會馬上被搶佔。被誰搶佔?誰等待最久?CAN任務!所以在t6時刻,切換到CAN任務。
- t7:CAN任務向佇列寫入資料,因為僅僅有一個空間供寫入,所以它馬上再次進入阻塞狀態。這時HMI任務、CAN任務都在等待空閒空間,只有接收任務可以繼續執行。
5.5 示例10: 傳輸大塊資料
本節程式碼為:FreeRTOS_10_queue_bigtransfer
。
FreeRTOS的佇列使用拷貝傳輸,也就是要傳輸uint32_t時,把4位元組的資料拷貝進佇列;要傳輸一個8位元組的結構體時,把8位元組的資料拷貝進佇列。
如果要傳輸1000位元組的結構體呢?寫佇列時拷貝1000位元組,讀佇列時再拷貝1000位元組?不建議這麼做,影響效率!
這時候,我們要傳輸的是這個巨大結構體的地址:把它的地址寫入佇列,對方從佇列得到這個地址,使用地址去訪問那1000位元組的資料。
使用地址來間接傳輸資料時,這些資料放在RAM裡,對於這塊RAM,要保證這幾點:
- RAM的所有者、操作者,必須清晰明瞭
這塊記憶體,就被稱為"共享記憶體"。要確保不能同時修改RAM。比如,在寫佇列之前只有由傳送者修改這塊RAM,在讀佇列之後只能由接收者訪問這塊RAM。 - RAM要保持可用
這塊RAM應該是全域性變數,或者是動態分配的記憶體。對於動然分配的記憶體,要確保它不能提前釋放:要等到接收者用完後再釋放。另外,不能是區域性變數。
FreeRTOS_10_queue_bigtransfer
程式會建立一個佇列,然後建立1個傳送任務、1個接收任務:
- 建立的佇列:長度為1,用來傳輸"char *"指標
- 傳送任務優先順序為1,在字元陣列中寫好資料後,把它的地址寫入佇列
- 接收任務優先順序為2,讀佇列得到"char *"值,把它打印出來
這個程式故意設定接收任務的優先順序更高,在它訪問陣列的過程中,接收任務無法執行、無法寫這個陣列。
main函式中建立了佇列、建立了傳送任務、接收任務,程式碼如下:
/* 定義一個字元陣列 */
static char pcBuffer[100];
/* vSenderTask被用來建立2個任務,用於寫佇列
* vReceiverTask被用來建立1個任務,用於讀佇列
*/
static void vSenderTask( void *pvParameters );
static void vReceiverTask( void *pvParameters );
/*-----------------------------------------------------------*/
/* 佇列控制代碼, 建立佇列時會設定這個變數 */
QueueHandle_t xQueue;
int main( void )
{
prvSetupHardware();
/* 建立佇列: 長度為1,資料大小為4位元組(存放一個char指標) */
xQueue = xQueueCreate( 1, sizeof(char *) );
if( xQueue != NULL )
{
/* 建立1個任務用於寫佇列
* 任務函式會連續執行,構造buffer資料,把buffer地址寫入佇列
* 優先順序為1
*/
xTaskCreate( vSenderTask, "Sender", 1000, NULL, 1, NULL );
/* 建立1個任務用於讀佇列
* 優先順序為2, 高於上面的兩個任務
* 這意味著讀佇列得到buffer地址後,本任務使用buffer時不會被打斷
*/
xTaskCreate( vReceiverTask, "Receiver", 1000, NULL, 2, NULL );
/* 啟動排程器 */
vTaskStartScheduler();
}
else
{
/* 無法建立佇列 */
}
/* 如果程式執行到了這裡就表示出錯了, 一般是記憶體不足 */
return 0;
}
傳送任務的函式中,現在全域性大陣列pcBuffer中構造資料,然後把它的地址寫入佇列,程式碼如下:
static void vSenderTask( void *pvParameters )
{
BaseType_t xStatus;
static int cnt = 0;
char *buffer;
/* 無限迴圈 */
for( ;; )
{
sprintf(pcBuffer, "www.100ask.net Msg %d\r\n", cnt++);
buffer = pcBuffer; // buffer變數等於陣列的地址, 下面要把這個地址寫入佇列
/* 寫佇列
* xQueue: 寫哪個佇列
* pvParameters: 寫什麼資料? 傳入資料的地址, 會從這個地址把資料複製進佇列
* 0: 如果佇列滿的話, 即刻返回
*/
xStatus = xQueueSendToBack( xQueue, &buffer, 0 ); /* 只需要寫入4位元組, 無需寫入整個buffer */
if( xStatus != pdPASS )
{
printf( "Could not send to the queue.\r\n" );
}
}
}
接收任務的函式中,讀取佇列、得到buffer的地址、列印,程式碼如下:
static void vReceiverTask( void *pvParameters )
{
/* 讀取佇列時, 用這個變數來存放資料 */
char *buffer;
const TickType_t xTicksToWait = pdMS_TO_TICKS( 100UL );
BaseType_t xStatus;
/* 無限迴圈 */
for( ;; )
{
/* 讀佇列
* xQueue: 讀哪個佇列
* &xReceivedStructure: 讀到的資料複製到這個地址
* xTicksToWait: 沒有資料就阻塞一會
*/
xStatus = xQueueReceive( xQueue, &buffer, xTicksToWait); /* 得到buffer地址,只是4位元組 */
if( xStatus == pdPASS )
{
/* 讀到了資料 */
printf("Get: %s", buffer);
}
else
{
/* 沒讀到資料 */
printf( "Could not receive from the queue.\r\n" );
}
}
}
執行結果如下圖所示:
5.6 示例11: 郵箱(Mailbox)
本節程式碼為:FreeRTOS_11_queue_mailbox
。
FreeRTOS的郵箱概念跟別的RTOS不一樣,這裡的郵箱稱為"櫥窗"也許更恰當:
- 它是一個佇列,佇列長度只有1
- 寫郵箱:新資料覆蓋舊資料,在任務中使用
xQueueOverwrite()
,在中斷中使用xQueueOverwriteFromISR()
。
既然是覆蓋,那麼無論郵箱中是否有資料,這些函式總能成功寫入資料。 - 讀郵箱:讀資料時,資料不會被移除;在任務中使用
xQueuePeek()
,在中斷中使用xQueuePeekFromISR()
。
這意味著,第一次呼叫時會因為無資料而阻塞,一旦曾經寫入資料,以後讀郵箱時總能成功。
main函式中建立了佇列(佇列長度為1)、建立了傳送任務、接收任務:
- 傳送任務的優先順序為2,它先執行
- 接收任務的優先順序為1
程式碼如下:
/* 佇列控制代碼, 建立佇列時會設定這個變數 */
QueueHandle_t xQueue;
int main( void )
{
prvSetupHardware();
/* 建立佇列: 長度為1,資料大小為4位元組(存放一個char指標) */
xQueue = xQueueCreate( 1, sizeof(uint32_t) );
if( xQueue != NULL )
{
/* 建立1個任務用於寫佇列
* 任務函式會連續執行,構造buffer資料,把buffer地址寫入佇列
* 優先順序為2
*/
xTaskCreate( vSenderTask, "Sender", 1000, NULL, 2, NULL );
/* 建立1個任務用於讀佇列
* 優先順序為1
*/
xTaskCreate( vReceiverTask, "Receiver", 1000, NULL, 1, NULL );
/* 啟動排程器 */
vTaskStartScheduler();
}
else
{
/* 無法建立佇列 */
}
/* 如果程式執行到了這裡就表示出錯了, 一般是記憶體不足 */
return 0;
}
傳送任務、接收任務的程式碼和執行流程如下:
- A:傳送任務先執行,馬上阻塞
- BC:接收任務執行,這是郵箱無資料,列印"Could not ..."。在傳送任務阻塞過程中,接收任務多次執行、多次列印。
- D:傳送任務從阻塞狀態退出,立刻執行、寫佇列
- E:傳送任務再次阻塞
- FG、HI、……:接收任務不斷"偷看"郵箱,得到同一個資料,打印出多個"Get: 0"
- J:傳送任務從阻塞狀態退出,立刻執行、覆蓋佇列,寫入1
- K:傳送任務再次阻塞
- LM、……:接收任務不斷"偷看"郵箱,得到同一個資料,打印出多個"Get: 1"
執行結果如下圖所示: