OpenEdv-开源电子网

 找回密码
 立即注册
正点原子全套STM32/Linux/FPGA开发资料,上千讲STM32视频教程免费下载...
查看: 5305|回复: 17

学习记录贴--自制小型调度MOE

[复制链接]

9

主题

538

帖子

0

精华

论坛元老

Rank: 8Rank: 8

积分
3371
金钱
3371
注册时间
2015-1-7
在线时间
794 小时
发表于 2016-7-4 17:46:09 | 显示全部楼层 |阅读模式
本帖最后由 ianhom 于 2017-2-27 18:30 编辑

一直觉得有些像z-stack和contiki这样的小型调度系统蛮有意思的,觉得把调度的部分抽离出来应该蛮实用,于是就边学习源码边加入自己的想法,开始了这么一个小型调度MOE的编写。编写的过程收获很大,不过发现写多了,忘得也快,还是开贴记录一下比较好,同时也希望能得到大家的指导和监督。

MOE是出于学习目的而自制的小型调度,包含事件驱动、消息机制、调试选项、各种实用软件模块和应用等,整体做了模块化设计。并非功能强大的RTOS(坛子里已经有很多大神自制了RTOS),仅仅是简易的调度,但适合一些简单的项目和MCU,也利于习惯裸奔的朋友们了解OS的少许概念。项目目前还在GitHub上利用业余时间开发中 ,有相同兴趣的朋友可以一起来做
在线浏览:https://github.com/ianhom/MOE
下载页面:http://ianhom.github.io/MOE/
QQ讨论群:475258651

此贴将逐步记录编写的思路,相关介绍也会逐步完善,成型后移植到原子的板子上(手头有块mini),希望大家多多指点、监督

记录内容传送门:
关于调度 http://www.openedv.com/forum.php ... 39&page=1#pid443469
关于事件驱动 http://www.openedv.com/forum.php ... 39&page=1#pid443534
关于定时器 http://www.openedv.com/forum.php ... 39&page=1#pid443902
关于外部函数 http://www.openedv.com/forum.php ... 39&page=1#pid449273
关于任务注册 http://www.openedv.com/forum.php ... 39&page=1#pid449278
关于PT协程 http://www.openedv.com/forum.php ... 39&page=1#pid470022
关于硬件驱动 http://www.openedv.com/forum.php ... 39&page=1#pid470024
关于硬件驱动接口 http://www.openedv.com/forum.php ... 39&page=1#pid470025
再谈调度 http://www.openedv.com/forum.php ... 39&page=1#pid470028
再谈定时器 http://www.openedv.com/forum.php ... 39&page=1#pid470029
再谈消息机制 http://www.openedv.com/forum.php ... 39&page=1#pid470032
关于X宏定义http://www.openedv.com/forum.php ... 39&page=1#pid490564
关于应用编写风格http://www.openedv.com/forum.php ... 5&fromuid=30747



机器生汇编,汇编生B,B生C,C生万物.... 经过长期对C语言的研究,目前只有两个方面不懂:这也不懂,那也不懂
https://github.com/ianhom
正点原子逻辑分析仪DL16劲爆上市
回复

使用道具 举报

9

主题

538

帖子

0

精华

论坛元老

Rank: 8Rank: 8

积分
3371
金钱
3371
注册时间
2015-1-7
在线时间
794 小时
 楼主| 发表于 2016-7-4 18:26:48 | 显示全部楼层
本帖最后由 ianhom 于 2016-7-4 18:28 编辑

关于调度
调度应该是最有意思的地方,也是因为对调度的兴趣,才促成了对MOE的开发。
我们常说的裸奔,就是一个while(1)不停的进行if或switch的判断,来实现程序不同分支的执行。当分支越来越复杂,我们就可以把这个分支看做一个独立的任务,只关心任务本身想实现的功能。当任务较多的时候,就要考虑如何让CPU运行这些任务了,这就是调度的方式。
调度的方式有很多,可以让每个任务依次执行(裸奔实际上就是这样处理的),也可以让有事件发生的任务投入执行(事件驱动机制),还可以增加任务优先级。不管哪种方式,调度的方式决定了系统的运行模式,甚至性能。一旦调度方式确定了,就可以围绕这个调度(scheduler)丰富系统的其他模块和接口,逐步形成一个可用的调度系统。
了解TI ZigBee协议栈z-stack的朋友应该知道,它的osal调度是个有趣巧妙的事件驱动调度方式。每个任务都有静态的优先级,当执行到调度部分时,会根据优先级依次检查每个任务是否有事件发生,若无事件则检查下一个次优先级的任务;若有事件发生,则调用该任务的处理部分,同时放弃后续任务的事件检查,再重新检查最高级的任务...这样的好处就是,高优先级的任务总能等到及时的调度,缺点就是所谓的低优先级任务总是被排在最后,对于无明显任务优先级区分的应用场合下,被排在后面的任务只能躺枪等其他任务完成以后再获得执行权,甚至永远得不到响应。
contiki的调度方式则不同,虽然没有完全看完contiki的调度部分,但了解到它调度的基本原则(更为复杂的调度方式后续讨论)是一个事件FIFO---哪个任务的事件先发生,就执行哪个任务。这样对于各任务优先级差别不大的应用场合是合理的。
起初MOE叫OSAL-Like,是想抽离z-stack中的osal部分。但在后来发现,个人偏好事件FIFO,加之z-stack声明了些使用限制,于是就修改了调度的方式,并更名为MOE。
MOE使用了事件FIFO或事件队列的方式,先发生事件的任务先处理。同时也提供了插队的机制来确保一些高优先级的事件,这样能提高一定实时性。如果想更进一步提高实时性,可以考虑在任务过程中调度其他任务的方式(参考contiki),等考虑清楚之后再添加。

机器生汇编,汇编生B,B生C,C生万物.... 经过长期对C语言的研究,目前只有两个方面不懂:这也不懂,那也不懂
https://github.com/ianhom
回复 支持 反对

使用道具 举报

9

主题

538

帖子

0

精华

论坛元老

Rank: 8Rank: 8

积分
3371
金钱
3371
注册时间
2015-1-7
在线时间
794 小时
 楼主| 发表于 2016-7-4 22:06:55 | 显示全部楼层
关于事件驱动
相信事件驱动应该是个很熟悉的概念,单片机系统总是对内外部所发生的事件作出相应的响应---按键按下、定时结束、通讯数据到来....这些在以往的裸奔系统中如果都用查询的方式实现,将会比较低效(至少CPU没有机会休眠),所以为了高效可以采用中断的方式,实际上每个中断都对应着事件。在事件驱动的系统中,每个任务是否得到执行,基本上是取决于是否有相关的事件发生,这样任务其实挺像中断处理程序的。但不同的是,每个任务可以响应各种不同的事件。这里的事件类似于一种软件中断,它不会像硬件中断那样,在发生的那一刻进着手准备响应的中断处理,而是在产生事件的任务(或中断)运行结束之后,程序运行至调度处理部分之后,再根据事件所属的任务进行任务调度,在任务内部,会根据不同的事件作出不同的响应。
或许我将之前的调度和这里的事件驱动分开描述并不合适,因为这两者关系非常紧密。正如之前所说,MOE初期是类osal的系统,后来更换了调度方式,发现事件的产生和处理也必须得相应更改。
OSAL在编译时为每个任务申请了一个2字节的变量用于保存改任务所发生的事件,因为每个任务仅有这一个16bit的变量保存事件,为了应付同一任务有多种多个事件的情况,osal通过事件变量中的每一位来表示一个事件,即一个2字节的事件变量可以同时保存16种事件---对应位为0表示该事件未发生,对应位为1表示该事件发生。说到这要注意两个问题:第一个问题就是只能有16个事件类别,不能每个按键或SPI、uart、i2c都设置个事件,16个事件明显不够用。这时可以进行归类,比如上述事件都可以归于系统事件,而在产生系统事件的同时产生一个消息用于标记具体是哪个事件,通过这个方法就可以扩展事件的数量。第二个问题就是对于同一个任务同一个事件的多次发生,比如两个按键同时按下,产生了系统事件,同时产生两个消息,但任务的2字节事件变量只有一位来标记这两个事件。为了解决这个问题,在处理消息的时候就需要做额外工作,当搜索到一个消息的之后,要继续搜索是否还有消息,如果有就继续产生改任务的系统事件,以便该任务能有机会再次被调度并处理下一个消息。
Contiki采用的是事件队列的方式,同一个任务的同一个事件或不同事件都可以依次记录上事件队列上,只要队列空间足够,每个事件都会得到处理。对于事件类型的数量,完全取决于记录类型的变量size,如果是一个字节就可以枚举256个事件类别。
正如前述,调度和事件的方式是紧密相关的,MOE采用了事件队列的方式,作出了两方面的修改:1、事件可以插队到第一个,即刚产生的事件可以在其他已产生的事件之前进行处理;2、可自动扩展事件队列长度,采用队列(一般数组形式实现)的一个缺点是队列长度固定,如果突发的事件群过多,而系统又没有来得及消化这些事件,那事件队列将累积至满,造成后续事件的丢失。而另一种数据结构可以有效解决长度的问题----链表。如果事件通过链表的方式来记录,在RAM资源充足的情况下可以不断扩展长度,且在无事件的时候释放多余的RAM,事件插队操作在链表上也更容易实现。但链表有个缺点,生产或释放节点时有一定的开销(malloc & free),同时使用链表所产生的额外RAM消耗也不可小觑,举例来说,如果一个事件产生,必要的信息为事件类别和所说任务,一个2个字节,而为了这两个字节,假设在32位的MCU上,链表节点需要额外4个字节记录下一个节点的地址,malloc节点的时候还需要head信息假设大约8个字节,在假设malloc以8字节为块分配HEAP,那记录一个事件就需要((4+2)%8+1)*8+8 = 16个字节,有效数据占比2/16=12.5%,所以用链表也很消耗RAM。MOE为了解决这个问题,将数组循环队列和链表结合起来,例如先生成一个长度为10的队列,当队列满的时候,再动态分配一个长度为10的队列,并接到之前的队列上去,通过链表的方式将两个队列关联起来,当事件消耗到一定数量,且额外增加的队列不在需要时,在释放该队列,这样就可以解决数组队列长度受限的问题,同时解决的链表有效数据低、频繁申请&释放的开销。
对于事件还有一个方面需要考虑,就是事件的定义。MOE可以定义256个事件类别,系统已经定义了一些如定时器事件、消息事件、测试事件等等事件类别,但对于不同的任务可能会需要定义新的事件类别,同时这个事件类别很可能会别其他任务所使用(任务间的通讯),所以需要考虑清楚采用何种方式定义事件。这里还要提出一点,就是MOE采用的模块化编程,有一个基本原则就是:“完成、并通过测试软件模块,不得进行任何修改,除非功能有误”。更多关于模块化的思路后续再记录,这里需要考虑一个好的方法,既可以让所有模块(内核和应用)知道事件类型定义,又不需要修改模块的代码(.C和.H)。这个问题留给我慢慢考虑吧


机器生汇编,汇编生B,B生C,C生万物.... 经过长期对C语言的研究,目前只有两个方面不懂:这也不懂,那也不懂
https://github.com/ianhom
回复 支持 反对

使用道具 举报

9

主题

538

帖子

0

精华

论坛元老

Rank: 8Rank: 8

积分
3371
金钱
3371
注册时间
2015-1-7
在线时间
794 小时
 楼主| 发表于 2016-7-5 19:49:37 | 显示全部楼层
关于定时器
完成调度和事件部分之后,基本的core就完成了,剩下的就是围绕这个core进行重要模块的添加。其中一个很重要的模块就是软件定时器模块,因为不仅仅外部模块需要计时功能,core也可能需要利用时间信息完成一些工作。
软件定时器的基本原理: 记录当前系统ms时钟,设定定时ms数,然后回到任务调度之前(因为任务是在有事件发生后才会被调度的,所以对计时结束的检查是放在调度之外进行轮询的),检查当“前系统ms时钟”与“之前记录的ms时钟”的差值是否大于设定的ms数,即可知道定时器是否计时结束,多个计时器通过链表连维护,当计时次数为0时,将会把该计时器节点从链表上删除。当计时结束后,将会产生事先设定的事件,此刻对应任务就可以做响应处理。例如任务中需要周期控制LED的亮灭,则该任务可以设置一个针对自己的周期定时器,设定事件为“周期定时事件”,然后把需要周期处理的LED操作放到对改事件的处理部分中即可。
在回到模块化设计方向,该软件定时器几乎全靠软件实现,唯一需要的硬件支持就是“获取系统ms时钟的函数”,该函数会根据不同的硬件所有不同,为了能兼容不同的MCU和硬件时钟获取方案,采用了函数注册的方式,将外部的“获取系统ms时钟的函数”注册到core的timer中即可。定时的精度基本取决于“获取系统ms时钟的函数”的精度。
机器生汇编,汇编生B,B生C,C生万物.... 经过长期对C语言的研究,目前只有两个方面不懂:这也不懂,那也不懂
https://github.com/ianhom
回复 支持 反对

使用道具 举报

10

主题

271

帖子

0

精华

金牌会员

Rank: 6Rank: 6

积分
1236
金钱
1236
注册时间
2015-5-14
在线时间
352 小时
发表于 2016-7-6 08:28:03 | 显示全部楼层
好东西,希望楼主开坛讲课,多多传授一些软件编程思想、模块化方面的知识!!!
30年众生牛马,60年诸佛龙象!
回复 支持 反对

使用道具 举报

9

主题

538

帖子

0

精华

论坛元老

Rank: 8Rank: 8

积分
3371
金钱
3371
注册时间
2015-1-7
在线时间
794 小时
 楼主| 发表于 2016-7-6 08:39:04 | 显示全部楼层
无痕幽雨 发表于 2016-7-6 08:28
好东西,希望楼主开坛讲课,多多传授一些软件编程思想、模块化方面的知识!!!

大神别闹
机器生汇编,汇编生B,B生C,C生万物.... 经过长期对C语言的研究,目前只有两个方面不懂:这也不懂,那也不懂
https://github.com/ianhom
回复 支持 反对

使用道具 举报

9

主题

538

帖子

0

精华

论坛元老

Rank: 8Rank: 8

积分
3371
金钱
3371
注册时间
2015-1-7
在线时间
794 小时
 楼主| 发表于 2016-7-18 13:25:02 | 显示全部楼层
关于外部函数
这里的外部函数是指内核需要的,但对不同的平台有变化的函数,比如系统ms时钟函数,每个平台的实现方式不一样,但对于内核而言,它只需要改函数返回ms时钟即可。加上有模块化的要求,内核中改函数不能随意更改。这样有几种方法解决:   

1. 统一函数名称,比如在内核中调用的函数名为MOE_Core_Sys_Tm(),那外部就必须用MOE_Core_Sys_Tm()函数名实现该函数。有点事简单直接。缺点是外部的函数也可能属于其他模块,函数名不适合更改等等。
2. 动态注册的方式,定义一个static的函数指针变量,用于保存需要调用的函数的指针,这样可以动态保存函数指针,即使封装成库也没有问题,缺点是需要RAM记录函数指针,如果改指针的值意外被修改,调用时会发生意外。
3. 静态函数指针的方式,在rom中定义函数指针并在初始化的时候赋值,这样不消耗宝贵的ram,也不易出错。
机器生汇编,汇编生B,B生C,C生万物.... 经过长期对C语言的研究,目前只有两个方面不懂:这也不懂,那也不懂
https://github.com/ianhom
回复 支持 反对

使用道具 举报

9

主题

538

帖子

0

精华

论坛元老

Rank: 8Rank: 8

积分
3371
金钱
3371
注册时间
2015-1-7
在线时间
794 小时
 楼主| 发表于 2016-7-18 13:31:51 | 显示全部楼层
本帖最后由 ianhom 于 2016-7-18 20:11 编辑

关于任务注册
完成任务的实现后,需要建立任务与内核的关联,就是要让内核知道如何找到任务并在以后调度它。z-stack是通过一个静态的任务数组来实现,还有一个对应的事件数组,对应位置的事件发生,就可以调用对应位置的任务。contiki则是没有静态的任务表,而是将任务处理函数指针包含在事件信息中,处理事件时自然就调度了任务处理函数。
MOE为了减少事件信息对RAM的开销,采用了ROM静态任务表的方式。方式同z-stack,任务处理函数罗列在任务数组中,在内核进行初始化时,将依次调用任务处理函数(这里与z-stack不同的是MOE的任务是没有初始化函数,而是将初始化作为一个事件,放到处理函数中。考虑到对事件检索的效率,在任务中初始化函数放到最后一个处理,确保在正常运行过程中其他事件能得到即使响应),让每个任务获取任务编号,以后事件发生后即可通过该任务编号找到对应的任务进行处理。
在z-stack中,注册任务需要填写三部分任务相关信息:1、初始化函数;2、处理函数;3函数声明。也就是说,如果增加或减少一个任务,就需要同时修改这三个地方,工作量稍多且重复,亦容易出现失误(初始化和处理不对应)。MOE目标是在Project文件中完成所有的项目配置信息,包括函数的注册,同时希望能更简洁地注册,减少工作量和失误的机会。上文中提到MOE任务没有初始化函数,故只需要填写任务的处理函数和函数申明两个内容即可。但仍不满足“一行有效信息即可”的目标。中途试过很多方法和技巧均未成功(宏定义变参比较接近但还是失败),最终在快放弃之际参考了X MACRO的方法,实现了只需要一个有效信息-函数名,就可以实现函数申明和数组罗列。使用的体验感觉不错,增加或减少注册的任务非常便捷,提高了调试效率。
注册任务同时还会提供另外一个信息,就是总任务数量。使用宏定义是个很方便的办法,但是在罗列所有任务的时候,其实任务总数量就已经得到了,所以这里采用sizeof(数组)/sizeof(任务)的方式得到任务总数,无需额外清点任务数量,也能避免失误。但在这里遇到另一个问题,如果不用常量宏定义,在其他需要用到任务总数量的模块中,就需要外部引用任务数组,而且这时将丢失数组长度信息,即sizeof(数组)/sizeof(任务)一直等于1。为了解决该问题,同时实现所有配置全部放到一个文件中的目标,MOE的任务数组和任务总数量是放在Project_Config.h头文件中(该头文件机会每个文件都会包含)。一般来讲,一个数组实例不应该放在头文件中,因为当这个文件被include第二次的时候,就会生产第二个同名实例,导致无法编译通过。为了解决这个问题,MOE的任务数组定义为static类型,这样即使出现同名也报错。但这样另一个问题又出现了,就是多个相同的实体浪费内存,对此将数组通过const的修饰定义在了ROM中,本来这类静态的信息没有必要放到宝贵的RAM中,这样就会浪费RAM资源。有人会说虽然不浪费RAM,但是会浪费ROM。对于这个问题,MOE利用了编译的一点特性:值相同的const类型数据将自动合并,即在ROM中只有一个实例,所有模块虽然拥有各自static的任务数组,但是他们指向的实例都是同一个,故这样,每个文件都可见任务数组的“完整”内容,自然就可以通过sizeof(数组)/sizeof(任务)来得到任务总数量。
其实这个做法有点冒险,毕竟这样就会暴露任务数组,即任何一个任务都可以乱调用其他任务的处理函数。但这里我还是想保留,一来我考虑其他方法让任务函数看不见这些任务数组;二来日后或许会改变调用机制,实现任务中调用其他任务,如果任务数组对任务可见,那可以更直接调用,获取一定实时性,当然考虑安全性进行下权衡,这点留着以后慢慢考虑吧。
机器生汇编,汇编生B,B生C,C生万物.... 经过长期对C语言的研究,目前只有两个方面不懂:这也不懂,那也不懂
https://github.com/ianhom
回复 支持 反对

使用道具 举报

28

主题

176

帖子

0

精华

高级会员

Rank: 4

积分
735
金钱
735
注册时间
2014-7-1
在线时间
179 小时
发表于 2016-7-26 10:06:11 | 显示全部楼层
不错,跟着楼主学习
回复 支持 反对

使用道具 举报

9

主题

538

帖子

0

精华

论坛元老

Rank: 8Rank: 8

积分
3371
金钱
3371
注册时间
2015-1-7
在线时间
794 小时
 楼主| 发表于 2016-8-29 22:03:42 | 显示全部楼层
关于PT协程应用
- PT协程应用可以使用PT宏定义实现中途退出和断点返回,但在比较大的应用程序中不可避免需要调用写子函数,这时PT宏就无法再这些子函数中使用,如果必须使用的话,只能将该子函数写成宏函数,简单的子函数可以尝试inline并在编译器中开启inline相关选项,但inline不是必然会内联,除非充分了解熟悉。
- PT协程应用中可以使用while(1),但在while中一定要有return或含有YEILD语句,确保任务能在合适的时候释放CPU,对于需要一直运行的while(1),建议在最后增加一句PT_WAIT(1),这样确保循环内的语句能得到尽快的执行,也能让其他任务得以运行。
机器生汇编,汇编生B,B生C,C生万物.... 经过长期对C语言的研究,目前只有两个方面不懂:这也不懂,那也不懂
https://github.com/ianhom
回复 支持 反对

使用道具 举报

9

主题

538

帖子

0

精华

论坛元老

Rank: 8Rank: 8

积分
3371
金钱
3371
注册时间
2015-1-7
在线时间
794 小时
 楼主| 发表于 2016-8-29 22:05:33 | 显示全部楼层
关于硬件驱动
- 为了支持更多的平台,有更好的移植性,MOE通过HAL(Hardware Abstract Layer硬件抽象层)实现应用于硬件的屏蔽。MOE规范硬件驱动接口,不同的平台的外设只要按照定义的驱动接口提供基本功能即可,MOE将会将驱动的功能通过API提供给应用task,这样即使更换了硬件平台,只要驱动实现功能一致性有保证,那应用部分完全可以做到无修改。
- 除了通过硬件驱动注册的方式,还提供了一种硬件直接到task的参考--静态函数指针。硬件的操作函数可以在编译之前建立与task的关联,实现task到硬件的直接操作。
机器生汇编,汇编生B,B生C,C生万物.... 经过长期对C语言的研究,目前只有两个方面不懂:这也不懂,那也不懂
https://github.com/ianhom
回复 支持 反对

使用道具 举报

9

主题

538

帖子

0

精华

论坛元老

Rank: 8Rank: 8

积分
3371
金钱
3371
注册时间
2015-1-7
在线时间
794 小时
 楼主| 发表于 2016-8-29 22:06:48 | 显示全部楼层
关于硬件驱动接口
- 硬件驱动接口设计为如下6个:   
   1. 初始化  :负责使能硬件,配置初始参数,传入回调函数。
   2. 配置    :负责配置硬件参数。
   3. 控制命令:硬件控制命令,例如使能/非能硬件功能或中断。
   4. 读取操作:如果可用,可以读取硬件数据。
   5. 写入操作:如果可用,可以写入数据到硬件。
   6. 中断处理:调用上层(MOE)传入的回调函数。
机器生汇编,汇编生B,B生C,C生万物.... 经过长期对C语言的研究,目前只有两个方面不懂:这也不懂,那也不懂
https://github.com/ianhom
回复 支持 反对

使用道具 举报

9

主题

538

帖子

0

精华

论坛元老

Rank: 8Rank: 8

积分
3371
金钱
3371
注册时间
2015-1-7
在线时间
794 小时
 楼主| 发表于 2016-8-29 22:09:23 | 显示全部楼层
再谈调度
- 目前MOE的调度方式是无优先级的,哪个任务有事件就执行哪个任务,后续的事件根据发生的时间先后排队。这样的方式比较适合一些没有明确优先级的应用中,不比担心所谓的高优先级会不停霸占CPU而使得低优先级任务不得以执行。这样方法没有实时性要求。
对于有实时性要求,有明确优先级的应用中,设想下列三种调度方式:
- 1.非抢占优先级调度
- 2.抢占式统一栈调度
- 3.抢占式独立栈调度
机器生汇编,汇编生B,B生C,C生万物.... 经过长期对C语言的研究,目前只有两个方面不懂:这也不懂,那也不懂
https://github.com/ianhom
回复 支持 反对

使用道具 举报

9

主题

538

帖子

0

精华

论坛元老

Rank: 8Rank: 8

积分
3371
金钱
3371
注册时间
2015-1-7
在线时间
794 小时
 楼主| 发表于 2016-8-29 22:10:44 | 显示全部楼层
再谈定时器
- 对定时器模块检查定时器节点的效率进行了改进。
- 所有定时器节点只在下列情况下更新剩余时间:
    - 新定时节点加入之前,大家需要统一剩余时间的起始时刻;
    - 已有计时器节点重计时之前,相当于新定时节点加入计时,理由同上;
    - 有计时节点计时结束,找出下一个即将到时间的节点,大家再次统一下时间。
机器生汇编,汇编生B,B生C,C生万物.... 经过长期对C语言的研究,目前只有两个方面不懂:这也不懂,那也不懂
https://github.com/ianhom
回复 支持 反对

使用道具 举报

9

主题

538

帖子

0

精华

论坛元老

Rank: 8Rank: 8

积分
3371
金钱
3371
注册时间
2015-1-7
在线时间
794 小时
 楼主| 发表于 2016-8-29 22:11:59 | 显示全部楼层
再谈消息机制
- 目前MOE的消息机制是由源任务或中断成产消息,然后把消息放到链表的末端。目标任务收到MSG时间后,会从消息链表的头端开始查看是否有目标任务为自己的消息,如果有就获取消息。
- 下一步考虑将消息的地址直接交给目标任务,目标任务在接收到MSG事件时,可以根据附带的消息地址得到消息内容,而不用历遍整个链表。为了实现该效果,需要修改事件结构体,以便携带更多信息。
机器生汇编,汇编生B,B生C,C生万物.... 经过长期对C语言的研究,目前只有两个方面不懂:这也不懂,那也不懂
https://github.com/ianhom
回复 支持 反对

使用道具 举报

9

主题

538

帖子

0

精华

论坛元老

Rank: 8Rank: 8

积分
3371
金钱
3371
注册时间
2015-1-7
在线时间
794 小时
 楼主| 发表于 2016-10-19 19:36:51 | 显示全部楼层
本帖最后由 ianhom 于 2016-10-19 19:39 编辑

关于X宏的应用
- 针对不同的项目,会有不同的任务加载到MOE中,为了注册相关任务到MOE中,需要进行函数声明,函数指针数组赋值,函数名列表赋值。而每增加或删除任务时都要进行类似的重复操作,为了简化配置工作量,增加调试便利性,采用了X宏的方法。  
- X宏(X macro)是利用宏定义来简单复制类似代码的一种运用。   
- 举个例子,在一个c源文件中,想建立一个函数指针数组,并建立一个包含上述函数名的字符串数组(可用于显示有哪些函数,某个函数的详细信息),如果使用传统的方法,需要如下步骤:   
[mw_shl_code=c,true]
typedef void (*PF_FUNC)(int a);
//1、先声明函数:
void Function_1(int a);
void Function_2(int a);
void Function_3(int a);  

//2、定义函数指针数组:
const PF_FUNC cg_apfFuncTable[] =
{
    Function_1,
    Function_2,
    Function_3
};

//3、定义函数名字符串数组
const char* cg_apcFuncName[] =
{
    "Function_1",
    "Function_2",
    "Function_3"
};[/mw_shl_code]
- 从上面明显看出,Function_X被输入了3次,如果再1~3之间在增加一个函数,还要注意这3个地方对应位置不能出错。增加了工作量,也容易出错。   
- 为了解决这个问题,来看看X宏的解决方法:   
[mw_shl_code=c,true]
//1、首先宏定义LIST
#define LIST \
             X(Function_1)\
             X(Function_2)\
             X(Function_3)

//2、函数声明
#define X(name) void name(int a);    //重定义X宏定义,指明X定义为函数声明
LIST                                 //根据上一行宏定义生成3个函数的函数声明代码
#undef X                             //取消X宏定义的内容

//3、定义函数指针数组:
const PF_FUNC cg_apfFuncTable[] =
{
#define X(name) name,                //重定义X宏定义为函数名
LIST                                 //根据上一行宏定义生成3个函数名作为函数指针数组的成员
#undef X                             //取消X宏定义的内容
};

//4、定义函数名字符串数组
const char* cg_apcFuncName[] =
{
#define X(name) #name,               //重定义X宏定义为函数名的字符串
LIST                                 //根据上一行宏定义生成3个函数名字符串
#undef X                             //取消X宏定义的内容
};[/mw_shl_code]
- 由此可以看出,只需要在定义LIST时书写一次Function_X,通过对X的定义转变,来自动生成对应的代码,减少编码冗余和笔误的机会。   
- 有了这个基础,可以很容易追加相关代码块,例如为每个函数定义一个变量计算函数调用次数。   
[mw_shl_code=c,true]
#define X(name) unsigned int name##_CallCnt = 0;
LIST                                 //将生成代码为:Function_X_CallCnt = 0;
#undef X[/mw_shl_code]
机器生汇编,汇编生B,B生C,C生万物.... 经过长期对C语言的研究,目前只有两个方面不懂:这也不懂,那也不懂
https://github.com/ianhom
回复 支持 反对

使用道具 举报

3

主题

177

帖子

0

精华

金牌会员

Rank: 6Rank: 6

积分
1597
金钱
1597
注册时间
2016-1-28
在线时间
263 小时
发表于 2017-2-15 19:50:59 | 显示全部楼层
不错,跟着楼主学习
宠辱不惊看庭前花开花落去留无意望天上云卷云舒。
回复 支持 反对

使用道具 举报

9

主题

538

帖子

0

精华

论坛元老

Rank: 8Rank: 8

积分
3371
金钱
3371
注册时间
2015-1-7
在线时间
794 小时
 楼主| 发表于 2017-2-27 18:29:57 | 显示全部楼层
关于应用编写风格
- 目前MOE应用程序支持三种应用风格:
    1. 针对事件处理的风格,类似于中断程序,当事件发生时,调用针对该事件处理的函数。该方法可以将状态机融合进应用。
    2. PT应用风格,线性自上而下的行文,多线程假象
    3. 上述两种风格的融合,PT风格做传统意义上线程的main,而事件处理则被当做传统意义上的ISR
- 对于有callback的模块(如定时器模块),可以使用callback直接调用事件的处理,不过这个就是在任务之外的处理了,即在事件产生之后和在任务处理之前的时间段内。

关于PT应用
- PT应用可以使用PT宏定义实现中途退出和断点返回,但在比较大的应用程序中不可避免需要调用写子函数,这时PT宏就无法再这些子函数中使用,如果必须使用的话,只能将该子函数写成宏函数,简单的子函数可以尝试inline并在编译器中开启inline相关选项,但inline不是必然会内联,除非充分了解熟悉。
- PT协程应用中可以使用while(1),但在while中一定要有return或含有YEILD语句,确保任务能在合适的时候释放CPU,对于需要一直运行的while(1),建议在最后增加一句PT_WAIT(1),这样确保循环内的语句能得到尽快的执行,也能让其他任务得以运行

关于事件响应风格应用程序
- 该风格的应用程序主体并不像以往的程序那样“面向过程”,程序并不描述一个时序或流程,而是将过程中对应的处理动作划归为对事件的响应。这样虽然结构零散,但思路反而更清晰。对于无真正多线程的机制下,这种风格的应用恰恰是多应用运行的真相,此问题后续讨论。
- MOE中,“初始化”也可以认为是一个事件,可以拥有对应事件处代码。但这里需要注意一点,如果采用switch来找对应的事件处理代码,不建议像习惯那样把初始化代码放在最前面,反而应该放在最后,这是考虑到switch分支的特性,将执行最不频繁的分支放到最后,有利于运行时的效率。

关于PT+事件响应风格
- 如上所述,PT风格更接近于入门时编写的C程序---一个自上而下的风格。而事件响应则没有行文顺序,来什么事件,执行相应的代码,更像是以往的中断。那对于“主程序+中断”的传统风格,我们现在也有了“PT+事件响应”。
- 这里举一个例子:应用中周期亮灭LED_1,这个过程中使用PT_DELAY来实现,整个主程序中周而复始。然后可以用一个按键,产生一个按下事件,调度任务后并不在延时处继续运行,而是跳转到事件响应处理函数,在这里可以点亮LED_2。而在延时结束后,继续回到主应用中Toggle LED_1。这一个的过程就和传统的“主程序+中断”对应上了。
- 但这里有地方需要注意:实现响应方式的事件,不能与PT应用中的某阻塞事件相同,否者将破坏PT原有的运行过程。例如PT应用中等待某事件才能继续运行,而对此事件有了对应的事件响应处理。则在进入该task的时候,就会直接跳入事件响应程序,而PT应用得不到进一步运行(目前的设计)。就好比在传统的主程序中不停判断某个中断标志是否置位,而在中断发生时,就会进入中断处理函数,处理完以后,中断标志也随即清除,再回到主程序中,就无法再看到该标志的变化。
机器生汇编,汇编生B,B生C,C生万物.... 经过长期对C语言的研究,目前只有两个方面不懂:这也不懂,那也不懂
https://github.com/ianhom
回复 支持 反对

使用道具 举报

您需要登录后才可以回帖 登录 | 立即注册

本版积分规则



关闭

原子哥极力推荐上一条 /2 下一条

正点原子公众号

QQ|手机版|OpenEdv-开源电子网 ( 粤ICP备12000418号-1 )

GMT+8, 2024-6-9 06:04

Powered by OpenEdv-开源电子网

© 2001-2030 OpenEdv-开源电子网

快速回复 返回顶部 返回列表