ALMO TP n°7 - Communications par Interruptions
Préambule
Ce TP porte sur les communications par interruptions entre une application logicielle, s'exécutant sur un processeur programmable, et les périphériques d'une plateforme matérielle. L'application logicielle utilisera maintenant une technique d'interruption, plutôt qu'une technique de scrutation, pour communiquer avec le TTY.
Plateforme matérielle
Comme l'illustre la figure ci-contre, la plateforme matérielle est toujours une architecture mono-processeur simulable avec le simulateur simul_almo_generic
. Cependant, elle contient deux nouveaux composants matériels :
- un contrôleur d'interruptions vectorisées
ICU
(Interrupt Controller Unit) - un contrôleur d'horloge programmable
TIMER
.
Dans cette plateforme, les deux périphériques TTY
et TIMER
transmettent leurs requêtes d'interruption au processeur par l'intermédiaire du composant ICU
. La ligne d'interruption 'TIMER-IRQ'
est connectée à l'entrée 'IRQ_IN[1]'
du contrôleur ICU
, et la ligne d'interruption 'TTY-GET-IRQ'
est connectée à l'entrée 'IRQ_IN[3]'
.
Comme d'habitude, commencez par recopier dans votre répertoire de travail, les fichiers sources spécifiques à ce TP :
$ cp -r /Infos/lmd/2019/licence/ue/LU3IN004-2019oct/sesi-almo/soft/tp07/sujet ./tp07
$ cd tp07
Segmentation de l'espace d'adressage
Puisqu'il y a deux nouveau périphériques, l'espace adressable est maintenant découpé en 10 segments :
- le segment du code utilisateur de l'application, à l'adresse
0x00400000
- le segment des données de l'application, à l'adresse
0x10000000
- le segment de la pile d'exécution, à l'adresse
0x20000000
- le segment du code de reset, à l'adresse
0xBFC00000
- le segment du code du système, à l'adresse
0x80000000
- le segment des données du système, à l'adresse
0x81000000
- le segment des données non cachables du système, à l'adresse
0x82000000
- le segment des registres du périphérique TTY, à l'adresse
0x90000000
- le segment des registres du périphérique TIMER, à l'adresse
0x91000000
- le segment des registres du périphérique ICU, à l'adresse
0x9F000000
- Dans votre répertoire de travail, complétez le fichier de script nommé
seg.ld
, qui définit les adresses de base de ces différents segments.
1. Concentrateur d'interruptions (ICU)
Le composant ICU
est un périphérique programmable, qui permet de concentrer jusqu'à 32 lignes d'interruption IN_IRQ[i]
vers une (ou plusieurs) sorties OUT_IRQ[k]
. Il peut être utilisé avec un ou plusieurs processeurs.
Note : lorsqu'il y a plusieurs processeurs P[k]
dans la plateforme matérielle, la ligne OUT_IRQ[k]
est connectée au processeurs P[k]
.
Le composant ICU fournit trois services :
- Le premier service est de permettre le masquage sélectif des 32 lignes d'interruption
IN_IRQ[i]
au moyen de registres de configurationICU_MASK[k]
de 32 bits.- S'il existe
K
sortiesOUT_IRQ[k]
, alors il existeK
registres deICU_MASK[k]
. - Pour autoriser une interruption
IN_IRQ[i]
à être transmise vers la sortieOUT_IRQ[k]
il faut que la valeur du bit[i]
du registreICU_MASK[k]
soit égale à'1'
. - L'ensemble des valeurs stockées dans le (ou les) registres
ICU_MASK[k]
définit alors la configuration du composant ICU. - Cette configuration permet au logiciel de "router" une ligne d'interruption d'un périphérique particulier
IN_IRQ[i]
vers un processeurP[k]
particulier, et donc de répartir les interruptions entre les différents processeurs. - Comme pour les autres périphériques, c'est le code de démarrage qui est chargé de définir la configuration du composant ICU.
- S'il existe
- Le second service est de réaliser, pour chaque sortie
OUT_IRQ[k]
, un OU logique entre les lignes d'interruption entrantesIN_IRQ[i]
qui ne sont pas masquées, et de transmettre le résultat sur le signalOUT_IRQ[k]
.
- Comme plusieurs lignes d'interruptions
IN_IRQ[i]
provenant de différents périphériques peuvent être routées vers le même processeurP[k]
, et que chaque processeur ne reçoit qu'un seul signalOUT_IRQ[k]
, il faut un mécanisme permettant au gestionnaire d'interruption (GIET) de déterminer quel est le périphérique qui a activé sa ligne d'interruption, pour exécuter l'ISR appropriée. Le troisième service fourni par le composant ICU est donc de renvoyer, pour chaque sortieOUT_IRQ[k]
, le numéro[i]
de l'interruption activeIN_IRQ[i]
, quand le processeur l'interroge.- Pour interroger le composant ICU, le processeur doit effectuer une lecture dans le registre
ICU_INDEX[k]
associé à la sortie[k]
. - Si plusieurs lignes d'interruption sont actives simultanément, le composant ICU renvoie toujours celle d'index le plus petit, ce qui définit une priorité fixe entre les 32 lignes d'interruption.
- Pour interroger le composant ICU, le processeur doit effectuer une lecture dans le registre
On rappelle que le vecteur d'interruptions est un tableau de pointeurs sur fonction. Chaque entrée dans ce tableau contient l'adresse d'une ISR. L'ISR de l'entrée [i]
du vecteur d'interruptions est associée à la ligne d'interruption connectée au port IN_IRQ[i]
du composant matériel ICU. Le code des ISR est défini dans le fichier irq_handler.c
. Ce code s'exécute en mode noyau et est donc rangé dans le segment seg_kcode
.
Dans ce TP nous n'avons qu'un seul processeur, et deux lignes d'interruption provenant d'une part du périphérique TTY et d'autre part du périphérique TIMER. Sur les 32 lignes d'entrées du composant ICU, deux sont donc utilisées, tandis qu'une seule ligne d'interruption est présente en sortie. Cela signifie qu'on n'utilise qu'un seul registre ICU_MASK
et un seul registre ICU_INDEX
dans le composant ICU.
- Complétez le fichier
reset.s
pour qu'il initialise le vecteur d'interruptions (écriture dans le tableau situé à l'adresse_interrupt_vector
), en sachant que la ligne d'interruption issue du TIMER est connectée à entréeIN_IRQ[1]
du composant ICU, tandis que celle issue du TTY est connectée à l'entréeIN_IRQ[3]
: la deuxième case du vecteur d'interruptions doit contenir l'adresse de la routine_isr_timer
, et la quatrième doit contenir l'adresse de la routine_isr_tty_get_task0
.
- En analysant les trois instructions du code de démarrage qui réalisent la configuration du composant ICU, déterminez à quelle adresse est implanté le registre de configuration
ICU_MASK
de l'ICU.
2. Contrôleur d'horloge programmable (TIMER)
Le composant TIMER
est un périphérique programmable contenant une ou plusieurs horloges. Chaque horloge fournit deux services :
- Chaque horloge contient un compteur de cycles qui peut être utilisé par un programme pour obtenir une référence de temps (date) absolue.
- Chaque horloge peut être programmée pour générer des interruptions périodiques.
Chaque horloge possède 4 registres adressables, et un registre interne non-adressable TIMER_COUNTER
, dont le passage à zéro déclenche l'interruption matérielle périodique :
TIMER_VALUE
(accessible à l'adresseseg_timer_base
) : registre de 32 bits pouvant être lu et écrit. Ce registre est incrémenté à chaque cycle lorsque le bit de mode correspondant est activé. Ce registre n'est pas utilisé pour la génération des interruptions périodiques.TIMER_MODE
(accessible à l'adresseseg_timer_base + 4
) : registre de 32 bits dont les valeurs définissent le mode de fonctionnement :- Bit n°0 : si ce bit est à
1
, alors le registreTIMER_COUNTER
est décrémenté à chaque cycle. - Bit n°1 : si ce bit est à
1
, alors le passage à zéro du registreTIMER_COUNTER
provoque une interruption matérielle.
- Bit n°0 : si ce bit est à
TIMER_PERIOD
(accessible à l'adresseseg_timer_base + 8
) : registre de 32 bits contenant la valeur rechargée dans le compteurTIMER_COUNTER
lors de son passage à zéro.TIMER_RESETIRQ
(accessible à l'adresseseg_timer_base + 12
) : pseudo-registre de 32 bits dans lequel l'écriture de n'importe quelle valeur désactive la ligne d'interruption matérielle du composant (la ligne d'interruption repasse à l'état bas, jusqu'au prochain passage à zéro du registreTIMER_COUNTER
).
- Regardez le fichier
irq_handler.c
et analysez le code de la fonction_isr_timer
. Déterminez quelles actions sont exécutées par l'ISR associée au TIMER.
La fonction main()
qui vous est fournie, dans main.c
, exécute une boucle dans laquelle elle affiche 1000 fois le message "hello world", puis se termine par un exit()
.
- Complétez cette fonction pour définir une période de 5000000 cycles, puis activer le composant d'horloge.
- Compilez le système logiciel en utilisant le
Makefile
qui vous est fourni, et lancez l'exécution de ce code sur le simulateursimul_almo_generic
.- Qu'observez-vous ?
- Comment expliquez-vous que le TTY continue à afficher les messages en provenance du TIMER, même après la terminaison de l'application ?
3. Lecture de caractères sur le terminal TTY
Dans le TP4, le logiciel utilisait une technique de scrutation directe du registre TTY_STATUS
du contrôleur TTY. plus précisément, l'application utilisateur utilisait l'appel système tty_getc(char *byte)
(défini dans le fichier stdio.c
) pour acquérir un caractère au clavier, puisqu'une application utilisateur ne peut pas directement accéder au périphérique TTY. L'appel système tty_getc
contient une boucle de scrutation qui fait appel, de façon répétée, à la fonction système _tty_read()
(définie dans le fichier drivers.c
), par l'intermédiaire d'un appel système. La fonction système _tty_read()
lit la valeur du registre TTY_STATUS
du terminal, en fonction du numéro du processeur courant, et renvoie 0
ou 1
selon que le registre TTY_READ
est vide ou plein. Cette fonction système n'est donc pas bloquante, mais la fonction utilisateur tty_getc()
est bloquante et ne retourne au programme appelant que lorsqu'un caractère a été saisi au clavier.
Cette méthode de scrutation a un défaut : c'est une méthode dite d'attente active (polling), dans laquelle le processeur n'exécute aucun travail utile pendant les millions (ou les milliards) de cycles durant lesquels il lit de façon répétée le contenu du registre TTY_STATUS
en attendant qu'un caractère soit saisi au clavier.
On va donc utiliser une autre technique, un peu plus compliquée, mais qui permet de découpler la "production" du caractère par le TTY, et la "consommation" du caractère par le programme utilisateur. Dans un contexte d'exécution multi-tâches où plusieurs applications logicielles s'exécutent en parallèle sur le même processeur, ce découplage permet aux autres applications utilisateurs de continuer à s'exécuter, même lorsqu'une application particulière est bloquée en attente d'un caractère.
Pour cela, on utilise un tampon mémoire appartenant au système d'exploitation et nommé _tty_get_buf[i]
. Ce tampon est protégé par une variable de synchronisation _tty_get_full[i]
qui indique si le tampon est plein ou vide. Comme on peut avoir jusque 32 terminaux écran/clavier dans une plateforme matérielle, le GIET définit 32 tampons indexés par [i]
: une paire _tty_get_buf[i]
/_tty_get_full[i]
pour chaque terminal TTY[i]
.
Lorsqu'un caractère est tapé au clavier, le terminal TTY[i]
génère une requête d'interruption TTY-GET-IRQ[i]
, qui va déclencher l'exécution de l'ISR associée à la réception d'un caractère. Cette ISR fait deux choses:
- elle lit le caractère présent dans le registre TTY_READ du contrôleur TTY[i], et écrit ce caractère dans le tampon
_tty_get_buf[i]
, - elle écrit la valeur
1
dans la variable de synchronisation_tty_get_full[i]
.
Remarque : c'est la commande de lecture dans le registre TTY_READ qui informe le contrôleur TTY qu'il doit désactiver la requête d'interruption TTY-GET-IRQ[i]. C'est donc cette commande de lecture qui acquitte l'interruption.
Pour lire un caractère, l'application logicielle utilise maintenant l'appel système tty_getc_irq(char *byte)
. Comme précédemment, cette fonction contient une boucle de scrutation qui s'exécute en mode utilisateur, et dans laquelle on appelle de façon répétée la fonction système _tty_read_irq()
par l'intermédiaire d'un appel système. La fonction système _tty_read_irq()
est non bloquante. Elle calcule l'index [i]
du terminal concerné en fonction du numéro de processeur courant, puis elle teste la valeur de la variable de synchronisation _tty_get_full[i]
. Si le tampon _tty_get_buf[i]
est vide, elle retourne 0
pour signaler un échec. Si le tampon est plein, elle copie le caractère dans la variable byte
passée en paramètre par l'application, remet à 0
la variable de synchronisation, et retourne la valeur 1
pour signaler un succès.
- Regardez le fichier
irq_handler.c
et expliquez ce que fait le code de l'ISR_isr_tty_get_task0()
(et par extension, le code de_isr_tty_get_indexed()
) ? Que se passe-t-il si le tampon de réception n'est pas vide lorsque l'ISR est exécutée ?
- Ouvrez le fichier
stdio.c
, et expliquez ce que fait l'appel systèmetty_getc_irq()
?
- Ouvrez le fichier
drivers.c
, et expliquez ce que fait la fonction système_tty_read_irq()
?
- Après avoir sauvé le fichier
main.c
sous un autre nom, éditez-le et modifiez la fonctionmain()
pour qu'elle lise un caractère au clavier après chaque affichage "hello world", en utilisant l'appel systèmetty_getc_irq()
.
Note : vous conserverez les interruptions en provenance du TIMER.
- Recompilez ce code et testez-le en simulation.