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.
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: