Page 1 of 1

Idustrial process regulation using a VB.NET PID controller Rate Topic: -----

#1 oldSwede  Icon User is offline

  • D.I.C Regular
  • member icon

Reputation: 4
  • View blog
  • Posts: 464
  • Joined: 08-January 16

Posted 12 July 2016 - 07:10 AM

In many industrial processes one wants to keep the process at a certain value. A simple example would be tank that is filled by a pump and emptied by a variable valve. If there is a need for constant pressure from the tank we need to keep the liquid in the tank at the same level all the time. This seems trivial but it is not. Another, harder example, would be to control a fan in such a way that we have constant airflow through the fan even when there is more or less obstacles hindering the gas flow from the fan. A non industrial example would be the cruise control of a car.

There are many ways to handle such a situation and a very common way, that has become more or less industrial standard, is to use a PID controller.

Quote

A proportional–integral–derivative controller (PID controller) is a control loop feedback mechanism (controller) commonly used in industrial control systems. A PID controller continuously calculates an error value as the difference between a desired setpoint and a measured process variable.


A PID controller would be nice to have in software. Of course, one can buy dedicated hw for PID control or use a PLC system with a built in PID controller part. However, there are situations where a software PLC may come in handy. Recently I ran in to such a situation and had to make one. I thought I would share it in case someone else needs it.

Title:
PID controller with optional threading and a simple simulation for testing.

Abstract:
This is a PID controller that is fully configurable.
The constants for P, I, D and output range are set at initialization.
For the threaded implementation also the loop time is set at initialization.
The PID is guarded against integral windup.

If you are new to PIDs please lookup PID controller on wikipedia for suitable parameters and a general description of what a PID is and does. With wrong parameters it will not seem useful. I've tried to put som suitable example parameters in the simulation, they are not optimal.
Requirements:
No special requirements. The program has been tested on Win7 32 bit and was developed with VS2013.
To run the simulation you need a windows form as shown in the attachment.

Usage:
Usage instructions is in the comments.
Usage is demonstrated in the simulation code.

PID code
Option Explicit On
Option Strict On
Option Infer Off
Imports System.Threading
''' <summary>
''' =================================================================================
''' PID controller
''' --------------
''' By initializing with the constructor that requires a time you start a thread that
''' will loop with approxemately the given interval, reading the current process
''' value and writing the pid out signal once for each loop.
''' 
''' By initializing with the constructor that does not take a time no thread is
''' started. Instead you need to call the CalculateOutput method regularly.
''' Note that if the time between calls differ a lot the PID might become instable.
''' What a lot is? If you vary more than doubling and halfing the time you will
''' definitly be in trouble...
''' 
''' For tuning see: https://en.wikipedia.org/wiki/PID_controller#Overview_of_methods
''' You will need to tune for each situation.
''' ---------------------------------------------------------------------------------
''' </summary>
Public Class PidController
    Private _currentProcessValue As Double = 0
    ''' <summary>
    ''' Write the current process value to be controlled here when using threaded execution.
    ''' </summary>
    Public WriteOnly Property currentProcessValue() As Double
        Set(ByVal value As Double)
            'This is maybe overkill since Integer writes are atomic but if someone changes the datatype...
            Interlocked.Exchange(_currentProcessValue, value)
        End Set
    End Property

    Private _pidOutSignal As Double = 0
    ''' <summary>
    ''' Read the control value to regulate the process here when using threaded execution.
    ''' </summary>
    Public Property pidOutSignal() As Double
        Get
            Return _pidOutSignal
        End Get
        Private Set(ByVal value As Double)
            'This is maybe overkill since Integer writes are atomic but if someone changes the datatype...
            Interlocked.Exchange(_pidOutSignal, value)
        End Set
    End Property

    Private _performCalculations As Boolean = False
    ''' <summary>
    ''' Controlling wheter or not the calculations are performed when using threaded execution.
    ''' </summary>
    ''' <value>True = activate PID = perform PID calculations, False = deactivate PID.</value>
    ''' <returns>True = PID active, False = PID inactive.</returns>
    Public Property performCalculations() As Boolean
        Get
            Return _performCalculations
        End Get
        Set(ByVal value As Boolean)
            _performCalculations = value
        End Set
    End Property

    'Values controlling the PID
    '--------------------------
    ''' <summary>
    ''' The desired process value.
    ''' </summary>
    Private _desiredProcessValue As Double = 0
    Public Property desiredProcessValue() As Double
        Get
            Return _desiredProcessValue
        End Get
        Set(ByVal value As Double)
            'This is maybe overkill since Double writes are atomic but if someone changes the datatype...
            Interlocked.Exchange(_desiredProcessValue, value)
            Interlocked.Exchange(_prevError, value)
        End Set
    End Property

    Private _minOutSignal As Double = 0
    Private _maxOutSignal As Double = 0
    Private _cProportional As Double = 0
    Private _cIntegral As Double = 0
    Private _cDerivative As Double = 0
    Private _dTime_ms As Integer = 0
    'PID internal values between calculations
    Private _startTime As DateTime = Nothing
    Private _prevI As Double = 0
    Private _prevError As Double = 0
    'The handle to the thread
    Private _pidRunner As Thread

    ''' <summary>
    ''' Creates PID controller and sets initial values.
    ''' Call the 'CalculateOutput' function regularly to get control output.
    ''' </summary>
    ''' <param name="minOutSignal">The minimum valid output signal value.</param>
    ''' <param name="maxOutSignal">The maximum valid output signal value.</param>
    ''' <param name="cProportional">The proportional constant (P).</param>
    ''' <param name="cIntegral">The integral constant (I).</param>
    ''' <param name="cDerivative">The derivative constant (D).</param>
    Public Sub New(minOutSignal As Double, maxOutSignal As Double, cProportional As Double, cIntegral As Double, cDerivative As Double)
        ' Setup values needed for the PID.
        _minOutSignal = minOutSignal
        _maxOutSignal = maxOutSignal
        _cProportional = -cProportional
        _cIntegral = -cIntegral
        _cDerivative = -cDerivative
        _performCalculations = False
        _startTime = DateTime.Now
    End Sub

    ''' <summary>
    ''' Creates PID controller and sets initial values.
    ''' Starts thread performing PID calculations approxematly every 'dTime_ms'.
    ''' Reads process value from 'currentProcessValue'.
    ''' Writes control output signal to 'pidOutSignal'.
    ''' Do NOT call the 'CalculateOutput' function directly when using this implementation.
    ''' </summary>
    ''' <param name="minOutSignal">The minimum valid output signal value.</param>
    ''' <param name="maxOutSignal">The maximum valid output signal value.</param>
    ''' <param name="cProportional">The proportional constant (P).</param>
    ''' <param name="cIntegral">The integral constant (I).</param>
    ''' <param name="cDerivative">The derivative constant (D).</param>
    ''' <param name="dTime_ms">
    ''' The desired recalculation time in ms. 
    ''' This probably doesn't work with less than 20 or so. 
    ''' Be careful, short times can give instability.
    ''' </param>
    Public Sub New(minOutSignal As Double, maxOutSignal As Double, cProportional As Double, cIntegral As Double, cDerivative As Double, dTime_ms As Integer)
        ' Setup values needed for the PID.
        _minOutSignal = minOutSignal
        _maxOutSignal = maxOutSignal
        _cProportional = -cProportional
        _cIntegral = -cIntegral
        _cDerivative = -cDerivative
        _dTime_ms = dTime_ms
        _startTime = DateTime.Now
        ' Setup the thread and start it but do not start calculating.
        _pidRunner = New Thread(AddressOf PidRunner)
        _pidRunner.IsBackground = True
        _performCalculations = False
        _pidRunner.Start()
    End Sub

    ''' <summary>
    ''' Perform PID calculations
    ''' </summary>
    ''' <param name="currentProcessValue">The process value.</param>
    ''' <returns>The control signal.</returns>
    Public Function CalculateOutput(currentProcessValue As Double) As Double
        Dim result As Double = 0, i As Double, p As Double, d As Double, currentErr As Double
        Dim elapsed As Integer

        Try
            elapsed = CInt((DateTime.Now - _startTime).TotalMilliseconds)
            _startTime = DateTime.Now
            currentErr = currentProcessValue - _desiredProcessValue

            p = _cProportional * currentErr
            i = _prevI + (_cIntegral * elapsed * currentErr)
            'The following is to avoid integral windup. See: https://en.wikipedia.org/wiki/Integral_windup
            i = If(i < _minOutSignal, _minOutSignal, i)
            i = If(i > _maxOutSignal, _maxOutSignal, i)
            d = _cDerivative * (currentErr - _prevError) / elapsed

            result = p + i + d
            'Make sure we don't send signals that can't be handled by the process system.
            result = If(result < _minOutSignal, _minOutSignal, result)
            result = If(result > _maxOutSignal, _maxOutSignal, result)

            'We need some values for the next calculation.
            _prevI = i
            _prevError = currentErr
        Catch oex As OverflowException
            Throw oex
        Catch ex As Exception
            Throw ex
        End Try
        Return result
    End Function

    ''' <summary>
    ''' Pacing och calling the PID calculations.
    ''' The loop time will be close to the sleep time, a few tests gave that the loop time differed from the sleep time at most by a few ms.
    ''' </summary>
    Private Sub PidRunner()
        Try
            While (True)
                Thread.Sleep(CInt(_dTime_ms))
                If (_performCalculations) Then
                    pidOutSignal = Me.CalculateOutput(_currentProcessValue)
                Else
                    Continue While
                End If
            End While
        Catch ex As Exception
            Throw ex
        End Try
    End Sub
End Class



Simulation code:
Option Explicit On
Option Strict On
Option Infer Off
Imports System.Threading

Public Class PID_sim

    Private _regulator As PidController
    'Using a System.Timers.Timer has the drawback of problems with disposing the form.
    'See http://stackoverflow.com/questions/3641147/object-disposed-exception-and-multi-thread-application
    'The easy way out - apart from ignoring the error, which would be ok in this particular case - is to use a Forms.Timer
    'You can try and change to a System.Timers.Timer and contemplate the interesting effects. :-)
    Private WithEvents _tmr As Windows.Forms.Timer
    Private _autoDisturbe As Boolean = False
    Private _startTime As DateTime
    Private _distTime As Integer = 0
    Private _prevCurrent As Double = 0

    Private Sub PID_sim_Load(sender As Object, e As EventArgs) Handles MyBase.Load
        'Set up PID parameters and simulation values
        txtTarget.Text = "-120"
        txtProcessMin.Text = "-200"
        txtProcessMax.Text = "200"
        txtOutMin.Text = "-100"
        txtOutMax.Text = "100"
        txtC_Prop.Text = "0,9"
        txtC_int.Text = "0,01"
        txtC_deriv.Text = "2"
        txtCurrent.Text = "0"
        txtOut.Text = "0"
        txtDisturbance.Text = "-15"

        ''Set up PID parameters and simulation values for a another kind of process
        'txtTarget.Text = "10,0"
        'txtProcessMin.Text = "0"
        'txtProcessMax.Text = "300"
        'txtOutMin.Text = "0"
        'txtOutMax.Text = "10"
        'txtC_Prop.Text = "0,0002"
        'txtC_int.Text = "0,00002"
        'txtC_deriv.Text = "0,0002"
        'txtCurrent.Text = "0"
        'txtOut.Text = "0"
        'txtDisturbance.Text = "0"

        _tmr = New Windows.Forms.Timer()
        _tmr.Interval = 100
        'Don't start the timer until a PidController has been instantiated
        'because it will call a non existent instance of the PidController.

        Me.CenterToParent()
    End Sub

    Private Sub tick() Handles _tmr.Tick
        Dim current As Double = 0
        Dim pOut As Double = 0
        Dim bCV As Integer = 0
        Dim bOV As Integer = 0
        Dim rnd As Integer = 0

        If (txtCurrent.InvokeRequired) Then
            Invoke(New MethodInvoker(AddressOf tick))
        Else
            _regulator.desiredProcessValue = CDbl(txtTarget.Text)

            'When using the threaded implementation to get the output from the PID.
            ' _regulator.currentProcessValue = CInt(txtCurrent.Text)
            ' pOut = _regulator.pidOutSignal

            'When using the implementation that is not threaded to get the output from the PID.
            pOut = _regulator.CalculateOutput(_prevCurrent)

            'This represents the controlled process
            current = _prevCurrent + pOut + CDec(txtDisturbance.Text)
            'Another type of process represented
            ' current = CDbl(txtDisturbance.Text) + Math.Sqrt(Math.Abs(pOut)) * 100
            'We can not exceed the process max and min
            If (current > CDec(txtProcessMax.Text)) Then current = CDec(txtProcessMax.Text)
            If (current < CDec(txtProcessMin.Text)) Then current = CDec(txtProcessMin.Text)
            _prevCurrent = current

            txtCurrent.Text = current.ToString("0")
            txtOut.Text = pOut.ToString("0")
            txtError.Text = (current - CDec(txtTarget.Text)).ToString("0")

            'Here we may introduce a varying disturbance to the process
            _distTime += (DateTime.Now - _startTime).Milliseconds
            _startTime = DateTime.Now
            If (_autoDisturbe And _distTime > 2000) Then
                _distTime = 0
                rnd = CInt(txtDisturbance.Text) + CInt((New Random(CInt(DateTime.Now.Second + DateTime.Now.Millisecond))).Next(-20, 21))
                txtDisturbance.Text = CStr(If(rnd < CInt(txtOutMin.Text) / 2 Or rnd > CInt(txtOutMax.Text) / 2, 0, rnd))
            End If

            'Showing bars representing the process value and output
            Try
                bCV = CInt(500 + current * 1000 / (CInt(txtProcessMax.Text) - CInt(txtProcessMin.Text)))
                bOV = CInt(500 + pOut * 1000 / (CInt(txtOutMax.Text) - CInt(txtOutMin.Text)))
                barCurrent.Value = If(bCV < 0, 0, If(bCV > 1000, 1000, bCV))
                barOut.Value = If(bOV < 0, 0, If(bOV > 1000, 1000, bOV))
            Catch ex As Exception
                'Do nothing
            End Try
        End If
    End Sub

    Private Sub btnSetValThreaded_Click(sender As Object, e As EventArgs) Handles btnSetValThreaded.Click
        _tmr.Stop()
        'When using the THREADED implementation of the PidController:
        ' _regulator = New PidController(CDec(txtOutMin.Text), CDec(txtOutMax.Text), CDec(txtC_Prop.Text), CDec(txtC_int.Text), CDec(txtC_deriv.Text), 100)
        'When using the NON threaded implementation of the PidController:
        _regulator = New PidController(CDec(txtOutMin.Text), CDec(txtOutMax.Text), CDec(txtC_Prop.Text), CDec(txtC_int.Text), CDec(txtC_deriv.Text))
        'Now that we have a PidController object we can start using it...
        _tmr.Start()
    End Sub

    Private Sub btnStartThreaded_Click(sender As Object, e As EventArgs) Handles btnStartThreaded.Click
        'Use only with threaded implementation.
        If (Not _regulator Is Nothing) Then _regulator.performCalculations = True
    End Sub

    Private Sub btnStopThreaded_Click(sender As Object, e As EventArgs) Handles btnStopThreaded.Click
        'Use only with threaded implementation.
        If (Not _regulator Is Nothing) Then _regulator.performCalculations = False
    End Sub

    Private Sub chkAutoDisturbe_CheckedChanged(sender As Object, e As EventArgs) Handles chkAutoDisturbe.CheckedChanged
        'Let disturbance vary by itself.
        _autoDisturbe = chkAutoDisturbe.Checked
    End Sub
End Class

Attached image(s)

  • Attached Image


Is This A Good Question/Topic? 0
  • +

Page 1 of 1