今天我给大家讲一下: 时间触发的嵌入式系统 : 我先给大家了一个程序:这个程序是在PC机上测试过,大家给点意见! #include "stdio.h" #define TASKmax 5 typedef unsigned long u32; typedef unsigned int u16; typedef unsigned char u8; struct Task { void (*Ptask)(void); u8 delay; u8 period; u8 Runme; }; struct Task task[TASKmax]; u8 Task_G; u8 Task1_time= 10;//10ms u8 Task2_time= 20; void task1(void) { int i=0; for(i=0;i<2;i++) { printf("1\r\n"); }} void task2(void ){ char j=0; for(j=0;j<2;j++) { printf("2\r\n"); };} u8 TASK_dele(u8 cn){ if(task[cn].Ptask ==0) return 0; task[cn].Ptask= 0x00; task[cn].delay= 0; task[cn].period= 0; task[cn].Runme= 0; return 1;} void Task_tr(void ) { u8 taskIndex; for(taskIndex=0;taskIndex< TASKmax;taskIndex++) { if(task[taskIndex].Runme>0) { (*task[taskIndex].Ptask)(); task[taskIndex].Runme--; } if(task[taskIndex].period==0) TASK_dele(taskIndex); } } u8 task_add(void (*function)(), u8 delay,u8 period) { u8 dex=0; while((task[dex].Ptask!=0) && (dex< TASKmax)) dex++; if(dex==TASKmax) return 0; task[dex].Ptask= function; task[dex].delay= delay; task[dex].period= period; task[dex].Runme= 1; return 1;} void Task_ref(void )//任务控制 { u8 Task_tran; Task_tran=Task_G; switch(Task_tran) { case task11: //时间判断 // break; case task12: //时间判断 //如果到了就到下一个状态 break; default:break; } } void main(){ //初始化定时器 控制标志位 //初始化须要的变量 //增加任务 //while(1); task_add(task1,10,1); task_add(task2,30,2); //Task_tr();定时器 控制标志位 }//以上是系统和基本思想,可以根据自己的程序更改。 stateMachine + timerTick + queue。 在RTOS环境下的多任务模型: 任务通常阻塞在一个OS调用上(比如从消息队列取数据)。 外部如果想让该任务运转,就要向消息队列发送消息。 任务收到消息时,根据当前状态,决定如何处理消息。这就是状态机。 任务将消息队列中的消息处理完毕后,重新进入阻塞状态。 任务在处理中,有时要延时一段时间,然后才继续工作: 为了充分使用CPU,可以通过OS调用让其它任务去工作。 OS通常会提供一个taskDelay调用。 当任务调用taskDelay时,即进入阻塞状态,直到超时,才重新进入可工作状态(就绪状态)。 下面说说裸奔环境下的多任务模型: 裸奔也可以多任务,但调度是由用户自主控制。 在RTOS环境下,一般提供抢占式调度。在裸奔时,一般是任务在处理告一段落后,主动结束处理。 RTOS环境下的任务,一般处于一个while(1)循环中。 while(1){ 从消息队列接收消息。如果没有,将阻塞。 处理消息。 } 裸奔下的任务,一般采用查询方式: { 查询是否有待处理的事件。 如果没有,返回。 如果有,根据任务的当前状态,进行处理。处理完毕后,可能返回,也可能将待处理事件全部处理完毕后再返回。 } 裸奔任务其实也处于一个while(1)循环中,只不过这个循环在任务外部。 main() { A_taskInit(); //任务的初始化 B_taskInit(); ... while(1){ A_taskProc(); //任务的处理 B_taskProc(); } } 状态机既适用于OS环境,也适用于裸奔环境。 但在裸奔环境下,状态可能被切分得更细。例如后面讲的如何在裸奔环境实现taskDelay()。 消息队列既适用于OS环境,也适用于裸奔环境。 在OS环境下,消息队列机制由OS提供。 在裸奔环境下,消息队列要自己来实现。如果对队列的概念不清楚,可参考《数据结构》教材。 这个队列机制,可做成通用模块,在不同的程序中复用。 消息队列用于缓冲事件。事件不知道什么时候会到来,也不能保证来了就能迅速得到处理。 使用消息队列,可以保证每个事件都被处理到,以及处理顺序。 一般在两种情况下会用到消息队列: 存储外部事件:外部事件由中断收集,然后存储到队列。 串口接收程序中的接收循环缓冲区,可理解为消息队列。 任务间通讯:一个任务给其它任务发送消息。 timerTick,就是系统中的时钟基准。OS中总是有一个这样的基准。 在裸奔时,我们要用一个定时器(或RTC或watchdog)来建立这个时间基准。 一个tick间隔可以设置为10ms(典型RTOS的缺省设置)。让定时器10ms中断一次,中断发生时给tickNum++。 以前,我在定时器中断中设置1S标志、200ms标志等等。时间相关的任务根据这些标志判断是否要执行。 近来,一般让任务直接去察看tickNum。两次相减来判断定时是否到达。 也可以在系统中建立一个通用定时器任务,管理与不同任务相关的多个定时器;在定时到达时,由定时器任务去调用相应的callback。 系统时钟基准是所谓“零耗时裸奔”的基础。 timerTick的分辨率,决定了只适于于较大的时间延时。 在做时序时的小延时,用传统方法好了。 OS中的taskDelay()在裸奔环境下的一种实现: OS环境: void xxxTask(void) { while(1){ //waitEvent //do step_1 taskDelay(TIME_OUT_TICK_NUM); //do step_2 } } 裸奔环境: void xxxTask(void) { static unsigned int taskStat = STAT_GENERAL; //任务状态变量 static timer_t startTick; timer_t currTick; if (taskStat == STAT_GENERAL) { //check event //if no event return; //do step_1 startTick = sysGetTick(); //sysGetTick()就是察看系统时间 taskStat = STAT_WAIT; return; } else if (taskStat == STAT_WAIT) { currTick = sysGetTick(); //sysGetTick()就是察看系统时间 if ((currTick - startTick) >= TIME_OUT_TICK_NUM) { //do step_2 taskStat = STAT_GENERAL; return; } else return; } 基于状态机控制的面向对象的前后台协从多任务系统设计
一、任务分析 根据题目要求,划分任务如下: 1、键盘扫描线程 2、灯显示线程 3、LED1-LED4四个独立线程 4、后台监视线程 5、串口收发中断 共计7个线程1个中断。 二、软件整体结构设计 后台 前台 串口中断 ---------| --------------- -------------- | V | int10ms中断 | | serial中断 | | ------------- --------------- -------------- | |监视monitor| | | | ------------- ----------------- -------------- | | |键盘扫描keyscan| | 收 RI 检查 | ---------| ----------------- -------------- | | ----------------- -------------- | 灯显示displed | | 发 TI 检查 | ----------------- -------------- | | -------------------------- -------------- | LED1-LED4四个线程lednp | | RETI | -------------------------- -------------- | ----------------- | END | ----------------- 图1 软件整体结构设计图 由图1可知,本软件是基于状态机控制的前后台协从多任务系统,其基本原理是通过均衡地分割CPU时间片达到并发多任务的目的。前台任务要求极精确定时,包括键盘扫描、灯显示、LED1-LED4控制,使用10ms定时中断作为步调,可保证时间误差不超过11ms。后台运行人机监控界面,实时性要求不高,只要满足人的生理要求(响应延迟不超过0.5秒)即可,因此将其放在后台,使用剩余时间片。串口收发属于随机事件,发生频率不高,但实时性要求严格,所以用中断实现最为妥帖。 三、功能描述 1、人机监控界面monitor (1)help显示在线帮助,说明各种命令的使用方法; (2)mb、mw、md以字节、字、长字格式向内存数据区写入数据/读出数据; 如:mX 地址 个数 数据 mb 10 2 ----- 从地址10开始读出2个字节数据并显示 mb 20 3 0xaa ----- 从地址20开始连续写入3个字节,值均为0xaa (3)ls显示各线程状态号; 如: ls monitor ----- 显示监控状态机的状态变迁 ls led1p ----- 显示LED1控制状态机的状态变迁 注:此命令只显示变化的状态号,不变化不显示,由此可以动态观察运行情况。例如:显示(0)->(1)->(4)...按CTRL+B键退出监视并显示提示符“%”;或者按CTRL+G切换到另一控制台继续输入命令/显示。 (4)bye挂起后台监控,以便节省能源,同时按下key1+key4+key8再次激活(只能在某些支持电源管理的单片机上实现); (5)其他命令:退格、CTRL+C(重启)、CTRL+G(切换控制台,支持2个显存); 2、键盘扫描keyscan 因为不需要软件去抖动,所以很简单。令key=P1即可。假定按下键盘为0。变量key保存采样的键值。 加入软件去抖动也很方便,只要把去抖动状态机层串接在keyscan前即可。这种模块化设计可以平滑增加新功能,不影响其他部分,而且可以实现相当复杂的处理算法。 keyscan每隔10ms采样一次键盘,相对于人手几百毫秒的机械运动就是实时的。 3、灯显示displed 因为是直接驱动,所以直接令 P3[7..4]=led 即可。假定0为亮、1为灭。变量led保存处理后的灯显示值。 4、串口收发中断 关于串口收发中断和显示API函数及缓冲区队列处理不再叙述,详见www.hjhj.com下载中心ucos51v2.02相关源代码。 5、四个LED控制 详见LED控制状态机一节的说明。 四、具体设计思路和采用技术 分析题目的功能需求,核心对象是LED,动作有四种:快闪灭、慢闪灭、快短亮长灭、慢长亮短灭,这样我们似乎要设计4种状态机进行控制,再进一步分析,其实本质只有闪灭和亮灭的区别,其他只不过是时间参数不同,这样我们只要设计两种状态机对应就可以了。再深入想一下,如果以后增加新的动作怎么办?或者这几个灯的动作需要互换位置如何才能不改动程序而灵活实现呢?用户的需求千奇百怪,怎么才能在不改程序的前提下满足未来用户不断增长和变化的需求呢?“数据驱动”技术可以解决这个问题。 ============ * 数据驱动 * ============ 数据是灵活的,程序是僵死的,用数据驱动程序流向,既灵活又稳定。(前提是严密的数据合法性检查)程序和数据在计算机里是分家的,程序位于ROM或只读RAM里,不可写,数据位于RAM里,可读写。如果把程序的流程用数据控制,那么不同的数据组合将产生千变万化的程序行为。我们可以把程序的所有功能写在同一个程序里,然后用数据配置定义个性化的程序行为。单一版本的软件为维护和管理带来了方便。 针对本题目,我们可以给每个灯设置一个配置项smsel,如下: if(smsel==1) 闪灭状态机处理; else if(smsel==2) 亮灭状态机处理; else //可以新增其他动作的状态机处理; 由上可知,所有灯的处理程序都是由以上同一个程序段处理,差别只在于各个灯的配置数据不同。LED1和LED2的私有smsel均为1,LED3和LED4的私有smsel均为0,如果有一天客户提出改变LED1为亮灭动作,只要改它的配置数据即可,根本不用动程序。如果要增加LED5、LED6等新的处理,仍然只需调用这段程序控制动作,不必增加代码,加几个smsel私有配置数据就可以了。总之,增加新灯和改变动作,只动数据,不改程序。 -------------------- ------------------- | 状态号state | ^ | LED1数据结构 | ^ ----------------- | ---------------- | |状态机选择smsel| | | LED2数据结构 | | ----------------- | ---------------- | | 亮时间on | | | LED3数据结构 | 数据区(属性) ----------------- | ---------------- | | 灭时间off | 私有数据 | LED4数据结构 | | ----------------- | ---------------- | | 闪亮时间fon | | | 。。。LEDn | V ----------------- | ------------------- | 闪灭时间foff | | | 闪灭状态机 | | ----------------- | | 亮闪状态机 | 行为动作(方法) |当前时间计数cnt| V | 其他状态机 | | -------------------- ------------------- | 键值keyval | 公有数据 图3 LED对象在内存里的映像 -------------------- 图2 LED数据结构 如图2、3所示,LED可以用面向对象(OOP)的方法分析。每个灯有自己的属性和方法,映射到内存中就是类的实例化。比如:每个灯有自己的私有数据,当前状态号、状态机选择、亮时间、灭时间、闪亮灭时间,计数,这些数据唯一确定了此灯与众不同的个性,是每个灯特有的。一个灯受A、B两个键的控制,这个键值对外可见,是公有数据。 如图3,所有灯对象使用同一个类方法,每个对象有自己独立的数据区,即:对象只有属性不同,调用的方法程序是同一个,这也可以说是一种数据驱动吧。其实,C++程序在内存里的映像就是图3。 class LED{ private: unsigned char state; unsigned char smsel; unsigned int on,off,fon,foff,cnt; public: unsigned char keyval; void smstate();//闪灭状态机 void lmstate();//亮灭状态机 } LED led1,led2,led3,led4; 以上是对应图3的C++伪代码。 五、LED控制状态机 11 --->--- | | --------- ----------------->| 0空闲 | 10 | <---------------------> | 00/01| | | ----------- 00/01 ---------------- | | 2闭态灭 |<----------| 1延时fon秒亮 |<---- | -----------<------- ---------------- | | | | | |10/11 | ---------------- | ----------------- | | | 4延时off秒灭 | ----| 3延时foff秒灭 |---- | ---------------- 00/01----------------- | | --------------- 无条件 图4 闪灭状态机变迁图 11 --->--- | | 无条件 --------- 无条件 --------------->| 0空闲 |<-------------- | <---------------> | | 00/01 | |10 | | ---------------- --------------- | ---| 2延时off秒灭 | | 1延时on秒亮 |---- ---------------- --------------- 图5 亮灭状态机变迁图 图4、5是LED控制状态机的变迁图,第一个数字是状态号,紧接着是状态描述,条件是AB键状态。如下: B键 A键 描述 0 0 A、B键同时按下,B键优先级高,忽略A键 0 1 B键按下,关闭灯 1 0 A键按下,闪/亮灯,优先级低于关闭,随时可被B键中断(10ms采样) 1 1 没有键按下,维持原状态 图4的状态2和4不判断按键情况,其他状态每一步都先检测按键输入。 图5的状态1和2不判断按键情况,0状态每一步都先检测按键输入。 现在根据题目要求配置数据,就是初始化每个灯的属性。 LED1-----fon=0.5;foff=0.5;off=5;smsel=1 LED2-----fon=0.3;foff=0.3;off=1;smsel=1 LED3-----on=3;off=4;smsel=2 LED4-----on=10;off=6;smsel=2 ----------------------------------- | state=0 smsel=1 on=0 off=5 |<-------LED1数据区 | fon=0.5 foff=0.5 cnt=? | ----------------------------------- | state=0 smsel=1 on=0 off=1 |<-------LED2数据区 | fon=0.3 foff=0.3 cnt=? | ----------------------------------- | state=0 smsel=2 on=3 off=4 |<-------LED3数据区 | fon=0 foff=0 cnt=? | ----------------------------------- | state=0 smsel=2 on=10 off=6 |<-------LED4数据区 | fon=0.5 foff=0.5 cnt=? | ----------------------------------- | 闪灭状态机(参见图4状态变迁图) |<-------公用的行为方法 | 亮闪状态机(参见图5状态变迁图) | ----------------------------------- 图6 LED对象实例化内存映像 所有未用成员变量均缺省初始化为0,状态号初始一律为0,cnt是临时变量,保存当前时间,不必初始化。 由图6可知,所有灯的控制都是同一段程序,内存中只有一个副本,每个灯的特质体现在其独立拥有的数据区(属性)。尽管每个灯的行为各异,时间参数不同,但它们都属于灯类。 如果想改变LED1的行为,非常简单,把smsel改成2即可。想改变闪烁频率和亮灭比,只要改fon和foff。软件上如果需要引入更多的灯控,只要改一下数据区结构体数组的下标,根本不用动程序,面向对象和数据驱动的设计方法保证了后期维护升级的便利和可靠。 下面结合实例说明一下工作过程: ====================== ====================== * 状态实例分析 *重点 * ====================== ====================== 1、先看看LED1,其状态变迁如图4所示。 初始状态号state=0,位于空闲状态,此时若无按键(11),则继续检查按键事件(10ms采样),下一状态仍为自己0。 若按下A键10,状态变迁到S1(亮灯并延时fon秒),LED1的fon=0.5,此态点亮灯LED1并延时0.5秒,同时随时检查B键是否按下,如果B键按下01或者A、B键同时按下00,则立即转到2状态关灯。否则,延时0.5秒后转3态。在3态关闭灯并延时foff秒(foff=0.5秒),同时随时检查B键是否按下,如果00/01则立即转2态。否则延时满0.5秒后回到1态,周而复始灯就按指定亮灭参数闪烁起来。 一旦进入2状态,立即关闭灯,下一步进入4态延时off秒(off=5秒),因为题目要求“off 状态>=5秒”,所以此状态内不进行按键检测,以保证至少关灯5秒。延时满5秒后回到0空闲态检测按键情况,如无按键或又按下B键则继续关灯(0态缺省保持关灯状态,所以此时保证>5秒闭灯)。如果按下A键则进入闪烁状态(此时保证了=5秒闭灯)。 2、再看看LED3,其状态变迁如图5所示。 初始状态号state=0,位于空闲状态,此时若无按键(11),则继续检查按键事件(10ms采样),下一状态仍为自己0。 在0态,若按下A键10,状态变迁到S1,亮灯并延时on秒(on=3秒),因为题目要求“on 状态>=3秒”,所以此状态内不进行按键检测,以保证至少亮灯3秒。延时满3秒后回到0空闲态检测按键情况,如无按键或又按下A键则继续亮灯(0态缺省保持亮灯状态,所以此时保证>3秒亮灯)。如果按下B键则进入闭灯状态(此时保证了=3秒亮灯)。 在0态,若按下B键01或同时按下A、B键00,状态变迁到S2,亮灯并延时off秒(off=4秒),因为题目要求“off 状态>=4秒”,所以此状态内不进行按键检测,以保证至少灭灯4秒。延时满4秒后回到0空闲态检测按键情况,如无按键或又按下B键/AB键则继续灭灯(0态缺省保持灭灯状态,所以此时保证>4秒灭灯)。如果按下A键则进入亮灯状态(此时保证了=4秒灭灯)。 可以保存每次采样到的键值,新值覆盖旧值,这样程序就能记住最后一次的按键值,以便状态变迁后处理。 ======== 如何调试 ======== 连接PC机和监控串口,输入以下命令: %ls led1p<回车> (0)-> 此时monitor后台交互界面显示LED1在0状态。按下A键,显示变成 (0)->(1)-> 表明LED1进入1状态,此时灯亮,过了0.5秒后显示 (0)->(1)->(3)-> 表明LED1进入3状态,此时灯灭,再过0.5秒后显示 (0)->(1)->(3)->(1)-> 灯又亮了,就这样每隔0.5秒在控制台上显示一个状态并改变一次灯的状态。过段时间,控制台可能显示 (0)->(1)->(3)->(1)->(3)->(1)->(3)->(1)-> 此时按下B键/同时按下AB键,控制台立即显示 (0)->(1)->(3)->(1)->(3)->(1)->(3)->(1)->(2)->(4)-> 表明LED1顺序进入2状态和4状态,灯马上灭了,延时5秒后,控制台显示 (0)->(1)->(3)->(1)->(3)->(1)->(3)->(1)->(2)->(4)->(0)-> 此时进入0状态,灯依然是灭的,继续等待按键。 在PC键盘按CTRL+B退出状态显示,出现提示符 % 或者按CTRL+G切换到控制台2继续输入命令。 在提示符处输入 %mb 0 16<回车> 00000000 00 01 00 00 01 F4 00 32 --- 00 32 00 10 00 00 01 00 ................ % 其中显示的内容为LED数据区,00状态号、01状态机选择、00 00亮时间、01 F4灭时间(5秒=500个10ms=0x1F4)、00 32闪亮时间(0.5秒=50个10ms=0x32)、00 32闪灭时间(0.5秒)、00 10当前时间计数(此时为16个10ms,即0.16秒)、00键值(没有键按下)。 后面的数据是LED2的,00状态号、01状态机选择、00亮时间高8位。 可见运行情况可以通过查看内存数据区或者打印状态号获得。 六、总结 基于状态机的协从多任务就是把一个大任务分成若干小片,每一步(此处为10ms)顺序执行所有任务的一个状态(节约时间,增加实时性),这样CPU资源被各个任务瓜分,从微观上看是顺序执行,从宏观上看每个任务都好象独占一个CPU,任务是并发的。其实CPU本身就是数字系统,不连续而是离散运行的,完全可以认为分配了时间片的任务单独拥有一个慢些的CPU,用此观点看这个程序更容易理解其工作原理。 这中结构的程序可以看作多任务,虽然没有OS,没有任务调度,但状态机把任务调度过程固化在结构里了。此时没有切换消耗,所以调度过程极为迅速,只是设计者比较累。有网友说,“在程序员心中,每个程序员都是一个OS”,大概就是这个意思。 状态机在汇编和C中均可实现。在汇编里用散转方法,注意参数合法性检查;在C里用switch-case方法。 由于中断比较关键,可以单独设计一个软定时进程,在中断里仅处理时间,其他任务挪到主循环里完成,以避免中断响应延迟。 本结构的程序还可以灵活增加新功能,比如键盘去抖动模块、同时按下双键的特殊处理模块等。本结构的延时delay程序不会浪费CPU资源。 状态机的两种写法
有限状态机FSM思想广泛应用于硬件控制电路设计,也是软件上常用的一种处理方法(软件上称为FMM--有限消息机)。它把复杂的控制逻辑分解成有限个稳定状态,在每个状态上判断事件,变连续处理为离散数字处理,符合计算机的工作特点。同时,因为有限状态机具有有限个状态,所以可以在实际的工程上实现。但这并不意味着其只能进行有限次的处理,相反,有限状态机是闭环系统,有限无穷,可以用有限的状态,处理无穷的事务。 有限状态机的工作原理如图1所示,发生事件(event)后,根据当前状态(cur_state),决定执行的动作(action),并设置下一个状态号(nxt_state)。 ------------- | |-------->执行动作action 发生事件event ----->| cur_state | | |-------->设置下一状态号nxt_state ------------- 当前状态 图1 有限状态机工作原理 e0/a0 --->-- | | -------->---------- e0/a0 | | S0 |----- | -<------------ | e1/a1 | | e2/a2 V ---------- ---------- | S2 |-----<-----| S1 | ---------- e2/a2 ---------- 图2 一个有限状态机实例 -------------------------------------------- 当前状态 s0 s1 s2 | 事件 -------------------------------------------- a0/s0 -- a0/s0 | e0 -------------------------------------------- a1/s1 -- -- | e1 -------------------------------------------- a2/s2 a2/s2 -- | e2 -------------------------------------------- 表1 图2状态机实例的二维表格表示(动作/下一状态) 图2为一个状态机实例的状态转移图,它的含义是: 在s0状态,如果发生e0事件,那么就执行a0动作,并保持状态不变; 如果发生e1事件,那么就执行a1动作,并将状态转移到s1态; 如果发生e2事件,那么就执行a2动作,并将状态转移到s2态; 在s1状态,如果发生e2事件,那么就执行a2动作,并将状态转移到s2态; 在s2状态,如果发生e0事件,那么就执行a0动作,并将状态转移到s0态; 有限状态机不仅能够用状态转移图表示,还可以用二维的表格代表。一般将当前状态号写在横行上,将事件写在纵列上,如表1所示。其中“--”表示空(不执行动作,也不进行状态转移),“an/sn”表示执行动作an,同时将下一状态设置为sn。表1和图2表示的含义是完全相同的。 观察表1可知,状态机可以用两种方法实现:竖着写(在状态中判断事件)和横着写(在事件中判断状态)。这两种实现在本质上是完全等效的,但在实际操作中,效果却截然不同。 ================================== 竖着写(在状态中判断事件)C代码片段 ================================== cur_state = nxt_state; switch(cur_state){ //在当前状态中判断事件 case s0: //在s0状态 if(e0_event){ //如果发生e0事件,那么就执行a0动作,并保持状态不变; 执行a0动作; //nxt_state = s0; //因为状态号是自身,所以可以删除此句,以提高运行速度。 } else if(e1_event){ //如果发生e1事件,那么就执行a1动作,并将状态转移到s1态; 执行a1动作; nxt_state = s1; } else if(e2_event){ //如果发生e2事件,那么就执行a2动作,并将状态转移到s2态; 执行a2动作; nxt_state = s2; } break; case s1: //在s1状态 if(e2_event){ //如果发生e2事件,那么就执行a2动作,并将状态转移到s2态; 执行a2动作; nxt_state = s2; } break; case s2: //在s2状态 if(e0_event){ //如果发生e0事件,那么就执行a0动作,并将状态转移到s0态; 执行a0动作; nxt_state = s0; } } ================================== 横着写(在事件中判断状态)C代码片段 ================================== //e0事件发生时,执行的函数 void e0_event_function(int * nxt_state) { int cur_state; cur_state = *nxt_state; switch(cur_state){ case s0: //观察表1,在e0事件发生时,s1处为空 case s2: 执行a0动作; *nxt_state = s0; } } //e1事件发生时,执行的函数 void e1_event_function(int * nxt_state) { int cur_state; cur_state = *nxt_state; switch(cur_state){ case s0: //观察表1,在e1事件发生时,s1和s2处为空 执行a1动作; *nxt_state = s1; } } //e2事件发生时,执行的函数 void e2_event_function(int * nxt_state) { int cur_state; cur_state = *nxt_state; switch(cur_state){ case s0: //观察表1,在e2事件发生时,s2处为空 case s1: 执行a2动作; *nxt_state = s2; } } 上面横竖两种写法的代码片段,实现的功能完全相同,但是,横着写的效果明显好于竖着写的效果。理由如下: 1、竖着写隐含了优先级排序(其实各个事件是同优先级的),排在前面的事件判断将毫无疑问地优先于排在后面的事件判断。这种if/else if写法上的限制将破坏事件间原有的关系。而横着写不存在此问题。 2、由于处在每个状态时的事件数目不一致,而且事件发生的时间是随机的,无法预先确定,导致竖着写沦落为顺序查询方式,结构上的缺陷使得大量时间被浪费。对于横着写,在某个时间点,状态是唯一确定的,在事件里查找状态只要使用switch语句,就能一步定位到相应的状态,延迟时间可以预先准确估算。而且在事件发生时,调用事件函数,在函数里查找唯一确定的状态,并根据其执行动作和状态转移的思路清晰简洁,效率高,富有美感。 总之,我个人认为,在软件里写状态机,使用横着写的方法比较妥帖。 竖着写的方法也不是完全不能使用,在一些小项目里,逻辑不太复杂,功能精简,同时为了节约内存耗费,竖着写的方法也不失为一种合适的选择。 在FPGA类硬件设计中,以状态为中心实现控制电路状态机(竖着写)似乎是唯一的选择,因为硬件不太可能靠事件驱动(横着写)。不过,在FPGA里有一个全局时钟,在每次上升沿时进行状态切换,使得竖着写的效率并不低。虽然在硬件里竖着写也要使用IF/ELSIF这类查询语句(用VHDL开发),但他们映射到硬件上是组合逻辑,查询只会引起门级延迟(ns量级),而且硬件是真正并行工作的,这样竖着写在硬件里就没有负面影响。因此,在硬件设计里,使用竖着写的方式成为必然的选择。这也是为什么很多搞硬件的工程师在设计软件状态机时下意识地只使用竖着写方式的原因,盖思维定势使然也。 TCP和PPP框架协议里都使用了有限状态机,这类软件状态机最好使用横着写的方式实现。以某TCP协议为例,见图3,有三种类型的事件:上层下达的命令事件;下层到达的标志和数据的收包事件;超时定时器超时事件。 上层命令(open,close)事件 ----------------------------------- -------------------- | TCP | <----------超时事件timeout -------------------- ----------------------------------- RST/SYN/FIN/ACK/DATA等收包事件 图3 三大类TCP状态机事件 由图3可知,此TCP协议栈采用横着写方式实现,有3种事件处理函数,上层命令处理函数(如tcp_close);超时事件处理函数(tmr_slow);下层收包事件处理函数(tcp_process)。值得一提的是,在收包事件函数里,在各个状态里判断RST/SYN/FIN/ACK/DATA等标志(这些标志类似于事件),看起来象竖着写方式,其实,如果把包头和数据看成一个整体,那么,RST/SYN/FIN/ACK/DATA等标志就不必被看成独立的事件,而是属于同一个收包事件里的细节,这样,就不会认为在状态里查找事件,而是总体上看,是在收包事件里查找状态(横着写)。 在PPP里更是到处都能见到横着写的现象,有时间的话再细说。我个人感觉在实现PPP框架协议前必须了解横竖两种写法,而且只有使用横着写的方式才能比较完美地实现PPP。 状态图的实现有三种方式
状态图的实现有三种方式: 1、switch 2、查表 3、函数指针 如果编译器支持函数指针,最好用指针。有些编编译器的函数指针,编译完代码会较长。如果不支持,只能用SWITCH. switch做层次式状态机会麻烦一些。 在状态机中,事件队列还是比较重要的。 void Fsm_dispatch(Fsm *me) { State s = me->State__; (*s)(me); if (me->Sig_ == (Signal)0) { me->Sig_ = (Signal)EXIT_SIG; (*s)(me); //源状态执行退出动作 */ me->Sig_ = (Signal)ENTRY_SIG; (*me->State__)(me); //目标状态执行进入动作 */ } } 发一个用于单片机上的程序!
#include #include "task_switch.h" #include "MAIN.H" //灯 #define LED1 P1_7 #define LED2 P1_6 #define LED3 P1_5 #define LED4 P1_4 #define LED5 P0_1 #define LED6 P3_7 #define ON_LED1() LED1=0 #define OFF_LED1() LED1=1 #define ON_LED2() LED2=0 #define OFF_LED2() LED2=1 #define ON_LED3() LED3=0 #define OFF_LED3() LED3=1 #define ON_LED4() LED4=0 #define OFF_LED4() LED4=1 #define ON_LED5() LED5=0 #define OFF_LED5() LED5=1 #define ON_LED6() LED6=0 #define OFF_LED6() LED6=1 //按钮 #define KEY1 P1_0 #define KEY2 P1_1 #define KEY3 P1_2 #define KEY4 P1_3 //OS运行标志 unsigned char OS_running; //堆栈申请 unsigned char idata Stack[MAX_TASK][S_DEPTH]; //运行时间 unsigned int Running_Time; //程序控制块 PCB pcb[MAX_TASK]; //当前运行任务的ID号 unsigned char Current_ID; /调用该函数使任务延时t个时钟节拍 / 输入参数:0 / 一个时钟节拍为10mS /// void OS_Delay(unsigned char t) { EA=0; //关中 pcb[Current_ID].Suspend=1; //任务挂起 pcb[Current_ID].Delay=t; //设置延迟节拍数 EA=1; //开中 task_switch(); //任务切换 } ///挂起任务 /创建一个任务 ///函数入口:Task_ID 分配给任务的唯一ID号 // // Task_Priority 任务优先级 / /// Task_p 任务入口地址 /// Stack_p 任务堆栈栈低地址 /// void Task_Create(unsigned char Task_ID,unsigned char Task_Priority,unsigned int Task_p,unsigned char Stack_p) { ((unsigned char idata *)Stack_p)[0]=Task_p; //将任务入口地址保存在堆栈 ((unsigned char idata *)Stack_p)[1]=Task_p>>8; pcb[Task_ID].Suspend=0; pcb[Task_ID].Task_SP=Stack_p+Num_PUSH_bytes+1; //设置好堆栈指针 pcb[Task_ID].Priority=Task_Priority; //设置任务优先级 } / /空闲任务,优先级最低/// 二个LED不停的闪烁 // void task_idle(void) { static unsigned long int i; //使用static申明局部变量,避免临时变量使用相同地址 while(1) { ON_LED1(); //LED1亮 for(i=0;i<0x2000;i++) //延迟 { } OFF_LED1(); //LED1关 for(i=0;i<0x2000;i++) { ON_LED6(); //LED6闪烁很快,看起来是一直亮的 OFF_LED6(); } } } // /任务1 检测按钮1 并控制LED2亮灭// void task_1(void) { // static unsigned int j; while(1) { ON_LED2(); while(KEY1)OS_Delay(6); //等待KEY1按键按下 while(!KEY1)OS_Delay(6); //等待KEY1释放 OFF_LED2(); while(KEY1)OS_Delay(6); while(!KEY1)OS_Delay(6); } } /任务2 检测按钮2 并控制LED3亮灭// void task_2(void) { // static unsigned int j; while(1) { ON_LED3(); while(KEY2)OS_Delay(5); while(!KEY2)OS_Delay(5); OFF_LED3(); while(KEY2)OS_Delay(5); while(!KEY2)OS_Delay(5); } } /任务3 检测按钮3 并控制LED4亮灭// void task_3(void) { // static unsigned int j; while(1) { ON_LED4(); while(KEY3)OS_Delay(5); while(!KEY3)OS_Delay(5); OFF_LED4(); while(KEY3)OS_Delay(5); while(!KEY3)OS_Delay(5); } } /任务4 控制LED5每秒闪一次// void task_4(void) { // static unsigned int j; while(1) { ON_LED5(); OS_Delay(100); //LED5每隔1S闪一次 OFF_LED5(); OS_Delay(100); } } ///主函数// void main(void) { EA=0; //关中 ET2=1; //定时器2开中断 T2CON=0x00; //定时器自动重装模式 T2MOD=0x00; RCAP2H=0xB1; RCAP2L=0xE0; //定时时间为10ms Task_Create(0,5,(unsigned int)(void *)(&task_idle),(unsigned char)Stack[0]); //任务0初始化 Task_Create(1,4,(unsigned int)(void *)(&task_1),(unsigned char)Stack[1]); //任务1初始化 Task_Create(2,3,(unsigned int)(void *)(&task_2),(unsigned char)Stack[2]); //任务2初始化 Task_Create(3,2,(unsigned int)(void *)(&task_3),(unsigned char)Stack[3]); //任务3初始化 Task_Create(4,1,(unsigned int)(void *)(&task_4),(unsigned char)Stack[4]); //任务4初始化 pcb[4].Task_SP-=Num_PUSH_bytes-1; //调整堆栈指针 SP=pcb[4].Task_SP; //保存堆栈指针 OS_running=0; //任务未开始运行 Current_ID=MAX_TASK-1; //当前任务为最后一个任务 TR2=1; //启动定时器2 EA=1; //开中断 while(1); //死循环。定时器中断发生后,任务开始调度 |
|
|
|
|
http://blog.sina.com.cn/s/blog_70ef2ee90101svpl.html