Retrouvez cet article dans : Linux Magazine Hors série 23
Un précédent article paru dans GNU/Linux Magazine vous présentait les microcontrôleurs Microchip PIC. Une autre famille de ces composants est très populaire et bénéficie d’un très bon support sous GNU/Linux : les AVR de chez Atmel.
La famille de microcontrôleurs AVR est très vaste à l’instar de la gamme PIC de Microchip et s’étend depuis le minuscule ATtiny15L et le monstrueux ATmega256. Pour cette introduction, nous avons choisi l’ATmega16. Il est parfait pour faire ses premiers pas :
- Il dispose d’une mémoire importante de 16Ko de Flash, 512 d’EEPROM et 1Ko de SRAM. Ces caractéristiques lui permettent d’accueillir un code compilé par... GCC. En comparaison, l’ATtiny15L peut également être utilisé sous GNU/Linux mais impérativement en assembleur.
- Il existe en boîtier PDIP 40 et est donc facile d’utilisation et de mise en œuvre, même sur une simple plaque pastillée ou une platine à essais. De plus, comme d’autres AVR il ne nécessite pas de composant supplémentaire comme un quartz ou autre.
- Il est très riche en fonctionnalités (timer, convertisseurs A/D, port série, fonctions sommeil, Watchdog, 32 lignes E/S programmables, etc.). Avec un seul composant, vous pouvez explorer toutes les fonctionnalités des AVR, et ce, pour un coût relativement modique. L’ATmega16 se vend environ 10 Euros.
Le programmeur
Pour charger le code dans l’ATmega16, inutile de faire appel à un programmeur (ou programmateur) particulier. Ce microcontrôleur se programme In Situ (ICSP).
Cela signifie que le composant, dans le montage final, peut être programmé sans intervention mécanique. Pour nous, cela veut surtout dire que la seule chose à faire pour programmer le composant est de le mettre en situation comme dans un montage terminé.
La figure 1 vous donne le détail des connexions entre le microcontrôleur et le port parallèle d’un PC. Habituellement, ce schéma est donné en deux parties. D’une part le composant en situation avec les broches pour la programmation (MISO, MOSI, SCK, /RESET et Masse) et de l’autre l’adaptateur parallèle avec la correspondance entre les broches en question et les lignes du port parallèle. C’est dans cette partie que se placent les résistances de protection. Celles-ci ne sont pas absolument nécessaires, mais mieux vaut être prudent avec des composants à 10 Euros pièce.
 
Fig. 1
La photo en figure 2 montre un tel adaptateur terminé. Les caches en plastique des connecteurs DB25 laissent suffisamment de place pour y loger les résistances.
Notez la présence de la résistance entre la ligne /RESET de l’ATmega16 et l’alimentation. Il s’agit d’une résistance de rappel (voir articles dans le présent numéro sur le port parallèle en source d’interruption et l’introduction à l’électronique).
Celle-ci permet de mettre /RESET au +5V en l’absence de signal et ainsi ne PAS provoquer de reset.
 
Fig.2
Logiciels
Pour Développer du code AVR et ATmega16 en particulier, rien de plus simple, nous pouvons utiliser le C si peu familier aux électroniciens, mais idéal pour nous. La chaîne de compilation GNU permet de développer pour l’AVR et il suffira souvent d’installer les paquets
gcc-avr,
binutils-avr et
avr-libc.
Il existe plusieurs outils permettant de programmer le code dans la mémoire Flash du microcontrôleur. Nous avons choisi UISP parce qu’il supporte énormément d’interfaces de programmation In Situ et qu’il est recommandé par Guido Socher, auteur de plusieurs articles sur les AVR pour LinuxFocus.
UISP, développé par Uros Platise et Marek Michalkiewicz, supportera aussi bien le montage donné ici (le dapa pour "Direct AVR Parallel Access") qu’avec les kits Atmel STK200, 300 et 500 ou encore l’avr910 sur port série.
UISP permet également de programmer les deux registres FUSE permettant de configurer le microcontrôleur. Chaque bit ou groupe de bits de ce registre divisé en deux (l’octet haut et l’octet bas) définissent une activation ou désactivation d’une fonctionnalité ou le choix d’une configuration.
Le tableau en annexe présente la signification de chaque bit et leur valeur par défaut. A la livraison, l’ATmega16 est livré en configuration par défaut suite aux tests d’usine.
La première manipulation avec UISP nous permettra également de vérifier le bon fonctionnement du composant et de l’adaptateur parallèle. Un seul bit nous intéresse et il se trouve dans le registre de configuration, c’est le bit 6 de l’octet haut, JTAGEN.
Celui-ci programmé par défaut (0 = programmé et 1 = non programmé) active les fonctionnalités de tests et empêche l’utilisation complète du port d’E/S C. Commençons par lire les registres :
|
|
% uisp -dlpt=/dev/parport0 \
-dprog=dapa --rd_fuses
Atmel AVR ATmega16 is found.
Fuse Low Byte = 0xe1
Fuse High Byte = 0x99
Fuse Extended Byte = 0xff
Calibration Byte = 0xb9 -- Read Only
Lock Bits = 0xff
BLB12 -> 1
BLB11 -> 1
BLB02 -> 1
BLB01 -> 1
LB2 -> 1
LB1 -> 1 |
Bonne nouvelle, notre adaptateur fonctionne et UISP a reconnu l‘ATmega16. Les options utilisées sont
-dlpt pour spécifier le port parallèle via
ppdev et
-dprog pour spécifier le type d’adaptateur que nous possédons.
--rd_fuses permet de lire les registres de configuration.
Nous trouvons respectivement 0xe1 (11100001) et 0x99 (10011001), la configuration par défaut telle que spécifiée dans la documentation. Nous devons changer 0x99 en 0xd9 (11011001) pour désactiver le bit JTAGEN :
|
|
% uisp -dlpt=/dev/parport0 -dprog=dapa \
--wr_fuse_h=0xd9
Atmel AVR ATmega16 is found.
Fuse High Byte set to 0xd9 |
Dès lors, nous pouvons utiliser le port C comme n’importe lequel des ports disponibles sur l’ATmega16. Revenons un instant sur la raison de ce changement (en dehors de la vérification du fonctionnement de l’adaptateur et d’UISP). Vous l’avez sans doute remarqué sur le schéma en figure 1, les broches du microcontrôleur ont plusieurs désignations.
Si nous prenons la broche 35, nous remarquons qu’il s’agit de PC5 (bit 5 du port C) mais également de TDI. Avec JTAGEN activé, la broche 35 n’est pas PC5, mais l’entrée pour les tests (TDI pour JTAG Test Data In).
Il en va de même pour quasiment toutes les broches du composant et une lecture de la documentation est nécessaire pour leur utilisation.
Ne vous effrayez pas devant les quelques 350 pages qu’elle comporte, il suffit de chercher le point précis qui nous préoccupe. Une lecture complète n’est ni utile ni souhaitable.
Notre code de test
Un microcontrôleur ne dispose pas des ressources mémoire d’un ordinateur. Très nombreux sont les composants ne se programmant qu’en assembleur pour cette raison. Ici, nous pouvons utiliser le C mais le code reste simple et concis.
A l’instar du classique "Hello World!" cher aux premiers programmes de tout langage, il existe un premier programme classique pour les microcontrôleurs : le clignotement d’une LED. C’est précisément l’objet du code suivant :
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
|
#include <avr/io.h>
void mondelai(unsigned short i)
{
while(i>0) {
i--;
}
}
int main(void)
{
DDRC|= _BV(PC5);
while (1) {
PORTC &= ~_BV(PC5);
mondelai(65000);
PORTC|= _BV(PC5);
mondelai(5000);
}
} |
Détaillons tout cela. Le fichier
avr/io.h est en fait une simple astuce permettant d’inclure les macros spécifiques au modèle de microcontrôleur. Dans le cas de l’ATmega16, il s’agit de
avr/iom16.h. La fonction
mondelai est une simple boucle de retard afin que nous ayons le temps de voir clignoter la LED. Notez que nous devons utiliser un type
unsigned short.
DDRC est le registre de direction pour le port C. Chaque ligne de chaque port peut être définie en entrée (0) ou en sortie (1). La macro
_BV correspond tout simplement Ã
(1 << (bit)). C’est une facilité permettant d’activer un bit au choix dans un octet. Ainsi, nous mettons à 1 le bit 5 du registre
DDRC. La ligne est en sortie.
Vient ensuite la boucle principale.
PORTC correspond à l’adresse du port C. Là encore, nous utilisons la macro
_BV pour jouer sur le bit 5. Nous procédons à un ET logique après avoir inversé l’octet obtenu et passons ainsi le bit à 0 pour mettre la ligne à la masse.
Nous appelons notre fonction de retardement puis nous procédons à l’opération inverse (avec un OU).
Nous enregistrons ce source dans un fichier
premierAVR.c puis construisons notre
Makefile :
Les points importants ici sont :
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
|
CC=avr-gcc
OBJCOPY=avr-objcopy
PARPORT=/dev/parport2
CFLAGS=-g -mmcu=atmega16 -Wall \
-Wstrict-prototypes -Os -mcall-prologues
all: premierAVR.hex
premierAVR.hex : premierAVR.out
$(OBJCOPY) -R .eeprom -O ihex \
premierAVR.out premierAVR.hex
premierAVR.out : premierAVR.o
$(CC) $(CFLAGS) -o premierAVR.out \
-Wl,-Map,premierAVR.map premierAVR.o
premierAVR.o : premierAVR.c
$(CC) $(CFLAGS) -c premierAVR.c
clean:
rm -f *.o *.map *.out *.hex |
- L’option -mmcu=atmega16 permet de spécifier le type de microcontrôleur que nous utilisons. D’elle découle les fichiers d’en-tête utilisés et ainsi des macros propres au modèle d’AVR.
- avr-objcopy permet de convertir le fichier objet dans un format intelligible par UISP
Après exécution de la commande make, nous obtenons plusieurs fichiers dont celui qui sera utilisé par UISP (
premierAVR.hex).
C’est une représentation hexadécimale du code qui sera chargé dans l’ATmega16. Le fichier
premierAVR.map est un résumé de l’occupation mémoire du microcontrôleur.
Nous ajoutons quelques lignes à notre
Makefile afin d’automatiser le chargement du code :
|
|
prog_erase:
uisp -dlpt=$(PARPORT) --erase -dprog=dapa
prog_burn:
uisp -dlpt=$(PARPORT) --upload \
if=premierAVR.hex -dprog=dapa -dno-poll -v
prog_verif:
uisp -dlpt=$(PARPORT) --verify \
if=premierAVR.hex -dprog=dapa -dno-poll -v
prog: prog_erase prog_burn prog_verif
cprog: premierAVR.hex prog_erase prog_burn prog_verif |
Les options utilisées sont respectivement
--erase,
--upload et
--verify pour l’effacement de la mémoire flash, le chargement/programmation du composant et la vérification de l’opération d’écriture.
Notez la présence de l’option
-dno-poll permettant de désactiver le polling sur le port et avoir un accès direct à ce dernier.
Il ne nous reste plus qu’à mettre le microcontrôleur en situation en l’alimentant et en connectant sur la broche 35 une résistance de 470 Ohms et une LED dont la cathode sera reliée à la masse commune (figure 3).
 
Fig. 3
Enfin, connectons l’adaptateur pour la programmation In Situ et exécutons joyeusement un make prog :
|
|
[...]
Erasing device ...
Reinitializing device
[...]
Uploading: flash
#######
(total 200 bytes transferred in 0.15s (1301 bytes/s)
[...]
Verifying: flash
#######
(total 200 bytes transferred in 0.13s (1584 bytes/s) |
Immédiatement après la programmation l’ATmega16 exécute le code dans sa mémoire flash et la LED clignote comme prévu. Si vous rencontrez des problèmes lors de la programmation, vérifiez que le port utilisé est bien le bon, que vous disposez des permissions adéquates et que les connexions sont correctes. Enfin, si le problème persiste, vous pouvez jouer sur les options de paramétrage des délais comme -dt_sck. N’oubliez pas, enfin, que par défaut Linux n’est pas en temps réel et que la charge système influe directement sur la gestion des délais.
Un peu plus loin
Notre premier code est non seulement simpliste mais relativement cavalier. En effet, la libc pour les microcontrôleurs AVR offre quelques facilités de développement. Ainsi, lorsqu’on souhaite utiliser des délais d’attente, nous pouvons utiliser les fonctions prévues dans avr/delay.h comme _delay_us ou _delay_ms. Malheureusement, dans le cas présent c’est insuffisant. La première fonction prend en paramètre un maximum de 768 microsecondes et la seconde une valeur identique divisée par la fréquence du microcontrôleur (1Mhz par défaut).
Comme spécifié dans delay.h, pour des délais plus importants, on utilise normalement l’un des timers inclus dans l’AVR. Un timer est une horloge/compteur interne. Il se configure en lui associant une source d’horloge et éventuellement un diviseur.
Le microcontrôleur fonctionnant par défaut à 1Mhz, le diviseur permet d’espacer les tick d’horloge. Pour utiliser un timer en C, il ne nous faut que quelques lignes. Commençons par inclure des fichiers d’en-tête appropriés, puis configurons le timer :
|
|
#include <avr/interrupt.h>
#include <avr/signal.h>
[...]
TCCR1B = _BV(CS10) | _BV(CS11) | _BV(WGM12); |
TCCR1B est le registre B permettant la configuration du Timer/Counter 1. A l’aide de OU logiques, nous activons certains bits qui nous intéressent. CS10, CS11 et CS12 nous permettent de choisir la source d’horloge et le diviseur à utiliser.
Ici 011 définit un diviseur de 64 (voir documentation page 112). Le bit WGM12 précise que le timer (TCNT1) est remis à zéro lorsqu’il y a correspondance avec le registre de comparaison.
Dès lors, l’ATmega16 en fonctionnement incrémentera le timer à une fréquence de 1Mhz/64 soit environ 15Hz. Il faut maintenant surveiller ce timer afin de l’utiliser. Pour cela, inutile d’écrire le moindre code, le microcontrôleur possède un registre de comparaison qu’il suffit d’initialiser avec la valeur de timer qui nous intéresse :
F_CPU sera défini avec la fréquence à laquelle fonctionne l’AVR. Nous configurons ici le registre de comparaison à la valeur correspondante à celle du diviseur pour obtenir une fréquence de 1Hz au final. OCR1A et le timer sont comparés à chaque cycle.
A présent, lorsqu’il y a correspondance, le timer est remis à zéro, mais nous en attendons plus. Nous voulons qu’une interruption se déclenche.
Pour ce faire, il faut configurer le registre TIMSK (Timer Interrupt Mask Register) et en particulier le bit 7 nommé OCIE1 (Timer/Counter1 Output Compare Match Interrupt Enable) :
Maintenant, lorsque qu’il y a correspondance entre le timer et OCR1A une interruption survient et nous pouvons l’utiliser. Il nous suffit de définir la macro de callback SIGNAL par ailleurs :
|
|
SIGNAL (SIG_OUTPUT_COMPARE1A)
{
PORTC|= _BV(PC5);
_delay_us(768);
PORTC &= ~_BV(PC5);
} |
Le signal concerné est SIG_OUTPUT_COMPARE1A, celui-là même généré à la correspondance de OCR1A. Nous réutilisons les macros d’activation de la broche 35.
Notez que cette fois, nous utilisons la fonction _delay_us avec sa valeur maximale en lieu et place de notre boucle d’attente.
Cela donnera une brève impulsion à la LED, suffisante pour être visible. Pour utiliser _delay_us nous devons définir F_CPU avec la fréquence du microcontrôleur avant d’inclure avr/delay.h.
La totalité du code devient donc :
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26
|
#include <avr/io.h>
#include <avr/interrupt.h>
#include <avr/signal.h>
#define F_CPU 1000000UL
#include <avr/delay.h>
SIGNAL (SIG_OUTPUT_COMPARE1A)
{
PORTC|= _BV(PC5);
_delay_us(768);
PORTC &= ~_BV(PC5);
}
int main(void)
{
DDRC|= _BV(PC5);
PORTC &= ~_BV(PC5);
TCCR1B = _BV(CS10) | _BV(CS11) | _BV(WGM12);
OCR1A = (F_CPU/64);
TIMSK = _BV(OCIE1A);
sei();
for (;;) {}
} |
Notez l’appel à la fonction sei() permettant le traitement des signaux qui devient transparent pour le développeur. A présent, notre code simpliste utilise presque pleinement les ressources et facilités mises à notre disposition à la fois par le microcontrôleur et par la libc AVR.
Encore trop de code
Et oui, j’ai dis que nous utilisions " presque pleinement " les ressources de l’AVR. Pour le type d’utilisation que nous avons ici, il n’est pas forcément nécessaire de développer son propre code pour faire clignoter la LED.
Il est possible de directement relier la sortie du timer avec une ligne du port D. L’une des lignes utilisables est OC1A/PD5 (19). Pour faire l’association, il suffit d’activer le bit COM1A0 du registre TCCR1A :
Dès lors, la correspondance entre le timer et OCR1A provoque une bascule sur la ligne OC1A/PD5 à la fréquence correspondante. Notez que le délai d’allumage et d’extinction de la LED sont égaux. Nous n’avons plus besoin de SIGNAL ni de la fonction de gestion seo(). Notre code se résume ainsi à cinq lignes :
|
|
DDRD|= _BV(PD5);
TCCR1B = _BV(CS10) | _BV(CS11) | _BV(WGM12);
TCCR1A = _BV(COM1A0);
OCR1A = (F_CPU/64);
for (;;) {} |
Je comptais conclure l’article sur ce dernier exemple, mais hypnotisé par la LED clignotant frénétiquement sur mon bureau, je ne peux m’empêcher de vous parler de la PWM.
PWM
La question de base qui mène ici à la PWM (Pulse Width Modulation) est : comment jouer sur l’intensité lumineuse de la LED. Que ceux qui s’apprêtaient à répondre qu’il suffit de jouer sur l’intensité du courant se détrompent.
Une LED n’est pas une ampoule à filament. Au-delà d’une certaine intensité la LED est détruite, en dessous de l’intensité conseillée, son bon fonctionnement n’est plus garanti.
La solution consiste donc à la faire clignoter de manière à utiliser la persistance rétinienne. Mais là encore, on peut se tromper. Ce n’est pas la fréquence de clignotement qui importe mais un rapport de phase.
On définit ainsi une fréquence constante et on module le rapport entre les périodes d’allumage et d’extinction. C’est la PWM ou modulation d’impulsions en largeur en bon français.
L’ATmega16, comme d’autres modèles de microcontrôleurs Atmel, dispose en interne de fonctionnalités permettant d’utiliser la PWM sans avoir à la coder dans un programme. Tout comme avec le code précédent, la PWM est directement liée avec la configuration du timer et du registre TCCR1A.
C’est la valeur donnée au comparateur OCR1A qui déterminera le rapport de phase.
Afin de bien voir et donc comprendre le fonctionnement de la PWM, configurons le timer nous permettant d’avoir une fréquence PWM d’environ 4KHz (avec un diviseur de 256) :
|
|
TCCR1B = _BV(CS12) | _BV(WGM12); |
Reste ensuite à configurer TCCR1A de manière à obtenir la PWM sur la sortie OC1A/PD5 :
|
|
TCCR1A = _BV(COM1A1) | _BV(WGM10) | _BV(WGM12); |
En activant les bits WGM10 et WGM12, nous configurons un mode dit "Fast PWM". Le bit COM1A1 définit le mode de comparaison et l’effet sur OC1A/PD5 (et OC1B/PD4).
Le timer est remis à zéro lorsqu’il atteint la valeur maximale (TOP) définie par la résolution PWM. Avec WGM10 et WGM12 cette résolution est de 8 bits et la valeur maximale de 255.
En activant COM1A1 on définit les règles suivantes : on active OC1A/PD5 lorsque le timer atteint la valeur maximale (TOP) et on désactive OC1A/PD5 lorsqu’il y a correspondance entre le timer et OCR1A.
Si OCR1A est égal à TOP (255) alors notre sortie est toujours active. Il n’y a plus de phase où la ligne est ramenée à la masse. C’est un cas particulier géré par l’AVR, il n’y a pas de changement du doute, même d’une microseconde. Si nous définissons OCR1A à 127 voici ce qui arrivera :
- Le timer au bout de 256 tick atteindra la valeur maximale TOP
- OC1A/PD5 va alors être active
- Le timer bouclera
- Le timer arrivera à 127, il y a correspondance avec OCR1A
- OC1A/PD5 est ramené à la masse, notre LED s’éteint
- le timer continue et arrive à nouveau à TOP
- OC1A/PD5 allume notre LED
- etc.
Dans ce cas de figure, les périodes allumées/éteintes de la LED sont égales, elle clignote. Avec un diviseur de 256 (CS12) et une fréquence de 1Mhz, cela devient parfaitement visible.
Si nous descendons encore la valeur de OCR1A, les périodes où la LED est allumée deviennent plus courtes que lorsqu’elle est éteinte. Elle ne scintille plus, elle clignote franchement. Faites l’essai avec le code suivant :
|
|
#include <avr/io.h>
#include <avr/interrupt.h>
#include <avr/signal.h>
int main(void)
{
DDRD|= _BV(PD5);
TCCR1B = _BV(CS12) | _BV(WGM12);
TCCR1A = _BV(COM1A1) | _BV(WGM10) | _BV(WGM12);
OCR1A=127;
for (;;) {}
} |
Compilez et chargez le code dans le microcontrôleur, puis changez la valeur de OCR1AL entre 0 et 255. On voit alors clairement ce qu’est la PWM.
Maintenant, remplacez _BV(CS12) par _BV(CS11) pour un diviseur de 8 au lieu de 256. La fréquence est telle que nous ne voyons plus le clignotement, l’intensité lumineuse de la LED semble différente. Plus loin encore, faisons varier OCR1A en plaçant dans la portée de la boucle for le code suivant :
|
|
for (i=15; i<255; i++)
{
OCR1A = i;
_delay_us(768);
}
for(i=255; i>15; i--)
{
OCR1A = i;
_delay_us(768);
} |
La LED n’est jamais complètement éteinte car OCR1AL n’est jamais à 0 et la LED "pulse". Nous pouvons ensuite jouer sur la vitesse de pulsation en définissant des délais plus longs que les 768 microsecondes.
La PWM permet d’influer sur le fonctionnement de composants pour lesquels il n’est pas possible de moduler l’intensité du courant ou la tension. Ceci s’applique aux LED comme ici, mais également à des moteurs. La PWM permet également la régulation de tensions. Il suffit d’utiliser un condensateur et on arrive ainsi à obtenir toute une gamme de tensions.
N’oubliez pas, le condensateur s’oppose à tout changement brusque de tension. Les convertisseurs digitaux/analogiques fonctionnent également de cette manière avec la PWM.
Conclusion
Le microcontrôleur ATmega16, comme le reste de la famille AVR d’Atmel, est un composant très riche. Nous n’avons ici fait qu’effleurer le sujet, même si cela paraît déjà beaucoup.
Mais son plus grand intérêt réside dans le fait de pouvoir le programmer en C grâce à des outils libres et une bibliothèque C vraiment complète.
Si vous souhaitez investir du temps et un peu d’argent dans le monde des microcontrôleurs, on peut affirmer sans trop de risque que les AVR sont un excellent point de départ. Vous pourrez toujours, par la suite, vous tourner vers l’assembleur puis aborder d’autres microcontrôleurs comme les PIC ou ceux produits par Freescale.
Personnellement, depuis que j’ai fait connaissance avec les AVR, je n’ai plus vraiment envie de toucher aux PIC.
 
Retrouvez cet article dans : Linux Magazine Hors série 23