
前言:PID 是那種「每個人都用過,但很少人說得清楚」的東西
你做馬達調速、恆溫控制、電池充電、平衡車,最後都會走到同一條路:量測實際值,跟目標比,然後修正輸出。這個循環就是回授控制,而 PID 是它最泛用的一種形式。
它紅了幾十年不是因為數學漂亮,而是因為三個參數就能涵蓋大多數系統,而且不需要你精確知道系統模型。
PID 迴路長什麼樣

連續形式的公式如下:
u(t) = Kp × e(t) + Ki × ∫e(t)dt + Kd × de(t)/dt
但 MCU 上跑的是離散版本,每一項都要換成「取樣點之間」的算法:

這裡有個很常見的災難:如果你的 Δt 不固定,I 與 D 兩項就等於在算錯的東西。把 PID 寫在 loop() 裡的專案,負載一變、參數表現就跟著變,原因就在這。
P、I、D 各自負責什麼

三個字各有分工,記法可以這樣:
- P 看現在:誤差多大就出多少力,反應最快,但純 P 一定會留下穩態誤差
- I 記舊帳:把過去的誤差慢慢補回來,專門消除穩態誤差 —— 代價是可能超調
- D 猜未來:看誤差變化率,提前踩煞車,抑制超調 —— 代價是對雜訊極度敏感
調參數的方法一:Ziegler-Nichols 閉環整定

查到 K_u 與 T_u 之後,直接照下表填參數:
| 控制器類型 | Kp | Ki | Kd |
|---|---|---|---|
| P-only | 0.5 × K_u | — | — |
| PI | 0.45 × K_u | 0.54 × K_u / T_u | — |
| PID | 0.6 × K_u | 1.2 × K_u / T_u | 0.075 × K_u × T_u |
這個方法的思路很聰明:先讓系統震起來,從震盪的資料反推參數。找到極限增益 K_u 與振盪週期 T_u 之後,直接查表就能算出 Kp、Ki、Kd。
要注意的是,這個過程會讓系統持續震盪,所以實務上不會在已經上線的設備上做;通常是拿同型機構在實驗台上先整定一次。
調參數的方法二:手動調諧口訣

沒有量測工具時,這個口訣就是最實用的路線圖。先 Kp、再 Ki、最後 Kd,每次只動一個參數,通常三輪就能到手感不錯的效果。
ESP32 實作
程式結構很單純,就是一組狀態加上一個計算函式。原作的版本把積分飽和的處理(退積分)也寫進去了,這一點很重要:
// PID 控制器結構體
typedef struct {
float Kp, Ki, Kd;
float integral;
float prev_error;
float dt;
float output_min, output_max; // 輸出限制(Anti-Windup)
} PIDController;
void PID_Init(PIDController *pid, float Kp, float Ki, float Kd, float dt) {
pid->Kp = Kp;
pid->Ki = Ki;
pid->Kd = Kd;
pid->integral = 0;
pid->prev_error = 0;
pid->dt = dt;
pid->output_min = 0;
pid->output_max = 255; // PWM 8-bit
}
float PID_Compute(PIDController *pid, float setpoint, float measurement) {
float error = setpoint - measurement;
// P term
float P = pid->Kp * error;
// I term (with anti-windup)
pid->integral += error * pid->dt;
float I = pid->Ki * pid->integral;
// D term (measurement derivative to avoid derivative kick)
float D = pid->Kd * (measurement - pid->prev_measurement) / pid->dt;
// 計算總輸出
float output = P + I - D;
// 輸出限制 + Anti-Windup
if (output > pid->output_max) {
output = pid->output_max;
pid->integral -= error * pid->dt; // 退積分
} else if (output < pid->output_min) {
output = pid->output_min;
pid->integral -= error * pid->dt;
}
pid->prev_error = error;
pid->prev_measurement = measurement;
return output;
}
void setup() {
PIDController motor_pid;
PID_Init(&motor_pid, 2.0, 5.0, 0.1, 0.01); // 100Hz 控制週期
// 每秒讀取編碼器轉速 → PID_Compute → 設定 PWM
}
那個「輸出飽和時不累積積分」的判斷,就是所謂的 Anti-Windup。少了它,你會遇到下面第一個問題。
三個一定會遇到的問題

這三個問題的共同點是:它們都不是「參數調不好」,而是架構沒處理好。積分飽和要寫 Anti-Windup、微分敏感要濾波、Δt 要固定 —— 這三件事做完,你才會發現參數其實不難調。
調參數之前,先確認這三個前提

我在專案上踩過最痛的一次,就是花了一整個下午調參數,最後發現問題是 ADC 的取樣時間不固定。把 PID 移到 Timer 中斷裡之後,同一組參數立刻正常 —— 這就是「前提比參數重要」的實例。
總結
PID 的知識量不大,但要調得好,靠的是紀律。四個重點:
- P 看現在、I 記舊帳、D 猜未來:三者的分工想清楚,就知道該動哪一個
- 先 Kp、再 Ki、最後 Kd:一次只調一個參數
- Δt 必須固定:用 Timer 觸發,別放在 loop() 裡碰運氣
- Anti-Windup 不能省:輸出飽和時停止累積積分
把這四件事做對,PID 就不再是玄學,而是一組有明確物理意義的旋鈕。
文章評論