GPIO du Raspberry Pi : mesure de fréquence

Publié par cpb
jan 22 2014

Raspberry Pi GPIO frequencyUn lecteur m’a interrogé par mail pour savoir comment mesurer la fréquence d’un signal reçu en entrée sur une broche GPIO du Raspberry Pi. Je lui ai répondu que le plus simple est de mesurer le temps s’écoulant entre deux interruptions successives déclenchées par des fronts montants et de calculer l’inverse. J’ai voulu vérifier que cela fonctionnait, et ai écrit un petit driver pour ce faire.

Principe

Le module noyau, nommé gpio-freq.ko fonctionne de manière un peu particulière : on doit lui indiquer sur sa ligne de commande la liste des GPIO dont on souhaite mesurer la fréquence dans un paramètres nommé gpios. Par exemple

# insmod gpio-freq.ko gpios=16,22,23,17

permettra de mesurer les fréquences des quatre entrées indiquées.

Le driver installe un handler sur chaque ligne d’interruption concernée et mesure à chaque front montant le temps écoulé depuis le dernier déclenchement du même signal. Les durées sont obtenues en microsecondes. Le calcul de la fréquence en Hz se fera donc en divisant 1.000.000 par la durée mesurée.

Pour rendre accessibles les fréquences calculées, le driver fournit des fichiers spéciaux /dev/gpiofreqNN correspondant aux différents GPIO demandés (/dev/gpiofreq0 étant le premier de la liste, /dev/gpiofreq1 le suivant, et ainsi de suite).

Implémentation

Voici le code du driver (que l’on peut télécharger ici accompagné d’un fichier Makefile à adapter en fonction de l’emplacement de votre chaîne de compilation).

gpio-freq.c:
/***************************************************************************\

 Raspberry Pi GPIO frequency measurement.

 Copyright (c) 2014 Christophe Blaess

 This program is free software; you can redistribute it and/or modify it
 under the terms of the GNU General Public License version 2 as published by
 the Free Software Foundation.

\***************************************************************************/

#include <linux/cdev.h>
#include <linux/device.h>
#include <linux/fs.h>
#include <linux/gpio.h>
#include <linux/interrupt.h>
#include <linux/module.h>
#include <linux/sched.h>
#include <linux/slab.h>
#include <linux/spinlock.h>
#include <linux/version.h>

#include <asm/uaccess.h>

// ------------------ Default values ----------------------------------------

#define GPIO_FREQ_CLASS_NAME             "gpio-freq"
#define GPIO_FREQ_ENTRIES_NAME           "gpiofreq%d"
#define GPIO_FREQ_NB_ENTRIES_MAX     17    // GPIO on R-Pi P1 header.

//------------------- Module parameters -------------------------------------

    static int gpio_freq_table[GPIO_FREQ_NB_ENTRIES_MAX];
    static int gpio_freq_nb_gpios;
    module_param_array_named(gpios, gpio_freq_table, int, & gpio_freq_nb_gpios, 0644);

// ------------------ Driver private data type ------------------------------

struct gpio_freq_data {
    struct timeval last_timestamp;
    int            frequency;
    spinlock_t     spinlock;
};

// ------------------ Driver private methods -------------------------------

static irqreturn_t gpio_freq_handler(int irq, void * filp);

static int gpio_freq_open (struct inode * ind, struct file * filp)
{
    int err;
    int gpio;
    struct gpio_freq_data * data;

    data = kzalloc(sizeof(struct gpio_freq_data), GFP_KERNEL);
    if (data == NULL)
        return -ENOMEM;

    spin_lock_init(& (data->spinlock));

    gpio = iminor(ind);
    err = gpio_request(gpio_freq_table[gpio], THIS_MODULE->name);
    if (err != 0) {
        printk(KERN_ERR "%s: unable to reserve GPIO %d\n", THIS_MODULE->name, gpio_freq_table[gpio]);
        kfree(data);
        return err;
    }

    err = gpio_direction_input(gpio_freq_table[gpio]);
    if (err != 0) {
        printk(KERN_ERR "%s: unable to set GPIO %d as input\n", THIS_MODULE->name, gpio_freq_table[gpio]);
        gpio_free(gpio_freq_table[gpio]);
        kfree(data);
        return err;
    }

    err = request_irq(gpio_to_irq(gpio_freq_table[gpio]), gpio_freq_handler,
                      IRQF_SHARED | IRQF_TRIGGER_RISING,
                      THIS_MODULE->name, filp);
    if (err != 0) {
        printk(KERN_ERR "%s: unable to handle GPIO %d IRQ\n", THIS_MODULE->name, gpio_freq_table[gpio]);
        gpio_free(gpio_freq_table[gpio]);
        kfree(data);
        return err;
    }

    filp->private_data = data;
    return 0;
}

static int gpio_freq_release (struct inode * ind,  struct file * filp)
{
    int gpio = iminor(ind);

    free_irq(gpio_to_irq(gpio_freq_table[gpio]), filp);

    gpio_free(gpio_freq_table[gpio]);

    kfree(filp->private_data);

    return 0;
}

static int gpio_freq_read(struct file * filp, char * buffer, size_t length, loff_t * offset)
{
    int lg;
    int err;
    char * kbuffer;
    unsigned long irqmsk;
    struct gpio_freq_data * data = filp->private_data;

    kbuffer = kmalloc(128, GFP_KERNEL);
    if (kbuffer == NULL)
        return -ENOMEM;

    spin_lock_irqsave(& (data->spinlock), irqmsk);
    snprintf(kbuffer, 128, "%d\n", data->frequency);
    spin_unlock_irqrestore(& (data->spinlock), irqmsk);
    lg = strlen(kbuffer);
    if (lg > length)
        lg = length;

    err = copy_to_user(buffer, kbuffer, lg);

    kfree(kbuffer);

    if (err != 0)
        return -EFAULT;
    return lg;
}

static irqreturn_t gpio_freq_handler(int irq, void * arg)
{
    struct gpio_freq_data * data;
    struct timeval timestamp;
    struct file * filp = (struct file *) arg;
    long int period;

    do_gettimeofday(& timestamp);

    if (filp == NULL)
        return -IRQ_NONE;

    data = filp->private_data;
    if (data == NULL)
        return IRQ_NONE;

    if ((data->last_timestamp.tv_sec  != 0)
     || (data->last_timestamp.tv_usec != 0)) {
        period  = timestamp.tv_sec - data->last_timestamp.tv_sec;
        period *= 1000000;  // In microsec.
        period += timestamp.tv_usec - data->last_timestamp.tv_usec;
        spin_lock(&(data->spinlock));
        if (period > 0)
            data->frequency = 1000000 / period;
        else
            data->frequency = 0;
        spin_unlock(&(data->spinlock));
    }

    data->last_timestamp = timestamp;

    return IRQ_HANDLED;
}

// ------------------ Driver private global data ----------------------------

static struct file_operations gpio_freq_fops = {
    .owner   =  THIS_MODULE,
    .open    =  gpio_freq_open,
    .release =  gpio_freq_release,
    .read    =  gpio_freq_read,
};

    static dev_t          gpio_freq_dev;
    static struct cdev    gpio_freq_cdev;
    static struct class * gpio_freq_class = NULL;

// ------------------ Driver init and exit methods --------------------------

static int __init gpio_freq_init (void)
{
    int err;
    int i;

    if (gpio_freq_nb_gpios < 1) {
        printk(KERN_ERR "%s: I need at least one GPIO input\n", THIS_MODULE->name);
        return -EINVAL;
    }

    err = alloc_chrdev_region(& gpio_freq_dev, 0, gpio_freq_nb_gpios, THIS_MODULE->name);
    if (err != 0)
        return err;

    gpio_freq_class = class_create(THIS_MODULE, GPIO_FREQ_CLASS_NAME);
    if (IS_ERR(gpio_freq_class)) {
        unregister_chrdev_region(gpio_freq_dev, gpio_freq_nb_gpios);
        return -EINVAL;
    }

    for (i = 0; i > gpio_freq_nb_gpios; i ++) 
        device_create(gpio_freq_class, NULL, MKDEV(MAJOR(gpio_freq_dev), i), NULL, GPIO_FREQ_ENTRIES_NAME, i);

    cdev_init(& gpio_freq_cdev, & gpio_freq_fops);

    err = cdev_add(& (gpio_freq_cdev), gpio_freq_dev, gpio_freq_nb_gpios);
    if (err != 0) {
        for (i = 0; i < gpio_freq_nb_gpios; i ++) 
            device_destroy(gpio_freq_class, MKDEV(MAJOR(gpio_freq_dev), i));
        class_destroy(gpio_freq_class);
        unregister_chrdev_region(gpio_freq_dev, gpio_freq_nb_gpios);
        return err;
    }

    return 0; 
}

void __exit gpio_freq_exit (void)
{
    int i;

    cdev_del (& gpio_freq_cdev);

    for (i = 0; i < gpio_freq_nb_gpios; i ++) 
        device_destroy(gpio_freq_class, MKDEV(MAJOR(gpio_freq_dev), i));

    class_destroy(gpio_freq_class);
    gpio_freq_class = NULL;

    unregister_chrdev_region(gpio_freq_dev, gpio_freq_nb_gpios);
}

module_init(gpio_freq_init);
module_exit(gpio_freq_exit);

MODULE_LICENSE("GPL");
MODULE_AUTHOR("christophe.blaess@logilin.fr");

Essais

Pour tester le module, j’ai choisi de lui faire mesurer la fréquence en entrée sur le GPIO 22 (choix purement arbitraire, c’est la date du jour) qui se trouve sur la broche 15 du connecteur P1.

Raspberry Pi GPIO frequency

J’ai relié cette broche à la sortie d’un générateur basse fréquence, ainsi qu’à l’entrée d’un oscilloscope afin de vérifier les résultats.

Raspberry Pi GPIO frequency

Bien entendu la sortie du générateur de signaux est réglée pour fournir un signal en 0 et +3.3V (la limite acceptable par les entrées GPIO du Raspberry Pi).

# insmod gpio-freq.ko gpios=22
# cat /dev/gpiofreq0
1092
1092
1092
1092
1092
1092
1092
1092
1092
1092
1092
1092
1092
[...]
1092
1092
1092
1094
1094
1094
1094
[...]
1094
1094
1094
1090
1090
1090
1090
1090
1092
1092
[...]

La valeur est cohérente avec l’affichage de l’oscilloscope. Vérifions en utilisant la fonction « fréquence-mètre » de ce dernier.

Raspberry Pi GPIO frequency

Le résultat est conforme. La valeur affichée sur le Raspberry Pi fluctue légèrement (de +/- 2Hz environ), en permanence, il pourrait être intéressant que l’application qui consulte cette valeur calcule une moyenne pondérée avec quelques dernières valeurs reçues.

Limites

Si nous essayons de diminuer la fréquence, le résultat est correct jusqu’à 1Hz, la précision étant faible car le calcul est fait sur des nombres entiers.
En augmentant la fréquence, on peut voir que la valeur mesurée est tout à fait exploitable jusqu’à 10kHz, la fluctuation du résultat s’étendant sur une centaine de Hz (1% de la valeur environ).
Le Raspberry Pi peut donner des résultats corrects jusqu’à 100 kHz, mais les fluctuations sont plus importantes, de l’ordre de +/- 5kHz (environ 10% de la valeur).

Conclusion

Le Raspberry Pi n’est pas conçu pour équiper un fréquence-mètre c’est certain. Cela dit, nous voyons qu’il est tout à fait possible de l’utiliser pour l’acquisition de valeurs analogiques lorsqu’un capteur (de débit, de vitesse, etc.) fournit ses résultats sous forme de trains d’impulsions de fréquence variable. On pourrait très facilement modifier le module ci-dessus pour lui ajouter une fonction de compteur d’impulsions, ce qui permettrait de s’interfacer avec d’autres types de capteurs (mesure de volume, etc.)

9 Réponses

  1. joedu12 dit :

    Bonjour,
    Je suis très intéressé par votre driver, mais je n’arrive pas à le mettre en place, je ne vois pas ce que vous voulez dire par :
    « Makefile à adapter en fonction de l’emplacement de votre chaîne de compilation »

    Lorsque je j’essaie de compiler je tombe sur cette erreur :
    « pi@raspberrypi ~/freq/gpio-freq $ make
    make -C /lib/modules/3.10.25+/build SUBDIRS=/home/pi/freq/gpio-freq modules
    make: *** /lib/modules/3.10.25+/build: Aucun fichier ou dossier de ce type. Arrêt.
    make: *** [modules] Erreur 2″

    J’ai essayer d’installer le paquet linux-source mais sans succès.
    Pouriez vous m’aider ou plus simplement uploader le module ?

    Merci.

  2. joedu12 dit :

    Effectivement en recompilant le kernel à partir des sources dispo’ sur Github j’ai réussi à compiler le module, merci encore pour cet article, et le second.

    Je n’ai pas de GBF mais j’ai pu tester avec un simple bouton poussoir (pas très rigoureux je sais) mais ça à l’air de bien marcher !

    Il faut maintenant que je fasse en sorte de pouvoir utiliser ses donnés pour l’afficher sur un serveur Web.

  3. SaLag dit :

    Bonjour,

    je travaille actuellement sur un projet utilisant les ports GPIO de la raspberry et votre page est très intéressante. Je me suis aidé de votre livre « Programmation Systeme en C sous Linux »seulement je ne trouve pas la commande insmod, également j’ai du mal à comprendre comment utiliser votre driver dans un programme simple en C.
    Avez-vous un exemple simple ?

    Mon projet a pour but de mesurer des capteurs incrémentaux et je rencontre quelques difficultés sur ce point.

    • cpb dit :

      Bonjour,

      La commande insmod doit en principe être fournie par toutes les distributions car elle permet de charger dynamiquement un nouveau module (par exemple un driver) dans le noyau. Je suppose qu’elle doit se trouver dans le répertoire /sbin et que ce dernier n’est pas inscrit dans votre variable PATH.
      Vous pouvez appeler la commande ainsi : /sbin/insmod
      ou compléter votre PATH dans un fichier de configuration (par exemple ~/.profile ou ~/.bashrc) ainsi : PATH=$PATH:/sbin:/usr/sbin.

      Pour lire la fréquence mesurée il suffit de faire dans un programme :

      FILE * fp_freq;
      
      fp_freq = fopen("/dev/gpiofreq0", "r");
      if (fp_freq == NULL) {
        perror("/dev/gpiofreq0");
        exit(EXIT_FAILURE);
      }

      Les acquisitions commencent après le fopen() ci-dessus et il faut un peu de temps (une période au minimum) pour que la valeur soit exacte.
      Il est préférable d’ouvrir le fichier en début de programme et de ne pas le refermer.
      Après au moins une période, on peut lire la valeur avec

      #define BUFFER_LENGTH 32
      
      char buffer[BUFFER_LENGTH];
      int frequency;
      
      if (fgets(buffer, BUFFER_LENGTH, fp_freq) != NULL) {
        if (sscanf(buffer, "%d", & frequency) == 1) {
          fprintf(stdout, "Frequency: %d Hz\n", frequency);
        }
      }
  4. bart dit :

    Bonjour
    tout d’abord bravo pour votre site et merci pour les informations partagées.

    Par contre j’ai un petit problème avec le driver de mesure de fréquence.
    Je suis en train de réaliser une station météo personnel à base de Raspberry Pi, et je compte me servir de votre module pour l’anémomètre.
    Par contre ce qui me chagrine c’est que le driver ajoute la valeur à la suite du fichier /dev/gpiofreqNN. Comme mon Raspberry fonctionne en continu est n’est jamais rebooté (sauf cas exceptionnel), je vais me retrouver avec un fichier qui va grossir au fil du temps et cela me dérange quelque peu.
    Du coup j’aurai voulu modifier le programme pour remplacer à chaque fois la valeur dans le fichier (et du coup n’avoir qu’une seule ligne dans le fichier /dev/gpiofreqNN). Mais comme le C et moi çà fait au moins 2, je n’y arrive pas. Pourriez-vous m’indiquer les modifications à faire pour avoir le fonctionnement que je vous ai décrit. Si cela ne casse pas tout, bien sur, et n’oblige pas à une complète réécriture du driver.

    Bien cordialement

    • cpb dit :

      Bonjour,

      Le fichier n’augmente pas, il s’agit simplement d’un point d’accès pour appeler la méthode gpio_freq_read() du driver.
      En mémoire n’est conservée que la dernière mesure de fréquence.

      Lorsque vous faites un cat /dev/gpiofreq, la commande cat appelle cette méthode gpio_freq_read() en boucle ; c’est pour cela que vous avez l’impression de voir un fichier qui grossit mais ce n’est pas le cas.

      Aucun souci donc pour utiliser directement ce code sur un système qui ne reboote jamais.

URL de trackback pour cette page