本帖最后由 gddddd 于 2022-4-2 17:37 编辑
@21小跑堂
#申请原创#二周目:DIY超轻量RTOS,自己用就要轻飘飘
依照惯例,首先感谢雅特力公司!感谢21IC电子论坛!联合举办了这么优秀的活动,我有幸参与并获得雅特力公司赠送的AT-START-F425开发板,不甚荣幸!
在单片机领域,裸机前后台的模式和RTOS的实时模式一直以来都不分伯仲,各有各的强项!当然为了解放生产力(传统前后台的一点点问题:实时性不能很好保证、CPU利用率不够高、强迫人们按着机器的顺序工作方式思考编码),越来越多的MCU中搭载了RTOS系统,但是。。在我们常用的一些MCU中,也有相当大一部分是资源及其捉襟见肘的,特别是部分Cortex-M0核心的MCU(AT32F425对比之下那是资源相当丰富可观的,不过有更轻巧的选择,能有更多ROM和RAM资源可用,轻飘飘的飞上云端它不香吗),资源只有仅仅16KB的ROM、2KB的RAM这样的MCU,怎么用上RTOS呢,强行上了RTOS后还有资源可以正常完成工作任务吗?所以自己DIY一款合用的超轻量RTOS还是有一定的必要性,这个也不算重复造轮子(因为本来就是站在轮子上的,MOS参考了一位歪果仁小伙伴参加1KB程序比赛的作品,特别是优先级轮转这点技巧很优美,即使代码量变小同时又能避免互斥锁的死锁,只是牺牲了一定的实时性),合用的才是最好的。希望这一贴评测可以起到一点点抛砖引玉的作用,让更多小伙伴有自己量身定做的RTOS可以使用。
为了实现轻飘飘,有必要使用到汇编语言,超轻量的MiniRTOS(以下简称MOS)就是完全利用汇编语言实现的,得以保证内核占用资源更小(ROM1.5KByte+RAM80Byte),也实现了一些最基本的功能API,为了更小当然也需要牺牲一部分功能,所以更多华丽的功能没了,小伙伴想要么,那得自己添上代码哦!
为了激发大家的兴趣点,先上一点图,大家看看MOS还值得一观就继续看下去,不感兴趣的小伙伴直接Ctrl+W是最好的选择^_^
下图是MOS纯内核的资源占用情况,纯内核是可以工作的最小系统,没有其他任务就一直调度空闲任务执行。
下图是MOS的示例程序之一,同一周目评测中的Coremark跑分程序一样,跑着官方跑马灯的同时进行Coremark得分测试,同时还运行一个守护程序报实时CPU占用,并当一次Coremark得分测试完成后继续下一轮测试。利用Coremark每MHz得分作为比较参考值的话,MOS与RT-Thread的得分相比,较RT-Thread性能下降1-(2.624/2.626)=0.0008即性能下降万分之八,还有优化空间,值得继续努力!还得称赞AT32F425一声,每MHz主频2.62分的得分算是很强的,不亏是降维打击的神器,M3内核的一众MCU瑟瑟发抖。
下图是MOS的示例程序之一,写的一个简单的多任务测试,利用邮箱进行线程间的通信和同步。故事内容取材至葫芦兄弟,情节纯属虚构,如有雷同,概不负责。
看到这里说明小伙伴还是对MOS或者说类似的超轻量RTOS有亿点点兴趣滴,那么接下来就谈谈要实现像MOS这样一款超轻量RTOS,需要做一些什么准备工作或者了解什么知识点。如果全部都铺开了讲那就太多了,因为MOS也是开源在GITEE上了,源代码中也有详尽的注释说明每一句汇编的意义(主要怕时间久了自己都记不得为什么这么操作了,毕竟用汇编的时间还是极少极少),所以只谈谈个人认为重要的两个关键点,抛抛砖哈。
操作系统的实现与编译器息息相关,这里使用的最常见的Keil5,就以Keil5作为例子,首先就是需要看看编译器编译好的代码中变量的定义方式,可能有人会说,变量直接用就好了,编译器都帮我们做完了还有什么可关心的,不是这么简单,变量在编译器中定义方式的不同,会直接影响到RTOS中实现功能时的具体操作,比如数据在内存中是大端还是小端存储,再比如堆的生长方向、栈的生长方向。这里插一下知识(姿势,好男人就要姿势多你懂的)点,很多教科书上都把堆和栈说成堆栈,但我个人的理解,堆和栈是不同的两种内存利用方式,一般在我们的MCU中,堆是向上生长的供malloc和free这样的内存管理函数使用或者是被Keil5用来定义全局变量,静态变量等使用,栈是向下生长的供PUSH和POP这样的进出栈指令使用,所以把栈叫做堆栈个人认为是不严谨的。为什么要提到这个知识点,看看下图可以了解到,在Keil5中,我定义的任务(也就是一个死循环函数)中的局部变量是怎么保存的,不是很多教科书说的局部变量多了就压入堆栈中这么简单哦,虽然普遍情况是这样没错,但特殊情况特殊处理,这个一定需要理解好,写RTOS时错了一点点那就走远了。比如我定义的任务栈顶是0x20002dd0,局部变量a是32位的,那么就应该入栈在0x200002dcc位置保存局部变量a吗?No..No..No,MOS中局部变量a其实还是被Keil5定义到0x20002dd0这里了,对的就是栈顶了,这其实是和MOS的设计规划有关,这里可以这么理解为,函数的栈顶位置就是函数堆底,这个任务堆就是存放任务的局部变量用的,所以MOS中创建任务函数时有参数指定局部变量的所占大小,也就给任务函数预留出了局部变量的空间。要问为什么需要这样?因为任务的堆和栈空间都是设计整体连续向下分配的,上一个任务的栈底就是下一个任务的堆顶(比如任务A:堆顶---堆底=栈顶----栈底=任务B:堆顶---堆底=栈顶---栈底=任务C:堆顶---堆底=栈顶---栈底。。。如此循环,请仔细理解一下为什么称之为任务堆的设定,不要说任务函数就只有栈空间那来什么堆的设定),为什么要这么非常规或者说反常规的设计规划也是有原因的,本来编译器是把每个函数的局部变量都压栈,然后函数的头部都会有一句类似subs sp,sp,#0x20的语句,这个sp减去的0x20就是局部变量所占空间大小,那么有一个问题就是编译器是可以明确知道局部变量有多少的,可是MOS就无法知道了,所以这里用了取巧的方法是直接跳过了对函数中sp指针的这一句操作代码,也就变成了前面的情况,我不管局部变量有多少,反正读写时就变成在sp栈顶就好了,但是栈顶还是必须要预留好可以满足局部变量存放的空间(也就是我称之为的任务堆空间),否则万一栈穿了或者堆穿了,带来的结果都可能是灾难性的,这也是为什么RTOS容易出错且不容易追踪定位到错误点的原因之一,所以确定在MOS中确定好内存使用的位置,是保证RTOS系统本身稳定性的最好方法之一。
这段查看变量定义的代码非常实用,特别贴出来。int init_global_a = 1;
int uninit_global_a;
static int inits_global_b = 2;
static int uninits_global_b;
void output(int a)
{
printf("任务:%d\n",a);
}
void task1(){
// 在任务起始增加一条NOP指令,防止Keil高级优化时的误操作
__nop();
mos_sleep(2000);
while (1)
{
//定义局部变量
int a=0;
static int inits_local_c=0x2222, uninits_local_c;
int init_local_d = 1;
char *p;
char str[10] = "zls";
//定义常量字符串
char *var1 = "1234567890";
char *var2 = "qwertyuiop";
//动态分配
uint32_t *p1=mos_malloc(4);
uint32_t *p2=mos_malloc(4);
a=mos_get_task_id(&task0);
output(a);
init_local_d=mos_get_task_stack_sp();
//释放
if(0 != p1) mos_free(p1); // 先判断一下p1是否是空指针(0),如果是空指针强行释放会进HardFault_Handler
if(0 != p2) mos_free(p2); // 先判断一下p2是否是空指针(0),如果是空指针强行释放会进HardFault_Handler
mos_mutex_lock(5); // 加锁5号互斥锁,5号互斥锁作为串口1资源锁
printf("----------------------------------------------------------\n");
printf("内部局部变量有初值\n");
printf(" a地址:%p, 数值:0x%8x\n", &a,a);
printf(" init_local_d地址:%p, 数值:0x%8x(当前任务栈指针的值)\n", &init_local_d,init_local_d);
printf("----------------------------------------------------------\n");
printf("栈区-局部变量地址\n");
printf(" a:%p\n", &a);
printf(" init_local_d:%p\n", &init_local_d);
printf(" p:%p\n", &p);
printf(" str:%p\n", str);
printf("\n堆区-动态申请地址\n");
printf(" p1:%p\n", p1);
printf(" p2:%p\n", p2);
printf("\n全局区-全局变量和静态变量\n");
printf("\n.bss段\n");
printf("全局外部无初值 uninit_global_a :%p\n", &uninit_global_a);
printf("静态外部无初值 uninits_global_b:%p\n", &uninits_global_b);
printf("静态内部无初值 uninits_local_c :%p\n", &uninits_local_c);
printf("\n.data段\n");
printf("全局外部有初值 init_global_a :%p\n", &init_global_a);
printf("静态外部有初值 inits_global_b :%p\n", &inits_global_b);
printf("静态内部有初值 inits_local_c :%p\n", &inits_local_c);
printf("\n文字常量区\n");
printf("文字常量地址 :%p\n",var1);
printf("文字常量地址 :%p\n",var2);
printf("\n代码区\n");
printf("本函数地址 :%p\n",&task0);
printf("子函数地址 :%p\n",&output);
printf("----------------------------------------------------------\n");
printf("我是任务%d,CPU占用:%d%%\r\n",mos_get_task_id(&task1),100-(mos_get_idle_count()*100/1024));
mos_mutex_unlock(5); // 5号互斥锁解锁
a=15; // 延时15秒并让cpu占用一直保持在30%左右
while(a--) {
mos_sleep(700); // MOS内置延时调用,不占用CPU
delay_nms(300); // 循环延时调用,占用CPU
}
}
为了得到超轻量的RTOS,我们需要付出(牺牲)些什么?说实话,要小还是得汇编语言上,付出的是更多的心力和头发(我秃了也变强了)。汇编语言在很大程度上可以让流程更短小精悍,特别是对部分代码的重复利用上,C语言比不过矣。而且有很多同功能但可以不同指令来优化,既让程序执行快还能让代码所占ROM小,那么对汇编指令集的熟悉必不可少。下面举个小例子:
movs r0,#0xA1
rors r0,#1
上面两句得到的结果是r0 = 0x80000050,指令执行周期是2周期,代码量是4字节
ldr r0,=0x80000050
上面一句在编译后会得到类似这样的(LDR r1,[pc,#580];@0x080004F4)结果也是r0 = 0x80000050,指令执行周期是2周期,代码量却是2+4(@0x080004F4存放了0x80000050这个常量)字节,如果这个常量多次引用的话,那么占用结果又要掉个个来算了
综上虽然运算结果和运算时间都相同,但ROM占用还是有差别的。
同时,学习汇编本身也是对C语言理解的很大补充,比如C函数调用时传参的实现,比如C语言指针在汇编中的具体怎么调用的。。。种种姿势点够学好久了!
除了使用汇编语言增大了编程难度,还需要付出(牺牲)些什么?要小,还得精简设计,不论是RAM占用上的内核构架设计,任务数据占用设计,种种设计都要精简,能怎么小就怎么小了去设计,所以功能上也因为设计上的压缩,连带着缩了水,除了必备功能(因为是以DIY自用为前提的,所以制作多功能项,再设计成可裁剪功能的方式也没多大必要)外,其他功能先砍了(牺牲了)再看(附带了一点点好处是,代码量减少了出错的几率也随之降低,排查问题也更方便一点)。
要问还得付出(牺牲)些什么?当然还有小伙伴你们的精力呀!万一你也掉坑里了,啃啃代码,写写代码可都是要花不少精力的嘛!哈哈哈^_^
那么如果掉坑里了,可以收获什么?小伙伴你会变秃了也变强的!期待你的强大!!!独强强不如众强强,大家强才是真的强!一人强强一家,家家强强国家!
最后附上本周目MOS开源网址,下次再见!
MOS开源网址:https://gitee.com/codeinmcu/mini-rtos
一周目内容:《一周目:步入RTOS新世界之一步步移植RTTN》 移步网址:https://bbs.21ic.com/icview-3202818-1-1.html
本周目内容:《二周目:DIY超轻量RTOS,自己用就要轻飘飘》 本次网址:https://bbs.21ic.com/icview-3206306-1-1.html
次回预告:《三周目:很便宜的CMSIS-DAP对标BluePill板》
敬请期待!
|