Yo soy teóricamente Conozco el funcionamiento de un controlador PID, pero nunca he implementado uno. Estoy implementando un método de control para conducir una válvula sobre PWM.
Detalles del caso de uso: Los sistemas tienen dos canales ADC, uno de entrada y otro de retroalimentación. La lectura de los canales ADC es de ejecución libre, con la toma de muestras suficientes.
La aplicación existente: Hay un bucle infinito, que hace dos trabajos solamente: Leer los valores del ADC y generar el PWM. Hay una interrupción del temporizador configurada para invocar a los 20 mseg. Así que "¿Ha transcurrido el tiempo?" en el diagrama de flujo de abajo será evaluado "Sí" después de cada 20 mseg. Abajo está el diagrama de flujo de lo que estoy haciendo ahora.
El siguiente es el programa que estoy investigando:
/*
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();
}
}
Una vez generado el PWM, su funcionamiento es libre. El ciclo de trabajo se mantendrá constante a menos que lo cambie, por lo que sólo se cambia periódicamente basado en el cálculo PI.
Para que quede claro, cuando digo "anular el valor del error integrado", me refería a pPI->integralError = 0.0;
en el programa C.
Planteamiento del problema: El tiempo total de ejecución del bucle cuando el temporizador no ha transcurrido es de aproximadamente 2 mseg. El tiempo de ejecución aumenta, por supuesto, cuando se realiza el cálculo de PI y se invoca la función de generación de PWM.
Estoy sondeando las dos señales:
- Salida de la retroalimentación a la salida del amplificador operacional que se utiliza.
- Entrada al sistema.
Mi pregunta es si el flujo operativo es correcto. ¿Estoy en lo cierto en cuanto a generar el PWM sólo después de que se haya realizado el cálculo PI, y en cuanto a restablecer el valor del error integrado a 0,0 cada vez que se asigna un nuevo Setpoint? Al probar con una entrada escalonada de 0-4V, 0,5 Hz, en el osciloscopio veo que el sistema tarda unos 120 mseg en elevar su salida a la entrada. Puedo correlacionar que los valores P e I tendrán que ser sintonizados para mejorar el tiempo. Este post no trata mucho de afinar los valores de los factores P e I.
Lectura relacionada: Las preguntas en electronics.stackexchange las he leído y están muy relacionadas: