CMSIS-RTOS2 入门教程 [1]——工作原理

工作原理 Theory of Operation

系统启动 System Startup

因为 main() 不再是一个线程,所以在到达 main 之前 RTX 不会干扰系统启动。一旦执行到达main(),建议你按照如下顺序来初始化硬件并启动内核:

  1. 硬件的初始化和配置,包括外设、内存、引脚、时钟和中断系统。
  2. 更新系统核心时钟,使用Cortex-M和Cortex-A各自的CMSIS-Core函数。
  3. 初始化 CMSIS-RTOS 内核,使用 osKernelInitialize() 函数。
  4. 创建一个任务(例如 app_main),这个任务用于创建其他任务,使用 osThreadNew() 函数创建任务。
  5. 开启任务调度,使用 osKernelStart() 函数。
  6. 注意:在执行 osKernelStart() 之前,只有 osKernelGetInfo, osKernelGetState, 以及一些任务创建函数 (osXxxNew) 可以被调用.

调度器 Scheduler

RTX 实现了一个低延迟的抢占式调度器。
RTX 的主要部分是在 handler 模式下执行的,例如:

  • SysTick_Handler:用于基于时间(time-based)的调度
  • SVC_Handler:用于基于锁(lock-based)的调度
  • PendSV_Handler:用于基于中断(interrupt-based)的调度
  • 个人感觉,SysTick_Handler用于同优先级的任务调度,SVC_Handler用于高优先级任务的抢占,PendSV_Handler用于从中断退回到线程的调度

为了降低ISR(中断服务函数)执行的延迟,这些系统中断被配置为使用可用的最低优先级组。
在这里插入图片描述
RTX调度器结合了优先级和基于循环的上下文切换。
上图中的例子包含四个线程(1、2、3和4)。线程1和线程2具有相同的优先级,线程3具有更高的优先级,线程4具有最高的优先级。只要线程3和线程4被阻塞,调度器就会在线程1和线程2之间按时间片进行切换(循环)。 注:可以配置轮循调度的时间片,RTX_Config.h 文件中的 Round-Robin Timeout 参数定义了在线程切换之前,线程将执行多长时间。默认值是5。取值范围为[1-1000]。在这里插入图片描述

在 Time == 2 期间,线程2通过一个 RTOS-call (在SVC中断服务函数中执行) 解除了线程3的阻塞。调度程序立即切换到线程3,因为线程3具有最高的优先级。

在 Time == 4 期间,发生了一个中断(ISR)并抢占SysTick_Handler。RTX不会向中断服务执行添加任何延迟。ISR的中断服务函数里使用一个RTOS-call来解除线程4的阻塞。PendSV 的 flag 被设置为延迟上下文切换,而不是立即切换到线程4。PendSV_Handler在SysTick_Handler返回后立即执行,并执行到线程4的延迟上下文切换。

在 Time == 5期间,当最高优先级的线程4被 阻塞RTOS-call(blocking RTOS-call) 再次阻塞时,立即切换回线程3。

在 Time == 5期间,线程3也使用了一个 阻塞RTOS-call。因此,调度器将切换回线程2

内存分配 Memory Allocation

RTX5的对象(objects)有:线程、互斥、信号量、计时器、消息队列、线程和事件标志,以及内存池。
它们需要专用的RAM内存。可以用osObjectNew()创建对象,用osObjectDelete()删除对象。对象的相关内存需要在其生存期内可用。

RTX5提供了三种不同的内存分配方法:(可以在同一个应用程序中混合使用所有内存分配方法。)

  • 全局内存池(Global Memory Pool)
    在这里插入图片描述

    1. 为所有对象使用一个全局内存池。它很容易配置,但在创建和销毁不同大小的对象时,可能存在内存碎片的缺点。
    2. 当内存池没有提供足够的内存时,对象的创建将失败,相关的osObjectNew()函数将返回NULL。
    3. 在RTX_Config.h 文件中配置。在这里插入图片描述
  • 特定对象内存池(Object-specific Memory Pools)
    在这里插入图片描述

    1. 为每个对象类型使用固定大小的内存池。该方法具有时间确定性,避免了内存碎片。
    2. 特定对象内存池 通过为每种对象类型使用 专用的 固定大小的 内存管理来避免内存碎片。这种类型的内存池是完全时间确定(time deterministic)的,这意味着对象的创建和销毁总是需要相同的固定时间。由于固定大小的内存池是特定于对象类型的,因此简化了对内存不足情况的处理。
    3. 当内存池没有提供足够的内存时,对象的创建将失败,相关的osObjectNew()函数将返回NULL。
    4. 在RTX_Config.h 文件中配置,对象特定内存池由各对象类型 各自启用。在这里插入图片描述
  • 静态对象内存(Static Object Memory)
    在这里插入图片描述

    1. 与动态内存分配(Dynamic memory allocation)不同,静态内存分配(Static memory allocation)在编译时就完成了分配。并且完全避免了系统内存不足的问题。这通常是一些重视安全的系统所需要的。
    2. 静态内存分配 可以在创建对象时使用属性提供用户定义的内存来实现,请参阅手动用户定义的分配。请特别注意以下限制:
    Memory type
    内存类型
    Requirements
    要求
    Control Block
    控制块
    4字节对齐。
    大小定义:
    osRtxThreadCbSize, osRtxTimerCbSize, osRtxEventFlagsCbSize, osRtxMutexCbSize, osRtxSemaphoreCbSize, osRtxMemoryPoolCbSize, osRtxMessageQueueCbSize.
    Thread Stack
    线程堆栈
    8字节对齐。
    大小是特定于应用程序的,即堆栈变量和帧的数量
    Memory Pool
    内存池
    4字节对齐。
    使用osRtxMemoryPoolMemSize计算大小
    Message Queue
    消息队列
    4字节对齐。
    使用osRtxMessageQueueMemSize计算大小
    1. 为了允许RTX5感知调试状态,让组件查看器(Component Viewer)能够识别控制块,这些控制块需要放置在单独的内存段中,即使用 attribute((section(…))).
    RTX ObjectLinker 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

    例:如何使用静态内存创建操作系统对象 代码示例

    // 定义静态分配的对象为线程1
    __attribute__((section(".bss.os.thread.cb")))
    osRtxThread_t worker_thread_tcb_1;
    

线程堆栈管理 Thread Stack Management

  1. 对于没有浮点单元的Cortex-M处理器,线程上下文(thread context)在本地堆栈上需要64字节。对于带有FP的Cortex-M4/M7,线程上下文在本地堆栈上需要200个字节。对于这些设备,默认的堆栈空间应该增加到最少300字节。
  2. 每个线程都提供了一个单独的堆栈,其中包含用于自动变量的线程上下文和堆栈空间,以及用于函数调用嵌套的返回地址。
  3. RTX线程的堆栈大小可以在 RTX_Config.h 里修改。
  4. RTX提供了堆栈溢出和堆栈利用率的可配置检查。RTX_Config.h 文件

低功耗运行 Low-Power Operation

系统线程osRtxIdleThread可以用来将系统切换到低功耗模式。进入低功耗模式的最简单形式是执行将处理器置于休眠模式(等待事件发生)的wfe函数。
Code Example:

#include "RTE_Components.h"
#include CMSIS_device_header            /* Device definitions                 */
 
void osRtxIdleThread (void) {
  /* 空闲任务(idle demon) 属于系统线程, 在没有其他线程运行时运行       */
  /* 即将运行                                                      */
 
  for (;;) {
    __WFE();                            /* 进入睡眠模式                 */
  }
}

注意:__WFE() 并非在每个Cortex-M实现中都可用。查询技术手册是否可用__WFE() 。

RTX内核计时器计时 RTX Kernel Timer Tick

RTX使用通用的OS Tick API来配置和控制它的周期性 Kernel Tick。
若要用其他计时器替代滴答计时器,只需要实现一个自定义版本的OS Tick API。

注意:
提供的OS Tick实现必须确保使用的计时器中断使用与服务中断相同的(低)优先级组,即RTX使用的中断不能相互抢占。有关详细信息,请参阅调度程序部分。

Tick-less低功耗运行 Tick-less Low-Power Operation

RTX5提供了对 tick-less运行的扩展,这对于广泛使用低功耗模式的应用程序非常有用,因为SysTick计时器也被禁用了。为了在这种省电模式中提供时间刻度,可以使用wake-up计时器产生计时器间隔。CMSIS-RTOS2函数 osKernelSuspend 和 osKernelResume 控制 tick-less运行。

利用这个功能可以让RTX5的线程管理器停止周期性的内核滴答中断。当所有活跃线程都被挂起时,系统进入power-down模式,并计算它可以在这种power-down模式下保持多长时间。在power-down模式下,处理器和外围设备可以关闭。只有一个wake-up计时器必须保持通电状态,因为这个计时器负责在power-down后唤醒系统。

Tick-less运行 由osRtxIdleThread线程控制。唤醒时间值(wake-up timeout value)是在系统进入power-down模式之前设置的。函数osKernelSuspend计算RTX Timer Ticks基准下的唤醒时间;此值用于设置在系统power-down模式下运行的wake-up计时器。

一旦系统恢复操作(通过唤醒时间超时或其他中断),RTX5线程调度器将使用osKernelResume函数启动。参数sleep_time指定系统处于关机模式的时间(在RTX Timer Ticks基准下)。

Code Example:示例代码

RTX5 Header File

CMSIS-RTOS2 API的每个实现都可以带来自己的附加特性。RTX5增加了几个函数用于 idle more,用于错误通知,用于特殊的系统定时器功能。它还使用宏来控制块和内存大小。
如果您需要在您的应用程序代码中的一些RTX特定函数,#include头文件rtx_os.h

超时时间 Timeout Value

超时值(Timeout Value)是几个osXxx函数的参数,允许有时间解析请求。超时值为0意味着RTOS不等待,函数立即返回,即使在没有资源可用的情况下也是如此。超时值为osWaitForever意味着RTOS无限等待,直到资源可用为止。或者使用不推荐的osThreadResume强制线程恢复。

超时值指定在时间延迟过期之前计时器滴答的数量。该值是一个上限,取决于自最后一个计时器滴答响以来实际经过的时间。

示例:

  • Timeout Value==0:即使没有资源可用,系统也不会等待RTOS函数立即返回。
  • Timeout Value==1:系统等待,直到下一次计时器滴答;取决于前一个计时器滴答声,它可能是一个非常短的等待时间。
  • Timeout Value==2:实际等待时间在1到2个计时器之间。超时值osWaitForever:系统等待无限,直到资源变得可用。
    在这里插入图片描述

在中断服务函数中调用函数 Calls from Interrupt Service Routines

可以从线程和中断服务例程(ISR)中调用以下CMSIS-RTOS2函数:

  • 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中调用的函数在ISR里被调用了,他们会验证中断状态并返回状态码osErrorISR。在某些实现中,可以使用HARD_FAULT向量捕获此条件。

SVC 功能 SVC Functions

SVC(Supervisor Calls)管理程序调用,是针对软件和操作系统的异常,用于生成系统功能调用。它们有时被称为软件中断。例如,一个操作系统可能通过SVC提供对硬件的访问,而不是允许用户程序直接访问硬件。因此,当用户程序想要使用某些硬件时,它会使用SVC指令生成异常。操作系统中的软件异常处理程序执行并向用户应用程序提供请求的服务。这样,对硬件的访问就在操作系统的控制之下,操作系统可以防止用户应用程序直接访问硬件,从而提供更健壮的系统。

SVCs还可以使软件更加便携,因为用户应用程序不需要知道底层硬件的编程细节。用户程序只需要知道应用程序编程接口(API)的函数ID和参数;实际的硬件级编程是由设备驱动程序处理的。

SVCs在Arm Cortex-M内核的特权处理模式下运行。SVC函数接受参数并可以返回值。函数的使用方法与其他函数相同;但是,它们是通过SVC指令间接执行的。当执行SVC指令时,控制器切换到特权处理程序模式。

在此模式下不禁用中断。为了保护SVC函数不受中断的影响,您需要在代码中包含disable/enable内部函数_disable_irq()和_enable_irq()。

您可以使用SVC函数来访问受保护的外围设备,例如,配置NVIC和中断。如果您以非特权(受保护)模式运行线程,并且需要从线程内部更改中断,那么这是必需的。

要在你的Keil RTX5项目中实现SVC功能,你需要:

  1. 将 SVC User Table文件svc_user.c添加到项目文件夹中,并将其包含到项目中。此文件可作为用户代码文件使用。
  2. 编写一个函数实现。例如:
    uint32_t svc_atomic_inc32 (uint32_t *mem) {
      // A protected function to increment a counter. 
      uint32_t val;
       
      __disable_irq();
      val  = *mem;
      (*mem) = val + 1U;
      __enable_irq();
       
      return (val);
    }
    
  3. 将函数添加到svc_user.c模块的SVC函数列表中:
    void * const osRtxUserSVC[1+USER_SVC_COUNT] = {
    (void *)USER_SVC_COUNT,
    (void *)svc_atomic_inc32,
    };
    
  4. 增加用户SVC函数数量:
    #define USER_SVC_COUNT  1       // Number of user SVC functions
    
  5. 声明一个由用户调用的函数包装来执行SVC调用。
    mdk5编译器v6版本:
    __STATIC_FORCEINLINE uint32_t atomic_inc32 (uint32_t *mem) {
      register uint32_t val;
           
      __ASM volatile (
        "svc 1" : "=l" (val) : "l" (mem) : "cc", "memory"
      );
      return (val);
    }
    

mdk5编译器v5版本:

	uint32_t atomic_inc32 (uint32_t *mem) __svc(1);

注意:
1. SVC函数0保留给Keil RTX5内核。
2. 在给SVC函数编号时,不要留空白。它们必须占据从1开始的连续数字范围。
3. SVC功能仍然可以被中断。

©️2020 CSDN 皮肤主题: 精致技术 设计师:CSDN官方博客 返回首页