I. Mise en Å“uvre d'une minuterie avec un bouton et une LED▲
Comme d'habitude, j'utiliserai le compilateur avr-gcc et sa bibliothèque avr-libc pour, cette fois, manipuler les interruptions du microcontrôleur. Pour illustrer les interruptions, l'idée simple d'une application de minuterie avec un bouton et une LED m'est venue à l'esprit. Cette application devra avoir le comportement suivant :
- la LED est normalement éteinte ;
- quand le bouton est pressé, la LED s'allume ;
- quand le bouton est relâché, la LED reste allumée un moment ;
- après une temporisation, la LED s'éteint.
Cette application sera pilotée par deux interruptions : l'une pour agir sur changement d'état de la broche reliée au bouton, l'autre pour contrôler la temporisation avant d'éteindre la LED. Les informations utiles sont dans la documentation complète (datasheet) de l'ATmega328P qui est le microcontrôleur de l'Arduino Uno.
II. Les interruptions sur Arduino▲
Parmi les 26 vecteurs d'interruption de l'ATmega328P (NDLR), il y en a trois en particulier nommés Pin Change Interrupts Requests : PCINT0, PCINT1 et PCINT2.
Note de la rédaction
En fait, pour gérer les interruptions externes, l'ATmega328P de l'Arduino Uno comprend deux types d'interruption : les interruptions « externes » (external interrupts) et celles sur « changement d'état » (pin change interrupts).
External interrupts
Il y a seulement deux broches d'interruption externe sur l'ATmega168/328 des Arduino Uno/Nano/Duemilanove : INT0 et INT1, mappées respectivement aux connecteurs 2 et 3 de la carte Arduino. Ces interruptions peuvent être configurées pour se déclencher sur front montant (RISING), sur front descendant (FALLING), sur changement d'état (CHANGE) ou sur l'état bas (LOW) du signal.
Pin change interrupts
Ce sont celles utilisées par l'auteur dans cet article. Sur les cartes Arduino Uno, ces interruptions peuvent être activées depuis n'importe quel connecteur, et même la totalité des 20 connecteurs de l'Arduino (A0 à A5 et D0 à D13). Elles sont regroupées sous trois vecteurs d'interruption PCINT0, PCINT1 et PCINT2 pour l'intégralité des 20 connecteurs.
Pin Change Interrupt Request 0 (connecteurs D8 Ã D13) (PCINT0_vect)
Pin Change Interrupt Request 1 (connecteurs A0 Ã A5) (PCINT1_vect)
Pin Change Interrupt Request 2 (connecteurs D0 Ã D7) (PCINT2_vect)
Elles sont déclenchées indifféremment sur front montant ou sur front descendant, et c'est au code d'interruption auquel il revient de déterminer l'événement qui a produit l'interruption. Quelle broche a déclenché le vecteur d'interruption ? Est-ce un front montant ou un front descendant du signal ?
Tout ceci complique sensiblement la gestion de ce type d'interruption.
À lire :
La plupart des broches d'entrées-sorties sont sensibles au changement d'état et peuvent générer les interruptions. Elles sont repérées sur le brochage du processeur : PCINT0, PCINT1… PCINT23. J'ai décidé d'utiliser la requête d'interruption PCINT0_vect avec la broche PCINT4 (Port B, broche 4). Depuis les schémas de l'Arduino Uno, on peut tracer la broche PB4 jusqu'au connecteur D12 de la carte, et j'ai donc relié un bouton-poussoir entre le connecteur D12 et la masse.
La broche restera à l'état haut lorsque le bouton est relâché grâce à la résistance interne de tirage (pull-up), et basculera à l'état bas quand l'utilisateur pressera le bouton qui reliera alors la broche à la masse.
Pour gérer la temporisation, j'utilise le Timer/Counter1 de l'ATMega328P, principalement parce que c'est le seul timer 16 bits tandis que les autres ne sont que des 8 bits, ce qui me permet ainsi de piloter des intervalles de temps plus longs. Ce timer est capable de générer des interruptions en fonction de différents événements, et je vais utiliser le vecteur d'interruption TIMER1 OVF, qui est déclenché lorsque le compteur déborde. Timer1 sera configuré en mode normal, fixé à une valeur avant d'être démarré ; le compteur s'incrémentera à une fréquence donnée par celle de l'horloge principale et affectée par un diviseur de fréquence, puis quand il atteindra la valeur 0xFFFF, il débordera et générera l'interruption dont j'avais besoin. Pour compter jusqu'à 100, je dois fixer la valeur du compteur à 0xFFFF - 100, le démarrer puis attendre le débordement. Le plus grand diviseur de fréquence possible est de 1024, l'horloge tourne à 16 MHz et le compteur peut compter jusqu'à environ 216. Le temps maximum avant débordement est donc de 216 x 1024 / 16.106 ≈ 4,2 secondes. Dans mon cas, je vais utiliser une temporisation de 2 secondes.
Pour la LED, je vais utiliser celle qui est intégrée à la carte de mon Arduino Uno, reliée au connecteur 13, c'est-à -dire la broche 5 du port B (PB5) de l'ATmega328P.
III. Le code▲
L'environnement Arduino propose plusieurs fonctions pour attacher des interruptions et les activer/désactiver. La bibliothèque avr-libc utilise des méthodes différentes (voir le manuel en ligne). La routine d'interruption devra être définie avec la macro ISR et le nom du vecteur d'interruption souhaité, comme ISR(PCINT0_vect) dans mon cas. Puis j'utilise les macros sei() et cli() pour activer et désactiver les interruptions. J'ai également utilisé sleep_mode() pour implémenter une boucle principale qui tourne dans le vide tout en consommant moins de ressources.
Note de la rédaction
Utiliser les instructions de la bibliothèque sleep permet de réduire considérablement la consommation du microcontrôleur en désactivant certaines horloges du système pendant la mise en veille. Seule une interruption permet alors de sortir du mode veille. Les sous-ensembles désactivés ne peuvent bien sûr plus générer d'interruption pour réveiller le système. On choisit en conséquence un mode veille parmi les six modes possibles en fonction des exigences de l'application et de la consommation du système. Le mode par défaut « idle » utilisé ici permet entre autres de réveiller le système au moyen de l'interruption extérieure Pin Change et de l'interruption du timer interne Timer Overflow. Une fois réveillé, le microcontrôleur redémarre, exécute la routine d'interruption, puis reprend l'exécution à l'instruction qui suit l'instruction sleep_mode().
Voici le code qui implémente tout cela :
#include <stdbool.h>
#include <avr/io.h>
#include <avr/interrupt.h>
#include <avr/sleep.h>
#define T1_MAX 0xFFFFUL
#define T1_PRESCALER 1024
#define T1_TICK_US (T1_PRESCALER/(F_CPU/1000000UL)) /* 64us @ 16MHz */
#define T1_MAX_US (T1_TICK_US * T1_MAX) /* ~4.2s @ 16MHz */
static
void
led_on
(
void
)
{
PORTB |=
_BV
(
PORTB5);
}
static
void
led_off
(
void
)
{
PORTB &=
~
_BV
(
PORTB5);
}
static
void
led_init
(
void
)
{
led_off
(
);
DDRB |=
_BV
(
DDB5); /* PORTB5 as output */
}
static
void
timer_stop
(
void
)
{
TCCR1B &=
~(
_BV
(
CS10)|
_BV
(
CS11)|
_BV
(
CS12)); /* stop timer clock */
TIMSK1 &=
~
_BV
(
TOIE1); /* disable interrupt */
TIFR1 |=
_BV
(
TOV1); /* clear interrupt flag */
}
static
void
timer_init
(
void
)
{
/* normal mode */
TCCR1A &=
~(
_BV
(
WGM10)|
_BV
(
WGM11));
TCCR1B &=
~(
_BV
(
WGM13)|
_BV
(
WGM12));
timer_stop
(
);
}
static
void
timer_start
(
unsigned
long
us)
{
unsigned
long
ticks_long;
unsigned
short
ticks;
ticks_long =
us /
T1_TICK_US;
if
(
ticks_long >=
T1_MAX)
{
ticks =
T1_MAX;
}
else
{
ticks =
ticks_long;
}
TCNT1 =
T1_MAX -
ticks; /* overflow in ticks*1024 clock cycles */
TIMSK1 |=
_BV
(
TOIE1); /* enable overflow interrupt */
/* start timer clock */
TCCR1B &=
~(
_BV
(
CS10)|
_BV
(
CS11)|
_BV
(
CS12));
TCCR1B |=
_BV
(
CS10)|
_BV
(
CS12); /* prescaler: 1024 */
}
static
void
timer_start_ms
(
unsigned
short
ms)
{
timer_start
(
ms *
1000UL);
}
ISR
(
TIMER1_OVF_vect) /* timer 1 interrupt service routine */
{
timer_stop
(
);
led_off
(
); /* timeout expired: turn off LED */
}
ISR
(
PCINT0_vect) /* pin change interrupt service routine */
{
led_on
(
);
timer_stop
(
);
if
(
bit_is_set
(
PINB, PINB4)) /* button released */
{
timer_start_ms
(
2000
); /* timeout to turn off LED */
}
}
static
void
button_init
(
void
)
{
DDRB &=
~
_BV
(
DDB4); /* PORTB4 as input */
PORTB |=
_BV
(
PORTB4); /* enable pull-up */
PCICR |=
_BV
(
PCIE0); /* enable Pin Change 0 interrupt */
PCMSK0 |=
_BV
(
PCINT4); /* PORTB4 is also PCINT4 */
}
int
main (
void
)
{
led_init
(
);
button_init
(
);
timer_init
(
);
sei
(
); /* enable interrupts globally */
while
(
true
)
{
sleep_mode
(
);
}
}
La fonction principale initialise les ressources matérielles, autorise les interruptions et tourne en boucle en mode veille. Quand une requête d'interruption arrive, le CPU est réveillé et la routine d'interruption associée est appelée.
Premièrement, la routine d'interruption (ISR) PCINT0 est appelée lorsqu'on appuie sur le bouton. Du fait que la routine est activée aussi bien sur front montant que sur front descendant, il faut tester le registre PINB pour connaître l'état de la broche connectée au bouton. Si l'état haut est constaté, le bouton est relâché et on démarre le timer. Il y a ici une complication due aux rebonds du bouton qui provoquent de multiples requêtes d'interruption lors d'un simple appui ou relâchement du bouton, mais en général cette mise en œuvre gère les rebonds de façon efficace. Lorsque le timer déborde, la routine d'interruption TIMER1 OVF ISR est appelée et la LED est éteinte.
IV. Compilation et téléversement du code▲
J'ai enfin compilé le programme et l'ai téléversé dans l'Arduino Uno connecté au port USB avec les commandes suivantes (notez que je développe sur une machine sous Linux Debian, mais ces mêmes commandes devraient fonctionner normalement sur d'autres systèmes d'exploitation moyennant quelques modifications) :
avr-gcc -g -Os -DF_CPU
=
16000000UL -mmcu
=
atmega328p -c -o timeswitch.o timeswitch.c
avr-gcc -mmcu
=
atmega328p timeswitch.o -o timeswitch
avr-objcopy -O ihex -R .eeprom timeswitch timeswitch.hex
avrdude -F -V -c arduino -p ATMEGA328P -P /dev/ttyACM0 -b 115200
-U flash:w:timeswitch.hex
En pressant le bouton reset, l'application démarre…
V. Notes de la Rédaction Developpez.com▲
Cet article est la traduction du billet écrit par Francesco Balducci (alias Balau). Retrouvez la version originale de cet article ainsi que les autres chapitres consacrés à Arduino sur son blog.
Nous remercions les membres de la Rédaction de Developpez.com pour le travail de traduction et de relecture qu'ils ont effectué, en particulier : f-leb, Vincent PETIT , Delias et Claude Leloup.