天天看點

簡單嵌入式系統軟體架構

本文為原創,以下連結有比較及時的更新:

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

關于元件層:

元件,顧名思義、組成部件。理想狀态下,更換某個元件,應該像更換一個積木一樣直覺、簡單;但現實往往是,需要像治療癌症一樣,清除掉一個癌細胞,損傷一片好的細胞。元件接口的設計是一門藝術,大神抽象出來的元件接密碼人歎為觀止。而有的人嘛,一個元件就是一個項目……

關于元件接口的設計,我的思考是: