博客
关于我
强烈建议你试试无所不能的chatGPT,快点击我
simple fsm状态机模板应用笔记(一)——simple fsm的设计思维和哲学
阅读量:4092 次
发布时间:2019-05-25

本文共 6860 字,大约阅读时间需要 22 分钟。

原文地址:

说在前面的话

好久没有整理代码了,最近一直在做ARMv8-M系统安全设计相关的研究,虽然忙,但不代表我对自己无聊的爱好——整理一些好玩的代码模板,或者说语法糖——失去了兴趣。人总是会变的,一段时间过去以后,发现过去写的代码真心看着“心累”——宏一律大写看着辣眼睛,比如以前写的状态机脚本,所有做“状态机脚本语法辅助”的宏都是大写,看着果然还是不舒服。这次,我修正了一下自己的编码风格:

“所有宏和枚举都是大写除非对应的宏或者枚举用于辅助脚本语法”,比如后面你们看到的那个例子。
所有的状态机关键字都小写了,是不是舒服很多?

如果只是换个格式,那未免也显得太没诚意了,这次的新模板具有以下特性:- 针对ARM架构进行效率上的优化- 为每一个状态机提供一个控制块,用于参数封装,并且每个控制块在内部都用掩码结构体进行私  有化保护- 状态机模板可以独立存在,实现上更简洁

首先,我们来说说这次的模板在效率上作了什么优化?是什么原理?

ARM的Thumb指令集有一个特点:所有的寻址都是间接寻址,尤其是对变量的访问,通常都要借助一个寄存器来保存变量的地址,例如下面的语句:

LDR   r0, =
<某个数组的基地址>
; 步骤1: 这是一个汇编伪代码,将某个数组的基地址复制到r0中,汇编器可以识别这种语法 LDR r1, [r0] ; 步骤2:r0里保存的是一个uint32_t变量的地址,我们把它读出来保存到r1里面 LDR r2, [r0, #4] ; 步骤3:读取uint32_t 数组的第二个word

这种方式实际上对面向对结构体的访问非常友好,而且如果你仔细观察你会发现:

1. 如果你访问的是一个静态变量或者全局变量,那么生成的汇编包含“步骤1”和“步骤2”
2. 如果你访问的是一个数组,那么一定会包含“步骤1”,然后每个元素的访问都对应一个步骤,也就是“步骤2”、“步骤三”
3. 你会发现,无论是单个静态变量的访问,还是批量数组或者结构体的访问,“步骤1”——也就是加载基地址的过程都是省不掉的。
在这种情况下,数组和结构体元素的访问共享同一个步骤一,这就比单个变量的访问要节省很多。
举一个例子:总有人问,外设寄存器是单独定义成类似全局变量的形式好,还是用结构体访问的形式好?根据上面的描述,答案
就很清楚了。同样的,普通的switch状态机,横竖要包含一个静态的状态变量,另外还有若干静态的参数,那么
“为什么不把状态变量和状态机用到的静态变量打包成一个结构体——也就是状态机控制块呢?”
实际上,根据上面的分析,哪怕这个状态机控制块只包含一个状态变量,它也不会比直接使用状态变量的方式增加更多的开销,相反,如果这个控制块包含更多其他的变量,我们就赚了!所以,我在模板上加入了以下的内容:

#define __simple_fsm(__FSM_TYPE, ...)                               \        DECLARE_CLASS(__FSM_TYPE)                                   \        DEF_CLASS(__FSM_TYPE)                                       \            uint_fast8_t chState;                                   \            __VA_ARGS__                                             \        END_DEF_CLASS(__FSM_TYPE) #define simple_fsm(__NAME, ...)                                     \        __simple_fsm(fsm(__NAME), __VA_ARGS__)

可以看到,状态机控制块至少包含了一个状态变量 chState,而用状态机要用到的其它变量可以通过 “…” 对应的 VA_ARGS 加入到结构体中来。例如,一个用于延时的状态机delay_1s需要一个计数器,我们可以写成如下的形式:

simple_fsm( delay_1s,        /* define all the parameters used in the fsm */    uint32_t wCounter;                  //!< a uint32_t counter)

这里,delay_1s 是状态机的名字,uint32_t wCounter; 是我们定义的参数(可以定义更多的参数)。显然,这两个东西放在一起让人有点不知所措,所以我们增加了一个语法的辅助宏:

#define def_params(...)         __VA_ARGS__

借助它,我们写出来的代码即便没有注释,也好懂多了:

simple_fsm( delay_1s,    def_params(        uint32_t wCounter;                      ))

那么,实现状态机的时候,我们如何访问控制块里面的成员变量呢?这就要看看状态机的实现宏了:

#define fsm_implementation(__NAME, ...)                                              \    fsm_rt_t __NAME( fsm(__NAME) *ptFSM __VA_ARGS__ )                           \    {                                                                           \        CLASS(fsm_##__NAME##_t) *ptThis = (CLASS(fsm_##__NAME##_t) *)ptFSM;     \        if (NULL == ptThis) {                                                   \            return fsm_rt_err;                                                  \        }   #define body(...)                                                               \        switch (ptThis->chState) {                                              \            case 0:                                                             \                ptThis->chState++;                                              \            __VA_ARGS__                                                         \        }                                                                       \                                                                                \        return fsm_rt_on_going;                                                 \    }

这里我们可以发现, implement_fsm() 和 body() 是配对使用的。你也许已经猜到了,状态机的具体实现代码是写在body的括号里的。具体可以看后面的例子,这里我们继续来讨论状态机控制块成员变量 的访问。

implement_fsm() 实际上规定了状态机的函数原形,它包含了一个指向状态机控制块的指针ptFSM,而这个指针随后就被还原为原始形式(控制块默认情况下实际上是一个掩码结构体,所以要访问内部成员必须要还原为原始形式):ptThis实际上就指向了我们实际使用的控制块,通过这个结构体指针,我们 就可以轻松的访问任何的成员变量。但到这里,不要急,为了让代码更好看一点,我们引入了一个专门 的辅助宏:

#ifndef this#   define this    (*ptThis)#endif

借助这一语法糖,我们可以毫无代价的在body()内部通过 “this.” 的方式访问成员变量,例如:

fsm_implementation (  delay_1s)    def_states(DELAY_1S)                    body (        state(  DELAY_1S,                           if (0 == this.wCounter) {
fsm_cpl(); } this.wCounter--; fsm_on_going(); ) )

如果我们的状态机要作为一个字模块提供给外部使用怎么办呢?别着急,这里有一个简单的宏,你可以放在头文件里面提供给别的.c文件来引用:

#define __extern_simple_fsm(__NAME, __FSM_TYPE, ...)                \        DECLARE_CLASS(__FSM_TYPE)                                   \        EXTERN_CLASS(__FSM_TYPE)                                    \            uint_fast8_t chState;                                   \            __VA_ARGS__                                             \        END_EXTERN_CLASS(__FSM_TYPE)                                \        extern fsm_rt_t __NAME( __FSM_TYPE *ptThis __VA_ARGS__ ); #define extern_simple_fsm(__NAME, ...)                              \        __extern_simple_fsm(__NAME, fsm(__NAME), __VA_ARGS__)

比如,我们要把delay_1s作为一个字状态机提供出去,我们可以在头文件里这么写:

extern_simple_fsm( delay_1s,    def_params(        uint32_t wCounter;                      ))

好吧,我承认,其实就是把定义的部分又抄了一遍并加了一个extern_的前缀,简单吧?通过上面的 宏定义,容易发现,因为使用了掩码结构体的形式,所以使用者是无法直接访问控制块内的成员变量的。

至此,控制块定义、使用和优化的部分我们就解释完毕了。如果你有任何疑问,欢迎跟贴讨论。

最后谈谈设计思维和哲学

这个状态机模板从发布第一个版本到小范围试用已经过去大半年了,其间,我被问得最多的问题是:

“你这已经不是C语言了”、“你实际上是制作了另外一个状态机脚本语言语法”、“为什么要做一个四不像的东西呢?”、“这个模板本质上和protoThread一样,你为什么要重复发明轮子呢?” 针对这些大家感兴趣的问题,如果我不从设计思维的角度给出答案,这个模板是很难让人接受的。下面我就以上问题,从设计思维上给出一个系统的答案:

首先,C语言原生态就不支持状态机,用C语言实现的状态机,本质上只是一种模拟。这跟C语言并不原生态支持面向对象,如果真的要大量使用面向对象进行编程,最好的办法是使用C++,而不使用OOPC去模拟是一样的——为什么呢?因为程序设计要专注于“应用逻辑的实现”本身,应该尽量避免被“某种技术”分心——对需要大量使用面向对象技术进行开发的程序来说,应用逻辑是我们应该更多关心的,而使用C模拟OO则是需要避免的。

同样的问题发生在状态机上,C语言不仅不支持状态机,甚至我们模拟状态机的技术本身也相当复杂、混乱。不像面向对象有C++,状态机的开发并没有一种语言与C具有传承关系(别说verlog,谢谢,有本事你去找个verlog编译器,编译出来的机器码主流MCU都能运行的)。这可怎么办呢?回到我们的目的本身:

程序设计要专注于“应用逻辑的实现”本身,应该尽量避免被“某种技术”分心

为了达到这个目的,一个可行的方案就是想方设法构造一种基于C语言的 “脚本语言”,使得状态机的开发者得以关注“状态机应用逻辑的实现”,而不必关心“状态机具体是如何使用C语言进行构造的”。也就是说,从一开始我们建立这个模板的目的就是要构造一种 状态机专用的脚本语言,使得这种语言可以极大的简化状态机的开发和表达。这种脚本语言根本就不用“看起来是C语言”,因为它从一开始就不是C语言。

另一方面,新的脚本语言在使用时,应该能“无缝”的与其它C语言代码(函数)融合在一起,这表现于:状态机的调用、参数传递、基本类似C的函数调用。简而言之,新的脚本语言:

设计的时候看起来是状态机,使用的时候看起来就像C语言

这与C++设计的时候是面向对象,使用的时候(可以)看起来就像C语言是类似的。基于上述思想,我们得以“狡辩说”:现在的状态机模板导致的结果是一个对C很友好的状态机脚本语言,而不是一个用C实现的“四不像”——当然,这对一部分人来说“其狡辩的本质是不随个人意志转移而改变的” 。

针对和protoThread技术原理类似的问题,其实如果你真的使用过protoThread就会发现,这两个模板在出发点上就是截然相反的:

  • protoThread 试图让人产生“我是在使用RTOS进行线程开发”的错觉,它极力隐藏的是它“状态机的本质”
  • simple fsm 从一开始,就让开发人员明确知道“我是在开发状态机”

足可见,虽然技术原理相同,但思维不同,最终使用的设计哲学也大相径庭。

最后,一个决定性的因素说明 simple fsm 不是一个简单的模板而是一个“新的(基于C的)脚本语言”,即simple fsm 使用了面向对象技术来封装状态机,这就从根本上决定了它不只是一种设计状态机的方式,而是一整套面向对象状态机设计的哲学,比如:

  • 一个状态机就是一个类
  • 状态机函数只是这个类的一个方法
  • 状态机所要用到的变量都作为成员变量封装在类中(每个状态机都有自己的上下文)
  • 状态机及其数据被封装在一起,且对外界提供私有化保护(掩码结构体实现的private)
  • 状态机类是可以多实例的
  • 每个状态机从一开始就是一个任务(有自己的上下文——注意,这里的上下文是一个广义的概念,并不局限于stack)
  • 支持面向对象开发带来的种种好处
  • 支持面向接口开发(注意,面向接口开发不是面向对象的专利)

综上所述:使用simple fsm开发的时候,我们只关心状态机如何设计,这也是为什么写出来的代码 从字面上看 更像状态机而不是C语言;而调用状态机的时候,又对C语言很友好——这当然是个优点。另外,如果你并不知道如何设计状态机,也不喜欢,那么推荐你用protoThread或者干脆RTOS,因为你用simple fsm就要清楚你写的就是TMD状态机!

欢迎大家踊跃讨论,拍砖。

—— 傻孩子 吐槽于 2017-10-14日夜

转载地址:http://bcnii.baihongyu.com/

你可能感兴趣的文章
搞不懂 Java 虚拟机性能调优,是因为你还没看过这个!
查看>>
Google 对外开放的这份工程实践文档,我爱了...
查看>>
复旦大学邱锡鹏教授:一张图带你梳理深度学习知识脉络
查看>>
我的电脑不联网,很安全。黑客:你还有风扇呢!
查看>>
全球最大编程问答社区 Stack Overflow 宣布裁员 15%!
查看>>
这个被微软雪藏十几年的官方插件,没想到原来这么好用
查看>>
太赞了!GitHub 标星 2.4k+,《可解释机器学习》中文版正式开放!
查看>>
程序员用 AI 修复百年前的老北京视频后,火了!
查看>>
漫话:为什么你下载小电影的时候进度总是卡在 99% 就不动了?
查看>>
我去!原来大神都是这样玩转「多线程与高并发」的...
查看>>
当你无聊时,可以玩玩 GitHub 上这个开源项目...
查看>>
B 站爆红的数学视频,竟是用这个 Python 开源项目做的!
查看>>
安利 10 个让你爽到爆的 IDEA 必备插件!
查看>>
自学编程的八大误区!克服它!
查看>>
GitHub 上的一个开源项目,可快速生成一款属于自己的手写字体!
查看>>
早知道这些免费 API,我就可以不用到处爬数据了!
查看>>
利用蒙特卡罗法,国外老哥成功制造出 100% 投篮命中的篮板!
查看>>
卧槽!VSCode 上竟然也能画流程图了???
查看>>
如何高效阅读 Spring 源码?你需要这个!
查看>>
86 岁还在录网课:MIT 老教授最新「线性代数」课程正式对外开放!
查看>>