Découvrez et utilisez les broches d'entrées-sorties du Raspberry Pi

GNU/Linux Magazine HS n° 075 | novembre 2014 | Yann Guidon
  • Actuellement 0 sur 5 étoiles
  • 1
  • 2
  • 3
  • 4
  • 5
Un des facteurs du succès du Raspberry Pi est son port d'entrées-sorties générales. Pour preuve, le nouveau modèle B+ fournit encore plus de broches GPIO ! On peut y accéder au moyen de quasiment tous les langages : nous allons voir ici comment le faire en Bash et en C, puis nous étendrons le nombre de signaux en sortie.

Le Raspberry Pi n'est pas juste un nano-ordinateur économique tournant sous Linux. Ses broches d'entrées-sorties l'ouvrent au monde des interfaces et c'est donc un microcontrôleur de luxe ! Mais ce n'est pas non plus un microcontrôleur comme sur les platines Arduino.

Tout d'abord, les caractéristiques électriques sont différentes : les tensions vont de 0 à 3,3V au lieu de 5V et on peut atteindre des vitesses bien plus élevées : les broches SPI peuvent monter à plusieurs dizaines de mégahertz ! Il faut donc soigner les circuits si on veut exploiter à fond le potentiel de la puce Broadcom, mais si vous désirez juste allumer une LED ou lire un bouton-poussoir, il n'y a pas de différence avec une autre carte.

1. Les connecteurs GPIO du Raspberry Pi

Il existe plusieurs révisions du Raspberry Pi. Le modèle A et le modèle B utilisent le même circuit imprimé, qui a évolué en trois ans, avant la refonte majeure du modèle B+. Dans cet article, nous allons nous concentrer uniquement sur les 26 broches de la figure 1, car elles seront utilisées quel que soit le modèle.

Fig. 1 : Le connecteur GPIO d'un Raspeberry Pi, révision 2013-2014

Par la suite, nous utiliserons uniquement la numérotation logique des GPIO. On trouve des projets qui numérotent les signaux en fonction des broches de la puce Broadcom, ou du connecteur P1. En pratique, la numérotation logique facilite l'écriture des programmes et la partie électronique se débrouille avec un amas de fils volants pour remettre les signaux dans l'ordre.

L'assignation de certaines broches a évolué : les premières révisions fournissaient les GPIO n°0, 1 et 21, qui ont été remplacées par les GPIO n°2, 3 et 27. Ces cartes de première génération sont maintenant rares et nous supposerons que vous disposez d'une révision récente.

Sur la figure 1, on aperçoit un groupe de huit pastilles à gauche : c'est le connecteur P5, destiné à être éventuellement soudé par l'utilisateur, sur l'autre face du circuit. Si vous trouvez ces pastilles sur votre carte, cela signifie aussi que vous avez une révision récente, qui dispose des GPIO n°2, 3 et 27. Le connecteur P5 fournit 4 signaux GPIO et des alimentations, mais il est légèrement décalé par rapport au connecteur principal, ce qui l'empêche d'être utilisé avec des plaques standards au pas de 2,54 mm.

Le nouveau modèle B+ a supprimé ce port, bricolable seulement par des experts, donc impopulaire. Heureusement, les concepteurs ont eu la bonne idée d'allonger le connecteur P1, qui passe à 40 broches et qui conserve un format très pratique. Les GPIO n°28 à 31 ne sont plus disponibles, car P5 a disparu, mais on gagne au change ! L'ordre des GPIO est toujours tiré par les cheveux (certainement pour des raisons de routage des pistes du circuit imprimé), mais leurs numéros sont maintenant consécutifs. Le modèle B+ permet désormais d'accéder à toutes les entrées-sorties de 2 à 27. Il n'y a plus de « trous » et l'assignation des broches est donc beaucoup plus facile. Le routage d'une carte fille reste un peu compliqué, mais le logiciel peut être moins complexe, donc plus efficace et rapide.

Dans cet article, nous nous intéresserons seulement à la fonction GPIO des broches, c'est-à-dire « General Purpose Input-Output », donc entrée-sortie généraliste. Cela signifie que l'utilisateur peut contrôler l'état de la broche et lire un signal connecté dessus. La plupart des broches de la puce BCM2835 fournissent aussi plusieurs fonctions alternatives, dont les plus intéressantes sont disponibles sur le connecteur P1. Les plus populaires sont le port série, le port SPI et le port I²C ; on trouve aussi une sortie PWM et I2S (complété par feu P5). De plus, chaque broche peut servir de source d'interruption.

Commençons par allumer une simple LED. Justement, Linux allume la LED ACT au moyen d'une broche GPIO, mais comment fait-il ?

Fig. 3 : Résumé du connecteur P1 (dessin compilé par Christophe Blaess)

2. L'interface GPIO de Linux

Quand il s'agit juste de bricoler un petit montage électronique, lent de surcroît, un langage interprété est le plus pratique à mettre en œuvre. Et si vous n'êtes pas encore converti au Python, voici comment lire et écrire sur les broches en ligne de commandes. C'est un peu lent, mais c'est très utile et Bash est déjà installé sur tous les systèmes GNU/Linux !

L'interpréteur de ligne de commandes Bash n'est pas réputé pour sa vitesse, d'autant plus qu'il doit passer par des appels système et des fichiers virtuels pour effectuer la moindre opération. Pourtant, cela suffit largement si vous voulez juste allumer une LED pour informer l'utilisateur d'une condition du système (réception d'un e-mail ou charge de processeur trop élevée), ou lire des interfaces (bouton-poussoir, capteur physique, ou communiquer avec un autre montage). Dans mon cas, cela me permet de manipuler les broches interactivement, en les connectant à un montage dont je veux m'assurer du fonctionnement, avant de commencer à coder en C.

2.1 L'interface GPIO de Linux

La clé de cette partie, c'est l'accès aux broches GPIO au travers des fichiers virtuels de Linux. La distribution standard Raspbian fournit un module du noyau, qui ajoute un répertoire contenant des fichiers spéciaux. Ceux-ci configurent et contrôlent les broches au moyen de simples caractères, ce qui est très pratique en ligne de commandes ou dans un script.

2.1.1 Configuration

Tout d'abord, n'oublions pas que les GPIO ne sont accessibles qu'à partir du compte root. Toutes les commandes doivent être précédées par su. Sinon, vous pouvez activer le bit setuid d'un script contenant ces commandes, en plus de l'attribut d'exécution, avec chmod. Mais pour commencer, le plus simple reste :

pi@pi:~$ sudo su

Ensuite, allons faire un tour du côté de /sys/class/gpio :

root@pi:/home/pi# cd /sys/class/gpio/

root@pi:/sys/class/gpio# ls -al

total 0

drwxr-xr-x 2 root root 0 juil. 31 13:35 .

drwxr-xr-x 37 root root 0 juil. 31 13:35 ..

--w------- 1 root root 4096 juil. 31 13:35 export

lrwxrwxrwx 1 root root 0 juil. 31 13:35 gpiochip0 -> ../../devices/virtual/gpio/gpiochip0

--w------- 1 root root 4096 juil. 31 13:35 unexport

Le fichier export (en écriture seule) permet d'ajouter des fichiers et répertoires virtuels correspondant à des broches du processeur central. Quand on écrit un nombre décimal valide dedans, un nouveau répertoire va apparaître. Par exemple, si on s'intéresse à la broche GPIO4 :

root@pi:/sys/class/gpio# echo 4 > export

root@pi:/sys/class/gpio# ls -al

total 0

drwxr-xr-x 2 root root 0 juil. 31 13:35 .

drwxr-xr-x 37 root root 0 juil. 31 13:35 ..

--w------- 1 root root 4096 juil. 31 13:43 export

lrwxrwxrwx 1 root root 0 juil. 31 13:43 gpio4 -> ../../devices/virtual/gpio/gpio4

lrwxrwxrwx 1 root root 0 juil. 31 13:35 gpiochip0 -> ../../devices/virtual/gpio/gpiochip0

--w------- 1 root root 4096 juil. 31 13:35 unexport

L'écriture du nombre 4 dans le fichier unexport, comme vous le devinez, effacera le nouveau répertoire.

Le contenu de ces répertoires est très intéressant :

root@pi:/sys/class/gpio# ls -al gpio4/

total 0

drwxr-xr-x 3 root root 0 juil. 31 13:43 .

drwxr-xr-x 4 root root 0 juil. 31 13:35 ..

-rw-r--r-- 1 root root 4096 juil. 31 13:49 active_low

-rw-r--r-- 1 root root 4096 juil. 31 13:49 direction

-rw-r--r-- 1 root root 4096 juil. 31 13:49 edge

drwxr-xr-x 2 root root 0 juil. 31 13:49 power

lrwxrwxrwx 1 root root 0 juil. 31 13:43 subsystem -> ../../../../class/gpio

-rw-r--r-- 1 root root 4096 juil. 31 13:43 uevent

-rw-r--r-- 1 root root 4096 juil. 31 13:49 value

On voit des fichiers qui servent à la gestion des interruptions (active_low, edge), mais celui qui nous intéresse ici est direction. Comme son nom l'indique, ce qu'on y écrira déterminera si la broche est une entrée ou une sortie.

2.1.2 Lecture

La broche est en lecture par défaut :

root@pi:/sys/class/gpio# cat gpio4/direction

in

On peut toujours forcer la direction, cela ne provoque pas d'erreur :

root@pi:/sys/class/gpio# echo in > gpio4/direction

L'état de la broche est lu dans le fichier virtuel value :

root@pi:/sys/class/gpio# cat gpio4/value

0

Et c'est tout. Qui a dit que ça devait être compliqué ?

2.1.3 Écriture

Corollairement, avant de changer l'état d'une broche, nous devons d'abord indiquer qu'elle doit être en sortie, avec le mot-clé out :

root@pi:/sys/class/gpio# echo out > gpio4/direction

Et comme vous l'aurez peut-être deviné, la valeur de la broche est aussi contrôlée en écrivant dans le fichier value :

root@pi:/sys/class/gpio# echo 0 > gpio4/value

root@pi:/sys/class/gpio# cat gpio4/value

0

root@pi:/sys/class/gpio# echo 1 > gpio4/value

root@pi:/sys/class/gpio# cat gpio4/value

1

Si vous avez connecté une LED dans le bon sens sur GPIO4 (broche 7 du connecteur P1), elle devrait s'allumer.

Ça y est, vous savez utiliser les entrées-sorties !

2.2 Des scripts

Pour réduire le nombre de caractères à écrire, donc le nombre d'erreurs, ces commandes sont souvent intégrées dans de petits scripts. Quelques vérifications sont ajoutées, mais on laisse le noyau détecter si le numéro du port est correct.

La première étape consiste à s'assurer que le répertoire de la broche existe bien. Une expression conditionnelle le détecte, et s'il n'est pas trouvé, il est créé :

[ -d /sys/class/gpio/gpio$1 ] ||

echo $1 > /sys/class/gpio/export

- -d retourne une valeur vraie si le répertoire existe ;

- || est l'expression OU : si la première partie est fausse, alors la deuxième partie est exécutée.

À ce point, si l'argument est valide, le répertoire de la broche doit exister. Si l'argument est absent, non numérique ou ne correspond pas à une broche accessible, le noyau ne va pas le créer. C'est ce que nous vérifions dans la deuxième étape : peut-on écrire dans le fichier direction ?

On vérifie cela avec -w. Ce test assure non seulement que le répertoire existe, mais aussi que l'utilisateur a le droit d'y accéder (car seul root le peut). En cas de succès, le bloc de code suivant (délimité par des accolades) est exécuté après l'expression ET, écrite avec &&.

Le premier script GPIO_in.sh lit l'état de la broche et reprend tous ces éléments :

#!/bin/bash

[ -d /sys/class/gpio/gpio$1 ] ||

echo $1 > /sys/class/gpio/export

-w /sys/class/gpio/gpio$1/direction ] && {

echo in > /sys/class/gpio/gpio$1/direction

echo -n "GPIO$1 ="; cat /sys/class/gpio/gpio$1/value

}

GPIO_on.sh, comme son nom l'indique, met la broche en argument à 1. Il commence de la même manière, mais change l'entrée en sortie avec le mot-clé out :

#!/bin/bash

[ -d /sys/class/gpio/gpio$1 ] ||

echo $1 > /sys/class/gpio/export

[ -w /sys/class/gpio/gpio$1/direction ] && {

echo out > /sys/class/gpio/gpio$1/direction

echo 1 > /sys/class/gpio/gpio$1/value

echo "GPIO$1 à 1"

}

GPIO_off.sh met la broche à zéro et ne diffère que par la valeur envoyée sur le port :

#!/bin/bash

[ -d /sys/class/gpio/gpio$1 ] ||

echo $1 > /sys/class/gpio/export

[ -w /sys/class/gpio/gpio$1/direction ] && {

echo out > /sys/class/gpio/gpio$1/direction

echo  0 > /sys/class/gpio/gpio$1/value

echo "GPIO$1 à 0"

}

N'oubliez pas de rendre ces fichiers exécutables, et même par les utilisateurs normaux (avec le bit setuid) :

root@pi~# chmod +xs GPIO_*

Grâce à ces quelques petits scripts, les GPIO ne devraient plus vous faire peur. Ce type d'accès est d'ailleurs proposé sur d'autres plateformes que le Pi, car c'est une fonctionnalité standard du noyau Linux [1].

2.3 Une petite application simple

Puisque le module GPIO du noyau nous abstrait des détails de bas niveau, la complexité de codage dépend du langage choisi et Bash n'est pas un exemple de clarté. Pourtant, il permet de réaliser facilement beaucoup de choses, sans rien installer d'autre, et c'est encore plus puissant quand on le combine avec d'autres logiciels en ligne de commandes. Il faut juste ne pas avoir peur de lire la page du manuel.

Dans cet exemple, nous allons utiliser sox, le « couteau suisse de la manipulation sonore », pour jouer un son. Notre script lira un fichier tant qu'une broche sera à un certain état, ce qui est la base de nombreux montages comme des installations interactives. La latence est trop grande pour une utilisation musicale, mais on peut déjà beaucoup s'amuser.

Commençons par installer sox :

root@pi# apt-get install sox

sox utilise l'interface OSS (Open Sound System) qui n'est pas installée par défaut. Ce détail est réglé avec la commande suivante :

root@pi# modprobe snd-pcm-oss

Ensuite, vous devez sélectionner le périphérique qui restituera le son. Pour la prise casque (Jack 3,5 mm), la commande est la suivante :

root@pi# sudo amixer cset numid=3 1

Le son peut aussi être envoyé sur la sortie HDMI, si vous disposez d'un convertisseur adapté ou d'une télévision :

root@pi# sudo amixer cset numid=3 2

Il est aussi possible d'utiliser une carte son connectée à l'un des ports USB :

root@pi# sudo amixer cset numid=3 3

Maintenant, vous pouvez lire un fichier, comme les sons fournis par ALSA pour tester les haut-parleurs :

root@pi# play /usr/share/sounds/alsa/Front_Center.wav

Il ne reste plus qu'à attendre qu'une des broches GPIO soit à l'état désiré. Une boucle en Bash fait l'affaire :

#!/bin/bash

 

[ -d /sys/class/gpio/gpio$1 ] ||

echo $1 > /sys/class/gpio/export
echo in > /sys/class/gpio/gpio$1/direction

# boucle infinie

while true

do

# attend que la broche $1 passe à 1 :
 until /bin/grep 1 /sys/class/gpio/gpio$1/value >> /dev/null

do

/bin/sleep .1 # libère le CPU

done
 /usr/local/bin/play /usr/share/sounds/alsa/Front_Center.wav

done

Dans cette version, le fichier est lu tant que la broche reste à 1. Mais aussi, la lecture continue lorsque la broche retourne à 0 avant la fin du fichier. C'est un fonctionnement de type monostable non réarmable : la lecture ne repart pas du début lorsqu'on rappuie en plein milieu.

La version suivante lance play en tâche de fond au moyen du caractère & : on peut ensuite l'interrompre avec la commande killall pour arrêter la lecture lorsque la broche change. On détecte cela en répliquant la boucle d'attente, mais avec une condition inverse :

#!/bin/bash

 

echo $1 > /sys/class/gpio/export
echo in > /sys/class/gpio/gpio$1/direction

# boucle infinie

while true

do

# attend que la broche $1 passe à 1 :
 until /bin/grep 1 /sys/class/gpio/gpio$1/value >> /dev/null

do

/bin/sleep .1 # libère le CPU

done
 # lance la lecture en tâche de fond

/usr/local/bin/play /usr/share/sounds/alsa/Front_Center.wav &

# attend que la broche $1 retourne à 0 :
 until /bin/grep   /sys/class/gpio/gpio$1/value >> /dev/null

do

/bin/sleep .1

done
 # interrompt la lecture (si elle n'est pas terminée)
 /usr/bin/killall play

done

La lecture n'est pas interrompue immédiatement pour deux raisons. D'une part, l'attente d'un dixième de seconde ajoute une certaine incertitude (Christophe Blaess présente plus loin une méthode plus efficace qui exploite les interruptions). D'autre part, les tampons de données sonores mettent approximativement autant de temps à se vider. Heureusement, il existe de nombreuses situations où cela ne dérange pas, autrement il faudrait faire appel à des systèmes bien plus complexes.

La situation se complique si vous voulez lire plusieurs sons, car on ne peut démarrer la lecture suivante que si la précédente s'est bien terminée. En d'autres termes, il faut savoir si play s'est bien arrêté, ce qui est un peu plus compliqué que s'il était lancé en tâche d'avant-plan (sans &). Bash nous fournit une solution avec la commande jobs qui liste les programmes lancés en arrière-plan. Cette commande retourne une chaîne vide si le programme s'est terminé, que ce soit tout seul ou bien forcé par kill. On peut donc scruter la broche GPIO simultanément et tester des conditions plus complexes.

Il serait aussi bienvenu de ne plus faire appel à grep pour comparer la valeur de la broche. Encore une fois, Bash le permet avec une syntaxe plus obscure : $(< fichier ) lit le fichier indiqué et met son contenu dans une variable que Bash peut comparer comme une chaîne de caractères normale, au moyen de l'opérateur == entouré des délimiteurs [[ et ]]. Cela consomme moins de ressources, puisque Bash ne lance plus de commandes externes. Tout cela est condensé dans la fonction playwhile() :

#!/bin/bash

SON_ON=/usr/share/sounds/alsa/Front_Center.wav

SON_OFF=/usr/share/sounds/alsa/Front_Left.wav

 

[ -d /sys/class/gpio/gpio7 ] ||

echo 7 > /sys/class/gpio/export

echo in > /sys/class/gpio/gpio7/direction &&

 

function playwhile() {

# $1 : état à tester

# $2 : fichier à lire
 # ne commence la lecture que si l'état est différent :

[[ $1 == "$(< /sys/class/gpio/gpio7/value)" ]] || /usr/local/bin/play -q $2 &

 # attend la fin de la lecture

while [[ "$(jobs)" ]]

do

jobs > /dev/null # le script ne fonctionne pas sans cette ligne redondante, je ne sais pas pourquoi.

[[ $1 == "$(< /sys/class/gpio/gpio7/value)" ]] && /usr/bin/killall /usr/local/bin/play >> /dev/null 2>&1

/bin/sleep .1

done

}

# Le corps du programme :

while true

do

playwhile 1 $SON_OFF

playwhile 0 $SON_ON

done

D'autres comportements plus complexes sont envisageables à partir de ces briques de base. Une fois que tout est mis au point, le fichier script pourra être exécuté au démarrage du système si on ajoute son chemin complet à la fin du fichier /etc/rc.local. Dans le script, les noms des programmes sont aussi donnés avec leur chemin absolu, car $PATH n'est pas encore initialisé à ce moment-là.

3. Accédez directement aux GPIO en C

Ces scripts sont donc relativement portables et vous pourrez réutiliser une grande partie de votre code sur d'autres types d'ordinateurs. Mais pour réaliser des fonctions plus complexes ou beaucoup plus rapides, le codage en langage C devient nécessaire, bien que le code soit moins portable.

Je vous propose maintenant une petite bibliothèque de code en C pur, destinée à contrôler les broches de votre carte à la framboise. C'est une évolution du code présenté en 2013 dans Open Silicium n°6 [2], je vous invite à consulter cet article pour y trouver les détails techniques. Depuis, le code a été remanié, corrigé et amélioré : l'expérience a permis de trouver un bug indigne, d'ajouter des fonctionnalités et d'augmenter le confort d'utilisation. Ce fichier source est à la base de plusieurs bibliothèques et projets, faisons donc dès maintenant les présentations.

Le code est fourni ici intégralement, dans un souci d'exhaustivité et parce qu'il n'est pas très long, mais vous pouvez aussi le télécharger sur le dépôt GitHub du magazine. Les plus curieux compareront avec la version originale pour découvrir quel bug stupide a bien pu être oublié précédemment...

3.1 Le principe

La puce BCM2835 au cœur du Raspberry Pi accède à ses broches au travers de circuits situés dans un espace mémoire particulier, à l'adresse physique GPIO_BASE. Seul root peut y lire et écrire, après avoir projeté cet espace dans sa mémoire virtuelle au moyen de la fonction mmap(). Ceci est effectué par la fonction PI_IOmmap().

La zone d'entrées-sorties contient de nombreux registres qui configurent chaque broche. Par exemple, il y a jusqu'à huit fonctions possibles par broche (dont BCM_GPIO_IN et BCM_GPIO_OUT), ce qui est encodé avec 3 bits. La fonction PI_GPIO_config() calcule l'adresse des bits correspondant au port à configurer et met à jour le registre de fonctions.

Enfin, le plus important est l'accès aux broches. Trois macros sont proposées : GPIO_LEV_N(N) retourne la valeur de la broche, alors que GPIO_SET_N(N) et GPIO_CLR_N(N) la modifient. Pour des raisons d'atomicité des opérations, il n'est pas possible d'indiquer directement la valeur d'une broche : le registre pourrait être en cours de modification par un autre programme, ce qui entraînerait l'instabilité du système (puisque certaines broches GPIO contrôlent directement des périphériques tels que la carte SD).

3.2 Les améliorations

Voici une liste d'améliorations :

- La fonction d'initialisation devait être explicitement appelée, mais elle est maintenant intégrée dans la fonction de configuration d'une broche. Chaque appel à PI_GPIO_config() teste si la zone mémoire des GPIO est bien projetée dans l'espace utilisateur. Ainsi, il n'y a plus de risque d'oublier un appel de fonction en début de programme, qui est aussi plus court d'une ligne.

- La configuration des ports d'entrées-sorties est aussi utilisée par ma bibliothèque SPI. D'ailleurs, la fonction PI_IOmmap() peut aussi être appelée par d'autres morceaux de code. Il n'est même plus nécessaire de se souvenir quel code a initialisé quoi en premier.

- L'appel à mmap() posait quelques soucis, car la valeur de retour n'était pas correctement testée. Une mise à jour du système d'exploitation en mai 2013 a révélé des problèmes dans le code de Gert & Dom, qui sont maintenant résolus en utilisant le symbole MAP_FAILED.

- Les messages d'erreurs ont été adaptés et on peut fournir notre propre routine en jouant avec les #define. Habituellement, j'utilise deux fonctions : erreur() utilise perror() si un appel système échoue ; err() utilise juste printf() pour les erreurs internes.

3.3 Le parachute

La dernière nouveauté est l'ajout de la fonction GPIO_parachute() qui remet automatiquement en entrées les broches configurées par le programme. Cela réduit le code que le développeur doit écrire et augmente aussi la sécurité du système : si l'application plante, l'électronique ne reste pas figée dans un état potentiellement dommageable (par exemple, si une impulsion ne doit pas durer trop longtemps pour ne pas griller un composant).

Pour réaliser cela, le code utilise la fonction standard atexit(), qui ajoute notre fonction de nettoyage GPIO_parachute() à une liste. Cette liste sera balayée dans le sens inverse lorsque le programme se terminera. Le seul cas où cela ne se produira pas est si le programme est interrompu avec le signal SIGKILL (avec la commande kill -9), car il ne peut être intercepté.

PI_GPIO_config() mémorise dans la variable GPIO_used toutes les broches qu'on lui demande de configurer. Au premier appel, il installe aussi le parachute : GPIO_parachute() consultera la variable en fin de programme pour désactiver les broches qui ne sont pas déjà en entrées. Le parachute utilise aussi PI_GPIO_config(), ce qui crée une situation de poule et d'œuf, résolue par une pré-déclaration de la fonction.

Afin de s'assurer que le parachute sera déployé dans tous les cas possibles, on connecte la plupart des signaux à exit(), au moyen de la fonction signal(). L'utilisateur peut toujours ajouter son propre gestionnaire de signaux, tant qu'il se termine par un appel à exit(). Mais le plus simple est d'enregistrer une ou plusieurs autres fonctions avec atexit(), car cela préservera aussi l'ordre d'appel.

Cette fonctionnalité peut être désactivée en définissant le symbole GPIO_NO_ATEXIT.

3.4 Le code source

Le code suivant est le code du projet. Il a été abondamment commenté pour en comprendre le fonctionnement :

01: /*
02: PI_GPIO.c (c) Yann Guidon 20130204
03: Accès aux E/S de la carte Raspberry Pi
04: Dérivé du code de Dom & Gert @ http://elinux.org/RPi_Low-level_peripherals v. 20130101
05:

06: 20140907 : tout sur stderr
07: */
08: 
09: #ifndef PI_GPIO
10: #define PI_GPIO
11: 
12: #include <stdio.h>
13: #include <sys/mman.h>
14: #include <fcntl.h>
15: #include <stdlib.h>
16: 
17: int mmap_fd=0;
18: 
19: #define GPIO_BASE (0x20200000)
20: #define BLOCK_SIZE (4096) // correspond à la taille d'une page de MMU (unité de gestion mémoire)
21: 
22: // si la fonction erreur() n'est pas déjà fournie
23: #ifdef PI_GPIO_ERR
24: #include <errno.h>
25: 
26: /* sortie avec un message d'erreur contextualisé */
27: void erreur(char *msg) {
28:  perror(msg);
29:  exit(EXIT_FAILURE);
30: }
31: 
32: // 2ème message d'erreur, sans perror
33: void err(char *msg) {
34:  fputs(msg, stderr);
35:  fputc('\n', stderr);
36:  exit(EXIT_FAILURE);
37: }
38: #endif
39: 
40: unsigned * PI_IOmmap(off_t where) {
41:  void* map;
42: 
43:  // ne réouvre pas /dev/mem si on l'a déjà ouvert
44:  if (mmap_fd <= 0) {
45:  // ouvre /dev/mem
46:  if ((mmap_fd = open("/dev/mem", O_RDWR|O_SYNC) ) < 0)
47:  erreur("Echec à l'ouverture de /dev/mem");
48:  }
49: 
50:  // projette les registres GPIO dans la mémoire de notre programme
51:  map = mmap(
52:  NULL, // projette où ça arrange le kernel
53:  BLOCK_SIZE, // les registres de contrôle tiennent dans une seule page
54:  PROT_READ|PROT_WRITE, // on veut lire et écrire
55:  MAP_SHARED, // partagé avec d'autres processus
56:  mmap_fd, // la mémoire
57:  where // adresse de la zone à accéder
58:  );
59: 
60:  if (map == MAP_FAILED)
61:  erreur("Echec de mmap()");
62: 
63:  return (unsigned*)map;
64: }
65: 
66: 
67: volatile unsigned *PI_gpio=NULL; // DOIT être volatile pour éviter que les optimisations du compilateur n'altèrent notre code
68: // Pour mettre des broches à 1 ou à 0 :
69: #define RPI_GPSET0 (7)
70: #define RPI_GPCLR0 (10)
71: #define RPI_GPLEV0 (13)
72: #define GPIO_SET *(PI_gpio+RPI_GPSET0)
73: #define GPIO_CLR *(PI_gpio+RPI_GPCLR0)
74: #define GPIO_LEV *(PI_gpio+RPI_GPLEV0)
75: 
76: #define GPIO_SET_N(N) GPIO_SET = (1 << N)
77: #define GPIO_CLR_N(N) GPIO_CLR = (1 << N)
78: #define GPIO_LEV_N(N) (((GPIO_LEV) >> N) &1)
79: 
80: #define BCM_GPIO_IN (0) // mode entrée
81: #define BCM_GPIO_OUT (1) // mode sortie
82: #define BCM_GPIO_ALT0 (4) // fonctions alternatives
83: #define BCM_GPIO_ALT1 (5)
84: #define BCM_GPIO_ALT2 (6)
85: #define BCM_GPIO_ALT3 (7)
86: #define BCM_GPIO_ALT4 (3)
87: #define BCM_GPIO_ALT5 (2)
88: #define BCM_GPIO_ALT3 (7)
89: 
90: 
91: #ifndef GPIO_NO_ATEXIT
92: unsigned long long int GPIO_used=0; // Liste des broches à désactiver
93: void GPIO_parachute(); // pré-déclaration
94: 
95: #include <signal.h>
96: #endif
97: 
98: // à appeler obligatoirement avant d'accéder aux broches !
99: void PI_GPIO_config(int port, int mode) {
100:  int registre, offset, temp;
101: 
102:  // le premier appel lance le mmap (pour pas oublier)
103:  if (PI_gpio == NULL)
104:  PI_gpio = PI_IOmmap(GPIO_BASE); // adresse des ports d'entrée-sortie
105: 
106:  if ((port >= 0) && (port < 32) // Ce code fonctionne jusqu'à 54 GPIO,
107:  // mais les macros ne supportent que 32 GPIO
108:  && (mode >= 0) && (mode < 8 )) {
109:  // Calcul du numéro de registre :
110:  registre = port/10;
111: 
112:  // lecture dudit registre :
113:  temp = *(PI_gpio+registre);
114: 
115:  // calcul de l'offset :
116:  offset = port - (registre*10); // modulo déguisé
117:  offset *= 3; // 3 bits par port
118: 
119:  // Effacer les bits précédents :
120:  temp &= ~(7 << offset);
121:  // ajouter le mode désiré :
122:  temp |= mode << offset;
123: 
124:  // réécriture du résultat :
125:  *(PI_gpio+registre) = temp;
126: 
127: #ifndef GPIO_NO_ATEXIT
128:  if (mode != 0) {
129:  // enregistrement de la broche pour la désactivation
130:  if ( GPIO_used == 0 ) {
131:  atexit(GPIO_parachute);
132:  // En cas de signal, termine le programme
133:  // et indirectement appelle GPIO_parachute :
134:  signal(SIGHUP, exit);
135:  signal(SIGINT, exit);
136:  signal(SIGQUIT, exit);
137:  signal(SIGSTOP, exit);
138:  signal(SIGTERM, exit);
139:  signal(SIGABRT, exit);
140:  signal(SIGKILL, exit);
141:  }
142:  GPIO_used |= 1UL << port;
143:  }
144: #endif
145:  }
146:  else {
147:  fprintf(stderr,"Mauvais numéro de port (%d) ou de mode (%d)\n", port, mode);
148:  exit(EXIT_FAILURE);
149:  }
150: }
151: 
152: #ifndef GPIO_NO_ATEXIT
153: 
154: void GPIO_parachute() {
155:  unsigned int GPIO_cache = GPIO_used;
156:  unsigned int mask = 1;
157:  int i=0;
158: 
159:  fputs("\nRemet les broches", stderr);
160:  while (GPIO_cache) {
161:  if (GPIO_cache & mask) {
162:  PI_GPIO_config(i, BCM_GPIO_IN);
163:  GPIO_cache &= ~mask;
164:  fprintf(stderr, " %d", i);
165:  }
166:  mask += mask; // décalage à gauche
167:  i++;
168:  }
169: 
170:  fputs(" en entrée\n", stderr);

171: }
172: 
173: #endif
174: #endif

3.5 Un petit exemple

La bibliothèque facilite beaucoup l'écriture de code, même en C. Pour preuve, voici le code source d'un petit programme qui recopie l'état d'une broche dans une autre. Il n'y a presque rien d'autre à gérer et puisque plusieurs fichiers standards sont déjà inclus, pas besoin de les déclarer !

01: /* test_in_out.c
02: version sam. juil. 26 10:05:03 CEST 2014
03: 
04: copie l'état d'une broche d'entrée vers une broche de sortie
05: 
06: gcc -Wall -o test test_in_out.c
07: */
08: 
09: #define PI_GPIO_ERR
10: #include "PI_GPIO.c"
11: #define PI_IN (4)
12: #define PI_OUT (25)
13: 
14: int main(int argc, char *argv[]) {
15:  PI_GPIO_config(PI_IN, BCM_GPIO_IN);
16:  PI_GPIO_config(PI_OUT, BCM_GPIO_OUT);
17: 
18:  while (1) {
19:  printf("GPIO%d=%d\n", PI_IN, GPIO_LEV_N(PI_IN));
20:  if (GPIO_LEV_N(PI_IN))
21:  GPIO_SET_N(PI_OUT);
22:  else
23:  GPIO_CLR_N(PI_OUT);
24:  }

25: }

Évidemment, pour que ce programme fonctionne, vous devez le lancer avec l'utilisateur root :

pi@pi:~$ gcc -Wall -o test test_in_out.c

pi@pi:~$ sudo ./test

GPIO4=0
GPIO4=0
GPIO4=0
...

4. Extension fiable du port GPIO

Voilà, nous pouvons interfacer notre microcontrôleur de luxe ! Cependant, les débutants peuvent être découragés par quelques détails qui font la différence entre un montage qui fait à peu près ce qu'on lui demande, et un montage fiable. Une différence qui ne manque pas de se révéler lorsqu'on passe d'un montage bricolé sur le coin d'une table à une installation définitive sur le terrain. Nous allons maintenant voir comment ajouter des signaux au connecteur pour contrôler des composants électroniques externes sans aucun risque.

Un des luxes des PC de l'an 2000, c'était le port d'imprimante parallèle, que l'on pouvait utiliser pour un nombre incroyable d'autres applications. Trois broches en sortie, cinq en entrée et huit bidirectionnelles, permettaient de connecter des appareils parfois farfelus (une interface Ethernet ?), expérimentaux (comme les innombrables montages publiés par Électronique Pratique) ou grand public (scanner, lecteur ZIP). Un exemple d'utilisation sous Linux a été décrit dans GNU/Linux Magazine France [3].

Le PC a malheureusement abandonné cette interface, au profit (douteux) du port USB, qui apporte son lot d'avantages... et d'inconvénients (en particulier la latence, ainsi que la portabilité des logiciels et des pilotes). Le Raspberry Pi arrive sur les talons d'Arduino et une nouvelle ère d'interfaçage commence, nous permettant de redécouvrir les joies du bitbanging, débarrassé des complexités accumulées par les PC modernes.

Mais rappelons-nous que les PC utilisent le port parallèle pour à peu près tout et n'importe quoi. En particulier, certains modèles y envoient des codes lors du démarrage, afin de diagnostiquer les problèmes de BIOS. Ce qui signifie qu'un montage électronique recevra des informations incohérentes ou indésirables si le montage est allumé en même temps que l'ordinateur.

Une parade consiste à doter le périphérique d'une certaine « intelligence », pour par exemple reconnaître des codes d'identification et d'activation, ce qui complexifie évidemment l'électronique, alourdit le développement et augmente le coût.

Qu'en est-il du Raspberry Pi ? Bien que le SoC BCM2835 soit assez bien documenté, ce n'est pas toujours clair ou utile, car le firmware propriétaire ainsi que le kernel et la configuration de l'utilisateur peuvent tout perturber. Les nouveaux « HATs » introduits avec le modèle B+ [4] utilisent une EEPROM I²C pour la configuration, mais ce nouveau standard n'est ni obligatoire, ni disponible sur les révisions antérieures.

4.1 État à l'allumage

Comment s'assurer de l'état des broches dès la mise sous tension, et qu'elles ne changeront pas avant qu'on le décide ? Pour en avoir le cœur net, j'ai observé les broches des ports d'entrées-sorties d'un Modèle B, révision 2, à l'aide d'un oscilloscope, ce qui donne le tableau suivant.

Port P1 :

Broche

État à

l'allumage

Fonction Remarques
1   Alim 3,3V  
3 1 GPIO2 I2C SDA : Pull-up 1.8K
5 1 GPIO3 I2C SCL : Pull-up 1.8K
7 0 GPIO4  
9   GND  
11 0 GPIO17  
13 0 GPIO27 impulsions parfois observées à l'allumage (SPI/série ?)
15 0 GPIO22  
17   Alim 3,3V  
19 0 GPIO10 SPI_MOSI
21 0 GPIO9 SPI_MISO
23 0 GPIO11 SPI_CLK
25   GND  
----- ---- ------------  
2   Alim 5V  
4   Alim 5V  
6   GND  
8 1 GPIO14 TxD => console, impulsions (voir /boot/cmdline.txt)
10 1 GPIO15 RxD (si c'est la réception, pourquoi est-il à 1 ?)
12 0 GPIO18  
14   GND  
16 0 GPIO23  
18 0 GPIO24  
20   GND  
22 0 GPIO25  
24 0 GPIO8 SPI_CE0
26 0 GPIO9 SPI_CE1 (observé à 1 une fois ?)

Port P5 :

Broche État Fonction  
1   Alim 5V  
3 0 GPIO28  
5 0 GPIO30  
7   GND  
------------------------------- -----------------------------    
2   Alim 3,3V  
4 0 GPIO29  
6 0 GPIO31  
8   GND  

Je ne dispose pas encore d'un modèle B+, sur lequel il faudra refaire les tests.

Évidemment, cela dépend aussi des logiciels installés, ainsi que des modules du noyau, statiques ou dynamiques. Par exemple, le port série a été désactivé sur mon système (en enlevant toute mention de /dev/ttyAMA0 dans /boot/cmdline.txt et /etc/inittab) et malgré cela, quelques impulsions sont encore observées à l'allumage sur GPIO14.

4.2 Le registre 74HCT273

Les changements et signaux transitoires sont très embêtants si vous voulez contrôler des périphériques comme des moteurs ou des lumières, ou tout autre actionneur qui n'aime pas que ses broches soient chatouillées. Particulièrement si des combinaisons de signaux sont interdites et pourraient endommager les circuits ou les données.

Une approche classique consiste à utiliser des interfaces de type série. Un microcontrôleur connecté sur ttyAMA0 ferait l'affaire, mais ce serait superflu. On dispose aussi du port SPI (interfaçable avec un registre à décalage 74HCT595 par exemple) ou I²C. Par contre, ce n'est pas idéal pour piloter des circuits à relativement haute vitesse, avec une latence inférieure à une microseconde.

La solution présentée ici utilise un autre circuit ; le 74HCT273 est un registre à 8 bits qui mémorise l'entrée lors d'un front montant sur la broche d'horloge et surtout, qui dispose d'une entrée de remise à zéro. Je choisis ce circuit au lieu d'un 373/573/574 lorsque l'état interne doit être contrôlé par un signal externe. Par exemple, les 573 disposent d'une sortie à 3 états, donc des résistances de rappel au 0V peuvent amener le signal à 0, mais c'est moins fiable et il faut plus de soudures. De plus, cela ne fait que déplacer le problème : la broche /OE, qui contrôle l'état de la sortie, doit elle-même être contrôlée par un autre circuit devant être remis à zéro...

Grâce à la broche /RESET du 273, nous pouvons être sûrs qu'un actionneur ne va pas se mettre en route si la carte plante, se rallume ou subit une interruption accidentelle d'alimentation. L'inconvénient est que si vous avez besoin de signaux initialisés à l'état haut, vous devrez ajouter un inverseur. Un simple transistor peut suffire, mais une vraie porte logique est nécessaire pour les signaux à haute vitesse.

Fig. 4 : Brochage du 74HCT273. Attention à l'ordre des broches, qui diffère de la plupart des circuits similaires !

La technologie HCT est similaire à HC (faible consommation, très peu de courant sur les broches d'entrées, fonctionne sous 5V) ; la différence est que ses entrées sont compatibles avec les circuits 3,3V comme le Raspberry Pi.

Note

Parfois, le 74HCT273 se déclenche et change de sortie lorsque des parasites sont injectés dans l'alimentation. Par exemple lors du branchement d'un autre appareil, ou doté d'une alimentation différente. Filtrez bien les alimentations !

Si je devais parler des inconvénients de ce circuit, je dirais juste que son brochage est inhabituel. Vérifiez bien la fonction de chaque broche, car l'organisation diffère d'autres circuits plus répandus comme le 245, 373, 573 ou 574. Heureusement, mon Pi a survécu à quelques erreurs de manipulation, où des broches en sorties se sont retrouvées connectées entre elles... Cela n'arrive pas qu'aux autres !

4.3 Soignez votre circuit de remise à zéro !

Évidemment, cette promesse de sécurité faite au début de cette section ne tient que si le circuit est correctement remis à zéro. Or, même éteinte, la carte conserve environ 1,4V sur le rail 5V et 0,5V sur le rail 3,3V. Cela se produit sans qu'on le voie, lorsqu'un écran HDMI est connecté et allumé, ce qui injecte des courants parasites...

Un circuit de reset « analogique » risque de conserver plus de 1V à ses bornes et mal effectuer le reset, surtout si une perte d'alimentation est courte (voir figure 5).

Fig. 5 : Un circuit de remise à zéro avec une diode, un condensateur et une résistance. Économique, mais pas très fiable.

Le principe est simple : à la mise sous tension, le condensateur est déchargé et il se charge au travers de la résistance. Les valeurs sont choisies afin que la tension de déclenchement soit atteinte en un dixième de seconde environ. Par exemple, une résistance d'1MΩet un condensateur de 100nF font l'affaire.

Lorsque l'alimentation est coupée, le condensateur se décharge au travers de la diode, mais cela laisse environ 0,7V à ses bornes, avant que la résistance n'ait d'effet. Et cela suppose que l'alimentation redescende effectivement à 0V, suffisamment longtemps. D'autre part, la lente montée de la tension n'est pas toujours compatible avec les broches très sensibles des circuits intégrés, ce qui risque de provoquer plusieurs impulsions.

Des circuits intégrés spéciaux sont prévus pour initialiser correctement un système numérique. Différentes versions existent, avec temporisation, drain ouvert, des tensions variées, un choix de niveaux actifs, fournis par de nombreux fabricants : MAX809, LM809, TCM809, TPS3809...

On peut facilement combiner cela avec un reset manuel. Comme le montre la figure 6, un bouton-poussoir ou tout autre capteur peut être mis en parallèle avec le circuit de remise à zéro, si la sortie de celui-ci est protégée par une résistance. Cela fournit une fonction d'arrêt d'urgence pour un montage sensible. Par exemple, une forte impulsion sur l'alimentation du Pi peut le forcer à rebooter sans exécuter les fonctions de parachute. Ça sent le vécu...

Fig. 6 : Un circuit de remise à zéro avec un circuit spécialisé, assisté d'un bouton-poussoir.

Ce circuit commence maintenant à ressembler à un système « sérieux ».

4.4 Exemple d'application

Nous allons illustrer le principe du registre externe avec un petit montage qui fournit 8 bits (ou plus). Ce registre peut contrôler des circuits sensibles ou bien en activer d'autres (au moyen d'une entrée d'inhibition), ce qui compense un peu le faible nombre de bits disponibles.

Parmi les inconvénients du Raspberry Pi original, on trouve l'ordre des broches GPIO : elles sont un peu mélangées et surtout, il n'y a pas beaucoup de numéros consécutifs. Pour la révision 2 du modèle B (la version actuellement la plus répandue), on a juste 4 broches successives (22, 23, 24, 25), ce qui ne permet pas d'exploiter confortablement les 8 bits du registre 273.

Si vous avez vraiment besoin de 8 bits, vous pouvez compléter le bus de données avec les 4 bits consécutifs du connecteur P5, avec deux gros bémols :

- Les broches sont décalées par rapport au connecteur principal P1, donc l'accès avec un connecteur standard est difficile (il faut souder des fils ou raboter un connecteur) ;

- Les quatre broches supplémentaires (28, 29, 30, 31) ont des numéros GPIO consécutifs, mais séparés des autres (22, 23, 24, 25) ce qui complique le code.

Le modèle B+, qui remplace le modèle B depuis juillet 2014, fournit plus de broches et « bouche des trous » dans la liste des numéros logiques, ce qui permet d'accéder à la séquence de GPIO consécutives de 2 à 27. Évidemment, selon les autres types de périphériques que vous utilisez (I²C, SPI, série...), vous ne pourrez en utiliser qu'une partie, mais l'amélioration est significative et le nouveau modèle mérite d'être adopté.

Autrement, si le connecteur P5 est inaccessible, il reste possible d'obtenir quand même 8 bits au moyen d'une petite astuce, même si la moitié des broches du 273 est inutilisée. En effet, l'horloge est sensible au front montant uniquement, et nous devrons appliquer une impulsion, donc deux fronts, sur cette broche. Le deuxième front peut adresser un autre 273 si on ajoute un petit circuit inverseur (voir figure 7).

Fig. 7 : Une paire de registres 273, échantillonnés sur un front différent d'un signal de contrôle, double le nombre de sorties et garantit un allumage propre du circuit.

Ainsi, avec 5 bits de sortie, nous en obtenons 8. Seul le bit d'horloge doit nécessairement être propre au démarrage ; les broches de données peuvent être à n'importe quel état puisque le 273 sera remis à zéro.

Pour l'inverseur, j'ai utilisé un 74LVC1G14, mais beaucoup d'autres circuits peuvent faire l'affaire, en fonction de ce dont vous disposez. Un simple 74HCT00 (quadruple NAND) fonctionne aussi, si l'une des broches d'entrée est connectée à l'alimentation positive. Bref, débrouillez-vous et faites attention aux tensions !

4.5 Plus d'extensions

Ce circuit fonctionne sans souci avec d'autres 273 qui partagent le bus de données (de 4 ou 8 bits, si vous utilisez P5 ou un modèle B+). Chaque broche GPIO libre peut adresser une autre paire de registres, tant qu'elle ne danse pas la samba à l'allumage.

Si vous avez vraiment besoin de beaucoup plus de sorties, alors un circuit décodeur de type 74HCT138 peut venir à la rescousse. Cette puce est un petit décodeur 3 bits vers 8, qui prend une combinaison de 3 bits pour activer une seule des 8 sorties (ce qui économise 5 des précieuses GPIO du Pi).

Le signal en sortie du 138 est inversé (actif à l'état bas), mais il suffit d'échanger la fonction des registres d'une paire pour compenser la polarité.

Il faut toutefois tenir compte des éventuelles impulsions parasites lors d'un changement de code : si on sélectionne la sortie 0 après la sortie 7, d'autres sorties pourraient recevoir des micro-impulsions et perturber les autres registres.

La solution consiste à allouer une broche supplémentaire du Pi pour activer une des broches d'inhibition du 138. Cela réduit l'économie de broches (il faut 4 broches d'adressage pour sélectionner une des 8 paires de 273) mais au moins, il n'est plus absolument obligatoire de les changer simultanément. Le code d'adresse peut être sur 3 GPIO consécutives (2, 3 et 4, dont celles utilisées par le bus I²C) et la broche d'inhibition (qui contrôle indirectement l'impulsion d'horloge) peut être affectée à une broche solitaire. On obtient alors jusqu'à 64 sorties fiables avec seulement 8 GPIO.

4.6 Codage

Le code qui fait tourner tout cela n'est pas très compliqué, mais il pose une nouvelle question : comment modifier simultanément plusieurs bits sans modifier les autres ? L'interface GPIO permet de mettre simultanément plusieurs bits à 0, ou bien à 1, mais on ne peut pas faire les deux en même temps...

Il faut nécessairement deux accès, un pour mettre à 0 les éventuels bits à 1, l'autre pour mettre à 1 les bits à 0. Pour éviter de se compliquer la vie, le code suivant met tous les bits à 0 (lignes 21, 27) avant d'écrire la valeur binaire (lignes 22, 28). Dans d'autres situations, cela peut créer des impulsions parasites durant le très court instant où la sortie passe à 0, mais ici, la valeur est verrouillée en aval par un des 273 externes.

Ensuite, comment envoyer une impulsion courte, mais pas trop, mais courte quand même, sans écrire beaucoup de code ? D'un côté, le cœur ARM tourne à 700 MHz, soit environ 1,4 ns par instruction en théorie. Si on veut attendre environ 20 ns, il faut forcer l'exécution de 14 instructions dans le vide...

La solution suivante utilise un autre moyen de temporisation : elle repose sur la latence du bus interne dédié aux entrées-sorties et certains périphériques lents. Ce bus périphérique est cadencé à seulement 250 MHz et est plus étroit (probablement 8 ou 16 bits de large), donc il faut plusieurs cycles pour accéder aux GPIO. Les différents tampons sur les bus ajoutent encore de la latence, et des attentes sont imposées lorsque le sens de transfert change. Pour des transferts efficaces, il est préférable de coder plusieurs lectures consécutives, suivies d'écritures consécutives, au lieu de les mélanger.

Mais puisque nous voulons attendre, nous allons justement faire l'inverse : intercaler une lecture au milieu des écritures. Nous n'avons pas besoin de la valeur lue, mais il ne faut pas non plus que le compilateur élimine le code correspondant, donc elle sera mélangée à une autre valeur volatile. C'est le rôle de la macro SETTLE (ligne 12) qui mélange le registre GPIO_LEV à la variable Dummy avec l'opérateur XOR.

Une fois que tout cela est testé et mesuré (grâce à un oscilloscope), il ne reste plus qu'à ajouter une autre fonction parachute, reg_cleanup(), qui effacera les registres à la fin du programme.

01: #include "PI_GPIO.c"
02: 
03: #define PIN_273_SEL (17) // con.: pin 11
04: 
05: #define PIN_273_DAT0 (22) // con.: pin 15
06: #define PIN_273_DAT1 (23) // con.: pin 16
07: #define PIN_273_DAT2 (24) // con.: pin 18
08: #define PIN_273_DAT3 (25) // con.: pin 22
09: 
10: volatile unsigned int Dummy;
11: // délai de 120 ns environ :
12: #define SETTLE Dummy^=(GPIO_LEV)
13: // Le xor vers une valeur volatile ne devrait pas être optimisé
14: // par le compilateur, l'accès aux registres GPIO non plus.
15: // Attention, car la durée dépend aussi des accès précédents
16: // aux registres d'entrées-sorties, en lecture comme en écriture.
17: 
18: // dure approx. 500ns :
19: void set_273(unsigned val) {
20:  // moitié basse
21:  GPIO_CLR = ( 15 << PIN_273_DAT0);
22:  GPIO_SET = ((15 & val) << PIN_273_DAT0);
23:  SETTLE;
24:  GPIO_SET_N(PIN_273_SEL);
25:  SETTLE;
26:  // moitié haute
27:  GPIO_CLR = ( 15 << PIN_273_DAT0);
28:  GPIO_SET = ((15 & (val>>4)) << PIN_273_DAT0);
29:  SETTLE;
30:  GPIO_CLR_N(PIN_273_SEL);
31:  SETTLE;
32: }
33: 
34: void reg_cleanup() {
35:  set_273(0);
36:  fputs("\nregistres remis à 0 ", stderr);
37: }
38: 
39: void reg_init() {
40:  PI_GPIO_config(PIN_273_SEL, BCM_GPIO_OUT);
41:  PI_GPIO_config(PIN_273_DAT0, BCM_GPIO_OUT);
42:  PI_GPIO_config(PIN_273_DAT1, BCM_GPIO_OUT);
43:  PI_GPIO_config(PIN_273_DAT2, BCM_GPIO_OUT);
44:  PI_GPIO_config(PIN_273_DAT3, BCM_GPIO_OUT);
45:  set_273(0);

46:  atexit(reg_cleanup);
47: }

Avec un temps d'exécution de 500 ns, la vitesse maximale est d'environ deux millions de rafraîchissements par seconde. Une temporisation plus courte améliorerait cette performance, mais lorsque ce n'est pas critique, on préfère optimiser la compacité du code. On réservera aussi des GPIO pour les signaux les plus rapides.

Conclusion

Les GPIO sont une ressource essentielle du Raspberry Pi, car ils lui ouvrent les portes vers d'innombrables périphériques sur mesure, qui n'existent pas sur d'autres bus. J'espère que ces codes sources vous rendront de grands services. Ils sont aussi la base pour d'autres fonctions plus évoluées, comme ma bibliothèque de fonctions SPI, ou la plupart de mes projets à base de Raspberry Pi. Amusez-vous bien !

Références

[1] La documentation de l'interface GPIO du kernel Linux :https://www.kernel.org/doc/Documentation/gpio/

[2] Guidon Y., « Comment contrôler les GPIO du Raspberry Pi par HTTP en C », Open Silicium n°6, mars 2013. http://connect.ed-diamond.com/Open-Silicium/OS-006/Comment-controler-les-GPIO-du-Raspberry-Pi-par-HTTP-en-C

[3] Guidon Y., « Interfaçage de GHDL avec le port parallèle sous Linux », GNU/Linux Magazine France n°133, décembre 2010, p. 74. http://connect.ed-diamond.com/GNU-Linux-Magazine/GLMF-133/Interfacage-de-GHDL-avec-le-port-parallele-sous-Linux. Code source : http://ygdes.com/GHDL/io_port/

[4] Adams J., « Introducing Raspberry Pi HATs », http://www.raspberrypi.org/introducing-raspberry-pi-hats/

Pour l'assignation des broches du connecteur et la correspondance avec le numéro de GPIO, consultez http://www.raspberrypi.org/documentation/hardware/raspberrypi/README.md (qui renvoie vers l'incontournable http://elinux.org/RPi_Low-level_peripherals ).

Voir aussi http://www.blaess.fr/christophe/files/article-2014-08-07/Connecteur_P1.pdf et http://www.blaess.fr/christophe/2014/08/06/b/.