Attentes passives sur GPIO

Publié par cpb
Avr 15 2013

GPIO select() poll()Nous avons déjà vu dans plusieurs articles qu’il était facile de manipuler les GPIO sous Linux depuis l’espace utilisateur avec l’interface /sys. Jusqu’à présent je ne m’étais intéressé qu’aux lectures et écritures, mais il est également possible de faire des attentes passives de changement d’état avec select() ou poll(). Voyons-en une mise en œuvre sur le Raspberry Pi.

Il est facile de faire une attente de changement d’état en écrivant un petit driver dans le kernel qui s’accrochera à l’interruption déclenchée par la broche GPIO. Nous l’avons même réalisé avec un driver RTDM pour Xenomai. Toutefois ceci pose un problème de portabilité, car le code n’est pas directement réutilisable sur une autre architecture (bien qu’il n’y ait pas beaucoup de variations). En outre cela impose un développement en mode noyau, nécessitant donc des privilèges d’administration et augmentant surtout le risque de crash système en cas d’erreur.

C’est pour simplifier ces accès que le noyau propose une interface passant par le système de fichiers virtuels /sys. On peut très bien réaliser des opérations sur les GPIO de manière plus efficace en employant par exemple mmap() pour accéder à une projection en mémoire virtuelle des ports d’entrée-sortie. Néanmoins ceci pose de gros problèmes de portabilité et d’évolutivité des applications. Je préfère donc, autant que possible, me contenter des accès via /sys.

Dans de nombreux cas d’application, il est important d’attendre passivement le changement d’état d’une broche (bouton, signal externe, capteur, etc.) en évitant absolument toute méthode de polling (scrutation en boucle périodique de l’état de l’entrée). Si la broche GPIO concernée est éligible pour le déclenchement d’une interruption, Linux nous permet d’utiliser l’appel-système select() ou l’appel poll() pour attendre un événement (un changement d’état qui se matérialisera par une interruption) sans consommer de CPU pendant la durée de l’attente.

Test avec select()

Voici un petit programme qui ouvre le fichier qu’on lui indique en argument, puis il attend l’arrivée d’un « événement exceptionnel » sur le descripteur. Ces « événements exceptionnels » traités par select() dépendent du type de descripteur de fichier. Pour une socket TCP par exemple, il peut s’agir de trames urgentes. Pour un descripteur d’état de GPIO dans sysfs, il s’agit d’un changement de niveau ayant déclenché une interruption.

Lorsqu’il est réveillé par un changement d’état, notre programme lit alors la valeur sur la broche et l’affiche précédée de l’heure avant de se rendormir en attente du prochain réveil.

gpio-select.c:
#include <fcntl.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/select.h>

int main(int argc, char * argv[])
{
    int fd;
    fd_set fds;
    struct timeval tv;
    char buffer[2];

    // On attend en argument un fichier "value" de GPIO.
    // Par exemple : /sys/class/gpio/gpio23/value
    if (argc != 2) {
        fprintf(stderr, "usage: %s \n", argv[0]);
        exit(EXIT_FAILURE);
    }
    // Ouvrir le fichier
    if ((fd = open(argv[1], O_RDONLY)) < 0) {
        perror(argv[1]);
        exit(EXIT_FAILURE);
    }

    while (1) {
        // Preparer la table des evenements exceptionnels attendus
        FD_ZERO(& fds);
        // Avec uniquement le descripteur du fichier.
        FD_SET(fd, & fds);
        // Attente passive (pas de timeout, donc infinie...
        if (select(fd+1, NULL, NULL, & fds, NULL) < 0) {
            perror("select");
            break;
        }
        // Lire l'heure du changement d'etat.
        gettimeofday(& tv, NULL);
        // Revenir au debut du fichier (lectures successives).
        lseek(fd, 0, SEEK_SET);
        // Lire la valeur actuelle du GPIO.
        if (read(fd, & buffer, 2) != 2) {
            perror("read");
            break;
        }
        // Effacer le retour-chariot.
        buffer[1] = '\0';
        // Afficher l'heure et l'etat.
        fprintf(stdout, "[%ld.%06ld]: %s\n", tv.tv_sec, tv.tv_usec, buffer);
    }
    close(fd);
    return EXIT_SUCCESS;
}

Essayons notre programme sur un Raspberry Pi auquel j’ai connecté un petit oscillateur à 4Hz sur la broche 16 du port d’extension. Ceci correspond au GPIO 23. Commençons donc par demander au kernel de nous offrir l’accès à ce GPIO depuis sysfs.

R-Pi login: root
Password:
root@R-Pi [/root]# cd /sys/class/gpio/
root@R-Pi [gpio]# ls
export     gpiochip0  unexport
root@R-Pi [gpio]# echo 23 > export
root@R-Pi [gpio]# ls
export     gpio23     gpiochip0  unexport
root@R-Pi [gpio]# cd gpio23/
root@R-Pi [gpio23]# ls
active_low  direction   edge        subsystem   uevent      value
root@R-Pi [gpio23]#

Essayons notre programme…

root@R-Pi [gpio23]# /root/gpio-select ./value
[47.742961]: 0
   (rien ne se passe...)
^C
root@R-Pi [gpio23]#

Avant de conclure que notre programme est erroné, vérifions sur quels types de fronts (montants ou descendants) du signal l’interruption est-elle déclenchée.

root@R-Pi [gpio23]# cat edge
none
root@R-Pi [gpio23]#

Voici donc l’explication. Par défaut aucune interruption n’est produite par les changements d’état sur les broches GPIO (heureusement, car avec les variations permanentes de tension se produisant sur les broches laissées libres, il y aurait des interruptions sans cesse, ce qui consomme du CPU). Configurons donc notre GPIO pour qu’il nous réveille sur les fronts montants.

root@R-Pi [gpio23]# echo rising > edge
root@R-Pi [gpio23]# /root/gpio-select ./value
[98.887860]: 1
[99.137953]: 1
[99.388067]: 1
[99.638183]: 1
[99.888280]: 1
[100.138399]: 1
[100.388500]: 1
[100.638604]: 1
[100.888726]: 1
[101.138819]: 1
[101.388940]: 1
[101.639035]: 1
[101.889145]: 1
[102.139269]: 1
[102.389365]: 1
[102.639479]: 1
[102.889577]: 1
[103.139686]: 1
[103.389810]: 1
[103.639901]: 1
^C
root@R-Pi [gpio23]#

Très bien. Les écarts entre les déclenchements sont bien de 250ms (soit 4Hz), et les valeurs lues sont toujours à 1 car le signal chaque fois de monter pour atteindre +3.3V. Essayons de détecter les fronts descendants…

root@R-Pi [gpio23]# echo falling > edge
root@R-Pi [gpio23]# /root/gpio-select ./value
[133.285615]: 0
[133.527846]: 0
[133.777937]: 0
[134.028055]: 0
[134.278160]: 0
[134.528262]: 0
[134.778380]: 0
[135.028480]: 0
[135.278598]: 0
[135.528694]: 0
[135.778802]: 0
[136.028923]: 0
[136.279026]: 0
[136.529134]: 0
[136.779234]: 0
[137.029345]: 0
[137.279467]: 0
[137.529560]: 0
^C
root@R-Pi [gpio23]#

Tout à fait logiquement, ce sont maintenant des valeurs à 0 qui sont toujours lues car le signal d’entrée vient donc de descendre pour déclencher l’interruption.

Essayons de détecter simultanément les deux types de fronts.

root@R-Pi [gpio23]# echo both > edge
root@R-Pi [gpio23]# /root/gpio-select ./value
[151.806837]: 0
[151.910795]: 1
[152.035833]: 0
[152.160890]: 1
[152.285962]: 0
[152.411002]: 1
[152.536047]: 0
[152.661102]: 1
[152.786164]: 0
[152.911212]: 1
[153.036265]: 0
[153.161328]: 1
[153.286373]: 0
[153.411429]: 1
[153.536494]: 0
[153.661542]: 1
[153.786587]: 0
[153.911645]: 1
[154.036705]: 0
[154.161756]: 1
[154.286890]: 0
^C
root@R-Pi [gpio23]#

Cela fonctionne également, les valeurs lues sont alors alternativement 0 et 1 et les déclenchements sont espacés de 125ms.

Et poll() ?

L’appel-système poll() est mal nommé. Il n’assure pas un polling tel qu’on peut l’entendre en programmation micro-contrôleur par exemple où l’on vient examiner l’état d’une entrée en boucle pour détecter ses variations. Sous Linux, poll() fonctionne de la même manière que select() en endormant passivement la tâche appelante jusqu’à ce qu’une interruption signale un événement susceptible d’intéresser le demandeur.

Voici l’implémentation du même programme en employant poll() plutôt que select().

gpio-poll.c:
#include <fcntl.h>
#include <poll.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>

int main(int argc, char * argv[])
{
    int fd;
    struct pollfd  fds;
    struct timeval tv;
    char buffer[2];

    if (argc != 2) {
        fprintf(stderr, "usage: %s \n", argv[0]);
        exit(EXIT_FAILURE);
    }
    if ((fd = open(argv[1], O_RDONLY)) < 0) {
        perror(argv[1]);
        exit(EXIT_FAILURE);
    }
    while (1) {
        fds.fd = fd;
        fds.events = POLLPRI;
        if (poll(& fds, 1, -1) < 0) {
            perror("poll");
            break;
        }
        gettimeofday(& tv, NULL);
        lseek(fd, 0, SEEK_SET);
        if (read(fd, & buffer, 2) != 2) {
            perror("read");
            break;
        }
        buffer[1] = '\0';
        fprintf(stdout, "[%ld.%06ld]: %s\n", tv.tv_sec, tv.tv_usec, buffer);
    }
    close(fd);
    return EXIT_SUCCESS;
}

Les résultats d’exécution sont exactement les mêmes que ceux du précédent programme.

Conclusion

L’attente passive de changement d’état sur un GPIO est une opération très utile dans de nombreuses applications où une entrée signale un événement (alarme, acquisition d’un capteur, pression sur un bouton, etc.). L’interface sysfs de Linux nous permet d’y accéder de manière simple et portable, même si le temps de prise en charge est un peu plus long que depuis un driver dans le kernel.

Notez que mes exemples ci-dessus sont écrits en C, du fait de ma familiarité avec ce langage, mais qu’ils pourraient parfaitement fonctionner dans d’autres langages. Si vous avez envie de les réécrire, par exemple en Python, n’hésitez pas à m’en faire part, j’en mettrai volontiers une copie ici.

PS : je tiens à remercier la personne d’Hydequip dont je n’ai pas noté le nom et avec qui j’ai parlé de ce thème il y a quelques semaines lors d’un atelier Cap’tronic à Rouen. C’est suite à cette conversation que j’ai eu envie d’approfondir ce sujet.

14 Réponses

  1. texane dit :

    Merci pour l article (et pour les autres aussi d ailleurs :)).

    Je travaille la dessus en ce moment, et j utilise les GPIOs la
    fois pour etre notifie de changements externes, et dans l autre
    sens pour reveiller un microcontrolleur.

    Pour etre complet, j aurais ajoute a ton article l utilisation
    possible de epoll (que tu connais surement :)) comme optimisation
    appreciable: entre autre chose, epoll permet d associer des donnees
    utilisateur aux evenements recus, et ainsi d eviter une recherche
    lors du declenchement d un evenment. Dans mon cas, chacune des GPIOs
    (4 en tout) est liee a une alerte pointee par une structure, pointeur
    que epoll me restitue directement.

    Fabien.

  2. Louis SABIRON dit :

    Bonjour,

    merci pour cet article.

    Suite à votre formation sur Rouen et avec l’indice de la fonction « poll », j’ai trouvé un exemple pour le langage Python:
    http://raspberrypi.stackexchange.com/questions/3440/how-do-i-wait-for-interrupts-in-different-languages

    Je n’ai pas encore pris le temps de mettre cela en pratique (j’ai switcher brutalement sur un autre projet pour l’instant). Je testerai rapidement avec ma carte et je vous enverrai un code d’exemple.

    J’ai également trouver un très beau code d’exemple (en Python toujours) pour la gestion du bus CAN qui lui a été testé avec succès. Idem, je vous enverrai cela rapidement.

    Notez que je m’intéresse maintenant à titre perso au Raspberry pi et je découvre que la communauté aime beaucoup le Python sur ce projet 🙂 . Avec un peu de recherche, je vais vite y trouver mon bonheur.

    Cordialement

    Louis SABIRON

  3. Thomas dit :

    Bonjour et merci pour tous vos articles que je trouve excellents et qui m’aident beaucoup.

    Pouvez vous expliciter le fonctionnement de select ? on lui donne un file descriptor mais vous dites que select est appelé via les interruptions. est ce qu’à chaque interruption (front montant ou descendant) sur les gpio le fichier /sys/class/gpio/gpio138/value change ? comme c’est du sysfs je pensais que ce fichier ne contenait rien et faisait juste un appel en lecture ou écriture aux fonctions gpio

    merci

  4. Franck dit :

    Bonjour,

    Merci pour toutes vos articles, votre sites m’aides beaucoup pour le développement sur mon Raspberry PI.

    J’ai en ma possession un clavier 83 touches et écran 2*40 caractères qui dialogue en I2C l’appuie touche lui est simulé par un horloge de 10ms environ qui lui est branché directement sur le GPIO18. Grâce à vos différents exemples tous fonctionne correctement.

    La seul chose qui me gène c’est qu’avant de lancer mon programme je doit faire les manipulation suivantes :

    sudo su
    cd /sys/class/gpio/
    echo 18 > export
    cd gpio18/
    echo rising > edge
    /root/monProg ./value

    N’est-il pas possible de faire cette ouverture directement dans mon programme?

    Merci.

    • cpb dit :

      Bonjour,
      Il est tout à fait possible de faire la configuration dans le programme avec une séquence du type (il faudrait ajouter les tests d’erreur)

      FILE * fp;
      fp = fopen("/sys/class/gpio/export", "w");
      fprintf(fp, "18\n");
      fclose(fp);
      
      fp = fopen("/sys/class/gpio/gpio18/edge", "w");
      fprintf(fp, "rising\n");
      fclose(fp);
  5. Dnis dit :

    Bonjour,

    J’ai repris un script en python pour compter des impulsions sur le GPIO25.
    la boucle lance un changement d’état sur une lampe et scrute les impulsions du GPIO.
    Sous PUTTY ça fonctionne très bien, mais si je le lance au démarrage dans RC.LOCAL, la lampe change bien d’état mais la fonction d’interruption ne fonctionne pas.

    Avez-vous une idée.

    Merci d’avance.

    Dnis

    • cpb dit :

      Bonjour,
      Je ne suis pas sûr de comprendre complètement. Vous avez une boucle principale qui attend des impulsions sur une entrée GPIO et modifie l’état sur une sortie GPIO ? Les paramètres (dans /sys/class/gpio/gpioXX/) « direction » et « edge » de la GPIO d’entrée sont bien renseignés ?

  6. Dnis dit :

    J’ai créé un programme en python, sur RPI qui a deux fonctions :
    – La fonction principale sous forme de boucle qui provoque le changement d’état d’une lampe.
    – La seconde est un appel à un script d’interruption qui est provoqué par une impulsion sur le GPIO 25 du RPI (my_callback).
    o Interruption par GPIO.add_event_detect(25, GPIO.RISING, callback=my_callback)

    Ce programme fonctionne très bien en le lançant depuis une fenêtre terminal PUTTY.

    Je l’ai integré dans rc.local, pour un lancement au démarrage.
    – Le programme est bien lancer puisque la lampe change d’état.
    – Mais le script my_callback n’est pas lancer lorsqu’une impulsion sur le gpio25 apparait.

    • cpb dit :

      Je ne connais pas la fonction GPIO.add_event_detect() ; je suppose qu’elle appartient à l’API d’une bibliothèque Python spécifique.
      Vous aurez plus de chances de trouver des informations dans la documentation de cette bibliothèque.

      Néanmoins, lorsqu’un programme ou un script fonctionne bien au lancement en ligne de commande et échoue lors du démarrage automatique au boot, il est bon de s’interroger sur les points suivants :

      • différences dans les chemins d’accès (variable PATH) : vérifier que toutes les commandes et scripts invoqués sont dans /bin ou /usr/bin ou sinon indiquer leur chemin complet.
      • localisation : le résultat affiché par une commande peut différer entre l’exécution en ligne de commande (avec la localisation fr_FR:UTF-8 par exemple) et celle par défaut dans le script de boot.
      • systèmes de fichiers : vérifier si les systèmes de fichiers spéciaux (/proc, /sys, /dev, etc.) sont effectivement montés au moment du démarrage du script.
      • permissions : si le démarrage se fait avec une identité particulièrement, vérifier si l’utilisateur a bien les mêmes droits que celui qui lance la commande depuis le terminal
      • dépendances entre sous-systèmes : l’application dépend-elle de fonctionnalités qui ne se trouvent initialisées qu’après le script de démarrage (systèmes de log, etc.)
  7. Bonjour,

    J’ai besoin de détecter des fronts montants sur deux GPIOS via select. Problème a chaque lancement du mon programme, le select s’enclenche toujours la premiere fois sans aucun changement d’état de mon GPIO. Ensuite le select marche parfaitement. Mais le premier appelle merde toujours. Une idée comme ça ?

    Au passage un petit résumé du code utilisé :

    (…)
    FD_ZERO(&fds);
    FD_SET(x, &fds);
    FD_SET(y, &fds);

    if (select(MAX(x, y) + 1, NULL, NULL, &fds, &select_timeout) <= 0)
    goto error;

    if (FD_ISSET(x, &fds))
    {
    lseek(x, 0, SEEK_SET);
    read(x, &buf, 1);
    }
    else if (FD_ISSET(y, &fds))
    {
    lseek(y, 0, SEEK_SET);
    read(y, &buf, 1);
    }
    (…)

    Merci d'avance,
    Arthur.

    • cpb dit :

      L’information « présence d’un front montant » est maintenue dans le noyau tant que la valeur du GPIO n’est pas lue. Je pense que cela signifie simplement qu’un front montant s’est présenté auparavant (avant le démarrage du programme).
      Je pencherais pour une solution consistant à lire systématiquement le descripteur avant le premier select().

      • Ok merci pour la réponse.
        En faites c’est exactement ce que j’ai fais en workaround en me disant c’est dirty mais c’est mieux que rien en attendant de comprendre mon erreur.

        Je vais garder ma solution actuelle alors.

URL de trackback pour cette page