REGINA是一个精简并且实用性很强的实时操作系统(RTOS)。其架构灵感部分来源于FreeRTOS。REGINA的体量小到让人惊讶,但这并不代表它不能实现RTOS的基本功能,REGINA实现了一个RTOS最有用的部分,包括多任务、定时器、信号量以及消息队列等。在多任务方面它支持无限制优先级,时间片轮转以及理论上的无限个任务。在定时器方面它支持周期或单次模式,在信号量方面它支持的类型有二值、计数、互斥以及递归互斥。消息队列方面它也实现了常见的RTOS操作形式。
如上面所说,REGINA的主体是较为齐全的,但其体量却非常小。我在编写它的时候尽量用最简洁的思路来实现逻辑,为此我抛弃了一些看似可有可无的部分,只为让它实现快速、高效、稳定。举个简单的例子,我用REGINA、UCOSIII、FreeRTOS在同一块板子上实现同样的简单逻辑,编译后的代码UCOSIII占用约25KB的FLASH,FreeRTOS非常优秀,仅仅占用了约10KB,但REGIAN却占用比前者更少的8KB,这足以说明其在体量上的优秀表现。不过REGINA的缺点也很明显,那就是配置单一,难以灵活地进行系统裁剪。
在我看来,REGINA是很有用的MCU多线程实现工具,如果不对具体细节过度地要求,那么它会是很理想的选择。而且其实现逻辑简单易懂,如果有兴趣的你使用了这个RTOS但发现其并不能满足你的需求,此时你便可以较为轻松地对内核进行更改和添加一些部分。不过在这里我需要说明一点,REGINA是遵循GPL协议的自由软件,你可以对它进行复制和更改,但不能对其加以任何限制。此外,对于那些想理解一个RTOS具体实现过程的朋友,我推荐你们使用它,它的体量会给人一种莫名的亲近感,我相信理解它要比理解其他的RTOS容易很多。并且REGINA的架构有很多巧妙之处,这也是值得思考的地方。目前我将REGINA移植到了ARM Cortex-M3、M4以及M7的内核上运行,它的性能表现也很让我满意。但我并不打算继续移植到新的平台,如果你有兴趣,不妨花点时间试一试,它的移植过程参考自UCOSIII,不会有很大的难度。REGINA的源码我已经上传到了github,欢迎有兴趣的朋友下载使用。 地址是:https://github.com/hlld/regina。
(由于REGINA2.0版本存在小的BUG,以及代码编写有一些难以阅读,系统的架构也存在一些问题。因此我又花了一些时间重新编写了REGINA3.0版本,并已经将源码上传到了GitHub。相比较于2.0版本,3.0修复了一些小问题,代码更加容易阅读,软件框架也更加合理。我将简单的使用例程放在了源码的/doc/目录下,有兴趣的朋友可以下载源码并测试使用,地址是https://github.com/hlld/regina-v3.0)
下面我贴上的是REGINA的源码组成、使用方式的简单例子及使用过程中应该注意的地方。如果你在使用时出现了难以解决的问题,欢迎发送邮件给我,我将尽可能地为你提供一个合理的解决方案。我的联系方式在每个源文件上都有给出。另外请使用REGINA的你原谅我不入流的英语水平,我坦诚地说源文件的很多地方注释都存在问题,但大致能表达我的意思。我认为用英语注释是一个很好的编程习惯,而且英文注释始终比中文看着要和谐一些。(在编写2.0版本时,我为了尽量加快进度,就删去了以前版本的所有注释,并且没有添上任何新的注释,虽然在一定程度上造成了代码的阅读困难,不过这并不会影响到REGINA的使用。而在3.0版本中,我重新添加了部分必要的注释,并且代码也按照一定的规范编写,因此程序的可阅读性有不小的提升)
以下是REGINA2.0的源码组成:
INCLUDE文件夹下包含配置头文件和内核头文件;PORT文件夹下包含了有关移植的相关源文件;SOURCE文件夹下包含的是内核源文件。
其中INCLUDE文件夹包含两个头文件,内容如下所示:
PORT文件夹内容如下如下所示,其中包含三个子文件夹:
每个子文件夹下包含移植相关的源文件文件如下所示:
SOURCE文件夹下内容如下所示,其中包含内核C源文件如下所示:
REGINA源码的根文件下还包含一个README文档,它较为详细地讲述了方法调用及一些注意事项。
以下是运行在STM32F103上的示例工程的MAIN.C文件,有兴趣的朋友通过它就基本能了解REGINA的使用方式。
#include "usart.h"
#include "led.h"
#include "regina.h"
T_tim_handl tim_handl;
void timer_func(void *arg)
{
INT32U *base = (INT32U *)arg;
D_print("the core tick is %d\n", *base);
}
#define task1_prior 1
T_task_handl task1_handl;
void task1_func(void *arg)
{
I_create_timer(&tim_handl, TRUE, 1000, timer_func, &g_btick);
while(1)
{
I_task_sleep(1000);
led1_on;
I_task_sleep(1000);
led1_off;
}
}
#define task2_prior 2
T_msgq_handl msg_handl;
T_task_handl task2_handl;
void task2_func(void *arg)
{
INT32U ptr[5] = {1, 2, 3, 4, 5};
I_create_msgq(&msg_handl);
while(1)
{
I_send_msg(msg_handl, ptr, 5, D_kp_wait);
ptr[0]++;
ptr[1]++;
ptr[2]++;
ptr[3]++;
ptr[4]++;
I_task_sleep(1000);
}
}
#define task3_prior 3
T_sem_handl sem_handl;
T_task_handl task3_handl;
void task3_func(void *arg)
{
I_create_sem(&sem_handl, V_sem_binary, 0);
while(1)
{
I_task_sleep(1000);
D_print("task 3 is running\n");
I_post_sem(sem_handl);
}
}
#define task4_prior 4
T_task_handl task4_handl;
void task4_func(void *arg)
{
RESULT res;
while(1)
{
res = I_wait_for_sem(sem_handl, 100);
if (res)
{
I_task_sleep(200);
led2_on;
I_task_sleep(200);
led2_off;
}
}
}
#define task5_prior 5
T_task_handl task5_handl;
void task5_func(void *arg)
{
INT32U* ptr, res = FALSE;
while(1)
{
res = I_wait_for_msg(msg_handl, TRUE, 100);
if (res)
{
D_tasks_lock()
ptr = (INT32U *)I_get_msg_data();
D_print("received message\n");
D_print("the msg is %d %d %d %d %d\n", ptr[0], ptr[1], ptr[2], ptr[3], ptr[4]);
D_print("the msg size is %d\n", I_get_msg_size());
D_tasks_unlock()
}
}
}
int main(void)
{
I_rtos_init();
UART_Init(115200);
LED_Init();
I_create_task(&task1_handl, task1_prior, 100, task1_func, NULL);
I_create_task(&task2_handl, task2_prior, 100, task2_func, NULL);
I_create_task(&task3_handl, task3_prior, 100, task3_func, NULL);
I_create_task(&task4_handl, task4_prior, 100, task4_func, NULL);
I_create_task(&task5_handl, task5_prior, 100, task5_func, NULL);
I_rtos_start();
}
该文件所描述的逻辑如下:
TASK1的死循环部分周期性的点亮和熄灭一个LED灯,在进入死循环前TASK1创建了一个定时器TIMER,TIMER每隔1秒会打印一次系统的时基(D_PRINT方法是直接引用的经过重定向的标准输出函数)。
TASK2的死循环部分每隔一秒会发送一次消息到MSG_HANDL指向的消息队列中,如果消息队列已满,则阻塞等待发送,直到消息发送成功,并且每次的消息指向的数值都在改变。在进入死循环前,TASK2创建了该消息队列。
TASK3的死循环部分每隔一秒会释放一次信号量,并且打印TASK3正在运行中。在进入死循环前TASK3创建了该信号量。
TASK4的死循环部分始终在等待TASK3释放的信号量。如果在100ms之内还没有等到,那么放弃本次等待并返回FALSE。如果在此期间成功等待到了信号量,则LED灯将闪烁一次。
TASK5的死循环部分始终在等待TASK2的消息。如果在100,之内还没有消息送达,那么放弃本次等待并返回FALSE。如果在次期间等待消息成功,则打印本次消息的内容。
MAIN方法中做的工作是首先初始化内核,接着初始化串口和LED,紧接着创建了5个TASK。最后启动内核。
使用的一些注意事项:
1、HANDL是一个指向结构体的指针,它是无类型的,这样能很好地将结构体隐藏起来。在创建和删除结构体时应传入该指针的地址而并不是指针指向的地址。
2、在任务中创建资源会有一部分不确定性,但只要确保是在高优先级任务创建资源而低优先级任务请求资源的条件下运行,那么任务依然能稳定运行。
3、用户不允许在中断服务方法中操作任务或者定时器,这是保证系统稳定、快速运行的关键。更广地来说,REGINA只允许两个方法在中断服务中被调用,分别是发送消息方法和释放信号量方法。它们的函数声明为如下:
RESULT I_post_semisr(T_sem_handl handl);
RESULT I_send_msgisr(T_msgq_handl handl, void *pdata, DWORD size);
4、当用户释放信号量时会立即返回释放结果,它并不具备延时等待释放的选项,这是我为简化信号量模型所放弃的一部分。
5、REGINA的系统资源都采用魔数(MAGIC_NUM)来标识和分类,你可以在很多内核的代码中看到它的身影。它的定义如下所示:
#define V_tcb_mn 0xA1A1
#define V_tim_mn 0xA2A2
#define V_msg_mn 0xA4A4
#define V_sem_mn 0xA8A8
MAGICNUM是判断资源是否存在和对资源进行分类的重要依据,如果在使用中你发现它与其它固定数值起了冲突,那么直接修改它的定义值就好。
6、当你调用I_rtos_init()方法后,REGINA会初始化一个系统时钟为其提供时间基准。最小的延时级别是毫秒,但REGINA也提供了精确的微秒级阻塞延时方法用以在内核初始化后进行外设初始化或精确延时等操作,它的方法声明如下:
void I_sleep_ms(FLOAT xms);
7、当任务调用等待消息方法后返回的是该方法的运行结果,并非消息值,如果要得到消息的信息需要调用以下的两个方法进行获取,它们的函数声明如下:
void* I_get_msg_data(void);
DWORD I_get_msg_size(void);
8、REGINA提供有安全临界区和任务调度器枷锁的相关宏定义,它们分别是如下:
D_enter_critical()
D_leave_critical()
D_tasks_lock()
D_tasks_unlock()
9、REGINA实现有相关安全检查、断言及任务运行时间统计的方法,用户通过配置宏定义对其功能进行裁剪。而REGINA的内存管理策略就是按照FreeRTOS的思路进行模仿编写的。用户可调用以下方法获取内核内存剩余情况和任务运行时间的统计情况:
DWORD I_get_free_heap_size(void);
DWORD I_get_free_heap_lrecord(void);
DWORD I_get_IDLE_rtick(void);
FLOAT I_get_IDLE_rrate(void);
DWORD I_get_rttask_rtick(void);
FLOAT I_get_rttask_rrate(void);
它们分别返回的是:当前内核内存剩余量、运行期间的最小内存剩余量、空闲任务运行的时间和运行时间占比、当前任务运行的时间和运行时间占比。
REGINA是我的第一个开源小项目,编写它的主要目的在于我想提供一种简化的RTOS使用方式,除去繁琐的裁剪和使用过程,使用较短的时间为MCU搭建起多线程的环境。同时它也是一个不错的RTOS自主实践小工具,如果有兴趣的你恰巧看到了这篇文章,不妨花点时间将它运行起来试一试吧。