第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 把这些原则串起来:一个简单的例子

说了这么多理论,咱们来点实际的。假设我们要实现一个功能:「当光照低于阈值时,自动关闭窗帘」

按照我们刚才的原则,代码结构应该是这样的:

  1. HAL 层:提供 HAL_Sensor_ReadLight()HAL_Motor_Run()
  2. OSAL 层:提供一个定时任务,每 100ms 调用一次 APP 层的检查函数。
  3. 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