Master Adaptive PID on ESP32 | A Complete Beginner’s Guide


Adaptive PID (Proportional-Integral-Derivative),PID controllers are widely used in industrial and embedded applications. However, traditional PID controllers with fixed parameters often struggle to cope with dynamic systems. Tuning parameters manually can be time-consuming and unstable.Adaptive PID addresses this problem by automatically adjusting gains based on system error, offering a smarter and more stable control strategy.
This is where Adaptive PID comes into play. And even better—we can implement it using the powerful yet affordable ESP32 microcontroller!

Adaptive PID

What is Adaptive PID?

Traditional PID controllers use fixed parameters for Kp, Ki, and Kd. In contrast, Adaptive PID dynamically adjusts these parameters based on the system state. For example:

  • Lower gains under light load to prevent oscillation
  • Increase Kp for large errors to improve response speed
  • Decrease Ki in stable regions to reduce overshoot

This intelligent gain adjustment makes it especially suitable for systems with highly dynamic behavior.

Adaptive PID Logic

  • Create a PID task (using FreeRTOS Task)
  • Read the actual input (e.g., temperature or speed)
  • Output the control signal (e.g., PWM to drive motor, fan, or heater)
  • Maintain a fixed control loop period (using Timer or vTaskDelayUntil() for accuracy)

Development Environment

Before starting your programming, make sure to complete the following preparations:

Project Structure

Create a clean ESP-IDF PID control project with the following structure:

adaptive_pid_voltage/
├── CMakeLists.txt              # Top-level build script
├── sdkconfig                   # Build-generated config file
├── components/                 # Reusable modules
│   └── pid_controller/
│       ├── CMakeLists.txt      # PID component build script
│       ├── pid_controller.c    # PID algorithm
│       └── pid_controller.h    # PID API
├── main/
│   ├── CMakeLists.txt          # Main app build script
│   ├── main.c                  # app_main and task setup
│   └── simulation.c            # Controlled system simulation
└── README.md                   # Project overview

To simplify onboarding, this example integrates the PID algorithm, ADC reading, and PWM control all in one file (main.c). This approach is intuitive and beginner-friendly for quickly grasping core concepts.

Adaptive PID Controller Code

#include <stdio.h>
#include <math.h>
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "esp_log.h"

static const char *TAG = "VoltagePID";

// PID parameters
float Kp = 1.0f, Ki = 0.5f, Kd = 0.1f;
float setpoint = 100.0f;         // Target motor speed (RPM)
float integral = 0.0f;
float last_error = 0.0f;

// Simulated control output voltage (0 ~ 3.3V)
float control_voltage = 0.0f;

// Simulated feedback value (motor speed)
float motor_speed = 0.0f;

void update_pid(float input) {
    float error = setpoint - input;
    float delta_error = error - last_error;

    // Adaptive gain tuning based on error magnitude
    if (fabsf(error) > 50) {
        Kp = 2.0f; Ki = 0.7f; Kd = 0.2f;
    } else if (fabsf(error) > 20) {
        Kp = 1.5f; Ki = 0.6f; Kd = 0.15f;
    } else {
        Kp = 1.0f; Ki = 0.5f; Kd = 0.1f;
    }

    integral += error;
    float derivative = delta_error;

    float raw_output = Kp * error + Ki * integral + Kd * derivative;

    // Simulate control voltage output: clamp between 0V and 3.3V
    control_voltage = raw_output / 100.0f;  // Scale the output (depends on application)
    if (control_voltage > 3.3f) control_voltage = 3.3f;
    if (control_voltage < 0.0f) control_voltage = 0.0f;

    last_error = error;

    // Log output to monitor behavior
    ESP_LOGI(TAG, "[PID] Speed: %.2f RPM | Error: %.2f | Kp: %.2f | Ki: %.2f | Kd: %.2f | Output Voltage: %.2f V",
             input, error, Kp, Ki, Kd, control_voltage);
}

// Simulate motor response with inertia based on control voltage
void pid_task(void *arg) {
    while (1) {
        update_pid(motor_speed);

        // Simulate motor acceleration (e.g., proportional to voltage)
        float target_speed = control_voltage * 40.0f; // e.g., 3.3V -> ~132 RPM
        motor_speed += (target_speed - motor_speed) * 0.05f;  // Simulate inertia/delay

        // Log the simulated speed for visualization
        ESP_LOGI(TAG, "[SIM] Target Speed: %.2f RPM | Current Speed: %.2f RPM",
                 target_speed, motor_speed);

        vTaskDelay(pdMS_TO_TICKS(100));  // Control cycle: 100 ms
    }
}

void app_main(void) {
    ESP_LOGI(TAG, "Starting Voltage-Controlled PID Simulation...");
    xTaskCreate(pid_task, "pid_task", 4096, NULL, 5, NULL);
}

Compile and Flash

After writing the code, you can use the ESP-IDF tools to build, flash, and monitor:

In the VS Code lower-left ESP-IDF toolbar:

  • Click Build project
  • Click Flash device
  • Click Monitor device

When running, you should see output like:

Output Explanation

Here’s a portion of the log output during simulation:

I (0) VoltagePID: Starting Voltage-Controlled PID Simulation…
I (100) VoltagePID: [PID] Speed: 0.00 RPM | Error: 100.00 | Kp: 2.00 | Ki: 0.70 | Kd: 0.20 | Output Voltage: 3.30 V
I (100) VoltagePID: [SIM] Target Speed: 132.00 RPM | Current Speed: 6.60 RPM
I (200) VoltagePID: [PID] Speed: 6.60 RPM | Error: 93.40 | Kp: 2.00 | Ki: 0.70 | Kd: 0.20 | Output Voltage: 3.30 V
…
I (1000) VoltagePID: [PID] Speed: 85.60 RPM | Error: 14.40 | Kp: 1.00 | Ki: 0.50 | Kd: 0.10 | Output Voltage: 1.82 V
I (1000) VoltagePID: [SIM] Target Speed: 72.80 RPM | Current Speed: 74.00 RPM
…
I (2000) VoltagePID: [PID] Speed: 98.20 RPM | Error: 1.80 | Kp: 1.00 | Ki: 0.50 | Kd: 0.10 | Output Voltage: 1.04 V
I (2000) VoltagePID: [SIM] Target Speed: 41.60 RPM | Current Speed: 97.00 RPM

Analysis:

  • Rapid response with large errors:
    Initially, the motor is stationary (0 RPM), and the error is 100. Adaptive PID increases the gains (Kp=2.0), resulting in maximum output voltage (3.3V) and rapid acceleration.
  • Stable control in mid-range:
    As the speed increases and the error reduces to around 20–50, the gains adjust to moderate levels (Kp=1.5). The system enters a balanced phase of fast convergence with minimal overshoot.
  • Fine-tuning with small errors:
    Once the error is small (e.g., <20), the controller reverts to lower gains (Kp=1.0) to avoid oscillations and gently approach the target speed. Output voltage becomes smoother.
  • No manual tuning required:
    The entire gain tuning is automatic. Adaptive PID dynamically adjusts parameters based on error magnitude, delivering both fast response and system stability.

Conclusion

By implementing Adaptive PID on the ESP32, we’ve demonstrated how automatic gain adjustment can improve control accuracy and stability. This system dynamically adapts its behavior based on error magnitude—speeding up convergence when needed and preventing overshoot during steady states.
Using a simulated motor speed controlled by voltage, we observed how Adaptive PID effectively changes gains on the fly to reduce oscillations and speed up response. This approach not only enhances flexibility but also makes the control system better suited to uncertain real-world conditions.
In the future, this framework can be extended to control other physical quantities like temperature or light intensity, using real ADC feedback and PWM outputs for practical hardware applications.