用 ESP32 實作 PID Control|打造你的智慧控制系統


PID Control 控制是現代控制系統中最常見也最實用的方法之一,從工業自動化到 DIY 機器人,都能見到它的身影。PID 是 比例(Proportional)、積分(Integral)、微分(Derivative) 的縮寫,這種控制方式能讓系統即時反應環境變化,像是調節馬達轉速、穩定溫度、甚至讓機器人在兩輪上平衡。

透過 ESP-IDF 開發框架,開發者現在可以直接在 ESP32 上建構具有精準回應與穩定性的智慧控制系統。在這篇文章中,將一步步帶你了解如何在 ESP32 上 設計、實作並調整一個 PID 控制器,使用 C 語言與 FreeRTOS,打造出屬於你的智慧應用。不論你想控制風扇、馬達還是溫度系統,透過 PID 控制,你將擁有穩定且靈活的核心技術。

PID Control

什麼是 PID Control 控制器?

PID 控制器是一種以「誤差」為核心的回授控制系統。簡單來說,它根據目標值(Setpoint)與實際輸出值(Measurement)之間的誤差(Error),動態調整輸出,使系統穩定在期望狀態。

PID 控制的基本公式如下:

u(t) = Kp * e(t) + Ki * ∫e(t)dt + Kd * de(t)/dt

其中:

  • 其中各項代表的意義如下:
  • u(t):控制器的輸出(例如 PWM、DAC 等控制訊號)
  • e(t):目標值與實際值之間的誤差(Setpoint – Measurement)
  • Kp:比例係數(Proportional Gain),對當前誤差立即反應
  • Ki:積分係數(Integral Gain),累積過去誤差,用於消除穩態誤差
  • Kd:微分係數(Derivative Gain),預測誤差變化,用來抑制震盪與過衝

ESP32 中的角色與實作方式

  • 建立 PID 任務(使用 FreeRTOS Task)
  • 讀取實際輸入值(如:溫度、速度)
  • 輸出控制訊號(PWM 控制馬達、風扇或加熱器)
  • 控制回圈週期(可用 Timer 或 vTaskDelayUntil() 精準控制)

開發環境

在開始編程之前,請確保已完成以下準備工作:

專案結構

建立一個乾淨的 ESP-IDF PID Control 專案如下:

esp32_pid_control/
├── main/
│   ├── main.c                   # Application entry (PWM/ADC init, PID task launcher)
│   ├── pid_controller.c         # PID core algorithm implementation
│   ├── pid_controller.h         # PID API header (structs/macros)
│   └── CMakeLists.txt           # Main component build rules
│
├── components/                  # (Optional) Shared modules
│   └── sensor_drivers/          # Sensor abstraction layer
│       ├── temperature.c        # Temp sensor interface 
│       ├── temperature.h        # Sensor calibration macros
│       └── CMakeLists.txt       # Component-specific linking
│
├── sdkconfig                    # ESP-IDF config (auto-generated)
└── CMakeLists.txt               # Top-level project config

為了讓讀者更容易上手,本篇範例程式碼採用單一檔案(main.c)方式撰寫,將 PID 演算法、ADC 讀取與 PWM 控制整合在同一檔案中。這樣的寫法更直觀,也利於快速上手與展示。

PID Control 任務與控制程式碼

pid_controller.h

typedef struct {
    float Kp;
    float Ki;
    float Kd;

    float setpoint;
    float integral;
    float prev_error;

    float output_min;
    float output_max;
} PIDController;

void pid_init(PIDController* pid, float Kp, float Ki, float Kd, float min_out, float max_out);
float pid_compute(PIDController* pid, float input, float dt);

pid_controller.c

#include "pid_controller.h"

void pid_init(PIDController* pid, float Kp, float Ki, float Kd, float min_out, float max_out) {
    pid->Kp = Kp;
    pid->Ki = Ki;
    pid->Kd = Kd;
    pid->integral = 0;
    pid->prev_error = 0;
    pid->output_min = min_out;
    pid->output_max = max_out;
}

float pid_compute(PIDController* pid, float input, float dt) {
    float error = pid->setpoint - input;
    pid->integral += error * dt;
    float derivative = (error - pid->prev_error) / dt;

    float output = pid->Kp * error + pid->Ki * pid->integral + pid->Kd * derivative;

    if (output > pid->output_max) output = pid->output_max;
    if (output < pid->output_min) output = pid->output_min;

    pid->prev_error = error;
    return output;
}

完整 Code

main.c

#include "pid_controller.h"
#include "driver/ledc.h"         // ESP32 LED PWM controller
#include "driver/adc.h"          // ESP32 ADC driver
#include "freertos/FreeRTOS.h"   // FreeRTOS core
#include "freertos/task.h"       // FreeRTOS task control

/* PWM Configuration */
#define FAN_PWM_CHANNEL LEDC_CHANNEL_0  // Use PWM channel 0
#define FAN_PWM_FREQ    25000           // 25kHz PWM frequency (inaudible for fans)
#define FAN_PWM_RES     LEDC_TIMER_10_BIT // 10-bit resolution (0-1023)
#define FAN_GPIO        18               // GPIO pin for fan control

PIDController fan_pid;  // PID controller instance

float read_temperature() {
    int raw = adc1_get_raw(ADC1_CHANNEL_0);  // 12-bit ADC read
    // Simplified conversion - replace with actual sensor calibration
    return (float)raw * 0.1f;  // 0.1°C per LSB
}

void set_fan_speed(float speed_percent) {
    // Convert percentage to duty cycle value
    int duty = (int)((speed_percent / 100.0f) * ((1 << FAN_PWM_RES) - 1));
    ledc_set_duty(LEDC_HIGH_SPEED_MODE, FAN_PWM_CHANNEL, duty);
    ledc_update_duty(LEDC_HIGH_SPEED_MODE, FAN_PWM_CHANNEL);  // Apply new duty
}

void pid_task(void* arg) {
    const TickType_t xInterval = pdMS_TO_TICKS(100); // 100ms control loop
    TickType_t xLastWakeTime = xTaskGetTickCount();

    // Initialize PID with Kp=2.0, Ki=0.5, Kd=1.0
    // Output limits: 0-100% (fan speed range)
    pid_init(&fan_pid, 2.0, 0.5, 1.0, 0.0, 100.0);
    fan_pid.setpoint = 30.0f;  // Target temperature: 30°C

    while (1) {
        vTaskDelayUntil(&xLastWakeTime, xInterval);  // Precise timing

        float temp = read_temperature();
        // Compute PID output with 0.1s time delta (matches 100ms loop)
        float output = pid_compute(&fan_pid, temp, 0.1f);  

        set_fan_speed(output);  // Apply control signal
    }
}

void app_main(void) {
    /* ADC Configuration */
    adc1_config_width(ADC_WIDTH_BIT_12);  // 12-bit resolution
    adc1_config_channel_atten(ADC1_CHANNEL_0, ADC_ATTEN_DB_11);  // 0-3.1V range

    /* PWM Timer Setup */
    ledc_timer_config_t ledc_timer = {
        .speed_mode = LEDC_HIGH_SPEED_MODE,
        .timer_num = LEDC_TIMER_0,
        .duty_resolution = FAN_PWM_RES,  // 10-bit resolution
        .freq_hz = FAN_PWM_FREQ,         // 25kHz
        .clk_cfg = LEDC_AUTO_CLK         // Automatic clock source
    };
    ledc_timer_config(&ledc_timer);

    /* PWM Channel Setup */
    ledc_channel_config_t ledc_channel = {
        .channel = FAN_PWM_CHANNEL,
        .duty = 0,  // Initial duty cycle (fan off)
        .gpio_num = FAN_GPIO,
        .speed_mode = LEDC_HIGH_SPEED_MODE,
        .hpoint = 0,  // Phase offset (not used)
        .timer_sel = LEDC_TIMER_0
    };
    ledc_channel_config(&ledc_channel);

    /* Create PID Control Task */
    xTaskCreate(pid_task,       // Task function
                "pid_task",     // Task name
                4096,          // Stack size (bytes)
                NULL,           // Parameters
                5,             // Priority (higher = more urgent)
                NULL);         // Task handle (not used)
}

說明

  • pid_compute() 是核心演算法,每次計算新的控制量。
  • 使用 adc1_get_raw() 簡化模擬溫度讀值(你可以替換成 NTC 或 DS18B20 的解析方式)。
  • set_fan_speed() 將 PID 輸出(0~100)對應為 PWM duty。
  • vTaskDelayUntil() 確保 loop 間隔穩定,對 PID 演算法的 dt 精度很重要。

編譯和燒錄

完成程式碼後,您可以使用 ESP-IDF 提供的命令進行編譯、燒錄和監控。

在 VS Code 的左下角 ESP-IDF 工具列:

  • 點選 Build project
  • 點選 Flash device
  • 點選 Monitor device

當程式開始執行後,您應該能在序列監控視窗中看到如下資訊(視您程式如何印出):

Current Temp: 28.7 °C
Setpoint: 30.0 °C
PID Output: 52.3 % (Fan speed)


這表示:

  • 感測器持續回報目前的溫度
  • PID 控制器根據設定目標值(Setpoint)與實際值(Measurement)的差異,計算控制輸出
  • 控制輸出(例如風扇轉速)會自動調整,以趨近目標溫度

當溫度逐漸接近目標,輸出也會自動趨緩,呈現平穩控制的效果,這就是 PID 控制器的實際應用成果!

結論

透過本篇實作,我們一步步完成了 PID 控制器的建構,並搭配 ESP32 的硬體資源(PWM、ADC、FreeRTOS)成功驅動出一個可調控輸出的智慧系統。無論你是用來控溫、控速,或未來整合更複雜的感測與網路模組,這樣的基礎架構都能為你的 IoT 或嵌入式應用打下堅實基礎。

PID 雖然是一個經典的控制演算法,但搭配現代 MCU 的彈性資源,仍然可以實現出許多強大又實用的功能。