RTX筆記13 - RTX5原理
1.系統啟動(System Startup)
mian函式不再以一個執行緒的形式執行,因此在main函式執行之前,RTX5不會干預系統的啟動。main函式執行之後,推薦按照以下的流程初始化硬體並啟動核心:
(1)硬體的初始化和配置,包括外設,記憶體,引腳,時鐘和中斷系統。
(2)使用CMSIS-Core函式更新系統核心時鐘。
(3)使用osKernelInitialize函式初始化CMSIS-RTOS核心。
(4)使用osThreadNew函式建立一個主執行緒(例如app_mian),然後在這個執行緒中建立和啟動物件。當然也可以直接在main函式中建立和啟動物件。
(5)使用OSKernelStart啟動RTOS排程器,該函式會配置system tick定時器以及初始化RTOS相關中斷。如果這個函式執行成功,則不會在返回,因此該函式之後的程式碼將不會被執行。
Note:在上述流程之後,不推薦應用程式修改NVIC的優先順序和分組;在執行osKernelStart之前,只能呼叫osKernelGetInfo、osKernelGetState和物件建立函式(osXxxNew)。
2.排程器(Scheduler)
RTX5實現了一個低延遲的搶佔式排程器。RTX5的主要部分在中斷模式下執行,例如:
為了縮短ISR執行的延遲,這些系統異常被配置為使用最低優先順序組,這種配置使得它們之間不會發生搶佔。因此,不需要中斷臨界區(即中斷鎖)來保護排程器。
執行緒排程和中斷執行
排程器包括優先順序排程和輪轉(round-robin)排程。上圖所示的示例包含四個執行緒(1、2、3和4)。執行緒1和執行緒2具有相同的優先順序,執行緒3的優先順序更高,執行緒4的優先順序最高。只要執行緒3和4被阻塞,排程程式就會按時間片線上程1和執行緒2之間切換(迴圈)。輪轉排程的時間片可參見“系統配置”中的“輪轉超時時間”配置。
執行緒2在時間索引2通過任意rtos呼叫(在SVC處理程式模式下執行)解除執行緒3的阻塞。排程程式立即切換到執行緒3,因為執行緒3具有最高的優先順序。執行緒4仍然被阻塞。
在時間索引4發生中斷(ISR)並搶佔SysTick_Handler。RTX不會增加中斷服務執行的延遲。ISR例程使用一個rtos呼叫來解除執行緒4的阻塞。PendSV標誌被設定以推遲上下文切換,而不是立即切換到執行緒4。在SysTick_Handler返回後立即執行PendSV_Handler,並執行到執行緒4的延遲上下文切換。一旦最高優先順序的執行緒4使用了阻塞的rtos呼叫,執行緒4再次阻塞,在時間索引5期間立即切換回執行緒3。
在時間索引5時,執行緒3也使用了阻塞的rtos呼叫。因此排程程式切換回執行緒2。在時間索引7時,排程程式使用輪詢機制切換到執行緒1,以此類推。
3.記憶體分配(Memory Allocation)
RTX5物件(執行緒、互斥鎖、訊號量、定時器、訊息佇列、執行緒和事件標誌以及記憶體池)需要專用的RAM記憶體。可以使用osObjectNew()呼叫建立物件,並使用osObjectDelete()呼叫刪除物件。相關的物件記憶體需要在物件的生命週期內可用。
RTX5提供了三種不同的記憶體分配方法:
(1)全域性記憶體池(Global Memory Pool):所有物件使用一個全域性記憶體池。它易於配置,但在建立和銷燬不同大小的物件時,可能會造成記憶體碎片。
(2)特定物件記憶體池(Object-specific Memory Pools):每個物件型別使用固定大小的記憶體池。該方法具有時間確定性,避免了記憶體碎片。
(3)靜態物件記憶體(Static Object Memory):在編譯期間保留記憶體,完全避免系統記憶體不足。這通常是一些安全關鍵系統所必需的。
可以在同一個應用程式中混合使用所有的記憶體分配方法。
3.1 全域性記憶體池
全域性記憶體池從一個記憶體區域分配所有物件。這種記憶體分配方法是RTX5的預設配置設定。
當記憶體池沒有提供足夠的記憶體時,物件的建立會失敗,相關的osObjectNew()函式將返回NULL。
3.2 特定物件記憶體池
特定於物件的記憶體池通過針對每個物件型別的專用固定大小的記憶體管理來避免記憶體碎片。這種型別的記憶體池是完全時間確定的,這意味著物件建立和銷燬總是花費相同的固定時間。由於固定大小的記憶體池特定於物件型別,因此可以簡化記憶體不足情況的處理。這種記憶體池可以為每個物件分別開啟。
當記憶體池沒有提供足夠的記憶體時,物件的建立會失敗,相關的osObjectNew()函式將返回NULL。
3.3 靜態物件記憶體
與動態記憶體分配相反,靜態記憶體分配需要在編譯時分配物件記憶體。
靜態記憶體分配可以通過在建立物件時使用屬性提供使用者定義的記憶體來實現,注意以下限制:
Memory type | Requirements |
---|---|
控制塊 (osXxxAttr_t::cb_mem) | 4位元組對齊. Size defined by osRtxThreadCbSize, osRtxTimerCbSize, osRtxEventFlagsCbSize, osRtxMutexCbSize, osRtxSemaphoreCbSize, osRtxMemoryPoolCbSize, osRtxMessageQueueCbSize. |
執行緒棧 (osThreadAttr_t::stack_mem) | 8位元組對齊. Size is application specific, i.e. amount of stack variables and frames. |
記憶體池 (osMemoryPoolAttr_t::mp_mem) | 4位元組對齊. Size calculated with osRtxMemoryPoolMemSize. |
訊息佇列 (osMessageQueueAttr_t::mq_mem) | 4位元組對齊. Size calculated with osRtxMessageQueueMemSize. |
為了允許RTX5感知除錯,例如元件檢視器,為了識別這些控制塊,需要將其放置在單獨的記憶體部分,即使用__attribute__((section(…)))。
RTX Object | Linker Section |
---|---|
Thread | .bss.os.thread.cb |
Timer | .bss.os.timer.cb |
Event Flags | .bss.os.evflags.cb |
Mutex | .bss.os.mutex.cb |
Semaphore | .bss.os.semaphore.cb |
Memory Pool | .bss.os.mempool.cb |
Message Queue | .bss.os.msgqueue.cb |
必須確保這些區段被放置在連續的記憶體中。當手動將編譯單元分配給記憶體段時,段最終會被分割到多個記憶體段,此時將不再連續。下面的程式碼示例展示瞭如何使用靜態記憶體建立OS物件。
1 /*---------------------------------------------------------------------------- 2 * CMSIS-RTOS 'main' function template 3 *---------------------------------------------------------------------------*/ 4 #include "RTE_Components.h" 5 #include CMSIS_device_header 6 #include "cmsis_os2.h" 7 8 //include rtx_os.h for types of RTX objects 9 #include "rtx_os.h" 10 11 //The thread function instanced in this example 12 void worker(void *arg) 13 { 14 while(1) 15 { 16 //work 17 osDelay(10000); 18 } 19 } 20 21 // Define objects that are statically allocated for worker thread 1 22 __attribute__((section(".bss.os.thread.cb"))) 23 osRtxThread_t worker_thread_tcb_1; 24 25 // Reserve two areas for the stacks of worker thread 1 26 // uint64_t makes sure the memory alignment is 8 27 uint64_t worker_thread_stk_1[64]; 28 29 // Define the attributes which are used for thread creation 30 // Optional const saves RAM memory and includes the values in periodic ROM tests 31 const osThreadAttr_t worker_attr_1 = { 32 "wrk1", 33 osThreadJoinable, 34 &worker_thread_tcb_1, 35 sizeof(worker_thread_tcb_1), 36 &worker_thread_stk_1[0], 37 sizeof(worker_thread_stk_1), 38 osPriorityAboveNormal, 39 0 40 }; 41 42 // Define ID object for thread 43 osThreadId_t th1; 44 45 /*---------------------------------------------------------------------------- 46 * Application main thread 47 *---------------------------------------------------------------------------*/ 48 void app_main (void *argument) { 49 uint32_t param = NULL; 50 51 // Create an instance of the worker thread with static resources (TCB and stack) 52 th1 = osThreadNew(worker, ¶m, &worker_attr_1); 53 54 for (;;) {} 55 } 56 57 int main (void) { 58 // System Initialization 59 SystemCoreClockUpdate(); 60 // ... 61 osKernelInitialize(); // Initialize CMSIS-RTOS 62 osThreadNew(app_main, NULL, NULL); // Create application main thread 63 osKernelStart(); // Start thread execution 64 for (;;) {} 65 }
4.執行緒棧管理(Thread Stack Management)
對於沒有浮點單元的Cortex-M處理器,執行緒上下文在本地堆疊上需要64個位元組。對於帶FP的Cortex-M4/M7,執行緒上下文在本地堆疊上需要200個位元組。對於這些裝置,預設堆疊空間應該增加到至少300位元組。
每個執行緒都有一個單獨的堆疊,用於存放自動變數的執行緒上下文和堆疊空間,以及函式呼叫巢狀時的返回地址。RTX執行緒的堆疊大小可以靈活配置,詳見執行緒配置一節。RTX提供了一個可配置的堆疊溢位和堆疊利用率檢查。
5.低功耗執行(Low-Power Operation)
可以使用系統執行緒osRtxIdleThread將系統切換到低功耗模式。進入低功耗模式最簡單的形式是__WFE函式的執行,該函式將處理器放入一個休眠模式,在那裡它等待一個事件。
1 #include "RTE_Components.h" 2 #include CMSIS_device_header /* Device definitions */ 3 4 void osRtxIdleThread (void) { 5 /* The idle demon is a system thread, running when no other thread is */ 6 /* ready to run. */ 7 8 for (;;) { 9 __WFE(); /* Enter sleep mode */ 10 } 11 }
Note:__WFE()並不是在每個Cortex-M中都可用。
6.RTX核心滴答定時器(RTX Kernel Timer Tick)
RTX使用通用的OS Tick API來配置和控制其週期性的核心Tick。要使用一個替代定時器作為核心滴答定時器,只需要實現一個自定義版本的OS Tick API。
Note:提供的OS Tick實現必須確保使用的定時器中斷使用與服務中斷相同的(低)優先順序組,即RTX使用的中斷不能相互搶佔。
無滴答定時器低功耗執行(Tick-less Low-Power Operation)
RTX5提供了擴充套件的無滴答操作,它在SysTick 定時器也被禁止的擴充套件低功耗應用中是有用的。為了在這種節能模式中提供一個時間滴答,一個喚醒定時器被用來獲得定時器間隔。CMSIS-RTOS2函式osKernelSuspend和osKernelResume控制無滴答操作。
使用這個功能允許RTX5執行緒排程器停止週期性的核心tick中斷。當所有活動執行緒被掛起時,系統進入下電狀態,並計算它能在這種下電模式下停留多長時間。在下電模式下,處理器和外設可以被關閉。只有一個喚醒定時器必須保持供電,因為這個定時器負責在斷電時間到期後喚醒系統。
無滴答操作由osRtxIdleThread執行緒控制。喚醒超時時間設定在系統進入下電模式前。osKernelSuspend函式計算喚醒超時(RTX Timer Ticks);此值用於設定在系統下電模式下執行的喚醒計時器。
一旦系統恢復操作(通過喚醒超時或其他中斷),RTX5執行緒排程程式將使用osKernelResume函式啟動。引數sleep_time指定系統處於下電模式的時間(在RTX Timer Ticks中)。
1 #include "msp.h" // Device header 2 /*---------------------------------------------------------------------------- 3 * MSP432 Low-Power Extension Functions 4 *---------------------------------------------------------------------------*/ 5 static void MSP432_LP_Entry(void) { 6 /* Enable PCM rude mode, which allows to device to enter LPM3 without waiting for peripherals */ 7 PCM->CTL1 = PCM_CTL1_KEY_VAL | PCM_CTL1_FORCE_LPM_ENTRY; 8 /* Enable all SRAM bank retentions prior to going to LPM3 */ 9 SYSCTL->SRAM_BANKRET |= SYSCTL_SRAM_BANKRET_BNK7_RET; 10 __enable_interrupt(); 11 NVIC_EnableIRQ(RTC_C_IRQn); 12 /* Do not wake up on exit from ISR */ 13 SCB->SCR |= SCB_SCR_SLEEPONEXIT_Msk; 14 /* Setting the sleep deep bit */ 15 SCB->SCR |= (SCB_SCR_SLEEPDEEP_Msk); 16 } 17 18 static volatile unsigned int tc; 19 static volatile unsigned int tc_wakeup; 20 21 void RTC_C_IRQHandler(void) 22 { 23 if (tc++ > tc_wakeup) 24 { 25 SCB->SCR &= ~SCB_SCR_SLEEPONEXIT_Msk; 26 NVIC_DisableIRQ(RTC_C_IRQn); 27 NVIC_ClearPendingIRQ(RTC_C_IRQn); 28 return; 29 } 30 if (RTC_C->PS0CTL & RTC_C_PS0CTL_RT0PSIFG) 31 { 32 RTC_C->CTL0 = RTC_C_KEY_VAL; // Unlock RTC key protected registers 33 RTC_C->PS0CTL &= ~RTC_C_PS0CTL_RT0PSIFG; 34 RTC_C->CTL0 = 0; 35 SCB->SCR |= (SCB_SCR_SLEEPDEEP_Msk); 36 } 37 } 38 39 uint32_t g_enable_sleep = 0; 40 41 void osRtxIdleThread (void) { 42 43 for (;;) { 44 tc_wakeup = osKernelSuspend(); 45 /* Is there some time to sleep? */ 46 if (tc_wakeup > 0) { 47 tc = 0; 48 /* Enter the low power state */ 49 MSP432_LP_Entry(); 50 __WFE(); 51 } 52 /* Adjust the kernel ticks with the amount of ticks slept */ 53 osKernelResume (tc); 54 } 55 }
7.超時值(Timeout Value)
超時值是一些osXxx函式的引數,以為處理請求留出時間。超時值為0意味著RTOS不等待,函式立即返回,即使沒有可用的資源。osWaitForever的超時值意味著RTOS將無限等待,直到資源可用為止。或者使用osThreadResume強制執行緒恢復,這是推薦的。
超時值指定在時間延遲過去之前的計時器滴答數。該值是一個上限,取決於自上次計時器滴答以來經過的實際時間。例如:
超時值0:系統不等待,即使沒有可用資源,RTOS函式也立即返回。
超時值1:系統等待,直到下一個計時器滴答;根據前一個計時器的滴答聲,它可能是一個非常短的等待時間。
超時值2:實際等待時間介於1到2個計時器時間間隔之間。
超時值osWaitForever:系統無限等待,直到資源可用。
8.從中斷服務程式呼叫(Calls from Interrupt Service Routines)
以下CMSIS-RTOS2函式可以從執行緒和中斷服務例程(ISR)中呼叫:
- osKernelGetInfo, osKernelGetState, osKernelGetTickCount, osKernelGetTickFreq, osKernelGetSysTimerCount, osKernelGetSysTimerFreq
- osThreadGetId, osThreadFlagsSet
- osEventFlagsSet, osEventFlagsClear, osEventFlagsGet, osEventFlagsWait
- osSemaphoreAcquire, osSemaphoreRelease, osSemaphoreGetCount
- osMemoryPoolAlloc, osMemoryPoolFree, osMemoryPoolGetCapacity, osMemoryPoolGetBlockSize, osMemoryPoolGetCount, osMemoryPoolGetSpace
- osMessageQueuePut, osMessageQueueGet, osMessageQueueGetCapacity, osMessageQueueGetMsgSize, osMessageQueueGetCount, osMessageQueueGetSpace
不能從ISR呼叫的函式會驗證中斷狀態,並返回狀態程式碼osErrorISR,以防它們是從ISR上下文呼叫的。在某些實現中,可以使用HARD_FAULT向量捕獲此條件。
Note:RTX在基於Armv7-M和Armv8-M架構的裝置的臨界區不禁用中斷,而是使用原子操作。因此,對於使用RTOS功能的中斷業務例程,無需配置中斷優先順序。
9.SVC功能(SVC Functions)
Supervisor Calls (SVC)是針對軟體和作業系統的異常,用於生成系統功能呼叫。它們有時被稱為軟體中斷。例如,作業系統可以通過SVC提供對硬體的訪問,而不是允許使用者程式直接訪問硬體。因此,當用戶程式需要使用某些硬體時,它會使用SVC指令生成異常。作業系統中的軟體異常處理程式執行並向用戶應用程式提供所請求的服務。這樣,對硬體的訪問就在作業系統的控制之下。
SVCs還可以使軟體更具可移植性,因為使用者應用程式不需要知道底層硬體的程式設計細節。使用者程式只需要知道應用程式程式設計介面(API)函式ID和引數;實際的硬體級程式設計是由裝置驅動程式處理的。
SVCs在Arm Cortex-M核心的特權處理程式模式下執行。SVC函式接受引數並可以返回值。該函式的使用方式與其他函式相同;但是,它們是通過SVC指令間接執行的。當執行SVC指令時,控制器更改為特權處理程式模式。
在此模式下不禁用中斷。為了保護SVC函式不被中斷,你需要在你的程式碼中包含內部函式__disable_irq()和__enable_irq()。
您可以使用SVC功能訪問受保護的外設,例如,配置NVIC和中斷。如果您在非特權(受保護)模式下執行執行緒,並且需要從執行緒內部更改中斷,則需要這樣做。
按照以下步驟,在你的Keil RTX5專案中實現SVC功能:
(1)將SVC使用者表文件svc_user.c新增到專案資料夾中,並將其包含到專案中。此檔案可作為使用者程式碼模板使用。
(2)寫一個函式,例如:
1 uint32_t svc_atomic_inc32 (uint32_t *mem) { 2 // A protected function to increment a counter. 3 uint32_t val; 4 5 __disable_irq(); 6 val = *mem; 7 (*mem) = val + 1U; 8 __enable_irq(); 9 10 return (val); 11 }
(3)將該函式新增到svc_user.c模組中的SVC函式表中:
1 void * const osRtxUserSVC[1+USER_SVC_COUNT] = { 2 (void *)USER_SVC_COUNT, 3 (void *)svc_atomic_inc32, 4 };
(4)增加使用者SVC函式的數量:
1 #define USER_SVC_COUNT 1 // Number of user SVC functions
(5)宣告一個由使用者呼叫的函式包裝器來執行SVC呼叫。例如:
Arm Compiler 6:
1 __STATIC_FORCEINLINE uint32_t atomic_inc32 (uint32_t *mem) { 2 register uint32_t val; 3 4 __ASM volatile ( 5 "svc 1" : "=l" (val) : "l" (mem) : "cc", "memory" 6 ); 7 return (val); 8 }
Arm Compiler 5 using __svc(x)
attribute:
1 uint32_t atomic_inc32 (uint32_t *mem) __svc(1);
Note:
SVC函式0為Keil RTX5核心保留。
SVC函式編號時不要留下間隙。它們必須佔據從1開始的連續數字範圍。
SVC函式仍然可以被中斷。
10.Arm C庫多執行緒保護(Arm C library multi-threading protection)
RTX5為Arm C庫提供了一個介面,以確保多執行緒應用程式中的靜態資料保護。
Arm C庫使用靜態資料來儲存errno、用於軟體浮點操作的浮點狀態字、指向堆基地址的指標和其他變數。Arm C的微庫(即microlib)不支援多執行緒應用程式的保護。