本文為原創,以下連結有比較及時的更新:
https://www.yuque.com/docs/share/334f4a3d-2974-49db-8f68-4db6601a0d21?# 《簡單嵌入式系統》
引言
本文描述的内容,适用範圍是簡單嵌入式系統。舉一些可能不恰當的例子,如手環、藍牙溫濕度傳感器、小家電這一類産品的軟體複雜程度,在我看來,就是一個簡單嵌入式系統可把控的。
基于此,提到簡單嵌入式系統的軟體架構,我腦海中立馬浮現這樣的畫面:

看到這張圖,不同的人,可能會有不同的感受:有的高手能一眼看破,能馬上進行萬千補充、引申;有的會心領神會,進而期待後面的内容;而有的,可能會一頭霧水,或懵懵懂懂。
就本人而言,我目前的技術水準是能用代碼将這張圖建構的相對穩定、完整;期望有一天,我(或我們)能站在萬米高空俯視這張圖,一眼看破框圖背後的種種玄機,輕松寫意地建構出一個個優雅的嵌入式系統軟體。
回歸正題,為什麼要進行以上框圖所示的層次劃分?我是這麼考慮的:
關于硬體層:
一般會說,設計該層次的目的在于封裝掉硬體的細節,使在其上層的軟體具備跨平台的移植性。不過在我看來,想要做到這點其實非常困難。就本人所在領域(BLE晶片)的開發而言,固件工程師一般都會在晶片廠商提供的 SDK 的基礎上進行開發。換晶片、換 SDK,幾乎不可能隻靠修改硬體層就能完成适配。
是以,在我看來,設計該層次的主要目的是為了友善維護,統一管理。通過對硬體相關的軟體子產品進行一定的抽象,我們能找到他們之間的一些共性:
比如,對硬體的操作,一般都會有一個初始化、工作、解初始化流程 。是以,接口設計上,會有:
void xxx_init(param);
void xxx_start(param);
void xxx_pause(param);
void xxx_deinit(param);
或者:
void xxx_init(param);
void xxx_send(param);
void xxx_recv_cb_register(param);
void xxx_deinit(param);
又比如,将片内外設的 IO 接口、外設配置單獨列在一個頭檔案中,友善進行統一的 IO 口、外設的适配:
#ifndef __XXX_COMMON_H__
#define __XXX_COMMON_H__
#include <stdint.h>
#include <stdbool.h>
#include "iic.h"
#include "spi.h"
/*****************************************
iic configuration
*****************************************/
#define XXX_IIC_IO_SDA_PORT
#define XXX_IIC_IO_SDA_PIN
#define XXX_IIC_MODE
/*****************************************
spi configuation
*****************************************/
#define XXX_SPI_IO_CS_PORT
#define XXX_SPI_IO_CS_PIN
#define XXX_SPI_MODE
#endif // __XXX_COMMON_H__
該層次各個子產品的職責範圍,應隻是進行單純的硬體操作。有多純?比如,對于 IO 口按鍵,在該層次,IO 口子產品應隻提供 IO 口的初始化、讀寫、中斷配置等功能,而不應該提供比如短按、長按、輕按兩下等動作觸發功能。因為按鍵長按等動作的實作,需要調用兩個該層次的軟體子產品:io_driver 和 timer_driver,這會導緻層次内子產品和子產品之間産生了依賴,如圖:
同層次,子產品和子產品之間不要産生依賴,這個可以了解,但上面這個圖,好像沒什麼問題啊,我們可以把硬體層分成兩個層次:片内外設和片外外設?
是可以的。
但這裡還有另外一個思路:在同一個層次内,我們仍然可以區分片内外設和片外外設,但可以通過一些 C 語言技巧來讓他們不産生依賴,進而可以不用再分出一個層次。還是以按鍵功能為例,最終我們要實作的效果的框圖是:
以上四個軟體子產品,可以假設其功能為:
button:和系統的消息總線對接、根據底層的按鍵事件,向系統發送按鍵消息。
drv_button:實作按鍵的觸發、消抖、短按、長按等邏輯,并在各個節點産生回調。
xxx_io:配置寄存器,提供 IO 口的讀、寫、中斷配置等操作
xxx_timer:配置寄存器,提供 timer 寄存器的配置等操作
把按鍵邏輯抽象出來,做到不對 io 和 timer 産生依賴,有一個好處:為上層的 button 元件相容各種按鍵類型提供便利。button 元件可以随意地組合 io, timer, 各種按鍵邏輯(IO 按鍵,觸摸按鍵,矩陣鍵盤等),統一進行管理。
另外,從硬體層的角度,在編寫 drv_button 的時候,也不需要為未來可能的應用過多的考慮,在接口的靈活性設計上花費精力。
綜上,對于下面這兩種層次架構:
其優缺點,我的了解是:
- 前者的 drv_button 的應用比較難。因為需要 button 元件自己、結合 timer 和 io 來實作完整的功能。後者的 drv_button 功能相對完善
- 前者相對比較靈活。button 元件可以相對随意地組合 drv_button, io, timer。後者在某些場景下,可能會導緻重寫 drv_button。舉個例子,drv_button_1 用到了 io1, drv_button_2 也用到了 io1, 這時候,編寫 drv_button 的工程師就得考慮 io 1 被複用的場景,否則會導緻 drv_button 需要重寫。
- 前者的層次架構簡單
簡而言之,前者在應對未來的需求改動上,比較有優勢;後者适用于需求比較固定的場景,它的實作相對簡單且符合直覺(一般人自然而然就會這麼寫)。
兩者的示例代碼如下:
。。。
好吧,第一個圖檔裡的架構有點寫不出來。。。感覺這裡有點過度設計了。
寫不出來的原因主要是:在不直接引用 xxx_timer 的情況下,比較難抽象出 drv_io_button/drv_matrix_button 子產品的邏輯;因為 drv_xxx_button 的實作和和 xxx_timer 子產品息息相關(也和 xxx_io 子產品的使用關聯性比較大),上層的 button 子產品需要了解比較多 drv_xxx_button 子產品的内在邏輯才能比較好的應用它們;這給上層子產品造成了過大的負擔。并且,在這種架構下的 button 子產品的實作,會随着需求的增加而變得過于龐大和複雜。
其實,上文中對于“片内外設”層,和“片内外設”層的概念的抽象,對于簡單嵌入式系統來說也是一個沒有必要的行為。
行文至此,我應該把前面的内容删除部分的,不過也可以保留着作為一種思路曆程的記錄。
經過反思,由于有 “drv_io_button” 和 “drv_matrix_button” 兩種按鍵驅動的需求,對于按鍵功能的實作,有如下設計:
見上圖,增加了 “bsp_button” 子產品,用于抽象出硬體驅動的一些共性,屏蔽底層硬體實作的細節,進而為上層提供一個簡單應用的接口(外觀模式)。
反思後的架構示例代碼如下:
硬體層實作示例代碼
bsp_button
該子產品主要為了封裝低層的硬體實作細節,向上層提供一個簡單的接口。
采用自頂向下的設計思想,先把接口寫出來,然後再根據這個接口來寫低層的實作代碼。
接口有:
/* bsp_button.h */
#define BSP_EVT_ID_BTN_BASE 0x0100
enum bsp_evt_id_btn_e
{
BSP_EVT_ID_BTN_INVALID = BSP_EVT_ID_BTN_BASE,
BSP_EVT_ID_BTN_SHORT,
BSP_EVT_ID_BTN_DOUBLE,
BSP_EVT_ID_BTN_LONG,
BSP_EVT_ID_BTN_CONTINUE,
};
typedef struct bsp_btn_evt_param_s
{
uint8_t type; // 0:io button, 1: matrix button
uint8_t state; // 0: released, 1: pressed
uint16_t number; // sequence number of the buttons
} bsp_btn_evt_param_t;
typedef struct bsp_btn_evt_s
{
uint16_t evt_id; // bsp_evt_id_btn_e
bsp_btn_evt_param_t* p_evt_param; // bsp_btn_evt_param_t
} bsp_btn_evt_t;
typedef void (*bsp_btn_evt_hanlder_t)(bsp_btn_evt_t);
uint8_t bsp_button_init(bsp_btn_evt_hanlder_t);
uint8_t bsp_button_deinit(void);
uint8_t bsp_button_enable(void);
uint8_t bsp_button_disable(void);
此處采用“事件回調”的方式來和上層對接,上層隻需要注冊一個回調函數給到該子產品,便可“坐等”各種事件的通知,然後再根據各種事件做相應的處理。
有一些細節、思路:
一、以下關于事件的定義
事件的參數使用的是 void * 類型,主要考慮到之後的擴充性。
typedef struct bsp_btn_evt_s
{
uint16_t evt_id; // bsp_evt_id_btn_e
void* p_evt_param; // bsp_btn_evt_param_t
} bsp_btn_evt_t;
目前該接口不能支援兩個或以上按鍵同時按下的情況;
但當未來有這種需求時,可再增加一些按鍵事件 ID,并另外定義一種事件參數類型來添加。
二、關于 BSP_EVT_ID_BTN_BASE
當 BSP 子產品增多(如 bsp_spi, bsp_iic)時,可通過不同的 BASE 來區分不同子產品并在考慮增加一個 bsp_config.h 檔案來統一定義、管理
三、bsp_btn_evt_param_t 中的 number 參數
用于辨別不同的硬體按鍵。是 bsp_button 這個子產品的核心!
從宏觀上來觀察這個子產品,它完成了具體的“硬體按鍵”到抽象的“按鍵事件”的映射;
上層在應用這個子產品的時候,可以再把“按鍵事件”映射為“系統消息”(也是上層(app_button 元件)的核心功能);
以此形成層層映射,由底至上搭建成套的基于“事件回調”機制的軟體架構。
drv_io_button
關于元件層:
元件,顧名思義、組成部件。理想狀态下,更換某個元件,應該像更換一個積木一樣直覺、簡單;但現實往往是,需要像治療癌症一樣,清除掉一個癌細胞,損傷一片好的細胞。元件接口的設計是一門藝術,大神抽象出來的元件接密碼人歎為觀止。而有的人嘛,一個元件就是一個項目……
關于元件接口的設計,我的思考是: