Robot sigue líneas con Arduino
Este artículo trata sobre la construcción y programación de un robot que sigue líneas con Arduino y un sensor de infrarojos.
Construcción del Robot
Materiales
El chasis que verás en las fotos es específico, creado a medida por la empresa What's Next? para el proyecto [Robots Boost Skills]. El resto de componentes son genéricos y se pueden comprar Arduinos oficiales, What's Next Yellow o cualquier clon compatible.
Esta es la lista de materiales:
- Chasis que permita 2 ruedas con motor DC analógicos y una rueda delantera.
- 2 Motores DC analógicos con reducción y ruedas.
- Arduino Uno o equivalente.
- 1 Sensor Pololu QTR-8A o QTR-8RC
- Arduino Motor Shield o alguno que tenga el mismo Chip L298
- Baterías, entre 9V y 12V
Construcción del Chasis
En el caso del robot del ejemplo, el chasis tiene todos los elementos necesarios. Si lo tienes que construir, aquí tienes algunos consejos:
- Se recomienda cuidar el centro de gravedad de robot para que no plante rueda y tenga la adherencia necesaria. Por ejemplo, las baterías deberían estar entre las ruedas motrices y la rueda delantera.
- La distancia al suelo del sensor infrarojo es muy importante, ha de estar muy cerca, pero no rozar.
- Hay que dejar espacio para los cables y para poder modificar las conexiones sin necesidad de desmontar todo el robot.
El sensor
En nuestro caso, el sensor QTR-8[A-RC] es un conjunto de 8 emisores y sensores infrarrojos en un mismo circuito. Esto permite una gran precisión, ya que tienes 8 lecturas cada vez. Los sensores están separados entre ellos 9,52mm. Estos sensores tienen una distancia al suelo recomendada de 3mm y un máximo de 6mm para el QTR-A y 9.5mm para el QTR-8RC.
La salida de los dos sensores es diferente, (analógica o digital), por lo que es importante distinguirlos y decidir cual vamos a usar. En nuestro caso, tenemos el QRT-8RC, por lo que, a partir de aquí, todo el manual se basa en este.
Puesto que vamos a usar 5V para alimentarlo, no es necesario unir los pines de 3.3V Bypass
Podemos conectar el pin LEDON que permite indicar con HIGH, LOW o PWM el estado de los LEDs. Si los apagamos, podemos consumir menos energía cuando no está leyendo.
El QTR8-RC mide la reflectancia con el tiempo entre un estado HIGH y uno LOW del pin de I/O.
La lectura típica en el QTR8-RC es la siguiente:
- Encender los LEDs (opcional)
- Poner la linea I/O a una salida y ponerla en HIGH.
- Dejar al menos 10 μs al sensor para que arranque.
- Poner la I/O a Input
- Medir el tiempo que tarda el voltaje en caer esperando a la I/O a que vuelva a LOW.
- Apagar los LEDs (opcional)
Estos pasos se pueden hacer en varias líneas I/O al mismo tiempo. Con mucha reflectividad, el tiempo de bajada a LOW debe ser mínimo. Con poca (superficie negra) el tiempo debe mayor. Esto funciona porque para leer hay un fototransistor y un capacitor, este recibe un HIGH y en función de la intensidad de la luz, tarda más o menos en quedarse en estado de LOW. De esta manera, con el color blanco tardará unos pocos microsegundos y con el negro unos pocos milisegundos. Puesto que en aproximadamente 1ms se tienen lecturas y se puede leer de todos los sensores a la vez esto da aproximádamente la capacidad de leer hasta a 1kHz, es decir, 1000 lecturas por segundo.
Si se necesitan lecturas a mucha frecuencia, se recomienda que el sensor esté muy cerca del suelo, recomendado 3,5 mm i máximo 9 mm.
Cuanto más lejos peor lecturas y cuanta más luz ambiental peor. Si es necesario, bloquearemos la luz ambiental con cinta aislante o similar.
De esta manera, el esquema de conexiones del robot queda de la siguiente manera:
Programación del robot
Nuestro robot, al igual que el Robot esquiva obstáculos con Arduino va leyendo los sensores y, en función de esto, va modificando la velocidad de las ruedas. A diferencia de los sensores de ultrasonidos, estos sensores veremos que no nos dan los datos en crudo. Por otro lado, son muy ràpidos haciendo las lecturas y no necesitan que pase un tiempo entre lecturas. Este sensor viene con una completa biblioteca con funciones que dan los datos ya tratados. No necesitamos hacer medias ni descartar valores extremos. La biblioteca calibra los sensores según las condiciones del circuito y nos da lo que calcula a partir de los datos que obtiene.
Leyendo del sensor
Para que funcione, necesitamos la biblioteca correspondiente. En los últimos Arduino IDE es tan simple como ir al gestor de Bibliotecas y importar la QTRSensors.
La biblioteca funciona tanto para el QTR-8RC como el QTR-8A, pero cambia la manera de configurarlo:
// Crear un objeto para 8 sensores en los pines digitales 2,4,5,6,7,10 y en los analogicos A4 A5
QTRSensors qtr;
qtr.setTypeRC();
qtr.setSensorPins((const uint8_t[]){19,18,2,4,5,6,7,10}, 8);
qtr.setEmitterPin(16);
Calibrar los sensores es lo primero que debería hacer el robot. Así, en la rutina de inicialización (setup), se recomienda lanzar este código:
void setup()
{
pinMode(A3, OUTPUT); // LED para indicar que está calibrando
// Configuración de los sensores. Aquí ya usamos los que quedan libres del motor shield.
qtr.setTypeRC();
qtr.setSensorPins((const uint8_t[]){19,18,2,4,5,6,7,10}, SensorCount);
qtr.setEmitterPin(16);
delay(500);
digitalWrite(A3, HIGH); // Encendemos un led conectado a A3 para indicar que estamos calibrando
// 2.5 ms RC read timeout (default) * 10 reads per calibrate() call
// = ~25 ms per calibrate() call.
// Call calibrate() 400 times to make calibration take about 10 seconds.
for (uint16_t i = 0; i < 400; i++)
{
qtr.calibrate();
}
digitalWrite(A3, LOW); // Ya ha calibrado
// Imprimir los valores mínimos obtenidos cuando se calibraba:
Serial.begin(9600);
for (uint8_t i = 0; i < SensorCount; i++)
{
Serial.print(qtr.calibrationOn.minimum[i]);
Serial.print(' ');
}
Serial.println();
// Imprimir los valores máximos obtenidos al calibrar:
for (uint8_t i = 0; i < SensorCount; i++)
{
Serial.print(qtr.calibrationOn.maximum[i]);
Serial.print(' ');
}
Serial.println();
Serial.println();
delay(1000);
}
Para leer del sensor, se puede usar la función readCalibrated() o read(). Con readCalibrated(), los valores obtenidos serán entre 0 (blanco) y 1000 (negro). Esto lee un sensor en concreto y retorna su lectura. Estos datos son en 'crudo' y pueden servir para todo tipo de automatismos o robots, pero para seguir líneas, la biblioteca de Pololu tiene sus funciones específicas.
Para la detección de líneas, se usar la función readLineBlack() si la línea es negra. El resultado de esta función es 0 si la línea está dentro o fuera de sensor 0 y 1000*(N-1) para cada sensor. Los valores para 8 sensores pueden ser, por tanto, 0, 1000, 2000 ... 7000 dependiendo de la posición de la línea.
Hemos modificado el código de ejemplo de la biblioteca para analizar los valores que obtiene el sensor con nuestros 8 sensores:
#include <QTRSensors.h>
// This example is designed for use with eight RC QTR sensors. These
// reflectance sensors should be connected to digital pins 3 to 10. The
// sensors' emitter control pin (CTRL or LEDON) can optionally be connected to
// digital pin 2, or you can leave it disconnected and remove the call to
// setEmitterPin().
//
// The setup phase of this example calibrates the sensors for ten seconds and
// turns on the Arduino's LED (usually on pin 13) while calibration is going
// on. During this phase, you should expose each reflectance sensor to the
// lightest and darkest readings they will encounter. For example, if you are
// making a line follower, you should slide the sensors across the line during
// the calibration phase so that each sensor can get a reading of how dark the
// line is and how light the ground is. Improper calibration will result in
// poor readings.
//
// The main loop of the example reads the calibrated sensor values and uses
// them to estimate the position of a line. You can test this by taping a piece
// of 3/4" black electrical tape to a piece of white paper and sliding the
// sensor across it. It prints the sensor values to the serial monitor as
// numbers from 0 (maximum reflectance) to 1000 (minimum reflectance) followed
// by the estimated location of the line as a number from 0 to 5000. 1000 means
// the line is directly under sensor 1, 2000 means directly under sensor 2,
// etc. 0 means the line is directly under sensor 0 or was last seen by sensor
// 0 before being lost. 5000 means the line is directly under sensor 5 or was
// last seen by sensor 5 before being lost.
QTRSensors qtr;
const uint8_t SensorCount = 8;
uint16_t sensorValues[SensorCount];
void setup()
{
pinMode(A3, OUTPUT); // LED para indicar que está calibrando
// Configuración de los sensores. Aquí ya usamos los que quedan libres del motor shield.
qtr.setTypeRC();
qtr.setSensorPins((const uint8_t[]){19,18,2,4,5,6,7,10}, SensorCount);
qtr.setEmitterPin(16);
delay(500);
digitalWrite(A3, HIGH); // Encendemos un led conectado a A3 para indicar que estamos calibrando
// 2.5 ms RC read timeout (default) * 10 reads per calibrate() call
// = ~25 ms per calibrate() call.
// Call calibrate() 400 times to make calibration take about 10 seconds.
for (uint16_t i = 0; i < 400; i++)
{
qtr.calibrate();
}
digitalWrite(A3, LOW); // Ya ha calibrado
// Imprimir los valores mínimos obtenidos cuando se calibraba:
Serial.begin(9600);
for (uint8_t i = 0; i < SensorCount; i++)
{
Serial.print(qtr.calibrationOn.minimum[i]);
Serial.print(' ');
}
Serial.println();
// Imprimir los valores máximos obtenidos al calibrar:
for (uint8_t i = 0; i < SensorCount; i++)
{
Serial.print(qtr.calibrationOn.maximum[i]);
Serial.print(' ');
}
Serial.println();
Serial.println();
delay(1000);
}
void loop()
{
// Lee los sensores calibrados para obtener una posición de la línea de 0 a 7000
// En este caso, la línea es negra, para blanca usamos readLineWhite()
uint16_t position = qtr.readLineBlack(sensorValues);
// Pintar los valores de los sensores de 0 a 1000 donde 1000 significa negro y 0 blanco
for (uint8_t i = 0; i < SensorCount; i++)
{
Serial.print(sensorValues[i]);
Serial.print('\t');
}
// Al final de la línea la posición:
Serial.println(position);
delay(250);
}
El resultado puede dar algo como esto:
292 292 244 196 244 292 244 292 <-- Los valores mínimos de cada sensor 772 2500 2500 2500 2500 2500 2500 2500 <-- Los valores máximos de cada sensor 16 3 1 1 1 1000 521 3 5342 <-- Donde pone 1000 está la línea 0 0 0 0 0 23 1000 219 6179 <-- El 6179 indica que está en el sensor 7 0 0 0 0 0 23 1000 411 6291 8 1 0 0 0 170 1000 50 5854 0 0 0 0 19 1000 235 0 5190 8 1 0 20 1000 992 23 1 4497 108 0 19 1000 544 0 19 0 3133 <-- Observa el el sensor 4 tiene 1000 y la posición indica 3133 (N-1 sensor) 0 21 1000 342 0 0 0 0 2254 8 1 590 1000 23 1 0 1 2628 100 0 0 503 1000 21 19 0 3436 108 0 0 19 1000 1000 19 0 4269 0 0 0 19 0 1000 1000 0 5500 16 3 0 0 0 3 1000 1000 6500 0 0 0 0 0 0 1000 871 6465 0 0 0 0 0 1000 659 0 5397 0 0 0 204 1000 50 21 0 3830 0 0 634 1000 19 0 19 0 2611 8 534 1000 22 1 1 1 1 1651 0 894 1000 19 0 0 0 0 1527 0 483 1000 19 0 0 0 0 1674 8 1 1000 1000 0 1 0 1 2500 100 0 234 1000 42 0 17 0 2599
Ejemplo del manual oficial del sensor para un sigue líneas simple con 3 sensores:
PID
El anterior ejemplo ya funciona razonablemente bien en una línea. No obstante, si queremos aplicar más velocidad, el tiempo de reacción y el error acumulado hace que se pueda salir de la línea. Por ello necesitamos una manera de corregir el rumbo antes de que sea demasiado tarde y suavizar el comportamiento del robot.
La lectura de qtr.readLineBlack(sensorValues); Retorna un número. En caso de utilizar los 8 sensores, el número va de 0 a 7000, siendo cada 1000*(N-1) para sensor. De esta manera, si está en el 3456, por ejemplo, quiere decir que está en el sensor 4, aunque parte de la línea está en el sensor 5. Así, lo ideal es que esté más o menos en esa posición, ya que el 4 y 5 son los sensores centrales. Por eso, en el ejemplo anterior calculamos el error como position - 3500. O en el ejemplo de 3 sensores era -1000.
Nuestra estrategia, al contrario que en los ejemplos anteriores, es fijar una velocidad base y calcular una corrección a esa velocidad en función del cálculo que se haga con el error obtenido de los sensores:
velocidad_izquierda = base_izquierda + corrección velocidad_derecha = base_derecha - corrección
En realidad las dos velocidades base deberían ser las mismas, pero se usan dos variables por si un motor es ligeramente más rápido que otro.
Control Proporcional
A continuación, vamos a utilizar la palabra "control", ya que estos conceptos son ampliamente utilizados en la robótica y el control industrial. Todo automatismo que intenta mantener un control (temperatura, nivel de agua, posición...) en función de lecturas de sensores, ha de tener una estrategia para hacerlo de la forma más inteligente posible. En el ejemplo básico inicial, el robot va dando bandazos de un lado a otro corrigiendo el rumbo sin parar. La idea es que corrija el rumbo antes de tener que hacerlo de forma tan brusca.
En el control proporcional, corregimos más o menos en función de la cantidad de error. Para ello, podemos establecer un coeficiente proporcional (Kp)
error = posición - 3500 corrección = Kp * error velocidad_izquierda = base_izquierda + corrección velocidad_derecha = base_derecha - corrección
Como se puede observar, a mayor error, mayor corrección. También se puede observar que no hemos dado un valor determinado a Kp. Este valor se consigue por prueba y error, de la misma manera que la velocidad base de las ruedas. A mayor velocidad base, más rápido va el robot y más posible es que se salga. A mayor Kp, mayor corrección y, por tanto, una respuesta más rápida, pero a cambio de giros más bruscos.
Control Integral
El control Proporcional ya es una mejora, pero en realidad sólo tiene en cuenta el error instantáneo y no sabe si se está acumulando error o es algo puntual. El control Integral va sumando los errores del pasado y permite averiguar si la dirección del robot está empeorando su posición. Es más, si el robot pierde la línea, la integral "recordará" por donde veía el error y podrá retornar el robot a la línea.
En el algoritmo anterior, añadimos la variable integral, (que inicialmente vale 0) y un nuevo coeficiente Ki
error = posición - 3500 integral = integral + error corrección = Kp * error + Ki * integral velocidad_izquierda = base_izquierda + corrección velocidad_derecha = base_derecha - corrección
Al igual que el otro coeficiente, el Ki también hay que ajustarlo por prueba/error.
Control Diferencial
Esta tercera estrategia sirve para detectar si el error está aumentando. Lo que hace es comparar el error actual con el pasado. Esto permite solucionar cambios bruscos de dirección.
error = posición - 3500 integral = integral + error diferencial = error - error_anterior corrección = Kp * error + Ki * integral + Kd * diferencial velocidad_izquierda = base_izquierda + corrección velocidad_derecha = base_derecha - corrección
error_anterior = error
El Kd también se ajusta por prueba y error.
Todos estos métodos pueden modificar mucho la velocidad o demasiado poco. Por ello, se recomienda hacer lo siguiente:
- Probar una velocidad base aceptable.
- Poner todos los K a 0 e ir probando a aumentar el Kp poco a poco hasta que no salga de la línea. Cuanto más lo subamos, más oscilará, por lo que hay que dejarlo justo cuando empieza a oscilar.
- Ir aumentando Ki hasta que empiece a oscilar más y dejarlo por debajo de esos valores.
- Ir aumentando Kd hasta que funcione bien.
- Aumentar la velocidad y los demás ajustes hasta que esté a nuestro gusto, (que nunca ocurrirá)
Mejoras
La primera mejora tiene que ver con un problema con la integral. Si pensamos en lo que puede pasar, si el robot está demasiado tiempo fuera de su posición, acumula mucho error. Este, en teoría, es neutralizado con el error de otro signo en el otro lado, pero si es demasiado, puede que no se neutralice y se salga por el otro lado. Esto se llama overshooting.
Enlaces
- (Obsoleto) Manual de la biblioteca del sensor Referencia de la biblioteca
- (Versión 4.) Manual Referencia
- Tutorial en castellano
PID:
Ver también: