1. 程式人生 > 其它 >韋東山freeRTOS系列教程之【第五章】佇列(queue)

韋東山freeRTOS系列教程之【第五章】佇列(queue)

目錄

需要獲取更好閱讀體驗的同學,請訪問我專門設立的站點檢視,地址:http://rtos.100ask.net/

系列教程總目錄

本教程連載中,篇章會比較多,為方便同學們閱讀,點選這裡可以檢視文章的 目錄列表,目錄列表頁面地址:

https://blog.csdn.net/thisway_diy/article/details/121399484

概述

佇列(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"

執行結果如下圖所示:

獲取更多嵌入式乾貨,請關注威信baiwenkeji