Dans le milieu industriel, l'intégration de périphériques séries (RS232) est monnaie courante : moteur pas à pas, système de refroidissement, matériel scientifique... Malheureusement, le constat est à chaque fois accablant. En effet, de façon très récurrente on s’aperçoit que les protocoles de communication diffèrent en fonction du fabricant : protocole ASCII sans CRC pour l'un, protocole avec CRC16 pour l'autre et même CRC32 pour d'autres ! Donc aucune homogénéité. C'est pour cette raison que nous proposons d'étudier le protocole Modbus série dans sa forme la plus simpliste (RTU).
Nous mettrons en œuvre, durant l'ensemble de cet article, un mini démonstrateur Modbus RTU en se servant des cartes Click MikroBUS et de l'API Qt, qui depuis la version 5.6 propose un module (QtSerialBus) donnant accès facilement à ce protocole.
1. Introduction
1.1 Un peu d'histoire, présentation du protocole Modbus
Créé en 1979 par Modicon, Modbus est un protocole de communication industriel, utilisé à la base pour les automates programmables, il se décline sous deux versions :
- Modbus RTU ou Modbus ASCII pour les lignes séries ;
- Modbus TCP pour l’Ethernet.
Il fonctionne sur la logique de maître-esclave. Les esclaves sont totalement passifs et seul le maître peut initier un échange de données, logique reposant sur le principe de la question/réponse. Principalement utilisé dans les réseaux d’automates programmables et fort de son succès, on le retrouve de plus en plus, surtout depuis son encapsulation dans les trames Ethernet. Facile à utiliser, à implémenter et robuste, il est devenu une référence dans les protocoles industriels.
1.2 La trame
Le protocole Modbus présente de nombreux avantages. Il est notamment flexible, il reste le même quel que soit le type de connexion utilisé, très pratique surtout quand on envisage une future évolution hardware (RS232 ou même RS485 vers Ethernet par exemple).
1.2.1 L'identifiant esclave (Slave ID)
L'identifiant esclave correspondra à l’identifiant de la trame pour savoir à quel esclave s’adresser (dans le cas où il existe plusieurs éléments sur le bus). C’est donc tout simplement l’identifiant unique de l'esclave.
Ce registre a une taille allouée de 1 octet. Donc on aura un champ d’identifiants possible allant de 1 à 254. Le 0 étant réservé pour le « broadcast ».
1.2.2 Le code fonction (Function Code)
Ce registre permet de définir la méthode d’accès à la donnée ainsi que son type. Le protocole Modbus propose 3 catégories de codes fonctions :
- Les codes fonctions publics : ils sont standards, documentés et nous les décrirons un peu plus tard (Booléen et 16 bits) ;
- Les codes fonctions utilisateurs : ce sont des champs d’adresses libres afin que l’utilisateur puisse rajouter ses propres codes fonctions. Par exemple, permettre un échange sur des « double » « float » ou entiers 32 bits ;
- Les codes fonctions réservés.
Notre exemple repose uniquement sur les codes fonctions publics, on retrouve donc les 6 codes fonctions essentiels :
Nom |
Adresse |
Nombres de registres |
Méthode |
Accès et droits |
« Read Coils » |
0x01 |
1 à 2000 |
Lecture |
Lecture/Écriture Booléen |
« Read Discrete Inputs » |
0x02 |
1 à 2000 |
Lecture |
Lecture Booléen |
« Read Holding Registers » |
0x03 |
1 à 2000 |
Lecture |
Lecture/Écriture 16 bits |
« Read Input Registers » |
0x04 |
1 à 2000 |
Lecture |
Lecture 16 bits |
« Write SingleCoil » |
0x05 |
1 |
Écriture |
Lecture/Écriture Booléen |
« Write Single Register » |
0x06 |
1 |
Écriture |
Lecture/Écriture 16 bits |
Pour plus de détails, le lecteur pourra se référer à la documentation officielle (de 121 pages) : http://www.modbus.org/docs/Modbus_Application_Protocol_V1_1b.pdf.
1.2.3 Le contenu de la trame
Regardons de plus près le contenu d'une trame Modbus sur la figure 1, qu’elle soit dans le mode RTU ou TCP. Cette figure illustre de façon exhaustive les différences entre les deux trames. Prenons l'exemple du TCP : un en-tête Ethernet a bien été ajouté et on perd le champ CRC du Modbus RTU tout simplement, car celle-ci intègre déjà son propre « checksum ».
Fig. 1 : Définition d'une trame modbus RTU/TCP.
Nous verrons par la suite que le niveau d’abstraction que propose Qt5 permet de ne pas se soucier de cet aspect-là.
2. Mise en place de l'environnement
Dans cette partie, nous aborderons l'aspect démarrage de notre plateforme et installation des outils utiles pour mener à bien l'ensemble du développement proposé pour la suite de cet article. Nous ferons en premier lieu une génération de notre image suivie d'une partie mise en route pour finir sur une partie SDK et mise en place de l'environnement Qt5.
2.1 Construction de notre distribution « WaRP7 »
Jamais « 2 sans 3 » : nous utiliserons comme à notre habitude, l'environnement Yocto/OE pour la construction de l'image de notre plateforme WaRP7 (Wearable Reference Platform) [1]. On notera tout de même qu'il conviendrait aussi d'utiliser le « build system » Buildroot [2], qui lui aussi intègre de façon mainline, le support de la WaRP7.
Revenons à notre image et qui plus est à l'installation des sources contenues au sein du Board Support Package (BSP) Freescale. L'installation se fera comme suit :
$ mkdir ~/bin
$ curl http://commondatastorage.googleapis.com/git-repo-downloads/repo > ~/bin/repo
$ chmod a+x ~/bin/repo
$ PATH=${PATH}:~/bin
$ mkdir warp7_glmf
$ cd warp7_glmf
$ repo init -u https://github.com/Freescale/fsl-community-bsp-platform -b pyro
$ repo sync
Le lecteur attentif aura remarqué l'utilisation de la version pyro du BSP ; celle-ci est, à ce jour, la dernière release stable du projet Yocto (2.3) [3] et intrinsèquement celle du BSP Freescale, rocko (2.4) étant toujours dans sa phase de développement.
Une fois l'ensemble des sources du BSP à disposition, tout n'est pas fini. En effet, il nous faudra aussi intégrer deux couches supplémentaires à notre environnement. La première couche à télécharger est la couche distributionmeta-warp7-distro. Celle-ci est mise à disposition par la communauté. Elle intègre par exemple :
- deux images (une minimaliste et une autre avec l'intégration de l’environnement Qt5) ;
- la gestion du wifi (en mode hotspot, afin de faciliter les déploiements sur cible) ;
- la gestion des cartes MikroBUS (via le driver Linux gpio-exporter, comme vu lors de l'article précédent [5]) ;
- ainsi que d'autres fonctionnalités.
Téléchargeons cette couche dans le répertoire sources, endroit où est stocké le BSP de notre cible :
$ cd sources/
$ git clone https://github.com/WaRP7/meta-warp7-distro.git
Dans un second temps, récupérons la couche permettant d'intégrer l'environnement Qt5 à notre distribution (sur la branche pyro, disponible depuis mi-juin, ceci de façon à être cohérent avec le BSP Freescale (et qui plus est, à la version de Yocto/OE que nous utilisons)) :
$ cd sources/
$ git clone -b pyro https://github.com/meta-qt5/meta-qt5.git
L'étape qui suit nous permettra la création de notre environnement. Afin d'avoir un ensemble cohérent, il conviendra de placer plusieurs variables lors de l’invocation de la commande :
- MACHINE : imx7s-warp, qui représente notre plateforme cible ;
- DISTRO : warp7. Obligatoire depuis la release morty(2.2), elle permet de spécifier le type de distribution à utiliser (fslc-framebuffer, fslc-wayland, fslc-x11, ...). Dans notre cas, nous utiliserons celle fournie au sein de la couche meta-warp7-distro.
La commande deviendra donc la suivante (où on passera aussi en paramètre du scriptsetup-environment, un répertoire de construction, ici warp7-build) :
$ MACHINE=imx7s-warp DISTRO=warp7 source setup-environment warp7-build/
...
You can now run 'bitbake <target>'
Common targets are:
core-image-minimal
meta-toolchain
meta-toolchain-sdk
adt-installer
meta-ide-support
Your build environment has been configured with:
MACHINE=imx7s-warp
SDKMACHINE=i686
DISTRO=warp7
EULA=
Il reste maintenant à définir les chemins vers les couches précédemment téléchargées. Pour ce faire, nous allons utiliser la commande bitbake-layers[4], celle-ci permet la gestion des couches au sein de notre environnement. Nous passerons en paramètre de cette commande, l'option add-layer qui permettra de mettre à jour le fichier conf/bblayers.conf, allons-y :
$ bitbake-layers add-layer ../sources/meta-qt5/
$ bitbake-layers add-layer ../sources/meta-warp7-distro/
$ bitbake-layers add-layer ../sources/meta-openembedded/meta-python/ (obligatoire de par sa dépendance à la meta-networking)
$ bitbake-layers add-layer ../sources/meta-openembedded/meta-networking/
Notre configuration est maintenant terminée, nous pouvons lancer la génération de notre image au travers de la commande bitbake :
$ bitbake qt5-image
Parsing recipes: 100% |#######################################################################################################################################################################| Time: 0:01:32
Parsing of 2168 .bb files complete (0 cached, 2168 parsed). 2949 targets, 296 skipped, 0 masked, 0 errors.
NOTE: Resolving any missing task queue dependencies
Build Configuration:
BB_VERSION = "1.34.0"
BUILD_SYS = "x86_64-linux"
NATIVELSBSTRING = "universal-4.8"
TARGET_SYS = "arm-poky-linux-gnueabi"
MACHINE = "imx7s-warp"
DISTRO = "warp7"
DISTRO_VERSION = "2.3"
TUNE_FEATURES = "arm armv7ve vfp thumb neon callconvention-hard cortexa7"
TARGET_FPU = "hard"
meta
meta-poky = "HEAD:f0d128ea0dfc2c403ff53a1ac1db3521854b63d5"
meta-oe
meta-multimedia = "HEAD:5e82995148a2844c6f483ae5ddd1438d87ea9fb7"
meta-freescale = "HEAD:84f328ebc6fdb632f47b2a971a136aabec0f798a"
meta-freescale-3rdparty = "HEAD:fd3962a994b2f477d3e81fa7083f6b3d4e666df5"
meta-freescale-distro = "HEAD:cd5c7a2539f40004f74126e9fdf08254fd9a6390"
meta-qt5 = "pyro:31761f625d2151e9d94d0d83067f90a5da6508e1"
meta-warp7-distro = "master:eb6288f029f72b6cab8c8c1296c9534b99d3220c"
meta-python
meta-networking = "HEAD:5e82995148a2844c6f483ae5ddd1438d87ea9fb7"
Pour ne pas changer, l'ensemble des fichiers générés lors de cette (très) longue phase de compilation se trouvera dans tmp/deploy/images/imx7s-warp. On retrouvera entre autres :
Fichier |
Descriptif |
qt5-image-imx7s-warp.sdcard.gz |
Le Root-File System |
zImage |
L'image Kernel |
modules-imx7s-warp.tgz |
Les « modules » Kernel |
u-boot.imx |
L'image bootloader |
2.2 Mise en route : bref rappel
- 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 2017.03+fslc+gac3b20c (Jun 07 2017 - 22:18:05 +0200)
CPU: Freescale i.MX7S rev1.2 at 792MHz
...
Hit any key to stop autoboot: 0
- Montage de la partition eMMC côté bootloader (au travers de la commande ums) :
=> ums 0 mmc 0
- Décompression et opération de copie côté système hôte sur le device spécifique :
$ cd tmp/deploy/images/imx7s-warp
$ gunzip qt5-image-imx7s-warp-20170612134152.rootfs.sdcard.gz
$ sudo dd if=qt5-image-imx7s-warp-20170612134152.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@cozmic) (gcc version 6.3.0 (GCC) ) #4 SMP PREEMPT Tue Jun 20 12:10:48 CEST 2017
...
WaRP7 powered by Yocto/OE 2.3 imx7s-warp /dev/ttymxc0
warp7 login: root
Password: iot
__ __ ____ ____ _____
\ \ / /_ _| _ \| _ \___ |
\ \ /\ / / _` | |_) | |_) | / /
\ V V / (_| | _ <| __/ / /
\_/\_/ \__,_|_| \_\_| /_/
root@iot:~#
2.3 SDK Qt5
2.3.1 Génération
Par habitude des auteurs, l'ensemble de l'applicatif en espace utilisateur se fera à travers le framework Qt ; il nous faudra donc générer une chaîne de compilation croisée compatible. Pour ce faire :
$ bitbake meta-toolchain-qt5
Une fois la génération terminée et comme d'habitude, le résultat de la compilation se trouvera dans tmp/deploy/sdk.
2.3.2 Installation
Elle se fera tout simplement en exécutant le script suivant où il conviendra de spécifier le chemin d'installation (/opt/warp7/sdk dans cet exemple) :
$ cd tmp/deploy/sdk
$./warp7-glibc-x86_64-meta-toolchain-qt5-cortexa7hf-neon-toolchain-2.3.sh
WaRP7 powered by Yocto/OE SDK installer version 2.3
=========================================================================
Enter target directory for SDK (default: /opt/warp7/2.3): /opt/warp7/sdk
2.3.3 Intégration à l'IDE Qt creator
Afin d'avoir l'environnement de compilation croisée intégré à notre IDE, il nous faudra :
- « sourcer » notre environnement, plaçons-nous dans un shell, et utilisons la commande ci-après :
$ . /opt/warp7/sdk/environment-setup-cortexa7hf-neon-poky-linux-gnueabi
Ou :
$ source /opt/warp7/sdk/environment-setup-cortexa7hf-neon-poky-linux-gnueabi
- Restons dans le Shell (car l'environnement est local au Shell courant dans ce cas) et lançons notre IDE favori : qtcreator &
- Dans Outils > Options > Compiler et Exécuter ;
- Puis dans la section Compilateurs > Ajouter , choisir GCC :
- On donnera un nom à notre GCC spécifique (par exemple WaRP7_GLMF),
- Puis nous spécifierons le chemin du compilateur : /opt/warp7/sdk/sysroots/x86_64-pokysdk-linux/usr/bin/arm-poky-linux-gnueabi/arm-poky-linux-gnueabi-gcc.
- Dans Versions de Qt, Qt est normalement automatiquement détecté. Sinon le lecteur devra ajouter le chemin vers qmake : /opt/warp7/sdk/sysroots/x86_64-pokysdk-linux/usr/bin/qt5/qmake
- Dans Kits, 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 « linux-oe-g++ ».
3. Mise en situation : création du prototype Modbus
La base de cet article reposera en la réalisation d'un mini démonstrateur Modbus RTU. Pour ce faire, nous mettrons un œuvre plusieurs notions logicielles et matérielles.
Ce mini-projet nous permettra de présenter au mieux la mise en pratique de ce protocole industriel, qui pour le coup, permettra au lecteur (du moins nous l’espérons) de se projeter de la meilleure des façons grâce à l'intégration des cartes cliks MikroBUS et l'utilisation de l'API Qt.
Nous commencerons en premier lieu par présenter le projet dans sa globalité, nous mettrons ensuite les mains dans la partie bas niveau pour réaliser une intégration à notre environnement Linux Embarqué, puis nous finirons par l'application finale, qui elle, intègrera le travail préalablement fourni et qui de plus, intègrera la partie Modbus RTU.
3.1 Présentation du projet
Comme introduit précédemment, nous travaillerons essentiellement avec les cartes Clicks de chez MikroElektronika, cela permet de disposer d'un panel de périphériques utilisable et cohérent. Cartes qui ont d'ailleurs déjà été utilisées dans différents articles de GLMF [5][6].
Le projet comportera :
- une application « Slave Modbus », qui sera localisée sur notre carte de développement. Application qui sera développée en C++ via le framework Qt5. Elle permettra la gestion :
- d'une connectivité RS232, permettant la communication série entre le maître et l'esclave ;
- d'une carte EEPROM, pour la gestion de l'identifiant Modbus « esclave ». Celle-ci nous sera utile pour découvrir la partie « read/write register » du protocole Modbus ;
- d'une carte intégrant un capteur de détection de flamme, celle-ci nous permettra d'explorer la partie « read input bit » du protocole Modbus ;
- d'une carte relais, pour faire des accès en lecture/écriture (write/read single coil), qui nous permettra par ailleurs de gérer des éléments externes (gestion d'une lampe par exemple) ;
- du retour température (capteur MPL3115 de chez NXP, interne à la WaRP7), qui lui permettra la gestion des « read input register ».
Comme vous l'aurez remarqué, nous ne parlerons pas ici de l'aspect « maître », nous préférons rester focus sur la partie embarqué, donc « esclave ». Toutefois, afin de valider notre projet, nous utiliserons une petite application permettant de faire ce travail.
QmodMaster [7] est à la fois compatible Windows et Linux et permet la communication Modbus (RTU & TCP/IP) avec les périphériques compatibles, donc pas de panique.
La figure 2 permet de résumer de façon graphique l'architecture logicielle et matérielle du mini-projet qui suit.
Fig. 2 : Structure du projet.
3.2 EEPROM Click
3.2.1 Présentation
Fig. 3 : EEPROM Click.
La EEPROM Click présentée en figure 3 (qui nous servira principalement pour le stockage du Slave ID Modbus), met à disposition une mémoire EEPROM 8k de chez ST de référence 24C08WP au format DIP8 (Dual Inline Package). Côté communication avec notre WaRP7, elle s'effectue au travers du protocole i2c (broches SDA et SCL sur la socket MikroBUS).
Sous GNU/Linux, il existe plusieurs façons d'accéder à un élément connecté sur un bus i2c (on mentionnera ici que ce sujet a déjà fait l'objet de différentes publications par Christophe Blaess ou encore Pierre Ficheux [8] [9]). On citera par exemple :
- smbus pour une utilisation en langage C ou encore en Python ;
- c-periphery, une librairie C [10] plus haut niveau permettant de piloter des périphériques i2c (mais pas que (spi, gpio, pwm, …)) ;
- i2c-tools, projet contenant un ensemble d'utilitaires pour la gestion du bus i2c permettant une utilisation depuis le shell ;
- ou sysfs pour les différents drivers Kernel (eeprom, capteurs de température, accéléromètres...).
i2c-tools : i2cget/i2cset/i2cdump
Insérons la carte munie de sa EEPROM, puis faisons quelques tests au travers des différents outils disponibles au sein du packagei2c-tools.
Commençons par vérifier si notre périphérique est reconnu, on se servira ici de la commandei2cdetectpour faire ceci :
root@imx7s-warp:~# i2cdetect -y 2
0 1 2 3 4 5 6 7 8 9 a b c d e f
00: -- -- -- -- -- -- -- -- -- -- -- -- --
10: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- --
20: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- --
30: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- --
40: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- --
50: 50 51 52 53 -- -- -- -- -- -- -- -- -- -- -- --
60: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- --
70: -- -- -- -- -- -- -- --
On remarquera l'ajout de l'argument passé à la commande i2cdetect : 2, qui ici représente le numéro du bus utilisé. Nous constatons que l'ensemble est bien reconnu (à partir de l'adresse 0x50), essayons maintenant d'écrire 3 octets à cette adresse (222 en ASCII, donc 0x323232 en hexadécimal), à partir du registre 0x00 (ceci afin de préparer notre Slave ID Modbus), icii2cset sera utilisé :
root@imx7s-warp:~# i2cset -y 2 0x50 0x00 0x32 0x32 0x32 i
Vérifions que celle-ci est bien fonctionnelle, pour ce faire utilisons la commande i2cdump :
root@imx7s-warp:~# i2cdump -y 2 0x50
No size specified (using byte-data access)
0 1 2 3 4 5 6 7 8 9 a b c d e f 0123456789abcdef
00: 32 32 32 ff ff ff ff ff ff ff ff ff ff ff ff ff 222.............
10: ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ................
…
On remarque que l'écriture sur notre mémoire EEPROM a bien été prise en compte.
Bien que l'ensemble paraissant fonctionnel et facilement intégrable pour notre application, nous ne retiendrons pas cette technique pour réaliser des accès i2c à notre périphérique. En effet, ce type de EEPROM étant supporté par le noyau Linux, il nous sera plus simple lors de notre intégration au prototype, de réaliser des accès au travers desysfs(simple fichier du point de vue de l'application).
3.2.2 Intégration à notre Image Kernel
Ayant fait le choix de partir sur une utilisation depuis le pseudo système de fichiers sysfs, il nous faudra intégrer le driver Kernel permettant la gestion de notre EEPROM, allons-y :
- Lançons l'interface de configuration de notre Kernel en modencurses :
$ bitbake linux-warp7 -c menuconfig
- Dans Devices Drivers > Misc Devices-> EEPROM support, il conviendra de cocher l'option suivante (de façon statique (*) à notre image noyau ; on pourrait effectivement placer celui-ci sous forme de module (M), comme le montre la figure 4).
Fig. 4 : Intégration EEPROM Click (CONFIG_EEPROM_AT24).
3.2.3 Device tree
Afin d'utiliser notre EEPROM au travers de sysfs, il nous faudra aussi spécifier l'utilisation de celle-ci dans notre fichier device-tree source :
&i2c3 {
eeprom@50 {
compatible = "24c08";
reg = <0x50>;
pagesize = <16>;
};
};
Essayons d'expliquer cette partie :
- &i2c3 : référence vers le label défini dans le fichier principal du SoC i.MX7 (imx7d.dtsi). Endroit où se trouve la définition du contrôleur i2c3 (interruption, horloge…). On utilisera souvent les références aux labels afin d'ajouter des éléments au sein de celui-ci (ajout d'éléments sur le bus : des nouveaux capteurs par exemple) ;
- eeprom@50 : on crée un nœud sous la forme node-name@unit-address, où eeprom spécifie le nom du nœud [11], on préfèrera généralement donner un nom spécifique au composant, conformément à la spécification du device-tree (exemple : cpu, serial, i2c…). La 2ème partie représente l'adresse de notre composant ;
- compatible = "24c08" : propriété sous la forme <manufacturer>,<type>, elle permet de faire le lien avec la partie driver. Dans notre cas il n'existe pas de driver associé au fabricant (ST), nous spécifierons uniquement le champtype, comme explicité dans la documentation officielle du noyau Linux :
- Documentation/devicetree/bindings/eeprom.txt
- reg = <0x50> : représente l'adresse i2c de notre EEPROM (0x50 dans notre cas), la même adresse que l'on retrouve dans la définition du nœud (unit-address) ;
- pagesize = <16> : permet de spécifier la longueur (en octets) lors d'un cycle d'écriture. Généralement, cette information se trouve dans la documentation technique du composant (voir figure 5).
Fig. 5 : Datasheet.
3.3 Relay Click
3.3.1 Présentation
Fig. 6 : Relay Click.
Déjà présentée lors d'un précédent article, la « Relay click » (voir figure 6) possède 2 relais de puissance, pilotables via les broches CS et broche PWM de la carte click. Rappelons qu'afin d'utiliser le module sur notre plateforme, il nous suffira simplement de faire un accès à la broche GPIO désirée.
Exemple avec la pin CS (RL2 de la carte Relay click) depuis la WaRP7 :
root@imx7s-warp:~# echo 119 > /sys/class/gpio/export
root@imx7s-warp:~# echo out > /sys/class/gpio/gpio119/direction
root@imx7s-warp:~# echo 1 > /sys/class/gpio/gpio119/value
3.3.2 Device tree
Comme expliqué en [5], les actions manuelles export/direction/unexport, peuvent devenir très vite un problème, surtout dans le cas où l'application démarre de façon automatique (obligation de scripter les étapes d'export, calcul des gpios en espace utilisateur, etc.). C'est pour ces raisons que nous allons une fois de plus utiliser le driver Kernelgpio-exporter, celui-ci a pour objectif de faire l'associationnom/numéro de GPIO au sein de l'espace utilisateur. Nous devrons, pour ce faire, mettre à jour le fichier dts de notre projet :
gpio_exporter: gpio-exporter {
compatible = "linux,gpio-exporter";
pinctrl-names = "default";
pinctrl-0 = <&pinctrl_gpioexporter_mikrobus>;
out_RL2 {
gpios = <&gpio4 23 GPIO_ACTIVE_HIGH>;
output;
initial-state = <0>;
};
...
};
L'ajout consistera en la création d'un nœud fils (child node)out_RL2, où celui-ci représentera le nom de la broche GPIO en espace utilisateur. Il est nécessaire par la suite, de spécifier quelques propriétés :
- une référence vers la GPIO à exporter (gpios) ;
- sa direction, en sortie dans notre cas (output) ;
- son état initial, état bas ici (initial-state = <0>).
Nous avons ici seulement utilisé la broche RL2 de la carte Relay Click, car la 1ère reste inutilisable dans notre contexte. En effet, ayant fait le choix d'empiler nos cartes Clicks, RL1 tombe sur la pin WP de la carte EEPROM Click, broche permettant la gestion du « write protect ». Un 1 logique sur celle-ci bascule la EEPROM dans le mode read-only, à l'inverse, un 0 sur la broche permet le Read/Write sur la EEPROM. Il n'est donc pour nous pas possible de venir interférer sur le fonctionnement de la mémoire.
3.4 Flame Click
3.4.1 Présentation
Fig. 7 : Flame click.
Autre « form factor » MikroBUS intéressant, la carte Flame Click de la figure 7. Celle-ci permet de détecter la présence d'une flamme en champ proche. La détection se fait via un phototransistor (PT334-6B), il est ainsi possible de générer une interruption lors de cette détection, le seuil étant ajustable via le potentiomètre analogique de la carte. L'état de l'interruption se gère depuis la broche INT de la socket MikroBUS, qui pour la WaRP7 se fera au travers la lecture d'une simple GPIO. Cette fonction nous sera très utile pour découvrir la gestion du « read-only » vis-à-vis du protocole Modbus.
3.4.2 Device tree
Tout comme la partie relais, il nous sera aussi intéressant de générer un nom spécifique au niveau de la broche gérant l'interruption, le fichier dts deviendra donc :
gpio_exporter: gpio-exporter {
compatible = "linux,gpio-exporter";
pinctrl-names = "default";
pinctrl-0 = <&pinctrl_gpioexporter_mikrobus>;
...
in_INT_FLAME {
gpios = <&gpio7 7 GPIO_ACTIVE_HIGH>;
};
};
On constate que la définition est plus succincte que la définition de la précédente GPIO (out_RL2). En effet, dans le cas présent, la GPIO étant gérée en entrée (input), il nous faudra seulement expliciter la broche GPIO à exporter (définie en entrée si pas de définition).
3.5 RS232 Click
3.5.1 Présentation
Fig. 8 : RS232 Click.
Ce module (voir figure 8) permet de rajouter une connectivité RS232 sur un quelconque design électronique ne disposant pas de celle-ci. Le principe repose sur l’utilisation d'un tranceiver RS232, le MAX3232. Celui-ci se base sur des signaux UART (au travers la socket MikroBUS de la WaRP7 dans notre cas), pour finir sur une conversion disponible sur un connecteur DB9.
3.5.2 Device tree
Concernant la partie device-tree, il nous faudra apporter des modifications afin d'utiliser la sortie UART de la WaRP7 :
&uart6 {
pinctrl-names = "default";
pinctrl-0 = <&pinctrl_uart6>;
...
status = "okay";
};
Explications :
- &uart6 : là aussi, c'est une référence au label principal uart6 du fichier imx7d.dtsi, on notera ici que uart6 deviendra ttymxc5 en espace utilisateur ;
- pinctrl-0 : propriété pour la gestion du multiplexage des broches (RX et TX dans notre application), sous-système pinctrl ;
- status = "okay" : la propriété status permet de valider ou non un périphérique. Dans notre cas, il nous faut valider celui-ci en mettant la valeur à « okay », il est aussi possible de passer par « ok », et à l'inverse pour invalider un périphérique : status = "disabled".
3.6 Intégration & Tests
3.6.1 Fichier dts « imx7s-warp-modbus.dts »
Rassemblons maintenant l'ensemble des différents éléments vus précédemment dans un fichier dts principal (spécifique à notre projet), que l'on nommera imx7s-warp-modbus.dtb, celui-ci inclura le fichier de définition de la carte WaRP7 (imx7s-warp.dts) :
#include "imx7s-warp.dts"
/ {
gpio_exporter: gpio-exporter {
compatible = "linux,gpio-exporter";
pinctrl-names = "default";
pinctrl-0 = <&pinctrl_gpioexporter_mikrobus>;
out_RL2 {
gpios = <&gpio4 23 GPIO_ACTIVE_HIGH>;
output;
initial-state = <0>;
};
in_INT_FLAME {
gpios = <&gpio7 7 GPIO_ACTIVE_HIGH>;
};
};
};
&i2c3 {
eeprom@50 {
compatible = "24c08";
reg = <0x50>;
pagesize = <16>;
};
};
&uart6 {
pinctrl-names = "default";
pinctrl-0 = <&pinctrl_uart6>;
assigned-clocks = <&clks IMX7D_UART6_ROOT_SRC>;
assigned-clock-parents = <&clks IMX7D_PLL_SYS_MAIN_240M_CLK>;
status = "okay";
};
&iomuxc {
pinctrl-names = "default";
imx7s-warp {
pinctrl_uart6: uart6grp {
fsl,pins = <
MX7D_PAD_ECSPI1_MOSI__UART6_DTE_RX 0x79
MX7D_PAD_ECSPI1_SCLK__UART6_DTE_TX 0x79
>;
};
pinctrl_gpioexporter_mikrobus: gpioexportergrp {
fsl,pins = <
MX7D_PAD_ECSPI2_SS0__GPIO4_IO23 0x14
MX7D_PAD_ENET1_RGMII_TD1__GPIO7_IO7 0x14
>;
};
};
};
3.6.2 Étape de compilation...
Après l'ensemble des modifications apportées (ajout du driver, création du fichier device-tree), il nous suffira de lancer la commande suivante afin de générerzImage + notre fichier dtbimx7s-warp-modbus.dtb :
$ bitbake linux-warp7
Reste maintenant à positionner les fichiers générés sur notre carte de développement.
3.6.3 Mise à jour des fichiers sur cible
Comme nous l'avons fait en début d'article, il faudra ici aussi monter la partition eMMC dans le mode mass storage (ums 0 mmc 0 sous u-boot pour un petit rappel), afin de transférer les fichiers sur la première partition (boot) :
- Concernant notre image noyau :
$ sudo cp tmp/work/imx7s_warp-poky-linux-gnueabi/linux-warp7/4.1-r0 /build/arch/arm/boot/zImage /media/<username>/Boot imx7s-/
- Le ficher dtb (device-tree blob) :
$ sudo cp tmp/work/imx7s_warp-poky-linux-gnueabi/linux-warp7/4.1-r0 /build/arch/arm/boot/dts/imx7s-warp-modbus.dtb /media/<username>/Boot imx7s-/
- Côté WaRP7, donc depuis le shell mis à disposition par u-boot, lançons la commande suivante avec comme argument notre fichier dtb :
=> setenv fdt_file imx7s-warp-modbus.dtb
Celle-ci permet la surcharge de 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.
3.6.4 Tests rapides en espace utilisateur
Avant de passer à la partie Qt, il nous est nécessaire de valider le bon fonctionnement de chaque sous-système de notre prototype Modbus.
- Commençons par valider la partie EEPROM :
- Pour ce faire, vérifions d'abord si le démarrage s'est passé correctement :
root@imx7s-warp:~# dmesg | grep at24
at24 2-0050: 1024 byte 24c08 EEPROM, writable, 16 bytes/write
- Faisons un accès en lecture :
root@imx7s-warp:~# cat /sys/bus/i2c/devices/2-0050/eeprom | hexdump -C
00000000 00 00 00 38 6e 0a 2e 2e 2e 0a ff ff ff ff ff ff |...8n...........|
00000010 ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff |................|
*
00000100 ff 49 32 43 20 6d 69 6b 72 6f 45 00 ff ff ff ff |.I2C mikroE.....|
00000110 ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff |................|
...
- Passons sur la partie «Relay click » :
- Essayons de faire un accès en écriture sur la broche RL2 :
root@imx7s-warp:~# echo 1 > /dev/mikrobus/out_RL2
- Au niveau de la « Flame click » :
- Essayons de faire un accès en lecture sur la broche INT :
root@imx7s-warp:~# cat /dev/mikrobus/in_INT_FLAME
1 (détection de flamme !)
Rappelons ici que l'ensemble des gpio's est disponible au travers de liens symboliques dans /dev/mikrobus/*.
4. Mise en situation : applicatif Qt
Ce petit projet a pour but de mettre en avant l’utilisation des cartes Click ainsi que leur pilotage via un protocole standard : Modbus RTU. Le tout sera utilisé via le framework Qt dans sa version 5.8.
4.1 Préparation de l'environnement
Tout comme lors de la création de notre Kit, il nous faudra avoir un Shell avec le bon environnement avant de pouvoir commencer notre projet. La première étape consiste en la création du projet Qt : Fichier > Nouveau Fichier ou Projet… > Qt Console Application. Une fois sur le projet, il nous faut ajouter 3 choses essentielles dans le fichier.pro :
- QT += serialbus, qui permet d'inclure le module QtSerialBus au projet (gestion CAN Bus et Modbus) ;
- 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 Intégration Qt : modules Clicks
4.2.1 EEPROM Click
La mémoire propose 8kio d’espace mémoire, bien plus que nécessaire pour notre projet. Comme présenté lors de la section 3, la lecture et l’écriture se feront directement au travers de sysfs à l’emplacement/sys/bus/i2c/devices/2-0050/eeprom. Par défaut, lors de l’écriture d’une donnée sur le fichier, on écrira toujours à l’adresse0x00de l’EEPROM. Dans notre projet, on va se contenter de sauvegarder uniquement l’ID du « slave » qui ici, est la WaRP7.
La gestion des fichiers sur Qt se fait via la classe Qfile[12] proposant bon nombre de fonctionnalités, dont les plus importantes : l’écriture et la lecture de données. Notre fichier d'entête sera donc composé d’une fonction de « set » et de « get » pour l’identifiant Modbus, ainsi qu’une variable membre de typeQFile pour les futurs accès au fichier :
public:
CEepromClick();
~CEepromClick();
public:
voidsetModbusID(quint32 value);
voidgetModbusID(quint32&value);
private:
QFile*m_eepromFile;
Le reste découle, on instancie la variable membre « m_eepromFile » avec comme paramètre l’emplacement du fichier :
m_eepromFile=newQfile(EEPROM_READ_WRITE);
…correspondant à :
#define EEPROM_READ_WRITE "/sys/bus/i2c/devices/2-0050/eeprom"
Pour terminer, il suffit de créer le code des fonctions d’accès. On introduit ici un nouvel objet : QdataStream[13] qui est une classe facilitant la sérialisation des données et qui épargnera au lecteur dans de très nombreux cas de se demander si le passage du Big-Indian au Little-Indian a été correctement fait. Dans notre cas, elle va transformer l’ID Modbus qui sera un quint32 (entier signé sur 4 octets) en un char * (tableau de caractères) :
- On le stocke sur l’EEPROM dans cette fonction :
voidCEepromClick::setModbusID(quint32 value)
{
if(m_eepromFile->open(QIODevice::ReadWrite))
{
QByteArraydata;
QDataStreamstr(&data,QIODevice::ReadWrite);
str << value;
m_eepromFile->write(data);
m_eepromFile->close();
}
}
- Puis on le récupère en désérialisant la donnée à l’aide de la même classe :
voidCEepromClick::getModbusID(quint32&value)
{
if(m_eepromFile->open(QIODevice::ReadOnly))
{
QByteArraydata;
data.resize(4);
m_eepromFile->read(data.data(),4);
QDataStreamstr(data);
str >> value;
qDebug()<<"Read Modbus ID is : "<< value;
m_eepromFile->close();
}
}
Et voilà ! Les deux fonctions sont prêtes à l’utilisation.
4.2.2 FLAME click
La Flame Click consiste uniquement à regarder si elle renvoie 1 ou 0. Le fichier concerné se situe à l’emplacement suivant : /dev/mikrobus/in_INT_FLAME. Pour cet objet, on aura une préférence pour une gestion sur évènement. Pour y arriver, on procèdera comme suit :
- Nous lirons très régulièrement le fichier pour voir si la valeur a changé, et si on détecte un changement, on crée un évènement auquel la classe qui instanciera notre objet devra se connecter.
- On crée un signal qui sera notre évènement, Qt intègre de très nombreuses fonctionnalités autour des QObject [14] (dont chacune de ses classes dérive). Notamment, la possibilité de créer une connexion entre un signal émis et une fonction appelée à son émission :
Q_SIGNALvoidflameDetected(bool);
- Nous aurons besoin d’une boucle de lecture, pour cela nous créons un QTimer[15] permettant de générer des évènements à des intervalles de temps définis et la fonction qui sera appelée à chaque évènement, dont le but sera de lire le fichier :
protectedslots:
voidcheckFile();
private:
QTimer*m_watcherTimer;
Pour le reste, on rajoute une classeQFilepour la lecture du fichier et une variable permettant de connaître l’état de notre phototransistor :
QFile*m_flameFile;
boolm_systemIsOnFire;
Vient la création du code, où l’on va connecter l’évènement duQTimersur la fonction de lecture :
connect(m_watcherTimer,
SIGNAL(timeout()),
this,
SLOT(checkFile()));
Puis on démarre ce timer, et on lui demande de créer un évènement toutes les 10 millisecondes :
m_watcherTimer->start(10);
La lecture du fichier consiste uniquement à lire le fichier et vérifier si l’état du capteur a changé afin d’éviter de saturer d’évènements l’utilisateur de cette classe :
voidCFlameClick::checkFile()
{
if(m_flameFile->open(QIODevice::ReadOnly))
{
boolvalue=m_flameFile->readAll().remove(1,1).toInt();
if(value!=m_systemIsOnFire)
{
m_systemIsOnFire=value;
Q_EMIT flameDetected(m_systemIsOnFire);
qDebug()<<QString(m_systemIsOnFire?"System is on Fire !!":"YEAH WE ARE ALL SAVED");
}
m_flameFile->close();
}
}
On y est : à chaque fois que le capteur va changer d’état, l’évènement flameDetected sera déclenché indiquant s'il a détecté ou non une flamme. De la même manière que pour le QTimer, l’utilisateur de la classe devra connecter ce signal à une fonction « SLOT » afin de pouvoir traiter l’évènement.
4.2.3 RELAY Click
Cette carte fonctionne sur le même principe que l’EEPROM. On va écrire sur un fichier pour lui faire changer d’état. Nous aurons donc une fonction « set » sur la position du relais :
voidsetPosition(boolvalue);
Et une classe QFile pour l’écriture de la donnée :
QFile*m_relayFile;
Et on peut enfin créer le cœur de la fonction :
voidCRelayClick::setPosition(boolvalue)
{
if(m_relayFile->open(QIODevice::ReadWrite))
{
m_relayFile->write(QByteArray::number(value));
m_relayFile->close();
}
}
Rien de plus simple et nous sommes déjà prêts à faire fonctionner ce relai !
4.3 Intégration Qt : Modbus RTU
Pour cette partie, nous aurons besoin des classes QmodbusRtuSerialSlave [16] et QserialPort [17]. On crée ainsi notre serveur Modbus :
QModbusRtuSerialSlave*m_modbusRTUSerialSlave;
Puis :
m_modbusRTUSerialSlave=newQModbusRtuSerialSlave();
On crée ensuite les connexions avec les différents signaux proposés par cette classe avec :
- dataWritten permettant de savoir quand l’utilisateur écrit une donnée :
connect(m_modbusRTUSerialSlave,
SIGNAL(dataWritten(QModbusDataUnit::RegisterType,int,int)),
this,
SLOT(messageReceived(QModbusDataUnit::RegisterType,int,int)));
- stateChanged permettant de connaître l’état du serveur Modbus :
connect(m_modbusRTUSerialSlave,
SIGNAL(stateChanged(QModbusDevice::State)),
this,
SLOT(stateChanged(QModbusDevice::State)));
- errorOccured permettant de savoir quand il y a une erreur :
connect(m_modbusRTUSerialSlave,
SIGNAL(errorOccurred(QModbusDevice::Error)),
this,
SLOT(errorOccurred(QModbusDevice::Error)));
Il faut ensuite créer la table de données Modbus et définir combien il y existe de registres par code fonction. On a défini précédemment qu’il y en aurait un de chaque :
QModbusDataUnitMap reg;
reg.insert(QModbusDataUnit::Coils,{QModbusDataUnit::Coils,0,1});
reg.insert(QModbusDataUnit::DiscreteInputs,{QModbusDataUnit::DiscreteInputs,0,1});
reg.insert(QModbusDataUnit::InputRegisters,{QModbusDataUnit::InputRegisters,0,1});
reg.insert(QModbusDataUnit::HoldingRegisters,{QModbusDataUnit::HoldingRegisters,0,1});
m_modbusRTUSerialSlave->setMap(reg);
On spécifie ensuite les paramètres de connexion pour la communication série :
m_modbusRTUSerialSlave->setConnectionParameter(QModbusDevice::SerialPortNameParameter, PORT_NAME);
Où PORT_NAME, correspond à /dev/ttymxc5.
m_modbusRTUSerialSlave->setConnectionParameter(QModbusDevice::SerialParityParameter, QSerialPort::NoParity);
m_modbusRTUSerialSlave->setConnectionParameter(QModbusDevice::SerialBaudRateParameter, QSerialPort::Baud115200);
m_modbusRTUSerialSlave->setConnectionParameter(QModbusDevice::SerialDataBitsParameter, QSerialPort::Data8);
m_modbusRTUSerialSlave->setConnectionParameter(QModbusDevice::SerialStopBitsParameter, QSerialPort::OneStop);
On récupère l’ID Modbus pour la mettre en paramètre :
quint32 modbusID;
m_eepromClick->getModbusID(modbusID);
m_modbusRTUSerialSlave->setServerAddress(modbusID);
Puis on démarre le serveur :
m_modbusRTUSerialSlave->connectDevice();
Pour la température, nous partons sur un simpleQTimerdont la fonction vient lire toutes les secondes la valeur de la température grâce aux fichiers suivants :
#define TEMP_RAW "/sys/bus/iio/devices/iio\:device0/in_temp_raw"
#define TEMP_SCALE "/sys/bus/iio/devices/iio\:device0/in_temp_scale"
Il suffit de les multiplier entre eux pour obtenir la température en degré. Après avoir lu la température, il faut mettre à jour le registre de cette manière :
m_modbusRTUSerialSlave->setData(QModbusDataUnit::InputRegisters,0, rawValue*scaleValue);
C'est une fonction dans laquelle on spécifie le code fonction, l’adresse et la valeur. La mise à jour de l’état de la Flame Click se fait de la même manière.
La dernière partie du code consiste à gérer les écritures. Cette fonctionnalité passe par la fonction SLOT messageReceived. Dans cette dernière, nous allons vérifier le code fonction, puis l’adresse. Notre cas est simple puisque nous nous contentons de l’adresse 0 pour tous nos registres :
caseQModbusDataUnit::Coils:
m_modbusRTUSerialSlave->data(QModbusDataUnit::Coils, address+i,&value);
// on testel'adresse
if(address+i==0)
m_relayClick->setPosition(value);
break;
caseQModbusDataUnit::HoldingRegisters:
m_modbusRTUSerialSlave->data(QModbusDataUnit::HoldingRegisters, address+i,&value);
// on testel'adresse
if(address+i==0)
m_eepromClick->setModbusID(value);
break;
Pour aller plus loin, il faudra créer des mappings de valeurs entre l’adresse et la fonctionnalité attendue.
Avant d'aborder la dernière partie de cet article, soulignons qu'il est à ce niveau, possible (et recommandé) de déployer notre application. Pour ce faire, deux possibilités :
- via Qt5 au travers d'une connexion wifi (en mode hotspot) ;
- ou en déployant le package yocto modbus-rtu, package de notre application (recette modbus-rtu_git.bb). Il faudra bien entendu lancer la commande bitbake modbus-rtu, mais ça vous savez le faire maintenant.
L'utilisateur souhaitant transformer la WaRP7 en mode hotspot, pourra lire quelques informations ici : https://github.com/WaRP7/meta-warp7-distro/blob/master/docs/wifi.md.
5. Mise en situation : rendu final
Nous voici à l'étape fatidique de l'article et aussi la partie la plus intéressante, la phase de test de notre prototype.
Commençons par lancer QmodMaster afin de paramétrer celui-ci à notre convenance (voir figure 9).
Fig. 9 : Paramétrage de QmodMaster.
Nous ne ferons pas ici la validation de tous les cas, ceci nous demanderait en effet un peu plus de place dans ces colonnes. Nous aborderons seulement l'aspect retour température et validation du capteur de flamme. Lançons-nous dans une requête de température. Pour ce faire, configurons QmodMaster pour réaliser un accès sur le code fonction Read Input Registers(voir figure 10).
Fig. 10 : Lecture Modbus température.
Nous constatons une température de 35°C qui est une valeur cohérente. Essayons maintenant de nous positionner sur lesRead Discrete Inputs, permettant ainsi de vérifier l'état de notre capteur (voir figure 11).
Fig. 11 : Lecture Modbus Flame Click.
Pour les lecteurs curieux, le setup complet, comprenant la WaRP7, les cartes Clicks superposées, est présenté en figures 12 et 13.
Fig. 12 : MikroBUS Power.
Fig. 13 : De profil !
Pour ne pas changer les bonnes habitudes (des auteurs), vous trouverez l'ensemble des codes sources et fichiers utilisés pour la réalisation du prototype, à savoir :
- La couche meta-warp7-distro, sur le référentiel git officiel de la WaRP7 : https://github.com/WaRP7/meta-warp7-distro, celle-ci comprendra aussi la recette de notre serveur Modbus RTU ainsi que son script init associé.
- Le code source de l'application Warp7ModbusRTU : https://github.com/Jeanrenet/Warp7ModbusRTU.
Conclusion
Il est maintenant venu le temps de conclure ; nous aurons découvert via cet article comment mettre en place un mini démonstrateur Modbus RTU à moindre coût (sauf pour la partie WaRP7 évidemment), facilement grâce à la diversité que proposent les modules Click de MikroElektronika et ce de façon industrielle de par l'utilisation d'outils reconnus dans ce milieu (Qt, Yocto/OE).
Nous aurons profité de la souplesse que propose l'API Qt pour développer d'une façon plus haut niveau notre petit projet. Nous ferons remarquer que pour les aficionados du langage C, il existe aussi un binding plus connu sous le nom de libmodbus.
Autre point notable, nous encourageons les lecteurs désireux de poursuivre sur la lancée de cet article, d'intégrer la partie TCP au prototype. L'avantage étant qu'il nécessitera uniquement de changer la connectique (RS232 Click <-> ETH Click) et la partie création du serveur.
Enfin, il serait maintenant intéressant de se servir des différents capteurs à disposition pour découvrir pourquoi pas, d'autres moyens de communication comme MQTT, SigFox, mais ça c'est une autre histoire…
Références
[1] TEXIER P.-J. et CHABRERIE J., « À la découverte de la WaRP7 », Open Silicium n°20 : http://connect.ed-diamond.com/Open-Silicium/OS-020/A-la-decouverte-de-la-WaRP7
[2] Buildroot & WaRP7 : https://github.com/buildroot/buildroot/tree/master/board/warp7
[3] Documentation du projet Yocto (version 2.3 : pyro) : http://www.yoctoproject.org/docs/2.3/mega-manual/mega-manual.html
[4] Documentation de la commande « bitbake-layers » : http://www.yoctoproject.org/docs/2.3/mega-manual/mega-manual.html#managing-layers
[5] TEXIER P.-J. et CHABRERIE J., « Le Relais 2 x 5V … Dans l'IoT ou l'art de piloter en BLE les périphériques de la WaRP7 », GNU/Linux Magazine n°200 : http://connect.ed-diamond.com/GNU-Linux-Magazine/GLMF-200/Le-Relais-2-x-5-V-dans-l-IoT-ou-l-art-de-piloter-en-BLE-les-peripheriques-de-la-WaRP7
[6] DELMAS L., « CANOPEN avec Raspberry-Pi », GNU/Linux Magazine n°205 : http://connect.ed-diamond.com/GNU-Linux-Magazine/GLMF-205/CANOpen-avec-Raspberry-Pi
[7] Application QmodMaster : https://sourceforge.net/projects/qmodmaster/
[8] BLAESS C., « Communiquer en i2c avec un capteur de température », GNU/Linux Magazine HS n°75 : http://connect.ed-diamond.com/GNU-Linux-Magazine/GLMFHS-075/Communiquer-en-i2c-avec-un-capteur-de-temperature
[9] FICHEUX P., « Comment ne pas écrire de pilote Linux », Open Silicium n°19 : http://connect.ed-diamond.com/Open-Silicium/OS-019/Comment-ne-pas-ecrire-de-pilotes-Linux
[10] Bibliothèque c-periphery : https://github.com/vsergeev/c-periphery
[11] Explication du device-tree par node name : http://elinux.org/Device_Tree_Usage#Node_Names
[12] Classe QFile : http://doc.qt.io/qt-5/qfile.html
[13] Classe QDatastream : http://doc.qt.io/qt-5/qdatastream.html
[14] Classe QObject : http://doc.qt.io/qt-5/qobject.html
[15] Classe QTimer : http://doc.qt.io/qt-5/qtimer.html
[16] Classe QmodbusRtuSerialSlave : https://doc.qt.io/qt-5/qmodbusrtuserialslave.html
[17] Classe QSerialPort : http://doc.qt.io/qt-5/qserialport.html