Exercises — Software testing in embedded — unit tests on host, HIL testing
Two words you must own before we start:
- Host = the PC you write code on (x86 laptop).
- Target / DUT = the microcontroller (MCU) the firmware really runs on. DUT = Device Under Test, the board sitting on your bench in a HIL rig.
Level 1 — Recognition
Exercise 1.1 (L1)
Classify each item as HOST-testable (pure logic, no real hardware) or HIL-only (needs real timing/hardware): (a) A CRC-16 checksum function over a byte array. (b) Interrupt latency of the UART receive handler. (c) A state machine deciding valve OPEN/CLOSED from booleans. (d) ADC electrical noise at 12-bit resolution.
Recall Solution
- (a) HOST — pure arithmetic on an array, no peripheral. ✅
- (b) HIL — latency is real time elapsed between an event and the interrupt service routine (ISR) running; the host mocks time so it cannot measure it.
- (c) HOST — booleans in, enum out, no hardware.
- (d) HIL — noise is an electrical property of the real analog-to-digital converter; a mock returns exactly the number you injected, so it has zero noise. Rule of thumb: if you can feed it inputs as plain numbers and check outputs as plain numbers, it is host-testable.
Exercise 1.2 (L1)
In the code below, name the one line that blocks host testing and rewrite it using a hal_* call.
void read_sensor(void) {
uint16_t v = ADC1->DR; // line A
store(process(v)); // line B
}Recall Solution
Line A — ADC1->DR reads a physical hardware register that does not exist on your PC, so the program crashes (or won't even link) on the host.
Fix using a HAL seam:
uint16_t v = hal_adc_read(ADC_CHANNEL_1);Now a mock can supply v on the host. Line B (process) was already pure logic and needs no change.
Level 2 — Application
Exercise 2.1 (L2)
A file has 1000 total lines: 120 are vendor driver code, 80 are thin HAL glue, and the rest is your custom logic. The parent note's target is Here "logic code" means your custom logic lines. Do you meet the target on the logic itself? What fraction of the whole file is logic?
Recall Solution
Logic lines . Fraction of the whole file that is logic: So 80% of the file is the logic you must test hard; the other 20% (driver + glue) is either vendor-tested or trivially thin. If you cover all 800 logic lines you hit the coverage bar comfortably. Meets the target. ✅
Exercise 2.2 (L2)
The throttle is read by a 12-bit ADC on a 3.3 V reference. "12-bit" means the ADC reports an integer from up to . The DAC in your HIL rig outputs 1.65 V into that ADC input. What raw ADC integer should the DUT read (ideal, no noise)?
Recall Solution
Max ADC code (this represents the full-scale voltage ). The conversion is linear — code is proportional to voltage: , so , floor . (Half of full scale is code 2047, one below the geometric midpoint because we count from 0.)
Exercise 2.3 (L2)
compute_pwm maps ADC code linearly to PWM duty (tenths of a percent), and caps at 1000. What does it return for the max ADC input 4095, and for code 2047?
Recall Solution
Linear map: , then clamp to .
- code : → hits the cap exactly. Matches the parent note's
assert(... == 1000). - code : .
Level 3 — Analysis
Exercise 3.1 (L3)
A UART-receive ISR must respond within a deadline . In a HIL run, a logic analyzer captures: byte arrives at , response GPIO rises at . Compute the measured latency and state pass/fail. Why could a host unit test never catch a failure here?
Recall Solution
Latency = response time − trigger time: Compare: → PASS (margin ). Why the host can't catch this: on the host, time is mocked — the ISR is called by your test harness as a plain function, so "how long until the hardware runs it" has no meaning. Real ISR latency depends on CPU clock, higher-priority interrupts, and cache/pipeline effects that only exist on the real target.
Exercise 3.2 (L3)
Look at the response-time figure. A change adds a slow blocking read to the motor loop. Before the change the loop measured ; the read takes . The HIL deadline is . Does the test still pass? By how much does the margin shrink?

Recall Solution
New response time = old + added blocking cost: Deadline is , and → FAIL. Old margin was (slack, green in the figure). New margin is (overrun, red). The margin shrank by exactly the we added. This is a timing regression — HIL exists precisely to catch it.
Exercise 3.3 (L3)
A test suite has 40 host unit tests at 3 ms each and 8 HIL tests at 4 s each. Compute total wall-clock time for each tier. What ratio (HIL time : host time) shows why you push logic to the host?
Recall Solution
Host total: . HIL total: . Ratio HIL : host . HIL is ~267× slower here. Every logic bug you can catch on the host saves you a ~267×-more-expensive round trip — that is the whole argument for the two-tier strategy.
Level 4 — Synthesis
Exercise 4.1 (L4)
Design the seams for this function so it becomes host-testable, and say what each hal_* mock returns/records:
void safety_check(void) {
if (ADC2->DR > 3000) // over-temperature
GPIOB->BSRR = (1u << 5); // set fault LED
else
GPIOB->BRR = (1u << 5); // clear fault LED
}Then write the two host test cases needed to cover both branches, and give the boundary ADC value that separates them.
Recall Solution
Seams (mock/stub them):
hal_adc_read(ADC_TEMP)— a stub you set; returns the injected value.hal_led_set(FAULT_LED, on)— a mock that records the last state (spy), so the test can read it back. Refactored:
void safety_check(void) {
bool over = hal_adc_read(ADC_TEMP) > 3000;
hal_led_set(FAULT_LED, over);
}Boundary: the branch flips between code (LED off, > is false) and (LED on). So the separating boundary sits between 3000 and 3001.
Two tests (cover both branches + boundary):
// branch 1: below/at threshold -> LED off
mock_set_adc(ADC_TEMP, 3000); safety_check(); assert(mock_led == 0);
// branch 2: above threshold -> LED on
mock_set_adc(ADC_TEMP, 3001); safety_check(); assert(mock_led == 1);Picking 3000 and 3001 (not 0 and 4095) tests the decision, which is where bugs live.
Exercise 4.2 (L4)
You have a Makefile with two targets (firmware for ARM, test_runner for host). In your CI pipeline you want the fast tier to gate every commit and the slow tier to run nightly. Draw the decision flow (as a mermaid graph) and justify which tier gates commits.
Recall Solution
Host tests gate every commit because they are ~hundreds of times faster (Ex 3.3) and debuggable; HIL runs nightly on the shared rig because it is slow and hardware-limited.
Level 5 — Mastery
Exercise 5.1 (L5)
Full HIL response-time test. The rig sets the throttle DAC to 0 V at absolute time . The DUT's PWM must fall below 5% duty. The logic analyzer shows PWM crosses 5% at . The deadline is 10 ms. (a) Compute the response time. (b) Pass or fail, and the remaining margin. (c) A fault-injection variant holds the ADC line at a stuck-high fault (throttle appears at max forever). Predict the PWM the DUT drives and whether the "drop below 5%" test can ever pass. What does this reveal about the combination of HIL + fault injection?
Recall Solution
(a) Response time is a delta of the two edges:
(b) → PASS, margin .
(c) Stuck-high ADC → the DUT believes throttle is at max code 4095 forever → compute_pwm returns the cap 1000 tenths = 100% duty (from Ex 2.3). It never drops below 5%, so the while(pwm > 5.0) loop spins forever → the test times out / fails — which is the correct result: the firmware has no independent way to detect the frozen sensor.
Insight: host tests verify the control math; plain HIL verifies timing on real hardware; HIL + fault injection verifies safety behaviour under broken hardware. Only the combination exposes that a stuck sensor drives the motor to full power — the most dangerous, most real-world failure of the three.
Exercise 5.2 (L5)
Budget a mixed strategy. You must reach logic coverage on 800 logic lines. Host tests cover 12 lines each; each host test costs 3 ms. HIL must additionally verify 3 timing deadlines at 4 s each. Compute: (a) minimum host tests needed, (b) host-tier wall-clock time, (c) total suite time including the 3 HIL tests, (d) is the plan sane for per-commit gating if the gate budget is 2 seconds?
Recall Solution
(a) Lines to cover . Tests needed host tests. (b) Host time . (c) HIL time . Total . (d) The gate runs only the host tier: budget → sane, gate on host. The 12 s of HIL runs nightly, not per-commit. This is exactly the two-tier split from Ex 4.2, now quantified.
Recall Quick self-check
Full-scale code of a 12-bit ADC ::: 4095 (that is , counting from 0) Why can't host tests measure ISR latency ::: host mocks time, so real elapsed duration has no meaning; only real hardware has a real clock Latency from two logic-analyzer timestamps ::: response edge minus trigger edge (a difference, never a single stamp) Which tier gates every commit and why ::: host unit tests — hundreds of times faster and easy to debug Where do you place threshold boundary tests ::: just below and just above the threshold, not at the extremes