|
| 1 | +# Traction Track Bring-up (kart-core, on stands) |
| 2 | + |
| 3 | +Step-by-step for the traction phases T1–T4 (SOFTWARE-STACK-PLAN.md §8). This is |
| 4 | +the `kart-core` firmware in `firmware/kart-core`, built with |
| 5 | +**`KART_TRACTION_ONLY_BENCH = 1`** (config.h) because the Steervo is away — so |
| 6 | +there is **no steering authority**. Valid only with the driven wheels off the |
| 7 | +ground. The LED strip is solid **magenta** in SAFE to signal this mode loudly. |
| 8 | + |
| 9 | +> Nothing here spins the motor until **T4**, which is supervised, with the ESC |
| 10 | +> powered and Ben at the bench. T1–T3 run with the **ESC unpowered**. |
| 11 | +
|
| 12 | +## Build & flash |
| 13 | + |
| 14 | +```bash |
| 15 | +pio run -d firmware/kart-core -e teensy41 # -> .pio/build/teensy41/firmware.hex |
| 16 | +pio run -d firmware/kart-core -e teensy41 -t upload # Ben flashes |
| 17 | +pio test -d firmware/kart-core -e native # host logic tests (no hardware) |
| 18 | +``` |
| 19 | + |
| 20 | +To return to the full-authority build later, set `KART_TRACTION_ONLY_BENCH 0` |
| 21 | +in `firmware/kart-core/src/config.h` and reflash. |
| 22 | + |
| 23 | +## Talk to it |
| 24 | + |
| 25 | +Over USB serial (`/dev/ttyACM*`) or the Pi UART (`/dev/serial0`, 115200): |
| 26 | + |
| 27 | +``` |
| 28 | +PING -> OK PONG |
| 29 | +VERSION -> OK VERSION kart-core 0.3.2-traction proto=1 |
| 30 | +STATUS -> rich gate/health snapshot (see below) |
| 31 | +WHEELRAW -> every wheel axis + button mask (axis/button discovery) |
| 32 | +I2C -> scan the DAC bus (lists ACKing addrs; empty = DAC unpowered) |
| 33 | +DACREAD -> read back the MCP4725 (verify it stored what we wrote) |
| 34 | +DACSET <pct> -> SAFE-only: hold a fixed throttle voltage for metering (10 s) |
| 35 | +GEAR <low|med|high> -> resync the open-loop gear model to the FarDriver app |
| 36 | +DISARM / FAULT_CLEAR -> stop / clear a latched fault from the Pi side |
| 37 | +``` |
| 38 | + |
| 39 | +`STATUS` fields used constantly for diagnosis: |
| 40 | +`state fault bench wheel thr brk hall hz10 contactor rev speed dac dacok dacfail plaus busrdy vstop thr0 wheelok`. |
| 41 | +The gate bits (`dac busrdy thr0 vstop plaus wheelok`, 1 = satisfied) tell you |
| 42 | +*why* arming/DRIVE is blocked; `dacok/dacfail` are cumulative DAC-write counters |
| 43 | +(watch the deltas to see I2C health under motor load). |
| 44 | + |
| 45 | +> ⚠️ **Hardware reality found during bring-up (corrects the ESC-unpowered |
| 46 | +> premise of T1–T3 below):** the throttle DAC is powered from the ESC's **ACC+ |
| 47 | +> rail, which is gated by the key**. So the DAC only works with the ESC keyed |
| 48 | +> on — you *cannot* verify the throttle DAC with the ESC off. `dac_ok` is |
| 49 | +> therefore a **DRIVE-entry gate**, not an arm gate, and the real power-up order |
| 50 | +> is **arm (contactor closes) → key on (DAC powers) → DRIVE → throttle.** See |
| 51 | +> "Bench operations & hard-won lessons" at the bottom — read that first. |
| 52 | +
|
| 53 | +## T0 — confirm the wheel axes (do this first) |
| 54 | + |
| 55 | +The pedal axis indices/ranges in `steering-wheel.md` were measured on the Pi's |
| 56 | +Linux `js` API. Through the Teensy's `USBHost_t36` Xbox decoder they **may |
| 57 | +differ** (triggers are sometimes `0..1023`, not `±32767`). Confirm before |
| 58 | +trusting the throttle: |
| 59 | + |
| 60 | +1. Plug the wheel into the mainboard USB-A. Watch for |
| 61 | + `INFO WHEEL_CONNECTED vid=0x0F0D pid=0x0152 type=…`. |
| 62 | +2. Run `WHEELRAW` repeatedly while pressing **only the throttle**, then **only |
| 63 | + the brake**, then turning the wheel. Note which `aN` moves for each and its |
| 64 | + released vs. fully-pressed values. |
| 65 | +3. If they differ from the defaults (`axis_thr=5`, `axis_brk=4`, `axis_steer=0`, |
| 66 | + released `-32767`, pressed `+32767`), set them live: |
| 67 | + ``` |
| 68 | + CFG axis_thr 5 |
| 69 | + CFG ped_released -32767 |
| 70 | + CFG ped_pressed 32767 |
| 71 | + ``` |
| 72 | + (RAM only — once confirmed, bake the values into `config.h` so they persist.) |
| 73 | + |
| 74 | +## T1 — throttle DAC (ESC unpowered) |
| 75 | + |
| 76 | +Goal: DAC output (ESC connector pin 27, TPS) tracks the throttle pedal, |
| 77 | +0.5 V released → ~4.3 V full, with a visible slew-rate ramp. |
| 78 | + |
| 79 | +1. Put a scope/DMM on the throttle DAC line (relative to throttle ground). |
| 80 | + At boot it should sit at **~0.5 V** (idle floor) in SAFE. |
| 81 | +2. **Arm:** hold **both paddles (LB+RB)** + press the **brake** + leave throttle |
| 82 | + released, for **1 second**. LED → amber (ARMED), contactor closes after the |
| 83 | + settle dwell. (ESC unpowered, so closing it is harmless.) |
| 84 | +3. **Drive:** press **Menu/Start** with the throttle at zero. LED → green |
| 85 | + (DRIVE). |
| 86 | +4. Press the **throttle**: the DAC voltage should rise smoothly (slew-limited, |
| 87 | + ~25 %/s) toward 4.3 V and snap back to 0.5 V instantly on lift. |
| 88 | +5. Press the **brake** while on throttle → DAC drops to idle (brake overrides). |
| 89 | +6. **Stop:** press **View/Select** (or send `DISARM`). LED → white-ish (back to |
| 90 | + SAFE in normal mode; magenta here in bench mode). |
| 91 | + |
| 92 | +Fault check: unplug the wheel during DRIVE → controlled stop → latched |
| 93 | +`WHEEL_LOST` (LED flashing red). Reconnect, `FAULT_CLEAR`, re-arm. |
| 94 | + |
| 95 | +## T2 — ESC discrete lines + contactor (ESC unpowered) |
| 96 | + |
| 97 | +On a DMM at the ESC connector, verify the MOSFET ground lines assert correctly: |
| 98 | + |
| 99 | +- **Contactor (pin 32):** open in SAFE; closes on ARM after the settle dwell; |
| 100 | + opens again on SAFE/FAULT. Precharge is the **always-on external 100 Ω BMS |
| 101 | + resistor** — not Teensy-controlled — so the bus is already charged before you |
| 102 | + ever arm. |
| 103 | +- **Brake-low (ESC pin 21):** asserts when the brake pedal passes threshold in |
| 104 | + DRIVE, and throughout a controlled stop. |
| 105 | +- **Reverse:** direction is set in the FarDriver app (motor direction); the |
| 106 | + firmware leaves the REV line at its natural sense (X button toggles it). |
| 107 | +- **Gear (ESC high-speed line):** this ESC's high-speed input is a momentary |
| 108 | + gear-up-cycle button (1→2→3→1 per pulse). Right paddle = upshift (1 pulse, |
| 109 | + clamps at HIGH), left paddle = downshift (2 pulses = down one in the wrap, |
| 110 | + only above LOW). Gear is tracked open-loop and shown on the LED |
| 111 | + (green/cyan/blue); resync with `GEAR <low|med|high>` if it drifts from the app. |
| 112 | + |
| 113 | +## T3 — hall speed |
| 114 | + |
| 115 | +Spin the rear wheel **by hand**. `STATUS` (`hall=`, `hz10=`) and the telemetry |
| 116 | +stream should show pulses and a non-zero frequency. While "moving," DRIVE entry |
| 117 | +is blocked (zero-speed is a DRIVE-entry gate) and a controlled stop waits for |
| 118 | +zero speed. After ~300 ms with no pulses it reads stopped again. |
| 119 | + |
| 120 | +## T4 — supervised wheel spin ⚡ (Ben + ESC powered, on stands) |
| 121 | + |
| 122 | +Only after T1–T3 pass. ESC powered, contactor live, rear wheel free. Arm → |
| 123 | +low-throttle wheel-spin → brake → fault drills (wheel pull, e-stop, implausible |
| 124 | +pedal). This is the §8 ⚡ gate and needs Ben's explicit go-ahead. |
| 125 | + |
| 126 | +--- |
| 127 | + |
| 128 | +# Bench operations & hard-won lessons |
| 129 | + |
| 130 | +Everything below was learned getting the kart to actually drive. **Read this |
| 131 | +before bench work** — much of it is non-obvious and cost real debugging time. |
| 132 | + |
| 133 | +## Driver control scheme (current, hardware-confirmed) |
| 134 | + |
| 135 | +The Hori enumerates through `USBHost_t36` as **Xbox One (joystickType 3)**, and |
| 136 | +its button/axis map there is **not** the Linux-`js` map in `steering-wheel.md`. |
| 137 | +Confirmed mapping (baked into `src/config.h`): |
| 138 | + |
| 139 | +| Control | Where | Notes | |
| 140 | +|---|---|---| |
| 141 | +| Throttle | axis **4**, `0…1023` | (not axis 5 / ±32767) | |
| 142 | +| Brake | axis **3**, `0…1023` | | |
| 143 | +| Steering | axis **0**, `±32767` | unused in traction build | |
| 144 | +| Left paddle | button bit **12** | downshift | |
| 145 | +| Right paddle | button bit **13** | upshift | |
| 146 | +| DRIVE button | button bit **6** | context-sensitive (below) | |
| 147 | +| X button | bit **2** | reverse-intent toggle | |
| 148 | + |
| 149 | +- **Arm:** hold **both paddles + brake**, throttle released, ~1 s → amber. |
| 150 | +- **DRIVE button (bit 6) is the whole flow:** ARMED→DRIVE, DRIVE→disarm, |
| 151 | + FAULT→clear. (DISARM/FAULT_CLEAR also over UART.) |
| 152 | +- **Gears (paddles, while armed/driving):** right = upshift, left = downshift. |
| 153 | + LED in DRIVE shows the gear: **green=LOW, cyan=MED, blue=HIGH**. |
| 154 | + |
| 155 | +## The power-up / drive / shutdown sequence (order matters) |
| 156 | + |
| 157 | +``` |
| 158 | +SAFE → arm chord → ARMED (contactor closes) → turn ESC KEY ON (DAC powers, |
| 159 | + dac=1) → DRIVE button → DRIVE → throttle. |
| 160 | +Shutdown: lift throttle → DRIVE button (disarm) → SAFE → THEN key off. |
| 161 | +``` |
| 162 | + |
| 163 | +- **Key after contactor** (hardware rule): never key the ESC before the |
| 164 | + contactor is closed. Arming closes the contactor; key on during ARMED. |
| 165 | +- DRIVE needs `dac=1` (key on). If you press DRIVE with `dac=0` it just sits in |
| 166 | + ARMED and times out (30 s). `STATUS` shows the gate. |
| 167 | +- **Disarm before keying off.** Keying off while driving cuts DAC power; the |
| 168 | + firmware only faults on that if throttle is pressed (it's graceful at a stop), |
| 169 | + but disarm-first is the clean path. |
| 170 | + |
| 171 | +## Flashing workflow (important) |
| 172 | + |
| 173 | +- **Flash from the Arduino IDE**, not `pio … -t upload`. The CLI's auto-reboot |
| 174 | + into the bootloader fails on this rig (the program button is hard to reach in |
| 175 | + the enclosure), *and* — crucially — the wheel only re-enumerates on the |
| 176 | + Teensy USB host after the IDE flow power-cycles it. A CLI soft-reboot leaves |
| 177 | + the wheel dark. |
| 178 | +- kart-core is a PlatformIO project; the IDE needs a flat sketch. Run |
| 179 | + **`firmware/kart-core/gen_arduino_sketch.sh`** to (re)generate |
| 180 | + `arduino/kart_core/` from the canonical `src/` + `lib/` + `firmware/common` |
| 181 | + (it vendors WDT_T4 and rewrites includes). Open `arduino/kart_core/kart_core.ino`, |
| 182 | + select Teensy 4.1, USB Type Serial, upload. **Never hand-edit `arduino/`.** |
| 183 | +- Confirm the flash took with `VERSION` (bump `kVersion` in `main.cpp` each |
| 184 | + change so you can tell). The Teensy is board-powered, so unplugging the USB |
| 185 | + data cable does **not** reboot it. |
| 186 | + |
| 187 | +## Monitoring (how to watch what's happening) |
| 188 | + |
| 189 | +There's no installed pyserial; use uv. Talk to `/dev/ttyACM0` @ 115200: |
| 190 | + |
| 191 | +```bash |
| 192 | +uv run --with pyserial python - <<'PY' |
| 193 | +import serial,time |
| 194 | +s=serial.Serial("/dev/ttyACM0",115200,timeout=0.3); time.sleep(0.2) |
| 195 | +s.write(b"STATUS\n"); time.sleep(0.3); print(s.read(s.in_waiting).decode()) |
| 196 | +PY |
| 197 | +``` |
| 198 | + |
| 199 | +For live tests, run a **background poller** that prints `STATUS`/`WHEELRAW` |
| 200 | +deltas (state, `thr`, `dac`, `dacfail`, `hall`, buttons) — unbuffered |
| 201 | +(`python -u`, `flush=True`) writing to a log you `tail`. This is how every drive |
| 202 | +test was debugged (which gate blocked, whether DAC writes failed under motor |
| 203 | +load, which button bit fired). Watch **`dacfail` deltas during the motor spin** |
| 204 | +to gauge I2C noise. Gotchas: the 20 Hz binary telemetry goes to `Serial2` |
| 205 | +(Pi UART) **not** USB, so over USB you only get command replies + `INFO` lines; |
| 206 | +and never `pkill -f` a pattern that also matches the launching shell (it |
| 207 | +self-kills) — use `fuser -k /dev/ttyACM0` to free the port. |
| 208 | + |
| 209 | +## Hardware quirks & gotchas discovered |
| 210 | + |
| 211 | +- **Throttle DAC (MCP4725, header J3, addr 0x60) is powered by ESC ACC+** → |
| 212 | + only alive with the key on. An empty `I2C` scan = key off, not a dead DAC. |
| 213 | +- **A missing series resistor (R10, 1 kΩ) in the DAC→ESC throttle path** made |
| 214 | + the DAC read fine at VOUT but deliver 0 V to the ESC — looked exactly like a |
| 215 | + dead DAC. If the ESC's throttle reads 0 in the app, check that path/`R10`. |
| 216 | +- **Motor EMI corrupts the DAC's I2C** once the wheel spins → DAC_ERROR. Each |
| 217 | + write is now read-back-verified (`writeThrottleRaw`) and only faults after |
| 218 | + `kDacFailMs` of continuous failure. Real fix is HW: stronger I2C pull-ups |
| 219 | + (2.2 k), routing SDA/SCL away from motor phases, DAC VCC decoupling. The |
| 220 | + Teensy I2C pins are **3.3 V only (not 5 V tolerant)** — pull-ups must not hard |
| 221 | + pull to 5 V. |
| 222 | +- **Hall input (pin 2) picks up noise:** the LED strip's 24 V **PWM** coupled in |
| 223 | + → false motion → blocked arming. Fixed by driving the LEDs **on/off only, |
| 224 | + never PWM**, plus an ISR glitch filter (`kHallMinIntervalUs`). With the ESC |
| 225 | + keyed on, the hall line is also noisy (`vstop` flickers) — harmless for DRIVE |
| 226 | + entry, but it can block FAULT_CLEAR/arming until the wheels truly stop. |
| 227 | +- **ESC high-speed line is a momentary gear-cycle button** (1→2→3→1 per |
| 228 | + grounding pulse), not a level select. Firmware pulses it (up=1, down=2) and |
| 229 | + tracks gear **open-loop** — resync with `GEAR` if it drifts. |
| 230 | +- **Motor direction + commutation are ESC-side:** a buzzing/vibrating motor = |
| 231 | + hall/phase mapping → fix with the **FarDriver app's motor self-learn**. |
| 232 | + Forward/reverse direction is also set in the FarDriver app. |
| 233 | +- **`KART_TRACTION_ONLY_BENCH = 1`** in `config.h` for all of the above |
| 234 | + (stands-only, no steering). Set to `0` for any ground-driving build. |
0 commit comments