Pilotage de GPIO avec l’API Libgpiod (partie 1)

Publié par cpb
Oct 15 2018

J’ai assisté il y a quelques jours, lors de l’édition 2018 des Kernel Recipes à une présentation par Bartosz Golaszewski  de la nouvelle interface des GPIO pour l’espace utilisateur de Linux. J’en avais eu un bref aperçu il y a quelques mois mais je n’avais pas encore pris le temps d’essayer cette API. Cet article est donc une brève présentation et mise en œuvre de ces outils.

Disponible depuis le noyau 4.8, cette API est amenée à remplacer l’accès via /sys/class/gpio qui est dorénavant considéré comme deprecated.

L’accès se fait via le système de fichiers devtmpfs monté sur le répertoire /dev. On y trouve une entrée par contrôleur de GPIO.

Voici un exemple sur une carte Raspberry Pi 3, sur laquelle est installée la distribution Raspbian 2018-06-27.

$ uname -a
Linux raspberrypi 4.14.34-v7+ #1110 SMP Mon Apr 16 15:18:51 BST 2018 armv7l GNU/Linux
$ ls -l /dev/gpiochip*
crw-rw---- 1 root gpio 254, 0 Sep 16 08:34 /dev/gpiochip0
crw-rw---- 1 root gpio 254, 1 Sep 16 08:34 /dev/gpiochip1
crw-rw---- 1 root gpio 254, 2 Sep 16 08:34 /dev/gpiochip2
$

Trois contrôleurs sont donc présents sur cette carte.

Accès depuis la ligne de commandes

Il existe des outils pour interroger, configurer et manipuler les GPIO depuis la ligne de commande. Malheureusement ils ne sont pas encore packagés sur la distribution Raspbian. Qu’à cela ne tienne, nous allons les compiler depuis les sources.

$ git clone https://git.kernel.org/pub/scm/libs/libgpiod/libgpiod
Cloning into 'libgpiod'…
remote: Counting objects: 3909, done.
remote: Total 3909 (delta 0), reused 0 (delta 0)
Receiving objects: 100% (3909/3909), 569.10 KiB | 0 bytes/s, done.
Resolving deltas: 100% (2691/2691), done.
$ cd libgpiod/

$ ls
autogen.sh bindings configure.ac COPYING Doxyfile include libgpiod.pc.in Makefile.am NEWS README src tests
$

Après avoir jeté un œil au fichier README, nous pouvons compiler et installer la bibliothèque Libgpiod et ses utilitaires. Nous devons tout d’abord installer quelques packages nécessaires pour la distribution Raspbian.

$ sudo apt install autoconf autoconf-archive libtool
[…]
$ ./autogen.sh --enable-tools=yes --prefix=/usr
autoreconf: Entering directory `.'
autoreconf: configure.ac: not using Gettext
autoreconf: running: aclocal --force -I m4
autoreconf: configure.ac: tracing
autoreconf: running: libtoolize --copy --force
libtoolize: putting auxiliary files in AC_CONFIG_AUX_DIR, 'autostuff'.
libtoolize: copying file 'autostuff/ltmain.sh'
libtoolize: putting macros in AC_CONFIG_MACRO_DIRS, 'm4'.
libtoolize: copying file 'm4/libtool.m4'
libtoolize: copying file 'm4/ltoptions.m4'
libtoolize: copying file 'm4/ltsugar.m4'
libtoolize: copying file 'm4/ltversion.m4'
libtoolize: copying file 'm4/lt~obsolete.m4'
autoreconf: running: /usr/bin/autoconf --force
autoreconf: running: /usr/bin/autoheader --force
[…]
config.status: creating config.h
config.status: executing depfiles commands
config.status: executing libtool commands

$ make
make all-recursive
make[1]: Entering directory '/home/pi/libgpiod'
Making all in include
make[2]: Entering directory '/home/pi/libgpiod/include'
make[2]: Nothing to be done for 'all'.
make[2]: Leaving directory '/home/pi/libgpiod/include'
Making all in src
make[2]: Entering directory '/home/pi/libgpiod/src'
Making all in lib
make[3]: Entering directory '/home/pi/libgpiod/src/lib'
CC libgpiod_la-core.lo
CC libgpiod_la-ctxless.lo
CC libgpiod_la-helpers.lo
CC libgpiod_la-iter.lo
make[2]: Entering directory '/home/pi/libgpiod'
make[2]: Leaving directory '/home/pi/libgpiod'
make[1]: Leaving directory '/home/pi/libgpiod'
$ sudo make install
Making install in include
make[1]: Entering directory '/home/pi/libgpiod/include'
make[2]: Entering directory '/home/pi/libgpiod/include'
make[2]: Nothing to be done for 'install-exec-am'.
/bin/mkdir -p '/usr/include'
/usr/bin/install -c -m 644 gpiod.h '/usr/include'
make[2]: Leaving directory '/home/pi/libgpiod/include'
make[1]: Leaving directory '/home/pi/libgpiod/include'
Making install in src
make[1]: Entering directory '/home/pi/libgpiod/src'
Making install in lib
make[2]: Entering directory '/home/pi/libgpiod/src/lib'
make[3]: Entering directory '/home/pi/libgpiod/src/lib'
/bin/mkdir -p '/usr/lib'
[…]
/bin/mkdir -p '/usr/lib/pkgconfig'
/usr/bin/install -c -m 644 libgpiod.pc '/usr/lib/pkgconfig'
make[2]: Leaving directory '/home/pi/libgpiod'
make[1]: Leaving directory '/home/pi/libgpiod'
$

Nous pouvons vérifier tout de suite la liste des nouveaux outils installés en tapant gpio suivi de deux tabulations pour que le shell propose les complétions disponibles.

$ gpio [TAB] [TAB]
gpio  gpiodetect  gpiofind  gpioget  gpioinfo  gpiomon  gpioset     
$

Hormis la commande gpio elle-même qui est associée à la bibliothèque WiringPi spécifique au Raspberry Pi, toutes les commande disponibles viennent d’être ajoutées. Nous allons les examiner l’une après l’autre.

gpiodetect

La commande gpiodetect affiche la liste des contrôleurs GPIO détectés, et pour chacun d’entre-eux le nom du driver associé et le nombre de lignes GPIO gérées.

$ gpiodetect
gpiochip0 [pinctrl-bcm2835] (54 lines)
gpiochip1 [brcmexp-gpio] (8 lines)
gpiochip2 [brcmvirt-gpio] (2 lines)
$

Il s’agit bien sûr de la même liste que celle de /dev/gpio*. Ces informations étaient déjà accessibles dans l’arborescence /sys/ ainsi :

$ cd /sys/class/gpio/
$ for chip in gpiochip*; do echo $chip $(cat $chip/label) $(cat $chip/ngpio); done
gpiochip0 pinctrl-bcm2835 54
gpiochip100 brcmvirt-gpio 2
gpiochip128 brcmexp-gpio 8

$

gpioinfo

La commande gpioinfo présente l’état des lignes GPIO du contrôleur demandé.

$ gpioinfo gpiochip2
gpiochip2 - 2 lines:
line 0: unnamed "led0" output active-high [used]
line 1: unnamed unused input active-high
$

Si on ne précise aucun contrôleur sur sa ligne de commande, gpioinfo affiche les informations pour tous les contrôleurs.

$ gpioinfo
gpiochip0 - 54 lines:
line 0: unnamed unused input active-high
line 1: unnamed unused input active-high
line 2: unnamed unused input active-high
line 3: unnamed unused input active-high
line 4: unnamed unused input active-high
line 5: unnamed unused input active-high
line 6: unnamed unused input active-high
line 7: unnamed unused input active-high
line 8: unnamed unused input active-high
line 9: unnamed unused input active-high
line 10: unnamed unused input active-high
line 11: unnamed unused input active-high
line 12: unnamed unused input active-high
line 13: unnamed unused input active-high
line 14: unnamed unused input active-high
line 15: unnamed unused input active-high
line 16: unnamed unused input active-high
line 17: unnamed unused input active-high
line 18: unnamed unused input active-high
line 19: unnamed unused input active-high
line 20: unnamed unused input active-high
line 21: unnamed unused input active-high
line 22: unnamed unused input active-high
line 23: unnamed unused input active-high
line 24: unnamed unused input active-high
line 25: unnamed unused input active-high
line 26: unnamed unused input active-high
line 27: unnamed unused input active-high
line 28: unnamed unused input active-high
line 29: unnamed unused input active-high
line 30: unnamed unused input active-high
line 31: unnamed unused input active-high
line 32: unnamed unused input active-high
line 33: unnamed unused input active-high
line 34: unnamed unused input active-high
line 35: unnamed unused input active-high
line 36: unnamed unused input active-high
line 37: unnamed unused input active-high
line 38: unnamed unused input active-high
line 39: unnamed unused input active-high
line 40: unnamed unused input active-high
line 41: unnamed unused input active-high
line 42: unnamed unused input active-high
line 43: unnamed unused input active-high
line 44: unnamed unused input active-high
line 45: unnamed unused input active-high
line 46: unnamed unused input active-high
line 47: unnamed unused output active-high
line 48: unnamed unused input active-high
line 49: unnamed unused input active-high
line 50: unnamed unused input active-high
line 51: unnamed unused input active-high
line 52: unnamed unused input active-high
line 53: unnamed unused input active-high
gpiochip1 - 8 lines:
line 0: unnamed unused output active-high
line 1: unnamed unused output active-high
line 2: unnamed unused output active-high
line 3: unnamed unused output active-high
line 4: unnamed unused input active-high
line 5: unnamed unused output active-high
line 6: unnamed unused output active-high
line 7: unnamed "led1" input active-high [used]
gpiochip2 - 2 lines:
line 0: unnamed "led0" output active-high [used]
line 1: unnamed unused input active-high
$


Les informations affichées sont les suivantes.

  • Offset : le numéro de la ligne GPIO pour le contrôleur. Dans la documentation de la plupart des Systems-On-Chip, les GPIO sont identifiées par un couple contrôleur:offset. Par exemple IO03:05 pour indiquer la sixième ligne (la numérotation commence à zéro) du contrôleur 3. C’est la valeur que l’on obtient ici.
    Pour le Raspberry Pi, nous avons eu l’habitude jusqu’ici d’utiliser directement les offsets du premier contrôleur gpiochip0. Par exemple, la broche 16 du connecteur H1 du Raspberry Pi 3 est accessible par la GPIO gpiochip0:23.
  • Name le nom attribué à cette ligne GPIO. Ce nom est configurable dans le device tree. Nous en reparlerons plus loin.
  • Consumer : le nom du driver ou du sous-système qui a réservé l’accès à cette ligne GPIO. Nous voyons par exemple “led1” et “led0” pour les lignes gpiochip1:7 et gpiochip2:0.
    Lorsqu’une ligne est exportée dans l’arborescence /sys, la réservation est faite au nom de sysfs.
  • Direction : input ou output en fonction du sens d’utilisation de la ligne GPIO. Au boot les Systems-On-Chip configurent la plupart de leurs lignes en entrées.
  • Active state : active-high ou active-low selon le type d’activation de la ligne GPIO.
  • flags: une série d’attributs peuvent être indiquées entre crochets :
    • used : la ligne GPIO est en cours d’utilisation par un driver ou sous-système du noyau.
    • open-drain (collecteur ouvert) : la sortie de la GPIO est assurée par un transistor simplement connecté à la masse. Écrire un 1 sur la ligne GPIO la reliera à la masse. Écrire un 0  la laissera flottante. Il faut généralement ajouter une résistance de pull-up. Ceci est principalement utile lorsque plusieurs sorties doivent être reliées ensemble, comme dans le cas d’un bus.
    • open-source (emetteur ouvert): configuration inverse de la précédente, le collecteur du transistor de sortie est relié à l’alimentation (+3.3V par exemple) et l’émetteur est flottant. On ajoute généralement une résistance de pull-down.

gpioget

La commande gpioget sert à lire l’état d’une broche GPIO. Nous allons l’utiliser pour lire la valeur d’entrée de la broche numéro 16, qui correspond à la ligne 23 du contrôleur gpiochip0.

Lignes GPIO non connectées
$ gpioget gpiochip0 23
0
$

L’entrée est libre et les broches du connecteur du Raspberry Pi disposent d’une résistance de pull-down. Nous lisons donc une valeur nulle.

Relions à présent cette entrée et la broche numéro 1 (+3.3V).

Ligne GPIO 16 reliée à Vcc
$ gpioget gpiochip0 23
1

$

Nous pouvons donc lire facilement la valeur d’entrée. Notez qu’il est possible de lire plusieurs entrées en une seule fois. Ce qui est plus simple qu’avec l’interface /sys/class/gpio.

$ gpioget gpiochip0 23 22 21 20
  1 0 0 0
$

gpioset

Tout naturellement, la commande gpioset sert à fixer une valeur de sortie sur une ligne GPIO. Toutefois son comportement est un peu surprenant a priori.

L’un des principaux reproches que l’on faisait à l’API reposant sur sysfs était que l’accès à une GPIO n’était pas associé à un processus. Une fois qu’une valeur était inscrite dans /sys/class/gpio/gpio23/value elle restait inscrite sur la broche correspondante (16 en l’occurrence) même si l’application qui gérait les entrées-sorties était finie depuis longtemps.

L’idée avec la nouvelle API est qu’à la fin du processus ayant ouvert une GPIO (fin volontaire sur exit() ou crash involontaire par signal), la ligne GPIO soit automatiquement libérée. Suivant la configuration du contrôleurs GPIO elle pourra alors reprendre immédiatement un état de haute impédance par mesure de précaution.

La commande gpioset propose donc plusieurs modes de fonctionnement :

  • exit : comportement par défaut, où sitôt la sortie configurée la commande se termine et libère la GPIO. Il s’agit donc d’un mode utilisé pour générer des impulsions sur une ligne de sortie.
  • wait : après activation de la GPIO de sortie, la commande attend un retour chariot sur son entrée standard. Ce mode est utile pour un pilotage interactif depuis la ligne de commande.
  • time : la commande gpioset attend avant de se terminer une durée que l’on précise avec ses options -s suivie d’une durée en seconde ou -u suvie d’une durée en microsecondes. Il est possible d’ajouter une option -b (pour background) afin que l’attente se fasse à l’arrière-plan.
  • signal : une fois la GPIO configurée la commande attend de recevoir un signal SIGINT (que le terminal envoie lors d’une pression sur Contrôle-C) ou SIGTERM (signal envoyé par défaut par la commande kill). Comme pour le mode time, l’ajout de l’option -b permet de laisser la commande en attente à l’arrière-plan. C’est le mode le plus utile pour les scripts.

  

Je relie la broche 16 du Raspberry Pi (ligne 23 du contrôleur gpiochip0) et la broche 15 (ligne 22 du même contrôleur). Je vais écrire un signal sur la sortie 22, et profiter du délai de cinq secondes où la commande sera à l’arrière-plan pour consulter l’entrée 23.

Ligne GPIO 16 reliée à la ligne 15
$ gpioget gpiochip0 23
0
$ gpioset -m time -s 5 -b gpiochip0 22=1
$ gpioget gpiochip0 23
1
(attente cinq secondes)
$ gpioget gpiochip0 23
0
$

Dans ce second essai, je vais laisser la commande en attente d’un signal. Pour connaître son PID et lui envoyer ultérieurement un signal, je vais consulter la variable $! du shell qui contient le PID du dernier processus envoyé à l’arrière-plan par le shell. C’est pour cette raison que je vais utiliser le & du shell et non pas l’option -b de gpioset.

$ gpioset -m signal gpiochip0 22=1 &

$ PID=$!
$ gpioget gpiochip0 23
1

$ kill $PID

$ gpioget gpiochip0 23
0

$

J’ai légèrement édité la capture d’écran pour supprimer les messages parasites du shell lorsqu’il envoie un processus à l’arrière-plan ou que ce dernier se termine.

On notera également que gpioset permet de configurer la sortie en mode active-low avec son option -l ainsi l’écriture d’un 0 activera la sortie (qui passera à +3.3V) et un 1 la ramènera à la masse.

gpiomon

La commande gpiomon permet de réaliser un travail qui n’était pas possible directement depuis la ligne de commande avec l’API reposant sur sysfs mais nécessitait d’écrire un programme spécifique (comme je l’avais fait dans l’article “Attente passive sur une GPIO“).

Il s’agit de monitorer les événements se passant sur une ou plusieurs broches d’entrée, d’afficher ou d’attendre l’arrivée d’un front montant ou descendant. Par défaut, gpiomon affiche les événements au fur et à mesure qu’ils se produisent (avec un format configurable). Il est possible d’utiliser son option -n suivi d’un nombre d’occurrences pour demander que la commande se termine une fois le nombre d’événements survenus.

Par défaut, les événements sont des changements d’état de la ligne d’entrée. Il est possible d’utiliser l’option -r (pour rising) afin de ne considérer que les transitions montantes (0 vers 1) ou l’option -f (pour falling) pour ne conserver que les transitions descendantes.

Dans l’exemple suivant, je vais invoquer gpioset pour qu’il active la sortie sur la ligne GPIO 22 (broche 15) pendant cinq secondes puis se termine. La broche 15 est reliée à la broche 16 comme précédemment. J’invoque gpiomon pour qu’il attende la transition descendante quand la première commande se terminera.

$ gpioset -m time -s 5 -b  gpiochip0 22=1
$ gpiomon -f -n 1 gpiochip0 23 (Cinq secondes s'écoulent) event: FALLING EDGE offset: 23 timestamp: [1538116251.893018202]
$

Dans ce second exemple, je laisse la commande gpiomon afficher les événements observés sur la ligne GPIO 23 tandis que, depuis un autre terminal, je fais basculer la ligne GPIO 22 reliée à la précédente.

[1]$ gpiomon gpiochip0 23
     [2]$ for i in $(seq 1 10); do gpioset -m time -s 1 gpiochip0 22=1; done
event: RISING EDGE offset: 23 timestamp: [1538119622.397979178]
event: FALLING EDGE offset: 23 timestamp: [1538119623.399084866]
event: RISING EDGE offset: 23 timestamp: [1538119623.406256964]
event: FALLING EDGE offset: 23 timestamp: [1538119624.407333330]
event: RISING EDGE offset: 23 timestamp: [1538119624.414514282]
event: FALLING EDGE offset: 23 timestamp: [1538119625.415573773]
event: RISING EDGE offset: 23 timestamp: [1538119625.422415612]
event: FALLING EDGE offset: 23 timestamp: [1538119626.423968121]
event: RISING EDGE offset: 23 timestamp: [1538119626.431198291]
event: FALLING EDGE offset: 23 timestamp: [1538119627.432301011]
event: RISING EDGE offset: 23 timestamp: [1538119627.439238110]
event: FALLING EDGE offset: 23 timestamp: [1538119628.440283122]
event: RISING EDGE offset: 23 timestamp: [1538119628.447077045]
event: FALLING EDGE offset: 23 timestamp: [1538119629.448084557]
event: RISING EDGE offset: 23 timestamp: [1538119629.455423946]
event: FALLING EDGE offset: 23 timestamp: [1538119630.456473697]
event: RISING EDGE offset: 23 timestamp: [1538119630.463395900]
event: FALLING EDGE offset: 23 timestamp: [1538119631.464926222]
event: RISING EDGE offset: 23 timestamp: [1538119631.472085872]
event: FALLING EDGE offset: 23 timestamp: [1538119632.473167915]
(Contrôle-C)
$

gpiofind

Dans l’exemple d’utilisation de la commande gpioinfo, nous avons pu voir que la colonne des noms des lignes GPIO est remplie de unnamed. Ces noms doivent être attribués dans le device tree mais il n’y en a pas dans celui livré avec la distribution Raspbian. Je me suis amusé à remplir les noms des lignes GPIO qui apparaissent sur le connecteur H1. Après avoir recompilé mon device tree et avoir rebooté, j’obtiens la liste suivante.

$ gpioinfo
gpiochip0 - 54 lines:
line 0: unnamed unused input active-high
line 1: unnamed unused input active-high
line 2: "PIN-#03" unused input active-high
line 3: "PIN-#05" unused input active-high
line 4: "PIN-#07" unused input active-high
line 5: "PIN-#29" unused input active-high
line 6: "PIN-#31" unused input active-high
line 7: "PIN-#26" unused input active-high
line 8: "PIN-#24" unused input active-high
line 9: "PIN-#21" unused input active-high
line 10: "PIN-#19" unused input active-high
line 11: "PIN-#23" unused input active-high
line 12: "PIN-#32" unused input active-high
line 13: "PIN-#33" unused input active-high
line 14: "PIN-#08" unused input active-high
line 15: "PIN-#10" unused input active-high
line 16: "PIN-#36" unused input active-high
line 17: "PIN-#11" unused input active-high
line 18: "PIN-#12" unused input active-high
line 19: "PIN-#35" unused input active-high
line 20: "PIN-#38" unused input active-high
line 21: "PIN-#40" unused input active-high
line 22: "PIN-#15" unused input active-high
line 23: "PIN-#16" unused input active-high
line 24: "PIN-#18" unused input active-high
line 25: "PIN-#22" unused input active-high
line 26: "PIN-#37" unused input active-high
line 27: "PIN-#13" unused input active-high
line 28: unnamed unused input active-high
line 29: unnamed unused input active-high
line 30: unnamed unused input active-high
[…]

En réalité un bug dans le kernel que j’utilisais empêchait d’avoir une liste partielle de lignes GPIO nommées. La correction est dans la branche linux-next et devrait apparaître dans le noyau 4.20 (ou 5.0 si la logique de changement de numéro majeur est la même que pour le passage de la branche 3.x à la branche 4.x).

J’ai laissé les broches non apparentes sur le connecteur H1 non nommées. La commande gpiofind permet de rechercher le numéro de contrôleur et le numéro de ligne GPIO en indiquant son nom.

$ gpiofind PIN-#16
gpiochip0 23
$ gpiofind PIN-#15
gpiochip0 22
$ gpiofind PIN-#00
$

Comme on le voit, la recherche d’un nom inexistant échoue sans afficher d’erreur. Néanmoins le code de retour de la commande est zéro si elle réussit à trouver une ligne GPIO et 1 sinon.

$ gpiofind PIN-#16
gpiochip0 23
$ echo $?
0
$ gpiofind PIN-#00
$ echo $?
1
$

Notez que l’on peut imbriquer cette commande dans l’invocation de gpioget ou gpioset, en prenant la précaution de vérifier sa valeur de retour.

$ LINE=$(gpiofind "PIN-#16")

$ if [ $? -eq 0 ]; then VALUE=$(gpioget $LINE); else VALUE=-1; fi

$ echo $VALUE
0

$

La même opération avec un nom inexistant donne :

$ LINE=$(gpiofind "PIN-#00")
$ if [ $? -eq 0 ]; then VALUE=$(gpioget $LINE); else VALUE=-1; fi>
$ echo $VALUE
-1
$

Conclusion

Nous voyons dans ce premier article que ces nouvelles commandes permettent de piloter efficacement et aisément les GPIO depuis le shell. Dans le prochain article je présenterai l’accès depuis un programme C.

URL de trackback pour cette page