A lightweight, plug-and-play boilerplate that eliminates blocking delays in ESP32 IoT projects β by splitting hardware and network tasks across two physical CPU cores using FreeRTOS.
Author: Sugavanam M
In standard Arduino/ESP32 development, the loop() function executes tasks sequentially. When your device tries to push data to a cloud endpoint (HTTP POST, MQTT publish, Firebase write), network latency causes the entire microcontroller to stall for several seconds.
During this freeze window β any incoming sensor readings, hardware interrupts, or button presses are permanently lost.
Standard Arduino Flow (Broken):
[Read Sensor] βββΊ [Upload to Cloud] βββΊ STALL (2β5 sec) βββΊ [Read Sensor] βββΊ ...
β
Hardware events missed here β
This is the single biggest reliability issue in real-world IoT deployments.
This framework solves the bottleneck by pinning tasks to the ESP32's two independent physical cores via FreeRTOS, with a thread-safe inter-core pipeline connecting them.
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β ESP32 Chip β
β β
β ββββββββββββββββββββ ββββββββββββββββββββββββ β
β β CORE 0 β β CORE 1 β β
β β Hardware Thread β β Telemetry Thread β β
β β β β β β
β β Β· Sensor Poll ββββββββΊβ Β· WiFi / MQTT β β
β β Β· GPIO Monitor β Queue β Β· Cloud Uploads β β
β β Β· Interrupts β(RTOS) β Β· Database Writes β β
β β β β β β
β β Never sleeps β
β β Network lag safe β
β β
β ββββββββββββββββββββ ββββββββββββββββββββββββ β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
| Core | Role | Characteristics |
|---|---|---|
| Core 0 | Hardware Thread | Continuous sensor polling, GPIO interrupts β never blocked |
| Core 1 | Telemetry Thread | WiFi negotiation, cloud uploads, DB writes β runs asynchronously |
| QueueHandle_t | Inter-Core Pipeline | Thread-safe data transfer β no race conditions, no memory corruption |
You do not need to understand RTOS internals to use this. Just drop your existing code into the designated application blocks β the framework handles all the concurrency.
Modify the ProjectPacket_t struct to match the variables your sensor produces:
typedef struct {
float temperature;
float humidity;
int objectDetected;
long timestamp;
} ProjectPacket_t;Paste your digitalRead(), analogRead(), or module polling code inside App_HardwareSensingLoop(). When an event occurs, push it to the inter-core queue:
void App_HardwareSensingLoop(void *pvParameters) {
ProjectPacket_t packet;
while (true) {
// Your sensor code here
packet.temperature = readTemperatureSensor();
packet.objectDetected = digitalRead(IR_PIN);
// Push to inter-core pipeline (non-blocking, 10ms timeout)
xQueueSend(globalDataQueue, &packet, pdMS_TO_TICKS(10));
vTaskDelay(pdMS_TO_TICKS(100)); // Poll every 100ms
}
}Paste your WiFi and cloud upload code inside App_NetworkCloudLoop(). This task automatically blocks until data arrives from Core 0 β consuming zero CPU while waiting:
void App_NetworkCloudLoop(void *pvParameters) {
ProjectPacket_t receivedData;
while (true) {
// Blocks here until Core 0 sends data β no polling needed
if (xQueueReceive(globalDataQueue, &receivedData, portMAX_DELAY) == pdPASS) {
// Upload to your cloud platform here:
uploadToGoogleSheets(receivedData);
// OR: publishMQTT(receivedData);
// OR: firebaseWrite(receivedData);
}
}
}void setup() {
Serial.begin(115200);
// Create the inter-core queue (holds up to 10 packets)
globalDataQueue = xQueueCreate(10, sizeof(ProjectPacket_t));
// Pin tasks to specific physical cores
xTaskCreatePinnedToCore(App_HardwareSensingLoop, "HW_Task", 4096, NULL, 2, NULL, 0);
xTaskCreatePinnedToCore(App_NetworkCloudLoop, "Net_Task", 4096, NULL, 1, NULL, 1);
// Kill the default Arduino loop() task (saves clock cycles)
vTaskDelete(NULL);
}
void loop() { /* Intentionally empty β FreeRTOS tasks take over */ }| Metric | Standard loop() |
This Framework |
|---|---|---|
| Sensor data loss during upload | β Yes | β Zero |
| Network stall affects hardware | β Yes | β Isolated |
| Code complexity | Low | Low (abstracted) |
| CPU utilization | ~100% busy wait | Optimized (task blocking) |
| Scalable to more tasks | β No | β Yes |
- Zero Data Loss β Hardware triggers are captured even under heavy network load
- Plug & Play β RTOS complexity is fully abstracted; just fill in your application code
- True Parallelism β Two tasks run simultaneously on two separate physical CPU cores
- Thread-Safe Pipeline β
QueueHandle_tprevents race conditions and memory corruption - Power Efficient β Default Arduino
loop()task is killed, freeing unnecessary clock cycles - Cloud Agnostic β Works with Google Sheets, Firebase, AWS IoT, MQTT, and any HTTP endpoint
ESP32_DualCore_Framework/
βββ ESP32_DualCore_Framework.ino # Main entry point β setup() and task init
βββ app_hardware.cpp # Core 0: sensor polling & hardware logic
βββ app_network.cpp # Core 1: WiFi, cloud upload & telemetry logic
βββ config.h # ProjectPacket_t struct & global queue handle
βββ README.md
| Category | Tool / Technology |
|---|---|
| Microcontroller | ESP32 (Dual-Core Xtensa LX6, 240 MHz) |
| Real-Time OS | FreeRTOS (built into ESP-IDF / Arduino Core) |
| IDE | Arduino IDE / VS Code + PlatformIO |
| Language | C++ (Arduino Framework) |
| Concurrency Primitives | QueueHandle_t, xTaskCreatePinnedToCore() |
- Understood the root cause of blocking delays in single-threaded MCU firmware
- Implemented true hardware-software task isolation using FreeRTOS on dual-core ESP32
- Used
QueueHandle_tas a thread-safe inter-core communication channel - Designed a reusable, abstracted firmware template applicable to any IoT use case
Feel free to fork this repository and use this structural blueprint as the foundation for your own IoT projects. Pull requests for improvements, additional cloud integrations, or hardware drivers are welcome!
This project is licensed under the MIT License β free to use, modify, and distribute for educational and personal projects.
Built to never miss a sensor tick β Sugavanam M