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 | 持续更新 |
RTX5已经和Keil-MDK解除绑定了,如果不使用Keil集成开发环境,免费使用RTX5的方法:
- 从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移植
-
新建一个ARM Cortex-M空工程,将所使用MCU的启动文件,gcc链接脚本,cmsis软件接口,标准外设库等文件放入工程目录,然后配置下编译选项即可。
-
将
rtos2
包含系统源码的文件夹复制到工程目录,进入\rtos2\RTX\Source
目录,ARM
和GCC
文件夹存放了rtos2系统调用和线程切换的汇编实现,这里删除ARMCC编译器相关代码,然后进入GCC
文件夹,由于这里使用GD32F30X,是cortex-m4f架构,因此只保留irq_cm4f.S
-
适配系统心跳时钟,
\rtos2\Source\os_systick.c
,该文件只有定义了SysTick
才会参与编译,实际上只需要将所使用MCU提供的CMSIS通用接口文件包含进来,这里#include "core_cm4.h"
,SysTick结构体在core_cm4.h文件中被定义了。
os_systick.c默认使用的是SysTick寄存器组结构体对内部定时器的寄存器进行修改,这里也可以改成直接操作寄存器,看个人喜好了。
-
配置rtos2系统选项,
\rtos2\Include\RTX_Config.h
,使用KEIL-MDK可以图形化配置,但这里我未购买keil使用许可,直接改RTX_Config.h头文件也是一样的。
每个选项的细节可以参考这里:https://blog.csdn.net/tyuthhh/article/details/104933826
-
配置空闲线程低功耗指令(这一步也可以不做),进入
\rtos2\RTX\RTX_Config.c
,添加__WFI();
配置CPU在空闲时等待中断唤醒。(对于单核CPU来说,使用WFI即可,多核需要考虑使用WFE,并且在某一核心释放自旋锁时使用SEV通知)
-
新建一个
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);
}
- 进行编译运行,可以看到资源占用情况,这里我给动态内存分配了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
函数中做当前线程结束时的一些动作:
- 释放当前线程持有的互斥锁
osRtxMutexOwnerRelease(thread->mutex_list)
- 如果有其他线程等待当前线程结束(thread join),那么也需要通知到thread->thread_join等待线程列表中的其他线程
osRtxThreadWaitExit(thread->thread_join, (uint32_t)osOK, FALSE);
- 找到下一个就绪线程,将当前执行线程指向就绪线程
osRtxThreadSwitch(osRtxThreadListGet(&osRtxInfo.thread.ready));
如果没有其他活动线程,那么自动切入idle线程,在rtos2中由于资源回收已经在svcRtxThreadExit
函数中完成了,因此idle线程就是个while(1)循环(rt-thread是在idle线程回收僵尸线程占用的资源) - 标记当前线程无效,并进行内核对象资源回收,
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