DCRouter is going into a car. Every rule R1 to R13 is verified, every planted bug caught. Then the safety team asks something the course has not: if a transistor in DCRouter's configuration register wears out after five years and a port-enable bit sticks at 0, does anything notice before a sensor's port is silently disabled? A design can be functionally perfect and still unsafe, because safety is about how it behaves when its own hardware fails.
How does a safety standard such as ISO 26262 change verification?
The obvious answer is "verify more thoroughly": more coverage, more proofs. All of that assumes the hardware works. Random hardware faults (stuck bits, upsets from radiation, wear-out) happen in correct designs. Safety requires mechanisms that detect them (parity, ECC, lock-step, watchdogs) and evidence that those mechanisms detect enough of them, which is a different kind of verification.
Concept | Meaning |
|---|---|
Safety mechanism | Hardware or software that detects or controls a fault: parity, ECC, duplicated logic compared in lock-step, timeouts |
Fault injection | Deliberately introducing faults (stuck-at, transient flips) in simulation, emulation or formal, and observing the outcome |
Fault classes | Safe (no effect on safety-relevant outputs), detected (a mechanism raised an alarm), dangerous (wrong output, no alarm) |
Diagnostic coverage | The share of potentially dangerous faults that the mechanisms detect |
Hardware metrics | Single-point and latent fault metrics with targets per ASIL: SPFM at least 99% and LFM at least 90% for ASIL D (fact V14) |
Fault injection is mutation testing (lesson 6.8) with a different question: not "does the testbench notice a design bug?" but "does the design notice a hardware failure?"
A 16-bit configuration register protected by a parity bit (illustrative). Every single stuck-at fault on the data and parity bits is injected in turn, a workload runs, and each fault is classified. Then the same faults are injected into a weaker design that protects only the upper byte.
"""Functional safety: measuring diagnostic coverage by fault injection.
A small safety-relevant block (illustrative): a 16-bit configuration register drives a
controller every cycle. A safety mechanism stores a parity bit with the register and checks it
every cycle. The model injects one fault at a time, a stuck-at-0 or stuck-at-1 on each of the
16 data bits and on the parity bit itself, runs a workload of 64 random register values, and
classifies each fault:
safe never changed any output the controller used (workload never exposed it)
detected the parity checker raised an alarm before or when an output went wrong
dangerous an output went wrong and no alarm was raised
Diagnostic coverage = detected / (detected + dangerous), over the 32 data-bit faults. The 2
faults on the parity bit itself never corrupt the register; they are faults in the safety
mechanism (here they raise a false alarm) and are reported separately. Then the same faults are
injected into a version with parity on the upper byte only (a weaker mechanism).
"""
import random
rng = random.Random(3)
WORK = [rng.randrange(1 << 16) for _ in range(64)]
def parity(x):
return bin(x).count("1") & 1
def run(fault, protected_bits):
bit, val = fault
alarm = wrong = False
for w in WORK:
stored, p = w, parity(w & protected_bits)
if bit < 16:
stored = (stored | (1 << bit)) if val else (stored & ~(1 << bit))
else:
p = val
if stored != w:
wrong = True
if parity(stored & protected_bits) != p:
alarm = True
if wrong or alarm:
break
if alarm:
return "detected"
return "dangerous" if wrong else "safe"
faults = [(b, v) for b in range(16) for v in (0, 1)] # data-bit faults
mech = [(16, 0), (16, 1)] # faults on the parity bit itself
for name, mask in [("parity on all 16 bits", 0xFFFF), ("parity on upper 8 bits", 0xFF00)]:
counts = {"safe": 0, "detected": 0, "dangerous": 0}
for f in faults:
counts[run(f, mask)] += 1
alarms = sum(run(f, mask) == "detected" for f in mech)
dc = counts["detected"] / (counts["detected"] + counts["dangerous"])
print(f"{name:24s} data faults {len(faults)}: safe {counts['safe']}, detected {counts['detected']}, dangerous {counts['dangerous']}, diagnostic coverage {100 * dc:.0f}%; parity-bit faults {len(mech)}: {alarms} false alarms")
if mask == 0xFFFF:
full = dc
else:
half = dc
assert full > 0.95 and half < 0.6
print("PASS: full parity detects every injected data-bit fault the workload exposes; protecting half the bits leaves half the data-bit faults undetected (dangerous)")It prints:
parity on all 16 bits data faults 32: safe 0, detected 32, dangerous 0, diagnostic coverage 100%; parity-bit faults 2: 2 false alarms
parity on upper 8 bits data faults 32: safe 0, detected 16, dangerous 16, diagnostic coverage 50%; parity-bit faults 2: 2 false alarms
PASS: full parity detects every injected data-bit fault the workload exposes; protecting half the bits leaves half the data-bit faults undetected (dangerous)How to read it: with parity over all 16 bits, every one of the 32 data-bit faults raises an alarm before or as it corrupts the value: 100% diagnostic coverage for single stuck-at faults. With parity over the upper byte only, the 16 faults on the lower byte corrupt the register silently: 50%, far below any ASIL target. The two faults on the parity bit itself corrupt nothing but raise false alarms; they are faults in the safety mechanism, which a real analysis tracks separately (as latent faults if they could instead hide an alarm). Real analyses weight faults by failure rate and include transient and multi-bit faults, but the method, inject, observe, classify, measure, is the same.
Safety requirements and mechanisms become features with their own checks and coverage.
A fault-injection campaign is planned like a regression: fault list, workloads, classification, metrics.
Traceability is mandatory: requirement to evidence, reviewed by an assessor (lesson 2.6).
Tool confidence: tools whose failure could hide a fault are qualified or cross-checked.
Process evidence: reviews, change control and independence are part of what is audited.
Practice | Purpose |
|---|---|
FMEDA (failure modes, effects and diagnostic analysis) | Expected fault classes and coverage per block, before injection confirms them |
Fault-injection campaigns in simulation, emulation and formal | Measured diagnostic coverage |
Safety mechanisms verified functionally too | A parity checker that never fires is a false pass (lesson 5.8) |
Independent safety assessment | Evidence judged against the standard |
A team (illustrative) verified that its ECC corrected single-bit errors and flagged double-bit errors, at the memory's interface. Fault injection later showed that faults in the ECC encoder itself, before the check bits were written, produced consistent but wrong data and check bits: the decoder saw no error. The mechanism was verified; its own failure modes were not. Injecting faults inside the safety mechanism found it.
Hardware can fail in the field (stuck bits, upsets); without mechanisms that detect those faults, a correct design can still produce wrong outputs silently.
The mechanism's own failure can make it miss or hide faults; verifying only its normal function misses that.
Safety adds a question: are hardware faults in the field detected in time?
Fault injection measures diagnostic coverage: safe, detected and dangerous faults.
Plan safety mechanisms as features, run fault campaigns, and keep traceability for assessment.