AWPLC 是 ZLG 自主研發(fā)的 PLC 系統(tǒng)(兼容 IEC61131-3),本文用定時器為例介紹一下如何擴展自定義功能塊。
背景
AWTK 全稱 Toolkit AnyWhere,是 ZLG 開發(fā)的開源 GUI 引擎,旨在為嵌入式系統(tǒng)、WEB、各種小程序、手機和 PC 打造的通用 GUI 引擎,為用戶提供一個功能強大、高效可靠、簡單易用、可輕松做出炫酷效果的 GUI 引擎。
AWPLC 是 ZLG 自主研發(fā)的 PLC 系統(tǒng)(兼容 IEC61131-3),其中 AWPLC 的運行時庫?(Runtime)?基于 ZLG TKC 開發(fā),可以移植到到任何主流 RTOS 和?嵌入式系統(tǒng)。AWPLC 的集成開發(fā)環(huán)境?(IDE)?基于 AWTK 開發(fā),可以運行在 Windows、MacOS 和 Linux 系統(tǒng)之上。AWPLC 的主要目標之一是把 PLC 中低代碼開發(fā)方法引入到嵌入式軟件,從而提高嵌入式軟件的開發(fā)效率和可靠性。
簡介
在前一篇文章中,我們說過,AWPLC 的重要特色之一就是高度可擴展,而且會內(nèi)置 ZLG 多年在嵌入式系統(tǒng)開發(fā)中積累的功能塊,包括各種算法、協(xié)議和實用功能,這將大大簡化嵌入式軟件的開發(fā)。
那怎么去開發(fā)自定義的功能塊呢?本文以 ZTIMER 為例介紹一下開發(fā)自定義功能塊的方法。ZTIMER 是一個帶計數(shù)功能的定時器,在前一篇文章中,我們用它實現(xiàn)了一個走馬燈的演示,其使用方法如下:
在 AWPLC 中,自定義功能塊和內(nèi)置功能塊具有同等待遇,因為它們都是按同樣的方式加入進來的。在進入正題前,我們先聊一下,系統(tǒng)的可擴展性以及實現(xiàn)方法。1.?可擴展性的好處在設計一個復雜軟件的架構(gòu)時,可擴展性是必須考慮的因素??蓴U展性至少帶來以下幾個好處:
- 可擴展性將軟件的框架與具體的實現(xiàn)分離開來,有助于降低系統(tǒng)的復雜度。系統(tǒng)的復雜性太高,會帶來一系列的問題,比如讓可理解性、可維護性和可靠性的降低,很多項目因此陷入無法掙脫的焦油坑里,最后士氣低落,人員流失,項目取消,公司蒙受巨大損失。在設計復雜軟件時,一定要存有敬畏之心;可擴展性將軟件變化的部分隔離開來,不但可以讓擴展的功能獨立變化,也可以方便的擴展新功能。在 AWPLC 中,?以后會擴展各種協(xié)議和算法的功能塊,必須保證 AWPLC 框架和這些擴展的功能塊是獨立的,才能讓開發(fā)工作順利進行;可擴展性有利于團隊的協(xié)作。?不同的通訊協(xié)議和算法,需要不同團隊的專家去開發(fā),可擴展性讓大家只要按相應的接口去實現(xiàn),就可以方便的集成起來,不需要太多跨團隊的交互。
2.?如何保證可擴展性
讓軟件系統(tǒng)具有可擴展性,通常并不是什么難事,只要做到下面兩點就可以了:
- 針對接口編程。這個是大家都知道的,在《軟件設計模式》等書里,都反復強調(diào)了,這里不再贅述;利用工廠模式隔離組件的創(chuàng)建。工廠模式也是人人都知道的,而且大家都覺得很"簡單"。但是能把工廠模式用好的程序員其實并不多見,一個主要原因就是很多人只會套用《軟件設計模式》的工廠模式,而《軟件設計模式》里幾個工廠模式在現(xiàn)實中并不實用。利用這些這些工廠模式,無法滿足 SOLID 原則中的開放封閉原則,增加一個新的擴展時,仍然需要修改對應的工廠。
AWPLC?功能塊的接口
要讓 AWPLC 支持擴展各種自定義的功能塊,首要條件條件是定義好功能塊的接口。
1.?功能塊的基類在面向?qū)ο蟮?C 語言編程中,我們用結(jié)構(gòu)?(struct)?來模擬類和接口。這里所說的接口是廣義的接口,而不是 C++或其它語言中只包含純虛函數(shù)的 interface,因為除了虛函數(shù)指針外,這里還有一些數(shù)據(jù)成員。
/**
*?@class?aw_plc_fb_t
* AWPLC 功能塊接口。
*/
struct?_aw_plc_fb_t?{
/**
*?@property?{bool_t}?en
*?是否啟用。
*/
uint8_t?en?:?1;
/**
*?@property?{bool_t}?eno
*?是否啟用輸出。
*/
uint8_t?eno?:?1;
/*private*/
const?aw_plc_fb_vtable_t*?vt;
};
2.?功能塊的虛函數(shù)
在功能塊的虛函數(shù)表中,還定義了一些描述性的常量,讓對象具有一點反射的能力,方便在運行時查詢它的一些狀態(tài)。順便說一下,在定義接口的虛函數(shù)時,通常不會有創(chuàng)建函數(shù),因為創(chuàng)建之前對象之前,是拿不到這個虛表對象的。但也不是絕對的,有時為了方便 clone,也可能提供一個 clone 函數(shù)或者 create 函數(shù)。
任何接口都要定義析構(gòu)函數(shù)?(destroy),在對象需要銷毀時,框架可以以統(tǒng)一的方式銷毀它。
typedef?struct?_aw_plc_fb_vtable_t?{
/*功能塊的類型名*/
const?char*?type;
/*輸入?yún)?shù)名稱列表,以?NULL?結(jié)束的字符串數(shù)組*/
const?char*?const*?ins;
/*輸出參數(shù)名稱列表,以?NULL?結(jié)束的字符串數(shù)組*/
const?char*?const*?outs;
/*輸入輸出參數(shù)名稱列表,以?NULL?結(jié)束的字符串數(shù)組*/
const?char*?const*?in_outs;
/*執(zhí)行函數(shù)*/
aw_plc_fb_exec_t?exec;
/*執(zhí)行函數(shù)(帶參數(shù))*/
aw_plc_fb_exec_ex_t?exec_ex;
/*獲取屬性(輸入輸出參數(shù))的值*/
aw_plc_fb_get_prop_t?get_prop;
/*獲取輸出的值*/
aw_plc_fb_get_output_t?get_output;
/*設置輸出的值*/
aw_plc_fb_set_input_t?set_input;
/*析構(gòu)函數(shù)*/
aw_plc_fb_destroy_t?destroy;
}?aw_plc_fb_vtable_t;
* 這個虛函數(shù)表和 AWTK/TKC 中的 object 虛函數(shù)表很相似,考慮到 object 為了做得通用,有點臃腫了,所以決定重新定義一套。
AWPLC?功能塊的工廠
前面我們說過,可擴展性除了針對接口編程外,離不開工廠模式的支持。功能塊的工廠其任務當然是創(chuàng)建功能塊了,所以提供了一個創(chuàng)建功能塊的函數(shù)。參數(shù) type 指定功能塊的類型,函數(shù)返回對應類型的功能塊:
/**
*?@method?aw_plc_fb_factory_create_fb
*?創(chuàng)建 fb。
*?@param {const char*} type 類型。
*
*?@return {aw_plc_fb_t*}?返回 fb 對象。
*/
aw_plc_fb_t*?aw_plc_fb_factory_create_fb(const?char*?type);
有了這個創(chuàng)建函數(shù),確實把創(chuàng)建任務與功能塊的實現(xiàn)分開了。但是請想一下,如果每次增加新的功能塊,都要修改這個創(chuàng)建函數(shù),而這個函數(shù)又屬于框架的一部分,框架是不是還是依賴于具體實現(xiàn)了呢?為了解決這個問題,我們需要提供一種注冊機制來實現(xiàn)依賴倒置,讓功能塊的實現(xiàn)者主動將創(chuàng)建函數(shù)注冊進來:
/**
*?@method?aw_plc_fb_factory_register
*?注冊創(chuàng)建函數(shù)。
*?@param {const char*} type 類型。
*?@param {aw_plc_fb_create_t} create 創(chuàng)建函數(shù)。
*
*?@return {ret_t}?返回 RET_OK 表示成功,否則表示失敗。
*/
ret_t?aw_plc_fb_factory_register(const?char*?type,?aw_plc_fb_create_t?create);
這種機制非常好用,真正滿足了 SOLID 原則中的開放封閉原則?(OCP):擴展新的功能無需修改框架代碼。在 ZLG 開源 GUI 引擎中,也大量使用了這種帶注冊功能的工廠模式,有興趣的朋友可以去看看 AWTK 的代碼。
ZTIMER
前面我們說過,可擴展性除了針對接口編程外,離不開工廠模式的支持。功能塊的工廠其任務當然是創(chuàng)建功能塊了,所以提供了一個創(chuàng)建功能塊的函數(shù)。參數(shù) type 指定功能塊的類型,函數(shù)返回對應類型的功能塊:
1.?ZTIMER?的結(jié)構(gòu)
在 C 語言中,一般用結(jié)構(gòu)來模擬類,把基類作為結(jié)構(gòu)的第一個成員來模擬繼承。這里必須讓 aw_plc_fb_t 作為 aw_plc_fb_ztimer_t 的第一個成員。
/**
*?@class?aw_plc_fb_ztimer_t
*?@parent?aw_plc_fb_t
*?@annotation?["fb"]
*?循環(huán)定時器。
*
*?>?當輸入 IN 為 TRUE 時,開始計時,輸出 Q 為 FALSE,ET 開始記錄過去的時間。
*?>?定時時間到時,COUNT 增加 1,?輸出 Q 在本次循環(huán)為 TRUE,ET 重置為?0。
*?>?輸入 IN 為 FALSE 時重置定時器。
*/
typedef?struct?_aw_plc_fb_ztimer_t?{
aw_plc_fb_t?fb;
/**
*?@property?{bool_t}?in
*?@annotation?["in"]
*?為 TRUE 開始計時,為 FALSE 時重置定時器。
*/
bool_t?in?:?1;
/**
*?@property?{iec_time_t}?pt
*?@annotation?["in"]
*?預設時間?(ms)。
*/
iec_time_t?pt;
...
}?aw_plc_fb_ztimer_t;
這里的 API 注釋采用了 AWTK 中定義的格式,但是對 annotation 做了一點擴展,增加了 3 個新的取值:
fb 表示這是一個功能塊;
in 表示這是一個輸入?yún)?shù);
out 表示這是一個輸出參數(shù)。
2.?ZTIMER?的實現(xiàn)
每個功能塊必須提供虛函數(shù)表中定義的函數(shù),不過主要代碼集中 exec 函數(shù)里(其它函數(shù)可以自動生成出來):
static?ret_t?aw_plc_fb_ztimer_exec(aw_plc_fb_t*?fb)?{
aw_plc_fb_ztimer_t*?ztimer?=?AW_PLC_FB_ZTIMER(fb);
if?(aw_plc_fb_before_exec(fb)?==?RET_OK)?{
ztimer->current_time?=?aw_plc_now_ms();
if?(ztimer->state?==?0?&&?!ztimer->prev_in?&&?ztimer->in)?{
ztimer->state?=?1;
ztimer->q?=?FALSE;
ztimer->et?=?0;
ztimer->count?=?0;
ztimer->start_time?=?ztimer->current_time;
}?else?{
if?(!ztimer->in)?{
ztimer->q?=?FALSE;
ztimer->state?=?0;
ztimer->et?=?0;
ztimer->count?=?0;
ztimer->start_time?=?ztimer->current_time;
}?else?if?(ztimer->state?==?1)?{
if?((ztimer->start_time?+?ztimer->pt)?<=?ztimer->current_time)?{
ztimer->q?=?TRUE;
ztimer->et?=?0;
ztimer->count++;
ztimer->start_time?=?ztimer->current_time;
}?else?{
ztimer->q?=?FALSE;
ztimer->et?=?ztimer->current_time?-?ztimer->start_time;
}
}
}
ztimer->prev_in?=?ztimer->in;
}
return?RET_OK;
}
3.?注冊?ZTIMER
功能塊需要注冊到前面介紹的功能塊工廠:
aw_plc_fb_factory_register(AW_PLC_FB_TYPE_ZTIMER,?aw_plc_fb_ztimer_create);
坦白的講,本文只是介紹了實現(xiàn)自定義功能塊的關(guān)鍵步驟,實際工作要麻煩很多。如果手工去做這些工作,開發(fā)一個功能塊還覺得好玩,而開發(fā)幾十個甚至幾百個功能塊,人不會變瘋就會變傻。下一篇文章會我們介紹一下,如何用代碼生成器來完成這些單調(diào)的工作,讓開發(fā)自定義功能塊成為一項快樂的工作。
AWPLC 目前還處于開發(fā)階段的早期,寫這個系列文章的目的,除了用來驗證目前所做的工作外,還希望得到大家的指點和反饋。如果您有任何疑問和建議,請在評論區(qū)留言。