Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
44 changes: 26 additions & 18 deletions BirdsEye.ino
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,7 @@ int detectedTrackIndex = -1;
unsigned long idleStartTime = 0;
bool idleTimerRunning = false;
bool newUiRaceActive = false;
unsigned long raceSessionStartedAt = 0; // For auto-idle grace period after RPM wake

// Runtime settings (loaded from SD in setup)
float settingLapDetectionDistance = 7.0;
Expand Down Expand Up @@ -215,33 +216,28 @@ uint16_t bleMTUConnHandle = 0; // Connection handle for deferred MTU read
///////////////////////////////////////////

const int tachInputPin = D0;
unsigned long tachLastUpdate = 0;
volatile int tachLastReported = 0; // Volatile for ISR/main-loop sharing
volatile int tachLastReported = 0; // Volatile: written by TACH_LOOP, read by display/logging/sleep
int topTachReported = 0;

// Debounce timing: ignore pulses faster than this (filters ignition ringing)
// 3000us = 3ms minimum gap, allows up to 20,000 RPM max (333Hz)
static const uint32_t tachMinPulseGapUs = 3000;
volatile uint32_t tachLastPulseUs = 0;

// Pulse period capture for RPM calculation (written by ISR, read by loop)
volatile uint32_t tachLastPeriodUs = 0;
// Sleep wake detection: set true by ISR on any valid pulse
volatile bool tachHavePeriod = false;

// Software gate to prevent interrupt storm from noisy inductive pickup.
// After accepting a pulse, we ignore subsequent ISR calls for tachMinPulseGapUs.
// This is PURELY a volatile flag - we do NOT use noInterrupts() in the ISR
// because that would disable ALL system interrupts and cause deadlocks.
volatile bool tachInterruptShouldProcess = true;
// Ring buffer: ISR writes pulse timestamps, TACH_LOOP reads and computes periods.
// Single-producer (ISR writes head), single-consumer (TACH_LOOP reads tail).
// 16 entries handles up to 20k RPM with 48ms of main-loop stall margin.
static const uint8_t TACH_RING_SIZE = 16;
volatile uint32_t tachRingBuf[TACH_RING_SIZE];
volatile uint8_t tachRingHead = 0; // ISR write index (only ISR writes)
static uint8_t tachRingTail = 0; // Main-loop read index (only TACH_LOOP writes)

// Filtered RPM state (updated in main loop, not ISR)
float tachRpmFiltered = 0.0f;

// Tunable settings
const int tachUpdateRateHz = 3;
static const float tachRevsPerPulse = 1.0f; // Magneto 4T single: wasted spark = 1 pulse/rev
static const float tachFilterAlpha = 0.20f; // EMA filter: 0=smooth, 1=instant
static const uint32_t tachStopTimeoutUs = 500000; // 500ms with no pulse = engine stopped
// Tunable constants
static const float tachRevsPerPulse = 1.0f; // Wasted spark = 1 pulse/rev
static const uint32_t tachStopTimeoutUs = 500000; // 500ms = engine stopped

///////////////////////////////////////////
// ACCELEROMETER GLOBALS
Expand Down Expand Up @@ -898,7 +894,11 @@ void trackDetectionLoop() {
activeTrackConfig.courseCount = 0;
}

// Create CourseManager
// Create CourseManager (delete existing Lap Anything one if RPM-wake created it)
if (courseManager != nullptr) {
delete courseManager;
courseManager = nullptr;
}
courseManager = new CourseManager(activeTrackConfig, crossingThresholdMeters);
courseManager->setSpeedThresholdMph(settingWaypointSpeed);
courseManager->setWaypointProximityMeters(settingWaypointDetectionDistance);
Expand Down Expand Up @@ -979,6 +979,12 @@ void createLapAnythingCourseManager() {
void checkAutoIdle() {
if (!newUiRaceActive) return;

// Grace period: don't auto-idle within first 3 minutes of a session.
// After RPM wake the car is often stationary (warming up, waiting for
// track session) and GPS needs time to reacquire. Without this, the
// 60s idle timer kills the session before the driver even moves.
if (millis() - raceSessionStartedAt < 180000UL) return;

if (gps_speed_mph >= 2.0) {
idleTimerRunning = false;
idleStartTime = 0;
Expand Down Expand Up @@ -1013,6 +1019,7 @@ void autoRaceModeCheck() {
newUiRaceActive = true;
enableLogging = true;
trackSelected = true; // Allow GPS_LOOP to feed timer + log
raceSessionStartedAt = millis();

// Create a minimal CourseManager if none exists yet (no track detected)
createLapAnythingCourseManager();
Expand Down Expand Up @@ -1152,6 +1159,7 @@ void exitSleepMode(bool rpmWake = false) {
newUiRaceActive = true;
enableLogging = true;
trackSelected = true;
raceSessionStartedAt = millis();
createLapAnythingCourseManager();
switchToDisplayPage(TACHOMETER);
} else {
Expand Down
27 changes: 20 additions & 7 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -141,9 +141,19 @@ loop() ~250 Hz

- ISR `TACH_COUNT_PULSE()` fires on falling edge of D0.
- 3 ms minimum pulse gap (supports up to ~20 000 RPM).
- Uses volatile flag gating instead of `noInterrupts()` to avoid deadlock.
- `TACH_LOOP()` applies EMA filter (alpha 0.20) and updates at 3 Hz.
- 500 ms timeout sets RPM to 0 (engine stopped).
- **Ring buffer architecture**: ISR timestamps every valid pulse into a
16-entry ring buffer (`tachRingBuf`). `TACH_LOOP()` drains the buffer
each main-loop iteration, computes mean inter-pulse period from ALL
accumulated pulses, and feeds the result through a 1D Kalman filter.
- **Kalman filter** replaces the old median-of-3 + EMA. Two floats of
state (RPM estimate `kalmanX` + uncertainty `kalmanP`). Process noise
Q = 800 (tuned for kart engine inertia). Measurement noise R scales
inversely with pulse count (more pulses = more confident).
- Time-based debounce only (3 ms). Old volatile flag gate removed — ISR
body is trivially fast (<1 µs) and cannot cause interrupt storms.
- `tachLastReported` updates every main-loop call (~250 Hz). Consumers
(display at 3 Hz, logging at 25 Hz) rate-limit themselves.
- 500 ms timeout sets RPM to 0 (engine stopped), resets Kalman state.

### 3. Accelerometer (`accelerometer.ino`)

Expand Down Expand Up @@ -410,9 +420,11 @@ Stored in `trackLayouts[MAX_LAYOUTS]` (max 10 per track).
| DOVEX header size | 1 024 bytes | `project.h` |
| Auto-idle timeout | 60 s at <2 mph | `BirdsEye.ino` |
| Track detect radius | 5 miles | `BirdsEye.ino` |
| Tach min pulse gap | 3 ms | `tachometer.ino` |
| Tach EMA alpha | 0.20 | `tachometer.ino` |
| Tach stop timeout | 500 ms | `tachometer.ino` |
| Tach min pulse gap | 3 ms | `BirdsEye.ino` |
| Tach ring buffer | 16 entries | `BirdsEye.ino` |
| Tach Kalman Q | 800 RPM² | `tachometer.ino` |
| Tach Kalman R_BASE | 2500 RPM² | `tachometer.ino` |
| Tach stop timeout | 500 ms | `BirdsEye.ino` |
| Display refresh | 3 Hz | `display_ui.ino` |
| Button debounce | 200 ms | `display_ui.ino` |
| SD SPI clock | 2 MHz | `BirdsEye.ino` |
Expand Down Expand Up @@ -452,7 +464,8 @@ This device operates in ignition-noise environments. Three layers of defense:
2. **ISR design**: Volatile flag gating (never `noInterrupts()` in ISR);
3 ms minimum pulse gap in tachometer.
3. **Software**: Multi-sample button reads (3x at 500 us), 200 ms refire
lockout, EMA filtering on RPM, 2 MHz SPI clock for SD stability.
lockout, Kalman-filtered RPM (absorbs ISR jitter), 2 MHz SPI clock for
SD stability.
4. **GPS serial buffer**: TIMER3 ISR drains Serial1 into a 4 KB RAM ring
buffer every 10 ms, preventing GPS data loss during SD card GC pauses
that can block writes for 100 ms–2 s.
Expand Down
Binary file modified TACHOMETER/tach_test_2.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
1 change: 1 addition & 0 deletions display_ui.ino
Original file line number Diff line number Diff line change
Expand Up @@ -274,6 +274,7 @@ void handleMenuPageSelection() {
newUiRaceActive = true;
enableLogging = true;
trackSelected = true;
raceSessionStartedAt = millis();
// Create CourseManager if not already created by track detection
createLapAnythingCourseManager();
switchToDisplayPage(GPS_SPEED);
Expand Down
1 change: 1 addition & 0 deletions gps_functions.ino
Original file line number Diff line number Diff line change
Expand Up @@ -565,6 +565,7 @@ void GPS_WAKE() {
// Reset buffer pointers (stale data from before sleep is useless)
gpsRxHead = 0;
gpsRxTail = 0;

startGpsSerialTimer(); // Resume serial drain ISR
myGNSS.checkUblox();
}
Expand Down
Loading