M2 SETI B4 / MS SE SE758


TP Modèle de périphérique

Introduction

Objectifs

L’objectif de ce TP est d’écrire la première partie d’un pilote pour un périphérique I2C :

et de le tester pour vérifier que tout fonctionne. Les prochains TP auront pour but de compléter ce pilote (communication avec l’espace utilisateur, interruptions, etc.).

Pré-requis

Environnement de TP

Nous allons reprendre le répertoire créé lors des TP précédents.

$ export TPROOT=xxx  # À adapter
$ cd $TPROOT

Pensez à adapter les commandes ci-dessus en fonction du nom donné au répertoire lors du premier TP.

Vous devez avoir gardé du TP précédent :

Squelette d’un pilote I2C

Code

Vous trouverez ci-dessous le squelette d’un pilote pour un périphérique I2C.

Notez que le nom des fonctions et des structures commence par foo. Par convention, on remplacera foo par le nom du périphérique (par exemple, pour un pilote pour le périphérique ADXL345, on nommera les fonctions adxl345_probe, adxl345_remove, etc. et les structures adxl345_idtable, adxl345_of_match, etc.).

Le contenu et le rôle de chaque partie de ce squelette a normalement été vu en cours.

#include <linux/init.h>
#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/of.h>
#include <linux/i2c.h>

static int foo_probe(struct i2c_client *client,
                     const struct i2c_device_id *id)
{
    /* ... */
}

static int foo_remove(struct i2c_client *client)
{
    /* ... */
}

/* La liste suivante permet l'association entre un périphérique et son
   pilote dans le cas d'une initialisation statique sans utilisation de
   device tree.

   Chaque entrée contient une chaîne de caractère utilisée pour
   faire l'association et un entier qui peut être utilisé par le
   pilote pour effectuer des traitements différents en fonction
   du périphérique physique détecté (cas d'un pilote pouvant gérer
   différents modèles de périphérique).
*/
static struct i2c_device_id foo_idtable[] = {
    { "foo", 0 },
    { }
};
MODULE_DEVICE_TABLE(i2c, foo_idtable);

#ifdef CONFIG_OF
/* Si le support des device trees est disponible, la liste suivante
   permet de faire l'association à l'aide du device tree.

   Chaque entrée contient une structure de type of_device_id. Le champ
   compatible est une chaîne qui est utilisée pour faire l'association
   avec les champs compatible dans le device tree. Le champ data est
   un pointeur void* qui peut être utilisé par le pilote pour
   effectuer des traitements différents en fonction du périphérique
   physique détecté.
*/
static const struct of_device_id foo_of_match[] = {
    { .compatible = "vendor,foo",
      .data = NULL },
    {}
};

MODULE_DEVICE_TABLE(of, foo_of_match);
#endif

static struct i2c_driver foo_driver = {
    .driver = {
        /* Le champ name doit correspondre au nom du module
           et ne doit pas contenir d'espace */
        .name   = "foo",
        .of_match_table = of_match_ptr(foo_of_match),
    },

    .id_table       = foo_idtable,
    .probe          = foo_probe,
    .remove         = foo_remove,
};

module_i2c_driver(foo_driver);

MODULE_LICENSE("GPL");
MODULE_DESCRIPTION("Foo driver");
MODULE_AUTHOR("Me");

Makefile

Pour rappel (voir le TP sur l’écriture du premier module), voici le fichier Makefile nécessaire pour compiler le module (en supposant que le fichier source est nommé foo.c) :

ifneq ($(KERNELRELEASE),)
# kbuild part of makefile
obj-m  := foo.o

else
# normal makefile
KDIR ?= /lib/modules/`uname -r`/build

default:
    $(MAKE) -C $(KDIR) M=$$PWD
endif

Attention ! La ligne $(MAKE) -C... qui indique la commande à exécuter pour construire la cible default doit être indentée à l’aide d’une tabulation et non d’un ou plusieurs espaces.

L’accéléromètre ADXL345

La carte de TP que nous aurions du utiliser est équipée d’un accéléromètre 3 axes (ADXL345 d’Analog Devices) connecté au contrôleur I2C0 via un bus I2C. L’accéléromètre dispose également d’une sortie d’interruption, reliée directement à une des entrées du FPGA.

À défaut de pouvoir réaliser en présentiel ce TP, la version spéciale de QEMU que vous avez récupérée lors du TP précédent contient un modèle (une simulation) de cet accéléromètre.

Ce modèle se comporte de façon très proche de l’accéléromètre physique, à quelques détails près :

Voici comment sont générées les données. À chaque mesure, un compteur c (initialisé à 0) est incrémenté. Les valeurs produites sur les différents axes sont :

Les premiers échantillons sont donc : (0,1,2), (-4,-5,-6), (8,9,10)

Cette information vous permettra de vérifier, dans la suite des TP, que vous récupérez bien toutes les données échantillonnées sans en perdre.

Vous allez progressivement écrire un pilote de périphérique pour cet accéléromètre simulé.

Commencez par regarder rapidement la documentation du composant réel :

L’arbre des périphérique fourni

Ce périphérique ADXL345 est déclaré dans l’arbre des périphériques que vous avez récupéré lors du TP précédent.

En voici l’extrait intéressant :

&v2m_i2c_dvi {
      adxl345: adxl345@53 {
          compatible = "qemu,adxl345";
          reg = <0x53>;
          interrupt-parent = <&gic>;
          interrupts = <0 50 4>;
      };
};

Ne considérez pas pour le moment les lignes commençant par interrupt.

&v2m_i2c_dvi pointe vers le contrôleur I2C auquel est virtuellement connecté l’accéléromètre. L’accéléromètre est placé à l’adresse 0x53 sur ce bus dans QEMU.

Travail à faire

Première étape

  1. Créez un répertoire de travail pour votre premier module :

    $ cd $TPROOT
    $ mkdir pilote_i2c
    $ cd pilote_i2c
  2. Copiez le squelette ci-dessus (en le nommant adxl345.c) ainsi que le Makefile

  3. Adaptez le squelette fourni pour :

    Adaptez également le Makefile fourni pour que le module puisse compiler.

  4. Compilez votre module

    $ make CROSS_COMPILE=arm-linux-gnueabihf- ARCH=arm KDIR=../linux-5.10.19/build/

    Votre module est maintenant dans le fichier adxl345.ko.

  5. Démarrez QEMU :

    $ cd ..
    $ ./qemu-system-arm -nographic -machine vexpress-a9 -kernel linux-5.10.19/build/arch/arm/boot/zImage \
    -dtb linux-5.10.19/build/arch/arm/boot/dts/vexpress-v2p-ca9.dtb -initrd rootfs.cpio.gz \
    -fsdev local,path=pilote_i2c,security_model=mapped,id=mnt -device virtio-9p-device,fsdev=mnt,mount_tag=mnt
  6. Après vous être loggué en tant que root, montez le partage avec la machine hôte en tapant, dans la console émulée par QEMU :

    $ mount -t 9p -o trans=virtio mnt /mnt -oversion=9p2000.L,msize=10240

    Si cette commande s’exécute correctement, vous aurez alors accès, dans le répertoire /mnt de la machine émulée par QEMU, au contenu du répertoire pilote_i2c de votre machine (et donc au module que vous venez de compiler).

  7. Chargez votre module (depuis QEMU) :

    $ insmod /mnt/adxl345.ko
  8. Vérifiez que votre module a le comportement attendu (affichage de votre message de découverte du périphérique)

  9. Déchargez votre module (rmmod adxl345) et vérifiez qu’il a bien le comportement attendu (affichage de votre message de retrait du périphérique)

  10. Vous pouvez arrêter QEMU (ctrl-a c puis quit)

Deuxième étape

Pour le moment votre pilote ne fait pas grand chose. Dans cette deuxième étape nous allons essayer de lire le registre DEVID de l’accéléromètre. Ce registre contient une valeur fixe (0xe5). Si nous récupérons cette valeur, cela montrera que l’on est capable de faire des échanges avec ce périphérique en utilisant l’API I2C du noyau.

Modifiez la fonction probe de votre module afin de récupérer la valeur de ce registre depuis le périphérique physique, et affichez la.

Indice : vous aurez besoin de faire une écriture suivie d’une lecture I2C. Les fonctions d’écriture (i2c_master_send) et de lecture (i2c_master_recv) ont été détaillées en cours. Regardez également le schéma en bas de la page 18/40 (19 dans le PDF) de la datasheet. Si vous ne voyez toujours pas comment faire, demandez à votre encadrant.

Pour recompiler et recharger votre module, référez-vous à l’étape précédente.

Troisième étape

Nous allons maintenant modifier notre pilote pour qu’il configure correctement le périphérique physique.

Dans la fonction probe, configurez les différents registres de l’accéléromètre. Nous utiliserons la configuration suivante, même si certaines valeurs ne sont pas implémentées dans la simulation (voir la documentation de l’accéléromètre) :

Dans la fonction remove, éteignez l’accéléromètre :


© Copyright 2020 Guillaume Duc. Le contenu de cette page est mis à disposition selon les termes de la Licence Creative Commons Attribution - Partage dans les Mêmes Conditions 4.0 International (à l'exception des exemples de code tirés du noyau Linux et qui sont distribués sous leurs licences d'origine).

Licence
Creative Commons