本文主要學習 FreeRTOS 中斷管理的相關知識,包括系統硬體中斷、 FreeRTOS 可管理的中斷、中斷屏蔽和一些其他註意事項等知識 ...
1、準備材料
STM32CubeMX軟體(Version 6.10.0)
Keil µVision5 IDE(MDK-Arm)
2、學習目標
本文主要學習 FreeRTOS 中斷管理的相關知識,包括系統硬體中斷、 FreeRTOS 可管理的中斷、中斷屏蔽和一些其他註意事項等知識
3、前提知識
3.1、STM32 的硬體中斷
根據STM32CubeMX教程4 EXTI 按鍵外部中斷實驗 “3、中斷系統概述表” 小節內容可知
- STM32F4 系列有 10 個系統中斷和82個可屏蔽的外部中斷
- 嵌套向量中斷控制器(NVIC)採用 4 位二進位數表示中斷優先順序,這 4 位二進位數表示的中斷優先順序又分為了搶占優先順序和次優先順序
當啟用FreeRTOS之後,NVIC中斷分組策略採用 4 位搶占優先順序且不可修改,對於 STM32 的硬體優先順序來說,優先順序數字越小表示優先順序越高,最高優先順序為0,如下所示為 STM32 的中斷列表
3.2、FreeRTOS 可管理的中斷
對於 STM32 處理器所有的硬體中斷來說,其中有些可以被 FreeRTOS 軟體管理,而有些特別重要的中斷則不能夠被 FreeRTOS 軟體所管理,這很好理解,比如系統的硬體 Reset 中斷,如果 Reset 中斷可以被FreeRTOS所管理,那麼在系統死機時用戶需要硬體複位,但 FreeRTOS 不能響應最終導致無法複位從而卡死
那麼哪些硬體中斷可以被 FreeRTOS 所管理呢?
這由 configLIBRARY_LOWEST_INTERRUPT_PRIORITY (中斷的最低優先順序數值) 和 configLIBRARY_MAX_SYSCALL_INTERRUPT_PRIORITY (FreeRTOS可管理的最高優先順序) 兩個參數決定,由於 NVIC 中斷分組策略採用 4 位搶占優先順序,因此中斷最低優先順序數值為 15 ,而 FreeRTOS 可管理的最高優先順序預設設置為 5
當配置參數 configLIBRARY_LOWEST_INTERRUPT_PRIORITY = 15 , configLIBRARY_MAX_SYSCALL_INTERRUPT_PRIORITY = 5 時,則表示在 STM32 的所有硬體中斷中優先順序為 0~4 的中斷 FreeRTOS 不可管理,而對於中斷優先順序為 5~15 的中斷 FreeRTOS 可以管理,具體如下圖所示
3.3、何為上下文?
在操作系統和嵌入式系統中,上下文(Context)是指程式執行過程中的當前狀態,包括所有的寄存器值、程式計數器(PC)值以及其他與執行環境相關的狀態信息。上下文記錄了程式執行的位置和狀態,使得程式可以在中斷、任務切換或函數調用等場景下進行正確的恢復和繼續執行
在 FreeRTOS 中,上下文通常與任務(Task)或中斷處理函數相關聯。當任務切換髮生時,當前任務的上下文會被保存,然後將控制權轉移到下一個任務,該任務的上下文會被恢復以便繼續執行。類似地,當中斷發生時,處理器會保存當前執行任務的上下文,併在中斷處理完畢後,恢復之前任務的上下文以繼續執行。
3.4、在 ISR 中使用 FreeRTOS API 函數
3.4.1、中斷安全版本 API
通常需要在中斷服務常式 (ISR) 中使用 FreeRTOS API 函數提供的功能,但許多 FreeRTOS API 函數執行的操作在 ISR 內無效,比如能夠讓任務進入阻塞狀態的 API 函數,如果從 ISR 調用這些 API 函數,因為它不是從任務調用,所以沒有有效的調用任務使其進入阻塞狀態
因此對於一些 API 函數,FreeRTOS 提供了兩種不同版本,一種版本供任務使用,另一種版本供 ISR 使用,在 ISR 中使用的函數名稱後面帶有 “FromISR” 的尾碼,關於這種設計的優缺點,感興趣的讀者可以自行閱讀 “Mastering_the_FreeRTOS_Real_Time_Kernel-A_Hands-On_Tutorial_Guide.pdf” 6.2小節內容
3.4.2、xHigherPriorityTaskWoken 參數
xHigherPriorityTaskWoken 參數是中斷安全版本 API 中常見的一個參數,該參數用於通知應用程式編寫者在退出 ISR 時是否應該進行上下文切換,因為在執行某個中斷期間,在進入中斷時和退出中斷後多個任務的狀態可能發生了改變,也即可能存在中斷某個任務,但返回另外一個任務的情況發生
如果通過 FreeRTOS API 函數解鎖的任務的優先順序高於運行狀態任務的優先順序,則根據 FreeRTOS 調度策略,應切換到更高優先順序的任務,但究竟何時實際切換到更高優先順序的任務則取決於調用 API 函數的上下文,有以下兩種情況
- 如果 API 函數是從任務中調用的,那麼在搶占式調度策略下,在 API 函數退出之前,API 函數內會自動切換到更高優先順序的任務
- 如果 API 函數是從 ISR 中調用的,那麼在中斷中不會自動切換到更高優先順序的任務,但是可以設置一個變數來通知應用程式編寫者應該執行上下文切換,也就是 FreeRTOS 中斷安全版本的 API 函數中經常見到的 xHigherPriorityTaskWoken 參數
如果應執行上下文切換,則中斷安全 API 函數會將 pxHigherPriorityTaskWoken 設置為 pdTRUE ,而且只能將其設置為 pdTRUE ,所以 pxHigherPriorityTaskWoken 指向的變數必須在第一次使用之前初始化為 pdFALSE
如果不通過上述的方法在退出 ISR 前執行上下文切換,那麼最壞的情況就是本來應該在退出 ISR 時切換到某個高優先順序的任務進行執行,但現在只能將其轉為就緒狀態,直到下一個滴答定時器到來進行上下文切換其才會轉為運行狀態
3.4.3、portYIELD_FROM_ISR() 和 portEND_SWITCHING_ISR() 巨集
在FreeRTOS教程2 任務管理 文章 "3.8、任務調度方法" 小節中,介紹了主動讓位於另一項同等優先順序任務的 API 函數 taskYIELD() ,它是一個可以在任務中調用來請求上下文切換的巨集,portYIELD_FROM_ISR() 和 portEND_SWITCHING_ISR() 都是 taskYIELD() 的中斷安全版本,他們兩個的使用方式相同,並且執行相同的操作
3.4.4、簡單總結
所以根據上面幾個小節的敘述,如果我們希望在 ISR 中使用 FreeRTOS 提供的 API 函數,則應該使用這些 API 函數的中斷安全版本,並且通過 xHigherPriorityTaskWoken 參數和 portYIELD_FROM_ISR() 巨集在退出 ISR 之前進行可能的上下文切換,其可能的一種應用結構如下所示
/*一個可能的在中斷中使用FreeRTOS API函數,然後進行上下文切換的例子*/
void An_Interrupt_Instance_Function(void)
{
//定義一個用於通知應用程式編程者是否應該進行上下文切換的變數,必須初始化為pdFALSE
BaseType_t highTaskWoken = pdFALSE;
//使用二值信號量API函數做演示
if(BinarySem_Handle != NULL)
{
//將中斷安全版本API函數的pxHigherPriorityTaskWoken參數指向 highTaskWoken
xSemaphoreGiveFromISR(BinarySem_Handle, &highTaskWoken);
//根據highTaskWoken決定是否要進行上下文切換
portYIELD_FROM_ISR(highTaskWoken);
}
}
但是不是所有中斷中都可以使用 FreeRTOS 提供的 API 函數,在 ISR 中使用 FreeRTOS API 函數總結如下所述
- 對於FreeRTOS可屏蔽的ISR中,如果要調用 FreeRTOS API 函數,則應該使用 FreeRTOS API 的中斷安全版本函數(函數名末尾為FromISR或FROM_ISR),不可以使用任務級的API函數
- 對於FreeRTOS不可屏蔽的ISR中,不能夠調用任何 FreeRTOS API函數
另外在 STM32CubeMX 軟體 NVIC 配置界面中,如果在某個中斷後面勾選了 “Uses FreeRTOS functions” 選項,根據上面的兩點描述可知,只能在 FreeRTOS 可屏蔽的ISR中使用 FreeRTOS API 函數,所以該中斷優先順序可選範圍會被強制到 15~5 之間,具體如下圖所示
3.5、任務優先順序和中斷優先順序
任務優先順序為軟體設置的一個屬性,設置範圍為1~(configMAX_PRIORITIES-1),數字越大優先順序越高,在搶占式調度方式中高優先順序的任務可以搶占低優先順序的任務
中斷優先順序為硬體響應優先順序,中斷分組策略4位全用於搶占優先順序的中斷優先順序數字設置範圍為0-15,數字越小優先順序越高
對於大多數的系統,其既會存在多個不同優先順序的任務,同時也會存在多個不同優先順序的中斷,它們之間的執行順序應該如下圖所示
3.6、延遲中斷處理
通常認為最佳實踐是使 ISR 儘可能短,下麵列出了可能的幾條原因
- 即使任務被分配了非常高的優先順序,它們也只有在硬體沒有中斷服務時才會運行
- ISR 會擾亂(添加“抖動”)任務的開始時間和執行時間
- 應用程式編寫者需要考慮任務和 ISR 同時訪問變數、外設和記憶體緩衝區等資源的後果,並防範這些資源
- 某些 FreeRTOS 埠允許中斷嵌套,但中斷嵌套會增加複雜性並降低可預測性,中斷越短,嵌套的可能性就越小
什麼時延遲中斷處理?
中斷服務程式必須記錄中斷原因,並清除中斷。中斷所需的任何其他處理通常可以在任務中執行,從而允許中斷服務常式儘可能快地退出,這稱為“延遲中斷處理”,因為中斷所需的處理從 ISR “延遲” 到任務。舉個例子,比如在 ADC 周期採集中,當一輪採集完成之後,ADC 採集完成中斷回調函數只負責將採集完成的值寫入緩存區,然後由其他任務對緩存區中的數據進行更複雜處理
將中斷處理推遲到任務還允許應用程式編寫者相對於應用程式中的其他任務確定處理的優先順序,並能夠使用所有 FreeRTOS API 函數
如果中斷處理被推遲的任務的優先順序高於任何其他任務的優先順序,則處理將立即執行,就像處理已在 ISR 本身中執行一樣。這種場景如下圖所示,其中任務 1 是普通應用程式任務,任務 2 是中斷處理被推遲的任務
那麼什麼情況下需要進行延遲中斷處理操作呢?
沒有具體絕對的規則,在以下列出的幾點情況下,將處理推遲到任務可能比較有用:
- 中斷所需的處理並不簡單。比如上面的舉例,如果 ADC 僅僅需要採集值,那麼在採集完成中斷回調函數中將採集值寫入緩存區即可,但是如果還需要對採集值進行複雜處理,那麼最好推遲到任務中完成
- 中斷處理是不確定的 - 這意味著事先不知道處理需要多長時間。
3.7、進行中斷屏蔽
FreeRTOS 中為什麼要屏蔽中斷?
想象這樣一個場景,當一個中等優先順序的任務 TASK1 正在通過串口輸出字元串 “Hello world!” 並且剛好輸出到 ”Hello“ 時,另外一個高級優先順序的任務 TASK2 突然搶占 TASK1 ,然後通過串口輸出字元串 “lc_guo” ,當兩個任務均執行完畢之後,你可能會在串口接受框中看到 ”Hellolc_guo world!“ 字元串
上述場景最終的結果與我們期望任務輸出的字元串不符,在操作系統中稱 TASK1 輸出字元串的操作不是原子的,可能被打斷的,因此在某些時候需要我們屏蔽掉中斷以保證某些操作為原子的,可以連續執行完不被打斷的,能夠連續執行完且不被打斷的程式段稱其為臨界段
那 FreeRTOS 中應該如何屏蔽中斷?
在 FreeRTOS 中提供了三組巨集函數方便用戶在合適的位置屏蔽中斷,在功能上屏蔽中斷和定義臨界代碼段幾乎是相同的,這幾組函數通常成對使用
/**
* @brief 屏蔽FreeRTOS可管理的MCU中斷
* @retval None
*/
void taskDISABLE_INTERRUPTS(void);
/**
* @brief 解除屏蔽FreeRTOS可管理的MCU中斷
* @retval None
*/
void taskENABLE_INTERRUPTS(void);
/**
* @brief 開始臨界代碼段
* @retval None
*/
void taskENTER_CRITICAL(void);
/**
* @brief 退出臨界代碼段
* @retval None
*/
void taskEXIT_CRITICAL(void);
/**
* @brief 開始臨界代碼段的中斷安全版本
* @retval 返回中斷屏蔽狀態uxSavedInterruptStatus,作為參數用於匹配的taskEXIT_CRITICAL_FROM_ISR()調用
*/
UBaseType_t taskENTER_CRITICAL_FROM_ISR(void);
/**
* @brief 退出臨界代碼段的中斷安全版本
* @param uxSavedInterruptStatus:進入臨界代碼段時返回的中斷屏蔽狀態,taskENTER_CRITICAL_FROM_ISR()返回的值
* @retval None
*/
void taskEXIT_CRITICAL_FROM_ISR(UBaseType_t uxSavedInterruptStatus);
4、實驗一:中斷各種特性測試
4.1、實驗目的
- 啟動 RTC 周期喚醒中斷,在周期喚醒中通過串口 USART1 不斷輸出當前 RTC 時間
- 創建任務 TASK_TEST ,在該任務中通過串口 USART1 輸出提示信息即可
4.2、CubeMX相關配置
首先讀者應按照 “FreeRTOS教程1 基礎知識” 章節配置一個可以正常編譯通過的 FreeRTOS 空工程,然後在此空工程的基礎上增加本實驗所提出的要求
本實驗需要初始化 USART1 作為輸出信息渠道,具體配置步驟請閱讀 “STM32CubeMX教程9 USART/UART 非同步通信” ,如下圖所示
本實驗需要配置 RTC 周期喚醒中斷,具體配置步驟和參數介紹讀者可閱讀 ”STM32CubeMX教程10 RTC 實時時鐘 - 周期喚醒、鬧鐘A/B事件和備份寄存器“ 實驗,此處不再贅述,這裡參數、中斷、時鐘如下圖所示
配置 Clock Configuration 和 Project Manager 兩個頁面,接下來直接單擊 GENERATE CODE 按鈕生成工程代碼即可
4.3、添加其他必要代碼
按照 “STM32CubeMX教程9 USART/UART 非同步通信” 實驗 “6、串口printf重定向”小節增加串口 printf 重定向代碼,具體不再贅述
然後在 rtc.c 文件下方重新實現 RTC 的周期喚醒回調函數,在該函數體內獲取當前 RTC 時間並通過 USART1 將時間輸出到串口助手,具體如下所述
/*周期喚醒回調函數*/
void HAL_RTCEx_WakeUpTimerEventCallback(RTC_HandleTypeDef *hrtc)
{
RTC_TimeTypeDef sTime;
RTC_DateTypeDef sDate;
if(HAL_RTC_GetTime(hrtc, &sTime, RTC_FORMAT_BIN) == HAL_OK)
{
HAL_RTC_GetDate(hrtc, &sDate, RTC_FORMAT_BIN);
char str[22];
sprintf(str,"RTC Time= %2d:%2d:%2d\r\n",sTime.Hours,sTime.Minutes,sTime.Seconds);
printf("%s", str);
}
}
最後在 freertos.c 中添加任務函數體內代碼即可,這裡無需實現具體功能,僅通過 USART1 串口輸出信息告知用戶該任務已執行即可,具體如下所述
/*測試任務函數*/
void TASK_TEST(void *argument)
{
/* USER CODE BEGIN TASK_TEST */
/* Infinite loop */
for(;;)
{
printf("TASK_TEST\r\n");
osDelay(pdMS_TO_TICKS(500));
}
/* USER CODE END TASK_TEST */
}
4.4、燒錄驗證
燒錄程式,打開串口助手,由於周期喚醒中斷每隔 1s 執行依次,TASK_TEST 任務大概每隔 500ms 執行有一次,因此通過串口助手輸出信息可以發現,每輸出兩次 ”TASK_TEST“ 就會輸出一次當前 RTC 時間,和預期一致,具體如下圖所示
上述任務流程應該如下圖所示
4.5、各種特性測試
4.5.1、中斷如果處理時間較長呢?
修改 RTC 周期喚醒中斷函數體,在函數體末尾增加 1s 延時函數 HAL_Delay(1000); 模擬中斷處理時間較長的情況,註意由於 RTC 周期喚醒中斷優先順序為 1 ,因此不能調用任何 FreeRTOS API 函數,包括延時函數,任務 TASK_TEST 不做任何改動,將修改後的程式重新編譯燒錄,觀察串口助手的輸出信息,具體如下圖所示
可以發現,只有最開始測試任務 TASK_TEST 執行了兩次,一旦 RTC 周期喚醒被執行那麼之後測試任務便得不到執行,為什麼會這樣?RTC 周期喚醒每隔 1s 執行一次,執行一次之後延時 1s 占用處理器,不斷迴圈,導致處理器沒有任何機會去處理 TASK_TEST
上述任務流程應該如下圖所示
4.5.2、任務如果處理時間較長呢?
修改測試任務 TASK_TEST 函數體,將其可以進入阻塞狀態的延時函數 osDelay() 修改為 HAL_Delay() 函數,同時將延時時間從 500ms 增加至 2s ,用於模擬任務一直運行的情況,RTC 周期喚醒中斷函數與 “4.3、添加其他必要代碼” 小節一致,具體如下所示
void TASK_TEST(void *argument)
{
/* USER CODE BEGIN TASK_TEST */
/* Infinite loop */
for(;;)
{
printf("TASK_TEST\r\n");
HAL_Delay(2000);
}
/* USER CODE END TASK_TEST */
}
將修改後的程式重新編譯燒錄,觀察串口助手的輸出信息,具體如下圖所示,從圖中可知 RTC 運行正常,本來應該連續運行 2s 的 TASK_TEST 並沒有影響到每隔 1s 輸出 RTC 時間的周期喚醒中斷,說明中斷搶占了 TASK_TEST 得到了執行,也就是說雖然我們希望 TASK_TEST 測試任務連續運行 2s ,但是其並沒有真正連續運行 2s,其在大概 1s 的時候被中斷了
4.5.3、進行中斷屏蔽
上述 ”4.5.2、任務如果處理時間較長呢?“ 小節闡述了一個問題,有時候我們希望我們的 TASK_TEST 任務是原子式執行的,不希望被中斷打斷,所以我們需要在任務函數體內屏蔽中斷,修改 TASK_TEST 任務函數體如下所示
void TASK_TEST(void *argument)
{
/* USER CODE BEGIN TASK_TEST */
/* Infinite loop */
for(;;)
{
//進入臨界段
//taskDISABLE_INTERRUPTS();
taskENTER_CRITICAL();
printf("TASK_TEST\r\n");
HAL_Delay(2000);
//退出臨界段
//taskENABLE_INTERRUPTS();
taskEXIT_CRITICAL();
}
/* USER CODE END TASK_TEST */
}
同時別忘記,FreeRTOS能夠屏蔽中斷優先順序為5~15,因此我們還需要在 STM32CubeMX 軟體的 NVIC 中將 RTC 周期喚醒中斷優先順序設置到該範圍內,這裡筆者將其設置為了 7 ,具體如下圖所示
將修改後的程式重新編譯燒錄,觀察串口助手的輸出信息,具體如下圖所示,可以發現 RTC 周期喚醒函數每隔 2s 才會得到一次輸出,這說明 TASK_TEST 任務整個函數體得到了連續運行,成功屏蔽掉了 RTC 周期喚醒中斷
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