Interruptions

Le problème de l'attente active

Introduction

Lors de la programmation d'un microcontrôleur, il arrive souvent que l'on "attende" qu'un registre change de valeur. Par exemple:

  • Qu'un bouton soit appuyé,
  • Qu'un octet soir reçu via la communication UART,
  • D'un certain temps,
  • Qu'un composant nous indique qu'il est prêt (par exemple qu'il a effectué une mesure),
  • ...

Un exemple: l'UART

Lors du TD2, nous avons implémenté la communication UART. Par exemple, l'envoi d'un octet peut ressembler à ceci :

void uart_send_byte(uint8_t data) {
    // Wait for empty transmit buffer
    while (!(UCSR0A & (1 << UDRE0)))
        ;
    // Put data into buffer, sends the data
    UDR0 = data;
}

On attend ici que le bit UDRE0 soit mis à 1 dans le registre UCSR0A. Cela indique que le registre de données est prêt pour une nouvelle transmission.

La boucle while ci-dessus bloque l'exécution du programme. C'est ce que l'on appelle une attente active, on parle aussi de scrutation (polling).

Supposons que nous ayons paramétré notre bus UART à 9600 bauds.


Chaque octet (en comptant le bit de start et de stop) prendra donc 10 / 9600 = 1.04 ms à être envoyé.


Si on envoie la chaîne de caractères Hello World! en appelant uart_send_byte pour chaque caractère, il faudra donc attendre 13 * 1.04 ms = 13.52 ms avant que la fonction ne se termine.


Pendant ce temps, le microcontrôleur ne peut rien faire d'autre.

Cette attente active est une mauvaise pratique.

Nous seulement elle bloque l'éxecution du programme, empechant une autre tâche de s'exécuter, mais elle consomme aussi beaucoup de ressources CPU.

Fonctionnement des interruptions

Définition

Les interruptions sont une fonctionnalité du microcontrôleur qui permettent de paramétrer des fonctions qui seront exécutées automatiquement lorsque certains événements se produisent.

Vecteur d'interruption

Lors du décorticage assembleur, nous avons vu que les premiers octets du firmware sont réservés à la table des vecteurs d'interruption.

Cette table contient les adresses des fonctions qui seront exécutées lorsqu'une interruption particulière se produira.

La liste de ces événements est définie par le microcontrôleur. Par exemple, à la page 49 de la datasheet de l'ATmega328P, on trouve un tableau décrivant cette liste.

Activation des interruptions

Il est possible de sélectionner les interruptions que l'on souhaite activer individuellement. On appelle cela le masquage des interruptions.

Pour utiliser une interruption, il faut donc l'activer dans le registre correspondant, puis activer les interruptions globalement.

Les interruptions de l'ATmega328P

Interruptions en C

Afin d'utiliser les interruptions, le constructeur fournit l'en-tête suivante:

#include <avr/interrupt.h>

Il est possible d'utiliser les fonctions sei() et cli() pour respectivement activer et
désactiver globalement les interruptions.

sei(); // Active les interruptions globalement
cli(); // Désactive les interruptions globalement

Afin d'activer une interruption, il faut l'activer spécifiquement dans le registre correspondant.

Par exemple, l'interruption qui nous indique que le bus UART est disponible peut s'activer en mettant à 1 le bit UDRIE0 du registre UCSR0B (cf page 160 de la datasheet).

Il faudra ensuite écrire la fonction qui sera exécutée lors de l'interruption. Cette routine peut être déclarée à l'aide de la macro ISR (Interrupt Service Routine):

ISR(USART_UDRE_vect) {
    // Code de la fonction
}

Note: Il est préférable de paramétrer les interruptions avant d'activer les interruptions globalement, afin d'éviter que l'interruption ne se produise avant que l'on ait eu le temps de finir nos initialisations.

En assemblant ces morceaux, on obtient:

#include <avr/interrupt.h>

ISR(USART_UDRE_vect) {
    // Code de la fonction
}

int main() {
    // Initialisation du bus UART
    // ...
    // Activation de l'interruption
    UCSR0B |= (1 << UDRIE0);

    // Activation des interruptions globalement
    sei();

    while (1) {
        // ...
    }
}

L'imbrication des interruptions

Lorsqu'une interruption se produit, et que l'interruption est active, le microcontrôleur exécute la fonction correspondante.

La valeur actuelle de l'adresse de programme est alors sauvegardée dans la pile, et les interruptions sont désactivées globalement, et réactivées lorsque la fonction se termine (pas d'imbrication par défaut)

Le code d'une interruption bloque donc non seulement l'exécution du programme principal, mais aussi l'exécution d'autres interruptions.

C'est pour cette raison qu'il est préférable de limiter le code exécuté dans une interruption. Si un calcul doit être fait, mieux vaut stocker les données dans une variable, et effectuer le calcul dans le programme principal.

Il est possible de réactiver globalement les interruptions depuis la fonction d'interruption (en appelant sei()), mais cela peut être dangereux.

En effet, si un grand nombre d'interruptions s'imbriquent, la pile pourrait se remplir et consommer toute la mémoire RAM disponible.

Buffer circulaire

Comme évoqué, il est préférable de limiter le code des interruptions au stockage des données qui seront traitées dans le programme principal.

Pour cela, une structure de données très utile est le buffer circulaire.