TP introduction au microcontrôleur 8 bits

É. Carry, J.-M Friedt

Questions sur ce TP :

  1. Quel est l’intérêt de l’outil make et de son fichier de configuration Makefile ?

  2. Quel est l’intérêt de définir le nom du programme par une variable dans une Makefile ?

  3. Quel est l’intérêt de séparer les fichiers contenant les fonctions liées à une même fonctionnalité du microcontrôleur ?

  4. Quelle option de gcc permet de lier un code à la bibliothèque statique libtoto.a ?

  5. Comment définir le chemin du répertoire contenant la bibliothèque libtoto.a ?

  6. Quel est le nom de l’interface du port série virtuel créé par Linux pour communiquer par USB avec le microcontrôleur lors de l’utilisation de la bibliothèque LUFA ?

L’objet de cette séance de travaux pratiques est de

  1. mieux comprendre le fonctionnement du compilateur gcc et en particulier ses options d’optimisation

  2. organiser son code en modules compilés séparément, idéalement réutilisables (bibliothèques de fonctions)

  3. automatiser la séquence de compilation de programmes séparés grâce à l’outil make

  4. exploiter les connaissances acquises, en particulier sur make, pour exploiter la communication USB en se liant à la bibliothèque LUFA.

Options d’optimisation

Un code C n’est pas bijectif avec un code assembleur : divers compilateurs vont générer diverses implémentations du même code C, et un même compilateur peut être configuré pour optimiser le code selon divers critères (taille, vitesse). Ces optimisations peuvent cependant avoir des effets indésirables s’ils ne sont pas maı̂trisés.

#include <avr/io.h> //E/S ex PORTB 
#define F_CPU 16000000UL

void mon_delai(long duree)
{long k=0;
 for (k=0;k<duree;k++) {};
}

int main(void){
  DDRB |=1<<PORTB5;
  DDRE |=1<<PORTE6;
  PORTB |= 1<<PORTB5;
  PORTE &= ~(1<<PORTE6);

  while (1){
    PORTB^=1<<PORTB5;PORTE^=1<<PORTE6;
    mon_delai(0xffff);
  }
  return 0;
}

L’exemple ci-dessus suit l’approche naı̈ve d’implémenter un retard par une boucle vide.

Le code se compile par avr-gcc -mmcu=atmega32u4 -Wall -o blink.out blink.c et l’assembleur résultant s’observe par avr-objdump -dS blink.out

  1. compiler avec l’option -O0 et observer la fonction mon_delai() résultante.

  2. compiler avec l’option -O2 et observer la fonction mon_delai() résultante.

  3. compiler avec l’option -O1 et observer la fonction mon_delai() résultante.

  4. compiler avec l’option -O2 après avoir préfixé la définition de la variable k par le terme volatile qui interdit toute hypothèse du compilateur sur la valeur de la variable, et observer la fonction mon_delai() résultante.

  5. compiler avec l’option -g -O2 et observer la fonction mon_delay() résultante.

Programmation modulaire – compilation séparée

Un programme est efficacement séparé en modules indépendants, voir en bibliothèques. La compilation séparée consiste à compiler chaque programme en objets indépendants, qui sont ensuite liés (linker) en un exécutable unique.

Dans cet exemple, nous désirons pouvoir réutiliser la fonction d’attente en le compilant comme objet qui peut être rappelé par divers programmes. Cependant, nous devons déclarer la présence de la fonction dans l’objet : c’est le rôle du fichier d’entête .h (header file).

Attention : un fichier d’entête ne contient jamais de code C.

#include <avr/io.h> //E/S ex PORTB 

#define F_CPU 16000000UL

#include "separe_delay.h"

int main(void){
  DDRB |=1<<PORTB5;
  DDRE |=1<<PORTE6;
  PORTB |= 1<<PORTB5;
  PORTE &= ~(1<<PORTE6);

  while (1){
    PORTB^=1<<PORTB5;PORTE^=1<<PORTE6;
    mon_delai(0xffff);
  }
  return 0;
}
void mon_delai(long duree)
{long k=0;
 for (k=0;k<duree;k++) {};
}
void mon_delai(long);

Ces fichiers se compilent (pour d’abord générer les deux objets qui sont ensuite liés en un binaire) par

avr-gcc -mmcu=atmega32u4 -Os -Wall -c separe_delay.c
avr-gcc -mmcu=atmega32u4 -Os -Wall -c separe_blink.c
avr-gcc -mmcu=atmega32u4 -o blink_separe.out separe_blink.o separe_delay.o

Un outil spécifique permet de tenir compte des dépendances : make, qui prend un fichier de configuration nommé par défaut Makefile.

Par ailleurs, la compilation séparée nécessite de déclarer les fonctions ou variables définies dans un autre objet. Les fichiers d’entête (header) regroupent les définitions de fonctions, tandis que la définition de variable préfixée de extern annonce une définition dans un autre objet (avec l’allocation de mémoire associée).

all:	separe_blink.out


separe_blink.out: separe_delay.o separe_blink.o
	avr-gcc -mmcu=atmega32u4 -Os -Wall -o separe_blink.out separe_blink.o separe_delay.o

separe_delay.o: separe_delay.c separe_delay.h
	avr-gcc -mmcu=atmega32u4 -Os -Wall -c separe_delay.c

separe_blink.o: separe_blink.c
	avr-gcc -mmcu=atmega32u4 -Os -Wall -c separe_blink.c

clean:
	rm *.o separe_blink.out 

flash: separe_blink.out
	avrdude -c avr109 -b57600 -D -p atmega32u4 -P /dev/ttyACM0 -e -U flash:w:separe_blink.out

Deux méthodes y sont classiquement définies : all indique la cible finale à atteindre, clean fournit les commandes de nettoyage du répertoire. Chaque ligne contient la cible, les dépendances puis la méthode à mettre en œuvre si les dépendances sont résolues.

Attention : chaque méthode commence par une tabulation et pas par des espaces.

Compléments sur le Makefile

Afin d’éviter de modifier en plusieurs emplacements du Makefile qui servira tout au long de ce TP le nom du programme, nous introduisons un nouveau concept : la variable.

Une variable de Makefile se définit par VARIABLE=valeur et sera appelée par $(VARIABLE) (attention – la convention n’est pas la même que pour le shell 1 qui sépare le nom de variable du $ par des accolades au lieu de parenthèses.

MCU=atmega32u4
REP=$(HOME)/enseignement/ufr/platforms/Atmega32/
#TARGET=gpio_int
#TARGET=timer_ovfirq
#TARGET=timer_irq
#TARGET=wdt
TARGET=wdt_rs232
#TARGET=input_capture
#TARGET=input_capture_sendai
#TARGET=PRN_villedavray_header
#TARGET=switch_sendai
#TARGET=tmp
#TARGET=usart_int
#TARGET=gpr

all:	$(TARGET).hex

$(TARGET).hex: $(TARGET).out
	avr-objcopy -Oihex $(TARGET).out $(TARGET).hex  # for MS-Windows (winavr requires .hex)
	avr-objdump -dSt $(TARGET).out > $(TARGET).lst

$(TARGET).out: $(TARGET).o
	avr-gcc  -L$(REP)/VirtualSerial/ -mmcu=$(MCU) -o $(TARGET).out $(TARGET).o -lVirtualSerial

$(TARGET).o: $(TARGET).c
	avr-gcc -Wall -mmcu=$(MCU) -I$(REP)/VirtualSerial/ -I$(REP)/lufa-LUFA-140928/ -DF_USB=16000000UL \
	-std=gnu99 -Os -c $(TARGET).c
	avr-gcc -Wall -mmcu=$(MCU) -I$(REP)/VirtualSerial/ -I$(REP)/lufa-LUFA-140928/ -DF_USB=16000000UL \
	-std=gnu99 -Os -S $(TARGET).c

flash: $(TARGET).out
	avrdude -c avr109 -b57600 -D -p $(MCU) -P /dev/ttyACM0 -e -U flash:w:$(TARGET).out

clean:
	rm *.o $(TARGET).out

Communication – appel à une bibliothèque

LUFA 2 est une bibliothèque riche de gestion du protocole USB, complexe à mettre en œuvre compte tenu de la variété des conditions d’utilisation et des transferts effectués lors de l’initialisation des transactions avec le PC. Ces initialisations ont en particulier vocation à informer le système d’exploitation du PC des capacités de la liaison USB (nombre de points de communication, débit, nature du pilote susceptible de prendre en charge ces transactions sur le PC).


Nous proposons une version allégée de la bibliothèque et un exemple simple de transactions sur port série virtuel, nommé sous GNU/Linux /dev/ttyACM0. Pour compiler un programme lié à cette bibliothèque :

  1. aller dans le répertoire VirtualSerial, y copier la bibliothèque libVirtualSerial.a,

  2. compiler l’exemple par make

  3. On pourra éventuellement coupler compilation et transfert du programme au microcontrôleur sur la carte Olinuxino32U4 par make flash_109.

  4. Constater le bon fonctionnement du logiciel par cat < /dev/ttyACM0

Ainsi, nous indiquons dans le Makefile le chemin vers cette bibliothèque au moment de l’édition de liens (linker) par -L./lib (chemin relatif par rapport au répertoire courant, qui pourrait être absolu). De la même façon, chaque objet a besoin des définitions des fonctions fournies par la bibliothèque telles que mentionnées dans les fichiers d’entête dont l’emplacement est défini par -I./include.

Noter la nécessité d’inclure le fichier d’entête VirtualSerial.h qui définit les fonctions nécessaires au fonctionnement de l’USB, ainsi que les structures de données

extern USB_ClassInfo_CDC_Device_t VirtualSerial_CDC_Interface;
extern FILE USBSerialStream;
#include <avr/io.h>
#include <avr/wdt.h>
#include <avr/power.h>
#include <avr/interrupt.h>
#include <string.h>
#include <stdio.h>

#include "VirtualSerial.h"
#include <util/delay.h>

extern USB_ClassInfo_CDC_Device_t VirtualSerial_CDC_Interface;
extern FILE USBSerialStream;

int main(void)
{
 char ReportString[20] = "World\r\n\0";

 SetupHardware();
/* Create a regular character stream for the interface so that it can be used with the stdio.h functions */
  CDC_Device_CreateStream(&VirtualSerial_CDC_Interface, &USBSerialStream);
  GlobalInterruptEnable();

  for (;;)
    {
     fprintf(&USBSerialStream,"Hello ");
     fputs(ReportString, &USBSerialStream);

//   les 3 lignes ci-dessous pour accepter les signaux venant du PC 
     CDC_Device_ReceiveByte(&VirtualSerial_CDC_Interface);

     CDC_Device_USBTask(&VirtualSerial_CDC_Interface);
     USB_USBTask();
     _delay_ms(500);
    }
}

À la compilation, make déroule alors la séquence

avr-gcc -mmcu=atmega32u4 -Os -Wall -c separe_delay.c
avr-gcc -c -mmcu=atmega32u4 -Wall -I. -I../lufa-LUFA-140928/ -DF_USB=16000000UL \
        -DF_CPU=16000000UL -Os -std=gnu99  VirtualSerial.c
avr-gcc -mmcu=atmega32u4 -L. separe_delay.o VirtualSerial.o -o VirtualSerial.elf -lVirtualSerial

L’interface USB est initialisée par CDC_Device_CreateStream(&VirtualSerial_CDC_Interface, &USBSerialStream); tandis que le chien de garde et la cadence d’horloge sont configurés par SetupHardware() (lire le code source de
VirtualSerial_lib/VirtualSerialConfiguration.c. Enfin, les trois lignes

CDC_Device_ReceiveByte(&VirtualSerial_CDC_Interface);
CDC_Device_USBTask(&VirtualSerial_CDC_Interface);
USB_USBTask();

dans la boucle infinie gèrent les messages venant du PC vers le microcontrôleur. Omettre ces lignes n’empêche par le bon fonctionnement de la liaison microcontrôleur vers PC, mais trop de caractères venant du PC vont remplir la pile d’entrée de l’USB et rendre le logiciel inopérant.

1 J.-M Friedt, S. Guinot, Programmation et interfaçage d’un microcontroleur par USB sous linux : le 68HC908JB8, GNU/Linux Magazine France, Hors Série 23 (November/Décembre 2005)


  1. C. Newham, Learning the bash Shell: Unix Shell Programming (In a Nutshell), Ed. O’Reilly (2005)

  2. Lightweight USB Framework for AVRs, www.fourwalledcubicle.com/LUFA.php