Interface logicielle (API) - Cœur (Cortex-M*) - Interruptions
Principe
Les interruptions sont un mécanisme qui permet d'exécuter immédiatement le code d'une fonction spécifique lorsqu'un événement donné se produit, en interrompant le déroulement normal du programme, pour permettre le traitement immédiat de l'événement concerné.
L'exécution normale du programme "principal" reprends une fois que la routine de traitement (gestion) de l'interruption a terminé son exécution.
Utilisation avec l'API
Dans la majorité des cas (et donc la majorité des drivers) la configuration des interruptions et leur utilisation est gérée par le driver.
La configuration au niveau du programme utilisateur consiste à enregistrer une routine qui sera appelée par le gestionnaire interne au driver, après avoir effectué les éventuels traitements nécessaires au préalable.
L'enregistrement des ces routines (callbacks) se fait par le biais de l'interface de chaque driver, et est détaillé sur la page de description de chaque driver.
Règles
Un callback DOIT respecter les règles suivantes :
- Être court
- Ne pas inclure de boucles
- N'utiliser que des variables locales ou volatiles
- Ne pas déclencher d'autres interruptions
Explications
Une routine de gestion d'une interruption est une fonction qui s'exécute en dehors du fonctionnement normal du programme. Pendant ce temps, le reste du programme (et les autres routines de traitement des interruptions) ne peuvent pas s'exécuter. Si une routine de traitement met trop de temps à traiter une interruption, il risque de se produire une autre interruption avant la fin, provoquant soit un appel récursif de la routine de traitement, soit une perte d'information (une interruption non vue, donc non traitée).
Pour ce faire, il faut éviter à tout prix le parcours de tableaux ou la recherche d'éléments dans une liste, qui sont des opérations très longues. Les boucles sont aussi à proscrire, car le risque de "boucler" pendant trop longtemps est très grand.
L'accès aux variables globales est une opération hasardeuse si le compilateur n'a pas été informé du caractère volatile de la variable en question. En effet, du point de vue du compilateur une routine de gestion d'une interruption n'est jamais appelée, et ne risque donc pas de modifier une variable utilisée dans une autre fonction. Il peut donc très bien optimiser l'accès à une variable et en stocker une copie dans un registre pendant tout le temps d'exécution d'une boucle, alors que l'interruption modifiera la variable en mémoire, et non pas dans le registre. La boucle attendra indéfiniment, et le programme sera bloqué.
Si la variable est déclarée "volatile" le compilateur forcera la relecture de la valeur de la variable à chaque test ou accès, et la
modification par l'interruption sera bien prise en compte.
Pour limiter les traitements à l'intérieur d'un "callback", il est conseillé d'utiliser des "drapeaux".
Les drapeaux sont des variables déclarées "volatile" servant à signaler au programme principal qu'un événement est survenu.
Un drapeau peut être binaire, ou véhiculer une information donnant plus d'indications au programme principal sur ce qu'il lui faut réaliser.
Dans le cas d'événements à haute fréquence, le drapeau pourra être un compteur, et le programme principal pourra éventuellement protéger l'accès à cette variable par l'utilisation de la fonction sync_lock_test_and_set(), qui désactive les interruptions avant de lire l'ancienne valeur, la remplace par la nouvelle valeur, réactive les interruptions, et renvoie l'ancienne valeur.
Enfin, il ne faut pas qu'un gestionnaire d'interruption fasse appel à une fonction qui attende ou risque d'attendre que d'autres événements se produisent pour se terminer. il s'agit notamment de toutes les fonctions fournies par les drivers de communication (UART, I2C, SPI, ...) dont le fonctionnement repose aussi sur des interruptions.
Par exemple, l'envoi d'un message se fera dans la boucle principale, l'interruption positionnant simplement un drapeau indiquant le message à envoyer.
Fonctionnement interne - NVIC
La gestion des interruptions sur les LPC passe par un "bloc" appelé NVIC (Nested Vector Interrupt Controller).
Dans certains cas, il peut-être nécessaire de modifier le fonctionnement interne de l'API, pour une application critique ou très pointue.
Cette partie donne quelques pistes et détails sur le fonctionnement interne de l'API pour la partie gestion des interruptions et interface avec le NVIC.
Table des vecteurs d'interruption
Pour que le micro-contrôleur sache à quelle fonction faire appel, il est nécessaire d'enregistrer l'adresse de la fonction concernée dans une table, appelée "table des vecteurs d'interruption".
Cette table dispose d'une entrée par interruption existante. La liste des interruptions et leur ordre est défini et fixe pour chaque micro-contrôleur, et cet ordre n'a pas beaucoup d'importance pour l'utilisation des interruptions.
Le lien entre les numéros d'interruptions (l'ordre) et les fonctions à appeler lorsque l'interruption concernée se produit est fait dans le fichier "core/bootstrap.c", dans la table vector_table[].
void *vector_table[] __attribute__ ((section(".vectors"))) = { &_end_stack, /* Initial SP value */ /* 0 */ Reset_Handler, NMI_Handler, HardFault_Handler, 0, /* [....] */ };
À noter l'attribut définissant la section dans laquelle le compilateur devra mettre cette table, ce qui permettra à l'éditeur de lien de placer cette table au bon endroit dans le binaire qui sera flashé sur le micro-contrôleur.
Les handlers et les "dummy handlers"
Les "handlers" sont les routines de gestion des interruptions.
Chacune des ces routines a une définition "factice" présente dans le fichier "core/bootstrap.c", à l'exception du Reset_Handler qui est obligatoire et est définit dans le même fichier. Ces définitions se présentent sous la forme suivante :
void UART_0_Handler(void) __attribute__ ((weak, alias ("Dummy_Handler")));
Cette notation permet de définir le nom de la fonction à appeler pour chaque interruption, sans être obligé de déclarer effectivement les fonctions. Chaque nom est déclaré comme étant un alias "faible" de la fonction Dummy_Handler(). Si lors de l'édition de lien il n'y a aucune autre définition pour cette fonction, alors l'adresse utilisée sera celle de la fonction Dummy_Handler(). Si une autre définition est présente avec le même nom (dans ce fichier ou dans un autre), elle prendra la place de celle-ci, et c'est l'adresse de l'autre fonction qui sera utilisée et mise dans la table des vecteurs d'interruption.
Numéros d'interruptions
#include "core/lpc_core.h"
Les numéros des vecteurs d'interruption sont définis dans le fichier d'entête "include/core/lpc_core.h".
Une partie de ces vecteurs correspond aux "exceptions" (entrées 1 à 15 dans la table des vecteurs d'interruption, souvent numérotées -15 à -1 dans la documentation).
Attention, certaines entrées ne sont pas des interruptions (entrées 0, 1 et 7) et toutes ne sont pas systématiquement présentes. leur position est fixe.
Ci-dessous les numéros des exceptions disponibles sur le micro-contrôleur LPC1224 :
/* Cortex-M0 Processor Exceptions Numbers */ /* Note : entry 0 is "end stack pointer" */ #define RESET_IRQ ( -15 ) /* 1 - Reset ... not an interrupt ... */ #define NMI_IRQ ( -14 ) /* 2 - Non Maskable Interrupt */ #define HARD_FAULT_IRQ ( -13 ) /* 3 - Cortex-M0 Hard Fault Interrupt */ /* Note : entry 7 is the 2’s complement of the check-sum of the previous 7 entries */ #define SV_CALL_IRQ ( -5 ) /* 11 - Cortex-M0 Supervisor Call Interrupt */ #define PEND_SV_IRQ ( -2 ) /* 14 - Cortex-M0 Pend SV Interrupt */ #define SYSTICK_IRQ ( -1 ) /* 15 - Cortex-M0 System Tick Interrupt */
Le reste des vecteurs d'interruption correspond aux vecteurs ou lignes d'interruption des différents modules, ou interruptions "externes". Il s'agit des numéros 0 à 31 (vecteurs 16 à 47). L'ordre et la présence de chaque vecteur dépend du micro-contrôleur.
Les numéros de ces vecteurs d'interruption sont définis dans le fichier d'entête "include/core/lpc_core.h", à la suite des précédents.
Voir la liste complète plus bas sur cette page, dans la section "Spécificités par micro-contrôleur".
Activation des interruptions
Activation / désactivation globale
static inline void lpc_enable_irq(void); static inline void lpc_disable_irq(void);
La fonction lpc_enable_irq() sert à autoriser toutes les interruptions configurées. Par défaut, au démarrage les interruptions sont autorisées. La fonction lpc_disable_irq() sert à bloquer la totalité des interruptions, sans les désactiver ou changer leur configuration individuelle. Les interruptions peuvent être à nouveau autorisées (selon leur configuration) en faisant appel à la fonction lpc_enable_irq().
Activation / désactivation par vecteur pour les interruptions externes
static inline void NVIC_EnableIRQ(uint32_t IRQ); static inline void NVIC_DisableIRQ(uint32_t IRQ);
Les fonctions NVIC_EnableIRQ et NVIC_DisableIRQ servent à activer / désactiver l'interruption dont le numéro est passé en paramètre.
Pour qu'un driver fonctionne sur interruption il est nécessaire d'activer l'interruption correspondante (et de définir une routine de gestion de l'interruption en utilisant le nom présent dans le fichier "core/bootstrap.c").
Seules les interruptions externes (numéros 0 à 31) peuvent être activées ou désactivées.
Déclenchement ou annulation d'une interruption externe par logiciel
static inline uint32_t NVIC_GetPendingIRQ(uint32_t IRQ); static inline void NVIC_SetPendingIRQ(uint32_t IRQ); static inline void NVIC_ClearPendingIRQ(uint32_t IRQ);
Il est possible de déclencher ou d'annuler de façon "factice" une interruption, que ce soit pour effectuer des tests, ou pour des besoins spécifiques.
Le déclenchement par logiciel se fait par le biais de la fonction NVIC_SetPendingIRQ().
L'annulation d'une interruption (par exemple lorsque les interruptions sont inhibées) se fait par le biais de la fonction NVIC_ClearPendingIRQ().
La fonction NVIC_GetPendingIRQ() renvoie 1 si l'interruption est en attente de traitement, ou 0 sinon.
Les trois fonctions prennent en paramètre le numéro de l'interruption externe concernée (numéros 0 à 31).
Gestion des priorités
static inline void NVIC_SetPriority(uint32_t IRQ, uint32_t priority) static inline uint32_t NVIC_GetPriority(uint32_t IRQ)
TODO.
Spécificités par micro-contrôleur
LPC82x
Liste des interruptions externes et nom des routines de gestion associés :
/* UARTS */ #define UART0_IRQ --> UART_0_Handler() #define UART1_IRQ --> UART_1_Handler() #define UART2_IRQ --> UART_2_Handler() /* I2C */ #define I2C0_IRQ --> I2C_0_Handler() #define I2C1_IRQ --> I2C_1_Handler() #define I2C2_IRQ --> I2C_2_Handler() #define I2C3_IRQ --> I2C_3_Handler() /* SPI */ #define SSP0_IRQ --> SSP_0_Handler() #define SSP1_IRQ --> SSP_1_Handler() /* Timers */ #define SCT_IRQ --> SCT_Handler() /* State Configurable Timer */ #define MRT_IRQ --> MRT_Handler() /* Multi-Rate Timer */ #define WKT_IRQ --> WKT_Handler() /* Wakeup Timer */ /* ADC */ #define CMP_IRQ --> Comparator_Handler() #define ADC_SEQA_IRQ --> ADC_SEQA_Handler() /* ADC Sequence A */ #define ADC_SEQB_IRQ --> ADC_SEQB_Handler() /* ADC Sequence B */ #define ADC_THCMP_IRQ --> ADC_THCMP_Handler() /* ADC Threshold comparator */ #define ADC_OVR_IRQ --> ADC_OVR_Handler() /* ADC Overrun */ /* GPIO */ #define PININT0_IRQ --> PININT_0_Handler() /* [...] */ #define PININT7_IRQ --> PININT_7_Handler() /* Other */ #define WDT_IRQ --> WDT_Handler() /* Watchdog */ #define BOD_IRQ --> BOD_Handler() /* Brown Out Detection */ #define SDMA_IRQ --> DMA_Handler() /* DMA */ #define FLASH_IRQ --> FLASH_Handler()
LPC11A04
LPC122x
Liste des interruptions externes et nom des routines de gestion associés :
/* UARTS */ #define UART0_IRQ --> UART_0_Handler() #define UART1_IRQ --> UART_1_Handler() /* I2C */ #define I2C0_IRQ --> I2C_0_Handler() /* SPI */ #define SSP0_IRQ --> SSP_0_Handler() /* Timers */ #define TIMER0_IRQ --> TIMER_0_Handler() #define TIMER1_IRQ --> TIMER_1_Handler() #define TIMER2_IRQ --> TIMER_2_Handler() #define TIMER3_IRQ --> TIMER_3_Handler() /* ADC */ #define COMPARATOR_IRQ --> Comparator_Handler() #define ADC_IRQ --> ADC_Handler() /* GPIO */ #define PIO_0_IRQ --> PIO_0_Handler() /* port 0 */ #define PIO_1_IRQ --> PIO_1_Handler() /* port 1 */ #define PIO_2_IRQ --> PIO_2_Handler() /* port 2 */ /* Other */ #define WDT_IRQ --> WDT_Handler() /* Watchdog */ #define BOD_IRQ --> BOD_Handler() /* Brown Out Detection */ #define DMA_IRQ --> DMA_Handler() /* DMA */ #define RTC_IRQ --> RTC_Handler() /* Real Time Clock (RTC) */ /* Wakeup */ #define WAKEUP0_IRQ --> WAKEUP_Handler() #define WAKEUP1_IRQ /* [...] */ #define WAKEUP9_IRQ #define WAKEUP10_IRQ #define WAKEUP11_IRQ