天天看點

當單片機遇到狀态機(一) QP架構的入門前言入門QP

前言

前些日子在微信上看到李肖遙的公衆号,裡面系統講述了QP架構,我很有感觸。我用QP架構很多年了,一開始是使用QM和QPC++,到後來抛棄了QM,直接使用QPC裸寫程式,到後來自己寫狀态機架構。可以這麼說,QP架構引導了我的技術成長。我共享的博文,雖然都以QP為起點進行展開,但很多東西,都是QP官網的資料所沒有的。我希望接受大家的意見、建議和批評,相信對我來說,會有更大的提升。

這一系列的博文,稱為《當單片機遇上狀态機》系列,暫時先規劃以下幾篇。

  • 入門QP

    讓大家開始使用QP,消除對QP的畏難心理,建立起初步的信心。這一步非常重要。

  • 從switch-case到架構的進化

    大家很難了解,自己用switch-case實作狀态機,用的好好的,幹嘛要用狀态機架構。這篇博文,就是為了說明,switch-case狀态機,是如何一步一步進化到一個狀态機架構的。我們所寫的這個狀态機架構,和QP之間,到底有着什麼關系,有着多少差距。

  • QP的高階使用和QM的使用

    QM作為一個輔助工具?它的作用是什麼?它是怎麼生成代碼的?它和QP之間是什麼關系?在這一篇裡,将會做詳細介紹。

  • QP的哲學

    精通QP,了解其哲學思想非常重要。它的哲學思想是什麼樣的?是如何展現的?

  • 其他

    後續的規劃,我希望根據大家的回報意見而定。我用狀态機架構多年,難免做不到換位思考,不能照顧到初學者的感受。希望大家踴躍回報意見。無論是贊揚還是批評,我都虛心接受。

符合下述條件的讀者,是這個系列的博文所針對的讀者群。

  • 了解狀态機,知道狀态機是什麼?
  • 對QP架構有興趣,有了解,但苦于不能快速入門。
  • 入門了QP架構,但對其中的技術精髓,不能掌握的。
  • 一直用switch-case狀态機,苦于程式規模,感覺難以維護的。
  • 使用RTOS,對任務同步和資源共享深深頭痛的。

入門QP

我們學習一個語言,或者一項技術,第一件要做的事情,就是實作一個類似于Hello world的最小程式。在單片機上,當然就是LED燈的閃爍。不說廢話了,先上代碼。

代碼結構

源碼我已經開源在gitee(qpstudy)。代碼結構,可以在Keil工程中看到,是一個QP的運作最小系統。QP版本使用的是最新的V6.9.3版本。

為了便于大家的學習,我抛棄了官方例程。官方例程有些繁瑣,裡面還有大量的doxygen格式的注釋,對初學者不友好。與官方例程相比,能删掉的部分,全部都删掉了,隻留下代碼和必要中文注釋,目的就是為了最大限度降低大家學習QP的入門門檻,也算是中國特色吧。這四個源碼,代碼未來我們程式架構的不同層次,以後所有的例程,就是以這個代碼結構為基礎,進行擴充。

還有一個需要說明的,第一個例程,我并沒有使用QM模組化工具進行LED狀态機的模組化和代碼生成。QM工具,本質上基于模型的開發方法,是形式化開發方法之一。在軟體開發中,這種方法一直飽受争議。這個世界現存的大部分軟體架構,是不存在所謂代碼生成工具的。目前我對QM等模組化工具持保守态度,軟體開發還是要回歸代碼本身,能利用工具,但不要依賴工具。QM工具,我認為是QP架構在營銷和商業上的需求推動的。是以,在未來的教程中,我将QM的使用,放在次要位置,主要還直接程式設計為主,我認為這樣才會給大家帶來真正的提升。

這四個源碼分别是:

  • main.c

    包含了硬體的初始化、QP架構的初始化、各狀态機子產品(暫定稱呼,嚴謹應叫AO子產品)的建構,架構的啟動等一系列流程。

  • bsp.c

    硬體初始化,此處僅包含SysTick的初始化和SysTick中斷函數。

  • ao_led.c

    LED狀态機的源碼。

  • hook.c

    QP架構的回調函數的實作,此處都為空函數,暫時不予實作。

  • evt_def.h

    事件的定義。QP架構的事件定義,使用枚舉實作。個人覺得,事件的定義,如果用字元串實作,更加有利于子產品的解耦和對分布式的支援(這個問題可參考後續的部落格《将軟總線進行到底》)。QP使用枚舉來定義事件,個人認為是為了降低RAM和CPU的開銷。

  • 其他
    • QP源碼
    • QP接口代碼

      QP架構對硬體平台或者RTOS的接口源碼。

    • MCU相關代碼,包含Startup檔案、CMSIS相關、固件庫相關代碼

QP的啟動流程

以下代碼就是QP架構的啟動過程。

#include "qpc.h"                                        // qpc架構頭檔案
#include "evt_def.h"                                    // 事件定義頭檔案
#include "bsp.h"                                        // 硬體初始化
#include "ao_led.h"                                     // LED狀态機

Q_DEFINE_THIS_MODULE("Main")        // 定義目前的子產品名稱,此名稱在QS和斷言中會使用。

ao_led_t led;                                           // 狀态機LED對象

int main(void)
{
    static QSubscrList sub_sto[MAX_PUB_SIG];            // 定義訂閱緩沖區
    static QF_MPOOL_EL(m_evt_t) sml_pool_sto[128];      // 定義事件池
    
    QF_init();                                          // 狀态機架構初始化
    QF_psInit(sub_sto, Q_DIM(sub_sto));                 // 釋出-訂閱緩沖區的初始化
    QF_poolInit(sml_pool_sto,                           // 事件池的初始化
                sizeof(sml_pool_sto),
                sizeof(sml_pool_sto[0]));
                
    ao_led_ctor(&led);                                  // 狀态機的建構
    
    return QF_run();                                    // 架構啟動
}
           

QP的回調函數

通常的調用,都是上層函數調用底層函數。如果使用了某個函數,需要上層實作,這樣就産生了底層對上層函數的調用,稱為回調函數(Call back),也叫鈎子函數(Hook)。

一般而言,回調函數,主要用于頂層功能在底層子產品裡的插入,或者實作底層子產品的定制功能。QP架構定義四個回調函數,需要QP的使用者來實作。

void QF_onStartup(void) {
    bsp_init();                                         // 硬體初始化
}
void QF_onCleanup(void) {}
void QV_onIdle(void) {}
void Q_onAssert(char_t const * const module, int_t const loc)
{
    (void)module;
    (void)loc;
    while (1);
}
           

QF_onStartup是用于QP架構啟動時,所調用的回調函數。一般可以執行一些初始化工作,比如硬體初始化,記憶體初始化。這也就是為什麼在main函數中沒有看到硬體初始化的原因。

QF_onCleanup與RTOS相關,暫時用不到。

QV_onIdle是QP架構空閑時,也就是沒有任何事件産生時,所執行的函數。

Q_onAssert是QP的斷言的實作。斷言,是程式一種檢查機制,當程式的執行發生異常時,用于檢查不可能發生情況。比如下面的函數,當函數func_add的兩個參數,都不可能大于或者等于100時,就可以對使用斷言進行檢查,以防禦可能出現的參數輸入錯誤。這種程式設計方式,也叫做防禦式程式設計。防禦式程式設計的思想就是,若崩潰,就崩潰的更猛烈些,以便在程式設計的早期,就發現程式錯誤,并強迫開發者解決掉。具體可以參考後續的博文《談防禦式程式設計》。

int func_add(int x, int y)
{
    Q_ASSERT(x < 100);
    Q_ASSERT(y < 100);

    return (x + y);
}
           

系統嘀嗒

在目前的曆程中,使用一個QP中自帶的協作式核心QV。在使用了QV核心的前提下,SysTick隻有一個作用,那就是為時間事件提供時間基準。

#include "bsp.h"
#include "stm32f10x.h"
#include "qpc.h"

void bsp_init(void)
{
    SysTick_Config(SystemCoreClock / 1000);         // 時間基準為1ms
    NVIC_SetPriority(SysTick_IRQn, 0);              // 設定中斷優先級
}

void SysTick_Handler(void)
{
    QF_TICK_X(0U, &l_SysTick_Handler);              // 時間基準
}
           

如果大家需要換一個晶片跑這個例程,那麼僅僅需要更換Keil RTE中的Deivce和這裡的代碼即可。隻有這裡的代碼是硬體相關的。以後大家寫程式,也是一樣,要執行硬體相關最小原則,也就是說,要把硬體相關的代碼壓縮到最低。後續也會有博文專門講這個話題(《将裝置抽象進行到底 驅動篇》)。

LED狀态機

LED狀态機是核心功能,學會了這個,就入門了QP。在QP中,AO(Active Object)是核心,QP的所有功能都是圍繞AO展開的,就好比在RTOS中任務是核心一樣。AO之間,純粹靠事件進行通信,原則上是不允許AO間共享全局變量的(詳細請參考後續《當單片機遇上并發 Actor篇》)。

LED狀态機的類定義

下面是頭檔案的定義。頭檔案中,主要定義了LED狀态機類,并聲明了類方法。這裡所說的類,是在邏輯上的類。在C語言中,沒有類的概念,隻能使用結構體替代類的實作。

#include "qpc.h"

#define AO_LED_QUEUE_LENGTH                 32

// LED類的定義
typedef struct ao_led_tag{
    QActive super;                                      // 對QActive類的繼承
    
    QEvt const *evt_queue[AO_LED_QUEUE_LENGTH];         // 事件隊列
    QTimeEvt timeEvt;                                   // 延時事件
    
    bool status;                                        // LED狀态
} ao_led_t;

// LED的類方法 構造函數
void ao_led_ctor(ao_led_t * const me);
           

LED狀态機是完全按照C語言面向對象的方法實作的。在C語言中,由于在語言層面并沒有對面向對象進行支援,是以面向對象的C開發,是運用了一些特殊技巧的。這些技巧,我們會在後續(《将面向對象進行到底 C語言篇》)進行詳細介紹。目前,為了增強大家入門的信心,我隻說與QP入門相關的東西。

QActive類,簡單說就是狀态機類。在定義一個狀态機對象時,需要從QActive類進行繼承。

LED狀态機類的實作

LED狀态機類的實作,共分為兩個部分,一是類方法的實作,二是類狀态的實作。

這裡隻有一個類方法,那就是LED類的構造函數。構造函數,是C++中的概念,C語言中并沒有這個概念,這裡與類相似,仍然是構造功能的模拟。從代碼可以看出,構造函數有幾個内容,一個必須的步驟,就是活動對象的構造和啟動。構造函數中的另一個内容,就是初始化一個時間事件的對象,因為每500ms要發送一個Evt_Time_500ms事件。

// 活動對象(AO,Active Object)LED的建構
void ao_led_ctor(ao_led_t * const me)
{
    // LED對象的變量初始化
    me->status = false;

    // 活動對象的建構
    QActive_ctor(&me->super, Q_STATE_CAST(&state_init));
    // 時間對象的建構
    QTimeEvt_ctorX(&me->timeEvt, &me->super, Evt_Time_500ms, 0U);
    // 活動對象的啟動
    QACTIVE_START(  &me->super,
                    1,                              // 優先級
                    me->evt_queue,                  // 事件隊列
                    AO_LED_QUEUE_LENGTH,            // 事件隊列深度
                    (void *)0,                      // 任務棧,RTOS相關,可忽略
                    0U,                             // 任務棧深度,RTOS相關,可忽略
                    (QEvt *)0);
}
           

LED狀态類有三個狀态,初始狀态,ON狀态和OFF狀态。

  • 初始狀态

    所有的初始狀态都是一樣的,就是先訂閱狀态機運作所需要的事件。然後直接跳轉到某個特定的狀态。實際上,事件的訂閱,不一定要在初始狀态裡執行。在狀态機運作時,随時都能訂閱事件,或者解除對事件的訂閱。

    這個事件的訂閱機制,就是在軟體設計模式中,大名鼎鼎的釋出-訂閱模式(可參考後續的博文《當單片機遇上設計模式 釋出-訂閱模式》)。釋出-訂閱模式的最大好處,就是子產品間的徹底解耦。這裡插入一個程式設計原則,好的程式,一定是解耦良好的程式。所謂耦合,就是子產品A變了,子產品B也得跟着變,否則,B子產品會運作不正常,子產品之間有依賴;所謂解耦,就是去除子產品之間的依賴,子產品A變了,子產品B無須改變。

    // 初始狀态
    static QState state_init(ao_led_t * const me, void const * const par)
    {
        // 事件Evt_Time_500ms的訂閱
        QActive_subscribe(&me->super, Evt_Time_500ms);
    
        return Q_TRAN(&state_on);
    }
               
  • ON狀态

    參數的傳輸

    從代碼中,可以看到,當産生事件時,架構會自動調用state_on函數,led對象,是通過參數me傳進來的,這個me指針,相當于C++裡的this指針,而所産生的事件,是通過參數e傳輸進來的。

    事件的處理

    大家注意到代碼裡有三個事件Q_ENTRY_SIG、Q_EXIT_SIG和Evt_Time_500ms。其中前兩個是系統事件,也就是QP架構預設支援的事件。Q_ENTRY_SIG是狀态進入事件,當進入一個狀态時,QP架構會預設執行這個事件。Q_EXIT_SIG是狀态退出事件,當退出一個狀态時,QP架構也會預設執行這個事件。Evt_Time_500ms是使用者事件,也就是我們自己定義的事件。Q_ENTRY_SIG和Q_EXIT_SIG并不強制定義,而我們要根據自己的需要,看在進入或者退出一個狀态時,是否有動作執行,來決定是否對這兩個系統事件進行實作。QP還有一個系統事件,Q_INIT_SIG,這個和階層化狀态機相關,以後再讨論。

    事件後的傳回值

    大家注意到每個狀态機在不同的case分支下,都有不同的傳回值,比如Q_HANDLED(),Q_TRAN(&state_off)或者Q_SUPER(&QHsm_top)。

    之是以有這些傳回值的不同,是為了在處理完畢一個事件後,告訴架構,下一步要幹什麼。Q_SUPER(&QHsm_top)告訴架構此事件被忽略,什麼也不處理;Q_HANDLED()告訴架構,此事件已經處理;而Q_TRAN(&state_off)告訴架構,需要跳轉到state_off狀态,架構這時會執行目前狀态的退出事件和下一個狀态的進入事件。

    QP架構的技術限制

    無論是事件處理的機制,還是傳回值的格式,都是QP架構的技術限制。任何一個軟體架構,在帶來程式設計便利的同時,也會帶來性能上的開銷和技術的限制。我們要使用一個架構,也就要遵守它制定的技術限制,否則架構就沒有辦法有效的運作。

    // LED的on狀态
    static QState state_on(ao_led_t * const me, QEvt const * const e)
    {
        switch (e->sig) {
            case Q_ENTRY_SIG:                           // 狀态的進入事件
                me->status = true;                      // 打開LED燈
                QTimeEvt_armX(&me->timeEvt, 500, 0U);   // 500ms後發送時間事件
                return Q_HANDLED();                     // 通知架構,事件已處理
    
            case Q_EXIT_SIG:                            // 狀态的退出事件
                QTimeEvt_disarm(&me->timeEvt);
                return Q_HANDLED();
    
            case Evt_Time_500ms:
                return Q_TRAN(&state_off);              // 通知架構,狀态轉移至state_off
    
            default:
                return Q_SUPER(&QHsm_top);              // 其他事件,在此時不處理
        }
    }
    
    // LED的Off狀态
    static QState state_off(ao_led_t * const me, QEvt const * const e)
    {
        switch (e->sig) {
            case Q_ENTRY_SIG:
                me->status = false;                     // 關閉LED燈
                QTimeEvt_armX(&me->timeEvt, 500, 0U);
                return Q_HANDLED();
    
            case Q_EXIT_SIG:
                QTimeEvt_disarm(&me->timeEvt);
                return Q_HANDLED();
    
            case Evt_Time_500ms:
                return Q_TRAN(&state_on);
    
            default:
                return Q_SUPER(&QHsm_top);              // 其他事件,在此時不處理
        }
    }
               
  • OFF狀态

    與ON狀态一樣,不再贅述。有人可以會提出疑問,在收到Evt_Time_500ms事件的時候,讓LED的狀态翻轉,不必跳轉到OFF狀态,不就節約了一個狀态嗎?的确,這樣寫的确更簡練,但我們的目的是為了展示狀态機的使用,是以可以增加了一個OFF狀态。