Semaine Athens Linux Device Drivers


Introduction

Le noyau Linux étant conçu pour fonctionner sur un grand nombre de plates-formes matérielles différentes, il a fallu rationaliser le fonctionnement des pilotes de périphériques. Cela a été fait au niveau de la communication entre le pilote et le matériel (device model) et entre le logiciel et le pilote (framework).

Physiquement, un périphérique est connecté au système au travers d'un bus (soit des bus sophistiqués permettant l'énumération des périphériques comme USB, PCI, soit des bus plus simples comme AXI...). Au niveau du noyau, cette relation est représentée par un pilote qui va gérer le bus (exemple : USB), un pilote qui va gérer le contrôleur de ce bus (exemple : contrôleur de bus USB) et un pilote qui va gérer un périphérique connecté à ce bus.

Au niveau de l'interface avec les applications, de nombreux périphériques différents offrent des fonctionnalités similaires. Plutôt que d'avoir une interface différente par périphérique avec l'espace utilisateur, des frameworks offrent une interface commune (exemple : port série, framebuffer...).

Schéma Device Model & Frameworks

Schéma Device Model & Frameworks

La suite de cette page va décrire de façon plus détaillée le modèle de périphérique (device model). La partie framework sera vue ultérieurement.

Structures de données génériques

Le modèle de périphérique du noyau se base sur trois structures de données définies dans include/linux/device.h :

Ces structures sont spécialisées (par héritage) en fonction du type de bus. Par exemple, en ce qui concerne le bus USB, struct device_driver est spécialisée en struct usb_driver.

Interactions bus / contrôleur / pilote de périphérique

À chaque type de bus (PCI, USB...) est associé un pilote de bus. Il assure plusieurs fonctions :

Exemple : cas du bus USB

Le pilote du bus

Le pilote du bus USB est situé dans drivers/usb/core. La structure struct bus_type représentant le bus USB est définie dans drivers/usb/core/driver.c :

struct bus_type usb_bus_type = {
    .name =         "usb",
    .match =        usb_device_match,
    .uevent =       usb_uevent,
};

et elle est enregistrée auprès du noyau dans la fonction usb_init dans drivers/usb/core/usb.c :

retval = bus_register(&usb_bus_type);

Ce pilote du bus USB va interagir avec les pilotes des contrôleurs dans drivers/usb/host et avec les pilotes des périphériques (partout ailleurs dans drivers/usb).

Un pilote de périphérique USB

Pour voir l'interaction avec le bus, nous allons étudier un pilote de périphérique USB assez simple (drivers/usb/misc/usbled.c2) qui pilote des LED branchées sur le port USB.

La première chose que doit faire le pilote de périphérique est déclarer quels sont les périphériques matériels que le pilote sait gérer. Sur le bus USB, les périphériques sont principalement identifiés par deux nombres : l'identifiant du fabriquant (idVendor) et l'identifiant du produit (idProduct), tous les deux sur 16 bits et qui suffisent généralement à faire la relation entre le périphérique matériel et le pilote correspondant.

/* table of devices that work with this driver */
static const struct usb_device_id id_table[] = {
    { USB_DEVICE(0x0fc5, 0x1223), .driver_info = DELCOM_VISUAL_SIGNAL_INDICATOR },
    { USB_DEVICE(0x1d34, 0x0004), .driver_info = DREAM_CHEEKY_WEBMAIL_NOTIFIER },
    { USB_DEVICE(0x1d34, 0x000a), .driver_info = DREAM_CHEEKY_WEBMAIL_NOTIFIER },
    { USB_DEVICE(0x1294, 0x1320), .driver_info = RISO_KAGAKU_LED },
    { },
};
MODULE_DEVICE_TABLE(usb, id_table);

La structure struct usb_device_id est définie dans include/linux/mod_devicetable.h et permet d'indiquer sur quels champs de l'USB (idVendor, idProduct ou autre) la comparaison doit être effectuée. La macro USB_DEVICE définie dans include/linux/usb.h permet de faire une comparaison sur idVendor et idProduct. Le champ driver_info est un entier spécifique au pilote et qui peut lui servir à savoir facilement quel type de périphérique correspond. Ici, le pilote sait gérer 4 périphériques matériels différents.

La macro MODULE_DEVICE_TABLE permet, à la compilation du module, d'extraire les informations de correspondance entre les périphériques et les pilotes pour que le module puisse être chargé automatiquement par udev lorsqu'un périphérique supporté est détecté.

Lorsque le pilote est chargé (au démarrage du noyau s'il est compilé en dur ou lors de son chargement s'il est compilé sous forme de module) il doit s'enregistrer auprès du pilote du bus USB à l'aide de la fonction usb_register. De même, lorsque le pilote est déchargé, il doit se désenregistrer à l'aide de la fonction usb_deregister. Ces appels sont cachés par la macro :

module_usb_driver(led_driver);

qui génère le code suivant :

static int __init led_driver_init(void)
{
    return usb_register(&led_driver);
}

static void __exit led_driver_exit(void)
{
    usb_deregister(&led_driver);
}
module_init(led_driver_init);
module_exit(led_driver_exit);

La structure led_driver de type struct usb_driver (voir sa définition dans include/linux/usb.h) est définie ainsi dans ce pilote :

static struct usb_driver led_driver = {
    .name =         "usbled",
    .probe =        led_probe,
    .disconnect =   led_disconnect,
    .id_table =     id_table,
};

Le champ name contient le nom du pilote qui doit être unique parmi les pilotes enregistrés pour le bus USB. Le champ id_table contient la table vue précédemment pour assurer la détection des périphériques utilisables avec ce pilote. Le champ probe indique la fonction a appeler lorsqu'un périphérique correspondant à ce pilote est détecté sur le bus USB et le champ disconnect la fonction a appeler lorsque le périphérique est débranché du bus.

Lorsqu'un périphérique est branché sur le bus, les opérations suivantes se produisent :

  1. Le contrôleur USB signale au pilote de bus USB qu'un nouveau périphérique est branché
  2. À partir des informations fournies par le périphérique (notamment l'identifiant du fabriquant et du produit), le pilote USB identifie quel pilote de périphérique est apte à gérer ce périphérique
  3. Le pilote du bus USB appelle alors la méthode indiquée par le champ probe de la structure struct usb_driver pour le pilote correspondant
  4. La méthode probe reçoit en argument une ou plusieurs structures décrivant le périphérique. Elle doit :

Dans notre exemple, la fonction led_probe crée des fichiers particuliers dans sysfs afin de pouvoir piloter la LED.

Lorsque le périphérique est débranché, le même processus se répète cette fois-ci en appelant la fonction disconnect du pilote de périphérique.

Exemple : bus platform

Tous les périphériques ne sont pas connectés à des bus qui permettent de faire de l'énumération (identifier quels périphériques y sont connectés). C'est souvent le cas des bus utilisés dans les systèmes embarqués (par exemple les bus I2C ou SPI) et dans les System-on-Chip (par exemple le bus AXI). Néanmoins, il est utile de conserver le même modèle de périphérique.

Dans ces cas, au lieu d'un système dynamique, la configuration de ces pilotes de périphérique doit être décrite de façon statique, soit directement dans le code source (sur la carte X, le périphérique Y est présent à l'adresse Z), soit, dans le cas de certaines architectures (PowerPC et ARM notamment), dans une structure de données particulière : le Device Tree.

Le bus platform est un bus virtuel dans le noyau Linux pour connecter tous les périphériques faisant partie du SoC (contrôleur ethernet, UART, contrôleur SPI...). Il fonctionne de la même façon que les autres bus du système à l'exception du fait que les périphériques ne sont pas identifiés dynamiquement mais statiquement.

Un pilote de périphérique pour le bus platform

Nous prendrons comme exemple le pilote drivers/leds/leds-gpio.c qui permet de piloter des LED connectés à des GPIO (General Purpose Inputs Outputs).

L'enregistrement en temps que pilote d'un périphérique sur le bus platform se fait ainsi :

static struct platform_driver gpio_led_driver = {
    .probe          = gpio_led_probe,
    .remove         = gpio_led_remove,
    .driver         = {
        .name           = "leds-gpio",
        .of_match_table = of_gpio_leds_match,
    },
};

module_platform_driver(gpio_led_driver);

Tout comme la macro module_usb_driver, la macro module_platform_driver génère le code suivant :

static int __init gpio_led_driver_init(void)
{
    return platform_driver_register(&gpio_led_driver);
}

static void __exit gpio_led_driver_exit(void)
{
    platform_driver_unregister(&gpio_led_driver);
}
module_init(gpio_led_driver_init);
module_exit(gpio_led_driver_exit);

Les méthodes platform_driver_register et platform_driver_unregister permettent respectivement d'enregistrer le pilote de périphérique auprès du pilote du bus platform et de supprimer le pilote.

Le pilote fournit au bus platform les informations permettant de faire le lien entre un périphérique et ce pilote par l'intermédiaire de la structure of_gpio_leds_match.

Cette structure est instanciée ainsi :

static const struct of_device_id of_gpio_leds_match[] = {
    { .compatible = "gpio-leds", },
    {},
};

L'arbre des périphériques (Device Tree)

Par définition, le bus platform ne dispose pas de mécanisme pour énumérer (détecter) les périphériques qui y sont connectés et donc de faire le lien entre un périphérique et son pilote. La liste des périphériques présents et leur configuration doit donc être fournie de façon statique au noyau. C'est également le cas pour d'autres bus ne permettant pas l'énumération (en tout cas pas de manière fiable) comme par exemple le bus I2C.

La méthode la plus courante, notamment pour les SoC à base de processeur ARM, est d'utiliser un Device Tree.

Un Device Tree est une structure de données arborescente servant à décrire une configuration matérielle. Elle provient d'Open Firmware, un standard de système de démarrage utilisé par de nombreuses machines à base de processeurs autres que x86 et notamment les machines Sun à base de processeurs SPARC, les machines à base de PowerPC (et notamment les anciennes machines Apple), etc. La spécification exacte de la syntaxe du Device Tree est disponible dans le document Power.org(TM) Standard for Embedded Power Architecture(TM) Platform Requirements (ePAPR) v1.1. Un résumé de la syntaxe, plus digeste et lisible est disponible sur le projet GitHub de DeviceTree.org.

De nombreux Device Tree sont fournis dans les sources du noyau. Par exemple, celui utilisé pour la carte de TP est dans arch/arm/boot/dts/socfpga_cyclone5_de1soc.dts. Ce fichier dts (Device Tree Source) inclut socfpga_cyclone5 qui inclut socfpga.dtsi qui lui-même inclut skeleton.dtsi.

Le Device Tree Source (fichier .dts) est compilé par l'outil dtc (Device Tree Compiler) pour donner un fichier binaire dtb (Device Tree Blob) qui est chargé en même temps que le noyau par le bootloader (U-Boot dans notre cas).

Il est possible d'appeler l'outil dtc manuellement (il fait partie des sources du noyau et est situé dans votre cas dans /home/users/XXX/linux-socfpga/build/scripts/dtc/dtc). Il est également possible de compiler un fichier dts à partir du Makefile à la racine des sources du noyau :

$ make O=build ARCH=arm CROSS_COMPILE=... socfpga_cyclone5_de1soc.dtb

Voici un extrait simplifié du Device Tree utilisé pour la carte de TP :

/ {
    #address-cells = <1>;
    #size-cells = <1>;

    cpus {
        #address-cells = <1>;
        #size-cells = <0>;

        cpu@0 {
            compatible = "arm,cortex-a9";
            device_type = "cpu";
            reg = <0>;
            next-level-cache = <&L2>;
        };
        cpu@1 {
            compatible = "arm,cortex-a9";
            device_type = "cpu";
            reg = <1>;
            next-level-cache = <&L2>;
        };
    };

    intc: intc@fffed000 {
        compatible = "arm,cortex-a9-gic";
        #interrupt-cells = <3>;
        interrupt-controller;
        reg = <0xfffed000 0x1000>,
              <0xfffec100 0x100>;
    };

    soc {
        #address-cells = <1>;
        #size-cells = <1>;
        compatible = "simple-bus";
        device_type = "soc";
        interrupt-parent = <&intc>;
        ranges;

        gmac0: ethernet@ff700000 {
            compatible = "altr,socfpga-stmmac", "snps,dwmac-3.70a", "snps,dwmac";
            reg = <0xff700000 0x2000>;
            interrupts = <0 115 4>;
            interrupt-names = "macirq";
            mac-address = [00 00 00 00 00 00];/* Filled in by U-Boot */
            clocks = <&emac0_clk>;
            clock-names = "stmmaceth";
            reset-names = "stmmaceth";
            snps,multicast-filter-bins = <256>;
            snps,perfect-filter-entries = <128>;
            max-frame-size = <3800>;
            tx-fifo-depth = <4096>;
            rx-fifo-depth = <4096>;
            status = "okay";
        };
    };
};

Dans le Device Tree, on trouve les informations de configuration (adresses, numéro d'interruption...) et de topologie. Dans l'exemple ci-dessus, le nœud ethernet@ff700000 (auquel est attribuée l'étiquette gmac0 pour s'y référer plus facilement ailleurs dans l'arbre) est un périphérique, connecté au bus soc (le nœud parent). Les registres de ce périphérique (ici le contrôleur MAC ethernet), sont mappés en mémoire à partir de l'adresse 0xff700000 pour une longueur de 0x2000 (propriété reg).

On trouve également des informations permettant de faire le lien entre un périphérique et son pilote. C'est la propriété compatible dont la valeur est de la forme vendor,model. Ici, le contrôleur ethernet est compatible avec les pilotes supportant altr,socfpga-stmmac, snps,dwmac-3.70a ou snps,dwmac.

Ce sont les pilotes qui, en s'enregistrant auprès du bus platform, indiquent avec quels types de périphériques ils sont compatibles. Le bus platform fait alors le lien entre la valeur de la propriété compatible dans le Device Tree et les valeurs fournies par les pilotes lors de leur enregistrement.

La documentation dans Documentation/devicetree/bindings/ décrit, pour chaque valeur du champ compatible, la liste des propriétés obligatoires et facultatives dans le nœud correspondant et leur signification.

Autre méthode : initialisation statique

Pour les systèmes n'utilisant pas de Device Tree, les périphériques sont instanciés statiquement.

Exemple pour la carte SH-2007 (architecture SH), dans arch/sh/boards/board-sh2007.c :

static int __init sh2007_io_init(void)
{
    /* ... */

    platform_add_devices(sh2007_devices, ARRAY_SIZE(sh2007_devices));
    return 0;
}

static struct platform_device *sh2007_devices[] __initdata = {
    &smsc9118_0_device,
    &smsc9118_1_device,
    &cf_device,
};

static struct platform_device cf_device  = {
    .name           = "pata_platform",
    .id             = 0,
    .num_resources  = ARRAY_SIZE(cf_resources),
    .resource       = cf_resources,
};

static struct resource cf_resources[] = {
    [0] = {
        .start  = CF_BASE + CF_OFFSET,
        .end    = CF_BASE + CF_OFFSET + 0x0f,
        .flags  = IORESOURCE_MEM,
    },

    /* ... */

    [2] = {
        .start  = evt2irq(0x2c0),
        .end    = evt2irq(0x2c0),
        .flags  = IORESOURCE_IRQ,
    },
};

Lors du démarrage de la carte, la fonction sh2007_io_init ajoute les périphériques listés dans le tableau sh2007_devices. Chaque élément de ce tableau pointe vers une structure struct platform_device qui indique le nom du périphérique (qui sera utilisé pour faire la correspondance avec le pilote, un peu à la manière du champ compatible du Device Tree) et un lien vers la liste des ressources utilisées (struct resource), ici, une zone mémoire IO (équivalent de la propriété req du Device Tree) et une IRQ (équivalent de la propriété interrupts du Device Tree).

Pour information, le pilote correspondant à pata_platform est drivers/ata/pata_platform.c qui est décrit par la structure :

static struct platform_driver pata_platform_driver = {
    .probe          = pata_platform_probe,
    .remove         = ata_platform_remove_one,
    .driver = {
        .name           = "pata_platform",
    },
};

Fonction probe

Pour reprendre notre exemple précédent de pilote de LED, la structure of_gpio_leds_match

static const struct of_device_id of_gpio_leds_match[] = {
    { .compatible = "gpio-leds", },
    {},
};

indique que le pilote est compatible avec gpio-leds. Donc si dans le Device Tree, un périphérique a pour propriété compatible = "gpio-leds", le bus platform appellera la fonction gpio_led_probe pour indiquer que le périphérique correspondant a été détecté.

static int gpio_led_probe(struct platform_device *pdev);

Cette fonction peut récupérer les informations stockées dans le Device Tree ou initialisées statiquement via une struct resource:

Pratique : Bus I2C et accéléromètre

La carte de TP dispose 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. Dans un premier temps, nous n'utiliserons pas ce signal d'interruption.

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

Commencez par lire rapidement la documentation du composant :

Introduction, le bus I2C

I2C est un bus, inventé par Philips Semiconductor (maintenant NXP), utilisé principalement pour relier des périphériques lents à un microprocesseur ou à un microcontrôleur. Il utilise seulement deux fils pour la communication : SDA (pour les données), et SCL pour l'horloge. Le débit standard du bus I2C est de 100 kbit/s (des versions plus rapides ont été développées par la suite).

Plusieurs entités peuvent être connectées sur le bus : un ou plusieurs maîtres et un ou plusieurs esclaves. Toutes les communications sont initiées par un maître qui fournit l'horloge de transmission sur le fil SCL. Les esclaves ne font que répondre aux requêtes des maîtres et se basent sur l'horloge fournie.

Chaque esclave dispose d'une adresse, généralement sur 7 bits et souvent codée en dur dans le composant. Chaque transmission émise par un maître commence par l'adresse de l'esclave concerné, ce qui permet à ce dernier de savoir que la transmission le concerne.

Les spécifications complètes du bus I2C sont disponibles sur le site de NXP : http://www.nxp.com/documents/user_manual/UM10204.pdf.

Le bus I2C dans le noyau Linux

Le bus I2C est modélisé dans le noyau Linux de la même manière que les autres bus vus précédemment.

Les pilotes des contrôleurs du bus I2C sont situés dans drivers/i2c/busses/ et les pilotes des périphériques connectés au bus sont répartis dans des sous-répertoires de drivers en fonction de leur type.

Les pilotes des périphériques I2C sont représentés par la structure struct i2c_driver définie dans include/linux/i2c.h. Cette structure hérite de struct device_driver.

Les pilotes s'enregistrent auprès du pilote du bus I2C grâce à la fonction i2c_add_driver et se désenregistrent grâce à la fonction i2c_del_driver. Ces deux fonctions sont traditionnellement appelées dans la fonction d'initialisation du module (xxx_init) et dans la fonction de nettoyage du module (xxx_exit).

Comme vu précédemment pour module_usb_driver() et module_platform_driver(), s'il n'y a rien besoin de faire d'autre dans les fonctions d'initialisation et de nettoyage, la macro module_i2c_driver permet de générer automatiquement ces fonctions.

Le noyau fournit un mécanisme permettant, dans certains cas, de détecter dynamiquement les périphériques branchés sur le bus. Nous n'utiliserons pas ce mécanisme par la suite mais la documentation décrit son fonctionnement pour les personnes curieuses.

Squelette d'un pilote de périphérique I2C

Voici le squelette d'un pilote de périphérique I2C (plus de détails dans Documentation/i2c/writing-clients) :

/* includes ... */

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, MODULE_AUTHOR, MODULE_DESCRIPTION... */

Instanciation (sans Device Tree)

Pour instancier le périphérique sur une plate-forme n'utilisant pas de Device Tree, la structure struct i2c_board_info permet de déclarer la liste des périphériques connectés au bus I2C et leurs adresses. Cette structure est passée à la fonction i2c_register_board_info lors du démarrage du noyau sur la carte.

static struct i2c_board_info myboard_i2c_devices[] __initdata = {
    {
        I2C_BOARD_INFO("foo", 0x42),
    }
};

void board_init(void)
{
    /* ... */
    i2c_register_board_info(0, myboard_i2c_devices, ARRAY_SIZE(myboard_i2c_devices));
    /* ... */
}

Instanciation (avec Device Tree)

Le contrôleur de bus I2C utilisé sur la carte de TP est décrit dans le Device Tree de la façon suivante (arch/arm/boot/dts/socfpga.dtsi, la propriété status provenant de arch/arm/boot/dts/socfpga_cyclone5_de1soc.dts) :

i2c0: i2c@ffc04000 {
    #address-cells = <1>;
    #size-cells = <0>;
    compatible = "snps,designware-i2c";
    reg = <0xffc04000 0x1000>;
    interrupts = <0 158 4>;
    clocks = <&l4_sp_clk>;
    speed-mode = <0>;
    status = "okay";
};

Les périphériques I2C connectés à ce contrôleur doivent être décrit comme des fils de ce nœud :

i2c0: i2c@ffc04000 {
    status = "okay";

    foo: foo@42 {
        compatible = "vendor,foo";
        reg = <0x42>;
    };
};

La propriété reg indique l'adresse du périphérique sur le bus I2C.

Fonctions probe et remove

La fonction probe a pour prototype :

static int foo_probe(struct i2c_client *client,
                     const struct i2c_device_id *id);

Elle est appelée lorsque le bus détecte le périphérique (ici la détection étant statique, elle est appelée au démarrage du pilote si le périphérique est listé dans le device tree ou dans la configuration statique de la carte).

Ses arguments sont :

La fonction probe est chargée d'initialiser le périphérique et de préparer l'utilisation du périphérique par l'espace utilisateur (le plus souvent en s'enregistrant auprès du framework approprié).

Si le pilote supporte plusieurs modèles de périphériques (donc si les tables foo_idtable et foo_of_match contiennent plus d'une entrée), il est possible de récupérer l'entrée précise qui a déclenché l'appel à la fonction probe en utilisant le code suivant (inspiré de drivers/mfd/mc13xxx-i2c.c

if (client->dev.of_node) {
    /* Le device tree a été utilisé */
    const struct of_device_id *of_id =
        of_match_device(foo_of_match, &client->dev);
    variant = of_id->data; /* Champ data de of_device_id */
} else {
    /* La structure foo_idtable a été utilisée */
    variant = id->driver_data; /* Second champ de i2c_device_id */
}

La fonction remove a pour prototype :

static int foo_remove(struct i2c_client *client);

Elle est responsable du désenregistrement du périphérique du framework et de son extinction.

Données privées

Un même pilote peut gérer plusieurs périphériques connectés au bus. Des données propres à la gestion de chacun de ces périphériques peuvent être stockés directement dans le champ data de la structure struct i2c_client pour pouvoir être utilisées facilement par la suite.

Deux fonctions sont définies à ces fins :

void i2c_set_clientdata(struct i2c_client *client, void *data);
void *i2c_get_clientdata(const struct i2c_client *client);

Communication sur le bus I2C

API de base

Deux fonctions de base sont disponibles pour envoyer et recevoir des données sur le bus I2C :

int i2c_master_send(struct i2c_client *client, const char *buf, int count);

Cette fonction envoie count octets depuis le tampon buf au périphérique client (l'adresse de ce périphérique est contenue dans cette structure). Elle retourne le nombre d'octets réellement envoyés.

int i2c_master_recv(struct i2c_client *client, char *buf, int count);

Cette fonction reçoit count octets depuis le périphérique client et les stocke dans le tampon buf. Elle retourne le nombre d'octets réellement reçus.

Dans le cas de l'I2C, il faut faire attention au protocole de communication avec le périphérique. Par exemple, pour l'accéléromètre, lire le contenu d'un registre implique l'envoi (écriture) du numéro du registre demandé (send d'un octet représentant le numéro du registre demandé) puis d'une réception (lecture) du contenu (recv d'un ou plusieurs octets). Voir la documentation de l'accéléromètre page 17 pour plus de détails.

Transferts de messages

Une fonction de plus haut niveau est disponible pour transmettre une série de messages, chacun de ces messages pouvant être une lecture ou une écriture.

int i2c_transfer(struct i2c_adapter *adap, struct i2c_msg *msg, int num);

Ses arguments sont :

La structure struct i2c_msg est définie dans include/uapi/linux/i2c.h :

struct i2c_msg {
    __u16 addr;     /* slave address */
    __u16 flags;
#define I2C_M_TEN               0x0010  /* this is a ten bit chip address */
#define I2C_M_RD                0x0001  /* read data, from slave to master */
#define I2C_M_STOP              0x8000  /* if I2C_FUNC_PROTOCOL_MANGLING */
#define I2C_M_NOSTART           0x4000  /* if I2C_FUNC_NOSTART */
#define I2C_M_REV_DIR_ADDR      0x2000  /* if I2C_FUNC_PROTOCOL_MANGLING */
#define I2C_M_IGNORE_NAK        0x1000  /* if I2C_FUNC_PROTOCOL_MANGLING */
#define I2C_M_NO_RD_ACK         0x0800  /* if I2C_FUNC_PROTOCOL_MANGLING */
#define I2C_M_RECV_LEN          0x0400  /* length will be first received byte */
    __u16 len;              /* msg length */
    __u8 *buf;              /* pointer to msg data */
};

Le champ addr peut être rempli par client->addr. La signification des différents drapeaux (champ flags) est décrite en détails dans Documentation/i2c/i2c-protocol.

Travail à faire

  1. Écrivez un squelette de pilote pour l'accéléromètre de la carte
  2. Ajoutez l'accéléromètre dans le Device Tree
  3. Chargez votre module
  4. Vérifiez que le noyau appelle bien la fonction probe (affichez un message)
  5. Dans la fonction probe, lisez et affichez l'identifiant (DEVID) de l'accéléromètre
  6. Dans la fonction probe, configurez les différents registres de l'accéléromètre. Nous utiliserons la configuration suivante (voir la documentation de l'accéléromètre) :
  7. Dans la fonction remove, configurez l'accéléromètre avec une configuration appropriée :

Pour le moment, le pilote n'offre aucune interface pour les applications. Ce point (important) sera traité dans la suite du cours.


  1. Texte issu de la traduction de la documentation de la structure struct bus_type dans <linux/device.h>.

  2. Depuis la version 4.8, la gestion de ces LEDs USB a été déplacée vers le module HID (Human Interface Device) dans drivers/hid/hid-led.c.


Retour au sommaire du cours


© Copyright 2017 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