Skip to main content

淺談優先權,從ARM Cortex-M到FreeRTOS設定

Opass
A life well lived

什麼是Exception?

任何讓程式脫離正常的執行流程的事件被稱為Exception。當Exception發生時,程式會停止手邊的工作,然後跳去執行Exception Handler,執行完後再回來繼續執行手邊的工作。

什麼是Interrupt?

在ARM的架構上來說,Interrupt是一種Exception,Interrupt通常是週邊裝置或是外部輸入所產生,也可以透過軟體設定產生。Interrupt的Exception Handler同時也被稱作ISR(Interrupt Service Routine)

Exception對Cortex-M而言有什麼意義?

Cortex-M有幾個不同的exception來源,但都會先交給NVIC處理。NVIC會負責處理Interrupt Request(IRQs)和Non-Maskable Interrupt(NVI) Request。

IRQs和NMI差再哪裡?

  • IRQ
    • 通常是週邊或外部輸入產生的
  • NMI
    • 通常是給watchdog timer(看門狗,一定時間處理器沒有回應時會主動打斷處理器)和brownout detector(處理器電壓偵測,當處理器的電壓低於某個水平時會發出警告) 另外還有SysTick,是處理器內部的timer,會週期性的對處理器發出中斷。通常是給embedded-OS使用。

下面這張表圖代表Cortex-M3, M4的exception type 可以觀察到exception number越小,代表事情越大條。而編號16~255總共240個例外都交給Interrupt使用。

雖然處理器提供了高達240個IRQ,但一般實作上並不會全部使用,一般只會使用16~100個Interrupt,好處是簡化設計,同時減少電力消耗。舉例而言,STM32F429的startup.s中 (請參閱Line152~242),只使用到了91個interrupt。

談NVIC,Nested vectored interrupt controller

NVIC是Cortex-M處理器的一部份。負責處理例外和中斷的設定,包括中斷的優先權和遮罩。他有一些很棒的特性

  • 彈性的控制和設定
    • 每個ISR都可以獨立做啟動與關閉
  • 允許巢狀中斷,也就是中斷時,還可以被中斷
    • 每個Exception都有自己的優先權,有些可以自由設定優先權,少數嚴重的例外不可以
      • Interrupt可以自由設定,可以改變
      • Reset, NMI, HardFault的優先權是固定的,不可改變
    • 當例外發生時,NVIC會比較例外的優先權,如果後來發生的exception優先權比較高,那就插隊先執行更優先的例外,也就是preemption。
  • 可以做中斷遮罩,也就是停用某些中斷
    • Cotex-M3, M4提供了幾個遮罩register,例如PRIMASK,你可以停用所有的exception(除了最嚴重的HardFault和NMI不可以停用),好處是當你在執行一些關鍵任務,不可以被打斷(例如看片執行real-time multimedia解碼時)。或是你可以使用BASEPRI register,來停用某個優先權以下的例外。

談優先權

每個Exception都有自己的優先權。對Cortex-M來說,有一件很簡單但重要的事情要記得,那就是

數值越小代表優先權越高

預設上,所有軟體可以設定的優先權預設都是0。優先權可以設定的範圍在0~15之間,背後意含是Reset(-3), NMI(-2), HardFault(-1)的優先權永遠比你設定的例外還高。

優先權的觀念

在ARM Cortex-M裡,設定優先權的register bits被分為兩個欄位

  • group priority(靠近MSB這一邊)(或稱為preempt priority)
  • subpriority within group(靠近LSB這一邊)

就是遵守幾個簡單的原則

  • 如果一個exception handler正在執行,其他例外發生時,如果group priority優先權比你高就可以插隊,如果跟你一樣或比你低,就乖乖等
  • 如果有多個相同優先權的例外處理在等待執行(Pending),那麼先比較subpriority,優先權比較高的排前面,如果還是一樣,Exception Number小的優先。

ARM Cortex-M 架構允許0~255個不同的優先權,總共256個。但是實際上各個使用Cortex-M的微處理器並廠商不會讓你自由設定256個優先權。舉例來說,TI Stellaris Cortex-M3 and ARM Cortex-M4 提供3個priority bits,允許你自訂8個優先權。NXP LPC17xx ARM Cortex-M3 提供了5個優先權bits,允許你自訂32個優先權。而STM32F429這塊使用Cortex-M4的板子,提供4個優先權bits。 這4個bits要如何分配給group priority和subpriority呢?透過設定Application interrupt and reset control register (AIRCR)可以改變,你可以設成以下形式,詳情請參閱stm32 programming manual p.213

  • GGGG
  • GGGS
  • GGSS
  • GSSS
  • SSSS

其中G代表Group priority field,S代表Subpriority field

談CMSIS和NVIC的關係

CMSIS 提供了控制NVIC register的function。你可以透過CMSIS來控制優先權。包括啟用、停用某個exception,設定某個Interrupt的優先權等。

值得一提的是其實每個優先權會佔用8個bits,但是其實只有bits[7:4]有用而已,bits[3:0]是don't care bits。詳情請參閱stm32_programming_manual p.200

了解這些後,我們可以來談FreeRTOS上和優先權有關的設定

從FreeRTOS的角度看優先權

首先,先搞清楚一件事!(非常重要)優先權有兩種,一種是「ARM Cortex-M上Exception的優先權」,另一種是「FreeRTOS kernel內不同Task的優先權」,這搞混接下來就不用玩了。

  • FreeRTOS kernel內不同Task的優先權
    • 跨平台的,任何地方只要跑FreeRTOS都一樣
    • 數值越小,優先權越低,數值越大,優先權越高,最高可以到你自己定義的configMAX_PRIORITIES-1
  • 例外中斷優先權
    • 相依於平台的,不同的系統架構都不同
    • 有的系統數值小代表優先權低,有的卻相反。不同系統有不同數量的中斷優先權。

不同架構有不同數量的優先權

FreeRTOS是設計給許多不同的處理器架構的RTOS,很多人會跑在ARM Cortex上,但正因為他是設計通用不同的處理器架構的,所以有些東西要特別注意。

第一件事就是優先權的數量,這是處理器的製造商決定的,就算都是使用Cortex-M的處理器,製造商允許的優先權數量也不同。如同我們一開始所講的,Cotex-M本身允許高達256個優先權。但大多數處理器製造商並不會全部實作出來,你通常只能使用3~5bits來設定優先權,也就是8、16、32個不同的優先權數量,取決於微處理器的製造商。

要了解你的處理器使用多少優先權,可以去看你廠商的文件,或是直接看你使用的CMSIS Library header files。可以看core_cm4.h這個檔中定義了__NVIC_PRIO_BITS的數值。如果是stm32f429,這個值是4,也就是允許16個不同大小的優先權。

ARM Cortex-M on FreeRTOS

Cortex-M雖然提供了8-bit的優先權register,但通常會分成兩個部份。preempt priority bits和subpriority bits。如同之前所講的,preempt bits決定一個interrupt可不可以插隊執行。subpriority bits決定了兩個具有相同preempt priority等待執行時,誰先執行。

FreeRTOS官方建議把所有的priority bits都設給preempt bits。因為這樣才不會把事情搞得過於複雜。絕大多數的系統預設都是FreeRTOS想要的設定,也就是全部指派給group priority,除了STM32以外。

如果你是STM32的使用者,同時還使用的STM32的 driver Lbirary。FreeRTOS官方要求在FreeRTOS開始執行前,把所有的priority bits都設給preempt priority bits。做法是在RTOS開始跑之前call

NVIC_PriorityGroupConfig(NVIC_PriorityGroup_4); //implementing in driver file misc.c

另一件要注意的事情是ARM本身的priority大小的定義和大多數的處理器不同,對ARM Cortex-M來說,數值越小代表優先權越高。也就是priority = 2 比priority = 5會更優先。

FreeRTOS的優先權

FreeRTOS的優先權分成兩類。一類是受RTOS管理,不會影響到critcal section的,另一類是不理會RTOS的。這兩者將以configMAX_SYSCALL_INTERRUPT_PRIORITY所設定的值為界。 freeRTOS本身有兩個重要的優先權設定,分別是

  • configKERNEL_INTERRUPT_PRIORITY
  • configMAX_SYSCALL_INTERRUPT_PRIORITY

有些port只實作configKERNEL_INTERRUPT_PRIORITY,有些架構兩者都實作。

對於那些只實作了configKERNEL_INTERRUPT_PRIORITY的port來說

configKERNEL_INTERRUPT_PRIORITY設定的是FreeRTOS Kernel本身使用的interrupt priority。有call FromISR結尾的API的interrupt handler必需要在這個priority下執行。那些沒有call API的handler可以選擇更高的priority,這樣就不會被RTOS kernel所延遲。

PS. Q:為什麼所有使用到FreeRTOS API的Interrupt必須要在KERNEL_INTERRUPT_PRIORITY下執行? A: 因為如果有一個Interrupt的優先權大於KERNEL_INTERRUPT_PRIORITY,就有可能在FreeRTOS正在進行QueueSend這種critical section的動作時打斷他,會悲劇。

對於那些兩者都實作的port

configKERNEL_INTERRUPT_PRIORITY設定的是FreeRTOS Kernel本身使用的interrupt priority。configMAX_SYSCALL_INTERRUPT_PRIORITY設定那些call FreeRTOS API的ISR最高能使用的優先權。

注意,對ARM Cortex-M3, M4來說,configMAX_SYSCALL_INTERRUPT_PRIORITY絕對不能設成0,原因下面有解釋

這樣設計有什麼好處?

  • 你可以寫一個發生Interrupt後負責處理的Task,他的優先權會比其他task還高。該Task會被Interrupt喚醒執行,Interrupt Handler本身應該寫得越短越好,也就是抓到資料,然後後喚醒高優先權的Task負責處理,迅速離開。外界看起來就像是Interrupt自己完成的一樣。這樣的好處是當handler Task在執行時,依然可以允許其他Interrupt執行。(要知道有些架構是不允許nested-interrupt的)
  • 更進一步,那些實作configMAX_SYSCALL_INTERRUPT_PRIORITY的port,允許巢狀中斷。優先權在Kernel和MAX_SYSCALL之間的Interrupt可以nest call,同時可以call FreeRTOS API。
  • 比MAX_SYSCALL優先權更高的ISR是絕對不會被RTOS本身影響的,他們回應速度很快,對於那些非常需要快速回應的控制系統而言,是很棒的功能,但是這類的ISR不能使用FreeRTOS提供的API。

談FreeRTOS的Tick

這張圖我們都看過,概念很簡單,兩個priority相同的task在time slice到了之後,會做context-switch,但是卻沒有思考過到底context-switch時會發生什麼事。

要注意到,在 FreeRTOS 中,preemptive scheduling 是可以設定的: http://www.freertos.org/implementation/a00011.html -commented by Jserv

實際上,進行Context-Switch是由Tick中斷所引發的。當Tick中斷發生時,TickHandler的程式碼如下

程式很簡單,Tick會先「設立中斷遮罩」,阻絕絕大部分的中斷,然後執行xTaskIncrementTick(),跳表一次,接著檢查context-switch的時機到了沒,如果是的話,設立Flag。之後離開,「取消中斷遮罩」。

注意到我說「阻絕大部分的中斷」背後的深意。阻絕的意思是下遮罩,但一旦下了遮罩,要是這時候有人發出中斷,那不就遺失了嗎?不是的,阻絕的意思是「依舊接受中斷,但不處理中斷」(請參閱p.203 stm32_programming_manual)。

為什麼我講「大部分」而不是全部?因為事實上,你所阻擋的只是優先權等與或低於configMAX_SYSCALL_INTERRUPT_PRIORITY的中斷。FreeRTOS 中的port.c中,有一個可以停用所有Interrupt的函式叫做ulPortSetInterruptMask。他會把BASEPRI register的值改成MAX_SYSCALL_INTERRUPT_PRIORITY,也就是所有優先權低等於 MAX_SYSCALL_INTERRUPT_PRIORITY的中斷全部不處理。但如果有一個優先權比configMAX_SYSCALL_INTERRUPT_PRIORITY更高的中斷發生時,板子將不理會RTOS Kernel,逕行處理。

看不懂inline-asm語法的話請參照here

這也是為什麼FreeRTOSConfig.h中會加「/* !!!! configMAX_SYSCALL_INTERRUPT_PRIORITY must not be set to zero !!!!」的原因。因為一旦BASEPRI register設成0,是無效果的意思。並不是遮住所有優先權的中斷。

在執行xQueueSend這種關鍵操作時,會發生Context-Switch嗎?

答案是:不會。 事實上執行的過程是這樣的

  1. call taskENTERCRITICAL()
    • _保存中斷向量遮罩的狀態,把中斷遮罩設成configMAX_SYSCALL_INTERRUPT_PRIORITY
  2. 檢查Queue有沒有滿,把東西塞到Queue裡面
  3. call taskEXIT_CRITICAL()
    • 恢復中斷向量遮罩的狀態

在critical section內,程式不會被優先權SYSCALL以下的中斷打斷,就不會發生放進Queue到一半,突然又被優先權更高中斷打擾。

為什麼Interrupt Handler內使用FreeRTOS時,要使用FromISR()結尾的API?

FreeRTOS提供的API中,有一類是FromISP結尾的,像是

  • xSemaphoreGiveFromISR()
  • xSemaphoreTakeFromISR()
  • xQueueSendFromISR()
  • xQueueReceiveFromISR()
  • ……etc

事實上,所有From_ISR開頭的API和一般API的差別是多了一個參數,叫做 signed BaseType_t *pxHigherPriorityTaskWoken

這個參數的用意是,通常當你使用.......FromISR()這類的API時,是為了做communication。告訴系統某個資源已經釋放、某個任務要執行了。作法是讓這個參數紀錄,是否有優先權更高的Task被喚醒了。

xHigherPriorityTaskWoken = pdFALSE; xSemaphoreGiveFromISR( xSemaphore, &xHigherPriorityTaskWoken );

因此在ISR的結尾,要記得呼叫

portYIELD_FROM_ISR( xHigherPriorityTaskWoken );

意思是,如果xHigherPriorityTaskWoken被改成True,那就發生Context-Switch,去執行優先權更高的那個任務吧。