Comment faire une action instantanément après un évènement ?

NIVEAU 2

Objectifs

  • Comprendre le principe d’interruption externe
  • Mettre en oeuvre les interruptions externes sur un microcontroleur STM32

Pré-requis

Vers le temps réel

Les systèmes embarqués sont développés pour réaliser des tâches répétitives et très spécifiques : réguler la température d’une zone, contrôler la vitesse de rotation d’un moteur, piloter un drône afin d’éviter des obstacles…
Ils sont très réactifs aux sollicitations extérieures et ils nécessitent la plupart du temps de pouvoir acquérir des informations de leur environnement pour pouvoir agir en conséquence sur une partie du système.

Pour cela, ils intégrent souvent :

  • divers capteurs (analogiques ou numériques) câblés sur les entrées du microcontroleur qui gère ce système
  • divers actionneurs (moteurs, LED…) câblés sur les sorties du microcontroleur qui gère ce système

Un programme embarqué a donc pour rôle de faire systématiquement :

  1. l’acquisition des grandeurs d’entrée
  2. le traitement et le stockage de ces grandeurs
  3. le calcul des commandes à appliquer sur les sorties
  4. la mise à jour des sorties

Pour faire ceci, il existe deux méthodes que nous allons détailler par la suite.

Scrutation

La première méthode pour pouvoir réaliser une application embarquée est de scruter les différentes entrées pour ensuite calculer les commandes à appliquer sur les diverses sorties afin de garantir l’exécution de la tâche que doit accomplir le système.

Mise en oeuvre

Dans le programme associé à cette application, on retrouve une boucle infinie dans laquelle les actions suivantes sont réalisées :

  • l’acquisition des grandeurs d’entrée
  • le traitement et le stockage de ces grandeurs
  • le calcul des commandes à appliquer sur les sorties
  • la mise à jour des sorties

Ces actions sont faites même s’il n’y a pas eu de changement sur les entrées. Les traitements et les diverses acquisitions des entrées ou les mises à jour des sorties peuvent prendre un temps non négligeable pour être exécutés sur un microcontroleur.

Chaque entrée, notée $E_x$, est testée l’une après l’autre. Les calculs sont faits en fonction des événements qui sont apparus sur les entrées. Les sorties sont ensuite actualisées en fonction de la commande à appliquer.

Exemple

Dans l’exemple suivant, le système embarqué est simple. Il permet de mettre à jour une sortie nommée ma_sortie en fonction d’une entrée nommée mon_entree à intervalle régulier.

#include "mbed.h"
DigitalOut ma_sortie(D10);
DigitalIn  mon_entree(D7);

int k;

void main(void){
   ma_sortie = 0;
   while(1){
      k = mon_entree.read();
      if(k == 1){
          ma_sortie = 0;
      }
      else{
          ma_sortie = 1;
      }
      wait(1.3);
   }
}

Interruption

Afin de pouvoir interagir plus rapidement avec son environnement et prendre en compte des événements extérieurs dès qu’ils arrivent, la plupart des microcontrôleurs sont dotés d’une capacité à interrompre l’exécution d’un programme pour se détourner vers une fonction particulière à exécuter lors de l’arrivée de cet événement extérieur. On appelle cela une interruption.

Pour cela, le microcontrôleur possède plusieurs entrées particulières qui permettent d’interrompre le programme principal. On verra dans des tutoriaux ultérieurs que d’autres modules du microcontrôleur (timers, ADC,…) peuvent également venir interrompre l’exécution du programme principal.

Cette méthode a pour intérêt qu’une sollicitation extérieure est prise en compte uniquement quand elle intervient. Le calcul associé à ce changement est alors réalisé que si une évolution dans les signaux d’entrée intervient.

Cette stratégie est la base des systèmes embarqués dits temps réel. Lors du développement d’une telle application, les concepteurs assurent que toute sollicitation extérieure aura une réponse dans un temps donné et défini à l’avance (en fonction des performances des systèmes matériels choisis).

Interruption sur une broche externe

Mais comment paramétrer une telle entrée sur les cartes Nucléo ?

Déclaration d’une entrée d’interruption

Toutes les broches du microcontroleur présent sur les cartes Nucléo L476RG sont utilisables comme source externe d’interruption.

L’ensemble des classes et des fonctions permettant de réaliser des opérations sur les entrées/sorties de la carte se trouvent dans la bibliothèque “mbed.h (ou “mbed-os.h“). Il est donc indispensable d’avoir au préalable inclut cette bibliothèque :

#include "mbed.h"

La première étape est la déclaration au compilateur de cette broche en entrée d’interruption. Il faut la placer après la déclaration des ressources externes (bibliothèques) et avant toute définition de fonction (voir tutoriel Tester ma première application sur Nucléo – Code d’exemple /* Déclaration des entrées/sorties */).

InterruptIn mon_int(PA_12);

Dans l’exemple précédent, on configure la broche PA_12 (du connecteur Morpho) en entrée numérique d’interruption. Dans ce code, il y a trois notions importantes :

  • InterruptIn‘, la classe définit le type de la broche, ici une entrée numérique d’interruption
  • mon_int‘, le nom de la variable qui va être associée au port donné dans les parenthèses
  • PA_12‘, le nom de la broche que l’on va associer sur le composant

Routine d’interruption

Une routine est une fonction appelée automatiquement par le microcontroleur. Dans le cas des interruptions externes, on peut associer une fonction spécifique à une broche d’entrée d’interruption.

Avant de pouvoir l’affecter à telle ou telle interruption, il faut d’abord écrire cette fonction d’interruption. Cette fonction doit se trouver en dehors de toute autre fonction. Son prototype doit être rappelé avant la fonction main.

#include "mbed.h"
void fonction_interruption1(void);

void main(void){
    ...
    while(1){
        ...
    }
}

/* Routine d'interruption */
void fonction_interruption1(void)
{
    // ACTIONS A REALISER LORS DE L'ACTIVATION DE L'INTERRUPTION 1
}

Association à une routine d’interruption

La plupart du temps, on cherche à détecter un changement d’état d’une entrée. Deux événements sont alors possibles avec une entrée numérique :

  • un front montant, le passage d’un ‘0’ à un ‘1’
  • un front descendant, le passage d’un ‘1’ à un ‘0’

Il est alors possible d’attacher à la broche d’interruption déclarée précédemment deux actions différentes en fonction de l’événement que l’on cherche à détecter. Pour cela, il existe deux fonctions dans la classe InterruptIn associée à la gestion des interruptions externes :

  • rise() : détection d’un front montant
  • fall() : détection d’un front descendant

N.B. La détection d’un niveau constant (‘0’ ou ‘1’) peut se faire sans l’utilisation des interruptions. C’est pourquoi ces fonctions ne sont pas implémentées dans la partie gestion des interruptions externes.

Pour associer une fonction à un changement d’état de type front montant, il faut utiliser la fonction suivante sur l’entrée à associer :

mon_int.rise(&fonction_interruption1);

Pour associer une fonction à un changement d’état de type front descendant, il faut utiliser la fonction suivante sur l’entrée à associer :

mon_int.fall(&fonction_interruption1);

Dans les deux cas précédents, on associe la fonction nommée fonction_interruption1 à un changement d’état de l’entrée mon_int (associée à la broche PA_12).

Exemple complet

On propose l’exemple suivant :

#include "mbed.h"
 
InterruptIn mybutton(BUTTON1); // Déclaration de l'interruption
DigitalOut myled(LED1);            // Déclaration de la sortie numérique
void pressed(void);                // Déclaration de la fonction d'interruption 1
void released(void);               // Déclaration de la fonction d'interruption 2

int delay = 1000000;             // 1 sec ou 10^6 us
 
void pressed(){                    // fonction d'interruption 1
        delay = delay>>3;          // permet de décaler la valeur binaire de 3 bits vers la droite - équivalent à une division par 2^3
}
void released(){                    // fonction d'interruption 2
        delay = delay<<3;          // permet de décaler la valeur binaire de 3 bits vers la gauche - équivalent à une multiplication par 2^3
}
 
int main(){
    mybutton.fall(&pressed);       // association de l'interruption 1 a un front descendant sur l'entree USER_BUTTON
    mybutton.rise(&released);      // association de l'interruption 2 a un front montant sur l'entree USER_BUTTON
    while(1) {
        myled=!myled;
        wait_us(delay);
    }
}

Tutoriel lié

MInE Prototyper Prototyper avec Nucleo et MBED

Nucleo – Faire une action instantanément après un évènement