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 算法设计

算法设计,不是让你去发明什么新算法,而是把业务逻辑转化成计算机能理解的步骤。我个人的习惯是:先想清楚边界条件,再写核心逻辑。

设计算法时,我重点关注这几个方面:

  1. 时间复杂度:嵌入式里实时性要求高,O(n²)的算法要慎用
  2. 空间复杂度:RAM有限,能原地操作就别申请新内存
  3. 可测试性:算法要能拆成独立的小函数,方便单元测试
  4. 可读性:别为了炫技写一行代码搞定,团队维护更重要

举个例子,一个简单的滑动平均滤波算法,我通常会这样设计:

/* 滑动平均滤波器 */
#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倍。所以,详细设计评审不是走过场,是真能救命的。

嗯,详细设计阶段的内容就这些。记住一句话:详细设计做得越细,编码阶段就越轻松。我见过太多项目,详细设计草草了事,结果编码阶段反复返工,最后交付质量一塌糊涂。别走那条路。