L'important choix de cartes intégrant des capteurs/actionneurs, en fait aujourd'hui, un choix difficile pour l'utilisateur final.C'est pour ces raisons que MikroElektronika a créé un standard : MikroBUS. Il facilite l'interaction entre microcontrôleur ou microprocesseur et les cartes d'extensions, appelées « add-ons », utilisant cette connectique.Cet article se propose d'explorer la partie MikroBUS d'un des tous derniers SBC (Single Board Computer) du marché : la WaRP7 (« WearAble Reference Platform »). On commencera dans un premier temps par une succincte présentation de la WaRP7 et d'une partie « board bring-up » (via Yocto/OE). Puis, viendra la partie MikroBUS qui sera mise en avant à travers un mini projet architecturé autour du Bluetooth Low Energy et du framework Qt5 pour Android tout en y intégrant une carte add-ons, carte qui se base sur le standard MikroBUS.
Le présent article s'inscrit dans la continuité des manipulations ayant permis la rédaction d'un article précédent paru dans le n°20 d'Open Silicium [1], les auteurs renverront vers celui-ci les lecteurs qui désirent avoir de plus amples connaissances sur la diversité que propose la cible utilisé dans ce numéro de Linux Magazine. Dans le premier article, il était question de découvrir la WaRP7 et ses capteurs au travers une application « IoT » minimaliste (récupération d'une température, la pression atmosphérique ainsi qu'une valeur représentant le rythme cardiaque), ceci en utilisant la connectivité Bluetooth et le frameworkQt5, le tout basé sur une distribution Yocto/OE mis à disposition par les auteurs.
Que le lecteur ne pouvant pas se procurer le premier article soit rassuré, on se permettra tout de même ici de faire des rappels (ouf !) quant aux éléments essentiels (présentation de la cible, génération et installation de l'image sur notre cible, une partie Bluetooth, puis quelques mots sur le SDK Yocto/Qt5).
1. Introduction
1.1 La cible : « WaRP7 », petit rappel
La plateforme cible [2] est composée de 2 cartes :
- Une carte « fille », qui est construite autour d'un System on Chip NXP i.MX7 Solo[3] (avec un coeur Cortex A7 + un coeur Cortex M4). De plus, cette carte embarquera la gestion de la connectivité (Wifi/BLE) ;
- Une carte « mère », qui contiendra l'ensemble des capteurs (Gyroscope, Altimètre, etc.), ainsi que l'extension MikroBUS (qui nous intéresse particulièrement dans cet article).
Fig. 1: Non, la WaRP7 n'est pas timide
1.2 Construction de notre distribution « IoT »
La génération de la distribution pour notre cible, repose sur l'utilisation du BSP (Board Support Package) NXP articulé autour du projet Yocto [4] et de deux couches supplémentaires (nous ne ferons pas ici, de détails quant à l'utilisation de Yocto/OE, le lecteur pourra en outre, se référer aux différents articles parus dans les précédents numéros d'open silicium).
La première étape consiste donc à récupérer l'ensemble des sources contenues dans le BSP. Pour ce faire, NXP utilise l'utilitaire repo (outil Python développé par Google [5]), ceci afin de permettre une meilleure gestion des référentiels Git compris dans celui-ci :
$ mkdir ~/bin
$ curl http://commondatastorage.googleapis.com/git-repo-downloads/repo > ~/bin/repo
$ chmod a+x ~/bin/repo
$ PATH=${PATH}:~/bin
$ mkdir warp7_glmf200
$ cd warp7_glmf200
$ repo init -u https://github.com/Freescale/fsl-community-bsp-platform -b krogoth
$ repo sync
Comme prévu, après récupération de notre base logicielle, nous pouvons maintenant télécharger les deux couches supplémentaires essentielles à la bonne construction de notre image.
Téléchargeons dans un premier temps la couche distribution mis à disposition par les auteurs [6], pour rappel, cette couche spécifique est intimement liée à l'article (ceci afin de ne pas se perdre dans le framework qu'est Yocto/OE) :
$ cd sources/
$ git clone https://github.com/bdx-iot/meta-iot.git
Dans un second temps, récupérons la couche permettant d'intégrer l'environnement Qt5 à notre distribution :
$ cd sources/
$ git clone https://github.com/meta-qt5/meta-qt5.git
Nous voilà maintenant en possession de l'ensemble des sources. L'étape d'après consiste en la création de l'environnement, on retrouve ci-après les différentes étapes :
- création du dossier de construction warp7-build/,
- appel du script oe-init-build-env,
- mise à jour de la variable MACHINE,
- puis un prompt concernant la licence FSL EULA (à accepter).
La commande suivante s'occupera donc de nous placer dans un environnement de travail spécifique à notre plateforme de développement :
$ MACHINE=imx7s-warpsource setup-environment warp7-build/
Il reste maintenant à définir les chemins vers les couches précédemment téléchargées, pour ce faire, modifions le fichier conf/bblayers.conf pour y rajouter les deux lignes suivantes :
${BSPDIR}/sources/meta-iot \
${BSPDIR}/sources/meta-qt5 \
Dernière étape avant de pouvoir lancer la construction de l'image, il nous faut spécifier la distribution utilisée (dans notre cas une distribution spécifique à l'article dérivé de la distribution poky) dans conf/local.conf, en mettant à jour la variable DISTRO :
DISTRO ?= 'iot'
Lançons maintenant la construction de notre image par le biais de la commande bitbake :
$ bitbake iot-qt-image
Après cette longue étape qu'est la construction, l'utilisateur pourra retrouver très facilement l'ensemble des fichiers utiles au bon démarrage de la plateforme (Bootloader, Kernel, RootFileSystem) dans tmp/deploy/images/imx7s-warp.
1.3 Premier Flash, premier Boot, …
Rappelons qu'afin de mettre à jour le système avec l'ensemble des binaires compilés, les étapes se déroulent comme suit :
- Connexion en liaison série (via un émulateur de terminal comme microcom), puis arrêt de l'auto-boot au niveau du bootloader (u-boot) :
$ microcom --speed 115200 --port /dev/ttyUSB0
...
U-Boot 2016.07+fslc+ge6b4241 (Sep 14 2016 - 11:21:52 +0200)
CPU: Freescale i.MX7S rev1.2 at 792MHz
...
Hit any key to stop autoboot: 0
- Montage de la partition eMMC côté bootloader :
=> ums 0 mmc 0
- Décompression et opération de copie coté système hôte sur le device spécifique :
$ cd tmp/deploy/images/imx7s-warp
$ gunzip qt-image-imx7s-warp-20161118195314.rootfs.sdcard.gz
$ sudo dd if=qt-image-imx7s-warp-20161118195314.rootfs.sdcard of=/dev/sdX (où X représente le device)
Une fois la copie terminée, l'utilisateur pourra redémarrer la carte pour voir apparaître la séquence de démarrage :
Starting kernel ...
Booting Linux on physical CPU 0x0
Linux version 4.1.32-fslc+g9d3f7f9 (pjtexier@bdxiot) (gcc version 5.3.0 (GCC) ) #1 SMP PREEMPT
...
IOT powered by Yocto/OE (Welcome to Bordeaux) 2.1.1 iot /dev/ttymxc0
iot login: root
Password: iot
___ ___ _____
|_ _/ _ \_ _|
| | | | || |
| | |_| || |
|___\___/ |_|
root@iot:~#
1.4 Activation de l'interface Bluetooth
Rappelons ici, comment attacher, initialiser et activer notre interface Bluetooth, afin que celle-ci soit accessible par les autres périphériques compatibles :
root@iot:~# hciattach ttymxc2 bcm43xx 3000000 flow -t 20
root@iot:~# hciconfig hci0 up
root@iot:~# hciconfig hci0 name 'Hello GLMF200' // Nom de l'interface visible depuis n'importe quel périphérique bluetooth
root@iot:~# hciconfig hci0 piscan
1.5 SDK Qt5
1.5.1 Génération
Nous avons évoqué lors de l'article précédent la notion de SDK et chaîne de compilation croisée (mais pas que). Ceci afin de pouvoir générer du code Qt5 sur notre cible. La commande ci-dessous permettra la génération d'un SDK générique et compatible Qt5 :
$ bitbake meta-toolchain-qt5
Une fois la génération terminée, le script d'installation du SDK se trouvera dans tmp/deploy/sdk.
1.5.2 Installation
Elle se fera tout simplement en exécutant le script suivant où il conviendra de spécifier le chemin d'installation (/opt/iot/sdk dans cet exemple) :
$ cd tmp/deploy/sdk
$./iot-glibc-x86_64-meta-toolchain-qt5-cortexa7hf-neon-toolchain-2.1.1.sh
IOT powered by Yocto/OE (Welcome to Bordeaux) SDK installer version 2.1.1
=========================================================================
Enter target directory for SDK (default: /opt/iot/2.1.1): /opt/iot/sdk
1.5.3 Intégration à l'IDE Qt creator
Afin de se voir l'environnement de compilation croisée, intégré à notre IDE préféré, il nous faudra :
- Dans
choisir
/opt/iot/sdk/sysroots/x86_64-pokysdk-linux/usr/bin/arm-poky-linux-gnueabi/arm-poky-linux-gnueabi-gcc
- Dans /opt/iot/sdk/sysroots/x86_64-pokysdk-linux/usr/bin/qt5/qmake
, Qt est normalement automatiquement détecté. Sinon le lecteur devra ajouter le chemin vers qmake :- Dans linux-oe-g++ » :
, il ne reste plus qu'à choisir les éléments précédemment mis en place (Compilateur et version), il sera aussi recommandé de spécifier le mkspecs «2. MikroBUS
Dans cette partie nous aborderons, dans un premier temps, le standard MikroBUS, nous présenterons autant les aspects matériels que les aspects logiciels que propose celui-ci. Nous ferons ensuite un bref tour d'horizon sur les modules que propose MikroElektronika.
2.1 Le standard : introduction
Le standard MikroBUS définit les connectiques des cartesprincipales (on parle souvent de carte mère)ainsi que celles des cartes additionnelles (add-on) utilisées pour l'interfaçage du microcontrôleur ou du microprocesseur avec les circuits et modules complémentaires. De plus, ilspécifie la disposition physique du brochage, les broches de communication et lesbroches d'alimentation utilisées.
On remarque, bien évidemment, que l'objectif de mikroBUS est de jouer sur laflexibilité matérielle, permettant ainsi un interfaçage plus facileavec un grand nombre de cartes complètes, qui plus est, standardisées (plus de 200 modèles commercialisés par MikroElektronika tout de même !), chacune avec un simple capteur (humidistance), un module radio(RFID), un écran (OLED), une connectique (RS232), ou tout autre module électronique[7].
A noter que mikroBUS est un standard ouvert et donc n'importe qui peut prétendre implémenter mikroBUS dans sa conception matérielle, à condition bien sûr de respecter les conditions de MikroElektronika.
2.2 Le standard : Description du connecteur MikroBUS
Le connecteur MikroBUS est composé de 16 broches (2x8). L'ensemble de celles-ci est toujours positionné de la même manière comme le montre la figure 2.
Fig. 2: pinout du standard MikroBUS
On retrouvera sur chaque module :
- Des broches par type de bus de communication : SPI, UART, I2C.
- 4 broches complémentaires : PWM (ou MLI en français), interruption matérielle (INT), entrée analogique (AN) et reset (RST).
- Puis un couple pour les alimentations :
- 3,3V / GND
- 5V / GND
2.3 Quelques Examples :
Il est bien évidemment compliqué de présenter ici l'ensemble des modules proposés par la firme MikroElektronika. Afin de donner au lecteur un bref aperçu des cartes disponibles, les auteurs auront choisi d'en exposer 2 au travers de cet article :
2.3.1 La « Relay Click Board » :
Fig. 3: La Relay Click sous les projecteurs
Cette carte embarque deux relais de puissance (G6D1AASI-5DC de référence Omron). Le pilotage vers la carte principale (dans notre cas la WaRP7) s'effectue via les broches suivantes du connecteur MikroBUS :
- Broche PWM pour le Relais 1 (RL1),
- Broche CS pour le Relais 2 (RL2).
De notre point de vue, ce ne sera ni plus ni moins que l'activation (état haut ou état bas) d'une broche GPIO (ouverture ou fermeture du relais).
2.3.2 La « custom » :
Fig. 4: L'inconnu au bataillon
Comme remarqué précédemment, il est possible de réaliser sa propre carte autour du standard MikroBUS. Dans cet exemple, le module est composé d'un gpio expander (microchip mcp23s08) pilotable au travers d'une interface SPI (mais aussi via le bus i2c). Cette option permet d'étendre les capacités de la carte principale quand celle-ci arrive à ses limites (ou si aucune GPIO physique n'est présente sur la carte mère).
3. Jouons un peu avec la « relay click board »
Viens maintenant le temps de jouer un peu avec notre module MikroBUS, celui-ci permettant d'étendre les capacités de notre WaRP7, nous mettant ainsi des relais à disposition (ceci afin de commander par exemple une lampe, ou tout autre objet ...). Dans ce chapitre nous verrons comment les piloter de façon simpliste en accédant aux GPIO depuis l'espace utilisateur. Nous aborderons ensuite le sujet du device tree et l'accès à la configuration des GPIO depuis celui-ci.
3.1 Plug !
Insérons notre module relais et constatons qu'il est très facile d'intégration entre notre plateforme cible et les modules MikroBUS (WaRP7 <-> Relay Click Board), ceci grâce à la standardisation :
Fig. 5: La WaRP7 en position !
3.2 And Play !
Sous GNU/Linux, l'accès aux GPIO [8] s'effectue dans /sys/class/gpio/gpioN où N représente le numéro de la GPIO (à condition que le driver associé soit présent → GPIO_SYSFS).
Nous savons par le biais de la schématique (disponible en [2]), que le relais 1 et sur la broche CPU « GPIO7_IO8 », qui, en espace utilisateur devient gpio200.
Le calcul est sous la forme : ((port_GPIO - 1)*32) + n°gpio.
Exemple pour le relais 1 : ((7-1)*32) + 8 = 200
La première étape consistera à exporter cette GPIO pour la rendre accessible depuis le système de fichier virtuel /sys, la commande si après créera donc un point d'accès gpio200 :
root@iot:~# echo 200 > /sys/class/gpio/export
L'interface étant disponible, il nous faut la paramétrer en tant que broche de sortie :
root@iot:~# echo out > /sys/class/gpio/gpio200/direction
Une fois configurée, il nous est possible de la piloter, ceci en agissant simplement sur la tension de sortie de la broche, via la commande suivante :
root@iot:~# echo 1 > /sys/class/gpio/gpio200/value
La broche de sortie passée à l'état haut, il nous est facile de constater le résultat sur la carte relais (led REL1), comme nous le montre la figure 6 :
Fig. 6: Relais 1 en fonctionnement
Nous ferons de même pour le relais numéro 2 (RL2 sur la « relay click ») qui lui est sur « GPIO4_IO23 » :
root@iot:~# echo 119 > /sys/class/gpio/export
root@iot:~# echo out > /sys/class/gpio/gpio119/direction
root@iot:~# echo 1 > /sys/class/gpio/gpio119/value
Fig. 7: Relais 2 en fonctionnement
3.2 Intégration au device-tree : « gpio-exporter & pin-muxing »
L'inconvénient des étapes (export/direction) précédentes réside dans le fait qu'il nous est obligatoire de les réexécuter à chaque démarrage de la WaRP7, car non statique au sein de la configuration matérielle. Il serait donc intéressant d'avoir une entrée statique. Nous irons un peu plus loin que la simple configuration des GPIO.
En effet, il serait aussi plaisant de pouvoir associer un nom à nos GPIO lors de la configuration de celles-ci, pour ce faire, nous intégrerons le travail de Martin FUZZEY[9] (qu'on ne manquera pas de remercier pour le travail effectué), qui a en effet développé un driver Kernel permettant d'avoir au sein de l'espace utilisateur, un lien nom/numérode GPIO. L'avantage de cette solution est que finalement, l'utilisateur ne se soucie plus du numéro de la GPIO, de ce fait, la configuration est beaucoup plus portable et maintenable.
3.2.1 Device tree : une petite introduction
L’arbre de périphériques (device tree ou encore DT) est une structure de données permettant de décrire le matériel d’un système, dérivée du format utilisé par Open Firmware pour encapsuler les informations de plateforme et les transmettre au noyau, qui utilise alors les données du DT pour trouver et enregistrer les périphériques du système.
La structure de données elle-même est un arbre de nœuds nommés et de propriétés. Chaque nœud contient des propriétés de simples paires « nom-valeur » et des nœuds fils. Afin d’être interprétée correctement par le noyau, l’arborescence doit suivre une structure prédéfinie. Une « liaison » (en anglais, binding) est une description de la façon dont un périphérique est décrit dans le DT. Un grand nombre de périphériques disposent de liaisons bien établies et documentées.
3.2.2 Anatomie de notre fichier dts « imx7s-warp-relay.dts »
Afin de ne pas surcharger le fichier dts (device tree source) principal (imx7s-warp.dts), les auteurs auront choisi de créer un fichier dts spécifique à l'intégration du module « relay click », en voici sa constitution :
#include "imx7s-warp.dts"
/ {
gpio_exporter: gpio-exporter {
compatible = "linux,gpio-exporter";
pinctrl-names = "default";
pinctrl-0 = <&pinctrl_gpioexporter_relay>;
out_RL1 {
gpios = <&gpio7 8 GPIO_ACTIVE_HIGH>;
output;
initial-state = <0>;
};
out_RL2 {
gpios = <&gpio4 23 GPIO_ACTIVE_HIGH>;
output;
initial-state = <0>;
};
};
};
&iomuxc {
pinctrl-names = "default";
imx7s-warp {
pinctrl_gpioexporter_relay: gpioexportergrp {
fsl,pins = <
MX7D_PAD_ENET1_RGMII_TD2__GPIO7_IO8 0x14
MX7D_PAD_ECSPI2_SS0__GPIO4_IO23 0x14
>;
};
};
};
Essayons de détailler celui-ci :
- #include "imx7s-warp.dts" fera référence au fichier principal la WaRP7 (définition de la carte), qui lui-même inclus la définition du SoC (System on Chip) i.MX7 (imx7d.dtsi).
- Au niveau du contenu du fichier :
- La création d'un nœud (node) avec son label associé, que l'on appellera gpio-exporter :
gpio_exporter: gpio-exporter {
compatible = "linux,gpio-exporter";
- Le mot clé compatible est une propriété qui permettra de faire le lien avec la partie driver (respectant la règle du plateform driver comme présenté par Pierre Ficheux en [10]). Ceci via la structure of_device_id qui sera utilisée avec le tableau gpio_exporter_dt_ids[]. Le lecteur curieux pourra jeter un coup d'oeil au code source dans tmp/work/imx7s_warp-poky-linux-gnueabi/linux-fslc-imx/4.1-1.0.x+gitAUTOINC+9d3f7f9343-r0/git/drivers/gpio/gpio-exporter.c :
static const struct of_device_id gpio_exporter_dt_ids[] = {
{ .compatible = "linux,gpio-exporter", },
} ;
- La propriété pinctrl-0 permet de donner la liste des broches qui seront soumises à une définition spécifique de leur état, ceci grâce au sous-système pinctrl qui permet de gérer le multiplexage des broches. Dans notre cas, la configuration s'effectuera dans le nœud fils pinctrl_gpioexporter_relay :
pinctrl-0 = <&pinctrl_gpioexporter_relay>;
- Il convient ensuite de créer un nœud fils (child node), le nom de celui-ci représentera le nom exposé au sein de l'espace utilisateur, nous garderons la même syntaxe que la carte « relay click », à savoir RL1 pour le pilotage du relais 1 :
out_RL1 {
gpios = <&gpio7 8 GPIO_ACTIVE_HIGH>;
output;
initial-state = <0>;
};
Il nous faudra aussi renseigner les propriétés à notre nœud fils :
- gpios : Référence vers la GPIO à exporter (binding standard défini dans les sources du Noyau Linux : Documentation/devicetree/bindings/gpio/gpio.txt). En plus du phandle (nœud vers le contrôleur associé), nous retrouverons le numéro de GPIO, 8 pour notre application. Puis le second argument signifie que celle-ci est active sur un niveau haut (GPIO_ACTIVE_HIGH).
- output : pour spécifier que l'on désire la positionner en sortie,
- initial-state : où on fixera sont état initial (état bas dans notre cas)
Il en sera de même pour le relais numéro deux, où seul le nom du sous-nœud et de la GPIO associé seront donc à modifier pour le piloter :
out_RL2 {
gpios = <&gpio4 23 GPIO_ACTIVE_HIGH>;
output;
initial-state = <0>;
};
};
Nous en resterons là quant à la présentation du fichier device tree, le lecteur pourra se référer au très bon article écrit par Thomas Petazzoni paru dans le n°17 d'Open Silicium [11].
3.3 Activation du driver Kernel
La partie device tree étant faite, il nous reste à activer le support du driver gpio-exporter au sein de notre image Kernel (zImage). Pour ce faire, nous passerons une fois de plus par l'environnement Yocto/OE, en invoquant la commande suivante :
$ bitbake linux-fslc-imx -c menuconfig
Puis dans
il nous faudra activer l'option comme le montre la figure 8.
Fig. 8: CONFIG_GPIO_EXPORTER au sein de menuconfig
L'ensemble des patchs relatifs au kernel (gpio-exporter, dts et fichier defconfig), sont directement intégrés au sein de la couche distribution mis à disposition par les auteurs : https://github.com/bdx-iot/meta-iot/tree/master/recipes-kernel (au travers une recette dérivée)
Nous pourrons dès à présent relancer une phase de compilation pour générer le fichier zImage et notre nouveau fichier dtb (transformation en un fichier binaire du fichier dts), qui sera donc imx7s-warp-relay.dtb :
$ bitbake linux-fslc-imx
3.4 Intégration
Nous allons, dans cette sous-partie, mettre à jour le fichier zImage ainsi que le fichier imx7s-warp-relay.dtb sur notre cible. Pour ce faire, il sera question, ici aussi, de monter la partition eMMC en mode mass storage via u-boot. Nous finirons naturellement cette sous-partie par une phase de test sur cible.
3.4.1 zImage et .dtb
Pour la copie de la zImage :
$ sudo cp tmp/work/imx7s_warp-poky-linux-gnueabi/linux-fslc-imx/4.1-1.0.x+gitAUTOINC+9d3f7f9343-r0/build/arch/arm/boot/zImage /media/<username>/Boot imx7s-/
Pour la copie du fichier dtb spécifique à la gestion du relais :
$ sudo cp tmp/work/imx7s_warp-poky-linux-gnueabi/linux-fslc-imx/4.1-1.0.x+gitAUTOINC+9d3f7f9343-r0/build/arch/arm/boot/dts/imx7s-warp-dtb /media/<username>/Boot imx7s-/
Plaçons-nous maintenant côté cible et spécifions à notre chargeur d'amorçage (u-boot dans notre cas) que nous souhaitons utiliser le nouveau fichier dtb fraichement généré (imx7s-warp-relay.dtb) intégrant la partie gpio-exporter pour la gestion de nos relais, mais nous souhaitons, dans une moindre mesure, que celui-ci soit pris en compte par défaut, ceci afin d'éviter l'obligation d'une connexion série à chaque démarrage de la cible, il nous suffit donc de faire :
=> setenv fdt_file imx7s-warp-relay.dtb
Ceci surcharge la variable fdt_file, variable qui contient la référence vers le fichier dtb à utiliser (de base, imx7s-warp.dtb dans les sources de u-boot). Sauvegardons maintenant notre environnement :
=> save
Saving Environment to MMC...
Writing to MMC(0)... done
Il nous est maintenant possible de démarrer notre cible via l'exécution de la commande bootcmd (qui contient une suite de commandes à exécuter au démarrage) :
=> run bootcmd
switch to partitions #0, OK
...
6390056 bytes read in 94 ms (64.8 MiB/s)
Booting from mmc ...
reading imx7s-warp-relay.dtb
37834 bytes read in 12 ms (3 MiB/s)
Kernel image @ 0x80800000 [ 0x000000 - 0x618128 ]
## Flattened Device Tree blob at 83000000
...
Starting kernel ...
3.4.2 Test en espace utilisateur
Afin de simplifier (encore plus) l'accès aux GPIO (via gpio-exporter), les auteurs auront préféré mettre en place la notion de lien symbolique afin de se créer un point d'accès dans /dev/mikrobus/*. En temps normal l'accès aux GPIO s'effectue dans /sys/devices/platform/gpio-exporter//out_RL*. Cette technique nous donnera donc le résultat suivant sur notre plateforme :
root@iot:~# ls -l /dev/mikrobus/out_RL*
lrwxrwxrwx 1 root root 50 Nov 18 10:26 /dev/mikrobus/out_RL1 -> /sys/devices/platform/gpio-exporter//out_RL1/value
lrwxrwxrwx 1 root root 50 Nov 18 10:26 /dev/mikrobus/out_RL2 -> /sys/devices/platform/gpio-exporter//out_RL2/value
Le script concernant la génération des liens symboliques est entièrement consultable sur le github des auteurs : https://github.com/bdx-iot/meta-iot/blob/master/recipes-iot/clicks-board-init/clicks-board-init/mikrobus.sh.
Tout comme en section précédente, nous pouvons de façon très simple, et ceci sans connaître le numéro de la GPIO, piloter nos relais (plutôt pas mal non ?) :
root@iot:~# echo 1 > /dev/mikrobus/out_RL1 // Pour activer le relais
root@iot:~# echo 0 > /dev/mikrobus/out_RL2 // Pour désactiver le relais
4. Mise en situation …
Nous clôturerons cet article sur un mini-projet (par simple habitude des auteurs) permettant ainsi au lecteur de mettre en pratique les notions évoquées tout au long de l'article. Celui-ci nous permettra d'aborder quelques nouvelles techniques, comme par exemple l'introduction au QML.
4.1 Présentation du projet
Nous nous servirons du travail effectué durant cette première partie de l' article, qui de ce fait, nous servira de base pour notre application « connectée ». Nous proposons ici de faire une évolution du serveur BLE du précédent article.
L'idée sera donc de créer :
- une application « serveur BLE », permettant le pilotage de notre carte « relay click », application qui sera sur la WaRP7.
- un client associé, qui, lui, sera sous forme d'application Android et qui enverra les données au serveur BLE (commande des relais).
La figure 9 (ci-après) illustre l'architecture globale que l'on se propose de mettre en œuvre durant cette dernière partie de l'article :
Fig. 9: structure du projet
4.2 Le Serveur
Interaction avec nos GPIO
La première étape consistera à créer une fonction nous permettant d’écrire sur les GPIO. Il suffit d’utiliser la classe QFile[12] qui permet la gestion de fichiers (et oui, sous Linux tout est fichier) :
m_relay1File = new QFile("/dev/mikrobus/out_RL1");
m_relay2File = new QFile("/dev/mikrobus/out_RL2");
Ceci étant fait, il ne reste plus qu’à ouvrir (via l'appel open()) le fichier (avec la permission d'écriture !), écrire la valeur souhaitée (0 ou 1) puis le refermer via l'appel close() :
m_relay1File->open(QIODevice::ReadWrite);
m_relay1File->write(data);
m_relay1File->close();
Dans le cas présent, « data » est de type QByteArray (simple tableau d’octets) contenant la valeur à envoyer sur notre GPIO. Nous verrons plus tard comment cette dernière est envoyée.
Et la partie BLE ?
Dans cet exemple, nous avons décidé de ne pas utiliser les services[13]/caractéristiques[14] standards proposés par le protocole Bluetooth, et de ce fait, nous avonscréé les nôtres pour les besoins de cet article, c’est bien plus ludique quand même !
La figure 10 indique comment nous allons architecturer nos données.
Fig. 10: Architecture du serveur BLE
Il faut donc d’abord avoir nos propresUUID(Service + Caractéristiques) :
#define SERVICE_UUID 0x1820
#define CHAR_UUID_RELAY1 0x2aa4
#define CHAR_UUID_RELAY2 0x2aa5
Que nous allons ensuite « caster » en QBluetoothUuid :
QBluetoothUuid serviceRelayUuid((quint32)SERVICE_UUID);
QBluetoothUuid charRelay1Uuid((quint32)CHAR_UUID_RELAY1);
QBluetoothUuid charRelay2Uuid((quint32)CHAR_UUID_RELAY2);
Les présentations étant faites, nous pouvons rentrer dans le vif du sujet, la création du serveur. Et bien, pour ce faire, rien de plus simple avec l'API proposé par Qt :
Nous déclarons dans un premier temps un « advertiser » qui définit la configuration de base de notre serveur, ceci grâce à la classe suivante :
QLowEnergyAdvertisingDatam_advertisingData;
Configurons son mode, son nom (GLMF_200_BLE) et nous y ajoutons un nouveau service (serviceRelay dans notre exemple) :
//Mode d'accessibilité
m_advertisingData.setDiscoverability(
QLowEnergyAdvertisingData::DiscoverabilityGeneral);
//Nom du serveur
m_advertisingData.setLocalName("GLMF_200_BLE");
//Ajout du service Relais
m_advertisingData.setServices(QList<QBluetoothUuid>() <<
serviceRelayUuid
);
Nous créons ensuite les caractéristiques voulues (charRelay1). Le descripteur va définir la gamme de fonctionnalités de la caractéristique associée.
QLowEnergyCharacteristicData charRelay1;
charRelay1.setUuid(charRelay1Uuid); //uuid définie de la caractéristique
charRelay1.setValue(QByteArray(2, 0));
charRelay1.setProperties(type); //précise le type de la propriété
const QLowEnergyDescriptorData clientConfig( //Descripteur standard
QBluetoothUuid::ClientCharacteristicConfiguration,
QByteArray(2, 0));
charRelay1.addDescriptor(clientConfig);
Puis nous les associons au service préalablement configuré. Nous définissons son type comme « Primaire » car il donne accès aux fonctionnalités standard des services :
//Couplage du service avec la caractéristique créée.
QLowEnergyServiceData serviceRelayData;
serviceRelayData.setType(QLowEnergyServiceData::ServiceTypePrimary);
serviceRelayData.setUuid(serviceRelayUuid);
serviceRelayData.addCharacteristic(charRelay1); //Ajout relai 1
serviceRelayData.addCharacteristic(charRelay2); //Ajout relai 2
La dernière étape arrive. Il ne reste plus qu’à créer le contrôleur, qui est, entre autres, le point d’entrée des périphériques Bluetooth. On ajoute alors le service relais à celui-ci et on démarre le serveur. Ainsi on rend la connexion possible :
//création du controlleur BLE
m_bleController = QLowEnergyController::createPeripheral();
//Permettra de savoir si un client se déconnecte afin de relancer l’”advertising”
connect(m_bleController,
SIGNAL(stateChanged(QLowEnergyController::ControllerState)),
this, SLOT(controllerStateChanged(QLowEnergyController::ControllerState)));
//ajout du service
m_serviceRelay = m_bleController->addService(serviceRelayData);
//permettra de récupérer les données reçues
connect(m_serviceRelay,
SIGNAL(characteristicChanged(QLowEnergyCharacteristic,QByteArray)),
this, SLOT(characteristicChanged(QLowEnergyCharacteristic,QByteArray)));
//démarrage "advertising"
m_bleController->startAdvertising(QLowEnergyAdvertisingParameters(),m _advertisingData, m_advertisingData);
Il ne reste donc plus qu’à implémenter la fonction qui traite la réception d’une donnée afin de savoir à quel relais elle s’applique et dans quel état nous devons le mettre. Il suffit assez simplement de lire l'UUID de la caractéristique reçue et de commuter celui concerné en fonction :
void CServerBLE::characteristicChanged(QLowEnergyCharacteristic c, QByteArray data)
{
switch(c.uuid().toUInt32())
{
case CHAR_UUID_RELAY1:
m_relay1File->open(QIODevice::ReadWrite);
m_relay1File->write(data);
m_relay1File->close();
break;
case CHAR_UUID_RELAY2:
m_relay2File->open(QIODevice::ReadWrite);
m_relay2File->write(data);
m_relay2File->close();
break;
}
}
Le travail est fini et le serveur est ainsi prêt à fonctionner. Si le lecteur veut s’y tenter, il pourra d'ores et déjà compiler l'application pour la déployer sur la cible (bitbake ServiceRelay).Pour de plus amples informations sur la gestion des paquets/déploiement, les auteurs renverront le lecteur sur un excellent article de Pierre Ficheux[15]).
Dans l’exemple complet proposé sur le Github des auteurs, nous avons rajouté la lecture de la température présente sur la Warp7 afin de donner un peu de vie à l’interface.
4.3 « Dans la peau du client »
Création de notre environnement
Le serveur est fonctionnel, donc prêt à être piloté. Il ne manque plus qu’à lui associer un client. L’idée ici est de proposer une application Android avec le framework Qt en QML.
QML (pour Qt Meta Language) est un langage de programmation graphique qui ressemble beaucoup au JSON. Il a été créé principalement pour les applications mobiles, son utilisation étant bien plus simple que le Designer standard pour plateforme mobile.
Avant de pouvoir démarrer le projet, il faut mettre en place notre environnement. Pour cela, il est suggéré ici, de suivre le tutoriel Qt pour Android qui est très bien documenté (http://doc.qt.io/qt-5/android-support.html), qui consiste en l'installation de la chaîne de compilation Qt pour Android ainsi que le NDK et le SDK fournis par Google. Il faudra bien entendu installer Java !
Afin de ne pas sacrifier de nombreuses pages, les auteurs renverront le lecteur sur le GitHub en ce qui concerne l'ensemble des sources à télécharger. A la suite de cela, nous pouvons alors ouvrir Qt Creator et :
- Dans
, il faudra ici remplir correctement les arborescences (JDK, NDK et SDK) comme le montre la figure 11.
Fig. 11: Configuration de notre environnement
- Puis dans
, il faudra créer le Kit associé (voir figure 12).
Fig. 12: Configuration de notre Kit
Un premier projet
En premier lieu, créons notre premier projet QML (Qt Quick Application) tout en sélectionnant le Kit Android pour le futur déploiement (smartphone).
Une application QML se scinde toujours en deux parties :
- une partie C++
- une partie script QML.
Il existe donc plusieurs outils pour communiquer entre les deux couches que nous expliciterons au cours de cette partie.
La partie C++
Dans cette partie, nous décrirons le client Bluetooth Low Energy. Et surtout, comment envoyer une donnée au serveur.
Dans un souci de simplicité, nous avons retiré les méthodes de découverte des périphériques présents. Partons du postulat que nous connaissons déjà l’adresse (MAC) de notre serveur. Nous chercherons simplement à nous y connecter de façon directe.
Pour connaître l'adresse physique de l'interface bluetooth de notre WaRP7, il suffira d'exécuter la commande suivante :
root@iot:~# hciconfig
hci0: Type: BR/EDR Bus: UART
BD Address: 43:43:A1:12:1F:AC ACL MTU: 1021:8 SCO MTU: 64:1
UP RUNNING PSCAN ISCAN
RX bytes:766 acl:0 sco:0 events:52 errors:0
TX bytes:1766 acl:0 sco:0 commands:52 errors:0
Voici donc l’introduction des premières lignes de code. Le type d’adresse peut être soit « Public » soit « Random ». Le premier définit un mode de fonctionnement standard avec une adresse « statique » et le second définit un mode dans lequel l’adressage peut évoluer, c’est une sécurité dont nous n’avons pas besoin pour la suite :
//création du contrôleur avec l'addresse du serveur en paramètre
m_controller = new QLowEnergyController(QBluetoothAddress(address), this);
//on crée les connections
//Pour récupérer les services présents
connect(m_controller,
SIGNAL(serviceDiscovered(QBluetoothUuid)),
this,
SLOT(addLowEnergyService(QBluetoothUuid)));
//Pour savoir quand la connection est faite
connect(m_controller,
SIGNAL(connected()),
this,
SLOT(deviceConnected()));
//on se connecte
m_controller->setRemoteAddressType(QLowEnergyController::PublicAddress);
m_controller->connectToDevice();
La connexion n’est pas instantanée, mais une fois faite, on lance alors la découverte des services associés au périphérique :
m_controller->discoverServices();
Puis, à la réception des services, il est possible de lancer la découverte des caractéristiques associées au service via la fonction « discoverDetails() » :
//on stock l'addresse du pointeur
m_connectedService = service;
if (m_connectedService->state() == QLowEnergyService::DiscoveryRequired)
{
//permet de récupérer une mise à jour des caractéristiques venant du serveur
connect(m_connectedService,
SIGNAL(characteristicChanged(QLowEnergyCharacteristic,QByteArray)),
this,
SLOT(serviceCharacteristicChanged(QLowEnergyCharacteristic,QByteArray)));
//Permet de découvrir les caractéristiques associées au service
connect(m_connectedService,
SIGNAL(stateChanged(QLowEnergyService::ServiceState)),
this,
SLOT(serviceDetailsDiscovered(QLowEnergyService::ServiceState)));
//démarrer la découverte des caractéristiques
m_connectedService->discoverDetails();
}
Et enfin, à la réception des caractéristiques, on conserve la valeur contenue dans celles-ciet on met à jour l’interface en fonction. Les caractéristiques se reconnaîtront grâce à leurs UUID:
//on récupère le service
QLowEnergyService *service = qobject_cast<QLowEnergyService *>(sender());
//on récupère ses caractéristiques
const QList<QLowEnergyCharacteristic> chars = service->characteristics();
//on les parcours pour les traiter
foreach (const QLowEnergyCharacteristic &ch, chars)
{
//relay1, on met à jour relay1
if (ch.uuid().toUInt32() == CHAR_UUID_RELAY1)
{
m_relay1Characteristic = ch;
m_relay1Changed = ch.value().toInt();
Q_EMIT relay1Changed();
}
//Si uuid relay2, on met à jour relay2
if (ch.uuid().toUInt32() == CHAR_UUID_RELAY2)
{
m_relay2Characteristic = ch;
m_relay2Changed = ch.value().toInt();
Q_EMIT relay2Changed();
}
}
Le plus dur est fait et, afin d'envoyer une nouvelle valeur au serveur en vue de changer l’état d’un des deux relais, il suffira d’utiliser la fonction suivante :
void CBLEDiscover::switchRelay1(bool value)
{
m_connectedService->writeCharacteristic(
m_relay1Characteristic,
QByteArray::number(value));
}
A partir de là, il ne reste plus qu’à créer notre interface !
La partie QML
Lorsque l’on crée un fichier QML, on peut soit le modifier sous la forme d’un script, soit via le designer QML de Qt. Il existe donc deux types de fichiers qml :
- Fichier.ui.qml qui pourra être édité via l’éditeur et/ou par script,
- Fichier.qml qui pourra être modifié uniquement par script
Généralement, les fichiers.ui.qml sont associés à un fichier.qml afin de créer les connexions vers le C++. Dans l’exemple, nous avons créé deux fichiers .qml (associés chacun à un fichier.ui.qml) :
- main.qml : qui consiste en un simple bouton permettant d’enclencher la connexion directement avec le serveur dont l’adresse est celle spécifiée dans le code principal.
- Relay.qml : permet de piloter les deux relais.
Pour que le QML puisse communiquer avec notre classe, il faut pour ce faire, passer celle-ci en paramètre. Nous pouvons alors charger la page main.qml :
QQmlApplicationEngine engine;
//Charger la classe
CBLEDiscover *bleDiscover = new CBLEDiscover();
engine.rootContext()->setContextProperty("BLEDiscover", bleDiscover);
//Charger le QML
engine.load(QUrl(QStringLiteral("qrc:/main.qml")));
Au chargement de l’application, nous aurons donc la page main.qml qui s’affichera constituée d’un simple bouton. Pour gérer l’évènement du clique, il suffit d’utiliser le script suivant :
connectButton.onClicked: {
BLEDiscover.start();
pageLoader.source = "Relay.qml"
}
Afin que la fonction start soit appelée, il est nécessaire qu’elle soit invocable[16] depuis le C++ :
Q_INVOKABLE void start();
L’objet pageLoader permet de charger une nouvelle page QML et est défini par :
Loader { id: pageLoader }
La page Relay.qml est alors chargée. Elle est composée de deux switchs qui permettront de piloter les deux relais. De la même manière, le changement d’état des switchs se gère de la façon suivante :
relay1.onClicked:{
if (relay1.checked == true)
BLEDiscover.switchRelay1(true)
else
BLEDiscover.switchRelay1(false)
}
L’application est bientôt terminée. Nous avons vu comment inséreret appeler une fonction depuis le QML vers le C++. Il ne reste plus qu’à expliquer comment mettre à jour la parité QML avec le C++. Pour réaliser le lien, Qt propose l’utilisation des Q_PROPERTY[17] qui seront liées à des signaux de mise à jour. Par exemple, si nous voulons mettre à jour l’état d’un relais nous définirons la Q_PROPERTY suivante :
//Q_PROPERTY
Q_PROPERTY(bool relay1State MEMBER m_relay1Changed NOTIFY relay1Changed)
//valeur membre
bool m_relay1Changed;
//signal à émettre lors d’une mise à jour
Q_SIGNAL void relay1Changed();
Et côté QML, la connexion au signal se fait de la manière suivante :
relay1 {
checked: BLEDiscover.relay1State;
font.pointSize: 7;
}
4.4 Le rendu final
Pour avoir un aperçu du projet réalisé, voici ci-après le fruit du travail réalisé durant cet article. La première étape consiste au lancement de l'application (.apk disponible ici : https://github.com/bdx-iot/RelayGUI/blob/master/GLMF_200_GUI_BLE.apk)
Fig. 13: Démarrage de l'application
L'étape suivante nous indique de nous connecter à notre périphérique (figure 14) :
Fig. 14: Connexion
Une fois connectés, nous avons la satisfaction de pouvoir interagir avec nos relais, lire la température du capteur Altimètre (MPL3115), mais surtout, de pouvoir jouer avec une simple ampoule comme le montre notre environnement sur la figure 15.
Fig. 15: IoT : L'environnement de test
Agissons maintenant sur le relais 1 (figure 16) :
Fig. 16: IoT : Rendu globale avec RL1 activé
Puis activons le 2ème relais (figure 17) :
Fig. 17: IoT : Rendu globale
Rappelons une fois de plus que le lecteur pourra retrouver l'ensemble des sources sur le référentiel git suivant : https://github.com/bdx-iot. Se trouvera sur celui-ci :
- La couche « meta-iot » mise à disposition par les auteurs, comprenant :
- La recette dérivée du Kernel (gestion du fichier dts et fichier defconfig),
- La recette permettant la gestion des liens symboliques :https://github.com/bdx-iot/meta-iot/tree/master/recipes-iot/clicks-board-init
- La recette du « serveur BLE » et son script init associé (pour un démarrage automatique de l'application) : https://github.com/bdx-iot/meta-iot/tree/master/recipes-iot/service-relay
- Ainsi que d'autres recettes.
- Les sources de l'application serveur : https://github.com/bdx-iot/ServiceRelay
- Les sources de l'application client : https://github.com/bdx-iot/RelayGUI
Conclusion
Dans cet article, nous avons découvert le standard MikroBUS et nous avons ainsi pu le mettre en application sur notre plateforme cible. De par l'intégration, nous avons d'autre part, découvert des notions comme le device tree ou encore le langage QML.
Finalement, nous avons mis en pratique l'ensemble des notions pour créer un véritable objet connecté (jusqu'à la création de la partie client pour la gestion de celui-ci), le tout basé sur du Bluetooth Low Energy. Ceci n'étant qu'une introduction au vu du potentiel, le lecteur pourra s'inspirer et imaginer d'autres applications (domotique) via les modules Mikrobus de MikroElektronika.
En parlant de domotique, n'oublions pas que cette plateforme dispose d'une interface caméra, autre périphérique qu'il serait intéressant de mettre en œuvre afin de créer un environnement connecté pour la surveillance de nos enfants (vidéo/température/gestion de la veilleuse), ceci en ne négligeant pas l'aspect low energy de notre objet bien entendu.
Références et Liens
[1] Article « A la découverte de la WaRP7 » par Pierre-Jean TEXIER et Jean CHABRERIE, Open Silicium n°20
[2] Site d'Element14 : https://www.element14.com/community/docs/DOC-79058/l/warp7-the-next-generation-iot-and-wearable-development-platform
[3] Page NXP présentant l'architecture du i.MX7 : http://www.nxp.com/products/microcontrollers-and-processors/arm-processors/i.mx-applications-processors/i.mx-7-processors/i.mx-7solo-processors-heterogeneous-processing-with-arm-cortex-a7-and-cortex-m4-cores:i.MX7S
[4] Documentatio du projet Yocto (version 2.1 : krogoth) : http://www.yoctoproject.org/docs/2.1/mega-manual/mega-manual.html
[5] Documentation relative à l'outil repo : https://source.android.com/source/using-repo.html
[6] Couche distribution des auteurs : https://github.com/bdx-iot/meta-iot
[7] Ensemble des cartes « click » par MikroElektronika : http://www.mikroe.com/click/
[8] Documentation dans les sources du Noyau Linux sur l'utilisation des GPIO en espace utilisateur : https://www.kernel.org/doc/Documentation/gpio/sysfs.txt
[9] Driver gpio-exporter : https://patchwork.kernel.org/patch/6207321/
[10] Article « Périphériques découvrables, découverts ou à découvrir » par Pierre FICHEUX, Open Silicium n°20
[11] Article « Introduction au « device Tree »sur ARM » par Thomas PETAZZONI, Open Silicium n°17
[12] Documentation de la classe Qfile : http://doc.qt.io/qt-5/qfile.htmlhttp://doc.qt.io/qt-5/qfile.html
[13] Spécifications des services bluetooth : https://www.bluetooth.com/specifications/gatt/services
[14] Spécifications des caractéristiques bluetooth : https://www.bluetooth.com/specifications/gatt/characteristics
[15] Article « IoT sous Yocto, un cas d'utilisation » par Pierre FICHEUX, Open Silicium n°20
[16] Explication de la macro Q_INVOKABLE : http://doc.qt.io/qt-5/qobject.html#Q_INVOKABLE
[17] Explication de la macro Q_PROPERTY : http://doc.qt.io/qt-5/qobject.html#Q_PROPERTY