Mbed OS 5 compatibility layer built on top of ESP-IDF/FreeRTOS, enabling Modbus RTU data logging on ESP32 without a native Mbed port. 18 headers, 6 compilation units — covering threading, GPIO, UART, interrupts, ADC, timers, and mutex primitives — across 5 architecture layers.
Mbed OS 5 targets exclusively ARM Cortex-M architectures. The ESP32 runs on Xtensa LX6 and is driven by ESP-IDF/FreeRTOS — no official Mbed port exists for this target, and porting the full RTOS would be disproportionate to the project scope.
The solution: isolate the application behind a compatibility layer that replays the Mbed OS 5 API on top of native ESP-IDF/FreeRTOS primitives. The application code (main.cpp) uses the Mbed API exclusively. Replacing layer 4 is sufficient to retarget the entire stack to an STM32 or nRF52.
| Mbed OS 5 API | ESP-IDF / FreeRTOS Equivalent | Non-Trivial Notes |
|---|---|---|
| Thread | xTaskCreatePinnedToCore() | Pinned to core 1. Handle stored for stack HWM monitoring. |
| Ticker | esp_timer (ESP_TIMER_TASK) | Callback runs in dedicated task, not ISR — allows gpio_set_level in watchdog callback. |
| Mutex | xSemaphoreCreateMutex() | Direct wrap. Priority inheritance handled by FreeRTOS. |
| Semaphore | xSemaphoreCreateCounting() | Used for producer/consumer sync between SensorPoll and 5 consumer tasks. |
| DigitalOut / GPIO | gpio_set_level() / gpio_driver | Renamed to mbed_hal_gpio_* to avoid symbol collision with ESP32 ROM. |
| Serial / UART | uart_driver_install() | 115200 baud, structured CSV + raw Modbus RTU frames on UART0. Symbol collision avoided via #define rename. |
| InterruptIn | gpio_isr_handler_add() | Rise/fall/any-edge routing via static instance map. ISR service installed once, shared across all instances. |
| AnalogIn | adc1_get_raw() / ADC1 | GPIO→ADC1 channel mapping. 12-bit width, 11dB attenuation (0–3.3V). ADC2 avoided — conflicts with Wi-Fi. |
| DHT11 (1-wire driver) | gpio_config() + esp_timer + portENTER_CRITICAL | Timer-based pulse measurement, not counter loops. Critical section for 40-bit decode (~2.8ms). Auto-retry × 3 with GPIO re-init on failure. |
SensorPoll is the sole producer. All other tasks consume latest_data protected by data_mutex.
Heap remained constant at 257 KB free out of 318 KB throughout the entire test run — zero memory leaks. Core 0 at 0–1% (no application tasks), Core 1 at 20–55% normal operation, peaking at 88% during OLED I2C bursts. Stack high-water mark: SensorPoll 3,436/4,096 bytes free. All tasks well within margins.
Each DHT11 read produces two Modbus frames: a FC03 request (8 bytes) and a response (9 bytes). Temperature and humidity are encoded as 16-bit big-endian integers multiplied by 10 — one decimal place without floating point on the bus. CRC-16 (polynomial 0xA001, standard Modbus) is computed and verified on both frames.
// Real UART output — 115200 baud DATA,11190,5,DHT11_ENV,15.0,65.0,OK MODBUS,11166,REQUEST,0x03,0x0000,2,01 03 00 00 00 02 C4 0B,CRC_OK MODBUS,11166,RESPONSE,0x03,150,15.0,650,65.0,01 03 04 00 96 02 8A 9A D8,CRC_OK SYS,16,257248,318816,19.3,13,0.0,0.0,0.0,HEALTHY // Relay trigger at threshold RELAY,197375,R1:OFF(20.0C/21.0),R2:ON(95.0%/65.0) SYS,197,257184,318816,19.3,13,0.0,55.2,28.4,HEALTHY