<- Back

Für Elise on a DC motor: the N20 learns to sing

Hi! This one is a detour, and I am not even a little bit sorry. If you have been following along, you know the motor control library landed the N20 within half a degree of wherever it is told to go. Somewhere in that log I mentioned - almost in passing - that I run the PWM at 25 kHz deliberately above human hearing, so the motor does not whine at you.

Then I sat there staring at that sentence. Because “the motor whines when you PWM it in the audible band” is not a defect I engineered around. It is a feature I engineered away. The motor has been trying to talk to me this entire project and I kept putting tape over its mouth.

So: what if we let it sing? Not a beep. Not a chirp. Beethoven. ♥️

Same rules as the big log - inline SVG diagrams you can hover for tooltips, and the deeper maths tucked into fold-out sections you can click open or skip entirely. Everything runs on hardware you already have if you followed the earlier logs: an N20, a DRV8833, and two GPIO pins.

One thing IS different this time. The last log came with a zip of the source; this one does not. I am packaging this firmware as a ready-to-flash binary instead, so that anyone with an ESP board and a motor driver can try it without installing a toolchain - and so it can ship with sensible safety defaults baked in rather than trusting everyone to read part 9 first. That makes this log pure explanation: every mechanism, every number and every design decision is described in full, but you will not find code blocks below. If you want to build it yourself, everything you need to know is here - and honestly, writing it yourself from this description is the better exercise.

The two pins we already have

Nothing new gets wired. This is the exact same channel A of the DRV8833 from the motor logs: AIN1 on GPIO 4, AIN2 on GPIO 5, motor across AOUT1/AOUT2, nSLEEP held high, grounds common. (In the flashable build both pins are settable at runtime, because your board is not my board.)

Two pins, one H-bridge, one motor. By the end of this log those two pins will be playing a monophonic reduction of Für Elise, and - if you want - spinning the shaft while they do it.

Part 1: why a motor makes sound at all

Let us start with the thing nobody ever says out loud: a DC motor and a loudspeaker are the same invention.

Not “similar”. The same. Both are a coil of wire sitting in a magnetic field. Push current through the coil and you get a force - the Lorentz force, the fundamental “current in a magnetic field feels a push” law that all of electromagnetism-meets-mechanics is built on. Change the current, and the force changes with it.

The only difference between the two devices is what you bolt to the coil, and it is a difference of purpose, not of principle:

LOUDSPEAKER DC MOTOR A speaker: the coil's force is handed to a large, light, floppy cone. Huge area, free to move, so it shovels air efficiently. That is all a cone is for. magnet coil cone (big, light, floppy) lots of air moved coil force → cone → pressure waves designed to be LOUD and move nothing heavy A motor: the coil's force is handed to a rotor on bearings, inside a stiff steel can. Tiny radiating area, and the can is built to resist flexing. It is an accidental speaker with the worst possible cabinet. steel can (stiff, heavy) coil shaft barely any air moved coil force → rotor → torque designed to be QUIET and move something heavy Same coil. Same magnet. Same Lorentz force. Different thing bolted to the front. Hover each panel.

A speaker gives the coil’s force to a cone: enormous area, nearly weightless, deliberately floppy. Its entire job is to convert force into moved air. A motor gives the same force to a rotor on bearings, wrapped in a stiff steel can whose entire job is to not flex. One is an air pump. The other is an air pump that has been carefully sabotaged.

But sabotaged is not the same as disabled. The forces are still real, and they still leak out as sound through three paths:

  1. The whole structure flexes. Torque on the rotor produces an equal and opposite reaction on the stator and the can (Newton’s third law is not optional). The can is stiff, but stiff is not infinite. It bulges, microscopically, in time with the current.
  2. Magnetostriction. Iron physically changes shape a tiny amount when the magnetic field through it changes. The stator laminations are literally growing and shrinking as you modulate the current.
  3. Everything mechanical rattling in sympathy - brushes against the commutator, gear teeth taking up their backlash, the shaft in its bearings.

Here is the part that makes this whole project possible, and it is worth saying slowly:

The motor does not need to rotate to make sound. Torque and rotation are different things.

A stalled motor still produces torque - in fact it produces the most torque, since there is no back-EMF opposing you. Alternate the current direction at 440 times a second and you get alternating torque 440 times a second. The rotor cannot spin up and back down that fast (it has inertia, and a gearbox hanging off it), so it barely moves at all. But the force is there, wrestling against the mounts, and the structure hums at exactly 440 Hz.

That is an A4. Congratulations, your motor is an instrument.

Part 2: pitch is just “how fast do you flip”

Musical pitch is frequency, full stop. A4 is 440 Hz because we all agreed it is. Every other note follows from equal temperament: the octave is split into twelve equal steps on a multiplicative scale, so each semitone multiplies the frequency by the twelfth root of two:

f(n) = 440 × 2^(n/12)        n = semitones away from A4

Twelve of those multiplications give exactly 2 - one octave up, double the frequency. That is the whole system.

So the score is not really “notes” as far as the firmware is concerned. It is a list of frequencies and durations. The whole of Für Elise, in the reduction I am playing, needs exactly twelve distinct pitches:

C4 261.63 E4 329.63 F4 349.23 G4 392.00 G♯4 415.30 A4 440.00
B4 493.88 C5 523.25 D5 587.33 D♯5 622.25 E5 659.26 F5 698.46

Twelve numbers, and the piece is a list of indices into them with a duration each.

(A small trap if you write your own: sharps cannot be spelled D#5 in the source, because # cannot appear in a C identifier - it is the preprocessor’s own sigil. You end up spelling it Ds5 or similar. Tiny thing, but it is exactly the kind of tiny thing that eats twenty minutes if nobody warns you.)

Transposition then becomes a single multiply, because on a multiplicative scale shifting by a fixed number of semitones is a fixed ratio - the same ratio for every note in the piece. Multiply every frequency by 2^(semitones/12) as you play it and the whole thing moves bodily up or down.

Shifting by 12 doubles every frequency in the piece - the whole thing jumps an octave, and stays in tune with itself, because everything moved by the same ratio. We will need this later, when we discover the motor is deaf in the bass.

Part 3: the naive version, and the trap inside it

The obvious first attempt: PWM one pin at the note frequency, hold the other low. Square wave on IN1, IN2 grounded. You will hear the note immediately. It works.

It also drives the motor forward, and you cannot stop it doing that.

Here is why, and this single idea is the spine of the entire log. Take any waveform you put across the motor and split it into two parts:

Every drive waveform is two signals wearing one coat The actual voltage across the motor: a square wave that spends 70% of each period positive. Its average is not zero. what you apply +V −V 0 splits, exactly and always, into The DC average. The rotor's inertia and the gearbox act as a mechanical low-pass filter: they respond to this and ignore the rest. This is speed. the AVERAGE (DC) slow enough for the rotor to follow → this is SPEED The AC swing about that average. Far too fast for a rotor with inertia and a gearbox to follow, so it never becomes rotation - it becomes vibration instead. the SWING about it (AC) far too fast for the rotor to follow → this is SOUND The motor separates these for you, for free, because it is mechanically a low-pass filter. Hover the boxes.

The average is a DC voltage. The rotor has mass and a gearbox; together they are a mechanical low-pass filter - they respond to slow things and ignore fast things. A DC average is as slow as it gets, so the rotor follows it faithfully. Average voltage means rotation.

The swing about that average is alternating, at hundreds of hertz. The rotor cannot possibly accelerate and decelerate that fast, so it does not try. That energy has nowhere to go but into flexing the structure. Swing means sound.

These two are independent. That is not a metaphor, it is linear superposition: the motor is (near enough) a linear system, so it responds to the sum of two inputs as the sum of its two responses. You can dial in one without the other.

Which tells you exactly what is wrong with the naive version. A square wave between +V and 0 has an average of V/2. There is no way to make that zero while keeping the swing - the waveform never goes negative, so its average cannot be zero unless the whole thing is zero. Pitch and speed are welded together: to play the note you must accept half-speed rotation, and to stop the rotation you must stop the note.

To break them apart we need to be able to go negative. Which is precisely what an H-bridge is for.

Part 4: the H-bridge, and going negative

From the motor control log: the DRV8833 contains four electronic switches arranged like the letter H, with the motor as the crossbar. Close the top-left and bottom-right and current flows one way through the winding; close the other diagonal and it flows the other way. Two input pins per motor give you four states:

IN1 IN2 State Voltage across motor
HIGH LOW Forward drive +V
LOW HIGH Reverse drive −V
LOW LOW Coast - winding open, free-wheels 0 (floating)
HIGH HIGH Brake - winding shorted, back-EMF fights motion 0 (clamped)

The two we care about are the first two, because they let the voltage be negative. Now spend equal time in each:

half period A:  IN1 = drive,  IN2 = 0   →  +V across the motor
half period B:  IN1 = 0,      IN2 = drive →  −V across the motor

The average of +V for half the time and −V for the other half is exactly zero. Not approximately - the two halves are equal and opposite by construction, so there is nothing left over. The motor gets no DC at all and does not turn.

The swing, meanwhile, is the biggest it has ever been: 2V peak to peak, twice what the naive unipolar version managed. So the bipolar drive is simultaneously the quietest possible rotation (none) and the loudest possible note.

Here are all three schemes side by side. Each trace is the voltage the motor actually sees, with the dashed blue line marking its average:

Unipolar: never goes negative, so the average is stuck at half the supply. You get the note AND unavoidable rotation, welded together. 1. Unipolar (the naive one) +V 0 avg = +V/2 no way to zero it sings, but always drives forward at half speed. Not a choice - a side effect. Bipolar, equal halves: +V and -V for exactly the same duration. The average is exactly zero by construction, and the swing is the full 2V peak-to-peak. 2. Bipolar, equal halves +V 0 −V avg = 0 exactly, not roughly maximum volume, zero rotation. This is spin 0 - the default. Bipolar, uneven halves: still full amplitude both ways, but it spends longer positive than negative. The leftover average is rotation, and what remains of the swing is still a healthy note. 3. Bipolar, uneven halves +V 0 −V avg > 0 set by the ratio sings AND spins. This is a non-zero spin - the maths is in part 8.

Part 5: volume, and running two frequencies down one wire

Full ±V is loud. It is also the entire supply slammed across a stalled winding, which - as we will quantify in part 9 - is a great deal of heat. We need a volume knob.

The trick is to notice we have two completely different timescales available, and the motor can only hear one of them. Chop each half-period with an ultrasonic carrier, and use the carrier’s duty cycle to set how much of the supply gets through:

Two frequencies, one pair of pins Zoomed out: one full period of E5, 1517 microseconds. At this scale the carrier is invisible - the halves look solid. This is the timescale your ear works on. zoomed out (what you hear) one period of E5 = 1517 µs 659 flips per second → the ear calls this a note Zoomed into one half-period: the 20 kHz carrier chopping at 70% duty. Each carrier cycle is 50 microseconds. Your ear cannot hear 20 kHz, and the winding's inductance smooths it anyway - so all this does is set amplitude. zoomed in (what sets volume) 50 µs 30 carrier cycles hide inside each half-period. Duty = amplitude = volume.

Thirty carrier cycles fit inside each half-period of an E5. Your ear integrates them into “how hard is it pushing”, and the flip between halves is what it hears as pitch. Two signals, one wire, no interference - because they live three decades apart in frequency.

The whole waveform therefore collapses to a very small idea: a pair of records, each saying “hold these two duty values for this long”, alternated forever. One record for the forward half, one for the reverse half. Everything the firmware does - silent buzzing, spinning while singing, the gentler unipolar mode - is just a different pair of those three numbers. Working out that pair for a given note is about fifteen lines of arithmetic, and the playback loop that alternates them does not know or care which mode produced them.

That separation turned out to be the single best structural decision in the project. The maths in part 8 got rewritten three times; the playback loop never changed once.

Choosing 20 kHz is not arbitrary, and the reasoning is a nice little squeeze from both sides:

(The motor library uses 25 kHz for exactly the same reason. Different number, same argument.)

Part 6: turning Beethoven into an array

Für Elise (Beethoven, WoO 59) is written in 3/8 and runs almost entirely in semiquavers - sixteenth notes. That makes the natural unit of time obvious: one sixteenth. Every note in the score is an integer number of them, so the entire piece stores as a flat list of (pitch, how-many-sixteenths) pairs, with a pitch of zero meaning a rest. One byte of duration is plenty - nothing in this piece is longer than four sixteenths.

The opening, written out that way, reads almost like the sheet music does:

Bar Notes (each 1 sixteenth unless marked)
pickup + 1 E5 · D♯5 · E5 · D♯5 · E5 · B4 · D5 · C5
A minor A4 (2) · C4 · E4 · A4
E major B4 (2) · E4 · G♯4 · B4
back to theme C5 (2) · E4 · …

One trick worth stealing if you write your own: define each entry through a macro that takes the pitch name once and uses the C preprocessor to produce both things you need from it - token-pasting it (##) onto a NOTE_ prefix to get the frequency constant, and stringifying it (#) to get a printable "E5" for the serial log. One source of truth per note. The score cannot drift out of sync with its own labels, because the label and the lookup are literally the same characters in the same place.

The motor is monophonic, and the piece has opinions about that

A motor can hold exactly one pitch at a time. It has one coil. Any given instant has one current, therefore one force, therefore one frequency. There is no chord available at any price.

Für Elise is written for two hands, so something has to give. The usual answer - and the one every single-tone-generator arrangement uses - is to fold the left hand into the right. Look at those arpeggios above: C4, E4, A4 and E4, Gs4, B4 are not melody. They are the left hand’s broken A-minor and E-major chords, played as single notes in the gaps where the melody is holding still. The melody note gets 2 units, and the accompaniment fills the next 3.

The result is one continuous line that your brain happily reassembles into the original. This works because of auditory stream segregation - present alternating high and low notes fast enough and the brain files them as two separate voices, even though only one sounded at a time. Beethoven’s writing was already exploiting that, which is why the reduction sounds so natural.

The form I ended up playing is A – B – A: the famous theme, the descending answer, then the theme again.

Articulation: why silence is load-bearing

The opening is E5 D#5 E5 D#5 E5. Two pitches, alternating. If you play each note for its full duration and immediately start the next, the motor never stops vibrating - and transitions between two frequencies with no gap is not what a piano does. A piano note has an attack: a sharp onset followed by decay. Without an onset, five notes become one long warbling buzz.

So every note is played slightly short: take its full length, subtract a fixed articulation gap, sound it for the remainder, then sit silent for the gap.

18 ms of nothing, and suddenly there are five distinct notes. It costs nothing and it is the difference between “a motor making noise” and “a motor playing a tune”.

One guard is needed there, and it is worth mentioning because it is a class of bug rather than a one-off. If you ever crank the tempo up far enough that a sixteenth note is shorter than the articulation gap, that subtraction goes negative - and in unsigned arithmetic negative means about four billion. A note that should have lasted 10 ms lasts 49 days. So the gap is clamped to zero whenever it would not fit, and notes get shortened rather than becoming apocalyptic. Any time you subtract two unsigned numbers whose order you have not personally guaranteed, stop and check.

That silence is also, conveniently, where the sketch hands time back to FreeRTOS - delay() yields, and the busy-wait in the next section does not.

Part 7: timing, and why I did not use an interrupt

Every instinct says “audio timing, use a hardware timer ISR”. I did not, and I want to defend that properly.

The playback loop is about eight lines: spin until the microsecond clock reaches the next transition deadline, flip to the other half-cycle record, write its two duty values, add that half’s duration to the deadline, repeat until the note’s total time is up. No interrupts, no timers, no callbacks.

Three arguments for that, in increasing order of how much they actually mattered:

1. There is nothing else to do. This sketch’s entire job is playing the piece. An ISR exists to let other work continue between events; there is no other work. A busy-wait is not wasteful here - it is the whole program, and it buys the tightest timing available.

2. ledcWrite in an ISR is a question I did not want to answer. Under the hood it takes a spinlock and is not marked IRAM_ATTR, which means calling it from an interrupt is fine right up until something touches flash and the ISR tries to execute code that is temporarily not there. That class of bug appears once every few hours and is miserable to find. The busy-wait sidesteps the entire question.

3. The accumulator pattern kills drift. This is the subtle one, and it is the reason the deadline is advanced rather than recomputed. Each new deadline is the previous deadline plus this half’s duration - never “now plus the duration”.

The difference matters more than it looks. The obvious alternative, a delayMicroseconds(halfPeriod) at the bottom of the loop, measures from whenever the previous duty writes happened to finish - so every single iteration silently adds however long those writes took. Two microseconds is nothing. Thirteen hundred flips a second is not: after one second you are 2.6 ms behind, the note is flat, and it gets flatter the longer it is held. With an accumulator, a late flip does not move the next deadline at all. The error is absorbed on the very next cycle instead of compounding, so timing jitter stays jitter and never becomes drift. It is the same reason real-time loops everywhere advance a deadline rather than sleeping for an interval.

The wrap-safe comparison, since the obvious version is a landmine

Arduino’s micros() is a 32-bit microsecond counter, so it overflows every 71.6 minutes. Written the obvious way - wait while the clock is less than the deadline - the loop either exits instantly or hangs for an hour whenever the counter wraps mid-note. Once every 71 minutes. Which is to say: never on the bench, and always during a demo.

The fix is to never compare two timestamps directly. Instead subtract them and compare the difference to zero, casting the unsigned result to signed. This works because unsigned subtraction in C is defined to wrap modulo 2³², so the difference comes out correct even when the operands themselves have wrapped - as long as the real interval is under about 35 minutes, which for a half-period of a musical note it comfortably is. Casting to signed then gives you a sensible “how far past the deadline are we”, negative before and positive after, straight through the rollover.

It is the same trick every well-written millis() scheduler uses, it costs nothing, and it is worth making a permanent habit: compare differences, not timestamps.

(The ESP-IDF build of this sidesteps the issue entirely - IDF’s microsecond clock is 64-bit signed, which does not wrap for roughly 292,000 years. A direct comparison is genuinely safe there. I kept the subtraction habit anyway.)

Is 1 µs resolution good enough for music?

Worth checking rather than assuming. The period gets rounded to whole microseconds, so every pitch is very slightly off. Musical pitch error is measured in cents - hundredths of a semitone, 1200 to the octave:

Note Wanted Period Rounded Actual Error
C4 261.63 Hz 3822.19 µs 3822 261.643 Hz +0.09 cents
A4 440.00 Hz 2272.73 µs 2273 439.947 Hz −0.21 cents
E5 659.26 Hz 1516.85 µs 1517 659.196 Hz −0.17 cents
F5 698.46 Hz 1431.72 µs 1432 698.324 Hz −0.34 cents

Worst case about a third of a cent. A trained ear resolves maybe 5–6 cents; the just-noticeable difference for most people is nearer 10–25 cents in this register. We are fifteen times better than we need to be, and the tuning error is comfortably the least wrong thing about a motor playing Beethoven.

Part 8: spinning and singing at the same time

Now the fun part, and the part with the prettiest maths.

Go back to part 3’s insight: average is speed, swing is sound, and they superpose. In the symmetric bipolar drive the two halves are equal, so the average is zero. Make them unequal and the leftover average is rotation. The note keeps playing.

Let t be the fraction of each period spent driving forward. t = 0.5 is the symmetric case. The DC average is straightforward:

average = V × (2t − 1)

t = 0.5 → zero. t = 1 → full forward. Nice and linear.

The volume is the interesting one, and it is a classic Fourier result. A rectangular wave swinging between +V and −V with duty t has a fundamental component - the bit at the note frequency, the bit you actually hear - of amplitude:

fundamental = (4V / π) × sin(π t)
Click for where that sin(πt) comes from

The general result for a rectangular pulse train of peak-to-peak height H and duty d is that the n-th harmonic has amplitude (2H / nπ) · sin(nπd). Our wave swings from −V to +V, so H = 2V, and for the fundamental n = 1:

A₁ = (2 · 2V / π) · sin(πt) = (4V/π) · sin(πt)

Sanity check at t = 0.5: sin(π/2) = 1, giving 4V/π ≈ 1.27V - which is exactly the textbook fundamental of a symmetric square wave, and yes, it genuinely is larger than the square wave’s own peak. That is not a mistake: the fundamental sine wave sticks out past the corners of the square while the harmonics subtract back down to flat. The square is the sum of the whole family, not of the fundamental alone.

The useful thing here is the shape of sin(πt) near its peak. It is flat there - the derivative is zero at t = 0.5 - so the first bit of rotation you ask for costs almost no volume at all. The price only bites as you approach the ends.

So: speed rises linearly with the asymmetry, and volume falls as sin(πt), which is flat at the top. That asymmetry is the whole story, and it is generous:

spin t Speed Volume
0 0.500 0% 100%
50 0.598 19.6% 95.3%
100 0.696 39.2% 81.6%
150 0.794 58.8% 60.3%
200 0.892 78.4% 33.2%
255 1.000 100% 0%
The trade: every volt spent on speed is a volt not swinging the note 100% 50% 0 0 100 200 255 spin Volume follows sin(pi*t) - flat at the top. The first 20% of speed costs under 5% of volume. volume = sin(πt) Speed rises linearly with the asymmetry - it is just the DC average. speed = 2t − 1 spin 100: 39% of full speed while keeping 82% of full volume. The sweet spot, and it is not a coincidence - it sits where the sine is still near its flat peak. spin 100: 39% speed, 82% volume Because sine is flat at its peak, rotation is nearly free at first and ruinously expensive at the end. Hover the curves.

Look at that top-left corner. Because sin is flat at its peak, the first slice of rotation is nearly free: 20% of full speed costs under 5% of the volume. Push to 39% speed and you still keep 82%. It only turns brutal past halfway, and at full speed the note is mathematically gone - t = 1 means the waveform never reverses, so there is no swing left to hear.

Which is why the firmware refuses to go all the way. Each half-cycle is guaranteed a floor of 100 µs, whatever the requested spin level asks for. On an E5 (period 1517 µs) that caps t at 0.934, which still leaves about 20% of full volume alive.

The point of that clamp is not the maths, it is the failure mode. Without it, a spin setting near the maximum produces a firmware that appears to have hung: the motor spins, nothing plays, no error anywhere. With it, the piece is always audible, no matter what anybody types in. Given this is going out as a binary for other people to poke at, I would much rather quietly clamp a setting than ship a silent mode that reads as a crash.

The gentler alternative

There is a second way to get the same DC average, and it trades volume for kindness. Instead of swinging between +V and −V, stay on one side and swing the duty about a DC level: current always flows the same direction, it just gets stronger and weaker at the note frequency.

The constraint that shapes this mode is one line of reasoning: the swing is limited by whichever rail is nearer. Centre the duty at 20% and you can only swing ±20% before you hit zero and clip. Centre it at 80% and you can only swing ±20% before you hit full scale. Centre it at 50% and you get the maximum ±50%. So the available volume is min(centre, full-scale − centre), which peaks exactly in the middle of the speed range and falls to nothing at both ends - a triangle, where the antiphase mode gave us a sine.

The honest comparison: unipolar tops out at exactly half the acoustic output of antiphase (2V/π versus 4V/π), and only at half speed. What you buy is a winding that never sees a polarity reversal and spends much of each cycle at low current. If you want to leave this running for a while, that is the one to pick. spinmode selects it.

The stutter I did not see coming

First spinning version sounded terrible in a way I did not predict. The motor lurched.

The articulation gaps. Every note ends with 18 ms of a full stop - both pins low, winding open, motor coasting. At spin 0 that is invisible; who cares if a stationary motor coasts. But when it is turning, the drive vanishes for 18 ms out of every 140 ms, plus entire rests. That is a 13% duty cycle of “no torque at all”, at the note rate. The shaft surges and sags and the whole thing sounds like a car with a bad clutch.

The fix is that silence has to mean silent, not absent. Gaps and rests now hold the DC bias and drop only the swing: zero AC, unchanged average. Musically silent, mechanically unbroken. A genuine both-pins-low stop still exists, but it is reserved for the end of the piece, where stopping is actually what you meant.

I like this bug because it is the DC/AC split biting back. Once you have decided that average means speed and swing means sound, “make it silent” must mean zero the swing and leave the average alone - not “turn everything off”. I had the right model and still reached for the wrong function.

Part 9: the honest section - does this damage the motor?

Someone was going to ask, so let me get there first, with numbers.

The uncomfortable arithmetic

A spinning motor limits its own current through back-EMF: it generates a voltage opposing the supply, proportional to speed, and the net driving voltage is the difference. Stall it and the back-EMF is zero, so current is limited by winding resistance alone. That is stall current, and it is the largest current a motor can draw.

Our singing motor is stalled. There is no way around that - not spinning is the entire point.

My first hope was that inductance would rescue us: at audio frequency, surely the current never has time to build? So I checked, and the answer is no, not even slightly. The winding’s electrical time constant is τ = L/R. For an N20-class motor, roughly R ≈ 10 Ω and L ≈ 1.5 mH, so τ ≈ 150 µs:

Note Half-period In units of τ Current reaches
A2 (110 Hz) 4545 µs 30.3 τ 100%
C4 (262 Hz) 1911 µs 12.7 τ 100%
E5 (659 Hz) 758 µs 5.1 τ 99.4%
A5 (880 Hz) 568 µs 3.8 τ 97.7%
2 kHz 250 µs 1.7 τ 81%

Five time constants is a fully settled exponential. At every pitch in this piece, the current reaches its full stall value before the polarity flips. Inductance rounds the corners of the current waveform; it does not limit its height. You would need to be up around 2 kHz before it started genuinely helping.

Which leaves exactly one thing standing between you and stall current: the PWM duty. amplitude is not a volume knob wearing a volume knob’s clothes - it is a current limit, and it is the only one you have.

And because heating goes as I²R, it is a quadratic one. At 6 V into 10 Ω:

amplitude Effective volts Current Winding dissipation
90 2.12 V 212 mA 0.45 W
128 3.01 V 301 mA 0.91 W
180 4.24 V 424 mA 1.79 W
255 6.00 V 600 mA 3.60 W

Read those last two rows carefully. Going from 90 to 180 - doubling the amplitude, which is roughly +6 dB, a clearly-louder-but-not-dramatic step - quadruples the heat. Going to full scale is eight times the dissipation of the quiet setting. An N20 is a gram or two of copper and iron with no fan, no fins, and a plastic gearbox pressed against one end. Three and a half watts into that, continuously, is not a plan.

(These numbers are for a 6 V N20 with R ≈ 10 Ω. Measure your own motor’s resistance with a multimeter across the terminals and redo the sums - it is two minutes and it is your motor.)

What actually fails, in order

  1. Winding heat. The one that matters. Enamel insulation on the wire has a temperature rating; exceed it and turns short together, permanently. Slow and cumulative - a 30-second piece is nothing, an afternoon on loop at full amplitude is a different story.
  2. Brush and commutator wear. This one is specific to what we are doing and I find it genuinely interesting. Normally the commutator rotates, so arcing and mechanical wear get spread evenly around the whole ring. Ours is stationary - one pair of segments takes every single current reversal, hundreds of times a second, in the same physical spot. It is localised pitting rather than distributed wear. Still slow, but it is a wear mechanism the motor’s designers never had in mind.
  3. Bearings. No meaningful concern. Vibration without rotation does not load them.

The test that actually matters

Touch the can after a couple of playthroughs. Warm is fine. If you cannot hold a finger on it for five seconds, you are past where I would run it continuously. Skin pain starts around 50 °C, comfortably below where enamel gives up, so your finger is a conservative and perfectly good thermometer.

Two structural mitigations beyond amplitude:

And the pleasant surprise: rotation helps. Spin it and back-EMF opposes the supply, cutting current, while the moving rotor stirs air inside the can. A singing motor that is also turning runs measurably cooler than a stalled one at the same drive level. Turning spin up is a thermal improvement as well as a party trick.

Part 10: making it louder (and only one of these is electrical)

The first thing I reached for was amplitude. The first thing I should have reached for was a screwdriver.

1. Mount it to something. This is by far the biggest win and it is free. Go back to part 1: what the motor lacks is not force, it is radiating area. A bare motor on the bench is a voice coil with no cone. Clamp it firmly to a large thin panel - a project box lid, a sheet of cardboard, an empty tin, the underside of a desk - and that panel becomes the cone. This is not a marginal gain; it is the difference between “is it on?” and “please stop”. Firm mechanical contact matters more than what the panel is made of. Every gram of that steel can is fighting to not make sound, so stop asking it to and give the vibration somewhere to go.

2. Find the resonance. The motor plus whatever you bolted it to has mechanical resonances - frequencies where the structure moves far more for the same force. The gap between a resonant note and a dead one can be enormous, and it is free volume sitting there waiting for you.

So the firmware has a sweep mode: it walks a chromatic scale from 100 Hz to 1600 Hz, holding each step for a quarter second and printing the frequency as it goes. Listen for where it suddenly leaps out at you, then transpose the piece into that band.

One detail in that sweep that is easy to get wrong: it multiplies by the twelfth root of two each step rather than adding a fixed number of hertz. Musical frequency space is multiplicative, so a geometric sweep spends equal time in every octave; a linear one would crawl through the bass and sprint through the treble, which is exactly backwards from how your ear wants to survey the range. Run it after mounting the motor, because the mount is half of what sets the resonances.

3. Transpose up regardless. Small motors are usually deaf below about 200 Hz - the structure is stiff and light, so it simply does not want to move slowly. The bass in Für Elise goes down to C4 at 262 Hz, and on my setup those notes are noticeably weaker than the E5s. transpose 12 fixes it, and because everything scales by the same ratio the piece stays perfectly in tune with itself. It also, per part 9, moves the whole piece into a slightly less thermally punishing region.

4. Raise the supply voltage. Once amplitude is at 255 the only electrical lever left is VM on the DRV8833. It works, and it obeys the same quadratic heating law, so it is the lever to pull last and with the numbers from part 9 in front of you.

5. Give it a cone, sort of. An off-centre mass on the shaft - the trick inside every phone’s vibration motor, an ERM, eccentric rotating mass - couples far more energy into the mount. A nut and a blob of hot glue is the entire build.

Closing thoughts

What I love about this detour is that nothing new was needed. Same motor, same driver, same two pins, same LEDC peripheral. Every single piece of hardware was already sitting on the bench doing its day job. The only thing that changed is that I stopped treating the motor’s whine as a defect to suppress and started treating it as a channel to modulate.

And the idea that made the whole thing tractable is one line long: the average is speed and the swing is sound, and they do not interfere. Once you have that, everything else is bookkeeping. Silence stops meaning “off” and starts meaning “zero swing, hold the average”. Volume stops being a mystery and becomes a duty cycle. Spinning-while-singing stops being a paradox and becomes a sin(πt) curve you can read a compromise off.

The correction I had to make to myself along the way is the one I want to keep. I assumed the coil’s inductance would tame the current at audio frequencies, because it felt right - fast switching, inductors resist change, surely. Then I worked out τ = L/R, put the half-periods next to it, and found the current fully settled at every pitch in the piece. My comfortable assumption was worth nothing and the two-line calculation was worth everything. Same lesson as the whole motor library, really: check the number, do not admire the intuition.

Try it on your own board

This one is meant to be played with, so it ships as a ready-to-flash binary rather than a source zip - and it flashes straight from the browser, no toolchain, no libraries, no Arduino IDE:

Try it yourself: Für Elise on your own ESP and motor

That page has a button that flashes your board from the page itself, and every step from wiring the H-bridge to typing play. It needs any ESP32-family board (the original, S2, S3, C3, C6 - the PWM peripheral it leans on is on all of them), any H-bridge motor driver, and any small brushed DC motor. If you have followed these logs you already own all three.

Everything in the tables above is adjustable live over the serial console and saved to flash. Please read part 9 before you turn the amplitude up. ♥️

Next up I get back to actual work: two TMAG5273s on one I2C bus, and this control library meeting the balance loop. Ariwoo has a robot to become.

But it can play Beethoven now. That felt worth a log. ♥️

Click for the glossary (every term in this log, defined)
Click for every control the firmware exposes

Because this ships as a binary rather than source, nothing is baked in at compile time - every one of these is settable at runtime over the serial console and saved to flash, so your settings survive a reboot.

Setting Default What it does
in1 / in2 4 / 5 The two H-bridge input GPIOs. Set these first - your board is not my board
carrier 20000 Ultrasonic PWM carrier in Hz. Keep above ~18 kHz or you will hear it
amplitude 96 Coil current: volume and heat, and heat goes as its square. Deliberately conservative - read part 9 before raising it
spin 0 DC bias, −255…255. Sign is direction, 0 stands still and buzzes
spinmode antiphase antiphase = louder; unipolar = cooler and half as loud
transpose 0 Semitones. 12 moves the piece up an octave, in tune with itself
tempo 140 Milliseconds per sixteenth note
gap 18 Articulation silence per note
pause 2500 Pause between repeats. Raise it to give the winding a rest
once 0 1 = play through once and stop
guardplay / guardrest 120 / 60 Thermal duty guard: after this many seconds of playing, force a cooling rest. 0 disables

Commands are play, stop, sweep, show, set <name> <value>, save, defaults.

Note the amplitude default is 96, not the 180 I run on my own bench. I know my motor; a binary handed to strangers does not know theirs, and part 9 explains exactly why an over-eager default is the one setting here that can actually damage something. Turn it up deliberately, with a finger on the motor can.

Thank you for reading all the way down here. Go and mount your motor to a biscuit tin. ♥️