298 lines
14 KiB
Markdown
298 lines
14 KiB
Markdown
# 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.
|