状态机漫谈 | 您所在的位置:网站首页 › n是状态函数嘛 › 状态机漫谈 |
(本文撰写于2021年情人节) 【说在前面的话】 在前面的一篇文章《从零开始的状态机漫谈(1)——万物之始的语言》中,我们介绍了状态机在整个计算机科学中宛如“世界基石”般的地位,同时介绍了一种“面向嵌入式环境”“高度简化”了的实用型状态图绘制方法——这里的“简化”是相对UML状态图的“繁杂”而言、且更接近课本上所使用的状态机图例;而这里的“实用”体现在:基于这套方法绘制的状态图是可以“无脑”而“严格”的翻译成C语言代码的。 在展开后续内容之前,不得不为大家解释清楚一个非常具有误导性的错误认知,即:状态机天然是非阻塞(non-blocking)的,因而可以用于在裸机状态下实现多任务。实际上,这种说法后半段是正确的,错就错在前半部分,比如,就前一篇文章中所提到的一个状态图: 翻译成下面的C语言代码,在逻辑上毫无问题: #include #include void print_hello(void) { //! 对应 start部分 uint8_t *s_pchSrc = "Hello"; do { //! 对应 Print Hello 状态 while(!serial_out(*s_pchSrc)); //! serial_out返回值为true的状态迁移 s_pchSrc++; //! 对应 "Is End of String"状态 if (*s_pchSrc == '\0') { //! true分支,结束状态机 return ; } //! false分支,跳转到 "Print Hello" 状态 } while(true); }怎么样?发现之前说法的错误之处了吧?——是的,状态机(状态图)所描述的逻辑与翻译后的代码是否具有“非阻塞”的特性是无关的——翻译的方式不同,代码的特性也不同——但无论使用何种翻译方式,只要翻译是正确的,最终代码所对应的“状态机逻辑”就是“等效”的,比如,上面的状态机也可以翻译成如下的非阻塞形式: #include #include typedef enum { fsm_rt_err = -1, fsm_rt_on_going = 0, fsm_rt_cpl = 1, } fsm_rt_t; #define PRINT_HELLO_RESET_FSM() \ do {s_tState = START;} while(0) fsm_rt_t print_hello(void) { static enum { START = 0, PRINT_HELLO, IS_END_OF_STRING, } s_tState = {START}; static const uint8_t *s_pchSrc = NULL; switch (s_tState) { case START: //! 这个赋值写法只在嵌入式环境下“可能”是安全的 s_pchSrc = "Hello world"; s_tState++; //break; case PRINT_HELLO: if (!serial_out(*s_pchSrc)) { break; }; s_tState = IS_END_OF_STRING; s_pchSrc++; //break; case IS_END_OF_STRING: if (*s_pchSrc == '\0') { PRINT_HELLO_RESET_FSM(); return fsm_rt_cpl; } s_tState = PRINT_HELLO; break; } return fsm_rt_on_going; }对比两个代码,可以清楚的发现这两个事实: 在状态机逻辑层面,两个代码都正确的翻译(表达)了状态图的逻辑; 在C代码的实际执行层面,一个是“不完成任务就绝不回来”的阻塞代码;一个是在状态执行间隙还会“悄悄”退出函数——释放处理器的非阻塞代码。 所以说,与上述情况类似,市面上不少关于状态机的说法其实都是“有待商榷”、甚至是“错误的”,比如: 状态机天然的是非阻塞代码; 因为状态机经常切换,因此实时性好; 状态机经常切换,没法以最快的速度响应事件,所以实时性差; 状态机执行效率低下; 状态机执行效率高; 状态机占用代码空间大; 状态机占用资源小,适合资源有限的小单片机; 任何状态机都可以翻译成普通的RTOS任务(注意,这里的说法强调的不是不是状态机代码在RTOS任务里执行,而是把状态图翻译成RTOS任务) …… 相信上述诸多误解和偏见中一定有一款是让你大为吃惊的。然而,如果你认为我这里列举出来的说法都是“错误的”,那么你就又错了。 这里的要点是——以上说法并不是“非黑即白”的,而是来源于某一些具体的状态机翻译方式,错就错在把某一种状态机翻译方式所具有的优点/缺点当成了整个状态机固有的优点/缺点——脱离了具体的状态机翻译方式,从而导致了“不准确”。 说了这么多,无非就是想让你们知道以下几点: 状态机/状态图的翻译方式众多; 不同翻译方式在代码的行为特性上存在天壤之别; 抛开具体翻译方式谈状态机特性都是耍流氓; 如果说状态图才是“新的源代码”,翻译C代码就是“新的汇编”,根据一定规则翻译状态图为C代码的过程就是”新的编译“。 下面我们就以大部分人第一次接触和使用状态机时常用的 switch 状态机为例,为大家介绍前一章所属状态图的翻译规则。 让我们上路吧! (本文撰写于2021年情人节) 【状态函数返回值的“小心思”】 对很多人来说,即便状态机“初恋”不是使用switch编写的函数,也一定逃不开使用函数作为状态机载体的形式(比如使用大量if-else作为基础的状态机)。观察状态图,你会发现状态机是有返回状值的: 比如图中右上角的“on-going”和右下角的“cpl”,分别表示状态机“正在工作(on-going)”和“已经完成(complete)”。图上的状态机算是比较简单的了,其它状态机可能还有返回其它信息的需求——比如,一个接收字符的状态机可能还需要返回“超时(timeout)”这样的信息——因此,定义一个专门的枚举类型来作为状态机函数的返回值就显得非常有必要: typedef enum { fsm_rt_on_going, fsm_rt_cpl, } fsm_rt_t;到了这里,有一个细节问题需要考虑,fsm_rt_on_going和fsm_rt_cpl分别对应怎样的具体值好呢?(或者干脆不管?)。要解决这个问题,实际上只有是站在状态机函数用户角度考虑进行考虑,才能找到不会违反用户直觉(屁股决定脑袋)的答案。从状态机调用者的角度来看,既然我们告诉TA状态机函数是非阻塞的,那么用户最关心的最基本问题恐怕就是: 状态机是否执行完成了? 状态机有没有遇到什么自己不能处理的错误? 对于第一个问题,显然其答案是一个布尔量: 如果返回false,则表明状态机还没有执行完成——需要继续执行(on-going); 如果返回true,则表明状态机已经执行完成(complete) 基于这样的原因,完全可以根据 中的定义,给我们的 fsm_rt_t 一个兼容的值,即: typedef enum { fsm_rt_on_going = 0, fsm_rt_cpl = 1, } fsm_rt_t;对于第二个问题,实际上,程序员之间有一个不成文的规定,即:错误码用负数表示,因此,我们可以引入一个“不问缘由的默认的错误码” (-1),并允许用户可以用除去(-1)以外的其它负数来编码更为具体的错误——这里就把这种自由度留给用户自己去发挥了,我们只需要在 fsm_rt_t 中引入(-1)就可以了: typedef enum { fsm_rt_err = -1, fsm_rt_on_going = 0, fsm_rt_cpl = 1, } fsm_rt_t;至此,我们完成了一个状态机返回值的定义过程,并隐含了以下的规则: 对于“确定”不会返回错误码的状态机函数来说,状态机函数的使用与bool量是兼容的; 用户可以使用负数来“自定义”错误码,并使用(-1)表示“不问缘由的默认错误码”; 需要特别强调的是,错误码表示发生了“状态机发生了预期之外、无法继续正常工作的情况”,比如,状态机函数需要一个指针,但你传了一个空指针;或是状态机函数收到了一个无效的输入参数,导致后续工作都无法正常执行,等等。 用户定义的其它状态值,比如超时之类的,它们必须是大于(1)的正数。 与错误码不同,这类用返回值是状态机正常工作的结果,属于状态机逻辑本身所能预期和处理的。所以,哪怕“超时”听起来像是一个“错误”,但它本质上还是状态机逻辑所预期会发生并能正确检测和处理的,因此并不会作为一个负数错误码来返回。 在这个系列后面的文章中,我们还会引入两个默认的正整数状态返回值到 fsm_rt_t这里就先不赘述了: //! \name finit state machine return value //! @{ typedef enum { fsm_rt_err = -1, //!< fsm error, error code can be get from other interface fsm_rt_cpl = 0, //!< fsm complete fsm_rt_on_going = 1, //!< fsm on-going fsm_rt_wait_for_obj = 2, //!< fsm wait for object fsm_rt_asyn = 3, //!< fsm asynchronose mode, you can check it later. } fsm_rt_t; //! @}借助 fsm_rt_t 类型的帮助,我们的状态机函数终于有了一个像样的外壳,比如: fsm_rt_t ([形参列表]) { ... return fsm_rt_on_going; //!< 默认的返回值 }为了方便大家的理解,我们就以“带超时功能的字符接收状态机”为例子,为大家介绍对应的状态图绘制方法以及对应的代码片段: 观察上图可以发现,状态机read_byte会在读取字符的同时进行一个简单的倒计数;如果在s_wCounter为0之前成功读取到了一个字节,则返回cpl(pchByte所指向的字节buffer将保存对应的字节);如果读取字节失败,但计数器还未到零,则返回 on_going——表明状态机还在工作中;如果计数器到达了0,则返回一个自定义的状态信息(timeout),用以表明发生了超时。在图中,不光矩形框内部多了一个名为 timeout 的黑色小圆点;在矩形框的外部(右侧)也出现了一个对应的扇出箭头,同样也标记了 timeout——这实际上是告诉我们,当状态机迁移到 timeout 终点时,将通过 timeout 箭头扇出,而状态机也将复位。 它对应的一个可能代码为: enum { fsm_rt_timeout = 4, //! |
CopyRight 2018-2019 实验室设备网 版权所有 |