三、软件架构设计原则:模块化设计、分层架构思想、高内聚低耦合、接口隔离原则

好,咱们进入第三个核心话题。说实话,软件架构设计原则这东西,听起来有点虚,但做分拣机嵌入式开发的人都知道——这些原则要是没用好,后期调试能让你怀疑人生。

我个人习惯把架构设计比作搭积木。你想想看,如果每块积木都乱七八糟地粘在一起,想换一块就得拆掉整个城堡。分拣机的软件也是这个道理。今天咱们就聊聊四个最核心的原则:模块化、分层、高内聚低耦合、接口隔离。

3.1 模块化设计:把大问题拆成小问题

模块化设计,说白了就是把一个复杂的系统拆成若干个独立的功能单元。每个单元负责一件明确的事。

我在做分拣机项目时,遇到过最头疼的事就是——一个文件里塞了三千行代码,什么电机控制、传感器读取、通信协议全混在一起。后来我学乖了,强制自己按功能拆模块。

模块划分的常见方式:

  • 按功能划分:电机控制模块、传感器采集模块、通信模块、逻辑处理模块
  • 按硬件划分:驱动层模块、中间件模块、应用层模块
  • 按流程划分:初始化模块、运行模块、故障处理模块

举个例子,分拣机的电机控制模块,它只负责三件事:接收速度指令、执行PID运算、输出PWM波形。它不需要知道包裹是从哪来的,也不需要知道传感器数据怎么处理。这就是模块化的精髓——各司其职。

/* 模块化设计的典型结构 */
// motor_control.h
#ifndef MOTOR_CONTROL_H
#define MOTOR_CONTROL_H

void Motor_Init(void);
void Motor_SetSpeed(int16_t speed_rpm);
void Motor_Stop(void);
int16_t Motor_GetCurrentSpeed(void);

#endif

// motor_control.c
#include "motor_control.h"

static int16_t current_speed = 0;

void Motor_Init(void) {
    // 初始化定时器、GPIO等
    current_speed = 0;
}

void Motor_SetSpeed(int16_t speed_rpm) {
    // 限幅处理
    if (speed_rpm > MAX_SPEED) speed_rpm = MAX_SPEED;
    if (speed_rpm < -MAX_SPEED) speed_rpm = -MAX_SPEED;
    
    // 执行PID运算
    current_speed = speed_rpm;
    // 更新PWM占空比
    PWM_UpdateDuty(speed_rpm);
}

我的经验之谈:模块化不是越细越好。我曾经把一个分拣机拆成四十多个模块,结果模块之间的调用关系比蜘蛛网还复杂。一般来说,一个模块的代码量控制在200-500行比较合适。超过800行,就该考虑拆分了。

3.2 分层架构思想:让代码有层次感

分层架构,就是把软件按抽象层级从下往上堆叠。底层处理硬件细节,上层处理业务逻辑。层与层之间通过接口通信。

我记得刚入行时,有个老工程师跟我说过一句话:「写嵌入式软件,最怕的就是应用层代码里直接操作寄存器。」当时我不理解,后来自己踩了坑才明白——一旦换了MCU型号,所有代码都得重写。

分拣机的典型分层架构是这样的:

层级 职责 典型内容
应用层 业务逻辑 分拣策略、包裹跟踪、异常处理
中间件层 数据转换、协议解析 Modbus协议栈、数据缓存、状态机
驱动层 硬件抽象 电机驱动、传感器读取、通信接口
硬件层 物理设备 MCU、电机、编码器、光电传感器

为什么要分层?说白了就是为了「隔离变化」。硬件换了,只改驱动层;业务逻辑变了,只改应用层。其他层纹丝不动。

注意:分层架构最忌讳的是「跨层调用」。应用层直接调用驱动层的函数,或者驱动层反过来调用应用层的逻辑,都会破坏分层的意义。我曾经在一个项目里发现,驱动层的定时器中断里直接调用了应用层的包裹计数函数——结果一换MCU,整个逻辑全乱了。

3.3 高内聚低耦合:模块内部的凝聚力和模块之间的松散度

这两个词经常一起出现,但意思完全不同。

  • 高内聚:一个模块内部的功能要紧密相关。比如电机控制模块,里面全是跟电机有关的东西,别把WiFi配置代码塞进去。
  • 低耦合:模块之间的依赖要尽量少。A模块改了,B模块不应该受影响。

我遇到过最典型的反面教材是什么?一个全局变量满天飞的项目。分拣机的速度、位置、状态全部定义成全局变量,任何模块都能随便读写。结果呢?调试时发现速度莫名其妙跳变,查了两天才知道是通信模块不小心改写了速度变量。

实现低耦合的几个技巧:

  • 尽量使用局部变量和静态变量,少用全局变量
  • 模块之间通过函数接口通信,不要直接访问对方的数据结构
  • 使用回调函数或消息队列解耦调用关系
/* 低耦合的通信方式示例 */
// 使用消息队列解耦
typedef struct {
    uint8_t msg_id;
    uint8_t data[8];
} Message_t;

// 发送模块只管发消息
void Sensor_SendData(uint16_t value) {
    Message_t msg;
    msg.msg_id = MSG_SENSOR_DATA;
    msg.data[0] = (value >> 8) & 0xFF;
    msg.data[1] = value & 0xFF;
    Queue_Send(&msg);
}

// 接收模块只管收消息
void Logic_ProcessMessage(void) {
    Message_t msg;
    if (Queue_Receive(&msg)) {
        switch (msg.msg_id) {
            case MSG_SENSOR_DATA:
                // 处理传感器数据
                break;
        }
    }
}

避坑指南:我曾经为了追求「绝对的低耦合」,把每个模块都设计成完全独立的,结果模块之间的数据传递变得极其复杂,性能也下降了。记住,低耦合不是零耦合。合理的依赖是必要的,关键是要控制依赖的方向和强度。

3.4 接口隔离原则:不要强迫模块依赖它不需要的东西

接口隔离原则,说白了就是「按需提供接口」。一个模块只需要暴露它真正需要暴露的函数,其他的都藏起来。

你想想看,如果电机控制模块暴露了二十个函数,但上层逻辑只需要用到其中三个。那剩下的十七个函数就是隐患——别人可能会乱调用,导致不可预期的行为。

我个人的做法是:每个模块只提供最精简的公共接口,内部函数全部用static修饰。

/* 接口隔离的典型做法 */
// 只暴露必要的接口
// motor_interface.h
#ifndef MOTOR_INTERFACE_H
#define MOTOR_INTERFACE_H

// 公共接口——只有这三个
void Motor_Init(void);
void Motor_SetSpeed(int16_t speed);
void Motor_Stop(void);

// 内部函数不暴露
// static void Motor_Calibrate(void);
// static void Motor_CheckFault(void);

#endif

接口隔离的好处:

  • 降低使用复杂度——调用者只需要知道少数几个函数
  • 减少误用风险——内部函数不会被外部误调用
  • 方便维护——修改内部实现不影响外部调用

嗯,这里要注意一点:接口隔离不是让你把所有函数都藏起来。该暴露的还是要暴露,关键是「只暴露必要的」。我见过有些工程师为了追求接口隔离,把模块设计得像个黑盒子,外部想获取一个状态值都得绕好几层——这就过头了。

我曾经犯过的错:在一个分拣机项目中,我把所有模块的接口都设计成「大而全」的,每个模块都提供了十几个函数。结果新同事接手后,根本分不清哪些函数是常用的,哪些是内部调试用的。后来我花了三天时间重构,把每个模块的公共接口精简到5个以内,代码的可读性和可维护性明显提升。

好了,这四个原则讲完了。总结一下我的个人体会:模块化是基础,分层是骨架,高内聚低耦合是肌肉,接口隔离是皮肤。四者缺一不可,但也不必追求完美。在实际项目中,根据团队规模和项目复杂度,适当取舍就好。

下一章咱们聊聊具体怎么把这些原则落地到分拣机的代码中。到时候我会拿一个真实的分拣机项目做例子,一步步拆解架构设计的过程。