我是老温,一名热爱学习的嵌入式工程师8 u8 T9 o3 M4 ^: A* K* x
关注我,一起变得更加优秀!
1 Y: u: [, ?% \工程师在进行嵌入式软件架构设计的时候,很多时候需要在灵活性与资源约束之间进行权衡。% F; V3 N6 d% u! V$ h
分层与封装虽然能提升可维护性和移植性,但过度设计容易引发性能损耗与开发效率下降。
& N8 \; H" w: ?0 G8 G本文尝试讨论,在嵌入式软件框架设计时,应如何避免框架过度分层和封装设计,以便让开发者在资源、性能与维护成本之间找到最佳的平衡点。/ w3 u4 B' {7 I# |6 h
pxk1vzixkxt64016294011.jpg
% x" S2 K) j* |& \一、嵌入式软件分层和封装接口的本质。, ?3 p5 X2 S5 b& }( @0 s
分层软件框架的工程学定义,单片机典型的四层框架设计示例。Application Layer // 业务逻辑实现//-------------------------------------Middleware Layer // 协议栈/算法库//-------------------------------------Driver Layer // 外设驱动封装//-------------------------------------Hardware Layer // 寄存器级操作每个层级通过明确定义的接口进行通信,上层模块仅能调用下层提供的接口,禁止跨层访问。
" L+ ]* e+ X8 A0 g这种设计将硬件操作与业务逻辑解耦,例如STM32的HAL库就将寄存器操作抽象为统一API。
$ z. r4 F" B3 N! j; D函数接口封装的技术内涵是,接口封装通过信息隐藏(Information Hiding)实现模块隔离,其核心要素包括:接口的访问权限控制、参数校验机制、运行错误的处理策略、不同软件版本的兼容性设计。, {+ A0 C _% w( y! \- _/ _5 ]! D
标准的驱动接口封装示例如下:, t! S/ ^1 R8 l, \& E
// SPI控制器接口定义typedef struct { int (*init)(uint32_t freq); int (*transfer)(uint8_t *tx_buf, uint8_t *rx_buf, size_t len); int (*deinit)(void);} spi_controller_t;二、软件分层和封装接口的设计必要性1 e4 C4 S+ z* j5 K6 W
在大型的嵌入式软件系统开发中,分层软件框架带来的工程价值,主要体现在:提升可维护性、增强复用性、便于协作开发、降低移植成本。0 }0 g @3 F7 `2 J8 B. @9 j- Y/ }
举个例子,通过封装Modbus协议接口,可以实现协议栈和物理层(RS485/CAN总线)的解耦,在通信介质改变时,能节省了80%的接口调试时间。// Modbus接口抽象typedef struct { int (*read_holding_reg)(uint8_t addr, uint16_t reg, uint16_t *data); int (*write_single_reg)(uint8_t addr, uint16_t reg, uint16_t value);} modbus_iface_t;三、经典分层架构的设计范式
6 K" Q' z. c3 C1 t3 w硬件抽象层(HAL)模式,以ARM架构的 mbed OS 架构为例,其HAL设计规范可以使同一应用代码运行在不同厂商的Cortex-M芯片上,设计如下:┌─────────────┐│ Application │└──────┬──────┘ ▼┌─────────────┐│ mbed API │└──────┬──────┘ ▼┌─────────────┐│ Chip Vendor ││ HAL Lib │└──────┬──────┘ ▼┌─────────────┐│ 寄存器操作 │└─────────────┘操作系统抽象层(OSAL)模式,采用适配器模式,可以使业务代码不依赖特定的RTOS内核,以便在不同的RTOS之间进行迁移。// 线程接口抽象typedef struct { void* (*create)(task_func_t func, const char *name, \ uint32_t stack_size, void *param); void (*delete)(void* handle);} os_thread_t;关于嵌入式软件的设计模式,可以回顾公众号以前的文章,点击->:嵌入式 C 语言设计模式
9 B* @# S' d* g0 w/ k( \; a o# b四、软件分层和接口封装的使用场景
, F4 v2 i( O$ u& u一般情况下,必须采用软件分层设计的场景,有以下这些:需支持多硬件平台、复杂的协议栈集成、长期的维护工作、大型开发团队协作。
5 m4 C0 a9 o& i* [! a而无需严格进行软件分层的场景,主要有:资源受限型的单片机、原型功能验证、高实时性的应用、短周期交付的项目。
/ G. H* d3 ]3 V$ X$ T8 z! C如果出现以下特征,则可能警示着接口过度设计,比如:内存占用超标、执行时间出现劣化、工程师开发效率下降,等等。" w0 g! z" W! q8 A! g8 V+ \
需要警惕如下的接口嵌套陷阱。// 过度封装示例result = hal_spi->controller->channel[0]->transfer(...);五、如何避免过度分层与封装设计
4 y$ x( t: d, P. p! \: T采用分层粒度控制策略,使用环形依赖检测工具(比如Lattix DSM工具)分析模块的耦合度,让软件架构满足以下原则:--单向依赖原则:禁止出现循环依赖。--接口精简原则:每个模块暴露的API不超过7个。--层级深度约束原则:API封装调用不超过5层。
6 @: g7 u6 Y4 w在实时数据采集系统里面,可以采用“条件编译分层”技术,利用编译器处理一些额外的工作,可以让嵌入式系统达到较优的实时性。// 性能关键路径消除抽象#ifdef OPTIMIZE_PERFORMANCE #define SPI_TRANSFER(data) REG_SPI_DR = (data)#else #define SPI_TRANSFER(data) hal_spi_transfer(data)#endif良好的函数封装接口设计,应该满足以下原则:--参数正交:修改某一参数时,不影响其他功能。--耗时确定:接口调用的耗时可以基本确定。--异常隔离:分层间的异常不会导致其他层崩溃。, Y. w f2 c' e5 N1 \. W# o9 x) i9 E1 w
六、总结
1 l! d4 }+ b8 c嵌入式软件虽然没有架构师相关的岗位,但优秀的嵌入式软件工程师,在规划和设计整个嵌入式系统的功能的时候,应当在可维护性和性能之间、在抽象成本和开发效率之间,寻找一个最佳的平衡点。9 C9 V/ o9 O7 Y
结合以往的项目生命周期、团队经验规模、硬件平台约束等参数,通过建立一个量化的评估体系,以便可以动态调整嵌入式系统的分层与封装策略。6 t( E; X5 X8 @% N; B6 A, R/ M
谨记一点:没有完美的分层架构,只有适合当前上下文的设计。0 k3 t% a+ N4 i: }9 ^
-END-
7 [( @9 m& R7 |往期推荐:点击图片即可跳转阅读
?4 }4 L- i8 v) c; O/ T# y
hcooxdyk2s164016294111.jpg
) B0 \( D) _& N- o
掌握这5个代码小技巧,让嵌入式软件调试更加高效!
8 Q Z: e8 `% F
ynye410voln64016294211.jpg
6 [8 y0 d M: G6 r* }5 D' A! |嵌入式Linux工业网关设计,离不开这个关键核心通信模块。
2 ?3 x9 S3 i/ G& D
froiyzgjnuf64016294311.jpg
9 h5 A: v" X, O7 Z嵌入式软硬件开发,工程师在预研阶段就要开始考虑如何降本增效!# a% K' N' d% c' e% n2 f& {( S% @
我是老温,一名热爱学习的嵌入式工程师
9 o H9 c/ D; k6 A7 t# l关注我,一起变得更加优秀!
, m4 k/ _! ~6 b! X7 _' i
aibjmbwsgxu64016294411.jpg
|