4. 系统架构设计文档:系统框图、模块划分、接口定义、数据流图

好,咱们进入第四个章节。系统架构设计文档,这玩意儿可以说是整个嵌入式软件的“骨架”。你想想看,一个项目几十号人,如果没有一张清晰的架构图,大家各写各的代码,最后联调的时候肯定乱成一锅粥。我见过太多项目,就是因为架构没理清楚,后期改需求改到崩溃。

今天我就把这块掰开了揉碎了讲。核心就四个东西:系统框图、模块划分、接口定义、数据流图。把这四个东西写清楚,你的架构文档就立住了。

4.1 系统框图——先画个“全家福”

系统框图是啥?说白了,就是把你整个系统里所有的硬件和软件模块,用方框画出来,再用线连起来。它回答了一个最根本的问题:系统里都有啥?它们之间怎么连的?

我个人习惯,画系统框图分两步走:

  1. 硬件拓扑图:先画出MCU、传感器、执行器、通信接口(比如UART、SPI、I2C、CAN)。别画得太细,把关键芯片和总线画出来就行。
  2. 软件分层图:在硬件之上,画出你的软件层次。比如驱动层、中间件层、应用层。每一层里有哪些模块,标清楚。

重点来了:系统框图一定要标注清楚通信协议和速率。比如“SPI @ 10MHz”、“UART 115200bps”。我在项目中遇到过,有人画框图只画了线,没写协议,结果硬件工程师和软件工程师对接口的理解完全不一样,白白浪费了两天时间。

4.2 模块划分——别把代码写成“一坨”

模块划分,考验的是你的抽象能力。好的模块划分,每个模块职责单一、接口清晰、可以独立测试。坏的模块划分,就是全局变量满天飞,一个函数干八件事。

我一般遵循这几个原则:

  • 高内聚、低耦合:一个模块只做一件事,并且把它做好。模块之间尽量少依赖。
  • 接口稳定:模块对外的接口(函数名、参数、返回值)一旦定下来,尽量不要改。内部实现随便重构。
  • 可替换性:比如你写了一个“温度传感器驱动模块”,如果以后换了另一款传感器,只需要替换这个模块,上层代码一行都不用改。

举个例子,一个典型的嵌入式系统模块划分:

模块名称 职责描述 对外接口
BSP(板级支持包) 封装MCU外设寄存器操作 bsp_gpio_init(), bsp_uart_send()
Driver(驱动层) 管理具体外设芯片(如传感器、LCD) sensor_read_temp(), lcd_display()
Middleware(中间件) 提供通用服务(如FATFS文件系统、FreeRTOS) f_open(), xTaskCreate()
App(应用层) 实现业务逻辑(如数据采集、报警判断) app_data_collect(), app_alarm_check()

我的小技巧:在文档里,每个模块都要写一段“设计理由”。比如“为什么要把文件系统单独抽成中间件?因为后续产品可能改用LittleFS,换起来方便。” 这样别人看文档时,能理解你的设计意图。

4.3 接口定义——把“约定”写进文档

接口定义,是架构文档里最容易被忽视、但坑最多的地方。接口定义不清楚,联调时就是灾难现场。

接口定义要包含哪些内容?我列个清单:

  • 函数原型:返回值、函数名、参数列表(含参数类型和说明)。
  • 参数范围:比如温度值范围是 -40 ~ 125℃,超出范围怎么处理?
  • 返回值定义:成功返回0,失败返回错误码。错误码要统一枚举。
  • 调用上下文:这个函数可以在中断里调用吗?会不会阻塞?
  • 时序要求:比如“两次调用间隔至少10ms”。

嗯,这里要注意。我曾经在一个项目里,A模块调用B模块的初始化函数,但B模块的初始化依赖某个GPIO的电平状态。A模块不知道这个前提,结果初始化总是失败。后来我们在接口文档里加了一行“调用前需确保GPIO_PIN_5为高电平”,问题就解决了。

看一个接口定义的例子:

/**
 * @brief 读取温度传感器数据
 * @param[out] temp_celsius 输出温度值,单位摄氏度
 * @return 0 成功
 * @return -1 传感器通信失败
 * @return -2 数据校验错误
 * @note 此函数会阻塞约50ms,不可在中断中调用
 */
int sensor_read_temp(float *temp_celsius);

4.4 数据流图——数据是怎么“跑”起来的

数据流图,说白了就是画清楚数据从哪来、经过谁、到哪去。它比框图更动态,能帮你发现数据路径上的瓶颈和风险。

画数据流图时,我习惯用这几个元素:

  • 圆圈:表示数据源或数据终点(比如传感器、云端)。
  • 方框:表示处理模块(比如滤波算法、协议解析)。
  • 箭头:表示数据流向,箭头上标注数据格式和速率。
  • 存储:表示数据暂存的地方(比如环形缓冲区、数据库)。

举个例子,一个温湿度采集系统的数据流:

  1. 传感器(SHT30)通过I2C输出原始数据(16位温度 + 16位湿度)。
  2. 驱动层读取原始数据,存入环形缓冲区。
  3. 中间件层从缓冲区取出数据,进行校准和滤波。
  4. 应用层每1秒读取一次校准后的数据,判断是否超限。
  5. 如果超限,通过UART发送报警信息到上位机。

避坑指南:我曾经在一个项目里,数据流图画得很漂亮,但没标注“数据丢失处理策略”。结果系统在高负载时,环形缓冲区溢出,数据丢了,但没有任何日志。后来我们在数据流图上加了一个“溢出处理”分支,要么丢弃旧数据,要么报警,这才算完整。

4.5 文档模板示例

最后,我给大家一个精简的模板,可以直接套用:

# 4. 系统架构设计文档

## 4.1 系统框图
[此处插入硬件拓扑图和软件分层图]

## 4.2 模块划分
| 模块名 | 职责 | 依赖模块 | 设计理由 |
|--------|------|----------|----------|
| ...    | ...  | ...      | ...      |

## 4.3 接口定义
### 模块A对外接口
- 函数:int module_a_init(void)
  - 功能:初始化模块A
  - 参数:无
  - 返回值:0成功,-1失败
  - 注意:必须在系统时钟初始化后调用

## 4.4 数据流图
[此处插入数据流图,并附文字说明]
- 数据源:传感器 → 驱动层 → 中间件 → 应用层 → 输出
- 关键路径:温度数据从采集到报警,延迟不超过100ms

好了,这一章就讲到这里。系统架构设计文档,说白了就是给整个项目画个“地图”。地图画清楚了,大家走路才不会迷路。下一章咱们聊聊详细设计文档,到时候我会讲讲怎么把每个模块的“内部构造”写清楚。