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)不支持多線程應用程序的保護。