IRQ

Vous savez maintenant récupérer un flux sur le port série, contrôler des GPIO, etc. Mais que se passe-t-il si on a envie de faire tout ça en même temps ? Si des caractères arrivent sur le port série alors qu'on est en train de faire une autre tâche (longue), on risque de les rater. De même, comment faire pour exécuter une tâche périodique en fond ? Pour cela nous allons avoir besoin de deux choses :

  • gérer les interruptions / exceptions,
  • générer automatiquement des interruptions à intervalles réguliers. C'est le rôle des timers.

Commençons par les interruptions.

Les exceptions sur processeurs à base de Cortex M

Introduction

Les exceptions sont des événements qui interrompent le cours normal d'exécution d'un programme. Lorsqu'une exception arrive, le programme en cours d'exécution est stoppé, et un bout de code spécifique à cette exception (appelé handler) est exécuté. Puis si l'exception n'était pas fatale, le programme reprend son cours comme si de rien n'était.

Les exceptions sont de deux types :

  • causées par un événement interne au processeur (Cortex M4) : reset, bus fault (quand on accède à une adresse non mappée en mémoire), division par 0, … Elles sont généralement graves et empêchent souvent la reprise du cours normal du programme interrompu.
  • causées par un événement externe au processeur : un périphérique qui a besoin de signaler quelque chose, un changement d'état d'une GPIO, ... On les appelle alors communément interruptions (interrupt request ou IRQ).

En pratique, on utilise souvent indifféremment le terme exception ou interruption

Le microcontrôleur STM32L475 est composé d'un coeur d'ARM (Cortex M4) et de périphériques. Nous aurons donc deux types d'exceptions : celles propres au Cortex, et les IRQ dues aux périphériques.

Lorsqu'une exception est traitée, le processeur doit sauvegarder son état actuel. Cette sauvegarde peut être automatique (c'est le cas sur les Cortex M) ou manuelle (SPARC, ARM7TDMI, etc.).

Les exceptions peuvent avoir différentes priorités, permettant à une exception prioritaire d'interrompre le traitement d'une autre moins prioritaire. Ces priorités peuvent être réglées manuellement ou fixes. Ces priorités sont représentées par un nombre. Sur Cortex M, plus ce nombre est petit, plus l'exception est prioritaire.

En plus de ces priorités, la plupart des exceptions peuvent être désactivées ou activées logiciellement.

Les exceptions du STM32L275

Le STM32L275 intègre un contrôleur d'interruption très souple appelé NVIC (Nested Vectored Interrupt Controller) qui permet de gérer :

  • 82 sources d'IRQ externes au processeur,
  • 1 NMI (Non Maskable Interrupt) : une interruption externe non désactivable,
  • les exceptions internes au processeur.

Chaque exception a un numéro qui lui est propre :

  • les exceptions internes au processeur et la NMI sont numérotées de 1 à 15,
  • les 82 IRQ externes sont numérotées de 16 à 98. Elles ont en plus leur propre numéro d'IRQ externe, appelé position dans le Reference Manual, qui vaut numéro d'exception - 16. Elles sont de priorités réglables et désactivables / activables à volonté. Il y a une par périphérique (UART0, UART1, SPI0, etc.).

Ces exceptions sont décrites en page 396 du Reference Manual : les exceptions internes en grisé (oui, il y en a bien 15 !), les IRQ en non grisé.

Table des vecteurs d'interruption

Le Cortex s'attend à avoir en mémoire une table donnant pour chaque exception l'adresse du handler associé. C'est ce qu'on appelle la table des vecteurs d'interruption. L'emplacement de cette table en mémoire est stocké dans le registre VTOR (situé à l'adresse 0xE000ED08). Au reset, le VTOR est chargé avec la valeur 0x00000000, ce qui veut dire que la table des vecteurs d'interruption est en flash. Mais vous avez la liberté de construire une autre table en RAM contenant les adresses de vos propres handlers, et de faire pointer VTOR sur cette table.

Traitement d'une exception

Lors de l'arrivée d'une exception :

  1. Le processeur sauvegarde automatiquement sur la pile les registres R0 à R3, R12, R14, PC, et xPSR.
  2. Il stocke dans LR une valeur spéciale EXC_RETURN signifiant "retour d'exception" : 0xfffffff1, 0xfffffff9 ou 0xfffffffd (voir ici pour les détails).
  3. Il va chercher l'adresse du handler à exécuter à l'adresse suivante en mémoire :  VTOR + 4*exception_number.
  4. Il saute à cette adresse et exécute le code qui s'y trouve.
  5. À la fin de ce code, on trouve typiquement l'instruction BX LR, qui signifie "branchement à EXC_RETURN". Le processeur recharge depuis la pile les registres sauvegardés, et reprend le cours normal de l'exécution du programme.

Le bit indiquant qu'une interruption est en train d'être traitée est clearé automatiquement lors de l'entrée ou de la sortie du handler. 

Vous avez du remarquer que le processeur sauvegarde sur la pile les registres caller saved (en plus du PC et des xPSR). Cela permet de coder les handlers d'interruption comme des fonctions C tout à fait normales ! Il suffit juste que le SP soit positionné à une adresse correcte avant le déclenchement d'une exception.

Cela pose un problème pour la NMI qui n'est pas désactivable et peut se déclencher dès le boot avant que le SP ne soit bien positionné. Pour cela, les Cortex disposent d'une fonctionnalité rusée. La table des vecteurs d'interruption est organisée ainsi :

Adresse

Vecteur
(numéro d'exception)

Numéro d'IRQ externe
("position")
Description

Table des vecteurs d'interruption

Exceptions internes au Cortex M4
0x00000000 0 - SP initial
0x00000004 1 - PC initial (Reset Handler)
0x00000008 2 - NMI Handler
0x0000000C 3 - Hard Fault Handler
0x00000010 4 - -
0x0000003C 15 - SysTick Handler
IRQ externes au Cortex M4
0x00000040 16 0 WWDG
0x00000044 17 1 PVD_PVM
0x00000048 18 2 TAMP_STAMP
0x0000004C 19 3 RTC_WKUP

Autrement dit :

  • la première entrée est la valeur à laquelle positionner le SP au reset,
  • la deuxième entrée est la valeur à laquelle positionner le PC au reset (= le point d'entrée du programme).

Au reset, le processeur va positionner automatiquement le SP et le PC avec les deux premières entrées de la table. Une interruption peut donc survenir tout de suite, elle sera traitée correctement : le pointeur de pile sera bien positionné. Et sinon, c'est ce qui se trouve à l'adresse contenue dans PC (le point d'entrée du programme, généralement _start) qui sera exécuté.

Toutes les exceptions du STM32L275 sont disponibles en page 393 du manuel de référence du processeur et leur nom CMSIS dans le fichier stm32l475xx.h en ligne 82.

Le NVIC

Le contrôleur d'interruption (NVIC) n'est pas spécifique au STM32L475 mais aux Cortex M. Sa documentation est donc dans la documentation officielle d'ARM sur les Cortex M4. Cette documentation est aussi disponible au format PDF en bas de cette page. ARM donnant la possibilié aux fabricants de customiser le NVIC dans une large mesure, nous vous recommandons en premier la documentation du NVIC de notre processeur disponible dans le Programming Manual, en page 207.  

Il dispose de plusieurs registres qui permettent de contrôler les IRQ externes (et seulement les externes). Les deux principaux sont :

  • NVIC_ISER : qui permet, en écrivant un 1 sur le bit n, d'activer l'IRQ externe numéro n (= l'exception numéro 16+n)
  • NVIC_ICER : qui permet, en écrivant un 1 sur le bit n, de désactiver l'IRQ externe numéro n (= l'exception numéro 16+n)

Par exemple, pour activer / désactiver les interruptions spécifiques au WATCHDOG, dont le numéro d'IRQ externe est 0 (numéro d'exception 16), on écrira ceci :

// Pour activer les IRQ du WATCHDOG
SET_BIT(NVIC_ISER[0], 1);

// Pour désactiver les IRQ du WATCHDOG
SET_BIT(NVIC_ICER[0], 1);

Seule l'écriture d'un 1 dans ces registres a un effet. Cela évite d'avoir à faire un read-modify-write pour activer ou désactiver une interruption dans le NVIC.

On peut aussi utiliser les fonctions de la CMSIS void NVIC_EnableIRQ(IRQn_Type IRQn) et void NVIC_DisableIRQ(IRQn_Type IRQn), disponibles dans le fichier core_cm4.h. Par exemple, pour activer / désactiver les interruptions spécifiques au SPI1, dont le numéro d'IRQ externe est 35, on écrira ceci :

// Active les IRQ du SPI1
NVIC_EnableIRQ(35);

// Désactive les IRQ du SPI1
NVIC_DisableIRQ(35);

Les exceptions internes ne sont pas modifiables par le NVIC.

Activation / désactivation de toutes les interruptions

Il est possible d'autoriser ou d'interdire toutes les interruptions (sauf le reset et la NMI), en positionant le registre PRIMASK du processeur. On modifie ce registre par les instructions assembleur suivantes :

  • cpsie i : active les interruptions
  • cpsid i : désactive les interruptions

Ces fonctions sont aussi disponibles en C dans la CMSIS, fichier stm32l475xx.h__enable_irq(void) et __disable_irq(void).

Au boulot !

Création d'une table de vecteurs d'interruptions par défaut

Les tables de vecteurs d'exceptions sont généralement écrites en assembleur (exemple), mais dans le cas des Cortex il est possible de les écrire en C. C'est ce que nous allons faire.

Dans un fichier irq.c, créez une table de vecteurs d'interruptions, sur ce modèle :

void *vector_table[] = {
    // Stack and Reset Handler
    &_stack,            /* Top of stack */
    _start,             /* Reset handler */

    // ARM internal exceptions
    NMI_Handler,        /* NMI handler */
    HardFault_Handler,  /* Hard Fault handler */
    MemManage_Handler,
    BusFault_Handler,
    UsageFault_Handler,
    0,                  /* Reserved */
    0,                  /* Reserved */
    0,                  /* Reserved */
    0,                  /* Reserved */
    SVC_Handler,        /* SVC handler */
    0,                  /* Reserved */
    0,                  /* Reserved */
    PendSV_Handler,     /* Pending SVC handler */
    SysTick_Handler,    /* SysTick hanlder */

    // STM32L475 External interrupts
    WWDG_IRQHandler,         /* Watchdog IRQ */
    PVD_PVM_IRQHandler,      /* ... */
    TAMP_STAMP_IRQHandler,   /* ... */
    ...

Cette table est un tableau de "pointeurs sur n'importe quoi" (void *), qu'on peuple avec les adresses de handlers par défaut.
Pour respecter la convention CMSIS, appelez les handlers d'exceptions internes XXX_Handler, et les handlers d'IRQ externes XXX_IRQHandler (exemple : DMA1_Channel1_IRQHandler).

Création des handlers d'interruptions par défaut

Dans le fichier irq.c, avant la table des vecteurs, définissez des handlers par défaut qui feront la chose suivante :

  • désactiver toutes les interruptions
  • faire une boucle sans fin

On pourra ainsi vite voir si on sait générer une interruption et si le bon handler est appelé.

Bien entendu, il est hors de question d'écrire 40 fois le même code ! Écrivez donc une macro MAKE_DEFAULT_HANDLER, qui prend en argument un nom de handler (par exemple truc_IRQHandler) et qui déclare et instancie la fonction truc_IRQHandler.

Les handlers par défaut seront amenés à être surchargés par d'autres fichiers C, qui voudront mettre en place leur propre handler. Pour cela, faites en sorte que la définition des handlers inclue bien l'attribut weak : void __attribute__((weak)) truc_IRQHandler(void) {...}

Initialisation des interruptions

Toujours dans irq.c, écrivez une fonction void irq_init(void), qui stocke dans VTOR l'adresse de la table des vecteurs d'interruptions. Le registre VTOR fait partie du System Control Block (SCB) dont vous trouverez la définition dans core_cm4.h.

Pensez à appeler irq_init dans votre main

Génération d'une interruption par l'appui du bouton B2 (bleu)

Nous allons faire en sorte que l'appui sur le bouton poussoir B2 (le bleu, relié à la broche PC13) génére une interruption qui fera toggler la LED verte.

Dans le STM32L475, les GPIO ne sont pas reliées directement au NVIC. Elles passent d'abord par un bloc appelé EXTI (cf ref manual page 400), capable de générer des interruptions sur état (haut ou bas), sur front (montant ou descendant), ou des "événements" (sortes d'interruptions spéciales dont on ne s'occupera pas dans cette UE). Il va donc falloir programmer l'EXTI pour qu'il génère une interruption sur front descendant de la broche PC13. Pour cela, lisez les sections 14.3 et 14.4, sachant que les GPIO sont des "lignes d'IRQ configurables". 

Une fois que l'interruption est déclenchée, elle reste active jusqu'à que vous l'acquittiez en écrivant ce qu'il faut dans le registre EXTI_PRn. Le pending register du NVIC est clearé automatiquement lors du handler d'interruption, il n'y a donc rien à faire de ce côté-là.

Créez dans un fichier buttons.c la fonction void button_init(void) qui

  • active l'horloge du port C,
  • configure la broche PC13 en GPIO et en entrée,
  • sélectionne la broche PC13 comme source d'IRQ pour EXTI13 (registre SYSCFG_EXTICRn, cf page 403),
  • configure l'EXTI selon la procédure décrite en 14.3.4.

Compilez, et testez en appuyant sur B2 que le handler par défaut des GPIO du PORTC est bien appelé.

Rajoutez maintenant dans buttons.c un handler spécifique, qui primera sur le handler par défaut. Ce handler fera les choses suivantes :

  • acquittement de l'interruption dans l'EXTI (registre EXTI_PR1)
  • toggle de la LED verte

Compilez et testez : à chaque appui sur B2 la LED verte doit toggler, et ce sans que cela perturbe votre programme habituel (affichage d'une l'image sur le color shield).

Conclusion

Vous savez maintenant comment générer et traiter des exceptions sur Cortex M. Nous allons voir dans la suite comment générer des interruptions périodiques pour effectuer des tâches de fond par exemple. En attendant, tag IRQ ! :-)

Fichier attachéTaille
PDF icon dui0553a_cortex_m4_dgug.pdf1.67 Mo