5.5.20Embedded Systems & Real-Time Software

Software testing in embedded — unit tests on host, HIL testing

2,447 words11 min readdifficulty · medium

What We're Solving

Embedded systems have three testing challenges:

  1. Slow iteration: Flashing firmware to hardware takes seconds/minutes
  2. Limited observability: No console, limited debugging on tiny MCUs
  3. 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_runner

Why 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

  1. Real-time behavior: ISR latency, task scheduling, race conditions
  2. Hardware quirks: ADC noise, GPIO bounce, clock drift
  3. Integration: Multiple peripherals interacting (SPI + UART + Timers)
  4. Electrical characteristics: Signal rise times, voltage thresholds

HIL Test Setup Architecture

Figure — Software testing in embedded — unit tests on host, HIL testing

Components:

  1. Device Under Test (DUT): Your embedded board running firmware
  2. Test Controller: PC or automation system running test scripts
  3. Stimulus Generation: DACs, signal generators to simulate sensors
  4. Response Capture: ADCs, logic analyzers to read DUT outputs
  5. 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 10ms

Why 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.xml

Why 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?
Host tests run on a fast PC with mocked time—they can't measure actual MCU interrupt latency, task scheduling, or hardware timing
What is a Hardware Abstraction Layer (HAL) in embedded testing?
A set of functions (hal_adc_read, hal_pwm_set) that isolate hardware access, allowing you to swap real implementations for mocks during testing
What is the 80% coverage rule for embedded host tests?
Aim to test 80%+ of logic code (algorithms, state machines) on host, since that's where most bugs hide—HAL glue and drivers are thin and vendor-tested
What does HIL stand for and what does it test?
Hardware-in-the-Loop—tests firmware on real target hardware with simulated sensors/loads to verify timing, integration, and electrical behavior
Why is HIL testing slower than host testing?
HIL requires flashing firmware to hardware (seconds), setting up test rigs, and running tests in real-time—host tests execute in microseconds with no hardware dependency
What is the correct ratio of host tests to HIL tests?
Aim for ~10:1 ratio—many fast host tests to catch logic bugs, fewer HIL tests to verify integration and real-time behavior

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?
That measured response time T_measured < T_deadline for all test cases, using real hardware clocks and measurement tools
What is the main mistake developers make with HIL testing?
Over-relying on HIL for all testing—it's too slow and expensive for catching logic bugs that host tests catch in milliseconds

Concept Map

includes

includes

includes

solved by

tier 1

tier 2

catches

requires

enables

allows

verifies

Embedded Testing Challenges

Slow Iteration

Limited Observability

Hardware Dependencies

Two-Tier Strategy

Host Unit Tests

HIL Testing

Logic Bugs Fast

Hardware Abstraction Layer

Mock Implementations

Inject Test Inputs

Real-Time Timing & Hardware

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.

Go deeper — visual, from zero

Test yourself — Embedded Systems & Real-Time Software