提到IDD(Intent-Driven Development,意图驱动开发),很多朋友第一反应是:这不就是把需求写清楚一点吗?我第一次听到这个说法时也是这么想的,直到团队在重构一套逆变器控制代码时,因为一段“直流母线过压到底该电压环管还是保护逻辑管”的争执,在会议室里吵了一个下午,我才意识到,IDD真正要解决的问题不是“多写文档”,而是让每个构块的边界、行为和意图在动代码之前就变得无歧义。从那以后,我开始尝试给系统的关键部件写构块规格说明书,结果发现,这份文档不仅平息了争论,还让测试用例、嵌入式联调、新人上手都变得顺了很多。
这篇文章不打算讲太理论的东西,而是从一个在嵌入式控制领域摸爬滚打多年的人的角度,结合一个逆变器控制构块的真实案例,聊聊构块规格说明书到底是什么、怎么写、写的时候容易踩哪些坑,以及它和IDD之间到底是什么关系,方便正被模块拆分、接口争议、故障状态混乱折磨的软件或固件团队参考。
1. 先搞清楚:IDD到底在驱动什么
1.1 意图不是需求,也不是方案
谈到IDD,最容易被混淆的概念就是“意图”和“需求”。需求描述的是系统应该做什么,比如“逆变器需要维持直流母线电压稳定”,这句话谁都能写,但写完之后代码该怎么组织、模块之间怎么协作,完全推不出来。意图则更偏向“为什么这个构块存在、它要对外承诺什么、它不希望谁来替它操心”。
举一个生活化的类比:你去餐厅点菜,需求是“我要一份少辣的宫保鸡丁”。如果只把这句话丢给后厨,后厨不知道你是不喜欢辣椒本身,还是只接受微辣、还是需要辣酱单独放。而意图驱动开发,是让顾客在点菜时明确说:“我希望吃到鸡肉的鲜嫩和花生的酥脆,辣度作为调味不能掩盖主味。”后厨听了这个意图,自然会调整炒制顺序和调料比例。
落到具体实现上,意图应该是“能用来做决策的”,它要在你面临设计取舍时给出倾向。比如逆变器电压环的意图是“维持母线电压动态稳定,对外表现为一个低输出阻抗的电压源”,那在写控制闭环的时候,带宽设计、抗扰动策略、软启动时间就都有了依据;如果意图只是“控制好电压”,那到底是以稳态精度优先,还是以动态响应优先,又成了吵架的由头。
1.2 构块规格说明书在IDD里的位置
IDD强调由意图驱动设计,而承载“意图+结构+行为”的产物,就是构块规格说明书。它区别于传统的需求文档,也区别于详细设计文档。需求文档通常站在系统外部描述用户价值,详细设计文档往往事无巨细地描述内部实现,而构块规格说明书站在构块与构块的交界处,表达三个问题:这个构块向外界承诺什么、外界必须满足什么条件、内部必须遵守什么约束。
这个概念有点像硬件领域的芯片数据手册。一块电源芯片的数据手册会写清楚输入电压范围、输出电压精度、使能逻辑、保护阈值、时序要求,但不会告诉你芯片内部用了几层poly、哪一个晶体管尺寸如何。软件或固件构块的规格说明书也应该这样:对外说清接口与行为契约,对内只约束关键质量属性,把具体实现留给开发人员。正因为如此,它才能成为实现、测试、集成、维护阶段共同引用的“单一事实来源”。
在我们的实践中,构块规格说明书不是软件开发完成后的补记文档,而是开发过程中先行的设计契约。团队拿到一个需求后,先识别边界,再写意图和接口,达成一致后才进入编码。代码实现反而成了一个“验证规格书是否可行”的过程,而不是一个“边写边想边界”的过程,这是IDD和传统开发流程最大的差别。
1.3 什么时候写这份说明书
一个常见的问题是:系统都还没定型,怎么敢先写规格书?我以前也有这个顾虑,后来发现,规格说明书侧重的是“契约”而不是“实现”,它允许接口演进,但要求边界和意图必须提前稳定。
比较实用的做法是:在技术方案评审之前写第一版,在原型验证之后修正接口细节,在正式开发之前冻结对外契约。第一版可以很粗,甚至只写意图声明和构块列表,但要能回答“为什么存在”。随着系统的细化,再逐步补充状态机、数据规格和验收场景。这样做的好处是,构块的边界可以在编码前通过评审暴露问题,不至于等代码写完了才发现模块划分不合理。
另一个合适的时间点,是重构旧系统的时候。老代码往往没有清晰的模块边界,如果直接动手拆代码,很容易被现有结构带偏。反过来,先根据现有行为整理出一份规格说明书,把“实际是什么”和“应该是什么”对照起来,再按意图重新划分构块边界,重构会安全很多。我们那次重构逆变器控制代码,走的也是这条路。
2. 一份能用的构块规格说明书包含哪些要素
2.1 意图声明:一句话说清“为什么存在”
一份规格说明书最重要的,不是接口列表,而是顶部的意图声明。我见过不少团队写的规格说明书,一上来就是函数原型、数据结构、流程图,读完根本不理解这个模块存在的意义。意图声明要对“为什么存在”这个问题给出说服力。
好的意图声明通常包含三个要素:服务对象、核心承诺、非目标。以逆变器电压环构块为例:
- 服务对象:电流环和PWM调制器。
- 核心承诺:将直流母线电压稳定在给定目标值,动态响应满足负载突变要求。
- 非目标:不承担过压/欠压的硬件保护职责,保护逻辑属于独立构块。
有了这个声明,职责边界就清楚了。电压环可以因为扰动产生较大的电流参考值,但如果硬件过压需要立即关断,那是保护构块的事情,两个模块不需要互相耦合。这个区分,当年在评审会上争了一下午,其实用一句话就能定下来。
2.2 接口与上下文:画清边界
接口部分最容易写成“函数签名列表”,但真正有价值的接口定义,需要包含语义信息。所谓语义接口,就是不仅说清“输入是什么、输出是什么”,还要说清“调用这个接口意味着什么、调用方需要承诺什么”。
比如电压环构块有一个输入叫“母线电压反馈值”,如果只写一个float变量,那实现者和测试者都可能会一头雾水:单位是伏特吗?更新频率是多少?是滤波后的值还是原始采样值?发生故障时会不会保持最后的正常值?这些语义如果不明确,接口就等于没有定义。所以我一般会在规格说明书里用一张表,列出每个接口的符号、单位、范围、更新语义、无效值处理方式。
上下文描述也很关键。要写明这个构块的上游和下游是谁,它的运行环境是什么。还是拿逆变器来说,电压环的输入依赖功率板上的ADC采样,输出要喂给电流环。如果ADC采样周期是100微秒而电压环规格书写的是“每1ms更新一次参考值”,这两个信息不一致的问题,在写规格书阶段就能被提出来,而不是等到联调时才发现波形乱跳。
2.3 行为状态机与异常边界
状态机是构块规格说明书里最直观也最容易出错的部分。它描述的是构块在不同条件下会处于什么状态、发生什么迁移、迁移时执行什么动作。对控制类构块来说,状态机不是流程图,它是系统“活”的体现。
我在实际项目中有一个强烈感受:如果状态机只画正常流程,那这份规格书基本等于没写。嵌入式系统里,大量的问题都出在异常状态上。母线过压、欠压、过流、通信丢失、参数非法,这些场景都必须在状态机中占一个位置。不要想着“异常处理写代码时再补”,因为只要代码阶段的压力一来,第一个被砍掉的就是异常处理。
异常边界还需要明确优先级。同一时刻可能同时发生过压和通信丢失,那构块应该先响应哪个?状态机的迁移条件和优先级必须写清楚,否则两个开发者可能实现出完全不同的行为,而测试用例却都显示“通过”。
2.4 数据、质量属性与验收场景
除了接口和行为,规格书还要定义数据字典和质量属性。数据字典描述构块内部使用的主要变量,包括类型、精度、更新频率、滤波策略。这么做的好处是,不会出现两个构块对同一个物理量用了不同单位而各自校验都“没问题”的情况。
质量属性是不能直接写进状态机的需求,比如控制周期的抖动上限、电压纹波的最大值、故障检测到保护动作的最大延迟。这些指标最好用数字定义,因为“响应要快”和“响应要快,3个控制周期内完成”是完全不同的两件事。量化之后,后续的性能测试才有依据。
验收场景则把上面这些内容转化成可执行的测试方向。它可以是一个用例文本,也可以是自动化测试用例的模板。比如“给定目标电压为400V,母线电压从350V阶跃到400V,观察电压环输出电流参考值的超调量小于5%,调节时间小于50ms”,这样的场景写完,测试人员基本不需要再动脑,直接照着搭环境就行。构块规格说明书是不是“能用”,某种意义上就看验收场景能不能被测试直接消费。
3. 动手写一份:逆变器电压环控制构块规格说明书
3.1 场景与背景设定
为了不空谈理论,我拆一个我们实际做过的案例:一台三相光伏逆变器,系统整体由MPPT模块、母线电压环、电流环、PWM调制器、保护逻辑、通信管理组成。传统做法是每个工程师各写各的模块,最后联调。老代码里的电压环控制在4个模块里都散落着代码,状态分散,排查问题很困难。我们的目标是用IDD方法,围绕关键构块编写规格说明书,再把代码按照规格重构。
背景信息给清楚之后,整个系统的控制分层是这样的:最上层是功率调度,决定逆变器应该输出多少有功;中间是电压环,负责维持直流母线电压;底层是电流环和PWM,负责把电压环给出的电流参考值跟踪到位。电压环构块处在承上启下的位置,它的规格说明书直接影响整个系统的动态性能。
3.2 意图与上下文定义
电压环构块的意图声明,我们最终定稿为:“接收功率调度给出的母线目标电压,根据实际母线电压反馈计算电流参考值,输出给电流环,使母线电压在负载突变时保持稳定;本构块不承担硬件级过压/欠压保护动作,保护由独立构块实现。”
上下文部分,我们明确写了上、下游模块以及关键信号流:
- 上游:功率调度(目标电压)、ADC采样模块(母线电压反馈)。
- 下游:电流环(电流参考值)、保护逻辑(状态上报)。
- 关键信号:母线电压采样周期固定100微秒,电压环控制周期1毫秒;目标电压更新由通信指令触发,非周期性。
写到这里就发现一个矛盾:控制周期1毫秒,采样周期100微秒,那电压环看到的是10个采样点的滤波结果,还是一种周期性多速率协调机制?这个问题如果不定义清楚,实现和测试都会乱。最终我们在规格书里规定:电压环每1毫秒运行一次,内部使用一个一阶低通滤波器处理10个采样点,滤波器截止频率为100Hz。这样,后续的算法设计、测试边界全部对齐。
3.3 接口与行为规格的落地写法
接口部分,我们没有急着写函数原型,而是先列语义接口。示意如下:
| 接口名称 | 方向 | 类型/单位 | 语义说明 | 取值范围 |
|---|---|---|---|---|
| 目标电压 | 输入 | float,V | 功率调度下发的母线电压目标值,非周期更新,需低通平滑处理 | 300~800V |
| 母线电压反馈 | 输入 | float,V | ADC采样经过滤波后的母线电压值,100微秒更新一次 | 0~850V |
| 负载状态标志 | 输入 | uint8 | 指示当前是否处于重负载切换过程,供电压环调整响应策略 | 0或1 |
| 电流参考值 | 输出 | float,A | 给电流环的有功电流参考,限制在正负最大电流范围内 | -50~50A |
| 构块状态 | 输出 | enum | 当前状态机的运行状态 | OFF/SOFT_START/RUN/... |
行为规格部分,我们用文字加列表描述状态机。状态机定义如下:
- OFF:初始状态,此时不输出任何电流参考值,等待使能信号。
- SOFT_START:软启动状态,目标电压按斜坡从0升到设定值,电流参考值限幅在较小范围,防止启动冲击。
- RUN:正常运行状态,电压环按PI控制器计算电流参考值,输出范围受最大电流限制。
- OVP_FAULT:母线过压状态,立即置过压标志并通知保护构块,电流参考值降为0。
- UVP_FAULT:母线欠压状态,通知保护构块,禁止输出电流参考值。
- FAULT_CLEAR_WAIT:故障状态保持一段时间,等待硬件复位或上位机清除指令。
迁移条件里,我们特别明确了优先级:OVP_FAULT和UVP_FAULT的检测优先级高于其他所有状态迁移;软启动如果中途发生过压,也必须直接切到OVP_FAULT,而不是等待斜坡完成。
3.4 用表格把约束和验收场景收口
构块规格说明书里,约束指标必须收口成可测量的数值。电压环的约束指标如下:
| 指标 | 数值 | 测试条件 |
|---|---|---|
| 电压稳态误差 | ≤1% | 母线电压400V,负载稳定 |
| 电压纹波 | ≤5V | 满载,PWM开关频率10kHz |
| 电压阶跃超调 | ≤5% | 目标电压从350V阶跃至400V |
| 调节时间 | ≤50ms | 目标电压阶跃,误差带2% |
| 故障响应时间 | ≤1ms | 母线电压超过阈值并触发OVP_FAULT |
| 控制周期 | 1ms±0.1ms | 控制器主循环周期 |
这些指标写完之后,我们又补了验收场景。其中一个场景是:“在光伏输入功率稳定、负载为10kW的条件下,将母线目标电压从350V调整为400V,记录母线电压波形,验证超调量小于5%、调节时间小于50ms。”另一个是:“模拟母线电压超过阈值820V,验证1ms内构块状态切换至OVP_FAULT,且电流参考值降为0。”写清楚这些,测试人员可以直接按表搭环境,不加戏,不自由发挥,联调效率提高了一大截。
因为规格书把意图写在了第一位,后面所有行为、状态、约束都围绕意图展开,代码实现阶段基本不需要再回答“这个变量放这里行不行”这类问题,跟着规格走就行了。
4. 编写中的常见坑与排查实录
4.1 意图写得像团队愿景,不解决任何决策问题
第一个坑是,意图声明写得太宏观。比如“保证逆变器稳定高效运行”,这句话放哪个构块上都能套,根本没法指导设计。我自己一开始也犯过这个错,写出来的意图被老工程师一句话噎回来:“这句话说了等于没说。”
要避免这个坑,最好的办法是写完意图之后做一个“删除测试”:如果把这个意图从规格书里删掉,后续的接口和状态定义是否会发生变化?如果不会,说明意图写得不够具体。真正有用的意图,应该是在你犹豫“这个异常该不该在这里处理”时,能给你明确提示的那句话。比如“保护逻辑独立实现”,就直接决定了过压判断不能写在电压环里。
4.2 接口边界漂移,构块越改越胖
第二个坑发生在开发过程中。由于联调压力,大家会把一些本来不属于构块的功能顺手加进去。比如为了调试方便,电压环内部加了一个手动写PWM占空比的“后门”,虽然功能上只是多一个接口,但它破坏了构块的边界。规矩一旦被打破,后面就会有更多“临时”功能进来,构块越改越胖,最终回到重构之前的状态。
排查这个问题可以用代码评审的一个问题来把关:“这个接口或变量的引入,是为了解决哪个意图声明里提到的问题?”如果答不上来,就说明它不属于这个构块。我们后来把这条写进了评审检查单,靠规格说明书对实现进行“反推审查”,边界漂移的问题明显减少了。
4.3 状态机只画了正常流程,故障状态全看代码
第三个坑是状态机漏掉了异常路径。很多开发者画状态机的时候满脑子都是“正常模式下怎么跑”,至于过压了怎么办、欠压了怎么办、通信断了怎么办,往往在代码里用几个if硬凑。但规格书的价值恰恰在于异常路径的确定性。
我的建议是,状态迁移表里专门给异常迁移留一列:正常迁移写一个优先级,异常迁移必须标注“是否允许打断当前正在执行的迁移动作”。拿电压环举例,如果在软启动过程中收到过压信号,是等斜坡完成再触发OVP_FAULT,还是立即触发?我们的规格书写了“立即触发”,但代码实现曾经做成了“等斜坡完成”,直到联调时模拟过压才暴露。如果规格书评审阶段就盯着迁移表逐行过,这个问题根本不会流到测试阶段。
4.4 规格书写完就“入档”,跟代码彻底脱节
还有一个更常见的坑,是规格书在项目启动时热情高涨地写完,然后被扔进共享盘里吃灰,代码和规格书迅速分道扬镳。想避免这个问题,光靠自觉不够,最好把规格书和代码放在同一个仓库里,随代码一起评审、一起变更,这样规格书和实现的对应关系才有人维护。
我们目前的做法是,规格书以Markdown格式放在源码仓库的docs/specs目录,每次涉及构块行为变更的MR,必须同时更新对应规格书的章节。一开始觉得繁琐,但习惯了以后,规格书反而成了代码评审最好的参考物,因为评审人可以直接对照规格书判断改动是否符合构块边界。如果你在团队里推IDD,一定要把“规格书随代码走”作为基本纪律。
4.5 一个小技巧:让规格书成为测试用例的来源
最后分享一个好用的实践:把规格书里的验收场景直接转成测试用例,而不是测试人员另起炉灶去猜。我们电压环构块的测试用例,几乎就是从规格说明书里“复制”出来的。
例如规格书里写了“故障响应时间≤1ms”,测试用例就定义为“在电压环RUN状态,通过模拟信号将母线电压瞬间拉高到820V,测量从状态切换到电流参考值降为0的时间”。更进一步的玩法是,在代码里为状态机增加可观测的探针点,这样自动化测试可以精确判定状态迁移是否发生在正确的时间点。规格书、测试用例、探针点三者对齐以后,回归测试变得非常有针对性,改动代码之后跑一轮就知道有没有破坏原有的行为契约。
我个人在这几轮迭代里的最大体会是,IDD和构块规格说明书并不是什么高深理论,它真正厉害的地方,是把“写代码前先想清楚”这件事变成了一种人人可执行的工程纪律。如果你现在正在做一个模块边界模糊、接口经常改、故障状态全靠代码里面体现的系统项目,不妨挑一个核心构块,先写一份一页纸的意图声明,再补上接口和状态机。这个过程可能会暴露出很多之前被忽略的矛盾,但正是这些矛盾暴露得越早,项目后期救火的时间就越少。