Récepteur radio-modélisme sur USB

Ou comment récupérer sur un PC les consignes émises par une radiocommande

Introduction

Quoi de plus normal pour un hacker que d’essayer de hacker tout ce qui bouge?

C’est probablement l’idée qui m’a traversé l’esprit quand j’ai mis en route ma première radiocommande: comment l’utiliser pour piloter tout et n’importe quoi? Une solution serait de concevoir intégralement un récepteur radio 2.4GHz et décoder le signal de la radiocommande. Cette solution permettrait de récupérer l’intégralité des voies de la radio. Elle est par contre particulièrement complexe.

C’est pourquoi, je vais vous proposer une solution détournée: récupérer les signaux de pilotage servomoteurs derrière un récepteur radio du commerce. Il suffit alors de lire les signaux modulés en largeur d’impulsion pour avoir la valeur émise par la radio.

Le montage que je vous propose permet de numériser les signaux destinés aux servomoteurs de modélisme. Les valeurs acquises sont ensuite envoyées à un PC via une prise USB.

Grâce au protocole HID, le montage est vu par le système d’exploitation (Windows, Linux, Mac) comme un joystick sans installer de pilotes particuliers.

Il ne reste plus qu’à exploiter les valeurs pour l’usage que vous aurez imaginé :

  • Piloter un simulateur de vol sans fil
  • Piloter un système déporté (robot, caméra IP, …)

Un peu de théorie

Un récepteur de modélisme sert à recevoir et décoder les ordres émis par la radiocommande et les envoyer aux actionneurs (servomoteurs, variateurs, …). Le pilotage des actionneurs de modélisme est standard:

  • Ils reçoivent des signaux carrés d’une période de 22 millisecondes
  • La largeur de l’état haut varie entre 1ms et 2ms
  • 1ms correspond à 0% (ou -100%)
  • 2ms correspond à 100 %
  • L’amplitude du signal est déterminée par la tension d’alimentation du récepteur
Signal à la sortie d’un récepteur AR400 alimenté en 3.3V. Notez bien la période de 22ms entre chaque impulsion

 

Impulsion 0% => environ 1ms

 

Impulsion 100% => environ 2ms

Il s’agit donc de signaux modulés en largeur d’impulsion ou PWM (Pulse Width Modulation).

Pour information, la largeur d’impulsion n’est que de 10% de la période totale pour permettre au récepteur de générer jusqu’à 10 voies séquentiellement:

Le fonctionnement du récepteur en est donc simplifié: il n’a qu’une impulsion à générer à la fois.

Mise en application

Choix du matériel

Le récepteur radio que j’ai choisi pour ce montage est un Spektrum AR400. Il s’agit d’un récepteur DSM2/DSMX 4 voies dédié à l’aéromodélisme. Il est en outre capable de gérer la télémétrie (future évolution du projet?).

Pour numériser les signaux PWM, je compte utiliser un microcontrôleur doté d’une connexion USB. Mon affinité me pousse naturellement chez Microchip. J’ai donc commencé le développement sur un PIC32MX795F512H que j’avais sous la main. Malheureusement, il se trouve que l’utilisation que je vais décrire dans cet article n’est pas bien supportée par mon compilateur, spécifiquement sur cette famille de PIC32. En effet, le contrôleur d’interruption ne semble pas bien configuré. Je suis donc passé sur un PIC32MX440F256H.

La carte de développement utilisée est une Pinguino Micro de chez Olimex, reprogrammée avec le bootloader USB de Mikroelektronika.

L’idée est d’utiliser les 4 modules Input Capture disponibles dans le PIC32. Ces modules permettent de faire des mesures temporelles sur des signaux digitaux. J’utilise donc un module par voie du récepteur.

Câblage

Le câblage électrique est des plus simple. La carte Pinguino est autonome. L’alimentation électrique provient directement de la prise USB. Un régulateur 3.3V présent sur la carte permet d’alimenter l’AR400 avec une tension compatible PIC32 (ces derniers peuvent être chatouilleux sur certaines broches avec le 5V par exemple).

Sur l’AR400, on note 5 rangées de 3 pins. De gauche à droite, on trouve:

  • Association/Télémétrie
  • Gaz
  • Ailerons
  • Gouverne de profondeur
  • Gouverne de direction

Ensuite, de haut en bas, on trouve:

  • Signal
  • +Vcc
  • Gnd

Voici donc le schéma de câblage (si l’on peut appeler ça comme ça…)

Le module Input Capture

Ce module est présent dans la plupart des microcontrôleurs Microchip. Il permet de récupérer la valeur d’un Timer sur un front de signal. Comme tous les modules d’un microcontrôleur, il est possible de le configurer:

  • Choix du premier front de déclenchement (montant ou descendant)
  • Mode de déclenchement (armement pour un front, sur les fronts montants, sur les fronts descendants, sur tous les fronts…)
  • Périodicité des interruptions (tous les fronts, tous les 2 fronts, …)
  • Pour les PIC32, choix de la taille du compteur (16 ou 32 bits)

J’ai choisi d’utiliser un Timer sur 32 bits, de déclencher sur tous les fronts, de commencer par un front montant et d’avoir une interruption à chaque déclenchement. Ce qui donne la configuration suivante (cf: datasheet du module Input Capture, page 6):

IC1CONbits.ICM = 0b110; //Simple Capture Event mode – every edge, specified edge first and every edge thereafter
IC1CONbits.FEDGE = 1; //Capture rising edge first
IC1CONbits.C32 = 1; //32-bit timer resource capture
IC1CONbits.ICI = 0; //Interrupt on every capture event
IC1CONbits.ICSIDL = 0; Continue to operate in CPU Idle mode
 
//Dupplication de la configuration sur tous les modules Input Capture    
IC2CON = IC1CON;
IC3CON = IC1CON;
IC4CON = IC1CON;

Cependant, le module Input Capture ne fonctionne pas seul: il travail conjointement avec le Timer 2. Il convient donc de le configurer également:

  • Source d’horloge: interne
  • Pas de prescaler (avec une horloge à 80MHz, la valeur de comptage devrait être comprise entre 80000 (1ms) et 160000 (2ms), ce qui donne une résolution plus qu’acceptable)
  • Timer sur 32 bits
  • Pas de GATE
  • Période de comptage 0xFFFFFFFF

Ce qui donne la configuration suivante (cf: datasheet du module timer, page 9):

T2CONSET.f1 = 0; //Internal peripheral clock
T2CONSETbits.T32 = 1; //TMRx and TMRy form a 32-bit timer
T2CONSETbits.TGATE = 0; //Gated time accumulation is disabled
T2CONSETbits.TCKPS0 = 0; //1:1 prescale value
T2CONSETbits.TCKPS1 = 0;
T2CONSETbits.TCKPS2 = 0;
TMR2 = 0;
PR2 = 0xFFFFFFFF;
T2CONSETbits.ON = 1; //Module is enabled

 

NB : tous les modules Input Capture du PIC32MX440 accèdent au Timer2.

La routine d’interruption

La majorité du code contenu dans le microcontrôleur s’exécute sur interruptions. Sur front montant, je lis et mémorise la valeur du Timer2. Sur front descendant, je calcul la durée de l’impulsion en faisant la différence des valeurs courante et précédente du Timer2.

NB : Dès qu’un évènement est capturé par un module Input Capture, il copie la valeur du registre TMR2 dans le registre ICxBUF, qui est en réalité une FIFO. Elle stocke les valeurs de comptage des derniers évènements acquis.

void IC1_ISR() iv IVT_INPUT_CAPTURE_1 ilevel 6 ics ICS_SOFT {
     unsigned long temp = 0;
     LATG.f6 = 1;
     sync = 1;
     TMR1 = 0; //reset timeout comm.
     temp = IC1BUF;
     IC1BUF = 0;
     if (toogle1 == 0) {
         ic1_start = temp;
         toogle1 = 1;
     }
     else {
         // value1 = CalculDelta(temp, ic1_start);
         
         // Calcul T2-T1, même si le timer déborde et repart à 0
         if (temp<ic1_start) {
            // théoriquement impossible sauf en cas de débordement
            value1 = 4294967295-(ic1_start-temp)+1;
         }
         else value1 = temp-ic1_start;
         toogle1 = 0;
     }
     debug_toogle++;
     if (debug_toogle >= 50) {
        debug_toogle = 0;
        LATD.f1 = !PORTD.f1;
     }
     IC1IF_bit = 0;
}

Je profite de la disposition du Timer 1 pour gérer un timeout: en cas de perte de la liaison radio, le signal PWM disparait et le Timer 1 n’est plus remis à zéro. Le débordement du timer (environ en 200ms) indique une perte du signal. A noter également la gestion du débordement du Timer 2: à 80MHz sur 32 bits, il survient assez rapidement (53 secondes).

NB : La voie de pilotage des gaz génère tout le temps un signal. En cas de perte de la radiocommande, une impulsion de 1ms est envoyée pour remettre les gaz à 0.

Cette routine est dupliquée pour tous les modules Input Capture.

ATTENTION! il ne faut pas utiliser de sous fonction commune aux 4 interruptions: elle créent un couplage nuisible au fonctionnement du programme.

NB : Il est impossible de remettre à 0 le flag ICxIF_bit tant que le registre ICxBUF n’a pas été lu intégralement.

La communication USB

Dans un premier temps, j’ai choisi de remonter les valeurs des voies sur un PC via un bus USB. Le plus simple à gérer pour Windows est un périphérique HID compatible joystick. J’ai donc récupéré et nettoyé le descripteur USB de mon simulateur de Joystick. Le nouveau descripteur gère 4 axes (X, Y, Rx et Ry) sur 16 bits.

Voici le descripteur :

{
    0x05, 0x01,                        // USAGE_PAGE (Generic Desktop)
    0x09, 0x04,                        // USAGE (Joystick)
    0xa1, 0x01,                        // COLLECTION (Application)
    0x09, 0x01,                        //   USAGE (Pointer)
    0xa1, 0x00,                        //   COLLECTION (Physical)
    0x09, 0x30,                        //     USAGE (X)
    0x09, 0x31,                        //     USAGE (Y)
    0x09, 0x34,                        //     USAGE (Ry)
    0x09, 0x33,                        //     USAGE (Rx)
    0x15, 0x00,                        //     LOGICAL_MINIMUM (0)
    0x27, 0xff, 0xff, 0x00, 0x00,    //     LOGICAL_MAXIMUM (65535)
    0x75, 0x10,                        //     REPORT_SIZE (16)
    0x95, 0x04,                        //     REPORT_COUNT (4)
    0x81, 0x02,                        //     INPUT (Data,Var,Abs)
    0xc0,                            //     END_COLLECTION
    0xc0                            // END_COLLECTION
}

 

Le rapport USB envoyé au PC est décrit de la manière suivante:

typedef union {
    char bytes[8];
    struct {
        unsigned int x_value;
        unsigned int y_value;
        unsigned int rx_value;
        unsigned int ry_value;
    };
} T_USB_Report;

La boucle principale de programme est donc très simple: elle récupère les 4 valeurs, leur soustrait 80000 et les divise par 1.22. On obtient ainsi une valeur comprise entre 0 et 65573. Il convient de borner les valeurs finales obtenues en cas de dépassement de la gamme (<0 ou >65535). Les valeurs sont ensuite stockées dans le rapport USB et envoyées au PC.

while (1) {
    MyReport.x_value = (value1-MIN_PERIOD)/1.22;
    MyReport.y_value = (value2-MIN_PERIOD)/1.22;
    MyReport.rx_value = (value3-MIN_PERIOD)/1.22;
    MyReport.ry_value = (value4-MIN_PERIOD)/1.22;
    HID_Write(MyReport.bytes, sizeof(MyReport));
    delay_ms(10);
}

Ainsi, il devient possible de récupérer sous Windows les 4 voies de la radiocommande, en USB et sans driver spécifique:

Assistant de configuration pour manette de jeux

Le programme complet

Voici le programme complet fonctionnel:

#include "USBdsc.c"
#define MIN_PERIOD        80000.0
 
typedef union {
    char bytes[8];
    struct {
        unsigned int x_value;
        unsigned int y_value;
        unsigned int rx_value;
        unsigned int ry_value;
    };
} T_USB_Report;
 
T_USB_Report MyReport;
 
int debug_toogle = 0;
 
unsigned long int ic1_start = 0;
unsigned long int value1 = 0;
int toogle1 = 0;
 
unsigned long int ic2_start = 0;
unsigned long int value2 = 0;
int toogle2 = 0;
 
unsigned long int ic3_start = 0;
unsigned long int value3 = 0;
int toogle3 = 0;
 
unsigned long int ic4_start = 0;
unsigned long int value4 = 0;
int toogle4 = 0;
 
int sync = 0;
 
char ReadBuffer[64], WriteBuffer[64];
 
unsigned long int CalculDelta (unsigned long int T2, unsigned long int T1) {
    // Calcul T2-T1, même si le timer déborde et repart à 0
    if (T2<T1) {
       // théoriquement impossible sauf en cas de débordement
       return 4294967295-(T1-T2)+1;
    }
    else return T2-T1;
}
 
void IC1_ISR() iv IVT_INPUT_CAPTURE_1 ilevel 6 ics ICS_SOFT {
     unsigned long temp = 0;
     LATG.f6 = 1;
     sync = 1;
     TMR1 = 0; //reset timeout comm.
     temp = IC1BUF;
     IC1BUF = 0;
     if (toogle1 == 0) {
         ic1_start = temp;
         toogle1 = 1;
     }
     else {
         // value1 = CalculDelta(temp, ic1_start);
         
         // Calcul T2-T1, même si le timer déborde et repart à 0
         if (temp<ic1_start) {
            // théoriquement impossible sauf en cas de débordement
            value1 = 4294967295-(ic1_start-temp)+1;
         }
         else value1 = temp-ic1_start;
         toogle1 = 0;
     }
     debug_toogle++;
     if (debug_toogle >= 50) {
        debug_toogle = 0;
        LATD.f1 = !PORTD.f1;
     }
     IC1IF_bit = 0;
}
 
void IC2_ISR() iv IVT_INPUT_CAPTURE_2 ilevel 5 ics ICS_SOFT {
     unsigned long temp = 0;
     temp = IC2BUF;
     IC2BUF = 0;
     if (toogle2 == 0) {
         ic2_start = temp;
         toogle2 = 1;
     }
     else {
         // value2 = CalculDelta(temp, ic2_start);
         
         // Calcul T2-T1, même si le timer déborde et repart à 0
         if (temp<ic2_start) {
            // théoriquement impossible sauf en cas de débordement
            value2 = 4294967295-(ic2_start-temp)+1;
         }
         else value2 = temp-ic2_start;
         toogle2 = 0;
     }
     IC2IF_bit = 0;
}
 
void IC3_ISR() iv IVT_INPUT_CAPTURE_3 ilevel 4 ics ICS_SOFT {
     unsigned long temp = 0;
     temp = IC3BUF;
     IC3BUF = 0;
     if (toogle3 == 0) {
         ic3_start = temp;
         toogle3 = 1;
     }
     else {
         // value3 = CalculDelta(temp, ic3_start);
         
         // Calcul T2-T1, même si le timer déborde et repart à 0
         if (temp<ic3_start) {
            // théoriquement impossible sauf en cas de débordement
            value3 = 4294967295-(ic3_start-temp)+1;
         }
         else value3 = temp-ic3_start;
         toogle3 = 0;
     }
     IC3IF_bit = 0;
}
 
void IC4_ISR() iv IVT_INPUT_CAPTURE_4 ilevel 3 ics ICS_SOFT {
     unsigned long temp = 0;
     temp = IC4BUF;
     IC4BUF = 0;
     if (toogle4 == 0) {
         ic4_start = temp;
         toogle4 = 1;
     }
     else {
         // value4 = CalculDelta(temp, ic4_start);
         // Calcul T2-T1, même si le timer déborde et repart à 0
         if (temp<ic4_start) {
            // théoriquement impossible sauf en cas de débordement
            value4 = 4294967295-(ic4_start-temp)+1;
         }
         else value4 = temp-ic4_start;
         toogle4 = 0;
     }
     IC4IF_bit = 0;
}
 
void USB_ISR() iv IVT_USB_1 ilevel 7 ics ICS_SRS {
     USB_Interrupt_Proc();
     USBIF_bit = 0;
}
 
void TMR1_ISR() iv IVT_TIMER_1 ilevel 1 ics ICS_SOFT {
     LATG.f6 = 0;
     /*
     value1 = 120000;
     value2 = value1;
     value3 = value1;
     value4 = value1;
     */
     sync = 0;
     TMR1 = 0;
     T1IF_bit = 0;
}
 
void ClearBuffer (char * buffer, int buffer_len) {
     int i = 0;
     for (i=0; i<buffer_len; i++) *(buffer+i) = 0;
}
 
void main() {
 
    toogle1 = 0;
    toogle2 = 0;
    toogle3 = 0;
    toogle4 = 0;
    ic1_start = 0;
    ic2_start = 0;
    ic3_start = 0;
    ic4_start = 0;
    value1 = 0;
    value2 = 0;
    value3 = 0;
    value4 = 0;
    debug_toogle = 0;
 
    AD1PCFG = 0xFFFF;
    JTAGEN_bit = 0;
    CHECON = 0x32;
    ODCB = 0;
    TRISD = 0xffff;
    TRISD.f1 = 0;
    TRISG.f6 = 0;
    LATG.f6 = 0;
    LATD.f1 = 0;
 
    IC1CONbits.ICM = 0b110;
    IC1CONbits.FEDGE = 1;
    IC1CONbits.C32 = 1;
    IC1CONbits.ICI = 0;
    IC1CONbits.ICSIDL = 0;
    
    IC2CON = IC1CON;
    IC3CON = IC1CON;
    IC4CON = IC1CON;
 
    T2CONSET.f1 = 0;
    T2CONSETbits.T32 = 1;
    T2CONSETbits.TGATE = 0;
    T2CONSETbits.TCKPS0 = 0;
    T2CONSETbits.TCKPS1 = 0;
    T2CONSETbits.TCKPS2 = 0;
    TMR2 = 0;
    PR2 = 0xFFFFFFFF;
    T2CONSETbits.ON = 1;
    
    PR1 = 65535;
    TMR1 = 0;
    T1CONSETbits.TCKPS0 = 1;
    T1CONSETbits.TCKPS1 = 1;
    T1CONSETbits.TGATE = 0;
    T1CONSETbits.SIDL = 0;
    T1CONSETbits.TCS = 0;
    T1CONSETbits.ON = 1;
 
    IC1IP0_bit = 0;
    IC1IP1_bit = 1;
    IC1IP2_bit = 1;
 
    IC2IP0_bit = 1;
    IC2IP1_bit = 0;
    IC2IP2_bit = 1;
 
    IC3IP0_bit = 0;
    IC3IP1_bit = 0;
    IC3IP2_bit = 1;
 
    IC4IP0_bit = 1;
    IC4IP1_bit = 1;
    IC4IP2_bit = 0;
    
    USBIP0_bit = 1;
    USBIP1_bit = 1;
    USBIP2_bit = 1;
    
    T1IP0_bit = 1;
    T1IP1_bit = 0;
    T1IP2_bit = 0;
 
    
    USBIE_bit = 1;
    IC1IE_bit = 1;
    IC2IE_bit = 1;
    IC3IE_bit = 1;
    IC4IE_bit = 1;
    T1IE_bit = 1;
    
    EnableInterrupts();
 
    HID_Enable(ReadBuffer, WriteBuffer);
    
    IC1CONbits.ON = 1;
    IC2CONbits.ON = 1;
    IC3CONbits.ON = 1;
    IC4CONbits.ON = 1;
 
    T4CONSETbits.ON = 1;
    
    while (1) {
          MyReport.x_value = (value1-MIN_PERIOD)/1.22;
          MyReport.y_value = (value2-MIN_PERIOD)/1.22;
          MyReport.rx_value = (value3-MIN_PERIOD)/1.22;
          MyReport.ry_value = (value4-MIN_PERIOD)/1.22;
          HID_Write(MyReport.bytes, sizeof(MyReport));
          delay_ms(10);
    }
}

 Conclusion

Ce montage assez simple à réaliser permet de récupérer dans un microcontrôleur, puis sur un PC, les valeurs de 4 voies radiocommandées.

L’utilisation du protocole HID permet de connecter le système sur n’importe quel PC ou autre RaspberryPi/LattePanda/BeagleBoard/…

Il devient donc possible de commander à longue distance un dispositif autonome. Les idées ne vous manqueront pas, j’en suis sûr 😉

  • Le récepteur AR400

Laisser un commentaire