¿Comprender el flujo de un controlador PI?

En teoría , soy consciente de cómo funciona un controlador PID, nunca he implementado uno. Estoy implementando un método de control para conducir una válvula sobre PWM.

Detalles del caso de uso: el sistema tiene dos canales ADC, uno para entrada y otro para retroalimentación. La lectura de los canales ADC es libre y se toman suficientes muestras.

Implementación existente: hay un bucle infinito, que solo realiza dos trabajos: leer valores de ADC y generar PWM. Hay una interrupción del temporizador configurada para invocar a los 20 mseg. Entonces '¿Ha transcurrido el tiempo?' en el diagrama de flujo a continuación se evaluará 'Sí' después de cada 20 mseg. A continuación se muestra el diagrama de flujo de lo que estoy haciendo a partir de ahora.

ingrese la descripción de la imagen aquí

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 que se genera el PWM, funciona libremente. El ciclo de trabajo permanecerá constante a menos que lo cambie, por lo que solo cambia periódicamente según el cálculo de PI.

En aras de la aclaración, cuando digo 'anular el valor del error integrado', quise decir pPI->integralError = 0.0;en el programa C.

Declaración del problema: el tiempo total necesario para la ejecución del bucle cuando el temporizador no ha transcurrido es de aproximadamente 2 ms. Por supuesto, el tiempo de ejecución aumenta cuando se realiza el cálculo de PI y se invoca la función de generación de PWM.

Estoy probando las dos señales:
- Salida de la realimentación a la salida del amplificador operacional que se utiliza.
- Entrada al sistema.

Mi pregunta es, ¿el flujo operativo es correcto? ¿Estoy en lo correcto acerca de generar PWM solo después de que se realiza el cálculo de PI y restablecer el valor del error integrado a 0.0 cada vez que se asigna un nuevo punto de ajuste? Cuando se prueba con una entrada escalonada de 0-4 V, 0,5 Hz, en el osciloscopio veo que el sistema tarda unos 120 ms en aumentar su salida a entrada. Puedo correlacionar que los valores P e I tendrán que ajustarse para mejorar el tiempo. Esta publicación no se trata mucho de ajustar los valores de los factores P e I.

Lectura relacionada: Preguntas sobre electronics.stackexchange que he leído y están estrechamente relacionadas:

¿Qué quiere decir con "anular el valor del error integrado"? Realmente desea integrar el error cada vez de muestra. Y el tiempo de muestra debe ser un número fijo de milisegundos... tal vez eso es lo que quiere decir con "tiempo transcurrido", pero no está claro. Además, PWM debería estar ejecutándose todo el tiempo (hardware, por lo general, pero podrían ser interrupciones) y no solo generarse como parte del cálculo de PI.
@SpehroPefhany: he editado la publicación para aclarar el "tiempo transcurrido" y el hecho de que PWM debería funcionar libremente sin importar qué.
No respondió a su primera pregunta: ¿Qué quiere decir con "anular ...". Mi otra única preocupación es que no tiene anti-windup en el término integral. Si, por alguna razón, el error nunca se reduce a cero, la integral se "terminará" y aumentará continuamente (eventualmente dando un desbordamiento). Cuando solucione el problema, el término I tardará un tiempo en volver a un valor razonable.
@SpehroPefhany y @Transistor: Lo que quise decir con "anular el valor del error integrado" es asignar pPI->integralErrora0.0
Preferiría ver el código real que está utilizando dentro del ciclo con buenos nombres para las variables. Algunas cosas pueden perderse en la traducción. - Convierte el código en diagrama de flujo, tengo que convertir el diagrama de flujo nuevamente en código.
@WedaPashi: "Restablecer la integral" podría ser un término mejor, pero se supone que no debe restablecer esto, excepto al encender. El término integral debe ser la integral de todos los errores desde que se enciende la alimentación. Creo que el anti-windup debería limitar la integral a ese valor que hace que el término integral sea 100%.
@HarrySvensson: he agregado el programa C. Gracias.
Pero no ha explicado por qué está restableciendo/anulando el término integral.
@Transistor: porque pensé que con un nuevo punto de ajuste, el error integrado (que se desarrolló en función de los puntos de ajuste anteriores) no tendría importancia. Creo que aquí es donde tengo una confusión fundamental/falta de comprensión.
Lo que a menudo es difícil de entender es que el término integral sirve como "memoria" de su salida. Por lo tanto, no hay "comparar la nueva lectura de ADC con la lectura anterior de ADC", como uno podría estar tentado a hacer. En su lugar, simplemente le dice al regulador PI "así es como están las cosas ahora", y luego le dará un nuevo valor, actualizando la parte integral continuamente.
Además, asegúrese de que está utilizando ciclos de temporizador PWM como unidad interna en todas partes: vuelva a escalar la lectura de ADC a ciclos de temporizador desde el principio. Un error común son los programas que usan valores brutos de ADC y "unidades PID abstractas" y ciclos de temporizador, en lugar de usar la misma unidad en todas partes. Un error aún peor es usar una unidad "comprensible para los humanos" como voltios o amperios dentro del firmware; esas unidades no tienen sentido para el programa o la CPU, sino solo para el programador. Hay muchos programas de desastre para novatos en los que el aspirante a programador introduce el flotador solo porque quiere voltios como unidad interna.
@Lundin: Efectivamente. Me tomó un tiempo entender esto cuando tenía algunos datos con los que jugar, pero agradezco su comentario, aclara mucho sobre la parte de integración en la implementación de PI.
Por cierto, lo más probable es que no haya ninguna razón por la que necesites usar float aquí. Los reguladores PID se pueden escribir con matemáticas enteras simples. Entonces, en caso de que esté usando una MCU sin una FPU, simplemente la explotó sin una buena razón.
@Lundin: Tengo que estar de acuerdo con esto. Hace un par de días deseché el código que haría enormes cálculos de coma flotante sin motivo alguno. Simplemente hago esto en los recuentos de ADC sin procesar. El sistema es mucho más eficiente en términos de tiempo de procesamiento y memoria de lo que solía ser. Usar los ciclos del temporizador PWM es una idea genial en la que nunca podría haber pensado, voy a hacer ese cambio esta noche :-)
En algún lugar de Internet, existe un código fuente abierto para un controlador PID basado en enteros de 32 bits. ¿Quizás esto? incrustadorelacionado.com/showarticle/121.php . Es matemática bastante simple, compartiría mi propio código de controlador PID pero desafortunadamente es propietario :(
@Lundin: Gracias por el enlace, y estoy totalmente de acuerdo con no recibir el código de usted, obviamente podría haber sido un gran aprendizaje para mí mirar su código, que sería mucho mejor que lo que tengo ahora. He escrito un módulo de controlador PI simple que funciona bien cuando lo probé con entradas de paso y rampa, y se basa en cálculos de punto fijo. Estoy aprendiendo, y es divertido! :-)
@Lundin, ¿cómo cambia la escala de los valores ADC a valores PWM? ¿No tendrá que hacerlo para cada carga en el sistema?
@AbdelAleem Son ecuaciones lineales simples. Tienes un máximo, un mínimo y un coeficiente, entonces quieres que corresponda a otra relación lineal. Por lo general, solo ADC_VAL / ADC_MAX = PWM_VAL / PWM_MAX donde desea resolver PWM_VAL.
@Lundin, ah, sí, dado que su actuador es relativamente lineal. Pero el problema que veo es que la ecuación lineal dependerá de la carga si no me equivoco.
@Lundin corrígeme si me equivoco: esta asignación de ADC_VAL a PWM_VAL solo es correcta para una carga en particular, ¿verdad?
@AbdelAleem Lo que sea que todas estas unidades signifiquen en la práctica es, por supuesto, específico de la aplicación. Su ADC podría estar midiendo muchas cosas y sus tics del temporizador PWM también podrían significar muchas cosas.
@Lundin Tengo que preguntar: si encuentro una relación lineal entre la señal de entrada y la señal de salida para una carga determinada, ¿por qué necesitaría un controlador (si descuidamos el impacto de las perturbaciones)?
@AbdelAleem Son solo las escalas que son lineales. Toma los valores brutos de ADC y los escala a los ticks PWM correspondientes. Pero en el medio se encuentra el regulador PID que determina la relación entre la entrada y la salida. Todo lo que decía en estos comentarios es usar la misma unidad en todas partes dentro de su programa, y ​​no volver a escalar innecesariamente a alguna unidad inútil "legible por humanos" como mA, grados o lo que sea que estos valores realmente representen.

Respuestas (1)

Yo: Pero no has explicado por qué estás reiniciando/anulando el término integral.

Weda: Porque pensé que con un nuevo punto de ajuste, el error integrado (que se desarrolló en base a los puntos de ajuste anteriores) no tendría importancia. Creo que aquí es donde tengo una confusión fundamental/falta de comprensión.

Te daré mi ejemplo de 'PI para principiantes' que parece ayudar a algunos en el trabajo:

  • Usaremos un controlador PI en el control de crucero de un automóvil.
  • La consigna es de 80 km/h.
  • La banda proporcional es de 10 km/h. Eso significa un 100 % de aceleración hasta 70 kph y una reducción del 10 % en la aceleración por cada 1 kph por encima de 70 kph, alcanzando el 0 % de aceleración a 80 kph.

Control solo proporcional

ingrese la descripción de la imagen aquí

Figura 1. Respuesta del control de crucero solo P. Tenga en cuenta que la velocidad de consigna de 80 km/h nunca se alcanza.

Encendemos el control de crucero. Acelera a 70 km/h al 100 % del acelerador. Ya debería quedar claro que nunca alcanzaremos los 80 km/h porque con la resistencia a la rodadura y al viento no podemos mantener los 80 km/h con potencia cero. Digamos que se establece a 77 kph al 30% de potencia. Eso es lo mejor que podemos conseguir con el control P-only.

Control proporcional-integral

ingrese la descripción de la imagen aquí

Figura 2. La respuesta con la adición del control integral.

Cuando se suma la acción integral, el término integral continúa aumentando a una tasa proporcional al error. Esto se puede ver en la curva integral de la Figura 2 como una alta tasa inicial de aumento debido al gran error inicial que cae a cero (línea de nivel) cuando finalmente se elimina el error.

ingrese la descripción de la imagen aquí

Figura 3. La función clásica de control PID. Fuente: Wikipedia - Controlador PID .

Una cosa que me di cuenta bastante tarde en la vida fue que a medida que la acción integral corrige la salida, el error cae a cero, por lo que la contribución del control proporcional también cae a cero. La salida cuando el error es cero se mantiene puramente por la acción integral.


Tenga en cuenta que si el punto de referencia cambia o la carga cambia (el automóvil se encuentra con una colina o con viento en contra), el error cambiará a un valor distinto de cero, el control P aumentará inmediatamente desde cero y la acción integral continuará desde su valor actual. - no de cero.


Hay un simulador simple de Excel PI en Engineers Excel y esto puede ser útil. no se si es lo mejor

Esto fue absolutamente útil para aclarar las confusiones y simplemente me llevó a escribir el código en lugar de buscar otras fuentes. ¡Gracias una tonelada!
Entonces, cuando conduzco un automóvil y presiono el pedal para alcanzar la velocidad deseada, ¿puede decir que este es el término integral en el trabajo? No tendría ningún sentido si fuera el control P, porque no puedo disminuir mi salida para disminuir mi error (definido como velocidad). Entonces, ¿cuál es el uso del control P para el control de velocidad?
@AbdelAleem (1) No, miro el velocímetro y veo que estoy por encima o por debajo de la velocidad y realizo un ajuste inicial del acelerador basado en eso. Eso es control proporcional. (2) Sí, puede disminuir su producción levantando el pie del pedal. (3) El error no se define como velocidad. Es la diferencia entre la velocidad deseada y la velocidad real. Tiene las mismas unidades que la velocidad. (4) El control P es exactamente eso. Da una salida proporcional al error. Es muy útil.
@Transistor no, la razón por la que alcanza la velocidad deseada es el control I, no el control P. El control P solo le dará un error de estado estable y NUNCA alcanzará la velocidad deseada. No puede disminuir la velocidad cuando intenta alcanzarla, que es lo que haría un único controlador P. ¿Cómo puede mantener la velocidad deseada cuando la salida de su controlador P es cero en ese punto? Eso es paradójico.
@Transistor, por supuesto, el error se define en términos de velocidad. Si tiene la unidad de velocidad, entonces se define como velocidad.
@AbdelAleem, mi respuesta aborda el problema del error de estado estable y cómo lo resuelve la acción integral.
@Transistor sí, lo sé, y es una respuesta increíble =)