天天看點

用 AWTK 和 AWPLC 快速開發嵌入式應用程式 (4)- 自定義功能塊(上)

AWPLC 目前還處于開發階段的早期,寫這個系列文章的目的,除了用來驗證目前所做的工作外,還希望得到大家的指點和回報。如果您有任何疑問和建議,請在評論區留言。

1. 背景

AWTK 全稱 Toolkit AnyWhere,是 ZLG 開發的開源 GUI 引擎,旨在為嵌入式系統、WEB、各種小程式、手機和 PC 打造的通用 GUI 引擎,為使用者提供一個功能強大、高效可靠、簡單易用、可輕松做出炫酷效果的 GUI 引擎

AWPLC 是 ZLG 自主研發的 PLC 系統(相容 IEC61131-3),其中 AWPLC 的運作時庫 (Runtime) 基于 ZLG TKC 開發,可以移植到到任何主流 RTOS 和 嵌入式系統。AWPLC 的內建開發環境 (IDE) 基于 AWTK 開發,可以運作在 Windows、MacOS 和 Linux 系統之上。AWPLC 的主要目标之一是把 PLC 中 低代碼開發方法 引入到嵌入式軟體,進而提高嵌入式軟體的開發效率和可靠性。

2. 簡介

在前一篇文章中,我們說過,AWPLC 的重要特色之一就是高度可擴充,而且會内置 ZLG 多年在嵌入式系統開發中積累的功能塊,包括各種算法、協定和實用功能,這将大大簡化嵌入式軟體的開發。

那怎麼去開發自定義的功能塊呢?本文以 ZTIMER 為例介紹一下開發自定義功能塊的方法。ZTIMER 是一個帶計數功能的定時器,在前一篇文章中,我們用它實作了一個走馬燈的示範,其使用方法如下:

用 AWTK 和 AWPLC 快速開發嵌入式應用程式 (4)- 自定義功能塊(上)

編輯切換為居中

添加圖檔注釋,不超過 140 字(可選)

在 AWPLC 中,自定義功能塊和内置功能塊具有同等待遇,因為它們都是按同樣的方式加入進來的。在進入正題前,我們先聊一下,系統的可擴充性以及實作方法。

2.1 可擴充性的好處

在設計一個複雜軟體的架構時,可擴充性是必須考慮的因素。可擴充性至少帶來以下幾個好處:

  • 可擴充性将軟體的架構與具體的實作分離開來,有助于降低系統的複雜度。系統的複雜性太高,會帶來一系列的問題,比如讓可了解性、可維護性和可靠性的降低,很多項目是以陷入無法掙脫的焦油坑裡,最後士氣低落,人員流失,項目取消,公司蒙受巨大損失。在設計複雜軟體時,一定要存有敬畏之心。
  • 可擴充性将軟體變化的部分隔離開來,不但可以讓擴充的功能獨立變化,也可以友善的擴充新功能。在 AWPLC 中, 以後會擴充各種協定和算法的功能塊,必須保證 AWPLC 架構和這些擴充的功能塊是獨立的,才能讓開發工作順利進行。
  • 可擴充性有利于團隊的協作。 不同的通訊協定和算法,需要不同團隊的專家去開發,可擴充性讓大家隻要按相應的接口去實作,就可以友善的內建起來,不需要太多跨團隊的互動。

2.2 如何保證可擴充性

讓軟體系統具有可擴充性,通常并不是什麼難事,隻要做到下面兩點就可以了:

  • 針對接口程式設計。這個是大家都知道的,在《軟體設計模式》等書裡,都反複強調了,這裡不再贅述。
  • 利用工廠模式隔離元件的建立。工廠模式也是人人都知道的,而且大家都覺得很"簡單"。但是能把工廠模式用好的程式員其實并不多見,一個主要原因就是很多人隻會套用《軟體設計模式》的工廠模式,而《軟體設計模式》裡幾個工廠模式在現實中并不實用。利用這些這些工廠模式,無法滿足 SOLID 原則中的開放封閉原則,增加一個新的擴充時,仍然需要修改對應的工廠。

3. AWPLC 功能塊的接口

要讓 AWPLC 支援擴充各種自定義的功能塊,首要條件條件是定義好功能塊的接口。

3.1 功能塊的基類

在面向對象的 C 語言程式設計中,我們用結構 (struct) 來模拟類和接口。這裡所說的接口是廣義的接口,而不是 C++或其它語言中隻包含純虛函數的 interface,因為除了虛函數指針外,這裡還有一些資料成員。

/**
 * @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; 
};

           

3.2 功能塊的虛函數

在功能塊的虛函數表中,還定義了一些描述性的常量,讓對象具有一點反射的能力,友善在運作時查詢它的一些狀态。順便說一下,在定義接口的虛函數時,通常不會有建立函數,因為建立之前對象之前,是拿不到這個虛表對象的。但也不是絕對的,有時為了友善 clone,也可能提供一個 clone 函數或者 create 函數。

任何接口都要定義析構函數 (destroy),在對象需要銷毀時,架構可以以統一的方式銷毀它。

typedef struct _aw_plc_fb_vtable_t {
  /*功能塊的類型名*/
  const char* type;
  /*輸入參數名稱清單,以 NULL 結束的字元串數組*/
  const char* const* ins;
  /*輸出參數名稱清單,以 NULL 結束的字元串數組*/
  const char* const* outs;
  /*輸入輸出參數名稱清單,以 NULL 結束的字元串數組*/
  const char* const* in_outs;
  /*執行函數*/
  aw_plc_fb_exec_t exec;
  /*執行函數(帶參數)*/
  aw_plc_fb_exec_ex_t exec_ex;
  /*擷取屬性(輸入輸出參數)的值*/
  aw_plc_fb_get_prop_t get_prop;
  /*擷取輸出的值*/
  aw_plc_fb_get_output_t get_output;
  /*設定輸出的值*/
  aw_plc_fb_set_input_t set_input;
  /*析構函數*/
  aw_plc_fb_destroy_t destroy;
} aw_plc_fb_vtable_t;

           
這個虛函數表和 AWTK/TKC 中的 object 虛函數表很相似,考慮到 object 為了做得通用,有點臃腫了,是以決定重新定義一套。

4. AWPLC 功能塊的工廠

前面我們說過,可擴充性除了針對接口程式設計外,離不開工廠模式的支援。功能塊的工廠其任務當然是建立功能塊了,是以提供了一個建立功能塊的函數。參數 type 指定功能塊的類型,函數傳回對應類型的功能塊:

/**
 * @method aw_plc_fb_factory_create_fb
 * 建立 fb。
 * @param {const char*} type 類型。
 *
 * @return {aw_plc_fb_t*} 傳回 fb 對象。
 */
aw_plc_fb_t* aw_plc_fb_factory_create_fb(const char* type);

           

有了這個建立函數,确實把建立任務與功能塊的實作分開了。但是請想一下,如果每次增加新的功能塊,都要修改這個建立函數,而這個函數又屬于架構的一部分,架構是不是還是依賴于具體實作了呢?為了解決這個問題,我們需要提供一種注冊機制來實作依賴倒置,讓功能塊的實作者主動将建立函數注冊進來:

/**
 * @method aw_plc_fb_factory_register
 * 注冊建立函數。
 * @param {const char*} type 類型。
 * @param {aw_plc_fb_create_t} create 建立函數。
 *
 * @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 的代碼。

5. ZTIMER

5.1 ZTIMER 的結構

在 C 語言中,一般用結構來模拟類,把基類作為結構的第一個成員來模拟繼承。這裡必須讓 aw_plc_fb_t 作為 aw_plc_fb_ztimer_t 的第一個成員。

/**
 * @class aw_plc_fb_ztimer_t
 * @parent aw_plc_fb_t
 * @annotation ["fb"]
 * 循環定時器。
 * 
 * > 當輸入 IN 為 TRUE 時,開始計時,輸出 Q 為 FALSE,ET 開始記錄過去的時間。
 * > 定時時間到時,COUNT 增加 1, 輸出 Q 在本次循環為 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 表示這是一個輸入參數。
  • out 表示這是一個輸出參數。

5.2 ZTIMER 的實作

每個功能塊必須提供虛函數表中定義的函數,不過主要代碼集中 exec 函數裡(其它函數可以自動生成出來):

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;
}

           

5.3 注冊 ZTIMER

功能塊需要注冊到前面介紹的功能塊工廠:

aw_plc_fb_factory_register(AW_PLC_FB_TYPE_ZTIMER, aw_plc_fb_ztimer_create);
            

坦白的講,本文隻是介紹了實作自定義功能塊的關鍵步驟,實際工作要麻煩很多。如果手工去做這些工作,開發一個功能塊還覺得好玩,而開發幾十個甚至幾百個功能塊,人不會變瘋就會變傻。下一篇文章會我們介紹一下,如何用代碼生成器來完成這些單調的工作,讓開發自定義功能塊成為一項快樂的工作。

繼續閱讀