状态机是嵌入式开发中常见的一种方法,但状态机的形式有很多,这里给大家分享一下经典的QP框架原理。
状态机基本术语
现态:是指当前所处的状态。条件:又称为“事件”,当一个条件被满足,将会触发一个动作,或者执行一次状态的迁移。动作:条件满足后执行的动作。动作执行完毕后,可以迁移到新的状态,也可以仍旧保持原状态。 动作不是必需的,当条件满足后,也可以不执行任何动作,直接迁移到新状态。次态:条件满足后要迁往的新状态。“次态”是相对于“现态”而言的,“次态”一旦被激活,就转变成新的“现态”了。
传统有限状态机Fsm实现方法
如图,是一个定时计数器,计数器存在两种状态,一种为设置状态,一种为计时状态
设置状态
“+” “-” 按键对初始倒计时进行设置当计数值设置完成,点击确认键启动计时 ,即切换到计时状态
计时状态
按下“+” “-” 会进行密码的输入。“+”表示1 ,“-”表示输入0 ,密码共有4位 确认键:只有输入的密码等于默认密码,按确认键才能停止计时,否则计时直接到零,并执行相关操作
嵌套switch
/*************************************** 1.列出所有的状态 ***************************************/ typedefenum{ SETTING, TIMING }STATE_TYPE; /*************************************** 2.列出所有的事件 ***************************************/ typedefenum{ UP_EVT, DOWN_EVT, ARM_EVT, TICK_EVT }EVENT_TYPE; /*************************************** 3.定义和状态机相关结构 ***************************************/ structbomb { uint8_tstate; uint8_ttimeout; uint8_tcode; uint8_tdefuse_code; }bomb1; /*************************************** 4.初始化状态机 ***************************************/ voidbomb1_init(void) { bomb1.state=SETTING; bomb1.defuse_code=6;//0110 } /*************************************** 5.状态机事件派发 ***************************************/ voidbomb1_fsm_dispatch(EVENT_TYPEevt,void*param) { switch(bomb1.state) { caseSETTING: { switch(evt) { caseUP_EVT://"+"按键按下事件 if(bomb1.timeout< 60) ++bomb1.timeout; bsp_display(bomb1.timeout); break; case DOWN_EVT: // "-" 按键按下事件 if(bomb1.timeout >0)--bomb1.timeout; bsp_display(bomb1.timeout); break; caseARM_EVT://"确认"按键按下事件 bomb1.state=TIMING; bomb1.code=0; break; } }break; caseTIMING: { switch(evt) { caseUP_EVT://"+"按键按下事件 bomb1.code=(bomb1.code<<1) |0x01; break; case DOWN_EVT: // "-" 按键按下事件 bomb1.code = (bomb1.code <<1); break; case ARM_EVT: // "确认" 按键按下事件 if(bomb1.code == bomb1.defuse_code){ bomb1.state = SETTING; } else{ bsp_display("bomb!") } break; case TICK_EVT: if(bomb1.timeout) { --bomb1.timeout; bsp_display(bomb1.timeout); } if(bomb1.timeout == 0) { bsp_display("bomb!") } break; } }break; } }
优点:
简单,代码阅读连贯,容易理解
缺点
当状态或事件增多时,代码状态函数需要经常改动,状态事件处理函数会代码量会不断增加
状态机没有进行封装,移植性差。
没有实现状态的进入和退出的操作。进入和退出在状态机中尤为重要
进入事件:只会在刚进入时触发一次,主要作用是对状态进行必要的初始化
退出事件:只会在状态切换时触发一次 ,主要的作用是清除状态产生的中间参数,为下次进入提供干净环境
状态表
二维状态转换表
状态机可以分为状态和事件 ,状态的跃迁都是受事件驱动的,因此可以通过一个二维表格来表示状态的跃迁。
(*) 仅当( code == defuse_code) 时才发生到setting 的转换。
/*1.列出所有的状态*/ enum { SETTING, TIMING, MAX_STATE }; /*2.列出所有的事件*/ enum { UP_EVT, DOWN_EVT, ARM_EVT, TICK_EVT, MAX_EVT }; /*3.定义状态表*/ typedefvoid(*fp_state)(EVT_TYPEevt,void*param); staticconstfp_statebomb2_table[MAX_STATE][MAX_EVENT]= { {setting_UP,setting_DOWN,setting_ARM,null}, {setting_UP,setting_DOWN,setting_ARM,timing_TICK} }; structbomb_t { constfp_stateconst*state_table;/*theState-Table*/ uint8_tstate;/*thecurrentactivestate*/ uint8_ttimeout; uint8_tcode; uint8_tdefuse_code; }; structbombbomb2= { .state_table=bomb2_table; } voidbomb2_init(void) { bomb2.defuse_code=6;//0110 bomb2.state=SETTING; } voidbomb2_dispatch(EVT_TYPEevt,void*param) { fp_states=NULL; if(evt>MAX_EVT) { LOG("EVTtypeerror!"); return; } s=bomb2.state_table[bomb2.state*MAX_EVT+evt]; if(s!=NULL) { s(evt,param); } } /*列出所有的状态对应的事件处理函数*/ voidsetting_UP(EVT_TYPEevt,void*param) { if(bomb1.timeout< 60) ++bomb1.timeout; bsp_display(bomb1.timeout); }
优点
各个状态面向用户相对独立,增加事件和状态不需要去修改先前已存在的状态事件函数。
可将状态机进行封装,有较好的移植性函数指针的安全转换 , 利用下面的特性,用户可以扩展带有私有属性的状态机和事件而使用统一的基础状态机接口
typedefvoid(*Tran)(structStateTableTag*me,Eventconst*e); voidBomb2_setting_ARM(Bomb2*me,Eventconst*e); typedefstructBomb { structStateTableTag*me;//必须为第一个成员 uint8_tprivate; }
缺点
函数粒度太小是最明显的一个缺点,一个状态和一个事件就会产生一个函数,当状态和事件较多时,处理函数将增加很快,在阅读代码时,逻辑分散。
没有实现进入退出动作。
一维状态转换表
实现原理:
typedefvoid(*fp_action)(EVT_TYPEevt,void*param); /*转换表基础结构*/ structtran_evt_t { EVT_TYPEevt; uint8_tnext_state; }; /*状态的描述*/ structfsm_state_t { fp_actionenter_action;//进入动作 fp_actionexit_action;//退出动作 fp_actionaction; tran_evt_t*tran;//转换表 uint8_ttran_nb;//转换表的大小 constchar*name; } /*状态表本体*/ #defineARRAY(x)x,sizeof(x)/sizeof(x[0]) conststructfsm_state_tstate_table[]= { {setting_enter,setting_exit,setting_action,ARRAY(set_tran_evt),"setting"}, {timing_enter,timing_exit,timing_action,ARRAY(time_tran_evt),"timing"} }; /*构建一个状态机*/ structfsm { conststructstate_t*state_table;/*theState-Table*/ uint8_tcur_state;/*thecurrentactivestate*/ uint8_ttimeout; uint8_tcode; uint8_tdefuse_code; }bomb3; /*初始化状态机*/ voidbomb3_init(void) { bomb3.state_table=state_table;//指向状态表 bomb3.cur_state=setting; bomb3.defuse_code=8;//1000 } /*状态机事件派发*/ voidfsm_dispatch(EVT_TYPEevt,void*param) { tran_evt_t*p_tran=NULL; /*获取当前状态的转换表*/ p_tran=bomb3.state_table[bomb3.cur_state]->tran; /*判断所有可能的转换是否与当前触发的事件匹配*/ for(uint8_ti=0;ievt==evt)//事件会触发转换 { if(NULL!=bomb3.state_table[bomb3.cur_state].exit_action){ bomb3.state_table[bomb3.cur_state].exit_action(NULL);//执行退出动作 } if(bomb3.state_table[_tran[i]->next_state].enter_action){ bomb3.state_table[_tran[i]->next_state].enter_action(NULL);//执行进入动作 } /*更新当前状态*/ bomb3.cur_state=p_tran[i]->next_state; } else { bomb3.state_table[bomb3.cur_state].action(evt,param); } } } /************************************************************************* setting状态相关 ************************************************************************/ voidsetting_enter(EVT_TYPEevt,void*param) { } voidsetting_exit(EVT_TYPEevt,void*param) { } voidsetting_action(EVT_TYPEevt,void*param) { } tran_evt_tset_tran_evt[]= { {ARM,timing}, } /*timing状态相关*/
优点
各个状态面向用户相对独立,增加事件和状态不需要去修改先前已存在的状态事件函数。
实现了状态的进入和退出
容易根据状态跃迁图来设计 (状态跃迁图列出了每个状态的跃迁可能,也就是这里的转换表)
实现灵活,可实现复杂逻辑,如上一次状态,增加监护条件来减少事件的数量。可实现非完全事件驱动
缺点
函数粒度较小(比二维小且增长慢),可以看到,每一个状态需要至少3个函数,还需要列出所有的转换关系。
QP嵌入式实时框架
特点
事件驱动型编程
好莱坞原则:和传统的顺序式编程方法例如“超级循环”,或传统的RTOS 的任务不同。绝大多数的现代事件驱动型系统根据好莱坞原则被构造,(Don’t call me; I’ll call you.)
面向对象
类和单一继承。
工具
QM ,一个通过UML类图来描述状态机的软件,并且可以自动生成C代码:
QS软件追踪工具:
QEP实现有限状态机Fsm
/*qevent.h----------------------------------------------------------------*/ typedefstructQEventTag { QSignalsig; uint8_tdynamic_; }QEvent; /*qep.h-------------------------------------------------------------------*/ typedefuint8_tQState;/*statusreturnedfromastate-handlerfunction*/ typedefQState(*QStateHandler)(void*me,QEventconst*e);/*argumentlist*/ typedefstructQFsmTag/*FiniteStateMachine*/ { QStateHandlerstate;/*currentactivestate*/ }QFsm; #defineQFsm_ctor(me_,initial_)((me_)->state=(initial_)) voidQFsm_init(QFsm*me,QEventconst*e); voidQFsm_dispatch(QFsm*me,QEventconst*e); #defineQ_RET_HANDLED((QState)0) #defineQ_RET_IGNORED((QState)1) #defineQ_RET_TRAN((QState)2) #defineQ_HANDLED()(Q_RET_HANDLED) #defineQ_IGNORED()(Q_RET_IGNORED) #defineQ_TRAN(target_)(((QFsm*)me)->state=(QStateHandler)(target_),Q_RET_TRAN) enumQReservedSignals { Q_ENTRY_SIG=1, Q_EXIT_SIG, Q_INIT_SIG, Q_USER_SIG }; /*fileqfsm_ini.c---------------------------------------------------------*/ #include"qep_port.h"/*theportoftheQEPeventprocessor*/ #include"qassert.h"/*embeddedsystems-friendlyassertions*/ voidQFsm_init(QFsm*me,QEventconst*e) { (*me->state)(me,e);/*executethetop-mostinitialtransition*/ /*enterthetarget*/ (void)(*me->state)(me,&QEP_reservedEvt_[Q_ENTRY_SIG]); } /*fileqfsm_dis.c---------------------------------------------------------*/ voidQFsm_dispatch(QFsm*me,QEventconst*e) { QStateHandlers=me->state;/*savethecurrentstate*/ QStater=(*s)(me,e);/*calltheeventhandler*/ if(r==Q_RET_TRAN)/*transitiontaken?*/ { (void)(*s)(me,&QEP_reservedEvt_[Q_EXIT_SIG]);/*exitthesource*/ (void)(*me->state)(me,&QEP_reservedEvt_[Q_ENTRY_SIG]);/*entertarget*/ } } 实现上面定时器例子 #include"qep_port.h"/*theportoftheQEPeventprocessor*/ #include"bsp.h"/*boardsupportpackage*/ enumBombSignals/*allsignalsfortheBombFSM*/ { UP_SIG=Q_USER_SIG, DOWN_SIG, ARM_SIG, TICK_SIG }; typedefstructTickEvtTag { QEventsuper;/*derivefromtheQEventstructure*/ uint8_tfine_time;/*thefine1/10scounter*/ }TickEvt; typedefstructBomb4Tag { QFsmsuper;/*derivefromQFsm*/ uint8_ttimeout;/*numberofsecondstillexplosion*/ uint8_tcode;/*currentlyenteredcodetodisarmthebomb*/ uint8_tdefuse;/*secretdefusecodetodisarmthebomb*/ }Bomb4; voidBomb4_ctor(Bomb4*me,uint8_tdefuse); QStateBomb4_initial(Bomb4*me,QEventconst*e); QStateBomb4_setting(Bomb4*me,QEventconst*e); QStateBomb4_timing(Bomb4*me,QEventconst*e); /*--------------------------------------------------------------------------*/ /*theinitialvalueofthetimeout*/ #defineINIT_TIMEOUT10 /*..........................................................................*/ voidBomb4_ctor(Bomb4*me,uint8_tdefuse){ QFsm_ctor_(&me->super,(QStateHandler)&Bomb4_initial); me->defuse=defuse;/*thedefusecodeisassignedatinstantiation*/ } /*..........................................................................*/ QStateBomb4_initial(Bomb4*me,QEventconst*e){ (void)e; me->timeout=INIT_TIMEOUT; returnQ_TRAN(&Bomb4_setting); } /*..........................................................................*/ QStateBomb4_setting(Bomb4*me,QEventconst*e){ switch(e->sig){ caseUP_SIG:{ if(me->timeout< 60) { ++me->timeout; BSP_display(me->timeout); } returnQ_HANDLED(); } caseDOWN_SIG:{ if(me->timeout>1){ --me->timeout; BSP_display(me->timeout); } returnQ_HANDLED(); } caseARM_SIG:{ returnQ_TRAN(&Bomb4_timing);/*transitionto"timing"*/ } } returnQ_IGNORED(); } /*..........................................................................*/ voidBomb4_timing(Bomb4*me,QEventconst*e){ switch(e->sig){ caseQ_ENTRY_SIG:{ me->code=0;/*clearthedefusecode*/ returnQ_HANDLED(); } caseUP_SIG:{ me->code<<= 1; me->code|=1; returnQ_HANDLED(); } caseDOWN_SIG:{ me->code<<= 1; return Q_HANDLED(); } case ARM_SIG: { if (me->code==me->defuse){ returnQ_TRAN(&Bomb4_setting); } returnQ_HANDLED(); } caseTICK_SIG:{ if(((TickEvtconst*)e)->fine_time==0){ --me->timeout; BSP_display(me->timeout); if(me->timeout==0){ BSP_boom();/*destroythebomb*/ } } returnQ_HANDLED(); } } returnQ_IGNORED(); }
优点
采用面向对象的设计方法,很好的移植性
实现了进入退出动作
合适的粒度,且事件的粒度可控
状态切换时通过改变指针,效率高
可扩展成为层次状态机
缺点
对事件的定义以及事件粒度的控制是设计的最大难点,如串口接收到一帧数据,这些变量的更新单独作为某个事件,还是串口收到数据作为一个事件。再或者显示屏,如果使用此种编程方式,如何设计事件。
QP 实现层次状态机 Hsm简介
初始化:
初始化层次状态机的实现:在初始化时,用户所选取的状态永远是最底层的状态,如上图,我们在计算器开机后,应该进入的是开始状态,这就涉及到一个问题,由最初top(顶状态)到begin 是有一条状态切换路径的,当我们设置状态为begin如何搜索这条路径成为关键(知道了路径才能正确的进入begin,要执行路径中过渡状态的进入和退出事件)
voidQHsm_init(QHsm*me,QEventconst*e) { Q_ALLEGE((*me->state)(me,e)==Q_RET_TRAN); t=(QStateHandler)&QHsm_top;/*HSMstartsinthetopstate*/ do{/*drillintothetarget...*/ QStateHandlerpath[QEP_MAX_NEST_DEPTH_]; int8_tip=(int8_t)0;/*transitionentrypathindex*/ path[0]=me->state;/*这里的状态为begin*/ /*通过执行空信号,从底层状态找到顶状态的路径*/ (void)QEP_TRIG_(me->state,QEP_EMPTY_SIG_); while(me->state!=t){ path[++ip]=me->state; (void)QEP_TRIG_(me->state,QEP_EMPTY_SIG_); } /*切换为begin*/ me->state=path[0];/*restorethetargetoftheinitialtran.*/ /*钻到最底层的状态,执行路径中的所有进入事件*/ Q_ASSERT(ip< (int8_t)QEP_MAX_NEST_DEPTH_); do { /* retrace the entry path in reverse (desired) order... */ QEP_ENTER_(path[ip]); /* enter path[ip] */ } while ((--ip) >=(int8_t)0); t=path[0];/*currentstatebecomesthenewsource*/ }while(QEP_TRIG_(t,Q_INIT_SIG)==Q_RET_TRAN); me->state=t; }
状态切换:
/*.................................................................*/ QStateresult(Calc*me,QEventconst*e) { switch(e->sig) {you caseENTER_SIG:{ break; } caseEXIT_SIG:{ break; } caseC_SIG: { printf("clear"); returnQ_HANDLED(); } caseB_SIG: { returnQ_TRAN(&begin); } } returnQ_SUPER(&reday); } /*.ready为result和begin的超状态................................................*/ QStateready(Calc*me,QEventconst*e) { switch(e->sig) { caseENTER_SIG:{ break; } caseEXIT_SIG:{ break; } caseOPER_SIG: { returnQ_TRAN(&opEntered); } } returnQ_SUPER(&on); } voidQHsm_dispatch(QHsm*me,QEventconst*e) { QStateHandlerpath[QEP_MAX_NEST_DEPTH_]; QStateHandlers; QStateHandlert; QStater; t=me->state;/*savethecurrentstate*/ do{/*processtheeventhierarchically...*/ s=me->state; r=(*s)(me,e);/*invokestatehandlers*/ }while(r==Q_RET_SUPER);//当前状态不能处理事件,直到找到能处理事件的状态 if(r==Q_RET_TRAN){/*transitiontaken?*/ int8_tip=(int8_t)(-1);/*transitionentrypathindex*/ int8_tiq;/*helpertransitionentrypathindex*/ path[0]=me->state;/*savethetargetofthetransition*/ path[1]=t; while(t!=s){/*exitcurrentstatetotransitionsources...*/ if(QEP_TRIG_(t,Q_EXIT_SIG)==Q_RET_HANDLED){/*exithandled?*/ (void)QEP_TRIG_(t,QEP_EMPTY_SIG_);/*findsuperstateoft*/ } t=me->state;/*me->stateholdsthesuperstate*/ } ... } me->state=t;/*setnewstateorrestorethecurrentstate*/ }img
t=path[0];/*targetofthetransition*/ if(s==t){/*(a)checksource==target(transitiontoself)*/ QEP_EXIT_(s)/*exitthesource*/ ip=(int8_t)0;/*enterthetarget*/ } else{ (void)QEP_TRIG_(t,QEP_EMPTY_SIG_);/*superstateoftarget*/ t=me->state; if(s==t){/*(b)checksource==target->super*/ ip=(int8_t)0;/*enterthetarget*/ } else{ (void)QEP_TRIG_(s,QEP_EMPTY_SIG_);/*superstateofsrc*/ /*(c)checksource->super==target->super*/ if(me->state==t){ QEP_EXIT_(s)/*exitthesource*/ ip=(int8_t)0;/*enterthetarget*/ } else{ /*(d)checksource->super==target*/ if(me->state==path[0]){ QEP_EXIT_(s)/*exitthesource*/ } else{/*(e)checkrestofsource==target->super->super.. *andstoretheentrypathalongtheway*/ ....
QP实时框架的组成
内存管理
使用内存池,对于低性能mcu,内存极为有限,引入内存管理主要是整个架构中,是以事件作为主要的任务通信手段,且事件是带参数的,可能相同类型的事件会多次触发,而事件处理完成后,需要清除事件,无法使用静态的事件,因此是有必要为不同事件创建内存池的。
对于不同块大小的内存池,需要考虑的是每个块的起始地址对齐问题。在进行内存池初始化时,我们是根据blocksize+header大小来进行划分内存池的。假设一个2字节的结构,如果以2来进行划分,假设mcu 4字节对齐,那么将有一半的结构起始地址无法对齐,这时需要为每个块预留空间,保证每个块的对齐。
事件队列
每一个活动对象维护一个事件队列,事件都是由基础事件派生的,不同类型的事件只需要将其基础事件成员添加到活动对象的队列中即可,最终在取出的时候通过一个强制转换便能获得附加的参数。
事件派发
直接事件发送:
QActive_postLIFO()
发行订阅事件发送:
竖轴表示信号(为事件的基类)
活动对象支持64个优先级,每一个活动对象要求拥有唯一优先级
通过优先级的bit位来表示某个事件被哪些活动对象订阅,并在事件触发后根据优先级为活动对象派发事件。
定时事件
非有序链表:
合作式调度器QV:
QP nano的简介
完全支持层次式状态嵌套,包括在最多4 层状态嵌套情况下,对任何状态转换拓扑的可保证的进入/ 退出动作
支持高达8 个并发执行的,可确定的,线程安全的事件队列的活动对象57
支持一个字节宽( 255 个信号)的信号,和一个可伸缩的参数,它可被配置成0 (没有参数), 1 , 2 或4 字节
使用先进先出FIFO排队策略的直接事件派发机制
每个活动对象有一个一次性时间事件(定时器),它的可配置动态范围是0(没有时间事件) , 1 , 2 或4 字节
内建的合作式vanilla 内核
内建的名为QK-nano 的可抢占型RTC内核(见第六章6.3.8节)
带有空闲回调函数的低功耗架构,用来方便的实现节省功耗模式。
在代码里为流行的低端CPU架构的C编译器的非标准扩展进行了准备(例如,在代码空间分配常数对象,可重入函数,等等)
基于断言的错误处理策略
代码风格:
责任编辑:彭菁
-
计数器
+关注
关注
32文章
2253浏览量
94343 -
计时器
+关注
关注
1文章
418浏览量
32630 -
嵌入式开发
+关注
关注
18文章
1022浏览量
47505 -
函数
+关注
关注
3文章
4304浏览量
62413 -
状态机
+关注
关注
2文章
492浏览量
27470
原文标题:经典的状态机QP框架与原理
文章出处:【微信号:strongerHuang,微信公众号:strongerHuang】欢迎添加关注!文章转载请注明出处。
发布评论请先 登录
相关推荐
评论