Mando RC y receptor. Programación en Arduino

Emisora RC y programación en Arduino


Índice:

  1. Conceptos generales sobre drones
  2. Material necesario y montaje de los componentes hardware
  3. → Mando RC y receptor. Programación en Arduino
  4. MPU6050 y su programación en Arduino
  5. Batería LiPo
  6. Control de estabilidad y PID
  7. Motores, ESC y su programación en Arduino
  8. Calibración de hélices y motores
  9. Software completo y esquema detallado
  10. Probando el Software completo antes de volar
  11. Como leer variables de Arduino en Matlab


Mando radio control (RC)

Una de las maneras más fáciles y baratas de comunicarse con nuestro futuro drone es utilizar un mando RC y un receptor. Son fáciles de utilizar y se pueden conseguir por 30€ en Ebay en sus versiones más básicas (4 canales). Como todo en la electrónica, el mundo de los mandos radio control es muy amplio, pero para nuestro primer drone es suficiente. Dispondremos de un canal para controlar el throttle, otro para el pitch, uno más para el roll y el último para el yaw. He decidido asociar los sticks con las siguientes funciones o movimientos del drone. Recomiendo seguir el mismo criterio ya que es el mas extendido por ser el mas intuitivo a la hora de controlar el drone. Aquí una foto de mi mando:

Comprar mando RC en Amazon

A pesar de ser algo más caro, este otro mando radio control ofrece unas prestaciones bastante superiores al mencionado arriba, y puede sernos útil el día de mañana si queremos añadir alguna nueva funcionalidad al drone.

El funcionamiento de un mando RC es muy simple. El movimiento de los sticks del mando es procesado y enviado por ondas de radio a nuestro receptor, que irá situado en el frame del drone. Esta información se compone de señales PWM de 50Hz que varían en función de la posición de cada stick. Si accionamos al mínimo alguna de las palanca del mando, el ancho de pulso que recibamos en el receptor será de 1ms, y si la accionamos al máximo, el ancho del pulso será de 2ms (estos valores pueden ser diferentes en cada mando). Si dejamos la palanca en el punto central obtendremos pulsos de 1.5ms aproximadamente, siempre manteniendo una frecuencia de 50Hz. Esta información será enviada a la placa Arduino y se utilizara como referencia para mover el drone.pulso mando RC

Como vemos en la imagen anterior, el mando no da exactamente pulsos de 1ms con el stick al mínimo, ni pulsos de exactamente 2ms subiéndolo a máximo. Estos pequeños errores de precisión del mando los corregiremos mas adelante al final de esta entrada.

Conexionado mando-Arduino

El receptor ira situado en el frame y es necesario alimentarlo y cablearlo de forma adecuada. Alimentaremos el receptor a +5V en cualquiera de los canales centrales, por ejemplo con la tensión de nuestra placa Arduino. Si hemos configurado bien la unión mando-receptor, al encender el mando debería encenderse un led rojo (en mi caso) que indica que funciona correctamente.

El conexionado necesario para ejecutar el código que os dejo al final de este artículo se muestra a continuación. Simplemente hay que alimentar el receptor en cualquiera de los canales centrales y cablear las cuatro señales a las correspondientes entrada digitales, en mi caso:

La funcionalidad de cada canal y el pin correspondiente se muestran a continuación: 

  • Pitch        ⇒  CH2  ⇒  IN 12
  • Throttle  ⇒  CH3  ⇒  IN 8
  • Roll         ⇒  CH1  ⇒  IN 9
  • Yaw        ⇒  CH4  ⇒  IN 7

Antes de continuar me gustaría analizar un poco más en detalle el funcionamiento de mi receptor y la forma en la que genera los pulsos. La siguiente imagen muestra una captura sacada con osciloscopio donde visualizo las señales de pitch, roll y yaw (no he metido el canal de throttle por que mi osciloscopio solo tiene 3 canales). De esta imagen he sacado los siguientes datos:

  • El receptor genera pulsos de 3.3V.
  • Los pulsos se generan de forma secuencial, hasta que no termina un pulso no empieza el siguiente. Nunca habrá más de un canal en estado HIGH cada vez.

Algunos mandos de radiocontrol más avanzados pueden enviar la información de todos los canales utilizando una sola señal (PPM). Tenéis todo lo necesario para leer señales PPM con Arduino en esta entrada.

Código Arduino (Sketch)

Todo el software que utilizaremos a lo largo de esta entrada está subido a GitHub:

Descargar ficheros en GitHub

La forma más sencilla de leer este tipo de pulsos es utilizar la función PulseIn() de Arduino. Lamentablemente esta función tan fácil de utilizar no sirve para esta aplicación (aunque sí para muchas otras). Pero, ¿Por qué? Como todos a estas alturas sabemos, el código de Arduino se ejecuta de manera secuencial, línea tras línea y de arriba abajo. Arduino no ejecuta una línea si no ha terminado de ejecutar la anterior. Cuando llegue a la línea PulseIn(), Arduino se quedará esperando en ella el tiempo que haga falta hasta recibir un pulso y medirlo. Como sabemos, el mando solo nos enviará un pulso por canal cada 20ms, por lo que esta función PulseIn() estará esperando a recibir el pulso (y sin hacer nada mas) durante 20ms. Teniendo en cuenta que contamos con 4 canales, la demora se puede prolongar hasta 80ms. Este intervalo de tiempo que a priori puede parecer muy corto, para nuestros drone es toda una eternidad y hará que nunca sea estable (esto lo veremos en otro apartado más adelante).

Localización del receptor en el frame

¿Cómo podemos leer las señales recibidas desde el mando de una forma eficiente? Utilizando interrupciones hardware, una por canal. El funcionamiento de una interrupción hardware es muy simple: ante un flanco positivo (cambio de estado de LOW a HIGH) o negativo (cambio de estado de HIGH a LOW) en alguna de las interrupciones (pins), Arduino detendrá la ejecución normal del programa y ejecutara la parte del código que hayamos asociado a esa interrupción en concreto. Una vez ejecutado esta parte de código, Arduino regresará al programa principal y seguirá ejecutándolo donde lo dejó. A continuación os explicaré como leer los canales de vuestro mando RC de forma que la lectura solo lleve unos poco microsegundos (100 veces menos que con la función PulseIn). Para poder poner esto en marcha necesitamos 4 interrupciones hardware, una por cada canal que queremos leer. La paca Arduino Nano cuenta únicamente con dos interrupciones hardware, pero existen librerías como EnableInterrupt.h que permiten convertir casi cualquier pin analógico o digital en interrupción. Esta librería es extremadamente simple de usar como veremos mas adelante.

Al final de este artículo podréis encontrar el sketch completo que utilizo para medir los pulsos con interrupciones hardware. El sketch es muy simple como veréis a continuación. Cuando se detecta un pulso (flanco positivo o negativo) en alguna de las cuatro interrupciones, se ejecuta la parte del código asociado a esa interrupción. El primer paso es identificar si hemos detectado un flanco positivo o uno negativo, para lo que bastará con leer el estado de la propia entrada digital. Si está en estado HIGH significará que hemos detectado un flanco positivo (comienzo del pulso) y si está en LOW, uno negativo (fin del pulso). Cuando se detecta un flanco positivo se registra el tiempo (el instante) en el que se ha dado la interrupción, para lo que utilizaremos la función micros() y lo guardamos en una variable. Una vez hecho esto Arduino sale de la interrupción y sigue ejecutando el código principal. Un tiempo después se detecta un flanco negativo (cambio de estado de HIGH a LOW), por lo que se vuelve a ejecutar la interrupción. Volvemos a leer el estado de la entrada que esta vez estará en estado LOW. Finalmente detenemos el correspondiente contador y calculamos el tiempo que ha transcurrido desde que lo hemos activado, que será el tiempo que ha durado el pulso. A continuación os dejo el código completo para que podáis leer los canales de vuestro mando:

La diferencia de mi sketch con la función PulseIn() es que mi programa no está esperando (y sin hacer nada más) hasta recibir un pulso para seguir ejecutando el loop principal. El programa solo deja de ejecutarse mediante interrupciones para poner en marcha un contador y después detenerlo. Todo el tiempo restante continúa funcionando y ejecutando el control principal. Gracias a esto conseguimos leer los 4 canales del mando RC prácticamente sin consumir nada de tiempo. Os dejo el código completo para que podáis leer los canales de vuestro mando. El conexionado necesario es el mismo que hemos visto mas arriba:

// Declaración de pines
#define pin_INT_Throttle 8 // Pin Throttle del mando RC 
#define pin_INT_Yaw 7      // Pin Yaw del mando RC 
#define pin_INT_Pitch 12   // Pin Pitch del mando RC
#define pin_INT_Roll 9     // Pin Roll del mando RC

#include <EnableInterrupt.h>
long loop_timer, tiempo_ejecucion;

// INTERRUPCIÓN MANDO RC --> THROTTLE
volatile long Throttle_HIGH_us;
volatile int RC_Throttle_raw;
void INT_Throttle() {
  if (digitalRead(pin_INT_Throttle) == HIGH) Throttle_HIGH_us = micros();
  if (digitalRead(pin_INT_Throttle) == LOW)  RC_Throttle_raw  = micros() - Throttle_HIGH_us;
}

// INTERRUPCIÓN MANDO RC --> PITCH
volatile long Pitch_HIGH_us;
volatile int RC_Pitch_raw;
void INT_Pitch() {
  if (digitalRead(pin_INT_Pitch) == HIGH) Pitch_HIGH_us = micros();
  if (digitalRead(pin_INT_Pitch) == LOW)  RC_Pitch_raw  = micros() - Pitch_HIGH_us;
}

// INTERRUPCIÓN MANDO RC --> ROLL
volatile long Roll_HIGH_us;
volatile int RC_Roll_raw;
void INT_Roll() {
  if (digitalRead(pin_INT_Roll) == HIGH) Roll_HIGH_us = micros();
  if (digitalRead(pin_INT_Roll) == LOW)  RC_Roll_raw  = micros() - Roll_HIGH_us;
}

// INTERRUPCIÓN MANDO RC --> YAW
volatile long Yaw_HIGH_us;
volatile int RC_Yaw_raw;
void INT_Yaw() {
  if (digitalRead(pin_INT_Yaw) == HIGH) Yaw_HIGH_us = micros();
  if (digitalRead(pin_INT_Yaw) == LOW)  RC_Yaw_raw  = micros() - Yaw_HIGH_us;
}

void setup() {
  // Declaración de interrupciones
  pinMode(pin_INT_Yaw, INPUT_PULLUP);                   // YAW
  enableInterrupt(pin_INT_Yaw, INT_Yaw, CHANGE);
  pinMode(pin_INT_Throttle, INPUT_PULLUP);              // POTENCIA
  enableInterrupt(pin_INT_Throttle, INT_Throttle, CHANGE);
  pinMode(pin_INT_Pitch, INPUT_PULLUP);                 // PITCH
  enableInterrupt(pin_INT_Pitch, INT_Pitch, CHANGE);
  pinMode(pin_INT_Roll, INPUT_PULLUP);                  // ROLL
  enableInterrupt(pin_INT_Roll, INT_Roll, CHANGE);

  // Serial.begin()
  Serial.begin(115200);
}

void loop() {
  // Nuevo ciclo
  while (micros() - loop_timer < 10000);
  tiempo_ejecucion = (micros() - loop_timer) / 1000;
  loop_timer = micros();

  // Monitor Serie
  Serial.print(RC_Throttle_raw);
  Serial.print("\t");
  Serial.print(RC_Pitch_raw);
  Serial.print("\t");
  Serial.print(RC_Roll_raw);
  Serial.print("\t");
  Serial.println(RC_Yaw_raw);
}

Para poder seguir es importante que hayas conseguido poner en marcha esta parte de forma adecuada. Os tenéis que asegurar que cada canal del mando corresponde con los movimientos del drone que hemos asignado. Moved la palanca asociada al Pitch y comprobad que es la variable RC_Pitch_raw la que cambia y haced lo mismo con los demás canales. En caso de no estén bien asignados, podéis modificar el nombre de las variables (recomendado) o modificar el cableado, pero asegurad que moviendo el stick asociado al movimiento Pitch es la variable RC_Pitch_raw la que cambia y no ninguna otra.

Como hemos visto arriba, el receptor no da exactamente pulsos de 1ms con el stick al mínimo, ni pulsos de exactamente 2ms subiéndolo a máximo. Estos pequeños errores de precisión del mando han de ser corregidos, especialmente para el canal destinado al throttle. Para ello utilizaremos el conjunto MandoRC_MaxMin de GitHub, que simplemente lee los cuatro canales y visualiza los valores máximo y mínimos mientras movemos los stick de un lado a otro. Anotaremos la duración de los cuatro pulsos en los extremos de cada canal, moviendo los cuatro stick del mando arriba/abajo e izquierda/derecha hasta conseguir los valores máximos y mínimos de pulso.

Estos valores después serán utilizados para escalar las lecturas del mando a nuestras necesidades para utilizarlos como consigna (referencia) para los controladores PID.

Estos números después se utilizarán para escalar el throttle. Para el Pitch, Roll y Yaw asumo que el mando es ideal y los pulsos varían exactamente entre 1ms y 2ms, ya que son menos críticos. Es importante que en el código que os dejo mas adelante para poner en marcha y controlar los motores modifiquéis los siguientes parámetros en función de lo que haya arrojado el software de arriba:

// AJUSTE MANDO RC - THROTLLE
const int us_max_Throttle_adj = 2000;
const int us_min_Throttle_adj = 950;
const float us_max_Throttle_raw = 2004; // <-- Si teneis la entrada Throttle invertida sustituid este valor
const float us_min_Throttle_raw = 1116; // <-- por este y viceversa

Ahora que ya sabemos cómo leer pulsos con nuestra placa Arduino, ¿qué hacemos con esta señal? ¿Cómo le decimos a nuestro drone que avance o que gire en una dirección determinada mediante un pulso en microsegundos?

Como ya sabemos, para poder girar o avanzar, el drone tiene que ser capaz de inclinarse manteniéndose estable en el aire. Para ello, tenemos que indicar al drone cuantos grados queremos que se incline en una dirección determinada para poder desplazarse hacia donde se le ha indicado.  Por lo tanto, habrá que usar estas señales del mando RC como referencia para inclinar el drone en un eje o en otro. Al fin y al cabo, esa es la función final del mando: indicar al drone cuantos grados queremos que se incline en un eje determinado para poder desplazarnos en una dirección concreta. Para ello, es necesario ‘procesar’ la información en microsegundos que nos llegar del receptor y convertirla a una señal proporcional en grado de inclinación.

Porgamos como ejemplo el stick asociado al eje Pitch. Tenemos que procesar la señal que recibimos del receptor (variable RC_Pitch_raw), de forma que con el stick es su posición central (sin moverlo ni arriba ni abajo), el drone reciba una consigna de 0º de inclinación, es decir, que se mantenga estable y sin inclinarse. Que, si por el contrario, movemos el stick hasta arriba, el drone reciba la orden de inclinarse -30º en ese eje para desplazarse en esa dirección. Y que, si movemos el stick hacia abajo, el drone reciba la orden de inclinarse unos +30º en ese eje para desplazarse en dirección contraria. Le estamos indicando al drone que se incline en el eje Pitch en un sentido o en otro (-30º/+30º) en función de la posición del stick del mando RC.

Para procesar las señales obtenidas del mando y transformarlas en consigna de inclinación, utilizaremos la función map() de Arduino. 

  // Ecuaciones de procesamiento
  RC_Throttle_consigna = map(RC_Throttle_raw, us_min_Throttle_raw, us_max_Throttle_raw, us_min_Throttle_adj, us_max_Throttle_adj);
  RC_Pitch_consigna    = map(RC_Pitch_raw, us_min_Pitch_raw, us_max_Pitch_raw, us_min_Pitch_adj, us_max_Pitch_adj);
  RC_Roll_consigna     = map(RC_Roll_raw, us_min_Roll_raw, us_max_Roll_raw, us_min_Roll_adj, us_max_Roll_adj);
  RC_Yaw_consigna      = map(RC_Yaw_raw, us_min_Yaw_raw, us_max_Yaw_raw, us_min_Yaw_adj, us_max_Yaw_adj);

Os dejo el software completo que incluye las ecuaciones de procesamiento. Podéis también descargarlo en el siguiente enlace:

// Declaración de pines
#define pin_INT_Throttle 8 // Pin Throttle del mando RC 
#define pin_INT_Yaw 7      // Pin Yaw del mando RC 
#define pin_INT_Pitch 12   // Pin Pitch del mando RC
#define pin_INT_Roll 9     // Pin Roll del mando RC

#include <EnableInterrupt.h>
long loop_timer, tiempo_ejecucion;
float RC_Throttle_consigna, RC_Pitch_consigna, RC_Roll_consigna, RC_Yaw_consigna;

// AJUSTE MANDO RC - THROTLLE
const int us_max_Throttle_adj = 2000;
const int us_min_Throttle_adj = 950;
const float us_max_Throttle_raw = 2004; // <-- Si teneis la entrada Throttle invertida sustituid este valor
const float us_min_Throttle_raw = 1116; // <-- por este y viceversa

// AJUSTE MANDO RC - PITCH
const float us_max_Pitch_raw = 1952;
const float us_min_Pitch_raw = 992;
const int us_max_Pitch_adj = -30;   // <-- Si teneis la entrada Pitch invertido sustituid este valor
const int us_min_Pitch_adj = 30;    // <-- por este y viceversa

// AJUSTE MANDO RC - ROLL
const float us_max_Roll_raw = 1960;
const float us_min_Roll_raw = 992;
const int us_max_Roll_adj = 30;     // <-- Si teneis la entrada Roll invertido sustituid este valor
const int us_min_Roll_adj = -30;    // <-- por este y viceversa

// AJUSTE MANDO RC - YAW
const float us_max_Yaw_raw = 1928;
const float us_min_Yaw_raw = 972;
const int us_max_Yaw_adj = 30;      // <-- Si teneis la entrada Yaw invertido sustituid este valor
const int us_min_Yaw_adj = -30;     // <-- por este y viceversa

// INTERRUPCIÓN MANDO RC --> THROTTLE
volatile long Throttle_HIGH_us;
volatile int RC_Throttle_raw;
void INT_Throttle() {
  if (digitalRead(pin_INT_Throttle) == HIGH) Throttle_HIGH_us = micros();
  if (digitalRead(pin_INT_Throttle) == LOW)  RC_Throttle_raw  = micros() - Throttle_HIGH_us;
}

// INTERRUPCIÓN MANDO RC --> PITCH
volatile long Pitch_HIGH_us;
volatile int RC_Pitch_raw;
void INT_Pitch() {
  if (digitalRead(pin_INT_Pitch) == HIGH) Pitch_HIGH_us = micros();
  if (digitalRead(pin_INT_Pitch) == LOW)  RC_Pitch_raw  = micros() - Pitch_HIGH_us;
}

// INTERRUPCIÓN MANDO RC --> ROLL
volatile long Roll_HIGH_us;
volatile int RC_Roll_raw;
void INT_Roll() {
  if (digitalRead(pin_INT_Roll) == HIGH) Roll_HIGH_us = micros();
  if (digitalRead(pin_INT_Roll) == LOW)  RC_Roll_raw  = micros() - Roll_HIGH_us;
}

// INTERRUPCIÓN MANDO RC --> YAW
volatile long Yaw_HIGH_us;
volatile int RC_Yaw_raw;
void INT_Yaw() {
  if (digitalRead(pin_INT_Yaw) == HIGH) Yaw_HIGH_us = micros();
  if (digitalRead(pin_INT_Yaw) == LOW)  RC_Yaw_raw  = micros() - Yaw_HIGH_us;
}

void setup() {
  // Declaración de interrupciones
  pinMode(pin_INT_Yaw, INPUT_PULLUP);                   // YAW
  enableInterrupt(pin_INT_Yaw, INT_Yaw, CHANGE);
  pinMode(pin_INT_Throttle, INPUT_PULLUP);              // POTENCIA
  enableInterrupt(pin_INT_Throttle, INT_Throttle, CHANGE);
  pinMode(pin_INT_Pitch, INPUT_PULLUP);                 // PITCH
  enableInterrupt(pin_INT_Pitch, INT_Pitch, CHANGE);
  pinMode(pin_INT_Roll, INPUT_PULLUP);                  // ROLL
  enableInterrupt(pin_INT_Roll, INT_Roll, CHANGE);

  // Serial.begin()
  Serial.begin(115200);
}

void loop() {
  // Nuevo ciclo
  while (micros() - loop_timer < 10000);
  tiempo_ejecucion = (micros() - loop_timer) / 1000;
  loop_timer = micros();

  // Ecuaciones de procesamiento
  RC_Throttle_consigna = map(RC_Throttle_raw, us_min_Throttle_raw, us_max_Throttle_raw, us_min_Throttle_adj, us_max_Throttle_adj);
  RC_Pitch_consigna    = map(RC_Pitch_raw, us_min_Pitch_raw, us_max_Pitch_raw, us_min_Pitch_adj, us_max_Pitch_adj);
  RC_Roll_consigna     = map(RC_Roll_raw, us_min_Roll_raw, us_max_Roll_raw, us_min_Roll_adj, us_max_Roll_adj);
  RC_Yaw_consigna      = map(RC_Yaw_raw, us_min_Yaw_raw, us_max_Yaw_raw, us_min_Yaw_adj, us_max_Yaw_adj);
  
  // Monitor Serie
  Serial.print(RC_Throttle_consigna);
  Serial.print("\t");
  Serial.print(RC_Pitch_consigna);
  Serial.print("\t");
  Serial.print(RC_Roll_consigna);
  Serial.print("\t");
  Serial.println(RC_Yaw_consigna);
}

Lo mas importante es asegurar que una vez procesadas las señales, el canal de throttle no ha quedado invertido. Es decir, con el stick al mínimo tenemos que obtener una salida de 1000μs aproximadamente, y con el stick al máximo una salida de 2000μs aproximadamente. Si tenéis el canal invertido, corregirlo cambiando de orden los parámetros de entrada. De esto:

const float us_max_Throttle_raw = 2004; // <-- Si la entrada Throttle está invertida sustituid este valor
const float us_min_Throttle_raw = 1116; // <-- por este y viceversa

A esto:

const float us_max_Throttle_raw = 1160; // <-- Si la entrada Throttle está invertida sustituid este valor
const float us_min_Throttle_raw = 2004; // <-- por este y viceversa

Comprobar también los demás canales, y en caso de tener alguno invertido, seguid el mismo procedimiento que con el canal throttle hasta ajustarlos como se muestra a continuación:

  • Moviendo stick de Pitch hacia arriba, RC_Pitch_consigna = -30º aprox.
  • Moviendo stick de Pitch hacia abajo, RC_Pitch_consigna = +30º aprox.
  • Moviendo stick de Roll hacia la derecha, RC_Roll_consigna = +30º aprox.
  • Moviendo stick de Roll hacia la izquierda, RC_Roll_consigna = -30º aprox.
  • Moviendo stick de Yaw hacia la derecha, RC_Yaw_consigna = +30º aprox.
  • Moviendo stick de Yaw hacia la izquierda,  RC_Yaw_consigna = -30º aprox.

Las implicaciones de no hacer esta modificación en los canales Pitch, Roll y Yaw no son tan graves, simplemente, cuando ordenáramos al drone ‘avanzar’, este retrocedería. Nada grave, pero conviene corregirlo.

Acordaos de cambiarlo también cuando descargues el software principal en la entrada número 9.


Continuar con la siguiente entrada:

  1. Conceptos generales sobre drones
  2. Material necesario y montaje de los componentes hardware
  3. Mando RC y receptor. Programación en Arduino
  4. → MPU6050 y su programación en Arduino
  5. Batería LiPo
  6. Control de estabilidad y PID
  7. Motores, ESC y su programación en Arduino
  8. Calibración de hélices y motores
  9. Software completo y esquema detallado
  10. Probando el Software completo antes de volar
  11. Como leer variables de Arduino en Matlab

5/5 - (4 votos)
59 Comentarios

Añadir un comentario

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *