Skip to content
Test Types & Methods

HIL vs SIL vs MIL Testing: When to Use Each

Compare MIL, SIL, and HIL testing approaches with cost, speed, and fidelity tradeoffs, plus Python examples for hardware-in-the-loop validation with TofuPilot.

JJulien Buteau
intermediate8 min readMarch 14, 2026

MIL, SIL, and HIL are three levels of hardware-in-the-loop testing that trade cost and speed for fidelity. Use MIL during algorithm development, SIL to validate compiled code, and HIL to verify behavior against real hardware before production.

What MIL, SIL, and HIL Mean

Model-in-the-Loop (MIL) runs your control algorithm as a simulation model against a simulated plant. No hardware involved. Fast iteration, low cost, but lowest fidelity.

Software-in-the-Loop (SIL) compiles your production code and runs it on a host machine against a simulated environment. Same binary, no hardware. Catches code generation bugs MIL misses.

Hardware-in-the-Loop (HIL) runs your production code on the target ECU or microcontroller, connected to a real-time simulator that emulates the physical environment. Highest fidelity, highest cost.

Comparison Matrix

DimensionMILSILHIL
CostLowLow-mediumHigh
SpeedFast (seconds)Medium (minutes)Slow (minutes-hours)
FidelityLowMediumHigh
Hardware requiredNoneNoneTarget ECU + simulator rack
Typical useAlgorithm designCode verificationSystem validation
CI-friendlyYesYesNo (dedicated bench)
Bug discoveryDesign errorsCodegen errorsIntegration, timing

When to Use Each

MIL: Early Algorithm Development

Use MIL when the algorithm is still changing, you need rapid iteration, hardware is not yet available, or you're running parametric sweeps. MIL is wrong when you need to validate timing behavior, interrupt latency, or bus communication.

SIL: Code Verification Before Hardware

Use SIL when code generation is complete, you want to catch regressions in CI, you're validating numerical equivalence between model and generated code, or hardware bench slots are scarce. SIL misses real-time timing faults and hardware-specific peripheral behavior.

HIL: Pre-Production System Validation

Use HIL when the ECU firmware is frozen, you need to validate timing and bus behavior, certification requires hardware evidence, or you're running overnight regression suites. HIL is expensive to set up and slow to iterate.

Example: Voltage Limit Test at Each Level

The same test logic runs at all three levels. Only the plug changes.

MIL: Simulated Sensor

tests/mil/test_voltage_limit.py
import openhtf as htf
from openhtf.util import units
from tofupilot.openhtf import TofuPilot


class SimulatedVoltageSensor(htf.plugs.BasePlug):
    """Returns a deterministic value from a simulation model."""

    def setUp(self):
        pass

    def read_voltage(self) -> float:
        return 11.8

    def tearDown(self):
        pass


@htf.plug(sensor=SimulatedVoltageSensor)
@htf.measures(
    htf.Measurement("battery_voltage")
    .in_range(minimum=10.0, maximum=14.5)
    .with_units(units.VOLT),
)
def measure_battery_voltage(test, sensor):
    test.measurements.battery_voltage = sensor.read_voltage()


def main():
    test = htf.Test(measure_battery_voltage, test_name="battery-voltage-mil")
    with TofuPilot(test):
        test.execute(test_start=lambda: "UNIT-MIL-001")

if __name__ == "__main__":
    main()

SIL: Compiled Code Against Simulated Bus

tests/sil/test_voltage_limit.py
import ctypes
import openhtf as htf
from openhtf.util import units
from tofupilot.openhtf import TofuPilot


class SILVoltageSensor(htf.plugs.BasePlug):
    """Calls compiled production C library via ctypes."""

    LIB_PATH = "./build/battery_monitor.so"

    def setUp(self):
        self._lib = ctypes.CDLL(self.LIB_PATH)
        self._lib.get_battery_voltage.restype = ctypes.c_float

    def read_voltage(self) -> float:
        return float(self._lib.get_battery_voltage())

    def tearDown(self):
        pass


@htf.plug(sensor=SILVoltageSensor)
@htf.measures(
    htf.Measurement("battery_voltage")
    .in_range(minimum=10.0, maximum=14.5)
    .with_units(units.VOLT),
)
def measure_battery_voltage(test, sensor):
    test.measurements.battery_voltage = sensor.read_voltage()


def main():
    test = htf.Test(measure_battery_voltage, test_name="battery-voltage-sil")
    with TofuPilot(test):
        test.execute(test_start=lambda: "UNIT-SIL-001")

if __name__ == "__main__":
    main()

HIL: Real ECU on Hardware Bench

tests/hil/test_voltage_limit.py
import can
import openhtf as htf
from openhtf.util import units
from tofupilot.openhtf import TofuPilot


class HILVoltageSensor(htf.plugs.BasePlug):
    """Reads voltage from real ECU over CAN bus."""

    CHANNEL = "can0"
    BUSTYPE = "socketcan"

    def setUp(self):
        self._bus = can.interface.Bus(channel=self.CHANNEL, bustype=self.BUSTYPE)

    def read_voltage(self) -> float:
        msg = self._bus.recv(timeout=1.0)
        raw = msg.data[2]
        return raw * 0.1

    def tearDown(self):
        self._bus.shutdown()


@htf.plug(sensor=HILVoltageSensor)
@htf.measures(
    htf.Measurement("battery_voltage")
    .in_range(minimum=10.0, maximum=14.5)
    .with_units(units.VOLT),
)
def measure_battery_voltage(test, sensor):
    test.measurements.battery_voltage = sensor.read_voltage()


def main():
    test = htf.Test(measure_battery_voltage, test_name="battery-voltage-hil")
    with TofuPilot(test):
        test.execute(test_start=lambda: "UNIT-HIL-001")

if __name__ == "__main__":
    main()

How TofuPilot Tracks Results Across All Levels

Each test run uploads to TofuPilot with the same measurement name (battery_voltage) regardless of level. The test_name field distinguishes the environment.

test_nameLevelTraceability
battery-voltage-milMILAlgorithm baseline
battery-voltage-silSILCodegen regression
battery-voltage-hilHILHardware validation

A unit that passes MIL but fails SIL indicates a code generation issue. A unit that passes SIL but fails HIL indicates a hardware integration issue.

Decision Framework

ActivityRecommended Level
Tuning control gainsMIL
Regression testing in CISIL
Verifying code generation outputSIL
Validating CAN/LIN bus behaviorHIL
Interrupt and timing verificationHIL
Overnight fault injection suiteHIL
Parametric sweep (100+ cases)MIL or SIL
Pre-release sign-offHIL
Root-cause analysis of field issueHIL

More Guides

Put this guide into practice