Ça faisait quelques temps que le concept me faisait de l’oeil: un RTOS avec une abstraction suffisante pour rendre votre code embarqué multi-plateforme, le tout sponsorisé par la fondation Linux. J’ai donc testé pour vous : développer un driver pour Zephyr RTOS, et je vais vous donner mes impressions et mes astuces.
En genèse du projet, mon tiroir à bidouilles contient plusieurs kits d’évaluation : un Nucleo STM32, un ESP32 et un nRF52DK. Et puis j’ai retrouvé quelques émetteurs-récepteurs 2.4 GHz nRF24 sur AliExpress, sans doute achetés pour un projet oublié. Or, le nRF24 n’a pas (encore) de support Zephyr.
J’ai commencé par écouter religieusement le sieur Marull-Paretas dans son Mastering Zephyr Driver Development, et c’est parti pour le fun!
Vous pouvez aussi vous référer au tutoriel de chez Bootlin.
1- Préparation du dossier de travail
Tout d’abord, j’ai forké l’application d’exemple suivante : https://github.com/zephyrproject-rtos/example-application.
Alors, évidemment, j’ai galéré à compiler l’exemple, car j’ai lu le Readme trop vite. Il y a une subtilité : le tutoriel de base fait télécharger les sources de Zephyr dans ~/zephyrproject, mais l’application-exemple les fait retélécharger dans le dossier my_workspace. Il y a probablement un peu de redondance là-dedans.
Voilà à quoi ressemble un de mes dossiers Zephyr standard, qui compile dans un venv Python après avoir invoqué le script d’initialisation Zephyr ~/zephyrproject/zephyr/zephyr-env.sh :
zephyr_esp32/
├── blinky
├── boards
├── CMakeLists.txt
└── README.md
Et voilà mon nouveau dossier Zephyr driver, possédant sa propre copie de Zephyr et son propre dossier de modules :
zephyr_driver
└── my-workspace
├── driver_nrf24l01
| ├── west.yml
| ├── CMakeList.txt
| ├── ...
|
├── modules
| └── hal
| ├── cmsis
| ├── espressif
| ├── nordic
| └── stm32
└── zephyr
├── arch
├── boards
├── cmake
├── CMakeLists.txt
├── ...
Pour compiler ce projet, pas besoin d’invoquer zephyr-env.sh, puisque tout est en local.
Autre révélation du moment, les modules, qui correspondent à des bouts de codes tierces parties des différents fabricants, sont téléchargés automatiquement par west, il suffit de les ajouter au fichier west.yml du projet et d’appeler :
west update
2- Rédaction du driver
Le nRF24 est un petit émetteur-récepteur 2.4 GHz qui communique via un bus SPI, avec tout un tas de registres décrits dans sa documentation comme d’habitude.
Il existe un driver pour Raspberry Pi très complet, utilisé comme inspiration. Le fichier header qui décrit les différents registres a été fort utile pour m’éviter les erreurs idiotes de recopie des valeurs.
Ensuite, le sensei Marull-Paretas nous dit que nous devons choisir entre :
- Implémenter une API générique existante de Zephyr, ou
- Créer une API customisée
L’API, c’est simplement la liste des fonctions accessibles à la partie applicative de Zephyr. C’est donc beaucoup mieux d’avoir une API générique, puisque ça permettrait de changer le composant pour une équivalence de manière transparente. Or, dans mon cas, l’API « radio propriétaire » n’existe pas encore, donc je vais commencer mon driver sans, et je le modifierai quand une API générique sera disponible.
Un premier travail est donc de modifier exemple-application pour ne garder que les fichiers relatifs au driver, les renommer, et modifier les fichiers de configuration pour compiler les nouveaux fichiers. J’en ai profité pour modifier les fichiers projets pour intégrer la target de travail, dans mon cas, ma carte Nucleo.
Une fois le squelette de l’application préparé, il est temps de remplir les champs avec la logique réelle du composant, avec la datasheet d’un côté et l’analyseur logique de l’autre.
Driver et application d’exemple
Le fichier le plus important est
./drivers/nrf24l01/nrf24l01.c
Mais ce fichier n’est pas standalone, il faut l’invoquer dans une application main :
./app/src/main.c.
Zephyr appelle l’initialisation de chaque driver (dans mon cas nrf24l01_init) avant de démarrer l’application. Si comme moi, vous aimez écrire un brouillon et vous soucier du découpage en fonctions dans un second temps, il est possible de commencer à coder avant d’avoir mis en place les fonctions d’API, en mettant simplement tout en vrac dans nrf24l01_init et avec un main.c vide (c’est une technique perso, ne faites pas ça si vous respectez la philosophie de toujours coder proprement).
Device tree
Les données du device tree sont à venir coller dans deux structures :
struct nrf24l01_config
struct nrf24l01_data
La structure config est statique, on a tendance à y mettre les références aux GPIOs et aux bus utiles au fonctionnement du driver. La structure data est modifiable, donc c’est là où finissent tout ce que j’eusse été tenté de déclarer comme variable globale.
Toutes les options du device tree sont listées et décrites dans un fichier Yaml appelé « bindings ». Ce fichier est à la fois une documentation du driver et une partie du code, il n’est absolument pas facultatif et il faut bien faire attention au nom du fichier et aux subtilisés typographiques. Histoire vécue : j’ai passé une heure à déboguer parce-que j’avais appelé ce fichier nrf24l01.yaml et pas nordic,nrf24l01.yaml.
./dts/bindings/nrf24/nordic,nrf24l01.yaml
Dans mon exemple, je n’ai pas eu besoin de créer un device tree complet personnalisé puisque j’utilise une carte d’évaluation, j’ai simplement ajouté le nrf24l01 dans un device tree overlay :
./app/boards/nucleo_f756zg.overlay
Options de configuration
J’ai également rajouté quelques options Kconfig à mon driver, notamment un switch entre fonctionnement en polling et fonctionnement en interruption :
./drivers/nrf24l01/Kconfig
Ces options sont ensuites sélectionnées côté application, dans
./app/prj.conf
Notez qu’il est également possible mettre ce type d’options dans le device tree. Techniquement ça ne change rien, philosophiquement, on doit mettre ce qui est matériel dans device tree et ce qui est logiciel dans le Kconfig. Il y a une zone grise entre configuration matérielle et configuration logique, puisque l’option « polling » va surtout être utilisée si vous ne disposez pas de l’interruption hardware, mais dans les drivers mainline de Zephyr, il est d’usage de mettre ça dans la configuration.
API custom
En ce qui concerne l’API customisée, elle est décrite dans le dossier include.
./include/app/drivers/nrf24.h
Attention, il n’est pas possible de donner aux fonctions API les mêmes noms que les fonctions en interne du driver. Dans mon cas j’ai triché et j’ai appelé mes fonctions API « nrf24_read » et « nrf24_write » et leurs implémentations « nrf24l01_read » et « nrf24l01_write ».
Côté application, c’est bien évidemment « nrf24_read » et « nrf24_write » qui sont appelés.
Tests CI
Dans mon cas, difficile de tester mon projet en workflow Github avec Qemu, puisqu’il s’agit de quelque chose de complètement hardware. J’ai donc supprimé les tests en rapport avec l’exécution pour ne vérifier que la bonne compilation sur les différentes plate-formes.
3 – Résultat
Le résultat est là : https://github.com/everedero/driver_nrf24l01
Mon application d’exemple consiste à prendre le driver nRF24 nouvellement créé pour le faire tourner sur les 3 plate-formes d’évaluation de mon tiroir à bidouilles, un STM32 Nucleo, un nRF52DK, et un ESP32 WROOM. Et c’est super facile, puisqu’il suffit juste de créer un device tree overlay pour chaque plate-forme, en connectant les bons câbles aux bons endroits, et le système se débrouille avec ça ! Pour démontrer la magie, mon exemple fait discuter ensemble les 3 plate-formes au travers de 3 rôles : Alice qui initie la conversation, Bob qui lui répond, et Eve qui se contente d’écouter discrètement.
Et ça marche !
4 – Conclusion
Comme souvent avec Zephyr, l’apprentissage comporte une taxe à l’entrée non négligeable. Cepedant, je suis contente d’avoir pris le temps d’en venir à bout, parce-que pouvoir porter le même code sur 3 plate-formes différentes quasiment instantanément, c’est quand même assez merveilleux et plein de potentiel.