Pilotage de GPIO avec l’API Libgpiod (Partie 3)

Publié par cpb
Oct 29 2018

Après l’étude des outils disponible en ligne de commande pour piloter des lignes GPIO (partie 1), puis celles de l’accès depuis un programme avec l’appel système ioctl() ou la version context-less de la bibliothèque Libgpiod (partie 2), nous poursuivons en examinant les fonctions bas niveaux de cette bibliothèque. Nous aborderons également le thème des performances d’accès aux GPIO selon la méthode choisie.

PROGRAMMATION AVEC LA BIBLIOTHÈQUE LIBGPIOD

Les fonctions context-less, nous l’avons vu, permettent un accès simple et rapide aux lignes GPIO pour la lecture. L’écriture aussi reste simple tant que nous souhaitons piloter une seule ligne pour sortir une impulsion ou un signal périodique. Si on souhaite gérer plusieurs lignes GPIO avec des valeurs déterminées dynamiquement (par exemple pour commander des relais en fonction de mesures obtenues par certains capteurs ou de demandes d’un opérateur), cette API context-less devient complexe d’usage.

De plus, pour certaines applications il peut être nécessaires de connaître des paramètres précis des lignes GPIO, par exemple le type de configuration de sortie (collecteur ouvert, drain ouvert, etc.). Pour cela nous préférerons utiliser l’API bas-niveau de Libgpiod.

Tous les exemples décrits ci-dessous sont disponibles dans mon dépôt Framagit : https://framagit.org/cpb/example-programs-using-libgpiod.

Structures de données

Nous manipulerons plusieurs types de données opaques, définis et gérés de manière interne dans la bibliothèque mais auxquels nous ne ferons référence que par des pointeurs :

  • struct gpiod_chip : un contrôleur de GPIO. Il nous permettra d’avoir accès à ses différentes lignes.
  • struct gpiod_line : une ligne GPIO, identifié par un offset au sein de son contrôleur.
  • struct gpiod_line_bulk : un groupe de lignes GPIO auxquelles on applique des opérations.
  • struct gpiod_chip_iter : un itérateur permettant de parcourir les différents contrôleurs disponibles.
  • struct gpiod_line_iter : un itérateur pour parcourir les lignes d’un contrôleur.

Liste des contrôleurs et lignes disponibles

L’accès à un contrôleur GPIO se fait en ouvrant le descripteur correspondant. On utilise l’une des fonctions suivantes :

struct gpiod_chip *gpiod_chip_open(const char *path);
// ex: chip = gpiod_chip_open("/dev/gpiochip2");

struct gpiod_chip *gpiod_chip_open_by_name(const char *name);
// ex: chip = gpiod_chip_open_by_name("gpiochip2");

struct gpiod_chip *gpiod_chip_open_by_number(unsigned int number);
// ex: chip = gpio_chip_open_by_number(2);

struct gpiod_chip *gpiod_chip_open_by_label(const char *label);
// ex: chip = gpio_chip_open_by_label("pinctrl-bcm2835");
// See device tree or kernel driver.

Ou la fonction générique suivante, qui recherche la plus adéquate des précédentes :

struct gpiod_chip *gpiod_chip_open_lookup(const char *string);

On peut également utiliser un itérateur pour parcourir les contrôleurs disponibles avec les fonctions :

struct gpiod_chip_iter *gpiod_chip_iter_new(void);
struct gpiod_chip *gpiod_chip_iter_next(struct gpiod_chip_iter *iter);
void gpiod_chip_iter_free(struct gpiod_chip_iter *iter);
void gpiod_chip_iter_free_noclose(struct gpiod_chip_iter *iter);

Si aucun argument n’est fourni sur sa ligne de commande, le programme list-gpio-lines.c parcourt tous les contrôleurs disponibles ainsi :

struct gpiod__chip_iter *iter;
struct gpio_chip *chip;
[...]
iter = gpiod_chip_iter_new();
if (iter == NULL)
[...]
while ((chip = gpiod_chip_iter_next(iter)) != NULL)
list_gpio_lines(chip);
gpiod_chip_iter_free(iter);

Si, au contraire, on lui fournit des arguments sur sa ligne de commande il tente d’ouvrir le(s) contrôleur(s) ainsi :

for (i = 1; i < argc; i ++) {
chip = gpiod_chip_open_lookup(argv[i]);
if (chip == NULL)
perror(argv[i]);
else
list_gpio_lines(chip);
}

Une fois un contrôleur ouvert, on peut connaître son nom, son label et son nombre de lignes avec les routines :

const char * gpiod_chip_name(struct gpiod_chip *chip);
const char * gpiod_chip_label(struct gpiod_chip *chip);
unsigned int gpiod_chip_num_lines(struct gpiod_chip *chip);

Pour accéder aux lignes GPIO d’un contrôleur, nous avons plusieurs possibilités. Tout d’abord on peut réclamer une ligne donnée par son numéro (son offset) :

struct gpiod_line *gpio_chip_get_line(struct gpiod_chip *chip,
unsigned int offset);

On peut aussi réclamer un ensemble (bulk) de lignes en passant un tableau d’offsets à la fonction :

int gpiod_chip_get_lines(struct gpiod_chip *chip,
unsigned int *offset,
int num_offset,
struct gpiod_line_bulk *bulk);

La structure gpiod_line_bulk qui est renseignée par cette fonction est de la forme :

struct gpiod_line_bulk {
struct gpiod_line *lines[GPIOD_LINE_BULK_MAX_LINES];
unsigned int num_lines;
};

Le champ num_lines indique le nombre de lignes inscrites dans le tableau lines. Il est toutefois possible d’itérer sur un ensemble bulk directement avec les macros :

struct gpiod_line_bulk bulk;
struct gpiod_line *line;
struct gpiod_line **save;
[...]
gpiod_line_bulk_foreach_line(&bulk, line, save) {
[...]
}

ou

struct gpiod_line_bulk bulk;
struct gpiod_line *line;
int offset;
[...]
gpiod_line_bulk_foreach_line_off(&bulk, line, offset) {
[...]
}

On peut également demander un tableau contenant toutes les lignes GPIO du contrôleur avec :

int gpiod_chip_get_all_lines(struct gpiod_chip *chip,
struct gpiod_line_bulk *bulk);

Et enfin, il est possible de rechercher une ou plusieurs lignes par leur noms :

struct gpiod_line *gpiod_find_line(struct gpiod_chip *chip,
const char *name);
int gpiod_find_lines(struct gpiod_chip *chip,
const char **names,
struct gpiod_line_bulk *bulk);

La liste de nom fournie à gpiod_find_lines() doit se terminer par un pointeur NULL.

Les informations sur une ligne peuvent être consultées avec les fonctions suivantes :

unsigned int gpiod_line_offset        (struct gpiod_line *line);
const char *gpiod_line_name          (struct gpiod_line *line);
const char *gpiod_line_consumer      (struct gpiod_line *line);
int gpiod_line_direction     (struct gpiod_line *line);
// GPIOD_LINE_DIRECTION_INPUT, GPIOD_LINE_DIRECTION_OUTPUT
int   gpiod_line_active_state  (struct gpiod_line *line);
// GPIOD_LINE_ACTIVE_STATE_HIGH, GPIOD_LINE_ACTIVE_STATE_LOW
bool gpiod_line_is_used       (struct gpiod_line *line);
bool gpiod_line_is_open_drain (struct gpiod_line *line);
bool gpiod_line_is_open_source(struct gpiod_line *line);

Le programme list-gpio-lines.c parcourt les lignes de chaque contrôleur trouvé et affiche ces informations ainsi :

struct gpiod_line_bulk bulk;
struct gpiod_line *line;
int offset;
const char *string;

fprintf(stdout, "%s - %s - %d lines\n",
    gpiod_chip_name(chip),
    gpiod_chip_label(chip),
    gpiod_chip_num_lines(chip));

if (gpiod_chip_get_all_lines(chip, &bulk) != 0) {
    perror("gpiod_chip_get_all_lines()");
    return -1;
}

gpiod_line_bulk_foreach_line_off(&bulk, line, offset) {
    fprintf(stdout, "  %2d: ", offset);

    string = gpiod_line_name(line);
    if (string == NULL)
        fprintf(stdout, "- ");
    else
        fprintf(stdout, "%s ", string);

    string = gpiod_line_consumer(line);
    if (string == NULL)
        fprintf(stdout, "() ");
    else
        fprintf(stdout, "(%s) ", string);;

    if (gpiod_line_direction(line) == GPIOD_LINE_DIRECTION_OUTPUT)
        fprintf(stdout, "out ");
    else
        fprintf(stdout, "in  ");

    if (gpiod_line_active_state(line) == GPIOD_LINE_ACTIVE_STATE_LOW)
        fprintf(stdout, "active-low ");

    if (gpiod_line_is_open_drain(line))
        fprintf(stdout, "open-drain ");
if (gpiod_line_is_open_source(line)) fprintf(stdout, "open-source "); if (gpiod_line_is_used(line)) fprintf(stdout, "* "); fprintf(stdout, "\n"); }

Voici un exemple d’exécution de ce programme sur un Raspberry Pi 3 :

$ ./list-gpio-lines 
gpiochip0 - pinctrl-bcm2835 - 54 lines
   0: - () in  
   1: - () in  
   2: - () in  
   3: - () in  
   4: - () in  
   5: - () in  
   6: - () in  
   7: - () in  
   8: - () in  
   9: - () in  
  10: - () in
[...] 50: - () in 51: - () in 52: - () in 53: - () in gpiochip1 - brcmexp-gpio - 8 lines 0: - () out 1: - () out 2: - () out 3: - () out 4: - () in 5: - () out 6: - () out 7: - (led1) in * gpiochip2 - brcmvirt-gpio - 2 lines 0: - (led0) out * 1: - () in $

Accès en lecture et écriture

Avant d’accéder à une ligne GPIO, il est d’usage d’en réserver l’accès. C’est le rôle des fonctions suivantes, qui fonctionne pour une ligne ou pour tout un ensemble. Elles renvoient 0 si elles réussissent et -1 en cas d’échec.

int gpiod_line_request_input(struct gpiod_line *line,
                             const char *consumer);

int gpiod_line_request_bulk_input(struct gpiod_line_bulk *bulk,
const char *consumer)
int gpiod_line_request_output(struct gpiod_line *line, const char *consumer,
int initial_value);

int gpiod_line_request_bulk_output(struct gpiod_line_bulk *bulk,
const char *consumer,
const int *initial_values);

Pour libérer l’accès à une ou plusieurs lignes, on emploiera :

void gpiod_line_release(struct gpiod_line *line);
void gpiod_line_release_bulk(struct gpiod_line_bulk *bulk);

Les opérations de lecture et d’écriture sont simples. Lorsqu’elles agissent sur un ensemble, il convient que le tableau passé en second argument soit assez grand pour contenir les valeurs lues ou à écrire.

int gpiod_line_get_value(struct gpiod_line *line);
int gpiod_line_set_value(struct gpiod_line *line,
int value);

int gpiod_line_get_value_bulk(struct gpiod_line_bulk *bulk, int *values);
int gpiod_line_set_value_bulk(struct gpiod_line_bulk *bulk, const int *values)

Le programme invert-gpio.c prend en argument les paramètres contrôleur et offset d’une ligne GPIO d’entrée et d’une ligne de sortie. Ensuite il boucle toutes les millisecondes en écrivant sur la ligne de sortie la valeur inverse de celle lue sur la ligne d’entrée. Hors traitement d’erreur (présent dans le vrai fichier), sa structure est en substance la suivante :

sscanf(argv[2], "%d", &input_offset);
sscanf(argv[4], "%d", &output_offset);

input_chip  = gpiod_chip_open_lookup(argv[1]);
output_chip = gpiod_chip_open_lookup(argv[3]);

input_line  = gpiod_chip_get_line(input_chip, input_offset);
output_line = gpiod_chip_get_line(output_chip, output_offset);

gpiod_line_request_input(input_line, argv[0]);
gpiod_line_request_output(output_line, argv[0], 0);
for (;;) { gpiod_line_set_value(output_line, 1 - gpiod_line_get_value(input_line)); usleep(1000); }
gpiod_line_release(input_line); gpiod_line_release(output_line);

Détection d’événements

Le programme ci-dessus se comporte de manière déplorable, en consommant du temps CPU inutilement (toutes les itérations où l’entrée n’a pas changé de valeur) et en risquant de manquer des événments brefs où l’entrée change deux fois d’état pendant la milliseconde de sommeil.

Il serait largement préférable d’attendre passivement que le noyau nous indique qu’une transition est survenue sur la ligne d’entrée pour faire l’écriture correspondant en sortie. Pour cela la bibliothèque Libgpiod met à notre disposition des fonctions d’attente. Tout d’abord on indique pour quel type d’événment on souhaite se mettre en attente

int gpiod_line_request_rising_edge_events(
struct gpiod_line *line, const char *consumer); int gpiod_line_request_bulk_rising_edge_events(
struct gpiod_line_bulk *bulk, const char *consumer); int gpiod_line_request_falling_edge_events(
struct gpiod_line *line, const char *consumer); int gpiod_line_request_bulk_falling_edge_events(
struct gpiod_line_bulk *bulk, const char *consumer); int gpiod_line_request_both_edges_events(
struct gpiod_line *line, const char *consumer); int gpiod_line_request_bulk_both_edges_events(
struct gpiod_line_bulk *bulk, const char *consumer);

On peut ensuite se mettre en attente de l’occurrence d’un événement souhaité :

int gpiod_line_event_wait(struct gpiod_line *line,
                          const struct timespec *timeout);

int gpiod_line_event_read(struct gpiod_line *line,
                          struct gpiod_line_event *event);

int gpiod_line_event_wait_bulk(
struct gpiod_line_bulk *bulk, const struct timespec *timeout, struct gpiod_line_bulk *event_bulk);

La première fonction attend que l’événement se produise avec une durée maximale si on le souhaite. Si on attend n’importe quel type de front, il convient de savoir si l’événement survenu est une montée ou une descente du signal. Pour cela on invoque la seconde fonction, qui renseigne une structure contenant le type d’événement et son horodatage.

struct gpiod_line_event {
    struct timespec ts;
    int event_type;
    // GPIOD_LINE_EVENT_RISING_EDGE, GPIOD_LINE_EVENT_FALLING_EDGE
};

Le programme wait-gpio-event.c reprend le même comportement que le précédent mais ne se réveille que lorsqu’un changement d’état sur la ligne GPIO d’entrée est détecté. Les modifications principales sont :

[...]
gpiod_line_request_both_edges_events(input_line, argv[0]);
gpiod_line_request_output(output_line, argv[0], 0);

gpiod_line_set_value(output_line, 
                     1 - gpiod_line_get_value(input_line));

for (;;) {
    gpiod_line_event_wait(input_line, NULL);
    if (gpiod_line_event_read(input_line, &event) == 0) {
        if (event.event_type == GPIOD_LINE_EVENT_RISING_EDGE)
            gpiod_line_set_value(output_line, 0);
        else
            gpiod_line_set_value(output_line, 1);
    }
}

La fonction gpiod_line_event_wait() utilise en interne l’appel système ppoll() pour réaliser l’attente passive. Si nous souhaitons réaliser nous même l’attente, en ajoutant par exemple des descripteurs de sockets réseau, de périphériques caractères (série, i2c…), de console, etc. on utilisera les fonctions suivantes pour obtenir le descripteur et lire l’événement survenu.

int gpiod_line_event_get_fd(struct gpiod_line *line);

int gpiod_line_event_read_fd(int fd,
struct gpiod_line_event *event);

PERFORMANCES

Nous avons vu dans cet article et les précédents plusieurs méthodes d’accès aux lignes GPIO. Certaines sont plus simples que d’autres, mais une question se pose : qu’en est-il des performances ? De la vitesse de commutation d’une ligne par exemple ?

Une méthode simple consiste à faire un petit programme qui alterne une sortie le plus rapidement possible et à observer le résultat à l’oscilloscope.

Les exemples ci-dessous ont été exécutés sur un Raspberry Pi 3, après l’avoir basculé en mode performance plutôt que ondemand :

$ sudo -i
# echo performance > /sys/devices/system/cpu/cpu0/cpufreq/scaling_governor
# exit

Commande gpioset depuis un script shell

Nous nous en doutons immédiatement, l’appel d’une commande depuis le shell ne sera pas la méthode la plus rapide. Vérifions quand même le fonctionnement. Pour cela, j’appelle la ligne de commande suivante :

$ while true; do gpioset /dev/gpiochip0 24=0; gpioset /dev/gpiochip0 24=1; done
Cycle complet d'appels gpioset
Cycle complet d’appels à gpioset

Nous voyons deux pics espacés de 7,13 millisecondes. Cet énorme écart est moins dû aux performances du shell (une boucle simple à incrémenter une variable durant environ 0,5 milliseconde) qu’au temps d’appel de la commande gpioset et à la configuration du contrôleur et de la ligne GPIO.  Il faut donc 3,5 millisecondes pour exécuter une commande gpioset. Zoomons sur un pic :

Sortie GPIO à l'état haut avec gpioset
Sortie GPIO à l’état haut avec gpioset

En observant de plus près l’un des pics, on voit qu’une fois la sortie activée, elle reste à l’état haut pendant 15 microsecondes environ, puis qu’elle est relâchée par la commande, retournant à l’état bas par une courbe de décharge d’une trentaine de microsecondes.

Décroissance du signal après libération de la ligne GPIO
Décroissance du signal après libération de la ligne GPIO

API context-less de Libgpiod

Nous pouvons reprendre le programme toggle-gpio.c de l’article précédent pour le modifier rapidement et faire une boucle la plus rapide possible autour de la fonction gpiod_ctxless_set_values().

C’est ce que réalise le programme ctxless-toggle-gpio.c :

[...]
for (;;) {
    gpiod_ctxless_set_value(argv[1], offset, 0, 0, argv[0], NULL, NULL);
    gpiod_ctxless_set_value(argv[1], offset, 1, 0, argv[0], NULL, NULL);
}
Cycle d'appels à gpiod_ctxless_set_value()
Cycle d’appels à gpiod_ctxless_set_value()

Le résultat est déjà nettement meilleur que le précédent, les pics sont espacés de 64 microsecondes.

Sortie GPIO à l'état haut avec gpiod_ctxless_set_value()
Sortie GPIO à l’état haut avec gpiod_ctxless_set_value()

La broche reste à l’état haut sept microsecondes avant de revenir à l’état bas en une vingtaine de microsecondes. On peut conclure qu’un changement d’état avec l’API contex-less de Libgpiod dure environ 32 microsecondes.

API bas-niveau de Libgpiod

L’étape suivante va consister à utiliser l’API bas-niveau de Libgpiod, celle que nous avons étudié dans cet article. Le programme low-level-toggle-gpio.c réalise ce travail :

[...]
for (;;) { gpiod_line_set_value(output_line, 0); gpiod_line_set_value(output_line, 1); }
Cycles autour de gpiod_line_set_value()
Cycles autour de gpiod_line_set_value()

Le résultat est sans appel, le temps d’un cycle complet est de 2,19 microsecondes, et la ligne ne revenant pas en haute impédance, il n’y a plus de courbe de décroissance comme dans les exemple précédentes.

Cette fonction permet donc de configurer la ligne GPIO en 1,09 microseconde. Elle présente quand même un très léger overhead par rapport à l’appel système ioctl() direct que nous avons évoqué dans l’article précédent.

Appel système ioctl()

Logiquement l’invocation directe de l’appel système sous-jacent à toutes ces fonctions devraient être plus performante. Il nous suffit de modifier légèrement le programme de l’article précédent pour obtenir un programme ioctl-fast-toggle-gpio.c :

[...]
output_values.values[0] = 0;
for (;;) {
    output_values.values[0] = 0;
    ioctl(output_request.fd, GPIOHANDLE_SET_LINE_VALUES_IOCTL, &output_values);
    output_values.values[0] = 1;
    ioctl(output_request.fd, GPIOHANDLE_SET_LINE_VALUES_IOCTL, &output_values);
}
Cycles autour de l'appel ioctl()
Cycles autour de l’appel ioctl()

En effet, une légère amélioration est perceptible, le cycle complet se déroulant en 1,76 microsecondes, donc la configuration d’une sortie durant 0,88 microseconde. Ceci est très correct pour une action depuis  l’espace utilisateur sur un processeur comme celui du Raspberry Pi 3.

CONCLUSION

Nous avons pu voir que l’API bas-niveau proposée par la bibliothèque Libgpiod est riche et assez simple à utiliser. Elle remplace avantageusement les accès via sysfs et offre la portabilité qui manque aux solutions comme WiringPi spécifiques à certaines plates-formes. La surcharge par rapport à l’invocation directe de l’appel-système ioctl() est minime, et la complexité sensiblement moindre.

RÉFÉRENCES

URL de trackback pour cette page