IMA4 2016/2017 ECP3

De Wiki de Projets IMA

Présentation du projet

Contexte

L'élève effectue son stage sur Lille 1 et peut donc passer à l'école pour récupérer du matériel.

Objectif

L'objectif du projet est la création d'un périphérique USB ludique du type catapulte ou lance-billes. La carte de contrôle est à réaliser à l'aide d'un micro-contrôleur.

Description du projet

Le but de ce projet est de réaliser un gadget USB constitué d'une partie mécanique et d'une carte électronique de contrôle. La carte de contrôle doit permettre au gadget d'être reconnu par l'ordinateur comme un périphérique USB (USB device) sur un bus USB géré par un contrôleur USB (USB Host).

Comme carte de contrôle vous utiliserez un Arduino UNO. Reprogrammez l'ATMega16u2 de cette carte. L'objectif est de le programmer pour le faire apparaître non pas comme un convertisseur USB/Série mais comme un périphérique de type USB-gadget.

Une fois l'ATMega16u2 reconnu ainsi par l'ordinateur, des fonctions doivent être ajoutées sur l'ATMega328p pour gérer des servo-moteurs par rapport aux commandes reçues de l'hôte USB. Le périphérique doit donc présenter des points d'accès en écriture, par exemple pour commander la rotation de l'objet, mais aussi des points d'accès en lecture, par exemple pour savoir si la rotation est bloquée en fin de course. Pour la version de production, il est demandé de programmer les ATMega avec avr-gcc.

Pour finir, réalisez la structure du gadget en contre-plaqué usiné à la découpeuse laser.

Cahier des charges

L'idée générale de ce projet est de reprogrammer une carte Arduino UNO pour en faire un périphérique USB ludique de type catapulte ou lance-bille. Tout d'abord, il faut reprogrammer le micro-logiciel de communication USB (firmware) de l'ATMega16u2. L'ATMega16u2 fait le lien entre le port USB de l'ordinateur et le micro-contrôleur ATMega328p. Afin de reprogrammer l'ATMega16u2, il faut utiliser la bibliothèque LUFA. LUFA (Lightweight USB Framework for AVRs) est une librairie open-source pour les microcontrôleurs AVR conçu pour le développement de périphériques et hôtes USB. Elle est écrit spécifiquement pour le compilateur AVR-GCC. Dès que l'ATMega16u2 sera reprogrammé et reconnu par l'ordinateur comme un périphérique de type USB-gadget, on pourra passer sur la programmation de l'ATMega328p afin d'y ajouter les fonctions nécessaires pour gérer les servo-moteurs. Le code sera intégralement codé en C avec le bibliothèque avr-gcc.

Planning prévisionnel

Diagramme de Gantt

Programmation USB

Utilisez la bibliothèque LUFA pour reprogrammer l'ATMega16u2.

Semaine 1

Prise en main de la bibliothèque LUFA

J'ai tout d'abord effectué des recherches sur la bibliothèque LUFA afin d'étudier les différentes possibilités que nous offre cette bibliothèque. Elle permet de créer des périphériques de différentes classes : Android, Audio, Generic, Joystick, Clavier, Stockage, Souris, Imprimante... Des exemples de projets Open-source sont même inclus dans le package.

J'ai fait des recherches également sur l'ATMega16u2. Alors que l'ATMega328p prend le soin de réaliser toutes les taches de l'Arduino, l'ATMega16u2 s'occupe de la connexion USB. Elle convertit les signaux USB provenant de l'ordinateur vers le port série SAM3X. Le protocole USB nommée DFU (Device Firmware Update) permet de mettre à jour de le firmware de l'ATMega16u2.


Ayant téléchargé la bibliothèque LUFA, j'ai implanté une USB Class Device de type Mouse dans l'Arduino afin de faire un essai :

  • J'ai tout d'abord modifié le Makefile fourni afin qu'il fonctionne avec l'Arduino et l'ATMega16u2.
Makefile
  • Ensuite j'ai compilé le programme afin de générer le fichier .hex pour flasher l'ATMega16u2.
make all
  • Il est nécessaire de télécharger le package dfu-programmer pour flasher l'ATMega16u2.
sudo apt-get install dfu-programmer
  • On réinitialise l'ATMega16u2 en reliant les broches RESET et GND du port ISCP de celui-ci
Broches à relier pour réinitialiser l'ATMega16u2
  • A ce stade-là, l'Arduino n'est plus vu de la même façon avec la commande lsusb :
Avant réinitialisation
Après réinitialisation
  • On efface l'ATMega16u2 :
sudo dfu-programmer atmega16u2 erase
  • On le reprogramme avec le fichier .hex :
sudo dfu-programmer atmega16u2 flash Mouse.hex
  • Enfin, on tape la commande :
sudo dfu-programmer atmega16u2 reset
  • On déconnecte et on reconnecte l'Arduino. On observe alors le résultat avec les commandes lsusb et lsusb -v pour plus de détails :
lsusb
lsusb -v

L'Arduino est bien vu comme une souris par linux.

Semaine 2

Recherches sur le protocole USB

J'ai effectué quelques recherches afin de me familiariser avec les périphériques USB et de comprendre leur fonctionnement.

Architecture

La norme USB permet le chaînage des périphériques, en utilisant une topologie en bus ou en étoile. L'architecture USB est composée d'un hôte (USB host) et de plusieurs périphériques (USB devices). Un hôte USB peut contenir plusieurs contôleurs hôte (host controllers) et chaque contrôleur hôte peut contrôler un ou plusieurs ports USB. Un périphérique USB peut être constitué de plusieurs sous-périphériques dans le cas d'un périphérique multi-fonctions tel qu'un webcam avec micro intégré. La communication USB est basée sur des canaux logiques appelés "pipes". Un pipe est une connexion entre le contrôleur hôte et une entité logique d'un périphérique qu'on appelle "endpoint". Il y a deux types de pipes :

  • Les "pipes" de message qui sont bi-directionnels et permettent de commander le périphérique (control transfert).
  • Les "pipes" de stream qui sont uni-directionnels et permettent le transfert de données de façon asynchrone, par interruptions ou en mode bulk.
Communication USB host/USB device
Classes USB

Il y a différentes classes de périphériques USB permettant à l'hôte de charger le bon driver pour chaque périphérique connecté. Un code est associé à chaque classe. Il est envoyé à l'hôte. Les principales classes sont les suivantes :

  • Audio pour les enceintes, microphones ...
  • Communications and CDC Control pour les convertisseurs USB-série, modems ...
  • Human Interface Device (HID) pour les claviers, souris, joysticks, écrans tactiles ...
  • Physical Interface Device (PID)
  • Mass storage pour les clés USB, lecteurs de cartes SD ...
  • Vidéo pour les webcams
  • Vendor-specific ou Unspecified pour des périphériques nécessitant des drivers spcéfiques


Les classes peuvent être utilisées comme interface d'un périphérique pour un périphérique multi-fonctions.

Architecture d'un périphérique USB multi-fonctions

Semaine 3

Étude de la bibliothèque LUFA

Dans la bibliothèque LUFA, il existe plusieurs types d'USB Class Driver. J'ai regardé et étudié les différents exemples proposés : Android Open Accessory, Audio 1.0, CDC-ACM (Virtual Serial), HID, MIDI, Mass Storage, Printer, RNDIS (Networking) et Still Image. Après lecture et étude, la classe CDC-ACM (Communications Device Class-Abstract Control Model) semble se rapprocher le plus de la tâche que je dois réaliser. Cette classe offre la possibilité de configurer des points d'accès en lecture et en écriture, ce qui est prévu pour le gadget que je dois réaliser. Je vais donc m'inspirer de cela pour ma programmation USB. La plupart des exemples Demo disponibles de type CDC ont une structure permettant de configurer les points d'accès : usb_classinfo_cdc_device_t.

Recherches sur "l'USB Gadget"

J'ai également fait des recherches sur la bibliothèque Linux-USB Gadget API Framework. Je pensais partir là dessus au début pour la programmation du gadget mais cela ne semble pas du tout correspondre à ce qu'il m'est demandé de faire dans le mesure où cette API permet à des périphériques embarquant GNU/Linux de se comporter comme un périphérique USB (USB device) dans le rôle d'esclave. Contrairement à une plate-forme de type Raspberry Pi, l'Arduino ne peut pas embarquer Linux. Je n'ai guère trouvé autre chose sous le nom de « USB Gadget », cela ne semble pas être un type de périphérique standard définit par l'USB Implementers Forum.

Semaine 4 & 5

Prise en main de la classe USB CDC-ACM avec LUFA

Afin de réaliser la programmation de l'Arduino en tant que périphérique USB CDC-ACM, j'ai étudié le fonctionnement des périphériques de ce type ainsi que les fonctions de la bibliothèque LUFA associées. La sous-classe ACM pour les périphériques CDC utilise deux interfaces et quatre endpoints :

  • Une interface de communication comprenant deux endpoints :
    • Un endpoint bi-directionel de type "contrôle"
    • Un endpoint unidirectionnel de type "interruption"
  • Une interface de données comprenant deux endpoints unidirectionnels de type bulk :
    • Un endpoint IN
    • Un endpoint OUT

Le tableau ainsi que le schéma ci-dessous décrivent l'architecture ainsi que l'utilisation des endpoints pour un périphérique CDC-ACM. Les endpoints EP2 et EP3, utilisant le transfert de données en mode bulk, constituent des point d'accès en lecture en en écriture pour l'envoi des codes de pilotage des servomoteurs du gadget.

Endpoint Direction Type de transfert Taille maximale des paquets Description
EP0 IN/OUT Control 64 Requêtes standard, requêtes de classe
EP1 IN Interrupt 16 Notifications d'état du périphérique vers l'hôte
EP2 IN Bulk 64 Transfert de données du périphérique vers l'hôte
EP3 OUT Bulk 64 Transfert de données de l'hôte vers le périphérique


Architecture d'un périphérique USB CDC-ACM


J'ai tout d'abord implémenté différents exemples de programmes utilisant la bibliothèque LUFA fournis par cette dernière: Virtualserial, LEDNotifier.. Cela m'a permis de mieux comprendre la configuration des interfaces de mon périphérique ainsi que la gestion des données sur la liaison USB.

Schematic shield arduino


L’atmega16u2 et l’atmega328p communiquent via leur liaison série. Le programme de l’atmega16u2 devra récupérer les données envoyées sur la liaison USB (venant de l’hôte) et ensuite envoyer un code via la liaison série. Ensuite au niveau de l’atmega328p je devrai récupérer les codes sur la liaison série pour ensuite faire fonctionner les moteurs correspondants. J’ai implémenté sur l’atmega328p un code permettant d’allumer et éteindre une led afin de faciliter les tests. J’ai ensuite utilisé l’exemple du projet USBtoSerial qui permet de récupérer des paquets envoyés par l’hôte et les retourne sur la liaison série.

La programmation d'un périphérique USB via la bibliothèque LUFA se compose au minimum d’un fichier descriptor.c , qui décrit la structure de notre périphérique et sera appelé par le programme principal, de la librairie LUFA, d’un fichier LUFAconfig.h utile a la compilation et d’un programme principal dans notre cas USB_gadget.c qui décrit le comportement de notre périphérique.

La programmation d’un périphérique commence par la définition d’une structure qui sera appelé ensuite par les fonctions de la classe CDC de la bibliothèque LUFA. Pour le cas d’un périphérique de type CDC cela se fait en utilisant la structure USB_ClassInfo_CDC_Device_t.

USB_ClassInfo_CDC_Device_t VirtualSerial_CDC_Interface =
{
  .Config =
           {
            .ControlInterfaceNumber = INTERFACE_ID_CDC_CCI,
            .DataINEndpoint         =
                                    {
                                       .Address                = CDC_TX_EPADDR,
                                       .Size                   = CDC_TXRX_EPSIZE,
                                       .Banks                  = 1
                                    },
            .DataOUTEndpoint        =
                                    {
                                       .Address                = CDC_RX_EPADDR,
                                       .Size                   = CDC_TXRX_EPSIZE,
                                       .Banks                  = 1,
                                    },
            .NotificationEndpoint   =
                                    {
                                       .Address                = CDC_NOTIFICATION_EPADDR,
                                       .Size                   = CDC_NOTIFICATION_EPSIZE,
                                       .Banks                  = 1,
                                    },
           },
};


Dans mon application nous définissons une interface nommée VirtualSerial_CDC_Interface et on lui renseigne plusieurs informations tels que les informations relatives aux points d’accès (adresse, type..) ainsi que le numéro de la configuration (ici 0).

Le programme fonctionne de la manière suivante : une partie initialisation et une boucle principal gérant la communication et le comportement principal de notre périphérique. La gestion des données se fait via deux buffers : l’un pour les données de la liaison USB vers la liaison série et l’autre pour le sens inverse. On gère les buffers grâce a des fonctions de la bibliothèque <LUFA/Drivers/Misc/RingBuffer.h>, elle permet facilement de vider notre buffer, de tester celui-ci il est vide ou non …

Dans la boucle principal, on commence tout d’abord par tester si le buffer de la communication USB vers liaison série est plein, si ce n’est pas le cas on regarde si l’on reçoit des données sur la liaison USB via CDC_Device_ReceiveByte(&VirtualSerial_CDC_Interface) que l’on stock dans notre buffer. On renvoie ensuite vers la liaison série via Serial_SendByte(RingBuffer_Remove(&USBtoUSART_Buffer)).

Pour la réception de paquet venant de l’atmega328p, il faut tout d’abord spécifié quel point d’accès nous allons utiliser, cela se fait via Endpoint_SelectEndpoint(VirtualSerial_CDC_Interface.Config.DataINEndpoint.Address) qui récupère l’adresse spécifiée plus haut, pour ensuite envoyer les paquets CDC_Device_SendByte(&VirtualSerial_CDC_Interface, RingBuffer_Peek(&USARTtoUSB_Buffer)).

Il y a deux fonctions principales à utiliser obligatoirement pour le bon fonctionnement de la liaison USB : CDC_Device_USBTask(&VirtualSerial_CDC_Interface) USB_USBTask(). Ainsi elles permettent de gérer les différents événement de notre périphérique USB, via l’appel de différentes fonctions.

Le fichier Descriptor.c contient les structures décrivant le périphérique. Ces structures sont utilisés par le périphérique pour décrire sa configuration, ses interfaces...lorsque l’hôte le demande par exemple.

USB_Descriptor_Device_t PROGMEM DeviceDescriptor permet de déclarer les informations concernant le périphérique.

USB_Descriptor_Configuration_t PROGMEM ConfigurationDescriptor permet de déclarer les descriptions des configurations, des interfaces et des points d’accès.


La version actuelle m’a permis de configurer une communication entre l’USB et la liaison série. Pour la suite j’ai voulu passer outre cette conversion et envoyer directement des codes en fonction de ce que l’on reçoit sur la liaison USB.

Programmation du driver USB pour piloter le gadget au clavier

Pour concevoir le driver de mon périphérique USB, je me suis inspiré de celui conçu pour piloter la tourelle USB lors du tutorat IMA 4 de système du semestre 7.

  • On énumère tout d'abord les périphériques USB disponibles sur le bus USB de la machine hôte. Dés que le gadget USB est trouvé, on sauve la "poignée" vers ce périphérique dans une variable globale de type libusb_device_handle * (fonction void enumeration(libusb_context *context)). Une fois le périphérique trouvé, on passe à la configuration (fonction void configuration_periph(libusb_device *device)).
  • On ouvre le périphérique
int status=libusb_open(device,&handle);
if(status!=0)
   {
     perror("libusb_open"); exit(-1);
   }
  • On récupère la configuration d'indice 0 du périphérique
 struct libusb_config_descriptor *config;
 status=libusb_get_active_config_descriptor(device,&config);
 if(status!=0)
   {
     perror("libusb_get_active_config_descriptor"); exit(-1);
   }
  • On détache le driver utilisé par le noyau qui peut s'être approprié les interfaces du périphérique avant notre driver (pour un périphérique CDC-ACM, il est fort probable que ce soit le cas)
for(i=0;i<(config->bNumInterfaces);i++)
   {
     interface=config->interface[i].altsetting[0].bInterfaceNumber;
     if(libusb_kernel_driver_active(handle,interface))
     {
       status=libusb_detach_kernel_driver(handle,interface);
       if(status!=0)
       {
         perror("libusb_detach_kernel_driver"); exit(-1);
       }
     }
   }
  • On utilise une configuration du périphérique
 int configuration=config->bConfigurationValue;
 status=libusb_set_configuration(handle,configuration);
 if(status!=0)
   {
     perror("libusb_set_configuration"); exit(-1);
   }
  • On s'approprie ensuite les interfaces
for(i=0;i<(config->bNumInterfaces);i++)
   {
     interface=config->interface[i].altsetting[0].bInterfaceNumber;
     printf("Numero d'interface : %d\n", interface);
     status=libusb_claim_interface(handle,interface);
     if(status!=0)
     {
       perror("libusb_claim_interface"); exit(-1);
     }
   }

J'ai du adapter la fonction int main() du driver pour un périphérique CDC-ACM permettant le transfert et la réception de données en mode bulk. Après avoir configuré le périphérique comme décrit ci-dessus, on envoie sur le point d'accès de contrôle des commandes spécifiques :

status = libusb_control_transfer(handle, 0x21, 0x22, ACM_CTRL_DTR | ACM_CTRL_RTS,0, NULL, 0, 0);
   if (status < 0) 
     {
       fprintf(stderr, "Error during control transfer: %s\n",libusb_error_name(status));
     }

Configuration du port série (9600 bauds = 0x2580 = 0x80, 0x25 en little endian):

  unsigned char encoding[] = { 0x80, 0x25, 0x00, 0x00, 0x00, 0x00, 0x08 };
  status = libusb_control_transfer(handle, 0x21, 0x20, 0, 0, encoding, sizeof(encoding), 0);
  if (status < 0) 
     {
       fprintf(stderr, "Error during control transfer: %s\n",
       libusb_error_name(status));
     }

La fonction "void envoi(unsigned char c)" permet d'envoyer un caractère à un périphérique en mode bulk-transfert via le endpoint d’adresse ep_out_addr :

void envoi(unsigned char c)
{
  int actual_length;
  if (libusb_bulk_transfer(handle, ep_out_addr, &c, 1,&actual_length, 0) < 0) 
    {
       fprintf(stderr, "Error while sending char\n");
    }
}

La fonction "int reception(unsigned char * data, int size)" permet quant à elle de recevoir des caractères en mode bulk-transfert via le endpoint d'adresse ep_in_addr :

int reception(unsigned char * data, int size)
{
   int actual_length;
   int status = libusb_bulk_transfer(handle, ep_in_addr, data, size, &actual_length, 1000);
   if (status == LIBUSB_ERROR_TIMEOUT) {
       printf("timeout (%d)\n", actual_length);
       return -1;
   } else if (status < 0) {
       fprintf(stderr, "Error while waiting for char\n");
       return -1;
   }
   return actual_length;
}

Programmation du gadget

Semaine 1

Programmation de l'ATMmega328p de l'Arduino via le port ISCP

Dès que l'Arduino est transformé en périphérique USB-Gadget après la reprogrammation de l'ATMega16u2, il est plus possible de programmer l'ATMmega328p via le port USB de l'Arduino. Il faut donc utiliser le port ISCP relier au l'ATMega328p pour programmer celui via le protocole SPI. On utilise pour cela un autre Arduino UNO. La seconde Arduino joue le rôle de programmeur AVR.

Utilisation d'un Arduino comme programmeur USB
Arduino Programmeur Arduino à programmer
Vcc/5V Vcc
GND GND
MOSI/D11 D11
MISO/D12 D12
SCK/D13 D13
D10 Reset


Montage des deux Arduinos en programmation SPI

Une fois nos arduinos connectés en SPI, j'ai suivi les étapes décrites ici qui permet via l'ide arduino de reprogrammer un second arduino rapidement.

Semaine 4 & 5

Programmation des moteurs du gadget

Afin de piloter les moteurs du gadget, j'ai implémenté des commandes PWM (Pulse width modulation) dans le programme pour l'ATMega328p de l'Arduino Uno. Sur cette carte, les broches 3, 5, 6, 9, 10 et 11 peuvent générer une PWM (ces broches comportent le symbole tilde ~) et la fréquence de la PWM est d'environ 490 Hz sauf sur les broches 5 et 6 ou est elle proche de 980 Hz.

Il est nécessaire de mettre les bonnes valeurs dans les différents registres de l'ATmega 328p pour configurer la PWM (ici exemple pour la broche 9) :

void init_pwm()
{
  
  cli();          

  DDRB |= (1 << DDB1)|(1 << DDB2);  // PB1 et PB2 en sortie

  TCCR1A = (1 << WGM10) | (1 << COM1A1); // none-inverting mode

  TCCR1B = (1 << WGM12) | (1 << CS10) |(1 << CS12); // démarrage du timer sans prescaler
  
  OCR1A= 0xFF;

  sei();
}

J'ai implémenté trois fonction pour commander le mouvement du moteur à droite, à gauche et l'arrêter :

void motor_right(){
  OCR1A = 0x80 ;
}

void motor_left(){
  OCR1A = 0x08 ;
}

void motor_stop(){
  OCR1A = 0xFF;
}

Il suffit simplement de modifier la valeur du registre OCR1A (duty cycle) selon le mouvement souhaité.

Le programme principal initialise la liaison série de l'Arduino à 9600 bauds ainsi que la PWM via un appel à la fonction décrite ci-dessus. Il attend ensuite la réception d'un caractère et fait bouger le moteur en conséquence.

Voici une courte vidéo montrant mon périphérique en marche: Vidéo USB-Gadget

Réalisation du gadget

Conclusion

Mon périphérique se comporte comme un convertisseur USB classique mais avec le temps qui m'etait imparti et les connaissances de base que j'avais sur le sujet ne m'ont pas permis d'implémenter les fonctionnalités demandées. Je me suis plus concentré dans la création d'un périphérique USB fonctionnel. Je n'ai pas eu le temps de réfléchir à la réalisation du gadget. Avec le stage à côté, j'aurais difficilement pu faire plus. J'espère que le travail que j'ai fourni vous conviendra.

Documents

Pour faciliter la programmation, mon projet comporte plusieurs dossiers :

  • Un dossier gadget comportant les différents programmes gérant le périphérique USB via la librairie LUFA et un script permettant de flasher l'atmega16u2 rapidement.
  • Un dossier USB pour la programmation du driver USB.
  • Un dossier Moteur pour les programmes liés à l'atmega328p gérant les moteurs.

Chaque dossier comporte un makefile qui permet de compiler le projet. J'ai aussi rédigé un README qui détaille l'utilisation de mon périphérique.

Média:USB_Gadget.zip

Sources

Librairie LUFA

http://rex.plil.fr/Enseignement/Systeme/Tutorat.Systeme.IMA4/index.html

http://libusb.sourceforge.net/

PWM On The ATmega328