MyRoboPath
microcontrollers15 min readUpdated 2026-03-09Intermediate

ESP32 FreeRTOS Dual-Core Architecture for Robotics

Harness both Xtensa LX6/LX7 cores on the ESP32: run high-frequency PID motor loops on Core 1 and Wi-Fi telemetry / ROS 2 bridging on Core 0 without jitter.

Kenji Takahashi
Kenji Takahashi
Principal Embedded Systems Engineer

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