wiki:SujetTP7

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 :

  1. le segment du code utilisateur de l'application, à l'adresse 0x00400000
  2. le segment des données de l'application, à l'adresse 0x10000000
  3. le segment de la pile d'exécution, à l'adresse 0x20000000
  4. le segment du code de reset, à l'adresse 0xBFC00000
  5. le segment du code du système, à l'adresse 0x80000000
  6. le segment des données du système, à l'adresse 0x81000000
  7. le segment des données non cachables du système, à l'adresse 0x82000000
  8. le segment des registres du périphérique TTY, à l'adresse 0x90000000
  9. le segment des registres du périphérique TIMER, à l'adresse 0x91000000
  10. 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 configuration ICU_MASK[k] de 32 bits.
    • S'il existe K sorties OUT_IRQ[k], alors il existe K registres de ICU_MASK[k].
    • Pour autoriser une interruption IN_IRQ[i] à être transmise vers la sortie OUT_IRQ[k] il faut que la valeur du bit [i] du registre ICU_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 processeur P[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.
  • Le second service est de réaliser, pour chaque sortie OUT_IRQ[k], un OU logique entre les lignes d'interruption entrantes IN_IRQ[i] qui ne sont pas masquées, et de transmettre le résultat sur le signal OUT_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 processeur P[k], et que chaque processeur ne reçoit qu'un seul signal OUT_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 sortie OUT_IRQ[k], le numéro [i] de l'interruption active IN_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.

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ée IN_IRQ[1] du composant ICU, tandis que celle issue du TTY est connectée à l'entrée IN_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'adresse seg_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'adresse seg_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 registre TIMER_COUNTER est décrémenté à chaque cycle.
    • Bit n°1 : si ce bit est à 1, alors le passage à zéro du registre TIMER_COUNTER provoque une interruption matérielle.
  • TIMER_PERIOD (accessible à l'adresse seg_timer_base + 8) : registre de 32 bits contenant la valeur rechargée dans le compteur TIMER_COUNTER lors de son passage à zéro.
  • TIMER_RESETIRQ (accessible à l'adresse seg_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 registre TIMER_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 simulateur simul_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ème tty_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 fonction main() pour qu'elle lise un caractère au clavier après chaque affichage "hello world", en utilisant l'appel système tty_getc_irq().

Note : vous conserverez les interruptions en provenance du TIMER.

  • Recompilez ce code et testez-le en simulation.
Last modified 5 years ago Last modified on Aug 26, 2019, 11:12:04 AM