用 ESP32 實作 PID Control|打造你的智慧控制系統
PID Control 控制是現代控制系統中最常見也最實用的方法之一,從工業自動化到 DIY 機器人,都能見到它的身影。PID 是 比例(Proportional)、積分(Integral)、微分(Derivative) 的縮寫,這種控制方式能讓系統即時反應環境變化,像是調節馬達轉速、穩定溫度、甚至讓機器人在兩輪上平衡。
透過 ESP-IDF 開發框架,開發者現在可以直接在 ESP32 上建構具有精準回應與穩定性的智慧控制系統。在這篇文章中,將一步步帶你了解如何在 ESP32 上 設計、實作並調整一個 PID 控制器,使用 C 語言與 FreeRTOS,打造出屬於你的智慧應用。不論你想控制風扇、馬達還是溫度系統,透過 PID 控制,你將擁有穩定且靈活的核心技術。

內容
什麼是 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 開發環境 (至少版本 v5.x 或更高)。
- ESP32 開發板。
- 一個 SPI 接口的 TFT LCD(如 ST7789)。
專案結構
建立一個乾淨的 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 的彈性資源,仍然可以實現出許多強大又實用的功能。