À l’assaut du sous-système noyau « Industrial I/O » ! (et du QML … !)

GNU/Linux Magazine n° 215 | mai 2018 | Pierre-Jean TEXIER - Jean CHABRERIE
Creative Commons
  • Actuellement 0 sur 5 étoiles
0
Merci d'avoir participé !
Vous avez déjà noté cette page, vous ne pouvez la noter qu'une fois !
Votre note a été changée, merci de votre participation !
Dans cet article, nous allons développer un driver de périphérique en utilisant à la fois le bus i2c et le sous-système Industrial I/O. Le but final sera la mise en place d'une petite application Qt/QML permettant d'afficher les informations d'un capteur sur écran LCD.

Espace utilisateur ou espace noyau ? Qui ne s’est jamais questionné lors d’un développement logiciel pour l’intégration d’un capteur sous GNU/Linux ?! Dans bien des cas, l’espace utilisateur est favorisé pour ce type de développement, où le confort d’avoir plus de flexibilité avec les différents bus de communication SPI ou i2c (langages, API…) n’est en effet pas négligeable contrairement au développement noyau, où seul le langage C est de rigueur.

Néanmoins, il existe (depuis 2009) un sous-système noyau très intéressant dédié aux capteurs pourvus d’un bus de communication i2c ou SPI. On retrouvera par exemple différents types : ADC, DAC, température, humidité, accéléromètre… Connu sous le nom de Industrial I/O, son but premier repose sur l’unification des différents capteurs que l’on peut retrouver dans un système embarqué. Permettant aussi un accès simplifié depuis le monde userspace (mais pas que …) !

Nous découvrirons ainsi durant l’article, comment développer un driver de périphérique pour le capteur FXOS8700CQ de chez NXP (composant qui intègre accéléromètre + baromètre et capteur de température), capteur bien entendu présent sur la WaRP7. Tout ceci en utilisant à la fois le bus i2c et le sous-système Industrial I/O. Et pour ne rien changer aux habitudes des auteurs, nous donnerons un sens à ce développement noyau, en mettant en place une petite application Qt/QML qui permettra d’afficher les différentes informations du capteur sur l’écran LCD de notre carte fétiche !

1. Espace noyau

De nombreuses fois mis en pratique dans les colonnes de GNU/Linux Magazine et même d’Open Silicium, le développement de pilotes de périphériques Linux n’est pas toujours un exercice facile (un des auteurs se rappelle d’ailleurs de ses premières sueurs sur le développement d’un driver USB), c’est pourquoi il est bon de rappeler que dans bien des cas, travailler en espace utilisateur semblera plus opportun. Sur ce sujet, les auteurs renverront à la lecture de l’excellent article au titre évocateur (« Comment ne pas écrire de pilotes Linux ») de Pierre Ficheux paru dans Open Silicium en [1].

Mais rassurez-vous (ou pas), c’est bel et bien de l’espace noyau dont nous allons parler dans cet article. L'intérêt d’écrire sur le sous-système Industrial I/O est motivé par deux raisons. La première semble assez évidente, elle concerne l’évolution constante du monde de l’« IoT », avec de plus en plus de capteurs à disposition, il est donc assez cohérent de présenter une technique permettant de développer (ou simplement utiliser) vos propres pilotes de périphériques Linux en utilisant le sous-système IIO. La deuxième raison concerne celle d’une récente lecture d’un superbe ouvrage consacré au développement de pilotes de périphériques Linux et qui plus est à destination de l’embarqué « Linux Device Drivers Development: Develop customized drivers for embedded Linux » aux éditions PACKT Publishing. Soulignons-le, un ouvrage écrit par un Français (cocorico), John Madieu, ingénieur Linux/Kernel embarqué.

1.1 Contexte

La WaRP7 étant la plateforme d’excellence pour le développement IoT (ou IdO pour les amoureux de la langue française), celle-ci embarque forcément un nombre non négligeable de capteurs, allant du capteur de pression/température (NXP MPL3115), jusqu’au gyroscope (NXP FXAS21002C) en passant par l’accéléromètre/magnétomètre (NXP FXOS8700CQ).

Note

Pour les curieux, il est à noter que la présentation de cette plateforme a déjà fait l’objet d’un article en [2].

Et c’est justement sur ce dernier capteur que nous prêterons toute notre attention. Il faut savoir que le FXOS8700CQ possède déjà un support au sein de notre noyau Linux (sur le fork de NXP et donc non mainline), mais malheureusement basé sur un sous-système qui mérite d’être changé pour ce genre de capteur, le sous-système input, plutôt réservé à la gestion du tactile, souris, clavier… C’est donc le moment et le candidat idéal pour migrer vers IIO.

1.2 Rappel : « Linux Device/Driver Model »

De par sa conception, le noyau GNU/Linux est aujourd'hui capable de fonctionner sur un grand nombre d'architectures (rappelons que 80 % des serveurs tournent sous GNU/Linux !). On retrouvera par exemple dans les sources du noyau, le support pour des architectures comme ARM, MIPS, X86, etc.

Avec une constante évolution sur la complexité des systèmes (on parlera ici de la topologie), ce n'est « que » depuis la version 2.5 (au début des années 2000 environ), qu'est apparu le Device Model (LDM : Linux Device Model).

L'impact direct de cette évolution concerne la communication entre :

  • le partie applicative et le pilote de périphérique (on parlera de framework, IIO dans notre cas, dont on parlera dans la suite de l'article) ;
  • le pilote de périphérique et le matériel.

La figure 1 permet d’illustrer les différentes interactions, en partant du matériel jusqu’à la couche applicative située en espace utilisateur.

Fig. 1 : Linux Device Model, Framework et userspace.

Globalement, celui-ci a pour but de rationaliser et standardiser le développement de pilotes de périphériques, où les idées principales sont :

  • permettre une meilleure réutilisation du code (entre périphériques et architectures - exemple : un pilote de périphérique doit être le même sur ARM ou sur MIPS même si bien entendu, le contrôleur de bus est différent !) ;
  • apporter une approche orientée objet au sein du noyau ;
  • proposer de nouveaux concepts : sysfs, kobjects, class, bus, driver, device...

Ainsi, le but premier est d'avoir une meilleure organisation au sein du noyau, par exemple le Device Model permettra d’isoler :

  • le pilote qui va gérer le protocole de communication (exemple : bus i2c) ;
  • le pilote qui va gérer le contrôleur du bus (SoC dépendant), tout en implémentant un ensemble de fonctions imposées par le pilote en charge du protocole ;
  • le pilote qui va gérer un périphérique connecté à ce bus (notre fameux pilote pour l'accéléromètre).

Le modèle de périphérique (Device Model) est donc articulé autour de trois structures de données (définies entre autres dans : include/linux/device.h).

1.2.1 struct bus_type

C’est la structure de données de plus bas niveau, celle qui représente le bus à proprement parler. « Un bus est un canal entre le processeur et un ou plusieurs périphériques. Pour les besoins du modèle, tous les périphériques sont connectés par l'intermédiaire d'un bus même s'il s'agit d'un bus interne virtuel (appelé platform bus) ».

Cette structure, qui est le premier composant du Device Model, se verra donc être utilisée dans ce qu’on appelle le pilote de bus (bus driver), rappelons que chaque bus a son propre pilote (USB, SPI, etc.). Celui-ci sera en charge d’enregistrer le type de bus dont il a la charge, de permettre l’enregistrement des différents pilotes de périphériques, de faire le lien entre les pilotes de périphériques et les périphériques détectés. Un exemple étant toujours plus approprié qu’un long discours, prenons l’exemple avec la partie pilote de bus i2c implémentant la structure de type bus_type. Le choix n’est pas anodin, rappelez-vous que notre accéléromètre/magnétomètre communique au travers cette même interface.

La structure qui représente le bus i2c est définie dans drivers/i2c/i2c-core.c :

struct bus_type i2c_bus_type = {

.name = "i2c",

.match = i2c_device_match,

.probe = i2c_device_probe,

...

};

Avec comme champ :

  • name qui représente le nom du bus que l’on retrouvera dans /sys/bus/name, deviendra donc /sys/bus/i2cen espace utilisateur ;
  • match, callback qui est appelé chaque fois qu’un périphérique est ajouté au bus. Elle aura pour rôle de vérifier si un périphérique peut être géré par un pilote de périphérique donné. Ceci en faisant une simple comparaison de chaîne de caractères (compatible) pour le cas du device-tree :

static int i2c_device_match(struct device *dev, struct device_driver *drv)

{

   struct i2c_client *client = i2c_verify_client(dev);

   struct i2c_driver *driver;

 

   if (!client)

      return 0;

 

   /* Attempt an OF style match */

   if (of_driver_match_device(dev, drv))

   return 1;

   ...

   return 0;

}

  • probe, appelé une fois que la fonction match() a été traitée avec succès. Elle sera en charge de faire les allocations des structures de données relatives au périphérique enregistré sur le bus. Une fois effectué, elle appellera la fonction probe() du pilote de périphérique utilisé pour le périphérique précédemment alloué.

1.2.2 struct device_driver

Remontons d’un niveau pour parler de la structure de base de chaque pilote de périphérique, si on devait faire une analogie avec la programmation orientée objet (au C++ par exemple), on pourrait la qualifier de classe de référence qu'on fera hériter en fonction des types de bus au sein du pilote de périphériques. Citons par exemple : struct i2c_driver (que l’on mettra en œuvre par la suite), struct spi_driver, struct usb_driver, et struct platform_driver. C’est elle qui représente le pilote qui sera capable de gérer les périphériques sur un bus donné (i2c par exemple). Comme vu plus tôt, c’est lui-même qui se verra être enregistré par le bus.

1.2.3 struct device

Difficile de faire plus clair, celle-ci représente le périphérique sur le bus, quel qu’il soit (i2c, SPI, etc.). Et tout comme la structure device_driver, chaque instance d’un périphérique héritera de cette structure en fonction du bus qu’il utilise : struct i2c_client (que l’on mettra en œuvre par la suite), struct spi_device, struct usb_device, struct platform_device, etc.

Prenons en exemple le cas d’un périphérique i2c, où celui-ci embarque la structure device présente dans include/linux/i2c.h :

struct i2c_client {

unsigned short flags;        /* div., see below */

unsigned short addr;         /* chip address - NOTE: 7bit */

                             /* addresses are stored in the */

                             /* _LOWER_ 7 bits */

char name[I2C_NAME_SIZE];

struct i2c_adapter *adapter; /* the adapter we sit on */

struct device dev;           /* the device structure */

int irq;                     /* irq issued by device */

...

};

Les lecteurs l’auront compris, la recette est assez « simple », mais peut vite être longue dans les explications, et bien que le sujet soit intéressant, il nous est impossible de tout expliquer dans les colonnes de GNU/Linux Magazine. Les auteurs renverront donc à la lecture des documents les plus à jour sur cet aspect. Citons par exemple la documentation du noyau GNU/Linux, ou plus récemment l’ouvrage de John Madieu, où un très bon chapitre traite du Device Model.

2. Le bus i2c : les bases de notre pilote de périphérique

Dans cette partie, nous poserons les premières briques de notre pilote de périphérique destiné à l’utilisation du FXOS8700CQ. C’est donc ici que nous allons établir le squelette pour accueillir le framework IIO.

Nous commencerons par instancier les structures/fonctions permettant de faire le support de notre capteur i2c au sein de notre pilote de périphérique. Puis, de par le fait que le bus i2c ne permet pas de faire de l’énumération (à l’inverse du PCI ou même de l’USB), nous aborderons la thématique du device-tree (seul cas présenté ici) afin d’instancier notre pilote.

2.1 Sortez le clavier !

Comme vu précédemment, les pilotes de périphériques qui reposent sur l’utilisation du bus i2c sont définis via la structure i2c_driver (qui hérite de la structure device_driver), c’est au travers celle-ci que nous pourrons effectuer les différents enregistrements, dans notre cas définissons notre structure sous la forme :

static struct i2c_driver fxos8700iio_driver = {

   .driver = {

      .name = "fxos8700iio",

      .of_match_table = of_match_ptr(fxos8700iio_of_match),

},

   .probe = fxos8700iio_probe,

   .remove = fxos8700iio_remove,

   .id_table = fxos8700iio_id,

};

Avec :

  • name qui représente le nom de notre pilote, ici fxos8700iio afin de le différencier du pilote déjà existant dans les sources de notre noyau (sous le nom de fxos8700) ;
  • of_match_table, champ de la structure device_driver. C’est un tableau qui permettra de lister les périphériques que le driver peut prendre en charge (en utilisant la propriété compatible). C’est entre autres les données qui seront utilisées par la fonction match() du pilote de bus. Dans notre cas, un seul périphérique sera géré par notre pilote. On essaiera aussi de respecter les conventions [3] relatives à la propriété compatible, à définir sous la forme <manufacturer>, <model>, où manufacturer est une simple chaîne qui définit le nom du fabricant et model une chaîne pour définir le modèle du périphérique :

static const struct of_device_id fxos8700iio_of_match[] = {

   {.compatible = "nxp,fxos8700iio"},

   { /* sentinel */ }

};

MODULE_DEVICE_TABLE(of, fxos8700iio_of_match);

  • id_table, aussi un champ permettant de lister les périphériques (device ID), défini avec une simple chaîne de caractères :

static const struct i2c_device_id fxos8700iio_id[] = {

   {"fxos8700iio", 0},

   { /* sentinel */ }

};

MODULE_DEVICE_TABLE(i2c, fxos8700iio_id);

Cette table sert dans le cas où le device-tree n’est pas utilisé (utilisation de la structure i2c_board_info), mais pas que. En effet, il sera important de remarquer que sur les versions noyau inférieures ou égales à 4.9, la présence du champ id_table est indispensable ! Même si le device-tree est utilisé pour faire l’instanciation du périphérique. Sans ce champ, le pilote de bus ne pourra pas enregistrer le pilote de périphérique comme le montre la fonction i2c_device_probe() du pilote de bus i2c :

static int i2c_device_probe(struct device *dev)

{

   struct i2c_client *client = i2c_verify_client(dev);

   ...

   driver = to_i2c_driver(dev->driver);

   if (!driver->probe || !driver->id_table)

      return -ENODEV;

   ...

   return status;

}

  • probe, c’est la fonction qui sera appelée par la fonction probe() du pilote de bus (celle vue précédemment). C’est par exemple ici qu’il conviendra de s’enregistrer auprès du framework noyau (IIO dans notre cas, mais que nous verrons dans la prochaine section), il sera aussi utile de tester si notre contrôleur de bus i2c permet d’utiliser différentes fonctionnalités du protocole utilisé (ici smbus, un sous-ensemble du protocole i2c), ceci en utilisant la fonction i2c_check_functionality(). Il sera aussi recommandé de tester si le périphérique est bien celui souhaité et si oui, enregistrer les données privées (via la fonction i2c_set_clientdata()) et l’initialiser (fonction fxos8700iio_config(), qui dans notre cas configure les différents registres du périphérique, fonction qui ne sera pas expliquée ici).

La fonction fxos8700iio_probe() prendra donc en paramètre un pointeur vers une structure de type i2c_client qui représente le périphérique, ainsi qu’un pointeur vers l’entrée du tableau fxos8700iio_id (même si nous utilisons la méthode du device-tree !) :

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

{

   if (!i2c_check_functionality(client->adapter, I2C_FUNC_SMBUS_BYTE_DATA | I2C_FUNC_SMBUS_WORD_DATA)) {

      dev_err(&client->dev, "SMBUS Byte/Word Data not supported\n");

      return -EOPNOTSUPP;

   }

 

   ret = i2c_smbus_read_byte_data(client, FXOS8700_REG_CHIP_ID);

   if (ret != FXOS8700_CHIP_ID) {

      dev_err(&client->dev, "Invalid chip id -> %d instead of %d!\n", ret, FXOS8700_CHIP_ID);

      return (ret < 0) ? ret : -ENODEV;

   }

 

   i2c_set_clientdata(client, <données privées>);

 

   ret = fxos8700iio_config(client);

   if (ret < 0) {

      dev_err(&client->dev, "Configuration failed!\n");

      return ret;

   }

}

  • remove, qui devra être responsable du désenregistrement au framework noyau, puis dans une moindre mesure, positionner notre périphérique en mode standby. Elle aura aussi comme argument un pointeur vers la structure représentant le périphérique :

static int fxos8700iio_remove(struct i2c_client *client)

{

   <données privées> = i2c_get_clientdata(client);

}

Et pour finir, afin de pouvoir s’enregistrer/désenregistrer au bus i2c, il nous faudra utiliser les fonctions i2c_add_driver() et i2c_del_driver(), pour ce faire nous pourrons utiliser la macro suivante, qui au final ne fait qu’encapsuler les deux fonctions (dans le cas où aucune autre fonction n’est utilisée) :

/* Registration */

module_i2c_driver(fxos8700iio_driver);

2.2 Gestion du pilote

2.2.1 Mise à jour de l’image Noyau

Les fondations du pilote de périphérique étant posées, nous pouvons maintenant passer à la prochaine étape : générer notre nouvelle image noyau ! Comme annoncé plus haut dans l’article, un pilote pour ce périphérique est déjà existant. Malheureusement, celui-ci est aujourd’hui intégré de façon statique à notre image noyau : vous l’aurez compris, il nous faudra enlever le support afin de tester notre pilote. Pour ce faire, lançons l'interface de configuration de notre noyau en mode ncurses :

$ bitbake linux-warp7 -c menuconfig

Puis, dans Devices Drivers > Misc Devices, il conviendra de désélectionner l’option suivante présentée en figure 2.

Fig. 2 : DROP fxos8700 support (Option CONFIG_SENSORS_FXOS8700).

Il faudra par la suite remettre à jour notre image noyau (compilation + déploiement), mais vous savez faire !

2.2.2 Mise à jour du fichier device-tree

Là aussi, la déclaration étant déjà présente, il conviendra simplement de mettre à jour la propriété compatible (pour faire le lien avec notre pilote !). L’ancienne définition du fichier imx7s-warp.dts :

fxos8700@1e {

   compatible = "fsl,fxos8700";

   reg = <0x1e>;

};

Deviendra donc la suivante :

fxos8700iio@1e {

   compatible = "nxp,fxos8700iio";

   reg = <0x1e>;

};

À noter que nous en avons profité pour mettre à jour le champ <manufacturer>, Freescale étant maintenant devenu NXP. Nous rappellerons que la propriété reg définit l’adresse i2c de notre périphérique.

Pour les curieux, le choix de l’adresse du périphérique est défini par la conception matérielle ; regardons les spécifications du capteur en figure 3.

Fig. 3 : Configuration des adresses du fxos8700.

Et en regardant la schématique de la WaRP7 en figure 4.

Fig. 4 : Schématique WaRP7 pour la partie fxos8700.

Nous retrouvons bien la correspondance entre SA0 et SA1 (mise à la masse), donc une adresse égale à 0x1E.

2.2.3 Compilation « Out of tree »

Pour des raisons de simplicité durant la phase de développement, nous n’intègrerons pas les sources de notre pilote de périphérique aux sources du noyau Linux. L’inconvénient de cette méthode est qu’il nous faudra à chaque fois recompiler les sources et il nous sera donc impossible de lier celui-ci de façon statique à notre image noyau, mais rien de bien gênant pour notre étude.

Note

Le lecteur souhaitant avoir son pilote dans les sources du noyau, pourra modifier le fichier Kconfig et Makefile associé.

Notre fichier Makefile permettant d’effectuer une compilation séparée des sources du noyau sera sous la forme :

obj-m := fxos8700iio.o

KERNELDIR ?= /lib/modules/$(shell uname -r)/build

 

all default: modules

install: modules_install

 

modules modules_install clean:

$(MAKE) -C $(KERNELDIR) M=$$PWD $@

Avec comme fichier source fxos8700.c, une variable KERNELDIR qu’il faudra définir, celle-ci permettant de spécifier le chemin vers les sources du noyau GNU/Linux, ainsi qu’une définition des différentes règles de compilation. Vous l’aurez deviné, il restera simplement à invoquer la commande make pour générer notre module :

~/Documents/fxos8700iio$ make KERNELDIR=<chemin vers>/linux-warp7/4.1-r0/build

make -C <chemin vers>/linux-warp7/4.1-r0/build M=/home/pjtexier/Documents/fxos8700iio modules

make[1] : on entre dans le répertoire « <chemin vers>/linux-warp7/4.1-r0/build »

   CC [M] /home/pjtexier/Documents/fxos8700iio/fxos8700iio.o

   Building modules, stage 2.

   MODPOST 1 modules

   CC /home/pjtexier/Documents/fxos8700iio/fxos8700iio.mod.o

   LD [M] /home/pjtexier/Documents/fxos8700iio/fxos8700iio.ko

make[1] : on quitte le répertoire « <chemin vers>/linux-warp7/4.1-r0/build »

Commande qui aura pour effet de générer notre fichier fxos8700.ko (pour Kernel Object). Fichier qu’il est déjà possible d’insérer sur notre cible via la commande insmod.

3. Le sous-système Industrial Input/Output

Dernière partie concernant le développement driver et surtout la plus importante de l’article, celle où il sera question d’intégrer la partie IIO, le sous-système noyau permettant d’interagir avec notre capteur (accéléromètre/magnétomètre et même température) depuis l’espace utilisateur. Nous commencerons très logiquement cette partie par une brève présentation de ce sous-système, puis nous repartirons sur une étape logicielle en complétant les fondations posées durant la deuxième partie de cet article. Enfin, nous finirons par déployer notre pilote de périphérique, permettant ainsi de valider le travail effectué jusqu’à présent.

3.1 Sous-système Industrial I/O

3.1.1 Un peu d’histoire...

Initialement développé par Jonathan Cameron courant 2009, le framework IIO se veut être le standard permettant de faire l’interface avec les capteurs (i2c/SPI) que l’on peut retrouver sur le marché grand public (téléphone, tablette, etc.), mais aussi de l’industrie (d’où le nom). L’intérêt notable de celui-ci est de combler le fossé que l’on peut retrouver entre deux sous-systèmes bien connus. Citons ici hwmon et input, où hwmon se veut plutôt utile dans du contrôle de système (température, ventilateur de PC…) et input pour la gestion souris, clavier, etc. Ainsi, IIO permet de faire le pont entre les deux !

3.1.2 L’architecture

Construit suivant deux axes, le sous-système permet à l’utilisateur d’interagir :

  • soit via le pilote en mode caractère : /dev/iio:deviceX, pour des accès : buffers, events...
  • soit par sysfs : /sys/bus/iio/devices/iio:deviceX/, nous permettant ainsi d’accéder au capteur lui-même (données brutes (sans conversion), échelle…). La figure 5 permet la représentation de façon très claire, du sous-système IIO.

Fig. 5 : Architecture du sous-système IIO.

3.2 Les mains dans le cambouis...

3.2.1 Fonction probe()

Comme annoncé dans la précédente section, la partie framework se doit d’être enregistrée dans la fonction probe() du pilote de périphérique. C’est donc ici qu’il conviendra de manipuler la première structure de celui-ci, iio_dev. Celle-ci représente notre capteur à proprement parler, permettant ainsi le paramétrage de cette interface. Pour ce faire, il nous faudra en premier lieu instancier cette structure, puis par la fonction devm_iio_device_alloc(), allouer de la mémoire pour notre périphérique. Nous enregistrerons les données privées, puis viendra la phase où il conviendra de renseigner les champs de la structure en question. La fonction probe() se verra être mise à jour de la façon suivante :

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

{

   struct iio_dev *indio_dev;

 

   indio_dev = devm_iio_device_alloc(&client->dev, sizeof(*data));

   if (!indio_dev) {

      ...

   }

 

   /* ici = fonctions de la section 2 */

 

   data = iio_priv(indio_dev);

   data->client = client;

 

   i2c_set_clientdata(client, indio_dev);

   indio_dev->dev.parent = &client->dev;

   indio_dev->info = &fxos8700iio_info;

   indio_dev->name = id->name;

   indio_dev->modes = INDIO_DIRECT_MODE;

   indio_dev->channels = fxos8700iio_channels;

   indio_dev->num_channels = ARRAY_SIZE(fxos8700iio_channels);

 

   ret = iio_device_register(indio_dev);

   if (ret < 0) {

      ...

   }

 

   return ret;

}

Expliquons brièvement les différents champs de notre structureindio_dev(iio_dev) :

  • indio_dev->channels = fxos8700iio_channels permet de définir l’ensemble du périphérique par l’utilisation de la structure iio_chan_spec :

static const struct iio_chan_spec fxos8700iio_channels[] = {

   /* Accelerometer */

   FXOS8700_ACC_CHANNEL(FXOS8700_REG_ACC_X_MSB, X),

   FXOS8700_ACC_CHANNEL(FXOS8700_REG_ACC_Y_MSB, Y),

   FXOS8700_ACC_CHANNEL(FXOS8700_REG_ACC_Z_MSB, Z),

   /* Magnetometer */

   FXOS8700_MAG_CHANNEL(FXOS8700_REG_MAG_X_MSB, X),

   FXOS8700_MAG_CHANNEL(FXOS8700_REG_MAG_Y_MSB, Y),

   FXOS8700_MAG_CHANNEL(FXOS8700_REG_MAG_Z_MSB, Z),

   /* Temperature */

   FXOS8700_TEMP_CHANNEL(FXOS8700_M_TEMP),

};

Regardons par exemple la définition de la partie accéléromètre :

#define FXOS8700_ACC_CHANNEL(reg, axis) { \

   .type = IIO_ACCEL, \

   .address = reg, \

   .modified = 1, \

   .channel2 = IIO_MOD_##axis, \

   .info_mask_separate = BIT(IIO_CHAN_INFO_RAW), \

   .info_mask_shared_by_type = BIT(IIO_CHAN_INFO_SCALE), \

}

Où, type permettra de spécifier le type de mesure (ici IIO_ACCEL, mais de la même manière IIO_MAGN et IIO_TEMP pour magnétomètre et température). Le champ address pour l’accès au registre i2c du capteur contenant la donnée. modified, permet l’utilisation des différentes définitions applicable au channel via le champ .channel2, où par exemple nous pourrons définir les axes du périphérique (IIO_MOD_X/IIO_MOD_Y/IIO_MOD_Z, ou dans le cas d’un capteur de CO2, IIO_MOD_CO2). Le champ info_mask_separate permettra de spécifier que la présente définition est uniquement applicable à ce channel en question (ici lecture brute, sans mise à l’échelle). Enfin, info_mask_shared_by_type, permet à l’inverse, de spécifier que la définition sera partagée avec les autres channels de même type (ici même échelle). L’ensemble de la définition nous permettra ainsi de retrouver en espace utilisateur une définition de type {direction}_{type}_{index}_{modifier}_{info_mask}, qui dans notre cas serait :

  • in_accel_x_raw : pour l’accéléromètre, avec in_accel_scale pour l’échelle ;
  • in_magn_x_raw : pour le magnétomètre, avec in_magn_scale pour l’échelle ;
  • in_temp_input : pour la température.
  • indio_dev->info = &fxos8700iio_info, permet de définir les différentes fonctions de callback pour l’accès à notre capteur. Pour ce faire, il convient d’utiliser la structure iio_info, permettant d’utiliser les différents « hooks » utilisés par IIO pour les accès en lecture/écriture. Dans notre cas, nul besoin de déclarer une fonction pour l’écriture, seule une fonction de lecture nous sera utile !

static const struct iio_info fxos8700iio_info = {

   .read_raw = fxos8700iio_read_raw,

   ...

};

Pour l’implémentation de la fonction de lecture (celle qui sera appelée chaque fois qu’une lecture côté sysfs aura lieu), voici comment procéder :

static int fxos8700iio_read_raw(struct iio_dev *indio_dev, struct iio_chan_spec const *channel, int *val, int *val2, long mask)

{

   int ret;

   struct fxos8700iio_data *data = iio_priv(indio_dev);

 

   switch (mask) {

   case IIO_CHAN_INFO_PROCESSED:

      switch (channel->type) {

      case IIO_TEMP:

         ret = i2c_smbus_read_byte_data(data->client, channel->address);

         if (ret < 0)

            return ret;

 

         *val = ret;

 

         return IIO_VAL_INT;

         ...

      }

   case IIO_CHAN_INFO_RAW:

      switch (channel->type) {

      case IIO_ACCEL:

         ret = i2c_smbus_read_byte_data(data->client, channel->address);

         if (ret < 0)

            return ret;

 

         *val = sign_extend32(ret, 7);

         return IIO_VAL_INT;

      case IIO_MAGN:

         ...

   case IIO_CHAN_INFO_SCALE:

      switch (channel->type) {

      case IIO_ACCEL:

         *val = 0;

         *val2 = fxos8700iio_nscale;

         return IIO_VAL_INT_PLUS_NANO;

      case IIO_MAGN:

         ...

   }

   return 0;

}

Le principe premier de cette fonction est d’utiliser les paramètres en argument (channel et mask) afin de rediriger vers l’élément voulu lors d’une lecture via sysfs. Par exemple, lors d’une lecture de données brute sur un des channel correspondant à l’accéléromètre (in_accel_x_raw par exemple), la variable mask nous permettra de lire l’élément (ici IIO_CHAN_INFO_RAW), puis en vérifiant le type de channel (channel->type), nous pourrons renvoyer la valeur du capteur par l’utilisation du pointeur val (paramètre de sortie de la fonction). return IIO_VAL_INT permet de retourner une valeur de type entier. Il en sera de même pour les lectures de IIO_CHAN_INFO_SCALE (lecture de l’échelle) et IIO_CHAN_INFO_PROCESSED (lecture de la température) ;

  • indio_dev->name = id->name : pour spécifier le nom du périphérique, ici fxos8700iio ;
  • indio_dev->modes = INDIO_DIRECT_MODE : permet de spécifier que nous ferons des accès via sysfs (software trigger, définie dans <sources du noyau>/include/linux/iio/iio.h) ;
  • indio_dev->num_channels = ARRAY_SIZE(fxos8700iio_channels) : afin de spécifier le nombre de channels.

3.2.2 Fonction remove()

Comme prévu, cette fonction sera en charge de réaliser le désenregistrement au framework (fonction iio_device_unregister) ainsi que la mise en stand-by de notre capteur (fxos8700iio_enable) :

static int fxos8700iio_remove(struct i2c_client *client)

{

   struct iio_dev *indio_dev = i2c_get_clientdata(client);

   iio_device_unregister(indio_dev);

 

   return fxos8700iio_enable(client, false);

}

3.3 do_deploy()

Maintenant le driver terminé, compilons-le à nouveau et mettons-le sur la cible afin de le tester. Pour ce faire, nous allons utiliser la commande insmod pour charger notre pilote :

root@imx7s-warp:~# insmod fxos8700iio.ko

fxos8700iio 3-001e: FXOS8700 configured!

Nous observons que la configuration du périphérique s’est faite sans embûche, vérifions maintenant les différents channels à disposition :

root@imx7s-warp:~# ls -l /sys/bus/iio/devices/iio\:device1/

-r--r--r-- 1 root root 4096 Dec 31 15:17 dev

-rw-r--r-- 1 root root 4096 Dec 31 15:17 in_accel_scale

-rw-r--r-- 1 root root 4096 Dec 31 15:17 in_accel_x_raw

-rw-r--r-- 1 root root 4096 Dec 31 15:17 in_accel_y_raw

-rw-r--r-- 1 root root 4096 Dec 31 15:17 in_accel_z_raw

-rw-r--r-- 1 root root 4096 Dec 31 15:17 in_magn_scale

-rw-r--r-- 1 root root 4096 Dec 31 15:17 in_magn_x_raw

-rw-r--r-- 1 root root 4096 Dec 31 15:17 in_magn_y_raw

-rw-r--r-- 1 root root 4096 Dec 31 15:17 in_magn_z_raw

-rw-r--r-- 1 root root 4096 Dec 31 15:17 in_temp_input

Bingo, l’ensemble de la configuration est disponible ! Faisons un test de lecture via sysfs :

root@imx7s-warp:~# cat /sys/bus/iio/devices/iio\:device1/in_temp_input

27

Et voilà, nous avons bien la valeur du capteur à disposition via notre pilote de périphérique ! Il en sera de même sur l’accéléromètre et le magnétomètre ! (à vous de jouer)

4. Un peu de QML …

Cette dernière partie propose d’étudier les premières étapes dans le développement d’une interface graphique en QML, le tout sur l’écran LCD de la WaRP7 (fraîchement supporté). On y présentera aussi les méthodes de récupération de données de notre capteur et on y introduira l’utilisation de la mémoire partagée, très efficace et utile dans les projets où de nombreux processus doivent s’échanger des données.

4.1 Préparation de l’environnement

La première étape à effectuer pour prétendre développer du code sur cible, et de se munir d’une chaîne de compilation croisée dédiée avec l’environnement Qt. Pour ce faire, rien de mieux que l’utilisation du projet Yocto, celui-ci nous permet via la commande bitbake meta-toolchain-qt5, de générer l’ensemble des outils nécessaires. Voici ci-dessous par exemple les étapes de l’installation :

$ cd tmp/deploy/sdk

$ ./warp7-glibc-x86_64-meta-toolchain-qt5-cortexa7hf-neon-toolchain-2.4.1.sh

WaRP7 powered by Yocto/OE SDK installer version 2.4.1

=========================================================================

Enter target directory for SDK (default: /opt/warp7/2.4.1) :

Puis, il conviendra de « sourcer » notre environnement afin d’avoir un accès aux outils de compilations croisées (compilateur, debugger, qmake, etc.) :

$ source /opt/warp7/2.4.1/environment-setup-cortexa7hf-neon-poky-linux-gnueabi

$ qtcreator &

Une fois cette étape effectuée, lançons QtCreator pour la création du projet : Fichier > Nouveau Fichier ou Projet… > Qt Console Application. Une fois sur le projet, il nous faut ajouter deux choses essentielles dans le fichier .pro :

  • target.path = /usr/bin/, qui indique à quel endroit sera installé le produit de la compilation ;
  • INSTALLS += target, qui indique d'installer le binaire généré (à l'endroit spécifié, dans notre cas /usr/bin/).

4.2 Récupération des données : « show me the code ! »

La récupération des données mises à disposition par le pilote de périphérique est comme nous l’avons vu plus haut, un simple accès en lecture via sysfs, définissons l’ensemble des channels :

#define TEMP "/sys/bus/iio/devices/iio:device1/in_temp_input"

#define ACCEL_X "/sys/bus/iio/devices/iio:device1/in_accel_x_raw"

#define ACCEL_Y "/sys/bus/iio/devices/iio:device1/in_accel_y_raw"

#define ACCEL_Z "/sys/bus/iio/devices/iio:device1/in_accel_z_raw"

#define ACCEL_SCALE "/sys/bus/iio/devices/iio:device1/in_accel_scale"

#define MAGNET_X "/sys/bus/iio/devices/iio:device1/in_magn_x_raw"

#define MAGNET_Y "/sys/bus/iio/devices/iio:device1/in_magn_y_raw"

#define MAGNET_Z "/sys/bus/iio/devices/iio:device1/in_magn_z_raw"

#define MAGNET_SCALE "/sys/bus/iio/devices/iio:device1/in_magn_scale"

Puis on développera une fonction qui permettra :

  • de lire dans un fichier tous les caractères présents ;
  • de retirer les caractères de fin de chaîne ;
  • de retourner la valeur interprétée en double.

Pour ce faire, on utilisera la classe QFile de Qt (classe permettant la gestion des fichiers). Pour l’accès à la donnée, il suffira d’ouvrir le fichier (avec le chemin en paramètre) puis de lire son contenu via la fonction readAll() :

double stringToValue(QString path)

{

   double value = 0;

   QFile file(path);

   if (file.exists())

   {

      if (file.open(QIODevice::ReadOnly))

      {

         QByteArray array = file.readAll().trimmed();

         value = array.toDouble();

         file.close();

      }

   }

   return value;

}

Note

La fonction trimmed() vient juste nous simplifier la vie en retirant les espaces en début et fin de chaîne, incluant aussi les caractères spéciaux : '\t', '\n', '\v', '\f', et '\r'.

Pour finir, il ne reste plus qu’à créer une boucle d’acquisition pour la mise à disposition des données. Utilisons la classe QTimer pour gérer cet aspect :

QTimer m_timer;

Puis dans le constructeur, nous définirons celui-ci en associant la fonction slot :

connect(&m_timer, SIGNAL(timeout()), this, SLOT(readData()));

m_timer.start(10);

La fonction readData() sera alors appelée toutes les 10 millisecondes. C’est dans cette dernière qu’on accèdera aux valeurs de l’accéléromètre, du magnétomètre et du capteur de température (toujours au travers du sous-système IIO) :

void CSensors::readData()

{

   // lecture des données

   m_temperature = stringToValue(TEMP);

   m_accelerometerX = stringToValue(ACCEL_X) * m_accelerometerScale;

   m_accelerometerY = stringToValue(ACCEL_Y) * m_accelerometerScale;

   m_accelerometerZ = stringToValue(ACCEL_Z) * m_accelerometerScale;

   m_magnetometerX = stringToValue(MAGNET_X) * m_magnetometerScale;

   m_magnetometerY = stringToValue(MAGNET_Y) * m_magnetometerScale;

   m_magnetometerZ = stringToValue(MAGNET_Z) * m_magnetometerScale;

   ...

}

4.3 Partage-moi si tu peux !

Pour la suite du projet, nous avons décidé de séparer la partie acquisition de la partie graphique (architecture producteur/consommateur). La communication entre les deux processus se fera donc via de la mémoire partagée. Il faut savoir qu’avec le framework Qt, la mémoire partagée s’utilise à travers la classe QSharedMemory. Cette classe permet de créer très facilement un segment de mémoire partagé, de lire et d’écrire dessus.

Commençons par inclure le fichier d’en-tête :

#include <QSharedMemory>

Puis faisons simplement hériter notre classe de cet objet :

class CSensors: public QSharedMemory

Il ne manque plus que deux lignes de code dans le constructeur et le tour est joué. Associons d’abord une clé au segment de mémoire :

this->setKey("DataExchangeMemory");

Puis, définissons la taille en octets pour celui-ci, avec les droits d’accès associés :

this->create(52, QsharedMemory::ReadWrite);

Lors de la création d’un segment de mémoire partagée, la taille de celui-ci devra être préalablement définie. Il faudra pour nos besoins définir :

  • trois valeurs de type double pour l’accéléromètre (X, Y et Z) ;
  • trois valeurs de type double pour le magnétomètre (X, Y et Z) ;
  • une valeur de type double pour le capteur de température.

Pour le reste, après la lecture des données des capteurs, on écrira directement dans le segment de mémoire. On utilisera ici la classe QDataStream qui permet de sérialiser très facilement les données. Il faut bien entendu utiliser les « mutex » proposés par Qt afin d’éviter tout accès concurrent durant une écriture.

if (this->isAttached()) // vérifier si le segment est attaché

{

   // écriture sur la mémoire partagée

   QByteArray sharedData;

   QDataStream stream (&sharedData, QIODevice::WriteOnly);

   stream << m_temperature;

   stream << m_accelerometerX;

   stream << m_accelerometerY;

   stream << m_accelerometerZ;

   stream << m_magnetometerX;

   stream << m_magnetometerY;

   stream << m_magnetometerZ;

   this->lock();

   char *to = (char*)this->data();

   memcpy(to, sharedData.data(), qMin(this->size(), (qint32) sharedData.size()));

   this->unlock();

}

else

   this->attach(); // attacher le segment

Et pour lire le segment de mémoire partagée depuis un autre processus, le principe reste le même :

QSharedMemory m_sharedMemory;

On initialisera ensuite la mémoire partagée :

m_sharedMemory.setKey("DataExchangeMemory");

m_sharedMemory.attach(QSharedMemory::ReadOnly);

Tout comme l’écriture, la lecture des données sera cadencée par le timeout du Timer (QTimer) :

if (m_sharedMemory.isAttached())

   {

      QByteArray data;

      m_sharedMemory.lock();

      data.setRawData((char*)m_sharedMemory.constData(), m_sharedMemory.size());

      m_sharedMemory.unlock();

      QDataStream stream(data);

      stream >> m_temperature

             >> m_accelerometerX

             >> m_accelerometerY

             >> m_accelerometerZ

             >> m_magnetometerX

             >> m_magnetometerY

             >> m_magnetometerZ;

   }

else

   m_sharedMemory.attach();

Le résultat est probant, l’accès à la donnée via le segment de mémoire est extrêmement rapide (quelques centaines de microsecondes). Ce type d’architecture peut s’avérer très utile dans le cas où un processus souhaite partager sa donnée avec de nombreux autres processus (mise à jour plus simple, moins de threads à gérer donc debug moins douloureux...).

4.4 Affiche-moi si tu peux !

Attaquons-nous maintenant à la partie graphique, pour ce faire nous allons créer un nouveau projet : Fichier > Nouveau Fichier ou Projet… > Qt Quick Application – Empty.Puis, une fois le projet ouvert, rajoutons la ligne suivante dans le fichier .pro :

QT += quick

Puis, la première chose à faire dans le fichier main.cpp, sera d’implémenter le mécanisme permettant de charger nos pages QML, ceci via la classe QQmlApplicationEngine qui permet par ailleurs de gérer les différents accès C++/QML :

QQmlApplicationEngine engine;

engine.load(QUrl(QStringLiteral("qrc:/main.qml")));

if (engine.rootObjects().isEmpty())

   return -1;

return app.exec();

Le QML est un langage de description d’interface. Sa syntaxe est très largement inspirée du langage C, notamment, pour l’utilisation des structures. Il existe ainsi de très nombreux objets prédéfinis afin de créer des états, des animations, des rectangles, des images.

L’idée globale du projet sera de créer un petit menu permettant d’afficher les différents éléments en provenance du driver de périphérique fxos8700iio (via sysfs). C’est aussi un moyen pour nous, de démontrer le potentiel de cette plateforme (i.MX7) qui, notons-le, ne possède pas de processeur graphique. L’ensemble du rendu graphique s’effectue au travers du module Qt Quick 2D Renderer.

On va donc créer les fichiers « *.qml » suivants :

  • main.qml : c’est le main ! Chargé en premier, ce sera le point d’entrée de l’application ;
  • MainMenu.qml : ce fichier permettra de créer le menu principal avec définition de sous-menus, méthode de navigation et chargement des sous-menus ;
  • MenuAccelerometer.qml : il permettra d’afficher une balle de foot dont la position évoluera en fonction des valeurs de l’accéléromètre ;
  • MenuTemperature.qml : il permettra d’afficher la valeur de la température (en degré Celsius.) ;
  • WarpButton.qml : il permettra de créer un bouton dont l’image de fond est configurable. On utilisera cet objet bouton pour le menu.

Commençons par créer une fenêtre prenant comme dimensions les limites de l’écran LCD (320 x 320). Avec à l’intérieur :

import QtQuick 2.9 // module QtQuick

import QtQuick.Window 2.2 // module Window

 

Window { // création d'une fenêtre

   visible: true

   width: 320

   height: 320

   MainMenu {

      anchors.fill: parent

   }

}

L’étape suivante consiste donc à créer notre menu. On va tout d’abord créer un objet bouton qui permettra de remplir le menu : l’objet WarpButton.qml. En QML, il existe déjà des objets boutons avec la gestion du clic. Nous allons juste ici rajouter une image de fond et faire en sorte que sa taille augmente lorsqu’on clique sur le bouton. Pour du tactile, cela permet de créer la sensation du clic.

Item {

   property string iconName // propriété contenant le chemin de l'image

   signal buttonClicked;    // signal indiquant le clic sur le bouton

   Button {

      id: button

      anchors.centerIn: parent

      width: parent.width * 0.60

      height: parent.height * 0.60

 

      background: Rectangle {

         id: background

         width: parent.width

         height: parent.height

         Image {

            id: image

            fillMode: Image.Stretch

            smooth: true

            anchors.centerIn: parent

            source: iconName

            sourceSize.width: parent.width * (button.pressed? 1.2: 0.9)

            sourceSize.height: parent.height * (button.pressed? 1.2: 0.9)

         }

      }

      onClicked: {

         buttonClicked()

      }

   }

}

Vous l’aurez compris, l’objet « parent » représente toujours l’objet qui le contient, donc, son parent. Pour des projets avec une forte profondeur, on préfèrera parfois utiliser l’id de l’objet que l’on souhaite appeler afin de rendre le code plus lisible.

Encore un peu de patience et nous aurons un premier résultat. Il manque tout de même l’ajout des images dans notre exécutable ! Sinon il ne pourra rien afficher. Qt propose sa propre gestion des ressources : le « Qt Resource File ». En clair, il permet de stocker des fichiers binaires dans notre application (exécutable). On va donc créer un fichier resources.qrc et on y ajoutera nos images (voir figure 6).

 

Fig. 6 : Les ressources !

Faisons un test, remplaçons l’objet MainMenu dans le fichier main.qml par le code suivant :

WarpButton {

   anchors.fill: parent

   id: temperatureBtn

   iconName: "qrc:/Images/Images/temperature.svg"

}

On crée un l’objet préalablement définit, on lui spécifie le chemin de son image dans les ressources (voir figure 7).

Fig. 7 : Bouton Température.

Allons un peu plus loin. Nous voulions créer un menu tactile permettant de sélectionner la fonctionnalité : il faudra pouvoir swiper entre les menus. QML propose déjà cette solution à travers l’objet SwipeView :

   SwipeView {

      id: view

      anchors.fill: parent

      currentIndex: 1 //index par défaut

      WarpButton {

         id: temperatureBtn

         iconName: "qrc:/Images/Images/temperature.svg"

      }

      WarpButton {

         id: acceleroBtn

         iconName: "qrc:/Images/Images/3d.svg"

      }

      WarpButton {

         id: magnetoBtn

         iconName: "qrc:/Images/Images/magnetic-field.svg"

      }

   }

   PageIndicator { //indicateur permettant d'identifier l'index en cours

      id: indicator

      count: view.count

      currentIndex: view.currentIndex

      anchors.bottom: view.bottom

      anchors.bottomMargin: 25

      anchors.horizontalCenter: parent.horizontalCenter

   }

On rajoutera ici un indicateur afin d’identifier notre position dans le menu. Le rendu de ce menu est visible en figure 8.

Fig. 8 : Menu principal.

Lors du clic sur le bouton, on obtient le résultat de la figure 9.

Fig. 9 : Menu principal – Sélection.

Puis sur un balayage de l’interface, on passe à la figure 10.

Fig. 10 : Menu principal – Balayage.

Après des résultats plutôt satisfaisants, il ne manque plus qu’à implémenter la gestion du clic et le chargement des menus associés. Pour le clic, on utilisera le signal buttonClicked créé dans l’objet WarpButton :

WarpButton {

   id: temperatureBtn

   iconName: "qrc:/Images/Images/temperature.svg"

   onButtonClicked: {

   }

}

Dans le cadre de la connexion avec les signaux/slots, Qt ajoute systématiquement le préfixe « on » suivi du nom du signal (avec la première lettre en majuscule). Donc le signal buttonClicked deviendra onButtonClicked afin de définir la fonction slot.

Le dernier point à aborder est donc le chargement d’une page et son déchargement. Pour cela, en QML on utilise l’objet Loader. Il va nous permettre de superposer de nouvelles pages :

Loader {

   id: mainLoader

   focus: true

   // définit une propriété permettant de valider si un "item" a été chargé

   property bool valid: item !== null

}

Et pour charger une page :

onButtonClicked: {

   mainLoader.source = "MenuTemperature.qml"

}

Afin de décharger une page et revenir au menu principal, il faudra ajouter des signaux dans les sous-menus indiquant au loader de décharger la page en cours :

   signal pageExit()

Et la connexion avec le signal se fera de la manière suivante :

   Connections {

      target: mainLoader.valid? mainLoader.item : null

      onPageExit: { mainLoader.source = "" }

   }

On spécifiera une cible target qui est la page chargée par le « Loader ». Et lors de l’activation du signal pageExit, on déchargera la page.

On va enfin pouvoir créer les deux sous-menus qui nous intéressent : Température et Accéléromètre. Commençons par le capteur de température, on va ici chercher à afficher la température et créer un dégradé de couleurs en fonction de cette dernière. Pour la suite du code, nous prenons pour acquis que la valeur DataManager.temperature renvoie la température en degré et nous l’expliciterons après :

Item {

   id:menuTemp

   width: 320

   height: 320

   signal pageExit()

   Rectangle {

      id:rect

      anchors.fill: parent

      // création d'un "layout" vertical

      ColumnLayout {

         anchors.fill: parent

         // zone de clic

         MouseArea {

            anchors.fill: parent

            // un double clic provoque la demande de déchargement de la page

            onDoubleClicked: menuTemp.pageExit()

         }

         Text {

            anchors.centerIn: parent

            text: DataManager.temperature + "°C"

            font.family: "lightsteelblue"

            font.pointSize: 40

         }

      }

      // couleur qui évolue en fonction de la température

      property color finalColor : Qt.rgba(0.56 * DataManager.temperature/30,

                                          1 / (DataManager.temperature /30),

                                          0.40 / (DataManager.temperature/30),

                                          1)

      // gradient de couleur du Rectangle

      gradient: Gradient {

         GradientStop { position: 0.0; color: rect.finalColor; }

         GradientStop { position: 1.0; color: "white"; }

      }

   }

}

On obtient le résultat de la figure 11 lorsqu’on clique sur le menu température.

Fig. 11 : Affichage de la température.

Et pour l’accéléromètre, nous avons décidé de proposer une « balle de foot » qui se déplace en fonction du comportement de ce dernier. Le code est plus simple et propose juste un rectangle avec un radiant important (permettant de le transformer en cercle). La position du rectangle ajustée en fonction de la position que nous récupérons de l’accéléromètre. Encore une fois, les valeurs DataManager.accelerometreX et DataManager.accelerometreY vont être explicitées :

   Rectangle {

      radius: 150

      width: 30

      height: 30

      Image {

         source: "qrc:/Images/Images/ball.svg"

         fillMode: Image.Stretch

         smooth: true

         anchors.centerIn: parent

         sourceSize.width: parent.width

         sourceSize.height: parent.height

      }

      x: -(DataManager.accelerometerX / 10) * 120 +160

      y: (DataManager.accelerometerY / 10 ) * 120 +160

   }

Et voici le résultat en figure 12 !

Fig. 12 : L’accéléromètre en action !

Pour faire bouger tout ça, il nous manque la récupération des données depuis la mémoire partagée et que le QML puisse lire ces données. Après avoir mis à jour les données via la mémoire partagée, on peut tout simplement exposer les variables au QML via la définition d’une Q_PROPERTY :

Q_PROPERTY(double temperature MEMBER m_temperature NOTIFY dataChanged)

Et pour que le QML en soit averti, il faut émettre le signal dataChanged() à chaque tour de boucle :

Q_EMIT dataChanged();

La dernière chose à faire est d’exposer la classe au QML dans le fichier main.c pour qu’il puisse accéder aux différents membres :

CDataManager *dataManager = new CdataManager;

QQmlApplicationEngine engine;

// export des classes vers QML

engine.rootContext()->setContextProperty("DataManager", dataManager);

Depuis le QML, l’accès à la donnée se fait donc de la manière mystérieuse et inexpliquée évoquée plus haut :

DataManager.temperature

La boucle est bouclée, et l’application peut enfin s’animer. L’ensemble de l’application est traité par le processeur à défaut d’avoir une partie graphique. L’application se révèle extrêmement fluide et l’utilisateur ne doit pas se restreindre dans le jeu de fonctionnalités.

5. fork me

Si vous souhaitez mettre en œuvre ce mini projet sur votre plateforme, vous trouverez les sources disponibles sur les référentiels Git suivants :

Conclusion

Dans cet article, nous avons pu faire une première introduction au sous-système noyau IIO qui somme toute reste légère, car nous n’avons en effet pas abordé les notions plus avancées comme les triggers IIO ou exploité des outils comme libiio, ni même parlé des notions de Power Management (ACPI). C’est pourquoi les auteurs recommanderont la lecture de l’ouvrage de John Madieu [4] pour les personnes désireuses de pousser l’étude plus loin. De plus, sachez qu’il existe une mailing list (linux-iio), concernant les différentes discussions relatives au sous-système. On notera d’ailleurs la forte implication des experts de Bootlin (anciennement Free Electrons : https://bootlin.com/blog/free-electrons-becomes-bootlin/), oui encore des Français !

Finalement, nous avons mis en pratique notre pilote de périphérique, via la command line dans un premier temps, puis au travers d'un mini-projet s’articulant autour de l’utilisation du framework Qt. Le tout permettant l’affichage des données en QML/C++.

Les concepts en main, il serait maintenant intéressant d’appliquer cette étude au gyroscope présent sur la même carte.

Références

[1] FICHEUX P., « Comment ne pas écrire de pilotes Linux », Open Silicium n°19, juillet 2016 : https://connect.ed-diamond.com/Open-Silicium/OS-019/Comment-ne-pas-ecrire-de-pilotes-Linux

[2] TEXIER P.-J. et CHABRERIE J., « A la découverte de la WaRP7 », Open Silicium n°20, octobre 2016 : https://connect.ed-diamond.com/Open-Silicium/OS-020/A-la-decouverte-de-la-WaRP7

[3] Spécifications du device-tree : https://github.com/devicetree-org/devicetree-specification/releases/tag/v0.2

[4] MADIEU J., « Linux Device Drivers Development : Develop customized drivers for embedded Linux », Packt Publishing, 2017.