您所在的位置:首页 >观点 >
这3种优雅的嵌入式软件架构,你值得拥有!_消息

时间:2023-06-03 05:10:35    来源:面包芯语

状态用 switch—case 组织起来, 将事件也用switch—case 组织起来, 然后让其中一个 switch—case 整体插入到另一个 switch—case 的每一个 case 项中 。

「程序清单 List4 :」

switch(StateVal){caseS0:switch(EvntID){caseE1:action_S0_E1();/*S0状态下E1事件的响应*/StateVal=newstatevalue;/*状态迁移,不迁移则没有此行*/break;caseE2:action_S0_E2();/*S0状态下E2事件的响应*/StateVal=newstatevalue;break;......caseEm:action_S0_Em();/*S0状态下Em事件的响应*/StateVal=newstatevalue;break;default:break;}break;caseS1:......break;......caseSn:......break;default:break;}

上面的伪代码示例只是通用的情况,实际应用远没有这么复杂。虽然一个系统中事件可能有很多种,但在实际应用中,许多事件可能对某个状态是没有意义的。


(相关资料图)

例如在程序清单 List4中,如果 E2、······ Em 对处在 S0 状态下的系统没有意义,那么在 S0 的 case 下有关事件E2、······ Em 的代码根本没有必要写,状态 S0 只需要考虑事件 E1 的处理就行了。

既然是两个 switch—case 之间的嵌套, 那么就有一个谁嵌套谁的问题, 所以说 switch—case法有两种写法:状态嵌套事件和事件嵌套状态。这两种写法都可以, 各有利弊, 至于到底选用哪种方式就留给设计人员根据具体情况自行决断吧。

关于 switch—case 法还有最后一点要说明, 因为 switch—case 的原理是从上到下挨个比较,越靠后,查找耗费的时间就越长,所以要注意状态和事件在各自的 switch 语句中的安排顺序,不推荐程序清单 List4 那样按顺序号排布的方式。出现频率高或者实时性要求高的状态和事件的位置应该尽量靠前。

如果说** switch—case 法是线性的**,那么表格驱动法则是平面的。表格驱动法的实质就是将状态和事件之间的关系固化到一张二维表格里, 把事件当做纵轴,把状态当做横轴,交点[Sn , Em]则是系统在 Sn 状态下对事件 Em 的响应 。

如图 4, 我把表格中的 Node_SnEm 叫做状态机节点, 状态机节点 Node_SnEm 是系统在 Sn状态下对事件 Em 的响应。这里所说的响应包含两个方面:输出动作和状态迁移。状态机节点一般是一个类似程序清单 List5 中的结构体变量 。

structfsm_node{void(*fpAction)(void*pEvnt);INT8Uu8NxtStat;};

程序清单 List5 中的这个结构体有两个成员:fpActionu8NxtStatfpAction是一个函数指针, 指向一个形式为void func(void * pEvnt)的函数,func这个函数是对状态转移中动作序列的标准化封装。

也就是说, 状态机在状态迁移的时候, 不管输出多少个动作、操作多少个变量、调用多少个函数,这些行为统统放到函数func中去做。

把动作封装好了之后,再把封装函数func的地址交给函数指针fpAction,这样,想要输出动作,只需要调用函数指针fpAction就行了。

再看看上面的func函数,会发现函数有一个形参pEvnt,这是一个类型为void *的指针, 在程序实际运行时指向一个能存储事件的变量,通过这个指针我们就能获知关于事件的全部信息,这个形参是很有必要的。

事件一般包括两个属性:事件的类型和事件的内容。

例如一次按键事件,我们不仅要知道这是一个按键事件,还要知道按下的到底是哪个键。

事件的类型和状态机当前的状态可以让我们在图 4 的表格中迅速定位,确定该调用哪个动作封装函数, 但是动作封装函数要正确响应事件还需要知道事件的内容是什么, 这也就是形参pEvnt的意义。

由于事件的多样性,存储事件内容的数据格式不一定一样,所以就把 pEvnt 定义成了void *型,以增加灵活性。

有关 fpAction 的最后一个问题:如果事件Em对状态Sn没有意义,那么状态机节点Node_SnEm 中的 fpAction 该怎么办?

我的答案是:那就让它指向一个空函数呗!前面不是说过么,什么也不干也叫响应。

u8NxtStat存储的是状态机的一个状态值。我们知道, 状态机响应事件要输出动作, 也就是调用函数指针 fpAction 所指向的那个封装函数, 函数调用完毕后程序返回主调函数, 状态机对事件的响应就算结束了, 下一步就要考虑状态迁移的问题了。

可能要保持本状态不变, 也可能要迁移到一个新的状态,该如何抉择呢?u8NxtStat 存储的状态就是状态机想要的答案!

图 4 的这张表格反映在 C 语言代码里就是一个二维数组,第 1 维就是状态机的状态,第 2维就是统一分类的事件,而数组的元素则是程序清单 List5 中的结构体常量。

如果程序中使用表格驱动法,还需要注意一些特别的事项。要将状态当做表格的横轴,那么就要求状态值集合必须满足以下条件:

“事件” 作为纵轴,其特点和要求与用来做横轴的“状态” 完全一致。在 C 语言提供的数据类型中, 没有比枚举更符合以上要求的可选项了, 极力推荐将状态集合和事件类型集合做成枚举常量。表格驱动法的优点:调用接口统一 ,定位快速。

表格驱动法屏蔽了不同状态下处理各个事件的差异性,因此可以将处理过程中的共性部分提炼出来,做成标准统一的框架式代码,形成统一的调用接口。

根据程序清单 List5 中的状态机节点结构体,做成的框架代码如程序清单 List6 所示。

表格驱动法查找目标实际上就是一次二维数组的寻址操作,所以它的平均效率要远高于switch—case 法。

「程序清单 List6 :」

externstructfsm_nodeg_arFsmDrvTbl[][];/*状态机驱动表格*/INT8Uu8CurStat=0;/*状态暂存*/INT8Uu8EvntTyp=0;/*事件类型暂存*/void*pEvnt=NULL;/*事件变量地址暂存*/structfsm_nodestNodeTmp={NULL,0};/*状态机节点暂存*/u8CurStat=get_cur_state();/*读取当前状态*/u8EvntTyp=get_cur_evnt_typ();/*读取当前触发事件类型*/pEvnt=(void*)get_cur_evnt_ptr();/*读取事件变量地址*/stNodeTmp=g_arFsmDrvTbl[u8CurStat][u8EvntTyp];/*定位状态机节点*/stNodeTmp.fpAction(pEvnt);/*动作响应*/set_cur_state(stNodeTmp.u8NxtStat);/*状态迁移*/.....

表格驱动法好则好矣,但用它写出来的程序还有点儿小问题,我们先来看看按照表格驱动法写出来的程序有什么特点 。

前面说过,表格驱动法可以把状态机调度的部分做成标准统一的框架代码,这个框架适用性极强, 不管用状态机来实现什么样的应用, 框架代码都不需要做改动, 我们只需要根据实际应用场合规划好状态转换图,然后将图中的各个要素(状态、事件、动作、迁移,有关“条件”要素一会儿再说)用代码实现就行了,我把这部分代码称作应用代码。

在应用代码的.c 文件中, 你会看到一个声明为 const 的二维数组, 也就是图 4 所示的状态驱动表格, 还会看到许多彼此之间毫无关联的函数, 也就是前面提到的动作封装函数。

这样的一份代码, 如果手头上没有一张状态转换图, 让谁看了也会一头雾水, 这样的格式直接带来了代码可读性差的问题。

如果我们想给状态机再添加一个状态,反映到代码上就是给驱动表格再加一列内容,同时也要新添加若干个动作封装函数。

如果驱动表格很大, 做这些工作是很费事儿的, 而且容易出错。如果不小心在数组中填错了位置, 那么程序跑起来就和设计者的意图南辕北辙了,

远没有在switch—case法中改动来得方便、安全。Extended State Machine 的最大特点就是状态机响应事件之前先判断条件,根据判定结果选择执行哪些动作,转向哪个状态。

也就是说,系统在状态 Sn 下发生了事件 Em 后,转向的状态不一定是唯一的,这种灵活性是 Extended State Machine 的最有价值的优点。

回过头来看看程序清单 List5 中给出的状态机节点结构体,如果系统在状态 Sn 下发生了事件 Em, 状态机执行完 fpAction 所给出的动作响应之后, 必须转到 u8NxtStat 指定的状态。

表格驱动法的这个特性直接杜绝了 Extended State Machine 在表格驱动法中应用的可能性, 所以表格驱动法的代码实现中不存在“条件” 这个状态机要素。ESM,你是如此的优秀,我怎么舍得抛弃你 ?!

再看图 4 所示的表格驱动法示例图,如果我们把表格中的代表事件的纵轴去掉,只留下代表状态的横轴,将一列合并成一格,前文提到的问题是不是能得到解决呢?不错!这就是失传江湖多年的《葵花宝典》 ——阉割版表格驱动法!!

阉割版表格驱动法,又名压缩表格驱动法,一维状态表格与事件 switch—case 的合体。压缩表格驱动法使用了一维数组作为驱动表格,数组的下标即是状态机的各个状态。

表格中的元素叫做压缩状态机节点, 节点的主要内容还是一个指向动作封装函数的函数指针, 只不过这个动作封装函数不是为某个特定事件准备的, 而是对所有的事件都有效的。

节点中不再强制指定状态机输出动作完毕后所转向的状态, 而是让动作封装函数返回一个状态, 并把这个状态作为状态机新的状态。

压缩表格驱动法的这个特点,完美的解决了 Extended State Machine 不能在表格驱动法中使用的问题。

程序清单 List7 中的示例代码包含了压缩状态机节点结构体和状态机调用的框架代码。

「程序清单 List7:」

structfsm_node/*压缩状态机节点结构体*/{INT8U(*fpAction)(void*pEvnt);/*事件处理函数指针*/INT8Uu8StatChk;/*状态校验*/};......u8CurStat=get_cur_state();/*读取当前状态*/......if(stNodeTmp.u8StatChk==u8CurStat){u8CurStat=stNodeTmp.fpAction(pEvnt);/*事件处理*/set_cur_state(u8CurStat);/*状态迁移*/}else{state_crash(u8CurStat);/*非法状态处理*/}.....

对照程序清单 List5,就会发现程序清单 List7 中 struct fsm_node 结构体的改动之处。首先, fpAction 所指向函数的函数形式变了,动作封装函数 func 的模样成了这样的了:

INT8Ufunc(void*pEvnt);

现在的动作封装函数func是要返回类型为INT8U的返回值的,这个返回值就是状态机要转向的状态, 也就是说, 压缩表格驱动法中的状态机节点不负责状态机新状态的确定, 而把这项任务交给了动作封装函数funcfunc返回哪个状态, 状态机就转向哪个状态。

新状态由原来的常量变成了现在的变量,自然要灵活许多。上面说到现在的动作封装函数func要对当前发生的所有的事件都要负责, 那么func怎么会知道到底是哪个事件触发了它呢?看一下func的形参void * pEvnt

在程序清单 List5 中我们提到过,这个形参是用来向动作封装函数传递事件内容的,但是从前文的叙述中我们知道, pEvnt 所指向的内存包含了事件的所有信息, 包括事件类型和事件内容 , 所以通过形参 pEvnt , 动作封装函数 func 照样可以知道事件的类型。

程序清单 List7 中struct fsm_node结构体还有一个成员u8StatChk, 这里面存储的是状态机 的一个状态,干什么用的呢?

玩 C 语言数组的人都知道,要严防数组寻址越界。

要知道,压缩表格驱动法的驱动表格是一个以状态值为下标的一维数组, 数组元素里面最重要的部分就是一个个动作封装函数的地址。

函数地址在单片机看来无非就是一段二进制数据, 和内存中其它的二进制数据没什么两样,不管程序往单片机 PC 寄存器里塞什么值,单片机都没意见。假设程序由于某种意外而改动了存储状态机当前状态的变量,使变量值变成了一个非法状态。

再发生事件时, 程序就会用这个非法的状态值在驱动表格中寻址, 这时候就会发生内存泄露,程序拿泄露内存中的未知数据当函数地址跳转,不跑飞才怪!

为了防止这种现象的发生, 压缩状态机节点结构体中又添加了成员u8StatChku8StatChk中存储的是压缩状态机节点在一维驱动表格的位置, 例如某节点是表格中的第 7 个元素, 那么这个节点的成员u8StatChk值就是 6。

看一下程序清单 List7 中的框架代码示例, 程序在引用函数指针fpAction之前, 先检查当前状态和当前节点成员u8CurStat的值是否一致,一致则认为状态合法,事件正常响应,如果不一致,则认为当前状态非法,转至意外处理,最大限度保证程序运行的安全。

当然,如果泄露内存中的数据恰好和u8CurStat一致,那么这种方法真的就回天乏力了。

还有一个方法也可以防止状态机跑飞,如果状态变量是枚举,那么框架代码就可以获知状态值的最大值, 在调用动作封装函数之前判断一下当前状态值是否在合法的范围之内, 同样能保证状态机的安全运行。

压缩表格驱动法中动作封装函数的定义形式我们已经知道了,函数里面到底是什么样子的呢?程序清单 List8 是一个标准的示例。

「程序清单List8:」

INT8Uaction_S0(void*pEvnt){INT8Uu8NxtStat=0;INT8Uu8EvntTyp=get_evnt_typ(pEvnt);switch(u8EvntTyp){caseE1:action_S0_E1();/*事件E1的动作响应*/u8NxtStat=newstatevalue;/*状态迁移,不迁移也必须有本行*/break;......caseEm:action_S0_Em();/*事件Em的动作响应*/u8NxtStat=newstatevalue;/*状态迁移,不迁移也必须有本行*/break;default:;/*不相关事件处理*/break;}returnu8NxtStat;/*返回新状态*/}

从程序清单 List8 可以看出, 动作封装函数其实就是事件switch—case的具体实现。函数根据形参pEvnt获知事件类型, 并根据事件类型选择动作响应, 确定状态机迁移状态, 最后将新的状态作为执行结果返回给框架代码。

有了这样的动作封装函数,Extended State Machine的应用就可以完全不受限制了!到此,有关压缩表格驱动法的介绍就结束了。

个人认为压缩表格驱动法是相当优秀的,它既有表格驱动法的简洁、高效、标准,又有 switch—case 法的直白、灵活、多变,相互取长补短,相得益彰。

上面说过,用 C 语言实现状态机主要有 3 种方法(switch—case 法、表格驱动法、函数指针法), 其中函数指针法是最难理解的, 它的实质就是把动作封装函数的函数地址作为状态来看待。不过,有了之前压缩表格驱动法的铺垫,函数指针法就变得好理解了,因为两者本质上是相同的。

压缩表格驱动法的实质就是一个整数值(状态机的一个状态)到一个函数地址(动作封装函数)的一对一映射, 压缩表格驱动法的驱动表格就是全部映射关系的直接载体。在驱动表格中通过状态值就能找到函数地址,通过函数地址同样能反向找到状态值。

我们用一个全局的整型变量来记录状态值,然后再查驱动表格找函数地址,那干脆直接用一个全局的函数指针来记录状态得了,还费那劳什子劲干吗?!这就是函数指针法的前世今生。

用函数指针法写出来的动作封装函数和程序清单 List8 的示例函数是很相近的, 只不过函数的返回值不再是整型的状态值, 而是下一个动作封装函数的函数地址, 函数返回后, 框架代码再把这个函数地址存储到全局函数指针变量中。

相比压缩表格驱动法,在函数指针法中状态机的安全运行是个大问题,我们很难找出一种机制来检查全局函数指针变量中的函数地址是不是合法值。如果放任不管, 一旦函数指针变量中的数据被篡改,程序跑飞几乎就不可避免了。

有关状态机的东西说了那么多,相信大家都已经感受到了这种工具的优越性,状态机真的是太好用了!其实我们至始至终讲的都是有限状态机(Finite State Machine现在知道为什么前面的代码中老是有fsm这个缩写了吧!), 还有一种比有限状态机更 NB 更复杂的状态机, 那就是层次状态机(Hierarchical State Machine一般简写为HSM)。

通俗的说,系统中只存在一个状态机的叫做有限状态机,同时存在多个状态机的叫做层次状态机(其实这样解释层次状态机有些不严谨, 并行状态机也有多个状态机, 但层次状态机各个状态机之间是上下级关系,而并行状态机各个状态机之间是平级关系)。

层次状态机是一种父状态机包含子状态机的多状态机结构,里面包含了许多与面向对象相似的思想, 所以它的功能也要比有限状态机更加强大, 当一个问题用有限状态机解决起来有些吃力的时候, 就需要层次状态机出马了。

层次状态机理论我理解得也不透彻, 就不在这里班门弄斧了,大家可以找一些有关状态机理论的专业书籍来读一读。要掌握状态机编程,理解状态机(主要指有限状态机)只是第一步,也是最简单的一步,更重要的技能是能用状态机这个工具去分析解剖实际问题:划分状态、 提取事件、 确定转换关系、规定动作等等,形成一张完整的状态转换图,最后还要对转换图进行优化,达到最佳。

把实际问题变成了状态转换图, 工作的一大半就算完成了, 这个是具有架构师气质的任务,剩下的问题就是按照状态图编程写代码了,这个是具有代码工特色的工作。

用心推荐,联合朋友开发了一个嵌入式培训班,0基础也可以参加。

对未来迷茫的同学或者朋友,可以来试试,无论是转行还是本专业的人。

只要认真完成课程,拿到一份好offer绝对不在话下。也有很多专科、双非本科院校的同学通过学习此课程拿到了一个好offer。

有兴趣的同学请加下方微信咨询:

标签:
这3种优雅的嵌入式软件架构,你值得拥有!_消息

这3种优雅的嵌入式软件架构,你值得拥有!_消息

状态机的实现无非就是三个要素:状态、事件、响应。转换成具体的行为,其实就三句话:发生了什么事?现在系

制作花灯作文_制作花灯

制作花灯作文_制作花灯

你们好,最近小信发现有诸多的小伙伴们对于制作花灯作文,制作花灯这个问题都颇为感兴趣的,今天小活为大家

山东泰山战大连人首发浮现,费莱尼冲锋,老莫复出,中场看点足

山东泰山战大连人首发浮现,费莱尼冲锋,老莫复出,中场看点足

在左边路位置,上一场比赛崔康熙派出了吉翔客串,而本轮对阵大连人,孙国文有望回到首发位置,他在训练中非

发展绿色算力,和你我有什么关系?_观速讯

发展绿色算力,和你我有什么关系?_观速讯

6月1日,首届中国绿色算力大会新闻发布会在北京召开。据发布会介绍,首届中国绿色算力大会将于2023年7月1日

世界信息:梵文在线转换器生成器(梵文在线转换)

世界信息:梵文在线转换器生成器(梵文在线转换)

来为大家解答以上的问题。梵文在线转换器生成器,梵文在线转换这个很多人还不知道,现在让我们一起来看看吧

因李从瑞辞任 中国金茂股东大会补充重选张增根为执董的议案 全球微动态

因李从瑞辞任 中国金茂股东大会补充重选张增根为执董的议案 全球微动态

张增根将于股东周年大会上退任,并符合资格及愿意膺选连任。

业内罕见!当了十一年银行行长,他今年辞职,董事会最新公告,称他“不称职”……_新要闻

业内罕见!当了十一年银行行长,他今年辞职,董事会最新公告,称他“不称职”……_新要闻

来源:每日经济新闻5月31日,青岛农商银行(002958,股价2 77元,市值154 4亿元)公布了将于6月20日召开的

关于女性的身体,男人们很少知道里面的冷知识

关于女性的身体,男人们很少知道里面的冷知识

人类的性别特征比较明显,很容易区分,但其实除了这些部分,男女之间还有很多差异,但并不明显。今天小编就

二三线城市适合做什么生意_三线城市做什么生意好

二三线城市适合做什么生意_三线城市做什么生意好

1、适合当地市场的就是最好的,你应该去考察一下你们当地的市场,看做什么生意有特色。2、开什么店有卖点,

【天天新视野】德塔颜色 618 大促

【天天新视野】德塔颜色 618 大促

-热点:大疆Inspire3尼康Z8松下S5M2X富士X-S20-相机镜头:松下索尼富士佳能尼康适马永诺-镜头:盘点松下14-

香港恒生科技指数大涨5.33% 零售、媒体娱乐及地产等板块涨幅居前

香港恒生科技指数大涨5.33% 零售、媒体娱乐及地产等板块涨幅居前

截至6月2日收盘,香港恒生指数上涨4 02%,恒生科技指数大涨5 33%。消费者主要零售商、媒体及娱乐、地产板块

一汽-大众ID.7 VIZZION进入上市倒计时,电动版迈腾你会买单吗? 天天讯息

一汽-大众ID.7 VIZZION进入上市倒计时,电动版迈腾你会买单吗? 天天讯息

进入2023年,中国新能源市场快速发展,车企竞争格局也在悄然发展改变,比亚迪在新能源市场一骑绝尘遥遥领先

2021lol乌迪尔主什么技能加点(S11乌迪尔技能加点推荐)

2021lol乌迪尔主什么技能加点(S11乌迪尔技能加点推荐)

音频解说1、详细答案:2、乌迪尔的技能有两种流派。一种流派为通过攻速和攻击特效来造成伤害,这种流派主Q

观速讯丨国米给球迷观看大屏幕欧冠决赛,收费20欧元!压力来到曼城一边!

观速讯丨国米给球迷观看大屏幕欧冠决赛,收费20欧元!压力来到曼城一边!

国米开放梅阿查球场给球迷观看大屏幕欧冠决赛,收费10-20欧元!压力来到了曼城一边!能够闯入欧冠决赛,国

京东3C数码企业及商采618开门红捷报频出 联想商用品牌成交额同比增长30%

京东3C数码企业及商采618开门红捷报频出 联想商用品牌成交额同比增长30%

自31日晚8点京东618全面开启,各条战线捷报频传,京东3C数码企业及商采也迎来了开门红!截至6月1日24点,超

微动态丨上天啦!广西桉树育种实验首次飞向太空

微动态丨上天啦!广西桉树育种实验首次飞向太空

记者从广西壮族自治区林业和草原局获悉,经中国载人航天工程办公室确认,5月30日9时31分,广西桉树育种实验

世界新动态:一二九运动标志着什么运动_一二九运动标志着什么

世界新动态:一二九运动标志着什么运动_一二九运动标志着什么

1、在大气层中最靠近地球表面的一层为:对流层。2、2、对流层是地球大气层靠近地面的一层,它同时是地球大

5月好文盘点:聚焦618,扫描互联网广告平台-环球关注

5月好文盘点:聚焦618,扫描互联网广告平台-环球关注

-5月好文推荐-文章目录【2023广告平台扫描】【聚焦618】【热点追踪】【趋势洞察】【2023广告平台扫描】“字

受台风“玛娃”影响 日本多地发布自然灾害预警

受台风“玛娃”影响 日本多地发布自然灾害预警

受台风“玛娃”影响日本多地发布自然灾害预警

微资讯!OPPO MR Glass开发者耳机宣布采用骁龙XR2+芯片组

微资讯!OPPO MR Glass开发者耳机宣布采用骁龙XR2+芯片组

OPPO在2023年增强现实世界博览会(AWE)上宣布了其混合现实(MR)眼镜。这款耳机采用高通的骁龙XR2+芯片组,将

V观财报|豆神教育要与字节成立教育产业联盟?把深交所也弄“懵”了

V观财报|豆神教育要与字节成立教育产业联盟?把深交所也弄“懵”了

V观财报|豆神教育要与字节成立教育产业联盟?把深交所也弄“懵”了,法院,合作方,豆神教育,上市公司,深...

环球微资讯!莱音珠宝黄金价格今天多少一克(2023年06月02日)

环球微资讯!莱音珠宝黄金价格今天多少一克(2023年06月02日)

莱音珠宝黄金价格今天多少一克(2023年06月02日)每日更新

6月2日淘宝大赢家每日一猜答案,因为简单爱,杰伦带你领略? 全球快看

6月2日淘宝大赢家每日一猜答案,因为简单爱,杰伦带你领略? 全球快看

2023淘宝天猫618活动已经开启,其中有一个淘宝大赢家每日一猜活动,不少小伙伴想知道6月2日的答案是什么,

虐爱电视剧印度版_虐爱电视剧

虐爱电视剧印度版_虐爱电视剧

1、真爱如血。2、外国的。3、比吸血鬼日记好看。本文分享完毕,希望对大家有所帮助。

广告

X 关闭

广告

X 关闭