4. 详细设计阶段:模块级设计规格、数据结构设计、算法设计、伪代码与流程图绘制
详细设计阶段,说白了就是把概要设计里那些「黑盒子」拆开,看清楚里面到底怎么干活。我个人习惯把这个阶段叫做「落地前的最后一公里」——你想想看,概要设计决定了系统长什么样,而详细设计决定了代码怎么写。
我记得刚入行那会儿,总觉得详细设计是浪费时间,恨不得直接撸代码。后来被坑过几次,才明白一个道理:没有详细设计直接写代码,就像没有图纸就砌墙。嗯,这里要注意,详细设计不是写文档给领导看,而是给自己和团队看的施工图。
4.1 模块级设计规格
模块级设计规格,就是给每个模块写一份「身份证」。我建议至少包含以下内容:
| 要素 | 说明 | 我的经验 |
|---|---|---|
| 模块名称 | 唯一标识,与概要设计一致 | 命名要统一,别一会儿用驼峰一会儿用下划线 |
| 功能描述 | 一句话说清楚模块干什么 | 控制在50字以内,说不清楚说明设计有问题 |
| 输入/输出 | 接口信号、数据类型、范围 | 我曾经漏掉一个异常输入,导致现场炸了 |
| 依赖关系 | 调用了哪些模块、被谁调用 | 画个依赖图,一眼就能看出循环依赖 |
| 时序要求 | 执行周期、响应时间、超时处理 | 嵌入式里时序是硬约束,别含糊 |
| 错误处理 | 异常场景、返回值、日志 | 我见过太多「假设不会出错」的设计 |
小技巧:模块规格写完后,找个人读一遍。如果对方能复述出模块的功能和接口,说明写清楚了。如果对方一脸懵,重写。
4.2 数据结构设计
数据结构设计,是详细设计里最容易出问题的地方。我见过一个项目,因为结构体设计不合理,导致内存浪费了30%。
设计数据结构时,我一般遵循这几个原则:
- 最小化原则:能用uint8就别用uint32,嵌入式里每一字节都珍贵
- 对齐原则:结构体成员按4字节对齐,否则编译器会偷偷塞填充
- 可扩展原则:预留保留字段,方便后续升级
- 初始化原则:定义时就用{0}清零,别指望运行时memset
举个例子,我之前做的一个传感器驱动模块,数据结构是这样设计的:
/* 传感器数据结构 */
typedef struct {
uint8_t id; /* 传感器ID,0-255 */
uint8_t type; /* 类型:0-温度,1-湿度,2-压力 */
uint16_t raw_value; /* 原始ADC值 */
float calibrated; /* 校准后的物理值 */
uint32_t timestamp; /* 时间戳,单位ms */
uint8_t status; /* 状态:0-正常,1-异常,2-离线 */
uint8_t reserved[3]; /* 保留,对齐到4字节 */
} SensorData_t;
避坑指南:我曾经在结构体里放了一个指针,结果在不同处理器间通信时出了大问题——指针地址在不同平台上完全不一样。记住:跨平台通信的结构体里不要放指针。
4.3 算法设计
算法设计,不是让你去发明什么新算法,而是把业务逻辑转化成计算机能理解的步骤。我个人的习惯是:先想清楚边界条件,再写核心逻辑。
设计算法时,我重点关注这几个方面:
- 时间复杂度:嵌入式里实时性要求高,O(n²)的算法要慎用
- 空间复杂度:RAM有限,能原地操作就别申请新内存
- 可测试性:算法要能拆成独立的小函数,方便单元测试
- 可读性:别为了炫技写一行代码搞定,团队维护更重要
举个例子,一个简单的滑动平均滤波算法,我通常会这样设计:
/* 滑动平均滤波器 */
#define FILTER_WINDOW_SIZE 10
typedef struct {
uint16_t buffer[FILTER_WINDOW_SIZE];
uint8_t index;
uint8_t count;
uint32_t sum;
} MovingAverage_t;
/* 初始化 */
void MovingAverage_Init(MovingAverage_t *ma) {
ma->index = 0;
ma->count = 0;
ma->sum = 0;
memset(ma->buffer, 0, sizeof(ma->buffer));
}
/* 添加新值,返回滤波结果 */
uint16_t MovingAverage_Add(MovingAverage_t *ma, uint16_t value) {
/* 减去旧值 */
ma->sum -= ma->buffer[ma->index];
/* 存入新值 */
ma->buffer[ma->index] = value;
ma->sum += value;
/* 移动指针 */
ma->index = (ma->index + 1) % FILTER_WINDOW_SIZE;
if (ma->count < FILTER_WINDOW_SIZE) {
ma->count++;
}
/* 返回平均值 */
return (uint16_t)(ma->sum / ma->count);
}
关键点:这个算法只用了一个取模运算,没有除法(除了最后一步),在MCU上效率很高。而且它只用了固定大小的RAM,不会动态分配内存。
4.4 伪代码与流程图绘制
伪代码和流程图,是详细设计的「可视化工具」。我个人的经验是:先画流程图理清逻辑,再写伪代码细化步骤。
伪代码的写法,我推荐遵循这几个原则:
- 用自然语言描述,别写具体语法
- 控制结构用关键词(IF、ELSE、WHILE、FOR)
- 每个步骤只做一件事
- 注释说明「为什么」,而不是「是什么」
下面是一个状态机处理的伪代码示例:
FUNCTION ProcessSensorData(sensorData)
// 输入:传感器原始数据
// 输出:处理后的状态码
IF sensorData.status == ERROR THEN
// 传感器异常,进入错误处理
CALL ErrorHandler(sensorData.id)
RETURN STATUS_ERROR
END IF
// 数据有效性检查
IF sensorData.raw_value < MIN_THRESHOLD OR
sensorData.raw_value > MAX_THRESHOLD THEN
// 数据超出合理范围,标记为无效
sensorData.status = INVALID
RETURN STATUS_INVALID
END IF
// 执行校准算法
calibratedValue = CALL CalibrationAlgorithm(sensorData.raw_value)
sensorData.calibrated = calibratedValue
// 更新状态
sensorData.status = NORMAL
RETURN STATUS_OK
END FUNCTION
流程图方面,我习惯用标准符号:矩形表示处理步骤,菱形表示判断,圆角矩形表示起止,平行四边形表示输入输出。下面是我画的一个传感器数据处理流程图:
核心要点:流程图要覆盖所有分支,包括正常路径和异常路径。我见过太多流程图只画了「happy path」,结果异常场景全靠现场调试。
4.5 详细设计评审要点
详细设计写完后,一定要做评审。我总结了一份检查清单,每次评审前我都会过一遍:
| 检查项 | 检查内容 | 常见问题 |
|---|---|---|
| 完整性 | 所有功能点都有对应的详细设计 | 遗漏边界条件处理 |
| 一致性 | 接口定义与概要设计一致 | 参数类型或顺序不一致 |
| 可测试性 | 每个模块都能独立测试 | 耦合度过高,无法mock |
| 可维护性 | 代码风格统一,注释充分 | 魔法数字满天飞 |
| 性能 | 算法复杂度满足实时性要求 | 循环嵌套过深 |
血的教训:我曾经在一个项目里,详细设计评审时发现了一个死锁问题。如果当时没发现,等代码写完再改,成本至少翻10倍。所以,详细设计评审不是走过场,是真能救命的。
嗯,详细设计阶段的内容就这些。记住一句话:详细设计做得越细,编码阶段就越轻松。我见过太多项目,详细设计草草了事,结果编码阶段反复返工,最后交付质量一塌糊涂。别走那条路。