EHCI humainement en russe

image

Présentation


Je souhaite la bienvenue à tous. Aujourd'hui, je veux partager mon expérience et toujours, à mon avis, expliquer clairement à ce sujet, à première vue, une norme simple pour le contrôleur hôte USB 2.0.

Au départ, vous pouvez imaginer qu'un port USB 2.0 n'est qu'à 4 broches, dont deux transmettent simplement des données (comme, par exemple, un port COM), mais en fait, tout n'est pas le cas, et même le contraire. Le contrôleur USB, en principe, ne nous permet pas de transférer des données comme via un port COM standard. EHCI est une norme assez complexe qui permet un transfert de données fiable et rapide du logiciel vers l'appareil lui-même, et dans la direction opposée.

Cet article peut vous être utile si, par exemple, vous n'avez pas les compétences suffisantes en écriture pour les pilotes et en lecture de documentation pour un matériel. Un exemple simple: vous voulez écrire votre système d'exploitation pour un mini-PC, afin que certaines distributions Windows ou Linux ne téléchargent pas de fer, et vous utilisez toute sa puissance exclusivement à vos propres fins.

Qu'est-ce que EHCI?


Eh bien, commençons. EHCI - Enhanced Host Controller Interface, est conçu pour transférer des données et des demandes de contrôle vers des périphériques USB, et dans l'autre sens, et dans 99% des cas, c'est un lien entre n'importe quel logiciel et un périphérique physique. EHCI fonctionne comme un périphérique PCI et utilise en conséquence MMIO (Memory-Mapped-IO) pour contrôler le contrôleur (oui, je sais que certains périphériques PCI utilisent des ports, mais ici j'ai tout généralisé). La documentation d'Intel ne décrit que le principe de fonctionnement, et il n'y a aucune indication sur tous les algorithmes écrits au moins en pseudo-code. EHCI dispose de 2 types de registres MMIO: Capability et Operational. Les premiers servent à obtenir les caractéristiques du contrôleur, tandis que les seconds servent à le contrôler. En fait, je vais attacher l'essence même de la connexion entre le logiciel et le contrôleur EHCI:

image

Chaque contrôleur EHCI possède plusieurs ports, chacun pouvant être connecté à n'importe quel périphérique USB. Veuillez également noter que EHCI est une version améliorée de UHCI, qui a également été développée par Intel quelques années plus tôt. Pour des raisons de compatibilité descendante, tout contrôleur UHCI / OHCI qui a une version inférieure à EHCI sera un compagnon d'EHCI. Par exemple, vous avez un clavier USB (et la plupart des claviers de l'année jusqu'à présent ont été comme ça) qui fonctionne sur USB 1.1 (notez que la vitesse maximale d'USB 1.1 est de 12 mégabits par seconde, et FullSpeed ​​USB 2.0 a une bande passante jusqu'à 480 Mbps), et si vous avez un ordinateur avec un port USB 2.0, lorsque vous connectez le clavier à l'ordinateur, le contrôleur hôte EHCI fonctionnera de quelque manière que ce soit avec USB 1.1. Ce modèle est illustré dans le diagramme suivant:

image

De plus, pour l'avenir, je tiens à vous avertir immédiatement que votre pilote peut ne pas fonctionner correctement en raison d'une situation aussi absurde: vous avez initialisé UHCI, puis EHCI, tout en ajoutant deux périphériques identiques, définissez le bit de contrôle du propriétaire du port sur le registre de port, puis UHCI a cessé de fonctionner, car EHCI fait automatiquement glisser le port sur lui-même, et le port sur UHCI ne répond plus, cette situation doit être surveillée.

Examinons également un diagramme montrant l'architecture EHCI elle-même:

image

À droite est écrit sur la file d'attente - à leur sujet un peu plus tard.

Registres du contrôleur EHCI


Pour commencer, je tiens à préciser une fois de plus que grâce à ces registres, vous contrôlerez votre appareil, ils sont donc très importants - et sans eux, la programmation EHCI est impossible.

Vous devez d'abord obtenir l'adresse MMIO qui est donnée à ce contrôleur, à offset + 0x10 sera l'adresse de nos registres tant attendus. Il y a une chose: tout d'abord, les registres de capacité vont, et seulement après eux - Opérationnels, donc, au décalage 0 (à partir de l'adresse précédente, que nous avons reçue au décalage 0x10 par rapport au début du MMIO de notre EHCI), il y a un octet - la longueur des registres de capacité.

Registres de capacité


Au décalage 2, le registre HCIVERSION est localisé - le numéro de révision de ce HC, qui prend 2 octets et contient la version BCD de la révision (ce BCD peut être trouvé sur Wikipedia).
Au décalage +4, le registre HCSPARAMS est localisé , sa taille est de 2 mots, il contient les paramètres structurels de l'appareil et ses bits affichent ce qui suit:

  • Bit 16 - Indicateurs de port - LED disponibles pour les périphériques USB connectés.
  • Bits 15:12 - le numéro du contrôleur compagnon affecté à ce contrôleur
  • Bits 11: 8 - le nombre de ports sur le contrôleur compagnon
  • Bit 7 - Règles de routage des ports - montre comment ces ports sont mappés aux ports compagnons
  • Bit 4 - Contrôle de l'alimentation du port - indique s'il est nécessaire de mettre sous tension chaque port, 0 - l'alimentation est fournie automatiquement
  • Bits 3: 0 - le nombre de ports pour ce contrôleur.
  • Au décalage +8 se trouve le registre HCCPARAMS - il affiche les paramètres de compatibilité, ses bits signifient ce qui suit:
  • Bit 2 - disponibilité de la file d'attente asynchrone,
  • Bit 1 - Disponibilité de la file d'attente périodique (séquentielle)
  • Compatibilité bits 0 - 64 bits

Registres d'opération


Au décalage 0, le registre USBCMD est le registre de commande du contrôleur, ses bits signifient ce qui suit:

  • Bits 23:16 - Interrupt Threshold Control - montre combien de micro-trames seront utilisées pour une trame régulière. Le plus grand, le plus rapide, mais s'il est supérieur à 8, les micro-images seront traitées à la même vitesse que pour le 8.
  • Bit 6 - interruption après chaque transaction dans la file d'attente asynchrone,
  • Bit 5 - est la file d'attente asynchrone utilisée
  • Bit 4 - utilisation de la file d'attente séquentielle,
  • Bits 3: 2 - la taille de FrameList'a (plus à ce sujet plus tard). 0 signifie 1024 éléments, 1 - 512, 2 - 256, 3 - réservés
  • Bit 1 - défini pour réinitialiser le contrôleur hôte.
  • Bit 0 - Marche / Arrêt
.
Ensuite, au décalage +4, il y a le registre USBSTS - l'état du contrôleur hôte,

  • Le bit 15 indique si une file d'attente asynchrone est utilisée.
  • Le bit 14 indique si une file d'attente séquentielle est utilisée,
  • Bit 13 - indique qu'une file d'attente asynchrone vide a été détectée,
  • Le bit 12 est défini sur 1, si une erreur s'est produite lors du traitement de la transaction, le contrôleur hôte arrêtera toutes les files d'attente.
  • Le bit 4 est défini sur 1, si une erreur grave se produit, le contrôleur hôte arrête toutes les files d'attente.
  • Bit 3 FrameList (Register) Rollover - défini sur 1 lorsque le contrôleur hôte a traité l'intégralité de la FrameList.
  • Bit 1 - Interruption d'erreur USB - Dois-je générer une interruption d'erreur?
  • Bit 0 - Interruption USB - défini après un traitement de transaction réussi, si IOC a été installé dans TD

Pas fatigué? Vous pouvez vous verser une mouette solide et amener le foie, nous sommes au tout début!

Au décalage +8, il y a un registre USBINTR - le registre d'activation d'interruption
Afin de ne pas écrire pendant longtemps, et encore plus, pour ne pas lire pendant longtemps, les valeurs des bits de ce registre peuvent être trouvées dans la spécification, un lien vers celui-ci sera laissé ci-dessous. Ici, j'écris juste 0, car Je n'ai absolument aucune envie d'écrire des gestionnaires, des interruptions de carte, etc., donc je pense que cela est presque complètement inutile.

À l'offset +12 (0x0C), se trouve le registre FRINDEX , dans lequel se trouve simplement le numéro de trame actuel, et je tiens à noter que les 4 derniers bits affichent le numéro de micro-trame, dans les 28 bits supérieurs le numéro de trame (également la valeur n'est pas nécessairement inférieure à la taille de frameList Mais si vous avez besoin d'un index, il vaut mieux le prendre avec un masque de 0x3FF (ou 0x1FF, etc.).

Le registre CTRLDSSEGMENT est à offset + 0x10; il montre au contrôleur hôte les 32 bits les plus significatifs de l'adresse de la feuille de trame.

Le registre PERIODICLISTBASE a un décalage de + 0x14, vous pouvez y mettre les 32 bits inférieurs de la feuille de trame, notez que l'adresse doit être alignée sur la taille de la page mémoire (4096).

Le registre ASYNCLISTADDR a un décalage de + 0x18, vous pouvez y mettre l'adresse de la file d'attente asynchrone, notez qu'elle doit être alignée à la limite de 32 octets, alors qu'elle doit être dans les quatre premiers gigaoctets de mémoire physique.

Le registre CONFIGFLAG indique si le périphérique est configuré. Vous devez définir le bit 0 après avoir terminé la configuration de l'appareil, il a un décalage de + 0x40.

Passons aux registres de ports. Chaque port a son propre registre d'état de commande, chaque registre de port est décalé + 0x44 + (PortNumber - 1) * 4 , ses bits signifient ce qui suit:

  • Bit 12 - alimentation du port, 1 - l'alimentation est fournie, 0 - non.
  • Le bit 8 - Port Rest - est défini pour réinitialiser l'appareil.
  • Bit 3 - Port Enable / Disable Change - défini lors du changement de l'état de "l'inclusion" du port.
  • Bit 2 - port activé / désactivé.
  • Bit 1 - Modifier l'état de la connexion, est défini sur 1, par exemple, si vous avez connecté ou déconnecté un périphérique USB.
  • Bit 0 - état de la connexion, 1 - connecté, 0 - non.

Passons maintenant au jus lui-même.

Structures de transfert de données et de requêtes


L'organisation d'une structure de traitement des demandes comprend des files d'attente et des descripteurs de transfert (TD).

Pour le moment, nous ne considérerons que 3 structures.

Liste séquentielle


La liste séquentielle (Périodique, Péréodique) est organisée comme suit:

image

Comme vous pouvez le voir dans le diagramme, le traitement commence par l'obtention de la trame souhaitée à partir du cadre de feuille, chacun de ses éléments occupe 4 octets et a la structure suivante:

image

Comme vous pouvez le voir sur l'image, le transfert d'adresse / descripteur de file d'attente est aligné à la limite de 32 octets, le bit 0 signifie que le contrôleur hôte ne traitera pas cet élément, les bits 3: 1 indiquent le type de ce que le contrôleur hôte traitera: 0 - TD isosynchrone (iTD), 1 - tour, 2 et 3 dans cet article, je ne considérerai pas.

File d'attente asynchrone


Le contrôleur hôte traite cette file d'attente uniquement lorsque la trame séquentielle est vide ou que le contrôleur hôte a traité l'intégralité de la liste série.

Une file d'attente asynchrone est un pointeur vers une file d'attente qui contient d'autres files d'attente qui doivent être traitées. Schéma:

image

qTD (descripteur de transfert d'élément de file d'attente)


Ce TD a la structure suivante:

image

Pointeur qTD suivant - un pointeur vers la continuation de la file d'attente pour le traitement (pour l'exécution horizontale), bit 0 Le pointeur qTD suivant indique qu'il n'y a plus de file d'attente.
Jeton qTD - jeton TD, affiche les paramètres de transfert de données:

  • Bit 31 - Basculement des données (plus à ce sujet plus tard)
  • Bits 30:16 - la quantité de données à transférer, après l'achèvement de la transaction, leur valeur diminue de la quantité de données transférées.
  • Bit 15 - IOC - Interruption à la fin - provoque une interruption une fois le traitement des descripteurs terminé.
  • Les bits 14:12 indiquent le numéro du tampon actuel vers / à partir duquel les données sont échangées, plus à ce sujet plus tard.
  • Bits 11:10 - le nombre d'erreurs autorisées. Ce tableau indique quand le nombre d'erreurs diminue:

    image

    Note de bas de page 1 - la détection de Babble ou Stall arrête automatiquement l'exécution de la tête de file d'attente. Référence 3 - Les erreurs de tampon de données sont des problèmes avec l'hôte. Ils ne tiennent pas compte des tentatives de périphérique.
  • 9: 8 - Code PID - type de jeton: 0 - jeton à l'entrée (de l'hôte à l'appareil), 1 - jeton à la sortie (de l'appareil à l'hôte), 2 - jeton «SETUP»
  • Les bits 7: 0 indiquent l'état TD:
    Le bit 7 indique que le TD est dans un état actif (c'est-à-dire que le contrôleur hôte traite ce TD)
    Bit 6 - Halted - indique qu'une erreur s'est produite et que l'exécution de TD s'est arrêtée.
    Bit 4 - Babble Detected - la quantité de données que nous avons envoyées à l'appareil, ou par tour, est inférieure à ce que nous transmettons, c'est-à-dire, par exemple, l'appareil nous a envoyé 100 octets de données, et nous lisons seulement 50 octets, puis 50 autres Le bit Halted sera également défini si ce bit est défini sur 1.
    Bit 3 - Erreur de transaction - Une erreur s'est produite lors de la transaction.

qTD Buffer Page Pointer List - l'un des 5 tampons. Il contient un lien vers l'endroit où en mémoire la transaction doit être effectuée (envoyer des données à l'appareil / recevoir des données de l'appareil), toutes les adresses dans les tampons, sauf la première, doivent être alignées sur la taille de la page (4096 octets).

Responsable de ligne


La tête de file d'attente a la structure suivante:

image

Pointeur de lien horizontal de tête de file d'attente - pointeur vers la file d'attente suivante, les bits 2: 1 ont les valeurs suivantes selon le type de file d'attente:

image

Capacités / caractéristiques des terminaux - caractéristiques des files d'attente:

  • Les bits 26:16 contiennent la taille de paquet maximale pour la transmission
  • Bit 14: Data Toggle Control - indique où le contrôleur hôte doit prendre la valeur initiale de Data Toggle, 0 - ignore le bit DT dans qTD, enregistre le bit DT pour la tête de file d'attente.
  • Bit 13:12 - caractéristiques de la vitesse de transmission: image
  • Bits 11: 8 - le numéro du noeud final auquel la demande est adressée
  • Bits 6: 0 - adresse de l'appareil

Capacités de point de terminaison: Queue Head DWord 2 - suite du mot double précédent:

  • Bits 29:23 - Numéro de concentrateur
  • Bits 22:16 - Adresse du hub

Pointeur de lien qTD actuel - pointeur vers le qTD actuel.

Nous passons au plus intéressant.

Pilote EHCI


Commençons par les requêtes que l'EHCI peut répondre. Il existe 2 types de demandes: Contrôle - à la commande, et Bulk - aux points de terminaison, pour l'échange de données, par exemple, la grande majorité des lecteurs flash USB (USB MassStorage) utilisent le type de transfert de données Bulk / Bulk / Bulk. La souris et le clavier utilisent également des demandes groupées pour le transfert de données.

Initialisez EHCI et configurez les files d'attente asynchrones et séquentielles:

// Base I/O Address PciBar bar; PciGetBar(&bar, id, 0); EhciController *hc = VMAlloc(sizeof(EhciController)); hc->capRegs = (EhciCapRegs *)(uintptr_t)bar.u.address; hc->opRegs = (EhciOpRegs *)(uintptr_t)(bar.u.address + hc->capRegs->capLength); // Read the Command register //    uint cmd = ROR(usbCmdO); // Write it back, setting bit 2 (the Reset bit) //   ,   2(Reset) // and making sure the two schedule Enable bits are clear. //  ,  2   WOR(usbCmdO, 2 | cmd & ~(CMD_ASE | CMD_PSE)); // A small delay here would be good. You don't want to read //     ,     // the register before it has a chance to actually set the bit //   ,         ROR(usbCmdO); // Now wait for the controller to clear the reset bit. //      Reset while (ROR(usbCmdO) & 2); // Again, a small delay here would be good to allow the // reset to actually become complete. //   ROR(usbCmdO); // wait for the halted bit to become set //    Halted    while (!(ROR(usbStsO) & STS_HCHALTED)); //     ,        // ,           128  hc->frameList = (u32 *)VMAlloc(1024 * sizeof(u32) + 8192 * 4); hc->frameList = (((uint)hc->frameList) / 16384) * 16384 + 16384; hc->qhPool = (EhciQH *)VMAlloc(sizeof(EhciQH) * MAX_QH + 8192 * 4); hc->tdPool = (EhciTD *)VMAlloc(sizeof(EhciTD) * MAX_TD + 8192 * 4); hc->qhPool = (((uint)hc->qhPool) / 16384) * 16384 + 16384; hc->tdPool = (((uint)hc->tdPool) / 16384) * 16384 + 16384; // Asynchronous queue setup //    EhciQH *qh = EhciAllocQH(hc); //     ,      // ,    qh->qhlp = (u32)(uintptr_t)qh | PTR_QH; //  ,  ,     qh->ch = QH_CH_H; qh->caps = 0; qh->curLink = 0; qh->nextLink = PTR_TERMINATE; qh->altLink = 0; qh->token = 0; //    for (uint i = 0; i < 5; ++i) { qh->buffer[i] = 0; qh->extBuffer[i] = 0; } hc->asyncQH = qh; // Periodic list queue setup //    qh = EhciAllocQH(hc); //     qh->qhlp = PTR_TERMINATE; qh->ch = 0; qh->caps = 0; qh->curLink = 0; qh->nextLink = PTR_TERMINATE; qh->altLink = 0; qh->token = 0; //   for (uint i = 0; i < 5; ++i) { qh->buffer[i] = 0; qh->extBuffer[i] = 0; } qh->transfer = 0; qh->qhLink.prev = &qh->qhLink; qh->qhLink.next = &qh->qhLink; hc->periodicQH = qh; //        for (uint i = 0; i < 1024; ++i) hc->frameList[i] = PTR_QH | (u32)(uintptr_t)qh; kprintf("FrameList filled. Turning off Legacy BIOS support..."); // Check extended capabilities //  BIOS Legacy support uint eecp = (RCR(hccParamsO) & HCCPARAMS_EECP_MASK) >> HCCPARAMS_EECP_SHIFT; if (eecp >= 0x40) { // Disable BIOS legacy support uint legsup = PciRead32(id, eecp + USBLEGSUP); kprintf("."); if (legsup & USBLEGSUP_HC_BIOS) { PciWrite32(id, eecp + USBLEGSUP, legsup | USBLEGSUP_HC_OS); kprintf("."); for (;;) { legsup = PciRead32(id, eecp + USBLEGSUP); kprintf("."); if (~legsup & USBLEGSUP_HC_BIOS && legsup & USBLEGSUP_HC_OS) { break; } } } } kprintf("Done\n"); // Disable interrupts //   //hc->opRegs->usbIntr = 0; MWIR(ehcibase, usbIntrO, 0); // Setup frame list //     //hc->opRegs->frameIndex = 0; WOR(frameIndexO, 0); //hc->opRegs->periodicListBase = (u32)(uintptr_t)hc->frameList; WOR(periodicListBaseO, (u32)(uintptr_t)hc->frameList); //       //hc->opRegs->asyncListAddr = (u32)(uintptr_t)hc->asyncQH; WOR(asyncListAddrO, (u32)(uintptr_t)hc->asyncQH); //    0 //hc->opRegs->ctrlDsSegment = 0; WOR(ctrlDsSegmentO, 0); // Clear status //   //hc->opRegs->usbSts = ~0; WOR(usbStsO, ~0); // Enable controller //  , 8 -,  //     //hc->opRegs->usbCmd = (8 << CMD_ITC_SHIFT) | CMD_PSE | CMD_ASE | CMD_RS; WOR(usbCmdO, (8 << CMD_ITC_SHIFT) | CMD_PSE | CMD_ASE | CMD_RS); while (ROR(usbStsO)&STS_HCHALTED); // Configure all devices to be managed by the EHCI // ,   //hc->opRegs->configFlag = 1; WOR(configFlagO, 1);\ // Probe devices //   EhciProbe(hc); 

En fait, le code pour réinitialiser le port à son état d'origine:

  volatile u32 *reg = &hc->opRegs->ports[port]; //    ,  100 *reg|=(1<<12)|(1<<20); Wait(100); //  ,  50  EhciPortSet(reg, PORT_RESET | (1<<12) | (1<<20) | (1<<6)); Wait(50); EhciPortClr(reg, PORT_RESET); // Wait 100ms for port to enable (TODO - what is appropriate length of time?) //  100    ,   , //  100    uint status = 0; for (uint i = 0; i < 10; ++i) { // Delay Wait(10); // Get current status //    status = *reg; // Check if device is attached to port //      if (~status & PORT_CONNECTION) break; // Acknowledge change in status //    -    if (status & (PORT_ENABLE_CHANGE | PORT_CONNECTION_CHANGE)) { EhciPortClr(reg, PORT_ENABLE_CHANGE | PORT_CONNECTION_CHANGE); continue; } // Check if device is enabled //    ,    if (status & PORT_ENABLE) break; } return status; 

Demande de contrôle à l'appareil:

 static void EhciDevControl(UsbDevice *dev, UsbTransfer *t) { EhciController *hc = (EhciController *)dev->hc; UsbDevReq *req = t->req; // Determine transfer properties //    uint speed = dev->speed; uint addr = dev->addr; uint maxSize = dev->maxPacketSize; uint type = req->type; uint len = req->len; // Create queue of transfer descriptors //   TDs EhciTD *td = EhciAllocTD(hc); if (!td) return; EhciTD *head = td; EhciTD *prev = 0; // Setup packet //   uint toggle = 0; uint packetType = USB_PACKET_SETUP; uint packetSize = sizeof(UsbDevReq); EhciInitTD(td, prev, toggle, packetType, packetSize, req); prev = td; // Data in/out packets packetType = type & RT_DEV_TO_HOST ? USB_PACKET_IN : USB_PACKET_OUT; u8 *it = (u8 *)t->data; u8 *end = it + len; //EhciPrintTD(td); while (it < end) { td = EhciAllocTD(hc); if (!td) return; toggle ^= 1; packetSize = end - it; if (packetSize > maxSize) packetSize = maxSize; EhciInitTD(td, prev, toggle, packetType, packetSize, it); it += packetSize; prev = td; } // Status packet //   td = EhciAllocTD(hc); if (!td) return; toggle = 1; packetType = type & RT_DEV_TO_HOST ? USB_PACKET_OUT : USB_PACKET_IN; EhciInitTD(td, prev, toggle, packetType, 0, 0); // Initialize queue head //   : EhciQH *qh = EhciAllocQH(hc); EhciInitQH(qh, t, head, dev->parent, false, speed, addr, 0, maxSize); // Wait until queue has been processed //       EhciInsertAsyncQH(hc->asyncQH, qh); EhciWaitForQH(hc, qh); } 

Code de traitement des files d'attente:

  if (qh->token & TD_TOK_HALTED) { t->success = false; t->complete = true; } else if (qh->nextLink & PTR_TERMINATE) if (~qh->token & TD_TOK_ACTIVE) { if (qh->token & TD_TOK_DATABUFFER) kprintf(" Data Buffer Error\n"); if (qh->token & TD_TOK_BABBLE) kprintf(" Babble Detected\n"); if (qh->token & TD_TOK_XACT) kprintf(" Transaction Error\n"); if (qh->token & TD_TOK_MMF) kprintf(" Missed Micro-Frame\n"); t->success = true; t->complete = true; } if (t->complete) .... 

Et maintenant la demande de point final (demande en masse)

 static void EhciDevIntr(UsbDevice *dev, UsbTransfer *t) { EhciController *hc = (EhciController *)dev->hc; // Determine transfer properties //    uint speed = dev->speed; uint addr = dev->addr; uint maxSize = t->endp->desc->maxPacketSize; uint endp = t->endp->desc->addr & 0xf; EhciTD *td = EhciAllocTD(hc); if (!td) { t->success = false; t->complete = true; return; } EhciTD *head = td; EhciTD *prev = 0; // Data in/out packets uint toggle = t->endp->toggle; uint packetType = t->endp->desc->addr & 0x80 ? USB_PACKET_IN : USB_PACKET_OUT; uint packetSize = t->len; EhciInitTD(td, prev, toggle, packetType, packetSize, t->data); // Initialize queue head //    EhciQH *qh = EhciAllocQH(hc); EhciInitQH(qh, t, head, dev->parent, true, speed, addr, endp, maxSize); //printQh(qh); // Schedule queue //    EhciInsertPeriodicQH(hc->periodicQH, qh); } 

Je pense que le sujet est assez intéressant, sur Internet en russe il n'y a presque pas de documentation, de descriptions et d'articles sur ce sujet, et s'il y en a, c'est très flou. Si le sujet du travail avec le développement matériel et OS est intéressant, alors il y a beaucoup à dire.

Docks: spécification

Source: https://habr.com/ru/post/fr426421/


All Articles