C#實現PID控制的模擬測試和曲線繪圖
本文分兩部分,一部分是講PID算法的實現,另一部分是講如何用動態的曲線繪制出PID運算的結果。
首先,PID算法的理論模型請參考自動控制理論,最早出現的是模擬PID控制,后來計算機成為控制器,由於計算機控制是一種采樣控制,需把模擬PID轉換成數字PID,就是模擬PID的離散化,兩者中間是香濃定理。當然這些和編程是沒關系的,我們只需要有個數字模型就能開展后面的工作了。
在編程時,可寫成:
絕對式計算公式
Uo(n) = P *e(n) + I*[e(n)+e(n-1)+...+e(0)]+ D *[e(n)-e(n-1)]
Uo(n-1) = P *e(n-1) + I*[e(n-1)+e(n-2)+...+e(0)]+ D *[e(n-1)-e(n-2)]
二者相減就得到增量式計算公式
Uo = P *(e(n)-e(n-1)) + I*e(n)+ D *[e(n)-2*e(n-1)+e(n-2)]
e(n)--------------------------本次誤差
接下來的任務就是用代碼來實現上面的公式了,我把PID運算部分做成一個類Class1供其他程序調用,開始只實現最基本的PID運算,沒有考慮從積分分離和死區處理,最重要的代碼如下:
- private float prakp, praki, prakd, prvalue, err, err_last, err_next, setvalue;
- int MAXLIM, MINLIM;
- //PID valculate
- public float pidvalc()
- {
- err_next = err_last; //前兩次的誤差
- err_last = err; //前一次的誤差
- err = setvalue - prvalue; //現在的誤差
- //增量式計算
- prvalue += prakp * ((err - err_last) + praki * err + prakd * (err - 2 * err_last + err_next));
- //輸出上下限值
- if (prvalue > MAXLIM)
- prvalue = MAXLIM;
- if (prvalue < MINLIM)
- prvalue = MINLIM;
- return prvalue;
- }

現在我們對PID運算部分的代碼改進一點,增加了積分分離和死區的功能,完整代碼如下:
- using System;
- using System.Collections.Generic;
- using System.Linq;
- using System.Text;
- namespace PIDtest
- {
- class Class1
- {
- private float prakp, praki, prakd, prvalue, err, err_last, err_next, setvalue, deadband;
- int index, UMAX, UMIN, MAXLIM, MINLIM;
- public float Prakp
- {
- set
- {
- prakp = value;
- }
- get
- {
- return prakp;
- }
- }
- public float Praki
- {
- set
- {
- praki = value;
- }
- get
- {
- return praki;
- }
- }
- public float Prakd
- {
- set
- {
- prakd = value;
- }
- get
- {
- return prakd;
- }
- }
- public float Setvalue
- {
- set
- {
- setvalue = value;
- }
- get
- {
- return setvalue;
- }
- }
- public Class1()
- {
- pidinit();
- }
- //PID參數初始化
- public void pidinit()
- {
- prakp = 0;
- praki = 0;
- prakd = 0;
- prvalue = 0;
- err = 0;
- err_last = 0;
- err_next = 0;
- MAXLIM = 800;
- MINLIM = -200;
- UMAX = 310;
- UMIN = -100;
- deadband = 2;
- }
- //PID valculate
- public float pidvalc()
- {
- err_next = err_last;
- err_last = err;
- err = setvalue - prvalue;
- //抗積分飽和
- if (prvalue > UMAX)
- {
- if (err < 0)
- index = 1;
- else
- index = 0;
- }
- else if (prvalue < UMIN)
- {
- if (err > 0)
- index = 1;
- else
- index = 0;
- }
- //積分分離
- else
- {
- if (Math.Abs(err) > 0.8 * setvalue)
- index = 0;
- else
- index = 1;
- }
- //死區
- if (Math.Abs(err) > deadband)
- prvalue += prakp * ((err - err_last) + index * praki * err + prakd * (err - 2 * err_last + err_next));
- else
- prvalue += 0;
- //輸出上下限制
- if (prvalue > MAXLIM)
- prvalue = MAXLIM;
- if (prvalue < MINLIM)
- prvalue = MINLIM;
- return prvalue;
- }
- }
- }
再用同樣的參數模擬測試,結果如下圖,到達穩定的設定值時間快很多,而且沒有出現振盪的現象,有明顯改善

第一部分完成對PID的算法實現,接下來開始第二部分動態繪制PID曲線圖。
開始還糾結於在哪畫圖,是在FORM上,還是PICTUREBOX上,動態獲取的數據放到一個什么樣的數組里還是LIST里?然后怎么讓曲線實現從左往右的平移?
參考了其他人的程序和資料后,其實不用這么糾結,分開來看這個問題雖然這次是個很小的問題,但跟大的項目思路是一樣的,分成圖形顯示和數據更新兩個部分來看,就豁然開朗了。
整個思路就是 繪制背景-繪制網格線-獲取數據-繪制曲線 這樣的循環,循環可以放到一個定時器里。對於使用什么樣的容器來存放圖形和曲線數據,只是實現的細節不同。
我用的在PICTUREBOX里繪圖,用一個POINTF[ ]數組來存放一次要顯示的所有數據,周期觸發PICTUREBOX的PAINT事件,同時把POINTF[ ]里的點坐標Y軸都刷新一次。如果用LIST來存放POINT的坐標值,可以使用方法lRemoveAt() 來移除第一個點,Add( )來實現在尾部加上最新的一個點,從而實現曲線的動態平移。
為了方便以后的重復使用,我做成了一個用戶控件的形式UserControl1
修改參考的代碼如下:
- <pre name="code" class="csharp">using System;
- using System.Collections.Generic;
- using System.ComponentModel;
- using System.Drawing;
- using System.Data;
- using System.Linq;
- using System.Text;
- using System.Windows.Forms;
- namespace WindowsFormsControlLibrary2
- {
- public partial class UserControl1 : UserControl
- {
- public UserControl1()
- {
- InitializeComponent();
- }
- Graphics g;
- //List<float> l = new List<float>();//儲存要繪制的數據
- Pen p = new Pen(Color.Green, 1);
- Pen p1 = new Pen(Color.Red, 1);
- //PointF ptfront = new PointF();
- //PointF ptbehond = new PointF();
- private int jiange = 86;//網格間距
- private int pianyi = 2;//繪圖兩點之間間隔
- private float value1;
- //Random r=new Random ();
- PointF[] data;
- public float Value
- {
- get
- {
- return value1;
- }
- set
- {
- this.value1 = value;
- }
- }
- //獲得一個數據
- private void getdata()
- {
- data[data.Length - 1].Y = value1;
- for (int i = 0; i < data.Length - 1; i++)
- data[i].Y = data[i + 1].Y;
- //放數據到LIST
- //if (l.Count >= 80)
- //{
- // l.RemoveAt(0);
- // l.Add(value1 );
- //}
- }
- //初始化數據存放數組
- private void UserControl1_Load_1(object sender, EventArgs e)
- {
- timer1.Enabled = true;
- timer1.Interval = 100;
- //l.Add(0);
- data = new PointF[pictureBox1.Width / pianyi];
- for (int i = 0; i < data.Length; i++)
- data[i].X += pianyi * i;
- /*
- for (int i = 0; i < pictureBox1.Width / pianyi; i++)
- {
- l.Add(r.Next(50));
- }
- */
- }
- private void pictureBox1_Paint_1(object sender, PaintEventArgs e)
- {
- g = e.Graphics;
- //畫網格線
- //for (int i = this.pictureBox1.Width; i >= 0; i -= jiange)
- //g.DrawLine(p, i, 0, i, this.pictureBox1.Width);
- //for (int i = this.pictureBox1.Height; i >= 0; i -= jiange)
- //g.DrawLine(p, 0, i, this.pictureBox1.Width , i);
- for (int i = 0; i < pictureBox1.Width; i++)
- if (i % jiange == 0)
- g.DrawLine(p, i, 0, i, this.pictureBox1.Height);
- for (int i = 0; i < pictureBox1.Height; i++)
- if (i % jiange == 0)
- g.DrawLine(p, 0, i, pictureBox1.Width, i);
- //畫數據曲線
- // ptbehond.X = 0;
- /*
- for (int i = 0; i < l.Count - 1; i++)
- {
- ptfront.X = ptbehond.X;
- ptfront.Y = l[i];
- ptbehond.X += pianyi;
- ptbehond.Y = l[i + 1];
- g.DrawLine(p1, ptfront, ptbehond);
- }
- */
- g.DrawCurve(p1, data);
- }
- //繪圖刷新周期
- private void timer1_Tick_1(object sender, EventArgs e)
- {
- getdata();
- this.pictureBox1.Refresh();
- }
- }
- }
最后,在FORM1中實現用來PID運算的類Class1和用來顯示數據曲線的用戶控件UserControl1的調用,代碼如下:
- using System;
- using System.Collections.Generic;
- using System.ComponentModel;
- using System.Data;
- using System.Drawing;
- using System.Linq;
- using System.Text;
- using System.Windows.Forms;
- namespace PIDtest
- {
- public partial class Form1 : Form
- {
- public Form1()
- {
- InitializeComponent();
- timer1.Interval = 5;
- timer1.Enabled = true;
- }
- Class1 pid = new Class1();
- //Random r = new Random();
- private void timer1_Tick(object sender, EventArgs e)
- {
- userControl11.Value = pid.pidvalc();
- }
- private void button1_Click(object sender, EventArgs e)
- {
- pid.pidinit();
- pid.Setvalue = float.Parse(textBox1setvalue.Text);
- pid.Prakp = float.Parse(textBox1prakp.Text);
- pid.Praki = float.Parse(textBox2praki.Text);
- pid.Prakd = float.Parse(textBox3prakd.Text);
- }
- }
- }