Key Engineering Takeaways
- •Pin deterministic control loops (PID, encoders) to Core 1 and non-deterministic tasks (WiFi, HTTP, micro-ROS) to Core 0.
- •Always use FreeRTOS Queues or atomic variables for thread-safe data transfer between tasks on different cores.
- •Set strict task stack sizes and monitor with uxTaskGetStackHighWaterMark() to prevent memory overflow crashes.
Prerequisites
- • C++ pointers & functions
- • Basic ESP32 programming
Required Hardware / Tools
- • ESP32 DevKit V1 / ESP32-S3
- • USB-C / MicroUSB Cable
Why Dual-Core FreeRTOS for Robotics?
Traditional single-threaded microcontrollers run a monolithic `loop()`. If a WiFi packet stalls or an OLED display draws a frame taking 35ms, your motor control loop halts, causing severe velocity oscillations and robotic arm jerking.
The **ESP32** features two 240MHz 32-bit cores:
- **Core 0 (Protocol Core - PRO_CPU)**: Manages WiFi, Bluetooth, micro-ROS agents, and web servers.
- **Core 1 (Application Core - APP_CPU)**: Dedicates 100% of its clock cycles to deterministic 1kHz motor PID calculations and sensor fusion.
Complete Dual-Core Motor Control Firmware
Here is the production-ready ESP32 FreeRTOS C++ template demonstrating core pinning and inter-task queues:
esp32_dual_core_robot.ino
cpp
#include <Arduino.h>
// Struct for inter-task communication
struct MotorCommand {
float targetVelocityLeft;
float targetVelocityRight;
};
QueueHandle_t motorCommandQueue;
TaskHandle_t taskPidHandle;
TaskHandle_t taskTelemetryHandle;
// CORE 1: Hard Real-Time PID Task (1000 Hz)
void taskPID(void *pvParameters) {
TickType_t xLastWakeTime = xTaskGetTickCount();
const TickType_t xFrequency = pdMS_TO_TICKS(1); // Exactly 1ms (1kHz)
MotorCommand cmd = {0.0f, 0.0f};
for (;;) {
// Check if new command arrived (non-blocking)
if (xQueueReceive(motorCommandQueue, &cmd, 0) == pdTRUE) {
// Apply new setpoints
}
// 1. Read Encoders
// 2. Compute PID Error: e(t) = target - actual
// 3. Output PWM to TB6612 / DRV8833 drivers
// Sleep until next exact 1ms tick
vTaskDelayUntil(&xLastWakeTime, xFrequency);
}
}
// CORE 0: Telemetry, WiFi & Serial Command Task (50 Hz)
void taskTelemetry(void *pvParameters) {
for (;;) {
// Process Serial / WiFi / micro-ROS incoming commands
if (Serial.available() > 0) {
float vL = Serial.parseFloat();
float vR = Serial.parseFloat();
MotorCommand newCmd = {vL, vR};
xQueueSend(motorCommandQueue, &newCmd, portMAX_DELAY);
}
vTaskDelay(pdMS_TO_TICKS(20)); // 50Hz update rate
}
}
void setup() {
Serial.begin(115200);
// Create FIFO Queue holding up to 10 commands
motorCommandQueue = xQueueCreate(10, sizeof(MotorCommand));
// Pin PID Task to Core 1 with High Priority (Priority 5)
xTaskCreatePinnedToCore(
taskPID, "PID_Loop", 4096, NULL, 5, &taskPidHandle, 1
);
// Pin Telemetry Task to Core 0 with Lower Priority (Priority 1)
xTaskCreatePinnedToCore(
taskTelemetry, "Telemetry", 4096, NULL, 1, &taskTelemetryHandle, 0
);
}
void loop() {
// Empty: FreeRTOS tasks manage execution!
vTaskDelete(NULL);
}Tags:#ESP32#FreeRTOS#Dual-Core#Multithreading#Embedded C++#PID Loop