民用飞机机载软件的适航符合性,这话题在饭桌上讲基本没人爱听,但在工程现场,它是能决定一个项目生死的东西。很多人第一次听到“符合性”两个字,脑子里浮现的是给软件跑一轮测试、出一份报告、盖个章,然后就可以装机飞了。真到现场你会发现完全不是这么回事:测试报告只是证据链里的一个环节,真正被审的是你从项目第一天到交付那天,整套工程活动的逻辑自洽和可追溯。我写这篇东西的出发点很简单——把我这几年在机载软件验证与审定对接上踩过的坑、理清的思路整理出来,给刚入行的验证工程师、系统工程师、以及从互联网或消费电子转过来做航空软件的朋友一个可参照的框架。下面不谈空泛概念,从“证明什么”开始,一路讲到需求追溯、覆盖分析、工具鉴定、常见问题排查,尽量把每个动作背后的理由说清楚。
1. 先把问题摆正:机载软件向谁证明什么
1.1 “符合性”这个词的真实含义
航空领域的“符合性”不是指软件质量好、bug 少、跑得稳,而是指你声称做到的每一件事,都有对应的证据,且这些证据能够被独立的第三方复核。这里的第三方就是审定方派出的审查代表,他们不关心你代码写得漂不漂亮,关心的是:你声明了哪些过程、这些过程的输出在哪、这些输出之间能不能对上、对不上的地方有没有解释。
所以机载软件的适航符合性,本质上是一份工程逻辑的自证材料。它的形式是一大堆文档、数据、记录、报告构成的生命周期数据集合,它的内核是一条闭环的证据链。你可以在某个环节用很土的手工表格,也可以用高度自动化的工具链,只要证据链完整、可追溯、可复核,都能成立;反过来,工具再先进、自动化程度再高,追溯断了、评审记录缺了、问题报告没闭环,一样过不了。
搞清这一点,后面所有的动作就都顺了。你在项目里做的每一件事,都要问自己一句:这件事将来谁来看?他要从哪份材料里看到?他能顺着材料找到上游和下游吗?问不清这三个问题,做的事情大概率是白做。
1.2 从飞机级安全目标倒推到软件等级
机载软件开发不是“先写代码再定要求”,而是反过来的:从飞机级的安全目标往下倒推。飞机级的功能危害评估先识别出各种失效状态,判断每种失效状态的严重程度——是灾难性的、危险性的、较大的、较小的,还是无安全影响。这个严重程度直接决定了系统的设计约束,也决定了系统分配给软件的开发保证等级。
行业里通常用 A 到 E 五个等级来对应这五种严重程度,A 级对应灾难性失效状态,要求最严;E 级基本没有安全影响,过程要求最宽。举个常见的例子,电传飞控的主计算通道软件,一旦失效可能导致飞机失去控制,那它就是 A 级;而客舱娱乐系统里的节目单管理软件,失效最多让乘客看不了电影,那大概率是 D 级甚至 E 级。
这个等级不是给自己贴标签用的,它直接决定后面每一个过程的严格程度:A 级要做的目标数量最多,B 级次之,往下逐级递减。行业里常说 A 级对应七十余个目标,实际数量以标准里的目标表原文为准,不同版本和统计口径会有差异,但那个量级的概念是对的。我见过新手最容易犯的错,是拿 B 级甚至 C 级的做法去套 A 级项目,前期省了力气,到了审定介入阶段全部返工,代价比一开始就按最高要求做还要大。
1.3 为什么软件不能“测够了就算安全”
硬件可以靠冗余、靠失效模式分析、靠疲劳寿命模型来说事,软件不行。软件的状态空间是组合爆炸的:几个整型变量、几个分支、几层嵌套,就能凭空造出天文数字量级的执行路径。更要命的是,软件失效往往不体现在“坏了”,而是体现在“它按照一段谁也没想到的逻辑,做了一件合理但错误的事”。
我做过一个很典型的例子:一个状态机在某两个事件同时到达时,会走一条设计文档里根本没有画出来的路径——不是死循环,也不是崩溃,而是安安静静地用了一个过期的参数。这种问题,靠随机测试几乎测不到,靠代码走查也容易漏,只能靠需求追溯和结构覆盖分析去把“未被执行过的逻辑”逼出来。
正因为测不完,航空软件工程才把重心从“测出多少 bug”转移到“过程是否可控、证据是否完整”。这不是对测试的否定,而是承认测试的边界。你把过程的每个环节都做扎实,才能让审定方相信:即使有残留缺陷,它的概率和影响已经被约束到了可接受的水平。
2. 证据链的骨架:四类生命周期数据怎么分工
2.1 计划类数据:先把“我要怎么做”说明白
计划类数据是整条证据链的头部。它回答的是“这个项目打算怎么做、做到什么程度、谁来负责”。核心的一份是机载软件审定方面的计划文件,业内通常叫 PSAC,它向审定方声明软件的等级、生命周期过程、采用的标准、工具使用情况、以及打算提交哪些数据。PSAC 一旦基线化,后面所有工作都要跟它对得上,改 PSAC 是一件很重的事情。
PSAC 之下还有一组计划:软件开发计划说明开发流程、组织分工、进度基线;软件验证计划说明验证策略、独立性安排、覆盖目标;软件配置管理计划说明基线怎么建、变更怎么控、状态怎么记;软件质量保证计划说明谁来做过程符合性检查、检查什么、记录在哪。除此之外还有三类标准文件——需求标准、设计标准、编码标准,它们约束“写出来的东西长什么样”。
提示:这三类标准最容易被当成摆设。实际上它们是审查代表最爱翻的材料之一,因为从标准就能看出你的团队是不是真的按统一规则在做,还是每个人一套风格。标准写得越具体、越可执行,后面追溯和评审越省事。
2.2 开发类数据:需求、设计、代码和它们之间的对应关系
开发类数据包括软件高层需求、低层需求、软件架构与详细设计、源代码、可执行目标码以及参数数据项。这一层的核心不是文件本身,而是它们之间的对应关系。高层需求必须能追溯到低层需求,低层需求必须能追溯到设计,设计必须能追溯到代码,代码必须能追溯到目标码。
很多团队在这一层栽跟头,不是因为文件写得差,而是因为颗粒度和编号规则没定好。常见的情况是:需求编号到设计编号之间用了人工映射表,前期改得动,后期需求一变,表就烂了。我的做法是在项目启动时就固定一套需求标识规则和追溯颗粒度,比如每条低层需求必须对应至少一条高层需求,每条设计单元必须至少承载一条低层需求,任何一条落单都要在评审里给出理由。
2.3 验证类数据:评审、分析、测试三条腿
验证类数据包括验证用例与规程、验证结果、各类评审与分析记录、追溯数据。这里要强调一点:验证不等于测试。验证包含评审、分析、测试三类手段,测试只是其中最贵、最耗时、也最容易被当成全部的那一类。
评审用来发现需求和设计层面的问题,分析用来处理那些无法通过执行来验证的内容——比如某些算法精度、某些时序约束、某些无法在目标环境中复现的边界条件,测试用来验证可执行目标码的行为。三条腿缺一条,验证的完整性就有缺口。我做过的项目里,分析类证据经常是最薄弱的环节,很多团队写不出像样的分析报告,最后只能硬凑测试用例,效率极低。
2.4 支持类与审定联络类数据:让整条链能被复核
支持类数据包括配置索引、生命周期环境配置索引、问题报告、以及最终的那份软件完成情况总结,业内叫 SAS。审定联络类数据则是整个过程中与审定方之间的往来记录、符合性矩阵、审查问题答复等。
SAS 是最后交付的关键文件,它把前面所有数据串起来,向审定方说明:目标是否达成、未达成的部分如何处理、各数据之间的关系是什么。符合性矩阵则把标准里的每一条目标与你的证据一一对应起来。这两份文件写得好不好,直接决定审查代表能不能在有限时间里看懂你的项目。我见过技术做得很扎实、但 SAS 写得像流水账的项目,审查时被反复追问,白白多花好几周沟通成本。
| 数据类别 | 典型文件 | 谁最关心 | 常见坑 |
|---|---|---|---|
| 计划类 | 审定计划、开发计划、验证计划、配置管理计划、质量保证计划 | 审查代表、项目经理 | 与实际执行两套皮,计划改了执行没改 |
| 开发类 | 高层需求、低层需求、设计说明、源码、目标码 | 开发、验证、审查代表 | 追溯颗粒度不统一,编号规则中途变更 |
| 验证类 | 验证用例与规程、验证结果、评审记录、分析报告 | 验证工程师、审查代表 | 分析类证据缺失,评审记录只有签名没有结论 |
| 支持与联络类 | 配置索引、问题报告、完成情况总结、符合性矩阵 | 审查代表、审定联络人 | SAS 与前期数据不一致,问题报告未闭环 |
3. 把目标拆成可检查的动作:追溯、覆盖与独立性
3.1 需求追溯:双向追溯的颗粒度怎么定
双向追溯的意思是:从任意一条高层需求能一路走到对应的测试用例,从任意一条测试用例也能回溯到它验证的那条需求。听起来简单,做起来最难的是颗粒度。
颗粒度太粗,一条高层需求对应十几条低层需求、几十个测试用例,一旦出问题定位不到;颗粒度太细,每条需求拆成一句话,追溯表变成几千行,维护成本高得离谱。我的经验是按可独立验证的功能点来拆:一条需求如果可以被单独设计、单独编码、单独测,那它就是一个追溯单元。举个具体的例子,“当空速大于阈值时,系统在 50 毫秒内输出告警”可以拆成三条:判定条件、时序约束、输出行为。这三条各自能测,各自能追溯,颗粒度就合适。
3.2 结构覆盖分析:语句、判定与 MC/DC 的差别
结构覆盖分析是把“代码里哪些逻辑被执行过”这件事量化。等级不同,要求不同:最低等级通常要求语句覆盖,中间等级要求判定覆盖,最高等级要求修正条件判定覆盖,也就是常说的 MC/DC。
用一句生活化的话解释这三者的差别:语句覆盖关心“每行代码有没有跑过”,判定覆盖关心“每个 if 的真假两边有没有都走过”,MC/DC 关心“每个判定里的每一个独立条件有没有单独决定过结果”。前两个容易达标,第三个经常把人卡住。
拿一段最常见的代码举例:
if ((a > 0 || b < 10) && c == OK) { /* 正常分支 */ }把三个条件记作 A、B、C,判定结果是 X。判定覆盖只需要 X 为真一次、为假一次。MC/DC 则要证明 A、B、C 每个都能独立影响 X。下面这组用例可以做到:
| 用例 | A(a>0) | B(b<10) | C(c==OK) | 判定结果 | 证明的独立影响 |
|---|---|---|---|---|---|
| 1 | 真 | 假 | 真 | 真 | 基准用例 |
| 2 | 假 | 假 | 真 | 假 | A 翻转导致判定翻转 |
| 3 | 假 | 真 | 真 | 真 | B 翻转导致判定翻转 |
| 4 | 真 | 假 | 假 | 假 | C 翻转导致判定翻转 |
在工程里我不会把这类向量手工列完,而是写脚本让工具去解算——尤其是判定复杂、条件多的时候,人工挑向量几乎必然出错。我建议的做法是:先用工具自动生成候选向量并给出覆盖报告,再人工复核每个向量在业务上是否可达、参数是否合法。工具能告诉你“逻辑上覆盖了”,但告诉不了你“这个输入在真实飞行中不可能出现”。
3.3 验证独立性:谁测、谁判、谁签字
验证的独立性是适航里一个硬约束。简单说,做验证的人不能是写这段代码的人,做判定的人不能是被判定的那个人。等级越高,独立性要求越强:最低等级允许开发人员自己验证自己的代码,中间等级要求验证人员与开发人员不同,最高等级还要求验证人员在组织上、汇报关系上与开发相对独立。
很多团队在这里吃亏。项目赶进度时,让开发顺手把测试写了,看起来效率高,实际上这既违反了独立性要求,也容易让测试用例继承开发人员的思维盲区——他写代码时没想到的边界条件,写测试时同样想不到。我的做法是:开发人员可以写单元级的自检脚本,但正式的验证用例必须由独立验证人员重写或至少重新设计,不能直接复用。
3.4 问题报告闭环:别让报告躺在系统里
问题报告是整条证据链的胶水。每一个在评审、测试、代码走查里发现的问题,都要有记录、有分析、有处理、有验证、有关闭。听起来是常识,但实际项目里最常见的情况是:问题报告开了几百条,关了一半,剩下的因为“影响不大”“下个版本处理”一直挂着,到最后做 SAS 时才发现关不掉。
我处理的方式是给每个问题报告定关闭判据:要么代码改了并回归通过,要么评估后确认不影响安全且给出了书面理由。任何一条问题报告不允许以“已知悉”状态结束。这样做前期累,后期省心,尤其是审定介入阶段,审查代表翻问题报告列表时会一条条看,闭环率是他判断项目成熟度的直接指标。
4. 实操流程:一次完整的符合性表明怎么推进
4.1 阶段一:计划基线与首次审查介入
项目启动后第一件事是把软件等级确定下来,然后基于等级裁剪标准里的目标集,形成项目自己的符合性活动清单。这份清单会直接支撑 PSAC 的编写。PSAC 写完、内部评审通过后,提交审定方,通常会安排一次正式的介入审查,业内常叫第一次阶段介入审查。
这次审查的目标只有一个:确认你的计划合理、等级判定正确、目标裁剪没有漏洞。审查代表会问得很细,比如“这条目标为什么不适用”“这个工具你打算怎么处理”“验证独立性怎么保证”。我的经验是,这一阶段多花两周把计划写扎实,后面能省两个月。计划阶段的问题最好是问题,一旦进入开发阶段再改计划,牵动的数据量是成倍的。
4.2 阶段二:需求、设计与早期验证
计划基线定下来后进入开发和早期验证。这个阶段的关键动作是:需求编写与评审、架构与详细设计、编码、以及针对需求和设计的评审与分析。同时要建立配置管理基线,把每一份输出的版本管起来。
这个阶段我特别想强调需求评审的深度。很多团队的评审停留在“文档格式对不对、章节全不全”,真正的技术评审很少。我推的做法是带着测试视角去评审需求:读一条需求时,当场问出至少三个边界场景。如果这条需求答不上来,说明它本身就有歧义,必须改。这个习惯能让后期验证用例的设计效率提高一大截。
4.3 阶段三:验证执行与覆盖分析
进入验证执行阶段,主要工作是把验证用例在目标环境上跑完,收集结果,做覆盖分析,对未覆盖的部分逐条给出说明。这个阶段往往会有第二次阶段介入审查,审查代表会看你的验证环境、看你的用例设计、看你对未覆盖代码的处理方式。
未覆盖代码的处理是这个阶段最费脑子的部分。有些代码确实不可达,比如防御性编程里的兜底分支、编译器插入的代码;有些代码可达但需要特殊条件,比如故障注入才能触发的路径。前者需要给出分析证据说明为什么不可达,后者需要设计专门的测试方法去覆盖。具体怎么区分、怎么处理,放到第 6 节展开。
4.4 阶段四:完成情况总结与最终审查
所有验证做完、问题报告闭环之后,进入收尾阶段:整理配置索引,编写软件完成情况总结,编制符合性矩阵,提交最终审查。这一步的工作量被严重低估——它不是简单地把前面文件拼起来,而是一次全局一致性检查。
我在这一步通常做三件事:第一,把符合性矩阵里每一条目标对应的证据实际打开看一遍,确认文件版本是最新的、内容真的对得上;第二,把 SAS 里声明的每一句话跟前面的计划、验证结果做交叉核对,任何对不上的地方要么改 SAS,要么改数据;第三,把所有问题报告再过一遍,确认没有漏关的、没有关闭理由含糊的。这三件事做完,最终审查基本不会有大的意外。
| 阶段 | 主要输出 | 关键动作 | 审查形式 |
|---|---|---|---|
| 计划 | 审定计划、各类计划与标准 | 等级确定、目标裁剪、计划评审 | 首次阶段介入审查 |
| 开发与早期验证 | 需求、设计、源码、评审与分析记录 | 需求深度评审、基线建立 | 第二次阶段介入审查 |
| 验证执行 | 验证结果、覆盖分析报告 | 用例执行、未覆盖项说明 | 第三次阶段介入审查 |
| 收尾 | 配置索引、完成情况总结、符合性矩阵 | 全局一致性核对、问题闭环 | 最终审查 |
5. 工具链与工具鉴定:别让自动化工具偷走你的信心
5.1 工具鉴定的判定逻辑
自动化工具能极大提升效率,但也带来一个新问题:如果工具本身出错了,它生成的证据还可信吗?这就是工具鉴定要回答的事。判定逻辑其实很朴素,就问两个问题:这个工具的输出会不会被后续的验证活动检查?如果工具出错,会不会把错误引入最终产品,或者掩盖掉本来能发现的错误?
如果两个答案都是“不会”,那这个工具不需要鉴定,把它当普通开发辅助工具用就行。如果工具输出直接进入产品、且没有任何后续活动能发现它的错误,那它就需要做完整鉴定,等级最高的那类工具要做的事情几乎等同于开发一个同等安全等级的软件——这代价非常大,所以工具选型时要尽量避开这种组合。
我见过团队稀里糊涂地把一个代码生成器用在了最高等级项目上,直到审查阶段才意识到要鉴定,临时补材料,最后不得不改设计、把生成代码转入人工评审流程,工期直接崩了。工具选型必须在计划阶段做,而且要和等级一起考虑。
5.2 工具链配置的几个实操考虑
工具链选型我一般看四点:一是功能覆盖度,需求管理、追溯、测试管理、覆盖分析能不能在一个环境里闭环,跨工具的数据交换最容易出错;二是输出格式的可导出性,证据最终要落到文档里,工具的私有格式会让归档很痛苦;三是脚本化能力,批量操作、批量生成报告、批量校验追溯链,这些都得靠脚本;四是长期可维护性,工具版本升级后历史数据能不能打开,这个在长周期项目里特别重要。
追的自动化,我的建议是核心追溯链一定要有独立的校验脚本,不能完全依赖工具自带的追溯矩阵。工具展示的是“它认为的追溯关系”,而你要确认的是“实际文件里的标识符是否真的对得上”。我写过一个几十行的脚本,定期扫描需求、设计、代码、测试用例里的标识符,把孤立节点、断链、重复编号全列出来,跑一次两分钟,能省掉后期几周的排查时间。
6. 常见问题与排查技巧实录
6.1 覆盖率死活不达标怎么办
这是最常被问到的问题。覆盖率卡住的原因通常有三类:一是代码里有不可达分支,比如防御性判断、编译优化留下的死代码;二是测试用例本身设计不足,没覆盖到某些条件组合;三是代码结构过于复杂,单个函数里嵌套太深,条件之间耦合严重。
排查顺序我建议从第三类开始,先看结构。如果一个函数的判定条件超过四五个,MC/DC 的向量需求会迅速爆炸,而且很难保证所有组合在业务上可达。这时候应该回到设计层面,把函数拆分,降低单个判定的复杂度,覆盖率自然就上去了。这是唯一一个“改代码比写测试更省事”的场景。
处理不可达代码要谨慎,不能自己下结论说“这段代码永远不会执行”就完事。要给出分析证据:从需求层说明该分支对应的保护逻辑在正常和故障场景下都不需要触发,从设计层说明该分支是编译器或框架引入的,配合代码结构分析报告提交。我见过最省事的做法是把不可达分支用一个统一的记录表管理起来,每条注明位置、原因、分析依据,审查时直接给表,比一次一次解释高效得多。
6.2 追溯断链的排查顺序
追溯断链的表象是“某条需求找不到对应的测试用例”或“某个测试用例找不到对应的需求”。排查时按这个顺序走:先确认标识符本身有没有写错、有没有重号,这是最容易出也最容易改的问题;再确认是不是新加的需求没进基线、或者旧需求被删了但引用没清;最后才怀疑是追溯关系本身设计有问题。
我踩过一次坑:需求编号里用了下划线和空格两种分隔符,脚本按一种规则解析,结果另外一半的追溯关系全部解析失败,报了几百条断链,实际代码一条问题都没有。后来我定了死规矩——标识符只允许大写字母、数字和连字符,长度固定,任何人不许例外。这个规矩让后面所有自动化校验的准确率几乎到了百分之百。
6.3 需求变更引发的连锁返工
需求变更是长周期项目的常态,问题不在于变更本身,而在于变更的传播没被控制住。一条高层需求改了一句话,可能牵动低层需求、设计、代码、测试用例、覆盖分析结果、追溯矩阵、甚至计划文件里的描述。
我的做法是给每次变更做一个影响清单,逐项列出受影响的对象和处理动作,处理完逐项签字。这个清单会成为配置管理记录的一部分,也是向审查代表说明变更受控的直接证据。别小看这张清单,它能防住大部分“改了这里忘了那里”的问题。
| 问题现象 | 常见根因 | 处理动作 |
|---|---|---|
| 覆盖率长期不达标 | 判定嵌套过深、条件耦合严重 | 拆分函数、降低单判定复杂度 |
| 某段代码始终不可达 | 防御性分支、编译器插入代码 | 出具结构分析证据,登记不可达清单 |
| 追溯批量报错 | 标识符规则不统一、脚本解析差异 | 统一标识符规则,固定长度与字符集 |
| 变更后测试失效 | 影响范围未识别完整 | 建立变更影响清单,逐项确认处理 |
| 问题报告关不掉 | 关闭判据不明确 | 明确关闭条件,禁止以“已知悉”结案 |
7. 我在项目里总结的几条经验
先说一条最反直觉的:证据的可读性比证据的数量更重要。我见过一个项目提交了几千页验证数据,每份都合规,但审查代表看了两天还没搞清楚这些数据之间的关系,最后要求项目组补一份数据关系说明。同样的内容,如果每份文件开头有三五行的定位说明——这份文件解决什么问题、上游是什么、下游是什么——审查效率能提高数倍。这个习惯我从第二年开始养成,后来所有文档模板里都强制加了这一段。
第二条是关于“提前”的:所有跟审定相关的事情都要提前一到两个阶段做。计划阶段把工具鉴定想清楚,开发阶段把独立性安排清楚,验证阶段把不可达清单理清楚,收尾阶段就不会手忙脚乱。我在项目里给自己定了个规矩,每到一个阶段结束,就拿最终审查的视角把当前数据过一遍,问自己“如果明天审定方来看,我能解释清楚吗”。这个问题问下来,总能提前发现几个大坑。
第三条是关于人的:适航符合性这件事,从来不是一个人的工作,它是开发、验证、配置管理、质量保证、审定联络几个角色的协同产物。任何一个角色缺位,证据链都会有个缺口。我的建议是,项目早期就把这几个角色的职责边界和交接物定义清楚,交接物具体到“什么文件、什么粒度、什么时间、交给谁”。边界清楚,返工就少;边界模糊,后面全是扯皮。
最后分享一个具体的小技巧。我在做覆盖分析时,会给每一个未覆盖项建一个编号,编号规则和问题报告的编号统一,然后在 SAS 里用一个表格把编号、位置、原因、证据、处理结论一次性列完。这套做法让审查代表不用一份份翻报告,看一张表就能掌握全部未覆盖情况。用了这个方法之后,我负责的项目在覆盖分析这一块的审查问询量下降了一大半,效果非常直接。