简介:面向嵌入式系统开发初学者与项目组的软件设计说明书模板,针对设计文档格式不统一、章节缺漏、评审无据可依的问题。模板以规范编号体系为骨架,含封面、版本历史与目录,正文分引言、软件设计约束、软件设计描述三大部分:引言细分为编写目的、读者对象、项目概述、文档概述、定义与参考资料;设计约束覆盖设计目标和原则、设计约束;设计描述给出总体设计、功能说明、功能框图、接口说明、程序编写与源文件列表等小节,可直接套用或按项目裁剪。压缩包共1个doc文件、约64KB,轻便易改,适合课程设计、毕业设计与嵌入式项目立项阶段快速产出规范文档。已有2574人学习下载,可作为团队统一文档规范、评审归档的底稿,也便于对照梳理文档管理、配置管理与质量管理的写作要点。
1. 从一份被退回三次的设计说明书说起:嵌入式系统开发为什么需要模板
答辩前一周,某高校一个做嵌入式系统综合设计的组把说明书交上去,被退回来三次,理由一次比一次具体:功能框图里的方框,在功能说明里找不到对应条目;主程序写完了,出口参数一栏空着;设计约束抄了一段芯片参数,看不出对设计产生了什么影响。三个人代码都跑通了,文档却过不了。
问题不在技术,在骨架。嵌入式系统开发由微处理器、外围硬件、嵌入式操作系统和应用程序四部分构成,每一部分在文档里都该有固定的落点:约束写在哪、总体设计写到什么颗粒度、主程序和子程序的参数怎么填、源文件列表怎么跟代码对齐。这份嵌入式系统开发软件设计说明书模板,把引言、设计约束、设计描述三大块和主程序七要素、子程序六要素固化成了填空结构。它适合正在做嵌入式方向毕设、课程设计、综合设计的人,也适合要给项目出一份能过评审的交付文档的从业者。
2. 需求先立住:读者对象、设计目标与约束清单怎么写才不空
模板的第一章是引言,六节:编写目的、读者对象、软件项目概述、文档概述、定义、参考资料。多数人在这里花十分钟就写完了,然后整份文档在后面开始塌方。引言真正的作用是给全文定规则——谁看、按什么标准看、术语怎么统一。这三件事没定,后面每一章都会各写各的。
2.1 读者对象分层:同一份说明书要同时喂饱四类人
模板原文只要求「简要说明本文档可能的读者对象」。一句话带过,等于没写。我一般把这一节做成一张表,把读者拆成四类,每类给一条明确的阅读路径:
| 读者 | 关注章节 | 阅读目的 | 期望看到什么 |
|---|---|---|---|
| 评审/导师 | 2、3.1、3.2 | 判断设计是否完整、可追踪 | 功能编号能对上框图方框 |
| 接手开发者 | 3.3、3.4 | 找到改哪里、加什么 | 函数原型、参数约定、文件职责 |
| 测试人员 | 3.2、3.3 | 构造输入、判断输出 | 接口异常分支、返回值含义 |
| 三个月后的自己 | 2.2、3.4 | 回忆当时为什么这么选 | 约束条件、版本对应关系 |
这张表写完,后面每一章该写多细就有依据了。比如 3.4 源文件列表,如果读者里有「接手开发者」,那列表就不能只写文件名,得写清楚每个文件负责哪几个功能编号。
2.2 设计目标与原则:写成可检验的陈述句
模板 2.1 的示例是「实现用户需求,软件系统具有良好的可扩充性等等」。这类话本身没错,但没有判定条件,评审时无法验证,写跟不写一个样。我的做法是每条目标后面必须跟一个「判定方式」,也就是怎么证明它做到了。
比如「可扩充性」这一条,不要停在形容词上,落到代码结构:新增一路传感器采集,不改动主循环调度代码,只在任务注册表里追加一行。这样目标就有了一个可指认的物理位置。
/* task_registry.h —— 可扩充性的落地形态:新增任务只追加一行注册 */ typedef struct { const char *name; void (*init)(void); void (*run)(void); unsigned int period_ms; /* 调度周期,0 表示事件驱动 */ } task_entry_t; /* task_registry.c:主循环只遍历这张表,不关心具体任务是谁 */ extern const task_entry_t g_tasks[]; extern const unsigned int g_task_count;这张注册表就是 2.1 里那句「具有良好的可扩充性」的证据。答辩时被问到「你的可扩充性体现在哪」,直接翻到这一页,指到g_tasks数组,再说明新增任务的改动量是「一行结构体初始化 + 一个 run 函数」,工作量可估算、风险可评估。原则条目同理,「模块间低耦合」可以落成「应用层不直接包含任何寄存器定义头文件」,这句话在 3.3 的源文件依赖关系里能验证。
一句能验证的设计目标,价值远高于十句正确的空话。写这一节的时候不妨反过来想:如果评审只给我三十秒,他能从这句话里问出什么问题,我能不能指到具体位置回答。
2.3 设计约束清单化:硬件平台、操作系统、开发语言、工具链
模板 2.2 列了六类约束:硬件平台、操作系统要求、开发语言、开发工具、容量和性能要求、灵活性和配置要求。最常见的写法是抄一段芯片手册,主频多少、Flash 多大,抄完就完了。约束的意义不在陈述事实,在于说明它逼你做了哪个设计决策。
我把每一条约束写成四列:约束项、取值、来源、影响的设计决策。
| 约束项 | 取值 | 来源 | 影响的设计决策 |
|---|---|---|---|
| 主控主频 | 72 MHz | 硬件方案定型 | 采样周期下限取 1 ms,不做浮点密集运算 |
| RAM | 48 KB | 选型成本约束 | 协议缓冲区固定 512 B,禁用动态内存分配 |
| 实时性 | 主循环周期 ≤ 10 ms | 被控对象响应要求 | 中断里只置标志,耗时处理全部放前台 |
| 开发语言 | C99 | 团队既有工具链 | 不使用变长数组和复合字面量的部分特性 |
| 操作系统 | 无(裸机前后台) | 功能规模不需要 RTOS | 用状态机替代任务阻塞等待 |
容量和性能这类约束一定要写数字,而且要写到能卡住人的程度。光写「RAM 48 KB」没用,得让编译器替你执行这条约束——把上限写进链接脚本,超了直接链接失败,而不是等到运行起来跑飞了才发现。
/* link.ld 片段:把容量约束变成编译期检查,而不是事后回忆 */ MEMORY { FLASH (rx) : ORIGIN = 0x08000000, LENGTH = 256K /* 代码+常量上限 */ RAM (rwx) : ORIGIN = 0x20000000, LENGTH = 48K /* 全局+堆+栈上限 */ } /* 栈顶单独留出,方便用填充法测峰值栈深 */ _Min_Stack_Size = 0x800;链接脚本里LENGTH写死之后,凡是 RAM 用量超标的版本都过不了构建,这比在文档里写一句「注意内存占用」有用得多。文档里那条约束因此也有了出处:不是我想写,是工程里真的这么卡着。
约束这一节还有一个容易漏的点:灵活性和配置要求。如果产品要支持不同型号的传感器或不同的通信波特率,得在约束里说明配置项存在哪、由谁改、改了要不要重新编译。这条不写清楚,后面 3.2 接口说明会变成一锅粥。
3. 总体设计怎么落地:功能说明、功能框图与接口说明的对应关系
第三章是整份文档的主体,模板给了三块:总体设计(功能说明、功能框图)、接口说明、程序编写。这一章写得好不好,判断标准只有一条——功能说明、框图、接口、代码能不能互相追踪。模板 3.1 里那五条要求(完全、准确、一致、直观简洁、可追踪)里,真正会卡人的是可追踪。
3.1 功能说明:每一条功能都要能被追踪
功能说明不要写成散文。我一般做成编号表,每条功能固定五个字段:触发条件、输入、处理、输出、异常分支。编号从 F-01 开始,全文只认这个编号。
| 编号 | 功能 | 触发条件 | 输入 | 输出 | 异常分支 |
|---|---|---|---|---|---|
| F-01 | 周期采集 | 1 ms 定时中断置标志 | ADC 四通道 | 原始值数组 | 连续 3 次超时转 F-05 |
| F-02 | 数据处理 | F-01 完成 | 原始值数组 | 工程量 | 越量程丢弃并计数 |
| F-03 | 上报 | 上报周期到 | 工程量 | 通信帧 | 发送失败重试 3 次 |
| F-04 | 参数配置 | 收到配置帧 | 帧载荷 | 更新参数区 | 校验失败回错误码 |
| F-05 | 故障处理 | 任一异常被置位 | 故障码 | 故障指示 + 上报 | 恢复后需人工确认 |
有了编号,后面所有章节都能引用它:框图方框标注 F-01,接口说明标注「服务于 F-03」,源文件列表标注「app_sample.c 实现 F-01、F-02」。这就是模板里「可追踪」的具体实现方式——不是靠文字描述,靠编号串联。
异常分支这一列最容易被省略。嵌入式项目的麻烦几乎都出在异常路径上:传感器不应答、通信超时、电源跌落。功能说明里把异常写出来,等于提前把测试用例列了一半。
3.2 功能框图:面向数据流与面向对象两种画法的选择
模板给了两条路:面向数据流用层次图/HIPO 图/结构图,面向对象用类图/状态图/顺序图。裸机或轻量 RTOS 的嵌入式项目,绝大多数走第一条,因为系统本质是「中断驱动 + 前后台循环 + 若干状态机」,类图反而绕。
我习惯分四层画:应用层(功能任务)、中间件(协议解析、参数存储、滤波算法)、驱动层(GPIO/UART/ADC/定时器)、硬件层(引脚与外设)。每一层的方框旁边必须标功能编号,标不出来的方框说明这个模块没有需求来源,要么补需求,要么删掉。
框图里画了状态机,代码里就得有对应的状态机实现,不能只是纸上有个圈。
/* app_fsm.c —— 框图里“采集流程”方框的落地实现 */ typedef enum { ST_IDLE, ST_SAMPLE, ST_PROCESS, ST_REPORT, ST_FAULT } app_state_t; static app_state_t s_state = ST_IDLE; static unsigned char s_retry; void app_fsm_step(void) { switch (s_state) { case ST_IDLE: /* 对应 F-01 的触发条件 */ if (timer_flag_take(TIM_SAMPLE)) s_state = ST_SAMPLE; break; case ST_SAMPLE: if (adc_read_blocking(&s_raw) == 0) { s_retry = 0; s_state = ST_PROCESS; /* 正常路径 F-01 -> F-02 */ } else if (++s_retry >= 3) { s_state = ST_FAULT; /* 连续 3 次超时转 F-05 */ } break; case ST_PROCESS: app_process(&s_raw, &s_value); /* F-02:越量程在内部丢弃 */ s_state = ST_REPORT; break; case ST_REPORT: s_state = (comm_send(&s_value) == 0) ? ST_IDLE : ST_REPORT; break; /* F-03 发送失败原地重试 */ case ST_FAULT: fault_indicate(); /* F-05:指示并等待人工确认 */ if (fault_ack_take()) s_state = ST_IDLE; break; } }这段代码的价值不在逻辑多复杂,在于它和 3.1 那张表是一一对应的:每个case都能指回一个功能编号,每个异常跳转都能指回「异常分支」那一列。评审翻到框图,看到「采集流程」方框标注 F-01/F-02/F-05,再翻到这页代码,追踪链就闭合了。
框图旁边还可以补一段文字说明,讲清楚层与层之间的调用方向:应用层只调中间件和驱动层提供的接口,驱动层不反向调用应用层。这条约定看着简单,但凡是没写下来的项目,后期十有八九会出现驱动里直接操作业务标志位的情况。
3.3 接口说明:把「软件接口」拆成三层写
模板 3.2 只有一句「详细描述软件接口」,这是全文最容易写虚的一节。我的做法是拆成三层,各写各的:
第一层是对外通信接口,写帧格式。用表格列字段:帧头、长度、命令字、载荷、校验、帧尾,每个字段写字节数、取值范围、含义。校验算法要写清楚是累加和还是 CRC,多项式是什么,初值是什么。
第二层是对内模块接口,写函数原型和返回值约定。上层只依赖这组原型,不碰寄存器,这样换硬件时改动范围可控。
/* iface_sensor.h —— 对内模块接口:上层只依赖这组原型 */ #ifndef IFACE_SENSOR_H #define IFACE_SENSOR_H /* 返回值约定:0 成功,-1 设备无响应,-2 通道号越界,-3 数据未就绪 */ int sensor_init(void); int sensor_read(short *out, unsigned char ch); /* ch 取值 0~3 */ void sensor_power(unsigned char on); /* 0 关断,1 上电,用于低功耗 */ #endif返回值约定这一段必须写进 1.5 定义里,全文统一。项目里最常见的翻车是同一个错误码在两个模块里含义相反,一个把 -1 当超时,一个把 -1 当参数错,联调时对着日志排查半天。约定写在文档里,就是给后来人留的后悔药。
第三层是对硬件接口,写引脚分配、外设配置、时序要求。引脚表要标复用功能和上下拉配置;时序要求写关键的建立保持时间、采样时刻、去抖时间。这一层不用抄手册,只写「本项目实际用到的部分」和「对软件有约束的部分」。
三层写完,3.2 接口说明才算有内容。判断标准很简单:一个没参与过这个项目的人,拿着这一节能不能写出一个能跑通的模拟器。能,就写到位了。
4. 详细设计:主程序七要素、子程序参数表与源文件列表
模板 3.3 是程序编写,把主程序和子程序各拆成七项和六项:功能、入口参数、出口参数、局部变量、函数调用、对象调用、流程图。这一节是很多人觉得「形式主义」的地方,恰恰也是最能在答辩里省时间的地方——评委翻到这里,通常是随机指一个函数问「这个参数什么意思」。
4.1 主程序七要素:没有参数也要写明「没有」
主程序一般没有入口参数和出口参数,由启动文件调用,返回类型是int但通常不返回。模板要求填,就得填,写「无,由启动文件Reset_Handler调用」也比空着强。空着给人的印象是这个组没读懂模板,而不是这个函数真的没有参数。
/* main.c —— 对应说明书 3.3.1 主程序七要素 */ #include "board.h" #include "app.h" static unsigned int s_tick; /* 局部变量:1 ms 时基,仅本文件可见 */ int main(void) { board_init(); /* 函数调用 1:时钟、GPIO、看门狗初始化 */ app_init(); /* 函数调用 2:任务注册、状态机复位、参数加载 */ for (;;) { app_poll(); /* 前台主循环,内部按注册表调度,不阻塞 */ watchdog_feed(); /* 喂狗放循环末尾,任一步卡死都会触发复位 */ } }对着这段代码,七要素可以逐项填:功能是「系统初始化与前后台调度」;入口参数「无」;出口参数「无(正常情况下不返回)」;局部变量只有s_tick,并注明只在定时中断里递增、在主循环里读取;函数调用两项,注明各自完成什么;对象调用写「无,本项目未使用 C++ 对象」;流程图描述初始化顺序和主循环的两步结构。
这里的细节值得多说一句:局部变量为什么要单列并注明可见性。因为嵌入式项目里全局变量滥用是常态,一个变量在中断里改、在主循环里读,如果不标注,接手的人很容易在主循环里对它做非原子操作。文档里标了「仅本文件可见」和「中断内修改」,至少能提醒下一个人加临界区。
4.2 子程序:入口出口参数和局部变量怎么写才有人看得懂
子程序六要素里,最容易写歪的是参数。只写类型不写取值范围,等于没写;写了范围不写边界行为,等于挖坑。我一般用一张小表把参数、范围、越界行为一次说清:
| 参数 | 方向 | 类型 | 取值范围 | 越界/异常行为 |
|---|---|---|---|---|
| out | 出口 | short* | 非空 | 空指针直接返回 -1,不写内存 |
| ch | 入口 | unsigned char | 0~3 | 越界返回 -2,不启动转换 |
| 返回值 | 出口 | int | 0/-1/-2/-3 | 见 1.5 定义表 |
局部变量要写清生命周期和用途。嵌入式里局部变量不只是「函数内的临时值」,还涉及栈深。如果某个函数里开了 256 字节的局部数组,评审问「栈够不够」的时候,答案应该在文档里而不是靠现场算。
/* app_filter.c —— 滑动平均滤波,局部变量占用要写进 3.3.2 */ #define FILTER_WIN 16 short app_filter_avg(short sample) { static short buf[FILTER_WIN]; /* 静态区:16*2 = 32 B,不进栈 */ static unsigned char idx; static long sum; /* 32 位累加,避免 16 位溢出 */ short out; sum -= buf[idx]; /* 减去最老样本 */ buf[idx] = sample; sum += sample; idx = (unsigned char)((idx + 1) % FILTER_WIN); out = (short)(sum / FILTER_WIN); /* 出口:滤波后工程量 */ return out; }这段代码在文档里的对应写法是:功能「对采样值做 16 点滑动平均」;入口参数sample范围「-32768~32767,超出由调用方限幅」;出口参数「滤波结果,范围同入口」;局部变量「buf/idx/sum为静态存储,共 36 B,不占栈;out为栈上 2 B」;函数调用「无」;流程图「减去最老样本 → 写入新样本 → 更新索引 → 求平均返回」。
参数表加这段说明,价值在于把「能不能改窗口长度」「改了影响多少内存」这类问题提前回答了。
4.3 源文件列表与配置管理:让文档和代码版本对得上
模板 3.4 是源文件列表,看起来最机械,实际上最影响交付印象。手写的文件列表十个有八个会漏,尤其是最后几天临时加的调试文件。我的做法是脚本生成,文档里贴生成结果,同时在 1.4 文档概述里注明「本表由脚本生成,代码变更后重新生成」。
# gen_filelist.py —— 扫描 src/ 生成说明书 3.4 源文件列表 import os import hashlib ROOT = "src" rows = [] for dirpath, _, files in os.walk(ROOT): for fn in sorted(files): if not fn.endswith((".c", ".h", ".s")): continue path = os.path.join(dirpath, fn) with open(path, "rb") as f: digest = hashlib.md5(f.read()).hexdigest()[:8] # 8 位摘要,标识版本 rows.append((path.replace("\\", "/"), os.path.getsize(path), digest)) # 直接输出 Markdown 表格行,粘进说明书即可 for p, size, h in rows: print(f"| {p} | {size} B | {h} |")这个脚本跑一遍,源文件路径、字节数、内容摘要一次出齐。摘要是关键:代码改了摘要就变,文档里的表还留着旧摘要,说明文档没跟着更新。这比人工核对可靠得多,也顺便把模板里提到的配置管理和变更管理落到了实处。
版本历史那张表也别糊弄。每一版的作者、参与者、起止日期、变更备注都要填,备注写清楚改了什么,不要写「修改若干」。答辩现场如果被问到「这份文档和代码哪一版是对应的」,能直接翻到版本历史指出来,比解释十分钟都管用。
5. 常见问题与避坑:模板填空最容易翻车的五个地方
模板给的是骨架,填进去的血肉还是得自己长。下面这几条是这类综合设计文档里出现频率最高的翻车点,按现象、原因、解决三段写,能对着自查。
5.1 内容层面的三类断链
第一类:框图有方框,功能说明没编号。现象是功能图画得很漂亮,一问某个方框对应哪条需求就卡住;原因是先画图后补功能,两边各写各的。解决办法是把工序倒过来:先出 3.1 功能编号表,再由表生成框图,图中每个方框必须标注至少一个 F 编号,标不出来的方框直接删掉。评审看到没有编号的方框,第一反应就是「这块是凑数的」。
第二类:设计约束抄手册,不看设计决策。现象是 2.2 写了半页芯片参数,但 3.3 的实现里看不出任何受约束的痕迹;原因是把约束当背景介绍写,而不是当输入条件写。解决办法是每条约束强制补一列「影响的设计决策」,填不出来的约束说明它对本项目没有实际影响,可以删。表格里能留下的,应该是真正卡过你的那几条。
第三类:接口说明只写正常路径。现象是协议帧格式写得很完整,但没写校验失败怎么办、超时重试几次、重试期间新帧到达怎么处理;原因是写接口的时候脑子里只有成功流程。解决办法是每个接口后面强制补三行:校验失败行为、超时阈值、并发或重入限制。接口说明里的异常分支,最后会直接变成测试用例,早写早省事。
5.2 流程层面的两类翻车
第四类:版本历史空着,答辩时文档和代码对不上。现象是评委问「你演示的这个版本对应文档哪一版」答不上来,或者文档里的函数名在代码里已经改了;原因是文档和代码两条线各改各的,中间没有同步动作。解决办法很土但有效:每次提交代码前跑一遍 4.3 那个脚本,重新生成源文件列表,顺手更新版本历史的一行备注。这个动作固定下来之后,文档和代码的偏差不会超过一次提交。
第五类:源文件列表手写,最后一天漏文件。现象是列表里少了两个驱动文件,或者路径还是旧的目录结构;原因不言自明——手工维护的清单一定会过期。解决办法同上,脚本生成。另外提醒一句,调试用的临时文件要么在提交前删掉,要么在列表里明确标注「调试用,不参与发布构建」,别让评审自己去猜哪个文件是干什么的。
还有一种情况值得单独提:多人分工时,每个人填自己那部分,术语和错误码各写各的。这种不一致不会在单章里暴露,但会在接口说明和源文件列表交叉的地方集中爆发。开工前花半小时统一 1.5 定义表,能省掉后面无数次对齐。
6. 验收与答辩:用一份检查表把设计说明书过一遍
文档写完不等于交得出去。我一般让组里在提交前做一遍交叉检查——不是自己看自己的部分,而是每人检查别人写的一章,因为作者对自己的省略是无感的。检查按下面这张表走,每一项都要求「能在文档里指到具体位置」,指不到就算不过。
| 检查项 | 通过标准 | 常见不通过表现 |
|---|---|---|
| 功能可追踪 | 每个框图方框有 F 编号,每个 F 编号有实现文件 | 框图有孤立方框 |
| 约束有落点 | 2.2 每条约束能指到一处设计决策 | 大半是芯片手册抄录 |
| 接口有异常 | 每个接口写了失败、超时、重入 | 只有正常帧格式 |
| 参数有边界 | 入口参数写了范围和越界行为 | 只写类型不写范围 |
| 文件能对上 | 源文件列表与当前代码一致 | 摘要或路径已过期 |
| 版本有记录 | 版本历史每行有实质备注 | 写「修改若干」 |
答辩的节奏也值得提前设计。讲的时候不要按章节顺序从头念,按「需求编号 → 框图 → 代码 → 验收结果」这条链走一遍,三分钟就能让人相信这套设计是自洽的。评委通常会挑两个地方发难:一是功能框图和功能说明的对不上,二是主程序或某个子程序的参数含义。这两处只要文档里编号和参数表是齐的,回答起来就是翻页的事,不用现场组织语言。
还有个具体技巧:把 3.3 的参数表和代码放在同一页或相邻两页。答辩时被问到某个函数,一翻页就能同时看到代码和参数说明,不用来回找。这个排版细节看着小,但能明显减少「你等一下我找找」的尴尬。
我吃过一次亏,代码全跑通,文档里源文件列表是两周前手写的版本,演示时评委随手点了一个列表里的文件,代码里已经改名了。那之后我每次交文档前,都强制走一遍「脚本重生成列表 + 版本历史补一行 + 交叉检查表过一遍」这三步,再没在同一个地方栽过。
这套模板本身不复杂,难的是把它当成工程的一部分去维护,而不是当成一份要交的作业去凑。希望帮到你。
本文还有配套的精品资源,点击获取