3.6 安全追溯——从安全目标到代码行的不破链
三丰百货店的20秒
1995年6月29日下午5点57分,首尔三丰百货店的A座在20秒内逐层坍塌坠为一堆废墟。502人死亡,937人受伤。这是现代历史上最严重的非恐怖主义建筑倒塌事故。
调查历时数月。调查者的方法是逆向追溯。
第一层:结构残骸分析。倒塌后的混凝土碎块和扭曲的钢筋被运到试验场。调查者从残骸中发现,五楼的楼板支柱的钢筋数量只有设计图纸要求的一半。原设计每根支柱16根钢筋,实际只用了8根。
第二层:施工记录追溯。调查者调出原始施工日志和材料采购单。发现施工方擅自将原设计的钢筋混凝土楼板改为现浇无梁楼板,且将支柱直径从80cm缩减到60cm。最关键的一处改动:支柱钢筋数量从16根减到8根。
第三层:审核记录追溯。调查者查阅了建设过程中的所有验收报告。发现结构变更的设计审核栏是空的,没有人审核过支柱缩小的结构安全性报告。总工程师曾经提出异议,但他的反对被管理层因“施工进度延迟会损失租金收入“为由否决。
第四层:决策链追溯。调查者最终追溯到三丰集团董事会的会议室,一个决定了“缩减支柱以增加商场使用面积“的商业决策从管理层逐级传导到施工方,没有任何人在任何节点对这一结构改动提出过正式的安全评估要求。
追溯链在四个环节全部断裂:设计变更←施工←审核←决策。没有一个环节是不可绕过的。没有一个环节会强制阻止“钢筋从16根减到8根“这种致命改动。每一条链条断裂的地方,都站着一个本可以阻止灾难的人,但制度没有赋予他阻止的权力,或者制度没有留下他尝试阻止的记录。
核心洞察:三丰百货店的悲剧是追溯链在四个环节全部断裂:设计、施工、审核、决策,没有一环不可绕过。 逆向追溯发现钢筋从 16 根减到 8 根,却没有一个环节强制阻止这种致命改动。每一条断裂处都站着一个本可阻止灾难的人,但制度没给他权力,也没留下他尝试阻止的记录。
安全追溯的本质:为调查者准备的链
三丰百货店的调查者面对的是一个“事后追溯“的场景。事故已经发生,他们需要回答:导致这个死亡的决策链是什么?
汽车功能安全的标准(ISO 26262)给出的答案是:你必须在灾难发生之前,就建好这条链。 在开发的每一刻都保持一条完整的、可验证的追溯链。从最顶层安全目标(“转向失控不可接受”)一直延伸到最底层的代码行(if (steering_torque > MAX_TORQUE) { enter_safe_state(); }),再延伸到验证这条代码行的单元测试,再延伸到确认这项系统功能正确的整车验证报告。
追溯链的双向性
安全追溯是双向的,而不是单向的“从上到下“:
| 方向 | 问题 | 场景 |
|---|---|---|
| 前向追溯(Forward Traceability) | “这条需求有没有被实现?” | 设计评审时:安全目标 SG-01 “防止非预期加速” → 是否被分配到了软件需求? → 是否被代码实现? |
| 后向追溯(Backward Traceability) | “这行代码为什么存在?” | 调查事故时:if (torque > limit) { ... } ← 这条代码是实现哪条需求的? ← 这条需求是满足哪个安全目标的? |
在三丰百货店的事故调查中,调查者做的是后向追溯,从残骸出发逆流而上。如果三丰集团在建造时建立了严格的前向追溯机制,让每一次设计变更都必须重新追溯到原始结构计算书、重新验算所有受影响的结构构件,那么钢筋从16根减到8根这件事在施工之前就会被阻止。
核心洞察:安全追溯要求在灾难发生之前就建好完整、可验证的追溯链,且它是双向的。 前向追溯回答“需求是否被实现“,后向追溯回答“这行代码为什么存在“。如果三丰集团在建时就有前向追溯,钢筋减半的改动会在施工前就被阻止——事后追溯永远不如事前建链。
从 HARA 到代码行:追溯链的五个层次
拆解一条完整的汽车功能安全追溯链。链条的起点是 HARA(Hazard Analysis and Risk Assessment,危害分析与风险评估),终点是代码行和测试用例。
第一层:HARA → 安全目标(Safety Goal)
HARA 分析的是“如果某功能失效,在某种驾驶场景下会导致什么危害“。分析的结果是安全目标。
场景:车辆在高速公路上以 120km/h 行驶
故障:电动助力转向(EPS)控制器输出错误力矩信号
危害:转向电机在无驾驶员意图的情况下产生转向力矩
暴露度:E4(高速公路驾驶,高暴露概率)
可控性:C3(120km/h 下非预期的突然转向,驾驶员极难控制)
严重度:S3(可能致命)
→ ASIL D
→ Safety Goal SG-03: "EPS 控制器不得在无驾驶员转向请求时输出大于 ±0.3Nm 的转向力矩"
安全目标的写法必须精确、可验证、无歧义。±0.3Nm 这个值来自车辆动力学仿真,超过这个力矩在 120km/h 下会导致车身偏航角速度超出驾驶员的可控范围。
第二层:安全目标 → 功能安全需求(FSR)
安全目标是“什么不能发生“。FSR 是“系统必须做什么来防止那个不能发生的事发生“。
SG-03 → FSR-03.1: 系统应持续监控驾驶员转向扭矩传感器的输出有效性
SG-03 → FSR-03.2: 当检测到转向扭矩传感器输出无效或异常时,系统应在 50ms 内进入安全状态(停止电机力矩输出)
SG-03 → FSR-03.3: 系统应在上电自检中验证转向扭矩传感器的信号链完整性
SG-03 → FSR-03.4: 转向电机力矩输出指令应通过独立于主控路径的监控路径进行合理性校验
注意 FSR-03.4 引入了一个关键的安全设计模式:双路径监控。主控路径计算电机力矩输出值,监控路径独立校核该值是否在许可范围内。如果两条路径的结论不一致,系统切断电机电源。这就是汽车的“第二双眼睛“。
第三层:FSR → 技术安全需求(TSR)
FSR 说的是“系统必须做什么“,这里的“系统“还是黑箱。TSR 把 FSR 分配到了具体的技术层面:哪个 ECU、哪个软件组件、哪条通信总线。
FSR-03.2 → TSR-03.2.1: EPS 主控 MCU 应以 1ms 周期读取转向扭矩传感器的三路冗余 PWM 信号
FSR-03.2 → TSR-03.2.2: 当三路 PWM 信号中两路或以上超出有效占空比范围(10%-90%)时,软件应在下一个控制周期内将 Motor_Torque_Request 信号置为 0x00
FSR-03.2 → TSR-03.2.3: Motor_Torque_Request 信号的传输应使用 E2E Profile 1 保护(CRC + Counter),由监控 MCU 在接收端校验
FSR-03.2 → TSR-03.2.4: 从检测到故障到电机力矩归零的总时间应在 35ms 以内(FSR 分配 50ms,其中 TSR 分配 35ms,剩余 15ms 分配给通信延迟和硬件响应时间)
从 FSR 到 TSR,要求从“50ms 内进入安全状态“被细化为三个具体的技术需求和一个时间预算的分解。这就是系统工程的核心工作:把顶层的安全约束分解为底层的技术约束,并确保分解后的约束加起来仍然满足顶层的约束。
第四层:TSR → 软件安全需求(SSR)→ 代码
TSR 分配到了 EPS 主控软件。现在软件工程师要把 TSR 转化为软件安全需求(SSR),然后写出对应的代码。
TSR-03.2.2 → SSR-03.2.2.1: 软件组件 Swc_TorqueMonitor 应以每 1ms 为周期被调度执行
TSR-03.2.2 → SSR-03.2.2.2: Swc_TorqueMonitor 应实现对三路 PWM 信号的两两交叉比较逻辑
TSR-03.2.2 → SSR-03.2.2.3: 比较逻辑的输出规则:
- 如果三路信号的有效性票数为 3/3:表决通过,正常输出
- 如果三路信号的有效性票数为 2/3:表决通过,但触发维护警告
- 如果三路信号的有效性票数为 1/3 或 0/3:表决不通过,进入安全状态
对应的代码(仅展示核心逻辑):
/* @req SSR-03.2.2.2: 三路 PWM 信号两两交叉比较 */
/* @req SSR-03.2.2.3: 基于有效票数表决的安全状态切换 */
void Swc_TorqueMonitor_Cyclic(void)
{
uint8 valid_flags = 0U;
/* 读取三路 PWM 信号并判断各自有效性 */
if (Torque_ReadPwmChannel(0U, &pwm_values[0U]) == E_OK) {
if ((pwm_values[0U].duty >= DUTY_MIN_PCT) &&
(pwm_values[0U].duty <= DUTY_MAX_PCT)) {
valid_flags |= (1U << 0U);
}
}
if (Torque_ReadPwmChannel(1U, &pwm_values[1U]) == E_OK) {
if ((pwm_values[1U].duty >= DUTY_MIN_PCT) &&
(pwm_values[1U].duty <= DUTY_MAX_PCT)) {
valid_flags |= (1U << 1U);
}
}
if (Torque_ReadPwmChannel(2U, &pwm_values[2U]) == E_OK) {
if ((pwm_values[2U].duty >= DUTY_MIN_PCT) &&
(pwm_values[2U].duty <= DUTY_MAX_PCT)) {
valid_flags |= (1U << 2U);
}
}
/* 基于有效票数进行安全决策 */
switch (valid_flags) {
case 0x07U: /* 111 - 三路全有效 */
torque_state = TORQUE_VALID;
break;
case 0x03U: /* 011 */
case 0x05U: /* 101 */
case 0x06U: /* 110 - 两路有效 */
torque_state = TORQUE_VALID_DEGRADED;
/* @req SSR-03.2.2.3: 两路有效触发维护警告 */
Dem_SetEventStatus(DEM_EVENT_TORQUE_1oo3_FAULT, DEM_EVENT_STATUS_FAILED);
break;
default: /* 符合MISRA Rule 16.4: 每个switch必须有default分支 */
/* @req SSR-03.2.2.3: 少于两路有效 → 安全状态 */
torque_state = TORQUE_INVALID;
Motor_DisableOutput();
break;
}
}
注意代码中关键的几个设计元素:
@req注释:将每一段代码精确关联到它所实现的安全需求。这就是代码级追溯:这行代码对应哪条需求的哪个条款。- MISRA 合规:
switch有default分支(Rule 16.4),所有未明确处理的valid_flags值全部进入安全状态。这是防御性编程:即使硬件故障导致valid_flags出现设计文档中未列出的值(比如 0x00),系统仍然安全。 - 分层决策:三路裁决细分为“全好/降级/危险“三层判断。这体现了功能安全的一个核心思想:并非所有故障都等于危险。系统应尽可能维持在降低性能的运行状态,避免一检测到异常就立即完全停止。
第五层:代码 → 单元测试 → 测试报告 → 追溯矩阵
代码写好了,但追溯链还没结束。你还需要用测试证明这行代码确实实现了它所声称的需求。
/* @req SSR-03.2.2.3 - 测试用例: 三路信号中仅一路有效时进入安全状态 */
void test_TorqueMonitor_2of3_invalid_activates_safe_state(void)
{
/* 配置 Mock:仅 PWM 通道 0 返回有效值 */
Torque_ReadPwmChannel_ExpectAndReturn(0U, &pwm_values[0U], E_OK);
Torque_ReadPwmChannel_IgnoreArg_data();
Torque_ReadPwmChannel_ReturnThruPtr_data(&pwm_valid_duty);
Torque_ReadPwmChannel_ExpectAndReturn(1U, &pwm_values[1U], E_OK);
Torque_ReadPwmChannel_IgnoreArg_data();
Torque_ReadPwmChannel_ReturnThruPtr_data(&pwm_invalid_duty_high);
Torque_ReadPwmChannel_ExpectAndReturn(2U, &pwm_values[2U], E_OK);
Torque_ReadPwmChannel_IgnoreArg_data();
Torque_ReadPwmChannel_ReturnThruPtr_data(&pwm_invalid_duty_low);
Motor_DisableOutput_Expect(); /* 预期:电机输出被禁用 */
Swc_TorqueMonitor_Cyclic();
}
这个测试用例的名称包含了追溯信息:它所验证的需求编号 SSR-03.2.2.3。通过测试报告生成工具,你可以自动生成一张追溯矩阵:
| SSR ID | 实现位置 | 测试用例 | 测试状态 | 覆盖率 |
|---|---|---|---|---|
| SSR-03.2.2.1 | Swc_TorqueMonitor.c L15 | 配置于 RTE 调度表,1ms 周期由 RTE 保证 | — | — |
| SSR-03.2.2.2 | Swc_TorqueMonitor.c L42-L68 | test_TorqueMonitor_2oo3_voting_algorithm | PASS | 100% |
| SSR-03.2.2.3 | Swc_TorqueMonitor.c L70-L92 | test_TorqueMonitor_* (5个用例覆盖所有票数组合) | PASS | 100% |
核心洞察:追溯链从 HARA 的安全目标一路分解到代码行,核心是“顶层约束被精确分解,且分解后加起来仍满足顶层“。 50ms 的安全状态被拆成 35ms 软件加 15ms 通信,再落到三路 PWM 表决代码;
@req注释是代码级追溯的唯一显式链接,而分层决策体现“并非所有故障都等于危险,应尽可能维持降级运行“。
追溯链断裂时:五个最常见的断裂点
在实际项目中,追溯链很少从头到尾保持完整。以下是最常见的五个断裂点,每个都对应着真实发生过的安全事故:
断裂点 1:安全需求被“隐含假设“替代
“扭矩传感器的校验逻辑?不需要写成独立需求,这是个传感器,厂商肯定已经校验过了,我们直接读值就行。”
回应:传感器厂商的校验满足你的 ASIL 等级要求吗?校验覆盖了通信链路上的故障模式吗?
断裂点 2:代码实现了需求,但注释没有标注出来
“我们的代码确实实现了 FSR-03.2,但是没有加 @req 注释,趁还记得的时候补上?不不不,三年后没人记得。现在不写,三年后就没有了。”
@req 注释看似是个形式主义,但它是代码与需求之间唯一的显式链接。没有它,后向追溯只能靠人脑记忆,而人脑的记忆在项目交付三个月后就开始模糊。
断裂点 3:单元测试覆盖了代码,但没有覆盖需求
“我们的 Swc_TorqueMonitor 单元测试达到了 95% 的分支覆盖率。”
回应:好的。但它测试了 需求的所有边界条件 吗?分支覆盖 100% 不等于需求覆盖 100%。测试用例的设计必须以需求为出发点。
断裂点 4:需求变更了,但追溯链没有更新
“需求 v1.7 把安全状态响应时间从 50ms 改为 35ms,但代码里的超时判断还是 50ms。没有人通知软件组。”
回应:需求管理系统必须有变更影响分析机制。一旦某条需求被修改,系统应自动通知所有受影响的上下游节点(代码、测试、配置参数、标定数据)。
断裂点 5:追溯链在“配置“和“标定“环节断裂
“代码里的超时阈值是 TIMEOUT_MS,它是个宏,定义在 Cal_Cfg.h 里。Cal_Cfg.h 是标定工程师用 Excel 生成的,没有和需求系统链接。”
回应:标定参数是实现安全需求的最末端载体。TIMEOUT_MS = 35 这个值的存在是因为 TSR-03.2.4 要求 35ms,但这个血统关系必须被显式记录。你可以把它写在宏定义的注释里:/* TSR-03.2.4 */ #define TIMEOUT_MS 35U。
核心洞察:追溯链最常断在五个“以为没问题“的地方。 隐含假设替代了安全需求、实现了需求却漏标
@req、测试覆盖了代码却没覆盖需求、需求变更没有传播到代码与配置、标定参数的血统关系未被记录。这些断裂点都是静默的——它们不报错,只是让三年后的追溯无路可走。
工具支撑:从 Excel 到专业追溯平台
早期追溯管理用 Excel:一列是需求ID,一列是测试用例ID,手工维护。这种方案在 15 条需求时可以工作,在 150 条时开始痛苦,在 1500 条时崩溃。
汽车行业当前的追溯管理工具:
| 工具 | 特点 |
|---|---|
| IBM DOORS / DOORS Next | 传统重型需求管理,军工和汽车广泛使用 |
| Polarion(Siemens) | 需求+测试+追溯一体化,与 ASPICE 流程深度集成 |
| codeBeamer(Intland / PTC) | 敏捷+合规平衡,适合中等规模项目 |
| Vector PREEvision | 覆盖 EE 架构到软件实现的完整追溯 |
| Arctic Core 的 @req 模式 | 轻量级代码内追溯,适合软件层面的精确关联 |
一个实际的工程策略是分层追溯:用 Polarion/DOORS 管理需求之间的追溯(HARA→FSR→TSR),用 @req 注释管理需求到代码的追溯,用测试框架的命名规范管理需求到测试的追溯,最后用生成脚本把这三层拼成一张完整的追溯矩阵。
核心洞察:Excel 追溯在 150 条需求时就开始崩溃,工程策略是“分层追溯“。 Polarion/DOORS 管需求之间的追溯,
@req注释管需求到代码,测试命名规范管需求到测试,最后用脚本拼成完整矩阵。越是依赖“人记得“的方案,越会在规模与时间里失效。
追溯链的意义:为什么这不能是一个“以后补“的文档?
许多工程师认为追溯链是“形式主义文档“:“我知道我的代码在干什么,为什么还要额外写这些注释和映射表?”
答案是:追溯链是为调查你的人写的。
三丰百货店倒塌时,最初的设计师和管理层也许都已经退休或转岗了。调查者面对的是一个由图纸、施工记录、验收报告、会议纪要拼凑出来的“证据拼图“,但关键的那几块拼图永远是缺失的。“谁决定把钢筋从16根减到8根?”“有谁提出过反对意见?”“设计变更后的结构安全性重新计算过吗?“这些问题没有答案,因为没有人觉得有必要在工程记录中显式回答它们。
你不希望你的代码有一天站在这个位置,作为一场事故调查中的“缺失拼图“。
追溯链的终极意义是应对最坏情况。 如果有一天,你的代码涉及了一起安全事故,调查委员会可以沿着你建立的追溯链,清晰地看到:
→ 这个安全目标是在 HARA 分析中确定的(文档:HARA报告 v2.3, 第4.7章)
→ 这个安全目标被分解为了 4 条 FSR(文档:FSR 规格书, 第3.2节)
→ FSR-03.2 被分解为了 4 条 TSR(文档:TSR 规格书, 第5.1节)
→ TSR-03.2.2 被分配到了 Swc_TorqueMonitor(文档:软件架构设计, 第8.3章)
→ 这个软件组件由 Swc_TorqueMonitor.c 实现,关键逻辑在 L42-L92
→ 关键代码有 @req SSR-03.2.2.3 显式关联
→ 对应的单元测试 test_TorqueMonitor_2of3_invalid_activates_safe_state 已通过(报告:UT_Report_v2.3, 第87页)
→ HIL 测试已验证电机在故障注入下确实归零(报告:HIL_Report_v2.3, 第112页)
→ 整车验证已确认在 120km/h 下的转向可控性(报告:Vehicle_Test_Report_v2.3, 第45页)
这是一条从一个致命危害场景一直延伸到一行 C 代码,然后再从这行 C 代码延伸到一台真实车辆的完整证据链。
这条链上的每一个链接都是人建立的:HARA 分析的工程师、系统架构师、软件工程师、测试工程师、HIL 工程师、整车验证工程师。每一个人都在链条中焊上了自己的一环。如果有任何人偷工减料没焊好,整条链就断了。
核心洞察:追溯链不是形式主义,是为调查你的人写的一部“因果档案“。 事故发生时,设计师和管理层可能早已转岗,能回答“谁把钢筋减半、谁反对过、重算过没有“的只有工程记录。链上的每个链接都是人焊上的,任何一环偷工减料整条链就断——这正是“焊死你负责的那一环“既是专业责任也是道德责任的原因。
那条没被追溯的需求
追踪矩阵的最后一行是空的。那是一条SSR(软件安全需求),它在架构评审中被分配给了某个模块,但实现它的函数至今没有打上 @req 标签。你打开那个模块的源代码,在对应函数上加上注释,重新跑一遍追溯脚本。绿色。
不是每一次都这么顺利。有时你会遇到一条需求,它在追溯矩阵里存在,在代码里有 @req 标签,在测试里有 @covers,但三个地方的描述对不上:矩阵说“超时后进入安全状态“,代码注释说“超时后复位“,测试只验证了“超时后不响应“。三个环节各说各话,链条看似完整,实则每一环都松了一扣。
把这三处对齐,比写新代码更难,也更值得做。因为追溯链不是写给别人看的文档,是你给“最坏情况下的自己“准备的保险单。
核心洞察:追溯链最隐蔽的断裂,是三个环节描述对不上:矩阵、代码注释、测试各说各话。 链条看似完整,实则每一环都松了一扣;对齐它们比写新代码更难,也更值得做。追溯链不是写给别人看的文档,是你给“最坏情况下的自己“准备的保险单。
本篇小结
- 安全追溯的本质:为事故调查者准备的、从安全目标到代码行的完整证据链。
- 追溯是双向的:前向追溯验证“需求是否被实现“,后向追溯回答“这行代码为什么存在“。
- 追溯链五层结构:HARA → 安全目标(SG)→ 功能安全需求(FSR)→ 技术安全需求(TSR)→ 软件安全需求(SSR)→ 代码(@req 注释)→ 单元测试 → 集成测试 → 测试报告 → 追溯矩阵。
- 五个常见断裂点:隐含假设、缺失 @req 注释、测试覆盖代码未覆盖需求、需求变更未传播、标定参数链路断裂。
- 工具支撑:DOORS / Polarion / codeBeamer 管理需求侧追溯,@req 注释管理代码侧追溯,脚本拼接完整矩阵。
- 追溯链的终极意义:为最坏情况准备一部“因果档案“。如果有一天你的代码被调查,这部档案就是你的答案。
- 焊死你负责的那一环。这是专业责任,也是道德责任。
【下集预告】:追溯链建立起从安全目标到代码行的完整证据链。但这条链上有一个经常被人忽视的环节:文档。如果代码是砖,文档就是施工图纸:没有图纸,十五年后没人知道哪面墙是承重的。第3.7章,我们将走进“文档即代码“的世界:如何把文档和代码放在同一个仓库里,如何在每次MR中自动验证文档是否与代码双向一致。因为文档过期比没有文档更危险。