
做電機轉速閉環、平衡小車、溫控系統或者無人機姿態控制時PID 幾乎是繞不開的第一個算法。很多人在學習時會發現公式看懂了程序也能跑但參數一調不對系統要么響應太慢要么震蕩發散。更麻煩的是網上關于 PID 的講解很多卻大多只給公式和代碼片段看完依舊不知道如何從零搭建一個仿真環境也不知道調參時應該按什么順序來。本文打算把 PID 從原理到仿真完整梳理一遍先講清楚比例、積分、微分三個環節到底在做什么再給出一套基于 Python 的可運行閉環仿真代碼同時補充 Simulink 的搭建思路最后整理調參方法和高頻問題排查清單。適合剛接觸自動控制的學生、入門嵌入式開發的工程師也適合需要快速回顧 PID 要點的開發者。1. 背景與核心概念1.1 什么是 PID 控制PID 是“比例Proportional、積分Integral、微分Derivative”三個英文單詞的縮寫本質上是一種負反饋控制律。它的輸入是目標值與實際值之間的誤差e(t)輸出是給執行機構的控制量u(t)。可以用一個生活中的例子來理解你用手淋浴調節水溫水涼了就開大熱水閥水熱了就關小熱水閥這是比例動作。如果水溫一直比目標溫度低一點你會在一個相對穩定的開度上慢慢把熱水閥再開大一點把“欠下的熱量”補回來這是積分動作。如果發現水溫正在快速上升你會提前把熱水閥往回關一點避免沖過頭這是微分動作。PID 控制器就是按照下面的公式把這三個動作組合起來u(t) Kp * e(t) Ki * ∫e(τ)dτ Kd * de(t)/dt其中Kp比例系數作用于當前誤差。Ki積分系數作用于歷史誤差的累積。Kd微分系數作用于誤差的變化趨勢。在離散控制系統中計算機只能按固定周期采樣因此上式會被改寫成差分形式這也是后面代碼實現的基礎。1.2 PID 控制解決什么問題先看開環控制的問題。假如要控制直流電機轉速PWM 占空比直接給一個固定值電機轉速會隨著負載變化而波動因為沒有反饋系統不知道轉速是否偏離目標。再看簡單的開關控制轉速低于目標就全速轉高于目標就停止這種控制方式實現簡單但輸出在最大值和最小值之間來回切換執行機構磨損嚴重系統也會有明顯抖動。PID 提供的是連續平滑的調節方式。它能根據誤差的大小、積累和時間趨勢動態調整控制量兼顧響應速度、穩定性和精度。常見的應用場景包括電機調速與伺服位置控制。加熱器或溫控箱的溫度控制。水箱或反應釜的液位控制。四軸飛行器的姿態與高度控制。移動機器人底盤速度閉環。電源系統的輸出電壓穩定控制。可以說PID 是自動控制領域最基礎也最通用的算法之一。即使后續學習 LQR、ADRC、模糊控制、模型預測控制等更復雜的算法理解 PID 仍然是非常有價值的起點。1.3 為什么需要先做仿真很多開發者習慣直接寫單片機代碼然后在真實設備上調試 PID。這種方式不是不行但效率通常比較低每改一次參數都要重新下載程序如果系統發散還可能損壞電機、加熱器或驅動器。仿真的價值主要體現在這幾個方面可以在不接硬件的條件下驗證算法邏輯是否正確。可以自由地在被控對象模型中加入延遲、飽和、噪聲和非線性環節提前暴露問題。可以批量對比多組 PID 參數快速觀察超調量、上升時間和穩態誤差等指標。可以把調好的參數作為真機調試的初值減少現場盲目試湊的次數。本文后面的實踐會圍繞兩個仿真環境展開一是 Python 腳本仿真適合快速驗證控制算法二是 Simulink適合更直觀地搭模型和展示信號流向。2. PID 控制原理拆解2.1 比例項 P響應當前誤差比例項是最直觀的部分u_p Kp * e(t)誤差越大控制量越大誤差趨近于零時控制量也趨近于零。比例系數Kp決定了系統對誤差的反應強度。Kp偏小時系統響應很慢受到擾動后需要較長時間才能回到目標值。Kp偏大時系統響應變快但容易產生超調甚至持續震蕩。比例控制的缺點是很難做到無靜差。以電機轉速控制為例如果負載需要一定的力矩來維持旋轉比例控制必須保留一個誤差才能產生足夠的 PWM 輸出這個誤差就是穩態誤差。2.2 積分項 I消除穩態誤差積分項把過去所有時刻的誤差累加起來u_i Ki * ∫e(τ)dτ只要誤差存在積分項就會不斷增大推動控制量逐漸偏向能消除誤差的方向。因此積分項的主要作用是消除穩態誤差。離散化之后積分通常寫成累加的形式integral error * dt u_i ki * integral積分項帶來的問題也不少。如果誤差長期存在積分量會累積到非常大導致控制量長時間停留在飽和區這就是“積分飽和”。系統需要等積分量慢慢退回來才能恢復控制表現為明顯的超調或滯后。后面第七節會專門說這個問題。2.3 微分項 D預測誤差趨勢微分項關注誤差的變化率u_d Kd * de(t) / dt誤差快速增大時微分項輸出一個反向作用力相當于“踩剎車”誤差快速減小時微分項也會反向作用緩和輸出變化。因此微分項可以增加系統阻尼抑制超調和震蕩。離散化方式derivative (error - prev_error) / dt u_d kd * derivative微分項的主要風險是放大噪聲。實際傳感器信號總有波動如果直接對誤差求差分小幅噪聲會被放大成很大的控制量毛刺。工程上通常會對測量值低通濾波或者采用“微分先行”的結構也就是只對實際測量值求微分而不是對誤差求微分。2.4 位置式 PID 與增量式 PID根據輸出形式的不同PID 還分為位置式和增量式。位置式 PID 直接輸出完整的控制量u(k)u(k) Kp * e(k) Ki * T * sum(e(i)) Kd * (e(k) - e(k-1)) / T它需要保存積分累加量適合輸出絕對控制量的場景比如閥門開度、PWM 占空比等。它的缺點是如果積分項處理不好容易積分飽和。增量式 PID 輸出的是控制量的增量Δu(k)Δu(k) Kp * (e(k)-e(k-1)) Ki * T * e(k) Kd * (e(k)-2*e(k-1)e(k-2)) / T最終輸出由累加得到u(k) u(k-1) Δu(k)增量式只需要最近三次誤差不需要保存總積分量適合步進電機、伺服系統等需要增量輸出的執行機構。它也不容易出現積分飽和問題因為輸出是在上一時刻基礎上累加的。實際選型時要根據執行機構的接收方式來判斷能接收絕對量就用位置式能接收脈沖增量就用增量式。3. 仿真環境準備與模型設計3.1 仿真工具選擇本文的仿真示例主要使用以下環境Python 3.x。numpy用于數值計算。matplotlib用于繪制響應曲線。這些庫都是常用的科學計算庫安裝命令如下pip install numpy matplotlib如果你更習慣 MATLAB/Simulink也可以用 Simulink 完成同樣的閉環仿真。沒有商業軟件許可時可以用 GNU Octave 作為部分替代或者直接使用 Python 腳本完成驗證。版本號不需要過分糾結重點是根據你本機環境調整。3.2 被控對象模型設計為了演示 PID 的調節效果本文選擇一個二階被控對象G(s) 1 / (s^2 1.5s 1)選擇二階系統的原因是一階系統只能體現慣性很難直觀展示超調和震蕩現象二階系統結構簡單卻能很好地表現 PID 各項參數對穩、快、準三個指標的影響。這個傳遞函數對應的狀態方程可以寫成dx1/dt x2 dx2/dt -x1 - 1.5*x2 u y x1其中x1是系統輸出x2是輸出的一階導數u是 PID 控制器輸出的控制量。在 Python 仿真中我們用歐拉法對這個連續系統做離散化近似。3.3 采樣周期對仿真的影響離散控制系統必須有固定的采樣周期dt。采樣周期過大系統動態會被漏掉仿真結果可能出現失真采樣周期過小計算量增加但精度不會無限提升。一般建議采樣周期取被控對象最小時間常數的 1/10 到 1/5。對于仿真來說dt 0.0011kHz 控制頻率通常已經能比較高精度地模擬連續系統動態。實際嵌入式系統中溫控系統可能用 0.5s 甚至更長的控制周期而電機電流環可能用 0.0001s 到 0.001s需要根據具體對象調整。4. Python 實現 PID 閉環仿真4.1 實現位置式 PID 類先寫一個最基礎的位置式 PID 類。它需要保存兩個歷史狀態積分累加量和上一次誤差。# pid_basic.py class PID: def __init__(self, kp, ki, kd, dt0.01): self.kp kp self.ki ki self.kd kd self.dt dt self.integral 0.0 self.prev_error 0.0 def reset(self): self.integral 0.0 self.prev_error 0.0 def update(self, error): 輸入當前誤差返回控制量 u(k)。 u(k) Kp*e(k) Ki*T*sum(e) Kd*(e(k)-e(k-1))/T self.integral error * self.dt derivative (error - self.prev_error) / self.dt output ( self.kp * error self.ki * self.integral self.kd * derivative ) self.prev_error error return output這個類本身不關心被控對象是什么只負責根據誤差計算控制量。使用時需要在循環里調用update把返回值交給執行機構或被控對象模型。reset方法在重新開始一次仿真時比較重要可以清空積分量和上一次誤差避免使用上一輪運行的殘留狀態。4.2 完整仿真腳本下面給出一個完整的閉環仿真腳本。被控對象用前一節的狀態方程遞推得到控制量還加入了一個簡單限幅模擬執行機構飽和。# pid_sim.py import numpy as np import matplotlib.pyplot as plt class PID: def __init__(self, kp, ki, kd, dt0.001): self.kp kp self.ki ki self.kd kd self.dt dt self.integral 0.0 self.prev_error 0.0 def reset(self): self.integral 0.0 self.prev_error 0.0 def update(self, error): self.integral error * self.dt derivative (error - self.prev_error) / self.dt output ( self.kp * error self.ki * self.integral self.kd * derivative ) self.prev_error error return output def plant_step(x, u, dt): 被控對象: G(s) 1 / (s^2 1.5s 1) 狀態方程: dx1/dt x2 dx2/dt -x1 - 1.5*x2 u x1 是系統輸出 yx2 是輸出的一階導數。 x1, x2 x dx1 x2 dx2 -x1 - 1.5 * x2 u x1_new x1 dx1 * dt x2_new x2 dx2 * dt return (x1_new, x2_new) def run_simulation(kp, ki, kd, setpoint1.0, dt0.001, total_time10.0): pid PID(kp, ki, kd, dt) x (0.0, 0.0) times [] outputs [] controls [] current_time 0.0 while current_time total_time: y x[0] error setpoint - y u pid.update(error) # 控制量限幅模擬執行機構飽和 u max(-10.0, min(10.0, u)) x plant_step(x, u, dt) times.append(current_time) outputs.append(y) controls.append(u) current_time dt return times, outputs, controls if __name__ __main__: times, outputs, controls run_simulation(kp3.0, ki1.0, kd0.5) plt.figure(figsize(10, 4)) plt.subplot(1, 2, 1) plt.plot(times, outputs, labely(t)) plt.axhline(y1.0, colorr, linestyle--, labelsetpoint) plt.xlabel(time (s)) plt.ylabel(output) plt.legend() plt.grid(True) plt.subplot(1, 2, 2) plt.plot(times, controls, labelu(t)) plt.xlabel(time (s)) plt.ylabel(control) plt.legend() plt.grid(True) plt.tight_layout() plt.show()代碼中每一步的作用都很明確PID類負責控制律計算。plant_step負責模擬被控對象在當前控制量下的動態變化。run_simulation把控制器和被控對象連接起來形成閉環。主程序執行仿真并畫出輸出曲線和控制量曲線。運行后可以看到系統最終能穩定到目標值1.0并且控制量u(t)是一個平滑變化的曲線。4.3 對比不同參數的響應效果PID 調參的核心是觀察不同參數下系統的響應。下面這段代碼可以一次性對比多組參數# pid_compare.py import matplotlib.pyplot as plt from pid_sim import run_simulation def compare_params(): param_sets [ {kp: 1.0, ki: 0.0, kd: 0.0, label: P only (Kp1.0)}, {kp: 3.0, ki: 0.0, kd: 0.0, label: P only (Kp3.0)}, {kp: 3.0, ki: 1.0, kd: 0.0, label: PI (Kp3.0, Ki1.0)}, {kp: 3.0, ki: 1.0, kd: 0.5, label: PID (Kp3.0, Ki1.0, Kd0.5)}, ] plt.figure(figsize(8, 5)) for p in param_sets: t, y, _ run_simulation(p[kp], p[ki], p[kd]) plt.plot(t, y, labelp[label]) plt.axhline(y1.0, colorr, linestyle--) plt.xlabel(time (s)) plt.ylabel(output) plt.title(PID Parameter Comparison) plt.legend() plt.grid(True) plt.show() if __name__ __main__: compare_params()運行之后你會看到這樣幾個典型現象純比例控制時系統可以穩定但最終輸出和目標值之間存在穩態誤差。增加積分項后穩態誤差逐漸被消除。加入微分項后超調量明顯下降系統更快進入穩定狀態。建議自己修改kp、ki、kd再運行幾次觀察曲線變化。這是理解 PID 各項作用最快的方式。4.4 增量式 PID 的實現增量式 PID 是嵌入式開發中很常用的形式。下面給出一個可直接使用的 Python 類# incremental_pid.py class IncrementalPID: def __init__(self, kp, ki, kd, dt0.01): self.kp kp self.ki ki self.kd kd self.dt dt self.prev_error 0.0 self.prev_prev_error 0.0 self.output 0.0 def reset(self): self.prev_error 0.0 self.prev_prev_error 0.0 self.output 0.0 def update(self, error): # 增量式 PID 輸出的是控制量增量 delta ( self.kp * (error - self.prev_error) self.ki * self.dt * error self.kd * (error - 2.0 * self.prev_error self.prev_prev_error) / self.dt ) self.output delta self.prev_prev_error self.prev_error self.prev_error error return self.output增量式 PID 不需要積分累加項只保留最近兩次歷史誤差輸出以增量形式累積。它適合輸出到步進電機、數字舵機和部分伺服驅動器。需要注意的是這里的ki仍然表示連續域積分增益離散化后積分項為Ki * T * e(k)與位置式代碼中的含義保持一致。5. 使用 Simulink 搭建 PID 仿真模型5.1 模塊搭建步驟如果你有 MATLAB/Simulink 環境可以用圖形化方式驗證 PID 控制效果。先新建一個 Simulink 模型然后按以下步驟搭建從 Simulink 庫瀏覽器中拖入以下模塊StepSources 庫作為目標值輸入。SumMath Operations 庫計算目標值與反饋值的誤差。PID ControllerContinuous 庫實現 PID 控制律。Transfer FcnContinuous 庫作為被控對象模型。ScopeSinks 庫顯示輸出曲線。雙擊Sum把符號設置為-表示“目標值減反饋值”。雙擊Transfer Fcn設置分子為[1]分母為[1 1.5 1]對應傳遞函數G(s) 1 / (s^2 1.5s 1)用信號線連接Step - Sum 正輸入Sum 輸出 - PID Controller - Transfer Fcn - Scope。再從Transfer Fcn輸出端引出反饋線連回Sum的負輸入端。保存模型比如命名為pid_demo.slx。如果你希望更直觀地觀察 P、I、D 三個通道各自對控制量的貢獻也可以不用PID Controller模塊而是分別用Gain、Integrator、Derivative三個模塊并聯再通過Add模塊合成輸出。這種方式在教學演示中更清晰。5.2 參數配置與運行雙擊Step設置階躍時間和終值Step time0.1Final value1雙擊PID Controller設置控制器參數Proportional3Integral1Derivative0.5然后把仿真結束時間設為10點擊運行。Scope 中可以看到系統的輸出響應曲線。如果需要對比不同參數可以復制一個 PID 模型再修改參數或者用 MATLAB 腳本批量執行% pid_compare.m % 需要把 PID Controller 的參數設置為工作區變量 kp、ki、kd kp_list [1, 3, 3]; ki_list [0, 1, 1]; kd_list [0, 0, 0.5]; figure; hold on; for i 1:length(kp_list) kp kp_list(i); ki ki_list(i); kd kd_list(i); sim(pid_demo); plot(tout, yout); end grid on; legend(P only Kp1, PI Kp3 Ki1, PID Kp3 Ki1 Kd0.5);這個腳本要求模型中的PID Controller參數是變量名kp、ki、kd否則每次都要手動修改。用腳本批量仿真可以非常高效地觀察參數變化趨勢。5.3 從仿真到嵌入式移植的注意點仿真中調好的參數不能直接照搬到真機。原因在于仿真模型往往忽略了執行機構延遲、死區和摩擦力。真實傳感器存在噪聲和量化誤差。實際系統可能存在非線性比如飽和、間隙、滯回。一個可行的流程是先在仿真中確定 PID 參數的大致范圍再用這些參數作為真機調試的初值然后在真機上微調。真機調試時推薦用串口把目標值、反饋值、誤差和控制量實時發送到上位機常用工具如 VOFA 可以直接繪制實時曲線能大幅提高調參效率。6. PID 調參方法與仿真驗證6.1 先 P 后 I 再 D 的試湊法最常用的調參順序是“先比例后積分再微分”。把Ki和Kd設置為 0只保留比例控制。從小到大逐漸增大Kp觀察系統響應響應太慢就加大Kp出現明顯震蕩就減小Kp。找到一組能穩定、但存在穩態誤差的Kp后加入積分項。從小到大增大Ki直到穩態誤差被消除。Ki過大時系統會引入振蕩需要回退一點。如果系統超調量偏大再加入微分項。從小到大增大Kd觀察振蕩和超調是否被抑制。Kd過大會放大噪聲出現控制量毛刺。這個順序能讓你在調試過程中清楚知道每一項帶來了什么變化。6.2 臨界比例度法Ziegler-Nichols 整定Ziegler-Nichols 是一種經典整定方法適合有一定模型但不想推導太細的場景。基本步驟如下關閉積分和微分只保留比例控制。從小到大增大Kp直到系統輸出出現等幅振蕩。記錄此時的臨界增益Ku和振蕩周期Tu。按經驗公式計算 PID 參數。一種常用計算公式如下控制器類型KpTiTdP0.5 * Ku--PI0.45 * Ku0.85 * Tu-PID0.6 * Ku0.5 * Tu0.125 * Tu注意這里的Ti是積分時間常數Td是微分時間常數。如果代碼中使用的是積分增益Ki和微分增益Kd需要換算Ki Kp / Ti Kd Kp * TdZiegler-Nichols 方法給出的參數通常比較激進實際應用中往往需要進一步微調。在仿真環境中可以先驗證效果再決定是否采用。6.3 用仿真量化動態指標調參時不能只靠“看曲線”還要用數值指標評估。常用的指標包括上升時間輸出首次到達目標值附近所需時間。超調量輸出最大值超出目標值的百分比。調節時間輸出進入并穩定在目標值誤差帶內所需時間。穩態誤差系統穩定后輸出與目標值之間的差值。在 Python 仿真中可以用簡單代碼計算超調量import numpy as np times, outputs, controls run_simulation(kp3.0, ki1.0, kd0.5) setpoint 1.0 steady_value outputs[-1] # 最后時刻的值作為穩態參考 overshoot (max(outputs) - setpoint) / setpoint * 100 print(f穩態值: {steady_value:.4f}) print(f超調量: {overshoot:.2f}%)把動態指標打印出來對比不同參數時會更客觀也更容易沉淀成自己的調參經驗。7. 常見問題與排查思路7.1 高頻問題排查表問題現象常見原因解決思路輸出發散曲線直接飛掉Kp 過大、采樣周期過大、對象不穩定降低 Kp減小采樣周期檢查開環對象系統持續震蕩Kp 過大或 Kd 過小先降 Kp再適當增加 Kd穩態誤差一直存在積分項未啟用或 Ki 太小增大 Ki確認積分累加未被限幅控制量長時間停在限幅值執行機構飽和、積分飽和加積分限幅、積分分離、輸出限幅有噪聲時控制量毛刺明顯D 項放大測量噪聲降低 Kd對測量值濾波改用微分先行仿真曲線正常真機表現差建模忽略了延遲、死區、摩擦在仿真中加入延遲和非線性后再驗證7.2 系統震蕩與穩態誤差如果系統持續震蕩第一反應不是繼續加參數而是先判斷震蕩來自哪個環節。先把Ki和Kd置零只保留比例控制看震蕩是否消失。如果純 P 控制已經不震蕩說明問題出在積分項或微分項上如果純 P 控制仍震蕩說明Kp太大了。穩態誤差的成因則相對集中要么沒有積分項要么積分項被限幅截斷。可以先增大Ki如果Ki增到一定程度后系統開始震蕩說明這次調節已經過頭需要退回一點并通過微分項來補償超調。7.3 積分飽和問題積分飽和是 PID 工程應用中最常見的問題之一。簡單來說當系統誤差長期為正時積分量會一直累加到一個很大的值即使誤差已經反向積分量也需要很長事件才能回落導致系統出現大超調和慢動態。常見處理方式有三種積分限幅給積分累加量設置上下限避免無限累積。積分分離誤差較大時不進行積分誤差進入較小范圍后再啟用積分。抗積分飽和只有控制量沒有進入飽和區時才允許積分累加。下面給出一個積分限幅的示例思路# 在 PID 類中增加積分限幅 integral_max 100.0 integral_min -100.0 self.integral error * self.dt if self.integral integral_max: self.integral integral_max elif self.integral integral_min: self.integral integral_min具體限幅范圍需要根據你的執行機構輸出范圍來定。7.4 微分噪聲問題微分項對噪聲非常敏感。傳感器的一點點抖動經過差分后會變成很大的控制量變化。如果你發現系統在穩定狀態附近控制量仍然上下亂跳大概率是微分項在放大噪聲。工程上常用三種策略適當減小Kd犧牲一些阻尼特性換取平穩輸出。對測量值做低通濾波再計算微分。采用“微分先行”結構也就是只對反饋值y(t)求微分而不是對誤差e(t)求微分。這樣可以避免設定值突變時產生微分尖峰。8. 最佳實踐與工程建議8.1 從簡單對象開始驗證學習 PID 時不要一開始就把系統弄得太復雜。先對一個明確的二階或一階對象做仿真確認控制器邏輯正確再逐步增加擾動、噪聲、飽和等環節。這樣可以逐步建立直覺哪些現象來自控制器本身哪些現象來自被控對象特性。8.2 輸出限幅與積分限幅結合使用實際執行機構一定有輸出范圍。即使仿真里模型再精確控制量過大也無法實現。因此在控制器輸出環節必須加限幅。更重要的是輸出限幅不能只限制最終輸出還要考慮積分項是否會被“憋大”。推薦在代碼中同時做輸出限幅和積分限幅避免積分項無限累加。8.3 固定控制周期并記錄數據離散 PID 的積分和微分都依賴采樣周期。如果控制周期不穩定相當于每一項系數都在隨機變化參數自然調不穩。工程中應該盡量使用定時器產生固定控制周期而不是依賴主循環的軟件延時。同時建議把目標值、反饋值、誤差、控制量都記錄下來。真機上可以用串口日志保存仿真中可以用列表保存。有了歷史數據才能回放問題而不是每次靠“感覺”調參。8.4 仿真參數不能直接搬到真機仿真和真機之間永遠有差距。仿真中調好的 PID 參數放到真機上通常需要重新整定。真機調試時先手工把執行機構轉到安全位置然后把最大輸出限制在一個較小值再逐步放開避免程序異常導致設備損壞。任何涉及真實設備的調試都應該先確認輸出限幅、急停邏輯和供電安全。9. 總結與進階學習路線9.1 本文學到的核心內容通過本文你應該已經掌握PID 的數學公式和比例、積分、微分三個環節的作用。位置式 PID 與增量式 PID 的區別與實現方式。基于 Python 的完整閉環仿真方法。使用 Simulink 搭建 PID 仿真模型的基本流程。試湊法和臨界比例度法的基本步驟。震蕩、穩態誤差、積分飽和、微分噪聲等常見問題的原因和排查思路。這些內容足以支撐你完成一個簡單的溫度控制、電機調速或小車速度閉環仿真項目。9.2 下一步可以學習什么PID 只是自動控制的一種基礎算法。如果你希望繼續深入可以考慮以下方向模糊 PID讓 PID 參數根據誤差狀態自適應調整適合難以建立精確模型的對象。ADRC 自抗擾控制把模型不確定性和外部擾動統一當作總擾動進行估計和補償工程實踐中有很多成功案例。L