14 KiB
TranspOtter
A hoverboard mainboard driven as a differential-drive robot base, with an ESP32-C3 Supermini as the motion controller.
| Folder | What it is |
|---|---|
hoverboard-firmware-hack-FOC/ |
The STM32 mainboard firmware (EFeru), configured here for heavy loads and programmatic moves |
esp32/ |
PlatformIO project for the ESP32-C3 Supermini: jerk-limited motion planning, speed-dependent steering, closed-loop distance and rotation |
The mainboard runs closed-loop speed control on each wheel. The ESP32 decides what the wheels should do and shapes every change in command so a loaded chassis never lurches.
Wiring
The ESP32 hangs off the left sideboard cable (USART2, the long one). Both ends are 3.3 V logic, so no level shifter is needed — just three signal-level connections and a regulator for power. Note that PA2/PA3 are not 5 V tolerant: nothing running at 5 V may touch these two wires.
Hoverboard mainboard ESP32-C3 Supermini
left sideboard cable (4-pin)
┌──────────────────────────┐
│ red 15 V, 200 mA max │──► buck 15V→5V ──► 5V pin
│ blue PA2 / USART2 TX │───────────────────► GPIO20 (ESP32 RX)
│ green PA3 / USART2 RX │◄────────────────── GPIO10 (ESP32 TX)
│ black GND │───────────────────► GND (also buck GND)
└──────────────────────────┘
| Mainboard | Wire | MCU pin | ESP32-C3 | Note |
|---|---|---|---|---|
| USART2 TX | blue | PA2 | GPIO20 | mainboard talks, ESP32 listens |
| USART2 RX | green | PA3 | GPIO10 | ESP32 talks, mainboard listens |
| GND | black | — | GND | must be common, connect this first |
| 15 V | red | — | 5V via buck converter | 200 mA max; never straight to the ESP32 |
Cross the two data wires: TX goes to RX. Pin numbering and wire colours are from
hoverboard-firmware-hack-FOC/docs/pictures/mainboard_pinout.png.
GPIO20 and GPIO10 are adjacent on the Supermini header and neither is a strapping pin. Steer clear of GPIO2, GPIO8 and GPIO9 for this link — they decide boot mode, GPIO9 is wired to the BOOT button (which would short the mainboard's TX line every time you flash), and GPIO8 drives the onboard LED. GPIO21 is left free on purpose: the ROM bootloader prints its log there at every reset.
Power
The 15 V rail on the sideboard cable cannot feed the Supermini directly — its 5V pin expects 5 V. Put a small buck converter (MP1584, LM2596 or similar) in between and set it to 5.0 V before connecting it. The ESP32-C3 peaks around 350 mA at 3.3 V, which is roughly 90 mA off the 15 V rail, comfortably inside the 200 mA budget.
Three things worth getting right:
- Do not power from USB and the buck at the same time. Most Supermini boards have no reverse-current protection on the 5V pin. Either unplug the buck while flashing over USB, or fit a Schottky diode in series with the buck output.
- The 15 V rail is only live when the mainboard is on. The ESP32 loses power when the board latches off (power button, low battery, inactivity timeout). That is intentional: no mainboard, no controller.
- Never feed 3.3 V or 5 V into the mainboard from the ESP32 side.
Choosing the other cable instead
To move the link to the right cable (USART3, PB10 = TX, PB11 = RX): swap the two
CONTROL_SERIAL_USART2 / FEEDBACK_SERIAL_USART2 lines in the firmware's
Inc/config.h for their USART3 equivalents and rebuild. The right cable is 5 V
tolerant and shorter; the left cable is longer and leaves USART3 free, which is
the only port that can do I²C if a character LCD is ever wanted.
Which cable the firmware listens on and which one you physically wire must match. They fail in a distinctive way if they don't: the mainboard is both deaf and mute, so you get three repeating low beeps (its serial-timeout code) and no feedback bytes at the ESP32. A single broken wire kills only one direction; losing both at once means wrong cable, swapped blue/green, or no common ground.
Mainboard firmware configuration
Configured in hoverboard-firmware-hack-FOC/Inc/config.h. Every deviation from
upstream is tagged [robot], so grep -n '\[robot\]' Inc/config.h lists them.
| Setting | Value | Why |
|---|---|---|
VARIANT_USART |
enabled | the ESP32 drives the board over UART |
CONTROL_SERIAL_USART2 / FEEDBACK_SERIAL_USART2 |
enabled | link on the left cable, bidirectional |
TANK_STEERING |
enabled | per-wheel commands; the ESP32 owns the kinematics |
CTRL_MOD_REQ |
SPD_MODE |
closed-loop rpm — holds commanded speed under load |
N_MOT_MAX |
200 rpm | ≈1.7 m/s on 6.5" wheels; low ceiling buys resolution and torque margin |
I_MOT_MAX / I_DC_MAX |
20 A / 22 A | headroom for heavy loads and hill starts |
ENABLE_ODOMETRY |
enabled | hall tick counters in the feedback frame |
RATE |
160 | on-board ramp limit, 2000 command units/s — a backstop under the ESP32's ramp |
INACTIVITY_TIMEOUT |
30 min | a robot idling between jobs should not switch itself off |
BEEPS_BACKWARD |
0 | programmatic reversing should not beep; fault beeps still work |
Why speed mode. In VLT_MODE a command sets motor voltage, so the same
command gives a different speed empty than loaded, and a slope changes it
again. SPD_MODE closes the loop on measured rpm: the commanded speed is the
speed you get, which is what makes a distance or rotation command repeatable.
The trade is that the wheels actively hold zero — the robot resists being
pushed, and a stalled wheel will pull up to I_MOT_MAX continuously. Keep an
eye on boardTemp under sustained load.
Build and flash
cd hoverboard-firmware-hack-FOC
./build.sh # ~34 KB, uses the variant set in Inc/config.h
./flash.sh -u # first flash of a stock board only: clears read-out protection
./flash.sh -b # build and flash
Power the board from its battery and hold the power button for the whole operation, or feed 3.3 V from the ST-Link with the battery disconnected. An erased chip cannot hold its own power latch, so the board switches itself off the moment you let go of the button.
GigaDevice GD32F103 mainboards
Some boards carry a GD32F103 instead of the STM32. Same firmware, same wiring:
./flash-gd32.sh -b # or: ./flash.sh -c gd32 -b
./flash-gd32.sh -s 0x20000 # if yours is a 128 KB part
There is no gd32f1x.cfg in OpenOCD; the GD32 is programmed through the same
stm32f1x driver. The one thing that reliably goes wrong is flash size
detection — GD32 parts often report the wrong size, and OpenOCD then refuses to
write past the size it believes in. The GD32 path states the size outright
rather than asking: 256 KB by default, which is the RCT6 these boards carry.
st-flash has no equivalent override, so GD32 always goes via OpenOCD.
ESP32 controller
cd esp32
pio run # build
pio run -t upload # flash over USB-C
pio device monitor # console at 115200
What it does
Jerk-limited motion. Linear and angular velocity are each planned with a
limit on acceleration and on how fast acceleration itself may change. A
command step becomes an S-curve, so a loaded chassis starts and stops without
the forward-backward rock you get from a step in torque. Tune with
A_MAX_MPS2, J_MAX_MPS3, ALPHA_MAX and J_MAX_ALPHA in
include/robot_config.h; lower jerk means gentler, slower transitions.
Speed-dependent steering. Angular authority is scaled by
gain(s) = STEER_GAIN_HIGH + (STEER_GAIN_LOW - STEER_GAIN_HIGH) · e^(-s / STEER_KNEE)
where s is the current speed as a fraction of maximum. With the defaults, a
full steer deflection spins on the spot at a standstill (gain 1.00) and becomes
a gentle curve at full speed (gain 0.22), with most of the change happening
inside the first 15% of the speed range. Raise STEER_KNEE to spread the
transition over a wider speed band; lower STEER_GAIN_HIGH for an even calmer
feel at speed. The gain is evaluated against actual speed every tick, so it
fades in and out as the robot accelerates.
Closed-loop moves. fwd and rot count hall ticks from the feedback frame
(5.76 mm of travel per tick on 6.5" wheels) and brake on a profile that accounts
for the jerk ramp, so they stop on the mark instead of sailing past it. A
straight move holds its starting heading; a rotation holds station. Both finish
inside ~10 mm / ~1°.
Console
| Command | Effect |
|---|---|
drive <throttle> <steer> |
−1..1 each; steer goes through the speed curve |
vel <m/s> <rad/s> |
physical units, no steering curve — the programmatic path |
fwd <metres> |
closed-loop straight move |
rot <degrees> |
closed-loop spin on the spot, + is left |
stop |
ramp down through the jerk limits |
estop |
zero the command immediately, then latch until arm |
zero |
reset the odometry accumulators |
speed <0..1> |
joystick throttle scale, saved to flash |
resp <0..1> |
acceleration sharpness, saved to flash |
tel [on|off], status |
telemetry stream / one-shot dump |
drive and vel are standing orders with a 2 s deadman: refresh them or the
controller ramps down. fwd and rot end by arriving.
Remote control
The ESP32 hosts its own WiFi network and serves the control page from flash — no router, no app, nothing to fetch.
- Join
transpotter, passwordotterdrive(both inrobot_config.h) - Open http://192.168.4.1
The page gives a touch joystick, a speed limit, a response slider with three presets, STOP / E-STOP / ARM / ZERO, and live telemetry at 10 Hz.
The browser sends a drive frame every 100 ms whether or not the stick moved,
and the firmware answers each one with telemetry — so the exchange doubles as a
heartbeat. Background the tab or walk out of range and REMOTE_DEADMAN_MS
(400 ms) ramps the machine down. The console keeps a relaxed 2 s deadman, since
nobody types at 5 Hz. The speed limit is applied on the device rather than in
the page, so a stale browser tab cannot ask for more than the saved limit.
Range is the weak point: the Supermini's onboard antenna measured a signal of 54 against a 27–100 spread of local networks. Fine across a room. If you need more, a C3 module with a u.FL connector beats any firmware change.
Response: acceleration sharpness
One setting scales acceleration and jerk on both axes between two tunings, live
and saved to flash. Top speed is separate — speed caps how fast, response
governs how hard it gets there.
| Acceleration | 0 → 0.8 m/s | Use | |
|---|---|---|---|
HAUL (0.00) |
0.50 m/s² | 1.75 s | Long distances at walking pace, loaded |
NORMAL (0.45) |
~0.95 m/s² | ~1.0 s | General purpose |
INDOOR (1.00) |
1.50 m/s² | 0.60 s | Tight, reactive spaces |
Both ends are measured on hardware, not calculated. The bounds are
A_MAX_MPS2/J_MAX_MPS3 (soft) and A_MAX_SHARP/J_MAX_SHARP (sharp) in
robot_config.h; the closed-loop braking profile reads the live values, so
fwd and rot stop correctly at any response setting.
Settings are written to NVS 1.5 s after the last change — dragging a slider does not hammer the flash.
Bring-up
Put the chassis on blocks with both wheels off the ground for steps 1–4.
- Flash both halves. Power the mainboard from its battery, press the power
button, and open the ESP32 console. It holds a zero command for 1.5 s so the
mainboard arms its motors, then prints
armed. status—link=1and a sane battery voltage mean the serial link is good in both directions. Iflink=0, the two data wires are probably not crossed.vel 0.2 0— both wheels should turn forward. If one runs backwards, setINVERT_LEFTorINVERT_RIGHTininclude/robot_config.hand reflash.zero,vel 0.2 0for a second,stop, thenstatus—distshould be positive andheadnear zero. A negative distance means both odometry channels are inverted; a large heading means one is. Fix withINVERT_ODOM_LEFT/INVERT_ODOM_RIGHT.- Measure the distance between the wheel contact patches and set
TRACK_WIDTH_M. On the floor,rot 360should come back to where it started — if it overshoots, the track width is set too small. - Check
fwd 1against a tape measure and adjustWHEEL_DIAMETER_Mif needed.
Then raise the limits in robot_config.h to suit the load.
Protocol
100 Hz, 115200 8N1, little-endian, XOR checksum. The mainboard cuts the motors if no valid command arrives for 0.8 s.
command (8 bytes) uint16 0xABCD │ int16 left │ int16 right │ uint16 checksum
feedback (22 bytes) uint16 0xABCD │ int16 cmd1, cmd2, speedR, speedL,
wheelR_cnt, wheelL_cnt, batVoltage,
boardTemp │ uint16 cmdLed │ uint16 checksum
left/right are ±1000, mapping linearly to ±N_MOT_MAX rpm in speed mode.
The wheel counters wrap at 9000 (100 wheel revolutions), which
hoverTickDelta() unwraps. The feedback layout above assumes ENABLE_ODOMETRY
— without it the two counters are absent and the frame is four bytes shorter.
The console grammar is the natural interface for an app to drive over BLE or WiFi later: the same verbs, one line each.
Safety
- Commission with the wheels off the ground. These motors have real torque.
estopis a software stop. Fit a physical battery disconnect for anything carrying a load.I_MOT_MAXis raised to 20 A. WatchboardTempduring the first loaded runs; the mainboard's own overheat cutoff is disabled until calibrated (see the temperature section ofInc/config.h).- Raising
N_MOT_MAXscales every speed in the system, including what a runaway would do.