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.
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.
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
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
Bonjour,
J’ai rédigé ma réponse sous forme d’un petit article : comprendre le fonctionnement de select().
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.
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)
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
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 ?
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.
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 :
PATH
) : vérifier que toutes les commandes et scripts invoqués sont dans/bin
ou/usr/bin
ou sinon indiquer leur chemin complet.fr_FR:UTF-8
par exemple) et celle par défaut dans le script de boot./proc
,/sys
,/dev
, etc.) sont effectivement montés au moment du démarrage du script.Merci pour le retour.
je vais approfondir…
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.
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.