i.MX7 : « Communication interprocesseur, donnons vie au Cortex M4 »

GNU/Linux Magazine n° 211 | janvier 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 !
Nous allons découvrir dans cet article comment appréhender le développement sur plateforme i.MX7. Nous développerons un démonstrateur IoT en associant acquisition des données via Cortex M4, communication interprocesseur et consommation des données côté Cortex A7.

Déjà présenté, même utilisé dans les colonnes de GNU/Linux Magazine pour du prototypage IoT, le System on Chip i.MX7 se veut être une référence sur sa capacité de traitement en environnement basse consommation (rappelons-le, trois fois plus efficace en énergie que l’i.MX6). La raison principale de cette performance repose sur son architecture asymétrique hétérogène (HMP en anglais), associant un cœur ARM Cortex A7 et un cœur Cortex M4, le tout sur une même puce.

En effet, très souvent associé à travers différents moyens de communication (SPI, i2c, UART...), microcontrôleur et microprocesseur permettent d'architecturer au mieux un développement logiciel et matériel. On utilisera par exemple la partie microcontrôleur pour la gestion des acquisitions (Bus CAN par exemple) et des sécurités avec un système d'exploitation temps réel (FreeRTOS pour n'en citer qu'un), et le microprocesseur pour la gestion de l’interface graphique, réseau (WiFi, BLE…).

1. Introduction

1.1 i.MX7 : bref rappel

Note

La WaRP7 disposant du System on Chip i.MX7 dans sa version Solo (pour 1 cœur cortex A7), la suite de l’étude tiendra compte uniquement de ce modèle. Soyez tout de même rassuré, car le concept reste valide sur la référence i.MX7 Duo, qui lui intègre 2 cœurs Cortex A7, et diverses gestions de périphériques en plus (EPD et PCIe par exemple).  

L’i.MX7 [1] se définit comme étant la nouvelle référence pour le développement d’applications sécurisées et faible consommation pour le marché de l’Internet des objets. De par son architecture Cortex A7/Cortex M4 mono puce, permettant de faire tourner à la fois, Linux d’un côté et un OS temps réel de l’autre, il vise à s’intégrer dans des applications telles que des terminaux de paiements, le monde des wearables, la santé connectée et bien d’autres applications.

Comme le montre la figure 1, celui-ci permet la gestion de nombreuses connectivités (i2c, SPI…), gestion de différentes mémoires externes, mais le réel plus, concerne son architecture à double cœur Cortex A7 - Cortex M4.  

Fig. 1 : i.MX7 solo.

1.2 Approche traditionnelle : microcontrôleur – microprocesseur

Fig. 2 : Microcontrôleur – microprocesseur.

Souvent basée sur une communication de type SPI, i2c ou même UART, l’association microcontrôleur/microprocesseur permet de répondre à de nombreuses exigences dans le milieu industriel. Citons en premier lieu l’aspect code métier, qui lui se trouvera sur le microcontrôleur (bare-metal ou RTOS), permettant ainsi de ne pas venir « linker » le projet sur du code GPL (point positif pour du code certifié par exemple). Autre point très important, la notion de prédictibilité des données, notion difficile, voire impossible à tenir sur un système GNU/Linux embarqué standard. Il faudra passer par l’étape d’un processus lourd comme l’intégration de Xenomaï ou PREEMPT-RT, moyennant ainsi des modifications noyau.

Dans ce type d’architecture, le microprocesseur verra son rôle dédié pour la gestion de l’affichage ou l’envoi des données (voir figure 2).

Enfin, cette association permet aussi une meilleure évolutivité du code. Pour plus d’informations sur la comparaison entre architecture hybride et non hybride, il sera recommandé de lire l’excellente étude de cas réalisée par Christophe Blaess [2].

Note

Ce genre d’architecture peut parfois concerner l’association d’un microprocesseur et d’un FPGA, citons par exemple la carte AFP6_SP du Français OposSom, mettant en œuvre un System on Chip i.MX6 accompagné d’un FPGA Cyclone 5 d’Altera. Permettant ainsi de se décharger des algorithmes logiciels coûteux sur CPU vers le FPGA.

1.3 Approche i.MX7

Sur ce type d’architecture multiprocesseur asymétrique hétérogène, les deux systèmes, bien que séparés,  partagent les mêmes ressources matérielles. Par exemple, un accès sur une broche GPIO, peut être réalisé par le cortex A7, mais aussi par le cortex M4. Nous verrons par la suite le mécanisme mis à disposition pour permettre le « safe sharing », permettant ainsi d’éviter les accès concurrents sur la couche matérielle. Côté communication interprocesseur, l’ensemble des accès se fait en mémoire partagée, sur une zone de la mémoire DDR dédiée pour cet échange. La figure 3 permet de schématiser l’écosystème.

Fig. 3 : Architecture i.MX7.

1.3.1 Les avantages d'une telle architecture

Les avantages d'une telle architecture sont très nombreux, citons quelques exemples :

  • plusieurs systèmes d’exploitation tournant sur une même puce ;
  • possibilité d’effectuer une session gdb sur l’application microcontrôleur (bare-metal ou FreeRTOS) et l’application microprocesseur (GNU/Linux) en même temps ;
  • un update firmware beaucoup plus simple (i.e : SWUpdate [3]) ;
  • une communication rapide entre les deux systèmes (directement en mémoire partagée) ;
  • moins de composants, donc une réduction sur le coût de la BOM (Bill Of Materials).

Et bien d’autres encore, à vous de tirer parti de cette architecture pour répondre aux besoins de vos divers projets.

1.3.2 Pourquoi et quand utiliser le cortex M4 ?

Bien que déjà évoqué en début d’article, permettons-nous de rappeler ici quelques cas typiques où l’utilisation du cortex M4 du System on Chip i.MX7 devient intéressante :  

  • le code métier doit s’exécuter à la mise sous tension (temps de démarrage classique GNU/Linux non concevable) ;
  • l’applicatif métier est soumis à haute certification ;
  • l’ensemble des acquisitions analogiques/numériques nécessite un temps de réponse rapide et déterministe.

Nous sommes bien entendu conscients que la liste n’est pas entièrement exhaustive, elle se veut être une base de réflexion pour le développement sur cible i.MX7.

2. Dans les entrailles du SoC i.MX7

Dans cette partie, nous allons nous intéresser principalement aux spécificités que propose l'i.MX7 pour la gestion de cette architecture, mélangeant à la fois Cortex A7 et Cortex M4.

Nous commencerons par explorer les sources du BSP FreeRTOS permettant de développer sur le cortex M4. Nous utiliserons dans notre cas, un fork des sources fourni par NXP [4] avec le support de notre carte d’évaluation, la WaRP7. Chaque sous-ensemble de celui-ci sera clairement explicité afin d'avoir une bonne compréhension pour la suite de l'article. Nous enchaînerons sur l’explication de la séquence de démarrage en introduisant la notion de zone mémoire. Nous finirons cette section en découvrant les mécanismes pour le partitionnement système (domaine, périphérique, mémoire), ainsi que les moyens de communication présents pour la communication interprocesseur.

2.1 Cortex M4 : le BSP FreeRTOS

Le BSP FreeRTOS pour i.MX7 est un kit de développement logiciel fournissant le support (quasi) complet pour notre SoC fétiche (pour vous aussi, non ?).

Celui-ci encapsule en premier lieu le système d’exploitation temps réel faible empreinte mémoire FreeRTOS pour microcontrôleur (rappelons-le inférieur à 10KB). Leader sur le marché, celui-ci a récemment fait l’objet d’un bel article dans GNU/Linux Magazine [5]. Il sera évidemment recommandé de se référer à cette publication pour approfondir les notions de programmation sur cet OS. Le BSP contient également les différents drivers de périphériques, la pile pour la gestion de communication pour les architectures multicœurs hétérogènes asymétriques, des exemples, ainsi que de nombreux fichiers utiles au support du processeur, fichiers que nous présenterons au fur et à mesure de cet article.

La figure 4 permet de visualiser l’architecture de notre BSP.

Fig. 4 : Architecture du BSP FreeRTOS i.MX7.

2.1.1 Téléchargement des sources

Le BSP dans sa version 1.0.1 est disponible sur le référentiel git des auteurs, il faudra veiller à exécuter la commande suivante pour récupérer l’ensemble des sources :

$ git clone https://github.com/texierp/freertos-warp7.git

2.1.2 Téléchargement de la chaîne de compilation

Le BSP propose deux systèmes de construction pour générer les exemples :

  • le premier étant la mise à disposition d’un projet basé sur la suite DS-5 de chez ARM ;
  • le deuxième, une suite reposant sur l’utilisation de CMake et GCC (armgcc).

L’idée étant ici de ne pas se perdre dans les méandres de la configuration d’un IDE, la deuxième solution nous donnera entière satisfaction pour la suite de l’article. Il faudra donc s’assurer d’avoir l’outil CMake disponible au sein de notre environnement, dans le cas contraire :

$ sudo apt-get install cmake

Puis téléchargeons la chaîne de compilation :

$ wget https://launchpad.net/gcc-arm-embedded/5.0/5-2016-q2-update/+download/gcc-arm-none-eabi-5_4-2016q2-20160622-linux.tar.bz2

Nous verrons dans la suite de l’article comment se servir de celle-ci.

2.1.3 Vingt mille lieues sous le BSP

Un tableau vaut mieux qu’un long discours, rentrons directement dans le vif du sujet et regardons comment l’ensemble est structuré :

Répertoire Contenu
doc/ Plusieurs documentations concernant la mise en œuvre, explication des différentes API.
examples/ Exemples d’applications.
examples/imx7s_warp_m4/ Exemples portés pour fonctionner sur WaRP7.
examples/imx7s_warp_m4/board.* Le fichier header définit un ensemble de macros spécifiques à la carte (GPIO, gestion du debug...). Le fichier source quant à lui, définira les horloges/domaines et la gestion du debug UART.
examples/imx7s_warp_m4/gpio_pins.* C’est l’endroit où sont définis les GPIO de notre carte (sous forme de structure => champ name, mux register, pad, pin number…).
examples/imx7s_warp_m4/pin_mux.* Contient la configuration du pin muxing (IOMUX) spécifique aux périphériques (CAN, UART, SPI…).
middleware/multicore/open-amp/ Contient la pile pour la communication interprocesseur. Celle-ci étant basée sur le framework OpenAMP pour le côté M4.
platform/ Contient les différents drivers de périphériques (ADC, UART, SPI…), le code de démarrage, les scripts ld, les fichiers en-têtes pour l’accès à la CMSIS (version 4.2) ainsi que des utilitaires.
rtos/FreeRTOS/ Contient le code source de l’OS avec l’inclusion du portage pour notre cible.

Autre élément notable, à la racine de chaque projet, nous retrouverons plusieurs fichiers (hormis la gestion des systèmes de construction armgcc pour CMake et ds5). Deux fichiers nous seront essentiels, FreeRTOSConfig.het hardware_init.c. Prenons un exemple avec l’application relay-click présente au sein du BSP :

$ ls -l examples/imx7s_warp_m4/demo_apps/rpmsg/relay-click/

total 36

drwxr-xr-x 5 pjtexier pjtexier 4096 oct.  12 21:38 armgcc

drwxrwxr-x 2 pjtexier pjtexier 4096 oct.  12 22:16 common

drwxr-xr-x 3 pjtexier pjtexier 4096 oct.   8 13:49 ds5

-rw-rw-r-- 1 pjtexier pjtexier 7639 juin  27  2016 FreeRTOSConfig.h

-rw-rw-r-- 1 pjtexier pjtexier 3778 oct.  12 14:49 hardware_init.c

-rw-rw-r-- 1 pjtexier pjtexier 9986 oct.  12 21:37 relay-click.c

  • FreeRTOSConfig.h : pour la configuration du noyau FreeRTOS, c’est par exemple l’endroit où il conviendra de paramétrer l’utilisation ou non des mutexes (#define configUSE_MUTEXES) ou encore d’inclure des éléments de l’API (#define INCLUDE_vTaskDelay). Pour de plus amples informations à l’égard de ce fichier,  en [6] se trouveune documentation plus approfondie.
  • hardware_init.c : c’est le fichier de plus haut niveau qui appellera individuellement chaque fonction des différents fichiers spécifiques à la carte (rappelez-vous : board.*, gpio_pins.* et pin_mux.*). C’est donc ce fichier qui sera garant de la bonne configuration (instanciation des GPIO, initialisation des horloges…).

2.2 Cortex M4 : mémoire, démarrage...

Il existe plusieurs types de mémoires disponibles. Le Cortex M4 fournit une mémoire locale (Tightly Coupled Memory pour TCM) que l’on utilisera dans notre étude. Cette mémoire est relativement petite (64KB), mais la plus performante [7] comparée à la DDR3 ou OCRAM qui sont aussi disponibles sur l’i.MX7.  Il faudra savoir qu’il existe seulement 32KB utilisable pour charger la section de code (.text) de par son architecture (Code Bus et System Bus).

Si nous regardons de plus près la définition de cette zone mémoire dans la documentation officielle du SoC i.MX7 (voir figure 5), celle-ci se découpe en deux parties (Lower and Upper).

Fig. 5 : Mémoire TCM côté cortex M4.

La partie TCML pour la section de code et la région TCMU pour la section de données (.data). L’ensemble des régions mémoire sont définies dans les scripts de linkage (via la directive MEMORY) dans freertos-warp7/platform/devices/MCIMX7D/linker/gcc/MCIMX7D_M4_typedemémoire.ld, regardons celui utilisé pour les besoins de notre projet, le fichier MCIMX7D_M4_tcm.ld :

/* Specify the memory areas */

MEMORY

{

  m_interrupts          (RX)  : ORIGIN = 0x1FFF8000, LENGTH = 0x00000240

  m_text (RX) : ORIGIN = 0x1FFF8240, LENGTH = 0x00007DC0

m_data (RW) : ORIGIN = 0x20000000, LENGTH = 0x00008000

}

Chose importante, cette zone mémoire est aussi accessible du côté cortex A7 (voir figure 6).

Fig. 6 : Mémoire TCM côté cortex A7.

Et c’est d’ailleurs par cette adresse mémoire (0x007F8000) qu’il nous sera possible de charger et démarrer notre application depuis notre chargeur d’amorçage u-boot. Pour ce faire, ce dernier nous met à disposition une commande permettant de démarrer un cœur auxiliaire sur une architecture telle que la nôtre. Cette action est possible par l’utilisation de la commande bootaux. Celle-ci prendra en paramètre, l’adresse mémoire où démarrer. Ce qui donnera par exemple :

=> bootaux 0x7F8000

## Starting auxiliary core at 0x007F8000 ...

… afin de démarrer notre cortex M4 avec l’image chargée à l’adresse 0x7F8000.

Mais avant toute chose, rappelons qu’à l’inverse d’un microcontrôleur standard, qui possède sa propre Flash NOR pour stocker et exécuter le firmware, notre plateforme, devra stocker le firmware sur un périphérique mass storage (dans notre cas au sein de la partition boot, juste à côté de l’image noyau et du fichier device-tree). Pour ce faire, il faudra à chaque démarrage, charger notre application en mémoire avant de pouvoir lancer le cœur cortex M4. Les commandes ci-après nous permettront de réaliser ceci :

=> fatload mmc 0:1 0x7F8000 main.bin // Chargement du binaire à l’adresse TCML depuis la première partition emmc

=> dcache flush

L’avantage d’une telle solution est qu’il nous suffira de remplacer le firmware lors d’une mise à jour. La séquence de démarrage pourra donc se résumer sous la forme présentée en figure 7.

Fig. 7 : Séquence de démarrage sur SoC i.MX7.

2.3 Resource Domain controller

Fig. 8 : Resource Domain Controller.

C'est sûrement l'élément central de notre System on Chip. Le RDC (voir figure 8), pour Resource Domain Controller est un sous-système permettant le partitionnement système (et bien plus). De par le fait que les deux cœurs distincts partagent les mêmes ressources, le RDC est donc le garant des accès périphériques et mémoire pour chaque bus maître (Cortex A7, Cortex M4, CSI, SDMA…). Il permettra  d’éviter les accès concurrents au niveau périphérique. Celui-ci définit ce que l’on appellera des « domaines » d’exécution. Nous retrouverons trois sous-parties, mais seulement les deux plus importantes seront présentées ici (la dernière concernant la gestion mémoire).

2.3.1 Domain ID

Le RDC de l’i.MX7 permet de gérer jusqu’à quatre domaines différents (de à 3). Le principe sera donc de placer dans chacun d’entre eux, les éléments qui nous intéressent. Pour gérer la configuration, chaque bus maître se voit associer un identifiant unique qu’on appelle Master Domain Assignment (MDA). Il conviendra donc d’associer chaque MDA à un domaine spécifique. Dans les exemples, nous placerons uniquement le Cortex M4 sur le Domaine 1. Les autres (Cortex A7, CSI…) étant à 0 par défaut lors de la mise sous tension.

Un exemple est toujours plus parlant, regardons comment placer le cortex M4 sur le domaine 1. Pour ce faire, il faudra se placer au sein du fichier freertos-warp7/examples/imx7s_warp_m4/board.c et par le biais de la fonction RDC_SetDomainID(), paramétrer celle-ci sous la forme :

void BOARD_RdcInit(void)                                           

{

    /* Cortex M4 = domain ID 1 */                                    

    RDC_SetDomainID(RDC, rdcMdaM4, BOARD_DOMAIN_ID, false);

}                                          

  • le premier paramètre est une référence au base pointer sous la forme : ((RDC_Type *)0x303D0000u) ;
  • le second paramètre concerne l’Identifiant MDA, dans notre cas nous prendrons celui représentant le cortex M4 (rdcMdaM4 étant un élément de l’énumération _rdc_mda dans freertos-warp7/platform/drivers/inc/rdc_defs_imx7d.h, représentant le registre RDC_MDA1) ;
  • le troisième paramètre, pour définir le domaine souhaité, BOARD_DOMAIN_ID est défini à 1 dans freertos-warp7/examples/imx7s_warp_m4/board.h :

#define BOARD_DOMAIN_ID (1)

  • le dernier paramètre permet de verrouiller la configuration jusqu’au prochain redémarrage.

2.3.2 Peripheral Domain Access Permission

Le RDC permet aussi de gérer finement les accès en lecture/écriture des périphériques en fonction du domaine d’exécution. Tout comme la gestion du Domain ID, chaque périphérique aura aussi son identifiant unique appelé PDAP (pour Peripheral Domain Access Permission). Pour gérer au mieux cette configuration, la fonction RDC_SetPdapAccess() nous permettra de gérer les différentes permissions. Prenons un exemple avec le seul périphérique alloué exclusivement en lecture/écriture par le cortex M4, l’UART de debug. Regardons dans le fichier freertos-warp7/examples/imx7s_warp_m4/board.c :

RDC_SetPdapAccess(RDC, BOARD_DEBUG_UART_RDC_PDAP, 3 << (BOARD_DOMAIN_ID * 2), false, false);

  • le premier paramètre est une référence au base pointer sous la forme : ((RDC_Type *)0x303D0000u) ;
  • le second concerne l’identifiant PDAP, ici BOARD_DEBUG_UART_RDC_PDAP fait référence à rdcPdapUart2 dans le fichier board.h (RDC_PDAP105) ;
  • le troisième est un registre de configuration (bit0 - bit7) qui conviendra de configurer en fonction du projet (voir figure 9).

Fig. 9 :  Registre RDC_PDAPn où n représente l’identifiant du périphérique.

Dans ce cas,le registre est sous la forme : 00001100,  configuration pour un accès exclusif en lecture/écriture sur le domaine 1.

  • Bit de configuration SREQ pour Semaphore REQuired. Il permet lors d’un partage de périphérique entre deux domaines, de forcer l’utilisation des sémaphores matérielles via les fonctions :
    • RDC_SEMAPHORE_Lock(NOM DU PDAP) ;
    • RDC_SEMAPHORE_Unlock(NOM DU PDAP)

Ceci permettra de garantir l’accès au(x) périphérique(s), c’est une technique qu’on appellera « safe sharing ». Par exemple, pour un accès en lecture/écriture sur le domaine 0 et sur le domaine 1, il suffira de configurer les permissions de la sorte :  3 << (BOARD_DOMAIN_ID * 2) | 0x2 , un registre maintenant égal à 00001111.

Note

Pour les notions plus avancées sur le « Safe Sharing », il sera conseillé de se reporter à la documentation officielle du System on Chip (iMX7SRM.pdf), section 3.2.2.3.

  • Le dernier paramètre permet de verrouiller la configuration jusqu’au prochain redémarrage.

La figure 10 résume la configuration de l’UART en image.

Fig. 10 :  RDC UART.

2.4 RPMsg : « Remote Processor Messaging »

RPMsg est un bus de communication basé sur la couche de transport VirtIO. Il permet la gestion des IPC (Inter Processor Communication) entre des contextes logiciels s’exécutant sur des architectures telles que l’i.MX7 (échanges entre cortex A7<-> cortex M4).

Basé sur une communication client/serveur, chaque périphérique RPMsg est défini comme un canal de communication avec le processeur distant (les périphériques RPMsg étant appelés canaux). Chaque canal se verra identifié par une adresse locale (source) et une adresse distante (destination).

Quant à VirtIO, c’est une couche d’abstraction transport basée sur la mémoire partagée. Elle fournit entre autres une API virtqueue qui permet d’envoyer et de recevoir des données avec le processeur distant en se basant sur une architecture à buffer circulaire appelée VRING.

Il serait impossible ici d’expliquer l’ensemble de cette implémentation, car sophistiquée, il faudra simplement retenir que :

  • RPMsg crée deux buffers circulaires VirtIO (pour chaque direction : TX et RX) ;
  • la taille de chaque buffer est de 512 octets (avec 16 octets pour l’en-tête RPMsg) et 256 buffers sont alloués pour chaque direction ;
  • VRING est un composant permettant de « manager » les descripteurs vers les buffers (les 256) en mémoire partagée.

Par défaut, notre cortex A7 (GNU/Linux) est le maître RPMsg, c’est lui qui sera en charge d’allouer les buffers VirtIO en mémoire vive (DDR3). L’emplacement des VRING ainsi que la longueur et la taille des buffers étant « hard coded », ils devront donc correspondre entre la partie Linux et FreeRTOS :

  • Pour Linux : arm/mach-imx/imx_rpmsg.c

#define RPMSG_BUF_SIZE (512)

...

ret = of_device_is_compatible(np, "fsl,imx7s-rpmsg");

if (ret) {

 /* hardcodes here now. */

 rpdev->vring[0] = 0x9FFF0000;                                                      

 rpdev->vring[1] = 0x9FFF8000;                                                

}

  • Pour FreeRTOS : middleware/multicore/open-amp/porting/imx7d_m4/platform_info.c

#defineVRING0_BASE0x9FFF0000

#define VRING1_BASE0x9FFF8000

Les adresses correspondent à notre DDR3 de 512 MB. Pour une DDR3 de 1GB, nous aurions par exemple :

rpdev->vring[0] = 0xBFFF0000;

rpdev->vring[1] = 0xBFFF8000;

2.4.1 Implémentation RPMsg

Côté Linux, NXP fournit un driver tty virtuel (imx-rpmsg-tty.ko) pour accéder à cette fonctionnalité. Une fois chargé, l’accès au périphérique se fera via l’interface /dev/ttyRMSG au sein de l’espace utilisateur comme défini au sein des sources du driver (drivers/rpmsg/imx_rpmsg_tty.c).

Le nom étant implémenté via la structure de données principale de tout driver tty : struct tty_driver. L’interface du périphérique est à configurer comme une ligne série standard (avec l’éternelle utilisation de termios), nous donnant ainsi accès aux primitives classiques : open/write/read/close, pour communiquer avec le processeur distant (cortex M4).

Côté Cortex M4, l’implémentation de RPMsg est encapsulée dans les sources du BSP FreeRTOS. Cette pile de communication est un dérivé du framework officiel OpenAMP [8] avec des modifications apportées par NXP. De plus, celle-ci est prévue pour fonctionner sur des applications « bare-metal » ou conjointement avec FreeRTOS. Les principaux changements par rapport à l'implémentation disponible dans OpenAMP sont :

  • le support de l’i.MX7 ;
  • l'ajout d’un mécanisme « sans-copie » ;
  • les corrections de bugs au niveau RPMsg & virtIO.

L’ajout du support RPMsg/virtIO, au sein d’un projet sur la partie cortex M4, se résumera à l’inclusion des fichiers présents dans middleware/multicore/open-amp.

Note

Le lecteur souhaitant de plus amples informations sur l’implémentation de RPMsg sur cortex M4, pourra se référer au document RPMsg_RTOS_Layer_User’s_Guide.pdf présent dans le dossier doc/ du BSP.

3. Mise en situation : création du prototype « Air Quality »

Maintenant que nous en savons un peu plus sur le fonctionnement de l’i.MX7, nous allons pouvoir nous intéresser à la partie pratique sur notre WaRP7. Par habitude des auteurs, nous allons jusqu’à la fin de l’article, mettre en œuvre un prototype démontrant le réel intérêt de développer un projet sur une architecture comme celle-ci.

Au travers de ce projet, nous mettrons en place un écosystème permettant de mesurer la qualité de l’air (intérieure). Pour ce faire, nous nous servirons de plusieurs éléments logiciels et matériels, comme l’utilisation de la carte MikroBUS Air Quality 2 Click pour la gestion du capteur, jusqu’à l’utilisation de Qt 5.8 pour la partie applicative GNU/Linux (gestion du tty) en passant par l’utilisation de FreeRTOS pour la partie microcontrôleur.

Nous commencerons par une brève présentation du projet afin de délimiter le périmètre d’action de chaque partie. Nous accorderons ensuite, un peu de temps à la mise en place de l’environnement, puis nous finirons par l’intégration logicielle au fur et à mesure sur les deux environnements distincts.

3.1 Présentation du projet

Comme introduit précédemment, le projet se découpera en deux grandes parties. L’idée consistera à créer :

  • une application FreeRTOS, qui sera sur la partie cortex M4 et développée en se servant des différentes API fournies par le BSP. Celle-ci sera en charge :
    • de gérer les acquisitions sur le capteur i2c pour la récupération des données relatives à la qualité de l’air. L’accès en lecture/écriture au bus i2c se verra être exclusivement réservé au cortex M4 (nul besoin de partager ce périphérique avec Linux !) ;
    • de gérer les accès en écriture sur une broche GPIO de la socket MikroBUS à disposition ;
    • de gérer les échanges avec le Cortex A7 afin de renvoyer les informations liées au capteur.
  • une application pour la gestion des échanges de données avec le Cortex M4, celle-ci sera développée en C++/Qt et permettra de mettre à disposition les données reçues (CO2 , TVOC et le statut du capteur). Celle-ci sera localisée sur la partie GNU/Linux.

3.2 Air Quality 2 Click

Fig. 11 :  Air Quality 2 Click.

La Air Quality 2 Click (voir figure 11), utile pour la mesure de la qualité de l’air au sein de notre étude, embarque un capteur iAQ Core du fabricant ams (iAQ pour Indoor Air Quality). Au travers d'une simple interface i2c, il nous est possible de récupérer :

  • le taux de concentration de CO2 (en ppm ; partie par million) ;
  • le taux de concentration de COV (Composés Organiques Volatils, en ppb ; partie par milliard) ;
  • le statut du capteur (OK, BUSY, READY, ERROR) ;
  • la résistance (en Ohm) du capteur.

Nous noterons tout de même qu’afin de pouvoir utiliser le capteur sans soucis sur la WaRP7, il sera nécessaire d’utiliser un régulateur de tension 5V vers 3.3V. En effet, la tension de sortie 3.3V prévue sur la socket MikroBUS n’est à ce jour pas fonctionnelle. Il faudra donc se servir du 5V présent sur la socket MikroBUS pour l’entrée du régulateur. Pour le setup, nous avons utilisé le LDO MCP1700 de chez Microchip.

3.3 Mise en place de l’environnement

Les parties concernant la mise en place de l’environnement Yocto/OE, ainsi que celles abordant la gestion du SDK ayant déjà fait l’objet d’un précédent article [9], les auteurs se permettront de renvoyer les lecteurs à se référer à celui-ci afin d’avoir à disposition l’ensemble des outils nécessaires (image, SDK et un environnement Qt5 configuré). On se permettra juste ici d’évoquer les modifications propres au fichier device-tree.

3.3.1 Un peu de device-tree

Comme annoncé dans la présentation du projet, le bus i2c se verra configuré pour être strictement réservé au Cortex M4 (car dédié à la réalisation des acquisitions du capteur iAQ Core). Il nous faudra donc invalider la gestion de ce bus côté Cortex A7 (GNU/Linux) afin de ne pas rendre la carte cible inutilisable (séquence de boot impossible). Pour ce faire, il nous faudra jouer sur les propriétés de celui-ci dans le fichier device-tree. Déjà fonctionnel au sein de la meta-warp7-distro, le Cortex M4 possède un fichier dts pour son propre support (imx7s-warp-m4.dts).

Le fichier se verra être mis à jour de la façon suivante (ex : devtool modify linux-warp7) :

#include "imx7s-warp.dts"

...

&i2c3 {

 status = "disabled";

};

Il nous faudra seulement rajouter une référence vers le label i2c3 et mettre à jour la propriété status permettant de valider ou nom un périphérique. Dans notre cas, nous souhaitons ne pas utiliser celui-ci via GNU/Linux, il suffira donc très simplement de remplir le champ concerné avec disabled.

Afin de générer le fichier dtb, le fruit de la compilation du fichier dts, il sera obligatoire de « sourcer » l’environnement lié à la chaîne de compilation croisée fournit par Yocto/OE, puis de lancer la commande suivante à la racine des sources du noyau Linux :

$ make arch=ARM imx7s-warp-m4.dtb

DTC     arch/arm/boot/dts/imx7s-warp-m4.dtb

3.3.2 Déploiement du fichier dts

Pour transférer le fichier dtb, nous ne dérogerons pas à la règle, nous établirons comme à notre habitude, une connexion série (via un émulateur de terminal comme microcom) :

$ microcom --speed 115200 --port /dev/ttyUSB0

Une fois la cible sous tension, il conviendra de stopper l'auto-boot :

Hit any key to stop autoboot:  0

De monter la partition eMMC côté bootloader (au travers de la commande ums) :

=> ums 0 mmc 0

Puis de copier notre fichier dtb sur la partition boot :

$ sudo cp tmp/work/imx7s_warp-poky-linux-gnueabi/linux-warp7/4.1-r0 /build/arch/arm/boot/dts/imx7s-warp-m4.dtb /media/<username>/Boot imx7s-/

Côté WaRP7, lançons la commande suivante avec comme argument notre fichier dtb :

=> setenv fdt_file imx7s-warp-m4.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 :

=> saveenv

Saving Environment to MMC...

Writing to MMC(0)... done

3.4 Normalisons nos échanges !

Sur ce type d’architecture à mémoire partagée, il est à la charge de l’utilisateur de créer son propre standard d’échange entre les deux contextes logiciels. Le protocole que les auteurs se proposent de mettre en place se veut être simpliste et facile de compréhension (sous forme de chaîne de caractères). Ci-dessous, le tableau récapitulatif des commandes :

Fonction Commande Paramètre Retour
Écriture !out_LED: booléen !out_LED:ok/nok
Lecture ?getAirQuality None CO2, TVOC, statut

Exemples :

  • Pour donner l’ordre au Cortex M4 de mettre la GPIO à l’état haut, il suffira d’envoyer la chaîne de caractère suivante : !out_LED:1.
  • Pour récupérer l’ensemble des données du capteur, la requête vers le Cortex M4 (via /dev/ttyRPMSG) devra être sous la forme : ?getAirQuality.
Note

La gestion de base des commandes (?/!), s’inspire du travail réalisé par la société Toradex sur leur robot TAQ : https://www.toradex.com/videos/taq-the-balancing-robot. L’architecture étant aussi basée sur un System on Chip i.MX7.

3.5 Applicatif FreeRTOS

Dans cette partie, nous aborderons la mise en place et le développement de notre programme FreeRTOS. Nous noterons que l’idée n’est pas tant de présenter ici le potentiel que peut nous proposer cet OS minimaliste, mais plutôt d’explorer les bénéfices qu’apporte cette architecture.

3.5.1 L’architecture logicielle

Notre objectif premier est ici d’assurer la mesure du capteur iAQ Core et de gérer les différents évènements utilisateur en provenance du Cortex A7. C’est logiquement que notre programme comportera deux tâches :

  • Une tâche « mesure », que l’on nommera iaqDataTask. Celle-ci sera chargée de récupérer les données du capteur à disposition (CO2, TVOC ...).
  • Une deuxième tâche « commande » appelée commandTask, en charge d’assurer les échanges de données entre les différents cœurs. C’est dans cette tâche que nous mettrons en place le canal RPMsg.
  • Afin de ne pas rendre indigeste la présentation du programme, nous ne présenterons pas ici l’aspect GPIO, toutefois, le lecteur souhaitant approfondir le sujet, pourra se rendre dans les sources du projet (l’implémentation étant présente et validée).

3.5.2 Création des tâches

Comme toutes tâches FreeRTOS que l’on souhaiterait enregistrer auprès de l’ordonnanceur, nous passerons par la fonction xTaskCreate() pour créer celles-ci :

  • Pour la tâche liée à la récupération des informations capteur :

if (!(pdPASS == xTaskCreate(iaqDataTask, "iaqData Task", APP_TASK_STACK_SIZE, NULL,tskIDLE_PRIORITY+1, NULL)))

  goto err;

  • Pour la tâche permettant les échanges de données avec le cortex A7 :

if (!(pdPASS == xTaskCreate(commandTask, "Command Task", APP_TASK_STACK_SIZE, NULL, tskIDLE_PRIORITY+2, NULL)))

  goto err;

Cette fonction prendra en paramètre un pointeur vers la fonction implémentant la tâche, une définition libre de la fonction (utile pour des phases de « debug » par exemple), la taille de la pile associée à la tâche, le(s) paramètre(s) associé(s) à celle-ci (void *), la priorité à laquelle devra s’exécuter la tâche et avec comme dernier paramètre le descripteur de la tâche (handle). Ici, tskIDLE_PRIORITY représente la priorité la plus basse, 0. C’est la priorité minimale correspondant à la tâche IDLE, première tâche créée au démarrage de l’ordonnanceur, c’est-à-dire à l’exécution de la fonction vTaskStartScheduler().

3.5.3 « iaqDataTask »

Simple dans son fonctionnement, cette tâche ne fera exclusivement que des accès i2c vers notre capteur iAQ Core afin de mettre à jour les champs de notre structure iaqData. Le tout cadencé par la fonction vTaskDelay() (mécanisme permettant de bloquer la tâche sur un nombre de Tick défini). Ici, 1.5s de par les spécifications du capteur (le capteur ne peut être interrogé qu'une fois par seconde maximum) :

static void iaqDataTask(void *pvParameters)

{ 

 while (1) {

  ...

  if ( !IAQ_ReadData( &iaqData ) )

   PRINTF("Reading problem ... %s\n"); 

  vTaskDelay(1500);  

 }

}

La structure de données étant sous la forme (cf. common/air-quality-click.h) :

typedef struct _iaq_data

{

    uint8_t     status;                 /*!< Status */

    uint16_t    CO2prediction;          /*!< CO2prediction */

    uint16_t    TVOCprediction;         /*!< TVOCprediction */

    uint32_t    resistance;             /*!< resistance */

} iaq_data_t;

3.5.4 « commandTask »

Brique logicielle de base de notre firmware, cette tâche suivra les étapes classiques pour la gestion des IPC via RPMsg. Voici le scénario d’implémentation :

1. On commence par allouer/initialiser le driver RPMsg (création du channel de communication) :

...

struct remote_device *rdev = NULL;    

struct rpmsg_channel *app_chnl = NULL;

...

rpmsg_rtos_init(0, &rdev, RPMSG_MASTER, &app_chnl);

Celle-ci permet d’initialiser notre Cortex M4 en tant que « remote », rdev est donc un pointeur vers ce nouveau périphérique. Le troisième paramètre étant la définition du rôle du processeur distant vis-à-vis du Cortex m4, RPMSG_MASTER pour le Cortex A7. Le dernier paramètre (app_chnl), est lui, un pointeur vers le channel RPMsg.

2. Dans la boucle while(1) de notre tâche, il nous faudra implémenter un mécanisme permettant de récupérer les messages provenant du Cortex A7, regardons la fonction prévue à cet effet :

rpmsg_rtos_recv_nocopy(app_chnl->rp_ept, &rx_buf, &len, &src, 0xFFFFFFFF);

rx_buff est un pointeur vers le buffer de réception alloué en mémoire partagée (VRING RX), src un pointeur vers l’adresse de l’expéditeur et le dernier paramètre un temps maximum en millisecondes sur la réception (0 permettra de rendre cette fonction non bloquante et inversement, 0xFFFFFFFF pour placer celle-ci dans un mode dit bloquant).

Une fois le message reçu, nous avons la possibilité de traiter le message dans un buffer alloué et dédié, copions les données (de rx_buf) vers celui-ci :

memcpy(buffer, rx_buf, len);

3. Nous pouvons maintenant analyser la trame de réception et observer que lors d’une comparaison avec une commande connue de notre protocole, les données seront mises en forme dans le buffer. Dans cet exemple, nous mettrons à jour celui-ci avec les éléments de la structure mis à jour par l’autre tâche (iaqDataTask) :

case '?':

 sscanf(buffer, "?%s", command);

 if (0 == strcmp(command, "getAirQuality"))

 {

  // CO2 Prediction (ppm)

  buffer[0] = iaqData.CO2prediction >> 8;

  buffer[1] = iaqData.CO2prediction & 0x00FF;

  // TVOC prediction (ppb)

  buffer[2] = iaqData.TVOCprediction >> 8;

  buffer[3] = iaqData.TVOCprediction & 0x00FF;

  // Status (RUNNING, BUSY, ...)

  buffer[4] = iaqData.status;

  buffer[5] = '\n';

  // Length

  len = 6;

 }

 ...

 break;

default:

4. Le message étant maintenant structuré, il nous faudra allouer notre buffer de transmission présent en mémoire partagée (VRING TX) :

tx_buf = rpmsg_rtos_alloc_tx_buffer(app_chnl->rp_ept, &size);

Et par la suite, copier notre payload vers celui-ci.

memcpy(tx_buf, buffer, len);

Enfin, il faudra envoyer tx_buf à l’adresse distante src (qui vous l’aurez compris, représente notre Cortex A7 !), avec un message d’une longueur len :

rpmsg_rtos_send_nocopy(app_chnl->rp_ept, tx_buf, len, src);

5. Pour finir, il faudra à la fin de cette séquence, libérer notre buffer de réception (VRING RX), afin de le rendre disponible pour les prochains transferts de données :

rpmsg_rtos_recv_nocopy_free(app_chnl->rp_ept, rx_buf);

3.5.5 Gestion de l’i2c

Maintenant indisponible côté GNU/Linux, il nous est possible de configurer le bus pour une utilisation dédiée au Cortex M4. Pour ce faire, éditons le fichier hardware_init.c (à la racine du projet), et plaçons-y la fonction suivante :

RDC_SetPdapAccess(RDC, rdcPdapI2c3, 3 << (BOARD_DOMAIN_ID * 2), false, true);

On retrouve bien le PDAP associé à la gestion de l’i2c3, le décalage de bit pour configurer le registre sous la forme 00001100 (domaine 1). Il sera aussi possible de verrouiller la configuration (dernier paramètre de la fonction RDC_SetPdapAccess()).

3.5.6 Compilation et déploiement

Le développement logiciel FreeRTOS étant terminé, compilons notre projet afin de le déployer sur cible. La première étape consistera en l’utilisation de notre chaîne de compilation et pour ce faire, il conviendra d’exporter la variable ARMGCC_DIR permettant de spécifier le chemin de celle-ci :

$ export ARMGCC_DIR=<chemin vers la toolchain>/gcc-arm-none-eabi-5_4-2016q2

Puis dans le dossier armgcc, la commande suivante permettra de démarrer le processus de compilation :

$ cd armgcc

$ ./build_release.sh

...

[100%] Linking C executable release/main.elf

[100%] Built target main

Ceci aura pour effet de générer fichier Makefile + firmware (fichier main.bin).

Comme dans la section 3.3.2 lors du déploiement du fichier dtb, il en sera de même pour le fichier armgcc/release/main.bin, il faudra en effet le placer sur la partition Boot, mais ça, vous savez maintenant le faire !

3.5.7 Premier boot… premiers tests…

Nous voilà fin prêts pour les premiers tests. Ne perdons pas de temps, allumons notre WaRP7 et ouvrons deux terminaux pour la gestion des différents ports série (/dev/ttyUSB0 pour la partie GNU/Linux et /dev/ttyUSB1 pour la partie FreeRTOS). La première chose à faire sera de se placer dans l’environnement de notre bootloader afin d’exécuter les commandes vues au chapitre 2.2 (chargement du firmware en mémoire et démarrage du cœur auxiliaire). La figure 12 permet de récapituler celles-ci (terminal de gauche pour la partie u-boot).

Fig. 12 : Démarrage du cortex M4.

Nous remarquons qu’à l’exécution de la commande bootaux, le Cortex M4 est de suite disponible (terminal de droite), et de façon très logique, nous constatons que le firmware FreeRTOS est déjà en cours d’exécution (nous le justifions par les traces de debug au niveau de la tâche iaqDataTask).

Note

Afin de gérer le démarrage automatique du Cortex M4 et ainsi de s’épargner les différentes étapes sous u-boot, il sera possible de surcharger dans un premier temps notre environnement comme ceci :

setenv m4_addr 0x7F8000

setenv m4_fw main.bin

setenv loadm4image fatload mmc ${mmcdev}:${mmcpart} ${m4_addr} ${m4_fw}

setenv m4boot "if run loadm4image; then dcache flush ; bootaux ${m4_addr}; fi"

Il faudra bien sûr sauvegarder ce contexte en eMMC :

saveenv

Enfin, pour lancer le cœur Cortex M4, il suffira de lancer la commande suivante :

run m4boot

À vous maintenant de surcharger la variable bootcmd afin d’y ajouter celle précédemment créée (m4boot), permettant ainsi un véritable auto-démarrage !

Mettons maintenant les mains dans la partie RPMsg pour valider la communication, et de ce fait, préparer le terrain pour la partie Qt.

Dans la configuration de notre noyau Linux, le driver tty n’est pas intégré de façon statique à celle-ci, ce dernier est donc uniquement disponible sous forme de module au sein de notre environnement (/lib/modules/4.1.36-4.1-1.0.x-imx-warp7+ga543d1b/kernel/drivers/rpmsg). Il faudra pour l’utiliser correctement, employer la commande modprobe pour charger ce dernier :

$ modprobe imx-rpmsg-tty

imx_rpmsg_tty rpmsg0: new channel: 0x400 -> 0x0!

Install rpmsg tty driver!

Ceci aura comme effet de rendre disponible notre interface de communication /dev/ttyRPMSG. Ce qui veut donc dire que nous pouvons d’ores et déjà envoyer des commandes de test à notre Cortex M4 ! Utilisons la commande echo pour gérer les envois de données (echo -n « bonjour GLMF » > /dev/ttyRPMSG). La figure 13 permet d’exposer plusieurs échanges entre les deux architectures :

Fig. 13 : Nos premiers échanges de données !

Nous avons pu durant cette partie, valider le concept d’échange et ainsi validé une partie de notre firmware.

Note

Pour la chargement automatique du module au démarrage, il sera préconisé de créer un fichier dans le dossier /etc/modules-load.d, et d’y inscrire le nom du driver à charger, exemple :

$ cat /etc/modules-load.d/imx-rpmsg-tty.conf

imx-rpmsg-tty

Il sera aussi possible de faire cette opération du côté de Yocto/OE via la variable KERNEL_MODULE_AUTOLOAD :

3.6 Applicatif Qt

3.6.1 La communication

La première étape va être de créer un service (ServiceRPMSG) permettant de communiquer via l’interface RPMsg avec le cortex M4.

À ce niveau de l’article, nous avons déjà à disposition notre canal de communication ttyRPMSG dans /dev. Celui-ci sera notre point d’accès pour la suite des évènements. Pour la configuration de celui-ci, nous utiliserons l’interface POSIX ; il suffira d’appliquer un ensemble de paramètres (structure termios) à un descripteur de fichier associé à notre ligne série virtuelle. Par défaut, nous utiliserons le mode canonique afin de traiter les données par une ligne se terminant par un séparateur (LF (Line Feed) - '\n').

Sous Linux tout est fichier, nous utiliserons la classe QFile fournit par Qt pour la gestion des lectures/écritures :

QString m_deviceName;

m_rpmsgDevice = new QFile(deviceName);

Ici devicename est équivalent à /dev/ttyRPMSG. Puis on appliquera la configuration suivante :

bool CRpmsgDevice::openDevice()

{

    if (m_rpmsgDevice->open(QIODevice::ReadWrite))

    {

        //récupération du handler

        int fd = m_rpmsgDevice->handle();

        //configuration du port

        struct termios cfg_tty;

        memset(&cfg_tty, 0, sizeof(cfg_tty));

        fcntl(fd, F_SETFL, 0);

        tcgetattr(fd, &cfg_tty);

        //mode canonique (lecture par ligne)

        cfg_tty.c_lflag = ICANON;

        cfg_tty.c_iflag &= ~IGNBRK;

        cfg_tty.c_lflag = 0;

        cfg_tty.c_oflag = 0;

        if (tcsetattr(fd,TCSANOW, &cfg_tty) != 0)

        {

            qDebug() << "Problème mise à jour attributs";

            return false;

        }

        return true;

    }

    else

    {

        qDebug() << "Impossible d'ouvrir le port "<<m_deviceName;

        return false;

    }

}

Il ne reste plus qu’à définir les fonctions d’écritures et de lectures sur le périphérique. Pour l’écriture de la donnée, on définit une taille maximum (MAX_DATA_SIZE) des requêtes à envoyer et à lire de 64 octets (suffisant pour notre projet) :

qint64 CRpmsgDevice::writeData(QByteArray command)

{

    qint64 status = m_rpmsgDevice->write(command);

    m_rpmsgDevice->flush();

    m_rpmsgDevice->readLine(MAX_DATA_SIZE);

    return status;

}

Pour la lecture de la donnée :

QByteArray CRpmsgDevice::readData(QByteArray command)

{

    m_rpmsgDevice->write(command);

    m_rpmsgDevice->flush();

    QByteArray array = m_rpmsgDevice->readLine(MAX_DATA_SIZE);

    return array;

}

Note

La fonction readLine() de QFile s’arrête de lire lorsque l’une des conditions suivantes est remplie :

  • lecture du caractère \n ;
  • la (taille maximale spécifiée - 1) octets est lue ;

Dans le cadre du projet, le Cortex M4 répond à chaque requête accompagnée du caractère \n à la fin de sa réponse.

Et voilà, les bases du projet sont posées. On va pouvoir mettre en place les fonctions d'acquisitions des données du capteur et les lire de façon périodique. La première étape consiste à récupérer la donnée.

Dans un premier temps, on instancie le périphérique et on l'ouvre :

//création du device

m_device = new CRpmsgDevice(RPMSG_PORT);

//ouverture du port

m_device->openDevice();

Puis pour récupérer la donnée, il suffit de respecter le protocole que nous nous sommes imposé avec le code FreeRTOS du Cortex M4. Créons une fonction readAllValues(), qui encapsulera la fonction readData() définie plus haut et nous lui passerons en paramètre la commande GET_VALUES =?getAirQuality. La donnée est alors récupérée dans un QByteArray (une classe Qt permettant de gérer les tableaux d’octets) :

QByteArray array = m_device->readData(GET_VALUES);

Comme annoncé plus haut, la donnée est stockée dans un tableau de la manière et dans l’ordre suivant :

  • un entier non signé 16 bits pour la valeur du CO2 ;
  • un entier non signé 16 bits pour la valeur du TVOC ;
  • un entier non signé 8 bits pour la valeur du statut.

La donnée étant sérialisée pour la communication interprocesseur. Il va donc falloir la désérialiser. Pour cela, nous utiliserons la classe QDataStream de Qt qui est prévue pour ça :

  • On définit les variables dans lesquelles seront stockées les données :

quint16         m_co2;

quint16         m_tvoc;

quint8          m_status;

  • Puis, on utilise la classe QDataStream afin de réaliser cette opération sur la donnée. On prendra le soin de spécifier l’endianness (ici Big Endian) de la donnée, qui doit être conforme à la façon dont elle a été sérialisée par le cortex M4 :

QDataStream str(array);

str.setByteOrder(QDataStream::BigEndian);

str >> m_co2

    >> m_tvoc

    >> m_status;

À partir de là, on récupère la donnée au rythme souhaité, via l’utilisation de la classe Qtimer :   

QTimer          *m_timer;

Il suffit alors de connecter le tick du timer à l’appel d’une fonction et de le démarrer :

//Création d'un timer

m_timer = new QTimer;

//Connexion à la fonction SLOT

connect(m_timer, &QTimer::timeout, this, &CRpmsgInterface::readData);

//Démarrage du Timer

m_timer->start(100);

À l’intérieur de la fonction readData() de la classe CRpmsgInterface, nous pourrons appeler la fonction readAllValues() et afficher les données au travers la fonction qDebug().

3.6.2 Test sur cible

Comme le montre la figure 14, une fois déployée en WiFi ou via le framework Yocto/OE (bitbake service-rpmsg), nous pourrons exécuter notre application sur la WaRP7 (localisée dans /usr/bin).

Fig. 14 :  Et voilà nos données du capteur !

Nous constaterons l’affichage des 3 valeurs dans le terminal. Celles-ci étant cohérentes avec les différentes études sur le sujet :

Environ 450 ppm est un taux normal de concentration CO2,  120 ppb est un seuil moyen pour cette mesure et 0 signifie que le capteur est OK.

Le setup ayant permis les mesures, est visible sur la figure 15.

Fig. 15 :  Notre WaRP7 et sa carte MikroBUS Air Quality 2 Click en place pour les mesures !

3.7 Compiler c’est livrer !

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 découvert le standard pour la communication interprocesseur sur System on Chip i.MX7 et nous avons ainsi pu le mettre en application sur notre plateforme cible. De par l'intégration, nous avons d'autre part, mis en œuvre des notions comme FreeRTOS et Qt5 pour la gestion du channel RPMsg.

Finalement, nous avons mis en pratique l'ensemble des notions pour créer un véritable projet sur notre WaRP7, qui encore une fois est basé sur l’utilisation d’une carte MikroBUS de chez MikroElektronika. Encore une belle démonstration de l’intérêt de ces cartes. Il serait maintenant intéressant de rajouter un peu plus de fonctionnalités à celui-ci. On pourra citer la création d’un serveur Mosquitto (ou même ZeroMQ) pour l’envoi des données à travers une interface réseau en y rajoutant la notion d’IPC (Inter Process Communication) au sein de l’espace utilisateur GNU/Linux.

L’idée serait de créer un deuxième service sur notre cible et ainsi récupérer les données provenant du ServiceRPMSG, le tout en utilisant des notions comme D-Bus et la mémoire partagée le tout via Qt5. Ceci fera probablement l’objet d’un prochain article dans votre magazine préféré.

Références

[1] Présentation de l’i.MX7 Solo : https://www.nxp.com/docs/en/fact-sheet/IMX7SOLODUALFS.pdf

[2] Du microcontrôleur aux systèmes Linux embarqués : https://www.blaess.fr/christophe/files/article-2014-11-15/Du-microcontroleur-a-Linux-embarque.pdf

[3] Site officiel du projet SWUpdate : https://sbabic.github.io/swupdate/

[4] BSP FreeRTOS NXP : https://www.nxp.com/webapp/Download?colCode=FreeRTOS_iMX7D_1.0.1_LINUX&appType=license&Parent_nodeId=1433867344829722440512&Parent_pageType=product&Parent_nodeId=1433867344829722440512&Parent_pageType=product

[5] MACÉ Q. et FRIEDT J.-M., « FreeRTOS, Application à la réalisation d’un analyseur de réseau numérique sur STM32 », GNU/Linux Magazine n°207, septembre 2017 : https://connect.ed-diamond.com/GNU-Linux-Magazine/GLMF-207/FreeRTOS-application-a-la-realisation-d-un-analyseur-de-reseau-numerique-sur-STM32

[6] Explication du fichier FreeRTOSConfig.h : http://www.freertos.org/a00110.html

[7] Analyse des performances mémoire sur i.MX7 : https://blog.printk.io/2017/05/i-mx-7-cortex-m4-memory-locations-and-performance/

[8] Projet OpenAMP (page wiki) : https://github.com/OpenAMP/open-amp/wiki

[9] TEXIER P.-J. Et CHABRERIE J., « Mise en oeuvre du Protocole Modbus (RTU) sur WaRP7 via Qt5  », GNU/Linux Magazine n°208, octobre 2017 : https://connect.ed-diamond.com/GNU-Linux-Magazine/GLMF-208/Mise-en-aeuvre-du-protocole-Modbus-RTU-sur-WaRP7-via-Qt5

Tags : FreeRTOS, WaRP7