USB sur registres: STM32L1 / STM32F1







USB de niveau encore plus bas (avr-vusb) sur les registres: point de terminaison en bloc utilisant l'exemple de stockage de masse

USB sur les registres: point de terminaison d'interruption en utilisant l'exemple de HID

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



Nous avons déjà rencontré le logiciel USB utilisant l'exemple d'AVR, il est temps de prendre des pierres plus lourdes - stm32. Nos sujets expérimentaux seront le classique STM32F103C8T6 ainsi qu'un représentant de la série basse puissance STM32L151RCT6. Comme auparavant, nous n'utiliserons pas les cartes de débogage achetées et HAL, préférant un vélo.



Puisqu'il y a deux contrôleurs dans le titre, cela vaut la peine de parler des principales différences. Tout d'abord, il s'agit d'une résistance de rappel indiquant à l'hôte USB que quelque chose y est coincé. Dans L151, il est intégré et contrôlé par le bit SYSCFG_PMC_USB_PU, mais dans F103 ce n'est pas le cas; vous devrez le souder à la carte de l'extérieur et le connecter soit à VCC, soit à la jambe du contrôleur. Dans mon cas, la jambe PA10 est passée sous mon bras. Sur quel UART1 est suspendu ... Et l'autre broche de UART1 entre en conflit avec le bouton ... J'ai jeté un tableau merveilleux, vous ne pensez pas? La deuxième différence est la quantité de mémoire flash: dans le F103 ses 64 ko, et dans le L151 jusqu'à 256 ko, que nous utiliserons un jour pour étudier les points de terminaison en masse. Ils ont également des réglages d'horloge légèrement différents et peuvent être suspendus sur différentes pattes avec des ampoules à boutons, mais ce sont déjà des bagatelles. Exemple pour F103est disponible dans le référentiel, il ne sera donc pas difficile d' adapter le reste des expériences avec le L151 pour cela. Les codes sources sont disponibles ici: github.com/COKPOWEHEU/usb



Principe général du travail avec USB



Le fonctionnement avec USB dans ce contrôleur est supposé à l'aide d'un module matériel. Autrement dit, nous lui disons quoi faire, il le fait et à la fin tire l'interruption «Je suis prêt!». En conséquence, nous n'avons pas besoin d'appeler presque quoi que ce soit à partir du main principal (bien que j'aie fourni la fonction usb_class_poll juste au cas où). Le cycle normal de travail est limité à un seul événement - l'échange de données. Le reste - réinitialisation, sommeil et autres - sont des événements exceptionnels et ponctuels.



Cette fois, je n'entrerai pas dans les détails de bas niveau de l'échange. Toute personne intéressée peut en savoir plus sur vusb. Mais permettez-moi de vous rappeler que l'échange de données ordinaires ne se fait pas par un octet, mais par paquet, et le sens de transmission est défini par l'hôte. Et il dicte également les noms de ces directions: la transmission IN signifie que l'hôte reçoit des données (et l'appareil transmet), et OUT signifie que l'hôte transmet des données (et nous recevons). De plus, chaque paquet a sa propre adresse - le numéro du point final avec lequel l'hôte veut communiquer. Pour l'instant, nous aurons un seul point de terminaison 0, responsable de l'appareil dans son ensemble (par souci de brièveté, je l'appellerai également ep0). À quoi sert le reste, je vous le dirai dans d'autres articles. Selon la norme, la taille de ep0 est strictement de 8 octets pour les appareils à faible vitesse (auxquels appartient le même vusb) et un choix de 8, 16, 32,64 octets pour une vitesse maximale comme la nôtre.



Que faire si les données sont trop petites et ne remplissent pas complètement la mémoire tampon? Tout est simple ici: en plus des données dans le paquet, leur taille est également transmise (cela peut être le champ wLength ou une combinaison de bas niveau de signaux SE0, indiquant la fin de la transmission), donc même si nous devons transférer trois octets à ep0 de 64 octets, alors exactement trois octets seront transférés ... En conséquence, nous ne gaspillerons pas de bande passante en générant des zéros inutiles. Alors ne soyez pas trop petit: si on peut se permettre de dépenser 64 octets, on passe sans hésiter. Entre autres choses, cela réduira quelque peu la charge du bus, car il est plus facile de transférer un morceau de 64 octets (plus tous les en-têtes et queues) à la fois que 8 fois 8 octets chacun (à chacun desquels, encore une fois, des en-têtes et des queues) ).



Et s'il y a trop de données au contraire? C'est plus compliqué ici. Les données doivent être divisées par la taille du point de terminaison et transférées en morceaux. Disons que la taille de ep0 est de 8 octets et que l'hôte essaie de transmettre 20 octets. À la première interruption, les octets 0-7 nous viendront, dans le deuxième 8-15, dans le troisième 16-20. Autrement dit, pour récupérer l'ensemble du colis, vous devez recevoir jusqu'à trois interruptions. Pour cela, dans le même HAL, un tampon délicat a été inventé, avec lequel j'ai essayé de le comprendre, mais après le quatrième niveau de transfert de la même chose entre les fonctions, j'ai craché. En conséquence, dans mon implémentation, la mise en mémoire tampon tombe sur les épaules du programmeur.



Mais l'hôte indique au moins toujours la quantité de données qu'il essaie de transférer. Lorsque nous transférons des données, nous devons en quelque sorte tromper les états de bas niveau des jambes pour indiquer clairement que les données sont terminées. Plus précisément, pour faire comprendre au module USB que les données sont terminées et que vous devez tirer les jambes. Cela se fait de manière évidente - en n'écrivant qu'une partie du tampon. Par exemple, si nous avons 8 octets dans le tampon, et que nous en avons écrit 4, alors évidemment nous n'avons que 4 octets de données, après quoi le module enverra la combinaison magique SE0 et tout le monde sera heureux. Et si nous écrivons 8 octets, cela signifie-t-il que nous n'avons que 8 octets, ou que ce n'est qu'une partie des données qui rentrent dans le tampon? Le module USB pense que le. Par conséquent, si nous voulons arrêter le transfert, après avoir écrit le tampon de 8 octets, nous devons écrire le prochain 0 octet. C'est ce qu'on appelle ZLP, Zero Length Packet. À quoi ça ressemble dans le code,Je vous le dirai un peu plus tard.



Organisation de la mémoire



Selon la norme, la taille du point final 0 peut atteindre 64 octets. Toute autre taille - jusqu'à 1024 octets. Le nombre de points peut également différer d'un appareil à l'autre. Le même STM32L1 prend en charge jusqu'à 7 points en entrée et 7 en sortie (sans compter ep0), soit jusqu'à 14 ko de tampons seuls. Ce qui, dans un tel volume, ne sera probablement jamais nécessaire à personne. Consommation de mémoire inacceptable! Au lieu de cela, le module USB supprime une partie de la mémoire du noyau partagée et l'utilise. Cette zone est appelée PMA (zone de mémoire de paquets) et commence par USB_PMAADDR. Et pour indiquer où se trouvent les tampons de chaque point de terminaison à l'intérieur, un tableau de 8 éléments chacun avec la structure suivante est alloué au début, et seulement ensuite la zone réelle pour les données:



typedef struct{
    volatile uint32_t usb_tx_addr;
    volatile uint32_t usb_tx_count;
    volatile uint32_t usb_rx_addr;
    volatile union{
      uint32_t usb_rx_count;
      struct{
        uint32_t rx_count:10;
        uint32_t rx_num_blocks:5;
        uint32_t rx_blocksize:1;
      };
    };
}usb_epdata_t;
      
      





Ici, vous définissez le début du tampon de transmission, sa taille, puis le début du tampon de réception et sa taille. Notez tout d'abord que usb_tx_count ne définit pas la taille réelle du tampon, mais la quantité de données à transférer. Autrement dit, notre code doit écrire des données à l'adresse usb_tx_addr, puis écrire leur taille dans usb_tx_count et seulement ensuite extraire le registre du module usb que les données sont écrites, le transférer. Faites encore plus attention au format étrange de la taille de la mémoire tampon de réception: c'est une structure dans laquelle 10 bits rx_count sont responsables de la quantité réelle de données lues, tandis que le reste est vraiment de la taille de la mémoire tampon. Il est nécessaire de connaître le morceau de fer sur lequel vous pouvez écrire et où commencent les données des autres. Le format de ce paramètre est également assez intéressant: l'indicateur rx_block_size indique dans quelles unités la taille est définie. S'il est remis à 0,puis en mots de 2 octets, alors la taille de la mémoire tampon est de 2 * rx_num_blocks, c'est-à-dire de 0 à 62. Et si elle est définie sur 1, alors dans des blocs de 32 octets, respectivement, la taille de la mémoire tampon s'avère alors être 32 * rx_num_blocks et se situe dans la plage de 32 à 512 (oui, pas jusqu'à 1024, telle est la limitation du contrôleur).



Pour placer des tampons dans cette zone, nous utiliserons une approche semi-dynamique. Autrement dit, allouer de la mémoire à la demande, mais pas la libérer (malloc / free ne suffisait pas encore à inventer!). Le début de l'espace non alloué sera indiqué par la variable lastaddr, qui pointe initialement vers le début de la PMA moins le tableau des structures discuté ci-dessus. Eh bien, chaque fois que la fonction de configuration du prochain point de terminaison usb_ep_init () est appelée, elle sera décalée de la taille de tampon qui y est spécifiée. Et la valeur souhaitée sera bien sûr entrée dans la cellule correspondante du tableau. La valeur de cette variable est réinitialisée lors d'un événement de réinitialisation, suivi d'un appel à usb_class_init (), dans lequel les points sont reconfigurés en fonction de la tâche de l'utilisateur.



Travailler avec des registres d'émission-réception



Comme il vient d'être dit, à la réception, nous lisons la quantité de données réellement reçues (le champ usb_rx_count), puis nous lisons les données elles-mêmes, puis nous tirons le module usb pour que le tampon soit libre, vous pouvez recevoir le paquet suivant. Pour la transmission, l'inverse: nous écrivons les données dans le tampon, puis définissons la quantité d'écritures dans usb_tx_count, et enfin tirons le module pour que le tampon soit plein, nous pouvons le transférer.



Le premier râteaucommencez lorsque vous travaillez avec le tampon lui-même: il n'est pas organisé en 32 bits, comme le reste du contrôleur, et non en 8 bits, comme vous pouvez vous y attendre. Et 16 bits chacun! En conséquence, il est écrit et lu sur 2 octets, aligné sur 4 octets. Merci ST d'avoir fait une telle perversion! Comme la vie serait ennuyeuse sans elle! Maintenant memcpy ordinaire est indispensable, vous devez clôturer des fonctions spéciales. À propos, si quelqu'un aime le DMA, il semble être capable de faire une telle transformation par lui-même, même si je ne l'ai pas testé.



Et puis le deuxième râteauavec l'écriture dans les registres du module. Le fait est que pour la configuration de chaque point de terminaison - pour son type (contrôle, volume, etc.) et son état - un registre USB_EPnR est responsable, c'est-à-dire que vous ne pouvez tout simplement pas en changer un peu, vous devez faire attention pour ne pas gâcher le reste. Et deuxièmement, il existe déjà quatre types de bits dans ce registre! Certains ne sont disponibles que pour la lecture (c'est génial), d'autres pour la lecture et l'écriture (également normal), d'autres ignorent l'enregistrement 0, mais lors de l'écriture de 1, ils changent l'état à l'opposé (le plaisir commence), et le quatrième, sur le au contraire, ignorez l'enregistrement 1, mais l'enregistrement 0 les remet à 0. Dites-moi, quel addict a pensé créer des bits dans un registre qui ignorent 0 et ignorent 1?! Non, je suis prêt à supposer que cela a été fait pour préserver l'intégrité du registre, lorsqu'il est accessible à la fois à partir du code et du matériel. Mais, que veux-tu,Était-ce trop paresseux de mettre l'onduleur pour que les bits soient réinitialisés en écrivant 1? Ou bien un inverseur pour que les autres bits soient inversés en écrivant 0? En conséquence, la définition de deux bits de registre ressemble à ceci (merci encore à ST pour une telle perversion):



#define ENDP_STAT_RX(num, stat) do{USB_EPx(num) = ((USB_EPx(num) & ~(USB_EP_DTOG_RX | USB_EP_DTOG_TX | USB_EPTX_STAT)) | USB_EP_CTR_RX | USB_EP_CTR_TX) ^ stat; }while(0)
      
      





Oh oui, j'ai presque oublié: ils n'ont pas non plus accès au registre par numéro. Autrement dit, les macros USB_EP0R, USB_EP1R, etc. ils l'ont fait, mais si le nombre est entré dans une variable, alors hélas. J'ai dû inventer mon propre USB_EPx () - et quoi faire.



Eh bien, pour respecter les formalités, je soulignerai que l'indicateur de disponibilité (c'est-à-dire que nous avons déjà lu les données précédentes) est défini par le masque de bits USB_EP_RX_VALID, et pour l'écriture (c'est-à-dire que nous avons écrit les données dans plein et peut être transféré) - par le masque USB_EP_TX_VALID.



Traitement des demandes IN et OUT



L'apparition d'une interruption USB peut signaler des choses différentes, mais pour l'instant nous allons nous concentrer sur les demandes de communication. L'indicateur d'un tel événement sera le bit USB_ISTR_CTR. Si nous l'avons vu, nous pouvons déterminer avec quel point l'hôte souhaite communiquer. Le numéro de point est caché sous le masque de bits USB_ISTR_EP_ID, et la direction IN ou OUT est cachée sous les bits USB_EP_CTR_TX et USB_EP_CTR_RX, respectivement.



Puisque nous pouvons avoir de nombreux points, et chacun avec son propre algorithme de traitement, nous allons créer des fonctions de rappel pour tous, qui seront appelées sur les événements correspondants. Par exemple, l'hôte a envoyé des données à endpoint3, nous lisons USB-> ISTR, en retirant que la requête est OUT et que le numéro de point est 3. Nous appelons donc epfunc_out [3] (3). Le numéro de point entre parenthèses est transmis si soudainement le code utilisateur veut accrocher un gestionnaire sur plusieurs points. Oh oui, même dans le standard USB, il est habituel de marquer les points d'entrée IN avec un 7ème bit armé. Autrement dit, le point final3 à la sortie aura le numéro 0x03 et à l'entrée - 0x83. De plus, ce sont des points différents, ils peuvent être utilisés simultanément, ils n'interfèrent pas les uns avec les autres. Eh bien, presque: dans stm32 ils ont un réglage du type (bulk, interruption, ...) pour la réception et la transmission. Donc, le même point 0x83th IN correspondra au rappel 'à epfunc_in [3] (3 | 0x80).



Le même principe s'applique pour ep0. La seule différence est que son traitement a lieu à l'intérieur de la bibliothèque et non à l'intérieur du code utilisateur. Mais que se passe-t-il si vous devez traiter des demandes spécifiques comme certaines HID - ne vous embêtez pas à choisir le code de la bibliothèque? Pour cela, il existe des rappels spéciaux usb_class_ep0_out et usb_class_ep0_in, qui sont appelés à des endroits spéciaux et ont un format spécial, dont je parlerai plus près de la fin.



Il convient de mentionner un autre point pas très évident lié à l'occurrence des interruptions de traitement des paquets. Avec les requêtes OUT, tout est simple: les données sont arrivées, les voici. Mais l'interruption IN n'est pas générée lorsque l'hôte a envoyé une demande IN, mais lorsque le tampon de transmission est vide. Autrement dit, en principe, cette interruption est similaire à l'interruption de sous-exécution de la mémoire tampon UART. Par conséquent, lorsque nous voulons transférer quelque chose vers l'hôte, nous écrivons simplement les données dans le tampon de transfert, attendons l'interruption IN et ajoutons ce qui ne convient pas (n'oubliez pas le ZLP). Et d'accord, même avec les endpoints "habituels", ils sont contrôlés par le programmeur, vous pouvez les ignorer pour l'instant. Mais à travers ep0, l'échange est toujours en cours. Par conséquent, son utilisation doit être intégrée à la bibliothèque.



En conséquence, le début du transfert est effectué par la fonction ep0_send, qui écrit l'adresse du début du buffer et la quantité de données à transférer dans la variable globale, après quoi, notez, il tire lui-même l'événement IN handler pour la première fois. À l'avenir, ce gestionnaire sera appelé sur les événements matériels, mais vous devez toujours donner un coup de pouce.



Eh bien, le gestionnaire lui-même est assez simple: il écrit la prochaine donnée dans le tampon de transfert, décale l'adresse du début du tampon et réduit le nombre d'octets restants pour le transfert. Une béquille distincte est associée au même ZLP et à la nécessité de répondre à certaines demandes avec un paquet vide. Dans ce cas, la fin du transfert est indiquée par le fait que l'adresse de données est devenue NULL. Et un paquet vide - qu'il est égal à la constante ZLPP. Les deux se produisent lorsque la taille est égale à zéro, donc aucun enregistrement réel ne se produit.



Un algorithme similaire devra être implémenté lorsque vous travaillez avec d'autres points de terminaison. Mais c'est la préoccupation de l'utilisateur. Et la logique de leur travail est souvent différente de celle de travailler avec ep0, donc dans certains cas, cette option sera plus pratique que la mise en mémoire tampon au niveau de la bibliothèque.



Logique de communication USB



L'hôte détermine le fait même de la connexion par la présence d'une résistance pull-up entre toute ligne de données et l'alimentation. Il réinitialise l'appareil, lui attribue une adresse sur le bus et essaie de déterminer exactement ce qui y était coincé. Pour ce faire, il lit les descripteurs de périphérique et de configuration (et, si nécessaire, des descripteurs spécifiques). Il peut également lire les descripteurs de chaîne pour comprendre ce que l'appareil s'appelle lui-même (bien que si la paire VID: PID lui est familière, il préférerait extraire les lignes de sa base de données). Après cela, l'hôte peut charger le pilote approprié et travailler avec le périphérique dans une langue qu'il comprend. Le langage qu'il comprend comprend des demandes et des appels spécifiques à des interfaces et des points de terminaison spécifiques. Nous y reviendrons aussi, mais nous avons d'abord besoin que l'appareil soit au moins affiché dans le système.



Traitement des requêtes SETUP: DeviceDescriptor



Une personne qui a au moins un peu bricolé l'USB aurait dû se méfier pendant longtemps: COKPOWEHEU, vous parlez de requêtes IN et OUT, mais SETUP est également précisé dans la norme. Oui, mais c'est plutôt une sorte de requête OUT, spécialement structurée et destinée exclusivement au point final 0. Parlons de sa structure et de ses fonctionnalités de travail.



La structure elle-même ressemble à ceci:



typedef struct{
  uint8_t bmRequestType;
  uint8_t bRequest;
  uint16_t wValue;
  uint16_t wIndex;
  uint16_t wLength;
}config_pack_t;
      
      





Les champs de cette structure sont considérés dans de nombreuses sources, mais je vous le rappellerai tout de même.

bmRequestType est un masque de bits, les bits dans lesquels signifient ce qui suit:

7: sens de transmission. 0 - d'hôte à périphérique, 1 - de périphérique à hôte. En fait, c'est le type de la prochaine transmission, OUT ou IN.

6-5: classe de requête

0x00 (USB_REQ_STANDARD) - standard (nous ne les traiterons que pour l'instant)

0x20 (USB_REQ_CLASS) - spécifique à la classe (nous y reviendrons dans les prochains articles)

0x40 (USB_REQ_VENDOR) - spécifique au fabricant ( J'espère que nous n'aurons pas à les toucher)

4-0: interlocuteur

0x00 (USB_REQ_DEVICE) - appareil dans son ensemble

0x01 (USB_REQ_INTERFACE) - interface séparée

0x02 (USB_REQ_ENDPOINT) -



point de terminaison

bRequest - demande wValue elle-même - petit champ de données 16 bits. En cas de demandes simples, pour ne pas conduire à des transferts à part entière.

wIndex est le numéro du destinataire. Par exemple, l'interface avec laquelle l'hôte souhaite communiquer.

wLength - la taille des données supplémentaires si 16 bits de wValue ne suffisent pas.



Tout d'abord, lors de la connexion d'un périphérique, l'hôte essaie de savoir ce qui y était exactement coincé. Pour ce faire, il envoie une requête avec les données suivantes:

bmRequestType = 0x80 (requête de lecture) + USB_REQ_STANDARD (standard) + USB_REQ_DEVICE (à l'appareil dans son ensemble)

bRequest = 0x06 (GET_DESCRIPTOR) - demande de descripteur

wValue = 0x0100 (DEVICE_DESCRIPTOR) - descripteur de périphérique dans son ensemble

wIndex = 0 - non utilisé

wLength = 0 - pas de données supplémentaires

Ensuite, il envoie une demande IN, où l'appareil doit mettre la réponse. Comme nous nous en souvenons, la demande IN de l'hôte et l'interruption du contrôleur sont faiblement couplées, nous allons donc écrire la réponse immédiatement dans le tampon de l'émetteur ep0. Théoriquement, les données de ce descripteur, et de tous les autres, sont liées à un périphérique spécifique, il est donc insensé de les placer au cœur de la bibliothèque. Les requêtes correspondantes sont passées à la fonction usb_class_get_std_descr, qui renvoie au noyau un pointeur vers le début des données et leur taille. Le fait est que certains descripteurs peuvent être de taille variable. Mais DEVICE_DESCRIPTOR n'en fait pas partie. Sa taille et sa structure sont standardisées et ressemblent à ceci:



uint8_t bLength; // 
uint8_t bDescriptorType; // .    USB_DESCR_DEVICE (0x01)
uint16_t bcdUSB; // 0x0110  usb-1.1,  0x0200  2.0.     
uint8_t bDeviceClass; // 
uint8_t bDeviceSubClass; //
uint8_t bDeviceProtocol; //
uint8_t bMaxPacketSize0; // ep0
uint16_t idVendor; // VID
uint16_t idProduct; // PID
uint16_t bcdDevice_Ver; //  BCD-
uint8_t iManufacturer; //   
uint8_t iProduct; //   
uint8_t iSerialNumber; //  
uint8_t bNumConfigurations; //  (   1)
      
      





Tout d'abord, faites attention aux deux premiers champs - la taille du descripteur et son type. Ils sont typiques de presque tous les descripteurs USB (sauf pour HID, peut-être). De plus, si bDescriptorType est une constante, alors bLength doit être compté presque manuellement pour chaque descripteur. À un moment donné, je me suis fatigué de cela et une macro a été écrite



#define ARRLEN1(ign, x...) (1+sizeof((uint8_t[]){x})), x
      
      





Il calcule la taille des arguments qui lui sont passés et la remplace au lieu du premier. Le fait est que parfois les descripteurs sont imbriqués, de sorte que l'un, par exemple, nécessite une taille dans le premier octet, un autre dans 3 et 4 (nombre de 16 bits) et le troisième dans 6 et 7 (encore une fois un nombre de 16 bits) . Les macros ne se soucient pas des valeurs exactes des arguments, mais au moins le nombre doit être le même. En fait, les macros de substitution en 1, en 3 et 4, ainsi qu'en 6 et 7 octets sont également présentes, mais je montrerai leur application avec un exemple plus typique.



Pour l'instant, regardons les champs 16 bits comme VID et PID. Il est clair que mélanger des constantes 8 bits et 16 bits dans un même tableau ne fonctionnera pas, plus les endiannes ... en général, les macros viennent à nouveau à la rescousse: USB_U16 (x).



En termes de sélection VID: PID est une question délicate. Si vous envisagez de produire des produits fabriqués en série, il vaut toujours la peine d'acheter une paire personnelle. Pour un usage personnel, vous pouvez récupérer quelqu'un d'autre sur un appareil similaire. Disons que j'ai des paires d'AVR LUFA et de STM dans mes exemples. Quoi qu'il en soit, l'hôte détermine les bogues d'implémentation spécifiques plutôt que l'affectation de cette paire. Parce que l'objectif de l'appareil est décrit en détail dans un descripteur spécial.



Attention, râteau!En fait, Windows lie les pilotes à cette paire, c'est-à-dire, par exemple, que vous avez assemblé le périphérique HID, montré le système et installé les pilotes. Et puis nous avons re-flashé l'appareil sous MSD (lecteur flash) sans changer VID: PID, alors les pilotes resteront anciens et, naturellement, l'appareil ne fonctionnera pas. Il va falloir passer à la "gestion du matériel", supprimer les pilotes et forcer le système à en trouver de nouveaux. Je pense que personne ne sera surpris que Linux n'ait pas ce problème: les appareils se branchent et fonctionnent.



StringDescriptor



Une autre caractéristique intéressante des descripteurs USB est l'amour des chaînes. Dans le modèle de descripteur, ils sont désignés par le préfixe i, tel que iSerialNumber ou iPhone... Ces lignes sont incluses dans de nombreux descripteurs et, franchement, je ne sais pas pourquoi il y en a autant. De plus, lorsque l'appareil est connecté, seuls iManufacturer, iProduct et iSerialNumber seront visibles. Quoi qu'il en soit, les chaînes sont les mêmes descripteurs (c'est-à-dire que les champs bLength et bDescriptorType sont présents), mais au lieu de la structure supplémentaire, il existe un flux de caractères de type Unicode 16 bits. La signification de cette perversion est encore une fois incompréhensible pour moi, car ces noms sont généralement donnés en anglais de toute façon, là où l'ASCII 8 bits suffirait. D'accord, vous voulez un jeu de caractères étendu, donc UTF-8 serait pris. Des gens étranges ... Pour la formation pratique des chaînes, il est pratique d'utiliser - devinez quoi - bien, des macros. Mais cette fois pas ma conception, mais des espions d'EddyEm. Puisque les chaînes sont des descripteurs, l'hôte les demandera en tant que descripteurs normaux,seulement dans le champ wValue remplacera 0x0300 (STRING_DESCRIPTOR). Et au lieu de l'octet le moins significatif, il y aura l'index de chaîne réel. Par exemple, la requête 0x0300 est une chaîne d'index 0 (elle est réservée à la langue du périphérique et est presque toujours égale à u "\ x0409") et la requête 0x0302 est une chaîne d'index 2.



Attention, râteau! Quelle que soit la tentation de ne coller qu'une chaîne dans iSerialNumber, même une chaîne avec une version honnête comme u `` 1.2.3 '' - ne le faites pas! Certains systèmes d'exploitation pensent qu'il ne devrait y avoir que des chiffres hexadécimaux, c'est-à-dire «0» - «9», «A» - «Z» et c'est tout. Vous ne pouvez même pas les points. Probablement, ils comptent en quelque sorte le hachage de ce "numéro" afin de l'identifier lors de la reconnexion, je ne sais pas. Mais j'ai remarqué un tel problème lors des tests sur une machine virtuelle avec Windows 7, elle a considéré l'appareil défectueux. Fait intéressant, Windows XP et 10 n'ont pas remarqué le problème.



ConfigurationDescriptor



Du point de vue de l'hôte, le périphérique représente un ensemble d'interfaces séparées, dont chacune est conçue pour résoudre un problème. Un descripteur d'interface décrit son appareil et les points de terminaison associés. Oui, les points de terminaison ne sont pas décrits par eux-mêmes, mais uniquement dans le cadre de l'interface. En règle générale, les interfaces avec une architecture complexe sont contrôlées par des requêtes SETUP (c'est-à-dire via ep0), dans lesquelles le champ wIndex correspond au numéro d'interface. Le maximum est autorisé à empocher le point final pour les interruptions. Et à partir des interfaces de données, l'hôte n'a besoin que de descriptions des points de terminaison et l'échange passera par eux.



Il peut y avoir plusieurs interfaces dans un même appareil, et des interfaces très différentes. Par conséquent, afin de ne pas se tromper où une interface se termine et une autre commence, le descripteur indique non seulement la taille de "l'en-tête", mais aussi séparément (généralement 3-4 octets) la taille totale de l'interface. Ainsi, l'interface se plie comme une poupée gigogne: à l'intérieur d'un conteneur commun (qui stocke la taille du "titre", bDescriptorType et la taille totale du contenu, y compris le titre), il peut y avoir quelques conteneurs plus petits, mais disposés en de la même façon. Et à l'intérieur de plus en plus. Voici un exemple de descripteur pour un périphérique HID primitif:




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
    0, // bNumEndpoints
    HIDCLASS_HID, // bInterfaceClass: 
    HIDSUBCLASS_NONE, // bInterfaceSubClass: 
    HIDPROTOCOL_NONE, // bInterfaceProtocol: 
    0x00, // iInterface
  )
  ARRLEN1(
    bLENGTH, //bLength
    USB_DESCR_HID, //bDescriptorType
    USB_U16(0x0101), //bcdHID
    0, //bCountryCode
    1, //bNumDescriptors
    USB_DESCR_HID_REPORT, //bDescriptorType
    USB_U16( sizeof(USB_HIDDescriptor) ), //wDescriptorLength
  )
  )
};
      
      





Ici, le niveau d'imbrication est petit, et aucun point de terminaison n'est décrit - eh bien, j'ai donc essayé de choisir un appareil plus simple. Une certaine confusion peut être causée ici par les constantes bLENGTH et wTOTALLENGTH égales à des zéros de huit et seize bits. Puisque dans ce cas des macros sont utilisées pour calculer la taille, il serait étrange de dupliquer leur travail et de compter les octets à la main. Comme c'est étrange d'écrire des zéros. Et les constantes sont une chose notable, contribuant à la clarté du code.



Comme vous pouvez le voir, ce descripteur se compose de l '"en-tête" USB_DESCR_CONFIG (stockant la taille complète du contenu y compris lui-même!), L'interface USB_DESCR_INTERFACE (décrivant les détails de l'appareil) et USB_DESCR_HID, qui en termes généraux indique quel type de HID nous rendons. Et exactement ce qu'en termes généraux: une structure HID spécifique est décrite dans un descripteur spécial HID_REPORT_DESCRIPTOR, que je ne considérerai pas ici, simplement parce que je le connais trop mal. Nous nous limiterons donc au copier-coller à partir d'un exemple .



Revenons aux interfaces. Étant donné qu'ils ont des nombres, il est logique de supposer qu'il peut y avoir plusieurs interfaces dans un seul appareil. De plus, ils peuvent être responsables à la fois d'une tâche commune (par exemple, l'interface de contrôle USB-CDC et l'interface de données), et de tâches fondamentalement indépendantes. Dites, rien ne nous empêche (sauf pour le manque de connaissances jusqu'à présent) sur un contrôleur d'implémenter deux adaptateurs USB-CDC plus une clé USB plus, par exemple, un clavier. De toute évidence, l'interface du lecteur flash ne connaît pas le port COM. Cependant, il y a des écueils ici, que, je l'espère, nous examinerons un jour. Il est également intéressant de noter qu'une interface peut avoir plusieurs configurations alternatives (bAlternateSetting) qui diffèrent, par exemple, par le nombre de points d'extrémité ou la fréquence de leur interrogation. En fait, c'est pour cela que cela a été fait: si l'hôte pense qu'il vaut mieux économiser la bande passante,il peut basculer l'interface vers le mode alternatif qu'il préfère.



Communication avec HID



De manière générale, les appareils HID simulent des objets du monde réel, qui n'ont pas tant de données qu'un ensemble de certains paramètres qui peuvent être mesurés ou définis (requêtes SET_REPORT / GET_REPORT) et qui peuvent notifier l'hôte d'un événement externe soudain (INTERRUPT). Ainsi, en fait, ces appareils ne sont pas destinés à l'échange de données ... mais qui l'a arrêté quand!



Nous n'aborderons pas les interruptions pour le moment, car elles ont besoin d'un point de terminaison spécial. Mais nous envisagerons de lire et de définir les paramètres. Dans ce cas, il n'y a qu'un seul paramètre, qui est une structure de deux octets, qui, par conception, sont responsables de deux LED, ou d'un bouton et d'un compteur.



Commençons par un plus simple - lecture sur demande HIDREQ_GET_REPORT. En fait, c'est la même requête que n'importe quel DEVICE_DESCRIPTOR, uniquement spécifique au HID. De plus, cette demande n'est pas adressée à l'appareil dans son ensemble, mais à l'interface. Autrement dit, si nous avons implémenté plusieurs périphériques HID indépendants dans un seul périphérique, ils peuvent être distingués par le champ wIndex de la demande. Certes, ce n'est pas la meilleure approche spécifiquement pour HID: il est plus facile de rendre le descripteur lui-même composite. Dans tous les cas, nous sommes loin de telles perversions, donc nous n'analyserons même pas quoi et où l'hôte a tenté d'envoyer: pour toute requête à l'interface et avec le champ bRequest égal à HIDREQ_GET_REPORT, nous retournerons les données réelles. En théorie, cette approche est destinée à renvoyer des descripteurs (avec tous les bLength et bDescriptorType), mais dans le cas de HID, les développeurs ont décidé de tout simplifier et d'échanger uniquement des données.Nous renvoyons donc un pointeur sur notre structure et sa taille. Eh bien, une petite logique supplémentaire comme des boutons de traitement et un compteur de requêtes.



Un cas plus complexe est une demande d'écriture. C'est la première fois que nous rencontrons des données supplémentaires dans une requête SETUP. Autrement dit, le cœur de notre bibliothèque doit d'abord lire la requête elle-même, et seulement ensuite les données. Et transférez-les dans la fonction utilisateur. Et je vous rappelle que nous n'avons pas de tampon. À la suite d'une magie de bas niveau, l'algorithme suivant a été développé. Le rappel sera toujours appelé, mais nous lui dirons de quel octet les données se trouvent maintenant dans le tampon de réception du point final (offset) et la taille de ces données (taille). Autrement dit, lorsque la demande elle-même est reçue, les valeurs de décalage et de taille sont nulles (il n'y a pas de données). Lorsque le premier paquet est reçu, le décalage est toujours nul et la taille est la taille des données reçues. Pour le second, offset sera égal à la taille de ep0 (car si les données devaient être fractionnées, ils le feront en fonction de la taille du point final), et la taille sera égale à la taille des données reçues.Etc. Important! Si les données sont acceptées, elles doivent être lues. Cela peut être fait soit par le gestionnaire en appelant usb_ep_read () et en retournant 1 (ils disent «je pensais là-bas moi-même, ne vous inquiétez pas»), ou simplement en retournant 0 («je n'ai pas besoin de ces données») sans lire - puis le noyau de la bibliothèque s'occupera du nettoyage. La fonction est construite sur ce principe: elle vérifie si les données sont disponibles et, le cas échéant, les lit et allume les LED.



Logiciel d'échange de données



Ici , je ne l' ai pas réinventer la roue, mais a pris un ready-made programme du précédent article .



Conclusion



C'est, en fait, tout. J'ai dit les bases du travail avec USB en utilisant un module matériel dans STM32, j'ai également touché un râteau. Compte tenu de la quantité de code beaucoup plus petite que l'horreur générée par STMCube, il sera plus facile de le comprendre. En fait, je ne l'ai toujours pas compris dans les nouilles Cube, il y a trop d'appels de la même chose dans différentes combinaisons. Beaucoup mieux pour comprendre l' option d'EddyEm , à partir de laquelle j'ai commencé. Bien sûr, il n'y a pas de jambages, mais au moins il convient à la compréhension. Je me vante également que la taille de ma version est presque 5 fois plus petite que celle de ST (~ 2,7 ko contre 14) - malgré le fait que je n'ai pas été impliqué dans l'optimisation et, bien sûr, vous pouvez toujours la réduire.



Je voudrais également noter la différence de comportement des différents systèmes d'exploitation lors de la connexion d'équipements douteux. Linux fonctionne juste même s'il y a des erreurs dans les descripteurs. Windows XP, 7, 10, à la moindre erreur, ils jurent que "l'appareil est cassé, je refuse de travailler avec". Et XP parfois même dans BSOD est tombé d'indignation. Oh, oui, ils affichent aussi constamment "l'appareil peut fonctionner plus vite", je ne sais pas quoi faire à ce sujet. En général, peu importe la qualité de Linux pour le développement, il pardonne trop, il est nécessaire de tester sur des systèmes moins conviviaux.



Autres plans: considérez d'autres types de points de terminaison (jusqu'à présent, il n'y avait qu'un exemple avec Control); considérez d'autres contrôleurs (disons, j'ai toujours at90usb162 (AVR) et gd32vf103 (RISC_V) qui traînent), mais ce sont des plans très éloignés. Ce serait également bien d'examiner de plus près les périphériques USB individuels comme les mêmes HID, mais pas non plus une tâche prioritaire.



All Articles