An OBD-style diagnostic monitor for a safety-relevant redundant sensor pair, built the full MBD way: requirements → Simulink/Stateflow model → generated C → MIL/SIL with structural coverage. Three rationality tests (electrical range, signal rate, and cross-channel correlation) feed a debounced fault-confirmation machine — NO_FAULT → PENDING → CONFIRMED → HEALING — that sets a DTC, captures a freeze-frame, drives the MIL, and heals only after a sustained clean run. The panel below is a live, in-browser run of the same logic — inject a fault and watch it confirm, store, and heal.
New here? Move the pedal — the two redundant sensors track together and the monitor stays green. Then hit a fault button: a sustained fault walks PENDING → CONFIRMED after a debounce, lights the MIL, and freezes the conditions. A single glitch never trips it. Cold engine? Diagnostics are disabled.
What you're watching: both sensors read the same pedal in %, so they sit on top of each other when healthy. The orange counter fills as a fault persists; at the confirm threshold the trace turns red — the DTC is stored and the MIL comes on. It stays on until a sustained clean run heals it.
Inject Fault clamps Sensor A to 4.90 V (OOR) — a sustained fault that walks PENDING → CONFIRMED and lights the MIL. Remove enters HEALING.
Inject Spike fires a single-sample +3 V burst — Rate (and Corr) LEDs flash, the counter ticks up by 2 then decays straight back to zero. A glitch never confirms. That is the debounce protection working exactly as designed.
Reset starts fresh.
A browser run of the diagnostic logic (range / rate / correlation rationality, a shared debounced fail counter, and the NO_FAULT→PENDING→CONFIRMED→HEALING machine) — the same logic and parameters as the Simulink/Stateflow model in the repo, and as verify_diag_logic.js. Not the Simulink artifact itself.
Three rationality tests, one debounced counter, and a four-state machine. The same logic runs in the console above.
Read it left-to-right: a redundant sensor pair feeds three rationality tests. Any failing test increments one shared debounced counter (but only while the enable gate is open — a warm engine). When the counter reaches the confirm threshold, the state machine latches a CONFIRMED DTC, captures a freeze-frame, and lights the MIL; a sustained clean run then heals it. A single-sample glitch raises the counter by one and it ages straight back to zero — so it never confirms.
range_bad = v1 < v_oor_lo || v1 > v_oor_hiRange: the primary sensor is electrically out of range (short to ground / battery, open circuit) — outside the band the real pedal travel never reaches.
rate_bad = |v1 − v1_prev| > dv_maxRate: the signal jumps more than is physically possible in one 10 ms sample — a spike, dropout, or EMI burst.
corr_bad = |pct1 − pct2| > corr_tolCorrelation: the two redundant channels disagree by more than tolerance — catches single-channel drift / stuck where one sensor is still in-range.
failCnt = clamp(failCnt ± 1, 0, confirm_cnt) // only if enabledDebounce: one shared counter rises on any failing test and decays on clean samples; confirmation needs persistence, so noise ages out.
NO_FAULT →(cnt>0) PENDING →(cnt≥confirm) CONFIRMED →(clean) HEALING →(heal_cnt clean) NO_FAULTState machine: PENDING is an unconfirmed DTC (MIL off); CONFIRMED stores the DTC + freeze-frame and lights the MIL; HEALING keeps the MIL on until a full clean run clears it (like OBD warm-up cycles).
Three layers. The signal source (grey, top) is the redundant sensor pair plus the operating point and the coolant-temperature enable gate. The monitor (teal, inside the dashed boundary) is the code-generated unit running at 100 Hz: rationality tests, the debounced counter, the Stateflow supervisor, and DTC/freeze-frame storage. The consumers (grey, bottom) are DTC memory, the MIL/cluster, and the off-board scan tool. The interface is small and explicit: v1·v2·rpm·load·ect in, state·dtc·mil·ff_valid out — the same contract as autosar/DiagMonitor.arxml.
The Verification column lists the planned MIL/SIL methods (run on a personal MATLAB). The monitor logic itself is re-proven headlessly today by verify_diag_logic.js — independent of Simulink — and exercised live in the console above.
Latest headless run · verify_diag_logic.js — REQ-DIAG-001…010 all pass.
Interface · 100 Hz (Ts = 10 ms)
| Signal | Dir | Type · Range |
|---|---|---|
v1 | in | single, V · 0 … 5 (APP1 primary) |
v2 | in | single, V · 0 … 5 (APP2 redundant) |
rpm | in | single, rpm · 0 … 7000 |
load | in | single, % · 0 … 100 |
ect | in | single, °C · −40 … 130 (enable) |
state | out | uint8 · 0 … 3 (NO_FAULT…HEALING) |
dtc | out | uint8 · bit0 range, bit1 rate, bit2 corr |
mil | out | boolean · 0 / 1 |
ff_valid | out | boolean · freeze-frame captured |
| ID | Requirement | Verification |
|---|---|---|
| REQ-DIAG-001 Functional | Flag a range fault when the primary sensor is electrically out of range. | MIL · tc_range · JS |
| REQ-DIAG-002 Functional | Flag a rate fault on an implausible step (> dv_max) between samples. | MIL · tc_rate · JS |
| REQ-DIAG-003 Functional | Flag a correlation fault when the redundant channels disagree by > corr_tol. | MIL · tc_corr · JS |
| REQ-DIAG-004 Safety | Confirm a DTC only after confirm_cnt debounced samples; a single-sample glitch never confirms. | MIL · tc_confirm, tc_glitch |
| REQ-DIAG-005 Functional | Report PENDING for an unconfirmed detected fault; MIL stays off. | MIL · tc_confirm |
| REQ-DIAG-006 Functional | Capture a freeze-frame of the operating point and tripping test at confirmation. | MIL · tc_confirm |
| REQ-DIAG-007 Functional | MIL on iff ≥ 1 DTC is confirmed (remains on through HEALING until cleared). | MIL · tc_confirm, tc_heal |
| REQ-DIAG-008 Functional | Heal a confirmed DTC only after heal_cnt consecutive clean samples; archive the DTC and clear the MIL; re-fail re-confirms. | MIL · tc_heal |
| REQ-DIAG-009 Functional | Run tests / accumulate failures only when enable conditions are met; counter frozen otherwise. | MIL · tc_enable |
| REQ-DIAG-010 Interface | Conform to the interface above; outputs stay within declared ranges (state ∈ [0,3], dtc ∈ [0,255]). | Analysis + all MIL tests |
The deployed monitor and its setup, in MISRA-aligned C — the same logic running in the console above. Embedded Coder produces this from the Simulink/Stateflow model in the repo; shown here in equivalent hand form.
/* diag_params.h — one parameter set (mirrors matlab/diag_params.m). * Monitor executes at Ts = 0.01 s (100 Hz). */ #define TS 0.01F /* sample time [s] */ /* sensor scaling: APP1 0.5..4.5V, APP2 0.25..2.25V over 0..100% */ #define V1_LO 0.5F #define V1_HI 4.5F #define V2_LO 0.25F #define V2_HI 2.25F /* rationality thresholds */ #define V_OOR_LO 0.20F /* electrical out-of-range low [V] */ #define V_OOR_HI 4.80F /* electrical out-of-range high [V] */ #define DV_MAX 1.50F /* max plausible step / sample [V] */ #define CORR_TOL 10.0F /* max |pct1-pct2| mismatch [%] */ /* confirmation / healing / enable */ #define CONFIRM_CNT 20U /* failing samples to confirm (=200 ms) */ #define HEAL_CNT 300U /* clean samples to heal (~3 cycles) */ #define WARM_MIN 60.0F /* coolant temp enabling diagnostics [C] */ /* DTC bitfield: bit0 range, bit1 rate, bit2 correlation */
/* diag_rationality_step — range + rate + correlation with a shared * debounced fail counter. Counter frozen unless enable conditions met. */ void diag_rationality_step(const DiagIn *const in, DiagWork *const w) { const real32_T pct1 = (in->v1 - V1_LO) / (V1_HI - V1_LO) * 100.0F; const real32_T pct2 = (in->v2 - V2_LO) / (V2_HI - V2_LO) * 100.0F; boolean_T range_bad = (in->v1 < V_OOR_LO) || (in->v1 > V_OOR_HI); /* REQ-DIAG-001 */ boolean_T rate_bad = fabsf(in->v1 - w->v1_prev) > DV_MAX; /* REQ-DIAG-002 */ boolean_T corr_bad = fabsf(pct1 - pct2) > CORR_TOL; /* REQ-DIAG-003 */ boolean_T any_bad = range_bad || rate_bad || corr_bad; w->testbits = (uint8_T)range_bad | ((uint8_T)rate_bad << 1) | ((uint8_T)corr_bad << 2); w->enable = (in->ect >= WARM_MIN); /* REQ-DIAG-009 */ if (w->enable) { if (any_bad) { if (w->failCnt < CONFIRM_CNT) w->failCnt++; } else { if (w->failCnt > 0U) w->failCnt--; } } w->any_bad = any_bad; w->v1_prev = in->v1; }
/* diag_supervisor_step — NO_FAULT->PENDING->CONFIRMED->HEALING (Stateflow). */ void diag_supervisor_step(const DiagIn *const in, DiagWork *const w, DiagOut *const out) { switch (w->state) { case DIAG_NO_FAULT: if (w->failCnt > 0U) w->state = DIAG_PENDING; /* REQ-DIAG-005 */ break; case DIAG_PENDING: if (w->failCnt >= CONFIRM_CNT) { /* REQ-DIAG-004 */ w->state = DIAG_CONFIRMED; w->dtc = w->testbits; if (!w->ff_valid) { /* REQ-DIAG-006 */ w->ff = (FreezeFrame){ in->rpm, in->load, in->ect, in->v1, in->v2, w->dtc }; w->ff_valid = true; } } else if (w->failCnt == 0U) { w->state = DIAG_NO_FAULT; } break; case DIAG_CONFIRMED: if (w->enable && !w->any_bad) { w->state = DIAG_HEALING; w->healCnt = 0U; } break; case DIAG_HEALING: if (w->any_bad) { w->state = DIAG_CONFIRMED; w->healCnt = 0U; } /* re-fail */ else if (++w->healCnt >= HEAL_CNT) { /* REQ-DIAG-008 */ w->dtc = 0U; w->ff_valid = false; w->failCnt = 0U; w->state = DIAG_NO_FAULT; } break; default: w->state = DIAG_NO_FAULT; break; } out->state = w->state; out->dtc = w->dtc; out->mil = (w->state == DIAG_CONFIRMED) || (w->state == DIAG_HEALING); /* REQ-DIAG-007 */ out->ff_valid = w->ff_valid; }
/* diag_monitor_types.h — I/O contract (REQ-DIAG-010). */ typedef enum { DIAG_NO_FAULT = 0, DIAG_PENDING, DIAG_CONFIRMED, DIAG_HEALING } DiagState; /* 0 … 3 */ typedef struct { real32_T rpm, load, ect, v1, v2; uint8_T code; /* tripping test bitfield */ } FreezeFrame; typedef struct { real32_T v1, v2; /* redundant sensor pair [V] */ real32_T rpm, load, ect; /* operating point + enable */ } DiagIn; typedef struct { DiagState state; /* supervisor state 0 … 3 */ uint8_T dtc; /* 0 = OK; bit0 range, bit1 rate, bit2 corr */ boolean_T mil; /* malfunction lamp */ boolean_T ff_valid; /* freeze-frame captured */ } DiagOut;
The Simulink model and Stateflow fault machine are constructed via the Simulink API in build_model.m — reviewable and diffable in git. Requirements, design, and test plan are in the repo and track the same rationality tests, debounce, and state machine shown above.
Exercised live above and checked against requirement thresholds in test/test_diag_monitor.m and verify_diag_logic.js — REQ-DIAG-001…010 all pass, independent of Simulink.
A hand-authored, schema-valid autosar/DiagMonitor.arxml — ports, sender/receiver interfaces, a 10 ms periodic runnable, and the I/O contract for integrating the monitor as an AUTOSAR Classic software component.
The rendered .slx diagram and the decision/condition/MCDC coverage report — produced by running build_model.m / collect_coverage.m on a personal MATLAB. Screenshots will be embedded here.