Robot pendule inversé — du PID réglé à la main au sim-to-realInverted Pendulum Robot — From Hand-Tuned PID to Sim-to-Real
TechnologiesTechnologies (9)
Deux chapitres sur le même robot. D'abord une régulation en cascade à 200 Hz sur ESP32, réglée à la main. Puis une politique entraînée par renforcement dans MuJoCo et embarquée sur la carte : 1 634 poids, 250 µs d'inférence sur un budget de 5 ms. Le premier transfert a échoué parce que la simulation était fausse. Identification du vrai moteur par interspectres, sans jamais démonter le robot, puis validation à 0,6 % sur la commande.Two chapters on the same robot. First a hand-tuned 200 Hz cascade controller on an ESP32. Then a policy trained with reinforcement learning in MuJoCo and flashed onto the board: 1,634 weights, 250 µs inference on a 5 ms budget. The first transfer failed because the simulation was wrong. Identification of the real motor by cross-spectra, without ever taking the robot apart, then validation within 0.6 % on the wheel command.
Lire la présentation complèteRead the full introduction
Deux chapitres sur le même robot. D'abord une régulation en cascade à 200 Hz sur ESP32, réglée à la main. Puis une politique entraînée par renforcement dans MuJoCo et embarquée sur la carte : 1 634 poids, 250 µs d'inférence sur un budget de 5 ms. Le premier transfert a échoué parce que la simulation était fausse. Identification du vrai moteur par interspectres, sans jamais démonter le robot, puis validation à 0,6 % sur la commande.Two chapters on the same robot. First a hand-tuned 200 Hz cascade controller on an ESP32. Then a policy trained with reinforcement learning in MuJoCo and flashed onto the board: 1,634 weights, 250 µs inference on a 5 ms budget. The first transfer failed because the simulation was wrong. Identification of the real motor by cross-spectra, without ever taking the robot apart, then validation within 0.6 % on the wheel command.
Parcourir les sectionsBrowse sections +
En brefAt a glance
En bref
Le robot — Un pendule inversé sur deux roues, en équilibre instable : il doit corriger sa position deux cents fois par seconde pour ne pas tomber. Châssis conçu en CAO et imprimé en 3D, ESP32, centrale inertielle MPU-6050, deux moteurs pas-à-pas. Environ 1 kg.
Chapitre 1 — Une régulation en cascade à 200 Hz, réglée à la main. La première architecture s'est révélée structurellement instable, démontrable au calcul plutôt qu'au réglage. Les redémarrages aléatoires venaient d'un bug dans le pilote I2C d'Espressif, remonté par résolution de l'adresse d'exception. Erreur d'angle ramenée à 0,064° d'écart-type.
Chapitre 2 — Le même robot piloté par un réseau de neurones entraîné par apprentissage par renforcement dans MuJoCo, puis exporté en C et embarqué : 1 634 poids, 250 µs d'inférence sur un budget de 5 ms. En simulation l'agent battait la cascade ; sur le robot réel il tremblait dix fois plus. Rien n'était cassé : la simulation était fausse.
Ce que ça a coûté, et ce que ça a appris — J'ai mesuré le vrai moteur à travers les capteurs du robot, sans jamais le démonter. Sa résonance est à 71 Hz, pas aux 113 Hz supposés d'après la documentation. Remis dans la simulation corrigée, l'ancien agent a reproduit le défaut que l'ancienne simulation cachait : 0,6 % d'écart sur la commande. C'est ce jour-là que la simulation a cessé de mentir.
At a glance
The robot — A two-wheeled inverted pendulum in unstable equilibrium: it must correct itself two hundred times per second or fall over. CAD-designed, 3D-printed chassis, ESP32, MPU-6050 inertial unit, two stepper motors. Around 1 kg.
Chapter 1 — A hand-tuned 200 Hz cascade controller. The first architecture turned out to be structurally unstable, provably so on paper rather than by tuning. The random reboots came from a bug inside Espressif's own I2C driver, traced by resolving the exception address. Angle error brought down to 0.064° standard deviation.
Chapter 2 — The same robot driven by a neural network trained with reinforcement learning in MuJoCo, then exported to C and flashed onto the board: 1,634 weights, 250 µs inference on a 5 ms budget. In simulation the agent beat the cascade; on the real robot it shook ten times harder. Nothing was broken: the simulation was wrong.
What it cost, and what it taught — I measured the real motor through the robot's own sensors, without ever taking it apart. Its resonance sits at 71 Hz, not the 113 Hz assumed from the datasheet. Put back into the corrected simulation, the old agent reproduced the very defect the old simulation had hidden: within 0.6 % on the wheel command. That is the day the simulation stopped lying to me.
Le montageThe build
Le montage
Matériel — ESP32 (Arduino core 3.3.2) · centrale inertielle MPU-6050 sur bus I2C à 100 kHz · deux moteurs pas-à-pas NEMA 17 (17HS3401) pilotés par drivers A4988 · roues de 65 mm · pack 18650. Châssis conçu en CAO et imprimé en 3D. Masse totale environ 1 kg.
Logiciel — C++ embarqué, environ 1 000 lignes, sans aucune bibliothèque de régulation externe. Boucle de commande à 200 Hz, politique de contrôle évaluée à 100 Hz. L'estimation d'angle fusionne accéléromètre et gyroscope par filtre complémentaire, cadencée par l'interruption matérielle du capteur.
The build
Hardware — ESP32 (Arduino core 3.3.2) · MPU-6050 inertial unit on a 100 kHz I2C bus · two NEMA 17 stepper motors (17HS3401) driven by A4988 drivers · 65 mm wheels · 18650 battery pack. CAD-designed, 3D-printed chassis. Total mass around 1 kg.
Software — Embedded C++, roughly 1,000 lines, with no external control library. Control loop at 200 Hz, control policy evaluated at 100 Hz. Angle estimation fuses accelerometer and gyroscope through a complementary filter, clocked by the sensor's hardware interrupt.
La régulation en cascade, réglée à la mainThe hand-tuned cascade controller
La régulation en cascade, réglée à la main
Première version du robot : quatre boucles imbriquées, réglées à la main, sans apprentissage. C'est le chapitre qui a servi de référence à tout le reste — sans un correcteur classique qui marche et dont les chiffres sont connus, il n'y aurait eu aucun moyen de dire si l'agent entraîné faisait mieux ou simplement autrement.
The hand-tuned cascade controller
The robot's first version: four nested loops, tuned by hand, no learning involved. This chapter became the reference for everything that followed. Without a classical controller that works and whose numbers are known, there would have been no way to tell whether the trained agent did better or merely differently.
Le point de départ : une architecture qui ne pouvait pas fonctionnerThe starting point: an architecture that could not work
Le point de départ : une architecture qui ne pouvait pas fonctionner
La première version commandait une vitesse de roue proportionnelle à l'inclinaison. Elle oscillait sans jamais se stabiliser. Le réflexe aurait été de continuer à triturer les gains — le calcul a montré que c'était inutile.
L'équation du pendule inversé s'écrit θ̈ = (g/L)·sin θ − (a/L)·cos θ. C'est l'accélération du point d'appui qui redresse le corps, pas sa vitesse.
En imposant v = Kp·θ + Kd·θ̇, l'accélération réelle devient a = Kp·θ̇ + Kd·θ̈ : la commande n'apporte plus aucun terme en θ. Le coefficient de θ reste donc −g, hors d'atteinte des gains : l'équation caractéristique s'écrit (L+Kd)s² + Kp·s − g = 0, dont le produit des racines vaut −g/(L+Kd) < 0. Il y a donc toujours une racine réelle positive, quelles que soient les valeurs des gains — le système est structurellement instable, aucun réglage ne pouvait le sauver.
Ce qui donnait l'illusion que ça marchait un peu, c'était la limite d'accélération du moteur, qui recréait accidentellement le bon comportement.
Correction — la sortie du régulateur devient une accélération, et la vitesse des roues en est l'intégrale.
The starting point: an architecture that could not work
The first version commanded wheel velocity proportional to tilt. It oscillated without ever settling. The reflex would have been to keep fiddling with gains — the maths showed that was pointless.
The inverted pendulum equation reads θ̈ = (g/L)·sin θ − (a/L)·cos θ. It is the acceleration of the support point that rights the body, not its velocity.
Imposing v = Kp·θ + Kd·θ̇ makes the real acceleration a = Kp·θ̇ + Kd·θ̈: the control law contributes no term in θ at all. The coefficient of θ therefore stays at −g, beyond the reach of the gains: the characteristic equation reads (L+Kd)s² + Kp·s − g = 0, whose product of roots is −g/(L+Kd) < 0. There is thus always a positive real root, whatever the gain values — the system is structurally unstable, and no amount of tuning could have saved it.
What gave the illusion it half-worked was the motor's acceleration limit, which accidentally recreated the right behaviour.
Fix — the controller's output becomes an acceleration, and wheel velocity is its integral.
L'architecture retenueThe architecture
L'architecture retenue
Quatre boucles imbriquées, chacune nettement plus lente que celle qu'elle contient (5 ms puis 25 ms, soit un facteur 5) — règle de conception pour qu'elles coopèrent au lieu de se battre.
Microsecondes · timer matériel — génère les impulsions de pas, en tâche de fond.
5 ms · boucle d'angle — empêche la chute : angle → accélération des roues.
25 ms · boucle de position — empêche la dérive : agit en déplaçant l'angle cible.
Dizaines de secondes · auto-calibration — apprend le point d'équilibre réel.
Le point non évident : la boucle de position ne commande jamais les moteurs. Son seul levier est de déplacer la consigne d'angle — elle pilote la position à travers l'équilibre.
The architecture
Four nested loops, each markedly slower than the one it contains (5 ms then 25 ms, a factor of 5) — a design rule that keeps them cooperating rather than fighting.
Microseconds · hardware timer — generates the step pulses in the background.
5 ms · angle loop — prevents falling: angle → wheel acceleration.
25 ms · position loop — prevents drift: acts by shifting the target angle.
Tens of seconds · self-calibration — learns the real equilibrium point.
The non-obvious part: the position loop never commands the motors. Its only lever is to move the angle setpoint — it steers position through the equilibrium.
Résultats mesurésMeasured results
Résultats mesurés
Erreur d'angle (écart-type) — 0,293° → 0,064°
Bruit vu par le gyroscope — 3,48 °/s → 1,08 °/s
Échecs de lecture capteur — 2,0 /s → 0
Plantages logiciels — 5 par heure → 0 sur 40 min de test
Maintien en position — dérive continue → bande de quelques millimètres
Les gains ont été trouvés par balayage systématique, une variable à la fois, avec mesure de l'écart-type à chaque valeur.
Measured results
Angle error (std. dev.) — 0.293° → 0.064°
Noise seen by the gyroscope — 3.48 °/s → 1.08 °/s
Sensor read failures — 2.0 /s → 0
Software crashes — 5 per hour → 0 over 40 min of testing
Position hold — continuous drift → a band of a few millimetres
Gains were found by systematic sweep, one variable at a time, measuring the standard deviation at each value.
Enquête 1 · Un plantage dans le pilote du fabricantInvestigation 1 · A crash inside the vendor's driver
Enquête 1 · Un plantage dans le pilote du fabricant
Des redémarrages aléatoires, sans message. La cause de reset imprimée par la puce distinguait deux familles de pannes ; les plantages logiciels montraient toujours la même adresse d'exception sur trois binaires différents — donc un bug déterministe, pas du hasard.
Résolution de l'adresse avec addr2line sur le fichier ELF :
i2c_ll_read_rxfifo → i2c_isr_receive_handler → i2c_master_isr_handler_default
Une écriture dans un tampon nul, à l'intérieur de la routine d'interruption du pilote I2C d'Espressif. Trois mitigations ont supprimé l'exposition — dont une faute personnelle : des appels au pilote timer effectués deux cents fois par seconde à l'intérieur d'une section critique, interruptions coupées, ce qui allongeait la latence et faisait déraper la machine à états du bus.
Résultat : zéro plantage sur les 40 minutes de test suivantes.
Investigation 1 · A crash inside the vendor's driver
Random reboots, with no message. The reset cause printed by the chip separated two families of failures; the software crashes always showed the same exception address across three different binaries — a deterministic bug, not chance.
Resolving the address with addr2line on the ELF file:
i2c_ll_read_rxfifo → i2c_isr_receive_handler → i2c_master_isr_handler_default
A write to a null buffer, inside the interrupt service routine of Espressif's I2C driver. Three mitigations removed the exposure — including one fault of my own: timer-driver calls made two hundred times per second inside a critical section with interrupts disabled, which stretched latency and derailed the bus state machine.
Result: zero crashes over the following 40 minutes of testing.
Enquête 2 · Un mode propre entretenu par mon propre filtreInvestigation 2 · A mode sustained by my own filter
Enquête 2 · Un mode propre entretenu par mon propre filtre
Le robot oscillait à une fréquence nette, mesurable sur les passages par zéro de l'erreur : 1,35 Hz. Une oscillation aussi régulière n'est pas du bruit — c'est un mode que quelque chose entretient.
Première tentation : en déduire la géométrie du robot, via f = √(g/L)/2π. Elle ne tient pas. Avec une commande en accélération, l'équation caractéristique de la boucle fermée s'écrit L·s² + Kd·s + (Kp − g) = 0, de pulsation propre √((Kp − g)/L) : la fréquence observée dépend des gains, et ne coïnciderait avec √(g/L) que pour Kp = 2g. Elle ne mesure donc pas L. Il faudrait pour cela un essai en pendule libre, régulateur coupé.
Ce que la mesure établit en revanche a suffi : le filtre de ma boucle externe coupait à 1,06 Hz, soit juste dans la bande de cette oscillation. Au lieu de rejeter le mode, il l'entretenait. Déplacer la coupure l'a supprimé.
La leçon est moins spectaculaire qu'une géométrie déduite à distance, mais plus solide : une fréquence bien mesurée suffit à identifier un coupable, même quand elle ne suffit pas à identifier un paramètre physique.
Investigation 2 · A mode sustained by my own filter
The robot oscillated at a clean, measurable frequency on the error's zero crossings: 1.35 Hz. An oscillation that regular isn't noise — it's a mode something is sustaining.
The tempting move is to infer the robot's geometry from it, via f = √(g/L)/2π. That doesn't hold. With the controller commanding acceleration, the closed-loop characteristic equation reads L·s² + Kd·s + (Kp − g) = 0, with natural frequency √((Kp − g)/L): the observed frequency depends on the gains, and would coincide with √(g/L) only for Kp = 2g. So it does not measure L. That would take a free-pendulum test, controller switched off.
What the measurement did establish was enough: my outer loop's filter cut off at 1.06 Hz, right inside the band of that oscillation. Instead of rejecting the mode, it was sustaining it. Moving the cutoff removed it.
The lesson is less spectacular than deducing geometry at a distance, but it holds: a well-measured frequency is enough to identify a culprit, even when it is not enough to identify a physical parameter.
Enquête 3 · Une expérience contrôlée pour isoler une panne matérielleInvestigation 3 · A controlled experiment to isolate a hardware fault
Enquête 3 · Une expérience contrôlée pour isoler une panne matérielle
La carte mourait par intermittence, sans laisser de trace. Plutôt que de changer des composants au hasard : deux phases de 180 secondes, même code, même montage — seule la commande moteur change.
Moteurs à l'arrêt — 180 s sans une seule interruption.
Moteurs à 1200 pas/s — mort au bout de 17 secondes.
La cause exacte a été tranchée par une observation sur vidéo : après la panne, les roues continuent de tourner à la dernière vitesse connue. Si l'interruption du timer bat encore, la puce est alimentée et ses interruptions sont servies — ce n'est donc ni une coupure d'alimentation, ni un reset. C'est un blocage de la tâche principale, pendant que le reste du système continue.
Ce détail a réinterprété toutes les données précédentes : les redémarrages enregistrés étaient les recouvrements manuels, pas la panne.
La réponse d'ingénierie a été de rendre la panne inoffensive plutôt que d'attendre d'en éliminer la cause : un homme mort dans la routine d'interruption, qui arrête les moteurs si la boucle de commande cesse de donner signe de vie, et un chien de garde matériel qui redémarre la carte au bout de deux secondes de blocage. Le robot se relève alors tout seul — et le message de cause au redémarrage suivant confirmera le diagnostic.
Investigation 3 · A controlled experiment to isolate a hardware fault
The board died intermittently, leaving no trace. Rather than swapping components at random: two 180-second phases, same code, same wiring — only the motor command changes.
Motors idle — 180 s without a single interruption.
Motors at 1200 steps/s — dead after 17 seconds.
The exact cause was settled by an observation on video: after the failure, the wheels keep turning at the last known speed. If the timer interrupt is still firing, the chip is powered and its interrupts are being served — so this is neither a power cut nor a reset. It is a hang of the main task, while the rest of the system carries on.
That detail reinterpreted all the earlier data: the reboots on record were the manual recoveries, not the failure itself.
The engineering answer was to make the failure harmless rather than wait to eliminate its cause: a deadman inside the interrupt service routine, which stops the motors if the control loop stops signalling life, and a hardware watchdog that restarts the board after two seconds of hang. The robot then gets back up on its own — and the reset-cause message on the next boot will confirm the diagnosis.
Apprentissage par renforcement et sim-to-realReinforcement learning and sim-to-real
Apprentissage par renforcement et sim-to-real
Je n'ai pas refait ce robot pour battre mon PID. Il se trouve que le réseau tient plus stable, mais ce n'était pas le but.
Je l'ai fait pour apprendre les outils sur lesquels ce domaine tourne réellement : un simulateur physique, l'apprentissage par renforcement, et l'écart entre ces deux mondes. Et je voulais les apprendre sur un problème que je connaissais déjà assez bien pour distinguer « ça marche » de « j'ai eu de la chance ».
Le robot est le même. Seul le contrôleur change.
Reinforcement learning and sim-to-real
I did not redo this robot to beat my PID. As it turns out the network does hold steadier, but that was never the point.
I did it to learn the tools this field actually runs on: a physics simulator, reinforcement learning, and the gap between those two worlds. And I wanted to learn them on a problem I already understood well enough to tell "it works" apart from "I got lucky".
The robot is the same. Only the controller changes.
Le modèle vient de la CAO, pas d'une estimationThe model comes from the CAD, not from an estimate
Le modèle vient de la CAO, pas d'une estimation
Le robot a été reconstruit dans MuJoCo à partir de l'assemblage SolidWorks : 6 maillages, articulation libre en 3D. L'axe des roues est localisé directement dans le maillage moteur, à (74,10 ; 39,68 ; 1021,00) mm, avec une rotation de −90° autour de Z pour passer au repère du modèle.
Le point de méthode : chaque paramètre du modèle porte l'étiquette de sa provenance — MESURÉ, DÉDUIT, ESTIMÉ ou SUPPOSÉ. Un modèle physique est un empilement d'hypothèses, et savoir lesquelles sont fragiles est la seule façon de savoir quoi remettre en cause quand il ment.
Au départ, 4 paramètres sur 22 restaient à établir. Après les mesures décrites plus bas, 2 sur 24.
The model comes from the CAD, not from an estimate
The robot was rebuilt in MuJoCo from the SolidWorks assembly: 6 meshes, a free 3D joint. The wheel axis is located directly inside the motor mesh, at (74.10, 39.68, 1021.00) mm, with a −90° rotation about Z to reach the model frame.
The methodological point: every parameter in the model carries a label for where it came from — MEASURED, DERIVED, ESTIMATED or ASSUMED. A physical model is a stack of assumptions, and knowing which ones are fragile is the only way to know what to question when it lies to you.
At the start, 4 parameters out of 22 were still unestablished. After the measurements described below, 2 out of 24.
L'apprentissageTraining
L'apprentissage
PPO (stable-baselines3), 15 observations, 2 actions, un réseau de deux couches de 32 neurones, soit 1 634 poids.
9 millions de pas en 15,7 minutes, 16 environnements en parallèle, sur processeur uniquement (Ryzen 7 7800X3D). Aucun GPU : un perceptron de 1 500 poids est plus rapide sur CPU que sur carte graphique, et la physique MuJoCo est de toute façon limitée par le processeur. Le réflexe « il faut un GPU » ne tient pas à cette échelle.
Récompense en cloches exp(−x²), toujours positive tant que le robot est debout : l'agent n'a jamais intérêt à mettre fin à l'épisode pour arrêter d'être puni.
Curriculum à mérite — la difficulté ne monte que si la survie dépasse 80 %, et le tampon d'expérience est vidé à chaque montée de palier. Chaque niveau doit donc se re-mériter avec la politique courante, au lieu d'être franchi sur la lancée des épisodes faciles précédents.
Training
PPO (stable-baselines3), 15 observations, 2 actions, a two-layer network of 32 units each, so 1,634 weights.
9 million steps in 15.7 minutes, 16 parallel environments, on CPU only (Ryzen 7 7800X3D). No GPU: a 1,500-weight perceptron runs faster on a CPU than on a graphics card, and MuJoCo physics is CPU-bound anyway. The reflex that "you need a GPU" does not hold at this scale.
Bell-shaped reward exp(−x²), always positive while the robot is upright: the agent never has an incentive to end the episode just to stop being punished.
Merit-based curriculum — difficulty only rises when survival exceeds 80 %, and the experience buffer is flushed at every step up. Each level must therefore be re-earned with the current policy, rather than cleared on the momentum of easier episodes.
L'export vers la carteExporting to the board
L'export vers la carte
La politique entraînée est traduite en C : un en-tête de 39 ko, les 1 634 poids en static const float donc placés en flash et non en RAM.
Validation — la version C est comparée à PyTorch sur 2 400 observations réelles, relevées sur le robot. Écart maximal 3 × 10⁻⁷, c'est-à-dire exactement la différence entre flottants 32 et 64 bits. La traduction n'introduit donc aucune erreur propre.
Coût mesuré à la compilation — +13,9 ko de flash, +48 octets de RAM.
Temps d'inférence sur la carte — 250 microsecondes (médiane mesurée, entre 222 et 254 selon les vols), sur un budget de 5 millisecondes. Soit 5 % du temps disponible : le réseau n'est pas le facteur limitant.
Garde-fous — sortie forcée à zéro au-delà de 14° d'inclinaison, coupure si l'action sature plus de 120 ms, et détection de NaN en amont et en aval du réseau. Un réseau embarqué qui pilote des moteurs doit pouvoir être court-circuité sans le consulter.
Exporting to the board
The trained policy is translated into C: a 39 kB header, the 1,634 weights as static const float so they live in flash, not RAM.
Validation — the C version is compared against PyTorch on 2,400 real observations recorded from the robot. Maximum deviation 3 × 10⁻⁷, which is precisely the difference between 32- and 64-bit floats. The translation introduces no error of its own.
Cost measured at compile time — +13.9 kB of flash, +48 bytes of RAM.
Inference time on the board — 250 microseconds (measured median, between 222 and 254 across flights), on a 5 millisecond budget. That is 5 % of the available time: the network is not the bottleneck.
Guardrails — output forced to zero beyond 14° of tilt, cutoff if the action saturates for more than 120 ms, and NaN detection both before and after the network. An embedded network driving motors must be overridable without consulting it.
Le premier transfert, et l'échec instructifThe first transfer, and the instructive failure
Le premier transfert, et l'échec instructif
En simulation, l'agent battait la cascade réglée à la main sur tous les critères.
Sur le robot réel, il tenait debout et tremblait dix fois plus.
Erreur d'angle (écart-type) — cascade 0,153° · agent 1,572°
Commande (écart-type) — cascade 136 pas/s · agent 878 pas/s
Rien n'était cassé. La simulation était fausse.
L'actionneur en vitesse de MuJoCo fait suivre la consigne à la roue instantanément. Un moteur pas à pas ne le fait pas : c'est un ressort magnétique, dont le couple vaut C = C_maintien · sin(50 · (θ_commandé − θ_rotor)). Demander une vitesse ne suffit pas à l'obtenir ; il faut que le rotor suive, et il a sa propre dynamique.
L'agent avait donc appris un monde où demander suffit à obtenir, et il y avait trouvé la stratégie optimale de ce monde-là : des inversions de commande très rapides, que le vrai moteur ne peut pas suivre. Il n'avait pas mal appris. Il avait parfaitement appris le mauvais problème.
The first transfer, and the instructive failure
In simulation, the agent beat the hand-tuned cascade on every criterion.
On the real robot, it stayed upright and shook ten times harder.
Angle error (std. dev.) — cascade 0.153° · agent 1.572°
Wheel command (std. dev.) — cascade 136 steps/s · agent 878 steps/s
Nothing was broken. The simulation was wrong.
MuJoCo's velocity actuator makes the wheel follow the command instantly. A stepper motor does not: it is a magnetic spring, whose torque is C = C_hold · sin(50 · (θ_commanded − θ_rotor)). Asking for a speed is not enough to get it; the rotor has to follow, and it has dynamics of its own.
So the agent had learned a world where asking is getting, and it had found that world's optimal strategy: very fast command reversals the real motor cannot track. It had not learned badly. It had learned the wrong problem perfectly.
L'identification, sans jamais toucher le robotIdentification, without ever touching the robot
L'identification, sans jamais toucher le robot
C'est le point de méthode dont je suis le plus content.
Un enregistreur à 200 Hz a été ajouté au firmware, avec vidage en CSV sur le port série. Puis une petite excitation sinusoïdale a été injectée dans la boucle de commande, avec balayage en fréquence, et l'identification faite par interspectres avec le signal injecté.
Pourquoi les interspectres — une première tentative par régression directe donnait un terme de gravité négatif, donc non physique. En boucle fermée, la commande est une fonction de l'angle : les régresseurs sont colinéaires et la régression ne peut pas les séparer. Le signal injecté est le seul signal exogène du système bouclé ; c'est donc le seul avec lequel corréler pour obtenir quelque chose de causal.
Déplacement total du robot sur toute la campagne : 45 à 48 mm. Aucun démontage, aucune chute. Le robot s'est mesuré lui-même, debout, avec ses propres capteurs.
Résonance du moteur — supposée 113 Hz · mesurée 71 Hz
Bruit du gyroscope — supposé 1,088 °/s · mesuré 0,062 °/s
Bruit de l'accéléromètre — supposé 0,150 m/s² · mesuré 0,205 m/s²
Micropas — 1600 ou 3200, non tranché · mesuré 1600
Sur le bruit du gyroscope, la conclusion est plus intéressante que le chiffre. L'ancienne valeur ne mesurait pas le capteur : elle mesurait la vibration mécanique des moteurs pas-à-pas. Or les deux n'ont pas la même nature. Un bruit de capteur est indépendant de tout ; une vibration est corrélée à l'activité moteur. Le modèle injectait du bruit blanc là où la réalité produit une perturbation structurée, ce qui n'est pas la même chose à apprendre.
Sur les micropas, le doute d'un facteur 2 a été levé par une vérification qui ne fait intervenir aucune masse : le rapport entre le terme de gravité et le gain d'actionnement doit valoir exactement g = 9,81. Mesuré 8,86 avec 1600 pas par tour, contre 4,43 avec 3200. Le premier est à 10 % de la bonne valeur, le second à un facteur 2. Aucune ambiguïté.
Identification, without ever touching the robot
This is the methodological point I am happiest with.
A 200 Hz recorder was added to the firmware, dumping CSV over the serial port. A small sinusoidal excitation was then injected into the control loop, with a frequency sweep, and the identification done by cross-spectra against the injected signal.
Why cross-spectra — a first attempt by direct regression produced a negative gravity term, which is not physical. In closed loop the command is a function of the angle: the regressors are collinear and regression cannot separate them. The injected signal is the only exogenous signal in the closed system, so it is the only thing worth correlating against to get something causal.
Total robot displacement over the whole campaign: 45 to 48 mm. No disassembly, no fall. The robot measured itself, standing, with its own sensors.
Motor resonance — assumed 113 Hz · measured 71 Hz
Gyroscope noise — assumed 1.088 °/s · measured 0.062 °/s
Accelerometer noise — assumed 0.150 m/s² · measured 0.205 m/s²
Microsteps — 1600 or 3200, undecided · measured 1600
On the gyroscope noise, the conclusion matters more than the number. The old figure was not measuring the sensor: it was measuring the mechanical vibration of the stepper motors. And the two are not the same kind of thing. Sensor noise is independent of everything; vibration is correlated with motor activity. The model was injecting white noise where reality produces a structured disturbance, which is not the same thing to learn.
On the microsteps, a factor-of-two doubt was settled by a check that involves no mass at all: the ratio between the gravity term and the actuation gain must equal exactly g = 9.81. Measured 8.86 with 1600 steps per revolution, against 4.43 with 3200. The first is within 10 %, the second off by a factor of two. No ambiguity left.
La validation, et c'est le résultat principalValidation, and this is the main result
La validation, et c'est le résultat principal
Une simulation corrigée ne prouve rien tant qu'on n'a pas vérifié qu'elle reproduit ce qu'elle avait manqué. J'ai donc remis l'ancien agent dans la simulation corrigée, avec le moteur mesuré :
Erreur d'angle — simulation corrigée 1,743° · robot réel 1,572°
Commande — simulation corrigée 873 pas/s · robot réel 878 pas/s
0,6 % d'écart sur la commande, 10,9 % sur l'inclinaison.
Ce qui compte ici n'est pas la précision du chiffre, c'est que la simulation n'a jamais été ajustée sur cette observation. Les seules choses recalées sont la résonance du moteur et les bruits de capteurs, mesurées indépendamment et pour elles-mêmes. La simulation reproduit donc, sans y avoir été aidée, précisément le défaut qu'elle avait caché.
C'est ce que veut dire « sim-to-real » en pratique : non pas qu'une politique traverse, mais qu'on sache pourquoi elle ne traversait pas.
Validation, and this is the main result
A corrected simulation proves nothing until you check that it reproduces what it had missed. So I put the old agent back into the corrected simulation, with the measured motor:
Angle error — corrected simulation 1.743° · real robot 1.572°
Wheel command — corrected simulation 873 steps/s · real robot 878 steps/s
Within 0.6 % on the command, 10.9 % on the tilt.
What matters here is not the accuracy of the number, it is that the simulation was never fitted on this observation. The only things recalibrated were the motor resonance and the sensor noise, measured independently and for their own sake. So the simulation reproduces, unaided, precisely the defect it had been hiding.
That is what sim-to-real means in practice: not that a policy transfers, but that you know why it was not transferring.
Le réentraînementRetraining
Le réentraînement
Avec le modèle de moteur fidèle branché, plus deux ajouts à la récompense : la pénalité sur l'inversion de commande passe de 0,15 à 0,35, et un terme nouveau pénalise l'angle de charge du moteur, c'est-à-dire l'écart entre position commandée et position réelle du rotor. On pénalise donc ce que le moteur subit, plutôt que la forme de la commande.
Résultats en simulation, robot au calme :
Erreur d'angle — ancien agent 1,743° · nouvel agent 0,106° · cascade 0,185°
Commande — ancien 873 pas/s · nouvel 47 pas/s · cascade 86 pas/s
Survie à difficulté maximale — ancien 25 % · nouvel 42 % · cascade 33 %
Poussée encaissée — ancien 0,38 N · nouvel 0,53 N · cascade 0,47 N
Deux réserves, et elles comptent. Ces chiffres sont ceux de la simulation. Sur le robot réel, aucune comparaison propre entre l'agent et la cascade n'a encore été faite : le témoin n'a pas été relancé pendant la même séance, et sans ça le rapport n'aurait aucune valeur. Par ailleurs l'avantage en poussée ne survit pas à un enchaînement : dans un scénario complet, à 0,55 N les deux tombent et à 0,50 N les deux tiennent.
Sur le robot réel, ce qui est établi : le nouvel agent a volé 76 secondes sans chute ni coupure, avec 536 pas/s d'écart-type de commande contre 878 pour l'ancien.
Retraining
With the faithful motor model plugged in, plus two additions to the reward: the command-reversal penalty goes from 0.15 to 0.35, and a new term penalises the motor's load angle, that is, the gap between commanded position and actual rotor position. What gets penalised is therefore what the motor undergoes, rather than the shape of the command.
Results in simulation, robot at rest:
Angle error — old agent 1.743° · new agent 0.106° · cascade 0.185°
Wheel command — old 873 steps/s · new 47 steps/s · cascade 86 steps/s
Survival at maximum difficulty — old 25 % · new 42 % · cascade 33 %
Push withstood — old 0.38 N · new 0.53 N · cascade 0.47 N
Two caveats, and they matter. These are simulation figures. On the real robot, no clean agent-versus-cascade comparison has been made yet: the control was not re-run during the same session, and without that the ratio would be worthless. Also, the advantage on push does not survive a sequence: in a full scenario, at 0.55 N both fall and at 0.50 N both hold.
On the real robot, what is established: the new agent flew for 76 seconds with no fall and no cutoff, with a command standard deviation of 536 steps/s against 878 for the old one.
Le plantage, et la boîte noireThe crash, and the black box
Le plantage, et la boîte noire
La carte redémarrait après quelques dizaines de secondes, toujours pendant que l'agent pilotait. Première hypothèse, et elle était fausse : l'agent martelait le pilote de timer.
Plutôt que de continuer à deviner, j'ai ajouté une boîte noire en RAM RTC — une zone qui survit au redémarrage. Une simple variable y contient l'étape en cours de la boucle de commande, relue au démarrage suivant. Quatre plantages, quatre fois la même réponse : bloqué dans la transaction I2C. Dont deux dans un mode de test qui ne lit pas le capteur et ne fait tourner aucune boucle de commande.
Ce n'était donc pas la politique, et ça ne l'avait jamais été.
La lecture du capteur a été déplacée dans sa propre tâche FreeRTOS sur le second cœur, volontairement non abonnée au chien de garde : un blocage devient ainsi un désarmement propre des moteurs au lieu d'un redémarrage sauvage. Résultat : 252 secondes de sollicitation à plein régime sans un seul plantage, contre deux plantages en 106 secondes auparavant.
Effet de bord mesuré, non recherché : les échecs de lecture I2C au repos sont passés de 1,01 par seconde à zéro, et le pire temps de boucle de 2 100 à 965 microsecondes.
The crash, and the black box
The board rebooted after a few tens of seconds, always while the agent was driving. First hypothesis, and it was wrong: the agent was hammering the timer driver.
Rather than keep guessing, I added a black box in RTC RAM — a region that survives a reboot. A single variable there holds the current stage of the control loop, read back on the next boot. Four crashes, four times the same answer: stuck in the I2C transaction. Two of them in a test mode that reads no sensor and runs no control loop at all.
So it was not the policy, and it never had been.
The sensor read was moved into its own FreeRTOS task on the second core, deliberately not subscribed to the watchdog: a hang then becomes a clean motor disarm instead of a hard reboot. Result: 252 seconds at full load without a single crash, against two crashes in 106 seconds before.
A measured side effect, not the one I was looking for: idle I2C read failures went from 1.01 per second to zero, and the worst-case loop time from 2,100 to 965 microseconds.
La direction, levée en fin de projetSteering, unlocked at the end of the project
La direction, levée en fin de projet
Limite ouverte depuis le début du projet : les deux broches STEP partageaient un même timer, donc les deux roues recevaient exactement les mêmes impulsions. Le robot ne pouvait qu'avancer et reculer, jamais tourner.
Le timer bat désormais à cadence fixe de 25 kHz, et chaque moteur porte son propre accumulateur en virgule fixe 16.16 ; quand un accumulateur déborde, cette roue-là émet un pas. C'est un algorithme de Bresenham appliqué au temps : deux vitesses indépendantes sur un seul timer, avec une erreur de phase bornée à un pas.
Un pupitre en Python (Tkinter) pilote le robot : joystick 2D, échelles de réglage, état en direct. Il ne touche pas au port série — il communique par fichiers avec le moniteur qui le détient, pour qu'il n'y ait jamais deux processus sur le même port.
Steering, unlocked at the end of the project
A limitation open since the start of the project: both STEP pins shared a single timer, so both wheels received exactly the same pulses. The robot could only go forward and backward, never turn.
The timer now runs at a fixed 25 kHz, and each motor carries its own 16.16 fixed-point accumulator; when an accumulator overflows, that wheel emits a step. It is Bresenham's algorithm applied to time: two independent speeds on a single timer, with phase error bounded to one step.
A Python (Tkinter) console drives the robot: 2D joystick, tuning scales, live state. It never touches the serial port — it communicates through files with the monitor that owns it, so that two processes are never on the same port.
Méthode de travailWorking method
Méthode de travail
Rien n'a été réglé à l'œil. Un moniteur série développé pour l'occasion enregistre l'intégralité du port série horodaté à la milliseconde et permet d'envoyer des commandes ; chaque paramètre est modifiable en direct et sauvegardé en mémoire flash. Un enregistreur embarqué à 200 Hz complète le dispositif pour les campagnes d'identification.
C'est ce qui rend les enquêtes ci-dessus possibles : sans journal horodaté, la résonance à 71 Hz et les 250 microsecondes d'inférence n'auraient été que des impressions.
Trois règles qui viennent du projet lui-même :
Chaque chiffre porte sa provenance. Dans le modèle MuJoCo, chaque paramètre est étiqueté MESURÉ, DÉDUIT, ESTIMÉ ou SUPPOSÉ. C'est ce qui a permis de savoir quoi remettre en cause quand la simulation a menti.
Un résultat négatif clairement établi vaut beaucoup. Savoir que la fréquence en boucle fermée ne mesure pas la géométrie, ou que l'ancien « bruit de gyroscope » était en fait une vibration moteur, a plus fait avancer le projet que plusieurs réglages réussis.
Les hypothèses se vérifient par un chemin indépendant. Le doute sur les micropas a été levé par un rapport qui ne fait intervenir aucune masse, donc immunisé aux erreurs du reste du modèle.
Sur l'écriture du code — Une bonne partie du code de ce projet est écrite avec Claude. Je m'en sers pour comprendre la méthode plutôt que pour la contourner : le diagnostic, la conception des campagnes de mesure et l'interprétation des résultats sont les miennes. Je le précise parce qu'un projet dont le principe est que chaque chiffre porte sa provenance doit s'appliquer la même règle à lui-même.
Working method
Nothing was tuned by eye. A purpose-built serial monitor records the entire serial port timestamped to the millisecond and allows commands to be sent; every parameter can be changed live and saved to flash. An on-board 200 Hz recorder completes the setup for identification campaigns.
That is what makes the investigations above possible: without a timestamped log, the 71 Hz resonance and the 250 microsecond inference time would have been nothing but impressions.
Three rules that came out of the project itself:
Every number carries its provenance. In the MuJoCo model, each parameter is labelled MEASURED, DERIVED, ESTIMATED or ASSUMED. That is what made it possible to know what to question when the simulation lied.
A clearly established negative result is worth a lot. Knowing that the closed-loop frequency does not measure geometry, or that the old "gyroscope noise" was in fact motor vibration, moved the project forward more than several successful tuning sessions.
Assumptions get verified through an independent path. The microstep doubt was settled by a ratio involving no mass at all, and therefore immune to errors elsewhere in the model.
On how the code was written — A good part of this project's code is written with Claude. I use it to understand the method rather than to skip it: the diagnosis, the design of the measurement campaigns and the interpretation of the results are mine. I mention it because a project whose whole principle is that every number carries its provenance should apply that rule to itself.
État actuel, et ce qui reste ouvertWhere it stands, and what is still open
État actuel, et ce qui reste ouvert
Projet en cours. Le robot vole sous politique entraînée, il tourne, et la simulation reproduit son comportement réel à quelques pourcents. Ce qui n'est pas établi est listé ici, parce que savoir où s'arrête ce qu'on a montré fait partie du résultat.
Le témoin cascade sur matériel. Aucune comparaison propre agent contre cascade n'a été faite sur le robot réel dans une même séance. Tant qu'elle manque, le gain du nouvel agent n'est chiffré qu'en simulation.
Le courant réel des drivers A4988. La résonance mesurée à 71 Hz suggère soit un couple à 40 % du nominal, soit des roues 2,5 fois plus lourdes qu'estimé. Une mesure de tension de référence sur le driver tranchera entre les deux.
Le pendule suspendu. Jamais réalisé. C'est l'essai qui séparerait l'inertie de la hauteur du centre de masse, deux grandeurs que la seule mesure en boucle fermée ne peut pas démêler.
Deux paramètres du modèle sur 24 restent non établis, tous deux des masses absentes de la CAO.
Code source — tout le projet est public sur GitHub : CAO, firmware, simulation, agent entraîné et journaux bruts du robot, d'où viennent les chiffres de cette page.
La suite — monter d'un cran : locomotion à pattes, et contrôle du mouvement sur humanoïde.
Where it stands, and what is still open
Work in progress. The robot flies under a trained policy, it steers, and the simulation reproduces its real behaviour to within a few percent. What is not established is listed here, because knowing where your evidence stops is part of the result.
The cascade control on hardware. No clean agent-versus-cascade comparison has been run on the real robot within a single session. Until it is, the new agent's advantage is a simulation figure only.
The real A4988 driver current. The 71 Hz measured resonance suggests either a torque at 40 % of nominal, or wheels 2.5 times heavier than estimated. A reference-voltage measurement on the driver will settle it.
The suspended-pendulum test. Never done. It is the experiment that would separate inertia from centre-of-mass height, two quantities that a closed-loop measurement alone cannot disentangle.
Two model parameters out of 24 remain unestablished, both of them masses missing from the CAD.
Source code — the whole project is public on GitHub: CAD, firmware, simulation, trained policy and the robot's raw logs, where every number on this page comes from.
Next — move up a level: legged locomotion, and motion control on a humanoid.
Compétences mises en œuvreSkills applied
Compétences mises en œuvre
Apprentissage par renforcement — PPO, conception de récompense, curriculum à mérite, entraînement parallélisé.
Simulation et sim-to-real — MuJoCo, modèle issu de la CAO, écart simulation/réel identifié et corrigé, modélisation d'actionneur.
Identification de système — excitation en boucle fermée, interspectres, colinéarité des régresseurs, vérification par chemin indépendant.
Automatique — régulation en cascade, séparation des bandes passantes, anti-emballement, analyse de stabilité.
Temps réel embarqué — inférence de réseau sur microcontrôleur, FreeRTOS multicœur, chien de garde, interruptions, budget de temps de boucle.
Fusion de capteurs — filtre complémentaire, calibration, caractérisation du bruit.
Débogage bas niveau — boîte noire en RAM persistante, dump d'exception, addr2line, analyse de causes de reset.
Électronique — bus I2C, drivers de moteurs pas-à-pas, modèle de ressort magnétique, micropas.
Démarche expérimentale — provenance de chaque paramètre, mesure avant hypothèse, une variable à la fois.
Skills applied
Reinforcement learning — PPO, reward design, merit-based curriculum, parallelised training.
Simulation and sim-to-real — MuJoCo, CAD-derived model, sim/real gap identified and closed, actuator modelling.
System identification — closed-loop excitation, cross-spectra, regressor collinearity, verification through an independent path.
Control theory — cascade control, bandwidth separation, anti-windup, stability analysis.
Real-time embedded — neural network inference on a microcontroller, multi-core FreeRTOS, watchdog, loop time budgeting.
Sensor fusion — complementary filter, calibration, noise characterisation.
Low-level debugging — black box in persistent RAM, exception dumps, addr2line, reset cause analysis.
Electronics — I2C bus, stepper motor drivers, magnetic spring model, microstepping.
Experimental method — provenance for every parameter, measure before hypothesis, one variable at a time.