Licence: Creative Commons BY SA 3.0 http://creativecommons.org/licenses/by-sa/3.0/fr/
Une interruption est un dispositif permettant d'interrompre, suite à un évènement, le fonctionnement d'un programme pour exécuter une fonction spécifique à cet évènement. C'est ce qui permet d'éviter d'avoir à faire ce qu'on appelle du “polling”, consistant à vérifier en boucle dans le programme si une valeur à changé, ralentissant d'autant le code, évitant au microcontrolleur de se mettre en veille et présentant le risque de rater un évènement si le programme était en train de faire autre chose.
Un endroit spécifique de la mémoire contient ce que l'on appelle un vecteur d'interruption contenant l'adresse de la fonction quoi doit être exécutée quand l'interruption se présente.
Il existe 3 types d'interruptions sur les MSP430:
Le reset est une interruption spéciale, qui a une priorité sur toutes les autres. Par défaut le reset pointe vers l'adresse de départ du programme, mais il est possible de changer cela afin d'effectuer une action avant le reset effectif. Ou d'utiliser le bouton reset pour faire autre chose.
Les NMI ne sont pas gérées par le même système d'interruption que les autres. Elles sont toutes désactivées automatiquement après l'exécution d'une d'entre elles et doivent être explicitement réactivées par l'utilisateur. Le vecteur d'interruption est situé à l'adresse 0xFFFC. Il existe trois sources d'interruptions :
Les interruptions masquables sont produites par les différents périphériques internes et externes du MSP430. Ce sont celles qui nous intéressent ici.
Contrairement à d'autres microcontrolleurs tels que les ATMEGA 328, les MSP430 disposent d'interruptions programmables sur chaque broche de PORT1 et PORT2. Ils sont configurés par 3 registres, PxIFG, PxIE et PxIES (avec x étant le numéro de port).
Les différents vecteurs sont définis à partir de l'adresse 0xFFE0. Attention les pointeurs de fonction sont définis sur un mot (deux octets).
Interruption | Nom en C | Décalage | Position réelle |
Port 1 | PORT1_VECTOR | 0x0004 | 0xFFE4 |
Port 2 | PORT2_VECTOR | 0x0006 | 0xFFE6 |
Convertisseur A/D | ADC10_VECTOR | 0x000A | 0xFFEA |
USCI A0/B0 Transmission | USCIAB0TX_VECTOR | 0x000C | 0xFFEC |
USCI A0/B0 Réception | USCIAB0RX_VECTOR | 0x000E | 0xFFEE |
Timer0)A CC1, TA0 | TIMER0_A1_VECTOR | 0x0010 | 0xFFF0 |
Timer0_A CC0 | TIMER0_A0_VECTOR | 0x0012 | 0xFFF2 |
Timer du Watchdog | WDT_VECTOR | 0x0014 | 0xFFF4 |
Comparateur A | COMPARATORA_VECTOR | 0x0016 | 0xFFF6 |
Timer1_A CC1-4, TA1 | TIMER1_VECTOR | 0x0018 | 0xFFF8 |
Timer1_A CC0 | TIMER1_A0_VECTOR | 0x001A | 0xFFFA |
NMI | NMI_VECTOR | 0x001C | 0xFFFC |
Reset [Priorité maximale] | RESET_VECTOR | 0x001E | 0xFFFE |
On n'a pas besoin de s'embêter avec la position réelle, la macro « interrupt(VECTOR) » contenue dans legacymsp430.h permet de faire le calcul automatiquement. Ainsi, on définit une fonction pour une interruption sur le PORT1, par :
interrupt (PORT1_VECTOR) Nomdelafonction(void) { // Corps de la fonction }
Ce registre est un registre drapeau, le bit correspondant à l'entrée du port ayant déclenché une interruption est mis à 1 par le moteur d'interruptions. Il peut aussi servir à déclencher des interruptions logicielles, c'est à dire que c'est l'utilisateur qui place un bit à 1 dans ce registre pour déclencher l'interruption.
Ce registre contrôle sur quel front est déclenchée l'interruption. Si le bit est à 0, c'est une transition bas vers haut qui déclenche, et inversement pour 1.
PxIESx | PxINx | PxIFGx |
0→1 | 0 | Peut être |
0→1 | 1 | Non changé |
1→0 | 0 | Non changé |
1→0 | 1 | Peut être |
Ce registre sert à contrôler si l'on souhaite utiliser les interruptions sur la broche correspondante.
#include <msp430g2553.h> #include <legacymsp430.h> // nécessaire pour les interruptions interrupt (PORT1_VECTOR) Port_1(void) // Façon de coder une fonction d'interruption sur le PORT1 { volatile int i; for (i = 0; i < 0x60; i++); // Petite pause pour l'anti-rebond while (! (P1IN & BIT3)); // On attend que l'utilisateur lache le bouton P1OUT ^= BIT6; // On inverse la led verte P1IFG &= ~BIT3; // On efface le drapeau pour permettre à l'interruption d'arriver à nouveau } int main(void) { volatile int i; // Arrêt du Watchdog WDTCTL = WDTPW | WDTHOLD; // P1.0 et P1.6 en tant que sorties P1DIR = BIT0 | BIT6; // P1.0=0 et P1.6=1 P1OUT = BIT3; // Il est nécessaire de mettre le registre de l'entrée à 1 P1REN = BIT3; // Et de mettre la résistance de pull-down // Règle l'interruption sur P1.3 P1IE |= BIT3; // On souhaite surveiller P1.3 P1IES |= BIT3; // Sur un front descendant P1IFG &= ~BIT3; // Effacer le registre drapeau _BIS_SR(GIE); // Lancer le module des interruptions for (;;) { // Boucle infinie // inversion de la led rouge P1OUT ^= BIT0; // pause pour le clignotement for (i = 0; i < 0x6000; i++); } }
Reprenons la définition de fonction d'interruption:
interrupt (PORT1_VECTOR) Nomdelafonction(void) { // Corps de la fonction }
Le code précédent est en fait transformé par le préprocesseur en :
void __attribute__((interrupt ((0x0004)))) Nomdelafonction(void) { // Corps de la fonction }
attribute est une fonction spéciale de gcc servant à donner au compilateur des informations sur la fonction. On peut indiquer beaucoup de choses, mais dans ce cas précis, on lui indique que cette fonction doit être placée au vecteur d'interruption ayant un offset de 0x0004.
Le code assembleur généré par gcc (option -S) devient alors
.global Port_1 .type Port_1,@function /*********************** * Interrupt Vector 2 Service Routine `Port_1' ***********************/ Port_1: .global __isr_2 __isr_2: // Reste de la fonction
Ceci indique au compilateur que la fonction est située à l'adresse __isr_2.
Ce code une fois compilé donne quand on utilise msp430-objdump -d une fonction située à l'adresse 0xc04c et une section .vector située à partir de l'adresse 0xffe0, contenant à l'adresse 0xffe4 (celle du vecteur d'interruption de PORT1 vu dans le tableau précédent) la fonction 0xc04c.
C'est ici l'avantage du format ELF, permettant d'indiquer la position d'une section en mémoire sans avoir à définir l'intégralité de la mémoire.
Ainsi le fichier avec des fonctions vides (main et Port1) sans informations de debug et compilé avec -Os fait 492 octets, alors que si il avait fallu définir la mémoire entière, il aurait fait 64K (Je donnerai plus de détails sur cette histoire de mémoire dans un autre article, en particulier pour expliquer comment une petite bête avec 16K de mémoire contient des adresses qui s'étendent sur 64K).
Il est possible de voir les différentes sections d'un fichier ELF avec l'outil readelf -a. Qui nous donne la taille de chacune de ces sections. Ainsi on voit que même si le fichier fait 492 octets, finalement, la majorité des données est constituée par les métadonnées de ELF et les sections spéciales telles que les vecteurs. On ne peut donc pas juger de la taille réelle d'un programme par sa taille sur le disque.
Cette fonction est une macro, remplacée par :
__bis_status_register((0x0008));
Elle sert en fait à mettre à 1 dans le registre SR (Status register) les bits correspondants à son argument.
La fonction __bic_status_register (ou _BIC_SR) sert à mettre cette valeur à 0.
Le bit 3 du registre SR (16 bits) est appellé GIE (Global Interrupt Enable) et sert à lancer le moteur d'interruptions.