Introduction
De nos jours le vélo prend une place de plus en plus importante dans la mobilité contemporaine. De plus en plus de vélos participent à la circulation ou se retrouvent garés dans les rues de nombreuses villes. La surveillance de ces vélos représente donc un enjeu important permettant de diminuer le nombre de vols des vélos et, potentiellement, d'améliorer la circulation. Smile ECS a décidé de réaliser un démonstrateur de système de gestion embarquée d'une flotte de vélos. L'objectif principal était de concevoir un système capable de suivre en temps réel la géolocalisation des vélos et de transmettre ces informations dans un Cloud pour une gestion optimale de la flotte.
Pour ce faire nous avons porté notre choix technique concernant le système d'exploitation embarqué vers Zephyr OS qui continue de gagner en popularité et dont nous avons déjà parlé sur notre blog. Dans ce cadre nous avons été amenés à développer et implémenter un driver pour assurer le support d'un module Wi-Fi dans Zephyr. Dans cet article nous allons aborder le sujet de développement de pilotes dans Zephyr en faisant appel à la notion de déchargement (offloading) dans l'OS.
Choix matériel
Dans ce projet nous avons imaginé un système embarqué qui sera attaché à un vélo. Ce système comporte 3 modules matériels essentiels : Module GPS, Module de communication et Microcontrôleur. Ces composants sont présentés sur le schéma ci-dessous.
Pour la fonction de géolocalisation nous avons choisi le shield X-NUCLEO-GNSS1A1 qui fournit des capacités GPS grâce à son module TESEO-LIV3F offrant ainsi des données de géolocalisation suffisamment précises pour notre projet. Quant à la transmission des données au Cloud, notre choix a été porté sur la carte Innophase inp3010 équipée du module de communication Talaria TWO qui fournit une connectivité Wi-Fi qu’on peut contrôler via des commandes AT.
Enfin, le microcontrôleur Nucleo L433RCP, fonctionnant sous Zephyr, a joué un rôle essentiel en assurant la gestion des échanges entre le module GPS et le module de communication en UART. Ces modules sont présentés sur le schéma ci-dessus.
|
|
|
Cet article se concentrera sur le développement du driver dédié au module Wi-Fi. Nous allons aborder les défis techniques rencontrés et les solutions mises en œuvre en utilisant Zephyr comme système d'exploitation temps réel.
Zephyr
Avant de plonger dans le développement technique de notre démonstrateur, il est essentiel de comprendre les fondements de Zephyr RTOS.
Zephyr RTOS est un projet open-source collaboratif fondé par la Linux Foundation sous licence Apache qui vise à développer un système d'exploitation temps-réel de haute qualité pour les dispositifs connectés à ressources limitées dont le dépôt principal se trouve sur github https://github.com/zephyrproject-rtos/. Zephyr est particulièrement bien adapté aux applications IoT grâce à son architecture et à sa pile réseau. Il est aussi conçu pour offrir des hautes performances en temps-réel, une sécurité renforcée, ainsi qu'une flexibilité et modularité remarquables. Zephyr offre ainsi une architecture modulaire qui permet de sélectionner uniquement les composants nécessaires pour une application, réduisant donc la taille de l'image et la consommation de ressources. L'architecture de Zephyr est représentée sur la figure ci-dessous.
La structure du projet Zephyr est conçue pour fournir tous les éléments de build nécessaires pour déployer des applications IoT complexes. Le projet a également développé un outil en ligne de commande polyvalent appelé "west" pour faciliter la gestion des référentiels et le développement. L'arborescence d'un projet Zephyr est présentée sur le schéma suivant :
Les fichiers *.dts jouent un rôle essentiel dans Zephyr car ils permettent de décrire la configuration matérielle. En utilisant les fichiers .dts décrivant notre modèle Nucleo L433RCP, on peut sélectionner le SoC (System-on-a-Chip) approprié et ajouter la prise en charge matérielle pour notre module Wi-Fi. Cette approche offre la flexibilité nécessaire pour configurer le matériel spécifique utilisé dans notre démonstrateur de gestion embarquée pour la flotte de vélos.
Les fichier *.conf sont des fichiers de configuration dans le projet Zephyr, cela permet de spécifier différentes options de configuration pour le projet en cours. Ils sont utilisés pour activer ou désactiver des fonctionnalités, définir des valeurs de paramètres et personnaliser le comportement du projet en fonction des besoins spécifiques.
Par exemple, on peut activer l’utilisation et la configuration d’UART pour une application ou un projet Zephyr en général via le fichier prj.conf approprié par :
- CONFIG_UART=y
Développement du pilote Wi-Fi
L'objectif du pilote est de faire en sorte que notre module soit reconnu par Zephyr non pas comme un dispositif utilisant une communication série (UART), mais comme étant un périphérique réseau. Cela permettra d'utiliser les API de gestion de réseau proposées par Zephyr.
Avant d’arriver à cette partie de Développement du pilote wifi j’avais déjà créé une application de test où j’utilisais directement l'UART depuis la couche applicative pour établir la communication et créer notre solution IoT. La structure de cette application est présentée sur le schéma ci-dessous :
Voici quelques détails sur les tâches représentées sur le schéma :
MAIN: tâche dans laquelle on initialise et on configure les périphériques (module Wi-Fi, module GPS) qui communiquent en série avec notre NUCLEO-l433rcp, on parle donc du binding, de la configuration de baud rate, bit de parité, bit stop, et des handshakes hardware ou software.
GPS_TASK: comme on n’a pas besoin d’envoyer des données au GPS il suffit de communiquer en mode single wire, dans le sens où on se met à l’écoute du GPS.
INP_TASK: cette tâche est responsable de l’envoi des commandes AT afin de configurer le module pour pouvoir exécuter les différents scénarios. Nous utilisons souvent le terme scénario car littéralement nous envoyons une suite de commandes AT pour se connecter au Wi-Fi, définir le protocole de communication, puis envoyer des données.
Pour envoyer des commandes au module automatiquement, cette tâche configure une interruption qui nous permet de recevoir les réponses du module sur un buffer puis les envoyer vers une autre tâche via une queue de messages MSG_Q.
PROCESS_TASK: Cette tâche est responsable du traitement des données brutes reçues par le GPS et le module Wi-Fi.
Les interactions entre les différentes couches de Zephyr dans le cadre de cette application de test sont représentées ci-dessous :
Comme il a été dit précédemment, nous souhaitons aller au-delà de l'approche d'utilisation pure de communication UART et utiliser les autres couches de la pile Zephyr. La pile que nous souhaitons utiliser est présentée ci-dessous:
Pour cela, nous allons procéder à la création d’un driver spécifique qui agit comme une interface entre l'UART et le reste du système Zephyr. Ce driver permettra d'offloader les API de gestion de réseau de Zephyr vers l'UART. Ainsi, au lieu d'utiliser directement l'UART pour gérer la communication réseau, dans cette nouvelle approche, nous souhaitons bénéficier des API de gestion de réseau et des protocoles de communication fournis par Zephyr.
Concrètement, cela signifie que notre application de niveau supérieur pourra utiliser les API de gestion de réseau et de communication de Zephyr, telles que les API Wi-Fi et les API réseau, pour gérer les connexions, les transferts de données et la communication avec le Cloud ou d'autres dispositifs. Le driver UART agira en coulisse, en traduisant les appels d'API de Zephyr en commandes AT appropriées via l'UART, et vice versa. Cela permet une intégration harmonieuse entre l'UART et le reste de l'écosystème Zephyr, une plus grande flexibilité dans la gestion de la communication réseau pour notre solution IoT.
Création de l’overlay et fichiers de configuration
Lors de la création du pilote pour notre module Wi-Fi, nous avons commencé par ajouter un overlay spécifique à ce module. L'overlay permet de configurer la plate-forme matérielle (SoC) pour prendre en charge le module Innophase INP3010 utilisant l'interface UART. Le contenu du répertoire lié au shield INP3010 est présenté ci-dessous :
Grâce à l'overlay, nous avons pu définir les détails matériels spécifiques au module INP3010 UART, notamment les broches nécessaires pour la communication UART et la vitesse de transmission ce qui va permettre dans la suite d’envoyer les commandes AT pour commander le module Wi-Fi. Ces informations ont été intégrées à la hiérarchie du périphérique UART existant. En conséquence, Zephyr est désormais capable de reconnaître le module INP3010 UART et de le gérer de manière appropriée.
Si on utilise le bus USART3 sur notre carte Nucleo, on peut utiliser les broches TX = PC4 et RX = PB11. Étant donné que notre module fonctionne avec un baudrate de 115200, on peut configurer ces paramètres en écrivant dans l'overlay comme suit :
&usart3 {
status = "okay";
pinctrl-0 = <&usart3_tx_pc4 &usart3_rx_pb11>;
pinctrl-names = "default";
current-speed = <115200>;
inp3010_device: innophase_inp3010_uart {
compatible = "innophase,inp3010-uart";
status="okay";
label = "innophase_inp3010_uart";
};
};
Les deux fichiers Kconfig sont essentiels pour configurer les paramètres matériels et déterminer la présence des modules INP3010 et INP3010 UART.
Le fichier Kconfig.shield définit la configuration du module en fonction de sa présence dans la liste des cartes.
config SHIELD_INNOPHASE_INP3010_UART
def_bool $(shields_list_contains,innophase_inp3010_uart)
D'autre part, le fichier Kconfig.defconfig permet de définir les configurations de la carte en fonction de ces modules, tout en spécifiant leur dépendance par rapport au support du réseau (NETWORKING). Les configurations du module INP3010 UART sont configurées en utilisant le choix WIFI_INP3010_BUS dans ce fichier, permettant de sélectionner le bus approprié (UART dans ce cas) en fonction de la présence du module INP3010 UART dans la carte.
if SHIELD_INNOPHASE_INP3010_UART
if NETWORKING
choice WIFI_INP3010_BUS
default WIFI_INP3010_BUS_UART if SHIELD_INNOPHASE_INP3010_UART
depends on WIFI_INP3010
endchoice
Endif # NETWORKING
Endif # SHIELD_INNOPHASE_INP3010_UART
En utilisant les configurations définies dans ce fichier, le pilote peut détecter et prendre en charge les modules appropriés en fonction du matériel réellement présent sur la carte. Ainsi, grâce à ces fichiers Kconfig, le pilote peut être configuré de manière dynamique pour s'adapter à différentes configurations matérielles, offrant une flexibilité et une compatibilité optimales lors du déploiement du démonstrateur de gestion embarquée de la flotte de vélos.
On ajoute le fichier “innophase,inp3010-uart.yaml” dans le répertoire zephyrproject/zephyr/dts/bindings/wifi. Ce fichier yaml a pour objectif de définir comment le pilote doit interagir avec la représentation matérielle du module Wi-Fi lorsqu'il est utilisé avec une interface UART. Le contenu du répertoire en question est présenté ci-dessous :
Implémentation du Pilote
Dans le répertoire des drivers Wi-Fi, nous avons implémenté le pilote spécifique pour le module Wi-Fi INP3010. Ce pilote est composé de plusieurs fichiers qui interagissent ensemble pour permettre la gestion de la communication avec le module INP3010. Le contenu de ce répertoire est présentée ci-dessous :
Le fichier "CMakeLists.txt" est un fichier de configuration CMake utilisé pour définir comment le driver doit être compilé et inclus dans le projet Zephyr si on souhaite l’utiliser :
if(CONFIG_WIFI_INP3010)
zephyr_include_directories(./)
zephyr_library_sources(
wifi_inp3010_socket.c
wifi_inp3010.c
wifi_inp3010_uart.c
inp3010_shell.c
inp3010_mqtt.c
)
endif()
"inp3010_log.h" est un fichier d'en-tête qui définit des macros de journalisation pour faciliter le débogage et le suivi des opérations du driver ainsi que l’affichage des erreurs.
Le fichier "Kconfig.inp3010wifi" contient les configurations spécifiques au driver Wi-Fi INP3010. Il permet de configurer les options du driver lors de la compilation du projet Zephyr :
# Innophase inp3010 TALARIA TWO WiFi driver options
menuconfig WIFI_INP3010
bool "INP3010 driver support"
default y
select SERIAL
select UART_INTERRUPT_DRIVEN
select GPIO
select RING_BUFFER
select CONFIG_NET_CONFIG_MY_IPV4_ADDR
select NET_SOCKETS
select NET_TCP
select NET_SOCKETS_SOCKOPT_TLS
select NET_SOCKETS_OFFLOAD
select NET_SOCKETS_POSIX_NAMES
select WIFI_OFFLOAD
select NET_OFFLOAD
if WIFI_INP3010
choice WIFI_INP3010_BUS
bool "Select BUS interface"
default WIFI_INP3010_BUS_UART
config WIFI_INP3010_BUS_UART
bool "UART Bus interface"
select SERIAL
# on peut modifier cette partie si on souhaite ajouter de l’i2c par exemple…
endchoice
config WIFI_INP3010_THREAD_PRIO
int "wifi_inp3010 threads priority"
default 2
config WIFI_INP3010_SOCKET
bool "INP3010 SOCKETS support"
select NET_SOCKETS
select NET_SOCKETS_POSIX_NAMES
select NET_TCP
select NET_IPV4
default y
endif # WIFI_INP3010
Couche d’abstraction UART
wifi_inp3010_uart.c implémente l'interface UART pour communiquer avec le module INP3010.
On utilise une structure de données appelée "inp3010_uart_data" pour représenter les informations associées à l'interface UART du module INP3010. Cette structure contient les éléments suivants :
struct inp3010_uart_data {
const struct device *dev; // Périphérique UART utilisé
enum inp3010_uart_fsm fsm; // État du gestionnaire FSM
uint8_t rx_count; // Compteur de réception
size_t rx_buf_size; // Taille du tampon de réception
char *rx_buf; // Pointeur vers le tampon de réception
struct ring_buf rx_rb; // File circulaire pour stocker les données reçues
uint8_t iface_rb_buf[INP3010_RING_BUF_SIZE]; // Tampon interne de la file circulaire
};
Le choix d’utilisation d’un tampon (buffer) dans notre cas est important vu que la réception des données est effectuée grâce à une interruption sur l’UART, qui est un mécanisme de traitement asynchrone qui nécessite d'être exécuté rapidement et de manière efficace. En stockant les données brutes reçues dans une file circulaire et en les traitant en dehors de l'ISR, on évite de réaliser des opérations coûteuses au sein de l'ISR, ce qui réduit le risque de bloquer ou de ralentir les autres opérations du système.
Le choix d’utilisation des files circulaires est motivé par leur mécanisme de gestion simple et stable des données reçues. Elles sont dimensionnées de manière à éviter les débordements et à gérer les cas où les données arrivent plus rapidement que le système ne peut momentanément les traiter.
En définissant le DT_DRV_COMPAT comme innophase_inp3010_uart, on permet au pilote de rechercher et de récupérer les informations de configuration du périphérique à partir du device tree, plutôt que de définir manuellement ces informations dans le code du pilote.
La couche d’Offload
Une fois la couche d'abstraction UART mise en place, la prochaine étape consiste à créer la couche d'offload (WI-FI offload et socket offload). Cette couche agit comme un point de contact entre l'interface UART et les différentes couches de gestion de réseau de Zephyr.
Son objectif principal est de recevoir les requêtes des API de Zephyr et d’envoyer les commandes AT appropriées pour chaque demande. Cette interaction permet de faciliter la communication entre le driver Wi-Fi INP3010 et le système d'exploitation Zephyr, permettant ainsi une intégration transparente du réseau et des capacités de communication Wi-Fi dans le cadre de l'écosystème Zephyr. La couche d'offload assure une gestion efficace des opérations de communication en traduisant les demandes d'API Zephyr en commandes AT spécifiques, permettant ainsi une intégration aisée du module Wi-Fi INP3010 dans les applications Zephyr. Grâce à cette couche d'offload, le driver est en mesure de répondre aux requêtes réseau de manière cohérente et performante, ce qui facilite l'utilisation du Wi-Fi dans les applications embarquées basées sur Zephyr.
struct inp3010_off_socket {
uint8_t index; // Index de la socket pour une identification unique
enum inp3010_transport_type type; // Type de transport utilisé par la socket (TCP/UDP)
enum inp3010_socket_state state; // État actuel de la socket
struct net_context *context; // Pointeur vers le net_context représentant la socket
enum net_sock_type sock_type; // Type de la socket (stream/datagram)
struct net_pkt *tx_pkt; // Pointeur vers le paquet en cours de transmission
struct sockaddr_in peer_addr; // Adresse et port du pair distant (remote peer)
struct k_sem read_sem; // Sémaphore pour synchroniser les opérations de lecture
struct k_sem accept_sem; // Sémaphore pour synchroniser les opérations d'acceptation de connexion
socklen_t addrlen; // Longueur de la structure d'adresse du pair distant
net_context_recv_cb_t recv_cb; // Fonction de rappel (callback) pour la gestion des données reçues
void *recv_data; // Pointeur vers les données reçues de la socket
uint16_t port; // Numéro de port de la socket
struct k_fifo fifo; // buffer FIFO pour la transmission des données
struct net_pkt *prev_pkt_rem; // Pointeur vers les données restantes dans le dernier paquet
};
Cette structure permet de stocker toutes les informations essentielles relatives à la gestion des sockets pour la couche d'offload du driver.
Couche d’Abstraction de périphérique WiFi_INP3010
Cette couche présente une abstraction des détails matériels du périphérique, permettant ainsi une meilleure portabilité du pilote entre différentes plates-formes et périphériques prenant en charge le même jeu d'API réseau. L'intégration du driver dans la pile réseau de Zephyr est présentée sur le diagramme suivant :
struct inp3010_dev {
struct k_mutex mutex; // Mutex pour verrouiller l'accès concurrent aux données
atomic_val_t mutex_owner; // Propriétaire actuel du mutex
unsigned int mutex_depth; // Profondeur du verrouillage (pour les verrous imbriqués)
struct net_if *iface; // Pointeur vers l'interface réseau associée à l'appareil
struct inp3010_bus_ops *bus; // Pointeur vers les opérations du bus de l'appareil
scan_result_cb_t scan_cb; // Callback pour les résultats des scans WiFi
struct inp3010_sta sta; // État de la station WiFi (SSID, mot de passe, etc.)
enum inp3010_request req; // Type de requête actuelle pour l'appareil
uint8_t mac[6]; // Adresse MAC de l'appareil
char buf[MAX_DATA_SIZE]; // Tampon pour stocker les données reçues
size_t buf_len; // Longueur actuelle du tampon de données
void *bus_data; // Données spécifiques au bus de l'appareil
struct inp3010_off_socket socket[INP3010_OFFLOAD_MAX_SOCKETS]; // Tableau de sockets déchargées
int sock_index; // Index pour identifier les sockets de manière unique
};
Pour établir une connexion Wi-Fi, la fonction inp3010_connect est utilisée. Elle envoie une commande AT appropriée pour se connecter à un réseau spécifié. La fonction vérifie ensuite la réponse reçue pour déterminer si la connexion a réussi ou non.
La fonction inp3010_disconnect permet de se déconnecter du réseau Wi-Fi actuel en envoyant une commande AT correspondante (AT+WDIS).
La fonction inp3010_iface_init est utilisée pour initialiser l'interface réseau. Elle enregistre l'interface auprès de la pile réseau et configure les fonctions de gestion des événements Wi-Fi via la structure inp3010_api.
Enfin, la structure inp3010_offload contient des fonctions de gestion des opérations réseau (send, recv, connect, etc.) qui seront offloadées au pilote INP3010 pour une gestion spécifique.
static const struct net_wifi_mgmt_offload inp3010_api = {
.wifi_iface.iface_api.init = inp3010_iface_init,
.scan = inp3010_mgmt_scan,
.connect = inp3010_mgmt_connect,
.disconnect = inp3010_mgmt_disconnect,
};
Ce qui permet par la suite, dans la couche applicative de se connecter en utilisant l’api “Net Management” fournie par zephyr :
Sockets OFFLOAD
L'objectif de cette partie n'est pas d'utiliser le module Wi-Fi en tant que fournisseur direct de client MQTT, où le module gérerait implicitement la pile réseau, la création et la configuration du socket, ainsi que la gestion des couches de transport (TCP, UDP) et de sécurité via des commandes AT comme at+mqttconf, at+mqttcon, at+mqttpub, etc. L'objectif ici est d'utiliser le module Wi-Fi en tant que fournisseur de socket client, tandis que Zephyr doit se charger de configurer la pile réseau et les autres couches protocolaires.
Le niveau "d'offloading" dans ce contexte se réfère à la capacité de décharger le CPU du module Wi-Fi d'un certain nombre de tâches réseau, en les confiant plutôt au CPU applicatif sur notre Nucleo. Cela permet de comparer les performances système et d'évaluer l'efficacité énergétique de chaque configuration, afin de déterminer quelle approche est la meilleure.
Lorsque le CPU du module Wi-Fi prend en charge la gestion de la pile réseau, il assume la responsabilité des opérations liées à la communication sans fil telles que la gestion des paquets, les protocoles de communication, le chiffrement, etc... Cette approche peut soulager le CPU applicatif de ces tâches spécifiques et lui permettre de se concentrer sur d'autres aspects du système.
En revanche, lorsque ces tâches réseau sont confiées au CPU applicatif, le module Wi-Fi se concentre sur la transmission et la réception de données de manière plus basique, en transférant la responsabilité du traitement du réseau à Zephyr. Cette configuration peut présenter des avantages en termes de flexibilité et de personnalisation des opérations réseau, ainsi que de meilleures performances dans certaines situations.
Pour cela, une approche consistait à créer un squelette de cette partie en utilisant des fonctions de test, définies comme suit :
static struct net_offload inp3010_offload = {
.get = inp3010_dummy_get,
.bind = inp3010_dummy_bind,
.listen = inp3010_dummy_listen,
.connect = inp3010_dummy_connect,
.accept = inp3010_dummy_accept,
.send = inp3010_dummy_send,
.sendto = inp3010_dummy_sendto,
.recv = inp3010_dummy_recv,
.put = inp3010_dummy_put,
};
Ensuite, dans la couche applicative, des essais ont été effectués pour utiliser ces API et créer une application MQTT. Cependant, à une certaine étape du processus, l'application tente d'effectuer un handshake pour établir une connexion TCP qui sera suivie par le header MQTT. Le module Wi-Fi ne permet pas de recevoir une réponse au niveau de l'état ACK (acquittement) pour la remonter à l'application. Il se contente uniquement de remonter les messages reçus se trouvant dans la partie PAYLOAD. Lors de la connexion, généralement pendant le processus de handshake, la longueur du payload est nulle, ce qui rend impossible la poursuite de cette implémentation avec le firmware actuel du module.
En conséquence, des modifications supplémentaires dans le firmware du module ou des ajustements dans l'implémentation de l'application MQTT pour gérer cette limitation seraient nécessaires pour finaliser la mise en œuvre du mécanisme de sockets OFFLOAD avec le module WiFi_INP3010.
Conclusion
Dans cet article, nous avons pu voir comment implémenter un driver Wi-Fi dans l'environnement du système d'exploitation embarqué Zephyr. Cela a été également une occasion de démontrer un exemple de fonctionnement de Zephyr dans le cas concret d'un projet IoT lié à la surveillance de la flotte des vélos.
Pour les utilisateurs familiers avec Linux RT, l'adoption de Zephyr peut sembler relativement simple car de nombreuses fonctions sont codées en POSIX, et l'architecture du système présente des similitudes, notamment avec l'utilisation de device trees et de fichiers de configuration.
Cependant, le démarrage avec Zephyr peut être délicat, notamment en l'absence d'exemples concrets fournis par le projet Zephyr ou sa communauté. La documentation de Zephyr peut parfois manquer de clarté, ce qui nécessite parfois de contourner cette problématique en se référant à la documentation de Linux RT. Cependant, il est important de noter que malgré certaines similitudes en terme de fonctions codées en POSIX, il peut y avoir des nuances spécifiques à Zephyr qui nécessitent une attention particulière.