模块的封装(五):状态机模板

出处:状态机模板

说在前面的话



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

    如果只是换个格式,那未免也显得太没诚意了,这次的新模板具有以下特性:

    - 针对ARM架构进行效率上的优化
    - 为每一个状态机提供一个控制块,用于参数封装,并且每个控制块在内部都用掩码结构体进行私
      有化保护
    - 状态机模板可以独立存在,实现上更简洁

 


首先,我们来说说这次的模板在效率上作了什么优化?是什么原理?
    
    ARM的Thumb指令集有一个特点:所有的寻址都是间接寻址,尤其是对变量的访问,通常都要借助
一个寄存器来保存变量的地址,例如下面的语句:

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

复制代码


   这种方式实际上对面向对结构体的访问非常友好,而且如果你仔细观察你会发现:
    1. 如果你访问的是一个静态变量或者全局变量,那么生成的汇编包含“步骤1”和“步骤2”
    2. 如果你访问的是一个数组,那么一定会包含“步骤1”,然后每个元素的访问都对应一个步骤,也就是“步骤2”、“步骤三”
    3. 你会发现,无论是单个静态变量的访问,还是批量数组或者结构体的访问,“步骤1”——也就是加载基地址的过程都是省不掉的。
        在这种情况下,数组和结构体元素的访问共享同一个步骤一,这就比单个变量的访问要节省很多。

    举一个例子:总有人问,外设寄存器是单独定义成类似全局变量的形式好,还是用结构体访问的形式好?根据上面的描述,答案
    就很清楚了。同样的,普通的switch状态机,横竖要包含一个静态的状态变量,另外还有若干静态的参数,那么

    “为什么不把状态变量和状态机用到的静态变量打包成一个结构体——也就是状态机控制块呢?”
    
    实际上,根据上面的分析,哪怕这个状态机控制块只包含一个状态变量,它也不会比直接使用状态变量的方式增加更多的开销,
    相反,如果这个控制块包含更多其他的变量,我们就赚了!所以,我在模板上加入了以下的内容:

 

  1.  
  2. #define __simple_fsm(__FSM_TYPE, ...)                               \
  3.         DECLARE_CLASS(__FSM_TYPE)                                   \
  4.         DEF_CLASS(__FSM_TYPE)                                       \
  5.             uint_fast8_t chState;                                   \
  6.             __VA_ARGS__                                             \
  7.         END_DEF_CLASS(__FSM_TYPE)
  8.  
  9. #define simple_fsm(__NAME, ...)                                     \
  10.         __simple_fsm(fsm(__NAME), __VA_ARGS__)

复制代码


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

 

  1.  
  2. simple_fsm( delay_1s,
  3.     
  4.     /* define all the parameters used in the fsm */ 
  5.    uint32_t wCounter;                  //!< a uint32_t counter
  6. )

复制代码



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

  1.  
  2.     #define def_params(...)         __VA_ARGS__

复制代码


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

  1.  
  2. simple_fsm( delay_1s,
  3.     def_params(
  4.         uint32_t wCounter;                  
  5.     )
  6. )

复制代码



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

  1.  
  2. #define fsm_implementation(__NAME, ...)                                              \
  3.     fsm_rt_t __NAME( fsm(__NAME) *ptFSM __VA_ARGS__ )                           \
  4.     {                                                                           \
  5.         CLASS(fsm_##__NAME##_t) *ptThis = (CLASS(fsm_##__NAME##_t) *)ptFSM;     \
  6.         if (NULL == ptThis) {                                                   \
  7.             return fsm_rt_err;                                                  \
  8.         }  
  9.  
  10. #define body(...)                                                               \
  11.         switch (ptThis->chState) {                                              \
  12.             case 0:                                                             \
  13.                 ptThis->chState++;                                              \
  14.             __VA_ARGS__                                                         \
  15.         }                                                                       \
  16.                                                                                 \
  17.         return fsm_rt_on_going;                                                 \
  18.     }

复制代码


    这里我们可以发现, implement_fsm() 和 body() 是配对使用的。你也许已经猜到了,状态机的具体
    实现代码是写在body的括号里的。具体可以看后面的例子,这里我们继续来讨论状态机控制块成员变量
    的访问。
    implement_fsm() 实际上规定了状态机的函数原形,它包含了一个指向状态机控制块的指针ptFSM,而
    这个指针随后就被还原为原始形式(控制块默认情况下实际上是一个掩码结构体,所以要访问内部成员
    必须要还原为原始形式):ptThis实际上就指向了我们实际使用的控制块,通过这个结构体指针,我们
    就可以轻松的访问任何的成员变量。但到这里,不要急,为了让代码更好看一点,我们引入了一个专门 
    的辅助宏:

  1.  
  2. #ifndef this
  3. #   define this    (*ptThis)
  4. #endif

复制代码


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

  1.  
  2. fsm_implementation (  delay_1s)
  3.     def_states(DELAY_1S)               
  4.  
  5.     body (
  6.         state(  DELAY_1S,               
  7.             if (0 == this.wCounter) {
  8.                 fsm_cpl();              
  9.             }
  10.             this.wCounter--;
  11.             fsm_on_going();             
  12.         )
  13.     )
  14.  

复制代码


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

  1.  
  2. #define __extern_simple_fsm(__NAME, __FSM_TYPE, ...)                \
  3.         DECLARE_CLASS(__FSM_TYPE)                                   \
  4.         EXTERN_CLASS(__FSM_TYPE)                                    \
  5.             uint_fast8_t chState;                                   \
  6.             __VA_ARGS__                                             \
  7.         END_EXTERN_CLASS(__FSM_TYPE)                                \
  8.         extern fsm_rt_t __NAME( __FSM_TYPE *ptThis __VA_ARGS__ );
  9.  
  10. #define extern_simple_fsm(__NAME, ...)                              \
  11.         __extern_simple_fsm(__NAME, fsm(__NAME), __VA_ARGS__)  

复制代码


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

  1.  
  2. extern_simple_fsm( delay_1s,
  3.     def_params(
  4.         uint32_t wCounter;                  
  5.     )
  6. )

复制代码


    好吧,我承认,其实就是把定义的部分又抄了一遍并加了一个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实现的“四
不像”——当然,这对一部分人来说“其狡辩的本质是不随个人意志转移而改变的”  : p。

        针对和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日夜

 


如何使用




1. 如何定义一个状态机

    语法:

  1.  
  2. simple_fsm( <状态机名称>,
  3.     def_params(
  4.         参数列表                
  5.     )
  6. )

复制代码



    例子:

  1.  
  2.  
  3. /*! fsm used to output specified string */
  4. simple_fsm( print_string,
  5.     def_params(
  6.         const char *pchStr;        //!< point to the target string
  7.         uint16_t hwIndex;          //!< current index
  8.         uint16_t hwLength;        //!< claimed length of the target string, it is used to prevent buffer overflow 
  9.     )
  10. )

复制代码



     这里,实际上我们为目标状态机控制块定义了一个专用的类型,可以用fsm()对这个状态机加以引用。需要说明
的是,这个类型本质上是一个掩码结构体,也就是说你无法通过这个类型直接访问控制块的成员变量。这也是它安
全的地方——当然,防君子不防小人。
     语法:

  1.  
  2.      fsm(<状态机名称>)

复制代码


     例子:

  1.  
  2.      static fsm( print_string ) s_fsmPrintString;     //! 定义了一个本地的状态机控制块

复制代码


 



2. 如何extern一个状态机
         很多时候,我们的状态机会作为一个模块,提供给别的.c文件来使用(直接调用或者作为子状态机被
    调用,那么这种情况下应该如何处理呢?

    语法:

  1.  
  2. extern_simple_fsm( <状态机名称>,
  3.     def_params(
  4.         参数列表                
  5.     )
  6. )

复制代码



    例子:

        在某个头文件中写入如下的内容:

  1.  
  2. #include "simple_fsm.h"
  3.  
  4. ...
  5.  
  6. /*! fsm used to output specified string */
  7. extern_simple_fsm( print_string,
  8.     def_params(
  9.         const char *pchStr;        //!< point to the target string
  10.         uint16_t hwIndex;          //!< current index
  11.         uint16_t hwLength;        //!< claimed length of the target string, it is used to prevent buffer overflow 
  12.     )
  13. )

复制代码


 



3. 如何实现一个状态机控制块的初始化函数
    很多复杂的状态机其服务本身是需要初始化的,简单说就是它的控制块在状态机使用前,必须进行
初始化,这类初始化是通过用户自定义的初始化函数来实现的,那么如何编写这类初始化函数呢?

 


[注意] 无论状态机多简单,初始化函数都不能省略。
    原因很简单,这样写出来的代码兼容性最好。有的说,控制块如果你不初始化,就是自动放到ZI段去了,
    编译器会自动帮你初始化为0。即便如此,这也是不妥的,原因如下:
    a. 不能依赖编译器,因为ANSI-C并没有规定不初始化的变量一定会被自动初始化为0
    b. 如果控制块是来自堆,就没有人帮你初始化状态机控制块了,别忘控制块里至少还有一个状态变量
    c. 作为子状态机使用的时候,为了节省空间,不同时运行的子状态机可以用union共享同一块Memory,
        这种情况下,状态机使用前不初始化问题很严重。




    语法:

  1.  
  2. fsm_initialiser( <状态机名称>,
  3.     args(           
  4.         <状态机初始化函数的形参列表,参数用逗号隔开,如果真的没有形参,可以省略该部分>
  5.         /* 注意,即便没有形参,你也是需要initialiser来初始化状态机的 */
  6.     ))
  7.  
  8.     init_body (
  9.         <初始化函数的函数体,用普通C语言语法即可>
  10.         /* 如果初始化过程中发生了任何错误需要放弃初始化并立即退出,使用 abort_init() */
  11.     )

复制代码



    例子:
 

  1.  
  2. fsm_initialiser( print_string,
  3.     args(           
  4.         const char *pchString, uint16_t hwSize
  5.     ))
  6.  
  7.     init_body (
  8.         if (NULL == pchString || 0 == hwSize) {
  9.             abort_init();                                       //!< illegal parameter
  10.         } else if (strlen(pchString) < hwSize) {
  11.             abort_init();                                       //!< buffer overflow
  12.         }
  13.  
  14.         this.pchStr = pchString;  
  15.         this.hwLength = hwSize;
  16.     )

复制代码


 



4. 如何extern一个状态机初始化函数
     当一个状态机包含初始化函数时,如果要把该状态机提供给别的.c使用,我们还需要把对应的初
始化函数也extern出去。

    当你使用 extern_fsm_initialiser 的时候,我们的宏木板还会自动定义一个函数原型,这样,你就可以
    用这个函数圆形去定义指向 当前初始化函数 的函数指针。函数原型的名称如下:

   <状态机名称>_init_fn

    语法:

  1.  
  2. extern_simple_fsm_initialiser( <状态机名称>,
  3.     args(           
  4.         <状态机初始化函数的形参列表,参数用逗号隔开,如果真的没有形参,可以省略该部分>
  5.         /* 注意,即便没有形参,你也是需要initialiser来初始化状态机的 */
  6.     ))

复制代码



    例子:
        在某个头文件中写入如下的内容:

  1.  
  2. #include "simple_fsm.h"
  3.  
  4. ...
  5.  
  6. /*! fsm used to output specified string */
  7. extern_simple_fsm_initialiser( print_string,
  8.     args(           
  9.         const char *pchString, uint16_t hwSize
  10.     ))
  11.  
  12. ...
  13.  

复制代码


这里,系统顺便定义了一个函数原型,print_string_init_fn,你可以用print_string_init_fn 直接定义函数指针:

  1.  
  2.     print_string_init_fn *fnInit = &print_string_init;   //!< <状态机名称>_init  就是初始化函数的函数名。

复制代码


 



5. 如何初始化一个状态机
    对于一个需要初始化的状态机,我们应该如何对它进行初始化呢?

    语法:

  1.  
  2. init_fsm(   <状态机名称>, <目标状态机控制块的地址>, 
  3.     args( 
  4.         <状态机初始化函数的实参列表,参数用逗号隔开,如果没有实参,可以省略该部分> 
  5.      ));
  6.  
  7. 该函数的返回值是地址:
  8.      NULL          初始化过程中出错
  9.      ! NULL   <目标状态机控制块的地址>
  10.  

复制代码



     例子:

  1.  
  2. //! 定义了一个状态机控制块
  3. static fsm(print_string)  s_fsmPrintString;
  4.  
  5. #define DEMO_STRING   "Hello FSM World!\r\n"
  6.  
  7.     if (NULL == init_fsm(    print_string, & s_fsmPrintString,
  8.         args( 
  9.             DEMO_STRING,                      //!< target string    
  10.             sizeof(DEMO_STRING) - 1))) {      //!< String Length
  11.          /* failed to initialize the FSM, put error handling code here */
  12.      }
  13.  

复制代码


 



6. 如何实现一个状态机

    语法:

  1.  
  2. fsm_implementation(  <状态机名称>, 
  3.         args( <状态机的形参列表,参数用逗号隔开,如果没有形参,可以省略这部分> )
  4.     )
  5.     def_states( <列举所有状态机状态,用逗号隔开,确保状态机的入口状态列在第一的位置> )               
  6.  
  7.     <局部变量列表>
  8.  
  9.     body (
  10.          on_start(  
  11.              <状态机复位后第一次运行时,运行且只运行一次的代码,通常放一些状态机内部的初始化代码,如果无所事事,可以省略这个部分>
  12.          )  
  13.       
  14.         <状态机所有的状态实现>
  15.     )

复制代码



    例子:

  1.  
  2. fsm_implementation(  print_string )
  3.     def_states( CHECK_LENGTH, OUTPUT_CHAR )               
  4.  
  5.     body (
  6.          on_start(  
  7.              this.hwIndex = 0;         //!< reset index
  8.          )  
  9.       
  10.         ...
  11.     )

复制代码



 



7. 如何extern一个状态机函数

    当你使用 extern_fsm_implementation 的时候,我们的宏木板还会自动定义一个函数原型,这样,你就可以
    用这个函数圆形去定义指向 当前初始化函数 的函数指针。函数原型的名称如下:

   <状态机名称>_fn


    语法:

  1.  
  2. extern_fsm_implementation(  <状态机名称>, 
  3.         args( <状态机的形参列表,参数用逗号隔开,如果没有形参,可以省略这部分> )
  4.     )

复制代码




    例子:
        在某个头文件中写入如下的内容:

  1.  
  2. #include "ooc.h"
  3. #include "simple_fsm.h"
  4.  
  5. ...
  6.  
  7. extern_fsm_implementation(  print_string );
  8.  
  9. ...
  10.  

复制代码



这里,系统顺便定义了一个函数原型,print_string_fn,你可以用print_string_fn 直接定义函数指针:

  1.  
  2.     print_string_fn *fnFSM = &print_string;   //!< <状态机名称> 就是状态机函数的名称。

复制代码



 



8. 如何实现一个状态
     状态必须在body()内实现,具体形式如下:

    语法:

  1.  
  2.      state( <状态名称>,
  3.  
  4.          <状态实现代码,C语言实现>
  5.  
  6.          fsm_on_going();
  7.      )

复制代码



    在实现状态的过程中,状态的切换要通过 transfer_to() 来实现,它将立即终止当前状态代码的执行,
并跳转到目标状态中,其语法如下:

  1.  
  2.      transfer_to( <目标状态的名称> )

复制代码



    有些时候,我们只希望更新状态机的状态,而并不希望立即终止当前状态机的执行,则可以用
update_state_to() 来实现。通常update_state_to() 配合 “省缺状态结尾处的fsm_on_going()” 来
直接 fall-through 到紧随着当前状态的下一个状态来执行,这实际上是利用switch的fall-through特性
来实现某些情况下的状态机性能提升。其语法如下:

  1.  
  2.      update_state_to( <目标状态的名称> )

复制代码


    实际上 transfer_to() 等效于以下的组合

  1.  
  2.      update_state_to( <目标状态> )
  3.      fsm_on_going();

复制代码



     状态实现的时候,如果需要更新状态机的返回值,则可以使用下列方式:

  1.  
  2.      fsm_on_going()                         立即终止当前状态,并让状态机返回fsm_rt_on_going;
  3.      fsm_cpl()                                  立即终止当前状态,复位状态机,并让状态机返回fsm_rt_cpl;
  4.      fsm_reset()                               仅复位状态机,不影响状态机返回值(通常配合fsm_on_going() 
  5.                                                     和fsm_report() 使用)
  6.      fsm_report( <任意负数> )          立即终止当前状态,并返回错误码(任意小于等于fsm_rt_err)的值

复制代码



     例子:

  1.  
  2. fsm_implementation(  print_string )
  3.     def_states( CHECK_LENGTH, OUTPUT_CHAR )               
  4.  
  5.     body (
  6.          on_start(  
  7.              this.hwIndex = 0;         //!< reset index
  8.          )  
  9.       
  10.         state ( CHECK_LENGTH,
  11.             if ( this.hwIndex >= this.hwLength ) {
  12.                  fsm_cpl();               
  13.             }
  14.             update_state_to ( OUTPUT_CHAR );              //! deliberately ignore the following fsm_on_going() in order to fall through to next state
  15.             // fsm_on_going();        
  16.         )
  17.  
  18.         state ( OUTPUT_CHAR,
  19.              if (SERIAL_OUT( this.pchStr[ this.hwIndex ] )) {
  20.                    this.hwIndex++;
  21.                    transfer_to ( CHECK_LENGTH );
  22.              }
  23.  
  24.              fsm_on_going();
  25.         )
  26.     )

复制代码



9. 如何调用一个状态机
     状态机(包括子状态机)的调用方式是一样的,假设状态机已经被初始化过了,那么可以使用
下面的方法进行调用(放在超级循环里面,或者放在某个状态里面是一样的):

    语法:

  1.  
  2.     call_fsm ( <状态机名称>, <状态机控制块的地址>
  3.         args( <状态机的实参列表,参数用逗号隔开。如果没有实参,可以省略该部分> )
  4.     )
  5.  
  6. 该函数的返回值是状态机的运行状态 fsm_rt_t:
  7.      fsm_rt_err                状态机出现了意料之外的,且自身无法处理的错误,例如无效的参数
  8.      fsm_rt_on_going           状态机正在执行
  9.      fsm_rt_cpl                状态机已经完成

复制代码



    例子:

  1.  
  2.     static fsm(print_string) s_fsmPrintSting;
  3.  
  4.     void main(void)
  5.     {
  6.          ...
  7.          while(1) {
  8.              ...
  9.              if (fsm_rt_cpl == call_fsm( print_string, &s_fsmPrintString )) {
  10.                   /* fsm is complete, do something here */
  11.              }
  12.          }
  13.     }

复制代码


 



10. 如何前置声明一个状态机
         有些时候,在我们正式通过 simple_fsm 宏定义一个状态机之前,当前状态机就要被其它(当前状态机)所依赖的关键类型所引用,
比如,定义指向当前状态机的指针啊,函数指针啊,之类的——简而言之,前置引用的问题如何解决呢?

    语法:

  1.  
  2. declare_simple_fsm( <状态机名称> )

复制代码



    例子:

        在某个头文件中写入如下的内容:

  1.  
  2. #include "simple_fsm.h"
  3.  
  4.  
  5. declare_simple_fsm(print_string);
  6. extern_fsm_implementation(print_string);
  7. extern_simple_fsm_initialiser( print_string,
  8.     args(           
  9.         const char *pchString, uint16_t hwSize
  10.     ));
  11.  
  12. typedef struct {
  13.     fsm(print_string) *ptThis;          //!< a pointer points to fsm obj
  14.     print_string_fn *fnTask;             //!< a function pointer, point to fsm function
  15.     print_string_init_fn *fnInit;       //!< a function pinter, points to initialisation function
  16. } vtable_t;
  17.  
  18.  
  19. /*! fsm used to output specified string */
  20. simple_fsm( print_string,
  21.     def_params(
  22.         vtable_t Methods;
  23.         const char *pchStr;        //!< point to the target string
  24.         uint16_t hwIndex;          //!< current index
  25.         uint16_t hwLength;        //!< claimed length of the target string, it is used to prevent buffer overflow 
  26.     )
  27. )

复制代码


 


一个简单的例子




     这里,我们展示了一个简单的状态机例子,用于周期性的通过串口输出“hello”。我们可以看到,
这个例子里定义了两个状态机,print_hello 用于打印字符串,并调用另外一个子状态机delay_1s用于
实现一个差不离的延时(代码里用了一个随便写的常数10000,领会精神就好)。

     print_hello 状态机的结构相当简单,前半部分是字符串的输出——简单粗暴的为每一个字符分配 
一个状态;后半部分演示了子状态机的调用方式:首先对子状态机进行初始化(如果这个子状态机确
实需要这个步骤);紧接着是通过一个专门的状态来进行子状态机调用。我们通过子状态机的返回值
来了解子状态机的状态——正在进行(on going),完成(cpl )还是发生了什么错误(返回值为负数)

 

  1.  
  2.  
  3. /***************************************************************************
  4. *   Copyright(C)2009-2017 by Gorgon Meducer<[email protected]> *
  5. *                                                                         *
  6. *   This program is free software; you can redistribute it and/or modify  *
  7. *   it under the terms of the GNU Lesser General Public License as        *
  8. *   published by the Free Software Foundation; either version 2 of the    *
  9. *   License, or (at your option) any later version.                       *
  10. *                                                                         *
  11. *   This program is distributed in the hope that it will be useful,       *
  12. *   but WITHOUT ANY WARRANTY; without even the implied warranty of        *
  13. *   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the         *
  14. *   GNU General Public License for more details.                          *
  15. *                                                                         *
  16. *   You should have received a copy of the GNU Lesser General Public      *
  17. *   License along with this program; if not, write to the                 *
  18. *   Free Software Foundation, Inc.,                                       *
  19. *   59 Temple Place - Suite 330, Boston, MA  02111-1307, USA.             *
  20. ***************************************************************************/
  21.  
  22. /*============================ INCLUDES ======================================*/
  23. #include ".\app_cfg.h"
  24.  
  25. /*============================ MACROS ========================================*/
  26. /*============================ MACROFIED FUNCTIONS ===========================*/   
  27.  
  28. #ifndef SERIAL_OUT
  29. #define SERIAL_OUT(__BYTE)      serial_out(__BYTE)
  30. #endif
  31.  
  32. /*============================ TYPES =========================================*/
  33.  
  34. /*! \brief you can use simple fsm at any where you want with little cost. 
  35.            E.g.
  36.             
  37. *! \brief function that output a char with none-block manner
  38. *! \param chByte target char
  39. *! \retval true the target char has been put into the output buffer 
  40. *! \retval false service is busy
  41. */
  42. extern bool serial_out(uint8_t chByte);
  43.  
  44.  
  45.  
  46. /*============================ GLOBAL VARIABLES ==============================*/
  47. /*============================ LOCAL VARIABLES ===============================*/
  48. /*============================ PROTOTYPES ====================================*/
  49.  
  50. /*! /brief define fsm delay_1s
  51. *!        list all the parameters
  52. */
  53. simple_fsm( delay_1s,
  54.     
  55.     /* define all the parameters used in the fsm */ 
  56.     def_params(
  57.         uint32_t wCounter;                  //!< a uint32_t counter
  58.     )
  59. )
  60.  
  61. /*! /brief define fsm print_hello
  62. *!        list all the parameters
  63. */
  64. simple_fsm( print_hello,
  65.     def_params(
  66.         fsm(delay_1s) fsmDelay;             //!< sub fsm delay_1s
  67.     )
  68. )
  69.  
  70. /*============================ IMPLEMENTATION ================================*/
  71.  
  72.  
  73.  
  74. /*! /brief define the fsm initialiser for FSML delay_1s
  75. *! /param wCounter  an uint32_t value for the delay count
  76. */
  77. fsm_initialiser(delay_1s,               //!< define initialiser for fsm: delay_1s
  78.     /*! list all the parameters required by this initialiser */
  79.     args(           
  80.         uint32_t wCounter               //!< delay count
  81.     ))
  82.  
  83.     /*! the body of this initialiser */
  84.     init_body (
  85.         this.wCounter = wCounter;       //!< initialiser the fsm paramter
  86.     )
  87. /* End of the fsm initialiser */
  88.  
  89.  
  90. /*! /brief Implement the fsm: delay_1s
  91. *         This fsm only contains one state.
  92. */
  93. fsm_implementation(  delay_1s)
  94.     def_states(DELAY_1S)                //!< list all the states used in the FSM
  95.  
  96.     /* the body of the FSM: delay_1s */
  97.     body (
  98.         state(  DELAY_1S,               //!< state: DELAY_1s
  99.             if (!this.wCounter) {
  100.                 fsm_cpl();              //!< FSM is completed
  101.             }
  102.             this.wCounter--;
  103.             fsm_on_going();             //!< on-going
  104.         )
  105.     )
  106. /* End of fsm implementation */
  107.  
  108.  
  109. fsm_initialiser(print_hello)
  110.     init_body ()
  111.  
  112. /*! /brief Implement the fsm: delay_1s
  113. *         This fsm only contains one state.
  114. */
  115. fsm_implementation(print_hello)
  116.  
  117.     /*! list all the states used in the FSM */
  118.     def_states(PRINT_H, PRINT_E, PRINT_L, PRINT_L_2, PRINT_O, DELAY)
  119.  
  120.     body(
  121. //! the on_start block are called once and only once on the entry point of a FSM
  122. //        on_start(
  123. //            /* add fsm parameter initialisation code here */
  124. //        )
  125.  
  126.         state(PRINT_H,
  127.             if (SERIAL_OUT('H')) {
  128.                 transfer_to(PRINT_E);   //!< transfer to state PRINT_E
  129.             }
  130.             fsm_on_going();             //!< on going
  131.         )
  132.         
  133.         state(PRINT_E,
  134.             if (SERIAL_OUT('e')) {
  135.                 transfer_to(PRINT_L);
  136.             }
  137.             fsm_on_going();
  138.         )
  139.  
  140.         state(PRINT_L,
  141.             if (SERIAL_OUT('l')) {
  142.                 transfer_to(PRINT_L_2);
  143.             }
  144.             fsm_on_going();
  145.         )
  146.  
  147.         state(PRINT_L_2,
  148.             if (SERIAL_OUT('l')) {
  149.                 transfer_to(PRINT_O);
  150.             }
  151.             fsm_on_going();
  152.         )
  153.  
  154.         state(PRINT_O,
  155.             if (!SERIAL_OUT('o')) {
  156.                 fsm_on_going();
  157.             }
  158.  
  159.             //! initialize the internal sub fsm
  160.             init_fsm(   delay_1s,           //!< FSM: delay_1s
  161.                         &(this.fsmDelay),   //!< the fsm control block
  162.                         args(10000));       //!< pass parameters to the initialiser
  163.  
  164.             //! update the state to DELAY without yield, so it will fall-through to the following state directly
  165.             update_state_to(DELAY);
  166.         )
  167.  
  168.  
  169.         state(DELAY,
  170.             /*! call the sub fsm */
  171.             if (fsm_rt_cpl == call_fsm(delay_1s, &(this.fsmDelay))) {
  172.                 fsm_cpl();
  173.             }
  174.             fsm_on_going();
  175.         )
  176.     )
  177.  
  178. /* EOF */

复制代码


 

猜你喜欢

转载自blog.csdn.net/wuhenyouyuyouyu/article/details/81385999