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

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 !

URL de trackback pour cette page