Schema A turns a motorSchema B needs a bigger supply24 V, 20 A per axis

BTS7960 Motor Drive Wiring Set

Two wiring schemas for the brushed-motor build - bench (one axis, the supply you already own) and operational (three axes) - plus the tuning the machine needed before a geared 24 V motor would break away from standstill.

Chapter 01

What to build, in what order

One motor on the desk today with the supply you own; three motors only after the 24 V rail can hold 60 A.

Bridge inputs per axis 2 RPWM + LPWM, no inverter
MCU pins used 7 6 PWM + 1 shared enable
Bench supply 10 A enough for one unloaded motor
Operational draw 60 A 3 x 20 A rated, 1440 W

Four decisions carry the whole set, and the bench run settled the fourth one.

  1. Two pins per axis, not one. A BTS7960 module is two independent half-bridges. Direction is chosen by which input carries the duty, so an axis needs two PWM lines. Six PWM pins on the board cover three axes.
  2. No inverter, no PWM expander. An inverter would let one pin do the job through locked antiphase, but it also keeps both bridges switching at standstill and heats the motor for nothing. Six PWM pins exist on the board, so the simple path wins.
  3. The enable line is the safety, and it needs one resistor after all. Both inputs idle low once the sketch owns them, which on this bridge means brake - not coast, and not a runaway. inhibitAllOutputs() is the first statement in setup(), before the serial port and before any delay, so the eight candidate GPIOs are driven low as early as software can reach them, and writeMotors() raises the enables only after the first duty is on the pins. What software cannot cover is the boot ROM window before setup() runs: the ESP8266 holds GPIO2 high through its own boot strap, and a floating R_EN reads as enabled, which drove one axis at full duty for the first fraction of a second and made the head jump at power-up. Fit a 10 kOhm pull-down from each module's jumpered R_EN/L_EN pair to GND; the bridge is then inhibited from the instant the rail comes up, whatever the board's pins are doing. R_EN and L_EN stay jumpered together, so one board hole still enables both halves.
  4. Duty is average motor voltage, so the floor is a machine setting. The first build scaled duty from a 15 percent floor to full at 180 deg/s. A 60 deg/s pan slew therefore asked for 24 percent duty, which is about 5.8 V on a 24 V rail: not enough to break a geared motor away from standstill. The shipped floor is now 40 percent at 90 deg/s full scale, and both numbers change live through /tune.

What the bench actually proved

D9 + D5 on the pan axis: 122 deg of travel, duty peaking at 100 percent, GPIO16 high, and the motor turning. The three faults that stood between wiring and rotation were all on the power side, not in the firmware: module VCC on 3V3 instead of 5 V, no common point between the 24 V return and the board ground, and the motor across the supply instead of M+ / M-.

Check the supply label before wiring anything

An S-120-12 label reads 12 V / 10 A / 120 W. The 24 V member of that family is the S-240-24 at 24 V / 10 A / 240 W. On a 12 V rail a 24 V motor turns at roughly half speed and half torque, which reads exactly like a "weak driver" fault while the wiring is perfectly fine.

Chapter 02

The four parts, as they actually terminate

Terminal order, printed ratings and the numbers that size fuse, wire and heatsink.

Component plate with terminal layouts and ratings

Five panels showing the PSU screw terminal order L N PE minus-V plus-V, the BTS7960 module with B+ B- M+ M- screw terminals and the eight-pin logic header, the Wemos D1 pins used by the motor build, the 350 W gear motor with its two leads, and the circuit protection (fuses and cooling only; no added passives).
Vector reconstruction, not a photograph. Terminal order and logic-header order follow the printed silkscreen of the common 43 A module; electrical figures are from the Infineon BTS 7960 data sheet Rev. 1.1 (2004-12-07).
PartFigure that mattersWhere it comes from
Bridge supply window5.5 - 27.5 V, lock-out above 27.6 Vdata sheet 4.3.4 / 4.1.1
Current limit43 A typ low side at 25 C (34 A min, 61 A max)data sheet 4.3.7
Path resistance16 mOhm typ at 25 C, so 6.4 W at 20 Adata sheet basic features
PWM ceiling25 kHz with active freewheelingdata sheet 4.2
Logic inputhigh from 2.0 V, absolute limit 5.3 Vdata sheet 4.4.6
Current sense8500:1, so 1 kOhm gives 1 V per 8.5 Adata sheet 4.4.4
Thermal path1.0 K/W junction to case, 35 K/W to air on 6 cm2data sheet 5
Motor350 W, 24 V, 20 A ratednameplate, not yet measured

3.3 V drives the inputs, but `VCC` still wants 5 V

The input threshold is high from 2.0 V worst case with a 5.3 V ceiling, so an ESP8266 pin is inside the window with margin. No level shifter, no 5 V buffer. The module's own VCC logic supply is a different pin and is not optional: on 3V3 the gate drive never opens fully, which reads as a dead motor while every signal measures correct. It draws about 10 mA, so the board's 5V pin carries it.

Chapter 03

One axis, two inputs, one enable

The pin map split by header strip, the bridge truth table, and what a hold-and-reverse cycle looks like on the wires.

Pin map, bridge truth table and one hold-and-reverse cycle

Pin map table assigning pan to D9 and D13, tilt to D10 and D12, jaw to D11 and D15, enable to D2 and D4; bridge truth table showing coast, brake and both drive directions; timing chart with the smoother rate trace, the RPWM duty trace rising from a minimum floor, and the LPWM trace taking over after a 60 ms zero-drive dwell.
Truth table condensed from the BTS 7960 data sheet section 4.4.5. Timing shape follows the shipped tuning: 90 deg/s full scale, 40 percent break-away floor, 60 ms reversal dwell, both duty numbers live-tunable.

The two header strips are split by function, so one ribbon leaves each side: PWM pairs on the top strip, enable lines on the bottom strip. Every ESP8266 GPIO appears twice on the Uno footprint, which is what makes the split free - the labels below name the hole this layout uses, not a different signal.

AxisRPWM, top stripLPWM, top stripEnable, bottom strip
PanD9 / GPIO2D13 / GPIO14D2 / GPIO16
TiltD10 / GPIO15D12 / GPIO12D4 / GPIO4
JawD11 / GPIO13D15 (SCL) / GPIO5D14 (SDA) / GPIO4

D4 and D14 are the same GPIO4, so modules 2 and 3 get their own hole from one line. Three modules, three holes, no splitter - and R_EN plus L_EN stay jumpered at each module, which is what turns one hole into two enabled half-bridges.

AxisBench proof
Pan122 deg of travel, duty to 100 percent, EN 1, motor turns
Tiltpins verified from the core variant file, module not yet fitted
Jawsame; GPIO5 is D15/SCL on the top strip and D3 on the bottom
EnableEN 0 at boot, EN 1 after the first duty write

Chapter 04

Schema A - bench, one motor

Turns a motor

One module, one motor, the 10 A supply already on the desk, the bring-up order that works, and the four faults that stand between a correct schematic and a turning shaft.

Schema A - single axis bench wiring

Bench schematic from mains through a 24 V supply, a 30 A fuse, one BTS7960 module to one 350 W motor; a separate USB charger powers the Wemos D1, whose 5 V and ground feed the module logic directly, with two PWM lines and one enable line direct to the header; all grounds return to a single star point at the supply.
Wire colours match the legend in the figure. Gauge and fuse values are printed on each run.

The supply question answers itself at this size: one motor spinning free draws its no-load current, which is a small fraction of the 20 A nameplate. That is what makes the existing 10 A unit adequate for this test. Loading the shaft or stalling it is out of scope here, because the draw then climbs past what the supply can hold.

RunGaugeCarriesNote
Mains to supply1.5 mm2220 V, ~1 A2 A fuse and a switch in the live wire, PE to the case
+V to B+2.5 mm2 (AWG 13)24 V, bench load30 A blade fuse in this run, keep under 1 m
-V to B-2.5 mm2 (AWG 13)24 V returnsame length as the positive run
M+ / M- to motor2.5 mm2 (AWG 13)chopped 24 Vswap the pair to invert the turning direction
Wemos 5V to VCC0.35 mm25 V, ~10 mAlogic supply only, never the motor rail
Wemos GND to module GND0.5 mm2logic returnmodule ground then goes to the star point
Signal lines0.35 mm23.3 V, 2 kHzdirect wire from board pin to module pin, no series resistors

Bring-up order

The Output Pin Test card in the simulator is the instrument: it commands the axis, then reports the duty the board is really writing, the enable state and the travel it measured. No multimeter is needed to prove the firmware side.

  1. Supply alone: 24.0 V +/- 0.5 V at the module's B+ / B- screw terminals, not at the supply terminals.
  2. Board alone, over USB: the page auto-connects, telemetry arrives, EN 0, all six duties 0, and the shaft turns freely by hand.
  3. Pin test on one axis: RPWM 100% 3s holds one input at full duty for three seconds. M+ to M- must read close to the rail while the hold is live.
  4. Full pin test: travel above 30 deg with duty peaking near 100 percent and EN 1 is a pass. The card prints the verdict.
  5. Duty floor: if the shaft hesitates from standstill, raise the floor slider until it breaks away cleanly. That is a live change, not a reflash.
  6. Failsafe: pull the client. The axis stops inside 350 ms and holds, because both inputs low with enable high is a brake.
  7. Thermal: one minute of motion must leave the module plate under 60 C.

Four faults, in the order they actually happened

Every one of these reads like a dead driver while the signal side measures perfect. Module VCC on 3V3 instead of 5 V, so the gate drive never opens fully. No common point between the 24 V return and the board ground, so a 100 percent duty has no reference. The motor wired across the supply instead of M+ / M-, so the bridge drives nothing. And a duty floor too low for the gearbox: 24 percent of 24 V is 5.8 V, which a geared motor answers by sitting still.

Chapter 05

Schema B - operational, three motors

Needs a 24 V rail that holds 60 A

The same logic wiring three times, plus bus fusing, an emergency stop and a supply sized for the sum.

Schema B - three axes and the roar on one fused 24 V bus

Operational schematic with a 24 V source feeding an 80 A main fuse and a latching emergency stop into positive and return bus bars, three 30 A branch fuses each feeding one BTS7960 module and one 350 W motor, and a control side where a USB charger powers the Wemos D1 that supplies 5 V, ground and three separate signal wires per axis: enable, RPWM and LPWM. A fourth row carries the audio chain on the same board and the same ground: a 24 V tap into a 12 V buck converter, the DFPlayer Mini fed by 5 V, ground and the D8 serial line, its DAC and ground pins feeding the TDA7297 inputs, and the amplifier driving the TOA horn. A protection strip below lists the fuse order, the amplifier supply limit and the jaw gate.
Branch values come from the 20 A nameplate; bus values from the 60 A sum. Both source options are drawn because the choice is a cost decision, not an electrical one. The sound chain sits on the same sheet because it shares the board, the ground and the 24 V rail.

Where the 24 V current goes when all three axes pull rated load

Pan axis, rated20A
Tilt axis, rated20A
Jaw axis, rated20A
Branch fuse, each30Aopens above rated, below the bridge limit
Bridge current limit43Atyp, per half-bridge
Rated nameplate draw per axis at 24 V; the bridge limit and fuse values are the protection points around it.
ItemValueWhy that number
Source24 V at 62.5 A, or a 24 V battery plus charger3 x 20 A = 60 A, 1440 W
Main fuse80 A ANLabove the 60 A sum, protects the bus bar
Emergency stopDC-rated, 60 A, in the +24 V railalso cuts the enable line, so axes go limp
Bus bars13 mm2 (AWG 6)60 A continuous with margin
Branch runs4 mm2 (AWG 11), under 1.5 m20 A rated draw per axis
Power wiring4 mm2 (AWG 11)direct B+ and B- to bus / supply, no extra capacitors needed
Coolingheatsink plus airflow per module6.4 W at 20 A through 16 mOhm

A 24 V rail leaves 3.5 V of headroom before the bridge locks out

The BTS7960 module includes its own onboard decoupling and buffer. Direct wiring from the 24 V rail to B+ / B- and from the board pins to the logic header has been bench-verified with the motor under full load.

id: blueprints
title: Print sheets - the two wiring blueprints
nav: Blueprints
eyebrow: Chapter 06
tag: target Print these
lead: The same two schemas as connection blueprints: pictorial, monochrome, with a title block, a bill of materials and the notes a builder needs at the bench.

The block schematics above answer "what connects to what". These two sheets answer "what do I hold in my hand, and which hole does this wire go into". They are drawn in black and white on purpose: line weight and dash pattern carry the net, so a laser print and a screen read the same, and a photocopy loses nothing.

Sheet 1 of 2 - bench test wiring, one axis

Monochrome connection blueprint. Top row: a 24 V enclosed switching supply with its seven-terminal barrier strip, a 5 V USB charger, and a 350 W brushed gear motor. Bottom row: a Wemos D1 with its used holes labelled 5V, GND, D2, D9 and D13, and a BTS7960 module with its eight-pin header, two chips, aluminium plate and four screw terminals. Direct wires connect the board pins to the module header, a jumper links R_EN to L_EN, and a common-ground callout points at the module GND pin. No external resistors or capacitors are used. A legend maps line styles to nets, seven notes list the faults each rule prevents, and a title block carries the drawing number and the verified status.
Sheet ADR-W-001. Status line records the measured result: 122 deg of travel on the pan axis with duty peaking at 100 percent.

Sheet 2 of 2 - operational wiring, three axes and the audio chain

Monochrome connection blueprint for the three-axis build, with every conductor drawn as its own line. A 24 V 62.5 A supply and a dashed battery alternative feed an 80 A ANL fuse and a latching mushroom stop, then two vertical bus bars carry the return and the plus 24 V down the sheet. Three identical branches hang off them, each with a 30 A fuse, a BTS7960 module with its eight-pin header and four screw terminals, and one 350 W motor for pan, tilt and jaw. At the right, the Wemos D1 shows all twelve used holes including D8 for the DFPlayer serial line, and nine separate signal wires leave it to the module headers; three enable wires pass through a three-pole auxiliary contact ganged to the mushroom. The bus bars continue down into a fourth band that carries the audio chain: a 24 V to 12 V buck converter, the TDA7297 amplifier, the TOA horn, the DFPlayer Mini with its micro-SD slot, the eleven audio runs listed by name, and three audio notes covering the amplifier supply limit, the DAC pins and the jaw gate.
Sheet ADR-W-002, revision C. Forty-one runs, direct wiring, no added resistors or capacitors: nine signal wires, six motor leads, six branch conductors, seven supply conductors, two chained logic wires and eleven audio runs. Status line is honest, because it needs a 24 V rail that holds 60 A.
SheetCoversStatus on the title block
ADR-W-001one axis, one module, one motor, the supply on the deskverified on hardware
ADR-W-002three axes, bus bars, branch fusing, emergency stop, audio chainnot built, needs a 60 A rail

Both sheets carry the same conventions: a reference designator on every part (PS1, U1, M1, F2, C1, R4), a filled dot for a joint, a crossing without a dot for two wires that only pass, and a hollow circle for an optional joint such as the battery source. Neither sheet bundles or hides a conductor. Where three wires do the same job on three axes, the sheet draws three wires, because a bundle on a drawing is a wire nobody strips.

Chapter 07

What changes in the firmware

One build, no flag

One header for the mapping, one output stage, two live-tunable numbers. The kinematics, the HTTP API and the chain playback are untouched.

The brushed motor cannot consume an angle, and the smoother already tracks angular velocity, so the conversion is one mapping instead of a new control loop. Holding a direction ramps the commanded angle, the ramp is a rate, the rate becomes duty - which means the smoother's ramp is the motor's soft start. That ramp is jerk-limited: the commanded acceleration slews toward its demand over jerkRampSec, so the duty a bridge sees rises on a curve rather than a step.

SeamFileClassification
Rate to duty mapping, dead rate, break-away floor, reversal dwellfirmware/libraries/DinoMotion/src/dino_drive.hadd
Output stage, writeMotors()firmware/d1_wifi_receiver/d1_wifi_receiver.inoreplaced writeServos()
Second input per axis and the shared enable pinsame sketch, ESP8266 pin blockadd
PWM range and frequency, enable held low at bootsame sketch, setup()add
Live tuning, /tune and $tunesame sketch, HTTP and serial handlersadd
Bench duty hold, /drive and $drivesame sketchadd
Kinematics, HTTP API, chain playbackunchangedreuse
Jerk limit and braking-distance speed capfirmware/libraries/DinoMotion/src/fourier_smoother.hadd
Mapping assertionssim/dino_sim.cpp, driveTest()add
Servo output stage, Servo.h, the ESP32 branch, DINO_DRIVE_BTS7960deletedremove

One sketch, one command:

arduino-cli compile --fqbn esp8266:esp8266:d1 \
  --libraries firmware/libraries firmware/d1_wifi_receiver

It prints [Drive] 3x BTS7960, PWM 2000 Hz, min duty 40%, bridges inhibited until first write at boot, and lands at 30 percent flash and 45 percent RAM.

Two numbers per axis are machine settings, not constants, so they move without a reflash:

RouteFormEffect
GET /tune?axis=0&floor=650&full=90HTTP, key-gatedpan floor to 65 percent, full scale to 90 deg/s
$tune 0 650 90USB serialsame, over the cable
GET /statusHTTPreports tune[3] so the page can show what the board holds
GET /drive?axis=0&duty=1000&ms=3000HTTP, key-gatedholds one bridge input at a fixed duty, then expires
GET /smooth?w=12&amp=2.4&hz=0.25&jerk=0.12HTTP, key-gatedfilter cutoff, breathing, and the jerk ramp that shapes every duty climb
$smooth 12 2.4 0.25 0.12USB serialsame, over the cable

The floor is the value that decides whether a geared motor starts at all. Raise it until the shaft breaks away cleanly from standstill; lower it if the axis lurches.

rate below the dead rate

Holding - both inputs low, enable high

The axis brakes rather than coasts, because both bridge halves pull the motor terminals to ground. A head on a worm gear stays where it was left.

Permitted

new direction command, chain playback

Always available

current sense readable

Reason code

rate < 3 deg/s

Open loop, and the doc says so

Nothing on this drive measures where the joint actually is. The firmware knows the angle it commanded; friction, load and a missed dwell all leak real position away from that model. The software clamp on each axis is the only limit there is. The upgrade is one 10 kOhm potentiometer per joint into an ADS1115, which needs the I2C pair - and this layout spends GPIO5 on the jaw and GPIO4 on the enables, so joint feedback and a third axis compete for the same two pins. That is the same wall as the axis count: it moves when the controller moves.

Chapter 08

Sound matching: DFPlayer Mini, TDA7297 amp and TOA speaker

Sound matching

One Wemos D1 owns the jaw motor and DFPlayer. Closing the jaw stops the recording; reopening starts it again. Physical jaw and speaker timing still require a bench test.

Playback behavior

The receiver uses its smoothed jaw angle for both motor output and audio control. It does not need a browser to run an armed chain. The angle is a software estimate, not a measured linkage position.

StateSame Wemos D1 and DFPlayerBrowser preview without hardware
Jaw opens to at least 38 degreesSet volume, then start the selected SD trackStart the recorded roar once
Jaw moves between 25 and 65 degreesScale volume to the configured ceiling, rounded to DFPlayer stepsSmoothly adjust gain to the jaw opening
Jaw reaches 25 degrees or lessSend STOP, bypassing normal command spacingFade to zero over 20 ms and stop the source
Jaw reopens after closingRestart the selected track from its beginningRestart the recording from its beginning
Recording ends while the jaw stays openNo automatic restart; close and reopen for another roarNo loop and no stacked sources
Browser disconnects during an armed chainBoard continues the chain and jaw-driven audioBrowser is not required for physical playback
USB or Wi-Fi controls a boardBoard owns physical soundBrowser audio stays silent to avoid duplicate sound

Firmware volume and PLAY commands are spaced by at least 100 ms. Initial volume is sent before PLAY, so crossing the opening threshold is not a promise of immediate speaker output. STOP bypasses this spacing. A real DFPlayer can add its own playback latency.

Load the roar and control playback

  1. Format the micro-SD card as FAT32. Copy the recorded roar as the first file onto an empty card. Command 0x03 selects storage index 1, not a filename such as /mp3/0001.mp3.
  2. Connect the DFPlayer to the same Wemos D1 using the wiring below. Use its DAC output for the amplifier input.
  3. Open and close the jaw with the existing controls or an armed chain. No second Arduino or browser sound command is needed.
CommandEffect
/sound?track=N or /sound?play=1Select the track for the next jaw-gated start; does not force sound with a closed jaw
/sound?vol=25Set the jaw-volume ceiling within DFPlayer's 0..30 range
/sound?stop=1Stop and inhibit playback until the jaw closes; a later opening can start again
$sound 1 25 / $sound stopSerial equivalents for track/ceiling selection and stop

HTTP sound commands require k=<device key> when a key is configured. The TDA7297 amplifier needs a separate 12 V supply or buck converter from the motor rail; its chip supply range is 6.5..18 V. Never feed it directly from 24 V.

The sound chain is not a separate drawing. It sits on both sheets above, because it shares the board, the ground and the 24 V rail with the motors: Schema B carries it as a fourth row, and sheet ADR-W-002 draws its eleven runs conductor by conductor with the buck converter, the amplifier, the horn and the DFPlayer in the same title block as the drive stage.

ComponentRating / ModelWiringPurpose
Audio PlayerDFPlayer Mini5 V from WeMos, RX to D8MP3/WAV playback from micro-SD
Micro-SD CardFAT32, up to 32 GBin DFPlayer slotRoar copied first for index 1
AmplifierTDA7297 dual-bridge12 V DC (buck from 24 V)15 W + 15 W power stage
Horn SpeakerTOA 8 Ohm / 30 WSPK_OUT+ and SPK_OUT-Directional, high-decibel roar
Control Line9600 baud 8N1 serialWeMos D8 -> DFPlayer RXPacket: 7E FF 06 03 00 00 01 FE F7 EF

Use the DFPlayer DAC output for an external amplifier, never its bridged speaker output. Do not connect either TDA7297 speaker terminal to ground. See the DFRobot connection guidance and command implementation.

Run py -E tools/test_jaw_audio.py to check the receiver scheduler's emitted commands for opening, partial closure, immediate STOP, reopening and operation without client input. This checks software packets, not electrical UART delivery or speaker output.

Chapter 09

Three axes is the ceiling, and a head wants more

A convincing neck needs four or five channels, this board holds three, and the gap has exactly one cheap answer.

Nine GPIOs on a Wemos D1 can drive an output: 0, 2, 4, 5, 12, 13, 14, 15, 16. Three are spoken for before any motor: GPIO1 and GPIO3 carry the serial telemetry, and GPIO6 to GPIO11 belong to the flash chip. GPIO0 looks free and is not: a BTS7960 input holds its line low, and GPIO0 low at reset selects UART flash mode, so the board would never boot.

That leaves eight. Six become the three PWM pairs and two become the enable lines, which is exactly zero spare. A fourth axis is not a wiring problem, it is a controller problem.

Usable outputs 8 GPIO0 excluded, see above
Spent on PWM 6 two per axis, no inverter
Spent on enable 2 GPIO4 reaches two holes
Spare 0 a 4th axis needs new silicon
ControllerPWM availableAxes with 2 PWM + enableWhat it costs
Wemos D1, ESP82668 usable outputs3shipped today, zero spare pins, IRAM at 94 percent
ESP32 dev board16 LEDC channels, ~25 usable GPIO8one bounded port, per-axis enable becomes real
ESP8266 + PCA968516 channels over I2C, 2 board pins5 with enables on the same chip1526 Hz ceiling, I2C no longer free for joint feedback, and the ESP8266 is already nearly full
One board per group3 axes eachunboundedtwo boards in one head means two playheads, so neck and jaw drift apart

What a head actually needs

Smoothness and axis count are different problems. A fourth motor buys a new degree of freedom, not smoother motion: jerk comes from gearbox backlash, a duty floor set too low, a reversal dwell too short for the mass, and a jerk ramp set faster than the linkage can absorb. Tune those four before buying a motor. What more axes buy is the look of a living neck - a base joint that leads and a head joint that follows.

ChannelJobDrive
Neck yawlook left and rightbrushed, BTS7960
Neck pitch, upperraise and drop the skullbrushed, BTS7960
Neck pitch or roll, basethe lead-and-follow that reads as alivebrushed, BTS7960
Jawmandiblebrushed, but a 350 W motor is oversized for it
Eyes or blinksmall, fast, lighta 9 g hobby servo on one pin, no bridge

That is five channels, four of them bridges. The plan is therefore an ESP32 with four BTS7960 modules plus one servo pin, and the ESP8266 rig stays exactly as it is until the mechanics for the fourth joint exist.

The port is bounded, because the logic is already Arduino-free

dino_motion.h, fourier_smoother.h, dino_autonomy.h and dino_drive.h carry the kinematics, the smoother, the chain player and the rate-to-duty mapping with no Arduino include, and they compile on the host today. An ESP32 port therefore touches the sketch only: the pin map, analogWriteRange and analogWriteFreq become analogWriteResolution and analogWriteFrequency, and ESP8266WebServer becomes WebServer. Nothing in the motion math moves.

Do not buy the fourth axis by deleting the enable line

Four axes fit the ESP8266's eight outputs exactly - and only by leaving zero pins for enable. Tying R_EN and L_EN to 5 V instead would drop the one invariant that keeps a reset from twitching a motor, and it would do so on the build with the least flash headroom. The pin arithmetic says three; respect it and change the controller instead.

Chapter 10

Proof already on record

Host checks verify motion and emitted audio commands. Browser measurements verify closed-jaw silence. Physical DFPlayer output, actual jaw timing and the remaining motor measurements are separate bench checks.

CheckCommandResultState
Rate-to-duty mapping, dwell, rollover, single-input invariantsim/dino_sim.exe --testdino_drive self-check: all assertions passedverified
Kinematics and failsafesim/dino_sim.exe --testdino_motion self-check: all assertions passedverified
Chain playbacksim/dino_sim.exe --testdino_autonomy self-check: all assertions passedverified
Pin map validation, both sidessim/dino_sim.exe --test, bun tools/parity.mjsdino_pins self-check: all assertions passed; 156 pin maps judged identically (dino_pins.h vs dino_pins.js)verified
Pin map over the wireheadless page with a faked boardduplicate GPIO refused locally with 0 requests sent; Apply sends all nine roles on /pins; a board-reported remap overrides the selectors; /pins?panr=0 answers 400 not_output on panr; Peta default sends /pins?reset=1verified
Simulator parity, kinematicsbun tools/parity.mjsparity: 71 frames identicalverified
Simulator parity, smootherbun tools/parity.mjsparity: 180 smoother frames identical (fourier_smoother.h vs fourier_smoother.js)verified
Jerk bound and no overshootsim/dino_sim.exe --testfourier_smoother self-check: all assertions passed; peak jerk equals the configured ceiling, first tick after a step only ramps, overshoot below 0.05 degverified
Jerk before and after the limiter30 deg step at 150 deg/s, 60 fps71 986 -> 10 000 deg/s3, the configured ceiling; acceleration ramps 167, 333, 500, 667 instead of jumping to 1200verified
Browser jerk readout and live traceheadless page, mode 3, 60 deg stepjerk chip peaks at 4533 deg/s3 with a 0.30 s ramp and never above; angle stops at 119.47 of 120verified
Three speed sliders and the wireheadless page with a faked board/speed carries p=60&t=60&j=96 at follow 60 x convert 1.00, p=150&t=150&j=240 at convert 2.50, p=100&t=100&j=160 at follow 40verified
Raw target rate independent of frame rateheadless page, CPU throttled 1x, 6x and 20x25.6, 23.9 and 25.0 deg/s measured against a 25 deg/s slider, no backward stepverified
Setting slots and resetheadless pagesave, load, reload and reset restore every knob; both saved slots survive the resetverified
Board reaches the smoothing slidersheadless page with a transportfour /smooth?w=..&amp=..&hz=..&jerk=.. requests, one per sliderverified
Board actions refuse to lie when offlineheadless page, no transportdisarm and tune print Belum ada koneksi board with zero requests sent; board-only controls disabledverified
Audio packets, jaw trigger and gain mappingsim/dino_sim.exe --testdino_sound self-check: all assertions passedverified
Receiver audio lifecyclepy -E tools/test_jaw_audio.pyStart during changing volume, partial fade, immediate STOP, reopen, offline operation and stop inhibition passedverified
Browser open-close-reopen outputDeployed build 23437e2a, real jaw slider and Web Audio analyserPeak amplitude 0.331 -> 0 -> 0.331verified
Browser suppression when hardware connectsLocal browser analyser, connection-state simulationPreview RMS 0.1236, connected RMS 0verified
Firmware buildpy -E tools/flash.py build326 532 B flash (31 percent), 45 percent RAM; runtime pin map plus boot inhibitverified
Physical jaw and DFPlayer synchronizationFlash receiver, load SD recording, exercise jaw and standalone chainNo board detected during the audio verification run; UART delivery and speaker timing not measuredopen
Enable line raised only after the first dutytelemetry tail on USBEN 0 at boot, EN 1 after the first commandverified
Power-up jump ("kaget")Reported on the bench: the head jerked at every power-upCause: pins unclaimed until setup() reached its pin block, roughly 300 ms in, while GPIO2 sits high through the ESP8266 boot strap and a floating R_EN reads as enabled. inhibitAllOutputs() is now the first statement in setup(), ahead of Serial.begin and the 200 ms delay. Measured on the board after flashing: [Boot] every output pulled low at 79 msverified
Boot ROM window before setup()Same report; cannot be reached from softwareRequires a 10 kOhm pull-down from each jumpered R_EN/L_EN pair to GND, so the bridge is inhibited from the instant the rail comes upopen
Runtime pin map on the boarduv run tmp/pins_serial.py COM3, COM3 attached$pins 0 ... rejected not_output on panR, a duplicate rejected duplicate on en2, the remap 2,14,15,12,13,4,16,16,0 applied, survived a reset from EEPROM, and $pins reset restored 2,14,15,12,13,5,16,4,0 across another resetverified
Pin map in the telemetry tailsame runPins:2,14,15,12,13,5,16,4,0 present on every line; the removed sweep leaves no Cal: or Lim: segmentverified
Desktop app flashes the boardbuilt exe on its headless seam, uv run tmp/watch_console_windows.py --deploy COM3Compiled and uploaded to COM3, esptool wrote 365 744 B; 0 console windows, where the same run before the fix showed one arduino-cli.exe window per callverified
Upload sync flakethree deploys through the appone Timed out waiting for packet header in three attempts on a healthy board; a single retry of that one message is now built in, go test ./... pins it to the upload stageverified
Axis card names no fixed pinheadless pagechip reads GPIO2 / 15 / 13 and follows a remap to GPIO2 / 15 / 4; sliders carry the model limits (30-150, 60-120, 20-70) and the presets read 30/60/90/120/150, 60/75/90/105/120, 20/37/54/70, so no control offers an angle the board would clampverified
Single-input invariant on the wirestelemetry Drive:motor,... during a pan sweepRPWM ramps while LPWM stays 0, and the reverseverified
Duty ramp after the tuning changepan to 150 deg over USB1000 -> 752 -> 472 -> 0, was 1000 -> 442 -> 209verified
Pan axis under powerOutput Pin Test card, D9 + D5122 deg of travel, duty peak 100 percent, EN 1, motor turnsverified
Tilt and jaw axessame card, D10 + D6 and D11modules not yet fittedopen
Motor no-load currentclamp meter on B+to be measuredopen
Module temperature at 20 AIR thermometer on the plate after one minuteto be measuredopen
  1. S1

    Infineon BTS 7960 High Current PN Half Bridge NovalithIC data sheet, Rev. 1.1, 2004-12-07 - supply window, current limit, path resistance, logic thresholds, current sense ratio, truth table, thermal resistance and layout notes.

    https://www.infineon.com/assets/row/public/documents/10/57/infineon-bts7960-ds-en.pdf
  2. S2

    NXP PCA9685 16-channel 12-bit PWM controller data sheet, Rev. 4, 2015-04-16 - checked and set aside: 1526 Hz maximum output frequency, and the board already has six usable PWM pins.

    https://www.nxp.com/docs/en/data-sheet/PCA9685.pdf
  3. S3

    DFRobot DFPlayer Mini connection guidance - DAC outputs feed an external amplifier; SPK outputs drive a small speaker directly.

    https://wiki.dfrobot.com/dfr0299
  4. S4

    DFRobotDFPlayerMini command implementation - storage-index play 0x03, volume 0x06 and stop 0x16.

    https://github.com/DFRobot/DFRobotDFPlayerMini/blob/master/DFRobotDFPlayerMini.cpp
  5. S5

    Repository firmware and host assertions - dino_drive.h, d1_wifi_receiver.ino output stage, driveTest() in sim/dino_sim.cpp.

  6. S6

    Part labels supplied by the owner - 350 W 24 V 20 A brushed gear motor, BTS7960 43 A module, S-120-12 supply, 5 V USB charger.