2. 嵌入式软件架构基础:分层架构思想、模块化设计原则、接口隔离原则

好,咱们接着聊。上一章我讲了门控系统的整体轮廓,这一章咱们深入一点,聊聊架构的“地基”。

说白了,嵌入式软件架构不是什么玄学。它就是一套组织代码的规矩。规矩定好了,后面写代码就像搭积木,省心省力。规矩没定好,那就是一团乱麻,改一个bug能引出三个新bug。

我个人习惯,在动手写第一行代码之前,先花30%的时间把架构想清楚。这30%的时间,往往能省下后面70%的返工时间。

2.1 分层架构思想:把复杂问题拆成“三明治”

分层架构,你想想看,就像做三明治。面包、生菜、肉饼、芝士,一层叠一层。每一层只管自己的事,不越界。

在咱们门控系统里,我通常把软件分成三层:

  • 硬件抽象层(HAL):直接跟寄存器、中断打交道。比如读一个IO口电平,发一个PWM波。
  • 业务逻辑层(BLL):处理门控的核心逻辑。比如“关门指令来了,先判断门是否关到位,再判断防夹力是否触发”。
  • 应用层(AL):负责跟外部通信、处理用户命令。比如接收CAN总线上的开门指令,或者上报故障码。

为什么要这么分?我在项目中遇到过一件事:某次更换了主控芯片,从STM32F4换成了GD32。如果代码是“一锅粥”式的写法,那几乎所有的逻辑都要重写。但因为我们用了分层架构,只需要重写最底层的HAL层,上面的业务逻辑层和应用层几乎没动。这就是分层的好处——隔离变化

核心原则:上层可以调用下层,但下层绝不能反过来调用上层。这叫“单向依赖”。一旦出现下层调用上层,就产生了循环依赖,架构就乱了。

举个例子,业务逻辑层需要知道门是否关到位。它不应该直接去读寄存器,而是调用HAL层提供的接口:HAL_GetDoorClosedStatus()。至于这个接口是读GPIO还是读SPI,业务逻辑层完全不用关心。

2.2 模块化设计原则:高内聚,低耦合

模块化,说白了就是“分而治之”。把一个大系统切成一个个小模块,每个模块只干一件事,并且把这件事干好。

我经常跟团队说一句话:“一个函数,如果它超过200行,那它一定干了不止一件事。拆!”

模块化设计有两个关键指标:

  • 内聚性:模块内部元素之间的关联程度。越高越好。
  • 耦合性:模块与模块之间的关联程度。越低越好。

咱们拿门控系统的“电机控制”模块来举例:

模块 职责 对外接口
电机控制模块 控制电机正反转、调速、刹车 Motor_Start(), Motor_Stop(), Motor_SetSpeed()
防夹检测模块 检测电流或力矩,判断是否夹到人 AntiPinch_Check()
门位置检测模块 读取编码器或限位开关,判断门位置 Position_GetStatus()

你看,每个模块的职责都很清晰。电机控制模块不需要知道防夹检测模块是怎么算的,它只需要在收到Motor_Stop()指令时停下来就行。

我的一个小习惯:每个模块都单独建一个.c和.h文件。模块内部的函数用static修饰,只暴露必要的接口。这样别人用你的模块时,想乱来都难。

2.3 接口隔离原则:别让模块“知道太多”

接口隔离原则,英文叫Interface Segregation Principle,简称ISP。它是SOLID原则里的“I”。

什么意思呢?就是说:一个模块不应该被迫依赖它不需要的接口

嗯,这里要注意。很多新手容易犯一个错:写一个“万能”的头文件,里面塞满了各种函数声明。然后所有模块都包含这个头文件。结果呢?电机控制模块明明只需要3个函数,却被迫看到了20个不相关的函数声明。这就像你去餐厅吃饭,只想点一碗面,服务员却把整本菜单都塞给你,让你自己找。

我曾经在一个项目里见过这样的代码:

// 一个“万能”的接口头文件
#ifndef __ALL_INTERFACES_H__
#define __ALL_INTERFACES_H__

void Motor_Start(void);
void Motor_Stop(void);
void Motor_SetSpeed(uint8_t speed);
void AntiPinch_Check(void);
void Position_GetStatus(void);
void CAN_SendMessage(uint32_t id, uint8_t *data);
void CAN_ReceiveMessage(uint32_t *id, uint8_t *data);
void LED_On(void);
void LED_Off(void);
// ... 还有几十个函数声明

#endif

这种写法,耦合度极高。只要任何一个接口改了参数,所有包含这个头文件的模块都得重新编译。而且,你根本分不清哪个模块到底用了哪些接口。

正确的做法是什么?按模块拆分接口

// motor_interface.h - 电机控制模块专用接口
#ifndef __MOTOR_INTERFACE_H__
#define __MOTOR_INTERFACE_H__

void Motor_Start(void);
void Motor_Stop(void);
void Motor_SetSpeed(uint8_t speed);

#endif

// can_interface.h - CAN通信模块专用接口
#ifndef __CAN_INTERFACE_H__
#define __CAN_INTERFACE_H__

void CAN_SendMessage(uint32_t id, uint8_t *data);
void CAN_ReceiveMessage(uint32_t *id, uint8_t *data);

#endif

这样,每个模块只包含它需要的头文件。电机控制模块只需要#include "motor_interface.h",它根本不知道CAN通信的存在。这就是接口隔离——让模块只看到它该看到的

避坑指南:我曾经在一个项目里,为了图省事,把所有的结构体定义都放在一个头文件里。结果后来要修改一个结构体成员,导致十几个文件都要重新编译。从那以后,我坚持“每个模块的头文件只包含该模块需要的数据类型”。

2.4 小结:架构是“慢工出细活”

好了,这一章的内容就这些。分层架构、模块化、接口隔离,这三个东西听起来简单,但真正用好并不容易。

我见过很多项目,一开始图快,代码写得飞起。结果到了后期,改一个bug要花三天,因为根本不知道改了这个地方会影响哪里。而架构好的项目,前期虽然慢一点,但后期维护起来非常轻松。

你想想看,是愿意前期多花点时间把架构搭好,还是愿意后期天天加班改bug?反正我选前者。

一句话总结:好的架构,让代码像乐高积木一样,可以自由组合、替换、升级。坏的架构,让代码像一团浆糊,牵一发而动全身。

下一章,咱们聊聊具体的实现——如何用状态机来管理门控系统的各种状态。那才是真正“动手”的时候。