詳論單片機固件模塊化架構設計(精華)

[導讀] 爲什麼寫本文?做公號兩月,遇到一些初學單片機的同學,剛剛入手做單片機開發,還沒有涉及到使用RTOS,且剛入手直接上RTOS可能會有些難度,有的使用的相對較老單片機資源還有限,也不適合跑RTOS。或者使用RTOS,在整體思路上比較迷茫,不知從何入手,所以本文來聊聊我對單片機程序的整體框架設計的一些思路體會。

爲啥要討論架構

單片機系統開發人員的目標之一是在編程環境中創建固件,以實現低成本系統、軟件可靠性以及快速的開發迭代時間。 實現這種編程環境的最佳方法實踐是使用統一的固件架構體系結構,該體系結構在產品開發過程中充當框架並支持“固件模塊化”,或稱爲子系統。

如果不採用統一的設計架構,那麼其業務需求耦合關係複雜,不採用先設計-後開發的方法論,想到哪裏寫到哪裏,則程序後期維護將變得異常艱辛,而引入潛在bug/缺陷的風險也將大大增加,且不具備多人協同開發的可能。

可以結合固件模塊化、可測試性和兼容性的正確組合的設計體系架構結構應用於任何固件開發項目,以最大程度地提高代碼可複用性,加快固件調試速度並提高固件可移植性。

模塊化架構設計?

模塊化編程將程序功能分解爲固件模塊/子系統,每個模塊執行一個功能,幷包含完成該功能所需的所有源代碼和變量。
在這裏插入圖片描述

模塊化/子系統化有助於協調團隊中許多人的並行工作,管理項目各個部分之間的相互依賴關係,並使設計人員、系統集成人員能夠以可靠的方式組裝複雜的系統。 具體來說,它可以幫助設計人員實現和管理複雜性。 隨着應用程序的大小和功能的增長,需要模塊化才能將它們分成單獨的部分(無論是作爲“組件”,“模塊”還是“子系統”)。 然後,每個這樣分離的部分就成爲模塊化體系結構的一個元素。 這樣,可以使用定義明確的界面隔離和訪問每個組件。 此外,模塊化編程可提高固件的可讀性,同時簡化固件的調試,測試和維護。

**即便是一個人獨立開發一個項目,這樣做依然在代碼的調試、可讀性、可移植性方面是最佳實踐的整體策略。如果代碼設計良好,則在其他項目可以輕鬆應用。而且模塊經過上一項目的測試驗證,在新的項目中再次應用其缺陷風險將大幅降低。所以每做一個項目,以這種策略不斷積累模塊"輪子"組件,隨着經驗的增長,積累的“輪子”就越來越多,也越來越好。所以其優點是顯而易見的,否則每做一個項目,都從輪子造起,開發時間長不說,開發水平也得不到提高,重複性工作也很枯燥。**比如前文中談到的非易失存儲管理子系統,如設計良好,就變成一個可靠的可移植的輪子。這段話請深入理解,並拿走不謝!

固件模塊原理

固件開發中模塊化編程的基本概念是創建固件模塊。 從概念上講,模塊代表關注點分離。 在計算機科學中,關注點分離是將計算機程序分解爲功能很少重疊的獨特功能的過程。 關注點是程序的任何關注點或功能,並且與功能或行爲同義。關注點分離的發展傳統上是通過模塊化和封裝來實現的,其實也就是解耦思想。

固件模塊可以分爲幾種類型:

  • 與很多上層用戶模塊都有關的代碼被實現爲單獨的固件模塊。 常見的如底層硬件相關的抽象實現。例如,hal_adc.c 是ADC用戶模塊的固件模塊,而hal_timer.c是Timer用戶模塊的固件模塊。
  • 用於特定純軟件算法的代碼被實現爲單獨的固件模塊。 例如,alg_filter.c是執行軟件過濾器(例如中值過濾器,均值過濾器或加權均值過濾器、IIR/FIR濾波)的固件模塊。
  • 特定應用程序的代碼實現爲單獨的固件模塊。 例如,app_battery.c是電池充電器應用程序的固件模塊。
    特定工具的代碼實現爲單獨的固件模塊。 例如,debug_print.c是用於實現日誌打印功能的固件模塊。

實施估計模塊化設計的一些規則:

  • 所有與模塊相關的功能都應集成到單個源文件中,這是高內聚的體現。
  • 模塊對外提供一個頭文件,該文件聲明瞭該模塊的所有資源(硬件依賴/宏/常量/變量/函數)。儘量用struct將緊密相關的變量進行集總封裝。
  • 在源文件中包括自檢代碼部分,以實現該模塊模塊的所有自檢功能。
  • 固件模塊的接口應經過精心設計和定義。
  • 由於固件取決於硬件,因此需要在源文件頭中明確提及硬件的相關性。比如利用宏將硬件依賴轉定義,或者利用函數將基本操作進行封裝。則在新的架構體系,僅僅需要移植這部分實現即可使用。
  • 通常,固件模塊可供其他團隊成員在其他項目中使用。 可能涉及到管理更改,缺陷修復、所有者應維護模塊。 源文件頭應包含“作者”和“版本”信息。
  • 固件在某種程度上取決於編譯器。 源文件頭中應聲明基於什麼開發環境進行過驗證,以指定編譯器或與IDE相關的信息。

需要注意的是,模塊化設計會引入一些調用開銷,也可能增加固件尺寸大小。在實際實現時,折中考量。不要過度模塊化,所以建議採用高內聚、低耦合的實現策略。在前面文章中有談到過的呼吸機PB560的設計,看過其代碼,本打算解讀一下其代碼設計,但讀下來發現,其設計過度模塊化了,沒有實現高內聚的思想。其源代碼很多源文件僅僅實現了一個函數,而不是把一類問題集中抽象實現,後來就放棄了其代碼解讀。

如何拆分模塊?

做工程開發,一定是需求驅動的。第一件事需要對需求有比較清晰的認知,然後才能設計一個比較合理的框架。我們需要實現什麼?大致總體設計過程策略我的基本採用如下圖所示思路(我比較喜歡繪圖,圖會讓人比較直觀)
在這裏插入圖片描述

問自己第一個問題是:這個項目要實現什麼主要功能?這個來自哪裏?如果是實際產品開發,則可能來自市場的需求,如果是自己的DIY項目,也一定會YY出一個大致的想法?總之不管源自何方,需求總要先梳理清楚。那麼需求一般意義上包含哪些呢?

  • 哪些是硬件IO接口需求,比如開關量輸入,ADC採樣,I2C/SPI通信等等
  • 哪些是業務邏輯需求,比如要採集一個傳感器量數據,控制一個加熱裝置,那麼這是高內聚的需求。
  • 哪些是算法相關的技術需求,比如產品中哪些信號需要濾波處理,哪些需要做頻域分析等等。
  • 是否有對外的通信協議需求。
  • 是否有業務數據需要歷史存儲,或者設備參數需要掉電保存
  • 是否需要有日誌打印需求。
  • 不一而足。

結合固件模塊原理以及相關指導原則,那麼將相關性高的需求,抽象實現在一系列的模塊中,在由這一系列模塊配合實現某個相關性高的業務需求,再進一步這些模塊就變成一個子系統。多個子系統在main.c的調度下,協調完成產品的整體功能。

如何集成調度

對於某些不使用RTOS的應用而言,可以使用如下的框架進行:

void main(void)
{
   /*各模塊初始化*/
   init_module_1();
   init_module_2();   
   ....
   while(1)
   {
       /*實現一個定時調度策略*/
       if(timer50ms)
       {
           timer50ms = 0app_module_1();
       }
       if(timer100ms)
       {
           timer100ms = 0app_module_2();
       }
       
       /*異步請求處理,如中斷後臺處理*/
       if(flag1)
       {
           communication_handler();
       }
       .....
   }
}

對於基於RTOS的集成實現舉例:

void task1(void)
{
    /*處理子系統相關的初始化*/
    init_task1();
    while(1)
    {
       /*應用相關調用*/
       task1_mainbody();
       ....
    }
}
....
void taskn(void)
{
    /*處理子系統相關的初始化*/
    init_taskn();
    while(1)
    {
       /*應用相關調用*/
       taskn_mainbody();
       ....
    }
}

void main(void)
{
    /*一些基本硬件相關初始化,比如IO,時鐘,OS tick定時器等*/
    init_hal();
    ......
    
    /*一些基本RTOS初始化*/
    init_os();
    
    /*任務創建*/
    os_creat("task1",task1,棧設置,優先級,...);
    ......
    os_creat("taskn",taskn,棧設置,優先級,...);
    
    /*啓動OS調度器,交由OS調度管理應用任務*/
    os_start();
}

具體不同的RTOS,其函數名各有不同,但大致思路一般都差不多。

總結一下

本文從爲什麼需要模塊化設計整體架構,到這樣做的好處,以及具體做的一些指導原則,再到實際中如何實現,怎麼做到高內聚低耦合,提供了一些個人工作中的體會以及思路。同時對於裸機程序整體框架、基於RTOS的集成框架做了兩個demo,基本能解決大部分的框架思路問題。將前文中的一些個人推崇的原則,在加粗總結下:

  • 所有與模塊相關的功能都應集成到單個源文件中,這是高內聚的體現。
  • 模塊對外提供一個頭文件,該文件聲明瞭該模塊的所有資源(硬件依賴/宏/常量/變量/函數)。儘量用struct將緊密相關的變量進行集總封裝。
  • 在源文件中包括自檢代碼部分,以實現該模塊模塊的所有自檢功能。
  • 固件模塊的接口應經過精心設計和定義。
  • 由於固件取決於硬件,因此需要在源文件頭中明確提及硬件的相關性。比如利用宏將硬件依賴轉定義,或者利用函數將基本操作進行封裝。則在新的架構體系,僅僅需要移植這部分實現即可使用。
  • 通常,固件模塊可供其他團隊成員在其他項目中使用。 可能涉及到管理更改,缺陷修復、所有者應維護模塊。 源文件頭應包含“作者”和“版本”信息。
  • 固件在某種程度上取決於編譯器。 源文件頭中應聲明基於什麼開發環境進行過驗證,以指定編譯器或與IDE相關的信息。

極力建議採用先設計-後開發的模式,忌諱逐步debug,想到哪裏寫到哪裏。當然對於新手學習而言,後一種模式,可以逐步漸進迭代,也可以比較快的增長經驗。當然如何取捨,全憑個人意願。

相信您如深入閱讀,細細體會,應該從設計思想上得到些領悟,有所提高。如果能幫助到您,則我心甚慰,也不枉辛苦碼了這麼多字。
版權聲明:所有文章版權歸嵌入式客棧所有,如商業使用,須嵌入式客棧授權。歡迎關注微信公衆號,內容更豐富。
在這裏插入圖片描述

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章