本文主要學習 FreeRTOS 消息隊列的相關知識,包括消息隊列概述、創建刪除複位隊列、寫入/讀取數據到隊列等關於隊列的基礎知識 ...
1、準備材料
STM32CubeMX軟體(Version 6.10.0)
Keil µVision5 IDE(MDK-Arm)
2、學習目標
本文主要學習 FreeRTOS 消息隊列的相關知識,包括消息隊列概述、創建刪除複位隊列、寫入/讀取數據到隊列等關於隊列的基礎知識
3、前提知識
3.1、什麼是消息隊列?
在一個實時操作系統構成的完整項目中一般會存在多個任務和中斷,多個任務之間、任務與中斷之間往往需要進行通信, FreeRTOS 中所有的通信與同步機制都是基於隊列來實現的,我們可以把隊列結構想象成如下圖所示樣子
在實際使用中,隊列深度以及隊列中數據類型都可以由用戶自定義,消息隊列是一個共用的存儲區域,其可以被多個進程寫入數據,同時也可以被多個進程讀取數據,為了讓接收任務知道數據的來源,以確定數據應該如何處理,通常可以使用單個隊列來傳輸具有兩者的結構數據的值和結構欄位中包含的數據源,如下圖所示
3.2、創建隊列
隊列在使用前必須先創建,和創建任務類似, FreeRTOS 也提供了動態或靜態記憶體分配創建隊列兩個 API 函數,具體函數聲明如下所示
/**
* @brief 動態分配記憶體創建隊列函數
* @param uxQueueLength:隊列深度
* @param uxItemSize:隊列中數據單元的長度,以位元組為單位
* @retval 返回創建成功的隊列句柄,如果返回NULL則表示因記憶體不足創建失敗
*/
QueueHandle_t xQueueCreate(UBaseType_t uxQueueLength, UBaseType_t uxItemSize);
/**
* @brief 靜態分配記憶體創建隊列函數
* @param uxQueueLength:隊列深度
* @param uxItemSize:隊列中數據單元的長度,以位元組為單位
* @param pucQueueStorageBuffer:隊列棧空間數組
* @param pxQueueBuffer:指向StaticQueue_t類型的用於保存隊列數據結構的變數
* @retval 返回創建成功的隊列句柄,如果返回NULL則表示因記憶體不足創建失敗
*/
QueueHandle_t xQueueCreateStatic(UBaseType_t uxQueueLength,
UBaseType_t uxItemSize,
uint8_t *pucQueueStorageBuffer,
StaticQueue_t *pxQueueBuffer);
/*example:創建一個深度為5,隊列單元占uint16_t大小隊列*/
QueueHandle_t QueueHandleTest;
QueueHandleTest = xQueueCreate(5, sizeof(uint16_t));
3.3、向隊列寫入數據
任務或者中斷向隊列寫入數據稱為發送消息。通常情況下,隊列被作為 FIFO(先入先出)使用,即數據由隊列尾部進入,從隊列首讀出,當然可以通過更改寫入方式將隊列作為 LIFO(後入先出)使用,向隊列中寫入數據主要有三組 FreeRTOS API 函數,具體如下所示
/**
* @brief 向隊列後方發送數據(FIFO先入先出)
* @param xQueue:要寫入數據的隊列句柄
* @param pvItemToQueue:要寫入的數據
* @param xTicksToWait:阻塞超時時間,單位為節拍數,portMAXDELAY表示無限等待
* @retval pdPASS:數據發送成功,errQUEUE_FULL:隊列滿無法寫入
*/
BaseType_t xQueueSend(QueueHandle_t xQueue,
const void * pvItemToQueue,
TickType_t xTicksToWait);
/**
* @brief 向隊列後方發送數據(FIFO先入先出),與xQueueSend()函數一致
*/
BaseType_t xQueueSendToBack(QueueHandle_t xQueue,
const void * pvItemToQueue,
TickType_t xTicksToWait);
/**
* @brief 向隊列前方發送數據(LIFO後入先出)
*/
BaseType_t xQueueSendToFront(QueueHandle_t xQueue,
const void * pvItemToQueue,
TickType_t xTicksToWait);
/**
* @brief 以下三個函數為上述三個函數的中斷安全版本
* @param pxHigherPriorityTaskWoken:用於通知應用程式編寫者是否應該執行上下文切換
*/
BaseType_t xQueueSendFromISR(QueueHandle_t xQueue,
const void *pvItemToQueue,
BaseType_t *pxHigherPriorityTaskWoken);
BaseType_t xQueueSendToBackFromISR(QueueHandle_t xQueue,
const void *pvItemToQueue,
BaseType_t *pxHigherPriorityTaskWoken)
BaseType_t xQueueSendToFrontFromISR(QueueHandle_t xQueue,
const void *pvItemToQueue,
BaseType_t *pxHigherPriorityTaskWoken);
另外還有一組稍微特殊的向隊列寫入數據的 FreeRTOS API 函數,這組函數只用於隊列長度為 1 的隊列,在隊列已滿時會覆蓋掉隊列原來的數據,具體如下所述
/**
* @brief 向長度為1的隊發送數據
* @param xQueue:要寫入數據的隊列句柄
* @param pvItemToQueue:要寫入的數據
* @retval pdPASS:數據發送成功,errQUEUE_FULL:隊列滿無法寫入
*/
BaseType_t xQueueOverwrite(QueueHandle_t xQueue, const void *pvItemToQueue);
/**
* @brief 以下函數為上述函數的中斷安全版本
* @param pxHigherPriorityTaskWoken:用於通知應用程式編寫者是否應該執行上下文切換
*/
BaseType_t xQueueOverwriteFromISR(QueueHandle_t xQueue,
const void *pvItemToQueue,
BaseType_t *pxHigherPriorityTaskWoken);
3.4、從隊列接收數據
任務或者中斷從隊列中讀取數據稱為接收消息。從隊列中讀取數據主要有兩組 FreeRTOS API 函數,具體如下所示
/**
* @brief 從隊列頭部接收數據單元,接收的數據同時會從隊列中刪除
* @param xQueue:被讀隊列句柄
* @param pvBuffer:接收緩存指針
* @param xTicksToWait:阻塞超時時間,單位為節拍數
* @retval pdPASS:數據接收成功,errQUEUE_FULL:隊列空無讀取到任何數據
*/
BaseType_t xQueueReceive(QueueHandle_t xQueue,
void *pvBuffer,
TickType_t xTicksToWait);
/**
* @brief 從隊列頭部接收數據單元,不從隊列中刪除接收的單元
*/
BaseType_t xQueuePeek(QueueHandle_t xQueue,
void *pvBuffer,
TickType_t xTicksToWait);
/**
* @brief 以下兩個函數為上述兩個函數的中斷安全版本
* @param pxHigherPriorityTaskWoken:用於通知應用程式編寫者是否應該執行上下文切換
*/
BaseType_t xQueueReceiveFromISR(QueueHandle_t xQueue,
void *pvBuffer,
BaseType_t *pxHigherPriorityTaskWoken);
BaseType_t xQueuePeekFromISR(QueueHandle_t xQueue, void *pvBuffer);
3.5、查詢隊列
FreeRTOS 還提供了一些用於查詢隊列當前有效數組單元個數和剩餘可用空間數的 API 函數,具體如下所述
/**
* @brief 查詢隊列剩餘可用空間數
* @param xQueue:被查詢的隊列句柄
* @retval 返回隊列中可用的空間數
*/
UBaseType_t uxQueueSpacesAvailable(QueueHandle_t xQueue);
/**
* @brief 查詢隊列有效數據單元個數
* @param xQueue:被查詢的隊列句柄
* @retval 當前隊列中保存的數據單元個數
*/
UBaseType_t uxQueueMessagesWaiting(const QueueHandle_t xQueue);
/**
* @brief 查詢隊列有效數據單元個數函數的中斷安全版本
*/
UBaseType_t uxQueueMessagesWaitingFromISR(const QueueHandle_t xQueue);
3.6、阻塞狀態
當出現下麵幾種情況時,任務會進入阻塞狀態
- 當某個任務向隊列寫入數據,但是被寫的隊列已滿時,任務將進入阻塞狀態等待隊列出現新的位置
- 當某個任務從隊列讀取數據,但是被讀的隊列是空時,任務將進入阻塞狀態等待隊列出現新的數據
當出現下麵幾種情況時,任務會退出阻塞狀態
- 進入阻塞狀態的任務達到設置的阻塞超時時間之後會退出阻塞狀態
- 向滿隊列中寫數據的任務等到隊列中出現新的位置
- 從空隊列中讀數據的任務等到隊列.中出現新的數據
當存在多個任務處於阻塞狀態時,如果同時滿足解除阻塞的條件,則所有等待任務中 優先順序最高的任務 或者 優先順序均相同但等待最久的任務 將被解除阻塞狀態
3.7、刪除隊列
/**
* @brief 刪除隊列
* @param pxQueueToDelete:要刪除的隊列句柄
* @retval None
*/
void vQueueDelete(QueueHandle_t pxQueueToDelete);
3.8、複位隊列
/**
* @brief 將隊列重置為其原始空狀態
* @param xQueue:要複位的隊列句柄
* @retval pdPASS(從FreeRTOS V7.2.0之後)
*/
BaseType_t xQueueReset(QueueHandle_t xQueue);
3.9、隊列讀寫過程
如下圖展示了用作 FIFO 的隊列寫入和讀取數據的情況的具體過程 (註釋1)
4、實驗一:嘗試隊列基本操作
4.1、實驗目標
- 創建一個用於任務間、任務與中斷間信息傳輸的深度為 10 的隊列 TEST_QUEUE
- 創建一個任務 TASK_SEND 實現按鍵掃描響應,當 KEY2、KEY1、KEY0 按鍵按下時分別向隊列 TEST_QUEUE 中發送不同消息
- 創建一個任務 TASK_RECEIVE 實現從隊列 TEST_QUEUE 中接收信息,根據接收到的不同信息通過串口輸出不同內容
- 啟動一個 RTC 周期喚醒中斷,每隔 1s 向隊列 TEST_QUEUE 中發送一條消息
4.2、CubeMX相關配置
首先讀者應按照 “FreeRTOS教程1 基礎知識” 章節配置一個可以正常編譯通過的 FreeRTOS 空工程,然後在此空工程的基礎上增加本實驗所提出的要求
本實驗需要初始化開發板上 KEY2、KEY1和KEY0 用戶按鍵做普通輸入,具體配置步驟請閱讀“STM32CubeMX教程3 GPIO輸入 - 按鍵響應”,註意雖開發板不同但配置原理一致,如下圖所示
本實驗需要初始化 USART1 作為輸出信息渠道,具體配置步驟請閱讀“STM32CubeMX教程9 USART/UART 非同步通信”,如下圖所示
本實驗需要配置 RTC 周期喚醒中斷,具體配置步驟和參數介紹讀者可閱讀”STM32CubeMX教程10 RTC 實時時鐘 - 周期喚醒、鬧鐘A/B事件和備份寄存器“實驗,此處不再贅述,這裡參數、時鐘配置如下圖所示
由於需要在 RTC 周期喚醒中斷中使用 FreeRTOS 的 API 函數,因此 RTC 周期喚醒中斷的優先順序應該設置在 15~5 之間,此處設置為 7 ,具體如下圖所示
單擊 Middleware and Software Packs/FREERTOS,在 Configuration 中單擊 Tasks and Queues 選項卡,首先雙擊預設任務修改其參數,然後單擊 Add 按鈕按要求增加另外一個任務,具體如下圖所示
然後在下方單擊 Add 按鈕增加一個深度為 10 的隊列,具體如下圖所示
配置 Clock Configuration 和 Project Manager 兩個頁面,接下來直接單擊 GENERATE CODE 按鈕生成工程代碼即可
4.3、添加其他必要代碼
按照 “STM32CubeMX教程9 USART/UART 非同步通信” 實驗 “6、串口printf重定向” 小節增加串口 printf 重定向代碼,具體不再贅述
首先應該在 freertos.c 中添加使用到的頭文件,如下所述
#include "stdio.h"
#include "queue.h"
然後在 rtc.c 文件下方重新實現 RTC 的周期喚醒回調函數,在該函數體內發送數據 ”9“ 到隊列 TEST_QUEUE 中,具體如下所述
/*周期喚醒回調函數*/
void HAL_RTCEx_WakeUpTimerEventCallback(RTC_HandleTypeDef *hrtc)
{
uint16_t key_value = 9;
BaseType_t pxHigherPriorityTaskWoken;
//向隊列中發送數據,中斷安全版本
xQueueSendToBackFromISR(TEST_QUEUEHandle, &key_value, &pxHigherPriorityTaskWoken);
//進行上下文切換
portYIELD_FROM_ISR(pxHigherPriorityTaskWoken);
}
最後在 freertos.c 中添加任務函數體內代碼即可,任務 TASK_SEND 負責當有按鍵按下時發送不同的數據到隊列 TEST_QUEUE 中,任務 TASK_RECEIVE 則負責當隊列中有數據時從隊列中讀取數據並通過串口輸出給用戶 ,具體如下所述
/*發送任務*/
void TASK_SEND(void *argument)
{
/* USER CODE BEGIN TASK_SEND */
uint16_t key_value = 0;
/* Infinite loop */
for(;;)
{
key_value = 0;
//按鍵KEY2按下
if(HAL_GPIO_ReadPin(KEY2_GPIO_Port,KEY2_Pin) == GPIO_PIN_RESET)
key_value = 3;
//按鍵KEY1按下
if(HAL_GPIO_ReadPin(KEY1_GPIO_Port,KEY1_Pin) == GPIO_PIN_RESET)
key_value = 2;
//按鍵KEY0按下
if(HAL_GPIO_ReadPin(KEY0_GPIO_Port,KEY0_Pin) == GPIO_PIN_RESET)
key_value = 1;
//如果有按鍵按下
if(key_value != 0)
{
BaseType_t err = xQueueSendToBack(TEST_QUEUEHandle, &key_value, pdMS_TO_TICKS(50));
if(err == errQUEUE_FULL)
{
xQueueReset(TEST_QUEUEHandle);
}
//按鍵消抖
osDelay(300);
}
else
osDelay(10);
}
/* USER CODE END TASK_SEND */
}
/*接收任務*/
void TASK_RECEIVE(void *argument)
{
/* USER CODE BEGIN TASK_RECEIVE */
UBaseType_t msgCount=0,freeSpace=0;
uint16_t key_value=0;
/* Infinite loop */
for(;;)
{
msgCount = uxQueueMessagesWaiting(TEST_QUEUEHandle);
freeSpace = uxQueueSpacesAvailable(TEST_QUEUEHandle);
BaseType_t result = xQueueReceive(TEST_QUEUEHandle, &key_value, pdMS_TO_TICKS(50));
if(result != pdTRUE)
continue;
printf("msgCount: %d, freeSpace: %d, key_value: %d\r\n", (uint16_t)msgCount, (uint16_t)freeSpace, key_value);
osDelay(100);
}
/* USER CODE END TASK_RECEIVE */
}
4.4、燒錄驗證
燒錄程式,打開串口助手,可以發現每隔一定時間 TASK_RECEIVE 任務會從隊列中接收到 ”9“,當按鍵 KEY2 按下時 TASK_SEND 任務向隊列中發送 ”3“,同時 TASK_RECEIVE 任務會從隊列中接收到 ”3“ 表示任務 TASK_SEND 發送成功,同理按鍵 KEY1 按下時發送接收 ”2“ ,按鍵 KEY0 按下時發送接收 ”1“ ,整個過程串口輸出信息如下圖所示
5、註釋詳解
註釋1:圖片來源於 Mastering_the_FreeRTOS_Real_Time_Kernel-A_Hands-On_Tutorial_Guide.pdf
參考資料
Mastering_the_FreeRTOS_Real_Time_Kernel-A_Hands-On_Tutorial_Guide.pdf