Joystick USB sur PIC32

Concevez vos propres contrôleurs de jeux !

Envie d’améliorer vos expériences de jeux? Alors, concevez vos propres interfaces utilisateurs: tableaux de bord de voiture ou d’avion, manette de jeu, adaptateur pour de vieux périphériques… Avec un PIC équipé d’un module USB tout devient imaginable.

Je vais vous expliquer ici comment implanter la couche HID pour que votre PIC soit reconnu en temps que contrôleur de jeux par votre OS préféré.

Introduction

Tout d’abord, plantons le décor. J’ai mené ce projet avec comme objectif final la réalisation d’un adaptateur USB pour manette de Nintendo 64 (pour les plus jeunes, voir la page Wikipedia). N’ayant pas accès au matériel nécessaire à la réalisation finale dans l’immédiat, l’adaptateur est en stand-by, mais devrait voir le jour prochainement…

Le matériel

Voici le matériel dont vous aurez besoin:

J’ai réalisé ce projet en étant en déplacement. J’ai donc utilisé ma « trousse de survie en milieu hostile »: Mikromedia for PIC32 + MikroC for PIC32 + Visual TFT. J’ai utilisé l’écran tactile pour émuler un pad analogique et des boutons poussoirs.

Un point sur le protocole HID

Bien que l’Interface USB HID porte son nom, elle n’utilise pas les fonctionnalités HID. Elle communique de manière brute (raw) avec le PC en s’appuyant uniquement sur les drivers HID. Elle est ainsi configurée par le descripteur USB généré par MikroC. Il est possible de modifier ce descripteur pour déclarer n’importe quel périphérique d’interface utilisateur.

Mais qu’est-ce qu’un descripteur? C’est très simple: il s’agit un bloc d’octets envoyés par le périphérique à chaque connexion à un PC afin de s’identifier. Ce bloc est interprété par le PC pour configurer automatiquement le driver HID. Le descripteur décrit (tiens tiens…) également le format du rapport envoyé périodiquement par le périphérique. Ce rapport contient toutes les variables acquises par le périphérique (valeur des axes, état des boutons…). Ainsi, l’utilisateur n’a pas ou peu de configuration à faire (sauf pour des cas très spéciaux): la magie du plug’n’play ! La rédaction de ce descripteur sera l’étape la plus importante et la plus critique de ce projet.

La première chose à faire est d’identifier les besoins: nombre d’entrées, de quels types … Pour ce faire, j’ai repris le matchup de la manette de N64:

  • Axe X
  • Axe Y
  • Croix directionnelle
  • Boutons C (haut, bas, gauche et droite), A, B, Z, L, R, START

J’ai donc deux types d’entrés: analogiques (x2) et tout ou rien (x14).

Voici donc la structure de données que je vais utiliser. Je décide de coder les signaux analogiques sur 8 bits signés. Les boutons se contentent eux d’un bit chacun.

bit 0bit 1bit 2bit 3bit 4bit 5bit 6bit 7
octet 0Axe X
octet 1Axe Y
octet 2Croix directionnelleBoutons C
octet 3ABZLRSTART

Les deux derniers bits du dernier octet sont inutilisés. Ce n’est pas grave, mais il faut les faire apparaitre dans le descripteur pour obtenir un nombre entier d’octets dans le rapport.

Après avoir compulsé rapidement les deux PDF de documentation pour s’imprégner de la philosophie HID, il est temps d’ouvrir le logiciel HID Descriptor Tool.

UDT

Le panneau de gauche propose les différents champs qu’il est possible d’inclure dans le descripteur de l’interface HID. Le panneau de droite représente votre descripteur.

Vous pouvez voir sur la capture d’écran le descripteur que j’ai utilisé pour mon joystick. La règle qu’il faut toujours respecter est d’obtenir un rapport codé sur un nombre entier d’octets. Je m’explique: la plus petite unité utilisée en informatique pour coder des informations est l’octet (8 bits), le rapport HID est envoyé octet par octet au PC et se dernier doit connaitre précisément l’utilité ce chacun de ces octets.

Regardons plus en détail le descripteur:

  • USAGE_PAGE (Generic Desktop): Déclare un périphérique d’usage général (joystick). Plusieurs périphériques peuvent être déclarés sur une même interface HID.
  • USAGE (Joystick): Identifie le type de périphérique décrit dans la USAGE_PAGE.
  • COLLECTION (Application): Déclare le descripteur.
    • USAGE (Pointer): Déclare un périphérique de pointage (ensemble d’axes analogiques)
    • COLLECTION (Physical): Déclare un descripteur physique.
      • USAGE (X): Déclare l’axe X.
      • USAGE (Y): Déclare l’axe Y.
      • LOGICAL_MAXIMUM (127): Déclare la valeur de la borne maximum des axes (8 bits signés dans mon cas).
      • LOGICAL_MINIMUM (-128): Déclare la valeur de la borne minimum des axes (8 bits signés dans mon cas).
      • REPORT_SIZE (8): Longueur en bits du champ représentant un axe.
      • REPORT_COUNT (2): Nombre de champ dans le descripteur (2 axes sur 8 bits).
      • INPUT (Data,Var,Abs): Type de données pour les axes: variable.
    • END_COLLECTION: fin du descripteur physique.
    • USAGE (Hat Switch): Déclare un PoV Hat. C’est le bouton en forme de chapeau sur le haut des joysticks. Il est fait pour changer le point de vue (PoV <=> Point of View) dans les jeux. Je vais l’utiliser pour la croix directionnelle.
    • LOGICAL_MINIMUM (0): Valeur logique minimum. C’est l’indice du premier bit représentant la première position du hat.
    • LOGICAL_MAXIMUM (3): Valeur logique maximum. C’est l’indice du dernier bit représentant la dernière position du hat.
    • PHYSICAL_MINIMUM (0): Valeur physique minimum. C’est la valeur d’angle du hat liée à la valeur logique minimum.
    • PHYSICAL_MAXIMUM (270): Valeur physique maximum. C’est la valeur d’angle du hat liée à la valeur logique maximum.
    • UNIT (Eng Rot:Angular Pos): Unité des valeurs physiques du hat: rotation en degré
    • REPORT_SIZE (4): Longueur en bits du champ représentant le hat.
    • REPORT_COUNT (1): Nombre de champ dans le descripteur (1 seul hat déclaré).
    • INPUT (Data,Var,Abs): Type de donnée pour le hat: variable.
    • USAGE_PAGE (Button): Déclare un descripteur de boutons.
    • USAGE_MINIMUM (No Buttons Pressed):Valeur représentant tous les bits au repos
    • USAGE_MAXIMUM (Button 10): Valeur représentant le dernier bit activé.
    • LOGICAL_MINIMUM (0): Valeur logique minimum. Valeur du bit représentant un bouton au repos.
    • LOGICAL_MAXIMUM (1): Valeur logique maximum. Valeur du bit représentant un bouton activé.
    • REPORT_SIZE (1): Longueur en bits du champ représentant un bouton.
    • REPORT_COUNT (10): Nombre de champ dans le descripteur (10 boutons).
    • UNIT_EXPONENT (0): Pas de facteur multiplicatif à appliquer sur les champ du descripteur.
    • UNIT (None): Pas d’unité pour un bouton.
    • INPUT (Data,Var,Abs): Type de donnée pour les boutons: variable.
  • REPORT_SIZE (1): Longueur en bits du champ de padding.
  • REPORT_COUNT (2): Nombre de champ dans le descripteur (2 bits vides à la fin du descripteur).
  • INPUT (Cnst,Var,Abs): Type de donnée pour le hat: constant. ⚠ Il est capital de mettre ces champ en constant autrement le périphérique ne marche pas.
  • END_COLLECTION: fin du descripteur.

Voici votre descripteur prêt à être implanté dans votre microcontrôleur. Il reste à l’enregistrer en fichier header (.h) pour pouvoir l’exploiter dans votre code source:

char ReportDescriptor[71] = {
    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)
    0x25, 0x7f,                    //     LOGICAL_MAXIMUM (127)
    0x15, 0x80,                    //     LOGICAL_MINIMUM (-128)
    0x75, 0x08,                    //     REPORT_SIZE (8)
    0x95, 0x02,                    //     REPORT_COUNT (2)
    0x81, 0x02,                    //     INPUT (Data,Var,Abs)
    0xc0,                          //   END_COLLECTION
    0x09, 0x39,                    //   USAGE (Hat switch)
    0x15, 0x00,                    //   LOGICAL_MINIMUM (0)
    0x25, 0x03,                    //   LOGICAL_MAXIMUM (3)
    0x35, 0x00,                    //   PHYSICAL_MINIMUM (0)
    0x46, 0x0e, 0x01,              //   PHYSICAL_MAXIMUM (270)
    0x65, 0x14,                    //   UNIT (Eng Rot:Angular Pos)
    0x75, 0x04,                    //   REPORT_SIZE (4)
    0x95, 0x01,                    //   REPORT_COUNT (1)
    0x81, 0x02,                    //   INPUT (Data,Var,Abs)
    0x05, 0x09,                    //   USAGE_PAGE (Button)
    0x19, 0x00,                    //   USAGE_MINIMUM (No Buttons Pressed)
    0x29, 0x0a,                    //   USAGE_MAXIMUM (Button 10)
    0x15, 0x00,                    //   LOGICAL_MINIMUM (0)
    0x25, 0x01,                    //   LOGICAL_MAXIMUM (1)
    0x75, 0x01,                    //   REPORT_SIZE (1)
    0x95, 0x0a,                    //   REPORT_COUNT (10)
    0x55, 0x00,                    //   UNIT_EXPONENT (0)
    0x65, 0x00,                    //   UNIT (None)
    0x81, 0x02,                    //   INPUT (Data,Var,Abs)
    0x75, 0x01,                    // REPORT_SIZE (1)
    0x95, 0x02,                    // REPORT_COUNT (2)
    0x81, 0x03,                    // INPUT (Cnst,Var,Abs)
    0xc0                           // END_COLLECTION
};

La variable ReportDescriptor peut être envoyée au PC sur l’USB par votre microcontrôleur.

Mise en place avec MikroC

Pour programmer ma Mikromedia, j’utilise MikroC et Visual TFT (pour l’interface graphique). Je ne vais pas détailler l’interface graphique, mais plutôt me concentrer sur la couche USB. Le départ est identique à celui utilisé pour l’Interface HID Universelle: configuration de l’horloge, déclaration des buffers USB (read et write), génération d’un descripteur avec HID Terminal et initialisation du module USB dans le programme. Voici les lignes importantes du programme qui concernent uniquement l’USB:

#include "USB_DSC.c"
#define USB_BUFFER_LEN       64
 
unsigned char readbuff[USB_BUFFER_LEN] ;
unsigned char writebuff[USB_BUFFER_LEN] ;
 
T_USB_Report USB_Report;
 
void USB_Isr() iv IVT_USB_1 ilevel 7 ics ICS_SRS {
    USB_Interrupt_Proc();        // USB servicing is done inside the interrupt
    USBIF_bit = 0;
}
 
void main() {
    EnableInterrupts();
 
    /* Interruption USB sur vecteur 7*/
    USBIP0_bit = 1;
    USBIP1_bit = 1;
    USBIP2_bit = 1;
    USBIE_bit = 1;
 
    HID_Enable(&readbuff,&writebuff);
    
    while (1) {
        HID_Write(USB_Report.bytes, sizeof(USB_Report));
    }
}

Le rapport USB est une variable de type T_USB_Report déclaré comme suit:

typedef union {
    char bytes[4];
    struct {
        char x_value;
        char y_value;
        unsigned pov_hat :4;
        unsigned button_1 :1;
        unsigned button_2 :1;
        unsigned button_3 :1;
        unsigned button_4 :1;
        unsigned button_5 :1;
        unsigned button_6 :1;
        unsigned button_7 :1;
        unsigned button_8 :1;
        unsigned button_9 :1;
        unsigned button_10:1;
    };
} T_USB_Report;

On y retrouve tous les champ déclarés dans le descripteur. L’union permet d’y accéder comme un tableau d’octets (pour l’envoi) ou champ par champ (pour y insérer les valeurs).

Ensuite, il faut modifier le fichier USBdsc.c pour y modifier le descripteur. Il faut remplacer celui généré par HID Terminal par celui que nous avons créé avec USB Descriptor Tool. Trouvez la variable hid_rpt_desc et remplacé sont contenu par celui généré par UDT:

const struct {
  char report[USB_HID_RPT_SIZE];
}hid_rpt_desc =
  {
    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)
    0x25, 0x7f,                    //     LOGICAL_MAXIMUM (127)
    0x15, 0x80,                    //     LOGICAL_MINIMUM (-128)
    0x75, 0x08,                    //     REPORT_SIZE (8)
    0x95, 0x02,                    //     REPORT_COUNT (2)
    0x81, 0x02,                    //     INPUT (Data,Var,Abs)
    0xc0,                          //   END_COLLECTION
    0x09, 0x39,                    //   USAGE (Hat switch)
    0x15, 0x00,                    //   LOGICAL_MINIMUM (0)
    0x25, 0x03,                    //   LOGICAL_MAXIMUM (3)
    0x35, 0x00,                    //   PHYSICAL_MINIMUM (0)
    0x46, 0x0e, 0x01,              //   PHYSICAL_MAXIMUM (270)
    0x65, 0x14,                    //   UNIT (Eng Rot:Angular Pos)
    0x75, 0x04,                    //   REPORT_SIZE (4)
    0x95, 0x01,                    //   REPORT_COUNT (1)
    0x81, 0x02,                    //   INPUT (Data,Var,Abs)
    0x05, 0x09,                    //   USAGE_PAGE (Button)
    0x19, 0x00,                    //   USAGE_MINIMUM (No Buttons Pressed)
    0x29, 0x0a,                    //   USAGE_MAXIMUM (Button 10)
    0x15, 0x00,                    //   LOGICAL_MINIMUM (0)
    0x25, 0x01,                    //   LOGICAL_MAXIMUM (1)
    0x75, 0x01,                    //   REPORT_SIZE (1)
    0x95, 0x0a,                    //   REPORT_COUNT (10)
    0x55, 0x00,                    //   UNIT_EXPONENT (0)
    0x65, 0x00,                    //   UNIT (None)
    0x81, 0x02,                    //   INPUT (Data,Var,Abs)
    0x75, 0x01,                    // REPORT_SIZE (1)
    0x95, 0x02,                    // REPORT_COUNT (2)
    0x81, 0x03,                    // INPUT (Cnst,Var,Abs)
    0xc0                           // END_COLLECTION
  };

Il faut également modifier la taille du descripteur: const char USB_HID_RPT_SIZE = 71; Cette ligne est au début du fichier USBdsc.c .

Enfin, il reste à affecter les valeurs du rapport USB avec celles acquises par le microcontrôleur, et envoyer le rapport au PC. Pour l’envoi, il est possible de choisir une période de boucle de l’ordre de 10ms. La fluidité du périphérique est bonne et la charge de calcul et de la liaison reste correcte.

Le projet pour Mikromedia est disponible sur Libstock.

Fonctionnement

Si tous s’est bien passé, une fois votre microcontrôleur programmé et branché, vous devriez le trouver en tant que contrôleur de jeux:

  • L'interface du PIC32

Démonstration en vidéo

Utilisation avec Zelda: Ocarina of Time

 

Laisser un commentaire