L'USB d'un point de vue logiciel

1) Introduction
2) Considérations générales
3) Accès USB sous Microsoft Windows 98 et ultérieur
4) Accès USB sous Linux
Les liens utiles

Cette page a été réalisée, en avril 2003, par 3 élèves de 2ème année de SUPÉLEC Campus de Rennes dans le cadre de leur projet :
  • Fabien CHEVALIER
  • Sylvain RICHERIOUX
  • Nicolas SINÈGRE

  • 1) Introduction

    Si vos lectures vous ont amenées jusqu'ici, c'est que vous êtes intéressé par le fait de pouvoir communiquer avec un périphérique USB que vous avez certainement déjà branché sur votre ordinateur. Ce document va vous présenter la manière d'accéder à votre périphérique USB depuis votre OS favori. Sont couverts par ce document les systèmes informatiques Microsoft Windows et Linux.


    2) Considérations générales

    Commençons par rappeler la manière de dialoguer avec un périphérique simple, tel qu'un port série, un port parallèle, ou une carte d'extension ISA. Le dialogue se fait en utilisant :
    - des ports d'entrées sorties (et éventuellement des entrées sorties projetées en mémoires (MMIO)).
    - des interruptions.

    La programmation d'un tel périphérique se fait alors de manière simple. On utilise des instructions du style :

    >> copier le contenu du registre R1 vers le port d'entrée sortie P0.
    ou dans le cas d'entrées sorties projetées en mémoire
    >> copier le contenu du registre R1 vers la mémoire P0.

    Pour pouvoir programmer un driver de périphérique, il suffit de connaître l'agencement des registres sur le périphérique, et de dialoguer avec ceux-ci suivant le protocole défini par le constructeur.

    Malheureusement pour le connaisseur d'un langage d'assemblage intel 8086, motorola 68000, Microchip PIC ou autre, ces connaissances ne seront que très peu utiles.
    En effet, le périphérique n'est pas connecté physiquement sur le bus de la machine, donc on ne peut dialoguer avec celui-ci qu'avec de simples instructions d'entrées sorties.
    Les caratéristiques de fonctionnement d'un périphérique sont définies par ses terminaisons (EP0, EP1, ...), leur sens (IN ou OUT), et leur mode de transfert (Control, Interrupt, Bulk ou Isochrone), et le protocole au niveau de chaque terminaison. On ne peut dialoguer avec un périphérique USB que par l'intermédiaire de l'hôte USB.
    Comme si les choses n'étaient pas assez compliquées, la norme USB originelle définit deux types d'hôtes USB, les hôtes OHCI et UHCI. L'hôte UHCI privilégie la simplicité du matériel au détriement du logiciel, et l'hôte OHCI fait l'inverse. Les agencements des registres et la manière de communiquer est bien évidemment tout à fait différente pour ces deux hôtes.

    Une autre raison qui empêche le dialogue direct avec les périphériques USB est que toutes les requêtes doivent passer par l'hôte USB. Il faut donc un arbitrage, pour savoir qui peut utiliser l'hôte USB. Ceci ne peut être fait que par le système d'exploitation.


    3) Accès USB sous Microsoft Windows 98 et ultérieur


    Le support de l'USB à été introduit des Windows 95 OSR2, mais à un niveau assez embrionnaire et peu fiable. On peut affirmer sans trop mentir que le support USB sous Windows a vraiment commencé à être utilisé à partir de Windows 98.

    On dispose de plusieurs moyens d'accéder à un périphérique USB sous Windows :

    A compléter par la suite...


    4) Accès USB sous Linux

    Le support USB sous Linux est moins développé que sous Windows. Les drivers de périphériques USB pour Linux ont été développés à partir de 1999, avec les premiers drivers pour claviers et souris USB. La description se base ici sur la version 2.4.

    Il existe plusieurs moyens d'accéder à un périphérique USB sous linux.

    Seule la dernière méthode sera détaillée ici. Elle utilise l'"USB device filesystem", ou en raccourci "usbdevfs" ou "usbfs". Il s'agit d'un système de fichier, se trouvant en général dans le répertoire /proc/bus/usb. (pour la suite, nous utiliserons l'abbréviation usbfs).

    Comment utiliser l'usbfs ?
    Pour un programme C/C++ : ajouter la directive
    #include <linux/usbdevice_fs.h>

    Pour un autre langage de programmation : il faudra convertir les structures de données et les définitions de constantes des fichiers /usr/include/linux/usb.h, /usr/include/linux/usb_ch9.h, /usr/include/linux/usbdevice_fs.h.

    Comment trouver le périphérique XXX ?
    La question est ici de localiser un périphérique connecté sur une votre ordinateur. L'usbfs contient un répertoire par bus USB sur la machine. Sur un PC typique, on trouve deux bus USB. Les répertoires correspondants sont appelés "001" et "002". Dans chacun de ces répertoires se trouve un fichier par périphérique USB connecté au bus, hôte USB compris. Les noms de ces fichiers ne sont pas significatifs, il s'agit en fait d'un simple chiffre.

    Voici comme exemple le système de fichier usbfs sur ma machine :
    [root@localhost fabien]# tree /proc/bus/usb/
    /proc/bus/usb/
    |-- 001
    | `-- 001
    |-- 002
    | |-- 001
    | `-- 002
    |-- devices
    `-- drivers

    Les deux fichiers 001 correspondent au hub de chacun des deux bus. Le fichier 002 correspond à ma manette de jeux USB.
    Je suis sûr qu'une question vous taraude. Comment faire pour savoir à quel périphérique USB correspond quel fichier ? Il existe plusieurs méthodes. Le plus simple est d'ouvrir chaque fichier, et ensuite d'y lire des octets. Les premiers octets reçus correspondent à une structure C qui est définie dans <linux/usb_ch9.h>.

    struct usb_device_descriptor {
    __u8 bLength;
    __u8 bDescriptorType;

    __u16 bcdUSB;
    __u8 bDeviceClass;
    __u8 bDeviceSubClass;
    __u8 bDeviceProtocol;
    __u8 bMaxPacketSize0;
    __u16 idVendor;
    __u16 idProduct;
    __u16 bcdDevice;
    __u8 iManufacturer;
    __u8 iProduct;
    __u8 iSerialNumber;
    __u8 bNumConfigurations;
    } __attribute__ ((packed));

    On y trouve les principales caractéristiques d'un périphérique USB.

    Comment communiquer avec mon périphérique USB ?
    La manière de communiquer dépend du type de tranfert (Bulk, Isochrone, Interrupt, Control).
    Le type de transfert plus simple à gérer est certainement le transfert Bulk.
    Il suffit de remplir la structure de données suivante :

    struct usbdevfs_bulktransfer {
    unsigned int ep;
    unsigned int len;
    unsigned int timeout; /* in milliseconds */
    void *data;
    };

    et d'appeler la fonction C suivante : ioctl(fd, USBDEVFS_BULK, pointeur sur la structure usbdevfs_bulktransfer);

    Exemple de manipulation de ces notions - lecture de données Bulk :

    int fd;

    fd = open("/proc/bus/usb/001/002", O_RDWR);

    if(fd != -1) {

       struct usb_bulktransfer bulk;
       int ret;

       bulk.ep = USB_DIR_IN | 1;
       bulk.len = 1024;
       bulk.timeout = 1000;
       bulk.data = malloc(1024);

       ret = ioctl(fd, USBDEVFS_BULK, &bulk);

       if (ret < 0)
          printf("Error in bulk transfer");
       else {
          /* les données sont disponibles dans bulk.data */
       }
       close(fd);
    }

    Exemple de manipulation de ces notions - écriture de données bulk :

    int fd;

    fd = open("/proc/bus/usb/001/002", O_RDWR);

    if(fd != -1) {

       struct usb_bulktransfer bulk;
       int ret;

       bulk.ep = 1;
       bulk.len = 1024;
       bulk.timeout = 1000;
       bulk.data = malloc(1024);

       ret = ioctl(fd, USBDEVFS_BULK, &bulk);

       if (ret < 0)
          printf("Error in bulk transfer");
       else {
          /* les données sont disponibles dans bulk.data */
       }
       close(fd);
    }


    Les transfers de type Interrupt IN sont beaucoup plus difficiles à gérer. La principale difficulté réside dans la fait qu'ils ont lieu de manière asynchrone. Une fois le transfert initialisé, le programme reçoit des signaux (au sens Unix du terme) qu'il doit intercepter.

    Je préfère utiliser la pédagogie par l'exemple dans ce cas.
    Voici donc un extrait de code d'un programme qui permet de lire les transferts de type interruption en provenance d'un périphérique HID.
    La fontion hid_interrupt_enable() active la gestion des interruptions USB pour le periphérique dont le descripteur de fichier est donné.
    La fontion hid_interrupt_disable() arrête la gestion des interruptions.
    La fonction hid_signal() est utilisée en interne (c'est elle qui est appelée à chaque fois qu'un transfert interruption a été reçu).

    /* global variables */

    static char buffer[8];
    static hid_interrupt_handler hid_intr_handler;
    static struct usbdevfs_urb *hid_intr_urb;
    static int hid_intr_fd = 0;

    static void hid_signal(int signum, siginfo_t * si, void* data)
    {
       struct usbdevfs_urb *purb = (struct usbdevfs_urb *) si->si_addr;
       struct usbdevfs_urb *read;
       int ioctl_ret;

        if(purb->status == -2) {
          //this is like dark magic: you don't know why it is, but it is.
          //i.e. it means the urb has been discarder by ioctl(hid_intr_fd, USBDEVFS_DISCARDURB...)
          return;
        }
        else if (ioctl(hid_intr_fd, USBDEVFS_REAPURB, &read )) {
           fprintf(stderr, "Error in urb reading: %s\n", strerror(errno));
        }
        else {
          hid_intr_handler(read->buffer);
          ioctl_ret = ioctl( hid_intr_fd, USBDEVFS_SUBMITURB, read );
          if (ioctl_ret) {
             fprintf(stderr, "Error resubmitting urb: %s\n", strerror(errno));
          }
       }
    }

    int hid_interrupt_enable(DEVICEHANDLE dh, unsigned char ep, hid_interrupt_handler h)
    {
       int ret = 0;

       if(hid_intr_fd == 0) {
          struct usbdevfs_urb *purb = malloc(sizeof(struct usbdevfs_urb));
          struct sigaction act;

          /* zeroing buffer*/
          memset(purb, 0, sizeof(struct usbdevfs_urb));

          /*sigaction */
          act.sa_sigaction = hid_signal;
          sigemptyset(&act.sa_mask);
          act.sa_flags = SA_SIGINFO;
          sigaction(SIGRTMAX, &act, 0);

          hid_intr_handler = h;

          /* Ensure the endpoint address is correct */
          ep |= USB_DIR_IN;

          /* preparing the urb */
          purb->type = USBDEVFS_URB_TYPE_INTERRUPT;
          purb->endpoint = ep;
          purb->buffer_length = sizeof(buffer);
          purb->buffer = (unsigned char *)buffer;
          purb->flags = 0;
          purb->signr = SIGRTMAX;
          hid_intr_fd = dh;
          hid_intr_urb = purb;

          ret = ioctl(hid_intr_fd, USBDEVFS_SUBMITURB, purb);
          if (ret != 0) {
             fprintf(stderr, "error submitting interrupt URB at endpoint 0x%x: %s\n",
             ep, strerror(errno));
             ret = -1;
          }
       }
       else
           ret = -1;

    return ret;
    }

    int hid_interrupt_disable()
    {
       int ret = 0;

       if(hid_intr_fd)
       {
          ret = ioctl(hid_intr_fd, USBDEVFS_DISCARDURB, hid_intr_urb);
          if (ret != 0) {
             fprintf(stderr, "error discarding interrupt URB: %s\n", strerror(errno));
             ret = -1;
          }

          free(hid_intr_urb);
          hid_intr_urb = 0;
          hid_intr_fd = 0;
       }
       else
          ret = -1;

    return ret;
    }

    A noter : L'extrait de code précédent ne fonctionne pas sous linux 2.4, mais uniquement à partir de linux 2.5 (un bug dans la série 2.4 des noyaux linux fait planter la machine avec un Kernel Panic).


    Les liens utiles


    www.linux-usb.org : le site centralisant le développement de l'USB sous linux : accès aux principales documentations, archives des mailing-lists
    La documentation USB du noyau : regarder dans /usr/src/linux/Documentation/usb.

    Retour vers la page de la documentation USB-HID Retour vers les autres docs