发新帖本帖赏金 10.00元(功能说明)我要提问
返回列表
打印
[活动专区]

【AT-START-F425测评】+二周目:DIY超轻量RTOS,自己用就要轻飘飘

[复制链接]
2044|12
手机看帖
扫描二维码
随时随地手机跟帖
跳转到指定楼层
楼主
gddddd|  楼主 | 2022-3-19 13:28 | 只看该作者 |只看大图 回帖奖励 |倒序浏览 |阅读模式
本帖最后由 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板》
      敬请期待!




使用特权

评论回复

打赏榜单

ArterySW 打赏了 10.00 元 2022-03-25
理由:非常认同

沙发
muyichuan2012| | 2022-3-25 11:51 | 只看该作者
感谢分享

使用特权

评论回复
板凳
mutable| | 2022-3-25 15:28 | 只看该作者
有移植过程么

使用特权

评论回复
地板
ArterySW| | 2022-3-25 17:17 | 只看该作者
移植好的代码工程可以上传一下吗    这样可以更多小伙伴一起愉快地玩耍。

使用特权

评论回复
5
gddddd|  楼主 | 2022-3-25 21:47 | 只看该作者
ArterySW 发表于 2022-3-25 17:17
移植好的代码工程可以上传一下吗    这样可以更多小伙伴一起愉快地玩耍。

多谢版主的打赏!
是我的疏忽,只上了开源链接,忘记把AT32F425的代码放帖子里了,现在补上方便感兴趣的小伙伴快速下载
MiniRTOS-0.8.0(task)(AT32F425)(完成版).zip (330.6 KB) 任务示例
MiniRTOS-0.8.0(only)(AT32F425)(完成版).zip (349.29 KB) 纯内核示例
MiniRTOS-0.8.0(displayval)(AT32F425)(完成版).zip (329.87 KB) 变量地址示例
MiniRTOS-0.8.0(coremark)(AT32F425)(完成版).zip (353.03 KB) coremark跑分示例

使用特权

评论回复
评论
muyichuan2012 2022-4-1 18:02 回复TA
太给力了 ,感谢分享源码示例 
6
gddddd|  楼主 | 2022-3-25 22:07 | 只看该作者
本帖最后由 gddddd 于 2022-3-25 22:08 编辑

移植的话,非常简单,mos.inc中已经注明了参数可选的就可以改改,与自己使用的MCU匹配(主要是RAM大小和内核在RAM中的位置定义匹配上)就好了,但因为主要适配对象是Cortex-M0核心,所以跑在M3、M4时并未针对其作出指令优化,有点小遗憾,后续有时间了再优化。创建任务按示例main.c即可,MOS本身的API不多只有几个(常用的就休眠、互斥锁和邮箱)都在mos.h中申明了,主要目的还是轻量,小而美。其他该使用什么库函数还是怎么使用,比如示例中就是用的AT32F425的官方例程移植

使用特权

评论回复
7
mutable| | 2022-4-1 16:48 | 只看该作者
gddddd 发表于 2022-3-25 22:07
移植的话,非常简单,mos.inc中已经注明了参数可选的就可以改改,与自己使用的MCU匹配(主要是RAM大小和内 ...

受教了

使用特权

评论回复
8
andygirl| | 2022-4-2 16:58 | 只看该作者
一说超轻量,就有点兴趣了

使用特权

评论回复
9
mxkw0514| | 2022-4-3 17:10 | 只看该作者
这里说的“邮箱"是什么东西呢?如何理解它呢?

使用特权

评论回复
10
mxkw0514| | 2022-4-3 17:33 | 只看该作者
好多学校取消汇编这门课程了,汇编现在还用在哪些地方呢?

使用特权

评论回复
11
gddddd|  楼主 | 2022-4-3 18:45 | 只看该作者
邮箱每个任务都各有,需要任务间通信时就向其他任务的邮箱里发送自约定规则的邮件信息,比如0x01112233约定为1号任务发来的邮件,通知当前任务17秒后执行34号代码,运行时长51秒诸如此类信息

使用特权

评论回复
12
gddddd|  楼主 | 2022-4-3 18:58 | 只看该作者
汇编语言怎么说好呢,现代编程时可以一点都不用到,但其实编译器在后台还是有一步工作将代码编译为汇编,然后再转为汇编对应的机器码(处理器只认机器码),那么有编译器帮我们做过了编译为汇编这一步,学习汇编好像也不是必须的了,但学习汇编虽然枯燥,汇编语言对核心代码段的手动优化还是非常有必要的,所以一般rtos中处理上下文的核心代码还是使用了与机器相关的汇编(汇编的最大缺点就是与机器息息相关,导致可移植性降低)。还有就是自我感觉学习使用汇编也是一个开发大脑的过程,怎么样使用最少的汇编代码实现功能非常烧脑

使用特权

评论回复
发新帖 本帖赏金 10.00元(功能说明)我要提问
您需要登录后才可以回帖 登录 | 注册

本版积分规则

9

主题

378

帖子

1

粉丝