Développement logiciel

Dans cette section, nous allons ajouter un pilote de périphérique personnalisé pour nos voyants et boutons. Nous testerons ce pilote à partir de la ligne de commande, puis dans un exemple d’application.

Créer un pilote de périphérique

Le noyau Linux contient déjà un pilote pour les composants GPIO de notre architecture matérielle.

Ici, nous avons choisi de créer notre propre pilote afin d’illustrer les mécanismes de base pour accéder aux registres d’un périphérique. Vous pourrez vous en inspirer pour la suite de l’activité.

Déclarer le périphérique dans le device tree

Revenez dans le terminal où vous avez lancé bitbake.

Dans ce terminal, toutes les commandes doivent être exécutées à partir du dossier /Data/etudiant/soc2020/build.

Si vous ne l’avez pas encore fait dans le shell courant, exécutez les commandes suivantes :

cd /Data/etudiant/soc2020/poky
source oe-init-build-env ../build

Dans le dossier meta-eseo, créez la structure de dossiers où nous allons modifier la recette de construction du noyau :

mkdir -p ../meta-eseo/recipes-kernel/linux/files

Créez le fichier meta-eseo/recipes-kernel/linux/files/zybo-minimal.dts avec le contenu suivant :

#include "zynq-zybo.dts"

/ {
    amba_pl: amba_pl {
        #address-cells = <1>;
        #size-cells = <1>;
        compatible = "simple-bus";
        ranges ;

        simple-io-device {
            compatible = "eseo,simple-io-device";
            reg = <0x41200000 4 0x41210000 4>;
        };
    };
};

Nous déclarons le bus entre le processeur et la logique programmable sous le nom amba_pl. Nous déclarons un périphérique simple-io-device connecté à ce bus. Le champ compatible permet d’associer ce périphérique à une famille de composants qui pourraient être gérés par le même pilote.

Le champ reg indique les plages d’adresse réservées à ce périphériques sur le bus. Ici, nous déclarons deux plages :

Vérifiez que ces adresses correspondent à celles affichées dans Vivado. Modifiez-les si nécessaires.

Créez ensuite le fichier meta-eseo/recipes-kernel/linux/linux-xlnx_%.bbappend avec le contenu suivant :

FILESEXTRAPATHS_prepend := "${THISDIR}/files:"

SRC_URI += "file://zybo-minimal.dts;subdir=git/arch/${ARCH}/boot/dts"

PACKAGE_ARCH = "${MACHINE_ARCH}"

KERNEL_DEVICETREE_zybo-zynq7 += "zybo-minimal.dtb"

Ce fichier est une extension de la recette de construction du noyau Linux, dans laquelle nous ajoutons le choix d’un autre fichier device tree que celui par défaut.

Créer un module noyau

Dans le dossier meta-eseo, créez la structure de dossiers des sources d’un nouveau pilote de périphériques :

mkdir -p ../meta-eseo/recipes-kernel/simple-io-driver/files

Créez le fichier meta-eseo/recipes-kernel/simple-io-driver/files/simple-io-driver.c avec le code ci-dessous. Ce module s’enregistre comme pilote pour le périphérique simple-io-device. Il lit le contenu du device tree afin de connaître les adresses des deux composants GPIO. Il crée deux entrées sysfs qui permettront d’accéder directement aux états des LED et des boutons. Il définit deux commandes ioctl pour gérer les entrées/sorties depuis une application.

#include <linux/fs.h>
#include <linux/io.h>
#include <linux/module.h>
#include <linux/slab.h>
#include <linux/of_address.h>
#include <linux/of_platform.h>
#include <linux/uaccess.h>

MODULE_LICENSE("GPL");
MODULE_AUTHOR("ESEO");
MODULE_DESCRIPTION("simple-io-driver - Custom I/O driver");

#define DEVICE_NAME "simple-io-device"

/// Données utilisées par le driver.
static struct {
    /// Numéro majeur du périphérique caractère.
    int major;
    /// Classe du périphérique.
    struct class *class;
    /// Périphérique caractère.
    struct device *chrdev;
    /// Le registre où écrire l'état des LED.
    void *leds_reg;
    /// Le registre où lire l'état des boutons.
    void *btns_reg;
} simple_io_driver_data;

/** Affecter l'état des LED.
 *
 * Chaque bit du paramètre state représente le nouvel état d'une LED.
 * Le bit de poids faible correspond à la LED numéro 0 sur la carte.
 *
 * @param state Le nouvel état des LED.
 */
static void simple_io_driver_set_leds(int state) {
    iowrite32(state, simple_io_driver_data.leds_reg);
}

/** Lire l'état des boutons.
 *
 * Chaque bit du résultat représente l'état courant d'un bouton.
 * Le bit de poids faible correspond au bouton numéro 0 sur la carte.
 *
 * @return L'état des boutons.
 */
static int simple_io_driver_get_btns(void) {
    return ioread32(simple_io_driver_data.btns_reg);
}

/** Affecter l'état des LED à partir d'un attribut du périphérique dans /sys
 *
 * Écrire la valeur sous forme de chaîne de caractères dans
 * /sys/class/eseo/simple-io-device/leds.
 * Le préfixe 0x permet d'entrer une valeur en hexadécimal.
 *
 * @param dev Un pointeur sur la structure de données représentant le périphérique.
 * @param attr Un pointeur sur la structure de données de l'attribut du périphérique à affecter.
 * @param buf La chaîne de caractère qui contient la valeur à affecter.
 * @param count La longueur de la chaîne de caractères.
 * @return Le nombre d'octets traités.
 */
static ssize_t leds_store(struct device *dev, struct device_attribute *attr, const char *buf, size_t count) {
    int arg;
    if (kstrtoint(buf, 0, &arg)) {
        dev_err(dev, "Invalid value.\n");
    }
    else {
        simple_io_driver_set_leds(arg);
    }
    return count;
}

/** Attribut permettant l'accès aux LED, en écriture seule.
 *
 * L'attribut est automatiquement associé à la fonction leds_store ci-dessus.
 */
static DEVICE_ATTR_WO(leds);

/** Lire l'état des boutons à partir d'un attribut du périphérique dans /sys
 *
 * La lecture du fichier /sys/class/eseo/simple-io-device/btns
 * retourne une chaîne de caractères représentant une valeur entière en
 * hexadécimal avec le préfixe 0x.
 *
 * @param dev Un pointeur sur la structure de données représentant le périphérique.
 * @param attr Un pointeur sur la structure de données de l'attribut du périphérique à affecter.
 * @param buf La chaîne de caractère qui recevra la valeur de l'attribut.
 * @return Le nombre d'octets écrits dans buf.
 */
static ssize_t btns_show(struct device *dev, struct device_attribute *attr, char *buf) {
    return scnprintf(buf, PAGE_SIZE, "0x%x\n", simple_io_driver_get_btns());
}

/** Attribut permettant l'accès aux boutons, en lecture seule.
 *
 * L'attribut est automatiquement associé à la fonction btns_show ci-dessus.
 */
static DEVICE_ATTR_RO(btns);

/** Interface ioctl
 *
 * @param f Un descripteur du fichier sur lequel la fonction ioctl est appelée.
 * @param cmd La commande à exécuter.
 * @param arg L'argument de la commande (ici un pointeur).
 * @return Un code d'erreur.
 */
static long int simple_io_driver_ioctl(struct file *f, unsigned cmd, unsigned long arg) {
    int value;
    switch (cmd) {
        case 0:
            copy_from_user(&value, (int*)arg, sizeof(int));
            simple_io_driver_set_leds(value);
            break;

        case 1:
            value = simple_io_driver_get_btns();
            copy_to_user((int*)arg, &value, sizeof(int));
            break;

        default:
            pr_err("Invalid ioctl command: %u\n", cmd);
            return -EINVAL;
    }
    return 0;
}

/// Interface du pilote pour les opérations sur les fichiers.
static struct file_operations simple_io_driver_ops = {
    .unlocked_ioctl = simple_io_driver_ioctl
};

static int simple_io_driver_remove(struct platform_device *pdev);

/** Initialisation du pilote de périphérique.
 *
 * @param dev Un pointeur sur la structure de données représentant le périphérique.
 * @return Un code d'erreur, 0 en cas de succès.
 */
static int simple_io_driver_probe(struct platform_device *pdev) {
    int err = 0;
    struct device *dev = &pdev->dev;
    struct resource leds_res, btns_res;

    // Créer un périphérique caractère "simple-io-device"
    // pour la classe de périphériques "eseo".
    simple_io_driver_data.major = register_chrdev(0, DEVICE_NAME, &simple_io_driver_ops);
    if (simple_io_driver_data.major < 0) {
        err = simple_io_driver_data.major;
        dev_err(dev, "Failed to register character device. Error: %d\n", err);
        goto probe_error;
    }

    simple_io_driver_data.class = class_create(THIS_MODULE, "eseo");
    if (IS_ERR(simple_io_driver_data.class)) {
        err = PTR_ERR(simple_io_driver_data.class);
        dev_err(dev, "Failed to create device class. Error: %d\n", err);
        goto probe_error;
    }

    simple_io_driver_data.chrdev = device_create(simple_io_driver_data.class, NULL, MKDEV(simple_io_driver_data.major, 0), NULL, DEVICE_NAME);
    if (IS_ERR(simple_io_driver_data.chrdev)) {
        err = PTR_ERR(simple_io_driver_data.class);
        dev_err(dev, "Failed to create character device. Error: %d\n", err);
        goto probe_error;
    }

    // Lire le champ "reg" du périphérique dans le device tree
    // pour connaître la région du plan d'adressage associée aux GPIO des LED
    // et des boutons.
    err = of_address_to_resource(dev->of_node, 0, &leds_res);
    if (err) {
        dev_err(dev, "Failed to read memory segment for LEDs. Error: %d\n", err);
        goto probe_error;
    }
    dev_info(dev, "Found LEDs at address: 0x%08x\n", leds_res.start);

    err = of_address_to_resource(dev->of_node, 1, &btns_res);
    if (err) {
        dev_err(dev, "Failed to read memory segment for buttons. Error: %d\n", err);
        goto probe_error;
    }
    dev_info(dev, "Found buttons at address: 0x%08x\n", btns_res.start);

    // Réserver les régions du plan d'adressage associées au GPIO.
    // Associer les adresses physiques aux adresses virtuelles du noyau
    // et obtenir des pointeurs sur ces régions.
    simple_io_driver_data.leds_reg = devm_ioremap_resource(dev, &leds_res);
    if (IS_ERR(simple_io_driver_data.leds_reg)) {
        err = PTR_ERR(simple_io_driver_data.leds_reg);
        dev_err(dev, "Failed to map memory segment for LEDs. Error: %d\n", err);
        goto probe_error;
    }

    simple_io_driver_data.btns_reg = devm_ioremap_resource(dev, &btns_res);
    if (IS_ERR(simple_io_driver_data.btns_reg)) {
        err = PTR_ERR(simple_io_driver_data.btns_reg);
        dev_err(dev, "Failed to map memory segment for buttons. Error: %d\n", err);
        goto probe_error;
    }

    // Créer des fichiers dans /sys pour les attributs "leds" et "btns".
    err = device_create_file(simple_io_driver_data.chrdev, &dev_attr_leds);
    if (err) {
        dev_err(dev, "Failed to create /sys endpoint for LEDs. Error: %d\n", err);
        goto probe_error;
    }

    err = device_create_file(simple_io_driver_data.chrdev, &dev_attr_btns);
    if (err) {
        dev_err(dev, "Failed to create /sys endpoint for buttons. Error: %d\n", err);
        goto probe_error;
    }

    return 0;

probe_error:
    simple_io_driver_remove(pdev);
    return err;
}

/** Terminaison du pilote de périphérique.
 *
 * La plupart des ressources allouées dans simple_io_driver_probe sont
 * "device-managed" (cf les fonctions préfixées par "devm_").
 * Elles sont désallouées automatiquement si l'initialisation échoue ou
 * lorsque ce module se termine.
 *
 * @param dev Un pointeur sur la structure de données représentant le périphérique.
 * @return Un code d'erreur, 0 en cas de succès.
 */
static int simple_io_driver_remove(struct platform_device *pdev) {
    struct device *dev = &pdev->dev;

    device_remove_file(dev, &dev_attr_leds);
    device_remove_file(dev, &dev_attr_btns);

    if (!IS_ERR(simple_io_driver_data.chrdev)) {
        device_destroy(simple_io_driver_data.class, MKDEV(simple_io_driver_data.major, 0));
    }

    if (!IS_ERR(simple_io_driver_data.class)) {
        class_destroy(simple_io_driver_data.class);
    }

    if (simple_io_driver_data.major >= 0) {
        unregister_chrdev(simple_io_driver_data.major, DEVICE_NAME);
    }

    return 0;
}

/// Table de critères pour la détection du périphérique.
static struct of_device_id simple_io_driver_of_match[] = {
    { .compatible = "eseo,simple-io-device", },
    { /* end of list */ },
};

MODULE_DEVICE_TABLE(of, simple_io_driver_of_match);

/// La structure de données permettant d'enregistrer ce pilote de périphérique auprès du noyau.
static struct platform_driver simple_io_driver_driver = {
    .driver = {
    .name = "simple-io-driver",
    .owner = THIS_MODULE,
    .of_match_table	= simple_io_driver_of_match,
    },
    .probe  = simple_io_driver_probe,
    .remove = simple_io_driver_remove,
};

/** Initialisation du module.
 *
 * @return Un code d'erreur, 0 en cas de succès.
 */
static int __init simple_io_driver_init(void) {
    return platform_driver_register(&simple_io_driver_driver);
}

/// Terminaison du module.
static void __exit simple_io_driver_exit(void) {
    platform_driver_unregister(&simple_io_driver_driver);
}

module_init(simple_io_driver_init);
module_exit(simple_io_driver_exit);

Ajoutez le fichier meta-eseo/recipes-kernel/simple-io-driver/files/Makefile avec le contenu suivant (attention à bien utiliser des tabulations pour indenter, pas des espaces) :

obj-m := simple-io-driver.o
ccflags-y := -std=gnu99 -Wno-declaration-after-statement

SRC := $(shell pwd)

all:
	$(MAKE) -C $(KERNEL_SRC) M=$(SRC)

modules_install:
	$(MAKE) -C $(KERNEL_SRC) M=$(SRC) modules_install

clean:
	rm -f *.o *~ core .depend .*.cmd *.ko *.mod.c
	rm -f Module.markers Module.symvers modules.order
	rm -rf .tmp_versions Modules.symvers

Ajoutez le fichier meta-eseo/recipes-kernel/simple-io-driver/simple-io-driver_1.0.bb avec le contenu suivant :

SUMMARY = "Linux kernel module for my simple I/O"
LICENSE = "CLOSED"

inherit module

SRC_URI = "file://Makefile \
           file://simple-io-driver.c \
          "

FILESEXTRAPATHS_prepend := "${THISDIR}/files:"

S = "${WORKDIR}"

Dans le fichier build/conf/local.conf, modifiez la variable CORE_IMAGE_EXTRA_INSTALL de la manière suivante :

CORE_IMAGE_EXTRA_INSTALL = "peekpoke simple-io-driver"

Pour finir, modifiez le fichier scripts/uEnv.txt en remplaçant zynq-zybo.dtb par zybo-minimal.dtb.

Recompiler et redémarrer

bitbake core-image-minimal

Mettez la carte hors tension et retirez la carte microSD.

Copiez les fichier suivants sur la carte microSD :

Insérez à nouveau la carte microSD dans le lecteur de la carte Zybo et remettez-la sous tension.

Commander les voyants et les boutons

Sur la cible, vérifiez que le module simple-io-driver est chargé :

lsmod

Cette commande allume les voyants 0 et 2 :

echo 5 > /sys/class/eseo/simple-io-device/leds

Cette commande affiche l’état des boutons :

cat /sys/class/eseo/simple-io-device/btns

Exécuter une application sur la carte ZYBO

Créer une nouvelle application dans l’arborescence de PetaLinux

Dans le dossier meta-eseo, créez la structure de dossiers des sources d’un nouveau programme :

mkdir -p ../meta-eseo/recipes-extended/simple-io-demo/files/src

Créez le fichier meta-eseo/recipes-extended/simple-io-demo/files/src/simple-io-demo.c avec le code ci-dessous.

#include <stdio.h>
#include <fcntl.h>
#include <time.h>
#include <sys/ioctl.h>

static void set_leds(int device, int state) {
    ioctl(device, 0, &state);
}

static int get_btns(int device) {
    int res;
    ioctl(device, 1, &res);
    return res;
}

int main(int argc, char* argv[]) {
    // Ouvrir les fichiers d'accès aux LED et boutons.
    // Configurer la bufferisation du fichier d'accès aux LED
    // pour que la mise à jour se fasse à chaque fin de ligne.
    int device = open("/dev/simple-io-device", O_RDWR);
    if (device < 0) {
        fprintf(stderr, "Could not open device file /dev/simple-io-device. Error: %d\n", device);
        return device;
    }

    // Configurer un intervalle de 10 ms
    struct timespec ts = {
        .tv_sec  = 0,
        .tv_nsec = 10e6
    };

    // L'utilisateur doit presser les boutons dans l'ordre.
    // Les voyants indiquent quel bouton presser.
    int progress = 1;
    set_leds(device, progress);
    while (progress < 0x10) {
        if (get_btns(device) == progress) {
            progress <<= 1;
            set_leds(device, progress);
        }
        nanosleep(&ts, NULL);
    }

    close(device);

    return 0;
}

Ajoutez le fichier meta-eseo/recipes-extended/simple-io-demo/files/CMakeLists.txt :

cmake_minimum_required(VERSION 3.8)
project(simple-io-demo)

add_executable(simple-io-demo src/simple-io-demo.c)

install(TARGETS simple-io-demo DESTINATION bin)

Ajoutez la recette meta-eseo/recipes-extended/simple-io-demo/simple-io-demo_1.0.bb :

DESCRIPTION = "Simple I/O demo program"
LICENSE = "CLOSED"

FILESEXTRAPATHS_prepend = "${THISDIR}/files:"

SRC_URI = "\
    file://CMakeLists.txt \
    file://src/simple-io-demo.c \
"

S = "${WORKDIR}"

inherit cmake

Dans le fichier build/conf/local.conf, modifiez la variable CORE_IMAGE_EXTRA_INSTALL de la manière suivante :

CORE_IMAGE_EXTRA_INSTALL = "peekpoke simple-io-driver simple-io-demo"

Recompiler et redémarrer

bitbake core-image-minimal

Mettez la carte hors tension et retirez la carte microSD.

Remplacez le fichier suivant sur la carte microSD :

Insérez à nouveau la carte microSD dans le lecteur de la carte Zybo et remettez-la sous tension.

Exécuter l’application

Sur la cible, chargez le nouveau module et lancez l’exécutable simple-io-demo à partir de la ligne de commande.