Leer señales PPM de radiocontrol con Arduino o STM32, paso a paso

En esta entrada aprenderemos cómo leer señales PPM de un receptor radiocontrol utilizando Arduino o STM32. El recetor que he utilizado es el fs-ia6b y el mando el FlySky i6 FS-i6 2.4G.

Pulso PPM

Comprar mando y receptor radiocontrol

Conexión entre receptor y Arduino 

Alimentaremos el receptor de los 5V y GND de la placa Arduino. Utilizaremos la entrada D2 para la lectura de los pulsos ya que este pin es una interrupción hardware:

Conexión receptor con Arduino PPM

Aprendiendo a leer señales PPM con Arduino

Existen dos tipo de señales PPM en función del nivel de tensión que se utilice para generar los impulsos (Vcc o GND). Podemos seleccionar qué método usar desde las opciones de configuración del mando. Para este artículo tomaré como base el primer caso (MODO 1), aunque ya veréis como una vez terminado el articulo podréis adaptarlo vosotros mismo a la otra versión sin problemas:

Modos de pulso PPM

El receptor fs-ia6b, a pesar de tener 6 canales, envía 8 impulsos (me imagino que compartirá firmware con la versión de 8 canales). La lectura de los canales se basa en conocer en qué instante se da cada flanco. Como veis en la figura de abajo, y trabajando con MODO 1, para 8 canales tenemos 18 flancos (9 negativos, 9 positivos). La relación entre el numero de canales y el numero de flancos es la siguiente:

número de flancos = 2 * número de canales + 2

número de flancos = 2 * 8 + 2 = 18 flancos

Para conocer la anchura de cada pulso necesitamos conocer en qué instante se da cada uno de estos 18 flancos. Con esta información podemos calcular el ancho de cada pulso haciendo la resta entre entre un flanco dado y el anterior. Como veis, los flancos número 0 y 17 no aportan nada, ya que solo se utilizan para bajar el nivel de tensión a 0V y ‘dibujar’ los pulsos entre 0V y Vcc.

Flancos de pulso PPM

La longitud de un pulso cualquier (i) se puede calcular haciendo la resta del instante en el que se han dado los dos flancos del que está compuesto (j). Esta frase puede sonar un tanto complicada, pero no tiene mucho misterio como veremos más adelante a la hora de programar esta expresión:

duración_pulso[i] = flanco[j] – flanco[j-1]

Por ejemplo, imaginemos que para el canal número 3 hemos registrado los instantes en el que se dan ambos flancos como se indica en la figura inferior. Como veis, el tiempo que se registra es el tiempo absoluto, el tiempo transcurrido desde que hemos iniciado la placa. Para calcular la duración del pulso, bastaría con restar 131187us – 129887us lo que da un resultado de 1300us, que sería la duración de pulso del canal número 3:

Cálculo de pulso PPM con Arduino

Para registrar en instante en el que se da cada flanco utilizaremos interrupciones hardware. Cada vez que haya un cambio de esta en la entrada digital D2, se activará la interrupción button_ISR() y se ejecutará esta parte del código. Como veis, lo que hacemos es guardar en un array el instante (micros()) en el que se activa la interrupción, es decir, en el instante en el que recibimos cada uno de los 18 flancos. Para detectar cuando ha terminado la ráfaga de 8 canales y resetear el contador, utilizo una condición if, donde compruebo si entre un flanco y el siguiente han transcurrido más de 2500us (2.5ms). La longitud de pulso máxima es de 2000us (2ms), por lo que si entre flancos transcurren más de 2000us, se que nos encontramos ante una nueva ráfaga:

void button_ISR() {
  if (micros() - pulso_instante[contador_flaco - 1] > 2500) contador_flaco = 0;
  pulso_instante[contador_flaco] = micros();
  contador_flaco++;
}

Al final de cada ráfaga tendremos un array pulso_ instante[] de 18 elementos, con índices del 0 al 17.

Partiendo de este array, calcularemos la duración de cada pulso (de cada canal) utilizando el siguiente procedimiento. En primer lugar lugar, utilizando un if, nos aseguraremos de que no ejecutamos esta parte del código a nos ser que hayamos recibido toda la ráfaga de canales, en mi caso, los 18 flancos. Si se cumple la condición, utilizaremos un bucle for para recorrer cada uno de los 8 canales y calcular la diferencia de tiempo entre un flanco y el flanco anterior. Para ignorar el primer y el último flanco, ya que no aportan nada a la duración de los pulsos, comenzaremos el bucle for en 1 y no es 0. Utilizando la siguiente expresión podremos calcular la duración de los 8 pulsos:

if (contador_flaco == 18) {
  for (uint8_t i = 1; i <= numero_canales; i++) {
    Mando_canal[i] = pulso_instante[2 * i] - pulso_instante[2 * i - 1];
  }
}

Por ejemplo, en el primer ciclo del bucle for, cuando i = 1, obtenemos la siguiente expresión. De esta forma hemos conseguido saltarnos el primer flanco, que está en el índice cero del array. Así, cuando i = 1 leemos la duración de pulso del primer canal (la diferencia de tiempo entre los flancos 2 y 1):

Mando_canal[1] = pulso_instante[2] - pulso_instante[1];

Lo mismo sucederá en el índice numero 8 o último canal. Cuando i = 8 obtendremos la siguiente expresión, y calcularemos la diferencia entre el flanco número 16 y el flanco número 15, es decir, la duración del último canal. Si os fijáis, el pulso número 17, y que no aporta nada, es ignorado por la expresión:

Mando_canal[8] = pulso_instante[16] - pulso_instante[15];

Código completo para Arduino

#define numero_canales 8
uint64_t pulso_instante[numero_canales * 2 + 2];
uint16_t Mando_canal[numero_canales];
uint8_t contador_flaco = 1;

void setup()
{
  Serial.begin(115200);
  pinMode(2, INPUT);
  attachInterrupt(digitalPinToInterrupt(2), button_ISR, CHANGE);
  pulso_instante[0] = micros();
}

void loop() {
  if (contador_flaco == 18) {
    for (int i = 1; i <= numero_canales; i++) {
      Mando_canal[i] = pulso_instante[2 * i] - pulso_instante[2 * i - 1];
      Serial.print(Mando_canal[i]);
      Serial.print("\t");
    }
    Serial.println();
  }
}

void button_ISR() {
  if (micros() - pulso_instante[contador_flaco - 1] > 2500) contador_flaco = 0;
  pulso_instante[contador_flaco] = micros();
  contador_flaco++;
}

Código completo para STM32

Utilizo el pin PC11 para la interrupción, pero podéis utilizar cualquier entrada digital. 

#define numero_canales 8
uint64_t pulso_instante[numero_canales * 2 + 2];
uint16_t Mando_canal[numero_canales];
uint8_t contador_flaco = 1;

void setup()
{
  Serial.begin(115200);
  pinMode(PC11, INPUT);
  attachInterrupt(digitalPinToInterrupt(PC11), button_ISR, CHANGE);
  pulso_instante[0] = micros();
}

void loop() {
  if (contador_flaco == 18) {
    for (int i = 1; i <= numero_canales; i++) {
      Mando_canal[i] = pulso_instante[2 * i] - pulso_instante[2 * i - 1];
      Serial.print(Mando_canal[i]);
      Serial.print("\t");
    }
    Serial.println();
  }
}

void button_ISR() {
  if (micros() - pulso_instante[contador_flaco - 1] > 2500) contador_flaco = 0;
  pulso_instante[contador_flaco] = micros();
  contador_flaco++;
}

Añadir un comentario

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