0x6A Logbook

0x6A Logbook
Shi6a的筆記本
  1. 首頁
  2. 程式開發
  3. 正文

PID 演算法完整教學:從比例積分微分控制到 ESP32 實作

2026 年 5 月 21 日 772點熱度 0人點贊 0條評論

PID 演算法完整教學封面

前言:PID 是那種「每個人都用過,但很少人說得清楚」的東西

你做馬達調速、恆溫控制、電池充電、平衡車,最後都會走到同一條路:量測實際值,跟目標比,然後修正輸出。這個循環就是回授控制,而 PID 是它最泛用的一種形式。

它紅了幾十年不是因為數學漂亮,而是因為三個參數就能涵蓋大多數系統,而且不需要你精確知道系統模型。

PID 迴路長什麼樣

PID 控制迴路方塊圖

連續形式的公式如下:

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

但 MCU 上跑的是離散版本,每一項都要換成「取樣點之間」的算法:

PID 離散化公式與程式對應

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

P、I、D 各自負責什麼

P、I、D 的作用與調太大的後果

三個字各有分工,記法可以這樣:

  • P 看現在:誤差多大就出多少力,反應最快,但純 P 一定會留下穩態誤差
  • I 記舊帳:把過去的誤差慢慢補回來,專門消除穩態誤差 —— 代價是可能超調
  • D 猜未來:看誤差變化率,提前踩煞車,抑制超調 —— 代價是對雜訊極度敏感

調參數的方法一:Ziegler-Nichols 閉環整定

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。少了它,你會遇到下面第一個問題。

三個一定會遇到的問題

PID 三個常見問題與解法

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

調參數之前,先確認這三個前提

調參數前的三個前提

我在專案上踩過最痛的一次,就是花了一整個下午調參數,最後發現問題是 ADC 的取樣時間不固定。把 PID 移到 Timer 中斷裡之後,同一組參數立刻正常 —— 這就是「前提比參數重要」的實例。

總結

PID 的知識量不大,但要調得好,靠的是紀律。四個重點:

  • P 看現在、I 記舊帳、D 猜未來:三者的分工想清楚,就知道該動哪一個
  • 先 Kp、再 Ki、最後 Kd:一次只調一個參數
  • Δt 必須固定:用 Timer 觸發,別放在 loop() 裡碰運氣
  • Anti-Windup 不能省:輸出飽和時停止累積積分

把這四件事做對,PID 就不再是玄學,而是一組有明確物理意義的旋鈕。

標籤: 教學 生產力
最後更新:2026 年 9 月 30 日

shi6a

這個人很懶,什麼都沒留下

點贊
< 上一篇
下一篇 >

文章評論

razz evil exclaim smile redface biggrin eek confused idea lol mad twisted rolleyes wink cool arrow neutral cry mrgreen drooling persevering
取消回覆

COPYRIGHT © 2026 0x6A Logbook. ALL RIGHTS RESERVED.

Theme Kratos Made By Seaton Jiang