First commit
This commit is contained in:
@@ -0,0 +1,297 @@
|
||||
# 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.
|
||||
Reference in New Issue
Block a user