Vous êtes sur la page 1sur 18

1. Qu'est-ce qu'un microcontrôleur ?

La figure 1 montre l'architecture simplifiée d'un système informatique, qui comporte quatre
éléments:
- une mémoire morte (ROM = Read Only Memory) qui contient les instructions. Son contenu est
permanent, et reste intact lorsque le système n'est plus alimenté (Généralement de type FLASH).
- un processeur, qui exécute les instructions. Il est cadencé par une horloge (H).
- une mémoire vivre (RAM= Random Access Memory), qui permet de stocker des données. Son
contenu est perdu lorsque le courant est “coupé”.
- des circuits d'Entrée-Sortie (I/O=Input/Output). Ce sont les circuits qui permettent l'interaction
avec l'extérieur.

Bus d'adresses

Sorties
PC

Processeur ROM RAM I/O

H
Entrées
Horloge

Bus de données

Figure
1

Une carte-mère d'un PC possède cette même architecture. La mémoire morte est appelée BIOS.
C'est le premier programme qui s'exécute lorsqu'on allume le PC.
Le processeur (par exemple un Pentium 4) est un circuit intégré contenant des millions de
transistors, fonctionnant à une fréquence très élevée. Malgré la très faible consommation de chacun
de ces transistors (technologie C-MOS), la complexité du circuit et la fréquence élevée (jusqu'à
plusieurs GHz) font que le processeur consomme une énergie importante (plusieurs dizaines de
Watt). Il est donc nécessaire de le refroidir avec un refroidisseur et un ventilateur.
Les barrettes de mémoire vive (SD-RAM, DDR, DDR-2, etc) ont généralement des capacités
exprimées en centaines de MiB (millions d'octets) ou en GiB (milliards d'octets).
Les circuits d'entrées-sorties correspondent au clavier, à la souris et à l'écran (interface homme-
machine), mais aussi aux lecteurs de disques, CD, DVD, etc.

Un microcontrôleur est un système informatique contenu dans un seul circuit intégré.


L'architecture est la même que cette présentée sur la figure 1. Par rapport à une carte-mère de PC,
les éléments qui constituent un microcontrôleur sont plus simples, moins puissants, leurs capacités
sont plus limitées:
- la mémoire morte contient généralement de 1 à quelques dizaines de kiB.
- le processeur est cadencé à des fréquences de quelques MHz ou dizaines de MHz, et ne consomme
qu'une fraction de Watt. Son jeu d'instructions est plus simple.
- la mémoire vivre est généralement très limitée: de quelques centaines de Bytes, à quelques
dizaines de kiB, selon les modèles.
Exemple :Atmega 32 EEPROM 1Ko,SRAM 2Ko, ROM 32Ko
- les circuits d'entrée-sortie sont simplement des entrées logiques (pour lire une valeur binaire, par
exemple un interrupteur) et des sorties logiques (capables de fournir quelques mA, par exemple
pour commander une LED). Certain microcontrôleurs ont aussi des entrées analogiques: des
convertisseurs analogiques-numériques (ADC= Analog to Digital Converter), et parfois des sorties
analogiques: des convertisseurs numériques analogiques (DAC=Digital to Analog Converter).

L'intérêt des microcontrôleurs est leur coût très faible (quelques centaines ou milliers de CFA),
leur faible consommation (quelques dizaines de mA) et leur taille très réduite (un seul circuit
intégré, ayant de 6 à quelques centaines de pattes). Ils sont donc utilisés dans de très nombreuses
applications.

Plusieurs fabricants proposent des microcontrôleurs (Microchip, Atmel, Texas-Instrument,


FreeScale, NXP, Cypress, etc). Chaque fabricant propose souvent plusieurs familles de
microcontrôleurs (PIC et dsPIC chez Microchip; AVR, AVR32 et ARM chez Atmel, etc).
Chaque famille comporte souvent des dizaines de modèles, qui diffèrent principalement par les
tailles mémoires et le nombre de pattes d'entrée-sortie.

A titre d'exemple, le processeur Atmega168, de la famille des AVR d'Atmel, contient:


- un processeur 8 bits, avec une centaine d'instructions, cadencé entre 1 et 20MHz (généralement
8MHz), avec 32 registre 8 bits.
- une mémoire morte de type flash (technologie similaire à celle des clés USB) de 16 kiB.
- une mémoire vive de 1 kiB.
- une mémoire EEPROM de 512 B.
- 22 pattes d'entrée-sorties, dont 6 connectées à un convertisseur analogique-numérique de 10 bits
de résolution. Certaines de ces pattes ont également d'autres fonctions spécifiques (lignes séries,
capture d'événements, etc).
Voici le symbole utilisé dans le logiciel de dessin de schémas et de circuits imprimés KiCad:
Premier schéma à microcontrôleur

Voici le brochage d'un microcontrôleur, de la famille Atmel largement utilise (ATtiny2313.pdf).

Chaque patte est associée à un ou plusieurs noms, tous un peu incompréhensibles pour un débutant.
Nous pouvons identifier trois groupes de pattes: les alimentations, les pattes de programmation et
les pattes d'entrées-sorties pour l'application proprement dite.
Les microcontrôleurs sont réalisés en technologie CMOS et nécessitent une alimentation unique.
Les AVR acceptent une tension comprise entre 2.7et 5.5 volt. Deux piles AA ou AAA 1.5 volt en
série peuvent donc convenir. La borne négative de l'alimentation est désignée Gnd
(Ground=masse). C'est la patte 10 du ATtiny2313. La borne positive est appelée Vcc, c'est la
patte 20.
La patte /RESET est la patte 1. Ignorons pour le moment le fait que cette patte a aussi d'autres noms
(PA2, etc). Le /RESET est une entrée du circuit. La barre sur son nom signifie que l'entrée est
active à zéro. On le note aussi, comme dans ce texte, avec le signe / (slash) précédent le nom. Il
sera donc nécessaire de fixer à l'état 1 la patte /RESET pour le fonctionnement normal du
microcontrôleur. C'est une résistance reliée au Vcc qui va jouer ce rôle. On parle de résistance de
rappel (pull-up: tirer vers le haut). Une valeur d'environ 10 kOhm convient dans ce cas.
La programmation des AVR se fait par l'intermédiaire de trois signaux: MOSI, MISO et SCK. Il
n'est pas nécessaire de comprendre le rôle exact de ces signaux, qui sont générés par le programme
AVRISP qui tourne sur le PC et transmis par le dongle de programmation. Notons toutefois qu'il
s'agit du signal d'horloge (Spi ClocK), de l'entrée du microcontrôleur (Master Out Slave In) et de la
sortie du microcontrôleur (Master In Slave Out). Ces trois signaux correspondent aux pattes 17 à
19.

Les autres pattes de l'ATtiny2313 sont les pattes d'entrée-sortie. Elles sont regroupées en 3 ports:
PORTA, PORTB et PORTD. Notez tous les ports ne sont pas complets, et que les pattes de
programmation font aussi partie du PORTB. En prenant quelques précautions, on peut aussi les
utiliser comme entrées-sorties. Par contre, la programmation par le connecteur AVR-ISP n'est plus
possible si la patte /Reset n'est pas réservée exclusivement à cette fonction. On peut donc dire qu'en
pratique, l'ATtiny2313 a 17 pattes d'entrée-sortie.
Il nous reste maintenant à choisir notre application. Prenons la plus simple: une seule patte du
microcontrôleur sera utilisée, pour y brancher une diode lumineuse (LED) avec une résistance de
limitation du courant reliée en série vers la masse. Nous avons donc maintenant le schéma complet
de notre montage.

100nF 2 Vcc
10k
20
1
Vcc Reset 6 /Reset
Connecteu
ATtiny2313 r AVR-
ISP
PD6 SCK
11 3 SCK
LED 19
MISO 4 MISO
18
MOSI 5 MOSI
1k Gnd 17

10 1 Gnd

Nous notons la présence d'un condensateur de découplage entre le + et le -. Les électroniciens


savent qu'il est utile pour filtrer l'alimentation. On peut apprécier la simplicité: seulement 5
composants et un connecteur. Ce connecteur AVR-ISP permet à la fois d'apporter l'alimentation à
notre montage, ainsi que les signaux de programmation et le Reset, dans la phase de
développement du programme correspondant à l'application.

Voici le schéma pour un autre processeur de la famille AVR,


l'ATmega168:

100n 2 Vcc
10
F
7 2 k
0 1
Vcc Reset
AVcc
Connecteur
ATmega16 AVR-ISP
8
SCK 3 SCK
LE PC5 19
28
D MISO 4 MISO
18
MOSI 5 MOSI
17
1k
Gnd

8 22 1 Gnd

Notons qu'il n'y a que peu de différences. Deux pattes sont connectées à la masse. Une patte
supplémentaire est présente pour alimenter la partie analogique (AVcc). Notons que, par hasard,
les numéros des pattes /Reset, SCK, MISO et MOSI ont les mêmes numéros. Mais ils n'ont pas
les mêmes positions sur le circuit, vu qu'il s'agit d'un DIL28 et non plus d'un DIL20 !
Les entrées-sorties des microcontrôleurs

Dans les années 1970, au moment où les microprocesseurs sont apparus, des circuits intégrés
logiques discrets étaient utilisée pour réaliser les entrées et les sorties du système. Pour les entrées,
des passeurs permettaient de lire les données lors de la sélection de l'adresse correspondante. Pour
les sorties, des registres PIPO (Parallel In Parallel Out) étaient utilisés pour mémoriser les
valeurs.

Entrées Sorties

Registre PIPO

Bus de données
Passeurs

Pour simplifier les schémas, les fabricants de microprocesseurs ont proposé des circuits intégrés
contenant tout ce qui est nécessaire pour les entrées-sorties. Comme le nombre d'entrées et le
nombre de sorties varient d'une application à une autre, ces circuits offraient la possibilité de
programmer certaines pattes comme des entrées et d'autres comme des sortie. Intel a proposé son
circuit 8255, où des groupes de 4 ou 8 pattes pouvaient être définis ensemble comme entrées ou
comme sorties. Mais Motorola a fait école avec son PIA (MC6820), en offrant une flexibilité
complète, chaque patte pouvant être définie individuellement comme une entrée ou une sortie.

Nous allons ici décrire les entrées-sorties des circuits de la famille des microcontrôleurs AVR,
similaires aux PIA, mais plus riches. Voici le schéma simplifié correspondant à chaque patte
d'entrée-sortie, illustré pour la patte PB2:
Lec ture de
PINB passeurs

patt
PORT e
Bus de PB2
B2
donnée E c riture dans PORT B
s 22
DDR
B2
Ec riture dans
DDRB

On y trouve deux passeurs. L'un permet à tout instant de lire d'état de la patte. L'autre permet
d'imposer une valeur logique à la patte, lorsqu'il s'agit d'une sortie.
On y trouve aussi deux bascules. Chacune fait partie d'un registre 8 bit. L'une de ces bascules est
appelée DDR (Data Direction Register). Elle permet d'activer ou non la sortie. L'autre est appelée
PORT. Elle donne l'état devant être passé à la sortie.
Le tableau suivant donne la fonctionnalité correspondant à l'état des deux bascules:

DD POR Rôle de la sortie


0R 0T Entrée
0 1 Entrée
1 0 Sortie, état 0
1 1 Sortie, état 1
Ce schéma est très pratique. Il permet en effet de choisir le rôle de chaque patte d'entrée-sortie, et de
le choisir à tout instant. Certains dispositifs de communication nécessitent en effet qu'un patte soit
une entrée à certains moments et une sortie à d'autres. C'est le cas par exemple de l'interface de
claviers PS-2 des PC.
Voici le schéma généralement utilisé pour lire la valeur d'un interrupteur ou d'un bouton-poussoir.

Entrée du
B outon mic roc ontrôleur
pous s
oir

Lorsque le bouton-poussoir est pressé, l'entrée du microcontrôleur reçoit la valeur « 0 ». Lorsqu'il


est ouvert, une résistance est nécessaire pour qu'une valeur « 1 » soit transmise à l'entrée. On
l'appelle résistance de rappel, ou pull-up.
Sans cette résistance, l'entrée serait en l'air. Or les entrée C-MOS ont une impédance très élevée,
leur état est donc indéterminé lorsqu'elles ne sont pas reliées. On observe facilement dans la
pratique qu'une entrée en l'air change de valeur à chaque instant, sous l'influence des perturbations
électromagnétiques ambiantes.
Avec ce schéma, la valeur lue sur l'entrée sera 0 lorsque le bouton est pressé (donc actif) et 1
lorsque le bouton est relâché. Il ne faut pas oublier cette particularité lors de l'écriture des
programmes: la condition est inversée, le signal est actif à zéro.

Dans le tableau indiquant le rôle des entrées-sorties en fonction des bits DDR et PORT, on remarque
que lorsque DDR est à zéro, l'état de PORT n'a pas d'effet. Certains fabricants de microcontrôleurs
ont eu l'idée de compléter le schéma de la manière suivante.

DD POR Rôle de la sortie


0R 0T Entrée, haute impédance
0 1 Entrée, avec pull-up
1 0 Sortie, état 0
1 1 Sortie, état 1

Une résistance a été intégrée sur le circuit intégré. Notez qu'une résistance est beaucoup plus
coûteuse en place sur le circuit intégré qu'un transistor MOS. Sa valeur est d'environ 50 kOhm.
Le rôle de cette résistance est donc d'imposer la valeur logique « 1 » à l'entrée lorsque celle-ci en
l'air (non connectée). C'est ainsi qu'un bouton-poussoir pourra être connecté très simplement à une
entrée d'une AVR, sans besoin de résistance externe.
Manipulation de champs de bits en C

Les ports d'entrée-sortie des microcontrôleurs sont le plus souvent vus pas l'application comme des
bits séparés, alors qu'ils sont physiquement adressés par groupe de 8 bits. Il faut donc disposer des
outils nécessaires pour manipuler séparément des bits dans des champs de bits.
Trois problèmes se posent:
– mettre un ou plusieurs bits à la valeur 1 (set bit)
– mettre un ou plusieurs bits à la valeur 0 (clear bit)
– tester la valeur d'un bit (test bit).
Prenons un exemple concret: les différents bits du PORTD d'un ATtiny2313 sont utilisés à diverses
fins, certains comme entrées, d'autres comme sorties. Sur la broche PD6 se trouve une LED, qu'on
souhaite allumer ou éteindre à certains moments. Sur les broches PD2 et PD3 se trouvent des
boutons-poussoirs.

Set bit
Pour mettre un bit à la valeur 1, le problème se pose de la manière suivante: le PORTD contient les
valeurs: x7 x6 x5 x4 x3 x2 x1 x0, toutes à priori inconnues. Après l'opération Set Bit sur le bit
PD6, on souhaite obtenir les valeurs suivantes: x7 1 x5 x4 x3 x2 x1 x0 dans le PORTD.
Les lois de l'algèbre de Boole nous affirment les égalités suivantes:
A.0=0
A . 1 = A (1 est l'élément neutre du ET
A + 0 = A (0 est l'élément neutre du
OU) A + 1 = 1
On remarque rapidement que la fonction OU va nous permettre de réaliser la mise à 1 d'un bit:
x7 x6 x5 x4 x3 x2 x1 x0
0 1 0 0 0 0 0 0
------------------------ OU
x7 1 x5 x4 x3 x2 x1 x0
L'opérateur OU s'appliquant à un champ de bits s'écrit | en C. Il ne faut pas le confondre avec
l'opérateur || qui s'applique à deux valeurs vues comme des booléans (la valeur 0 correspondant
à faux et toute valeur différente de zéro correspondant à vrai).
L'opération Set Bit s'écrit donc, dans notre exemple:
PORTD = PORTD | 0b01000000;
La syntaxe suivante est équivalente, mais plus compacte à écrire:
PORTD |= 0b01000000;
Noter les valeurs directement en binaire, ou en hexa-décimal, ou encore en décimal, rend les
programmes peu lisibles. On préfèrera la syntaxe suivante:
PORTD |= (1<<PD6);
La constante PD6 vaut 6, dans les déclarations standard proposées pour les AVR. C'est le rang du
bit dans l'octet, ou autrement dit la puissance de deux correspondante. L'opérateur de décalage est
utilisé ici pour mettre le bit à sa place.
Remarque importante: l'expression (1<<PD6) est évaluée à la compilation et non à l'exécution, vu
qu'elle ne comporte que des constantes. Choisir d'écrire de manière lisible ne pénalise donc pas les
performances du programme, ni la taille du binaire !
Clear bit
De la même manière, on utilisera l'opération logique ET pour la mise à 0 d'un bit. Mais l'élément
neutre est alors le 1:
x7 x6 x5 x4 x3 x2 x1 x0
1 0 1 1 1 1 1 1
----------------------- ET
x7 0 x5 x4 x3 x2 x1 x0
D'où l'expression:
PORTD = PORTD & 0b10111111;
On préfèrera:
PORTD &= ~(1<<PD6);
L'opérateur ~ effectue une inversion bit-à-bit sur un champ de bits.

Test bit
L'utilisation d'un bouton-poussoir doit permettre d'effectuer un débranchement dans le cours du
programme: si le bouton est pressé, alors...
C'est la structure if (condition) ... du C.
Une condition est simplement représentée par un nombre: la condition est fausse si le nombre vaut
0, et vraie dans le cas contraire.
La lecture des valeurs se trouvant sur les broches du PORTD se fait en lisant la valeur de PIND.
On cherche une opération logique dont le résultat sera 0 si le bit testé vaut 0, et dont le résultat sera
non-nul si le bit testé vaut 1. C'est la fonction ET qui va être utilisée:
pd pd pd pd pd pd pd p
7 0 6 0 5 0 4 0 3 0 2 1 1 0 d0
-------------------------------------- ET
0 0 0 0 0 pd2 0 0
En C, on écrit: if ( PIND & (1<<PD2) ) ...
La valeur binaire contenant le bit qu'on souhaite tester s'appelle un masque. En effet, l'opération ET
entre un champ de bits et cette valeur permet de maquer tes bits qui ne nous intéressent pas, afin de
ne garder que le bit, ou les bits, à tester.
ON
Exemple PD2
O LED
Soit le schéma suivant: F P
D6 PD3
Une pression sur les boutons-poussoirs ON et OFF ATtiny2313
doivent respectivement allumer et éteindre la LED. Notez que ne sont représentés que les
Voici le programme correspondant: entrées-sorties. Le reste du montage est
void int main() { présenté dans la fiche « Premier schéma à
DDRD = (1<<PD6); microcontrôleur ».
PORTD = (1<<PD2) | (1<<PD3); //
pull-up sur les entrées
while (1) {
if (!(PIND & (1<<PD2)) PORTD |= (1<<PD6);
if (!(PIND & (1<<PD3)) PORTD &= ~(1<<PD6);
}
}
Modulation de Largeur d'Impulsion (MLI, PWM)

Il est très souvent nécessaire de faire varier la puissance transmise à une charge. Par exemple,
l'intensité d'une lampe doit varier ou la vitesse d'un moteur doit être réglée. La première idée qui
vient à l'esprit est de faire varier la tension ou le courant dans la charge. Mais il faut pour cela des
circuits électronique complexes. Il est généralement beaucoup plus simple d'alterner des instants où
la puissance maximale est transmise à la charge avec des moments où aucune puissance n'est
transmise.
La technique la plus utilisée est la Modulation de Largeur d'Impulsion (MLI) ou Pulse Width
Modulation (PWM) en anglais.

Comme on le voit sur la figure, le signal est de période constante, mais la durée de la partie active
du signal varie. Dans la première partie, 25% de la puissance maximale est envoyée à la charge, vu
que le signal est à « 1 » durant 25% du temps alors qu'il est à « 0 » le reste du temps. De même, la
puissance passe à 50% au milieu du tracé et à 75% sur la dernière partie.
La fréquence du signal va dépendre de l'application. Pour commander une diode lumineuse, la
fréquence doit être supérieure à 100Hz, pour que l'oeil humain ne voie pas le clignotement. Dans ce
cas, c'est bien l'oeil qui effectue l'intégration du signal pour en percevoir une valeur moyenne. Pour
un moteur à courant continu, ce sont à la fois l'inductance de son circuit électrique et son inertie
mécanique qui participent à cette intégration. Les fréquences des signaux PWM peuvent aller
couramment jusqu'à des centaines de kHz. Mais plus la fréquence est élevée, plus les pertes
électriques à l'instant des commutation sont importantes et peuvent dissiper de l'énergie dans les
éléments de commutation.

Voici un programme qui génère un signal


PWM:
int main() {
DDRD = (1<<PD5);
char commande=25;
char temps;
volatile char i;
while (1) {
PORTD |= (1<<PD5);
for(temps=0; temps<commande; temps++){
for (i=0; i<50; i++);
}
PORTD &= ~(1<<PD5);
for(temps=0; temps<(100-commande); temps++){
for (i=0; i<50; i++);
}
}
}
Après les initialisations de la sortie et des variables, la boucle infinie alterne la partie active et la
partie inactive du signal. Les attentes sont obtenues par une simple boucle d'attente active, dont le
nombre d'instructions détermine le temps. Attention: le terme volatile est nécessaire pour que le
compilateur, dans son mode optimisé, ne supprime pas les boucles for constatant qu'elles sont vides.
Ce programme donne l'impression que la diode lumineuse est à demi-intensité. C'est lié au fait que
la réponse de l'oeil n'est pas linéaire, mais logarithmique. On remarque aussi que le PWM est visible
en déplaçant rapidement la diode devant l'oeil.

Beaucoup de microcontrôleurs disposent à l'intérieur de circuits dédiés facilitant la génération de


signaux PWM. Ils sont basés sur des timers (décrits plus en détail dans la fiche Les Timers).
De manière simplifiée, le principe est le suivant:

=0
Set

H Sortie PWM

CNT
Reset

OC

Un compteur binaire (CNT) est actionné par une horloge de fréquence réglable, dérivée de l'horloge
du processeur. Deux comparateurs sont présents. L'un permet de mettre la sortie à 1 lors du passage
du compteur à la valeur 0, l'autre permet de mettre la sortie à 0 lorsque le compteur atteint la valeur
contenue dans un registre (OC: Output Compare)

Voici un programme pour le microcontrôleur ATtiny2313. Notez que tous les microcontrôleurs de la
famille AVR ne sont pas identiques au niveau des timers.
int main() {
DDRB|=(1<<PB2); // pin PB2 en sortie TCCR0A|
=(0b11<<WGM00); // mode fast PWM TCCR0A|
=(0b10<<COM0A0); // signal actif à "1"
TCCR0B|=(0b100<<CS00); // choix de l'horloge: 8MHz divisé par 256
OCR0A=64; // valeur du PWM (25% de 256)

while(1) { // il n'y a plus rien à faire !


}
}
Il n'y a malheureusement pas moyen de choisir la pin sur laquelle les PWM est généré. Dans ce cas,
il s'agit de la pin PB2, notée aussi OC0A (Output Capture timer0 A).
Remarquez le simplicité du programme et la boucle while(1) totalement vide. Le processeur est
donc libre pour exécuter d'autres tâches !
Introduction aux interruptions

Avec un microcontrôleur, les sorties sont parfaitement maîtrisées au cours du temps. En effet, toutes
les sorties sont à zéro au moment du reset, il est possible de les modifier à tout moment et chaque
sortie conserve son état tant qu'on ne le change pas.
Pour les entrées, c'est plus compliqué. La lecture d'une entrée est une sorte de photographie de sa
valeur. Une autre lecture s'effectuant un instant plus tard permet d'avoir une nouvelle valeur. Mais le
moment exact d'un changement d'une entrée est difficile à connaître. Pire, deux changements
successifs peuvent être manqués.
Le lecture des entrées est en fait un échantillonnage. Toute la théorie correspondante s'applique...
La figure met en évidence ce mécanisme, dans le cas d'un échantillonnage à fréquence fixe.

Entré

e Signal

lu

Echantillonnage

On voit que le signal lu ne correspond pas bien au signal d'entrée. On a même dans ce cas une
impulsion perdue au centre du tracé.

Le mécanisme d'interruptions, existant sur pratiquement tous les processeurs, permet de résoudre
ce problème. En quelques mots, une interruption permet à une routine de s'exécuter à la suite d'un
événement.
En programmation, on appelle routine ou sous-routine une partie de programme se terminant par
l'instruction RET (return, retour). L'instruction CALL (appel) permet d'exécuter les instructions de
la routine et de revenir au programme appelant après l'instruction RET. En langage C, les
procédures utilisent ce mécanisme.
Une routine d'interruption (Interrupt Routine ou Interrupt Sub-Routine, ISR) se présente comme
une routine normale, sauf qu'elle se termine généralement par l'instruction RETI (return from
interrupt). Mais cette routine n'est jamais appelée explicitement par un programme. Son exécution a
pour cause un événement. Les événements peuvent êtres externes au microcontrôleur, comme par
exemple le changement d'état d'une entrée. Ils peuvent aussi être internes, comme par exemple ceux
liés à un timer.

Examinons le programme suivant:


#include <avr/io.h>
#include <avr/interrupt.h>

ISR (PCINT_vect) {
PORTD^=(1<<PD6); // inverse PD6
}
int main() {
PORTB|=(1<<PB0)|(1<<PB1); // pull-up sur les entrées

PCMSK=(1<<PCINT0)|(1<<PCINT1); // Active les interruptions


// par PCINT0 et 1 (PB0 et 1)
GIMSK=(1<<PCIE); // Active Pin Change Interrupt
sei(); // Active l'ensemble des interruptions

while(1) { // il n'y a rien à faire !


}
}
Il est nécessaire d'importer les symboles liés aux interruptions, par #include <avr/interrupt.h>.
La routine d'interruption débute par ISR (no_du_vecteur). Le numéro du vecteur correspond au rang
dans une table située au début de la mémoire flash.
La routine d'interruption change l'état de la sortie PD6, pour mettre en évidence les événements.
Rappel: le signe ^ est le ou exclusif sur un champ de bit.
Le programme principal contient trois lignes pour l'initialisation des interruptions choisies:
1) L'activation des interruptions provenant des entrées PCINT0 et PCINT1 (et exclusion des autres
entrées PCINT2 à PCINT7), en activant les bits correspondant dans le Pin Change Mask (PCMSK).
2) L'activation de l'interruption PCINT et donc de la routine d'interruption qui lui est associée, par la
mise à un du bit PCIE dans le General Interrupt Mask (GIMSK).
3) L'activation générale des interruptions par l'instruction sei() (= set interrupt). L'instruction cli()
(=clear interrupt) permettrait de désactiver toutes les interruptions. On l'utilise parfois pour une
courte routine qui serait sensible à la durée d'exécution.
Finalement, la boucle sans fin while(1) ne fait rien dans ce cas. Et pourtant, chaque changement sur
PB0 ou PB1 va changer l'état de PD6 !

Application : compteur d'impulsions


Remplaçons la routine d'interruption dans le programme précédent par la suivante:
ISR (PCINT_vect) {
IF (PINB&(1<<PB0)) Compteur0++;
IF (PINB&(1<<PB1)) Compteur1++;
}
Les variables Compteur0 et Compteur1 vont totaliser les flanc montant des signaux sur PB0 et PB1
respectivement. Attention, ces variables devront être déclarées par :
volatile char Compteur0, Compteur1;
pour éviter une optimisation abusive que le compilateur pourrait effectuer.

A retenir : Lorsque d'autres interruptions doivent être utilisées, on retrouve généralement les trois
étapes d'initialisation:
1) Réglage des conditions particulières de l'interruption choisie
2) Activation de l'interruption choisie
3) Activation générale des interruptions (sei())
Graphes d'état et machines d'état
Beaucoup d'applications peuvent être décrites par un graphe d'état. Il est composé d'un certain
nombres d'états (représenté généralement par un cercle ou un ovale) et de transitions (représentées
par des flèches), allant chacune d'un état vers un autre état.
Chaque état est décrit par son nom (ou par son numéro). Il correspond aussi à une valeur bien
déterminée des sorties du système. Chaque transition est associée à une condition logique sur les
entrées du système.
Prenons un exemple concret. On cherche à automatiser le mouvement de descente puis de montée
d'une perceuse. Un moteur, pouvant être activé dans les deux sens, va faire descendre puis monter
le bloc de perçage (comportant le moteur entrainant la mèche). Deux interrupteurs de fin de course
sont placés en bas et en haut, pour terminer respectivement la descente et la montée. Le bouton-
poussoir Start permet à l'ouvrier de démarrer le perçage. Voici un graphe d'état le fonctionnement de
ce système.

Arrêt
0 0
Start
Haut

Montée Bas Descente


0 0
0 0

Les deux valeurs binaires associées à chaque état correspondent aux sorties Avance et Recule qui
commandent le moteur.
On peut réaliser une machine d'état, appelée aussi système logique séquentiel synchrone, en
utilisant des bascules pour mémoriser l'état, un système combinatoire pour calculer l'état futur en
fonction de l'état présent et des entrées, et un autre système combinatoire pour calculer la valeur
des sorties en fonction de l'état présent.
Dans notre problème de perceuse, le système combinatoire de sortie est inexistant, parce qu'on peut
coder les états directement avec les valeurs de sorties. On a donc le schéma suivant:

D1 Q1 Avance
Système
Start combinatoire
Bas D2 Q2 Recule
Haut

Horloge
Il reste à déterminer la table de vérité du système combinatoire. Comme il a cinq entrées, la table
de vérité aura 32 lignes (2 à la puissance 5). Plutôt que de chercher à la remplir dans l'ordre binaire
habituel, on va examiner successivement chaque état présent, et voir les états futurs possibles, en
fonction des entrées. Ainsi, à l'état arrêt correspond deux états futurs possibles: l'état arrêt (on ne
change pas d'état) ou l'état descente, lorsqu'on a pressé sur Start. Les deux premières lignes
représentent ces deux cas. Comme les valeurs de Haut et de Bas n'interviennent pas dans cet état, on
les représente par X. Chacune de ces deux lignes correspond donc à quatre lignes de la table de
vérité.
En étudiant les deux autres états, on peut compléter le tableau par les quatre lignes suivantes.
Finalement, la valeur 11 qui ne représente aucun état dans notre graphe, doit être associée à l'état
futur Arrêt pour éviter que la machine d'état ne se bloque dans une état indéterminé (état puis).

Start B Haut Q Q Q0+ Q1+


a 0 1
0 X X 0 0 0 0
1 X X 0 0 0 1
X 0 X 0 1 0 1
X 1 X 0 1 1 0
X X 0 1 0 1 0
X X 1 1 0 0 0
X X X 1 1 0 0

A partir de cette table décrivant les états, on peut écrire une table de vérité sous sa forme habituelle
et appliquer la méthode des table de Karnough pour trouver le schéma logique du système
combinatoire.
La réalisation pratique de ce montage nécessiterait au moins trois circuits intégrés: deux bascules
avec un circuit 74HC74, un oscillateur avec un NE555, et le système combinatoire, sous forme
d'une ROM telle que l'EEPROM 28C64, qu'il faudra préalablement programmer.
Pour simplifier la réalisation, peut-on rêver d'un circuit à 8 patte: 3 pour les entrées, 2 pour les
sorties, 2 pour les alimentations et une pour la remise à zéro ? Un microcontrôleur peut jouer ce
rôle... et bien d'autres encore !
Voici un schéma complet, basé sur un micrcontrôleur ATtiny13 dans une boitier DIL8, ne
nécessitant comme composants additionnels qu'une résistance et un condensateur de découplage :

100nF 2 Vcc
10k
8
1
Vcc Reset 6 /Reset
Connecteu
ATtiny1 r AVR-
3 ISP

Avance 3 2
PB PB2
7
3 SCK

PB1 4 MISO
6
Recule 4 3
PB
PB0 5 MOSI
Gn 5

d 4 1 Gnd

Start
Bas
Haut
Programmation d'une machine d'état en C
Les structures de contrôle d'un langage de programmation, telles que les if et les while, permettent
d'écrire un programme dont le fonctionnement est décrit par un graphe d'état. Reprenons l'exemple
simple de la perceuse semi-automatique, dont voici le graphe d'état :

Arrêt
0 0
Start
Haut

Montée Bas Descente


0 0
0 0

L'organigramme suivant réalise la machine d'état.

Le programme correspondant en C est très simple:


Initialisations
#include <avr/io.h>
int main() {
Avance=0, Recule=0 PORTB|=(1<<PB0)|(1<<PB1)|(1<<PB2);
DDRC|=(1<<PC5)|(1<<PC4);
while(1) {
Start PORTC&=~((1<<PC5)|(1<<PC4);
while(PINB&(1<<PB0)); PORTC|=(1<<PC5);
while(PINB&(1<<PB1)); PORTC&=(1<<PC5); PORTC|
=(1<<PC4); while(PINB&(1<<PB2));
}

Bas
}
Avance=0,
Avec cette manière de procéder, l'état du système est en fait mémorisé
Recule=1
dans le compteur ordinal (PC=programm counter) du processeur. Par
Haut exemple, l'état descente correspond à l'exécution des instructions liées
aux troisième while du programme. Il faut bien noter que dans ce
programme, le boucle infinie
while(1) correspond au parcours complet du graphe d'état.

Lorsqu'un graphe d'état devient complexe, il devient très vite compliqué de trouver l'organigramme
et d'écrire le programme correspondant. De plus, une petite modification du graphe d'état peut avoir
une grande incidence sur le programme. On préfèrera alors utiliser une technique très simple,
utilisant une variable pour mémoriser l'état du système. Dans notre exemple, voici le programme
correspondant. Seule la partie principale du programme est présentée:
char etat = 0;
while (1) {
switch (etat) {
case 0: PORTC&=~((1<<PC5)|(1<<PC4));
if (!(PINB&(1<<PB0))) etat = 1; break;
case 1: PORTC&=~(1<<PC4); PORTC|=(1<<PC5);
if (!(PINB&(1<<PB1))) etat = 2; break;
case 2: PORTC&=~(1<<PC5); PORTC|=(1<<PC4);
if (!(PINB&(1<<PB2))) etat = 0; break;
}
}
On notera que, contrairement au programme précédent, aucune boucle while ne se trouve à
l'intérieur de la boucle infinie while(1). Cela signifie que cette boucle s'exécute un très grand
nombre de fois par seconde (quelques centaine de milliers avec un processeur AVR à 8 MHz).
Marche à suivre : La programmation d'une machine d'état, à partir d'un graphe d'état même très
compliqué, peut s'écrire de la manière suivante:
- après les initialisations (direction des ports, pull-up, etc), une boucle infinie while(1) contient
une instruction switch avec un case correspondant à chaque état.
- dans chaque case, on programme les valeurs des sorties du système associés à cet état.
- chaque transition partant d'un état est ensuite réalisée en plaçant dans le case correspondant
une instruction if avec la condition logique sur les entrées associées à cette transition, suivie de
l'assignation de la variable etat avec sa nouvelle valeur.
La lisibilité des programmes présentés n'est pas bonne. En effet, l'utilisation directe des ports
d'entrée et de sortie rend la lecture difficile. Voici le même programme, qui produira le même code,
mais écrit de manière plus lisible:
#define PinStart PINB
#define BitStart PB0
#define PullUpStart PORTB
#define PinBas PINB
#define BitBas PB1
#define PullUpBas PORTB
#define PinHaut PINB
#define BitHaut PB2
#define PullUpHaut PORTB
#define PortAvance PORTC
#define BitAvance PC5
#define DdrAvance DDRC
#define PortRecule PORTC
#define BitRecule PC4
#define DdrRecule DDRC
enum {Arret, Descente, Montee};
int main() {
PullUpStart|=(1<<BitStart); PullUpBas|=(1<<BitBas);
PullUpHaut=(1<<BitHaut);
DdrAvance|=(1<<BitAvance); DdrRecule|=(1<<BitRecule);
char etat = Arret;
while (1) {
switch (etat) {
case 0: PortAvance&=~(1<<BitAvance); PortRecule&=~(1<<BitRecule);
if (!(PinStart&(1<<BitStart))) etat = Descente; break;
case 1: PortRecule&=~(1<<BitRecule); PortAvance|=(1<<BitAvance);
if (!(PinBas&(1<<BitBas))) etat = Montee; break;
case 2: PortAvance&=~(1<<BitAvance); PortRecule|=(1<<BitRecule);
if (!(PinHaut&(1<<BitHaut))) etat = Arret; break;
}
}
}
Programmation d'un grafcet en C
Le Grafcet est un manière de représenter un automatisme, très utilisée dans le monde francophone.
Il diffère des graphes d'état par la terminologie utilisée et sur un point fondamental :
– Les termes utilisée sont un peu différents: on parle d'étapes et non d'état. La condition d'une
transition s'appellent la réceptivité. Lorsque qu'une étape donnée peut être suivie soit d'une
étape, soit d'une autre, on parle de divergence en OU.
– Mais de manière plus fondamentale, alors qu'une machine d'état ne peut avoir qu'un seul état
actif à un instant donné, plusieurs étapes d'un Grafcet peuvent être actives en même temps.
En effet, le concept de divergence en ET permet l'activation de plusieurs étapes par le
franchissement d'une transition.
Le but de cette fiche n'est pas de présenter la théorie des Grafcet, le lecteur se réfèrera à l'un des
nombreux documents pédagogiques qu'on trouve sur Internet à ce sujet. Le but est simplement de
présenter une technique simple permettant de « traduire » un Grafcet en un programme C, en vue de
l'exécuter sur un microcontrôleur.
Voici le squelette du programme permettant d'implémenter un
Grafcet:
#include <avr/io.h>
#define MaxEtapes 10 // nombre maximal d'étapes
#define MaxTransitions 10 // nombre maximal de transitions
char Etapes[MaxEtapes]; // Variables des étapes, vrai si une étape est active
char Transitions[MaxTransitions]; // vrai si la transition est franchissable
void InitIO() { // Initialisation des entrées et sorties
Pull...|=(1<<Bit...); ...
Ddr...|=(1<<Bit...); ...
}
void LitEntrees() { // Lecture des entrées if(!
(Pin...&(1<<Bit...))) ...=1; else ...=0; ...
}
void CalculeTransitions() { // Recherche des transitions franchissables
Transitions[x]=Etapes[y] && Receptivite; ...
}
void DesactiveEtapes() { // Désactive les étapes précédant les transitions franchissables
if (Transitions[x]) Etapes[y]=0; ...
}
void ActiveEtapes() { // Active les étapes suivant les transitions franchissables
if (Transitions[x]) { Etapes[z]=1; } ...
}
void AffecteSorties () { // Gère les sorties en fonction des étapes actives
SortieX=Etapes[x]; ...
if(SortieX)Port...|=(1<<Bit...); else Port...&=~(1<<Bit...); ...
}
int main () { // Programme principal
InitIO();
int i; for (i=0; i<MaxEtapes; i++) Etapes[i]=0;
Etapes[0]=1; // Activation de l'étape initiale
while (1) { // boucle infinie
AffecteSorties();
LitEntrees();
CalculeTransitions();
DesactiveEtapes();
ActiveEtapes();
}
}
Le tableau Etapes[] est constitué d'une variable booléenne pour chaque étape du Grafcet. Chaque
étape peut être inactive (valeur nulle) ou active (valeur non nulle). Au début du programme, seule
l'étape initiale est activée.
De même, le tableau Transitions[] indique pour chaque transition si elle est non-franchissable
(valeur nulle) ou franchissable (valeur non nulle).
La boucle principale while(1) est constituée de l'appel de cinq procédures qui correspondent à la
théorie des Grafcet. En début de boucle, la procédure AffecteSorties() active toutes les sorties
qui doivent l'être en fonction des étapes actuellement actives.
On aura par exemple : Avance=Etapes[1]; suivi plus loin de :
if(Avance)PortAvance|=(1<<BitAvance); else PortAvance&=~(1<<BitAvance);
Afin de rendre le programme bien lisible, la procédure LitEntrees() lit l'état de chaque entrée et
affecte le résultat à une variable nommée selon le nom de l'entrée. Par exemple: char Start;
La lecture se fera alors par exemple de la manière suivante:
if (!(PinStart&(1<<BitStart))) Start=1; else Start=0;
La procédure CalculeTransitions() va déterminer pour chaque transition si elle est
franchissable ou non. Rappelons qu'une transition est franchissable si l'étape qui la précède est
active et si sa réceptivité est vraie. On aura donc par exemple:
Transitions[0]=Etapes[0] && Start;
Les procédures DesactiveEtapes() et ActiveEtapes() correspondent au franchissement
des transitions franchissables:
– l'étape ou les étapes précédant chaque transition franchissable sont désactivées
– l'étape ou les étapes suivant chaque transition franchissable sont activées.
La boucle principale va ensuite s'exécuter à nouveau, en commençant par activer les sorties qui
doivent l'être en fonction des étapes devenues actives.
Exemple : le problème de la perceuse, décrit dans la fiche « Graphes d'état et machines d'état » peut
être écrit de la manière suivante : (les définitions n'ont pas été écrites ici)
void InitIO() { DdrUp|
void ActiveEtapes() {
=(1<<BitUp); DdrDown|
if (Transitions[0]) Etapes[descente]=1;
=(1<<BitDown); PullUpStart|
if (Transitions[1]) Etapes[montee]=1;
=(1<<BitStart); PullUpBas|
if (Transitions[2]) Etapes[arret]=1;
=(1<<BitBas); PullUpHaut|
}
=(1<<BitHaut);
char Down, Up;
}
void AffecteSorties () {
char Etapes[MaxEtapes];
Down=Etapes[descente];
char Transitions[MaxTransitions]; Up=Etapes[montee];
enum {arret, descente, montee};
if(Up) PortUp|=(1<<BitUp);
char Start, Bas, Haut; else PortUp&=~(1<<BitUp);
void LireEntrees() { if(Down) PortDown|=(1<<BitDown);
if(!(PinStart&(1<<BitStart))) Start=1; else PortDown&=~(1<<BitDown);
else Start=0; if(! }
(PinBas&(1<<BitBas))) Bas=1; int main () { // Programme principal
else Bas=0; InitIO(); int i;
if(!(PinHaut&(1<<BitHaut))) Haut=1; for (i=0; i<MaxEtapes; i++) Etapes[i]=0;
else Haut=0; Etapes[0]=1;
} while (1) {
void CalculeTransitions() { AffecteSorties();
Transitions[0]=Etapes[arret] && Start; LireEntrees();
Transitions[1]=Etapes[descente] && Bas; CalculeTransitions();
Transitions[2]=Etapes[montee] && Haut; DesactiveEtapes();
} ActiveEtapes();
void DesactiveEtapes() { }
if (Transitions[0]) Etapes[arret]=0; }
if (Transitions[1]) Etapes[descente]=0;
if (Transitions[2]) Etapes[montee]=0;
}

Pour un problème aussi simple, cette méthode peut sembler bien compliquée !
Son avantage est qu'elle consiste simplement à suivre systématiquement chaque élément du Grafcet.
Elle se prête donc bien à des Grafcet complexe.
On peut facilement lui ajouter des temporisations (voir fiche « Temporisations dans les Grafcet et
les machines d'état »).
REFERENCES :
Pierre-Yves Rochat, Notes de cours Programmation des microcontrôleurs

Vous aimerez peut-être aussi