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)不支持多线程应用程序的保护。