simple fsm狀態機模板應用筆記(一)——simple fsm的設計思維和哲學

原文地址:https://www.amobbs.com/thread-5668532-1-1.html

說在前面的話

好久沒有整理代碼了,最近一直在做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日夜

如何使用

1. 如何定義一個狀態機

語法:

simple_fsm( <狀態機名稱>,
    def_params(
        參數列表                
    )
)

例子:

/*! fsm used to output specified string */
simple_fsm( print_string,
    def_params(
        const char *pchStr;        //!< point to the target string
        uint16_t hwIndex;          //!< current index
        uint16_t hwLength;        //!< claimed length of the target string, it is used to prevent buffer overflow 
    )
)
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章