GPIO, Pandaboard et temps réel – 4 – Gestion des interruptions

Publié par cpb
Mai 28 2012

GPIO, Pandaboard et temps réel

Nous avons vu dans les articles précédents comment écrire sur une broche de sortie du connecteur d’extension de la Pandaboard depuis 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 nomme polling, 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-ci interrompt 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 est disponible 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 le manuel de référence de la Pandaboard comme valant 139.

Connecteur d'extension de la Pandaboard

La broche 10 reste en entrée comme dans l’article précédent.

Documentation du connecteur d'extension A de la Pandaboard

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).

 

Générateur Basse Fréquence

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.

Signal sur broche d'entrée 10 de la PandaboardVisiblement 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.

Réponse aux interruptions - Kernel LinuxNous 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.

Latence des interruptions GPIO de la Pandaboard

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 avec ce 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.

Latence des interruptions sous Xenomai

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 !

3 Réponses

  1. gch dit :

    Je viens de trouver cet article, juste un petit détail à propos de xenomai: le fait qu’il soit nécessaire d’appeler rtdm_irq_enable pour activer l’interruption peut être considéré comme un bug, dû au patch Adeos. Cela a été corrigé dans les patches Adeos fournit avec xenomai 2.6.1

  2. Pierrot dit :

    Bonjour Christophe,

    J’ai tenté de faire tourner ce code sur le Raspberry Pi.
    Il semble que “set_irq_type” ne soit plus disponible dans linux/interrupt.h car à la compilation j’obtenais “implicit declaration of function ‘set_irq_type’ ”

    J’ai donc supprimé la ligne “set_irq_type(numero_interruption, IRQF_TRIGGER_RISING);” et remplacer la ligne :
    if ((err = rtdm_irq_request(& irq_rtdm, numero_interruption, handler_interruption, 0, THIS_MODULE->name, NULL)) != 0) {

    par :
    if ((err = rtdm_irq_request(& irq_exemple, num_irq, exemple_handler, RTDM_IRQTYPE_SHARED | RTDM_IRQTYPE_EDGE, THIS_MODULE->name, NULL)) !=0) {

    Après compilation et transfert sur la carte, je charge le module, j’appuie plusieurs fois sur mon bouton poussoir reliant mon entrée gpio 23 au 3,3v, puis invoque cat /proc/xenomai/irq :

    /MyDEV/06_GPIO_rtdm # insmod rpi-rtdm.ko
    /MyDEV/06_GPIO_rtdm # cat /proc/xenomai/irq
    IRQ CPU0
    3: 1134 [timer]
    108: 0 Exemple
    259: 0 [virtual]

    Aucune interruption n’est prise en compte.

    A noter que sans xenomai, je parviens a detecter des interruptions avec le même montage :

    /MyDEV/02_GPIO_inter # insmod rpi-gpio.ko
    /MyDEV/02_GPIO_inter # cat /proc/interrupts
    CPU0
    3: 14053 ARMCTRL BCM2708 Timer Tick
    52: 10 ARMCTRL BCM2708 GPIO catchall handler
    65: 6 ARMCTRL ARM Mailbox IRQ
    66: 0 ARMCTRL VCHIQ doorbell
    75: 7942676 ARMCTRL dwc_otg, dwc_otg_pcd, dwc_otg_hcd:usb1
    77: 148 ARMCTRL bcm2708_sdhci (dma)
    83: 452 ARMCTRL uart-pl011
    84: 1297 ARMCTRL mmc0
    108: 10 GPIO rpi_gpio
    Err: 0

    Une idée du problème?

    Voici le code complet :

    #include
    #include
    #include
    #include
    #include
    #include
    #include
    #include

    #include
    #include

    static rtdm_irq_t irq_exemple;
    static int exemple_handler(rtdm_irq_t * irq);

    #define GPIO_IN 23
    #define GPIO_OUT 9

    static int __init exemple_init (void)
    {
    int err;
    int num_irq = 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_exemple, num_irq, exemple_handler, RTDM_IRQTYPE_SHARED | RTDM_IRQTYPE_EDGE, “Exemple”, NULL)) !=0) {
    gpio_free(GPIO_OUT);
    gpio_free(GPIO_IN);
    return err;
    }

    rtdm_irq_enable(& irq_exemple);
    return 0;
    }

    static void __exit exemple_exit (void)
    {
    rtdm_irq_disable(& irq_exemple);
    rtdm_irq_free(& irq_exemple);
    gpio_free(GPIO_OUT);
    gpio_free(GPIO_IN);
    }

    static int exemple_handler(rtdm_irq_t * irq)
    {
    static int value = 0;
    gpio_set_value(GPIO_OUT, value);
    value = 1 – value;
    return RTDM_IRQ_HANDLED;
    }

    module_init(exemple_init);
    module_exit(exemple_exit);
    MODULE_LICENSE(“GPL”);

URL de trackback pour cette page