03 · Embedded Systems · Protocol · Complete

Mbed OS 5 HAL
+ Modbus RTU on ESP32

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.

C/C++ESP-IDF Mbed OS 5FreeRTOS Modbus RTUCRC-16 DHT11SSD1306
The Problem

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.

5-Layer Architecture
Layer 5
Application — main.cpp
Pure Mbed OS 5 API: DHT11, Ticker, Mutex, Semaphore, Thread. Fully portable — no ESP-IDF calls.
Layer 4
HAL Compatibility — mbed_hal_esp32.cpp / mbed_hal_serial.cpp
18 headers in drivers/, hal/, platform/. 6 compilation units. Maps Thread, Ticker, Mutex, Semaphore, DigitalOut, Serial, InterruptIn, AnalogIn, and DHT11 to ESP-IDF/FreeRTOS. The core of the project.
Layer 3
Business Drivers
DHT11.h (1-wire with critical section), LCD_I2C via U8g2 (full-buffer SSD1306), ModbusMaster, CSVLogger.
Layer 2
ESP-IDF
gpio_driver, uart_driver, i2c_driver, esp_timer. Native hardware abstraction.
Layer 1
FreeRTOS + Hardware
Xtensa LX6 dual-core @ 240 MHz. FreeRTOS scheduler. All task synchronization runs here.
HAL Mapping — Mbed → ESP-IDF/FreeRTOS
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.
7-Task Concurrent Execution

SensorPoll is the sole producer. All other tasks consume latest_data protected by data_mutex.

SensorPoll DHT11 read every 2s under portENTER_CRITICAL (2.8ms). Up to 3 retries on failure. Signals 5 consumers via semaphore. Producer · Core 1
ModbusRTU Builds FC03 request (8B) + response (9B). Temp/humidity encoded as 16-bit big-endian × 10. CRC-16 validated both directions. Consumer · UART
OLEDDisp U8g2 full-buffer mode — 1KB frame composed in RAM, sent as single I2C burst. No flicker. Shows values, relay states, heap, CPU. Consumer · I2C
CSVLogger Structured CSV output on UART0. Timestamped rows with all sensor data and system metrics. Consumer · UART
RelayCtrl Polls every 2050ms. Hysteresis ±1 unit on both relays — prevents chatter when sensor oscillates around threshold. Only state changes logged. Consumer · GPIO25/26
SystemMetrics FreeRTOS runtime stats: heap usage, stack HWM per task, CPU load per core. Real-time dashboard output. Monitor · Both Cores
Watchdog (Ticker) Fires every 3s. Detects sensor freeze if no new reading in 9s. Implemented via esp_timer in task mode. Safety · Core 1
Validation Metrics
100%
DHT11 Success Rate
100%
CRC Valid (180 frames)
19.3%
Heap Usage (stable)

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.

Modbus RTU Implementation

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
Hardware
ESP32-DevKit Xtensa LX6 dual-core 240 MHz, 520 KB SRAM, 4 MB Flash
DHT11 — GPIO4 1-wire protocol, integer resolution, 2s acquisition interval. Critical section during 40-bit decode (2.8ms).
SSD1306 OLED — GPIO21/22 128×64, I2C at 0x3C. U8g2 full-buffer mode — 1KB frame, single burst, no flicker.
Relay R1 — GPIO25 Active-low, temperature threshold. Hysteresis ±1°C. Triggered at 21°C, released at 20°C in testing.
Relay R2 — GPIO26 Active-low, humidity threshold. Active continuously at 65–95% humidity during test run.