Une pierre qui roule de poche, partie 1 : une illusion haptique de 2006, un ESP32-C6 et un ampli I2S

Celui-là m’accompagne depuis plusieurs années, pas depuis un week-end. L’idée est simple à énoncer et étonnamment difficile à arrêter de peaufiner : construire un petit objet à tenir en main qui vous laisse sentir une bille rouler et glisser dans un tube qui n’existe pas physiquement. Vous inclinez le boîtier, et une bille virtuelle court d’un bout à l’autre, cogne la paroi, repart en arrière. Aucune masse mobile à l’intérieur. Toute la sensation est synthétisée.
Tout a commencé au colloque tenu en mémoire de Vincent Hayward, l’un des deux auteurs de l’article qui sert de base à ce projet. Assis là, j’ai réalisé que de cette belle démonstration il ne restait, autant que je sache, qu’un seul exemplaire en état de marche dans le monde. Ça m’a dérangé. J’étais à peu près sûr de connaître un bon actionneur capable aussi de produire un impact net, et qu’un matériel très minimal suffirait à ramener l’illusion à la vie. Ce qui me manquait, pendant des années, c’était le temps : entre un travail à plein temps et les devoirs de parent, la construction ne se faisait jamais. Ce qui a fini par tout débloquer, c’est d’utiliser Claude Code pour avancer vite dans les petites fenêtres dont je dispose, et c’est comme ça que le firmware, les deux chemins matériels et l’application compagnon ont vraiment pris forme.
C’est toujours en cours. J’ai deux chemins matériels qui fonctionnent dans le firmware, la physique continue d’être affinée, et le boîtier est petit, destiné à rester un objet à tenir en main. Cet article est la partie 1 : l’idée, la physique, le firmware, et le premier des deux montages, celui qui restitue la bille sous forme de son via un amplificateur I2S. Une petite note d’honnêteté d’entrée : ce montage audio, je l’ai validé sur l’établi, les cartes branchées en vrac, mais je ne l’ai jamais refermé dans un boîtier fini. L’unité que j’ai réellement assemblée et que je trimballe, c’est le montage à pont en H de la partie 2, qui pilote un moteur directement. Les deux chemins font tourner exactement la même simulation.
L’illusion à la base de tout ça
L’ensemble est une reproduction d’un joli petit article : Hsin-Yun Yao et Vincent Hayward, An Experiment on Length Perception with a Virtual Rolling Stone, Eurohaptics 2006, pages 325 à 330. Le PDF est hébergé sur la page du laboratoire d’haptique de McGill.
Le dispositif décrit dans l’article est élégant. On donne aux gens un tube à tenir en main avec un seul actionneur vibrotactile à l’intérieur, et on synthétise la sensation d’une bille roulant le long de l’intérieur du tube à mesure qu’on l’incline. Pas de vraie bille. L’actionneur joue une vibration dont la hauteur suit la vitesse de la bille virtuelle, plus un transitoire net à chaque fois que la bille touche une paroi de bout. Ensuite on demande : les gens peuvent-ils estimer la longueur du tube virtuel uniquement à partir de cette sensation ? La réponse est oui, mieux que le hasard, et le plus intéressant est que les gens semblent utiliser un modèle interne de la gravité pour le faire. Ils ne chronomètrent pas un son, ils font mentalement rouler une bille sous gravité et lisent jusqu’où elle est allée.
C’est une illusion haptique au sens le plus pur : la perception de quelque chose de physique (une bille, un tube, une longueur) créée entièrement à partir d’un signal de vibration à une dimension. Cela se place juste à côté d’un thème dont j’ai déjà parlé, à savoir qu’on a des musées pour les illusions d’optique mais presque rien pour le toucher, et du projet connexe d’illusions haptiques imprimées en 3D de mes années de labo, dont tout l’intérêt était qu’on peut fabriquer une illusion haptique pour pas cher et la mettre dans la main de quelqu’un. Ce projet est le cousin motorisé et programmable de ceux-là : au lieu d’une géométrie astucieuse, l’illusion vit dans le firmware.
Ce que fait le boîtier
Le matériel est volontairement minimal :
- Un ESP32-C6 (RISC-V, 160 MHz, un seul cœur). Pas cher, BLE intégré, et largement assez rapide.
- Une centrale inertielle BNO085 9 axes pour mesurer l’inclinaison du boîtier.
- Une sortie haptique. C’est là que les deux montages divergent : un amplificateur audio I2S pilotant un actionneur TITAN Haptics (cet article), ou un pont en H pilotant un moteur (partie 2).
Le firmware lit l’inclinaison, fait tourner une petite simulation physique de la bille, et transforme le mouvement de la bille en vibration en temps réel. Inclinez le boîtier et la bille accélère vers le bas. Remettez-le à plat et la bille roule en roue libre, ralentie par le frottement. Inclinez de l’autre côté et la bille repart, court vers la paroi opposée, et la cogne avec un choc dont l’intensité dépend de sa vitesse.
Il y a en fait trois modes de simulation dans le firmware aujourd’hui, sélectionnables à l’exécution :
- Roulement : la bille roule comme une sphère pleine.
- Glissement : la bille glisse avec un frottement de Coulomb, donc elle peut rester collée sur les pentes douces.
- Billes : une boîte 3D avec trois billes de masse et de taille différentes qui rebondissent les unes sur les autres et sur les parois. Celui-là est arrivé pendant que je jouais, et il est vraiment amusant.
La physique, en bref
Les modes roulement et glissement sont à une dimension. La seule entrée venant du monde réel est sin(α), le sinus de l’angle d’inclinaison du tube, que je lis directement sur le vecteur gravité de la centrale. À partir de là, c’est de la mécanique de manuel.
Pour une sphère pleine qui roule sans glisser, le moment d’inertie vole une partie de l’accélération, donc :
ẍ = (g / 1,4) · sin(α)
Le 1,4 est le facteur 1 + 2/5 d’une sphère pleine. Dans le firmware cela se réduit à une seule constante G_FACTOR = 7,0 m/s².
Pour le mode glissement j’ajoute le frottement de Coulomb, avec une zone morte où la pente est trop douce pour vaincre le frottement statique et la bille reste simplement immobile :
ẍ = g · sin(α) − g · µ · sgn(sin(α)) · cos(α)
J’intègre ça avec un pas trapézoïdal à 1 kHz, en gardant l’accélération précédente pour que l’intégration reste stable. La bille est contrainte dans [0, cavité]. Quand elle atteint une paroi en se déplaçant encore vers elle, c’est un impact : j’enregistre la vitesse, je fais rebondir la vélocité avec un coefficient de restitution de 0,5, et je signale l’événement à la couche haptique. Un petit bout d’état de détection de front empêche l’impact de se redéclencher tant que la bille repose contre une paroi sous la gravité, ce qui produirait sinon un bourdonnement désagréable.
La boîte à billes est le même esprit en 3D : chaque bille reçoit l’accélération de la boîte et la gravité dans le référentiel non inertiel du boîtier, entre en collision avec les six parois, et entre en collision avec les autres billes avec une vraie impulsion à masses inégales le long de la normale de contact. Le signal haptique est piloté par la plus grande impulsion de collision à chaque pas, donc une lourde bille d’acier qui claque la paroi ne se sent pas comme la légère bille en plastique.
La forme du firmware
L’ESP32-C6 n’a qu’un cœur, donc je m’appuie sur FreeRTOS pour garder un timing propre. Trois tâches :
taskIMU(priorité 3) lit le BNO085 aussi vite que l’I2C le permet et met à jour un ensemble de variables globalesvolatile. L’I2C est la partie lente, donc elle a sa propre tâche et est découplée du reste.taskPhysics(priorité 5) tourne à exactement 1 kHz viavTaskDelayUntil. À chaque pas elle récupère la dernière inclinaison, fait avancer la physique, appelle la couche haptique, et publie un instantané de télémétrie.taskComms(priorité 1) gère les commandes série et BLE et diffuse la télémétrie à environ 58 Hz, avec un état d’une ligne une fois par seconde.
Un détail petit mais nécessaire : sur le C6 la tâche Arduino loopTask est surveillée par le chien de garde, et mon loop() se contente de se garer sur vTaskDelay(portMAX_DELAY) à l’infini. Donc je retire la tâche loop du chien de garde dans setup(), sinon la carte se réinitialise toutes les dix secondes. La vie en embarqué.
Un seul drapeau de compilation #define HBRIDGE, défini par environnement dans platformio.ini, bascule tout le code spécifique au matériel. La physique est identique octet pour octet entre les deux montages. C’était un objectif délibéré : je voulais la même bille dans les deux boîtiers, pour que toute différence ressentie vienne de l’étage de sortie, pas de la simulation.
Montage un : restituer la bille en audio
Le premier montage traite la bille comme un son, ce qui se révèle un choix très naturel pour l’haptique. Un actionneur à bobine mobile et un petit haut-parleur sont mécaniquement la même bête : une bobine qui pousse une masse. Si vous savez synthétiser un son de roulement crédible, vous savez le faire sentir.

La carte est une Adafruit ESP32-C6 Feather et l’amplificateur est un Adafruit MAX98357A, un ampli Class-D I2S. Le BNO085 se branche sur le connecteur STEMMA QT, donc aucune soudure pour le capteur. Les trois broches I2S de l’ampli (horloge de bit, sélection de mot, données) sortent des broches du connecteur SPI de la Feather, et la sortie, qui irait normalement vers un haut-parleur, va plutôt vers l’actionneur.
Nomenclature (montage I2S)
Tout ici est du standard du commerce. Les prix sont approximatifs, début 2026.
| Pièce | Référence complète | Où l’acheter |
|---|---|---|
| Carte MCU | Adafruit ESP32-C6 Feather, STEMMA QT. Réf. Adafruit 5933, module ESP32-C6-MINI-1 | adafruit.com/product/5933, environ 15 USD. Aussi distribuée par Digikey et Mouser sous Adafruit 5933 |
| Amplificateur I2S | Breakout amplificateur Class-D I2S 3W Adafruit MAX98357A. Réf. Adafruit 3006, puce Analog Devices MAX98357A | adafruit.com/product/3006, environ 6 USD. La puce nue est MAX98357AETE+T chez Digikey et RS |
| Centrale inertielle | Adafruit BNO085 9 axes, STEMMA QT. Réf. Adafruit 4754, capteur CEVA Hillcrest BNO085 | adafruit.com/product/4754, environ 20 USD |
| Actionneur haptique | TITAN Haptics TacHammer Drake LFi (la variante optimisée pour l’impact de leur actionneur à bobine mobile LMR large bande ; la pièce qui rend l’ensemble réellement palpable, voir le coup de chapeau plus bas) | titanhaptics.com/drake |
| Câble STEMMA QT | Câble Qwiic / STEMMA QT JST-SH 4 broches, 50 mm. Réf. Adafruit 4399 | adafruit.com/product/4399 |
| Alimentation | LiPo 3,7 V avec connecteur JST-PH 2 broches (la Feather la charge en interne), plus un câble USB-C | n’importe quel distributeur de LiPo |
| Boîtier | Coque imprimée en 3D, une bande de mousse acoustique, du ruban kapton | à fournir soi-même |
La Feather a la charge LiPo intégrée et un port STEMMA QT, donc la centrale ne demande aucune soudure et le seul câblage est les trois lignes I2S plus l’alimentation et la masse vers l’amplificateur.
Synthétiser le son de roulement
La vibration de roulement est une table d’onde indexée par position. La table est une période d’une arche de sinus négative, longue de 30 échantillons, et j’y accède avec la position de la bille en millimètres modulo 30. Ça paraît une façon étrange de faire un son, mais elle a une belle propriété : comme la table est indexée par la position, et non par le temps, la hauteur entendue monte automatiquement avec la vitesse de la bille. Va vite, balaie vite la table, hauteur plus élevée. Ralentis, hauteur plus basse. Ça tombe gratuitement de la géométrie, et ça correspond au comportement « la hauteur suit la vitesse » de l’article d’origine. L’amplitude monte aussi avec la vitesse, et en dessous d’un petit seuil de vitesse je la coupe entièrement pour qu’une bille au repos soit silencieuse.
L’impact est séparé : une courte salve rectangulaire dont l’amplitude monte avec la vitesse d’impact et dont la durée s’étire de 2 ms pour une tape jusqu’à 9 ms pour un choc violent.
Alimenter l’I2S à exactement 22050 Hz depuis une boucle à 1 kHz
Voici la partie dont je suis discrètement fier. La physique tourne à 1 kHz, mais l’audio doit sortir à 22050 Hz. Cela fait 22,05 échantillons par pas physique, ce qui n’est pas un entier. Si j’écrivais simplement 22 échantillons à chaque pas, je ferais tourner l’audio légèrement lentement (22000 Hz), et la hauteur serait subtilement fausse pour toujours.
Alors je tiens un accumulateur fractionnaire. À chaque pas j’y ajoute 0,05, et j’écris 22 échantillons, sauf que chaque fois que l’accumulateur dépasse 1,0 j’écris 23 échantillons à la place et je soustrais 1,0. Sur 1000 pas cela fait 950 pas de 22 plus 50 pas de 23, soit exactement 22050 échantillons par seconde. La fréquence d’échantillonnage à long terme est pile bonne, sans dérive, et l’appel i2s_channel_write vers le tampon DMA cadence naturellement la boucle. Pas cher, exact, et ça marche.
Le montage Feather a aussi une NeoPixel qui sert de voyant d’état : blanc faible au démarrage, rouge bloqué si la centrale n’est pas trouvée, vert une fois que ça tourne.
L’actionneur TITAN Haptics, ou ce qui a rendu ce projet si simple à construire
Un haut-parleur vous laisse entendre la bille, mais pour la sentir correctement il faut un actionneur haptique conçu pour ça. C’est précisément là que le travail d’origine était difficile : en 2006, Yao et Hayward ont dû fabriquer leur propre actionneur pour obtenir une vibration avec un impact assez net pour que l’illusion fonctionne. Vingt ans plus tard, je n’ai pas eu à le faire. J’ai contacté TITAN Haptics, et il se trouve qu’ils fabriquent exactement ce qu’il faut : le TacHammer Drake LFi, la variante optimisée pour l’impact de leur actionneur à bobine mobile large bande. Il est particulièrement doué pour restituer des impacts, des coups nets et discrets plutôt qu’un simple bourdonnement, ce qui est exactement ce dont la pierre qui roule a besoin chaque fois que la bille cogne une paroi, et c’est ce qui rend ce montage si simple et si efficace.
J’utilise donc un TacHammer Drake LFi (photographié en haut de cet article), piloté directement par la sortie de l’amplificateur I2S. C’est un actionneur à bobine mobile LMR à plage de fonctionnement ultralarge (TITAN annonce environ 5 à 300 Hz et un pic de 19 G), donc le même signal qui ferait un son fait une sensation, et il produit un impact net à la demande. Le plus joli : la fiche technique du Drake de TITAN liste à la fois les amplificateurs audio Class-D et les pilotes de moteur à pont en H parmi les façons compatibles de le piloter, ce qui correspond exactement aux deux montages de cette série. C’est la seule pièce qui fait passer le projet de « jolie démo de traitement du signal » à « attends, je sens vraiment la bille », donc elle a mérité sa propre ligne dans la nomenclature ci-dessus et sa propre section ici.
Un grand coup de chapeau chaleureux à mon amie Ashley Huffman et à toute l’équipe de TITAN Haptics. Ashley est une véritable figure du domaine : elle anime le podcast Haptics Club et écrit la newsletter All Things Haptics. Elle s’est enthousiasmée pour mon idée et m’a aidé à obtenir le matériel rapidement pour faire fonctionner tout ça. Ash, t’es la reine ! C’est un vrai plaisir de construire sur du matériel fait par des gens qui se soucient vraiment du toucher, et c’est encore mieux quand l’une d’elles est une amie.
Voir la bille : l’application compagnon
Comme tout l’intérêt est une chose qu’on ne peut pas voir, j’ai écrit une application compagnon en Python qui se connecte en USB série ou en BLE et dessine ce que le firmware ressent. C’est l’outil que j’utilise réellement pour régler la physique.

Elle montre le tube incliné avec la bille animée à sa position réelle, colorée par la vitesse, qui fait flasher la paroi à l’impact. À droite se trouvent des tracés défilants de position et de vélocité. Le panneau du bas est la télémétrie en direct : mode, longueur de cavité, position, vélocité, fréquence physique mesurée. Elle peut aussi rejouer la synthèse du son de roulement dans les haut-parleurs de l’ordinateur avec exactement le même modèle de table d’onde que le firmware, ce qui est un moyen rapide de comparer le ressenti sans reflasher la carte.
Passer en mode billes bascule la vue principale vers une boîte 3D :

La boîte 3D, la flèche de gravité qui suit la façon dont je tiens le boîtier, les trois billes de masse différente : tout cela est de la donnée en direct venant de la carte à 58 Hz via le lien série. C’est une petite chose, mais regarder la simulation et la sentir dans sa main en même temps, c’est ce qui rend la boucle de réglage rapide.
Où ça en est
Ce qui marche aujourd’hui : la physique, le firmware pour les deux chemins de sortie, l’application compagnon, et la télémétrie BLE et série. Ce chemin audio, je l’ai validé sur l’établi, avec la Feather, l’amplificateur et l’actionneur TITAN Haptics câblés en vrac. Je ne l’ai jamais refermé dans un boîtier fini. L’unité que j’ai réellement assemblée, avec la mousse et le kapton, c’est le montage à pont en H de la partie 2, parce que la carte tout-en-un faisait un boîtier plus petit. Donc le montage audio est une approche qui marche plus qu’un objet fini, et c’est son état honnête.
La suite, en gros : un tube plus long. Le boîtier reste un objet à tenir en main, mais l’illusion d’origine vit dans la sensation de longueur, et un corps plus long donne plus de place à la bille virtuelle pour courir et rend l’illusion plus convaincante. Ensuite, une batterie embarquée pour le rendre autonome, et un petit test utilisateur dans l’esprit de l’article d’origine pour voir si les gens arrivent à lire la longueur du tube virtuel à partir de ma version de l’illusion.
Il y a une direction qui m’enthousiasme particulièrement. Lors de ce même colloque en mémoire de Vincent Hayward, l’un des fils conducteurs était Miller et coll., Sensing with tools extends somatosensory processing beyond the body, Nature 2018 : quand vous tenez une tige et que quelque chose la frappe, votre système nerveux sait dire où le long de la tige l’impact a eu lieu, uniquement à partir de la vibration qui remonte l’outil. La pierre qui roule synthétise déjà un impact quand la bille virtuelle cogne une paroi. L’étape suivante naturelle est de synthétiser un impact localisable, en façonnant le transitoire pour que vous sentiez non seulement que la bille a cogné, mais où le long du tube elle a cogné. Cela ferait passer le boîtier d’une illusion de longueur à une illusion de position le long d’un outil, ce qui est exactement le genre de chose vers quoi tendait cette lignée de travaux.
Le code est sur GitHub à github.com/Leicas/esp32-ball. La partie 2 prend la même bille et la pilote via un pont en H à la place, sur une carte sans aucun amplificateur, ce qui est un tout autre jeu de compromis.