第1章:软件架构设计原则——打好地基才能盖高楼
各位同学,大家好。我是你们这门课的主讲人。今天咱们正式开始聊《智能窗帘电机软件架构与设计实战》。
第一节课,我不打算直接扔代码。我想先聊聊「软件架构设计原则」。为什么?
我见过太多工程师,上来就写代码,写到一半发现耦合太严重,改一个功能要动十个文件。嗯,那种感觉,就像你装修房子,墙都刷好了才发现插座位置不对——拆了重来,成本太高。
所以,咱们先把地基打牢。今天重点讲四个核心原则:分层架构、模块化设计、接口隔离、依赖倒置。这四个东西,说白了就是让你写的代码「好改、好测、好复用」。
1.1 分层架构:把代码像抽屉一样整理
先问大家一个问题:你写嵌入式代码,是不是经常把所有逻辑都塞在一个 main.c 里?
我以前也这么干过。有一次做一个窗帘电机项目,客户突然说要加一个「遇阻停止」功能。我打开那个 2000 行的 main.c,找了半天才找到电机控制的那段代码。改完之后,又发现定时器中断和它冲突了……那周我加了三天班。
从那以后,我养成了一个习惯:分层。
在智能窗帘电机这个项目里,我建议把软件分成三层:
- HAL(硬件抽象层):直接跟芯片寄存器、GPIO、定时器打交道。说白了,就是「让电机转起来」的那层。
- OSAL(操作系统抽象层):管理任务调度、消息队列、延时。如果以后换 RTOS,只改这一层就行。
- APP(应用层):业务逻辑。比如「窗帘开到 50%」、「检测到光线变暗自动关闭」。这一层不应该知道底层用的是 STM32 还是 GD32。
核心原则:上层可以调用下层,下层绝对不能反过来调用上层。这叫「单向依赖」。
举个例子,APP 层想控制电机转 5 秒,它只需要调用 HAL 层的一个接口,比如 HAL_Motor_Run(5000)。至于这个函数内部是用的 PWM 还是 GPIO 翻转,APP 层完全不用管。
你想想看,如果以后要换电机驱动芯片,我只需要改 HAL 层。APP 层的代码一行都不用动。这就是分层的好处。
1.2 模块化设计:每个模块只干一件事
分层是纵向切分,模块化是横向切分。
我习惯把每个功能独立成一个模块。比如:
- 电机控制模块:负责正转、反转、停止、速度调节。
- 传感器模块:负责读取光照、温度、遇阻信号。
- 通信模块:负责解析蓝牙/WiFi 指令。
- 状态管理模块:负责记录窗帘当前开合百分比。
每个模块都有自己的 .h 和 .c 文件。模块之间通过接口通信,尽量不要直接访问对方的全局变量。
我的一个小习惯:每个模块的 .h 文件里,只暴露必要的函数声明。内部用的静态函数,全部用 static 修饰。这样别人看你的代码,一眼就知道「哪些是给别人用的,哪些是自己用的」。
我记得有一次,团队里新来了一个同事,他负责写通信模块。他写完蓝牙解析之后,直接在我的电机控制模块里加了一个全局变量来传数据。我当时就跟他讲:「兄弟,你这样搞,以后我改电机控制,还得先看看你的代码有没有依赖我的变量。咱们还是用接口吧。」
模块化设计,说白了就是「高内聚、低耦合」。每个模块内部可以很复杂,但模块之间要尽量简单。
1.3 接口隔离原则:不要让你的模块「被迫」实现不需要的方法
这个原则听起来有点绕。我举个例子你就明白了。
假设你写了一个「窗帘控制接口」,里面定义了三个方法:
// 不好的设计:一个接口塞了太多功能
typedef struct {
void (*Open)(void);
void (*Close)(void);
void (*Stop)(void);
void (*SetSpeed)(int speed);
void (*GetLightLevel)(void); // 获取光照强度
void (*SetTimer)(int seconds); // 设置定时
} CurtainInterface;
问题在哪?
如果你的电机控制模块只需要 Open、Close、Stop,它却被迫要实现 GetLightLevel 和 SetTimer。这就是「接口污染」。
我建议的做法是:把大接口拆成小接口。
// 好的设计:每个接口只干一件事
typedef struct {
void (*Open)(void);
void (*Close)(void);
void (*Stop)(void);
} MotorInterface;
typedef struct {
int (*GetLightLevel)(void);
} SensorInterface;
typedef struct {
void (*SetTimer)(int seconds);
} TimerInterface;
这样,电机控制模块只需要依赖 MotorInterface,传感器模块依赖 SensorInterface。谁用谁实现,互不干扰。
注意:接口隔离不是让你把接口拆得越碎越好。拆得太碎,管理成本反而上升。我的经验是:一个接口里不要超过 5 个方法,而且这些方法必须在逻辑上属于同一类功能。
1.4 依赖倒置:让高层模块不依赖底层细节
这个原则名字听着很学术,其实核心就一句话:抽象不应该依赖细节,细节应该依赖抽象。
什么意思?
在传统的嵌入式开发里,我们经常这样写:
// 传统写法:APP 层直接调用 HAL 层
void APP_OpenCurtain(void) {
HAL_GPIO_WritePin(MOTOR_GPIO, MOTOR_PIN, HIGH);
HAL_Delay(100);
HAL_GPIO_WritePin(MOTOR_GPIO, MOTOR_PIN, LOW);
}
这样写有什么问题?
APP 层直接依赖了 HAL 层的具体实现。如果以后换了芯片,HAL_GPIO_WritePin 这个函数可能就不存在了。你得把 APP 层的代码全部重写。
依赖倒置的做法是:在中间加一层抽象。
// 定义抽象接口
typedef struct {
void (*MotorRun)(int direction, int duration);
} IMotor;
// APP 层只依赖这个抽象接口
void APP_OpenCurtain(IMotor *motor) {
motor->MotorRun(FORWARD, 5000);
}
// HAL 层实现这个接口
void HAL_MotorRun(int direction, int duration) {
// 具体硬件操作
}
IMotor motor = {
.MotorRun = HAL_MotorRun
};
看到了吗?APP 层不再直接调用 HAL 层,而是通过一个抽象接口 IMotor 来操作。HAL 层负责实现这个接口。
这样做的好处是:APP 层的代码可以完全独立于硬件。你甚至可以写一个模拟的 MotorRun 函数,在 PC 上做单元测试。
总结一下:依赖倒置的核心就是「面向接口编程,而不是面向实现编程」。我在做窗帘电机项目时,所有模块之间的调用,都尽量通过接口指针来传递。这样后期维护起来,真的轻松很多。
1.5 把这些原则串起来:一个简单的例子
说了这么多理论,咱们来点实际的。假设我们要实现一个功能:「当光照低于阈值时,自动关闭窗帘」。
按照我们刚才的原则,代码结构应该是这样的:
- HAL 层:提供
HAL_Sensor_ReadLight()和HAL_Motor_Run()。 - OSAL 层:提供一个定时任务,每 100ms 调用一次 APP 层的检查函数。
- APP 层:实现
APP_AutoCloseCheck(),里面调用传感器接口获取光照,判断是否低于阈值,如果是则调用电机接口关闭窗帘。
模块划分:
- 传感器模块(Sensor.c/h)
- 电机控制模块(Motor.c/h)
- 自动控制模块(AutoCtrl.c/h)
接口设计:
ISensor接口:只包含GetLightLevel()IMotor接口:只包含Run()和Stop()
依赖关系:
- AutoCtrl 模块依赖 ISensor 和 IMotor 接口(依赖倒置)
- Sensor 模块实现 ISensor 接口
- Motor 模块实现 IMotor 接口
你看,这样设计出来的代码,每个模块职责清晰,接口明确。以后想加一个「根据温度自动开关」的功能,只需要新增一个 AutoTempCtrl 模块,再实现一个 ITempSensor 接口就行了。完全不影响现有代码。
避坑指南:我曾经在一个项目里,为了追求「完美分层」,把接口拆得太细,结果一个简单的功能要跨 5 个文件才能看懂。后来我学乖了:原则是死的,人是活的。在小型项目里,适当简化分层是可以的。但核心思想——「高内聚、低耦合」——一定要守住。
小结
今天咱们聊了四个原则:
- 分层架构:HAL / OSAL / APP,各司其职。
- 模块化设计:每个模块只干一件事,接口清晰。
- 接口隔离:不要让你的模块被迫实现不需要的方法。
- 依赖倒置:面向接口编程,而不是面向实现。
这些原则,说白了就是让你写的代码「好改、好测、好复用」。下一节课,咱们会基于这些原则,开始搭建智能窗帘电机的实际工程框架。到时候,我会带着大家一步步写代码。
嗯,今天就到这里。有什么问题,欢迎在课程群里讨论。
公众号:蓝海资料掘金营,微信deep3321