<?xml version="1.0" encoding="utf-8" standalone="yes" ?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
  <channel>
    <title>Embedded | Antoine Weill--Duflos</title>
    <link>https://antoine.weill-duflos.fr/en/tag/embedded/</link>
      <atom:link href="https://antoine.weill-duflos.fr/en/tag/embedded/index.xml" rel="self" type="application/rss+xml" />
    <description>Embedded</description>
    <generator>Hugo Blox Builder (https://hugoblox.com)</generator><language>en-us</language><lastBuildDate>Sat, 30 May 2026 00:00:00 +0000</lastBuildDate>
    <image>
      <url>https://antoine.weill-duflos.fr/media/icon_hu_d686267daab28486.png</url>
      <title>Embedded</title>
      <link>https://antoine.weill-duflos.fr/en/tag/embedded/</link>
    </image>
    
    <item>
      <title>Pocket Rolling Stone</title>
      <link>https://antoine.weill-duflos.fr/en/project/esp32-rolling-stone/</link>
      <pubDate>Sat, 30 May 2026 00:00:00 +0000</pubDate>
      <guid>https://antoine.weill-duflos.fr/en/project/esp32-rolling-stone/</guid>
      <description>&lt;p&gt;The Pocket Rolling Stone is a small handheld device that lets you &lt;strong&gt;feel a virtual ball rolling and sliding inside a tube that does not physically exist&lt;/strong&gt;. You tilt it, and a synthesized marble runs from one end to the other, hits the wall, and rolls back. Nothing moves inside. The sensation is generated entirely in firmware on an ESP32-C6, from tilt sensed by a BNO085 IMU.&lt;/p&gt;
&lt;p&gt;It is a reproduction of a haptic illusion described by Hsin-Yun Yao and Vincent Hayward in &lt;em&gt;An Experiment on Length Perception with a Virtual Rolling Stone&lt;/em&gt; (Eurohaptics 2006), where people could estimate the length of a virtual tube purely from the rolling sensation, apparently using an internal model of gravity. The project was sparked at the &lt;a href=&#34;https://eurohaptics.org/events/workshop-in-memoriam-of-vincent-hayward/&#34; target=&#34;_blank&#34; rel=&#34;noopener&#34;&gt;workshop held in memoriam of Vincent Hayward&lt;/a&gt;, where it became clear that essentially one working set of the original demo was left, worth rebuilding on modern, minimal hardware. It sits in the same family as the lab&amp;rsquo;s &lt;a href=&#34;../3dprintedillusions/&#34;&gt;3D printed haptic illusions&lt;/a&gt; and the broader question of &lt;a href=&#34;../../post/haptic-illusions/&#34;&gt;why touch has no museum of illusions&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;A 1 kHz physics loop simulates three modes (rolling like a solid sphere, sliding with Coulomb friction, and a 3D box of three marbles of different mass), and the ball&amp;rsquo;s motion is rendered as a real-time vibration. The same signed haptic signal drives two completely different output stages, selected by a single build flag:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;I2S audio path&lt;/strong&gt;: an Adafruit ESP32-C6 Feather plus a MAX98357A amplifier driving a TITAN Haptics actuator, streaming a position-indexed wavetable at 22050 Hz.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;H-bridge path&lt;/strong&gt;: a Microbots CodeCell C6 Drive board (onboard IMU and H-bridge) driving a motor directly through PWM, with no amplifier at all. This is the build that was actually assembled into a small handheld unit; the I2S path was validated on the bench.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;A Python companion app connects over USB serial or BLE to visualize and tune the simulation live.&lt;/p&gt;
&lt;p&gt;A direction I want to explore next: rendering a &lt;em&gt;localizable&lt;/em&gt; impact along the tube, so you feel where the virtual ball strikes, drawing on &lt;a href=&#34;https://cim.mcgill.ca/~haptic/pub/LM-ET-AL-NAT-18.pdf&#34; target=&#34;_blank&#34; rel=&#34;noopener&#34;&gt;Miller et al., &lt;em&gt;Sensing with tools extends somatosensory processing beyond the body&lt;/em&gt;, Nature 2018&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;This is an ongoing personal side project, not a product. The full story is documented in two build posts:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&#34;../../post/esp32-rolling-stone/&#34;&gt;Part 1: the illusion, the physics, and the I2S build&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&#34;../../post/esp32-rolling-stone-hbridge/&#34;&gt;Part 2: driving the ball with an H-bridge&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The firmware and companion app are open source at &lt;a href=&#34;https://github.com/Leicas/esp32-ball&#34; target=&#34;_blank&#34; rel=&#34;noopener&#34;&gt;github.com/Leicas/esp32-ball&lt;/a&gt;.&lt;/p&gt;
</description>
    </item>
    
    <item>
      <title>A Pocket Rolling Stone, Part 1: A 2006 Haptic Illusion, an ESP32-C6, and an I2S Amp</title>
      <link>https://antoine.weill-duflos.fr/en/post/esp32-rolling-stone/</link>
      <pubDate>Sat, 30 May 2026 00:00:00 +0000</pubDate>
      <guid>https://antoine.weill-duflos.fr/en/post/esp32-rolling-stone/</guid>
      <description>&lt;p&gt;















&lt;figure  id=&#34;figure-the-part-that-makes-this-build-feel-real-titan-haptics-actuators-this-first-build-renders-the-virtual-ball-as-audio-and-drives-one-of-these-more-on-them-and-on-my-friend-ashley-huffman-who-got-them-to-me-further-down&#34;&gt;
  &lt;div class=&#34;d-flex justify-content-center&#34;&gt;
    &lt;div class=&#34;w-100&#34; &gt;&lt;img alt=&#34;A clear plastic case held in one hand, with four small black cylindrical TITAN Haptics actuators seated in foam cutouts, each with thin red and black lead wires&#34; srcset=&#34;
               /en/post/esp32-rolling-stone/featured_hu_5845204be01c0e90.webp 400w,
               /en/post/esp32-rolling-stone/featured_hu_d1077f3167290f3a.webp 760w,
               /en/post/esp32-rolling-stone/featured_hu_cb24e0e4817501a0.webp 1200w&#34;
               src=&#34;https://antoine.weill-duflos.fr/en/post/esp32-rolling-stone/featured_hu_5845204be01c0e90.webp&#34;
               width=&#34;573&#34;
               height=&#34;760&#34;
               loading=&#34;lazy&#34; data-zoomable /&gt;&lt;/div&gt;
  &lt;/div&gt;&lt;figcaption&gt;
      The part that makes this build feel real: TITAN Haptics actuators. This first build renders the virtual ball as audio and drives one of these. More on them, and on my friend Ashley Huffman who got them to me, further down.
    &lt;/figcaption&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;This one has been with me for several years, not a weekend. The idea is simple to state and surprisingly hard to stop tuning: build a small handheld object that lets you &lt;strong&gt;feel a ball rolling and sliding inside a tube that does not physically exist&lt;/strong&gt;. You tilt the device, and a virtual marble runs from one end to the other, bumps the wall, rolls back. There is no moving mass inside. The whole sensation is synthesized.&lt;/p&gt;
&lt;p&gt;It started at the &lt;a href=&#34;https://eurohaptics.org/events/workshop-in-memoriam-of-vincent-hayward/&#34; target=&#34;_blank&#34; rel=&#34;noopener&#34;&gt;workshop held in memoriam of Vincent Hayward&lt;/a&gt;, one of the two authors of the paper this is based on. Sitting there, it struck me that of this lovely demonstration there was, as far as I could tell, essentially one working set left in the world. That bothered me. I was fairly sure I knew a good actuator that could also produce a crisp impact, and that very minimal hardware would be enough to bring the illusion back to life. The piece I was missing, for years, was time: between a day job and parent duty, the build kept not happening. What finally unlocked it was using Claude Code to move fast in the small windows I do have, which is how the firmware, the two hardware paths, and the companion app actually came together.&lt;/p&gt;
&lt;p&gt;It is still very much ongoing. I have two different hardware paths working in firmware, the physics keeps getting refined, and the device is small, meant to stay a handheld object. This post is Part 1: the idea, the physics, the firmware, and the first of the two builds, the one that renders the ball as &lt;strong&gt;sound through an I2S amplifier&lt;/strong&gt;. A small honesty note up front: this audio build I validated on the bench, with the boards loose, but never closed up into a finished enclosure. The unit I actually assembled and carry around is the H-bridge build in &lt;a href=&#34;../esp32-rolling-stone-hbridge/&#34;&gt;Part 2&lt;/a&gt;, which drives a motor directly. Both paths run the exact same simulation.&lt;/p&gt;
&lt;h2 id=&#34;the-illusion-this-is-built-on&#34;&gt;The illusion this is built on&lt;/h2&gt;
&lt;p&gt;The whole thing is a reproduction of a lovely little paper: Hsin-Yun Yao and Vincent Hayward, &lt;em&gt;An Experiment on Length Perception with a Virtual Rolling Stone&lt;/em&gt;, Eurohaptics 2006, pages 325 to 330. The &lt;a href=&#34;https://cim.mcgill.ca/~haptic/pub/HY-VH-EH-06.pdf&#34; target=&#34;_blank&#34; rel=&#34;noopener&#34;&gt;PDF lives on the McGill haptics lab page&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;The setup in the paper is elegant. They give people a handheld tube with a single vibrotactile actuator inside, and they synthesize the feeling of a ball rolling along the inside of the tube as you tilt it. No real ball. The actuator plays back a vibration whose pitch tracks how fast the virtual ball is moving, plus a sharp transient every time the ball hits an end wall. Then they ask: can people estimate the &lt;strong&gt;length&lt;/strong&gt; of the virtual tube just from this feeling? The answer is yes, better than chance, and the really interesting part is that people seem to use an internal model of &lt;strong&gt;gravity&lt;/strong&gt; to do it. They are not just timing a sound, they are mentally rolling a ball under gravity and reading off how far it went.&lt;/p&gt;
&lt;p&gt;That is a haptic illusion in the purest sense: a perception of something physical (a ball, a tube, a length) created entirely from a one-dimensional vibration signal. It sits right next to a theme I have written about before, that &lt;a href=&#34;../haptic-illusions/&#34;&gt;we have museums for optical illusions but almost nothing for touch&lt;/a&gt;, and the related &lt;a href=&#34;../../project/3dprintedillusions/&#34;&gt;3D printed haptic illusions&lt;/a&gt; project from my lab days, where the whole point was that you can build a haptic illusion cheaply and put it in someone&amp;rsquo;s hand. This project is the powered, programmable cousin of those: instead of a clever piece of geometry, the illusion lives in firmware.&lt;/p&gt;
&lt;h2 id=&#34;what-the-device-actually-does&#34;&gt;What the device actually does&lt;/h2&gt;
&lt;p&gt;The hardware is deliberately minimal:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;An &lt;strong&gt;ESP32-C6&lt;/strong&gt; (RISC-V, 160 MHz, single core). It is cheap, it has BLE, and it is more than fast enough.&lt;/li&gt;
&lt;li&gt;A &lt;strong&gt;BNO085&lt;/strong&gt; 9-DOF IMU to sense how the device is tilted.&lt;/li&gt;
&lt;li&gt;One haptic output. This is where the two builds diverge: an &lt;strong&gt;I2S audio amplifier driving a TITAN Haptics actuator&lt;/strong&gt; (this post), or an &lt;strong&gt;H-bridge driving a motor&lt;/strong&gt; (&lt;a href=&#34;../esp32-rolling-stone-hbridge/&#34;&gt;Part 2&lt;/a&gt;).&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The firmware reads the tilt, runs a small physics simulation of the ball, and turns the ball&amp;rsquo;s motion into a vibration in real time. Tilt the device and the ball accelerates downhill. Level it out and the ball coasts, slowing with friction. Tip it the other way and the ball reverses, runs to the far wall, and hits it with a thump whose strength depends on how fast it was going.&lt;/p&gt;
&lt;p&gt;There are actually three simulation modes in the firmware now, selectable at runtime:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Rolling&lt;/strong&gt;: the ball rolls like a solid sphere.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Sliding&lt;/strong&gt;: the ball slides with Coulomb friction, so it can stick on shallow slopes.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Marbles&lt;/strong&gt;: a 3D box with three marbles of different mass and size bouncing off each other and the walls. This one drifted in while I was playing, and it is genuinely fun.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id=&#34;the-physics-briefly&#34;&gt;The physics, briefly&lt;/h2&gt;
&lt;p&gt;The rolling and sliding modes are one-dimensional. The only input from the real world is &lt;code&gt;sin(α)&lt;/code&gt;, the sine of the tube&amp;rsquo;s tilt angle, which I read straight off the IMU&amp;rsquo;s gravity vector. From there it is textbook mechanics.&lt;/p&gt;
&lt;p&gt;For a solid sphere rolling without slipping, the moment of inertia steals some of the acceleration, so:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; class=&#34;chroma&#34;&gt;&lt;code class=&#34;language-fallback&#34; data-lang=&#34;fallback&#34;&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;ẍ = (g / 1.4) · sin(α)
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;The &lt;code&gt;1.4&lt;/code&gt; is the &lt;code&gt;1 + 2/5&lt;/code&gt; factor for a solid sphere. In the firmware this collapses to a single constant &lt;code&gt;G_FACTOR = 7.0&lt;/code&gt; m/s².&lt;/p&gt;
&lt;p&gt;For the sliding mode I add Coulomb friction, with a dead zone where the slope is too shallow to overcome static friction and the ball just stays put:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; class=&#34;chroma&#34;&gt;&lt;code class=&#34;language-fallback&#34; data-lang=&#34;fallback&#34;&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;ẍ = g · sin(α) − g · µ · sgn(sin(α)) · cos(α)
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;I integrate this with a trapezoidal step at 1 kHz, keeping the previous acceleration around so the integration stays stable. The ball is constrained to &lt;code&gt;[0, cavity]&lt;/code&gt;. When it reaches a wall and is still moving into it, that is an &lt;strong&gt;impact&lt;/strong&gt;: I record the speed, bounce the velocity with a restitution coefficient of 0.5, and flag the event for the haptic layer. A small piece of edge-detection state stops the impact from re-firing while the ball is resting against a wall under gravity, which would otherwise produce an ugly buzz.&lt;/p&gt;
&lt;p&gt;The marble box is the same spirit in 3D: each marble gets box acceleration and gravity in the non-inertial frame of the device, collides with the six walls, and collides with the other marbles using a proper unequal-mass impulse along the contact normal. The haptic signal is driven by the largest collision impulse on each tick, so a heavy steel marble slamming the wall feels different from the light plastic one.&lt;/p&gt;
&lt;h2 id=&#34;the-firmware-shape&#34;&gt;The firmware shape&lt;/h2&gt;
&lt;p&gt;The ESP32-C6 is single core, so I lean on FreeRTOS to keep the timing clean. Three tasks:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;taskIMU&lt;/code&gt;&lt;/strong&gt; (priority 3) reads the BNO085 as fast as I2C allows and updates a set of volatile globals. I2C is the slow part, so it gets its own task and is decoupled from everything else.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;taskPhysics&lt;/code&gt;&lt;/strong&gt; (priority 5) runs at exactly 1 kHz using &lt;code&gt;vTaskDelayUntil&lt;/code&gt;. Each tick it grabs the latest tilt, steps the physics, calls the haptic layer, and publishes a telemetry snapshot.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;taskComms&lt;/code&gt;&lt;/strong&gt; (priority 1) handles serial and BLE commands and streams telemetry out at about 58 Hz, with a one-line status dump once a second.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;One small but necessary detail: on the C6 the Arduino &lt;code&gt;loopTask&lt;/code&gt; is watched by the task watchdog, and my &lt;code&gt;loop()&lt;/code&gt; just parks on &lt;code&gt;vTaskDelay(portMAX_DELAY)&lt;/code&gt; forever. So I delete the loop task from the watchdog in &lt;code&gt;setup()&lt;/code&gt;, otherwise the board resets itself every ten seconds. Embedded life.&lt;/p&gt;
&lt;p&gt;A single &lt;code&gt;#define HBRIDGE&lt;/code&gt; build flag, set per environment in &lt;code&gt;platformio.ini&lt;/code&gt;, switches all the hardware-specific code. The physics is byte-for-byte identical between the two builds. That was a deliberate goal: I wanted the &lt;strong&gt;same ball&lt;/strong&gt; in both devices, so any difference I feel is the output stage, not the simulation.&lt;/p&gt;
&lt;h2 id=&#34;build-one-rendering-the-ball-as-audio&#34;&gt;Build one: rendering the ball as audio&lt;/h2&gt;
&lt;p&gt;The first build treats the ball as a &lt;strong&gt;sound&lt;/strong&gt;, which turns out to be a very natural fit for haptics. A voice-coil haptic actuator and a small speaker are mechanically the same animal: a coil pushing a mass. If you can synthesize a believable rolling sound, you can feel it.&lt;/p&gt;
&lt;p&gt;















&lt;figure  id=&#34;figure-the-i2s-build-before-assembly-the-blue-board-is-an-adafruit-max98357a-i2s-class-d-amplifier-the-black-board-is-an-adafruit-esp32-c6-feather-the-amplifier-output-goes-to-the-titan-haptics-actuator-instead-of-a-speaker-this-is-as-far-as-this-build-got-physically-validated-on-the-bench-never-boxed-into-an-enclosure&#34;&gt;
  &lt;div class=&#34;d-flex justify-content-center&#34;&gt;
    &lt;div class=&#34;w-100&#34; &gt;&lt;img alt=&#34;On a wooden desk, a small blue Adafruit MAX98357A I2S amplifier breakout with a green screw terminal on the left, and a larger black Adafruit ESP32-C6 Feather board on the right, with two loose header strips above them&#34; srcset=&#34;
               /en/post/esp32-rolling-stone/feather-max98357a_hu_8cec9694ee62d2f.webp 400w,
               /en/post/esp32-rolling-stone/feather-max98357a_hu_3144dfb912ec271.webp 760w,
               /en/post/esp32-rolling-stone/feather-max98357a_hu_97b93c80f4c067db.webp 1200w&#34;
               src=&#34;https://antoine.weill-duflos.fr/en/post/esp32-rolling-stone/feather-max98357a_hu_8cec9694ee62d2f.webp&#34;
               width=&#34;760&#34;
               height=&#34;573&#34;
               loading=&#34;lazy&#34; data-zoomable /&gt;&lt;/div&gt;
  &lt;/div&gt;&lt;figcaption&gt;
      The I2S build, before assembly. The blue board is an Adafruit MAX98357A I2S Class-D amplifier. The black board is an Adafruit ESP32-C6 Feather. The amplifier output goes to the TITAN Haptics actuator instead of a speaker. This is as far as this build got physically: validated on the bench, never boxed into an enclosure.
    &lt;/figcaption&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;The board is an &lt;strong&gt;Adafruit ESP32-C6 Feather&lt;/strong&gt; and the amplifier is an &lt;strong&gt;Adafruit MAX98357A&lt;/strong&gt; I2S Class-D amp. The BNO085 hangs off the STEMMA QT connector, so there is no soldering for the sensor. The amp&amp;rsquo;s three I2S pins (bit clock, word select, data) come off the Feather&amp;rsquo;s SPI header pins, and the output, normally going to a speaker, goes instead to the actuator.&lt;/p&gt;
&lt;h3 id=&#34;bill-of-materials-i2s-build&#34;&gt;Bill of materials (I2S build)&lt;/h3&gt;
&lt;p&gt;Everything here is off-the-shelf. Prices are rough and from early 2026.&lt;/p&gt;
&lt;table&gt;
  &lt;thead&gt;
      &lt;tr&gt;
          &lt;th&gt;Part&lt;/th&gt;
          &lt;th&gt;Full reference&lt;/th&gt;
          &lt;th&gt;Where to buy&lt;/th&gt;
      &lt;/tr&gt;
  &lt;/thead&gt;
  &lt;tbody&gt;
      &lt;tr&gt;
          &lt;td&gt;MCU board&lt;/td&gt;
          &lt;td&gt;Adafruit ESP32-C6 Feather, STEMMA QT. Adafruit PID 5933, module ESP32-C6-MINI-1&lt;/td&gt;
          &lt;td&gt;&lt;a href=&#34;https://www.adafruit.com/product/5933&#34; target=&#34;_blank&#34; rel=&#34;noopener&#34;&gt;adafruit.com/product/5933&lt;/a&gt;, about 15 USD. Also stocked by Digikey and Mouser under Adafruit 5933&lt;/td&gt;
      &lt;/tr&gt;
      &lt;tr&gt;
          &lt;td&gt;I2S amplifier&lt;/td&gt;
          &lt;td&gt;Adafruit MAX98357A I2S 3W Class-D amplifier breakout. Adafruit PID 3006, chip Analog Devices MAX98357A&lt;/td&gt;
          &lt;td&gt;&lt;a href=&#34;https://www.adafruit.com/product/3006&#34; target=&#34;_blank&#34; rel=&#34;noopener&#34;&gt;adafruit.com/product/3006&lt;/a&gt;, about 6 USD. Bare chip is MAX98357AETE+T at Digikey and RS&lt;/td&gt;
      &lt;/tr&gt;
      &lt;tr&gt;
          &lt;td&gt;IMU&lt;/td&gt;
          &lt;td&gt;Adafruit BNO085 9-DOF IMU, STEMMA QT. Adafruit PID 4754, sensor CEVA Hillcrest BNO085&lt;/td&gt;
          &lt;td&gt;&lt;a href=&#34;https://www.adafruit.com/product/4754&#34; target=&#34;_blank&#34; rel=&#34;noopener&#34;&gt;adafruit.com/product/4754&lt;/a&gt;, about 20 USD&lt;/td&gt;
      &lt;/tr&gt;
      &lt;tr&gt;
          &lt;td&gt;&lt;strong&gt;Haptic actuator&lt;/strong&gt;&lt;/td&gt;
          &lt;td&gt;&lt;strong&gt;TITAN Haptics TacHammer Drake LFi&lt;/strong&gt; (the impact-tuned variant of their wideband LMR voice-coil actuator; this is the part that makes the whole thing feel real, see the shoutout below)&lt;/td&gt;
          &lt;td&gt;&lt;a href=&#34;https://titanhaptics.com/drake/&#34; target=&#34;_blank&#34; rel=&#34;noopener&#34;&gt;titanhaptics.com/drake&lt;/a&gt;&lt;/td&gt;
      &lt;/tr&gt;
      &lt;tr&gt;
          &lt;td&gt;STEMMA QT cable&lt;/td&gt;
          &lt;td&gt;JST-SH 4-pin Qwiic / STEMMA QT cable, 50 mm. Adafruit PID 4399&lt;/td&gt;
          &lt;td&gt;&lt;a href=&#34;https://www.adafruit.com/product/4399&#34; target=&#34;_blank&#34; rel=&#34;noopener&#34;&gt;adafruit.com/product/4399&lt;/a&gt;&lt;/td&gt;
      &lt;/tr&gt;
      &lt;tr&gt;
          &lt;td&gt;Power&lt;/td&gt;
          &lt;td&gt;3.7 V LiPo with JST-PH 2-pin (the Feather charges it onboard), plus a USB-C cable&lt;/td&gt;
          &lt;td&gt;any LiPo distributor&lt;/td&gt;
      &lt;/tr&gt;
      &lt;tr&gt;
          &lt;td&gt;Enclosure&lt;/td&gt;
          &lt;td&gt;3D-printed shell, a strip of acoustic foam, kapton tape&lt;/td&gt;
          &lt;td&gt;self-sourced&lt;/td&gt;
      &lt;/tr&gt;
  &lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;The Feather has onboard LiPo charging and a STEMMA QT port, so the IMU needs no soldering and the only wiring is the three I2S lines plus power and ground to the amplifier.&lt;/p&gt;
&lt;h3 id=&#34;synthesizing-the-rolling-sound&#34;&gt;Synthesizing the rolling sound&lt;/h3&gt;
&lt;p&gt;The rolling vibration is a &lt;strong&gt;position-indexed wavetable&lt;/strong&gt;. The table is one period of a negative sine arch, 30 samples long, and I index into it with the ball&amp;rsquo;s position in millimetres modulo 30. That sounds like a strange way to make a sound, but it has a beautiful property: because the table is indexed by &lt;em&gt;position&lt;/em&gt;, not by time, the pitch you hear automatically rises with the ball&amp;rsquo;s speed. Move fast, sweep through the table fast, higher pitch. Slow down, lower pitch. It falls out of the geometry for free, and it matches the pitch-tracks-velocity behaviour from the original paper. The amplitude scales with speed too, and below a small speed threshold I silence it entirely so a resting ball is silent.&lt;/p&gt;
&lt;p&gt;The impact is separate: a short rectangular burst whose amplitude scales with the impact speed and whose duration stretches from 2 ms for a tap up to 9 ms for a hard slam.&lt;/p&gt;
&lt;h3 id=&#34;feeding-i2s-at-exactly-22050-hz-from-a-1-khz-loop&#34;&gt;Feeding I2S at exactly 22050 Hz from a 1 kHz loop&lt;/h3&gt;
&lt;p&gt;Here is the part I am quietly proud of. The physics runs at 1 kHz, but the audio needs to come out at 22050 Hz. That is 22.05 samples per physics tick, which is not an integer. If I just wrote 22 samples every tick I would be running the audio slightly slow (22000 Hz), and the pitch would be subtly wrong forever.&lt;/p&gt;
&lt;p&gt;So I keep a fractional accumulator. Every tick I add 0.05 to it, and write 22 samples, except that whenever the accumulator crosses 1.0 I write 23 samples instead and subtract 1.0. Over 1000 ticks that is 950 ticks of 22 plus 50 ticks of 23, which is exactly 22050 samples per second. The long-run sample rate is dead on, with no drift, and the &lt;code&gt;i2s_channel_write&lt;/code&gt; call into the DMA buffer naturally paces the loop. Cheap, exact, and it just works.&lt;/p&gt;
&lt;p&gt;The Feather build also has a NeoPixel that doubles as a status light: dim white while booting, red and stuck if the IMU is not found, green once it is running.&lt;/p&gt;
&lt;h2 id=&#34;the-titan-haptics-actuator-or-what-made-this-so-simple-to-build&#34;&gt;The TITAN Haptics actuator, or what made this so simple to build&lt;/h2&gt;
&lt;p&gt;A speaker will let you &lt;em&gt;hear&lt;/em&gt; the ball, but to &lt;em&gt;feel&lt;/em&gt; it properly you want a purpose-built haptic actuator. This is exactly where the original work had it hard: in 2006, Yao and Hayward had to &lt;strong&gt;build their own custom actuator&lt;/strong&gt; to get a vibration with a clean enough impact for the illusion to land. Twenty years later, I did not have to. I got in touch with &lt;strong&gt;TITAN Haptics&lt;/strong&gt;, and it turns out they make exactly the right thing: the &lt;strong&gt;TacHammer Drake LFi&lt;/strong&gt;, the impact-tuned variant of their wideband voice-coil actuator. It is specifically good at rendering &lt;strong&gt;impacts&lt;/strong&gt;, sharp discrete taps rather than just a buzz, which is exactly what the rolling stone needs every time the ball hits a wall, and it is what makes this build so simple and so effective.&lt;/p&gt;
&lt;p&gt;So I am using a &lt;strong&gt;TacHammer Drake LFi&lt;/strong&gt; (pictured at the top of this post), driven straight off the I2S amplifier output. It is an LMR voice-coil actuator with an ultrawide operating range (TITAN quotes roughly 5 to 300 Hz and a 19 G peak), so the same signal that would make a sound makes a feeling, and it produces a crisp impact on demand. The lovely part: TITAN&amp;rsquo;s own &lt;a href=&#34;https://titanhaptics.com/drake/&#34; target=&#34;_blank&#34; rel=&#34;noopener&#34;&gt;spec sheet for the Drake&lt;/a&gt; lists both Class-D audio amplifiers and H-bridge motor drivers among the compatible ways to drive it, which is precisely the two builds in this series. This is the single part that takes the project from &amp;ldquo;neat signal-processing demo&amp;rdquo; to &amp;ldquo;wait, I can actually feel the ball,&amp;rdquo; so it has earned its own line in the bill of materials above and its own section here.&lt;/p&gt;
&lt;p&gt;A big, warm shout-out to my friend &lt;strong&gt;Ashley Huffman&lt;/strong&gt; and the team at &lt;strong&gt;TITAN Haptics&lt;/strong&gt;. Ashley is genuinely a force in this field: she hosts the &lt;a href=&#34;https://thehapticsclub.com/&#34; target=&#34;_blank&#34; rel=&#34;noopener&#34;&gt;Haptics Club&lt;/a&gt; podcast and writes the &lt;a href=&#34;https://haptics.substack.com/&#34; target=&#34;_blank&#34; rel=&#34;noopener&#34;&gt;All Things Haptics&lt;/a&gt; newsletter. She got excited about my idea and helped me get the hardware fast to get this running. Ash, you da queen! It is a real pleasure to build on hardware made by people who genuinely care about touch, and an even bigger pleasure when one of them is a friend.&lt;/p&gt;
&lt;h2 id=&#34;seeing-the-ball-the-companion-app&#34;&gt;Seeing the ball: the companion app&lt;/h2&gt;
&lt;p&gt;Because the whole point is a thing you cannot see, I wrote a Python companion app that connects over USB serial or BLE and draws what the firmware is feeling. It is the tool I actually use to tune the physics.&lt;/p&gt;
&lt;p&gt;















&lt;figure  id=&#34;figure-the-companion-app-in-rolling-mode-live-over-usb-the-tilted-tube-and-ball-on-the-left-position-and-velocity-history-on-the-right-and-the-live-telemetry-panel-at-the-bottom-the-board-reports-a-steady-1000-hz-physics-loop&#34;&gt;
  &lt;div class=&#34;d-flex justify-content-center&#34;&gt;
    &lt;div class=&#34;w-100&#34; &gt;&lt;img alt=&#34;Screenshot of the companion app showing a dark UI titled Virtual Rolling Stone, ESP32-C6, Yao and Hayward Eurohaptics 2006. A grey tube is tilted down to the left at minus 6.9 degrees with an orange ball partway along it and a red flash on the left wall. On the right, a position history trace shows a triangle wave and a velocity trace shows the ball oscillating. A panel reads mode ROLLING, cavity 1000 mm, position 548 mm, velocity plus 1.4 m per s, physics 1000 of 1000 Hz&#34; srcset=&#34;
               /en/post/esp32-rolling-stone/viz-rolling_hu_13e405a38069a73f.webp 400w,
               /en/post/esp32-rolling-stone/viz-rolling_hu_ca995601256eb4c5.webp 760w,
               /en/post/esp32-rolling-stone/viz-rolling_hu_d354f1c1785d795c.webp 1200w&#34;
               src=&#34;https://antoine.weill-duflos.fr/en/post/esp32-rolling-stone/viz-rolling_hu_13e405a38069a73f.webp&#34;
               width=&#34;760&#34;
               height=&#34;485&#34;
               loading=&#34;lazy&#34; data-zoomable /&gt;&lt;/div&gt;
  &lt;/div&gt;&lt;figcaption&gt;
      The companion app in rolling mode, live over USB. The tilted tube and ball on the left, position and velocity history on the right, and the live telemetry panel at the bottom. The board reports a steady 1000 Hz physics loop.
    &lt;/figcaption&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;It shows the tilted tube with the ball animated at its real position, coloured by speed, flashing the wall on impact. On the right are scrolling position and velocity traces. The bottom panel is the live telemetry: mode, cavity length, position, velocity, measured physics rate. It can also replay the rolling-sound synthesis through your computer speakers using the exact same wavetable model as the firmware, which is a quick way to A/B the feel without flashing the board.&lt;/p&gt;
&lt;p&gt;Switching to marble mode flips the main view to a 3D box:&lt;/p&gt;
&lt;p&gt;















&lt;figure  id=&#34;figure-marble-mode-a-3d-box-with-three-marbles-of-different-mass-and-radius-the-live-gravity-vector-drawn-as-an-arrow-and-the-per-axis-position-history-the-panel-shows-the-raw-accelerometer-in-g-and-the-current-impact-energy&#34;&gt;
  &lt;div class=&#34;d-flex justify-content-center&#34;&gt;
    &lt;div class=&#34;w-100&#34; &gt;&lt;img alt=&#34;Screenshot of the companion app in marbles mode showing a 3D red box tilted in space with a gravity arrow and small marbles inside, labelled heavy 1.5 mm, medium 1.0 mm, light 0.7 mm, with X and Y position history traces on the right and a panel showing accelerometer readings in g and an impact value&#34; srcset=&#34;
               /en/post/esp32-rolling-stone/viz-marbles_hu_4df3ff5384fa7777.webp 400w,
               /en/post/esp32-rolling-stone/viz-marbles_hu_c2181fac9f4bf17.webp 760w,
               /en/post/esp32-rolling-stone/viz-marbles_hu_d9e0560a205920ca.webp 1200w&#34;
               src=&#34;https://antoine.weill-duflos.fr/en/post/esp32-rolling-stone/viz-marbles_hu_4df3ff5384fa7777.webp&#34;
               width=&#34;760&#34;
               height=&#34;485&#34;
               loading=&#34;lazy&#34; data-zoomable /&gt;&lt;/div&gt;
  &lt;/div&gt;&lt;figcaption&gt;
      Marble mode. A 3D box with three marbles of different mass and radius, the live gravity vector drawn as an arrow, and the per-axis position history. The panel shows the raw accelerometer in g and the current impact energy.
    &lt;/figcaption&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;The 3D box, the gravity arrow tracking how I hold the device, the three marbles of different mass: all of it is live data coming off the board at 58 Hz over the serial link. It is a small thing, but watching the simulation and feeling it in your hand at the same time is what makes the tuning loop fast.&lt;/p&gt;
&lt;h2 id=&#34;where-this-is-at&#34;&gt;Where this is at&lt;/h2&gt;
&lt;p&gt;Working today: the physics, the firmware for both output paths, the companion app, and BLE and serial telemetry. This audio path I validated on the bench, with the Feather, the amplifier, and the TITAN Haptics actuator wired up loose. I never closed it into a finished enclosure. The unit I actually assembled, foam and kapton and all, is the H-bridge build in &lt;a href=&#34;../esp32-rolling-stone-hbridge/&#34;&gt;Part 2&lt;/a&gt;, because the all-in-one board made for a smaller package. So the audio build is a working approach more than a finished object, which is the honest state of it.&lt;/p&gt;
&lt;p&gt;What is next, roughly: a &lt;strong&gt;longer tube&lt;/strong&gt;. The device stays a handheld object, but the original illusion lives in the sense of length, and a longer body gives the virtual ball more room to run and makes the illusion more convincing. After that, an &lt;strong&gt;onboard battery&lt;/strong&gt; so it is untethered, and a small user test in the spirit of the original paper to see whether people can read the virtual tube length from my version of the illusion.&lt;/p&gt;
&lt;p&gt;There is one direction I am especially excited about. At that same Vincent Hayward workshop, one of the threads was &lt;a href=&#34;https://cim.mcgill.ca/~haptic/pub/LM-ET-AL-NAT-18.pdf&#34; target=&#34;_blank&#34; rel=&#34;noopener&#34;&gt;Miller et al., &lt;em&gt;Sensing with tools extends somatosensory processing beyond the body&lt;/em&gt;, Nature 2018&lt;/a&gt;: when you hold a rod and something strikes it, your nervous system can tell &lt;em&gt;where&lt;/em&gt; along the rod the impact landed, purely from the vibration that travels up the tool. The rolling stone already synthesizes an impact when the virtual ball hits a wall. The natural next step is to synthesize a &lt;em&gt;localizable&lt;/em&gt; impact, shaping the transient so you feel not just that the ball hit, but where along the tube it hit. That would turn the device from a length illusion into a position-along-a-tool illusion, which is exactly the kind of thing this line of work was reaching toward.&lt;/p&gt;
&lt;p&gt;The code is on GitHub at &lt;a href=&#34;https://github.com/Leicas/esp32-ball&#34; target=&#34;_blank&#34; rel=&#34;noopener&#34;&gt;github.com/Leicas/esp32-ball&lt;/a&gt;. &lt;a href=&#34;../esp32-rolling-stone-hbridge/&#34;&gt;Part 2&lt;/a&gt; takes the same ball and drives it through an H-bridge instead, on a board with no amplifier at all, which is a completely different set of trade-offs.&lt;/p&gt;
</description>
    </item>
    
    <item>
      <title>A Pocket Rolling Stone, Part 2: Driving the Ball with an H-Bridge on a CodeCell ESP32-C6</title>
      <link>https://antoine.weill-duflos.fr/en/post/esp32-rolling-stone-hbridge/</link>
      <pubDate>Sat, 30 May 2026 00:00:00 +0000</pubDate>
      <guid>https://antoine.weill-duflos.fr/en/post/esp32-rolling-stone-hbridge/</guid>
      <description>&lt;p&gt;















&lt;figure  id=&#34;figure-the-finished-h-bridge-unit-this-is-the-build-i-actually-assembled-and-keep-around-a-codecell-c6-drive-wrapped-up-small-with-a-usb-c-cable-for-power-and-flashing-the-audio-build-from-part-1-never-got-boxed-up-like-this-it-stayed-on-the-bench&#34;&gt;
  &lt;div class=&#34;d-flex justify-content-center&#34;&gt;
    &lt;div class=&#34;w-100&#34; &gt;&lt;img alt=&#34;A small black tape-wrapped rectangular block held in one hand, with a USB-C cable plugged into one end&#34; srcset=&#34;
               /en/post/esp32-rolling-stone-hbridge/featured_hu_55609ed5e5d778d1.webp 400w,
               /en/post/esp32-rolling-stone-hbridge/featured_hu_51bd6e9e4f3cc1cd.webp 760w,
               /en/post/esp32-rolling-stone-hbridge/featured_hu_1438624f51795477.webp 1200w&#34;
               src=&#34;https://antoine.weill-duflos.fr/en/post/esp32-rolling-stone-hbridge/featured_hu_55609ed5e5d778d1.webp&#34;
               width=&#34;573&#34;
               height=&#34;760&#34;
               loading=&#34;lazy&#34; data-zoomable /&gt;&lt;/div&gt;
  &lt;/div&gt;&lt;figcaption&gt;
      The finished H-bridge unit. This is the build I actually assembled and keep around: a CodeCell C6 Drive wrapped up small, with a USB-C cable for power and flashing. The audio build from Part 1 never got boxed up like this, it stayed on the bench.
    &lt;/figcaption&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;In &lt;a href=&#34;../esp32-rolling-stone/&#34;&gt;Part 1&lt;/a&gt; I built a handheld device that lets you feel a virtual ball rolling inside a tube, based on &lt;a href=&#34;https://cim.mcgill.ca/~haptic/pub/HY-VH-EH-06.pdf&#34; target=&#34;_blank&#34; rel=&#34;noopener&#34;&gt;Yao and Hayward&amp;rsquo;s 2006 length-perception experiment&lt;/a&gt;, and I rendered the ball as audio through an I2S amplifier driving a TITAN Haptics actuator. This is the other half of the project: the &lt;strong&gt;same ball, same physics, no amplifier&lt;/strong&gt;. Here the haptic output is a vibration motor driven directly by an &lt;strong&gt;H-bridge&lt;/strong&gt;. It is also the build I actually finished and keep around, because the all-in-one board packs everything into a smaller unit than the Feather plus amplifier.&lt;/p&gt;
&lt;p&gt;If you have not read Part 1, the short version is: an ESP32-C6 reads tilt from a BNO085 IMU, runs a 1 kHz physics simulation of a ball rolling and sliding in a tube (plus a 3D marble-box mode), and turns the ball&amp;rsquo;s motion into a vibration in real time. A single &lt;code&gt;#define HBRIDGE&lt;/code&gt; build flag in &lt;code&gt;platformio.ini&lt;/code&gt; switches between the two output stages, and the physics code is identical between them. So everything here is about the output.&lt;/p&gt;
&lt;h2 id=&#34;why-a-second-build-at-all&#34;&gt;Why a second build at all&lt;/h2&gt;
&lt;p&gt;The I2S build is great, but it carries an audio amplifier, and you do not actually need one to drive a haptic actuator. A very common, simpler approach is an &lt;strong&gt;H-bridge&lt;/strong&gt;: two half-bridges that let you push current through the actuator in either direction, with the average voltage set by PWM duty cycle. The TacHammer Drake is perfectly happy being driven this way; TITAN even lists H-bridge motor drivers among the recommended ways to run it.&lt;/p&gt;
&lt;p&gt;So the question for this build was: can I get the same convincing rolling-stone feel by driving the same Drake LFi straight from an H-bridge, with no audio path at all? Mostly, yes, and for our needs it makes a much simpler and more compact solution. With the right board there is no soldering required at all.&lt;/p&gt;
&lt;h2 id=&#34;the-hardware-codecell-drive-and-drivecell&#34;&gt;The hardware: CodeCell Drive and DriveCell&lt;/h2&gt;
&lt;p&gt;This build runs on a &lt;strong&gt;CodeCell ESP32-C6 Drive&lt;/strong&gt; board from &lt;a href=&#34;https://microbots.io&#34; target=&#34;_blank&#34; rel=&#34;noopener&#34;&gt;Microbots&lt;/a&gt;. It is a tidy little board for exactly this kind of thing:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;ESP32-C6, same chip as the Feather build, so the physics is unchanged.&lt;/li&gt;
&lt;li&gt;An &lt;strong&gt;onboard BNO085&lt;/strong&gt; IMU, wired internally on I2C (SDA on IO8, SCL on IO9) and managed by the CodeCell library. No external sensor, no STEMMA cable.&lt;/li&gt;
&lt;li&gt;An onboard &lt;strong&gt;DriveCell&lt;/strong&gt; H-bridge, exposed in the library as &lt;code&gt;Drive1&lt;/code&gt; and &lt;code&gt;Drive2&lt;/code&gt;. I use &lt;code&gt;Drive1&lt;/code&gt; (IN1 on IO22, IN2 on IO21) as the haptic actuator and leave &lt;code&gt;Drive2&lt;/code&gt; spare.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;One nice consequence of the onboard everything is that the build is physically smaller than the Feather plus amplifier plus separate IMU. For a handheld device that matters.&lt;/p&gt;
&lt;p&gt;















&lt;figure  id=&#34;figure-an-earlier-angle-before-i-taped-it-fully-shut-the-board-and-foam-padding-nested-in-the-3d-printed-shell-once-closed-it-becomes-the-small-black-block-in-the-photo-at-the-top&#34;&gt;
  &lt;div class=&#34;d-flex justify-content-center&#34;&gt;
    &lt;div class=&#34;w-100&#34; &gt;&lt;img alt=&#34;A 3D-printed black shell held in one hand with a strip of dark foam on top and kapton tape, the board and padding nested inside before the case was closed&#34; srcset=&#34;
               /en/post/esp32-rolling-stone-hbridge/assembled-internals_hu_7d6dd65a917418e.webp 400w,
               /en/post/esp32-rolling-stone-hbridge/assembled-internals_hu_1a2061d719fc69c7.webp 760w,
               /en/post/esp32-rolling-stone-hbridge/assembled-internals_hu_2dbb5aca3cbb1831.webp 1200w&#34;
               src=&#34;https://antoine.weill-duflos.fr/en/post/esp32-rolling-stone-hbridge/assembled-internals_hu_7d6dd65a917418e.webp&#34;
               width=&#34;573&#34;
               height=&#34;760&#34;
               loading=&#34;lazy&#34; data-zoomable /&gt;&lt;/div&gt;
  &lt;/div&gt;&lt;figcaption&gt;
      An earlier angle, before I taped it fully shut: the board and foam padding nested in the 3D-printed shell. Once closed it becomes the small black block in the photo at the top.
    &lt;/figcaption&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;h3 id=&#34;bill-of-materials-h-bridge-build&#34;&gt;Bill of materials (H-bridge build)&lt;/h3&gt;
&lt;p&gt;The appeal of this build is how short the list is, and it is exactly why this is the version I finished. The CodeCell C6 Drive already has the IMU and the H-bridge on board.&lt;/p&gt;
&lt;table&gt;
  &lt;thead&gt;
      &lt;tr&gt;
          &lt;th&gt;Part&lt;/th&gt;
          &lt;th&gt;Full reference&lt;/th&gt;
          &lt;th&gt;Where to buy&lt;/th&gt;
      &lt;/tr&gt;
  &lt;/thead&gt;
  &lt;tbody&gt;
      &lt;tr&gt;
          &lt;td&gt;All-in-one board&lt;/td&gt;
          &lt;td&gt;Microbots CodeCell C6 Drive. Module ESP32-C6-MINI-1-H8 (8 MB flash), with an onboard BNO085 IMU, a VCNL4040 light/proximity sensor, and a dual H-bridge driver&lt;/td&gt;
          &lt;td&gt;&lt;a href=&#34;https://microbots.io/products/codecell-c6-drive&#34; target=&#34;_blank&#34; rel=&#34;noopener&#34;&gt;microbots.io/products/codecell-c6-drive&lt;/a&gt;, €32.99&lt;/td&gt;
      &lt;/tr&gt;
      &lt;tr&gt;
          &lt;td&gt;Haptic actuator&lt;/td&gt;
          &lt;td&gt;A TITAN Haptics TacHammer Drake LFi, driven from the H-bridge&lt;/td&gt;
          &lt;td&gt;&lt;a href=&#34;https://titanhaptics.com/drake/&#34; target=&#34;_blank&#34; rel=&#34;noopener&#34;&gt;titanhaptics.com/drake&lt;/a&gt;&lt;/td&gt;
      &lt;/tr&gt;
      &lt;tr&gt;
          &lt;td&gt;Power&lt;/td&gt;
          &lt;td&gt;3.7 V LiPo with JST connector, plus a USB-C cable&lt;/td&gt;
          &lt;td&gt;any LiPo distributor&lt;/td&gt;
      &lt;/tr&gt;
      &lt;tr&gt;
          &lt;td&gt;Enclosure&lt;/td&gt;
          &lt;td&gt;3D-printed shell, acoustic foam, kapton tape&lt;/td&gt;
          &lt;td&gt;self-sourced&lt;/td&gt;
      &lt;/tr&gt;
  &lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;That is the whole bill. No separate amplifier, no external IMU, no driver board. For a cheap, self-contained unit you can actually close up and pocket, that is exactly the point.&lt;/p&gt;
&lt;h2 id=&#34;generating-the-signal-with-ledc-pwm&#34;&gt;Generating the signal with LEDC PWM&lt;/h2&gt;
&lt;p&gt;There is no DMA audio stream here. Instead I drive the two H-bridge input pins with the ESP32&amp;rsquo;s &lt;strong&gt;LEDC&lt;/strong&gt; peripheral: a 20 kHz PWM carrier at 8-bit duty resolution. 20 kHz is above hearing, so the carrier itself is silent, and the actuator only responds to the envelope I impose on the duty cycle.&lt;/p&gt;
&lt;p&gt;The clever bit from Part 1 carries straight over. The rolling vibration is the same &lt;strong&gt;position-indexed wavetable&lt;/strong&gt;, one period of a negative sine arch, indexed by ball position in millimetres. Because it is indexed by position rather than time, the perceived pitch rises with ball speed automatically, exactly as in the audio build, even though here I am only updating the duty cycle once per physics tick at 1 kHz rather than streaming 22050 samples a second. The 1 kHz update is fast enough to carry the rolling texture and the impact transients that the skin cares about.&lt;/p&gt;
&lt;h3 id=&#34;unipolar-routing-rumble-on-one-pin-impact-on-the-other&#34;&gt;Unipolar routing: rumble on one pin, impact on the other&lt;/h3&gt;
&lt;p&gt;The audio build had it easy: an I2S sample is signed, so a bipolar waveform swinging positive and negative is the natural thing. PWM duty cycle is unsigned. You cannot write a negative duty.&lt;/p&gt;
&lt;p&gt;The trick I settled on is to &lt;strong&gt;split the signal by sign across the two H-bridge pins&lt;/strong&gt;:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;The rolling rumble uses the negative-arch wavetable, which is always less than or equal to zero. I route that to the &lt;strong&gt;minus&lt;/strong&gt; pin (IN2): duty proportional to the magnitude.&lt;/li&gt;
&lt;li&gt;The impact pulse is a short positive burst. I route that to the &lt;strong&gt;plus&lt;/strong&gt; pin (IN1).&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;On any given tick, exactly one pin is active and the other is held at zero duty. So the actuator gets a clean rumble drive in one polarity and a sharp impact kick in the other, all from a single signed signal computed exactly the way the audio build computes it. The signed value is the shared language between the two builds, and each output stage just renders it the way its hardware wants.&lt;/p&gt;
&lt;p&gt;The impact, as in Part 1, scales both amplitude and duration with impact speed: from a 2 ms tick for a gentle touch up to a 9 ms thump for a hard wall hit.&lt;/p&gt;
&lt;h2 id=&#34;embedded-gotchas-worth-writing-down&#34;&gt;Embedded gotchas worth writing down&lt;/h2&gt;
&lt;p&gt;A few things bit me on this board and are worth recording, because they are the kind of thing you lose an evening to.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;GPIO hold across deep sleep.&lt;/strong&gt; The CodeCell can deep-sleep, and the ESP32 can latch GPIO states across sleep with a hold feature. If a pin was held from a previous power cycle, configuring it again does nothing until you explicitly release the hold. So at startup I call &lt;code&gt;gpio_hold_dis&lt;/code&gt; on the H-bridge pins, the sensor power pin, and the I2C pins before configuring them. Without that, the first boot after a sleep can come up with a dead actuator or a dead sensor bus, intermittently, which is the worst kind of bug.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Sensor and LED power enables.&lt;/strong&gt; On this board the sensor and the status LED sit behind enable pins (IO18 and IO20). They have to be driven high early in &lt;code&gt;setup()&lt;/code&gt;, before the I2C scan, or the BNO085 simply is not there.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Single-core watchdog.&lt;/strong&gt; Same as the Feather build: the C6 is single core, my &lt;code&gt;loop()&lt;/code&gt; parks forever, so I delete the Arduino loop task from the task watchdog or the board resets every ten seconds.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Avoid the JTAG pins.&lt;/strong&gt; GPIO 4 through 7 are JTAG on the ESP32-C6. Easy to grab one by accident for an output and then wonder why debugging is weird. I keep them clear on both boards.&lt;/p&gt;
&lt;h2 id=&#34;how-it-compares-to-the-i2s-build&#34;&gt;How it compares to the I2S build&lt;/h2&gt;
&lt;p&gt;Side by side, the two builds feel different in instructive ways.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Bandwidth.&lt;/strong&gt; The audio build runs the output at 22050 Hz and can render fine texture and crisp transients. The H-bridge build updates at 1 kHz. For the rolling rumble and the impacts that is plenty, and honestly most of what your skin resolves in this band comes through fine. The audio build still has the edge on the sharpest, highest-frequency detail.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Simplicity and size.&lt;/strong&gt; The H-bridge build is smaller and has fewer parts: no amplifier, onboard IMU, onboard H-bridge. It is the build I would reach for to make a cheap, self-contained unit.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;The signal model is shared.&lt;/strong&gt; This is the part I like most. The same position-indexed wavetable, the same impact model, the same signed envelope drive both. The audio build streams it as samples, the H-bridge build splits it across two pins. The ball is the same ball.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The companion app does not care which build is on the other end of the wire. It speaks the same serial and BLE protocol either way, so I can watch and tune both:&lt;/p&gt;
&lt;p&gt;















&lt;figure  id=&#34;figure-the-same-companion-app-from-part-1-here-watching-the-rolling-physics-the-protocol-is-identical-between-the-two-builds-so-the-tooling-is-shared&#34;&gt;
  &lt;div class=&#34;d-flex justify-content-center&#34;&gt;
    &lt;div class=&#34;w-100&#34; &gt;&lt;img alt=&#34;Screenshot of the companion app in rolling mode: a dark UI with a grey tube tilted down to the left with an orange ball on it and a red wall flash, position and velocity history traces on the right, and a telemetry panel showing mode ROLLING, position, velocity, and a 1000 Hz physics rate&#34; srcset=&#34;
               /en/post/esp32-rolling-stone-hbridge/viz-rolling_hu_13e405a38069a73f.webp 400w,
               /en/post/esp32-rolling-stone-hbridge/viz-rolling_hu_ca995601256eb4c5.webp 760w,
               /en/post/esp32-rolling-stone-hbridge/viz-rolling_hu_d354f1c1785d795c.webp 1200w&#34;
               src=&#34;https://antoine.weill-duflos.fr/en/post/esp32-rolling-stone-hbridge/viz-rolling_hu_13e405a38069a73f.webp&#34;
               width=&#34;760&#34;
               height=&#34;485&#34;
               loading=&#34;lazy&#34; data-zoomable /&gt;&lt;/div&gt;
  &lt;/div&gt;&lt;figcaption&gt;
      The same companion app from Part 1, here watching the rolling physics. The protocol is identical between the two builds, so the tooling is shared.
    &lt;/figcaption&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;h2 id=&#34;where-this-is-going&#34;&gt;Where this is going&lt;/h2&gt;
&lt;p&gt;Both builds are working and both produce a convincing rolling-stone feel, which still slightly surprises me given there is nothing moving inside. The project is ongoing: the enclosure is a prototype, the device stays handheld (it is small), and the next steps are a longer tube to make the length illusion more convincing, an onboard battery to cut the cord, and a small length-perception test in the spirit of the original paper.&lt;/p&gt;
&lt;p&gt;If you want to build one, the firmware for both targets is on GitHub at &lt;a href=&#34;https://github.com/Leicas/esp32-ball&#34; target=&#34;_blank&#34; rel=&#34;noopener&#34;&gt;github.com/Leicas/esp32-ball&lt;/a&gt;, with the two PlatformIO environments (&lt;code&gt;feather&lt;/code&gt; for the I2S build, &lt;code&gt;hbridge&lt;/code&gt; for this one) and the Python companion app. Start with &lt;a href=&#34;../esp32-rolling-stone/&#34;&gt;Part 1&lt;/a&gt; for the physics and the audio path, then flip the build flag and feel the difference.&lt;/p&gt;
</description>
    </item>
    
  </channel>
</rss>
