Illustration de l'article

De Scratch au pilotage de robot : un voyage technique et ludique

Pour laisser un commentaire

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

Illustration du robot

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'analyse initiale : tout semblait si simple...

L'enfant exprimait son souhait clairement : utiliser Scratch pour piloter son robot. Mon analyse initiale était optimiste :

  • Scratch est open source et accepte les plugins → faisabilité validée (première erreur...)
  • Le robot fonctionne par infrarouge → on peut reproduire ses commandes
  • Il suffit d'un ATtiny85 avec une LED IR et un protocole USB/serial
  • L'ATtiny85 peut gérer facilement la modulation 38kHz et l'USB (seconde erreur...)

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".

Première étape : comprendre le protocole du robot

Analyse du signal infrarouge

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.

Le problème du bruit ambiant

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 :

Schéma théorique de réception IR avec amplification et filtrage

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 !

TSOP1838 avec indication 3V sur le boîtier

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 :

Schéma d'alimentation TSOP1838 avec pont diviseur

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.

Capture et analyse du protocole

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.

Analyseur logique Saleae

Premier succès ! J'obtiens un beau signal de télécommande :

Trame infrarouge NEC

Le motif est caractéristique du protocole NEC. À noter que le signal est inversé car le TSOP1838 a une sortie active au niveau bas :

  • Préambule : 9ms HIGH + 4,5ms LOW
  • Données : 32 bits encodés par la durée des espaces
    • Bit '0' : 560μs HIGH + 560μs LOW
    • Bit '1' : 560μs HIGH + 1690μs LOW
  • Structure des 32 bits :
    • 8 bits : adresse (addr) - identifie l'appareil
    • 8 bits : adresse inversée (~addr) - vérification d'intégrité
    • 8 bits : donnée (data) - commande à exécuter
    • 8 bits : donnée inversée (~data) - vérification d'intégrité
  • Logique de vérification : Le tilde (~) représente l'inversion binaire. Si addr = 0x42, alors ~addr = 0xBD. Cette redondance permet de détecter les erreurs de transmission : si addr + ~addr ≠ 0xFF, la trame est corrompue.
  • Logique de l'adresse : L'adresse permet à plusieurs appareils de coexister sans interférences. Chaque télécommande a son adresse unique, et seul l'appareil correspondant réagit aux commandes.
  • Fin : 560μs HIGH final

Décodage automatisé

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

Mapping complet des commandes

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.

Questionnement sur l'adresse 0x00

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 !

Deuxième étape : envoyer les commandes

Génération du signal IR

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.

Schéma de modulation

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.

Implémentation sur ATtiny85

L'ATtiny85 dispose de deux timers :

  • Timer0 : 8 bits, utilisable pour la modulation PWM
  • Timer1 : 8 bits, nécessaire pour l'USB bit-banging

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...

Le piège de l'ATtiny85 : quand les ressources manquent

Le problème des timers

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 :

Module modulateur IR 38kHz

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.

Le problème du timing

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.

Changement de stratégie : passage au RP2040

Choix du nouveau microcontrôleur

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 :

RP2040-Zero
CaractéristiqueATtiny85 (Trinket 5V)RP2040 (RP2040-Zero)
Fréquence16MHz133MHz (dual-core)
Flash8KB2MB
RAM512B264KB
Timers2 Timers8 Timers + PIO
USBPas d'USB natifUSB natif
I/O6 pins26 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 !

Architecture dual-core

Le RP2040 étant dual-core, on peut séparer les tâches :

Core 0 (core principal) :

  • Gestion du contrôleur USB natif pour la communication série
  • Contrôle des strips LED WS2812B
  • Parsing des commandes et envoi des réponses

Core 1 (core dédié) :

  • Génération des signaux IR
  • Queue des commandes robot

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);
}

Implémentation des strips LED

Les strips WS2812B utilisent un protocole série propriétaire :

  • 1 bit = 1,25μs (800kHz)
  • Format : 24 bits GRB par LED (8 bits par composante de couleur)
  • Validation : >50μs de silence (déclenchement de l'affichage)

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 :

  • Désactiver les interruptions pendant la transmission
  • Utiliser des timers précis ou calculer la durée des instructions pour faire des boucles d'attente, ce qui est fastidieux
  • Écrire de l'assembleur optimisé
  • Gérer le multiplexage des séquences si le processeur gère plusieurs protocoles en parallèle

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 :

  • Son propre séquenceur cadencé jusqu'à 133MHz
  • 4 programmes simultanés (32 instructions max chacun)
  • Interface directe avec les pins GPIO
  • FIFOs de communication avec le CPU

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);
}

Génération IR sur RP2040

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étails de sécurité

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).

Le défi de l'intégration Scratch

Le problème de l'accès série

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.

Architecture pont WebSocket

Après recherche, les autres logiciels (mBlock, ArduinoBlocks...) utilisent une architecture en passerelle :

Architecture en passerelle

Le pont natif :

  • Écoute sur un port WebSocket (ex: ws://localhost:8765)
  • Traduit les commandes WebSocket en commandes série
  • Retourne les réponses du microcontrôleur

Tentative en Go puis passage à Swift

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 ! 😊

Implémentation du pont série Swift

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 !

Interface de la passerelle Swift

Le problème des extensions Scratch officielles

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 :

  • Modification du code source de Scratch
  • Recompilation complète
  • Déploiement d'une version personnalisée

C'est un processus lourd, inadapté à un projet personnel.

Solution : TurboWrap

Heureusement, il existe TurboWrap, un fork communautaire de Scratch qui ajoute des fonctionnalités avancées, notamment :

  • Chargement d'extensions tierces via URL
  • API WebSocket native (puisque c'est une application Electron)
  • Compatibilité totale avec les projets Scratch

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.

Développement de l'extension

TurboWrap avec l'extension robot et LED

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.

Mission accomplie !

Au final, l'enfant dispose maintenant d'un système complet lui permettant de :

  • Programmer visuellement avec des blocs Scratch
  • Contrôler son robot (mouvements, séquences)
  • Créer des animations LED synchronisées
  • Apprendre les concepts de programmation de manière ludique

Le détour technique était nécessaire, mais le résultat en vaut la peine !

Ressources techniques détaillées

thierry-f-78/robot