Electronic – Understanding the flow of a PI Controller

cpid controller

I am theoretically aware of how a PID Controller works, have never implemented one. I am implementing a control method for driving a valve over PWM.

Use Case details:
The systems has two ADC channels, one for input and the other for feedback.
The reading of ADC channels is free-running, with sufficient samples being taken.

Existing implementation:
There is an infinite loop, which does two jobs only: Read ADC values and Generate PWM.
There is timer interrupt configured to invoke at 20 msec. So 'Has the time elapsed?' in flowchart below will be evaluated 'Yes' after every 20 msec.
Below is the flowchart of what I am doing as of now.

enter image description here

Following is the program that I am looking into:

/*
    Some information on variables that are being used:

    CURR_OUT_CH is Feedback channel
    CMD_INP_CH is the channel where external input is applied.
    So, ADC_Val.fADC_Final_mAVal[CURR_OUT_CH] is where I am receiving the value of feedback
    And, ADC_Val.fADC_Final_mAVal[CMD_INP_CH ] is where I am receiving the value of external input that I am trying to achieve
    MAX_ALLOWABLE_DUTY_CYCLE  is a macro set to ((uint16_t)480) which Maps to 60% - This is a requirement.

        (Op-Amp output is in mV, I convert it into mA based on resistor values)

    (Config[chUser_Config_Mode].uiMaxCMD_I) is 350. (max current allowed through, in mA)
*/

#define RESTRICT(x, low, high)   (x = (x)<(low)?(low):((x)>(high)?(x=high):(x)))

typedef struct {

    float fFeedback;
    float fOutput;
    float Kp;
    float Ki;
    float fIntegralError;
    float fSetpoint;

} PIControl_t;

PIControl_t PI;
uint16_t Load_Dutycount;

void PICompute(PIControl_t *pPI) 
{
    // I know that if PI is already a global, then taking the pointer doesn't make sense here,
    // but, I may have to add another PI for a different sensor here, that is why I have used 
    // it this way!

    // Instantaneous error is always local
    float fError = 0.0;

    // The classic PID error term
    fError = pPI->fSetpoint - pPI->fFeedback;

    // Compute the integral term
    pPI->fIntegralError += (pPI->Ki * fError);

    // Run all the terms together to get the overall output
    pPI->fOutput = (pPI->Kp * fError) + (pPI->fIntegralError);
}

void Update_PWM_Module(void)
{
    // Might want to get rid of this fCount, lets see.
    float fCount = 0.0;

    // Timer hasn't generated an interrupt yet (Integration time hasn't elapsed)
    // ISR sets the bCompute variable - Flags are Not the best way, but does what it should.
    // And, Timer doesn't start counting if bCompute is set
    if(!bCompute)
    {
        // No control action needed, return!
        return;
    }

    // Assign the feedback value read for PI output computation
    PI.fFeedback = ADC_Val.fADC_Final_mAVal[CURR_OUT_CH];

    // Compute the PI Controller output
    PICompute(&PI);

    // Formulate the value to be used to generate PWM
    ADC_Val.fADC_Final_mAVal[CURR_OUT_CH] = ADC_Val.fADC_Final_mAVal[CURR_OUT_CH] + PI.fOutput;

    // Map Output to no. of counts
    fCount = (float) ((ADC_Val.fADC_Final_mAVal[CURR_OUT_CH] * MAX_ALLOWABLE_DUTY_CYCLE) / (float)(Config[chUser_Config_Mode].uiMaxCMD_I));

    // Convert into compatible Duty Count type - uint16_t
    Load_Dutycount = (uint16_t) fCount;

    // Bound the output count between worst case lower and higher points
    RESTRICT(Load_Dutycount, MIN_DUTY_CYCLE_COUNT, MAX_ALLOWABLE_DUTY_CYCLE);

    // Generate PWM
    Generate_PWM(Load_Dutycount);

    // Assign the latest external input value read from ADC as the Setpoint for PI computation
    PI.fSetpoint = ADC_Val.fADC_Final_mAVal[CMD_INP_CH] ;

    // Not sure about this --- Because I think with a new Setpoint, the integrated error (which was developed based on previous Setpoints) will have no significance.
    PI.fIntegralError = 0.0;

    // Start integration all over again (Timer doesn't start counting if bCompute is set)
    bCompute = false;
}    

int main(void)
{
    // Some code for Power-ON initialization like,
    //    ADC
    //    Timer
    //    PWM
    //    PI variables
    //    Everything else which needs one-time initialization before going into the infinite loop

    while(1)
    {
        Read_ADC();
        Update_PWM_Module();
    }
}

Once the PWM is generated, its free-running. The Duty cycle will reamin constant unless I change it, so its only changed periodically based on the PI Computation.

For the sake of clarification, when I say 'nullify the value of Integrated error', I meant pPI->integralError = 0.0; in C program.

Problem Statement:
The overall time taken for execution of loop when timer has not elapsed is roughly 2 msec.
The execution time does of course increase when PI computation is done and PWM generate function is invoked.

I am probing the two signals:
– Output of the feedback at output of Operational amplifier that is used.
– Input to the system.

My questions is, is the operational flow correct? Am I correct about generating PWM only after PI Computation is done, and resetting the value of the integrated error to 0.0 whenever a new Setpoint is assigned?
When tested with a step input of 0-4V, 0.5 Hz, on oscilloscope I see that system takes about 120 msec to rise its output to input.
I can correlate that P and I values will have to tuned to improve on the time. This post is not much about tuning the values of P and I factors.

Related reading:
Questions on electronics.stackexchange I have read through and are closely related:

Best Answer

Me: But you haven't explained why you are resetting / nullifying the integral term.

Weda: Because I thought with a new setpoint, the integrated error (which was developed based on previous setpoints) would have no significance. I think this is where I have fundamental confusion / lack of understanding.

I'll give you my 'PI for beginners' example that seems to help some at work:

  • We'll use a PI controller on a car cruise control.
  • The setpoint is 80 kph.
  • The proportional band is 10 kph. That means 100% throttle up to 70 kph and 10% reduction in throttle for every 1 kph above 70 kph reaching 0% throttle at 80 kph.

Proportional-only control

enter image description here

Figure 1. Response of P-only cruise control. Note that 80 kph setpoint speed is never achieved.

We switch the cruise control on. It accelerates to 70 kph at 100% throttle. It should be clear already that we will never reach 80 kph because with rolling and wind resistance we can't maintain 80 kph with zero power. Let's say it settles down at 77 kph at 30% power. That's as good as we can get with P-only control.

Proportional-integral control

enter image description here

Figure 2. The response with the addition of integral control.

When integral action is added the integral term continues to rise at a rate proportional to the error. This can be seen in Figure 2's integral curve as a high initial rate of rise due to the large initial error falling to zero rise (level line) when the error is finally eliminated.

enter image description here

Figure 3. The classic PID control function. Source: Wikipedia - PID controller.

One thing that dawned on me rather late in life was that as the integral action corrects the output the error falls to zero so the contribution of the proportional control also falls to zero. The output when the error is zero is maintained purely by the integral action.


Note that if the setpoint changes or the loading changes (the car meets a hill or a headwind) that the error will change to a non-zero value, the P control will immediately rise from zero and the integral action will continue from its current value - not from zero.


There's a simple Excel PI simulator over at Engineers Excel and this may be of use. I don't know if it's the best.

Related Topic