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:
pure logic on a PC
→ component unit tests on ESP32/STM32
→ integration tests
→ HIL on Raspberry Pi
→ whole-system testsThe 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:
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:
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:
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:
input_driver:
GPIO/expander/ADC → raw_mask
phase_detector_core:
raw_mask + timestamp → phase_event_t
phase_service:
phase_event_t → FreeRTOS queue/MQTT/CANThe step() pattern
An infinite task is inconvenient to test:
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:
return esp_timer_get_time() - s_start_us > 5000000;Better approach:
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:
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:
- 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.