Skip to content

Commit f5594c7

Browse files
Ballistyxxclaude
andcommitted
kart-core: fix ~10% STEER_STATUS loss — make CAN RX interrupt-driven
The steering link flapped (STEER_LINK_OK dropping/recovering, spamming the dash notification): the Steervo sent STEER_STATUS at a steady 50 Hz but the Teensy received only ~45/s with periodic ~100-300 ms zero-delivery gaps. Diagnosed with live error counters on both nodes over a 70 s capture. Through every gap: Steervo tx_error_counter=0, bus_error_count=0; Teensy REC=0, ESR1 ack/crc/frm/stf/bit0/bit1 all 0. Clean protocol engines on both sides while the app still missed frames => loss above the protocol engine (not the bus, not bit-timing; dropping 250k->125k changed nothing). Root cause: RX was the polled 6-deep hardware FIFO (enableFIFO, no interrupt), and FlexCAN_T4 read() services the FIFO only ~50% of calls (a FIFO/mailbox fairness random() — the other half fall through to readMB and return 0). That halved the drain rate, overflowed the tiny FIFO under normal loop jitter, and dropped frames that the controller had already ACKed (hence Steervo TEC stayed 0). The 64-deep RX_SIZE_64 software ring was never used. Fix: interrupt-driven RX. enableFIFOInterrupt() + onReceive(onCanFrame); the ISR drains the hardware FIFO into the software ring on arrival, and events() (in the loop) dispatches to onCanFrame() in loop context (g_steerLink stays single-threaded). CANTEST reworked to count loopback frames through the same ring path. Verified on the bench (both firmwares reflashed at 250k): STEER_STATUS 50.6/s, zero-delivery windows 0 (was 11/70 s), dashboard steerLink transitions 0 (was 18/25 s), 0% link-down (was 5.5%). CANTEST 20/20 PASS; 64/64 host tests pass. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
1 parent 28457fc commit f5594c7

4 files changed

Lines changed: 104 additions & 58 deletions

File tree

firmware/common/kart_common/kart_can.h

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,8 +22,15 @@ namespace kart {
2222
// selects its bit timing with a compile-time TWAI_TIMING_CONFIG_* switch — the
2323
// macro keeps that selection and the Teensy FlexCAN setBaudRate() in lockstep.
2424
// BOTH nodes must agree. Supported values (have a TWAI mapping in steervo):
25-
// 1000000, 500000, 250000, 125000. Drop to 125000 for even more margin if the
26-
// bench bus still logs errors at 250000.
25+
// 1000000, 500000, 250000, 125000.
26+
//
27+
// NB: an earlier ~4-6% STEER_STATUS loss (periodic ~100-300 ms gaps that flapped
28+
// STEER_LINK_OK) was NOT bitrate/bit-timing — dropping to 125000 changed nothing,
29+
// and CAN error counters on both nodes stayed clean through the gaps. Root cause
30+
// was Teensy-side: polled FlexCAN_T4 read() services the RX FIFO only ~50% of
31+
// calls, overflowing the 6-deep hardware FIFO and dropping (still-ACKed) frames.
32+
// Fixed by making RX interrupt-driven (enableFIFOInterrupt + onReceive; see
33+
// kart-core main.cpp). Bitrate stays at the intended 250000.
2734
#define KART_CAN_BITRATE 250000
2835
constexpr uint32_t kCanBitrate = KART_CAN_BITRATE; // Hz; both nodes must match
2936

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

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,8 +22,15 @@ namespace kart {
2222
// selects its bit timing with a compile-time TWAI_TIMING_CONFIG_* switch — the
2323
// macro keeps that selection and the Teensy FlexCAN setBaudRate() in lockstep.
2424
// BOTH nodes must agree. Supported values (have a TWAI mapping in steervo):
25-
// 1000000, 500000, 250000, 125000. Drop to 125000 for even more margin if the
26-
// bench bus still logs errors at 250000.
25+
// 1000000, 500000, 250000, 125000.
26+
//
27+
// NB: an earlier ~4-6% STEER_STATUS loss (periodic ~100-300 ms gaps that flapped
28+
// STEER_LINK_OK) was NOT bitrate/bit-timing — dropping to 125000 changed nothing,
29+
// and CAN error counters on both nodes stayed clean through the gaps. Root cause
30+
// was Teensy-side: polled FlexCAN_T4 read() services the RX FIFO only ~50% of
31+
// calls, overflowing the 6-deep hardware FIFO and dropping (still-ACKed) frames.
32+
// Fixed by making RX interrupt-driven (enableFIFOInterrupt + onReceive; see
33+
// kart-core main.cpp). Bitrate stays at the intended 250000.
2734
#define KART_CAN_BITRATE 250000
2835
constexpr uint32_t kCanBitrate = KART_CAN_BITRATE; // Hz; both nodes must match
2936

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

Lines changed: 43 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,7 @@ namespace cfg = kart::cfg;
4646

4747
namespace {
4848

49-
constexpr const char *kVersion = "0.4.1-contactor";
49+
constexpr const char *kVersion = "0.4.2-canrx";
5050

5151
// ── Pin map (docs/SMCSKart-Mainboard/README.md). Output lines are
5252
// MOSFET-switched grounds: HIGH = asserted at the ESC, LOW = released. ──
@@ -407,22 +407,36 @@ void sendCanStd(uint32_t id, const uint8_t *data, uint8_t len) {
407407
else g_canTxFail++;
408408
}
409409

410-
// Drain inbound CAN: the only frames we consume are STEER_STATUS heartbeats.
411-
void serviceCan(uint32_t now) {
412-
CAN_message_t msg;
413-
while (g_can.read(msg)) {
414-
g_canRxAny++; // any traffic at all — proves the bus + our RX path are alive
415-
if (msg.flags.extended) continue; // no extended IDs are for us
416-
if (msg.id == kart::kIdSteerStatus) {
417-
kart::SteerStatus st;
418-
if (kart::unpack_steer_status(msg.buf, msg.len, st)) {
419-
g_steerLink.on_status(st, now);
420-
g_steerStatusRx++;
421-
}
410+
// Inbound CAN is interrupt-driven: the FIFO interrupt (enableFIFOInterrupt in
411+
// setup) drains the 6-deep hardware FIFO into the 64-deep software ring the
412+
// instant a frame arrives, and onCanFrame() below is dispatched from events()
413+
// in loop context (never the ISR — so g_steerLink stays single-threaded).
414+
//
415+
// This replaces the old polled `while (g_can.read())`: read() randomly services
416+
// the FIFO only ~50% of the time (a FlexCAN_T4 FIFO/mailbox fairness hack), so
417+
// under a FIFO-only RX config it left the tiny hardware FIFO to overflow, ACKing
418+
// then silently dropping ~10% of STEER_STATUS in ~100 ms bursts.
419+
void onCanFrame(const CAN_message_t &msg) {
420+
g_canRxAny++; // any traffic at all — proves the bus + our RX path are alive
421+
if (msg.flags.extended) return; // no extended IDs are for us
422+
if (msg.id == kart::kIdSteerStatus) {
423+
kart::SteerStatus st;
424+
if (kart::unpack_steer_status(msg.buf, msg.len, st)) {
425+
g_steerLink.on_status(st, millis());
426+
g_steerStatusRx++;
422427
}
423428
}
424429
}
425430

431+
// Drain the interrupt-filled RX ring, dispatching each frame to onCanFrame().
432+
// Bounded so a burst can't monopolize the loop; the ring holds the rest.
433+
void serviceCan(uint32_t now) {
434+
(void)now;
435+
for (int i = 0; i < 32; i++) {
436+
if ((g_can.events() >> 12) == 0) break; // rxBuffer drained
437+
}
438+
}
439+
426440
// STEER_SET at 50 Hz. The setpoint streams continuously; the ENABLE bit is set
427441
// only when steering is explicitly armed (`STEER ON`), the wheel is present,
428442
// and the Steervo link is fresh + calibrated + not faulted.
@@ -763,12 +777,15 @@ void cmdSteer(Stream &out) {
763777
// Steervo's NO_ACK self-test + the `canrx` counter to localize a dead bus.
764778
void cmdCanTest(Stream &out) {
765779
out.println("INFO CANTEST entering FlexCAN internal loopback (transceiver NOT tested)");
780+
// RX is interrupt-driven (ISR -> ring -> events() -> onCanFrame), so count the
781+
// self-received loopback frames through that same path via g_canRxAny rather
782+
// than polling read(). Loopback disconnects the external bus, so g_canRxAny
783+
// moves only for our own frames here.
766784
g_can.enableLoopBack(true);
767785
delay(2);
768-
CAN_message_t m;
769-
while (g_can.read(m)) {} // drain
786+
for (int i = 0; i < 64 && (g_can.events() >> 12); i++) {} // drain ring clean
770787
const int kN = 20;
771-
int rx = 0;
788+
uint32_t before = g_canRxAny;
772789
for (int i = 0; i < kN; i++) {
773790
CAN_message_t tx{};
774791
tx.id = kart::kIdSteerSet;
@@ -777,18 +794,13 @@ void cmdCanTest(Stream &out) {
777794
tx.buf[0] = (uint8_t)i;
778795
g_can.write(tx);
779796
uint32_t t0 = millis();
780-
while ((uint32_t)(millis() - t0) < 8) {
781-
if (g_can.read(m) && !m.flags.extended && m.id == kart::kIdSteerSet &&
782-
m.len == 1 && m.buf[0] == (uint8_t)i) {
783-
rx++;
784-
break;
785-
}
786-
}
797+
while ((uint32_t)(millis() - t0) < 8) g_can.events(); // pump ring drain
787798
}
799+
int rx = (int)(g_canRxAny - before);
788800
g_can.enableLoopBack(false);
789-
while (g_can.read(m)) {} // drain any stragglers before normal RX resumes
801+
for (int i = 0; i < 64 && (g_can.events() >> 12); i++) {} // drain stragglers
790802
// A healthy controller loops nearly all frames back; allow a couple of misses
791-
// from FIFO/poll timing without calling it a failure.
803+
// from timing without calling it a failure.
792804
bool pass = rx >= kN - 2;
793805
out.print("OK CANTEST loopback tx=");
794806
out.print(kN);
@@ -1130,12 +1142,16 @@ void setup() {
11301142
attachInterrupt(digitalPinToInterrupt(kPinHallPulses), hallIsr, RISING);
11311143

11321144
// Steering CAN bus (CAN3, mainboard pins 30/31). KART_CAN_BITRATE per can-ids.md.
1133-
// FIFO (accept-all) so read() polls inbound STEER_STATUS without per-mailbox
1134-
// RX setup; write() still uses the TX mailboxes.
1145+
// Interrupt-driven RX FIFO: the ISR drains the 6-deep hardware FIFO into the
1146+
// 64-deep software ring (RX_SIZE_64) on arrival, and onCanFrame() is dispatched
1147+
// from events() in loop(). Polled read() (with its ~50% FIFO/mailbox random
1148+
// skip) overflowed the tiny hardware FIFO and silently dropped frames.
11351149
g_can.begin();
11361150
g_can.setBaudRate(kart::kCanBitrate);
11371151
g_can.setMaxMB(16);
11381152
g_can.enableFIFO();
1153+
g_can.enableFIFOInterrupt();
1154+
g_can.onReceive(onCanFrame);
11391155

11401156
#if KART_TRACTION_ONLY_BENCH
11411157
g_dsm.set_traction_only_bench(true);

firmware/kart-core/src/main.cpp

Lines changed: 43 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,7 @@ namespace cfg = kart::cfg;
4646

4747
namespace {
4848

49-
constexpr const char *kVersion = "0.4.1-contactor";
49+
constexpr const char *kVersion = "0.4.2-canrx";
5050

5151
// ── Pin map (docs/SMCSKart-Mainboard/README.md). Output lines are
5252
// MOSFET-switched grounds: HIGH = asserted at the ESC, LOW = released. ──
@@ -407,22 +407,36 @@ void sendCanStd(uint32_t id, const uint8_t *data, uint8_t len) {
407407
else g_canTxFail++;
408408
}
409409

410-
// Drain inbound CAN: the only frames we consume are STEER_STATUS heartbeats.
411-
void serviceCan(uint32_t now) {
412-
CAN_message_t msg;
413-
while (g_can.read(msg)) {
414-
g_canRxAny++; // any traffic at all — proves the bus + our RX path are alive
415-
if (msg.flags.extended) continue; // no extended IDs are for us
416-
if (msg.id == kart::kIdSteerStatus) {
417-
kart::SteerStatus st;
418-
if (kart::unpack_steer_status(msg.buf, msg.len, st)) {
419-
g_steerLink.on_status(st, now);
420-
g_steerStatusRx++;
421-
}
410+
// Inbound CAN is interrupt-driven: the FIFO interrupt (enableFIFOInterrupt in
411+
// setup) drains the 6-deep hardware FIFO into the 64-deep software ring the
412+
// instant a frame arrives, and onCanFrame() below is dispatched from events()
413+
// in loop context (never the ISR — so g_steerLink stays single-threaded).
414+
//
415+
// This replaces the old polled `while (g_can.read())`: read() randomly services
416+
// the FIFO only ~50% of the time (a FlexCAN_T4 FIFO/mailbox fairness hack), so
417+
// under a FIFO-only RX config it left the tiny hardware FIFO to overflow, ACKing
418+
// then silently dropping ~10% of STEER_STATUS in ~100 ms bursts.
419+
void onCanFrame(const CAN_message_t &msg) {
420+
g_canRxAny++; // any traffic at all — proves the bus + our RX path are alive
421+
if (msg.flags.extended) return; // no extended IDs are for us
422+
if (msg.id == kart::kIdSteerStatus) {
423+
kart::SteerStatus st;
424+
if (kart::unpack_steer_status(msg.buf, msg.len, st)) {
425+
g_steerLink.on_status(st, millis());
426+
g_steerStatusRx++;
422427
}
423428
}
424429
}
425430

431+
// Drain the interrupt-filled RX ring, dispatching each frame to onCanFrame().
432+
// Bounded so a burst can't monopolize the loop; the ring holds the rest.
433+
void serviceCan(uint32_t now) {
434+
(void)now;
435+
for (int i = 0; i < 32; i++) {
436+
if ((g_can.events() >> 12) == 0) break; // rxBuffer drained
437+
}
438+
}
439+
426440
// STEER_SET at 50 Hz. The setpoint streams continuously; the ENABLE bit is set
427441
// only when steering is explicitly armed (`STEER ON`), the wheel is present,
428442
// and the Steervo link is fresh + calibrated + not faulted.
@@ -763,12 +777,15 @@ void cmdSteer(Stream &out) {
763777
// Steervo's NO_ACK self-test + the `canrx` counter to localize a dead bus.
764778
void cmdCanTest(Stream &out) {
765779
out.println("INFO CANTEST entering FlexCAN internal loopback (transceiver NOT tested)");
780+
// RX is interrupt-driven (ISR -> ring -> events() -> onCanFrame), so count the
781+
// self-received loopback frames through that same path via g_canRxAny rather
782+
// than polling read(). Loopback disconnects the external bus, so g_canRxAny
783+
// moves only for our own frames here.
766784
g_can.enableLoopBack(true);
767785
delay(2);
768-
CAN_message_t m;
769-
while (g_can.read(m)) {} // drain
786+
for (int i = 0; i < 64 && (g_can.events() >> 12); i++) {} // drain ring clean
770787
const int kN = 20;
771-
int rx = 0;
788+
uint32_t before = g_canRxAny;
772789
for (int i = 0; i < kN; i++) {
773790
CAN_message_t tx{};
774791
tx.id = kart::kIdSteerSet;
@@ -777,18 +794,13 @@ void cmdCanTest(Stream &out) {
777794
tx.buf[0] = (uint8_t)i;
778795
g_can.write(tx);
779796
uint32_t t0 = millis();
780-
while ((uint32_t)(millis() - t0) < 8) {
781-
if (g_can.read(m) && !m.flags.extended && m.id == kart::kIdSteerSet &&
782-
m.len == 1 && m.buf[0] == (uint8_t)i) {
783-
rx++;
784-
break;
785-
}
786-
}
797+
while ((uint32_t)(millis() - t0) < 8) g_can.events(); // pump ring drain
787798
}
799+
int rx = (int)(g_canRxAny - before);
788800
g_can.enableLoopBack(false);
789-
while (g_can.read(m)) {} // drain any stragglers before normal RX resumes
801+
for (int i = 0; i < 64 && (g_can.events() >> 12); i++) {} // drain stragglers
790802
// A healthy controller loops nearly all frames back; allow a couple of misses
791-
// from FIFO/poll timing without calling it a failure.
803+
// from timing without calling it a failure.
792804
bool pass = rx >= kN - 2;
793805
out.print("OK CANTEST loopback tx=");
794806
out.print(kN);
@@ -1130,12 +1142,16 @@ void setup() {
11301142
attachInterrupt(digitalPinToInterrupt(kPinHallPulses), hallIsr, RISING);
11311143

11321144
// Steering CAN bus (CAN3, mainboard pins 30/31). KART_CAN_BITRATE per can-ids.md.
1133-
// FIFO (accept-all) so read() polls inbound STEER_STATUS without per-mailbox
1134-
// RX setup; write() still uses the TX mailboxes.
1145+
// Interrupt-driven RX FIFO: the ISR drains the 6-deep hardware FIFO into the
1146+
// 64-deep software ring (RX_SIZE_64) on arrival, and onCanFrame() is dispatched
1147+
// from events() in loop(). Polled read() (with its ~50% FIFO/mailbox random
1148+
// skip) overflowed the tiny hardware FIFO and silently dropped frames.
11351149
g_can.begin();
11361150
g_can.setBaudRate(kart::kCanBitrate);
11371151
g_can.setMaxMB(16);
11381152
g_can.enableFIFO();
1153+
g_can.enableFIFOInterrupt();
1154+
g_can.onReceive(onCanFrame);
11391155

11401156
#if KART_TRACTION_ONLY_BENCH
11411157
g_dsm.set_traction_only_bench(true);

0 commit comments

Comments
 (0)