Coroutine 的出现主要是为了解决rtos资源占用过多的情况, 对于rtos的任务来说,每一个任务都会有个栈,栈空间的大小一般为4K左右,可能会更多,也可能会少点,根据实际情况确定。对于一些资源缺乏的单片机来说,无法提供rtos所消耗的资源,但是有些单片机的开发者也想借助rtos的平台来开发,这个时候coroutine的作用就体现出来了。
raw os的coroutine设计主要是基于空闲任务,所以coroutine的优先级是最低的。既然是借助于空闲任务去实现的,那就有必要先谈谈空闲任务。
空闲任务
实时操作系统的设计一般都具有空闲任务,当系统没有事件可以处理的时候,系统就会运行空闲任务,空闲任务里面一般会cpu睡眠进入低功耗的状态,直到下一次中断唤醒cpu走出低功耗状态。也就是说,低功耗的实现是空闲任务的主要目的。
raw os 的空闲任务设计功能比较复杂,空闲任务根据宏开关CONFIG_RAW_SYSTEM_PREEMPTABL来区分抢占情况下的空闲任务还是非抢占任务下的空闲任务。在抢占情况下的空闲任务里面还有一个功能宏开关RAW_SYSTEM_CHECK。
当RAW_SYSTEM_CHECK这个宏打开时主要是运行在系统测试阶段,测试阶段时会启动监视任务栈的功能,一旦某一个任务的栈空间小于原始栈大小的12%,就立马系统停掉,用户就可以很清楚的知道是哪一个任务栈空间有问题了。
等到产品阶段时可以把RAW_SYSTEM_CHECK这个宏开关关了,可以使用用户自定义的低功耗函数。
RAW_SYSTEM_CHECK这个宏开关关闭的时候可以使用基于空闲任务的事件触发机制。具体的请参照以下的章节。
基于空闲任务的事件触发机制
Raw os的事件触发系统是基于空闲任务的,这个模块的设计可以配合raw os也可以不配合。此模块有以下特点:
1 基于UML的状态机理念设计,实现了有限状态机(fsm)以及层次状态机(HSM)。
2 实现了活动对象(ACTIVE OBJECT)的特性,一个活动对象包含了一个消息队列以及一个状态机。活动对象是具有优先级的,这样任务的实时性能够得到保证。消息队列的设计能够保证即时信号不丢失。
3活动对象(ACTIVE OBJECT)一共有64个优先级,对于小系统而言足够了。
4 整个系统消耗RAM只有几个字节,rom 消耗小于1K。而且64个活动对象共享一个栈空间。
5 消息系统支持紧急事件发送。一般的时候事件发到消息队列尾部,紧急事件可以发到头部。
6 支持活动对象超时机制,即在活动对象内部允许向某个活动对象启动一个软件定时器,超时了可以自动向指定的活动对象发送超时信号。
7 系统最大关中断时间为顺序15句C语言,在keil mdk 编译器(-O2)下总共有48句顺序汇编。假设一个机器时钟周期运行一句汇编,如果使用12M系统的8051, 系统最大关中断时间约为4us, 在72M的m3系统上为0.6us。这样的实时性对于中断反应来说是可以接受的。
8 系统任务的最大延迟时间为某个活动对象的状态机执行的最长的路径。所以设计短时间执行的状态机成为实时性关键。通常情况下状态机可以分解,写出短路径的状态机不是件难事。
9 整个事件触发系统为非抢占系统,也就是说不用担心各活动对象状态机之间的资源共享问题。唯一需要注意的是中断那边和状态机之间的资源冲突,因为有了消息机制,强制中断通过消息系统发送事件,一定程度上避免了临界区的问题。
10 基于状态机的编程对于解决某些复杂逻辑往往能出奇制胜。状态机也是目前科学界的根本基础。
11 基于状态机编程是目前最节省功耗的编程方式,裸机编程通常需要在超循环里面刻意的等待一个事件,一般为轮询。但是状态机只要没有事件就可以进入低功耗睡眠,其功耗之低是裸机编程无法匹敌的。对于rtos来说繁复的任务切换消耗了cpu大量的资源,其cpu利用率远低于状态机方式编程。
12 基于状态机的编程是一种非常机械的行为,通常画出状态机转换图,基本代码也就出来了,所见即所得,并不像某些人认为那样,状态机是很难编程的。 以下比较Raw os的事件触发系统和裸机编程的优势:
1 裸机通过大量的switch 和if一定程度上也可以实现状态机,但是软件规模大了,整个代码东跳西跳,随机性太大,基本代码无从维护。基于raw os 的事件触发机制避免了随意的switch和if, 整个框架井然有序,非常的机械性,代码是很容易维护的。适合大规模软件作战。
2裸机编程的任务最大的延迟是整个超循环轮询一圈,这样的机制是非常的糟糕,加入新的模块后,时间上更加无法估计,这是一种灾害编程。只能通过大量的异步中断去做实时性的东西,但是大量的代码在中断里面执行,会阻塞比它低优先级的中断,很容易造成其它中断得不到响应,进而数据丢失。基于raw os的事件触发机制所承受的任务最大延迟永远是活动对象的状态机执行最长的路径,不会随着模块的增加造成任务的最大延迟发生任何变化。实时性得到了保障。
3 裸机编程唯一的优势是简洁明了,适合初学者快速上手。基本C语言会用的可以快速投入产出,学习周期短。但是这个简单是有代价的,一旦系统一复杂那就是很难控制了。基于raw os 的事件触发机制非常容易上手,而且更能简洁的解决复杂逻辑性问题。
4 裸机编程实现低功耗是一件难事,很多情况下只是会轮询等待事件的发生,从而进入不到低功耗模式,但是基于raw os 的事件触发机制有事件发生才会运行,没事件发生一直处于睡眠状态,对于功耗的降低是不可言语的。
5资源使用问题,也许有人会说裸机省却了很多资源,其它rtos无法进入底资源的单片机运行,所以我们只能裸奔。对于传统的rtos来说,不幸这个是事实。对于8051这种长期不灭的怪物来说,甚至没有硬件的栈,资源也极其短缺,rtos的基础是每一个任务需要一个栈,栈的大小一般为512字节以上。8051 的确承受不起。但是基于raw os 的事件触发机制所有的活动对象共享一个栈,避免了资源的消耗,系统占的ram只有几个字节。申明一个活动对象也只要36个字节,相对裸机编程资源上并不会浪费多少。
以下比较raw os的事件触发系统和rtos编程的优势:
1 第一还是资源,尤其是ram, 目前的单片机紧缺的是ram通常不太缺rom。rtos每一个任务需要一个栈,但是raw os的事件触发系统所有的活动对象只需要一个栈,这个不是其它蜂鸟rtos所能比拟的。
2 第二实时性问题,很多rtos内核api是关中断的,比如ucos 2 的内核最大关中断时间在m3上可以超过20多us。对于中断相应这块很不利。rtos内核api是即使实现了最大关中断为0的特性,但是任务的延迟还是存在的。内核api必然要关抢占,一关抢占,任务的最大延迟时间就上升了。只要基于raw os的事件触发系统最长的状态机路径小于rtos的最大任务延迟,其实时性必然超越rtos。很多情况下这个是能做到的,因为rtos的最大任务延迟时间很长,有几十个us到上百个us。所以基于raw os的事件触发系统是很有机会击败很多rtos的实时性, 无论是中断延迟,还是任务延迟。
3 在某种特殊情况下,比如在不支持浮点数的cpu运算,运算浮点数运算,必然会造成状态机时间运行增大,这种情况下基本很难分解状态机。怎么办?如果系统的实时性要求不是那么高的话,使用浮点数运算也没问题,如果系统需要很高的实时性,那就请你不要使用浮点数运算了。什么?你一定需要浮点数运算又需要实时性?如果资源允许的话请你上raw os, raw os的事件触发系统做为raw os 的一个子模块,紧紧的和raw os连接在了一起。在raw os的事件触发系统上做的所有工作在raw os上全部无条件支持。也许事件触发系统配合raw os进行无差别的编程,是目前最优的方案。
4 功耗问题,rtos因为各任务间的频繁切换,造成了cpu的资源过度的浪费,其cpu利用率比起事件触发编程底的多,而且功耗也更大。raw os 的事件触发编程应该是目前最省功耗的编程方式了。
下面来介绍下raw os加上raw os 的事件触发编程的优势:
1 实时性,实时性永远是最重要的第一个话题,在一些高度安全的领域,系统无法响应外面的输入的话,将会产生灾难性的后果。目前主流商业rtos理论上的调度依据都是根据rms调度来的。Rms 理论简而言之任务越少,系统实时性越有保障,任务越多实时性越不能保障。假设系统的空闲任务运行raw os 的事件触发编程机制,就可以把更多的任务hold住,idle任务一共可以hold住64个活动对象,简而言之是是64个状态机。但是idle任务对于整个系统而言只是一个任务,idle内部分解任务再多也不会改变这一点。只有实时性更为紧迫的任务可以不放在idle任务里。这样设计的话会急剧减少任务数量的开支,任务实时性更有保障。
2功耗问题,因为更多的任务在idle里面完成,减少了任务的数量也就减少了任务的切换,功耗降低了不少。
3 关于资源,因为idle 任务里面运行的是事件触发机制,所有任务共享一个栈。资源会降低很多很多,最明显的是减少了任务栈的开销。比如在64K ram 里运行ucos 2 会非常吃紧,但是引入了这个机制后,64K ram 还是很丰富的晚餐。
综上所述raw os 加上基于空闲任务的事件驱动机制的在实时性以及资源利用,cpu的利用率以及功耗上是无与伦比的。
基于空闲任务的事件驱动机制是有一部分是基于状态机(FSM + HSM)来实现的,下面的篇幅会着重讲解状态机机制。
基于状态机的原理以及实践
状态机编程的历史很可能久于传统的操作系统, 传统的一个大while 循环模式普遍用到了状态机模式编程, 状态机一般是基于fsm 的有限状态机,或者更先进点的是hsm 分层的状态机。具体的fsm 以及hsm 状态机的概念读者自行参考有关概念学习,这里不再表述。推荐看Practical UML Statecharts in c/c++这本书。raw os 的状态机理念,设计部分参考了它,和著名的面向事件的操作系统QP具有异曲同工之妙。
状态机编程的其中一个优势是所有的任务可以共享一个栈,这样可以避免传统的操作系统一个任务一个栈空间的局限。所以能普遍被资源短缺的单片机系统采用,另外一个优势是面对复杂的逻辑时,往往能化繁为简,出奇制胜。
raw os巧妙地把状态机编程引进到了空闲任务里,这样的话系统空闲的时候可以进行面向状态机的编程,更进一步发挥出系统使用的效率。
需要注意的是,状态机必须要配合事件触发机制,才能真正发挥出来应有的效果。
按照目前的状态机理论而言,状态机一般而言有两种:第一种为fsm(有限状态机)第二种为hsm(层次状态机)。
有限状态机(FSM)是表示有限个状态及在这些状态之间的转移和动作等行为的数学模型,在计算机领域有着广泛的应用。通常FSM包含几个要素:状态的进入与退出、状态的触发、状态触发后引发的动作。
file:///C:/Users/ASUS/AppData/Local/Temp/ksohtml/wps_clip_image-19306.png
上图中演示的是一个fsm(有限状态机)一共有4个状态,状态1,状态2,状态3,状态4。状态1到状态2的转移是由事件1来触发的,状态2到状态3的出发是由事件2来触发的,状态3到状态4的触发是由事件3来触发的。状态4到状态1的触发是由事件4来触发的。 基于状态机的编程理念是用户需要把需求分割成为不同的状态,用状态机的理念去解决问题。
file:///C:/Users/ASUS/AppData/Local/Temp/ksohtml/wps_clip_image-6302.png
上图粗略的演示了一个层次状态机的模型,A1是一个父状态,A11是A1的一个子状态,A12也是A1的一个子状态。A11是初始状态,对于状态机来说都会有一个确定的初始状态,事件1会触发A11状态转变为A12状态。事件2会触发A12状态转变为A13状态。事件3会触发A13状态转变为A11状态。
以上只是介绍了状态机的一些最基本的概念,更多的需要读者在实际工作中把具体的事情抽象成状态机进一步去解决实际问题。
状态机API
1 RAW_U16 hsm_top(void *me, STATE_EVENT *e)
函数功能: Hsm 状态机顶层状态。
此函数的参数有2个,分别含义如下: me: 无用参数 e:无用参数
函数的返回值: STM_RET_IGNORED
2 RAW_U16 fsm_init(STM_STRUCT *me, STATE_EVENT *e)
函数功能: 初始化fsm 有限状态机
此函数的参数有2个,分别含义如下: me:fsm状态机控制模块地址 e:事件
函数的返回值: STM_SUCCESS
3 void fsm_exceute(STM_STRUCT *me, STATE_EVENT *e)
函数功能: 根据传入的事件去执行有限状态机
此函数的参数有2个,分别含义如下: me:fsm状态机控制模块地址 e:事件
4 void hsm_init(STM_STRUCT *me, STATE_EVENT *e)
函数功能: 初始化hsm分层的状态机
此函数的参数有2个,分别含义如下: me:hsm状态机控制模块地址 e:事件
函数的返回值: 无
5 void hsm_exceute(STM_STRUCT *me, STATE_EVENT *e)
函数功能: 根据传入的事件去执行层次状态机
此函数的参数有2个,分别含义如下:
me:hsm状态机控制模块地址 e:事件
函数的返回值: 无
6 RAW_U16 is_hsm_in_state(STM_STRUCT *me, stm_state_handler state)
函数功能: 判断当前状态机的状态是不是state 的状态或者state 的子状态。
此函数的参数有2个,分别含义如下: me:hsm状态机控制模块地址 state:与之比较的状态。
函数的返回值: 1: 当前模块是state 的状态或者state 的子状态。 0 : 当前模块不是state 的状态或者state 的子状态。
事件触发机制的原理以及实践
前面啰啰嗦嗦讲了一堆,读者很可能云里雾里,这coroutine到底怎么使用呢?这还是得先讲事件触发机制的功能以及API。因为使用好这个思想不是件容易的事,讲清楚更是一件难事,不过一旦使用好,如鱼得水,异常强大的功能。
事件触发机制的代码在raw_idle_event.c, 实现的功能主要是完成活动对象(ACTIVE OBJECT)之间的通信以及唤醒ACTIVE OBJECT,并且完成调度的功能。
每一个ACTIVE OBJECT都有自己的优先级,状态机以及自己的队列。具体的结构如下:
typedef struct ACTIVE_EVENT_STRUCT_CB { ACTIVE_EVENT_STRUCT *act; (1) IDLE_QUEUE_MSG *queue; (2) RAW_U8 end; (3) } ACTIVE_EVENT_STRUCT_CB;
(1) 处代码的act指针指向的结构体包含了状态机以及优先级等。 (2) 处代码是一个queue, 可以存储信息。 (3) 处代码是queue的大小。
ACTIVE OBJECT的最高优先级是0, 最低优先级是63, 总共允许有64个active object。
定义活动对象举例如下: ACTIVE_EVENT_STRUCT_CB active_idle_task[] = { { &l_bomb22.a1, q1, ARRAY_SIZE(q1)}, /*more define here*/ };
以上代码定义了一个优先级为0的活动对象,相当于创建一个任务。
下图演示的是active object之间的通讯。
file:///C:/Users/ASUS/AppData/Local/Temp/ksohtml/wps_clip_image-24906.png
可以看到active object之间可以互发信息, 中断也能发给active object 消息。只要active object的队列里面含有消息,它就处于就绪状态,随时准备被调度。如果有两个以上的active object处于就绪状态,这个时候调度器会根据优先级来选择调度最高优先级的active object运行。
事件调度器的大概流程如下: void idle_run() { while { if (有事件处理) { 选择高优先级的active object (1) 从高优先级的active object的队列里收消息 (2) 把消息传入状态机执行 (3) } else { 低功耗处理 (4) } } } 调度器会在(1)处代码的地方选择最高优先级的active object, (2)处会从队列里接收消息,在(3)处把消息传入状态机(fsm 或者hsm),然后执行。如果没有事件处理的话,整个系统在(4)处会进入低功耗模式。
事件触发机制API
1 RAW_U16 idle_event_end_post(ACTIVE_EVENT_STRUCT *me, RAW_U16 sig, void *para)
函数功能: 往active object 发送一个消息, 消息的内容为2个参数,分别为sig以及para。
此函数的参数有2个,分别含义如下: me: ACTIVE_EVENT_STRUCT的控制块指针,ACTIVE_EVENT_STRUCT包含了active object。 sig:消息信号。 para:消息参数。
函数的返回值: RAW_IDLE_EVENT_EXHAUSTED:事件消息用完,需要开大ACTIVE_EVENT_STRUCT的控制块的队列。
RAW_SUCCESS:发送成功。
2 RAW_U16 idle_event_front_post(ACTIVE_EVENT_STRUCT *me, RAW_U16 sig, void *para)
函数功能: 往active object 发送一个紧急消息, 消息会插入在队列头部。消息的内容为2个参数,分别为sig以及para。
此函数的参数有2个,分别含义如下: me: ACTIVE_EVENT_STRUCT的控制块指针,ACTIVE_EVENT_STRUCT包含了active object。 sig:消息信号。 para:消息参数。
函数的返回值: RAW_IDLE_EVENT_EXHAUSTED:事件消息用完,需要开大ACTIVE_EVENT_STRUCT的控制块的队列。
RAW_SUCCESS:发送成功。
3 RAW_U16 idle_tick_arm(ACTIVE_EVENT_STRUCT *me, RAW_TICK_TYPE ticks)
函数功能: 触发一个定时器,定时器超时的时候会像该active object发送超时信号STM_TIMEOUT_SIG。
此函数的参数有2个,分别含义如下: me: ACTIVE_EVENT_STRUCT的控制块指针,ACTIVE_EVENT_STRUCT包含了active object。 ticks: 超时的时间。
函数的返回值: RAW_NOT_CALLED_BY_ISR: 不能在中断环境中使用。 RAW_IDLE_TICK_ADD_FAILED:定时器触发失败,原因是现有的定时器还没触发,需要等到现有的触发器出发之后再触发新的定时器。 RAW_SUCCESS:触发定时器成功。
4 RAW_U16 idle_tick_disarm(ACTIVE_EVENT_STRUCT *me)
函数功能: 解除指定的活动对象的定时器。活动对象的定时器必须要处于激活状态,否则返回失败。
此函数的参数有1个,分别含义如下: me: ACTIVE_EVENT_STRUCT的控制块指针。
函数的返回值: RAW_IDLE_TICK_DELETE_FAILED:解除定时器失败,因为定时器没有处于激活状态 RAW_SUCCESS:解除定时器的触发功能。
事件触发机制内部API
1 void idle_event_init(void)
函数功能: 空闲事件初始化,使用空闲事件时,必须先要初始化。
函数参数: 无。
函数的返回值: 无
2 RAW_U16 event_post(ACTIVE_EVENT_STRUCT *me, RAW_U16 sig, void *para, RAW_U8 opt_send_method)
函数功能: 往active object 发送一个紧急消息, 消息根据不同的opt_send_method会插入在队列头部或者尾部。消息的内容为2个参数,分别为sig以及para。
此函数的参数有4个,分别含义如下: me: ACTIVE_EVENT_STRUCT的控制块指针,ACTIVE_EVENT_STRUCT包含了active object。 sig:消息信号。 para:消息参数。 opt_send_method:SEND_TO_END会插在队列尾部,否则插入到队列头部。
函数的返回值: RAW_IDLE_EVENT_EXHAUSTED:事件消息用完,需要开大ACTIVE_EVENT_STRUCT的控制块的队列。
RAW_SUCCESS:发送成功。
3 void idle_run()
函数功能: 调度活动对象,当一个活动对象处于最高优先级时,调度器会调度它运行。
函数参数: 无。
函数的返回值: 无
[ 本帖最后由 凌海滨 于 2013-11-12 14:06 编辑 ]
|