離開工控行業已經有一段時間了,最近回憶起以前的工作,又對 PID 算法有了興趣。所以寫了一個小項目,希望可以幫到需要的人,也算是對那段工作經歷的一個總結。
這是一個 winform 的項目。負載是一個水箱,有一個進水口,一個出水口。設定值為液位,通過控制進水口的閥門開度使液位達到設定值,傳感器的滯后時間為10秒。每秒執行一次 PID 算法(對於運動控制的項目需要將采樣時間調低)。
結果:
左圖采用原生 PID 調節,右圖采用積分分離后的 PID 調節(在誤差小於一定值的情況下積分才開始累積)。可以看出積分分離可以有效的抑制超調量,但是會增加調節時間。
由於微分調節對系統穩定性影響較大,不建議初學者使用。
在分配 PID 的各項參數時,除了使用 “自動控制理論” 中計算傳遞函數,還可以通過試湊的方法。先確定比例的大致范圍,再加入積分。加入積分時,需要先將積分值調到很大(積分值大表示效果較弱),再慢慢降低。
窗口中的控件:
label : lblInfo1(用於顯示超調)lblInfo2(用於顯示調節時間)
button:btnStart(開始普通 PID 算法)btnStart2(開始改進型 PID 算法)(主要采用積分分離算法)
numericupdown:numP(比例值)numI(積分值)
panel:panel2(用於繪圖顯示 PID 調節過程)
代碼:
using System; using System.Collections.Generic; using System.ComponentModel; using System.Data; using System.Drawing; using System.Linq; using System.Text; using System.Threading.Tasks; using System.Windows.Forms; namespace PID { public partial class Form1 : Form { PID frmPid; Box frmBox; const int yBase = 500; const int yMul = 5; const int xMul = 1; int time = 0;//上次采樣時間 時間為秒 Point lastPoint; decimal maxLevel = 0;//最大值用於求超調 public Form1() { InitializeComponent(); frmPid = new PID(); frmBox = new Box(20, 0.3m, 0.1m, 0m, 0.5m); Init(); } //初始化 private void Init() { using (Graphics g = panel2.CreateGraphics()) { Pen whitePen = new Pen(Brushes.White, 2000); Point point1 = new Point(0, 0); Point point2 = new Point(0, 2000); g.DrawLine(whitePen, point1, point2); } maxLevel = 0; time = 0; lastPoint = new Point(0, yBase); } private void btnStart_Click(object sender, EventArgs e) { Start(0); } private void btnStart2_Click(object sender, EventArgs e) { Start(1); } /// <summary> /// 開始 /// </summary> /// <param name="number">0使用普通pi調節,1使用改進pi調節</param> private void Start(int number) { Init(); frmPid.Init(0.8m, numP.Value, numI.Value, 0, 1); frmBox.Init(); Pen bluePen = new Pen(Brushes.Blue, 1); using (Graphics g = panel2.CreateGraphics()) { Point point1 = new Point(0, yBase - Convert.ToInt32(frmPid.Target * 100) * yMul); Point point2 = new Point(1000, yBase - Convert.ToInt32(frmPid.Target * 100) * yMul); g.DrawLine(bluePen, point1, point2); } bool complete = false; for (int i = 0; i < 1000; i++) { { time++; frmBox.ChangeLevel(); Pen blackPen = new Pen(Brushes.Black, 1); using (Graphics g = panel2.CreateGraphics()) { Point point = new Point(time * xMul, yBase - Convert.ToInt32(frmBox.GetLevel() * 100) * yMul); g.DrawLine(blackPen, point, lastPoint); lastPoint = point; } decimal degreeIn = frmPid.GetOutPutValue(frmBox.GetLevel(), number); frmBox.ChangeDegreeIn(degreeIn); } if (frmBox.GetLevel() > maxLevel) { maxLevel = frmBox.GetLevel(); } if ((Math.Abs(frmBox.GetLevel() - frmPid.Target) / frmPid.Target < 0.01m) && (!complete)) { complete = true; lblInfo2.Text = "調節時間:" + time; } } decimal up = 0; if (maxLevel > frmPid.Target) { up = (maxLevel - frmPid.Target) / frmPid.Target; } lblInfo1.Text = "超調:" + up.ToString("0.000"); } } public class Box { private List<decimal> levelList; private decimal area; //底面積 平方米 private decimal maxFlowOut = 0.05m; //出水閥最大流量立方每秒 private decimal maxFlowIn = 0.1m; //進水閥最大流量 立方每秒 private decimal degreeIn; //進水閥開度 private decimal degreeOut; //出水閥開度 /// <summary> /// 構造函數 /// </summary> /// <param name="area">底面積</param> /// <param name="maxFlowIn">進水閥最大流量 立方每秒</param> /// <param name="maxFlowOut">出水閥最大流量立方每秒</param> /// <param name="degreeIn">進水閥開度</param> /// <param name="degreeOut">出水閥開度</param> public Box(decimal area, decimal maxFlowIn, decimal maxFlowOut, decimal degreeIn, decimal degreeOut) { this.area = area; this.maxFlowOut = maxFlowOut; this.maxFlowIn = maxFlowIn; this.degreeIn = degreeIn; this.degreeOut = degreeOut; this.levelList = new List<decimal>(); this.levelList.Add(0); } public void Init() { this.levelList = new List<decimal>(); this.levelList.Add(0); } private decimal GetActualLevel() { return this.levelList[this.levelList.Count - 1]; } /// <summary> ///每調用一次表示經過了一秒 /// </summary> public void ChangeLevel() { decimal myflow = this.degreeIn * this.maxFlowIn - this.degreeOut * this.maxFlowOut;//增加的流量 decimal level = this.GetActualLevel() + myflow / this.area;//新的液位 if (level < 0) { level = 0; } if (level > 1) { level = 1; } this.levelList.Add(level); while (this.levelList.Count > 10) { this.levelList.RemoveAt(0); } } public decimal GetLevel() { return this.levelList[0]; } /// <summary> /// 改變進水閥開度 /// </summary> public void ChangeDegreeIn(decimal degreeIn) { this.degreeIn = degreeIn; } } /// <summary> /// PID控制類 /// </summary> public class PID { /// <summary> /// 積分累計值 /// </summary> public decimal IntegralValue { get; set; } /// <summary> /// 設定值 /// </summary> public decimal Target { get; set; } /// <summary> /// 比例 /// </summary> public decimal P { get; set; } /// <summary> /// 積分 /// </summary> public decimal I { get; set; } /// <summary> /// 輸出限幅 /// </summary> private decimal MinOutPut { get; set; } /// <summary> /// 輸出限幅 /// </summary> private decimal MaxOutPut { get; set; } public void Init(decimal target, decimal p, decimal i, decimal minOutput, decimal maxOutput) { this.Target = target; this.P = p; this.I = i; IntegralValue = 0; if (minOutput > maxOutput) { throw new Exception("下限幅不能大於上限幅"); } this.MinOutPut = minOutput; this.MaxOutPut = maxOutput; } /// <summary> /// 獲得輸出值 /// </summary> /// <param name="feedBack">反饋值</param> /// <param name="number">0普通算法,1改進后的算法</param> /// <returns></returns> public decimal GetOutPutValue(decimal feedBack, int number) { decimal error = this.Target - feedBack; if (this.I > 0) { if (number == 0) { this.IntegralValue += error / this.I; } else { if ((Math.Abs(error) < 0.5m)) { this.IntegralValue += error / this.I; } } } decimal output = error * this.P + this.IntegralValue; if (output < this.MinOutPut) { return this.MinOutPut; } if (output > this.MaxOutPut) { return this.MaxOutPut; } return output; } } }