简单嵌入式系统软件架构

本文为原创,以下链接有比较及时的更新:
https://www.yuque.com/docs/share/334f4a3d-2974-49db-8f68-4db6601a0d21?# 《简单嵌入式系统》

引言

本文描述的内容,适用范围是简单嵌入式系统。举一些可能不恰当的例子,如手环、蓝牙温湿度传感器、小家电这一类产品的软件复杂程度,在我看来,就是一个简单嵌入式系统可把控的。

基于此,提到简单嵌入式系统的软件架构,我脑海中立马浮现这样的画面:

简单嵌入式系统软件架构_第1张图片

看到这张图,不同的人,可能会有不同的感受:有的高手能一眼看破,能马上进行万千补充、引申;有的会心领神会,从而期待后面的内容;而有的,可能会一头雾水,或懵懵懂懂。

就本人而言,我当前的技术水平是能用代码将这张图构建的相对稳定、完整;期望有一天,我(或我们)能站在万米高空俯视这张图,一眼看破框图背后的种种玄机,轻松写意地构建出一个个优雅的嵌入式系统软件。

回归正题,为什么要进行以上框图所示的层次划分?我是这么考虑的:

关于硬件层:

一般会说,设计该层次的目的在于封装掉硬件的细节,使在其上层的软件具备跨平台的移植性。不过在我看来,想要做到这点其实非常困难。就本人所在领域(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 
#include 

#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,这会导致层次内模块和模块之间产生了依赖,如图:

简单嵌入式系统软件架构_第2张图片

同层次,模块和模块之间不要产生依赖,这个可以理解,但上面这个图,好像没什么问题啊,我们可以把硬件层分成两个层次:片内外设和片外外设?

简单嵌入式系统软件架构_第3张图片

是可以的。

但这里还有另外一个思路:在同一个层次内,我们仍然可以区分片内外设和片外外设,但可以通过一些 C 语言技巧来让他们不产生依赖,从而可以不用再分出一个层次。还是以按键功能为例,最终我们要实现的效果的框图是:

简单嵌入式系统软件架构_第4张图片

以上四个软件模块,可以假设其功能为:

button:和系统的消息总线对接、根据底层的按键事件,向系统发送按键消息。

drv_button:实现按键的触发、消抖、短按、长按等逻辑,并在各个节点产生回调。

xxx_io:配置寄存器,提供 IO 口的读、写、中断配置等操作

xxx_timer:配置寄存器,提供 timer 寄存器的配置等操作

把按键逻辑抽象出来,做到不对 io 和 timer 产生依赖,有一个好处:为上层的 button 组件兼容各种按键类型提供便利。button 组件可以随意地组合 io, timer, 各种按键逻辑(IO 按键,触摸按键,矩阵键盘等),统一进行管理。

另外,从硬件层的角度,在编写 drv_button 的时候,也不需要为未来可能的应用过多的考虑,在接口的灵活性设计上花费精力。

综上,对于下面这两种层次架构:

简单嵌入式系统软件架构_第5张图片

其优缺点,我的理解是:

  • 前者的 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” 两种按键驱动的需求,对于按键功能的实现,有如下设计:

简单嵌入式系统软件架构_第6张图片

见上图,增加了 “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

关于组件层:

组件,顾名思义、组成部件。理想状态下,更换某个组件,应该像更换一个积木一样直观、简单;但现实往往是,需要像治疗癌症一样,清除掉一个癌细胞,损伤一片好的细胞。组件接口的设计是一门艺术,大神抽象出来的组件接口令人叹为观止。而有的人嘛,一个组件就是一个项目……

关于组件接口的设计,我的思考是:

你可能感兴趣的:(软件设计,软件架构,嵌入式)