Control PID de posición para un motor DC con encoder incremental

Un controlador PID es un mecanismo de control por realimentación ampliamente usado en sistemas de control industrial. Este calcula la desviación o error entre un valor medido y un valor deseado. (Wikipedia.)

En Internet hay multitud de controladores PID para el control de temperatura o de velocidad, sin embargo, encontrar información sobre el control de posición es bien escaso. En esta página presento dos controladores PID para PIC (Microchip) para controlar la posición de un motor, y está enfocado en el 18F4550, pero puedes utilizar cualquiera de la familia 18FXX5X. Para otros microcontroladores PIC tendrías que hacer adaptaciones, teniendo presente que necesitarás una interrupción externa y dos salidas PWM.

El montaje del circuito es bien sencillo, en la imagen de arriba puedes ver las pocas conexiones que tiene. Si no tenemos en cuenta el cristal de cuarzo y el USB sólo hay dos entradas para decodificar el encoder y dos salidas PWM. Es importante acondicionar las señales del encoder con alguna puerta lógica que posea disparador Schmitt. Yo utilizo el clásico 74LS14, que es un inversor lógico (puerta Not) porque posee histéresis (esta característica es lo importante). La mayoría de los encoders dan las señales de cuadratura directamente de los leds receptores y no suelen estar acondicionadas, entonces lo hemos de implementar nosotros.

El puente en H puede ser cualquiera que se adapte al motor que vayas a utilizar. Para los que no tienen mucha experiencia en puentes en H decirles que es importante que tenga protección para cuando los dos niveles de entrada están en "alto" y ha de llevar los 4 diodos de protección para eliminar las corrientes inversas que produce las bobinas del motor. Yo utilizo puentes en H que compro en Internet y a día de hoy son muy económicos y están preparados para controlar motores y bobinas.

En esta imagen puedes ver el motor que he utilizado, es un motor con encoder incorporado de 334 pulsos por revolución (ppr). El encoder me permite alimentarlo con 5 voltios (también funciona con 3,3V), pero hay que tener cuidado porque muchos encoders de hoy en día funcionan sólo con 3,3 voltios y si los alimentas con 5 se podrían estropear. Antes de hacer nada hemos de informarnos sobre esta característica y la distribución de los pines. En este enlace puedes ver la configuración del patillaje de algunos modelos de encoders ópticos de media/alta resolución.

Motor con encoder óptico.

El motor lo he alimentado con 5 voltios, a través del puente en H, y he llegado hasta los 12. La velocidad del motor es clave porque si va excesivamente rápido, como el microcontrolador ha de decodificar el encoder y traducirlo a posición, el exceso de velocidad le haría perder pulsos, entonces hemos de poner una tensión de alimentación no muy alta, y luego, una vez que funciona todo correcto, puedes ir incrementado la tensión del motor (en realidad sería al puente en H) y asegurarte de que cuenta perfecto. Cuando usamos encoders incrementales, la "velocidad" del microcontrolador es clave, por eso he utilizado un PIC rápido y con PLL interno para aumentar al máximo la frecuencia de ejecución, en este caso a 48MHz. Luego explico esto con más detalles.

Comentado los puntos importantes sobre el hardware, vamos a pasar al software. Pulsa en el siguiente enlace para descargar este proyecto. Luego iremos viendo qué es cada parte, cómo utilizarlo y la descripción de los programas.

(Si usas antivirus Avast has de añadir una exclusión para poder ejecutar el fichero ".exe" que contiene. Para analizar este o cualquier otro archivo puedes hacer clic aquí)

Abre el ZIP y descomprímelo; dentro de la carpeta "controlPID" verás dos carpetas llamadas "CCS" y "Freebasic". La carpeta CCS contiene las dos versiones de control PID (en dicho lenguaje) y todo lo necesario para poder compilar. En Freebasic está el código fuente y ejecutable para establecer la comunicación entre el microcontrolador y el PC. No te preocupes si tu lenguaje de programación es otro, eso lo puedes cambiar. Yo utilizo el modo "Bulk transfer" porque me es muy cómodo y no crea tiempos de latencia, pero haciendo unas modificaciones en el programa del PIC puedes eliminar esa parte y configurarlo para comunicarte como mejor estés acostumbrado, como por ejemplo desde un terminal serie.

La carpeta "CCS" contiene dos versiones de control PID. "normalPID.c" es el control PID convencional, está basado en el algoritmo publicado por Brett Beauregard. La segunda versión es prácticamente igual, se llama "reformedPID" (PID mejorado), pero le he añadido unas líneas de código en la parte matemática que mejora notablemente el comportamiento del motor, dándole más suavidad y estabilidad al posicionarse, incluso es más sencillo de sintonizar (encontrar las constantes PID óptimas).

Comentando los códigos.

Pondré una imagen del código y debajo hago los comentarios.

  • Es para un PIC 18F4550, pero puedes poner cualquier PIC de la familia 18Fxx5x.
  • La velocidad interna de ejecución es de 48MHz, pero como cada instrucción máquina consume 4 ciclos de reloj, en realidad estará funcionando a 12MHz. En el esquema puse un cristal de 20MHz, pero puedes usar otros cristales de otras frecuencias. Si este es tu caso sólo has de cambiar una línea del código, tal como lo explico aquí.
  • Observa la línea que pone "#DEVICE HIGH_INTS=TRUE", esto es para avisar al compilador que ha de priorizar las interrupciones externas. Más abajo del código se vuelve a tocar este tema.
  • Yo uso un bootloader para cargar los programas, es decir, no uso un "quemador". Si tú no usas un bootloader has de eliminar o comentar (poniendo //) la línea que hace referencia a ese tema.
  • Amén de las declaraciones de puertos y todo eso, luego viene la parte de configurar el USB y lo tengo configurado para el modo (o clase) Bulk Transfer. Si tú prefieres usar otro modo de comunicación has de modificar esa parte. Lo normal hubiera sido crear un puerto virtual tipo "COMx", pero no me gusta cómo trabaja los PICs (al menos con CCS C) en este sentido, especialmente cuando manejo interrupciones externas y comunicación serie. El modo "Bulk" elimina esos problemas y lo hace perfecto, sin crear tiempos de latencia ni perturbaciones en el resto de las interrupciones. De todas formas, para este proyecto puedes usar el tipo de comunicación que prefieras. La cuestión es poder recibir 4 bytes, que es la información para posicionar el motor (el "Setpoint")
  • Esto es la decodificación del encoder y lo transforma a posición. La variable "contador" almacenará los pulsos del encoder, es decir, la posición del motor. Verás arriba a la izquierda "int_ext fast"; eso hace que la interrupción externa sea prioritaria sobre todas las demás. Para que surta efecto, como comenté antes, ha de estar arriba del todo del código esta otra línea #DEVICE HIGH_INTS=TRUE".
  • "Compute()" se encarga de calcular todos los errores (proporcional, integral y derivativo) y el resultado lo guarda en una variable global llamada "Out". Esta variable luego la usaremos en el código principal para saber si el motor ha de ir hacia delante o hacia atrás, y no sólo eso, además "Out" contiene el valor que ha de salir en la patilla PWM correspondiente. Modifiqué una línea al código original (en "normalPID.c") para aumentar la estabilidad. Se trata de que cuando el valor de ITerm sobrepasa el valor máximo, ponerlo a cero en vez de mantenerlo en el valor máximo. Sucede que mientras el motor hace el recorrido para posicionarse, "ITerm" va acumulando el error integral y para cuando el motor llega a la posición designada, no le da tiempo a minimizar ese error y hace que el motor se pase un poco más allá de dicha posición. Este "truquillo" no es muy ortodoxo, pero da buen resultado. "Inp" (input), es decir, el valor del contador se carga en una variable global a través de la interrupción externa y ese valor es cargado dentro de la función "Compute()"; de esta manera el programa sólo toma ese dato cuando realmente va a hacer los cálculos. "Compute()" sólo realizará los cálculos si se cumple el tiempo de muestreo.
  • Verás que dentro de la carpeta CCS (en el interior del ZIP) hay dos programas, uno llamado "normalPID.c" que es el control PID común y corriente, y el otro se llama "reformedPID.c". Ambos son casi identicos, lo que cambia son unas líneas de código dentro de la función Compute(). "reformedPID.c" tiene implementada unas modificaciones del código PID convencional en la que se consigue mejorar notablemente el comportamiento del motor, haciendo que el posicionamiento de éste sea mucho más suave, progresivo y evita oscilaciones; y no sólo eso, resulta mucho más sencillo encontrar las constantes PID óptimas.
  • La función SetTunings() nos permite invocarla cuando lo necesitemos. El programa lo he hecho lo más simple que he podido para que fuese comprensible, pero con el tiempo, si añades por tu parte unas líneas de programación, puedes llamar a esta función y modificar las constantes sin necesidad de tener que volver a re-programar el PIC; esto es especialmente cómodo mientras estamos buscando los tediosos valores KP, KI y KD. Observa que dentro de esa función se les está incluyendo el tiempo. Esto permite agilizar los cálculos evitando tener que hacer esa operación en la función "Compute()", es decir, que se calcula una sola vez y el tiempo ya va incluido (en las constantes PID) cada vez que se invoque la función "Compute()".
  • SetSampleTime() es el tiempo de muestreo. Aquí lo único que hacemos es evitar que sea cero o negativo.

Después viene el cuerpo del programa, con muchas configuraciones que omito describir porque ya está explicado a modo de comentarios dentro del mismo código. Pasamos directamente al "músculo" del programa que es la parte que hace mover el motor.

  • Esto pertenece al bucle infinito, y es la parte que hace mover el motor hacia un lado o hacia el otro y con una fuerza determinada. Una vez calculado los errores en la función "Computer()" se transfiere ese valor a la variable global "out". El signo de esta variable (si es positivo o negativo) es lo que hace que el motor gire en un sentido o en el otro, y el valor que contiene es el valor de PWM que irá al motor, es decir, con una determinada fuerza. Es muy importante la estructura de "If - Else" encadenadas que ves en el código, porque se ha de decidir sólo por una de esas condiciones, y la prioridad es que si el motor ha llegado al punto designado, se pare. Para ello no uso la variable "out" sino la variable global "error", que es la que realmente tiene el error que hay en ese instante, ya que "out" puede contener errores acumulativos que todavía no están actualizados. Esto permite que una vez que llegue el motor a su destino, se pare completamente, evitando residuos innecesarios, o lo que es lo mismo, evitar que el motor esté consumiendo corriente sin hacer nada.
  • Si se cumple que existe algún dato recibido, entonces lo cargamos en el "Setpoint", que es la posición que queremos que vaya el motor. Recuerda que yo aquí estoy usando el USB en modo "Bulk transfer", tu puedes modificar esta parte y ponerlo, por ejemplo, como CDC (puerto virtual de comunicaciones) o lo que mejor conozcas.

Antes de ponerlo en marcha y otras cuestiones:

  • Si al poner en marcha el circuito y al mover el eje el motor gira sin parar, significa que la polaridad del motor está invertida por tanto has de cambiar la polaridad del motor.
  • Si modificas valores numéricos que están dentro del código, recuerda que esto es lenguaje C, esto significa que al tener declarado una variable como "double" (esto es notación en coma flotante) no podemos poner valores enteros. Ejemplo: si tengo puesto "KP=2.0;", no se debe de poner "KP=2;" y omitir el punto decimal por considerarlo innecesario. Si el compilador no ve el punto decimal lo interpreta como un entero y no como punto flotante pese a que está declarada como flotante, lo cual hace errar los cálculos y crearnos muchos dolores de cabeza.
  • Si por la razón que sea necesitas que el motor gire en sentido contrario a como lo hace normalmente, has de enrocar (intercambiar) las entradas del encoder, y también, la polaridad del motor.
  • El PIC tiene una patilla en el que es necesario poner un condensador (mayor de 250 nF) para estabilizar la tensión interna del USB; en los PICs 18F4550 sería entre el pin 18 y masa. De otro modo te daría fallos intermitentes o sencillamente no funcionaría la comunicación USB.
  • En este enlace puedes ver la configuración del patillaje de algunos modelos de encoders ópticos de media/alta resolución.
  • Debido a que es un encoder incremental, en la práctica necesitarás que cuando se ponga en marcha el PIC lo primero que haga sea llevar el motor (con reductora) a la posición cero real (en este sentido sería como cuando controlas un motor de pasos) porque, si no, considerará la posición cero allá donde esté en el momento de poner en marcha el circuito. Pero te aconsejo que antes de añadir esta parte primero hagas las pruebas sin este detalle (tal como lo presento). Cuando todo te funcione bien y lo tengas bien sintonizado, entonces le añades unas líneas de programación para que el PIC encuentre la posición cero real; simplemente es hacer que el motor vaya hacia atrás hasta que dé con una foto-célula (esto hay que hacerlo justo antes de pasar a loop del programa principal), en ese instante se pondrá el valor del contador de pulsos a cero y eso es todo.
  • En el ZIP de descarga hay una carpeta con el driver necesario para hacer funcionar el modo "Bulk". Windows 8 y 10 no permite por defecto instalar ningún driver que no esté firmado y ni siquiera te preguntará. Hay que lidiar un poco pero si sigues los pasos que indica esta web resolverás ese problema y podrás instalar el driver sin problemas. Cuando lo hagas y reinicies has de ir directamente a la instalación del driver y ya no tendrás problemas.

Sintonización del Controlador PID.

En el código tengo puesto las constantes PID que son óptimas para mi caso. En el tuyo es probable que el motor sea diferente y/o tengas un encoder con otra resolución. Estos cambios hace que mis constantes PID puedan ser diferentes a las que tú necesitas, entonces hay que re-sintonizar dichas constantes.

Se trata de ir probando valores para las constantes KP, KI, KD y tiempo de muestreo.

  • Control proporcional: Este es el control más importante, el que realmente mueve el motor la mayor parte del recorrido. Imagina que KP=1, KI=0 y KD=0 (al poner 0 anulamos ese tipo de control). Ahora imagina que el motor está en la posición 0 y queremos llevarlo a la posición 23000. El control proporcional tendrá casi todo recorrido el valor de 255 (el valor máximo del PWM) hasta que finalmente llegamos a la posición 23000-255, es decir a posición 22745, a partir de ese momento el valor del PWM irá bajando en proporción a la posición en la que se va acercando al punto designado. Si aumentamos el valor de KP, haríamos que se acercase un poco más, pero esto tiene un límite. Si el motor está libre de carga, por regla general, lo que sucede es que por la propia inercia del movimiento tiende a pasarse de la posición designada y se produce oscilaciones cada vez más lenta (si KP es lo suficientemente bajo, de lo contrario no pararía de hacerlo), y aún así no se pone en el lugar que le pedíamos, sino un poco más allá o más acá de la posición designada.
  • Control derivativo (o diferencial): La velocidad es una derivada, por eso se llama así. El control derivativo lo que hace es restar velocidad al motor en la medida que se acerca al punto designado. Este tipo de control elimina la oscilación que mencioné anteriormente en el control proporcional y también lo hará con el control integral. Un valor demasiado alto en la constante derivativa (KD) produce comportamientos especialmente ruidosos u oscilaciones arbitrarias. El control derivativo es muy sensible a los cambios porque trata de darle al motor la velocidad de llegada correcta. Por regla general (siempre hay excepciones, porque depende de la naturaleza del proyecto) la constante derivativa suele tener un valor mucho más elevado que las otras constantes.
  • Control Integral: El control proporcional "intenta" llegar a la meta, el derivativo ajusta la velocidad, por tanto evita las oscilaciones, pues el control integral es la parte más sensible de este asunto: Cuando el motor se acerca a la meta, en el mejor de los casos, lo que hace es que si ve que todavía falta un poco para llegar al punto designado eleva el valor del PWM, hasta que consigue hacer que el motor se mueva lo suficiente. Una manera de experimentar esto es poner un valor muy pequeño a la constante integral (KI), si mueves el eje del motor notarás en tus dedos que hace un esfuerzo progresivo (cada vez más fuerte) para posicionarse en el mismo punto que en el que estaba. Un valor alto en KI sucede lo mismo que con KP y KD, el eje del motor oscila exageradamente.
  • Tiempo de muestreo: Esta parte es también muy importante. Es la unidad de tiempo que le dice al PIC cada cuánto tiempo ha de realizar los cálculos. No actúa sobre el control proporcional, pero sí lo hace sobre el control integral y derivativo por razones matemáticamente obvias. El tiempo de muestreo es más importante de lo que parece y en la medida que se modifica este parámetro observarás que mejora o empeora el comportamiento del motor.

En resumen:

KP = Es la que realmente hace mover al motor la mayor parte del recorrido.

KI = Incrementa, en tiempos discretos, el valor del PWM, y lo hace en proporción a la desviación.

KD = Ajusta la velocidad de llegada y eso tiene el efecto añadido de eliminar las oscilaciones que pueda provocar el control proporcional e integral.

SampleTime = Acelera o disminuye los efectos del control integral y derivativo.

El control de posición de motores con control PID no es sencillo, pero a medida que acumules experiencia sabrás qué valores has de poner en esas constantes, hasta dejarlo perfecto. Mi consejo es comenzar con KI=0 (para anular el control integral), y poner KP=1 y KD=1. Entonces haces mover al motor hacia un lado y hacia otro, y si ves que el motor se pasa, aumentas KD, hasta que finalmente deje de hacerlo. Aumentar la constante KP nos daría mucha velocidad en el posicionamiento, pero si te excedes se pondrá a oscilar. Una vez que tengas todo esto de manera aceptable nos falta un último empuje y de eso se encarga el control integral. Para la constante integral has de comenzar con valores muy pequeños y poco a poco ir elevando esa cifra hasta conseguir el ajuste perfecto.

¿Cuándo un controlador puede ser sólo P ó PI ó PD?

Según sea la naturaleza de tu proyecto te puedes encontrar que haciendo funcionar sólo la parte proporcional (P) ya te funciona estupendamente, o sólo la parte proporcional-integral (PI), o sólo la parte proporcional-derivativa (PD).

Control P: Cuando hay un amplio margen de error, existe mucha resolución (cuanto más mejor) y existe esfuerzo continuado, este controlador puede ir bien.

Control PI: Cuando la exactitud sí es crítica, en cualquier resolución y existe un esfuerzo continuado, este controlador puede ir bien.

Control PD: La exactitud aquí depende del tipo de proyecto, en cualquier resolución, con fuertes inercias, este controlador puede ir bien.

Como ya sabrás hay muchos tipos de aplicaciones de controladores PID, bien sea para posicionar un motor, controlar la velocidad, mantener una temperatura, manejar galvos, etc. Cada uno, dependiendo de la naturaleza del proyecto te puedes encontrar que no siempre es necesario un controlador PID completo.

Vídeos de demostración.

Control de precisión, con más detalle.

Se trata de ver con más detalle la nula separación entre los dos palitos de helado. Observa especialmente el punto más inferior en el que casi se tocan; al ir y volver el carro se posiciona en el exacto lugar que antes, sin oscilaciones. De estar mirando la tecla que iba a pulsar para hacer mover el carro, me desplaza con la cámara hacia la derecha y a veces se pierde de vista ese detalle, pero al darme cuenta volvía al ángulo adecuado; esto sucede a lo largo del vídeo.

Control de inercia y esfuerzo.

Se trata de "arrastrar" un peso de 500gr; son tres imanes de magnetrones de microondas domésticos. En el vídeo no llega a los 500 porque para que se pudiera ver los dígitos de la báscula, tuve que inclinarla y eso hace perder un poco de peso (en apariencia). Ten en cuenta que el motor mueve directamente el carro, no hay engranajes ni reducciones para aumentar el par. El vídeo demuestra que a pesar de llevar una pesada carga y la inercia que provoca, el controlador PID sigue siendo igual de preciso. He utilizado las mismas constantes PID en los tres vídeos de esta página.