Derivada del error y control proporcional derivativo.

En la actualización anterior se leían los sensores para localizar la posición de la línea, y dependiendo de lo lejos que la línea estuviera del centro de la placa de sensores se asignaba un error. Ese error es proporcional a la distancia de la línea al centro y lo usaremos para establecer un control proporcional, cuanto más lejos más gira el robot para centrarse en la línea. El problema con este controlador proporcional es que nunca conseguiremos que el robot se centre sobre la línea, siempre va a estar oscilando sobre ésta como se podía apreciar en el video, el robot llega, se pasa y corrige, y así indefinidamente.

Para conseguir que el robot se centre sobre la línea y no oscile tenemos que implementar un control derivativo (además del proporcional), para ello se ha de conocer la derivada del error, es decir como cambia el error respecto al tiempo, que es lo mismo que la velocidad con que la línea se desplaza entre los sensores. Con un control derivativo nos podemos anticipar al error futuro, observando como varía éste y anticipando la respuesta antes de que se produzca obteniendo su derivada.

Lo primero para poder medir la velocidad con la que cambia el error es tener la capacidad de medir el tiempo que ha pasado entre dos medidas de error ((velocidad de cambio = (error_anterior - error)/tiempo), para ello tenemos que usar un timer. Se va a crear una interrupción principal para que el programa que controla el robot se ejecute en intervalos de tiempo fijos, de esta forma entre una medida y otra de la placa de sensores conoceremos el tiempo que ha pasado que siempre es el mismo, es el tiempo de la interrupción que configuremos en el timer, por lo que la derivada del error pasa a ser error_anterior -error.

Configurar un timer con una interrupción que llame a una función cada x tiempo son unas pocas líneas de código:

void inicializar_timer1(void) //Configura el timer y la interrupción.
{
    OCR1A= 0x0C35;
    TCCR1B |=((1<<WGM12)|(1<<CS11)|(1<<CS10));    //Los bits que no se tocan a 0 por defecto
    TIMSK1 |= (1<<OCIE1A);
    sei();
}

Si añadimos esto a los programas anteriores se configura el timer1 de la Baby Orangutan, para que cada 10 ms se active una interrupción que llame a una función para que le atienda . Cuando se produce la interrupción el micro ejecuta el código que le indiquemos, más información sobre las interrupciones en los AVR y WinAVR aquí. Por lo que para probar que está bien configurado el timer1 de la baby Orangutan escribimos el siguiente código para que se ejecute cuando se active la interrupción:

ISR(TIMER1_COMPA_vect)
{
    PORTD |=(1<<LEDP);
    _delay_ms(5);
    PORTD &= ~(1<<LEDP);
    TIFR1 |= (1<<OCF1A);
}


Para trabajar con las interrupciones del WinAVR debemos poner el siguiente include al principio del programa: #include <avr/interrupt.h>.

Cada vez que el timer alcance la cuenta que le hemos indicado en el registro OCR1A (10 ms) el micro enciende el led rojo de la placa de usuario, cuenta 5 ms y lo apaga, conectando el osciloscopio al led podemos comprobar que el timer está correctamente configurado. Código completo.


Se puede ver como cada 10 ms se ejecuta lo que hay dentro de ISR(TIMER1_COMPA_vect) y funciona como se había esperado. Si en lugar de encender el led, leemos la placa de sensores para saber el error, esta lectura se realizará cada 10 ms, por lo que restando el error medido al anterior podemos conocer la velocidad de cambio de éste, ya que el tiempo es constante entre todas las medidas. Si estamos leyendo sensores en analógico o usamos la placa de sensores de condensador este cálculo es directo, (error - error_anterior) nos dará la derivada del error ya que el error se puede considerar continuo, tenemos la medida real de la distancia del sensor a la línea y el tiempo de muestreo es constante.

Si usamos sensores digitales no lo podemos hacer de una manera tan directa, ya que sólo sabemos si el sensor está a 1 ó a 0, el error lo tenemos en pasos equivalentes a la distancia entre sensores, por lo que no tenemos una derivada si no que buscamos la velocidad media de cambio del error entre dos sensores. Por lo que una forma de tener una medida precisa de la velocidad del error es medir el tiempo que tardan los sensores en cambiar, cuanto tiempo transcurre desde que se activa un sensor hasta que se activa otro. Conociendo la distancia entre sensores que es fija e igual para todos y el tiempo podemos conocer la velocidad media de desplazamiento entre sensores de la línea bajo la placa de sensores.

Por lo que una función para determinar la derivada/velocidad media del error (distancia de la línea al centro de la placa de sensores) podría ser la siguiente:

int obtener_errord(void)
{
    int error = 0;
    static int error_old = 0;
    static int errord=0;
    static int errord_old = 0;
    static int tic = 1;
    static int tic_old = 1;

    int diferencia = 0;

    if(((PINB & 0x10) != 0) && ((PINB & 0x20) != 0))
        error=0;

    else if((PINB & 0x20) != 0) //D0 PB5 +1
        error = 1;

    else if((PINB & 0x10) != 0) //I0 PB4 -1
        error = -1;

    else if((PIND & 0x04) != 0) //D1 PD2 +3
        error = 3;

    else if((PINC & 0x08) != 0) //I1 PC3 -3
        error = -3;

    else if((PIND & 0x10) != 0) //D2 PD4 +5
        error = 5;

    else if((PINC & 0x04) != 0) //I2 PC2 -5
        error = -5;

    else if((PIND & 0x80) != 0) //D3 PD7 +7
        error = 7;

    else if((PINC & 0x02) != 0) //I3 PC1 -7
        error = -7;

    else
        {
            if (error_old < 0)
                error = -9;
            else if(error_old > 0)
                error = 9;
        }

    //Cálculo de la velocidad media del error.
    if (error == error_old)
    {
        tic = tic + 1;
        if(tic > 30000)
            tic = 30000;
        if(tic > tic_old)
            errord = errord_old/tic;
    }
    else
    {
        diferencia = error - error_old;
        errord = Kd*(diferencia)/tic; //error medio
        errord_old = errord;
        tic_old=tic;
        tic=1;
    }

    error_old = error;
    return(errord);
}

Se comienza leyendo los sensores para determinar la posición de la línea, no se lee el error medio de todos los sensores activados, ya que ésto debido al grosor de la línea y al espacio entre sensores puede dar lugar a errores, es decir, puede pasar de un error a otro (distancia) con para distinto desplazamiento, lo que sería una medida errónea de la velocidad. Leyendo los sensores de uno en uno del centro hacía fuera y considerando que la línea está en el sensor que se active antes evitamos el problema anterior.

El cálculo de la velocidad media de cambio del error se hace aquí:

    //Cálculo de la velocidad media del error.
    if (error == error_old)
    {
        tic = tic + 1;
        if(tic > 30000)
            tic = 30000;
        if(tic > tic_old)
            errord = errord_old/tic;
    }
    else
    {
        diferencia = error - error_old;
        errord = Kd*(diferencia)/tic; //error medio
        errord_old = errord;
        tic_old=tic;
        tic=1;
    }
    error_old = error;
    return(errord);

Lo primero decir que a esta función la vamos a llamar a través de una interrupción por lo que tic++ representa incrementos fijos de tiempo. Una vez que hemos leído el error, la distancia de la línea a la placa de sensores, se compara con el error anterior, si es igual incrementamos la cuenta de tiempo y se termina la función. Si no es igual al anterior se compara de cuanto ha sido el cambio, es decir cuantos sensores se ha desplazado la línea, diferencia = error - error_old; y se cálcula la velocidad errord = Kd*diferencia/tic , es como calcular la velocidad, nada más que al espacio lo llamamos error (distancia de la línea a la placa de sensores), Kd es una constante que se usa para ajustar el regulador y tic lleva la cuenta desde la última vez que se produjo un cambio, es decir, cuanto tiempo ha tardado la línea de pasar de un sensor a otro. Calculado el error la función lo devuelve.

Cuando no cambia el error está función puede devolver dos cosas, el errord (valor devuelto por la función) calculado cuando cambio, y otro errord menor. Para ello cuando cambia el error y se cálcula la velocidad de cambio también se guarda el tiempo que tardó en cambiar (aunque va implícito en el anterior), esa velocidad se deja fija en el siguiente intervalo y se resetea la cuenta de tics. Si la cuenta de tics excede la cuenta de tics del cálculo anteior significa que ha pasado más tiempo y el error aún no ha cambiado, por lo que se empieza a  calcular la velocidad de cambio el error según va pasando más tiempo.

Aunque puede parece un poco lío es calcular el espacio y la velocidad, velocidad = espacio/tiempo, nada más que a espacio lo llamamo error. Conociendo la velocidad podemos anticipar la respuesta del robot y evitar las oscilaciones.

Un ejemplo, tenemos los 8 sensores:
I3 -7
I2 -5
I1 -3
I0  -1
D0 1
D1 3
D2 5
D3 7

Por ejemplo leemos el sensor D2 y un tiempo después leemos el sensor D3, lo que significa que la línea se está desplazando de izquierda a derecha. Si aplicamos la fórmula
errord = Kd*(error - error_old)/tic; nos queda errord = Kd*(7-5)/t, un errord positivo, si este errord es muy grande le dice al robot que gire más hacía la izquierda que en el próximo instante de tiempo la línea va a estar más alaejada del centro, por lo que nos anticipamos a lo que va a suceder.

Ahora por ejemplo leemos D0 y el anterior sensor leído fue D1, errord = Kd*(1 - 3)/t, da un errord negativo que indica al robor que la línea se está desplazando hacía la izquiera, si este errord es lo suficientemente grande el robot empezará a "enderezar" la dirección antes de llegar al centro de la placa de sensores y pasarnos, metiendonos en la zona de los sensores I y empezando a oscilar sobre la línea, como ocurre sólo con el regulador proporcional cuando no miramos la derivada para anticiparnos a lo que va a suceder.

La función de error proporcional es la misma que la anterior:

int obtener_errorp(void)
{
    char errorp=0;
    static char ultimo_errorp=0;
    char contador_sensor=0;

    if(((PINB & 0x10) != 0) && ((PINB & 0x20) != 0))
    {
        errorp=0;
        return(0);
     }

    if((PINC & 0x02) != 0) //I3 PC1 -7
    {
        errorp = errorp - 0x07;
        contador_sensor++;
    }

    if((PINC & 0x04) != 0) //I2 PC2 -5
    {
        errorp = errorp - 0x05;
        contador_sensor++;
    }

    if((PINC & 0x08) != 0) //I1 PC3 -3
    {
        errorp = errorp - 0x03;
        contador_sensor++;
    }

    if((PINB & 0x10) != 0) //I0 PB4 -1
    {
        errorp = errorp - 0x01;
        contador_sensor++;
    }

    if((PINB & 0x20) != 0) //D0 PB5 +1
    {
        errorp = errorp + 0x01;
        contador_sensor++;
    }

    if((PIND & 0x04) != 0) //D1 PD2 +3
    {
        errorp = errorp + 0x03;
        contador_sensor++;
    }

    if((PIND & 0x10) != 0) //D2 PD4 +5
    {
        errorp = errorp + 0x05;
        contador_sensor++;
    }

    if((PIND & 0x80) != 0) //D3 PD7 +7
    {
        errorp = errorp + 0x07;
        contador_sensor++;
    }

    if(contador_sensor != 0)
    {
        errorp = errorp / contador_sensor;
        ultimo_errorp = errorp;
        return(Kp * (int)errorp);
    }
    else
    {
        if(ultimo_errorp < 0)
            errorp = -0x09;
        else
            errorp = 0x09;

        ultimo_errorp = errorp;
        return((int)errorp * Kp);
    }       
}

Devuelve el error de la línea al centro de la placa de sensores que multiplicamos por una constante para ajustar el regulador.

El regulador y control de los motores lo implementamos en la ISR del timer1, para que tenga lugar con un tiempo fijo, necesario para poder hacer la derivada o velocidad media.

ISR(TIMER1_COMPA_vect)
{
    PORTD |=(1<<LEDP);

    int errort=0;
    int proporcional = obtener_errorp();
    int derivativo = obtener_errord();

    errort = proporcional + derivativo;

    if(errort > velocidad)
        errort = velocidad;
    else if(errort < - velocidad)
        errort = - velocidad;
   
    if(errort>0)
    {
        M1_forward(velocidad - errort);     //Motor derecho.
        M2_forward(velocidad);              //Motor izquierdo.
        PORTB |= (1<<LEDV);
        PORTD &= ~(1<<LEDR);
    }
    else if(errort<0)
    {
        M1_forward(velocidad);              //Motor derecho. 
        M2_forward(velocidad + errort);     //Motor izquierdo.
        PORTD |= (1<<LEDR);
        PORTB &= ~(1<<LEDV);
    }

    else
    {
        M1_forward(velocidad);      
        M2_forward(velocidad);
        PORTB &= ~(1<<LEDV);
        PORTD &= ~(1<<LEDR);
    }

    PORTD &= ~(1<<LEDP);
    TIFR1 |= (1<<OCF1A);
}

Comenzamos llamando a las funciones anteriores que nos devuelve el error proporcional y la velocidad media del error (espacio y velocidad), el error total es la suma de los dos, el valor de los errores obtenidos por las funciones ya van multiplicados por dos constantes, Kp y Kd, que utilizamos para ajustar el comportamiento del error, aumentandolas o disminuyendolas hacemos que cada uno de los errores tenga más o menos peso en el resutado final. Puede parecer un poco confuso pero una vez visto la idea es muy sencilla.

Una vez que tenemos el error total pasamos a ajustar la velocidad de cada motor, sí éste es mayor que cero giramos a la izquierda y si  no a la derecha, cuando el motor de un lado va más deprisa que el del otro se enciende su led, cuando ambos van a la misma velocidad los dos leds se apagan.

Al principio de la ISR encendemos el led de la placa y al final se apaga, de esta forma tenemos una manera rápida de comprobar el tiempo de ejecución del código sin tener que usar un osciloscopio, si el led brilla poco nos sobre tiempo (en este programa casi no brilla)m y si brilla mucho mala señal. Ésto es importante porque estamos haciendo divisiones y para estos micros eso puede llevar mucho tiempo, por lo que hay que comprobar que para el tiempo que fijemos el código que escribimos tiene tiempo de ejecutarse.

El programa completo del regulador del robot para verlo funcionar sobre la línea está aquí.

Al principio del programa encontramos lo siguiente:
/*********** Ajuste comportamiento robot *********/
//Constantes Regulador PD.
int Kp=10;
int Kd=65;
int velocidad = 100; //Máxima 255.
/*************************************************/

Sirven para ajustar el comportamiento del robot, para ello se establece una velocidad media y se va incrementando la Kp para que el robot siga la línea sin salirse, una vez hecho ésto se va incrementando la Kd hasta que el robot siga la línea sin balancear. Llegados a este punto aumentamos la velocidad y repetimos el procesos desde los valores de Kp y Kd que teníamos.

Para ver la importancia de estas constantes y como realmente la velocidad media del error vale para anticiparse y que el robot no oscile, un video:


Comenzamos con el valor de Kd a cero, es decir, sólo tenemos error proporcional y pasamos de la derivada y como se ve en el video el robot no pasa de oscilar. Los leds que indican que un motor gira más rápido que el otro sólo se encienden cuando la línea está en el lado opuesto, por lo que es muy difícil (por no decir imposible) que se pueda seguir sin entrar en oscilación. Luego probamos a ir aumentando la Kd y le damos el valor 65 para el mismo valor de Kp y velocidad anterior, y como se puede ver la respuesta del robot es muy distinta, hemos dejado de oscilar, también se puede apreciar como se encienden y se apagan los leds de los motores cuando la línea está a un mismo lado de la placa de sensores, lo que significa que el error derivativo es mayor y nos anticipamos a lo que viene.

Para ver más o menos la velocidad del robot pongo otro video, las constantes no están ajustadas, sólo se ha probado dos o tres valores para ver lo que salía.


El resultado es bueno, en el segundo corte que es el que más velocidad tiene de los dos, la velocidad está en 220 de 255, y creo que ya pasamos el metro por segundo. El regulador no está ajustado, he puesto los valores que me parecían 2 ó 3 veces por lo que todavía se puede mejorar la respuesta, se observa como el robot oscila y se para un poco. No sé cuál es la medida de la pista para calcular la velocidad, pero después del primer corte cuento que el robot da 4 vueltas en  algo menos de 30 segundos, es decir 7.3 s por vuelta aproximadamente.

Y comparo con este otro video en la misma pista:


Son los MiniZ del año pasado del Cosmobot y al más rápido de los dos le cuento las 4 primeras vueltas a 8.2 s por vuelta, y si no recuerdo mal el año pasado quedó cuarto en la clasificación, no recuerdo la velocidad. Por lo que mala noticia para el MiniZ 2009, un robot de bajo coste y sin ajustar ya le ha superado =(

Bueno lo básico para tener el robot funcionando ya se ha hecho, falta ajustar el robot y ponerle la velocidad al máximo para ver el resultado final, cosa que haré en los próximos días/semanas en una pista grande, sin baches.., y que tengo medida. Además de ajustar el regulador queda hacer el código para los interruptores e implementar el ADC para leer la batería, pero es menos importante, la parte que iba a determinar si el robot valía para el metro por segundo o no, era implementar el  este regulador. Los sensores se leen en digital y si no hay luz ambiente (aunque ésta también se carga a los analógicos si los satura) pues creo que no habrá problemas, pero habría que hacer pruebas con distintas iluminaciones.

El código sólo está escrito y visto que funciona, no lo he repasado por lo que podrá tener algún error y se puede mejorar, lo he hecho según lo que he ido pensando y parece que funciona así que lo dejo así.

En principio el primer resultado del robot parece bastante bueno, por lo que para ser el primer velocista diferencial que hago y con las limitaciones de las piezas elegidas pues estoy contento. Queda ajustar el regulador pero no sé cuando lo haré porque tengo que dedicar tiempo a otros proyectos. Lo que queda demostrasdo es que un robot velocista de bajo coste para presentarse a un concurso y no quedar de los últimos se puede hacer en dos o tres de fines de semana y no es tan difícil, este ha sido mi primer velocista diferencial por lo que he tenido que pensar casi todo desde cero, y me he pasado más tiempo escribiendo que haciendo...

Para cualquier duda sobre todo ésto, idea, etc... : foro.