Sur un projet récent, un petit appareil grand public de type bouton, le genre de carte où chaque millimètre carré doit gagner sa place, j’ai pris ce qui me semblait être une décision raisonnable. L’ESP32-S3 a un USB natif. Pas de FTDI, pas de programmateur, pas de header UART qui prend de la place. J’ai donc mis une seule prise USB-C sur la carte et je l’ai utilisée comme interface de programmation, de debug et de communication utilisateur. Réglé.

Quelques itérations de firmware plus tard, je l’ai brické.

Pas de façon dramatique, type “fumée”. Brické de façon plus discrète, plus frustrante : le stack USB s’est fait reconfigurer par du code qui roulait bien sur le banc, mais qui n’énumérait plus comme cible programmable au prochain boot. Je ne pouvais plus le flasher en USB, et je n’avais pas d’autre porte d’entrée. L’appareil restait là, à clignoter avec assurance, sans aucun mécanisme pour accepter une nouvelle image de firmware.

Ce qui a suivi, c’est quelques heures de recherche, puis la partie dont personne ne te parle : dessouder le module ESP32-S3, le module complet avec son blindage EMI intégré, pas juste la puce, d’un petit PCB grand public, sans soulever le shield du module, sans arracher les pads de la carte, et sans cuire les composants à côté. La canette de métal sur le dessus est attachée par refusion. Si tu pousses trop de chaleur pour libérer le module, la canette se décolle du module avant que le module se décolle de la carte.

Et ça, c’était pour un seul appareil pris au piège, sur le banc, avec les bons outils. Le service sur le terrain, ce n’est pas une option.

Ce que veut vraiment dire “USB natif”

L’ESP32-S3, et une liste grandissante d’autres MCU, n’a pas besoin de puce USB-to-UART. Le microcontrôleur parle USB lui-même. Le bootloader ROM en usine présente un device CDC sur USB, c’est ce avec quoi esptool discute quand tu flashes une puce neuve. Tu branches le câble dans ton laptop, tu lances l’upload, la puce se programme.

Les clients adorent ça. WebSerial dans les navigateurs Chromium veut dire qu’ils peuvent flasher, debugger et streamer les logs depuis une page web. Tu donnes une URL à quelqu’un de non-technique, il met à jour son propre prototype. Pour la vitesse d’itération en phase de prototypage, c’est un vrai gain.

Le piège, c’est que le périphérique USB sur la puce est configurable. Ton firmware décide quel genre de device la puce présente : CDC, HID, mass storage, classe vendor, plusieurs en même temps. Le bootloader ROM n’est actif que sous une condition de reset spécifique. Une fois que ton application boote et reconfigure le stack USB, disons que ton code (ou un bout copié d’un exemple, ou quelque chose qu’une IA t’a généré sans flagger le risque) décide d’énumérer comme Mass Storage Class pour faire une mise à jour fancy par drag-and-drop, le device cesse d’être une cible CDC programmable. C’est devenu une clé USB. Si ton code ne préserve pas un chemin de retour vers le bootloader, tu ne peux plus flasher en USB.

Pour rejoindre le bootloader, il faut tirer GPIO0 à zéro pendant le reset. Sur un dev board, il y a un bouton BOOT. Sur un produit grand public scellé avec une seule prise USB-C, il n’y a rien.

Pourquoi c’est pire que ça en a l’air

Un bug comme ça, sur papier, ça se lit comme une “petite régression de firmware”. Ça ne l’est pas.

Les pins concernés ne sont pas exposés dans un produit fini. EN et GPIO0 ne sont pas sortis quelque part à moins que quelqu’un l’ait planifié. Dans mon cas, ils n’étaient sortis nulle part où je pouvais les atteindre sans enlever le module.

Le chemin de récupération est invasif. Pas de pin de strap accessible veut dire enlever le module de la carte. Avec un module blindé, tu ne chauffes pas juste une puce, tu chauffes une canette de métal collée à un frame par-dessus le composant que tu essaies de sauver.

L’OTA n’est pas un backup. L’OTA a besoin d’un firmware fonctionnel qui boote assez loin pour démarrer la mise à jour. Si c’est ton chemin de boot qui est brisé, l’OTA ne peut pas t’aider.

Donc un seul mauvais commit sur le stack USB, poussé tard un vendredi, fonctionnel sur le banc, brisé au prochain reset, devient une job de rework de plusieurs heures par unité. Sur un prototype, c’est un après-midi désagréable. Sur vingt unités déployées, c’est un problème.

Les trois pads de cuivre

Toutes les cartes ESP32-S3 que je conçois aujourd’hui reçoivent une empreinte TC2030. Ce n’est pas un connecteur, c’est un patron de pads sur le PCB, six pastilles de cuivre exposées, conçues pour un câble Tag-Connect avec des pogo pins à ressort du côté câble. Zéro coût BOM sur l’appareil. Pas de trous traversants, pas de shroud, pas de plastique. Ça rentre où un header 6-pin ne rentrerait jamais.

Ce qui vit sur ces pads : RX, TX, GND, plus EN et GPIO0. Avec ça d’exposé, peu importe ce que mon firmware fait au stack USB. Je tire GPIO0 à zéro, je pulse EN, et je flashe en UART avec esptool --port /dev/cu.usbserial-.... La prise USB-C native reste propre pour l’expérience côté utilisateur : logs, WebSerial, OTA, démos. Les pads Tag-Connect, c’est la porte d’ingénierie, utilisée seulement quand la porte d’entrée est coincée.

L’empreinte est assez petite pour que je puisse presque toujours la faire rentrer, même sur des PCB serrés à un seul bouton. La partie chère, c’est le câble, qui vit dans mon tiroir de bureau, pas sur chaque unité livrée.

Ce que je dirais à n’importe qui qui livre sur un ESP32-S3

Utilise l’USB natif. Utilise WebSerial. C’est parmi les meilleurs gains d’expérience client et de vitesse d’itération que j’ai ajoutés à mon workflow depuis des années. Mais n’en fais pas ta seule porte de retour vers la puce. Du firmware qui reconfigure le descripteur USB, particulièrement le genre de code “intelligent” que les outils d’IA sont contents de générer sans mentionner la gestion de la récupération, peut silencieusement t’enlever la capacité de reprogrammer. La carte ne te le dit pas. La puce ne te le dit pas. C’est la prochaine itération sur le banc qui te le dit.

Trois pads. Un câble. L’après-midi que tu sauves est le tien.