Nous avons vu dans les articles précédents comment écrire sur une broche de sortie du connecteur d’extension de la Pandaboarddepuis l’espace utilisateur, puis depuis le kernel. Nous avons également réussi à lire l’état de broches GPIO d’entrée. Cette fois, nous allons améliorer cette lecture en gérant l’occurence d’événements par l’intermédiaire d’interruptions.
Principe
Lorsqu’on veut connaître à un moment donné l’état d’un capteur par exemple, la lecture simple d’une broche d’entrée suffit. Si l’on veut être capable de répondre à des sollicitations extérieures, par exemple activer l’ouverture d’un portillon lorsqu’un bouton a été pressé, il suffit de lire en boucle l’état du bouton et de réagir dès qu’il est au niveau attendu. C’est ce que l’on nommepolling, une technique utilisée depuis longtemps dans les automates industriels et dans la programmation de nombreux micro-contrôleurs.
Les choses se compliquent lorsque l’on désire suivre avec finesse l’état de nombreux capteurs, tout en réalisant d’autres tâches en même temps (calcul, interface utilisateur, etc.). Pour simplifier le traitement, on utilise plutôt un mécanisme d’interruption : le processeur est doté d’un contrôleur d’interruption qui surveille l’état des lignes d’entrée et peut à tout moment signaler un changement d’état au microprocesseur. Celui-ciinterrompt son travail en cours, et exécute des opérations enregistrées au préalable.
Implémentation sous Linux
Sous Linux, le contrôleur d’interruption (APIC) est piloté par le noyau, et lorsqu’une demande d’interruption se présente (par exemple l’arrivée d’un caractère sur un port série), le code préprogrammé est exécuté dans le kernel. Ce code, ce gestionnaire (handler) bas-niveau, assure l’acquittement de l’interruption vis-à-vis de l’APIC. Puis il invoque les éventuelles routines de service installées par les drivers. Ces routines devront effectuer un travail en réponse à l’événement représenté par l’interruption (par exemple réveiller une tâche en attente de données sur une liaison série).
Les ports GPIO configurés en entrée peuvent déclencher des interruptions de différentes manières. Le type de déclenchement le plus courant est celui sur front montant, c’est-à-dire lorsqu’une transition de 0 à 1 (logique) se produit sur la broche d’entrée.
Nous allons traiter les interruptions en écrivant un petit module du kernel Linux qui installera un gestionnaire pour le numéro d’interruption qui nous intéresse ; ce numéro est obtenu grâce au sous-sytème GPIO du noyau (et dépend de l’architecture).
Nous ferons ensuite la même expérience avec un module écrit pour Xenomai en utilisant l’API RTDM (dont nous avons parlé dans le précédent article), et nous comparerons les résultats.
Gestion des interruptions dans le kernel Linux
Notre premier exemple consistera en un module pour noyau 2.6.38.8 standard, dont le fichier de configuration pour Pandaboard estdisponible ici.
Ce module installe un gestionnaire pour l’interruption associée au GPIO d’entrée. À chaque fois qu’un front électrique montant se présente sur cette broche, le handler est appelé et bascule une broche GPIO de sortie.
J’ai choisi d’utiliser en sortie la broche 14, dont le GPIO est indiqué dans lemanuel de référence de la Pandaboard comme valant 139.
La broche 10 reste en entrée comme dans l’article précédent.
Voici le code source de notre module.
irq-gpio-linux.c: #include <linux/fs.h> #include <linux/gpio.h> #include <linux/interrupt.h> #include <linux/irq.h> #include <linux/module.h> #define GPIO_IN 138 #define GPIO_OUT 139 irqreturn_t handler_irq_gpio(int irq, void * ident) { static int value = 0; gpio_set_value(GPIO_OUT, value); value = 1 - value; return IRQ_HANDLED; } static int numero_interruption = 0; int __init exemple_init (void) { int err; numero_interruption = gpio_to_irq(GPIO_IN); if ((err = gpio_request(GPIO_OUT, THIS_MODULE->name)) != 0) return err; if ((err = gpio_direction_output(GPIO_OUT, 0)) != 0) { gpio_free(GPIO_OUT); return err; } if ((err = gpio_request(GPIO_IN, THIS_MODULE->name)) != 0) { gpio_free(GPIO_OUT); return err; } if ((err = gpio_direction_input(GPIO_IN)) != 0) { gpio_free(GPIO_IN); gpio_free(GPIO_OUT); return err; } if ((err = request_irq(numero_interruption, handler_irq_gpio, 0, THIS_MODULE->name, THIS_MODULE->name)) != 0) { gpio_free(GPIO_IN); gpio_free(GPIO_OUT); return err; } set_irq_type(numero_interruption, IRQF_TRIGGER_RISING); return 0; } void __exit exemple_exit (void) { free_irq(numero_interruption, THIS_MODULE->name); gpio_free(GPIO_IN); gpio_free(GPIO_OUT); } module_init(exemple_init); module_exit(exemple_exit); MODULE_LICENSE("GPL");
Nous allons envoyer un signal périodique sur l’entrée GPIO 138, grâce à un générateur basse fréquence (GBF).
Pour éviter les problèmes de surtension en entrée, nous vérifions d’abord à l’oscilloscope que l’amplitude du signal ne dépasse pas 1.8V.
Visiblement le signal plafonne à 1.5V, et les fronts montants se présentent approximativement toutes les deux millisecondes.
Compilons notre module avec un Makefile spécifique, en précisant l’emplacement des sources du noyau installé sur la Pandaboard.
$ make -f Makefile-linux ARCH=arm CROSS_COMPILE=~/cross-panda/usr/bin/arm-linux- KERNEL_DIR=~/Projets/Panda/linux-2.6.38.8-vanilla/ cp -f Makefile-linux Makefile make -C /home/cpb/Projets/Panda/linux-2.6.38.8-vanilla/ SUBDIRS=/home/cpb/Articles/Blog/article-2012-05-25 modules make[1]: entrant dans le répertoire « /home/cpb/Projets/Panda/linux-2.6.38.8-vanilla » Building modules, stage 2. MODPOST 1 modules CC /home/cpb/Articles/Blog/article-2012-05-25/irq-gpio-linux.mod.o LD [M] /home/cpb/Articles/Blog/article-2012-05-25/irq-gpio-linux.ko make[1]: quittant le répertoire « /home/cpb/Projets/Panda/linux-2.6.38.8-vanilla » $
Vérifions, sur la Pandaboard les interruptions déjà gérées par des drivers, puis insérons notre module et examinons à nouveau la liste
[Panda]# cat /proc/interrupts CPU0 CPU1 39: 2 0 GIC TWL6030-PIH 44: 430 0 GIC DMA 69: 31 0 GIC gp timer 88: 257 0 GIC omap_i2c 89: 0 0 GIC omap_i2c 93: 0 0 GIC omap_i2c 94: 0 0 GIC omap_i2c 102: 0 0 GIC serial idle 104: 0 0 GIC serial idle 105: 0 0 GIC serial idle 106: 0 0 GIC serial idle 109: 470 0 GIC ehci_hcd:usb1 115: 1121 0 GIC mmc0 124: 0 0 GIC musb-hdrc 125: 0 0 GIC musb-hdrc 372: 1 0 twl6030 twl6030_usb 378: 0 0 twl6030 twl6030_usb 379: 0 0 twl6030 rtc0 IPI0: 0 0 Timer broadcast interrupts IPI1: 654 727 Rescheduling interrupts IPI2: 0 0 Function call interrupts IPI3: 2 6 Single function call interrupts IPI4: 0 0 CPU stop interrupts LOC: 42035 35007 Local timer interrupts Err: 0 [Panda]# /sbin/insmod /root/irq-gpio-linux.ko [Panda]# cat /proc/interrupts CPU0 CPU1 39: 2 0 GIC TWL6030-PIH 44: 467 0 GIC DMA 69: 31 0 GIC gp timer 88: 257 0 GIC omap_i2c 89: 0 0 GIC omap_i2c 93: 0 0 GIC omap_i2c 94: 0 0 GIC omap_i2c 102: 0 0 GIC serial idle 104: 0 0 GIC serial idle 105: 0 0 GIC serial idle 106: 0 0 GIC serial idle 109: 594 0 GIC ehci_hcd:usb1 115: 1173 0 GIC mmc0 124: 0 0 GIC musb-hdrc 125: 0 0 GIC musb-hdrc 298: 0 0 GPIO irq_gpio_linux 372: 1 0 twl6030 twl6030_usb 378: 0 0 twl6030 twl6030_usb 379: 0 0 twl6030 rtc0 IPI0: 0 0 Timer broadcast interrupts IPI1: 680 751 Rescheduling interrupts IPI2: 0 0 Function call interrupts IPI3: 2 6 Single function call interrupts IPI4: 0 0 CPU stop interrupts LOC: 43797 35903 Local timer interrupts Err: 0 [Panda]#
Nous voyons qu’un handler a été enregistré pour l’interruption 298. Connectons le signal issu du générateur sur la broche 10.
[Panda]# cat /proc/interrupts CPU0 CPU1 39: 2 0 GIC TWL6030-PIH 44: 500 0 GIC DMA 69: 31 0 GIC gp timer 88: 257 0 GIC omap_i2c 89: 0 0 GIC omap_i2c 93: 0 0 GIC omap_i2c 94: 0 0 GIC omap_i2c 102: 0 0 GIC serial idle 104: 0 0 GIC serial idle 105: 0 0 GIC serial idle 106: 0 0 GIC serial idle 109: 603 0 GIC ehci_hcd:usb1 115: 1288 0 GIC mmc0 124: 0 0 GIC musb-hdrc 125: 0 0 GIC musb-hdrc 298: 13885 0 GPIO irq_gpio_linux 372: 1 0 twl6030 twl6030_usb 378: 0 0 twl6030 twl6030_usb 379: 0 0 twl6030 rtc0 IPI0: 0 0 Timer broadcast interrupts IPI1: 750 823 Rescheduling interrupts IPI2: 0 0 Function call interrupts IPI3: 3 6 Single function call interrupts IPI4: 0 0 CPU stop interrupts LOC: 48396 39003 Local timer interrupts Err: 0 [Panda]#
En quelques secondes, notre carte a traité plus de treize mille interruptions. Utilisons maintenant l’oscilloscope afin de visualiser en haut l’entrée du signal sur la broche 10 et en bas, la sortie de la broche 14.
Nous voyons bien qu’à chaque front montant du signal envoyé sur la broche 10 (en haut), le handler d’interruption commute la valeur de la broche 14 (en bas).
Les plus attentifs ont sûrement remarqué que la synchronisation (onglet SCE en bas) se fait à présent sur un front montant du signal de sortie et non plus sur le signal d’entrée. Ceci évite les fluctuations d’affichage. En outre, le point de synchronisation se trouve au début de la douzième case comme l’indique l’onglet TG-PT (Trigger Point) en bas de l’écran. Les points de synchronisation sont numérotés à partir de zéro. Le petit symbole « T » en haut à droite de l’écran indique l’emplacement du point de synchronisation.
Notre système répond donc bien aux interruptions. Le signal est stable. Mais une question se pose souvent dans les systèmes industriels devant répondre à des sollicitations extérieures : « quelle est la latence des interruptions ? » ou, autrement dit, « combien de temps s’écoule après l’arrivée du front montant en entrée et avant que le handler ait pu répondre ? » Pour le savoir, nous allons « zoomer » fortement sur l’image précédente.
La latence est ici d’environ 5 microsecondes. Il faut bien noter que cette valeur fluctue beaucoup. La synchronisation de l’oscilloscope se faisant sur le signal du bas, c’est celui du haut (l’entrée) que nous voyons bouger en permanence. Parfois, il saute vers la gauche en dehors de l’écran, indiquant que le noyau n’a pas pu traiter l’interruption avant – au moins – 22 microsecondes, la largeur de l’écran jusqu’au signal de réponse.
Gestion des interruptions par Xenomai
Pour améliorer les performances temps-réel, il y a deux solutions principales aujourd’hui : appliquer le patch Linux-rt sur notre noyau standard, et utiliser notre même module (recompilé), ou utiliser Xenomai en ré-écrivant le module suivant son API spécifique RTDM. J’ai choisi la seconde solution, car il s’agit la plupart du temps de celle qui offre les meilleures performances (toutefois pour une application réelle, ceci devrait être vérifié avec des tests prolongés).
Voici donc un petit module employant l’API RTDM de Xenomai pour réaliser exactement la même tâche que précédemment.
irq-gpio-rtdm.c: #include <linux/fs.h> #include <linux/gpio.h> #include <linux/interrupt.h> #include <linux/module.h> #include <rtdm/rtdm_driver.h> #define GPIO_IN 138 #define GPIO_OUT 139 static rtdm_irq_t irq_rtdm; static int handler_interruption(rtdm_irq_t * irq) { static int value = 0; gpio_set_value(GPIO_OUT, value); value = 1 - value; return RTDM_IRQ_HANDLED; } static int __init exemple_init (void) { int err; int numero_interruption = gpio_to_irq(GPIO_IN); if ((err = gpio_request(GPIO_IN, THIS_MODULE->name)) != 0) { return err; } if ((err = gpio_direction_input(GPIO_IN)) != 0) { gpio_free(GPIO_IN); return err; } if ((err = gpio_request(GPIO_OUT, THIS_MODULE->name)) != 0) { gpio_free(GPIO_IN); return err; } if ((err = gpio_direction_output(GPIO_OUT, 1)) != 0) { gpio_free(GPIO_OUT); gpio_free(GPIO_IN); return err; } if ((err = rtdm_irq_request(& irq_rtdm, numero_interruption, handler_interruption, 0, THIS_MODULE->name, NULL)) != 0) { gpio_free(GPIO_OUT); gpio_free(GPIO_IN); return err; } set_irq_type(numero_interruption, IRQF_TRIGGER_RISING); // Voir le commentaire de Gilles Chanteperdrix en fin d'article rtdm_irq_enable(& irq_rtdm); return 0; } static void __exit exemple_exit (void) { rtdm_irq_disable(& irq_rtdm); rtdm_irq_free(& irq_rtdm); gpio_free(GPIO_OUT); gpio_free(GPIO_IN); } module_init(exemple_init); module_exit(exemple_exit); MODULE_LICENSE("GPL");
Lors de la compilation, avec un Makefile spécifique également, on précise l’emplacement du script xeno-config
installé avec Xenomai.
$ make -f Makefile-xenomai ARCH=arm CROSS_COMPILE=~/cross-panda/usr/bin/arm-linux- KERNEL_DIR=~/Projets/Panda/linux-2.6.38.8-xenomai XENOCONFIG=~/Projets/Panda/xenomai/bin/xeno-config cp -f Makefile-xenomai Makefile make -C /home/cpb/Projets/Panda/linux-2.6.38.8-xenomai SUBDIRS=/home/cpb/Articles/Blog/article-2012-05-25 modules make[1]: entrant dans le répertoire « /home/cpb/Projets/Panda/linux-2.6.38.8-xenomai » CC [M] /home/cpb/Articles/Blog/article-2012-05-25/irq-gpio-rtdm.o Building modules, stage 2. MODPOST 1 modules CC /home/cpb/Articles/Blog/article-2012-05-25/irq-gpio-rtdm.mod.o LD [M] /home/cpb/Articles/Blog/article-2012-05-25/irq-gpio-rtdm.ko make[1]: quittant le répertoire « /home/cpb/Projets/Panda/linux-2.6.38.8-xenomai » $
Comme précédemment, vérifions les interruptions gérées avant et après insertion de notre module. Naturellement, il faut d’abord booter sur un noyau comprenant Xenomai (compilé par exemple avecce fichier de configuration).
[Panda]# cat /proc/xenomai/irq IRQ CPU0 CPU1 29: 568565 568700 [timer] 416: 0 0 [critical sync] 417: 0 0 [IPI0] 420: 2 0 [IPI3] 421: 0 0 [virtual] 424: 0 0 [virtual] [Panda]# /sbin/insmod /root/irq-gpio-rtdm.ko [Panda]# cat /proc/xenomai/irq IRQ CPU0 CPU1 29: 674274 674435 [timer] 298: 0 0 irq_gpio_rtdm 416: 0 0 [critical sync] 417: 0 0 [IPI0] 420: 2 0 [IPI3] 421: 0 0 [virtual] 424: 0 0 [virtual] [Panda]#
La ligne d’interruption est bien apparue dasn la liste de celles gérées par Xenomai. Branchons le générateur sur l’entrée.
[Panda]# cat /proc/xenomai/irq IRQ CPU0 CPU1 29: 772983 773168 [timer] 298: 9712 0 irq_gpio_rtdm 416: 0 0 [critical sync] 417: 0 0 [IPI0] 420: 2 0 [IPI3] 421: 0 0 [virtual] 424: 0 0 [virtual] [Panda]#
Nous pouvons observer à l’oscilloscope l’entrée et la sortie, comme avec le noyau Linux.
Le signal est beaucoup plus stable qu’avec le noyau standard. Et nous pouvons voir sur la capture ci-dessus que le temps de réaction est sensiblement meilleur (le calibre est maintenant réglé sur une microseconde par division), puisqu’il est passé de 5 microsecondes à un peu plus de 2,5 microsecondes. Cet oscilloscope ne dispose pas de fonction de mémorisation prolongée (une sorte de rémanence infinie) qui nous permettrait de visualiser les latences maximales. Toutefois, il est probable que celles-ci ne dépasseraient pas dix à quinze microsecondes avec Xenomai.
Conclusion
Cette expérience permet de mettre en relief une autre amélioration apportée par Xenomai (après celle des timers vus dans le second article) : la qualité du traitement des interruptions. La durée de prise en charge d’un événement externe est à la fois plus stable et plus courte qu’avec le kernel Linux standard. C’est un paramètre important pour la mise en œuvre d’une application temps réel.
Remarques et commentaires bienvenus !