Posted on :: Tags: , , , , ,

Retour d'expérience ADNT : TDD/BDD sur Raspberry Pi Pico assisté par IA

Par ADNT Sàrl & Florian Mahon (florian.mahon@adnt.io) (ingénierie firmware / systèmes embarqués — RP2040)


Ces derniers mois, j'ai bossé sur un bootloader en Rust pour le RP2040. Mais le vrai défi, ce n'était pas le bootloader lui-même, c'était d'intégrer des agents IA dans mon quotidien de dev embarqué sans que ça dérape. Parce que les pièges sont nombreux : l'IA qui génère trop de code, des implémentations à côté de la plaque, du refactoring en boucle…

J'ai d'abord essayé les approches classiques : des workflows avec des tickets bien détaillés, des spécifications exhaustives. À chaque fois, même constat : les tickets s'empilent, le code gonfle, on perd le cap… et on se retrouve avec une dette technique qui nous ralentit plus que l'IA ne nous accélère. Je passais mon temps à piloter l'agent au lieu de livrer.

Alors j'ai changé de méthode. J'ai décidé de travailler comme si j'étais en binôme avec un collègue, en appliquant une discipline agile toute simple :

petits pas, objectifs testables, tests d'abord, branche courte, revue, intégration.

L'idée de cet article, c'est de partager cette approche et ce qu'elle m'a appris. Le projet est open source, téléchargeable, et testable sur une Raspberry Pi Pico, vous pouvez essayer par vous-même.

Pour être clair : le but n'était pas de "faire un bootloader" pour le plaisir. Le bootloader est une brique technique au service de quelque chose de plus ambitieux : une plateforme de TDD/BDD firmware sur du vrai matériel, avec la même fluidité qu'en dev logiciel classique.

La première brique, un bootloader et les outils pour l'instrumenter.

1. Roadmap minimale : trois exigences

Avant d'écrire la moindre ligne de code, j'ai posé trois exigences :

  1. Double banque A/B pour pouvoir revenir en arrière si ça casse.
  2. Exécution du firmware en RAM pour garder un jitter faible et un comportement plus déterministe.
  3. Respecter la séquence de boot du RP2040 (ROM → boot2 → application).

Sur le RP2040, un second stage bootloader (boot2) s'exécute avant l'application pour initialiser l'accès à la flash QSPI (XIP) 4. Crispy s'insère juste après : il choisit une banque, copie le firmware en RAM et lui passe la main.

flowchart TD A[RESET] --> B[Boot ROM] B --> C[boot2] C --> D[Crispy Bootloader] D --> E[Firmware en RAM]

Comme tout projet embarqué qui se respecte, j'ai commencé par faire clignoter une LED. Base Rust, outillage VSCode + Pico SDK, compilation, flash, blink. OK, l'environnement tourne. Mais ça ne suffit pas : ce workflow-là exige que je sois devant l'écran en permanence.

J'ai donc rapidement basculé vers quelque chose de plus automatisable :

  • des logs via defmt (et RTT),
  • des scripts pour piloter l'exécution,
  • et dès que possible, des tests d'intégration.

C'est à partir de là que l'IA a commencé à être vraiment utile. Je ne lui balançais plus des prompts dans le vide — je lui donnais un contexte concret, et surtout un accès à la chaîne de debug (sonde + probe-rs/GDB) pour qu'on puisse observer ensemble l'état réel du système 3.

La suite s'est faite en itérations courtes :

  1. Un boot minimal + un firmware minimal pour valider que ça démarre.
  2. Des tests BDD pour vérifier le comportement.
  3. Un outil d'upload et un protocole de communication.
  4. L'extension du bootloader, juste ce qu'il faut pour supporter ces tests.
  5. Nettoyage, consolidation, et on repart.

3. Chaîne minimale : bootloader + firmware

Mes tentatives précédentes m'avaient appris un truc : si on laisse l'IA partir dans tous les sens, on se retrouve vite avec une base de code énorme et ingérable. J'ai donc découpé en petites étapes complètes — chacune couvrant les couches nécessaires, mais testable à chaque incrément.

La première étape, c'était d'avoir :

  • un bootloader minimal,
  • un firmware Rust minimal,
  • des retours observables (defmt/RTT),
  • et des scripts pour valider que tout marche.

Ce choix a eu deux effets immédiats :

  1. j'avais rapidement un système testable depuis mon poste ;
  2. l'IA n'était plus un "générateur de code" — elle devenait un vrai partenaire de discussion, contrainte par des résultats mesurables.

À partir de là, plus besoin de rédiger des prompts de trois paragraphes. La boucle de test parlait d'elle-même, et l'IA proposait des modifs que je pouvais valider dans la foulée sur la cible.


4. Outillage et consolidation

Une fois la chaîne minimale stable, j'ai construit les outils pour rendre la boucle build → upload → boot → validation reproductible :

Un CLI (crispy-upload) pour :

  • envoyer des binaires dans les banques,
  • configurer le bootloader,
  • et automatiser les tests.

Le protocole a été consolidé :

  • ajout d'un contrôle d'intégrité (CRC) sur les firmwares,
  • formalisation du protocole,
  • et création d'une librairie Python équivalente pour certains scénarios.

À ce stade, on avait un chemin minimal, stable et testable : bootloader → copie en RAM → exécution → comportement observable. Le firmware Rust servait de firmware étalon : petit, déterministe, parfait pour stabiliser la chaîne sans ajouter la complexité d'un SDK complet.


5. Refactoring et debug avec probe-rs

Avec l'outillage en place et une bonne couverture de tests, j'ai pu refactorer sereinement :

  • mutualisation des structures entre firmware et bootloader,
  • création de bibliothèques communes,
  • nettoyage du code.

En parallèle, j'ai mis en place un environnement de debug fiable dans VSCode avec probe-rs.

Bonus inattendu : contribuer à probe-rs

Cette phase m'a réservé une surprise : le firmware marchait (les tests le prouvaient), mais le debug plantait régulièrement. Frustrant — la cible est saine, mais l'outillage ne suit pas.

Premier problème : en combinant tests et instrumentation, on a isolé un bug dans probe-rs lui-même. En travaillant avec l'IA sur un repro minimal, on a fini par soumettre un correctif upstream (PR 6). Le workflow est redevenu stable.

Deuxième problème : même après ce fix, le firmware en RAM restait galère à débugger. Les breakpoints matériels ne marchaient pas comme prévu (limitation connue du Cortex-M0+/FPB avec du code relocalisé). Avec l'IA, on a implémenté des breakpoints software dans une version modifiée de probe-rs 2. C'est une implémentation légère, pas proposée en upstream, mais elle a débloqué le workflow et accéléré considérablement les itérations.

Ce que j'en retiens : même les outils de dev méritent qu'on les investigue. La méthode rigoureuse (tests + repro + instrumentation) s'applique tout autant quand le problème vient de l'outillage.


6. Du Rust au C++ : validation croisée

Avec cette base stable et debuggable, l'étape d'après consistait à vérifier la compatibilité avec l'écosystème C++ du Pico SDK. La règle du jeu était claire :

le SDK doit rester standard, sans patch — pas question de maintenir un fork du SDK juste pour notre bootloader.

Les tests existants ont été décisifs : ils ont immédiatement mis en évidence un problème d'initialisation matérielle (PLL/timers/clocks). Le bootloader laissait le hardware dans un état que le Pico SDK ne s'attendait pas à trouver.

La méthode, toujours la même :

  • tests → repro,
  • instrumentation → observation,
  • IA → hypothèses et expériences ciblées,
  • fix → non-régression.

En moins d'une journée, on avait un firmware C++ minimal qui tournait avec le SDK standard.


7. Architecture en services

Ensuite est venue une refonte importante : structurer le bootloader en "services". Ce n'était pas pour le plaisir de l'architecture — c'était un besoin concret.

  • Déterminisme : rendre la boucle principale explicite et prévisible.
  • Lisibilité : chaque responsabilité dans son module.
 let services = [
        ServiceType::UsbTransport(UsbTransportService::new()),
        ServiceType::Trigger(TriggerCheckService::new()),
        ServiceType::Update(UpdateService::new()),
        ServiceType::Led(LedBlinkService::new()),
    ];

Ce découpage a aussi fait apparaître un besoin qu'on n'avait pas anticipé : une zone de staging en RAM lors de l'upload, avant d'écrire en flash. Logique, quand on y pense — les contraintes de la flash (XIP, interruptions, multicore) documentées dans le Pico SDK 4 l'imposent.


8. Rendre l'IA utile sans perdre la maîtrise

Si je ne devais retenir qu'une chose de ce projet, ce serait ça :

L'IA devient utile quand elle est contrainte par des tests et nourrie avec des faits réels.

Des prompts courts, mais avec de vraies données

Au fil du projet, j'ai convergé vers un format de prompt qui marche à tous les coups : des faits mesurables plutôt que de longues explications.

Concrètement, au lieu de rédiger trois paragraphes de contexte, je fournis :

  • Objectif : ce que je veux obtenir (une phrase)
  • Repro : 4 à 6 étapes pour reproduire le problème sur la cible
  • Observé : ce qui se passe vs. ce qui devrait se passer, avec les logs
  • Instrumentation : données probe-rs / GDB (PC, SP, registres, dump mémoire)
  • Demande : 3 hypothèses ordonnées, chacune avec un test minimal

Ce format ne s'est pas imposé du jour au lendemain — il a émergé naturellement. Mais une fois en place, la collaboration devient méthodique : hypothèses → expériences → validation sur cible → non-régression.


Conclusion

Crispy, ce n'est pas juste "un bootloader A/B". C'est la fondation d'une approche TDD/BDD firmware sur matériel réel, avec une boucle d'itération rapide et fiable — comme en dev logiciel classique, mais avec la vérité du hardware.

Ce que ce projet m'a surtout appris, ce n'est pas un trick technique. C'est une méthode qui permet de rester efficace — y compris quand on travaille avec un agent IA.

Cinq conseils pour intégrer l'IA en embarqué

  1. Montez une chaîne testable de bout en bout dès le départ Avant d'élargir le périmètre, établissez un flux complet : build → upload → boot → validation sur cible. Cette colonne vertébrale rend chaque itération mesurable et limite la dérive.

  2. Donnez à l'IA de quoi tester, pas juste de quoi générer L'IA est vraiment utile quand elle peut s'appuyer sur des faits : sorties de tests, logs, mesures, états observables. Plus le contexte est concret, plus ses suggestions sont pertinentes.

  3. Nourrissez vos prompts avec des données réelles Pas besoin de pavés de texte. Un scénario qui échoue, les étapes pour le reproduire, les logs, les résultats d'instrumentation — ça suffit. Et ça converge beaucoup plus vite.

  4. Gardez un flux de développement cadré Branches courtes, revues systématiques, intégration fréquente. L'IA accélère la production, mais sans discipline, la vitesse se transforme en dette technique.

  5. Traitez vos tests comme un livrable à part entière Les tests sont la source de vérité. S'ils sont instables ou incomplets, tout le reste devient fragile — y compris l'apport de l'IA. Investissez dans des tests fiables, reproductibles, et orientés comportement.

Et la suite ?

Ce projet m'a permis de valider une démarche agile adaptée à l'embarqué et d'intégrer l'IA dans un workflow qui apporte vraiment quelque chose. La chaîne est en place : bootloader, firmware, outils, tests, debug.

Avec ces bases, on peut absorber la complexité — nouveaux firmwares, nouvelles briques, intégration continue sur hardware — tout en gardant l'IA comme accélérateur, sans perdre le contrôle.

Prochaine étape : durcir et industrialiser tout ça. Runners, tests d'intégration élargis, et extension aux briques de communication rapide.


Références

  1. Repo Crispy (README, structure, Quick Start, tests, philosophie IA) https://github.com/ADNTIO/crispy-bootloader-rp2040-rs

  2. Documentation crispy-upload (note sur probe‑rs modifié + software breakpoints, install via make install-probe-rs) https://docs.rs/crate/crispy-upload/0.2.0

  3. probe‑rs (toolchain de debug — CLI/DAP/GDB server, etc.) https://github.com/probe-rs/probe-rs https://probe.rs/docs/

  4. Pico SDK docs — hardware_flash (contraintes XIP/IRQ/cores) + mention PICO_NO_FLASH https://www.raspberrypi.com/documentation/pico-sdk/hardware.html

  5. rp2040-boot2 (second stage bootloader — boot2 et placement dans l'image) https://github.com/rp-rs/rp2040-boot2

  6. PR probe‑rs (correctif lié au workflow PicoProbe / debug) https://github.com/probe-rs/probe-rs/pull/3810

  7. Fork probe‑rs (version modifiée avec breakpoints software pour debug firmware en RAM) https://github.com/fmahon/probe-rs