1. Today’s topic

How to separate application logic from ESP-IDF, STM32 HAL, and real hardware so that hundreds of tests can run on Linux/Windows in seconds. The right sequence:

text
pure logic on a PC
→ component unit tests on ESP32/STM32
→ integration tests
→ HIL on Raspberry Pi
→ whole-system tests

The central idea: the less a module knows about GPIO, UART, FreeRTOS, and HAL, the easier it is to demonstrate that its logic works correctly.

2. Why this matters

Without a board you can test:

text
EC25:
  AT parser, fragmented UART input, timeout, state machine.
Traffic light:
  debounce, AC pulse window, RED/YELLOW/GREEN, conflict.
RS-485/CAN:
  CRC, codec, Modbus parser, CAN ID map.
Fault manager:
  fault classification, retry limits, degraded mode.
Configuration:
  defaults, validation, migration, A/B selection.

3. Theory

Host unit tests

They run on a PC and should be fast and deterministic, without real time, FreeRTOS tasks, or MCU registers. We test:

text
phase_detector_update()
at_parser_feed()
modbus_crc16()
config_validate()
fault_policy_decide()
mqtt_message_encode()

Target unit tests

They run on ESP32/STM32 and test FreeRTOS, the allocator, hardware timers, GPIO loopback, UART loopback, DMA, and callback/interrupt order. HIL

Tests the physical system: optocouplers, EC25, I2C, CAN/RS-485, power, watchdog, and OTA rollback.

The core + adapter pattern

Poor approach:

c
void phase_detector_update(void)
{
    int red = gpio_get_level(RED_GPIO);
    int64_t now = esp_timer_get_time();
    xQueueSend(phase_queue, ...);
    ESP_LOGI(TAG, "Phase changed");
}

Better approach:

text
input_driver:
  GPIO/expander/ADC → raw_mask
phase_detector_core:
  raw_mask + timestamp → phase_event_t
phase_service:
  phase_event_t → FreeRTOS queue/MQTT/CAN

The step() pattern

An infinite task is inconvenient to test:

c
void modem_task(void *arg)
{
    while (1) {
        modem_service_step(&s_modem, platform_time_us());
        vTaskDelay(pdMS_TO_TICKS(10));
    }
}

Now a test calls modem_service_step() with virtual time.

Time as an argument

Poor approach:

c
return esp_timer_get_time() - s_start_us > 5000000;

Better approach:

c
bool timeout_expired(int64_t now_us, int64_t start_us, int64_t timeout_us)
{
    return now_us - start_us >= timeout_us;
}

4. Common mistakes

  • Testing a function together with HAL.
  • Using real sleep(5) delays.
  • Checking internal indices rather than behavior.
  • Testing only the happy path.
  • A mock always returns success.
  • Writing a separate copy of the algorithm inside the test.
  • Treating coverage as proof of quality.

5. Practical assignment

Create a host-compatible phase_detector_core. Interface:

c
typedef enum {
    PHASE_NONE = 0,
    PHASE_RED,
    PHASE_YELLOW,
    PHASE_GREEN,
    PHASE_CONFLICT,
} phase_t;
typedef struct {
    bool initialized;
    uint8_t stable_mask;
    uint8_t candidate_mask;
    uint64_t candidate_since_us;
    uint32_t debounce_us;
    uint32_t sequence;
} phase_detector_t;
typedef struct {
    bool emitted;
    phase_t phase;
    uint8_t mask;
    uint32_t sequence;
    uint64_t timestamp_us;
} phase_event_t;

Tests:

text
- RED emitted only after debounce.
- same stable phase is not emitted twice.
- short GREEN glitch does not replace RED.
- stable GREEN replaces RED.
- RED+GREEN is CONFLICT.

6. What to try next

  • Add tests for the EC25 AT parser.
  • Add config migration tests.
  • Add fault manager tests.
  • Run host tests in CI before building firmware.

Exercise

Test debounce without sleeping. Describe the sequence of raw masks and timestamps that proves a short GREEN glitch does not replace stable RED.

Self-check criteria: Use virtual time and public behavior; avoid relying on internal indices or duplicating the implementation in the test.

Show the supplied answer

Feed the pure detector explicit timestamps and masks: establish stable RED, inject GREEN for less than the configured debounce interval, then return to RED. Assert observable phase events and absence of a spurious GREEN event.

Exercise

Classify an AT parser test, a UART DMA loopback test, and a modem power-loss test as host, target, or HIL tests. Explain what each can actually prove.

Self-check criteria: State the boundary and limitation of each test; a host mock cannot prove physical signal timing or actual modem recovery.

Show the supplied answer

AT parsing with supplied byte fragments is a host test of pure parsing logic. UART DMA loopback is a target test of peripheral/driver behavior. Modem power loss is a HIL test of hardware and recovery integration.