Skip to content

Commit 08f625b

Browse files
Ballistyxxclaude
andcommitted
kart-core 0.4.15-spdspeedo: speedometer on the ESC SPD pulse (pin 22)
Bench bring-up settled the speed source. The pin-2 "hall pulses" line (ESC pin 18) emits a fixed ~340 Hz signal whenever the ESC is energized — present even with the wheel dead still — so it never tracked road speed. The ESC's SPD "digital speed pulse" output (ESC pin 13 -> Teensy pin 22), once set to 6 pulses + "isolated pulse" in the FarDriver app, is a clean signal: silent at rest, clean pulses (dropped=0) once the wheel turns. See docs/speedometer.md. - Add a second tach input on pin 22 (spdIsr -> g_spdCount) and stream both pins side by side in HALLDIAG (hall_*, spd_*, plus a pin-22 analog envelope adc_* that confirmed the line is not analog). - Point the speed estimator at g_spdCount: hz10, vehicle_stopped, telemetry hall_count/hall_hz_x10, and STATUS hall=/hz10= now come from SPD. Pin-2 instrumentation is retained in HALLDIAG for reference only. - vehicle_stopped is SPD-based with a conservative 500 ms stop timeout and a 250 ms window, so low-speed cogging bursts (packets with ~30 ms gaps on stands) are never mistaken for stopped. - Pi bridge: hall_hz_x10 now carries SPD; HALL_MPH_PER_HZ set to a provisional geometry estimate (0.057 mph/Hz, 6 pulses/motor-rev x 8.37 reduction, 16" tyre) pending road calibration. Streams to the dashboard speed dial. Diagnostic scaffolding used to reach this (a hard-capped, auto-expiring DBGTHR throttle inject and a pin-9 misalignment probe) was added and fully removed; motion authority is pedal-only again. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
1 parent 28e8cc1 commit 08f625b

6 files changed

Lines changed: 321 additions & 53 deletions

File tree

docs/speedometer.md

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
# Speedometer — SPD pulse source (bench bring-up, July 2026)
2+
3+
The dashboard speed reads the **ESC SPD "digital speed pulse" output**
4+
(ESC pin 13 "Light-Blue" → **Teensy pin 22 / A8**), **not** the pin-2 "hall
5+
pulses" line. This was settled on the bench by driving the motor at known
6+
throttle steps (wheels lifted) and streaming both lines with `HALLDIAG`.
7+
8+
## Why not pin 2 (ESC pin 18 "Hall Pulses")
9+
10+
Pin 2 emits a **fixed ~340 Hz signal (1947/3895 µs, a 1:2 pattern) whenever the
11+
ESC is energized** — present even with the wheel *dead still* (4–20 % throttle
12+
produced only winding whine, no rotation, yet pin 2 read 340 Hz). It goes to 0
13+
only when the ESC key is off. A real speed component rides faintly on top once
14+
the wheel turns (its per-window *max* crept to ~410 Hz), but the fixed 340 Hz
15+
swamps it. **Pin 2 does not track road speed. Abandoned.** Its instrumentation
16+
is retained in `HALLDIAG` (`hall_*` fields) for reference only.
17+
18+
## Why SPD (pin 22) works — with the right ESC config
19+
20+
Out of the box SPD was silent/erratic. Two FarDriver settings fixed it:
21+
- **Speed pulses: 2 → 6** (3× resolution).
22+
- **Speedometer mode: "pulse" → "isolated pulse"** (clean, isolated output).
23+
24+
After that, with the motor actually rotating (≥~35 % on stands), SPD is a
25+
**clean signal**: `dropped=0` (no EMI/sub-120 µs noise), consistent `spd_min_us`
26+
≈ 1154 µs. It is **silent at rest** and switches on with rotation — exactly the
27+
correlation pin 2 lacks. The wire is direct (ESC-13 → pin-22, no RC/pullup in
28+
the harness); the Teensy provides an internal `INPUT_PULLUP`.
29+
30+
**Low-speed burstiness is cogging, not signal quality.** On stands at a crawl the
31+
motor lurches between detents, so SPD arrives in packets (tight bursts ~1154 µs
32+
apart) with gaps up to ~30 ms. At real road speed the motor turns smoothly and
33+
SPD becomes a continuous, countable stream. `kHallWindowMs=250` averages the
34+
bursts; `kHallStopTimeoutMs=500` keeps an inter-burst gap from being mistaken
35+
for "stopped".
36+
37+
## Firmware wiring (`0.4.15-spdspeedo`+)
38+
39+
- `kPinSpd = 22`, `spdIsr` counts `g_spdCount` (glitch-filtered by
40+
`kHallMinIntervalUs`), same as the pin-2 hall ISR.
41+
- The speed estimator `g_hall` reads **`g_spdCount`** (was `g_hallCount`). It
42+
drives `hz_x10`, `vehicle_stopped`, telemetry `hall_count`/`hall_hz_x10`, and
43+
STATUS `hall=`/`hz10=`.
44+
- `vehicle_stopped` (a safety gate for DRIVE-entry + controlled-stop completion)
45+
is therefore SPD-based, with the conservative 500 ms timeout above.
46+
47+
## Scale factor (NEEDS road calibration)
48+
49+
`mph = SPD_hz × (wheel_circumference) / (SPD pulses per wheel revolution)`.
50+
51+
Provisional geometry estimate (Pi `HALL_MPH_PER_HZ`, env-tunable):
52+
- Drivetrain reduction motor→axle = 2.37 (gearbox) × 60/17 (sprocket) = **8.37**.
53+
- Assuming **6 pulses/motor-rev** → 6 × 8.37 ≈ **50.2 pulses/wheel-rev**.
54+
- 16" tyre → circumference ≈ 50.3".
55+
- `50.27 / 50.2 × 3600 / 63360`**0.057 mph/Hz** → default on the Pi.
56+
57+
**This is unverified** — "6 speed pulses" may be per motor-rev, per electrical
58+
cycle, or per wheel-rev; the ratio changes the factor a lot. Calibrate by
59+
driving a measured distance (or counting wheel revs at a steady speed) and set
60+
`HALL_MPH_PER_HZ` on the Pi accordingly. Until then the dial tracks real speed
61+
proportionally but the magnitude is provisional.
62+
63+
## Diagnosing again
64+
65+
`HALLDIAG [secs]` streams both pins per 100 ms window: `hall_*` (pin 2, raw),
66+
`spd_*` (pin 22), plus `adc_*` (a pin-22 analog envelope — confirmed *not*
67+
analog: pinned ~1023 at idle). `HALLDIAG OFF` stops it.

firmware/kart-core/arduino/kart_core/config.h

Lines changed: 16 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -108,9 +108,22 @@ constexpr uint32_t kPrechargeMaxMs = 5000; // hard cap -> shed + fault
108108
constexpr uint32_t kPrechargeCooldownMs = 10000; // min off-time between cycles
109109
constexpr uint32_t kContactorSettleMs = 500;
110110

111-
// ── Hall speed (T3) ──
112-
constexpr uint32_t kHallWindowMs = 100;
113-
constexpr uint32_t kHallStopTimeoutMs = 300;
111+
// ── Speed sensing (T3) ──
112+
// The speedometer reads the ESC's SPD "digital speed pulse" output (ESC pin 13
113+
// -> Teensy pin 22), NOT the pin-2 "hall pulses" line. Bench-confirmed (July
114+
// 2026): pin 2 emits a fixed ~340 Hz signal whenever the ESC is energized —
115+
// present even with the wheel dead still — so it does not track road speed.
116+
// SPD is silent at rest and produces clean pulses once the wheel turns
117+
// (configure the ESC for 6 pulses + "isolated pulse"). See docs/speedometer.md.
118+
//
119+
// The window is a bit longer than a raw-tach default to average out low-speed
120+
// cogging bursts (on stands the motor lurches, so SPD arrives in packets with
121+
// gaps up to ~30 ms). The stop timeout is deliberately conservative: only after
122+
// this long with NO SPD pulse is the vehicle called stopped, so an inter-burst
123+
// gap (or a brief coast below SPD's threshold) is never mistaken for stopped.
124+
constexpr uint32_t kHallWindowMs = 250;
125+
constexpr uint32_t kHallStopTimeoutMs = 500;
126+
// Glitch filter for both tach ISRs (rejects sub-interval edges): see below.
114127
// Hall ISR glitch filter: ignore edges closer than this. Rejects isolated
115128
// glitches/crosstalk while passing real hall pulses. Max admissible pulse
116129
// rate = 1e6 / kHallMinIntervalUs Hz. (Continuous noise — e.g. LED PWM — is

firmware/kart-core/arduino/kart_core/kart_core.ino

Lines changed: 106 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -48,11 +48,12 @@ namespace cfg = kart::cfg;
4848

4949
namespace {
5050

51-
constexpr const char *kVersion = "0.4.9-freeshift";
51+
constexpr const char *kVersion = "0.4.15-spdspeedo";
5252

5353
// ── Pin map (docs/SMCSKart-Mainboard/README.md). Output lines are
5454
// MOSFET-switched grounds: HIGH = asserted at the ESC, LOW = released. ──
55-
constexpr uint8_t kPinHallPulses = 2;
55+
constexpr uint8_t kPinHallPulses = 2; // ESC pin 18 (raw motor-hall tach)
56+
constexpr uint8_t kPinSpd = 22; // ESC pin 13 (SPD digital speed pulse)
5657
constexpr uint8_t kPinReverse = 3;
5758
constexpr uint8_t kPinBrakeLow = 4;
5859
constexpr uint8_t kPinSpeedHigh = 5;
@@ -234,6 +235,33 @@ void hallIsr() {
234235
}
235236
}
236237

238+
// ── SPD tach ISR (ESC pin 13 "digital speed pulse" -> Teensy pin 22) ──
239+
// The dedicated digital-speedo output, a candidate cleaner source than the raw
240+
// hall on pin 2. Instrumented identically so HALLDIAG can compare the two lines
241+
// side by side (even spacing = clean square-wave tach vs. the hall's 1:2 jitter).
242+
volatile uint32_t g_spdCount = 0; // glitch-filtered
243+
volatile uint32_t g_spdCountRaw = 0; // every edge, no filter
244+
volatile uint32_t g_lastSpdUs = 0; // last ACCEPTED (filtered) edge
245+
volatile uint32_t g_lastSpdRawUs = 0; // last raw edge
246+
volatile uint32_t g_spdIntMinUs = 0xFFFFFFFFu; // min raw inter-edge, this window
247+
volatile uint32_t g_spdIntMaxUs = 0; // max raw inter-edge, this window
248+
volatile bool g_spdRawSeen = false;
249+
void spdIsr() {
250+
uint32_t now = micros();
251+
g_spdCountRaw++;
252+
if (g_spdRawSeen) {
253+
uint32_t dt = now - g_lastSpdRawUs;
254+
if (dt < g_spdIntMinUs) g_spdIntMinUs = dt;
255+
if (dt > g_spdIntMaxUs) g_spdIntMaxUs = dt;
256+
}
257+
g_lastSpdRawUs = now;
258+
g_spdRawSeen = true;
259+
if ((uint32_t)(now - g_lastSpdUs) >= cfg::kHallMinIntervalUs) {
260+
g_lastSpdUs = now;
261+
g_spdCount++;
262+
}
263+
}
264+
237265
// ── HALLDIAG: bench streaming of the raw tach line to diagnose the speedo ──
238266
// `HALLDIAG [ON|secs]` streams one INFO line per window (~10 Hz) to the issuing
239267
// port for kHallDiagDefaultMs (auto-stops); `HALLDIAG OFF` stops it.
@@ -245,7 +273,14 @@ uint32_t g_hallDiagUntilMs = 0;
245273
uint32_t g_hallDiagNextMs = 0;
246274
uint32_t g_hallDiagLastFilt = 0;
247275
uint32_t g_hallDiagLastRaw = 0;
276+
uint32_t g_hallDiagLastSpdFilt = 0;
277+
uint32_t g_hallDiagLastSpdRaw = 0;
248278
uint32_t g_hallDiagLastMs = 0;
279+
// Analog envelope of pin 22 (A8) across the current HALLDIAG window, updated
280+
// from loop() while streaming; catches an analog speedo voltage or low-going
281+
// pulses the edge interrupt might miss.
282+
uint16_t g_adcMin = 1023, g_adcMax = 0;
283+
uint32_t g_adcSum = 0, g_adcCount = 0;
249284

250285
// ─────────────────────────────────────────────────────────────────────────
251286
// Low-level output helpers
@@ -596,7 +631,7 @@ kart::DriveInputs gatherInputs(uint32_t now) {
596631
g_implausibleActive &&
597632
(uint32_t)(now - g_implausibleSinceMs) >= cfg::kPedalImplausibleMs;
598633

599-
g_hall.update(g_hallCount, now);
634+
g_hall.update(g_spdCount, now); // speed from SPD (pin 22), not pin-2 hall
600635

601636
// Steering setpoint from the wheel axis (sent every tick via STEER_SET).
602637
g_steerSetpointCdeg = g_steerLink.axis_to_setpoint(rawAxis(g_axisSteer));
@@ -780,7 +815,7 @@ void sendTelemetry(uint32_t now, const kart::DriveInputs &in) {
780815
t.brake_pct = (uint8_t)(g_brakePct + 0.5f);
781816
t.steer_setpoint_cdeg = g_steerSetpointCdeg;
782817
t.steer_measured_cdeg = g_steerLink.last_status().measured_cdeg;
783-
t.hall_count = g_hallCount;
818+
t.hall_count = g_spdCount; // SPD pulses (speedometer source)
784819
t.hall_hz_x10 = g_hall.hz_x10();
785820
t.batt_dv = 0;
786821
t.batt_da = 0;
@@ -949,7 +984,7 @@ void cmdStatus(Stream &out) {
949984
out.print(" brk=");
950985
out.print(g_brakePct, 1);
951986
out.print(" hall=");
952-
out.print(g_hallCount);
987+
out.print(g_spdCount); // SPD pulse count (speedometer source; pin-2 raw via HALLDIAG)
953988
out.print(" hz10=");
954989
out.print(g_hall.hz_x10());
955990
out.print(" contactor=");
@@ -1086,7 +1121,15 @@ void handleCommand(const String &line, Stream &out) {
10861121
g_hallDiagLastRaw = g_hallCountRaw;
10871122
g_hallIntMinUs = 0xFFFFFFFFu;
10881123
g_hallIntMaxUs = 0;
1124+
g_hallDiagLastSpdFilt = g_spdCount;
1125+
g_hallDiagLastSpdRaw = g_spdCountRaw;
1126+
g_spdIntMinUs = 0xFFFFFFFFu;
1127+
g_spdIntMaxUs = 0;
10891128
interrupts();
1129+
g_adcMin = 1023;
1130+
g_adcMax = 0;
1131+
g_adcSum = 0;
1132+
g_adcCount = 0;
10901133
g_hallDiagLastMs = now;
10911134
g_hallDiagNextMs = now + kHallDiagPeriodMs;
10921135
g_hallDiagUntilMs = now + durMs;
@@ -1268,6 +1311,7 @@ void servicePort(Stream &port, String &buffer) {
12681311

12691312
void setup() {
12701313
pinMode(kPinHallPulses, INPUT_PULLUP);
1314+
pinMode(kPinSpd, INPUT_PULLUP);
12711315
pinMode(kPinPps, INPUT);
12721316
pinMode(kPinReverse, OUTPUT);
12731317
pinMode(kPinBrakeLow, OUTPUT);
@@ -1293,6 +1337,7 @@ void setup() {
12931337

12941338
g_usbHost.begin();
12951339
attachInterrupt(digitalPinToInterrupt(kPinHallPulses), hallIsr, RISING);
1340+
attachInterrupt(digitalPinToInterrupt(kPinSpd), spdIsr, RISING);
12961341

12971342
// Steering CAN bus (CAN3, mainboard pins 30/31). KART_CAN_BITRATE per can-ids.md.
12981343
// Interrupt-driven RX FIFO: the ISR drains the 6-deep hardware FIFO into the
@@ -1338,43 +1383,76 @@ void serviceHallDiag(uint32_t now) {
13381383
if ((int32_t)(now - g_hallDiagNextMs) < 0) return;
13391384
g_hallDiagNextMs = now + kHallDiagPeriodMs;
13401385

1341-
// Snapshot + reset the ISR-owned window stats with interrupts briefly masked.
1386+
// Snapshot + reset the ISR-owned window stats for BOTH tach lines (pin 2 raw
1387+
// hall, pin 22 SPD) with interrupts briefly masked.
13421388
noInterrupts();
1343-
uint32_t filt = g_hallCount;
1344-
uint32_t raw = g_hallCountRaw;
1345-
uint32_t minUs = g_hallIntMinUs;
1346-
uint32_t maxUs = g_hallIntMaxUs;
1389+
uint32_t filt = g_hallCount, raw = g_hallCountRaw;
1390+
uint32_t minUs = g_hallIntMinUs, maxUs = g_hallIntMaxUs;
13471391
g_hallIntMinUs = 0xFFFFFFFFu;
13481392
g_hallIntMaxUs = 0;
1393+
uint32_t sFilt = g_spdCount, sRaw = g_spdCountRaw;
1394+
uint32_t sMinUs = g_spdIntMinUs, sMaxUs = g_spdIntMaxUs;
1395+
g_spdIntMinUs = 0xFFFFFFFFu;
1396+
g_spdIntMaxUs = 0;
1397+
uint16_t adcMin = g_adcMin, adcMax = g_adcMax;
1398+
uint32_t adcSum = g_adcSum, adcCount = g_adcCount;
1399+
g_adcMin = 1023;
1400+
g_adcMax = 0;
1401+
g_adcSum = 0;
1402+
g_adcCount = 0;
13491403
interrupts();
13501404

13511405
uint32_t winMs = now - g_hallDiagLastMs;
13521406
if (winMs == 0) winMs = 1;
13531407
uint32_t dFilt = filt - g_hallDiagLastFilt;
13541408
uint32_t dRaw = raw - g_hallDiagLastRaw;
1409+
uint32_t dSFilt = sFilt - g_hallDiagLastSpdFilt;
1410+
uint32_t dSRaw = sRaw - g_hallDiagLastSpdRaw;
13551411
g_hallDiagLastFilt = filt;
13561412
g_hallDiagLastRaw = raw;
1413+
g_hallDiagLastSpdFilt = sFilt;
1414+
g_hallDiagLastSpdRaw = sRaw;
13571415
g_hallDiagLastMs = now;
13581416

13591417
Stream *o = g_hallDiagOut ? g_hallDiagOut : &Serial;
13601418
o->print("INFO HALLDIAG win_ms=");
13611419
o->print(winMs);
1362-
o->print(" filt="); // filtered edges this window (drives speed)
1420+
// pin 2 (raw motor hall)
1421+
o->print(" hall_filt=");
13631422
o->print(dFilt);
1364-
o->print(" raw="); // ALL edges this window
1423+
o->print(" hall_raw=");
13651424
o->print(dRaw);
1366-
o->print(" dropped="); // edges the 120 us glitch filter rejected
1367-
o->print(dRaw >= dFilt ? dRaw - dFilt : 0);
1368-
o->print(" fhz=");
1369-
o->print(dFilt * 1000u / winMs);
1370-
o->print(" rhz=");
1425+
o->print(" hall_hz=");
13711426
o->print(dRaw * 1000u / winMs);
1372-
o->print(" min_us="); // tightest raw inter-edge gap (spacing evenness)
1427+
o->print(" hall_min_us="); // spacing evenness (hall shows ~1:2 jitter)
13731428
o->print(minUs == 0xFFFFFFFFu ? 0 : minUs);
1374-
o->print(" max_us=");
1429+
o->print(" hall_max_us=");
13751430
o->print(maxUs);
1376-
o->print(" lvl="); // instantaneous pin level (stuck-line check)
1377-
o->println(digitalRead(kPinHallPulses));
1431+
// pin 22 (SPD digital speed pulse) — the candidate clean source
1432+
o->print(" spd_filt=");
1433+
o->print(dSFilt);
1434+
o->print(" spd_raw=");
1435+
o->print(dSRaw);
1436+
o->print(" spd_hz=");
1437+
o->print(dSRaw * 1000u / winMs);
1438+
o->print(" spd_min_us="); // even spacing here => clean square-wave tach
1439+
o->print(sMinUs == 0xFFFFFFFFu ? 0 : sMinUs);
1440+
o->print(" spd_max_us=");
1441+
o->print(sMaxUs);
1442+
o->print(" spd_lvl=");
1443+
o->print(digitalRead(kPinSpd));
1444+
// pin 22 (A8) analog envelope this window: 0..1023 over 0..3.3 V. Pinned high
1445+
// (~1023) => idle/high-Z (pullup wins, no signal). Swinging min~0/max~1023 =>
1446+
// a digital square wave present. A steady mid value that tracks speed =>
1447+
// analog speedo voltage.
1448+
o->print(" adc_min=");
1449+
o->print(adcMin);
1450+
o->print(" adc_max=");
1451+
o->print(adcMax);
1452+
o->print(" adc_avg=");
1453+
o->print(adcCount ? (uint16_t)(adcSum / adcCount) : 0);
1454+
o->print(" adc_n=");
1455+
o->println(adcCount);
13781456
}
13791457

13801458
void loop() {
@@ -1403,6 +1481,13 @@ void loop() {
14031481

14041482
servicePort(Serial, g_usbRx);
14051483
servicePort(Serial2, g_piRx);
1484+
if (g_hallDiag) { // sample pin 22 (A8) voltage across the window
1485+
uint16_t a = (uint16_t)analogRead(kPinSpd);
1486+
if (a < g_adcMin) g_adcMin = a;
1487+
if (a > g_adcMax) g_adcMax = a;
1488+
g_adcSum += a;
1489+
g_adcCount++;
1490+
}
14061491
serviceHallDiag(now);
14071492

14081493
#if KART_ENABLE_WATCHDOG

firmware/kart-core/src/config.h

Lines changed: 16 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -108,9 +108,22 @@ constexpr uint32_t kPrechargeMaxMs = 5000; // hard cap -> shed + fault
108108
constexpr uint32_t kPrechargeCooldownMs = 10000; // min off-time between cycles
109109
constexpr uint32_t kContactorSettleMs = 500;
110110

111-
// ── Hall speed (T3) ──
112-
constexpr uint32_t kHallWindowMs = 100;
113-
constexpr uint32_t kHallStopTimeoutMs = 300;
111+
// ── Speed sensing (T3) ──
112+
// The speedometer reads the ESC's SPD "digital speed pulse" output (ESC pin 13
113+
// -> Teensy pin 22), NOT the pin-2 "hall pulses" line. Bench-confirmed (July
114+
// 2026): pin 2 emits a fixed ~340 Hz signal whenever the ESC is energized —
115+
// present even with the wheel dead still — so it does not track road speed.
116+
// SPD is silent at rest and produces clean pulses once the wheel turns
117+
// (configure the ESC for 6 pulses + "isolated pulse"). See docs/speedometer.md.
118+
//
119+
// The window is a bit longer than a raw-tach default to average out low-speed
120+
// cogging bursts (on stands the motor lurches, so SPD arrives in packets with
121+
// gaps up to ~30 ms). The stop timeout is deliberately conservative: only after
122+
// this long with NO SPD pulse is the vehicle called stopped, so an inter-burst
123+
// gap (or a brief coast below SPD's threshold) is never mistaken for stopped.
124+
constexpr uint32_t kHallWindowMs = 250;
125+
constexpr uint32_t kHallStopTimeoutMs = 500;
126+
// Glitch filter for both tach ISRs (rejects sub-interval edges): see below.
114127
// Hall ISR glitch filter: ignore edges closer than this. Rejects isolated
115128
// glitches/crosstalk while passing real hall pulses. Max admissible pulse
116129
// rate = 1e6 / kHallMinIntervalUs Hz. (Continuous noise — e.g. LED PWM — is

0 commit comments

Comments
 (0)