Apprendre à utiliser les interruptions sur Arduino en langage C

Mise en œuvre d'une minuterie

Ce tutoriel est le quatrième de la série sur la programmation de carte Arduino en langage C :

À chaque fois, l'idée est de s'affranchir des facilités offertes par le fameux « langage Arduino » dans l'EDI standard. Ici, les programmes seront développés en langage C « pur » grâce aux outils de la chaîne de compilation avr-gcc.

L'objectif est double :

  • développer des codes optimisés, efficaces et compacts ;
  • démystifier le fonctionnement d'un microcontrôleur et prendre le contrôle des entrées-sorties, sans fard, en attaquant directement les registres du microcontrôleur.

Dans ce nouveau volet, l'auteur manipule les interruptions matérielles au travers d'une application de minuterie fonctionnant avec un bouton et une LED.

Commentez Donner une note à l'article (5)

Article lu   fois.

Les deux auteur et traducteur

Traducteur :

Liens sociaux

Viadeo Twitter Facebook Share on Google+   

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, PCINT1PCINT23. 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.

ATmega328P
Brochage ATmega328P / 28 broches PDIP
Image non disponible
Broche PB4 reliée au connecteur D12 de l'Arduino Uno

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.

Image non disponible
Arduino Uno avec un bouton pour gérer une application de minuterie

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 :

timeswitch.c
Sélectionnez
#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) :

 
Sélectionnez
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.

Vous avez aimé ce tutoriel ? Alors partagez-le en cliquant sur les boutons suivants : Viadeo Twitter Facebook Share on Google+   

  

Licence Creative Commons
Le contenu de cet article est rédigé par Francesco Balducci et est mis à disposition selon les termes de la Licence Creative Commons Attribution - Partage dans les Mêmes Conditions 3.0 non transposé.
Les logos Developpez.com, en-tête, pied de page, css, et look & feel de l'article sont Copyright © 2013 Developpez.com.