USB sur registres: interrompre le point de terminaison en utilisant HID comme exemple





Un niveau encore plus bas (avr-vusb)

USB sur les registres: STM32L1 / STM32F1

USB sur les registres: point de terminaison en bloc sur l'exemple de stockage de masse



USB sur registres: point de terminaison isochrone sur l'exemple de périphérique audio Nous



continuons à traiter l'USB sur les contrôleurs STM32L151. Comme dans la partie précédente, il n'y aura rien de dépendant de la plate-forme ici, mais ce sera dépendant de USB. Plus précisément, nous considérerons le troisième type de point final - interruption. Et nous le ferons en utilisant l'exemple d'un appareil composite "clavier + tablette" ( lien vers la source ).

Au cas où, je vous préviens: cet article (comme tout le monde) est plutôt un synopsis de ce que j'ai compris en comprenant ce sujet. Beaucoup de choses sont restées «magiques» et je serai reconnaissant s'il y a un spécialiste qui peut les expliquer.



Tout d'abord, permettez-moi de vous rappeler que le protocole HID (Human Interface Device) n'est pas destiné à échanger de grandes quantités de données. L'ensemble de l'échange est basé sur deux concepts: un événement et un état . Un événement est un message ponctuel qui se produit en réponse à un impact externe ou interne. Par exemple, l'utilisateur a appuyé sur un bouton ou déplacé la souris. Ou sur un clavier, j'ai désactivé NumLock, après quoi l'hôte est forcé et le second à envoyer la commande appropriée pour le réparer, en envoyant également le signal de frappe NumLock et en le réactivantl'affiche sur l'indicateur. Les points d'interruption sont utilisés pour signaler des événements. Un état est une sorte de caractéristique qui ne change pas comme ça. Eh bien, disons la température. Ou ajustez le niveau du volume. C'est-à-dire quelque chose par lequel l'hôte contrôle le comportement de l'appareil. Le besoin de cela se fait rarement sentir, par conséquent, l'interaction est la plus primitive - via ep0.



Ainsi, le but d'un point d'interruption est le même que celui d'une interruption dans un contrôleur - pour rapporter rapidement un événement rare. Mais l'USB est une chose centrée sur l'hôte, donc l'appareil n'a pas le droit de démarrer le transfert de lui-même. Pour contourner cela, les développeurs USB ont mis au point une béquille: l'hôte envoie périodiquement des demandes de lecture de tous les points d'interruption. La fréquence de la demande est configurée par le dernier paramètre dans EndpointDescriptor (cela fait partie du ConfigurationDescriptor). Nous avons déjà vu le champ bInterval dans les chapitres précédents, mais sa valeur a été ignorée. Maintenant, il a enfin trouvé une utilité. La valeur a une taille de 1 octet et est définie en millisecondes, nous serons donc interrogés à des intervalles de 1 ms à 2,55 secondes. Pour les appareils à faible vitesse, l'intervalle minimum est de 10 ms. La présence d'une béquille avec des points d'interruption qui interroge pour nous signifieque même en l'absence d'échange, ils gaspilleront la bande passante du bus.



La conclusion logique: les points d'interruption ne concernent que les transactions IN. En particulier, ils sont utilisés pour transmettre des événements à partir du clavier ou de la souris, pour notifier des changements dans les lignes de service du port COM, pour synchroniser le flux audio et autres. Mais pour tout cela, vous devrez ajouter d'autres types de points. Par conséquent, afin de ne pas compliquer l'exemple, nous nous limiterons à la mise en œuvre du dispositif HID. En fait, nous avons déjà fait un tel dispositif dans la première partie, mais là des points supplémentaires n'ont pas du tout été utilisés, et la structure du protocole HID n'a pas été prise en compte.



ConfigurationDescriptor



static const uint8_t USB_ConfigDescriptor[] = {
  ARRLEN34(
  ARRLEN1(
    bLENGTH, // bLength: Configuration Descriptor size
    USB_DESCR_CONFIG,    //bDescriptorType: Configuration
    wTOTALLENGTH, //wTotalLength
    1, // bNumInterfaces
    1, // bConfigurationValue: Configuration value
    0, // iConfiguration: Index of string descriptor describing the configuration
    0x80, // bmAttributes: bus powered
    0x32, // MaxPower 100 mA
  )
  ARRLEN1(
    bLENGTH, //bLength
    USB_DESCR_INTERFACE, //bDescriptorType
    0, //bInterfaceNumber
    0, // bAlternateSetting
    2, // bNumEndpoints
    HIDCLASS_HID, // bInterfaceClass: 
    HIDSUBCLASS_BOOT, // bInterfaceSubClass: 
    HIDPROTOCOL_KEYBOARD, // bInterfaceProtocol: 
    0x00, // iInterface
  )
  ARRLEN1(
    bLENGTH, //bLength
    USB_DESCR_HID, //bDescriptorType
    USB_U16(0x0110), //bcdHID
    0, //bCountryCode
    1, //bNumDescriptors
    USB_DESCR_HID_REPORT, //bDescriptorType
    USB_U16( sizeof(USB_HIDDescriptor) ), //wDescriptorLength
  )
  ARRLEN1(
    bLENGTH, //bLength
    USB_DESCR_ENDPOINT, //bDescriptorType
    INTR_NUM, //bEdnpointAddress
    USB_ENDP_INTR, //bmAttributes
    USB_U16( INTR_SIZE ), //MaxPacketSize
    10, //bInterval
  )
  ARRLEN1(
    bLENGTH, //bLength
    USB_DESCR_ENDPOINT, //bDescriptorType
    INTR_NUM | 0x80, //bEdnpointAddress
    USB_ENDP_INTR, //bmAttributes
    USB_U16( INTR_SIZE ), //MaxPacketSize
    10, //bInterval
  )
  )
};
      
      





Le lecteur attentif peut immédiatement remarquer les descriptions des points finaux. Avec le second, tout est en ordre - le point IN (puisque l'addition avec 0x80) est de type interruption, la taille et l'intervalle sont spécifiés. Mais le premier semble être déclaré OUT, mais en même temps interrompre, ce qui contredit ce qui a été dit précédemment. Et le bon sens aussi: l'hôte n'a pas besoin de béquilles pour transférer quoi que ce soit sur l'appareil à tout moment. Mais de cette façon, d'autres râteaux sont contournés: le type de point final dans STM32 n'est pas défini pour un point, mais uniquement pour la paire IN / OUT, donc cela ne fonctionnera pas pour définir le point 0x81st sur le type d'interruption, mais sur 0x01st contrôler. Cependant, ce n'est pas un problème pour l'hôte, il enverrait probablement les mêmes données dans le point de masse aussi ... ce que, cependant, je ne vérifierai pas.



Descripteur HID



La structure du descripteur HID est très similaire au fichier de configuration "nom = valeur", mais contrairement à lui, "nom" est une constante numérique de la liste spécifique USB, et "valeur" est aussi une constante ou une variable de taille 0 jusqu'à 3 octets.



Important:pour certains "noms", la longueur de la "valeur" est spécifiée dans les 2 bits les moins significatifs du champ "nom". Par exemple, prenons LOGICAL_MINIMUM (la valeur minimale que cette variable peut prendre en mode normal). Le code de cette constante est 0x14. En conséquence, s'il n'y a pas de "valeur" (il semble que cela ne se produit pas, mais je ne discuterai pas - pour une raison quelconque, ce cas a été saisi), alors le descripteur contiendra un seul numéro 0x14. Si la "valeur" est 1 (un octet), alors 0x15, 0x01 sera écrit. Pour une valeur à deux octets, 0x1234, 0x16, 0x34, 0x12 seront écrits - la valeur est écrite de bas en haut. Eh bien, avant le tas, le numéro 0x123456 sera 0x17, 0x56, 0x34, 0x12.



Naturellement, je suis trop paresseux pour mémoriser toutes ces constantes numériques, nous allons donc utiliser des macros. Malheureusement, je n'ai jamais trouvé un moyen de les amener à déterminer eux-mêmes la taille de la valeur passée et à se développer en 1, 2, 3 ou 4 octets. Par conséquent, j'ai dû faire une béquille: une macro sans suffixe est responsable des valeurs 8 bits les plus courantes, avec un suffixe 16 pour les valeurs 16 bits et avec 24 pour les valeurs 24 bits. Des macros ont également été écrites pour des valeurs "composites" comme la plage LOGICAL_MINMAX24 (min, max), qui s'étendent sur 4, 6 ou 8 octets.



Comme pour les fichiers de configuration, il existe des «sections» appelées pages (usage_page) qui regroupent les périphériques par objectif. Par exemple, il y a une page avec des périphériques de base tels que des claviers, des souris et juste des boutons, il y a des manettes de jeu et des manettes de jeu (je recommande sincèrement de regarder lesquels! Il y en a aussi pour les chars, et pour les vaisseaux spatiaux, et pour les sous-marins et pour tout le reste ), il y a même des affichages ... Certes, où chercher des logiciels qui peuvent fonctionner avec tout cela, je n'en ai aucune idée.



Dans chaque page, un appareil spécifique est sélectionné. Par exemple, pour une souris, c'est un pointeur et des boutons, et pour une tablette - un stylet ou le doigt d'un utilisateur (quoi?!). Ils désignent également les éléments constitutifs de l'appareil. Donc, une partie du pointeur est ses coordonnées X et Y. Certaines caractéristiques peuvent être regroupées dans une "collection", mais pourquoi je ne comprends pas vraiment pourquoi cela est fait. Dans la documentation, les champs sont parfois marqués de quelques lettres sur le but du champ et comment l'utiliser:



Californie Collection (application) Informations de service, ne correspondant à aucune variable
CL Collection (logique) - / -
CP Collection (phisical) - / -
Dv Valeur dynamique valeur d'entrée ou de sortie (variable)
MC Contrôle momentané indicateur d'état (1 drapeau armé, 0 effacé)
OSC Contrôle d'un coup événement unique. Seule la transition 0-> 1 est traitée




Il y en a d'autres, bien sûr, mais ils ne sont pas utilisés dans mon exemple. Si, par exemple, le champ X est marqué comme DV, il est alors considéré comme une variable de longueur différente de zéro et sera inclus dans la structure du rapport. Les champs MC ou OSC sont également inclus dans le rapport, mais ont une taille de 1 bit.



Un rapport (paquet de données envoyé ou reçu par l'appareil) contient les valeurs de toutes les variables qui y sont décrites. La description du bouton ne parle que d'un seul bit occupé, mais pour les coordonnées relatives (combien la souris a bougé, par exemple), au moins un octet est requis, et pour les coordonnées absolues (comme pour un écran tactile), au moins 2 octets sont nécessaires. De plus, de nombreux contrôles ont leurs propres limitations physiques. Par exemple, un ADC du même écran tactile peut avoir une résolution de seulement 10 bits, c'est-à-dire donner des valeurs de 0 à 1023, que l'hôte devra mettre à l'échelle jusqu'à la résolution plein écran. Par conséquent, en plus de l'objectif de chaque champ, le descripteur spécifie également la plage de ses valeurs admissibles (LOGICAL_MINMAX), plus parfois la plage de valeurs physiques (en millimatres là-bas, ou en degrés) et la présentation dans le le rapport est obligatoire.La représentation est définie par deux nombres: la taille d'une variable (en bits) et leur nombre. Par exemple, les coordonnées de toucher l'écran tactile dans l'appareil que nous créons sont définies comme suit:



USAGE( USAGE_X ), // 0x09, 0x30,
USAGE( USAGE_Y ), // 0x09, 0x31,
LOGICAL_MINMAX16( 0, 10000 ), //0x16, 0x00, 0x00,   0x26, 0x10, 0x27,
REPORT_FMT( 16, 2 ), // 0x75, 0x10, 0x95, 0x02,
INPUT_HID( HID_VAR | HID_ABS | HID_DATA), // 0x91, 0x02,
      
      





Ici, vous pouvez voir que deux variables sont déclarées, variant dans la plage de 0 à 10000 et occupant deux sections de 16 bits dans le rapport.



Le dernier champ indique que les variables ci-dessus seront lues par l'hôte (IN) et explique exactement comment. Je ne décrirai pas ses drapeaux en détail, je ne m'attarderai que sur quelques-uns. L'indicateur HID_ABS indique que la valeur est absolue, c'est-à-dire qu'aucun historique ne l'affecte. La valeur alternative HID_REL indique que la valeur est un décalage par rapport à la précédente. L'indicateur HID_VAR indique que chaque champ est responsable de sa propre variable. La valeur alternative HID_ARR indique que les états de tous les boutons de la liste ne seront pas transmis, mais uniquement les numéros des boutons actifs. Cet indicateur s'applique uniquement aux champs à un seul bit. Au lieu de transmettre 101/102 états de tous les boutons du clavier, vous pouvez vous limiter à quelques octets avec une liste de touches enfoncées. Ensuite, le premier paramètre REPORT_FMT sera responsable de la taille du nombre, et le second du nombre.



Puisque la taille de toutes les variables est définie en bits, il est logique de se demander: qu'en est-il des boutons, car leur nombre peut ne pas être un multiple de 8, ce qui entraînera des difficultés d'alignement lors de la lecture et de l'écriture. Il serait possible d'allouer un octet à chaque bouton, mais alors le volume du rapport augmenterait considérablement, ce qui est désagréable pour les programmes à grande vitesse comme l'interruption. Au lieu de cela, les boutons sont essayés d'être placés plus près les uns des autres, et l'espace restant est rempli de bits avec l'indicateur HID_CONST.



Maintenant, nous pouvons, sinon écrire un descripteur à partir de zéro, alors au moins essayer de le lire, c'est-à-dire déterminer à quels bits tel ou tel champ correspond. Il suffit de compter les INPUT_HID et les REPORT_FMT correspondants. Gardez simplement à l'esprit que j'ai créé de telles macros, personne d'autre ne les utilise. Dans les descripteurs d'autres personnes, vous devrez rechercher des constantes input, report_size, report_count ou même numériques.



Vous pouvez maintenant apporter le descripteur complet:



static const uint8_t USB_HIDDescriptor[] = {
  //keyboard
  USAGE_PAGE( USAGEPAGE_GENERIC ),//0x05, 0x01,
  USAGE( USAGE_KEYBOARD ), // 0x09, 0x06,
  COLLECTION( COLL_APPLICATION, // 0xA1, 0x01,
    REPORT_ID( 1 ), // 0x85, 0x01,
    USAGE_PAGE( USAGEPAGE_KEYBOARD ), // 0x05, 0x07,
    USAGE_MINMAX(224, 231), //0x19, 0xE0, 0x29, 0xE7,    
    LOGICAL_MINMAX(0, 1), //0x15, 0x00, 0x25, 0x01,
    REPORT_FMT(1, 8), //0x75, 0x01, 0x95, 0x08     
    INPUT_HID( HID_DATA | HID_VAR | HID_ABS ), // 0x81, 0x02,
     //reserved
    REPORT_FMT(8, 1), // 0x75, 0x08, 0x95, 0x01,
    INPUT_HID(HID_CONST), // 0x81, 0x01,
              
    REPORT_FMT(1, 5),  // 0x75, 0x01, 0x95, 0x05,
    USAGE_PAGE( USAGEPAGE_LEDS ), // 0x05, 0x08,
    USAGE_MINMAX(1, 5), //0x19, 0x01, 0x29, 0x05,  
    OUTPUT_HID( HID_DATA | HID_VAR | HID_ABS ), // 0x91, 0x02,
    //  1 
    REPORT_FMT(3, 1), // 0x75, 0x03, 0x95, 0x01,
    OUTPUT_HID( HID_CONST ), // 0x91, 0x01,
    REPORT_FMT(8, 6),  // 0x75, 0x08, 0x95, 0x06,
    LOGICAL_MINMAX(0, 101), // 0x15, 0x00, 0x25, 0x65,         
    USAGE_PAGE( USAGEPAGE_KEYBOARD ), // 0x05, 0x07,
    USAGE_MINMAX(0, 101), // 0x19, 0x00, 0x29, 0x65,
    INPUT_HID( HID_DATA | HID_ARR ), // 0x81, 0x00,           
  )
  //touchscreen
  USAGE_PAGE( USAGEPAGE_DIGITIZER ), // 0x05, 0x0D,
  USAGE( USAGE_PEN ), // 0x09, 0x02,
  COLLECTION( COLL_APPLICATION, // 0xA1, 0x0x01,
    REPORT_ID( 2 ), //0x85, 0x02,
    USAGE( USAGE_FINGER ), // 0x09, 0x22,
    COLLECTION( COLL_PHISICAL, // 0xA1, 0x00,
      USAGE( USAGE_TOUCH ), // 0x09, 0x42,
      USAGE( USAGE_IN_RANGE ), // 0x09, 0x32,
      LOGICAL_MINMAX( 0, 1), // 0x15, 0x00, 0x25, 0x01,
      REPORT_FMT( 1, 2 ), // 0x75, 0x01, 0x95, 0x02,
      INPUT_HID( HID_VAR | HID_DATA | HID_ABS ), // 0x91, 0x02,
      REPORT_FMT( 1, 6 ), // 0x75, 0x01, 0x95, 0x06,
      INPUT_HID( HID_CONST ), // 0x81, 0x01,
                
      USAGE_PAGE( USAGEPAGE_GENERIC ), //0x05, 0x01,
      USAGE( USAGE_POINTER ), // 0x09, 0x01,
      COLLECTION( COLL_PHISICAL, // 0xA1, 0x00,         
        USAGE( USAGE_X ), // 0x09, 0x30,
        USAGE( USAGE_Y ), // 0x09, 0x31,
        LOGICAL_MINMAX16( 0, 10000 ), //0x16, 0x00, 0x00, 0x26, 0x10, 0x27,
        REPORT_FMT( 16, 2 ), // 0x75, 0x10, 0x95, 0x02,
        INPUT_HID( HID_VAR | HID_ABS | HID_DATA), // 0x91, 0x02,
      )
    )
  )
};
      
      





En plus des champs discutés précédemment, il existe également un champ aussi intéressant que REPORT_ID. Puisque, comme il ressort des commentaires, notre appareil est composite, l'hôte doit en quelque sorte déterminer les données qu'il reçoit. Pour cela, ce champ est nécessaire.



Et un autre champ sur lequel j'aimerais attirer votre attention est OUTPUT_HID. Comme son nom l'indique, il n'est pas responsable de la réception d'un rapport (IN), mais de la transmission (OUT). Il est situé dans la section du clavier et décrit les indicateurs CapsLock, NumLock, ScrollLock ainsi que deux indicateurs exotiques - Compose (un drapeau pour entrer des caractères qui n'ont pas leurs propres boutons comme á, µ ou) et Kana (entrer des hiéroglyphes) . En fait, pour le bien de ce champ, nous avons commencé le point OUT. Dans son gestionnaire, nous vérifierons si les voyants CapsLock et NumLock doivent être allumés: il n'y a que deux diodes sur la carte et sont câblés.



Il existe un troisième champ lié à l'échange de données - FEATURE_HID, nous l'avons utilisé dans le premier exemple. Si INPUT et OUTPUT sont destinés à transmettre des événements, alors FEATURE est un état qui peut être lu ou écrit. Certes, cela ne se fait pas via des points de terminaison dédiés, mais via l'ep0 habituel au moyen de requêtes appropriées.



Si vous regardez attentivement le descripteur, vous pouvez restaurer la structure du rapport. Plus précisément, deux rapports:



struct{
  uint8_t report_id; //1
  union{
    uint8_t modifiers;
    struct{
      uint8_t lctrl:1; //left control
      uint8_t lshift:1;//left shift
      uint8_t lalt:1;  //left alt
      uint8_t lgui:1;  //left gui.   hyper,   winkey
      uint8_t rctrl:1; //right control
      uint8_t rshift:1;//right shift
      uint8_t ralt:1;  //right alt
      uint8_t rgui:1;  //right gui
    };
  };
  uint8_t reserved; //        
  uint8_t keys[6]; //   
}__attribute__((packed)) report_kbd;

struct{
  uint8_t report_id; //2
  union{
    uint8_t buttons;
    struct{
      uint8_t touch:1;   //  
      uint8_t inrange:1; //   
      uint8_t reserved:6;//  1 
    };
  };
  uint16_t x;
  uint16_t y;
}__attribute__((packed)) report_tablet;
      
      





De plus, nous les enverrons en appuyant sur les boutons du tableau. puisque nous n'écrivons qu'un exemple d'implémentation, et non un dispositif complet, nous le ferons de manière barbare - en envoyant deux rapports, dans le premier desquels "appuyer" sur les touches, et dans le second - "relâcher". De plus, avec un énorme délai "stupide" entre les messages. Si vous n'envoyez pas de rapport avec les touches «relâchées», le système considérera que la touche est toujours enfoncée et la répétera. Naturellement, il n'est question d'aucune efficacité, sécurité aussi, mais cela fera l'affaire pour le test. Oh ouais, où sans un autre râteau! La taille de la structure doit correspondre à ce qui est décrit dans le descripteur, sinon Windows fera semblant de ne pas comprendre ce qu'il en attend. Comme d'habitude, Linux ignore ces erreurs et fonctionne comme si de rien n'était.



Lors des tests, je suis tombé sur un effet secondaire amusant: dans Windows7, lorsque vous cliquez sur «l'écran tactile», la fenêtre d'écriture manuscrite apparaît. Je ne connaissais pas cette fonctionnalité.



Si vous avez un appareil fini



... et je veux le regarder de l'intérieur. Tout d'abord, bien sûr, nous regardons, vous pouvez même à partir d'un utilisateur ordinaire, ConfigurationDescriptor:



lsusb -v -d <VID:PID>
      
      





Pour le descripteur HID, je n'ai pas trouvé (et n'ai pas cherché) de meilleur moyen que depuis la racine:



cat /sys/kernel/debug/hid/<address>/rdes
      
      





Par souci d'exhaustivité, il serait utile d'ajouter ici comment regarder des choses similaires dans d'autres systèmes d'exploitation. Mais je n'ai aucune connaissance pertinente, peut-être qu'ils vous le diront dans les commentaires. Il est souhaitable, bien sûr, sans installer de logiciel tiers.



Conclusion



C'est, en fait, tout ce que j'ai déterré sur HID. Le plan minimum - pour apprendre à lire des descripteurs prêts à l'emploi, émuler plusieurs appareils en même temps et implémenter l'entrée de la tablette - est terminé. Eh bien, la philosophie des points d'interruption a été considérée en même temps.



Comme au mauvais moment, j'ai laissé un peu de documentation dans le référentiel au cas où les concepteurs de l'USB-IF décideraient de ruiner à nouveau le site.



All Articles