Solutions temps réel sous Linux

Linux, Livres, Temps-réel | Publié par cpb
mai 15 2012

Aujourd’hui arrive en librairie mon nouveau livre qui traite des solutions libres pour obtenir un comportement temps réel souple ou strict avec Linux.

Pour plus de détails…

GPIO, Pandaboard et temps réel – 2 – sorties depuis l’espace kernel

Embarqué, Linux, Microprocesseur, Temps-réel | Publié par cpb
mai 14 2012

GPIO Pandaboard et temps-réelNous avons observé dans l’article précédent comment programmer et commander depuis l’espace utilisateur les GPIO. Il s’agit, nous l’avons vu, de broches du microprocesseur que nous pouvons affecter au choix en entrée ou en sortie et sur lesquelles il est possible de lire ou d’écrire des valeurs électriques.

Nous avons utilisé l’interface offerte par le noyau à travers le pseudo-système de fichiers /sys pour écrire un oscillateur s’exécutant dans l’espace utilisateur. Nos premiers essais réalisés avec un script shell souffrait d’une granularité excessive (plusieurs millisecondes) dans la gestion du temps. Un programme en C offrait une bien meilleure résolution (de l’ordre de la centaine de microsecondes). Ordonnancé en temps partagé les fluctuations étaient parfois très importantes (plusieurs millisecondes au pire). En utilisant l’ordonnancement temps réel souple de Linux, les fluctuations étaient beaucoup plus limitées.

Nous allons à présent réaliser les mêmes opérations depuis l’espace kernel, en écrivant d’abord des modules pour le noyau Linux standard puis en nous intéressant à un module pour Xenomai.

Timer du noyau Linux vanilla

Le noyau Linux classique nous offre une interface simple pour programmer des actions différées, et éventuellement répétitives, les structures timer_list. Notre premier exemple est construit autour de ces structures. Il programme l’exécution périodique d’une fonction toutes les millisecondes. Cette fonction basculera une sortie GPIO comme nous l’avons fait dans l’article précédent, mais en s’appuyant sur l’API kernel <linux/gpio.h>.

oscillateur-gpio-timer.c : 
#include <linux/cdev.h>
#include <linux/device.h>
#include <linux/fs.h>
#include <linux/gpio.h>
#include <linux/module.h>
#include <linux/sched.h>
#include <linux/version.h>
#include <asm/uaccess.h>

static void timer_oscillateur(unsigned long);
static struct timer_list timer;

// 138 Broche 10 du port J3 (Expansion A) de la Pandaboard
#define GPIO_OSCILLATEUR 138

static int __init init_oscillateur (void)
{
  int err;

  if ((err = gpio_request(GPIO_OSCILLATEUR, THIS_MODULE->name)) != 0) {
    return err;
  }
  if ((err = gpio_direction_output(GPIO_OSCILLATEUR, 1)) != 0) {
    gpio_free(GPIO_OSCILLATEUR);
    return err;
  }
  init_timer (& timer);
  timer.function = timer_oscillateur;
  timer.data = 0;
  timer.expires = jiffies + HZ/1000;
  add_timer(& timer);

  return 0;
}

static void __exit exit_oscillateur (void)
{
  del_timer(& timer);
  gpio_free(GPIO_OSCILLATEUR);
}

static void timer_oscillateur(unsigned long unused)
{
  static int value = 0;
  gpio_set_value(GPIO_OSCILLATEUR, value);
  value = 1 - value;
  mod_timer(& timer, jiffies + HZ/1000);
}

module_init(init_oscillateur);
module_exit(exit_oscillateur);
MODULE_LICENSE("GPL");

Comme nous le voyons, la durée de ces timers s’expriment en jiffies, en ticks système, dont la fréquence est contenue dans la constante symbolique HZ. IL y a HZ ticks par seconde. En programmant un déclenchement dans HZ/1000 ticks, on diffère donc l’action d’une milliseconde.

Connecteur d'extension de la Pandaboard

Pour compiler ce code (et les modules suivants), il va falloir utiliser un fichier Makefile spécifique. Lors de l’invocation de la commande make, nous devrons indiquer:

  • ARCH=arm pour la Pandaboard
  • CROSS_COMPILE= suivi du préfixe à employer pour utiliser la chaîne de compilation croisée
  • KERNEL_DIR= suivi du chemin d’accès au répertoire contenant les sources du noyau de la Pandaboard

À titre d’exemple, sur mon poste l’invocation de make se fait ainsi:

$ make ARCH=arm CROSS_COMPILE=~/cross-panda/usr/bin/arm-linux- KERNEL_DIR=~/Projets/Panda/linux-2.6.38.8
make -C /home/cpb/Projets/Panda/linux-2.6.38.8 SUBDIRS=/home/cpb/Documents/Livres/Articles/Blog/article-2012-05-14  modules
make[1]: entrant dans le répertoire « /home/cpb/Projets/Panda/linux-2.6.38.8 »
  CC [M]  /home/cpb/Documents/Articles/Blog/article-2012-05-14/oscillateur-gpio-timer.o
  CC [M]  /home/cpb/Documents/Articles/Blog/article-2012-05-14/oscillateur-gpio-hrtimer.o
  CC [M]  /home/cpb/Documents/Articles/Blog/article-2012-05-14/oscillateur-gpio-rtdm.o
  Building modules, stage 2.
  MODPOST 3 modules
  CC      /home/cpb/Documents/Articles/Blog/article-2012-05-14/oscillateur-gpio-hrtimer.mod.o
  LD [M]  /home/cpb/Documents/Articles/Blog/article-2012-05-14/oscillateur-gpio-hrtimer.ko
  CC      /home/cpb/Documents/Articles/Blog/article-2012-05-14/oscillateur-gpio-rtdm.mod.o
  LD [M]  /home/cpb/Documents/Articles/Blog/article-2012-05-14/oscillateur-gpio-rtdm.ko
  CC      /home/cpb/Documents/Articles/Blog/article-2012-05-14/oscillateur-gpio-timer.mod.o
  LD [M]  /home/cpb/Documents/Articles/Blog/article-2012-05-14/oscillateur-gpio-timer.ko
make[1]: quittant le répertoire « /home/cpb/Projets/Panda/linux-2.6.38.8 »
$

Il faut ensuite transférer le code sur la cible, s’y connecter et charger le module.

$ scp oscillateur-gpio-timer.ko root@192.168.5.152:/root
root@192.168.5.152's password:
oscillateur-gpio-timer.ko                                          100%   92KB  92.4KB/s   00:00
$ ssh root@192.168.5.152
root@192.168.5.152's password:
[Panda]# /sbin/insmod /root/oscillateur-gpio-timer.ko 
[Panda]#

Sur l’oscilloscope, nous voyons bien un signal carré sur la broche 10 du connecteur d’extension de la Pandaboard.

Oscillateur kernel avec API timer

Pourtant ce signal est plutôt décevant. Nous avons demandé au noyau d’invoquer notre routine qui bascule l’état de la sortie toutes les millisecondes, et nous observons que les fronts successifs du signal sont espacés d’un peu moins de 4 carreaux, ce qui correspond à 8 millisecondes (réglage Sweep sur 2ms/division).

Ceci s’explique par le fait que l’API timer du noyau s’appuie sur des ticks système. Or nous ne connaissons pas leur durée. Mais la constante symbolique HZ doit nous renseigner. Elle est configurée lors de la préparation de la compilation du noyau et se trouve donc dans fichier .config associé. Par habitude, j’active toujours les options « Kernel .config support » et « Enable access to .config through /proc/config.gz » (menu General setup) des noyaux que je compile afin d’embarquer une copie du fichier de configuration sur la cible.

[Panda]# zcat config.gz | grep CONFIG_HZ
CONFIG_HZ=128
[Panda]#

Le tick du système est donc de 1/128 secondes, ce qui correspond à 7,81 millisecondes. Le noyau ne peut pas nous fournir de timer de résolution plus fine avec cette configuration.

Retirons notre module du kernel et essayons d’améliorer le fonctionnement.

[Panda]# /sbin/rmmod /root/oscillateur-gpio-timer.ko 
[Panda]#

Les timers de haute précision du noyau Linux

Heureusement des timers de meilleure précision sont disponibles sous Linux. Ils ont été développés par Thomas Gleixner et Ingo Molnar dans le cadre de Linux-rt puis intégrés dans le noyau standard aux alentours de la version 2.6.26. Il s’agit de l’API <hrtimer.h> utilisable si l’option « High Resolution Timer Support » du menu « Kernel Features » a été activée lors de la compilation du kernel.

Cette fois la définition des durées se fait en nanosecondes (ce qui ne signifie évidemment pas que le noyau soit capable de respecter la précision de la nanoseconde). En voici un petit exemple, où la période peut être précisée au chargement du module (en microsecondes pour conserver une meilleure lisibilité).

oscillateur-gpio-hrtimer.c :
#include <linux/cdev.h>
#include <linux/device.h>
#include <linux/fs.h>
#include <linux/hrtimer.h>
#include <linux/gpio.h>
#include <linux/ktime.h>
#include <linux/module.h>
#include <linux/sched.h>
#include <linux/version.h>
#include <asm/uaccess.h>

static enum hrtimer_restart timer_oscillateur(struct hrtimer *);
static struct hrtimer htimer;

static int periode_us = 1000;
module_param(periode_us, int, 0644);
static ktime_t kt_periode;

// 138 Broche 10 du port J3 (Expansion A) de la Pandaboard
#define GPIO_OSCILLATEUR 138

static int __init init_oscillateur (void)
{
  int err;

  kt_periode = ktime_set(0, 1000 * periode_us);

  if ((err = gpio_request(GPIO_OSCILLATEUR, THIS_MODULE->name)) != 0) {
    return err;
  }
  if ((err = gpio_direction_output(GPIO_OSCILLATEUR, 1)) != 0) {
    gpio_free(GPIO_OSCILLATEUR);
    return err;
  }
  hrtimer_init (& htimer, CLOCK_REALTIME, HRTIMER_MODE_REL);
  htimer.function = timer_oscillateur;
  hrtimer_start(& htimer, kt_periode, HRTIMER_MODE_REL);

  return 0;
}

static void __exit exit_oscillateur (void)
{
  hrtimer_cancel(& htimer);
  gpio_free(GPIO_OSCILLATEUR);
}

static enum hrtimer_restart timer_oscillateur(struct hrtimer * unused)
{
  static int value = 0;
  gpio_set_value(GPIO_OSCILLATEUR, value);
  value = 1 - value;
  hrtimer_forward_now(& htimer, kt_periode);
  return HRTIMER_RESTART;
}

module_init(init_oscillateur);
module_exit(exit_oscillateur);
MODULE_LICENSE("GPL");

Après compilation (comme précédemment) et transfert sur la cible, nous chargeons le module.

[Panda]# /sbin/insmod /root/oscillateur-gpio-hrtimer.ko
[Panda]#

Nous voyons tout de suite la différence.

Timer Kernel HrTimer 1000 microsecondes

Notre oscillateur semble bien basculer toutes les millisecondes, puisqu’il y a un cycle complet sur chaque division durant 2ms. Vérifions avec un calibre plus précis.

Timer Kernel HrTimer 1000 microsecondes

Le hrtimer est bien déclenché toutes les millisecondes. Essayons de diminuer cette durée en commençant par une période de 100 microsecondes.

[Panda]# /sbin/rmmod /root/oscillateur-gpio-hrtimer.ko
[Panda]# /sbin/insmod /root/oscillateur-gpio-hrtimer.ko periode_us=100
[Panda]#

Le résultat est concluant. Le calibre de l’oscilloscope est cette fois sur 20 microsecondes/division.

Timer Kernel HrTimer 100 microsecondes

Continuons à diminuer la période avec 10 microsecondes.

[Panda]# /sbin/rmmod /root/oscillateur-gpio-hrtimer.ko
[Panda]# /sbin/insmod /root/oscillateur-gpio-hrtimer.ko periode_us=10
[Panda]#

L’oscillateur conserve sa précision. C’est remarquable.

Timer Kernel HrTimer 10 microsecondes

Toutefois lorsque la charge système augmente ou si de nombreuses interruptions surviennent, les fronts du signal se décalent sensiblement. Pour charger fortement en appels système notre carte, il y a une astuce simple : invoquer la commande :

[Panda]# top -d 0

Il y a alors des fluctuations assez importantes sur notre oscillateur. Voici une capture sur laquelle on voit que le niveau haut du signal dure près de onze microsecondes, et le niveau bas presque douze. Il y a d’autres fluctuations beaucoup plus grandes (mais difficiles à capturer !).

Timer Kernel HrTimer 10 microsecondes

Cette sensibilité aux interruptions et à la quantité d’appels système est typique des systèmes temps-réel souples comme le noyau Linux classique.

Un timer avec Xenomai et RTDM

Pour obtenir une meilleure tenue de notre signal, nous allons utiliser l’extension temps réel strict Xenomai. Pour l’installer sur la Pandaboard, on se reportera à cet article.

Xenomai offre une API pour le développement de drivers temps réel nommée RTDM (Real Time Driver Model). Nous en avons déjà eu un aperçu dans cet article. Nous allons utiliser ce principe pour basculer notre broche GPIO.

oscillateur-gpio-rtdm.c :
#include <linux/version.h>
#include <linux/device.h>
#include <linux/module.h>
#include <linux/sched.h>
#include <linux/cdev.h>
#include <linux/gpio.h>
#include <linux/fs.h>
#include <asm/uaccess.h>
#include <rtdm/rtdm_driver.h>

static int periode_us = 1000;
module_param(periode_us, int, 0644);

static void timer_oscillateur(rtdm_timer_t *);
static rtdm_timer_t rtimer;

// 138 Broche 10 du port J3 (Expansion A) de la Pandaboard
#define GPIO_OSCILLATEUR 138

static int __init init_oscillateur (void)
{
  int err;

  if ((err = gpio_request(GPIO_OSCILLATEUR, THIS_MODULE->name)) != 0) {
    return err;
  }
  if ((err = gpio_direction_output(GPIO_OSCILLATEUR, 1)) != 0) {
    gpio_free(GPIO_OSCILLATEUR);
    return err;
  }

  if ((err = rtdm_timer_init(& rtimer, timer_oscillateur, "Oscillateur")) != 0) {
    gpio_free(GPIO_OSCILLATEUR);
    return err;
  }

  if ((err = rtdm_timer_start(& rtimer, periode_us*1000, periode_us*1000, RTDM_TIMERMODE_RELATIVE)) != 0) {
    rtdm_timer_destroy(& rtimer);
    gpio_free(GPIO_OSCILLATEUR);
    return err;
  }
  return 0;
}

static void __exit exit_oscillateur (void)
{
  rtdm_timer_stop(& rtimer);
  rtdm_timer_destroy(& rtimer);
  gpio_free(GPIO_OSCILLATEUR);
}

static void timer_oscillateur(rtdm_timer_t * unused)
{
  static int value = 0;
  gpio_set_value(GPIO_OSCILLATEUR, value);
  value = 1 - value;
}

module_init(init_oscillateur);
module_exit(exit_oscillateur);
MODULE_LICENSE("GPL");

Il n’y a rien de compliqué dans ce module. Nous le compilons avec le même Makefile que précédemment, mais il doit pouvoir trouver les fichiers d’en-tête de Xenomai, en interrogeant le script xeno-config. Chargeons notre module (après avoir déchargé le précédent), en lui demandant directement une période de 10 microsecondes.

[Panda]# /sbin/rmmod /root/oscillateur-gpio-hrtimer.ko 
[Panda]# /sbin/insmod /root/oscillateur-gpio-rtdm.ko periode_us=10
[Panda]#

Nous observons que le signal est bien net et régulier, et ne fluctue pas avec la charge système.

 

Timer Xenomai RTDM de 10 microsecondes

Il est possible d’utiliser des périodes encore plus courtes. La capture ci-dessous a été obtenue avec un timer de période 2 microsecondes, alors qu’un top -d 0 tournait sur la carte.

Timer Xenomai RTDM à 2 microsecondes

Nous voyons que l’activité en appels système du noyau Linux (top) ne perturbe pas le timer implémenté par Xenomai.

Conclusion

Nous avons examiné plusieurs méthodes pour réaliser des sorties sur une broche GPIO de la Pandaboard. Il nous reste à présent à nous intéresser aux entrées… Ce sera l’objet du prochain article.

Si vous souhaitez plus d’information sur les aspects temps réel abordés ici, je ne peux que vous conseiller mon nouveau livre « Solutions temps réel sous Linux » qui sera disponible en librairie (quel heureux hasard) à partir de demain !

GPIO, Pandaboard et temps réel – 1 – Sorties depuis l’espace utilisateur

Embarqué, Linux, Microprocesseur, Shell, Temps-réel | Publié par cpb
mai 09 2012

GPIO Pandaboard et temps-réel

Les GPIO (General Purpose Input Output) sont des broches du microprocesseur permettant de réaliser des opérations d’entrée-sortie électriques programmables. Chaque broche peut être affectée en entrée ou en sortie par programmation et utilisée aisément pour communiquer avec des périphériques externes.

La Pandaboard utilise un processeur Texas OMAP4430, construit autour d’un coeur Cortex A9. La documentation techique de l’OMAP4430 (plus de 5500 pages…) précise que le processeur dispose de six modules GPIO, chacun offrant 32 lignes d’entrées-sorties. Sur les 192 ports GPIO théoriquement présents, une bonne partie est déjà employée pour la carte elle-même mais quelques uns sont accessibles à l’utilisateur.

Connecteurs d’extension

Nous trouvons sur la Pandaboard deux connecteurs utlisables pour des expérimentations et extensions futures. Numérotés J3 et J6, ils sont également repérés par les libellés « Expansion Connector A » et « Expansion Connector B » comme on peut le voir sur la photo ci-contre.Connecteurs d'extension de la Pandaboard.

Le détail des broches disponibles est fourni dans le manuel de référence de la Pandaboard (pages 43 et 44), certaines d’entre elles ayant une signification pour le système.

 

Par exemple, dans le tableau ci-dessous nous pouvons remarquer que la broche numéro 10 est associée à deux fonctionnalités : un signal Chip Select pour une communication avec une autre carte ou une entrée sortie GPIO numéro 138.

GPIO du connecteur A

Certaines broches ont des affectations figées car elles sont reliées à des composants sur la carte, mais d’autres peuvent être utilisées pour réaliser des entrées sorties personnalisées.

Nous remarquons la présence sur la broche numéro 1 d’un signal d’alimentation +1.8 V que nous pourrons utiliser pour l’envoyer sur les entrées de notre choix (dans le prochain article) et de la masse du signal sur les broches 27 et 28.

Attention, les opérations sur ce connecteur sont très risquées car elles se répercutent directement sur les broches du processeur, sans protection. Je décline donc toute responsabilité si une manipualtion décrite ici sonne le glas de votre Pandaboard. Pour vous convaincre encore, sachez que j’ai grillé une carte Pandaboard (enfin, un processeur OMAP4 mais c’est le principal) simplement en confondant la broche 2 (alimentation +5V) et la broche 1 (alimentation +1.8V) pour envoyer un signal sur une entrée GPIO. Donc : prudence et concentration sont de rigueur !

 Nous allons donc souder sur le connecteur quelques fils afin d’accéder facilement aux broches 1, 10 et 28.

Broches soudées sur connecteur d'extensionLa broche 1 de chaque connecteur (en haut à gauche lorsqu’on le regarde de face) est repérée par un carré, la 2 se trouve en dessous, la 3 à droite du 1, la 4 en dessous de la 3 et ainsi de suite.

Naturellement, la soudure se fait plutôt sur la face opposée, où les numérotations sont inversées.

Soudure au verso du connecteur d'extensionPour cet article, nous n’utiliserons pas l’alimentation (broche 1), mais j’ai profité de l’occasion pour la souder en même temps les deux autres.

Accès aux GPIO depuis l’espace utilisateur avec le shell

Démarrons notre carte en utilisant un système fait maison, comme décrit dans les articles numéro 1, numéro 2, numéro 3, numéro 4 et numéro 5 d’une précédente série. Puis connectons depuis un PC distant en utilisant le protocole SSH.

[~]$ ssh root@192.168.5.152
root@192.168.5.152's password:
[Panda]#

Le prompt du shell sur la Pandaboard est préfixé par « [Panda] » pour éviter les confusions avec le système hôte.

Vérifions pour commencer la tension sur la broche 10 avec un simple voltmètre.

GPIO en sortie à 0V

La tension est nulle, mais la broche est par défaut affectée en entrée (avec une haute impédance). Nous allons modifier cela.

[Panda]# cd /sys/class/gpio/
[Panda]# ls
export       gpio62       gpiochip128  gpiochip32   gpiochip96
gpio1        gpiochip0    gpiochip160  gpiochip64   unexport
[Panda]#

Par défaut, le port GPIO 138 n’est pas accessible depuis le pseudo système de fichier /sys/, mais nous pouvons le réclamer ainsi.

[Panda]# echo 138 > export 
[Panda]# ls
export       gpio138      gpiochip0    gpiochip160  gpiochip64   unexport
gpio1        gpio62       gpiochip128  gpiochip32   gpiochip96
[Panda]#

Le port 138 est maintenant accessible vérifions son sens de fonctionnement.

[Panda]# cd gpio138/
[Panda]# cat direction
in
[Panda]#

Il est affecté en entrée par sécurité au boot. Basculons-le en sortie.

[Panda]# echo out > direction 
[Panda]# cat direction 
out
[Panda]# cat value
0
[Panda]#

La valeur inscrite sur le port est 0, ce qui ne nous surprend pas puisque la tension sur la borne est nulle. Modifions la valeur.

[Panda]# echo 1 > value 
[Panda]#

Le voltmètre affiche maintenant 1,8 V (la tension de référence pour les GPIO, à ne pas dépasser pour les entrées !).

GPIO en sortie à 1,8 V.

Écrivons à nouveau sur le port.

[Panda]# echo 0 > value 
[Panda]#

Le voltmètre affiche à nouveau 0.00. Libérons le port GPIO pour terminer cette expérience.

[Panda]# cd /sys/class/gpio/
[Panda]# echo 138 > unexport 
[Panda]#

Application des GPIO avec un script shell

Nous savons écrire une valeur sur une broche de sortie depuis la ligne de commande, automatisons cela dans un petit script shell qui va faire osciller la broche 10.

/usr/local/bin/oscillateur-gpio.sh :
#! /bin/sh

GPIO=138  # Broche 10 du port "Expansion A"

cd /sys/class/gpio

echo ${GPIO} > export

cd gpio${GPIO}

echo out > direction

while true
do
	echo 1 > value
	usleep 1000
	echo 0 > value
	usleep 1000
done

Le script réalise de petites pause de 1000 microsecondes, soit une milliseconde après chaque changement d’état de la broche.

Connectons un oscilloscope à la place du voltmètre pour voir évoluer le signal.

Oscilloscope sur port de sortie GPIONous apercevons sur l’écran un signal périodique qui alterne entre 0 et 1,8V. Toutefois, en regardant de plus près, les durées que nous avions indiquées dans le script ne sont pas respectées.

Oscillateur GPIO par script shellLe signal change de niveau toutes les 3,3 millisecondes environ et non pas à chaque milliseconde. Il s’agit de la granularité du usleep qui pose problème.Nous pouvons améliorer ceci en utilisant un petit programme en C qui réalise le même travail mais s’appuie directement sur la libC en passant outre les latences dûes au shell et aux utilitaires système (issus de la Busybox en l’occurrence).

oscillateur-gpio-user.c :
#include <signal.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>

static volatile int exit_loop = 0;

void handler_signal(int unused)
{
    exit_loop = 1;
}

int main(int argc, char * argv[])
{
    int value = 1;
    char path[256];
    int gpio = 138;
    FILE * fp;

    // En argument on peut preciser un GPIO different de 138
    if ((argc > 2) || ((argc == 2) && (sscanf(argv[1], "%d", & gpio) != 1))) {
        fprintf(stderr, "usage: %s [gpio]\n", argv[0]);
        exit(EXIT_FAILURE);
    }

    // Exporter le GPIO dans le systeme de fichiers
    snprintf(path, 256, "/sys/class/gpio/export");
    if ((fp = fopen(path, "w")) == NULL) {
        perror(path);
        exit(EXIT_FAILURE);
    }
    fprintf(fp, "%d\n", gpio);
    fclose(fp);

    // Intercepter Controle-C pour finir proprement
    signal(SIGINT, handler_signal);

    // Basculer le GPIO en sortie
    snprintf(path, 256, "/sys/class/gpio/gpio%d/direction", gpio);
    if ((fp = fopen(path, "w")) == NULL) {
        perror(path);
        exit(EXIT_FAILURE);
    }
    fprintf(fp, "out\n");
    fclose(fp);

    // Ecrire alternativement la valeur du GPIO
    snprintf(path, 256, "/sys/class/gpio/gpio%d/value", gpio);
    if ((fp = fopen(path, "w")) == NULL) {
        perror(path);
        exit(EXIT_FAILURE);
    }
    while (! exit_loop) {
        fprintf(fp, "%d\n", value);
        fflush(fp);
        value = 1 - value;
        usleep(1000);
    }

    // De-exporter le GPIO du systeme de fichiers
    if ((fp = fopen("/sys/class/gpio/unexport", "w")) == NULL) {
        perror("/sys/class/gpio/unexport");
        exit(EXIT_FAILURE);
    }
    fprintf(fp, "%d\n", gpio);
    fclose(fp);
    return EXIT_SUCCESS;
}

Après compilation (en utilisant un cross-compiler comme indiqué dans cet article), nous transférons le code sur la cible.

$ ~/cross-panda/usr/bin/arm-linux-gcc -Wall -o oscillateur-gpio-user oscillateur-gpio-user.c
$ scp oscillateur-gpio-user root@192.168.5.152:/root/
root@192.168.5.152's password:
oscillateur-gpio-user                                               100% 6270     6.1KB/s   00:00
$

Sur la cible nous lançons le programme (et nous pouvons l’arrêter avec Contrôle-C).

[Panda]# /root/oscillateur-gpio-user 
    (Contrôle-C)
[Panda]#

Le signal est plus précis, nous pourrions calibrer la durée des sommeils pour avoir exactement une milliseconde, ou utiliser un timer logiciel comme ceux founis par setitimer().

Oscillateur GPIO par programme C - Temps partagé

Toutefois, sur l’écran de l’oscilloscope, le signal n’est pas très stable, ses fronts montants et descendants tremblent en permanence, signe classique d’un comportement lié à l’ordonnancement en temps partagé.

Pour améliorer la précision nous pouvons lancer le programme en ordonnancement temps réel souple avec chrt.

[Panda]# chrt -f99 /root/oscillateur-gpio-user
    (Contrôle-C)
[Panda]#

Oscillateur GPIO par programme C - Temps réel souple

Le signal est beaucoup plus stable, mais il y a encore des petits « sauts » de temps à autres. Avec un oscilloscope à mémoire, nous pourrions voir apparaître parfois des fronts décalés d’une durée assez significative.

Conclusion

Afin d’améliorer la précision d’un signal, il est nécessaire d’adopter une approche temps réel. Ceci peut se réaliser de différentes manières. Dans le prochain article nous verrons comment gérer les GPIO depuis l’espace kernel en écrivant un module pour le noyau Linux standard, puis nous nous rapprocherons du temps réel strict en pilotant les GPIO depuis un module kernel de Xenomai, avec l’API RTDM.

Mesure de précision des timers de RTDM / Xenomai

Linux, Temps-réel | Publié par cpb
mai 07 2012

Cette petite expérience va nous permettre de mesurer la précision des timers kernel programmés en utilisant l’API RTDM (Real Time Driver Model) proposée par Xenomai dans sa version 2.6.0. Il est important de noter que RTDM est une spécification pour le développement de drivers temps-réel pour Linux qui pourra prochainement être utilisée directement avec le noyau « patché » Linux-rt. Il est donc utile de commencer à s’intéresser à cette API pour tout les développements kernel ayant spécifiquement trait au temps réel.

Module dans l’espace noyau

Notre module va installer un timer avec une période d’une milliseconde. A chaque déclenchement, le timer mesurera l’écart par rapport au déclenchement précédent et proposera ses résultats dans une interface RTDM accessible depuis l’espace utilisateur.

Voici un exemple d’implémentation, les fichiers sources se trouvent dans cette archive.

mesure-periode-rtdm.c: 
#include <linux/version.h>
#include <linux/device.h>
#include <linux/module.h>
#include <linux/sched.h>
#include <linux/cdev.h>
#include <linux/fs.h>

#include <asm/uaccess.h>

#include <rtdm/rtdm_driver.h>

#define nom_device_rtdm "mesure_periode_rtdm"

static int read_mesure_periode(struct rtdm_dev_context * contexte, rtdm_user_info_t * info, void * buffer, size_t lg);
static int open_nonrt_mesure_periode (struct rtdm_dev_context * contexte, rtdm_user_info_t * info, int flags);
static int close_nonrt_mesure_periode (struct rtdm_dev_context * contexte, rtdm_user_info_t * info);
static void handler_mesure_periode (rtdm_timer_t * timer);

static struct rtdm_device rtdev_mesure_periode = {
    .struct_version = RTDM_DEVICE_STRUCT_VER,
    .device_flags   = RTDM_NAMED_DEVICE,
    .context_size   = 0,
    .device_name    = nom_device_rtdm,
    .open_nrt       = open_nonrt_mesure_periode,
    .ops = {
        .close_nrt = close_nonrt_mesure_periode,
        .read_nrt  = read_mesure_periode,
    },
    .device_class     = RTDM_CLASS_TESTING,
    .device_sub_class = 1,
    .profile_version  = 1,
    .driver_name      = nom_device_rtdm,
    .driver_version   = RTDM_DRIVER_VER(1,0,0),
    .peripheral_name  = nom_device_rtdm,
    .provider_name    = "cpb",
    .proc_name        = nom_device_rtdm,
};

static nanosecs_abs_t  periode_mini;
static nanosecs_abs_t  periode_maxi;
static nanosecs_abs_t  somme_periodes;
static int             nb_periodes;
static rtdm_mutex_t mtx_statistiques;

static rtdm_timer_t rtimer;
static nanosecs_rel_t periode_desiree = 1000000; // 1 ms

static int __init init_mesure_periode (void)
{
    int err;

    rtdm_mutex_init(& mtx_statistiques);

    periode_mini = -1;
    periode_maxi = 0;
    somme_periodes = 0;
    nb_periodes = 0;

    rtdm_dev_register(& rtdev_mesure_periode);
    if ((err = rtdm_timer_init(& rtimer, handler_mesure_periode, "Timer_mesure_periode")) != 0) {
        rtdm_dev_unregister(& rtdev_mesure_periode, 0);
        rtdm_mutex_destroy(& mtx_statistiques);
        return err;
    }

    if ((err = rtdm_timer_start(& rtimer, 1000000000, periode_desiree, RTDM_TIMERMODE_RELATIVE)) != 0) {
        rtdm_timer_destroy(& rtimer);
        rtdm_dev_unregister(& rtdev_mesure_periode, 0);
        rtdm_mutex_destroy(& mtx_statistiques);
        return err;
    }

    return 0;
}

static void __exit exit_mesure_periode(void)
{
    rtdm_timer_stop(& rtimer);
    rtdm_timer_destroy(& rtimer);
    rtdm_dev_unregister(& rtdev_mesure_periode, 0);
    rtdm_mutex_destroy(& mtx_statistiques);
}

static int read_mesure_periode(struct rtdm_dev_context * contexte, rtdm_user_info_t * info, void * buffer, size_t lg)
{
    char local_buffer[80];
    nanosecs_abs_t  _periode_mini;
    nanosecs_abs_t  _periode_maxi;
    nanosecs_abs_t  _somme_periodes;
    int             _nb_periodes;

    rtdm_mutex_lock(& mtx_statistiques);
    _periode_mini   = periode_mini;
    _periode_maxi   = periode_maxi;
    _somme_periodes = somme_periodes;
    _nb_periodes    = nb_periodes;
    rtdm_mutex_unlock(& mtx_statistiques);

    snprintf(local_buffer, 80, "%lld %lld %lld %d\n",
        _periode_mini, _periode_maxi, _somme_periodes, _nb_periodes);
    if (rtdm_safe_copy_to_user(info, buffer, local_buffer, strlen(local_buffer)) != 0)
        return -EFAULT;
    return strlen(local_buffer);
}

static int open_nonrt_mesure_periode(struct rtdm_dev_context * contexte, rtdm_user_info_t * info, int flags)
{
    return 0;
}

static int close_nonrt_mesure_periode(struct rtdm_dev_context * contexte, rtdm_user_info_t * info)
{
    return 0;
}

static void handler_mesure_periode (rtdm_timer_t * timer)
{
    nanosecs_abs_t         heure;
    static nanosecs_abs_t  precedente;
    nanosecs_abs_t         periode;

    heure = rtdm_clock_read();
    rtdm_mutex_lock(& mtx_statistiques);
    if (nb_periodes > 0) {
        periode = heure - precedente;
        if (periode > periode_maxi)
            periode_maxi = periode;
        if ((periode_mini == -1) || (periode_mini > periode))
            periode_mini = periode;
        somme_periodes += periode;
    }
    nb_periodes ++;
    rtdm_mutex_unlock(& mtx_statistiques);
    precedente = heure;
}

module_init(init_mesure_periode);
module_exit(exit_mesure_periode);
MODULE_LICENSE("GPL");

Compilons ce module, chargeons-le et vérifions qu’il soit bien enregistré. Naturellement, il est nécessaire d’avoir installé un noyau Linux/Xenomai comme décrit dans cet article ou celui-ci.

# make
make -C /lib/modules/2.6.38.8-xenomai/build SUBDIRS=/home/cpb/Documents/Livres/Articles/Blog/article-2012-05-07  modules
make[1]: entrant dans le répertoire « /usr/local/src/linux-2.6.38.8-xenomai »
  CC [M]  /home/cpb/Documents/Livres/Articles/Blog/article-2012-05-07/mesure-periode-rtdm.o
  Building modules, stage 2.
  MODPOST 1 modules
  CC      /home/cpb/Documents/Livres/Articles/Blog/article-2012-05-07/mesure-periode-rtdm.mod.o
  LD [M]  /home/cpb/Documents/Livres/Articles/Blog/article-2012-05-07/mesure-periode-rtdm.ko
make[1]: quittant le répertoire « /usr/local/src/linux-2.6.38.8-xenomai »
gcc -I/usr/xenomai/include -D_GNU_SOURCE -D_REENTRANT -Wall -Werror-implicit-function-declaration -pipe -D__XENO__ -lrtdm -L/usr/xenomai/lib -lxenomai -lpthread -lrt -L /usr/xenomai/lib -o mesure-periode mesure-periode.c -lrtdm -lxenomai
# insmod ./mesure-periode-rtdm.ko 
# cat /proc/xenomai/rtdm/named_devices
Hash	Name				Driver		/proc
24	rttest-timerbench0             	xeno_timerbench	rttest-timerbench0
55	rttest-switchtest0             	xeno_switchtest	rttest-switchtest0
EE	mesure_periode_rtdm            	mesure_periode_rtdm	mesure_periode_rtdm
#

Application dans l’espace utilisateur

Contrairement aux drivers Linux habituels, RTDM ne fournit pas de fichier spécial (avec un type, un numéro majeur et un numéro mineur) pour accéder aux fonctionnalités implémentés. Le driver n’est pas accessible par un appel open() sur un fichier de /dev mais par une opération spécifique rt_dev_open() employant le nom du périphérique.

Voici un petit utilitaire qui ouvre le périphérique mesure_periode_rtdm et lit les valeurs. Il affiche sur sa sortie standard les informations obtenues.

mesure-periode.c: 
#include <fcntl.h>
#include <stdlib.h>
#include <stdio.h>
#include <string.h>
#include <unistd.h>

#include <rtdm/rtdm.h>

#define LG_BUFFER 80

int main (int argc, char * argv[])
{
    int fd;
    int i;
    char buffer[LG_BUFFER];
    long long int min, max, somme;
    int nb;
    fd = rt_dev_open("mesure_periode_rtdm", O_RDONLY);
    if (fd < 0) {
        fprintf(stderr, "%s: %s\n", argv[1], strerror(-fd));
        exit(EXIT_FAILURE);
    }

    while ((i = rt_dev_read(fd, buffer, LG_BUFFER)) > 0) {
        buffer[i] ='\0';
        if (sscanf(buffer, "%lld %lld %lld %d", &min, &max, &somme, &nb) == 4)
            printf("min = %lld, max = %lld, moy = %lld\n", min, max, somme / nb);
    }
    rt_dev_close(fd);
    return EXIT_SUCCESS;
}

Nous pouvons lancer le programme, il affichera sur sa sortie standard les valeurs statistiques obtenues.

# export LD_LIBRARY_PATH=/usr/xenomai/lib/
# ./mesure-periode
[...]
min = 968234, max = 1036666, moy = 999995
min = 968234, max = 1036666, moy = 999995
min = 968234, max = 1036666, moy = 999995
min = 968234, max = 1036666, moy = 999995
min = 968234, max = 1036666, moy = 999995
min = 968234, max = 1036666, moy = 999995
min = 968234, max = 1036666, moy = 999995
min = 968234, max = 1036666, moy = 999995
min = 968234, max = 1036666, moy = 999995
[...]
#

Résultats

Sur ce poste, nous voyons que le timer à 1kHz a subi des fluctuations maximales de 37 microsecondes environ, après un quart d’heure de fonctionnement. La machine n’était pas excessivement chargée, ni en interruptions ni en processus. Avec une charge beaucoup plus élevée, on pourrait probablement s’attendre à des fluctuations approchant la centaine de microsecondes.

Cette exécution s’est faite sur un poste de bureautique d’entrée de gamme, qui n’est donc pas particulièrement conçu pour le temps réel (notamment dans la gestion des interruptions SMI). Avec un PC industriel et un noyau bien configuré, j’ai déjà observé des fluctuations inférieures à 35 microsecondes sur deux jours alors que la machine était très fortement chargée, tant en activité CPU (processus, appel-système) qu’en interruptions diverses.

Si vous obtenez des résultats intéressants avec cette expérience, communiquez-les moi et je serai heureux de les faire figurer ici.

Xenomai 2.6.0 sur Ubuntu 12.04

Linux, Temps-réel | Publié par cpb
mai 02 2012

Xenomai 2.6.0 sur Ubuntu 12.04J’ai vérifié hier que Xenomai 2.6.0 (la dernière version stable à ce jour) s’installe bien sur la nouvelle distribution Ubuntu. Pas de souci, il suffit de procéder comme pour la distribution précédente (voir cet article) en utilisant ce fichier de configuration pour le noyau Linux 2.6.38.8.