當(dāng)單片機遇到狀態(tài)機——入門QP
來源:技術(shù)讓夢想更偉大
作者:ming_mei
前言
前些日子在微信上看到李肖遙的公眾號,里面系統(tǒng)講述了QP框架,我很有感觸。我用QP框架很多年了,一開始是使用QM和QPC++,到后來拋棄了QM,直接使用QPC裸寫程序,到后來自己寫狀態(tài)機框架。
可以這么說,QP框架引導(dǎo)了我的技術(shù)成長。我共享的博文,雖然都以QP為起點進行展開,但很多東西,都是QP官網(wǎng)的資料所沒有的。我希望接受大家的意見、建議和批評,相信對我來說,會有更大的提升。
這一系列的博文,稱為《當(dāng)單片機遇上狀態(tài)機》系列,暫時先規(guī)劃以下幾篇:
入門QP
讓大家開始使用QP,消除對QP的畏難心理,建立起初步的信心。這一步非常重要。
從switch-case到框架的進化
大家很難理解,自己用switch-case實現(xiàn)狀態(tài)機,用的好好的,干嘛要用狀態(tài)機框架。這篇博文,就是為了說明,switch-case狀態(tài)機,是如何一步一步進化到一個狀態(tài)機框架的。我們所寫的這個狀態(tài)機框架,和QP之間,到底有著什么關(guān)系,有著多少差距。
QP的高階使用和QM的使用
QM作為一個輔助工具?它的作用是什么?它是怎么生成代碼的?它和QP之間是什么關(guān)系?在這一篇里,將會做詳細介紹。
QP的哲學(xué)
精通QP,理解其哲學(xué)思想非常重要。它的哲學(xué)思想是什么樣的?是如何體現(xiàn)的?
其他
后續(xù)的規(guī)劃,我希望根據(jù)大家的反饋意見而定。我用狀態(tài)機框架多年,難免做不到換位思考,不能照顧到初學(xué)者的感受。希望大家踴躍反饋意見。無論是贊揚還是批評,我都虛心接受。
入門QP
我們學(xué)習(xí)一個語言,或者一項技術(shù),第一件要做的事情,就是實現(xiàn)一個類似于Hello world的最小程序。在單片機上,當(dāng)然就是LED燈的閃爍。不說廢話了,先上代碼。
代碼結(jié)構(gòu)
代碼結(jié)構(gòu),可以在Keil工程中看到,是一個QP的運行最小系統(tǒng)。QP版本使用的是最新的V6.9.3版本。
為了便于大家的學(xué)習(xí),我拋棄了官方例程。官方例程有些繁瑣,里面還有大量的doxygen格式的注釋,對初學(xué)者不友好。與官方例程相比,能刪掉的部分,全部都刪掉了,只留下代碼和必要中文注釋,目的就是為了最大限度降低大家學(xué)習(xí)QP的入門門檻,也算是中國特色吧。這四個源碼,代碼未來我們程序架構(gòu)的不同層次,以后所有的例程,就是以這個代碼結(jié)構(gòu)為基礎(chǔ),進行擴充。
還有一個需要說明的,第一個例程,我并沒有使用QM建模工具進行LED狀態(tài)機的建模和代碼生成。QM工具,本質(zhì)上基于模型的開發(fā)方法,是形式化開發(fā)方法之一。在軟件開發(fā)中,這種方法一直飽受爭議。這個世界現(xiàn)存的大部分軟件框架,是不存在所謂代碼生成工具的。目前我對QM等建模工具持保守態(tài)度,軟件開發(fā)還是要回歸代碼本身,能利用工具,但不要依賴工具。QM工具,我認為是QP框架在營銷和商業(yè)上的需求推動的。因此,在未來的教程中,我將QM的使用,放在次要位置,主要還直接編程為主,我認為這樣才會給大家?guī)碚嬲奶嵘?/p>
這四個源碼分別是:
main.c 包含了硬件的初始化、QP框架的初始化、各狀態(tài)機模塊(暫定稱呼,嚴謹應(yīng)叫AO模塊)的構(gòu)建,框架的啟動等一系列流程。
bsp.c 硬件初始化,此處僅包含SysTick的初始化和SysTick中斷函數(shù)。
ao_led.c LED狀態(tài)機的源碼。
hook.c QP框架的回調(diào)函數(shù)的實現(xiàn),此處都為空函數(shù),暫時不予實現(xiàn)。
evt_def.h 事件的定義。QP框架的事件定義,使用枚舉實現(xiàn)。個人覺得,事件的定義,如果用字符串實現(xiàn),更加有利于模塊的解耦和對分布式的支持(這個問題可參考后續(xù)的博客《將軟總線進行到底》)。QP使用枚舉來定義事件,個人認為是為了降低RAM和CPU的開銷。
其他
QP源碼 QP接口代碼 QP框架對硬件平臺或者RTOS的接口源碼。 MCU相關(guān)代碼,包含Startup文件、CMSIS相關(guān)、固件庫相關(guān)代碼
QP的啟動流程
以下代碼就是QP框架的啟動過程。
#include?"qpc.h"????????????????????????????????????????//?qpc框架頭文件
#include?"evt_def.h"????????????????????????????????????//?事件定義頭文件
#include?"bsp.h"????????????????????????????????????????//?硬件初始化
#include?"ao_led.h"?????????????????????????????????????//?LED狀態(tài)機
Q_DEFINE_THIS_MODULE("Main")????????//?定義當(dāng)前的模塊名稱,此名稱在QS和斷言中會使用。
ao_led_t?led;???????????????????????????????????????????//?狀態(tài)機LED對象
int?main(void)
{
????static?QSubscrList?sub_sto[MAX_PUB_SIG];????????????//?定義訂閱緩沖區(qū)
????static?QF_MPOOL_EL(m_evt_t)?sml_pool_sto[128];??????//?定義事件池
????
????QF_init();??????????????????????????????????????????//?狀態(tài)機框架初始化
????QF_psInit(sub_sto,?Q_DIM(sub_sto));?????????????????//?發(fā)布-訂閱緩沖區(qū)的初始化
????QF_poolInit(sml_pool_sto,???????????????????????????//?事件池的初始化
????????????????sizeof(sml_pool_sto),
????????????????sizeof(sml_pool_sto[0]));
????????????????
????ao_led_ctor(&led);??????????????????????????????????//?狀態(tài)機的構(gòu)建
????
????return?QF_run();????????????????????????????????????//?框架啟動
}
QP的回調(diào)函數(shù)
通常的調(diào)用,都是上層函數(shù)調(diào)用底層函數(shù)。如果使用了某個函數(shù),需要上層實現(xiàn),這樣就產(chǎn)生了底層對上層函數(shù)的調(diào)用,稱為回調(diào)函數(shù)(Call back),也叫鉤子函數(shù)(Hook)。
一般而言,回調(diào)函數(shù),主要用于頂層功能在底層模塊里的插入,或者實現(xiàn)底層模塊的定制功能。QP框架定義四個回調(diào)函數(shù),需要QP的使用者來實現(xiàn)。
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框架啟動時,所調(diào)用的回調(diào)函數(shù)。一般可以執(zhí)行一些初始化工作,比如硬件初始化,內(nèi)存初始化。這也就是為什么在main函數(shù)中沒有看到硬件初始化的原因。
QF_onCleanup與RTOS相關(guān),暫時用不到。
QV_onIdle是QP框架空閑時,也就是沒有任何事件產(chǎn)生時,所執(zhí)行的函數(shù)。
Q_onAssert是QP的斷言的實現(xiàn)。斷言,是程序一種檢查機制,當(dāng)程序的執(zhí)行發(fā)生異常時,用于檢查不可能發(fā)生情況。比如下面的函數(shù),當(dāng)函數(shù)func_add的兩個參數(shù),都不可能大于或者等于100時,就可以對使用斷言進行檢查,以防御可能出現(xiàn)的參數(shù)輸入錯誤。這種編程方式,也叫做防御式編程。防御式編程的思想就是,若崩潰,就崩潰的更猛烈些,以便在編程的早期,就發(fā)現(xiàn)程序錯誤,并強迫開發(fā)者解決掉。
int?func_add(int?x,?int?y)
{
????Q_ASSERT(x?100);
????Q_ASSERT(y?100);
????return?(x?+?y);
}
系統(tǒng)嘀嗒
在當(dāng)前的歷程中,使用一個QP中自帶的協(xié)作式內(nèi)核QV。在使用了QV內(nèi)核的前提下,SysTick只有一個作用,那就是為時間事件提供時間基準。
#include?"bsp.h"
#include?"stm32f10x.h"
#include?"qpc.h"
void?bsp_init(void)
{
????SysTick_Config(SystemCoreClock?/?1000);?????????//?時間基準為1ms
????NVIC_SetPriority(SysTick_IRQn,?0);??????????????//?設(shè)置中斷優(yōu)先級
}
void?SysTick_Handler(void)
{
????QF_TICK_X(0U,?&l_SysTick_Handler);??????????????//?時間基準
}
如果大家需要換一個芯片跑這個例程,那么僅僅需要更換Keil RTE中的Deivce和這里的代碼即可。只有這里的代碼是硬件相關(guān)的。以后大家寫程序,也是一樣,要執(zhí)行硬件相關(guān)最小原則,也就是說,要把硬件相關(guān)的代碼壓縮到最低。
LED狀態(tài)機
LED狀態(tài)機是核心功能,學(xué)會了這個,就入門了QP。在QP中,AO(Active Object)是核心,QP的所有功能都是圍繞AO展開的,就好比在RTOS中任務(wù)是核心一樣。AO之間,純粹靠事件進行通信,原則上是不允許AO間共享全局變量的。
LED狀態(tài)機的類定義
下面是頭文件的定義。頭文件中,主要定義了LED狀態(tài)機類,并聲明了類方法。這里所說的類,是在邏輯上的類。在C語言中,沒有類的概念,只能使用結(jié)構(gòu)體替代類的實現(xiàn)。
#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狀態(tài)
}?ao_led_t;
//?LED的類方法?構(gòu)造函數(shù)
void?ao_led_ctor(ao_led_t?*?const?me);
LED狀態(tài)機是完全按照C語言面向?qū)ο蟮姆椒▽崿F(xiàn)的。在C語言中,由于在語言層面并沒有對面向?qū)ο筮M行支持,因此面向?qū)ο蟮腃開發(fā),是運用了一些特殊技巧的。
QActive類,簡單說就是狀態(tài)機類。在定義一個狀態(tài)機對象時,需要從QActive類進行繼承。
LED狀態(tài)機類的實現(xiàn)
LED狀態(tài)機類的實現(xiàn),共分為兩個部分,一是類方法的實現(xiàn),二是類狀態(tài)的實現(xiàn)。
這里只有一個類方法,那就是LED類的構(gòu)造函數(shù)。構(gòu)造函數(shù),是C++中的概念,C語言中并沒有這個概念,這里與類相似,仍然是構(gòu)造功能的模擬。從代碼可以看出,構(gòu)造函數(shù)有幾個內(nèi)容,一個必須的步驟,就是活動對象的構(gòu)造和啟動。構(gòu)造函數(shù)中的另一個內(nèi)容,就是初始化一個時間事件的對象,因為每500ms要發(fā)送一個Evt_Time_500ms事件。
//?活動對象(AO,Active?Object)LED的構(gòu)建
void?ao_led_ctor(ao_led_t?*?const?me)
{
????//?LED對象的變量初始化
????me->status?=?false;
????//?活動對象的構(gòu)建
????QActive_ctor(&me->super,?Q_STATE_CAST(&state_init));
????//?時間對象的構(gòu)建
????QTimeEvt_ctorX(&me->timeEvt,?&me->super,?Evt_Time_500ms,?0U);
????//?活動對象的啟動
????QACTIVE_START(??&me->super,
????????????????????1,??????????????????????????????//?優(yōu)先級
????????????????????me->evt_queue,??????????????????//?事件隊列
????????????????????AO_LED_QUEUE_LENGTH,????????????//?事件隊列深度
????????????????????(void?*)0,??????????????????????//?任務(wù)棧,RTOS相關(guān),可忽略
????????????????????0U,?????????????????????????????//?任務(wù)棧深度,RTOS相關(guān),可忽略
????????????????????(QEvt?*)0);
}
LED狀態(tài)類有三個狀態(tài),初始狀態(tài),ON狀態(tài)和OFF狀態(tài)。
初始狀態(tài)
所有的初始狀態(tài)都是一樣的,就是先訂閱狀態(tài)機運行所需要的事件。然后直接跳轉(zhuǎn)到某個特定的狀態(tài)。實際上,事件的訂閱,不一定要在初始狀態(tài)里執(zhí)行。在狀態(tài)機運行時,隨時都能訂閱事件,或者解除對事件的訂閱。
這個事件的訂閱機制,就是在軟件設(shè)計模式中,大名鼎鼎的發(fā)布-訂閱模式。發(fā)布-訂閱模式的最大好處,就是模塊間的徹底解耦。這里插入一個程序設(shè)計原則,好的程序,一定是解耦良好的程序。所謂耦合,就是模塊A變了,模塊B也得跟著變,否則,B模塊會運行不正常,模塊之間有依賴;所謂解耦,就是去除模塊之間的依賴,模塊A變了,模塊B無須改變。
//?初始狀態(tài)
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狀態(tài)
參數(shù)的傳輸
從代碼中,可以看到,當(dāng)產(chǎn)生事件時,框架會自動調(diào)用state_on函數(shù),led對象,是通過參數(shù)me傳進來的,這個me指針,相當(dāng)于C++里的this指針,而所產(chǎn)生的事件,是通過參數(shù)e傳輸進來的。
事件的處理
大家注意到代碼里有三個事件Q_ENTRY_SIG、Q_EXIT_SIG和Evt_Time_500ms。其中前兩個是系統(tǒng)事件,也就是QP框架默認支持的事件。Q_ENTRY_SIG是狀態(tài)進入事件,當(dāng)進入一個狀態(tài)時,QP框架會默認執(zhí)行這個事件。Q_EXIT_SIG是狀態(tài)退出事件,當(dāng)退出一個狀態(tài)時,QP框架也會默認執(zhí)行這個事件。Evt_Time_500ms是用戶事件,也就是我們自己定義的事件。Q_ENTRY_SIG和Q_EXIT_SIG并不強制定義,而我們要根據(jù)自己的需要,看在進入或者退出一個狀態(tài)時,是否有動作執(zhí)行,來決定是否對這兩個系統(tǒng)事件進行實現(xiàn)。QP還有一個系統(tǒng)事件,Q_INIT_SIG,這個和層次化狀態(tài)機相關(guān),以后再討論。
事件后的返回值
大家注意到每個狀態(tài)機在不同的case分支下,都有不同的返回值,比如Q_HANDLED(),Q_TRAN(&state_off)或者Q_SUPER(&QHsm_top)。
之所以有這些返回值的不同,是為了在處理完畢一個事件后,告訴框架,下一步要干什么。Q_SUPER(&QHsm_top)告訴框架此事件被忽略,什么也不處理;Q_HANDLED()告訴框架,此事件已經(jīng)處理;而Q_TRAN(&state_off)告訴框架,需要跳轉(zhuǎn)到state_off狀態(tài),框架這時會執(zhí)行當(dāng)前狀態(tài)的退出事件和下一個狀態(tài)的進入事件。
QP框架的技術(shù)約束
無論是事件處理的機制,還是返回值的格式,都是QP框架的技術(shù)約束。任何一個軟件框架,在帶來編程便利的同時,也會帶來性能上的開銷和技術(shù)的約束。我們要使用一個框架,也就要遵守它制定的技術(shù)約束,否則框架就沒有辦法有效的運行。
//?LED的on狀態(tài)
static?QState?state_on(ao_led_t?*?const?me,?QEvt?const?*?const?e)
{
????switch?(e->sig)?{
????????case?Q_ENTRY_SIG:???????????????????????????//?狀態(tài)的進入事件
????????????me->status?=?true;??????????????????????//?打開LED燈
????????????QTimeEvt_armX(&me->timeEvt,?500,?0U);???//?500ms后發(fā)送時間事件
????????????return?Q_HANDLED();?????????????????????//?通知框架,事件已處理
????????case?Q_EXIT_SIG:????????????????????????????//?狀態(tài)的退出事件
????????????QTimeEvt_disarm(&me->timeEvt);
????????????return?Q_HANDLED();
????????case?Evt_Time_500ms:
????????????return?Q_TRAN(&state_off);??????????????//?通知框架,狀態(tài)轉(zhuǎn)移至state_off
????????default:
????????????return?Q_SUPER(&QHsm_top);??????????????//?其他事件,在此時不處理
????}
}
//?LED的Off狀態(tài)
static?QState?state_off(ao_led_t?*?const?me,?QEvt?const?*?const?e)
{
????switch?(e->sig)?{
????????case?Q_ENTRY_SIG:
????????????me->status?=?false;?????????????????????//?關(guān)閉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狀態(tài)
與ON狀態(tài)一樣,不再贅述。有人可以會提出疑問,在收到Evt_Time_500ms事件的時候,讓LED的狀態(tài)翻轉(zhuǎn),不必跳轉(zhuǎn)到OFF狀態(tài),不就節(jié)約了一個狀態(tài)嗎?的確,這樣寫的確更簡練,但我們的目的是為了展示狀態(tài)機的使用,因此可以增加了一個OFF狀態(tài)。
https://blog.csdn.net/ming_mei??請勿二次轉(zhuǎn)載,否則將舉報,謝謝
???????????????? ?END ????????????????? 關(guān)注我的微信公眾號,回復(fù)“加群”按規(guī)則加入技術(shù)交流群。
點擊“閱讀原文”查看更多分享,歡迎點分享、收藏、點贊、在看。
