Files
2026-09-20 18:55:16 +02:00

298 lines
14 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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](https://github.com/EFeru/hoverboard-firmware-hack-FOC)), 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
```bash
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:
```bash
./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
```bash
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.
1. Join **`transpotter`**, password **`otterdrive`** (both in `robot_config.h`)
2. 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.**
1. 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`.
2. `status` — `link=1` and a sane battery voltage mean the serial link is good
in both directions. If `link=0`, the two data wires are probably not crossed.
3. `vel 0.2 0` — both wheels should turn *forward*. If one runs backwards, set
`INVERT_LEFT` or `INVERT_RIGHT` in `include/robot_config.h` and reflash.
4. `zero`, `vel 0.2 0` for a second, `stop`, then `status` — `dist` should be
positive and `head` near zero. A negative distance means both odometry
channels are inverted; a large heading means one is. Fix with
`INVERT_ODOM_LEFT` / `INVERT_ODOM_RIGHT`.
5. Measure the distance between the wheel contact patches and set
`TRACK_WIDTH_M`. On the floor, `rot 360` should come back to where it
started — if it overshoots, the track width is set too small.
6. Check `fwd 1` against a tape measure and adjust `WHEEL_DIAMETER_M` if 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.
- `estop` is a software stop. Fit a physical battery disconnect for anything
carrying a load.
- `I_MOT_MAX` is raised to 20 A. Watch `boardTemp` during the first loaded runs;
the mainboard's own overheat cutoff is disabled until calibrated (see the
temperature section of `Inc/config.h`).
- Raising `N_MOT_MAX` scales every speed in the system, including what a runaway
would do.