嵌入式软件的模块化与分层方法

嵌入式软件一般按三层式架构进行设计.

分别是驱动层,业务逻辑层,应用层;

软件模块的设计遵循【低耦合高内聚】原则,有清晰的内外边界。

只要使用了另一个模块提供的函数、变量、宏、就是耦合;耦合越少,查问题时,就没有那么多藤可以顺藤摸瓜,所谓低耦合;

模块实现功能所需的东西,依赖关系、驱动调用、参数引用,越完整,出问题就越不需要向外部寻找原因,所谓高内聚;

所谓边界,就是区分里面和外面的里面和外面交换东西,必须要通过特定的门窗,而不是凿墙

.h文件里声明的extern函数、extern变量,定义的类型、宏;就是连同内外的门窗;

.c文件里定义的static函数、static变量,定义的类型、宏;就是被墙隔开的内容。

驱动层

每个驱动模块与一个具体的硬件模块或外设唯一关联;如:

  • ESP32提供的外设驱动库
  • ws2812
  • hx711
  • icmxxx
  • …各种自编或第三方驱动;

一般驱动不进行嵌套,比如在icmxxx.c内独立实现一个I2C模拟驱动。

业务逻辑(中间件)层

中间件是实现具体逻辑功能的模块,一个中间件模块以逻辑功能命名;

一个中间件模块要能够完整地实现该名称所表达的功能,如:

  • Logger, 实现数据记录相关的所有功能,像是文件读写、文件编号自增、缓存、启动、停止、调速、等等;
  • UAVCAN, 实现UAVCAN协议所需的所有功能,像是协议的数据流输入、处理、结果输出、回调、等等;

所谓边界,就是区分里面和外面的墙;里面和外面交换东西,必须要通过特定的门窗,而不是凿墙

比如Logger需要强制指定log文件名的功能,应当添加一个设置文件名的函数接口;

而不是:定义一个全局文件名变量、然后在其他模块中修改变量值。

通过对模块内和外的明确区分,可以让软件模块各司其职;

发现了bug,可以快速地定位到问题可能出现的范围。

中间件移植接口

每个中间件都需要调用驱动程序来实现功能,而不同平台下的驱动,或者系统调用可能是不同的。

这就是多态发挥作用的位置。

为了便于app在不同平台之间的移植,或者兼容不同的硬件模,在设计中间件模块时,应预留移植接口;

一般方法是定义一组移植接口函数、可以是:

  • 中间件.c内的extern函数定义;
  • 中间件_port.h内的extern函数定义;

更复杂的模块可以对驱动进行抽象后形成对象结构体,由其它中间件模块进行运行时动态注册;

根据模块的复杂度和需求,选择合适的接口设计。

应用层(表现层)

应用层的模块,通过整合中间件提供的功能,形成用户可见的软件行为;

如:主状态切换模式造成指示灯改变颜色、而应用程序内部则完成各种可见或不可见的软件步骤:

  1. 向储能模块的发送开关命令
  2. 向电源模块发送UPS开关命令
  3. 向算法发送模式切换命令
  4. 向Logger模块发送变速命令
  5. 向传感器模块发送复位命令
  6. …等等

经过一系列步骤,软件完成了作为一个产品的功能;

而这些步骤,发生在应用层。

模块、层间依赖关系设计

单模块遵循的低耦合高内聚的设计原则,同样适用与软件层次之间。

同一层的模块间耦合关系应保持单向依赖,使得模块依赖树形成有向无环图,以维持程序结构的整洁;

必要情况下,驱动模块之间的双向依赖可以存在,但要尽可能避免,并对相应的依赖关系作出特别注明,如用于优化运行效率的跨模块变量,某些未经过仔细考虑的历史代码引入的双向文件包含等。

不同层模块之间,只允许单向依赖,不允许低层次模块包含上层模块;

应用层模块不允许跨越中间件直接调用驱动层模块的任何资源。

合理的层内依赖关系:

  1. 传感器驱动 依赖 I2C驱动 依赖 GPIO驱动 依赖 ESP内核库;
  2. FLASH驱动 依赖 SPI驱动 依赖 GPIO驱动 依赖 ESP内核库;
  3. Logger中间件 依赖 Vehicle中间件 依赖 PSDK库;
  4. Sensor中间件 读取 IMU中间件 和 BARO中间件 的全局状态标志;

不可接受的隔层依赖:

  1. app.c 调用 iic.c 中的 iic_init()
  2. mainfsm 读取 ws2812驱动 中的状态变量

不可接受的跨层反向依赖:

  1. driver_spi.c包含sensor.h
  2. Sensor中间件 调用 app.c 里的函数
  3. Charger驱动 调用 mainfsm.c 的函数

已发布

分类

来自

标签: