我是老温,一名热爱学习的嵌入式工程师
0 i. q5 i: v6 v: V8 D Z关注我,一起变得更加优秀!
0 X! s" B: m* B' [6 p7 e工程师在进行嵌入式软件架构设计的时候,很多时候需要在灵活性与资源约束之间进行权衡。
9 y8 N5 y% j4 |+ I/ \9 Z1 X* E7 y" f分层与封装虽然能提升可维护性和移植性,但过度设计容易引发性能损耗与开发效率下降。
" X+ S" m/ w, N本文尝试讨论,在嵌入式软件框架设计时,应如何避免框架过度分层和封装设计,以便让开发者在资源、性能与维护成本之间找到最佳的平衡点。. E( _' K7 s' S& y) C
pxk1vzixkxt64016294011.jpg
$ @' `5 y" J5 l2 V
一、嵌入式软件分层和封装接口的本质。
7 U/ d& ^, d* l$ [- j: q: c; A分层软件框架的工程学定义,单片机典型的四层框架设计示例。Application Layer // 业务逻辑实现//-------------------------------------Middleware Layer // 协议栈/算法库//-------------------------------------Driver Layer // 外设驱动封装//-------------------------------------Hardware Layer // 寄存器级操作每个层级通过明确定义的接口进行通信,上层模块仅能调用下层提供的接口,禁止跨层访问。8 s$ }0 M/ U6 ?- o- {/ g# A6 S
这种设计将硬件操作与业务逻辑解耦,例如STM32的HAL库就将寄存器操作抽象为统一API。3 @4 C2 _2 [. y: A; n
函数接口封装的技术内涵是,接口封装通过信息隐藏(Information Hiding)实现模块隔离,其核心要素包括:接口的访问权限控制、参数校验机制、运行错误的处理策略、不同软件版本的兼容性设计。2 Q4 H- c9 L, ~9 G8 G
标准的驱动接口封装示例如下:; G# e1 j g& X0 Y+ ^3 }( t
// 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;二、软件分层和封装接口的设计必要性
. o j3 `" e6 f& E* s在大型的嵌入式软件系统开发中,分层软件框架带来的工程价值,主要体现在:提升可维护性、增强复用性、便于协作开发、降低移植成本。6 T1 ` E( \2 c: R" p/ f
举个例子,通过封装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;三、经典分层架构的设计范式
4 \" ]2 ~5 D2 z" E4 \) m硬件抽象层(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 语言设计模式
1 p+ X {7 q1 B& p S, c四、软件分层和接口封装的使用场景8 S# ]# L- t( r
一般情况下,必须采用软件分层设计的场景,有以下这些:需支持多硬件平台、复杂的协议栈集成、长期的维护工作、大型开发团队协作。
+ ?6 r5 Q5 y1 g0 y5 B$ l而无需严格进行软件分层的场景,主要有:资源受限型的单片机、原型功能验证、高实时性的应用、短周期交付的项目。$ [8 k: ~+ B9 `; X* o
如果出现以下特征,则可能警示着接口过度设计,比如:内存占用超标、执行时间出现劣化、工程师开发效率下降,等等。( l0 o3 U# D8 n2 Y& M2 q
需要警惕如下的接口嵌套陷阱。// 过度封装示例result = hal_spi->controller->channel[0]->transfer(...);五、如何避免过度分层与封装设计
! Q2 A/ w% V9 J' G8 b采用分层粒度控制策略,使用环形依赖检测工具(比如Lattix DSM工具)分析模块的耦合度,让软件架构满足以下原则:--单向依赖原则:禁止出现循环依赖。--接口精简原则:每个模块暴露的API不超过7个。--层级深度约束原则:API封装调用不超过5层。
3 l' g6 E, z) o9 R+ e" C. q. c在实时数据采集系统里面,可以采用“条件编译分层”技术,利用编译器处理一些额外的工作,可以让嵌入式系统达到较优的实时性。// 性能关键路径消除抽象#ifdef OPTIMIZE_PERFORMANCE #define SPI_TRANSFER(data) REG_SPI_DR = (data)#else #define SPI_TRANSFER(data) hal_spi_transfer(data)#endif良好的函数封装接口设计,应该满足以下原则:--参数正交:修改某一参数时,不影响其他功能。--耗时确定:接口调用的耗时可以基本确定。--异常隔离:分层间的异常不会导致其他层崩溃。' L- S2 @+ x/ h y# |6 L# k7 K
六、总结- C2 p2 u$ d7 M- G& Y' j
嵌入式软件虽然没有架构师相关的岗位,但优秀的嵌入式软件工程师,在规划和设计整个嵌入式系统的功能的时候,应当在可维护性和性能之间、在抽象成本和开发效率之间,寻找一个最佳的平衡点。
: x% ]# S& F; g5 w8 x# y* O结合以往的项目生命周期、团队经验规模、硬件平台约束等参数,通过建立一个量化的评估体系,以便可以动态调整嵌入式系统的分层与封装策略。
' x2 Q: D8 Y: A0 S谨记一点:没有完美的分层架构,只有适合当前上下文的设计。8 u* F; G7 Q/ ]- {7 X0 k T
-END-" ]" ~) h# q4 G+ o* H
往期推荐:点击图片即可跳转阅读1 ^7 F% {9 s+ Y1 C5 u
hcooxdyk2s164016294111.jpg
, N7 l$ t t, q掌握这5个代码小技巧,让嵌入式软件调试更加高效!& Q0 L" ^ D' U0 z( p9 p% n
ynye410voln64016294211.jpg
! {: f7 u- H% T0 r
嵌入式Linux工业网关设计,离不开这个关键核心通信模块。) T |0 I4 {6 V2 R# G
froiyzgjnuf64016294311.jpg
4 P; B% e. j6 P& Y
嵌入式软硬件开发,工程师在预研阶段就要开始考虑如何降本增效!
/ X- _, Q# U' r; d1 b# N0 b我是老温,一名热爱学习的嵌入式工程师
+ x. p& i1 k8 ~8 @& h0 [关注我,一起变得更加优秀!
2 [) x5 Q) ]/ u0 F0 K
aibjmbwsgxu64016294411.jpg
|