第三章 架构设计原则:模块化与解耦、接口标准化、可配置化设计

各位同行,咱们直接切入正题。量产阶段的软件架构,跟原型机那套玩法完全是两码事。原型机你只要能跑通就行,代码写得再乱,大不了重启一下。但量产不一样——成千上万台机器铺出去,每台的传感器偏差、马达特性、Wi-Fi信号强度都不一样。这时候架构设计要是没做好,那就是灾难。

我个人习惯,在项目启动阶段就把架构原则定死。说白了,就是三个关键词:模块化与解耦、接口标准化、可配置化设计。这三板斧砍下去,后面量产阶段的坑能少踩一半。

3.1 模块化与解耦:别让代码长成“毛线球”

我在项目中遇到过最头疼的事,就是接手一个“毛线球”项目——传感器采集、路径规划、电机控制、UI显示全揉在一个文件里,改一行代码要牵动七八个模块。这种代码,量产阶段根本没法维护。

模块化的核心是什么?高内聚、低耦合。每个模块只干一件事,模块之间通过定义好的接口通信,谁也不依赖谁的具体实现。

举个例子,扫地机器人的传感器模块:

// 传感器模块接口定义
typedef struct {
    int32_t (*get_bumper_status)(void);
    int32_t (*get_cliff_sensor)(uint8_t index);
    int32_t (*get_gyro_angle)(void);
    int32_t (*get_odometry)(int32_t *x, int32_t *y);
} sensor_if_t;

// 路径规划模块只调用接口,不关心底层实现
void navigation_task(void *param) {
    sensor_if_t *sensor = (sensor_if_t *)param;
    while(1) {
        int32_t bumper = sensor->get_bumper_status();
        // 处理逻辑...
    }
}

你看,路径规划模块根本不知道传感器是GPIO读的、I2C读的还是SPI读的。它只认这个接口结构体。哪天你要换传感器硬件,只需要重新实现这个接口,路径规划模块一行代码都不用改。

量产实战要点:

  • 每个模块的代码量控制在2000行以内,超过就考虑拆分
  • 模块间通信必须通过接口函数,禁止直接访问全局变量
  • 每个模块要有独立的头文件,只暴露必要的API

3.2 接口标准化:CAN和MQTT,两种场景两种玩法

接口标准化这件事,我吃过不少亏。早期做的一款产品,传感器模块用UART,电机模块用I2C,Wi-Fi模块用SPI,每个接口的协议格式都不一样。结果呢?联调的时候天天出问题,不是字节序搞反了,就是超时时间没对齐。

量产阶段,我强烈建议把接口标准化做死。这里重点说两个:CAN总线MQTT

3.4.1 CAN总线:底盘与传感器通信的首选

扫地机器人内部,底盘控制、传感器数据、电池管理这些模块,用CAN总线最合适。为什么?实时性好、抗干扰强、而且支持多主通信。

我一般这样定义CAN报文格式:

报文ID 数据域(8字节) 说明
0x100 Byte0: 左轮速度
Byte1: 右轮速度
Byte2: 控制模式
电机控制指令
0x200 Byte0-1: 电池电压(mV)
Byte2: 电量百分比
Byte3: 充电状态
电池状态上报
0x300 Byte0: 碰撞传感器状态
Byte1: 悬崖传感器状态
Byte2-3: 陀螺仪角度
传感器数据上报

嗯,这里要注意:报文ID的分配要有层次。我习惯用高4位表示模块类型,低4位表示具体指令。这样调试的时候,看ID就知道是哪个模块在通信。

3.4.2 MQTT:云端与App通信的标配

至于跟云端和手机App的通信,MQTT几乎是行业标准了。为什么?轻量、支持QoS、而且有发布/订阅机制,非常适合扫地机器人这种需要实时状态同步的场景。

我建议这样设计Topic结构:

// 设备状态上报
robot/{device_id}/status
{
    "battery": 85,
    "mode": "cleaning",
    "error_code": 0,
    "position": {"x": 1.2, "y": 3.4}
}

// 远程控制指令
robot/{device_id}/command
{
    "cmd": "start_cleaning",
    "params": {"mode": "edge", "suction": "high"}
}

// OTA升级通知
robot/{device_id}/ota
{
    "version": "2.1.0",
    "url": "http://...",
    "md5": "a1b2c3..."
}

避坑指南:我曾经因为Topic设计得太随意,导致量产后的设备跟云端通信老是丢数据。后来统一了Topic命名规范,所有Topic都按"设备类型/设备ID/功能域"的格式来,问题就解决了。另外,MQTT的QoS级别建议用1,既保证至少一次送达,又不会像QoS2那样增加太多开销。

3.3 可配置化设计:参数化与宏定义

量产阶段最怕什么?每台机器的硬件参数都不一样。比如轮子直径,理论上都是100mm,但实际生产出来可能是99.8mm到100.2mm之间。如果代码里写死了轮径,那每台机器跑出来的里程都不准。

可配置化设计就是解决这个问题的。我把它分成两个层次:编译时配置运行时配置

3.3.1 编译时配置:用宏定义搞定硬件差异

对于硬件版本、传感器型号这些在出厂前就确定的东西,用宏定义最合适。

// hardware_config.h
// 硬件版本定义
#define HW_VERSION_A     1
#define HW_VERSION_B     2

// 传感器型号选择
#define SENSOR_LIDAR_RPLIDAR_A1  0
#define SENSOR_LIDAR_SLAMTEC_R2  1

// 根据硬件版本选择配置
#if (HW_VERSION == HW_VERSION_A)
    #define WHEEL_DIAMETER_MM     100.0f
    #define WHEEL_BASE_MM         280.0f
    #define BATTERY_CAPACITY_MAH  5200
    #define SENSOR_LIDAR_TYPE     SENSOR_LIDAR_RPLIDAR_A1
#elif (HW_VERSION == HW_VERSION_B)
    #define WHEEL_DIAMETER_MM     102.0f
    #define WHEEL_BASE_MM         290.0f
    #define BATTERY_CAPACITY_MAH  6000
    #define SENSOR_LIDAR_TYPE     SENSOR_LIDAR_SLAMTEC_R2
#endif

你想想看,这样设计的好处是什么?产线只需要在编译时指定HW_VERSION,就能生成对应硬件版本的固件。不用维护多套代码,也不用担心搞错配置。

3.3.2 运行时配置:参数化让调试更灵活

对于用户偏好、工作模式这些需要动态调整的东西,我习惯用参数化配置。把配置项放到Flash或EEPROM里,运行时读取。

// 运行时参数结构体
typedef struct {
    uint8_t  suction_power;      // 吸力等级 1-5
    uint8_t  water_flow;         // 拖地水量 1-3
    uint8_t  cleaning_mode;      // 清扫模式
    uint16_t edge_clean_time_s;  // 沿边清扫时间(秒)
    uint16_t return_charge_time_s; // 回充超时时间(秒)
    uint8_t  language;           // 语音语言
    uint8_t  volume;             // 音量 0-100
} runtime_config_t;

// 默认配置
const runtime_config_t default_config = {
    .suction_power = 3,
    .water_flow = 2,
    .cleaning_mode = MODE_AUTO,
    .edge_clean_time_s = 300,
    .return_charge_time_s = 120,
    .language = LANG_CHINESE,
    .volume = 70
};

注意:运行时配置一定要做校验。我曾经遇到过一台机器,用户把吸力等级设成了6(合法范围是1-5),结果电机过载烧了。从那以后,所有配置项写入前都要做范围检查,非法值直接恢复默认。

3.4 三个原则的协同效应

模块化、接口标准化、可配置化,这三个原则不是孤立的。它们配合起来,效果是1+1+1>3。

举个例子:你要给扫地机器人增加一个“地毯识别”功能。

  • 模块化:新增一个carpet_detector模块,只负责检测地毯
  • 接口标准化:通过CAN总线发布地毯检测结果,其他模块订阅这个数据
  • 可配置化:地毯检测的灵敏度、触发阈值都做成参数,不同机型可以调

你看,这样设计,新增功能几乎不影响现有代码。量产阶段,这种扩展性太重要了。

总结一下我的经验:

  1. 模块化是骨架,决定了代码好不好维护
  2. 接口标准化是血管,决定了数据流不流得通
  3. 可配置化是肌肉,决定了产品适不适应不同场景

这三样做好了,量产阶段的软件工程化就成功了一大半。

好了,这一章就聊到这儿。下一章咱们讲讲状态机设计——扫地机器人这种多状态切换的产品,状态机设计不好,bug能让你改到怀疑人生。