第三章:嵌入式软件架构设计原则

好,咱们今天聊聊架构设计原则。说实话,我见过太多嵌入式项目,一开始代码写得飞起,三个月后连自己都看不懂了。为什么会这样?说白了,就是架构没打好底子。

嵌入式软件和桌面软件不一样。资源有限,实时性要求高,出了问题可能还得飞过去现场调试。所以架构设计,从一开始就得想清楚。我个人习惯,先定原则,再动手写代码。

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

模块化,就是把一个复杂系统拆成若干个独立的功能单元。每个模块只做一件事,做好一件事。

我在项目中遇到过这样一个案例:一个智能家居网关,初期把所有功能都塞在一个文件里——WiFi连接、MQTT协议解析、传感器数据采集、本地存储……全混在一起。结果呢?改一个WiFi重连逻辑,把传感器采集的定时器给改崩了。这就是典型的「牵一发而动全身」。

模块化的核心要点:

  • 单一职责:一个模块只负责一个功能
  • 接口清晰:模块之间通过定义好的接口通信
  • 独立编译:每个模块可以单独编译测试

举个例子,一个典型的嵌入式系统可以拆成这样:

// 模块划分示例
project/
├── drivers/          // 硬件驱动层
│   ├── uart.c
│   ├── spi.c
│   └── gpio.c
├── middleware/       // 中间件层
│   ├── mqtt.c
│   ├── flash_fs.c
│   └── ota.c
├── app/             // 应用层
│   ├── sensor_mgr.c
│   ├── network_mgr.c
│   └── device_ctrl.c
└── os/              // 操作系统抽象层
    ├── task.c
    └── queue.c

嗯,这里要注意:模块化不是越细越好。拆得太碎,接口数量爆炸,反而增加复杂度。我一般建议,一个模块的代码量控制在500-2000行之间。

3.2 分层:让每一层各司其职

分层架构,说白了就是「上层调用下层,下层不依赖上层」。你想想看,如果底层驱动直接调用了应用层的函数,那这个系统基本就废了——改个驱动,应用层也得跟着改。

嵌入式系统常见的分层方式:

层次 职责 典型内容
应用层 业务逻辑 传感器数据处理、用户交互
中间件层 协议栈、文件系统 MQTT、FATFS、OTA
驱动抽象层 硬件接口封装 UART、SPI、I2C 操作接口
硬件驱动层 直接操作寄存器 GPIO配置、中断处理

我曾经接手过一个项目,驱动层直接调用了应用层的回调函数。结果换了一个MCU,所有驱动都得重写,因为应用层的回调接口变了。这就是分层没做好的代价。

我的建议:每一层都定义好「向上提供的接口」和「向下需要的接口」。接口用结构体封装,方便替换实现。

3.3 高内聚低耦合:模块内部要紧密,模块之间要松散

高内聚,就是模块内部的功能要紧密相关。低耦合,就是模块之间的依赖要尽可能少。

你想想看,如果一个模块既处理传感器数据,又负责网络通信,还管着LED闪烁——这就是低内聚。改传感器逻辑的时候,不小心把网络通信的缓冲区给改了,bug就来了。

低耦合怎么做?我分享一个实用技巧:

// 不好的设计:模块之间直接调用
void sensor_task(void) {
    int data = read_sensor();
    network_send(data);  // 直接依赖网络模块
}

// 好的设计:通过消息队列解耦
void sensor_task(void) {
    int data = read_sensor();
    msg_queue_send(MSG_SENSOR_DATA, &data);  // 只发消息,不管谁处理
}

void network_task(void) {
    msg_t msg;
    msg_queue_recv(&msg);
    if (msg.id == MSG_SENSOR_DATA) {
        network_send(msg.data);
    }
}

我在项目中遇到过最典型的耦合问题:全局变量满天飞。一个全局变量被5个模块读写,改一个地方,其他4个地方全崩。后来我强制要求:模块之间只能通过函数接口或消息队列通信,禁止直接访问全局变量。

避坑指南:我曾经因为一个全局的「系统状态标志」被多个模块同时修改,导致死锁。后来改成每个模块维护自己的状态,通过事件通知机制同步。从此世界清净了。

3.4 关注点分离:把不同的事情分开处理

关注点分离,就是把不同维度的逻辑拆开。比如:业务逻辑和硬件操作分开,数据处理和界面显示分开,实时任务和非实时任务分开。

说白了,就是「别把鸡蛋放在一个篮子里」。一个函数里既做数学计算,又操作GPIO,还打印日志——这就是典型的关注点混在一起。

我常用的做法:

  • 业务逻辑:只关心「做什么」,不关心「怎么做」
  • 硬件操作:只关心「怎么控制硬件」,不关心「为什么控制」
  • 日志调试:独立模块,不影响主流程
  • 错误处理:统一策略,不分散在各处

举个例子:

// 关注点混在一起(不推荐)
void handle_button_press(void) {
    // 业务逻辑
    if (press_count > 5) {
        // 硬件操作
        GPIO_ResetBits(GPIOB, GPIO_Pin_12);
        // 日志
        printf("enter config mode\n");
        // 状态管理
        system_mode = MODE_CONFIG;
    }
}

// 关注点分离(推荐)
void handle_button_press(void) {
    if (is_config_mode_triggered()) {  // 只判断业务逻辑
        event_post(EVENT_ENTER_CONFIG);  // 发事件,不直接操作
    }
}

void event_handler(event_t evt) {
    switch (evt) {
        case EVENT_ENTER_CONFIG:
            led_set_config_mode();  // 硬件操作交给驱动
            log_info("enter config mode");  // 日志交给日志模块
            system_set_mode(MODE_CONFIG);  // 状态管理交给状态模块
            break;
    }
}

嗯,这里要注意:关注点分离不是让代码变复杂,而是让每个关注点可以独立修改、独立测试。你想想看,如果业务逻辑和硬件操作混在一起,换一个LED灯的控制方式,业务逻辑也得跟着改——这不合理。

总结一下这四个原则的关系:

  • 模块化是基础,把系统拆成小块
  • 分层是组织方式,让小块有层次
  • 高内聚低耦合是质量指标,衡量拆得好不好
  • 关注点分离是设计思路,指导怎么拆

这四个原则不是孤立的,它们互相支撑。模块化做得好,自然就高内聚低耦合;分层清晰了,关注点自然就分开了。

最后说一句:这些原则不是教条,是经验。我在项目里踩过的坑,总结出来就是这些原则。你刚开始可能觉得「多此一举」,但等你维护一个10万行代码的嵌入式项目时,就会明白——好的架构设计,省的是你未来的时间。