Worked examples — Software testing in embedded — unit tests on host, HIL testing
This page is a drill hall. The parent note told you the ideas — host unit tests versus HIL testing. Here we walk through every kind of scenario those ideas produce, one worked example per "cell". Guess the answer first (that's the Forecast line), then check yourself against the numbered reasoning.
Before any numbers, let us pin down the small pieces of arithmetic and every symbol the examples reuse, so no notation appears un-earned.
That is the entire toolbox. Everything below is these lines plus careful case-splitting. Two pictures make them concrete:


The scenario matrix
Every testing question in this topic falls into one of these cells. The examples that follow are labelled with the cell they cover, and together they hit all of them.
| Axis | Cell | What could go wrong / what it tests | Example |
|---|---|---|---|
| Signal sign | Positive input | mid-range mapping, rounding | Ex 2 |
| Signal sign | Zero input | 0 count → 0 V → 0% duty, off-by-one at bottom rail | Ex 1a |
| Signal sign | Saturated-high input | clipping / cap at the top rail | Ex 1 |
| Degenerate input | dt = 0 |
divide-by-zero → +inf | Ex 3 |
| Degenerate input | Empty buffer | length 0 → divide-by-zero in an average | Ex 3b |
| Limiting value | 0% and 100% duty | rounding at both rails | Ex 1, Ex 1a |
| Where to test | Pure logic vs hardware timing | choosing host vs HIL | Ex 4 |
| Real-time deadline | vs | ISR latency, missed deadline | Ex 5 |
| Mock correctness | Mock returns wrong / stale value | false green test | Ex 6 |
| Real-world word problem | Throttle → motor mapping end to end | full HIL loop | Ex 7 |
| Coverage / strategy | 80% host-coverage target | test-budget maths | Ex 8 |
| Exam-style twist | "This test passes — why is it worthless?" | flaky/tautological tests | Ex 9 |
Every cell is covered. Let's go.
Forecast: guess the voltage (a bit under 3.3 V? exactly 3.3 V?) and the duty (99.9%? 100%?) before reading on.
- Find the max count. 12-bit → counts , so max count . Why this step? The rail is where off-by-one bugs live; we must use , not .
- Convert to voltage. . Why this step? Confirms the top count maps to exactly — the ruler-end property.
- CCR, then duty. , capped at , so . Why this step? Saturated input should give saturated output; a bug that produced (CCR ) would overflow the timer.
Verify: Voltage V is the reference itself ✓ (units: count/count × V = V). Duty is the physical maximum; anything above is impossible, so the cap is doing its job ✓.
Forecast: is the voltage exactly 0 V or a tiny positive offset? Is the duty exactly 0%, or does rounding sneak in a stray count?
- Find the min count. The bottom rail of a 12-bit ADC is count . Why this step? The zero end has its own off-by-one danger (a bug returning instead of would leak a tiny always-on PWM).
- Convert to voltage. . Why this step? Confirms the bottom count maps to exactly V — the ruler's start.
- CCR, then duty. , so . Why this step? Zero input must give a dead motor. If rounding produced CCR , the motor would creep — exactly the bug a host test at count catches.
Verify: → V ✓; ✓. Both rails (Ex 1 = 100%, Ex 1a = 0%) now bounded, so the linear map is pinned at both ends ✓.
Forecast: is the count exactly , , or ? Is the CCR ? Is the duty exactly ?
- Voltage → count. . Why this step? is not an integer — the ADC must round. Half-scale voltage does not give half-scale count exactly; this asymmetry (0…4095 is an even span, so it has no exact middle) is a classic surprise.
- Count → CCR (the step the formula demands first). . Why this step? We must compute the integer CCR before the duty — the timer can only hold whole numbers, so the rounding here is real hardware behaviour, not a shortcut.
- CCR → duty. . Why this step? After CCR rounds to , the duty lands on an exact — the integer rounding actually cancelled the half-count offset from step 1. That is only visible if you keep CCR as a separate step.
Verify: count ✓; ✓; , inside a tolerance ✓. Keeping the CCR step honours the stated formula and shows the two roundings interact.
Forecast: a crash? a NaN? a huge number? Which output field poisons the result?
- Compute the derivative term literally. .
Why this step? In C floating-point, is not a crash — it yields
+inf. This is a silent poison, worse than a crash. - Propagate through the sum. Output . Why this step? Any finite term added to infinity stays infinity — the whole controller output becomes meaningless.
- The host-test fix. A unit test on host can inject cheaply in microseconds and assert
isfinite(output). On HIL you'd only see a motor slam to full power. Why this step? This is the whole reason degenerate inputs go in the host tier: fast, safe, repeatable.
Verify: IEEE-754 says and ; isfinite(+inf) is false ✓. So an assertion isfinite(pid_update(...)) correctly fails, catching the bug on host.
Forecast: does an empty buffer give , or something worse?
- Substitute the degenerate size. .
Why this step? over zero terms is , and dividing by gives — the worst float result,
NaN(Not-a-Number), which is not even equal to itself. - Trace the poison. Any comparison with
NaNis false, so a laterif (avg > threshold)silently takes the wrong branch — no crash, no warning. Why this step? Shows the empty buffer is a distinct hazard from Example 3:dt=0gave+inf, but0/0givesNaN, which behaves differently in comparisons. - The host-test fix. Inject an empty buffer on host and assert the function guards it (returns a defined default, e.g. , when ) rather than dividing. Why this step? Empty-collection edge cases are invisible on HIL until real startup — cheap to force on host.
Verify: is NaN; NaN == NaN is false (the defining property) ✓. A guarded implementation returning when makes the test pass; the naive one produces NaN and fails ✓.
Forecast: which two are pure maths, which two need real silicon?
- (a) CRC calculator → host. Why? Pure computation on bytes — same result on x86 as on the MCU. Mock nothing.
- (b) UART ISR latency → HIL. Why? Latency is real time; host tests mock the clock, so they cannot measure microseconds of actual response. Needs a logic analyzer on the response pin.
- (c) Kalman update → host. Why? Linear algebra with no I/O. Route sensor reads through the HAL and feed mock data.
- (d) GPIO rises to 3.0 V → HIL. Why? Voltage level is an electrical property; only real hardware and a scope can confirm it.
Verify: The rule is "does the answer change with real timing or real voltage?" — (b) and (d) yes → HIL; (a) and (c) no → host. Two/two split matches the parent's "logic on host, physics on hardware" principle ✓.
Forecast: pass or fail? margin in µs and as a percent?
- Compare worst case to deadline. → pass. Why this step? Real-time correctness is decided by the worst case, never the average — a single missed deadline is a failure.
- Compute absolute margin. . Why this step? Margin tells you how much future code (an added RTOS context switch, a slow I²C read) you can afford before the deadline breaks.
- Compute relative margin. . Why this step? Teams often require ≥ 20% headroom; clears it.
Verify: ✓; margin , i.e. of the deadline ✓ (see the timeline figure). Because we measured the max of 5000 runs (not the mean), the pass verdict is robust to jitter.

Forecast: green or red? and is a green here good news?
- Compute what the mock actually returns. Buggy mock → count , so duty . Why this step? We must trace the value the mock feeds the logic, not the value the test intended to inject.
- Evaluate the assertion. The test asserts
duty == 0, and duty is → the assertion passes. Why this step? A passing assertion is worthless if it passes for the wrong reason — the input never reached the logic. - Design a self-checking mock. Add a read-back assertion: after
set_adc(1, 3000), asserthal_adc_read(1) == 3000. This catches the stale mock before trusting any duty value. Why this step? Tests must verify their own plumbing; otherwise every "green" is suspect.
Verify: With the correct mock, count gives duty , which is nowhere near . So the read-back check hal_adc_read(1) == 3000 would fail on the buggy mock, exposing it ✓.
Forecast: how far off from 50% is 49.8%, and does it clear ±2%?
- Confirm the intended input. V is of full scale → the throttle is genuinely at half. Why this step? Establishes the "expected" number the assertion compares against.
- Compute the error. . Why this step? The spec is an absolute-percent tolerance, so we subtract directly.
- Compare to tolerance. → pass. Why this step? The gap is real hardware reality — ADC noise, DAC settling, integer rounding — and it lives well inside budget.
Verify: , and ✓. Sanity: the sub-1% error is consistent with the integer-rounding of Ex 2, so the number is physically believable, not too-good-to-be-true ✓.
Forecast: which denominator matters — all 10 000 lines, or just the testable logic?
- Identify the testable population. Vendor drivers are assumed tested, so the meaningful denominator is logic + HAL glue lines. Why this step? Counting untestable vendor code in the denominator would unfairly punish the score and hide real gaps.
- Logic-focused coverage. of custom logic. Why this step? This is the number the parent's formula really cares about — bugs hide in your logic.
- Whole-image coverage. . Why this step? Shows how a naive denominator makes a good test suite look bad; always state which denominator you used.
Verify: → ✓ (logic target met). → , which would fail a naive whole-image target — proving denominator choice matters ✓.
Forecast: what is the smallest and largest a duty CCR can ever be?
- Bound the quantity being asserted. A PWM CCR is always a non-negative integer: by construction.
Why this step? The assertion
>= 0is a tautology — it is true for every possible input, including a totally brokencompute_pwmthat returns . - Show it can't distinguish bug from correct. Max ADC should give CCR (from Ex 1). But a bug returning CCR also satisfies
>= 0, so the test is green while the motor is dead. Why this step? A good assertion must be falsifiable — it must fail for at least one wrong output. - Fix it. Replace with the expected value:
assert(hal_pwm_get(1) == 1000);. Why this step? Now a bug returning fails the test, which is the entire point.
Verify: 0 >= 0 is true, so a broken CCR of passes the weak assertion ✓ (it's worthless). The corrected == 1000 gives 0 == 1000 → false, correctly failing on the bug ✓.
Recall Quick self-check
Zero ADC count on a 3.3 V, TOP=1000 system gives what voltage and duty? ::: V and — both bottom rails pinned exactly.
Half-scale voltage (1.65 V on a 3.3 V, 12-bit ADC) gives which count, CCR (TOP=1000), and duty? ::: count , then CCR , then duty .
Why does dt = 0 in the PID give +inf but an empty buffer average gives NaN? ::: (nonzero over zero), but is indeterminate → NaN, which is not even equal to itself.
A worst-case ISR latency of 73 µs against a 100 µs deadline leaves what margin? ::: 27 µs, i.e. 27% headroom.
Why is assert(duty >= 0) worthless? ::: A duty is non-negative by construction, so the assertion is a tautology — it can never fail, even on a broken output.