本文为原创,以下链接有比较及时的更新:
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
#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,这会导致层次内模块和模块之间产生了依赖,如图:
同层次,模块和模块之间不要产生依赖,这个可以理解,但上面这个图,好像没什么问题啊,我们可以把硬件层分成两个层次:片内外设和片外外设?
是可以的。
但这里还有另外一个思路:在同一个层次内,我们仍然可以区分片内外设和片外外设,但可以通过一些 C 语言技巧来让他们不产生依赖,从而可以不用再分出一个层次。还是以按键功能为例,最终我们要实现的效果的框图是:
以上四个软件模块,可以假设其功能为:
button:和系统的消息总线对接、根据底层的按键事件,向系统发送按键消息。
drv_button:实现按键的触发、消抖、短按、长按等逻辑,并在各个节点产生回调。
xxx_io:配置寄存器,提供 IO 口的读、写、中断配置等操作
xxx_timer:配置寄存器,提供 timer 寄存器的配置等操作
把按键逻辑抽象出来,做到不对 io 和 timer 产生依赖,有一个好处:为上层的 button 组件兼容各种按键类型提供便利。button 组件可以随意地组合 io, timer, 各种按键逻辑(IO 按键,触摸按键,矩阵键盘等),统一进行管理。
另外,从硬件层的角度,在编写 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
关于组件层:
组件,顾名思义、组成部件。理想状态下,更换某个组件,应该像更换一个积木一样直观、简单;但现实往往是,需要像治疗癌症一样,清除掉一个癌细胞,损伤一片好的细胞。组件接口的设计是一门艺术,大神抽象出来的组件接口令人叹为观止。而有的人嘛,一个组件就是一个项目……
关于组件接口的设计,我的思考是: