Skip to content

01 · Advanced HIL Test Automation

Level 2 Module 9 mapped the physical components of a HIL rig. This module covers how you actually automate test execution against one — scenario scripting, real-time synchronization concerns, and the practical differences from the pure-CAPL automation you've built so far once a real-time simulation computer is in the loop.

About this module

The automation patterns and API shapes below (CAPL-driven orchestration talking to a real-time simulation host) reflect genuine, documented practice across common HIL platforms. No specific rig was used to produce this content — treat every snippet as an architectural reference to adapt to your actual platform's real API, not a captured run.

The orchestration split

A HIL test automation stack usually has two cooperating layers:

Layer Responsibility Typical tooling
Real-time layer Runs the plant model deterministically, every fixed timestep, no exceptions dSPACE/NI real-time OS, model built from Simulink or similar
Test orchestration layer Decides what scenario to run, sets model parameters, reads results, makes pass/fail calls CAPL/CANoe, Python, or vendor test-automation frameworks, running non-real-time

The orchestration layer never touches the real-time loop's internals directly — it reads and writes named parameters and signals exposed by the real-time model through a defined interface, exactly the same "don't poke internals directly" discipline as Level 2 Module 8's exported restbus control functions, just one layer further out.

// CAPL orchestration talking to a HIL real-time model's parameter interface
// (illustrative — the real API name/shape depends on your platform's CANoe integration)
on start
{
  HilSetParameter("VehicleSpeed_Setpoint", 0.0);
  HilSetParameter("RoadFriction_Coefficient", 0.9);   // dry asphalt
}

testcase tc_AbsActivatesOnLowFriction()
{
  testCaseTitle("ABS ECU activates within spec when friction drops sharply");
  HilSetParameter("VehicleSpeed_Setpoint", 80.0);
  testWaitForTimeout(2000);   // allow the plant model to reach steady state
  HilSetParameter("RoadFriction_Coefficient", 0.15);  // simulated ice
  HilApplyBrakePedal(0.8);

  testWaitForTimeout(300);
  testStepCheck("ABS active flag set", HilGetSignal("ABS_Active") == 1);
}

Scenario scripting: parameterize, don't hardcode

Every HIL testcase worth keeping should separate scenario parameters (speed, friction, load, ambient temperature) from the test logic that drives and checks them, so the same testcase body can run a matrix of conditions:

void RunAbsActivationScenario(float speedKph, float friction, float pedalForce, float expectedMaxMs)
{
  dword t0, t1;
  HilSetParameter("VehicleSpeed_Setpoint", speedKph);
  testWaitForTimeout(2000);
  HilSetParameter("RoadFriction_Coefficient", friction);
  HilApplyBrakePedal(pedalForce);
  t0 = timeNowNS() / 1000000;

  testWaitForSignal("ABS_Active", 1, 1000);
  t1 = timeNowNS() / 1000000;
  testStepCheck("ABS activation within spec time", (t1 - t0) <= expectedMaxMs);
}

testcase tc_AbsMatrix()
{
  testCaseTitle("ABS activation across speed/friction matrix");
  RunAbsActivationScenario(60.0, 0.15, 0.8, 250);
  RunAbsActivationScenario(120.0, 0.15, 0.8, 250);
  RunAbsActivationScenario(60.0, 0.05, 0.8, 300);   // near-ice, slightly relaxed spec
}

This is the same "cheat sheet" discipline as Level 1's exercise technique (equivalence partitioning applied to scenario space, not just signal values) — a matrix of representative operating points, not an exhaustive and impractical sweep.

Real-time synchronization pitfalls

| Pitfall | Cause | Mitigation |
|---|---|---| | Reading a signal mid-update | Orchestration layer polls at a moment between the real-time step writing a new value and it becoming "settled" | Read on a model-step-boundary event/callback rather than an arbitrary wall-clock timer, if your platform exposes one | | Setting a parameter that only takes effect next step | Parameter writes are often latched, not applied instantaneously | Always wait at least one model timestep after a parameter write before asserting dependent behavior | | Wall-clock test timeouts drifting from simulation time | Orchestration host isn't itself real-time; its clock can lag under load | Prefer waiting on model-reported simulation time or explicit signal conditions over fixed wall-clock sleeps where the platform allows it | | Overlapping scenarios | A new scenario's setpoints applied before the previous scenario's transients settle | Always include an explicit settle/return-to-baseline step between scenarios, as in RunAbsActivationScenario's initial 2000ms speed-settle wait |

Closed-loop validation: proving the loop is actually closed

Level 2 Module 9 stressed that HIL must be closed-loop. Advanced automation should include an explicit loop-closure sanity check as a standing testcase, not just assume the rig is wired correctly:

testcase tc_ClosedLoopSanityCheck()
{
  testCaseTitle("ECU's actuator output measurably changes plant model feedback");
  float before, after;
  before = HilGetSignal("SteeringRackPosition");
  HilInjectTorqueCommand(5.0);   // command the ECU to request 5Nm of assist
  testWaitForTimeout(200);
  after = HilGetSignal("SteeringRackPosition");
  testStepCheck("rack position changed in response to ECU command", after != before);
}

Running this before every scenario suite catches a mis-wired or mis-configured rig (open-loop by accident) immediately, rather than producing a suite full of misleadingly "passing" tests that never actually exercised the ECU's control behavior at all.

Cheat sheet

Concept Key point
Two-layer split Real-time plant model vs. non-real-time orchestration
Parameter/signal interface Orchestration never touches model internals directly
Scenario parameterization One testcase body, many representative operating points
Settle waits Always wait for transients before asserting or starting the next scenario
Loop-closure sanity check A standing testcase proving the rig is actually closed-loop

How It Actually Works: double-buffering is why parameter writes latch

The "parameter writes are often latched, not applied instantaneously" pitfall in this module's table has a specific, deliberate reason: hard real-time plant models almost universally use double-buffered (sometimes called "shadow" or "pending") parameter storage. The real-time thread reads only from an active buffer while computing each fixed timestep's physics, and every HilSetParameter() call from the non-real-time orchestration layer writes into a separate pending buffer instead of the active one directly. At a well-defined point — the start of the next timestep, after the current one's calculation has fully completed — the real-time scheduler atomically swaps pending into active.

This design exists to preserve the one property a HIL rig cannot ever sacrifice: within a single timestep, every value the physics calculation reads must be self-consistent. If a parameter write from the orchestration host (which runs asynchronously, on a non-deterministic OS) were allowed to land mid-calculation, the plant model could compute using a mix of old and new values that never existed at any real instant — for example, updating RoadFriction_Coefficient partway through the same step that's also computing wheel-slip forces derived from the old friction value, producing a physically impossible transient. Double-buffering makes torn reads/writes across the real-time/non-real-time boundary structurally impossible, at the cost of exactly the one-step latency this module tells you to wait out.

This mechanism is also precisely why testWaitForSignal on a model-step-boundary event (rather than a wall-clock poll) is more than a style preference: a signal read triggered by the step-boundary event is guaranteed to observe the buffer after that step's swap has completed, while a wall-clock poll can sample in the narrow window where the previous step's stale value is still active and the swap hasn't yet fired — intermittently reading one step's data late, purely as a function of how busy the orchestration host happens to be at that instant, which is exactly the flaky-test mechanism this module's Exercise 3 is asking you to diagnose.

Exercise

  1. RunAbsActivationScenario above waits a fixed 1000ms for ABS_Active regardless of scenario. Rewrite its signature and body to make that timeout itself a parameter, and explain why a single fixed timeout across a friction matrix (0.05 to 0.9) is a design smell.
  2. Design a tc_ClosedLoopSanityCheck-style test for a simulated engine-cooling HIL rig, where the real ECU commands a cooling-fan duty cycle that should measurably change a simulated coolant temperature over time. Name the two signals/parameters involved and the settle time you'd choose, with reasoning.
  3. Explain, using the synchronization pitfalls table, what could go wrong if a testcase reads ABS_Active on a plain 10ms wall-clock polling loop instead of a model-step-boundary event, on a heavily loaded orchestration host.