<image avec carte et casque>
Niveau encore plus bas (avr-vusb): habr.com/ru/post/460815
USB sur registres: STM32L1 / STM32F1
USB sur registres: point de terminaison en bloc utilisant l'exemple de stockage de masse
USB sur registres: point de terminaison d'interruption sur l'exemple HID
Aujourd'hui, regardons le dernier type de point final, isochrone. Il est conçu pour transférer des données essentielles au délai de livraison, mais ne garantit pas son succès. L'exemple le plus classique est celui des appareils audio: haut-parleurs, microphones.
Curieusement, ce type de point final s'est avéré être le plus stimulant (et c'est après tout ce que j'ai vu avec stm'kami!). Néanmoins, aujourd'hui, nous allons fabriquer un périphérique audio et en même temps terminer légèrement le noyau de la bibliothèque USB. Comme d'habitude, les codes sources sont disponibles:
github.com/COKPOWEHEU/usb/tree/main/4.Audio_L1
github.com/COKPOWEHEU/usb/tree/main/4.Audio_F1
Raffinement du noyau
Il est nécessaire d'affiner le noyau car STM ne peut avoir que des points isochrones avec double tamponnage, c'est-à-dire, grosso modo, il est impossible de rendre 0x01 isochrone, et de contrôler 0x81. C'est-à-dire qu'il est bien sûr possible d'écrire cela dans le descripteur USB, mais cela ne changera pas l'intérieur du contrôleur, et l'adresse réelle du point différera simplement de celle visible de l'extérieur. Ce qui, bien sûr, augmentera le risque d'erreurs, donc nous ne serons pas pervertis dans cette direction.
Il convient de noter que le double tampon se produit non seulement pour les points isochrones, mais également pour le volume, et il s'active et fonctionne différemment. Si les points isochrones activent automatiquement la mise en mémoire tampon, simplement parce qu'ils ne peuvent pas faire autrement, alors pour le paramètre de masse correspondant, vous devez utiliser le bit USB_EP_KIND spécial, qui doit être défini avec le paramètre de type de point réel.
En soi, la mise en mémoire tampon signifie que si auparavant un point correspondait à un tampon pour la transmission et un pour la réception, maintenant les deux tampons fonctionneront soit pour la transmission, soit pour la réception, et ils ne fonctionneront qu'ensemble. En conséquence, le réglage d'un point mis en mémoire tampon est très différent de celui habituel, car vous devez configurer non pas un tampon, mais deux. Par conséquent, nous ne sculpterons pas les conditions inutiles dans l'initialisation habituelle, mais créerons une fonction distincte usb_ep_init_double () basée sur celle-ci.
La réception et la transmission des paquets ne diffèrent pas tellement, bien qu'il ait fallu beaucoup plus de temps d'abord pour essayer de comprendre comment cela devrait fonctionner selon la logique ST, puis pour ajuster le sort d'Internet pour le faire fonctionner. Comme mentionné précédemment, si pour un point ordinaire deux tampons sont indépendants et diffèrent dans le sens de l'échange, alors pour un point tamponné, ils sont identiques et ne diffèrent que par le décalage. Modifions donc un peu les fonctions usb_ep_write et usb_ep_read pour qu'elles acceptent non pas un numéro de point, mais un numéro de décalage. Autrement dit, si auparavant ces fonctions supposaient l'existence de huit points doubles, maintenant - 16 points simples. En conséquence, le numéro de la nouvelle "demi-ligne" pour l'écriture est juste le numéro de l'habituel, multiplié par deux, et pour usb_ep_read il faut aussi en ajouter un (voir l'allocation des tampons dans le PMA). En fait,ceci est fait par les fonctions en ligne usb_ep_write et usb_ep_read pour les points réguliers. Mais regardons de plus près la logique tamponnée.
Selon la documentation, un tampon d'un tel point est disponible pour le matériel, le second pour le logiciel. Ensuite, ils commutent et encore une fois ne se gênent pas. Pour le point OUT, l'indicateur côté matériel est le bit USB_EP_DTOG_RX, qui doit être lu afin de comprendre lequel des tampons vient de terminer l'écriture et d'où le logiciel peut lire, respectivement. Lorsqu'il lit son tampon, vous devez secouer le bit USB_EP_DTOG_TX, qui change en fait les tampons. Je ne sais pas si c'est ce que l'on veut dire, mais cela fonctionne au moins.
Une situation symétrique aurait dû être avec des points IN. Mais en pratique, il s'est avéré que vous deviez vérifier et extraire USB_EP_DTOG_RX. Pourquoi pas TX je ne comprends toujours pas ... Merci à l'utilisateur kuzulis pour le lien vers github.com/dmitrystu/libusb_stm32/edit/master/src/usbd_stm32f103_devfs.c
En raison de la fonction en ligne, aucune surcharge spéciale n'a été ajoutée, à part l'initialisation. Mais vous pouvez, si vous le souhaitez, le jeter avec les indicateurs de l'éditeur de liens. Ou vous n'avez pas besoin de le jeter: il ne prend pas beaucoup de place et il n'est appelé que lors de l'initialisation. Ce n'est pas un HAL pour vous, où les fonctions sont non seulement lourdes, mais s'appellent également les unes les autres tout le temps.
En conséquence, les terminaux ont appris à travailler en mode tampon ... si vous ne leur respirez pas trop fort.
Pour l'utilisateur, la différence est petite: au lieu de usb_ep_init, utilisez usb_ep_init_double, et au lieu de usb_ep_write et usb_ep_read, utilisez respectivement usb_ep_write_double et usb_ep_read_double.
Périphérique AudioDevice
Et maintenant, lorsque nous avons déterminé le râteau technique, passons à la chose la plus intéressante: configurer un périphérique audio.
Selon la norme USB, un périphérique audio est un ensemble d'entités connectées les unes aux autres dans une certaine topologie, à travers laquelle passe le signal audio. Chaque entité a son propre numéro unique (bTerminalID, alias UnitID), par lequel d'autres entités ou points de terminaison peuvent s'y connecter, l'hôte l'utilise également s'il souhaite modifier certains paramètres. Et il est considéré comme le seul moyen de sortir de cette entité. Mais il peut n'y avoir aucune entrée du tout (s'il s'agit d'une borne d'entrée), ou il peut y en avoir plusieurs (bSourceID). En fait, en écrivant les numéros des entités à partir desquelles l'actuelle reçoit un signal audio dans le tableau bSourceID, nous décrivons la topologie entière, qui en conséquence peut s'avérer très rapide. Par exemple, je vais donner la topologie d'une carte son USB achetée (les numéros indiquent bTerminalID / UnitID):
lsusb et son décryptage
Bus 001 Device 014: ID 0d8c:013c C-Media Electronics, Inc. CM108 Audio Controller
#
Device Descriptor:
bLength 18
bDescriptorType 1
bcdUSB 1.10
bDeviceClass 0
bDeviceSubClass 0
bDeviceProtocol 0
bMaxPacketSize0 8
idVendor 0x0d8c C-Media Electronics, Inc.
idProduct 0x013c CM108 Audio Controller
bcdDevice 1.00
iManufacturer 1
iProduct 2
iSerial 0
bNumConfigurations 1
#
Configuration Descriptor:
bLength 9
bDescriptorType 2
wTotalLength 0x00fd
bNumInterfaces 4 #
bConfigurationValue 1
iConfiguration 0
bmAttributes 0x80
(Bus Powered)
MaxPower 100mA
# 0 -
Interface Descriptor:
bLength 9
bDescriptorType 4
bInterfaceNumber 0
bAlternateSetting 0
bNumEndpoints 0
bInterfaceClass 1 Audio
bInterfaceSubClass 1 Control Device
bInterfaceProtocol 0
iInterface 0
AudioControl Interface Descriptor:
bLength 10
bDescriptorType 36
bDescriptorSubtype 1 (HEADER)
bcdADC 1.00
wTotalLength 0x0064
bInCollection 2 # ! (2)
baInterfaceNr(0) 1 #
baInterfaceNr(1) 2 #
##### #####
# 1 InputTerminal (USB, )
AudioControl Interface Descriptor:
bLength 12
bDescriptorType 36
bDescriptorSubtype 2 (INPUT_TERMINAL)
bTerminalID 1 #
wTerminalType 0x0101 USB Streaming
bAssocTerminal 0
bNrChannels 2 #
wChannelConfig 0x0003 # -
Left Front (L)
Right Front (R)
iChannelNames 0
iTerminal 0
# 2 InputTerminal ()
AudioControl Interface Descriptor:
bLength 12
bDescriptorType 36
bDescriptorSubtype 2 (INPUT_TERMINAL)
bTerminalID 2
wTerminalType 0x0201 Microphone
bAssocTerminal 0
bNrChannels 1
wChannelConfig 0x0001
Left Front (L)
iChannelNames 0
iTerminal 0
# 6 OutputTerminal (), 9
AudioControl Interface Descriptor:
bLength 9
bDescriptorType 36
bDescriptorSubtype 3 (OUTPUT_TERMINAL)
bTerminalID 6
wTerminalType 0x0301 Speaker
bAssocTerminal 0
bSourceID 9 #
iTerminal 0
# 7 OutputTerminal (USB), 8
AudioControl Interface Descriptor:
bLength 9
bDescriptorType 36
bDescriptorSubtype 3 (OUTPUT_TERMINAL)
bTerminalID 7
wTerminalType 0x0101 USB Streaming
bAssocTerminal 0
bSourceID 8
iTerminal 0
# 8 Selector, 10
AudioControl Interface Descriptor:
bLength 7
bDescriptorType 36
bDescriptorSubtype 5 (SELECTOR_UNIT)
bUnitID 8
bNrInPins 1 #
baSourceID(0) 10 #
iSelector 0
# 9 Feature, 15
AudioControl Interface Descriptor:
bLength 10
bDescriptorType 36
bDescriptorSubtype 6 (FEATURE_UNIT)
bUnitID 9
bSourceID 15
bControlSize 1
bmaControls(0) 0x01
Mute Control
bmaControls(1) 0x02
Volume Control
bmaControls(2) 0x02
Volume Control
iFeature 0
# 10 Feature, 2
AudioControl Interface Descriptor:
bLength 9
bDescriptorType 36
bDescriptorSubtype 6 (FEATURE_UNIT)
bUnitID 10
bSourceID 2
bControlSize 1
bmaControls(0) 0x43
Mute Control
Volume Control
Automatic Gain Control
bmaControls(1) 0x00
iFeature 0
# 13 Feature, 2
AudioControl Interface Descriptor:
bLength 9
bDescriptorType 36
bDescriptorSubtype 6 (FEATURE_UNIT)
bUnitID 13
bSourceID 2
bControlSize 1
bmaControls(0) 0x03
Mute Control
Volume Control
bmaControls(1) 0x00
iFeature 0
# 15 Mixer, 1 13
AudioControl Interface Descriptor:
bLength 13
bDescriptorType 36
bDescriptorSubtype 4 (MIXER_UNIT)
bUnitID 15
bNrInPins 2 #
baSourceID(0) 1 #
baSourceID(1) 13
bNrChannels 2
wChannelConfig 0x0003
Left Front (L)
Right Front (R)
iChannelNames 0
bmControls(0) 0x00
iMixer 0
##### #####
# 1 () -
Interface Descriptor:
bLength 9
bDescriptorType 4
bInterfaceNumber 1
bAlternateSetting 0
bNumEndpoints 0
bInterfaceClass 1 Audio
bInterfaceSubClass 2 Streaming
bInterfaceProtocol 0
iInterface 0
# 1 () -
Interface Descriptor:
bLength 9
bDescriptorType 4
bInterfaceNumber 1
bAlternateSetting 1
bNumEndpoints 1
bInterfaceClass 1 Audio
bInterfaceSubClass 2 Streaming
bInterfaceProtocol 0
iInterface 0
AudioStreaming Interface Descriptor:
bLength 7
bDescriptorType 36
bDescriptorSubtype 1 (AS_GENERAL)
bTerminalLink 1
bDelay 1 frames
wFormatTag 0x0001 PCM
AudioStreaming Interface Descriptor:
bLength 14
bDescriptorType 36
bDescriptorSubtype 2 (FORMAT_TYPE)
bFormatType 1 (FORMAT_TYPE_I)
bNrChannels 2
bSubframeSize 2
bBitResolution 16
bSamFreqType 2 Discrete
tSamFreq[ 0] 48000
tSamFreq[ 1] 44100
Endpoint Descriptor:
bLength 9
bDescriptorType 5
bEndpointAddress 0x01 EP 1 OUT
bmAttributes 9
Transfer Type Isochronous
Synch Type Adaptive
Usage Type Data
wMaxPacketSize 0x00c8 1x 200 bytes
bInterval 1
bRefresh 0
bSynchAddress 0
AudioStreaming Endpoint Descriptor:
bLength 7
bDescriptorType 37
bDescriptorSubtype 1 (EP_GENERAL)
bmAttributes 0x01
Sampling Frequency
bLockDelayUnits 1 Milliseconds
wLockDelay 0x0001
# 2 () -
Interface Descriptor:
bLength 9
bDescriptorType 4
bInterfaceNumber 2
bAlternateSetting 0
bNumEndpoints 0
bInterfaceClass 1 Audio
bInterfaceSubClass 2 Streaming
bInterfaceProtocol 0
iInterface 0
# 2 ()
Interface Descriptor:
bLength 9
bDescriptorType 4
bInterfaceNumber 2
bAlternateSetting 1
bNumEndpoints 1
bInterfaceClass 1 Audio
bInterfaceSubClass 2 Streaming
bInterfaceProtocol 0
iInterface 0
AudioStreaming Interface Descriptor:
bLength 7
bDescriptorType 36
bDescriptorSubtype 1 (AS_GENERAL)
bTerminalLink 7
bDelay 1 frames
wFormatTag 0x0001 PCM
AudioStreaming Interface Descriptor:
bLength 14
bDescriptorType 36
bDescriptorSubtype 2 (FORMAT_TYPE)
bFormatType 1 (FORMAT_TYPE_I)
bNrChannels 1
bSubframeSize 2
bBitResolution 16
bSamFreqType 2 Discrete
tSamFreq[ 0] 48000
tSamFreq[ 1] 44100
Endpoint Descriptor:
bLength 9
bDescriptorType 5
bEndpointAddress 0x82 EP 2 IN
bmAttributes 9
Transfer Type Isochronous
Synch Type Adaptive
Usage Type Data
wMaxPacketSize 0x0064 1x 100 bytes
bInterval 1
bRefresh 0
bSynchAddress 0
AudioStreaming Endpoint Descriptor:
bLength 7
bDescriptorType 37
bDescriptorSubtype 1 (EP_GENERAL)
bmAttributes 0x01
Sampling Frequency
bLockDelayUnits 0 Undefined
wLockDelay 0x0000
##### #####
# 3 " " ( )
Interface Descriptor:
bLength 9
bDescriptorType 4
bInterfaceNumber 3
bAlternateSetting 0
bNumEndpoints 1
bInterfaceClass 3 Human Interface Device
bInterfaceSubClass 0
bInterfaceProtocol 0
iInterface 0
HID Device Descriptor:
bLength 9
bDescriptorType 33
bcdHID 1.00
bCountryCode 0 Not supported
bNumDescriptors 1
bDescriptorType 34 Report
wDescriptorLength 60
Report Descriptors:
** UNAVAILABLE **
Endpoint Descriptor:
bLength 7
bDescriptorType 5
bEndpointAddress 0x87 EP 7 IN
bmAttributes 3
Transfer Type Interrupt
Synch Type None
Usage Type Data
wMaxPacketSize 0x0004 1x 4 bytes
bInterval 2
Nous allons faire quelque chose de plus simple (j'ai pris le blanc d'ici ):
Ici, vous pouvez voir deux branches de propagation de signal indépendantes: soit de l'USB via une «fonctionnalité» à un «haut-parleur», soit d'un «microphone» à une autre «fonctionnalité» ”Sur USB. Le microphone et le haut-parleur ne sont pas simplement mis entre guillemets: ils ne sont pas sur ma carte de débogage, donc au lieu du son lui-même, nous utiliserons des boutons et des LED. Cependant, rien de nouveau. Les "fonctionnalités" dans mon cas ne font rien et sont ajoutées plus pour la beauté.
Il convient de préciser tout de suite que le signal dans ce modèle est considéré comme étant composé d'un ou plusieurs canaux logiques. Autrement dit, si, par exemple, je change un haut-parleur mono en un haut-parleur stéréo, la topologie elle-même restera inchangée, seul le format du signal changera.
Je n'ai pas creusé profondément dans les différences entre les types de «fonctionnalités» et d'autres entités, mais je ne dédaignerai pas de raconter un morceau de documentation.
1. Borne d'entrée
Comme son nom l'indique, c'est par son intermédiaire que le signal audio entre dans l'appareil audio. Cela peut être USB, cela peut être un microphone ordinaire, un microphone casque, même une matrice de microphones.
2. Terminal de sortie
Il est également assez évident - celui par lequel le son quitte notre appareil. Il peut s'agir du même USB, d'un haut-parleur, d'un casque, d'un haut-parleur dans le moniteur, de haut-parleurs de différentes fréquences et d'un tas d'autres appareils.
3. Unité de mixage
Il prend plusieurs signaux d'entrée, amplifie chacun d'eux d'une quantité prédéterminée et ajoute le résultat au canal de sortie. Si vous le souhaitez, vous pouvez régler le gain à zéro, ce qui le réduira à l'entité suivante.
4. L'unité de sélection
Prend plusieurs signaux d'entrée et redirige l'un d'entre eux vers la sortie.
5. Filtre (Feature Unit)
Prend un seul signal d'entrée, modifie les paramètres du son (volume, tonalité, etc.) et le transmet à la sortie. Naturellement, tous ces paramètres sont appliqués à l'ensemble du signal de la même manière, sans l'interaction des canaux logiques en son sein.
6. Unité de traitement
Mais cette chose vous permet déjà de manipuler des canaux logiques individuels dans chaque entrée. De plus, cela vous permet de rendre le nombre de canaux logiques dans la sortie différent du nombre dans l'entrée.
7. Unité d'extension
L'ensemble des entités non standard, de sorte que le fantasme maladif des fabricants d'équipement était libre. En conséquence, le comportement et les paramètres dépendront de cette fantaisie même.
Certaines entités ont des paramètres comme le gain ou le numéro de canal, qui peuvent être influencés par l'hôte à l'aide des requêtes setFeature / getFeature sur le numéro d'entité. Mais ici, pour être honnête, je ne comprends pas vraiment comment le vérifier. Vous avez probablement besoin d'une sorte de logiciel spécial, que je n'ai pas. Bon, d'accord, de toute façon je me suis lancé juste pour vérifier tous les types de points ... sur ma tête ...
Râteau dans le descripteur
Contrairement aux périphériques USB précédents, le descripteur ici est complexe, multicouche et a tendance à effrayer Windows en BSOD. Comme nous l'avons vu plus haut, la topologie d'un appareil autologue peut être assez complexe et étendue. Une interface entière se distingue par sa description. Évidemment, il ne contiendra pas de points de terminaison, mais il contiendra une liste de descripteurs d'entités et des descriptions de ce à quoi leurs entrées sont connectées. Je ne vois pas beaucoup de sens ici, il est plus facile de regarder le code et la documentation. Je ne noterai que le râteau principal: ici, il est décrit quelles interfaces avec les points d'extrémité correspondants se réfèrent spécifiquement à cet appareil. Par exemple, si vous souhaitez modifier ma configuration et supprimer le haut-parleur à partir de là, vous devrez non seulement supprimer la moitié des entités (merci les macros, au moins il n'y aura pas de problème avec le calcul de la longueur du descripteur), mais réduisez également le champ bInCollection à 1,puis supprimez le numéro de l'interface supplémentaire du tableau bInterfaceNr qui le suit.
En outre, il existe des interfaces responsables de l'échange de données. Dans mon cas, la 1ère interface est responsable du microphone, et la 2ème du haut-parleur. Il convient de prêter attention ici, tout d'abord, à deux variantes de chacune de ces interfaces. Un avec bAlternateSetting égal à 0, le second avec 1. Ils diffèrent par la présence d'un point final. Autrement dit, si notre appareil n'est pas actuellement utilisé, l'hôte bascule simplement sur cette interface alternative, qui n'est pas équipée d'un point de terminaison, et ne gaspille plus la bande passante du bus dessus.
La deuxième caractéristique des interfaces de données est le format du signal audio. Le descripteur correspondant spécifie le type de codage, le nombre de canaux, la résolution et la fréquence d'échantillonnage (qui est spécifiée par un nombre de 24 bits). Il existe de nombreuses options de codage, mais nous utiliserons la plus simple - PCM. En fait, il ne s'agit que d'une séquence de valeurs de la valeur instantanée du signal sans aucun codage, et la valeur est considérée comme un entier signé . La résolution du signal est définie à deux endroits (on ne sait pas pourquoi): le champ bSubFrameSize spécifie le nombre d'octets et bBitResolution spécifie le nombre de bits... On peut probablement remarquer que la plage de notre carte son ne va pas jusqu'à la plage complète du type de données, disons int16_t et n'est que de 10 bits.
Et enfin, le descripteur du point final réel. Il diffère également légèrement des habituels, car il fournit, d'une part, plusieurs options de synchronisation, et d'autre part, le numéro de l'entité à laquelle ce point est associé (bTerminalLink) . Les options de synchronisation sont écrites en bits de poids fort directement dans le type de point final (c'est pourquoi le point isochrone a été déplacé vers la branche par défaut dans la fonction d'initialisation), mais je n'ai pas traité de leurs détails, donc je ne peux pas vous le dire quelque chose d'intéressant. Au lieu de la synchronisation, nous utiliserons une minuterie de contrôleur régulière, qui générera des interruptions à environ la fréquence souhaitée.
Oh oui, j'ai presque oublié de mentionner un autre groupe de BSOD en testant les mauvais descripteurs. Je vous le rappelle encore: le nombre d'interfaces de données doit correspondre au numéro de bInCollection, et leurs numéros doivent correspondre au tableau qui le suit!
Texte masqué
, , . --.
La logique de l'appareil
Comme je l'ai déjà dit, pour les tests, il n'a aucun sens d'enfermer des composants articulés sur la carte de débogage, donc tous les tests seront effectués avec ce qui a déjà été installé - des boutons et des LED. Cependant, dans ce cas, cela ne constitue pas un problème: le "microphone" peut simplement générer une sinusoïde avec une fréquence de, par exemple, 1 kHz, et le "haut-parleur" allume la LED lorsque la valeur du seuil sonore est dépassée (disons , au-dessus de 10 000: avec les 16 bits de résolution spécifiés, ce qui correspond à la plage -32768 ... +32767, c'est environ un tiers).
Mais avec les tests, un petit problème est survenu: je n'ai pas trouvé de moyen facile de rediriger le signal du microphone vers le stdin d'un programme. Il semble qu'avant cela se faisait simplement en lisant / dev / dsp, mais maintenant quelque chose est cassé. Cependant, rien de critique, car il existe toutes sortes de bibliothèques d'interaction avec le multimédia - SDL, SFLM et autres. En fait, dans SFML, j'ai écrit un utilitaire simple pour lire à partir d'un microphone et, si nécessaire, visualiser le signal.
Je porterai une attention particulière aux limitations de notre appareil audio: pour autant que je sache, une requête IN isochrone est envoyée une fois par milliseconde (mais il peut y avoir beaucoup de OUT), ce qui limite le taux d'échantillonnage. Disons que la taille du point final est de 64 octets (en tenant compte de la mise en mémoire tampon, cela prend 128 octets en mémoire, mais l'hôte ne le sait pas), la résolution est de 16 bits, c'est-à-dire que 32 échantillons peuvent être envoyés à la fois . Étant donné un intervalle de 1 ms, nous obtenons une limite théorique de 32 kHz pour un canal. Le moyen le plus simple de contourner ce problème consiste à augmenter la taille du point de terminaison. Mais ici, nous devons nous rappeler que la taille du tampon PMA total n'est que de 512 octets. Moins la table de distribution des points, moins ep0, nous obtenons un maximum de 440 octets, soit 220 octets par point unique, en tenant compte de la mise en mémoire tampon. Et c'est la limite théorique.
Mais le fait que l'hôte puisse envoyer plusieurs requêtes OUT dans une même trame suggère que le périphérique peut faire de même. Reste à comprendre comment. Peut-être que cela est résolu par un paramètre de synchronisation compétent. Mais pour moi, cette question ne m'intéresse plus: les points isochrones fonctionnent, les points tamponnés fonctionnent, le périphérique audio fonctionne - la tâche est terminée.
Conclusion (commune pour le cycle)
Eh bien, nous nous sommes familiarisés avec le périphérique USB dans les contrôleurs STM32F103 et STM32L151 (et d'autres avec une implémentation similaire), avons été surpris par la logique de certaines solutions architecturales (j'ai été particulièrement impressionné par le registre USB_EPnR, cependant, le double tampon n'est pas non plus en retard derrière), a examiné tous les types de points de terminaison et les a vérifiés, en construisant les dispositifs appropriés. On peut donc dire que cette série d'articles est arrivée à une conclusion logique. Bien que cela, bien sûr, ne signifie pas que j'abandonnerai les contrôleurs ou l'USB: dans les plans lointains, je dois encore faire face à des appareils composites (jusqu'à présent, cela semble facile, mais les points isochrones ne sont pas de bon augure non plus) et USB sur les contrôleurs d'autres familles.