Aller au contenu principal

Interruptions et ordonnancement

Problème​

Considérons le code suivant (microcontrôleur AVR, où le bit UDRE0 du registre UCSR0A indique que le registre d'émission UDR0 est vide) :

#include <avr/io.h>

void uart_send(char *str) {
while (*str != '\0') {
// Waits to be ready to send next byte
while (!(UCSR0A & _BV(UDRE0)));
// Sends the next byte
UDR0 = *(str++);
}
}

Ce code fonctionne correctement, mais il bloque le processeur pendant toute la durée de l'envoi.

Ordonnancement​

Comment réaliser différentes tâches simultanément​

Le problème peut être résolu en utilisant un ordonnanceur, c'est-à-dire un système qui permet de définir l'ordre d'exécution des tâches. Il est aussi possible de réaliser des tâches (pseudo-)simultanément en utilisant de l'ordonnancement coopératif :

int main() {
setup();

while (1) {
uart_tick();
leds_tick();
}
}

Ici, uart_tick() et leds_tick() sont des fonctions coopératives : elles effectuent un peu de travail et rendent la main rapidement. Voici une façon d'organiser le code précédent selon ce principe :

#include <avr/io.h>
#include <string.h>

static char buffer[128];
static size_t position = 0;
static size_t length = 0;

void uart_send(char *str) {
position = 0;
length = strlen(str);
if (length > sizeof(buffer)) {
length = sizeof(buffer); // évite un débordement du buffer
}
memcpy(buffer, str, length);
}

void uart_tick() {
if (position < length) {
if (UCSR0A & _BV(UDRE0)) {
UDR0 = buffer[position];
position += 1;
}
}
}

Polling et interruptions​

Dans le premier exemple, nous attendions activement que le buffer soit disponible avec une boucle : c'est ce que l'on appelle le polling (scrutation).

Ensuite, nous avons introduit le concept simple de l'ordonnancement manuel, mais le microcontrôleur passera toujours beaucoup de temps à exécuter des tests qui sont pour la plupart faux.

Une caractéristique des microcontrôleurs peut nous aider ici : les interruptions. Il s'agit simplement de routines exécutées lorsqu'un événement quelconque se produit. Le processeur "saute" alors automatiquement à une fonction donnée (routine d'interruption, ou ISR).

Le code binaire que nous téléchargeons commence par une table de vecteurs d'interruption. Chaque entrée de cette table correspond à un type d'interruption et donne une adresse cible pour la routine à exécuter.

Table des vecteurs d&#39;interruption

Ring buffer​

Pour gérer l'envoi et la réception de données à l'aide d'un bus comme l'UART, nous pouvons utiliser un buffer en anneau, permettant de stocker les données qui ne sont pas encore traitées.

Il peut s'agir soit de données TX qui n'ont pas encore été envoyées, soit de données RX qui n'ont pas été lues par le code.

Buffer circulaire

Le principe est de stocker un tableau en mémoire avec deux index, le premier étant le prochain élément à lire, et le second le prochain élément à écrire.

Lorsque les index sont incrémentés, ils reviennent au début du tableau lorsqu'ils atteignent la fin (arithmétique modulo la taille du buffer).

Nous pouvons vérifier s'il y a des données à lire en comparant les index : le buffer est vide lorsqu'ils sont égaux.