Software testing in embedded — unit tests on host, HIL testing
What We're Solving
Embedded systems have three testing challenges:
- Slow iteration: Flashing firmware to hardware takes seconds/minutes
- Limited observability: No console, limited debugging on tiny MCUs
- Hardware dependencies: Code depends on sensors, timers, interrupts that don't exist on your dev machine
We solve this with a two-tier testing strategy:
- Unit tests on host (fast, isolated logic-focused)
- Hardware-in-the-loop (HIL) testing (real hardware, real-time verification)
Unit Tests on Host
Why This Works
Key Insight: Most embedded logic is pure computation that doesn't need real hardware:
- Sensor data processing algorithms (Kalman filters, PID controllers)
- Protocol parsing (UART frames, CAN messages)
- State machines and business logic
- Data structure operations
What we mock: The hardware-specific I/O—GPIO reads, ADC conversions, timer interrupts.
How to Structure for Host Testing
1. Hardware Abstraction Layer (HAL)
Separate hardware access from logic:
// ❌ BAD: Direct hardware access
void update_motor(void) {
uint16_t adc_value = ADC1->DR; // Direct register read
int pwm = compute_pwm(adc_value);
TIM2->CCR1 = pwm; // Direct register write
}
// ✅ GOOD: Abstracted through HAL
void update_motor(void) {
uint16_t adc_value = hal_adc_read(ADCHANNEL_1);
int pwm = compute_pwm(adc_value); // Pure logic!
hal_pwm_set(PWM_CHANNEL_1, pwm);
}Why this step? By routing all hardware access through hal_* functions, we can swap implementations: real hardware for production, mocks for testing.
2. Mock Implementation for Host
// mock_hal.c (compiled only for tests)
static uint16_t mock_adc_values[4] = {0};
void mock_hal_set_adc(int channel, uint16_t value) {
mock_adc_values[channel] = value;
}
uint16_t hal_adc_read(int channel) {
return mock_adc_values[channel];
}Now your test can inject inputs:
// test_motor_control.c
void test_motor_pwm_limits(void) {
mock_hal_set_adc(ADC_CHANNEL_1, 4095); // Max ADC value
update_motor();
assert(hal_pwm_get(PWM_CHANNEL_1) == 1000); // Check PWM cap
}Why this works: The logic function compute_pwm() runs on host CPU with mock data. No hardware needed.
3. Build System Setup
You need two compilation targets:
# Target: ARM Cortex-M4
firmware.elf: main.c motor_control.c hal_stm32.c
arm-none-eabi-gcc -mcpu=cortex-m4 -o firmware.elf $^
# Host: x86_64 Linux
test_runner: test_motor_control.c motorc mock_hal.c
gcc -DUNIT_TEST -o test_runner $^ -lunity
./test_runnerWhy separate builds? Different compilers, different HAL implementations, different entry points (main vs test framework).
Hardware-in-the-Loop (HIL) Testing
What HIL Tests That Host Tests Cannot
- Real-time behavior: ISR latency, task scheduling, race conditions
- Hardware quirks: ADC noise, GPIO bounce, clock drift
- Integration: Multiple peripherals interacting (SPI + UART + Timers)
- Electrical characteristics: Signal rise times, voltage thresholds
HIL Test Setup Architecture

Components:
- Device Under Test (DUT): Your embedded board running firmware
- Test Controller: PC or automation system running test scripts
- Stimulus Generation: DACs, signal generators to simulate sensors
- Response Capture: ADCs, logic analyzers to read DUT outputs
- Communication Bus: UART/CAN/Ethernet for command/telemetry
Example: Motor Control HIL Test
Scenario: Test that motor PWM responds correctly to ADC input changes.
Setup:
- Test controller sends analog voltage (simulating throttle) via DAC
- DAC output → DUT ADC input
- DUT PWM output → Counter/Timer on test controller
Test script (Python pseudocode):
def test_motor_response_time():
# Set throttle to 50%
test_rig.set_dac_voltage(ADC_THROTTLE, 1.65) # 1.65V = 50% of 3.3V
time.sleep(0.01) # Allow settling
# Measure PWM duty cycle
pwm_duty = test_rig.measure_pwm(PWM_MOTOR)
# Verify mapping: 50% throttle → 50% PWM (within 2%)
assert abs(pwm_duty - 50.0) < 2.0
# Measure response time
start = time.perf_counter()
test_rig.set_dac_voltage(ADC_THROTTLE, 0.0) # Throttle to 0
while test_rig.measure_pwm(PWM_MOTOR) > 5.0: # Wait for PWM to drop
pass
response_time = time.perf_counter() - start
assert response_time < 0.010 # Must respond within 10msWhy each step?
- Set DAC: Create repeatable analog input (real sensor would vary)
- Measure PWM: Verify control law math matches reality
- Time measurement: Catch timing regressions (e.g., added slow I²C read)
Automated HIL with CI/CD
Modern approach: HIL test rig on a Jenkins/GitLab runner
# .gitlab-ci.yml
hil-tests:
stage: test
tags:
- hil-runner # Runner with testrig attached
script:
- make flash_dut # Flash firmware to DUT
- python3 hil_tests/run_all.py
artifacts:
reports:
junit: hil_tests/results.xmlWhy automate? Regression detection—new commit breaks timing, test fails, blocks merge.
Recall Explain to a 12-Year-Old
Imagine you're building a robot that responds to a light sensor:
Testing on your computer (host tests): You pretend the sensor says "it's bright!" and check if your code calculates the right motor speed. This is super fast—you can test 100 ideas in a minute. But you're just testing your math and logic, not whether the real sensor works.
Testing with the real robot (HIL): You shine an actual flashlight at the real sensor and watch the real motors spin. This is slower—you have to walk to the robot, shine the light, measure stuff. But now you're testing everything together: the sensor, the wires, the microchip's speed, even if the motors shake too much.
Smart strategy: Do the fast computer tests first to fix90% of bugs. Then do the real robot tests to make sure it actually works in the real world. It's like checking your math homework on paper first (fast) before building the science fair project (slow but proves it works).
Connections
- Hardware Abstraction Layer (HAL) Design - Structure for testability
- Mocking and Stubbing in C - Techniques for host tests
- Real-Time Operating Systems (RTOS) - Timing guarantees HIL verifies
- Continuous Integration for Embedded - Automating both testiers
- Logic Analyzers and Oscilloscopes - Tools for HIL debugging
- Fault Injection Testing - Advanced HIL technique
#flashcards/coding
What are the twoiers of embedded software testing? :: Host-based unit tests (fast, logic-focused) and Hardware-in-the-Loop (HIL) testing (real hardware, real-time verification)
Why can't you test real-time behavior with host unit tests?
What is a Hardware Abstraction Layer (HAL) in embedded testing?
What is the 80% coverage rule for embedded host tests?
What does HIL stand for and what does it test?
Why is HIL testing slower than host testing?
What is the correct ratio of host tests to HIL tests?
In HIL testing, what is a Device Under Test (DUT)? :: The embedded board/system running your firmware that you're testing with external stimulus and measurement equipment
What timing requirement must HIL tests verify for real-time systems?
What is the main mistake developers make with HIL testing?
Concept Map
Hinglish (regional understanding)
Intuition Hinglish mein samjho
Embedded software testing ka sabse bada challenge yeh hai ki tumhara code ek chote se microcontroller mein chalta hai jisme limited memory hai, debuger mushkil se attach hota hai, aur har baar firmware flash karne mein time lagta hai. Agar tum har chhoti bug ke liye actual hardware pe test karoge, toh development bahut slow ho jayegi.
Solution yeh hai: pehle apne laptop pe test karo (host-based unit tests), jahan sab kuch fast hai—compiler bhi fast, test runner bhi fast. Tum hardware ko "mock" kar sakte ho, matlabagar tumhara code ADC sensor value padhta hai, toh test mein tum ek fake value de sakte ho aur dekhte ho ki tumhara PID controller sahi calculate kar raha hai ya nahi. Isse tumhare logic bugs jaldi pakde jate hain bina hardware ke pas jaye.
Lekin sirf laptop pe testing kafi nahi hai, kyunki real hardware mein timing issues, interrupt delays, aur electrical noise hoti hai jo simulate nahi ho sakta. Isliye Hardware-in-the-Loop (HIL) testing karte hain—actual microcontroller pe firmware chalate ho, aur ek test setup sensors simulate karte ho (DAC se voltage bhejte ho) aur outputs measure karte ho (oscilloscope se PWM dekhte ho). Yeh slow hai but yeh guarantee deta hai ki tumhara system real-world mein kaam karega.
Smart developers pehle 100 unit tests laptop pe chalate hain har commit pe (CI mein), aur phir major releases se pehle 10-15 HIL tests chalate hain test rig pe. Yeh balance rakhna zaroori hai—host tests se fast iteration, HIL tests se final confidence. Agar tum seedha hardware pe jump karoge without host tests, toh bahut time waste hoga simple bugs fix karne mein.