GD32F30X适配CMSIS RTOS2(RTX5内核)

关于版权许可

CMSIS软件包开源协议是Apache 2.0,因此可以用在闭源的商业项目中,CMSIS-RTOS2是一套ARM设计的操作系统API,目的是在不同的操作系统内核之间形成一套通用的上层API,CMSIS-RTOS2有主流的操作系统兼容层,例如freertos,rt-thread,huawei liteos-m等。
CMSIS-RTOS API也有一套默认的实时系统内核绑定,他们之间的对应关系如下:

cmsis软件包 keil rtx 状态
CMSIS RTOS RTX4 不维护
CMSIS RTOS2 RTX5 持续更新

image

RTX5已经和Keil-MDK解除绑定了,如果不使用Keil集成开发环境,免费使用RTX5的方法:
image

  • 从Github下载CMSIS软件包
  • 使用免费的编译器(Clang或GCC)和集成开发环境(ARM-Eclipse)

软件包下载

DAPLink集成的rtos2:
https://github.com/ARMmbed/DAPLink/tree/main/source/rtos2
CMSIS软件包,提供很多移植示例:
https://github.com/ARM-software/CMSIS_5/tree/develop/CMSIS/RTOS2

开发环境

编译器:GNU Arm Embedded Toolchain 10.3-2021.10
集成开发环境:Eclipse IDE for Embedded C/C++ Developers 2022-3

RTOS2移植

  1. 新建一个ARM Cortex-M空工程,将所使用MCU的启动文件,gcc链接脚本,cmsis软件接口,标准外设库等文件放入工程目录,然后配置下编译选项即可。


  2. rtos2包含系统源码的文件夹复制到工程目录,进入\rtos2\RTX\Source目录,ARMGCC文件夹存放了rtos2系统调用和线程切换的汇编实现,这里删除ARMCC编译器相关代码,然后进入GCC文件夹,由于这里使用GD32F30X,是cortex-m4f架构,因此只保留irq_cm4f.S


  3. 适配系统心跳时钟,\rtos2\Source\os_systick.c,该文件只有定义了SysTick才会参与编译,实际上只需要将所使用MCU提供的CMSIS通用接口文件包含进来,这里#include "core_cm4.h",SysTick结构体在core_cm4.h文件中被定义了。



    os_systick.c默认使用的是SysTick寄存器组结构体对内部定时器的寄存器进行修改,这里也可以改成直接操作寄存器,看个人喜好了。

  4. 配置rtos2系统选项,\rtos2\Include\RTX_Config.h,使用KEIL-MDK可以图形化配置,但这里我未购买keil使用许可,直接改RTX_Config.h头文件也是一样的。
    每个选项的细节可以参考这里:https://blog.csdn.net/tyuthhh/article/details/104933826

  5. 配置空闲线程低功耗指令(这一步也可以不做),进入\rtos2\RTX\RTX_Config.c,添加__WFI();配置CPU在空闲时等待中断唤醒。(对于单核CPU来说,使用WFI即可,多核需要考虑使用WFE,并且在某一核心释放自旋锁时使用SEV通知)
    image

  6. 新建一个main.c文件,放入用户代码,由于我在启动汇编文件startup_gd32f30x_hd.S中的c函数入口是entry那么c代码应该实现在entry函数中。由于rtos2初始化过程是由系统调用(SVC软中断)实现的,由于中断模式下使用MSP,因此裸机阶段的C栈需要适当给大点,一般2K字节的栈就够用了。

#include <string.h>
#include <stdbool.h>
#include "system_gd32f30x.h"
#include "cmsis_os2.h"
#include "rtt.h"

void app_main (void *argument) {
    const char * const ptr = "hello cmsis rtos2\n";
    unsigned len = strlen(ptr);
    (void)argument;

    while(true) {
        osDelay(1000);
        rtt_write(0, ptr, len);
    }
}


void entry(void) {
    const char * const ptr = "hello entry\n";
    unsigned len = strlen(ptr);

    SystemCoreClockUpdate();

    rtt_init();
    rtt_write(0, ptr, len);

    osKernelInitialize();                 // Initialize CMSIS-RTOS
    osThreadNew(app_main, NULL, NULL);    // Create application main thread
    osKernelStart();                      // Start thread execution
    // 这里需要加while(1)卡住执行
    // 防止当osKernelStart调用结束,而系统心跳中断未到来的这小段时间跑飞
    while(true);
}
  1. 进行编译运行,可以看到资源占用情况,这里我给动态内存分配了8KB,另外使用了打印日志的segger-rtt组件,其中输出缓冲区分配1024字节,输入缓冲区分配64字节。
    (如果只编译裸内核,预计代码大概5K,内存需求约2K)

用户线程的细节

在rtos2中,用户线程可自行结束,也就是说用户线程不一定要在一个whil(1)死循环中周期执行,若线程只执行一次,那么按照常规的函数编写就行了;作为对比,在FreeRTOS中是不允许线程自行结束,一旦线程函数return后,这个任务的指针就会成一个不确定值,任务调度的时候就跑飞,如果一个任务不需要了,需要调用vTaskDelete显式的将其删除(当然也可以修改内核添加线程自动结束功能)。因此rtos2使用起来就方便一些,顺带一提rt-thread也可以自行结束线程。
rtos2的线程代码示例

// 需要周期执行的线程
void app_main (void *argument) {
    (void)argument;
    while(true) {
        // do something...
    }
}
// 只执行一次的线程,线程函数没有返回值
void app_main (void *argument) {
    (void)argument;
    
    // do something...
}

freertos的示例

static void main_task(void* args)
{
    app_main();
    // 结束前必须调用vTaskDelete
    vTaskDelete(NULL);
}

当用户使用osThreadNew创建线程时,函数原型:

osThreadId_t osThreadNew (osThreadFunc_t func, void *argument, const osThreadAttr_t *attr)

实际上进行了一次__svcThreadNew系统调用,SVC0_3 (ThreadNew, osThreadId_t, osThreadFunc_t, void *, const osThreadAttr_t *)宏定义展开

__attribute__((always_inline))                                                 \
static inline osThreadId_t  __svcThreadNew (osThreadFunc_t a1, void * a2, const osThreadAttr_t * a3) {                             \
  register uint32_t __r0 __asm("r""0") = (uint32_t)a1;                                                              \
  register uint32_t __r1 __asm("r""1") = (uint32_t)a2;                                                              \
  register uint32_t __r2 __asm("r""2") = (uint32_t)a3;                                                              \
  register uint32_t __rf   __asm("r12") = (uint32_t)svcRtxThreadNew;                                                         \
  __asm volatile ("svc 0" : "=r"(__r0) : "r"(__rf),"r"(__r0),"r"(__r1),"r"(__r2) : );                                       \
  return (osThreadId_t) __r0;                                                             \
}

可以看到,在进SVC中断之前,将参数放在了r0~r3中,将目标函数指针svcRtxThreadNew放在了r12中,进入\rtos2\RTX\Source\GCC\irq_cm4f.S,SVC_Handler子程序可以看到实际是调用r12的函数svcRtxThreadNew

svcRtxThreadNew函数做一些线程创建前的准备工作:

在该函数中,对线程的LR寄存器做了配置,将其指向osThreadExit,由于线程函数和普通函数在ARM内核执行是没有区别的(ARM内核只是个无情的跑码机器),那么线程函数正常结束时,会将PC指向LR寄存器的地址(BX LR或者MOV PC, LR)也就是将控制权移交给osThreadExit

当然了osThreadExit 也是个系统调用,__svcThreadExit展开后实际是调用了svcRtxThreadExit函数。

svcRtxThreadExit 函数中做当前线程结束时的一些动作:

  1. 释放当前线程持有的互斥锁osRtxMutexOwnerRelease(thread->mutex_list)
  2. 如果有其他线程等待当前线程结束(thread join),那么也需要通知到thread->thread_join等待线程列表中的其他线程osRtxThreadWaitExit(thread->thread_join, (uint32_t)osOK, FALSE);
  3. 找到下一个就绪线程,将当前执行线程指向就绪线程osRtxThreadSwitch(osRtxThreadListGet(&osRtxInfo.thread.ready));如果没有其他活动线程,那么自动切入idle线程,在rtos2中由于资源回收已经在svcRtxThreadExit函数中完成了,因此idle线程就是个while(1)循环(rt-thread是在idle线程回收僵尸线程占用的资源)
  4. 标记当前线程无效,并进行内核对象资源回收,osRtxThreadFree(thread);

总结

cmsis rtos2自身提供了常见ARM架构的线程切换汇编文件,移植起来还是比较简单的;系统本身只提供了基础的内核对象和调度器功能,与他对标的应该是FreeRTOS这类简单系统,和rt-thread这种大而全的系统使用体验上就没法相比了。
优点:

  • 移植容易,代码量小容易理解,在RTOS使用SVC切入内核态思路不错(虽然大部分MCU不带MPU或MMU无法完成权限和地址空间隔离)
  • 最大限度的发挥M内核的优势,零中断延迟
  • RTX5的汽车级,工业级,医疗和铁路安全认证已经通过(只有裸内核认证了,认证后的版本应该要收钱)

缺点:

  • 中间件用的人不多,例如设备框架以及虚拟文件系统,这方面rt-thread做的非常不错
  • 未提供人机交互的shell
  • 未提供Posix兼容API,也不自带C库实现

rtos2适配到GD32F30X示例代码:
https://github.com/Yanye0xFF/gd32f30x_cmsis_rtos2

参考资料:
https://www.cnblogs.com/ivan0512/tag/RTX/

posted @ 2022-09-11 15:41  Yanye  阅读(1125)  评论(0编辑  收藏  举报