Tout commence par une demande simple de mon fils de 8 ans :
J’ai eu un robot a Noel je jouai avec et j’ai eu une idée piloté le robot sur l’ordinateur avec scratch
Parce que je voulais le piloter avec scratch
Une demande qui semblait triviale au départ s'est transformée en une aventure technique passionnante, jalonnée de découvertes, d'erreurs et d'apprentissages. Ce projet illustre parfaitement comment l'informatique embarquée révèle ses complexités couche après couche, et comment chaque solution technique engendre de nouveaux défis.
Voici l'histoire de ce projet, racontée avec ses péripéties techniques et ses apprentissages.
L'enfant exprimait son souhait clairement : utiliser Scratch pour piloter son robot. Mon analyse initiale était optimiste :
Cette analyse révèle un biais classique en informatique : celui de sous-estimer la complexité des interactions entre composants. Chaque élément pris individuellement semble simple, mais leur intégration révèle des contraintes cachées.
Comme souvent en informatique, la réalité allait se charger de rappeler que "c'est toujours plus compliqué que prévu".
Le robot se pilote manifestement par infrarouge. Pour comprendre le signal transmis, j'installe une LED réceptrice infrarouge. Mais première surprise : les néons du bureau créent un bruit parasite à 100Hz qui perturbe la réception.
Les tubes fluorescents et néons, alimentés par le secteur 50Hz, voient leur luminosité fluctuer à 100Hz (deux alternances par cycle). Cette modulation lumineuse constante se traduit par un signal parasite permanent sur la photodiode IR. Le signal utile de la télécommande (quelques millisecondes) se retrouve noyé dans ce bruit de fond.
Solution théorique : un filtre passe-haut pour éliminer les basses fréquences (< 1kHz) tout en préservant le signal IR (> 30kHz). Mais problème : le filtre va atténuer le signal déjà faible de la télécommande. Il faut donc aussi un amplificateur.
Chaîne de traitement nécessaire :
Cette approche analogique s'annonce complexe : choix des AOP, calcul des filtres, réglage des gains... Et si il existait une solution intégrée ?
Je recherche donc des composants dans mon stock, et je tombe sur un récepteur IR tout fait. Avec un peu de chance il sera compatible avec la télécommande du robot. Il s'agit d'un récepteur démodulateur infrarouge TSOP1838.
Ce petit composant contient déjà tout le nécessaire pour transformer le signal IR modulé en beau signal numérique exploitable, sans aucun composant externe. Plus besoin d'amplificateurs, de filtres ou de calculs : on branche directement la sortie sur l'analyseur logique et ça fonctionne !
Situation intéressante : bien que la datasheet officielle indique une alimentation 2.5V à 5.5V, l'exemplaire en stock porte clairement la mention "3V" sur son boîtier. Plutôt que de risquer de griller le seul composant disponible, adoptons une approche prudente et alimentons-le en 3V.
Le TSOP1838 consommant seulement 5mA, un pont diviseur de tension reste une solution viable pour les tests. Avec un courant si faible, l'influence sur la tension de sortie sera négligeable. Solution pragmatique :
Le courant circulant dans le diviseur de tension est d'environ 1mA (5V / 4,7kΩ). Selon la datasheet du TSOP1838, sa consommation maximale est de 5mA. Malgré cette différence, l'impact sur la tension de sortie reste négligeable car généralement ces composants ont une résistance interne de l'ordre du MΩ, aussi l'impact sera faible. En parallèle avec la résistance de 4,7kΩ, la charge du TSOP ne modifie pratiquement pas le point de fonctionnement du pont diviseur.
Pour le test, on se moque de la consommation de courant excessive due au pont diviseur et on se moque de la précision et de la stabilité. L'objectif est simplement de valider que le composant fonctionne avec la télécommande du robot.
La mesure au multimètre confirme bien 3V en sortie du pont. Le TSOP peut ainsi fonctionner dans des conditions sûres, et si cela s'avère insuffisant, on pourra toujours ajuster vers 5V par la suite. Dans le contexte de ce projet (pas de contrainte de stabilité long terme), cette approche est parfaitement justifiée.
J'utilise un analyseur logique Saleae pour capturer les trames IR. Cet outil très bon marché fait bien le travail pour des fréquences autour de 10MHz. Il ne faut pas compter sur le sampling à 24MHz, cela fonctionne mal, et le soft côté PC n'est pas top, mais il fait le boulot. Pour un produit à 6€, on peut tolérer ces points. Vu le prix et le positionnement de Saleae, je me demande si le produit est bien une Saleae ou simplement un design lowcost capable d'utiliser le soft gratuit de Saleae.
Premier succès ! J'obtiens un beau signal de télécommande :
Le motif est caractéristique du protocole NEC. À noter que le signal est inversé car le TSOP1838 a une sortie active au niveau bas :
Je capture toutes les commandes du robot avec un analyseur logique Saleae, exporte les données en CSV, et développe un programme Go pour décoder automatiquement les trames :
// Décodage d'un bit basé sur la durée de l'espace
if s.duration >= 1300 && s.duration < 1700 {
bits = append(bits, 1)
} else {
bits = append(bits, 0)
}
// Extraction des octets significatifs
b1 := decode(bits[0:8]) // Adresse
b3 := decode(bits[16:24]) // Commande
Test du décodeur :
go run decode.go ../capture-ir/move-right.csv
00 08
Résultat : j'obtiens tous les codes de commande du robot :
demo: 00 16
move-back: 00 44
move-forward: 00 47
move-left: 00 1c
move-right: 00 08
program: 00 09
step-back: 00 15
step-forward: 00 46
step-left: 00 5e
step-right: 00 45
Mission accomplie pour cette première étape ! Le protocole est maintenant parfaitement compris.
L'adresse 0x00
du robot interpelle : si l'adressage sert à éviter les conflits entre appareils, pourquoi utiliser zéro, qui est une valeur remarquable ? En réalité, avec seulement 256 valeurs possibles (8 bits), il serait illusoire de distribuer des adresses uniques par construction ou matériel à l'échelle mondiale. La méthode semble plutôt consister à "espérer" qu'il n'y ait pas de conflit entre devices dans un même environnement.
Cette limitation a d'ailleurs poussé vers un mode NEC étendu qui passe l'adressage à 16 bits offrant 65 536 valeurs, mais au prix d'une complexité accrue. Par ailleurs, les appareils grand public ont largement abandonné ce protocole au profit de solutions plus modernes. Vu sa simplicité, le NEC reste cependant actif pour les appareils bas de gamme et le DIY - exactement le cas de notre robot jouet !
Maintenant, il faut envoyer ces commandes au robot. Le principe semble simple : une LED IR sur un ATtiny85 devrait suffire. Mais le signal doit être modulé à 38kHz pour être reconnu par les récepteurs IR.
Pourquoi moduler ? Sans modulation, une LED IR qui s'allume et s'éteint lentement (quelques kHz) serait noyée dans le bruit ambiant : lumière du soleil, néons, ampoules... Tous ces éclairages contiennent de l'infrarouge qui perturbe la réception. La modulation à 38kHz permet au récepteur de filtrer ce bruit et de ne détecter que les signaux utiles.
La modulation 38kHz signifie que quand le protocole NEC demande un niveau HIGH, on génère en réalité une porteuse de 38kHz (26,3μs de période). Quand il demande un niveau LOW, on n'émet rien. Sur le schéma ci-dessus, le signal violet correspond au signal retourné par le démodulateur TSOP, tandis que le signal bleu représente le signal modulé 38kHz en sortie de l'ATtiny.
L'ATtiny85 dispose de deux timers :
Configuration PWM pour 38kHz sur Timer0 :
// Configuration Timer0 en Fast PWM
TCCR0A = (1 << WGM01) | (1 << WGM00) | (1 << COM0A1);
TCCR0B = (1 << CS00); // Prescaler = 1
OCR0A = 210; // TOP pour ~38kHz à 16MHz
Premier test : le système fonctionne ! Le robot avance. Mais il ne reste plus qu'à ajouter une communication USB/serial pour piloter le robot depuis un PC...
Et c'est là que les choses se compliquent... L'USB sur l'ATtiny85 utilise du "bit-banging" logiciel avec la bibliothèque V-USB qui implémente le protocole USB Low Speed. Ce protocole impose des contraintes strictes : trames de synchronisation toutes les 10ms maximum. Problème : une trame IR complète dure environ 67ms selon le protocole NEC. Ce télescopage temporel rend impossible l'émission d'une séquence IR sans interrompre la communication USB. Face à ce problème de séquençage, je décide de passer en mode IRQ pour traiter le signal IR 38kHz. L'idée : les commutations du signal IR se font en interruption, ce qui permet à l'USB de fonctionner dans le programme principal.
Première tentative : un seul timer à 2×38kHz (76kHz) qui s'occupe de tout le signal, modulation comprise. Résultat : l'IR fonctionne, mais pas l'USB qui décroche. Hypothèse : l'USB est sans cesse interrompu et cela perturbe la transmission du signal.
Seconde tentative : deux timers distincts. Un PWM pour la sortie à 38kHz, et un timer à un multiple de 560μs pour le séquencement du signal. On remarque que 9ms, 4,5ms et 1,6ms sont à peu près des multiples de 560μs. Résultat : l'IR fonctionne, mais l'USB ne fonctionne pas du tout. Le device s'annonce sur le PC et disparaît aussitôt.
À l'époque, j'ai conclu à tort que V-USB nécessitait un timer dédié. Rétrospectivement, cette analyse était probablement incorrecte, car l'examen du code source de V-USB ne révèle pas de configuration spécifique de timer.
Cette mauvaise analyse me dicte de libérer un timer, je choisi de libérer le modulateur. Je trouve sur Internet un modulateur 38kHz externe qui devrait libérer un timer :
Ironiquement, ce module coûte le prix d'un ATtiny85 (2€) pour un pauvre 555... Mais bon, je ne lance pas une production en série !
Une fois le modulateur externe reçu et intégré au montage, je teste à nouveau le système avec espoir. Malheureusement, la libération du timer ne règle rien du tout, et le système continue de ne pas fonctionner. Le problème n'était donc pas lié aux ressources matérielles mais bien aux contraintes de timing incompatibles. Le lecteur attentif aura sûrement remarqué que j'aurais pu tester cette hypothèse sans acheter le modulateur, en désactivant simplement la modulation dans le code... Mais un clic sur pour acheter est tellement plus rapide qu'une minute de réflexion.
L'USB Low Speed fonctionne à 1,5MHz, soit une période de 666ns avec une tolérance de ±1,5% (10ns). Sur un ATtiny85 à 16MHz, chaque cycle d'horloge dure 62,5ns. La plupart des instructions prennent 1 à 2 cycles (moyenne ~1,5 cycle par instruction). Une interruption à 76kHz (période 13μs) qui exécute plusieurs instructions peut facilement faire dériver le timing USB au-delà des ±10ns tolérés. À noter que la tolérance de dérive dans le signal (10ns) est plus faible que la durée d'exécution d'une seule instruction (~94ns). Le bit-banging USB nécessite une synchronisation parfaite que les interruptions périodiques perturbent constamment.
Avec du recul, j'aurais dû essayer une approche inversée : laisser la gestion du signal IR en programme principal et gérer l'USB par interruption. Cela aurait sûrement mieux fonctionné car le signal NEC IR a 10% de tolérance sur ses timers, soit ±56μs. Un polling USB pour vérifier qu'il n'y a pas de données dure ~34μs, largement compatible avec les tolérances IR. D'ailleurs pour éviter les perturbations USB, j'aurais pu utiliser un compteur hardware pour mesurer le tempo qui n'aurait pas été impacté par les IRQ et faire une attente active sur ce compteur.
Le bon fonctionnement reste toutefois aléatoire puisque cela implique que l'host n'envoie pas de données durant l'envoi d'une trame IR (67ms). Ce n'est tout de même pas un gage de fiabilité.
Conclusion : l'ATtiny85, malgré sa simplicité apparente, révèle ses limites dès qu'on sort des cas d'usage basiques. Il faut changer d'approche.
Direction internet pour trouver un processeur bon marché qui supporte l'USB nativement. Mon fils, curieux de mes recherches, regarde par-dessus mon épaule et découvre les strips de LED colorées dans les suggestions. Ses yeux s'illuminent : "Papa, on peut aussi piloter ça avec Scratch ?"
J'aurais pu lui expliquer que le cahier des charges était figé, mais j'ai simplement dit "ok" - c'était plus simple ! J'en profite donc pour commander deux strips de LED WS2812B, dont un organisé en matrice.
Après quelques recherches, le Raspberry Pi RP2040 (RP2040-Zero) s'impose :
Caractéristique | ATtiny85 (Trinket 5V) | RP2040 (RP2040-Zero) |
---|---|---|
Fréquence | 16MHz | 133MHz (dual-core) |
Flash | 8KB | 2MB |
RAM | 512B | 264KB |
Timers | 2 Timers | 8 Timers + PIO |
USB | Pas d'USB natif | USB natif |
I/O | 6 pins | 26 pins |
Prix | ~1-2€ | ~2-4€ |
De formation électronicien et habitué aux contraintes des logiciels embarqués en C, je trouve que le RP2040 représente une débauche de puissance pour ce que nous voulons faire. Notre projet utilise moins de 1% des ressources disponibles : quelques ko de code, 2 pins seulement, aucun timer nécessaire, quelques octets de RAM... En temps normal, j'aurais optimisé pour économiser chaque ressource. Mais parfois, il vaut mieux accepter le gaspillage plutôt que de se battre contre des incompatibilités fondamentales.
C'est clairement surdimensionné pour une LED IR, mais cela résout tous les problèmes d'un coup !
Le RP2040 étant dual-core, on peut séparer les tâches :
Core 0 (core principal) :
Core 1 (core dédié) :
Communication inter-core via FIFO :
// Core 0 → Core 1
rp2040.fifo.push(ROBOT_COMMAND_FORWARD);
// Core 1
if (rp2040.fifo.available()) {
int cmd = rp2040.fifo.pop();
sendNECCommand(robotCommands[cmd].address,
robotCommands[cmd].command);
}
Les strips WS2812B utilisent un protocole série propriétaire :
Heureusement, le RP2040 dispose de blocs PIO (Programmable I/O) qui facilitent grandement la gestion des protocoles à contraintes temporelles strictes.
Le problème traditionnel : Les protocoles comme WS2812B exigent des timings microseconde précis (±150ns). Sur un microcontrôleur classique, le programmeur doit :
La solution PIO : Le RP2040 intègre 8 machines d'état programmables indépendantes qui fonctionnent en parallèle du CPU principal. Chaque PIO dispose de :
Le développeur écrit un petit programme PIO en assembleur spécialisé. Ce jeu d'instructions est très restreint, voici la liste exhaustive : JMP, WAIT, IN, OUT, PUSH, PULL, MOV, IRQ, SET. Ce programme gère automatiquement les timings critiques. Une fois lancé, le PIO fonctionne de manière totalement autonome - plus besoin de désactiver les interruptions ou de surveiller les cycles !
Pour WS2812B concrètement : La bibliothèque Adafruit configure un PIO pour générer automatiquement les signaux 800kHz avec les bons rapports cycliques. Le CPU se contente d'alimenter le PIO avec les données couleur via FIFO, sans se soucier du timing.
#include <Adafruit_NeoPixel.h>
Adafruit_NeoPixel strip(NUM_LEDS, LED_PIN, NEO_GRB + NEO_KHZ800);
// Contrôle de luminosité (1/8 de la puissance max)
#define DIVIDE_COLOR 8
uint32_t limitedColor(uint8_t r, uint8_t g, uint8_t b) {
return strip.Color(r/DIVIDE_COLOR, g/DIVIDE_COLOR, b/DIVIDE_COLOR);
}
Le RP2040 génère facilement la modulation 38kHz avec ses PWM hardware, ou mieux encore avec ses blocs PIO dédiés. Mais au moment où j'ai développé ce code, je n'avais pas encore pris connaissance de l'usage des PIO. J'avais aussi mon module externe sous la main - autant l'utiliser ! Cela simplifie le code :
void sendNECBit(bool bit) {
digitalWrite(IR_LED_PIN, HIGH); // Pulse de 560μs
delayMicroseconds(NEC_BIT_PULSE);
digitalWrite(IR_LED_PIN, LOW);
if (bit) {
delayMicroseconds(NEC_BIT_1_SPACE); // 1690μs pour '1'
} else {
delayMicroseconds(NEC_BIT_0_SPACE); // 560μs pour '0'
}
}
Le code final se trouve dans robot-rp2040/robot.ino
.
Détail important : la luminosité du strip est volontairement limitée à 1/8 de sa capacité maximale. À pleine puissance, ces LED WS2812B sont très agressives pour les yeux et causent une fatigue oculaire importante. La limitation logicielle évite aussi les problèmes de consommation excessive (potentiellement plusieurs ampères pour 96 LED).
Il ne reste plus qu'à intégrer tout ça à Scratch. Facile ! Bah non... Scratch 3.0 est une application Electron qui s'exécute dans un sandbox sécurisé. Pour des raisons de sécurité, elle n'a pas accès au port série du système.
Après recherche, les autres logiciels (mBlock, ArduinoBlocks...) utilisent une architecture en passerelle :
Le pont natif :
ws://localhost:8765
)J'écris d'abord un programme en Go. Mais Go n'est pas très bien adapté au Mac car il n'y a pas d'API native pour les interfaces graphiques de macOS. Or pour un enfant, je ne voulais pas qu'il lance le programme en console.
Chez Apple, on fait du Swift et on n'a pas vraiment la place pour le reste. Je me résigne donc à refaire le logiciel en Swift. Ne connaissant rien au Swift et n'ayant pas envie d'apprendre... merci Claude Code ! 😊
Tout fonctionne parfaitement sur mon poste en mode développement, et je n'ai aucune idée de comment on déclare une fonction en Swift ! Top.
Je builds en mode release, et... ça ne fonctionne plus. Chouette, je vais donc devoir comprendre ce qui se passe.
À priori, un problème d'optimisations. Une fois la compilation faite en mode release, donc avec des optimisations (équivalent de -O2 pour gcc), le port série ne fonctionne plus.
Je vois des caractères parasites dans les échanges comme si on avait un problème de timing sur du RS232 hardware. À ma connaissance, en USB, on ne devrait pas avoir ces problèmes car les vitesses d'échange réelles sont gérées par USB et ce n'est pas vraiment du 115200 bauds.
Je ne sais pas trop comment fonctionne la mécanique du protocole. Je cherche sur internet et je vois que je ne suis pas seul dans ce cas.
Manifestement, Swift gère très mal les communications sur les devices de type /dev/cu.usbserial-*
ou /dev/cu.usbmodem-*
... Les recommandations sont d'utiliser une bibliothèque écrite en C. Claude Code a fait ça tout seul, et maintenant le programme fonctionne !
Ah zut, ce n'est pas fini ! Il faut maintenant que Scratch sache communiquer avec le pont. Comment fait-on une extension Scratch ?
Réponse décevante : les extensions fait maison pour le Scratch officiel nécessitent :
C'est un processus lourd, inadapté à un projet personnel.
Heureusement, il existe TurboWrap, un fork communautaire de Scratch qui ajoute des fonctionnalités avancées, notamment :
On peut donc charger une extension directement depuis une URL, il faudra juste faire en sorte que la passerelle expose une URL avec les données nécessaires pour faire fonctionner le robot et le strip.
Ne connaissant rien au développement d'extensions Scratch et n'ayant pas envie de me plonger dans cette API... encore merci Claude Code ! L'extension JavaScript gère les blocs visuels et la communication WebSocket avec le pont série.
Au final, l'enfant dispose maintenant d'un système complet lui permettant de :
Le détour technique était nécessaire, mais le résultat en vaut la peine !