Looking for Computer Science  & Information Technology online courses ?
Check my new web site: https://www.yesik.it !

Avec leur grande polyvalence, leur excellent support par des logiciels open-source et leur prix réduit, les processeurs AVR d'Atmel ont tout pour séduire. Le succès de la plate-forme Arduino en est la preuve. Si celle-ci est particulièrement accessible à l'amateur, elle n'est cependant pas la seule option possible pour faire ses premières armes avec un micro-contrôleur AVR. Surtout si vous être déjà familiers de la programmation de systèmes embarqués, ou si vous êtes curieux d'en explorer les aspects les plus techniques.

Ainsi, le projet USbooBie se révèle une alternative simple et économique pour découvrir le monde de la programmation AVR. Simple, car, tout comme Arduino, USnooBie peut se programmer directement à partir du port USB. Économique, puisqu'on peut trouver le kit à souder soi-même pour moins de 20$US (chez seeedstudio, par exemple). Pour ce prix, vous disposez d'un micro-contrôleur ATmega328P cadencé à 12MHz, d'un port USB (pour la programmation, la communication et l'alimentation) et de l'accès à toutes les broches d'E/S du micro-contrôleur.

L'USnooBie est avant tout conçu pour pouvoir réaliser des prototypes de périphériques USB puisqu'il donne un accès direct au port USB par les broches PORTD2 (connectée à D+) et PORTD7 (connectée à D-) du micro-contrôleur. Mais, ses caractéristiques en font aussi une plate-forme de développement AVR générique. Dans cette optique, nous allons voir comment écrire un premier programme pour AVR et le télécharger sur l'USnooBie à partir d'un PC sous Linux. Dans un premier temps, nous verrons une manière de faire simple et accessible à tous qui fait appel à l'IDE Arduino. Puis nous nous intéresserons à des outils de plus bas niveau, qui nous permettront de faire un tour d'horizon des techniques qui constituent le bread and butter du développeur pour la plate-forme AVR. Mais avant de commencer, quelques petites étapes de préparation sont à prévoir...

Préparation

Accéder au bootloader

Les micro-contrôleurs AVR qui peuvent se programmer directement par USB sans requérir un programmateur utilisent tous un même truc. Il s'agit d'un bootloader, installé dans la mémoire flash de l'AVR, et qui prend en charge le protocole de communication avec le logiciel de programmation.

Il y a plusieurs raisons pour lesquelles ce détail a son importance. Tout d'abord, cela implique que pour pouvoir être programmé sans programmateur, il faut que ce bootloader ait préalablement été installé sur le microprocesseur AVR. C'est le cas de celui fournit avec le kit UBnooBie, par contre, vous ne bénéficierez plus de cette possibilité si vous le remplacez par un autre micro-contrôleur AVR – à moins qu'un bootloader adéquat n'y ait déjà été copié. Par ailleurs, il existe toujours un risque de mauvaise manipulation qui aboutisse à l’effacement du bootloader. Dans ce cas, il faudra le réinstaller sur votre micro-contrôleur pour récupérer la possibilité de programmation par USB.

Dernière raison pour laquelle parler du bootloader: c'est que celui-ci est dans le chemin au démarrage du processeur. Sur Arduino, par exemple, le bootloader se met à l'écoute pendant un certain temps pour voir si vous téléchargez un programme. Le problème, c'est que si votre carte est reliée à un système qui communique dès la mise sous tension, le bootloader est susceptible de prendre les données reçues pour une tentative de programmation. USnooBie résout ce problème en requérant l'appui sur une combinaison de touches pour activer le bootloader au démarrage. La combinaison est la suivante:

  1. appuyer sur reset,
  2. appuyer sur bootloader,
  3. relâcher reset,
  4. relâcher bootloader.

À chaque fois qu'il sera nécessaire de redémarrer sous le bootloader pour télécharger un programme sur l'USnooBie, c'est cette combinaison qu'il faudra effectuer. Gardez-là en tête: elle va nous servir à de nombreuses reprises dans la suite de cet article...

Reconnaissance de l'émulateur USBasp

Lorsque l'USnooBie redémarre sur le bootloader pour être programmé, celui-ci émule le programmateur USBasp. C'est avec cet émulateur que le logiciel de programmation AVR communiquera pour télécharger un programme sur le micro-contrôleur. Sous Linux, cela se fait par l'intermédiaire d'un pseudo-fichier périphérique caractère situé sous /dev.

Cela implique que l'utilisateur courant ait les permissions pour écrire sur ce périphérique. Sous Debian Squeeze, ce n'est pas le cas par défaut. Ici et là, on trouve sur internet la suggestion de travailler sous root pour contourner le problème. Ce n'est pas une bonne idée! Sur un système utilisant udev (c'est la cas de la majorité des grandes distributions), un moyen nettement plus recommandable est d'utiliser une règle pour donner au périphérique les permissions adéquates lorsqu'il est détecté.

Sous Debian, la règle doit être ajoutée dans un nouveau fichier à placer dans dossier /etc/udev/rules.d. Le nom exact du fichier est à votre convenance, personnellement, j'utilise toujours le mot local pour préciser que c'est une règle spécifique à mon système et pas une règle standard:

sh# cat > /etc/udev/rules.d/90-local-USBasp.rules << EOF
# Custom rule for USBasp 

SUBSYSTEM=="usb", ENV{DEVTYPE}=="usb_device",ATTR{idVendor}=="16c0" , ATTR{idProduct}=="05dc", MODE="0660",SYMLINK+="USBasp",GROUP="dialout"
EOF

Le début de la règle sert à identifier le périphérique en question: un périphérique usb, avec l'identifiant de vendeur 16c0 et l'identifiant de produit 05dc. Ces valeurs, je ne les ai pas inventées: elles apparaissent simplement dans /var/log/syslog lorsque le périphérique est connecté et rebooté sur le bootloader:

sh# tail -F /var/log/syslog
...
[1292679.177172] usb 2-1.2: new low speed USB device using ehci_hcd and address 125
[1292679.279701] usb 2-1.2: New USB device found, idVendor=16c0, idProduct=05dc
[1292679.279706] usb 2-1.2: New USB device strings: Mfr=1, Product=2, SerialNumber=0
[1292679.279710] usb 2-1.2: Product: USBasp
[1292679.279713] usb 2-1.2: Manufacturer: www.fischl.de
...

Le reste de la règle sert à spécifier comment traiter ce périphérique. Ainsi, mode 0660 donne les permissions en lecture et écriture dessus pour son propriétaire et son groupe propriétaire. Le groupe propriétaire justement est fixé à dialout (par défaut, ce serait root). Normalement, c'est le groupe autorisé à utiliser le modem. Il sert aussi pour les adaptateurs USB-série. L'utilisation de ce groupe m'a semblé plus adapté que d'en créer un spécifique. Mais le choix est discutable.

Accessoirement je fais déjà partie du groupe dialout. Si ce n'est pas votre cas (ce n'est pas le cas par défaut sous Debian), vous devrez vous y ajouter:

sh# adduser sylvain dialout
Adding user `sylvain' to group `dialout' ...
Done.

L'ajout de l'utilisateur à son nouveau groupe sera effectif au prochain login de celui-ci.

Pour que la nouvelle règle soit prise en compte, vous devrez signaler à udev de recharger sa configuration:

sh# /etc/init.d/udev reload

Vous pouvez vérifier que tout est opérationnel en rebootant votre USnooBie sur le bootloader et en vérifiant dans /dev:

sh$ ls -ls /dev/USBasp 
0 lrwxrwxrwx 1 root root 15 Jul 28 15:41 /dev/USBasp -> bus/usb/002/006
sh$ ls -ls /dev/bus/usb/002/006
0 crw-rw-r-- 1 root dialout 189, 133 Jul 28 15:41 /dev/bus/usb/002/006

Programmation à partir de l'IDE Arduino

Dans la section précédente, nous avons configuré l'ordinateur sous Linux pour permettre la programmation de l'USnooBie. Il est temps maintenant de tester tout cela en téléchargeant un premier programme.

Installer et configurer l'IDE Arduino pour USnooBie

Le moyen le plus accessible de faire ses premiers tests avec l'USnooBie est d'utiliser l'IDE Arduino (et oui: l'UBnooBie est compatible Arduino!) Vous pouvez le télécharger sur le site d'Arduino http://www.arduino.cc/ ou l'installer à partir du gestionnaire de paquets de Debian:

sh# apt-get install arduino

Si vous installez l'IDE Arduino à la main à partir du site officiel, vous devrez tout de même installer sur votre machine certaines dépendances (notamment Java et les outils de développement AVR):

sh# apt-get install sun-java6-jre
sh# apt-get install gcc gcc-avr avr-libc avrdude

Dans tous les cas, pour pouvoir utiliser USnooBie comme cible pour vos développements à partir de l'IDE Arduino, il va falloir ajouter la configuration spécifique pour cette plate-forme à la liste du matériel supporté par l'IDE.

Celle-ci se trouve dans le fichier hardware/arduino/boards.txt qui peut être trouvé à partir de la racine de votre installation de l'IDE Arduino si vous avez fait l'installation manuellement, ou dans /usr/share/arduino/hardware/arduino/boards.txt si vous l'avez installé à partir du gestionnaire de paquets.

Une fois que vous aurez localisé ce fichier, il va falloir ajouter la définition correspondant à USnooBie à la fin de celui-ci:

sh# cd /path/to/arduino
sh# cat >> hardware/arduino/boards.txt << EOF
usnoobie.name=USnooBie (USBaspLoader ATmega328P at 12MHz)
usnoobie.upload.protocol=usbasp
usnoobie.upload.maximum_size=28672
usnoobie.upload.speed=115200
usnoobie.upload.disable_flushing=true
usnoobie.bootloader.low_fuses=0xFF
usnoobie.bootloader.high_fuses=0xD8
usnoobie.bootloader.extended_fuses=0xFF
usnoobie.bootloader.path=usnoobie
usnoobie.bootloader.file=usnoobie_atmega328p_12mhz.hex
usnoobie.bootloader.unlock_bits=0x3F
usnoobie.bootloader.lock_bits=0x0F
usnoobie.build.mcu=atmega328p
usnoobie.build.f_cpu=12000000L
usnoobie.build.core=arduino
# Les versions les plus récentes de l'IDE Arduino nécessitent aussi
# la ligne suivante:
usnoobie.build.variant=standard
EOF

Au prochain démarrage de l'IDE Arduino, USnooBie fera partie des cartes cibles. C'est celle-là qu'il faudra sélectionner pour la suite.

USnooBie dans IDE Arduino.png


Pour télécharger vos sketchs, il vous suffira d'activer le bootloader de l'USnooBie (en pressant les deux boutons), puis, dans l'IDE Arduino, de sélectionner la carte USnooBie. Comme nous allons le voir dans un instant, tout se passera exactement comme s'il s'agissait d'un autre modèle de carte Arduino.

Premier programme

Pour nous assurer qu'il est possible d'utiliser l'IDE Arduino pour programmer l'USnooBie, nous allons utiliser le classique et banal programme qui fait clignoter une LED (reliée à la broche 0 du port D):

void setup() {                
  pinMode(0, OUTPUT);     // Configure le port 0 en sortie
}
 
void loop() {
  digitalWrite(0, HIGH);   // Jour..
  delay(1000);              
  digitalWrite(0, LOW);    // Nuit...
  delay(1000);
}
USnooBie-LED.png

Montage de base avec l'USnooBie — Une LED connectée entre la masse (GND) et la broche PD0 constitue le montage de base pour tester l'UBnooBie et les outils de développement. Dans ce montage, c'est la cathode (la patte la plus courte de la LED) qui est reliée à la masse – puisqu'en fonctionnement le courant circulera de la broche PD0 vers la broche GND.


Une fois le programme écrit et prêt à télécharger, connectez si ce n'est déjà fait l'UBnooBie à un port USB alimenté de votre ordinateur, puis rebootez l'UBnooBie sur le bootloader en utilisant la combinaison de touches décrite au début de cet article. Il ne reste alors qu'à cliquer sur le bouton de téléchargement de l'IDE Arduino pour envoyer votre programme compilé. Dès la fin du téléchargement, l'USnooBie commence à l'exécution du programme: si votre LED est bien connectée à l'USNooBie, celle-ci doit alternativement s'allumer et s'éteindre. Et cela, tant que le micro-contrôleur sera alimenté. Pour télécharger un autre programme, il faudra à nouveau rebooter sur le bootloader.

Blink USnooBie.png

Télécharger un programme sur l'USnooBie — Le téléchargement d'un programme sur l'USnooBie se fait comme pour une carte Arduino.

Le message d'erreur « avrdude: error: usbasp_transmit: error sending control message: Broken pipe » semble causé par le fait que le bootloader rompt la connexion dès la fin du téléchargement. Ce message ne me parait pas signaler une anomalie de fonctionnement, et il peut vraisemblablement être ignoré...



Ports utilisés par le port USB sur l'USnooBie
ATmega328P Port Arduino
Broche Port
4 PORTD2 (PD2) digital pin 2
13 PORTD7 (PD7) digital pin 7

Brochage

À l'exception des ports PORTD2 et PORTD7 (utilisés pour la liaison USB) toutes les broches de l'USnooBie peuvent être utilisées comme sur Arduino, même si leur rôle n'est pas sérigraphié sur la carte. Pour vous éclairer voici une illustration de la correspondance entre les broches de l'ATmega328P et celles d'Arduino:

ATmega328P vs Arduino pin mapping.png

Correspondance entre les broches de l'ATmega328 et celles de l'Arduino.



GNU AVR Toolchain

Vous pouvez arrêter votre lecture ici. En effet, l'utilisation de l'IDE Arduino est très largement suffisante même pour des projets assez ambitieux. Et, si vous découvrez pour la première fois la programmation sur micro-contrôleur, inutile de vous embrouiller les idées avec des techniques plus complexes. Une fois plus familier avec cette plate-forme, et si vous désirez en exploiter au maximum les possibilités, vous envisagerez sans doute d'écrire des programmes plus proches du matériel. Il sera alors temps de passer à l'étape suivante, et de découvrir la suite d'outils classiques de la programmation AVR. En utilisation de base, les trois composants visibles de cette toolchain sont:

avr-gcc
Le compilateur chargé de transformer le code C en exécutable
avr-objcopy
L'outil chargé d'extraire de l'exécutable le code à télécharger
avrdude
Le programme responsable du téléchargement du code sur la cible AVR.
AVR toolchain.png

La toolchain AVR — Chaque outil de la chaîne de développement possède un rôle spécifique. Les productions de chaque outil intermédiaire servant à alimenter le suivant:

  • Le compilateur traduit en assembleur les fichiers sources écrit dans un langage de haut niveau;
  • L'assembleur transforme le code assembleur (issu de la compilation ou écrit manuellement par le programmeur) en fichier objet contenant uniquement du code binaire;
  • L'éditeur de liens (linker) rassemble les différents fichiers objets binaires et les bibliothèques en un seul fichier exécutable. L'exécutable peut être directement lancé sur un simulateur ou servir pour le débogueur;
  • Pour la programmation d'un micro-contrôleur, le programme object copy extrait de l'exécutable le code machine sous un format prêt à télécharger sur la cible;
  • Le programmeur est chargé de transférer le code vers la cible.


Même si le rôle fondamental de gcc est d'être un compilateur, dans la pratique, ce programme est capable également d'enchaîner automatiquement la compilation, puis l'invocation de l'assembleur et de l'éditeur de liens. Cela permet en une seule commande, de partir d'un code source pour arriver à l'étape du fichier exécutable. Une fois ce fichier obtenu, il restera à utiliser avr-objcopy pour en extraire le code à télécharger, puis enfin à le transférer sur la cible avec avrdude.

Installer les outils

Mais, avant de passer aux manipulations, il faut s'assurer que les différents outils sont disponibles. Comme illustré plus haut, ceux-ci sont regroupés en familles qui correspondent sur la plupart des distributions Linux à différents paquets. Sous Debian, et pour le développement pour cible AVR, les paquets à installer seront gcc-avr, binutils-avr et avrdude. Les bibliothèques C standard du paquet avr-libc seront aussi requises. Par contre, comme nous n'utiliserons pas ici le débogueur, il n'est pas nécessaire d'installer gdb-avr:

sh# apt-get install gcc-avr avr-libc binutils-avr avrdude

Remarque:

Ces paquets devraient déjà être installés si vous avez précédemment utilisé l'IDE Arduino pour programmer une cible AVR.

Premier programme

Comme plus haut, notre hello world de l'embarqué restera la LED qui clignote. Fonctionnellement, cet exemple fait la même chose que celui utilisant le SDK Arduino. Mais comme vous le constaterez le code est nettement plus ... euh ... impressionnant:

/*
 * main.c - USnooBie blinking LED (ATmega328P @ 12MHz)
 * par Sylvain Leroux (sylvain@chicoree.fr - www.chicoree.fr)
 * Sept. 2011
 *
 * Fait clignoter une LED reliée au port PD0
*/
 
#include <avr/io.h>
#include <avr/interrupt.h>
 
/*
 * Routine d'interruption pour le timer1
 */
ISR(TIMER1_COMPA_vect)
{
  PORTD ^= (1<<0);		// Inverser (xor 1) l'état du port PD0
}
 
/*
 * Programme principal.
 * L'exécution du code utilisateur commence ici
 */
int main(void)
{
  // Configuration des entrées/sorties
  DDRD |= (1<<0);		// Configurer la broche PD0 en sortie
  PORTD |= (1<<0);		// Initialiser la broche PD0 à 1 (LED allumée)
 
  // Réglage de l'horloge
  cli();			// Désactivation globale des interruptions
  TCCR1B |= 1<<CS12;		// Diviser l'horloge par 256
  OCR1A = 46875;		// Compter 46875 cycles pour 1 interruption/seconde
				// (car 256*46875 = 12000000)
  TCCR1B |= 1<<WGM12;		// Régler le timer en mode "Clear Timer on Compare"
  TIMSK1 |= 1<<OCIE1A;		// Générer une interruption à l'échéance du timer
  sei();			// Réactivation globale des interruptions
 
  // Boucle sans fin (tourne tant que l'UBnooBie n'est par rebooté)
  while(1) {
    // Rien à faire ici: tout est géré par la routine d'interruption
    // sur le timer1
  }
 
}

Avant de passer à l'analyse du programme, considérez pour l'instant qu'il fonctionne de manière un peu magique, et voyons pour commencer comment installer ce programme sur la cible USbooBie.

Compilation/téléchargement

Si tous les outils sont en place sur votre système et en supposant le code du programme enregistré dans un fichier main.c du répertoire courant, la compilation, l'assemblage et l'édition des liens peut être faite en utilisant la seule commande avr-gcc:

sh$ avr-gcc main.c  -mmcu=atmega328p

Si vous avez déjà utilisé gcc, la seule différence réside dans la présence de l'option -mmcu=atmega328p. Celle-ci indique pour quel processeur générer le code.

À l'issue de cette commande, si vous n'avez aucun message, c'est que tout c'est bien passé. Vous trouverez dans le répertoire courant un nouveau fichier nommé a.out:

sh$ ls
a.out  main.c

Avant de passer au téléchargement, il va falloir extraire le code du programme sous un format compréhensible par avrdude. Pour cela , nous allons utiliser avr-objcopy:

sh$ avr-objcopy -j .text -j .data -O ihex a.out rom.hex

Cette commande est un peu cabalistique. Les options -j .text -j .data signifient que nous souhaitons extraire du programme compilé la section .text (le code exécutable du programme) et la section .data (les données statiques) [1]. L'option -O ihex spécifie le format sous lequel le code doit être extrait. En l'occurrence, il s'agit ici du format Intel HEX utilisable avec avrdude. Quand à a.out c'est le programme exécutable duquel l'on veut extraire les données, et rom.hex, c'est le fichier de sortie dans lequel elles seront enregistrées. Ici encore, après l'exécution de la commande, vous pourrez constater l'apparition d'un nouveau fichier:

sh$ ls -l
total 12
-rwxr-xr-x 1 sylvain sylvain 4055 Sep 27 15:09 a.out
-rw-r--r-- 1 sylvain sylvain 1268 Sep 27 14:50 main.c
-rw-r--r-- 1 sylvain sylvain  860 Sep 27 15:13 rom.hex

Le fichier rom.hex est de taille nettement moins importante que l'exécutable. Il s'agit d'un fichier texte qui contient sous forme hexadécimale le code machine à télécharger. Si vous être curieux, vous pouvez l'examiner:

sh$ cat rom.hex 
:100000000C9434000C943E000C943E000C943E0082
:100010000C943E000C943E000C943E000C943E0068
:100020000C943E000C943E000C943E000C94400056
:100030000C943E000C943E000C943E000C943E0048
:100040000C943E000C943E000C943E000C943E0038
:100050000C943E000C943E000C943E000C943E0028
:100060000C943E000C943E0011241FBECFEFD8E04C
:10007000DEBFCDBF0E9464000C9494000C9400007D
:100080001F920F920FB60F9211248F939F93AF93ED
:10009000BF93EF93FF93DF93CF93CDB7DEB7ABE280
:1000A000B0E0EBE2F0E0908181E089278C93CF9182
:1000B000DF91FF91EF91BF91AF919F918F910F9041
:1000C0000FBE0F901F901895DF93CF93CDB7DEB77B
:1000D000AAE2B0E0EAE2F0E0808181608C93ABE2DA
:1000E000B0E0EBE2F0E0808181608C93F894A1E8CD
:1000F000B0E0E1E8F0E0808184608C93E8E8F0E033
:100100008BE197EB91838083A1E8B0E0E1E8F0E038
:10011000808188608C93AFE6B0E0EFE6F0E080810C
:0C01200082608C937894FFCFF894FFCF9E
:00000001FF

Maintenant que nous avons le fichier rom.hex, reste à le copier sur la cible. Pour cela, nous allons utiliser avrdude. Rebootez l'UBnooBie sur son bootloader en utilisant la combinaison de touche décrite en début d'article, puis exécutez la commande suivante:

sh$ avrdude -p m328p -c usbasp -U flash:w:rom.hex
avrdude: warning: cannot set sck period. please check for usbasp firmware update.
avrdude: AVR device initialized and ready to accept instructions

Reading | ################################################## | 100% 0.00s

avrdude: Device signature = 0x1e950f
avrdude: NOTE: FLASH memory has been specified, an erase cycle will be performed
         To disable this feature, specify the -D option.
avrdude: erasing chip
avrdude: warning: cannot set sck period. please check for usbasp firmware update.
avrdude: reading input file "rom.hex"
avrdude: input file rom.hex auto detected as Intel Hex
avrdude: writing flash (300 bytes):

Writing | ################################################## | 100% 0.04s

avrdude: 300 bytes of flash written
avrdude: verifying flash memory against rom.hex:
avrdude: load data flash data from input file rom.hex:
avrdude: input file rom.hex auto detected as Intel Hex
avrdude: input file rom.hex contains 300 bytes
avrdude: reading on-chip flash data:

Reading | ################################################## | 100% 0.02s

avrdude: verifying ...
avrdude: 300 bytes of flash verified

avrdude: safemode: Fuses OK
avrdude: error: usbasp_transmit: error sending control message: Broken pipe

Comme vous le constatez, on a ici et là quelques messages d'avertissement (warning), et le même message d'erreur pour terminer qu'avec le SDK Arduino (broken pipe). Je pense que l'on peut mettre cela sur l'émulation imparfaite d'un programmateur usbasp par le bootloader de l'USnooBie (?) Toujours est-il que le code a été téléchargé et vérifié. Immédiatement après, l'USnooBie a dû commencer à l’exécuter sans aucune intervention de votre part. Si vous faites les manipulations au fur et à mesure que vous lisez cet article, la LED doit déjà clignoter depuis quelques secondes. Dès que vous aurez réussi à vous arracher au spectacle fascinant de cette LED qui s'allume puis s'éteint (à la fréquence très précise de un cycle toutes les deux secondes – ou 0,5Hz) nous pourrons passer à l'explication du code.

Analyse du programme

La compréhension complète du code – et son éventuelle modification – nécessitent de disposer des spécifications (datasheet) du micro-contrôleur ATmega328P. Celles-ci sont disponibles à partir de la page produit de la famille ATmega sur le site d'Atmel. Le fichier à télécharger est un PDF intitulé ATmega48A/PA/88A/PA/168A/PA/328/P. Il s'agit tout de même d'un document de plus de 560 pages! Inutile de tout lire pour l'instant, puisque je vous désignerai les pages correspondantes au fur et à mesure des explications.

Si vous observez la structure du programme que je vous ai fourni plus haut, vous constaterez qu'il contient deux fonctions: une routine d’interruption, et le programme principal.

/*
 * Routine d’interruption pour le timer1
 */
ISR(TIMER1_COMPA_vect)
{
   // ...
}
 
/*
 * Programme principal.
 * L'exécution du code utilisateur commence ici
 */
int main(void)
{
 // ...
}

Routine d'interruption

Ce programme exploite le timer/counter1 du micro-contrôleur décrit à partir de la page 115 de la documentation. Ce composant peut notamment être configuré pour compter des tops d'horloge, et donc déclencher une action à intervalles réguliers. Commençons par la fin, et regardons l'action qui doit être effectué régulièrement:

/*
 * Routine d’interruption pour le timer1
 */
ISR(TIMER1_COMPA_vect)
{
  PORTD ^= (1<<0);		// Inverser (xor 1) l'état de la broche PD0
}

L'action exécutée est celle décrite dans le code de la fonction ISR(TIMER1_COMPA_vect). Ici, ce code tient en une seule ligne, et se contente d'inverser l'état de la broche PD0 (la technique est décrite pp78-79). Si elle était à l'état 1 (LED allumée), elle passera à l'état 0 (LED éteinte). Et inversement.

Le fonctionnement de notre programme implique donc que la broche 0 du PORTD soit configurée en sortie, et que le timer soit configuré pour déclencher une interruption chaque seconde (pour un clignotement à la fréquence de 0,5Hz). Tout ce travail d'initialisation est dévolu au programme principal.

Programme principal

Le programme principal (main) constitue le point d'entrée dans le code utilisateur. En français, c'est là que votre programme va commencer son exécution. Ici, le main est chargé de configurer le timer1 et la broche PD0 selon nos besoins, puis il va se "terminer" par une boucle infinie, c'est à dire une boucle active pendant lequel le micro-contrôleur ne fait rien d'utile. Par contre, à intervalle régulier, cette boucle sera suspendue (interrompue) par le timer, puis une fois la routine d'interruption finie, la boucle reprendra son cycle infini.

/*
 * Programme principal.
 * L'exécution du code utilisateur commence ici
 */
int main(void)
{
 // Configuration des entrées/sorties
 // ...
 
 // Réglage de l'horloge
 // ...
 
 // Boucle sans fin
 while() {}
}

Vous voyez que la première opération que j'effectue est la configuration des ports d'entrées/sorties. Ici, le montage n'utilise que la broche 0 du PORTD:

// ...
  // Configuration des entrées/sorties
  DDRD |= (1<<0);		// Configurer la broche PD0 en sortie
  PORTD |= (1<<0);		// Initialiser la broche PD0 à 1 (LED allumée)
DDRD |= (1<<0)
Tout d'abord je configure ce port comme une sortie. La documentation p79 explique que le registre DDRD permet de sélectionner individuellement les directions (entrée ou sortie) de chaque broche du port D. Chacun des 8 bits du registre correspond à une des 8 broches. Et mettre un bit à 1 positionne la broche correspondante en sortie.
PORTD |= (1<<0)
Le registre PORTD représente l'état de chacune des broches du port D. Aux mêmes pages que précédemment, on apprend que sur une broche en sortie, mettre le bit correspondant de PORTD à 1 positionne la broche à l'état électrique haut (la broche fournit du courant). Et à 0, celle-ci est à l'état électrique bas (elle draine du courant).

Passons maintenant à la programmation du timer. C'est sans doute la partie la plus compliquée du programme puisqu'elle fait intervenir plusieurs registres et implique des ajustements spécifiques à l'USnooBie. Si vous avez assemblé vous-même l'USnooBie, vous savez qu'il est cadencé à 12MHz par un quartz externe. Nous voulons que le timer déclenche une interruption chaque seconde. Comme celui-ci est incrémenté à chaque cycle d'horloge, il faudrait donc déclencher une interruption tous les douze millions de cycles. Sauf que le compteur utilisé par le timer 1 est un compteur 16bits. Donc limité à un maximum de 65535. Pour lever cette limitation, les micro-contrôleurs de la famille ATmega permettent de pré-diviser l'horloge (pre-scaling). Ainsi, au lieu d'être incrémenté chaque cycle, le timer ne le sera plus qu'un cycle sur 8, 64, 256 ou 1024.

Fréquence Diviseur Valeur du compteur pour 1s
12MHz ÷1 12000000    Trop grand pour un compteur 16bits
÷8 1500000    Trop grand pour un compteur 16bits
÷64 187500    Trop grand pour un compteur 16bits
÷256 46875    OK
÷1024 11718.75 Valeur décimale (imprécise)

Le nombre de combinaisons étant restreint, on peut se permettre de les énumérer comme je l'ai fait dans le tableau ci-contre. Des différentes combinaisons possibles, la seule exploitable est celle qui utilise le diviseur par 256. Dans ce cas, le compteur doit être configuré pour déclencher une interruption une fois arrivé à 46875. Cette valeur tient bien sur un entier 16bits et par ailleurs tombe juste, ce qui évite les erreurs d'arrondis cumulatives qui entraineraient une dérive.

Remarque:

Il est possible d'utiliser avec l'UBnooBie un autre oscillateur que celui de 12MHz suggéré dans le kit. Dans ce cas, il faut reprendre les calculs avec la fréquence de l'oscillateur utilisé, et se servir de la valeur obtenue dans le code ci-dessous.

La configuration complète du timer réside dans le code ci-dessous:

// ...
  // Réglage de l'horloge
  cli();			// Désactivation globale des interruptions
  TCCR1B |= 1<<CS12;		// Diviser l'horloge par 256
  OCR1A = 46875;		// Compter 46875 cycles pour 1 interruption/seconde
				// (car 256*46875 = 12000000)
  TCCR1B |= 1<<WGM12;		// Régler le timer en mode "Clear Timer on Compare"
  TIMSK1 |= 1<<OCIE1A;		// Générer une interruption à l'échéance du timer
  sei();			// Réactivation globale des interruptions
cli()/sei()
Toute la configuration a lieu entre une paire d'instructions cli()/sei(). Cela permet de désactiver les interruptions pendant la configuration. De cette manière, aucune interruption parasite n'est susceptible de se déclencher avant que nous n'ayons fini tous nos réglages.
TCCR1B |= 1<<CS12
La documentation p138 nous indique que le registre TCCR1B sert à configurer le timer1. La table p139 nous indique que pour une division de l'horloge par 256, il faut configurer le bit CS12 à 1 et CS11 et CS10 tous deux à 0. Au démarrage, tous les bits étant à 0, je n'ai qu'à positionner CS12 à 1.
OCR1A = 46875
Comme indiqué p117 et p127 de la documentation, le registre OCR1A sert de valeur maximale pour le timer quand celui-ci fonctionne en mode CTC (Clear Timer on Compare). C'est ici que nous allons mettre la valeur déterminée plus haut.
TCCR1B |= 1<<WGM12
Toujours p127 (et suivantes) la documentation décrit justement le mode de fonctionnement CTC: dans ce mode, lorsque le timer atteint la valeur configurée dans OCR1A, le timer est réinitialisé à 0. Et il repart donc pour un nouveau cycle. Le tableau p138 indique qu'il faut mettre la valeur 4 dans les bits WGM13:10 du registre de contrôle du timer1 TCCR1A pour utiliser ce mode. Ici encore, partant du principe que le registre est initialisé à 0, je ne positionne que le bit qui doit être à 1, à savoir WGM12.
TIMSK1 |= 1<<OCIE1A
Enfin, il faut configurer le timer pour déclencher une interruption à chaque fin de cycle. C'est le rôle du bit OCIE1A du registre TIMSK1 (masque d'interruption du timer1) comme indiqué p141 de la documentation.

Après toutes ces manipulations, le timer est configuré, et va commencer à déclencher une interruption chaque seconde dès que les interruptions seront réactivées (sei()). Si vous avez suivi toutes les explications, vous devriez être en mesure de modifier le code pour changer la fréquence de clignotement ou (soyons fous) pour ajouter d'autres LED reliées à d'autres ports du micro-contrôleur. Souvenez-vous juste qu'après chaque modification du code, il faut passer par le cycle gcc→objcopy→avrdude.

Make

Si vous vous êtes amusé à apporter des modifications au programme précédent, vous avez peut être remarqué qu'il devient vite fastidieux d'invoquer un à un les différents outils de la toolchain. Et encore: nous n'avons qu'un seul fichier source. Vous imaginez que cela deviendra vite pénible sur un projet à peine plus important.

En tant qu’habitué du shell et des environnements Unix-like, vous avez peut-être déjà eu le réflexe d'écrire un script pour invoquer ces différentes commandes:

sh$ cat > build.sh << EOF
#!/bin/sh
 
#
# Un script (pas terrible) pour invoquer la toolchain AVR
#
avr-gcc main.c -mmcu=atmega328p -std=c99
avr-objcopy -j .text -j .data -O ihex a.out rom.hex
avrdude -p m328p -c usbasp -U flash:w:rom.hex
EOF
sh$ chmod +x build.sh

Une fois ce genre de script écrit, il n'y a plus qu'à l'invoquer après chaque modification du code source:

sh$ ./build.sh
...

Cela facilite le travail, mais comme indiqué en commentaire, ce script n'est pas terrible. Par exemple, il continue son exécution malgré une erreur de compilation. Ou encore, il repasse par la phase de compilation, même si tout ce qui manque c'est rom.hex. Bien sûr, un script shell peut être nettement plus sophistiqué que celui que j'ai commis plus haut. Et on peut arriver à corriger toutes ces imperfections. Cependant, il existe un outil spécifique pour faciliter les tâches liées à la construction des différentes productions d'un projet. Cet outil, c'est make.

Pour connaitre les opérations à effectuer pour construire votre projet, le make a besoin que vous lui décriviez ces différentes tâches dans un fichier appelé makefile. À la différence d'un script shell ou d'un programme C, un makefile a une structure déclarative. C'est à dire que vous dites comment faire chaque étape – mais pas quand les faire. Ainsi, c'est le make lui même qui déterminera les étapes nécessaires et l'ordre dans lequel les effectuer.

L'écriture d'un makefile reste souvent un art ésotérique, mais les bases en en sont tout de même accessibles au novice. Voici un exemple adapté à notre projet AVR:

sh$ cat > Makefile << EOF
#
# Makefile simpliste pour un projet AVR
#
a.out:  main.c
       avr-gcc -mmcu=atmega328p -std=c99 main.c

rom.hex:a.out
	avr-objcopy -j .text -j .data -O ihex $< $@
'
install:rom.hex
	-avrdude -p m328p -c usbasp -U flash:w:$<
 
build:	a.out
 
all:	build	install
EOF

En quelques mots, un makefile est divisé en règles. Par exemple:

a.out:	main.c
	avr-gcc -mmcu=atmega328p -std=c99 main.c

Cette règle signifie que le fichier a.out (à gauche du :) dépend de main.c (à droite du :). Lors de son exécution, make va regarder si main.c est plus récent ou plus ancien que a.out. Si c'est le fichier source le plus récent des deux, make va en déduire que a.out est périmé. Et qu'il faut donc le reconstruire. Comment? En exécutant l'action spécifiée dans le makefile. En l’occurrence ici, en invoquant avr-gcc. Une remarque importante concernant les actions: le make peut être extrêmement pinailleur. Ainsi, pour être reconnue comme une action, la ligne doit commencer par une tabulation, et beaucoup de make refuseront de fonctionner si à la place vous mettez des espaces!

La règle suivante est à interpréter de la même manière, mais décrit la dépendance entre rom.hex et a.out:

rom.hex:a.out
	avr-objcopy -j .text -j .data -O ihex $< $@

Comme cette règle est facile à saisir, j'en ai profité pour introduire les variables (macro) $< et $@:

$<
Dans une action, cette variable est remplacée à l'exécution par le nom de la première dépendance (le premier nom de fichier à droite du :). Dans ce cas particulier, ce sera a.out.
$@
Dans une action, cette variable est remplacée à l'exécution par le nom du fichier à reconstruire (celui à gauche du :). dans ce cas particulier, ce sera rom.hex.

J'aurais aussi pu éviter d'utiliser ces variables et répéter respectivement a.out et rom.hex dans l'action. J'aurais gagné en lisibilité, mais au prix d'une répétition. Une alternative aurait été d'utiliser une variable utilisateur. Comme vous le voyez, même sur un projet aussi simple, le make offre beaucoup de souplesse et de nombreuses possibilités pour exprimer votre style personnel ou laisser transparaitre vos préférences. Quand je vous disais que l'écriture d'un makefile s'apparentait à art obscur...

Terminons cette rapide présentation par les règles suivantes:

install:rom.hex
	-avrdude -p m328p -c usbasp -U flash:w:$<

build:	a.out

all:	build	install

Celles-ci sont particulières, car les noms à gauche du deux-points ne représente pas des noms de fichiers. Dans le jargon du make on parle de phony targets. Pensez-y simplement à des moyens de grouper des dépendances entre elles. Ici encore, elles ne sont pas indispensables, mais c'est l'usage que d'avoir des cibles build (pour compiler), install (pour installer) et all (pour enchaîner ces étapes). Ah, un détail: il aurait été préférable de placer la règle pour all en premier dans le fichier, puisque par défaut make commence à résoudre les dépendances pour la première règle du fichier. Remarquez enfin, le petit moins devant la commande avrdure. Normalement, le make interrompt son exécution dès qu'une commande exécutée se termine par une erreur (erreur de compilation, par exemple). Or nous avons vu à plusieurs reprises que malheureusement, avrdude se termine systématiquement par l'erreur broken pipe lorsqu'on télécharge sur l'USnooBie. Le moins devant la commande avrdude indique au make de poursuivre malgré une erreur à ce niveau.

Voyons maintenant en quoi le makefile simplifie la construction de notre projet. Imaginons que nous n'ayons plus que les sources de notre projet (et le makefile):

sh$ rm -f rom.hex a.out
sh$ ls
main.c  Makefile

Rebootez vostre cible USnooBie sur le bootloader et tapez make all:

sh$ make all
avr-gcc -mmcu=atmega328p -std=c99 main.c
avr-objcopy -j .text -j .data -O ihex a.out rom.hex
avrdude -p m328p -c usbasp -U flash:w:rom.hex
...

Pour tout construire, le make a déduit du makefile qu'il a besoin de rom.hex, qui est construit à partir de a.out qui est construit à partir de main.c. Et en déroulant cette chaîne de dépendances, make a exécuté les différentes actions spécifiées. Remarquez que le make affiche par défaut les commandes exécutées, ce qui permet de suivre (et débugger) le fonctionnement de vos makefile. Si on modifie le fichier source, le make détectera que les productions intermédiaires sont périmées et ré-exécutera ces commandes:

sh$ touch main.c
sh$ make all
avr-gcc -mmcu=atmega328p -std=c99 main.c
avr-objcopy -j .text -j .data -O ihex a.out rom.hex
avrdude -p m328p -c usbasp -U flash:w:rom.hex

Ceci dit, pour l'instant, nous n'avons pas grand chose de plus qu'avec le script shell du début. Où l'on peut deviner la puissance du make, c'est si l'on ne supprime que rom.hex, par exemple:

sh$ rm rom.hex
sh$ make all
avr-objcopy -j .text -j .data -O ihex a.out rom.hex
avrdude -p m328p -c usbasp -U flash:w:rom.hex

Remarquez que le make n'a exécuté que les actions nécessaires pour les cibles périmées ou manquantes. Ici, a.out est toujours présent et à jour par rapport à main.c. Le make n'a donc commencé à travailler que sur la cible manquante rom.hex.

Jusqu'à présent nous avons utilisé la pseudo-cible all, mais il est aussi possible de demander la résolution d'autres cibles. Par exemple, pour juste vérifier la compilation, vous pourriez utiliser make build:

sh$ touch main.c 
sh$ make build
avr-gcc -mmcu=atmega328p -std=c99 main.c

Enfin, au contraire d'un script shell, un make s'arrête quand une erreur est rencontrée: inutile d'extraire rom.hex si les fichiers sources C ne compilent même pas:

sh$ rm a.out rom.hex
sh$ echo BUG >> main.c 
sh$ make all
avr-gcc -mmcu=atmega328p -std=c99 main.c
main.c:46: error: expected ‘=’, ‘,’, ‘;’, ‘asm’ or ‘__attribute__’ at end of input
make: *** [a.out] Error 1

Pour plus de détails sur l'utilisation de make et des makefiles, vous pouvez consulter le manuel de GNU make. Je vous renvoie aussi sur l'excellent Managing Projects with make [ Voir sur Amazon.fr] ou son édition plus récente (et plus spécialisée) Managing Projects with GNU make [ Voir sur Amazon.fr] publiés chez O'Reilly.

Conclusion

Voilà, cette introduction à la programmation pour cible AVR a été l'occasion de faire le grand tour: nous sommes partis de la technique la plus accessible, à savoir l'utilisation de l'IDE Arduino, puis nous avons abordés les outils professionnels qui permettent de manipuler au plus bas niveau le micro-contrôleur. Nous avons aussi en chemin évoqué quelques bornes pratiques, comme se référer à la datasheet de votre cible, ou utiliser make pour construire votre projet. Déjà, pris individuellement chacun de ces sujets est vaste. Et les aborder tous ici implique de ne se contenter que d'en effleurer la surface. Mais, j'espère vous avoir donné les informations nécessaires pour que vous soyez en mesure de les approfondir par vous même. Bonne découverte!

Ressources

Récupérée de « http://www.chicoree.fr/w/USnooBie »