Skip to content

Stop the temperature read stalling the packet stream, and make the speed read atomic - #7

Merged
PonderForge merged 4 commits into
mainfrom
fix/temp-polling-and-atomic-speed
Sep 27, 2026
Merged

PonderForge merged 4 commits into
mainfrom
fix/temp-polling-and-atomic-speed

Conversation

@taciturnaxolotl

@taciturnaxolotl taciturnaxolotl commented Sep 26, 2026 •

Copy link
Copy Markdown
Contributor

The temperatures were undefined behaviour

float updateEngineTemp() {
  if (ds.select(engineTempAddr)){
    if (cacheLife[0] > cacheTTL[0]) { ... return ds.getTempF(); }
    else { cacheLife[0]++; }
  }
}   /* falls off the end, no return */

hence gcc warnings

warning: control reaches end of non-void function [-Wreturn-type]  (x2)

getTempF() blocks for 750 ms

looking at the lib getTempF() issues CONVERT_T and then calls delayForConversion(), which is a blocking delay(750) at the default 12-bit resolution. Both sensors were read inline which would break the loop.

A few changes made this a bit faster:

  • 9-bit resolution, so the conversion is 94 ms instead of 750. 0.5°C is all we really need.
  • One sensor per 100 ms, which is about as fast as the sensor can report data

getSpeed() had a torn-read race

It read two volatile unsigned longs without disabling interrupts. On an 8-bit part that is four byte loads each, so an interrupt landing mid-read yields half the old value and half the new one. Now snapshotted with interrupts off, plus an explicit guard on a zero delta.

The magic constant for the speed conversions is also properly defined now

pulse rate speed sample interval
10 Hz 35.7 mph 100 ms
2 Hz 7.1 mph 500 ms
0.5 Hz 1.8 mph 2000 ms

Below about 0.94 mph the code reports a hard zero instead of allowing an eventual divide by zero.

Possible future fancy things

A fully non-blocking temperature read needs raw OneWire. This library exposes no way to start a conversion and collect it later: getTempC, getTempF and doConversion all wait internally, and readScratchpad is private. It might be possible but would be a bit tricky.

speed read atomic

Three problems, one of which was producing wrong data for months.

The temperature functions fell off the end without returning a value on the
cache-hit path, which ran 50 times out of every 51. That is undefined
behaviour: the caller got whatever was in the return register. A radiator
temperature frozen at exactly 48.4 in every CSV on the car is consistent with
one real early reading left sitting there and never updated again. A sensor
that has never answered now reports NAN instead of an invented number, which
is what the engine probe should have been doing all along: its address is
still all zeroes, so it has never been on the bus.

getTempF() also blocks. It issues CONVERT_T then waits out the conversion,
750 ms at the library's default 12-bit resolution, and both sensors were read
inline on every pass through a loop meant to turn over in 50 ms. Dropping to
9-bit resolution and reading one sensor per 100 ms cuts that by about 16x.
0.5 C is far finer than anything useful about coolant temperature.

To be accurate about the damage: this did not lose wheel interrupts. Arduino's
delay() spins on micros() with interrupts enabled, so the magnet ISR kept
timestamping throughout. The cost was packet cadence, not speed accuracy.

Separately, getSpeed() read two 32-bit ISR variables without disabling
interrupts. On an 8-bit part that is four byte loads each, so an interrupt
landing mid-read yields half the old value and half the new one, and a torn
deltaTime looks like a plausible speed rather than an obvious fault. Now
snapshotted with interrupts off, with an explicit guard on a zero delta.

Fully non-blocking temperature reads need raw OneWire: this library exposes no
way to start a conversion and collect it later, since getTempC, getTempF and
doConversion all wait internally and readScratchpad is private. Worth doing,
but not blind.
`lib install --git-url` is disabled unless library.enable_unsafe_install is
set, so that path always failed and the fallback clone was doing the work.
Compiling the firmware and the Pi's library separately proves they both
build. It proves nothing about whether they agree on the wire, which is the
failure that actually costs a race weekend.

This runs the real compiled firmware on a simulated ATmega328p, captures the
bytes it puts on the UART, and feeds them through SensorHub's real parser.
Three seconds of simulated car is about 59 packets and takes two seconds of
wall clock. Every frame must verify, the format byte must be understood, and
the sequence numbers must have no gaps.

It reads the UART off the peripheral's output interrupt rather than simavr's
console, which is a pretty-printer: it substitutes '.' for non-printable
bytes, so the format byte 0x01 arrives as 0x2E and nothing parses at all.

The last step corrupts every format byte and requires the check to fail,
because a harness that cannot fail turns a guess into a green tick.

Running it against this branch already shows the temperature fix working:
both probes report NAN, where the old undefined-behaviour path returned
whatever was in the register and looked like a plausible reading.
The comments in this file ran to 40% of it, with blocks of 30, 16 and 14
lines narrating debugging sessions that the commit messages already record.
Trimmed to the facts that change a decision: the timing table for the wheel
sensor, why the temperature read is structured the way it is, and why NAN.

The const cast on the sensor address is now a named one-line function, so the
next reader can see it is the library's signature at fault and not a mutation.

check_frames grew six copies of "if (bad) { printf; failures++ }". It is a
table now, so the seventh check is data rather than another branch.

And the simulation harness was overstating what it measures. Capture stalls
after about fifty frames whatever the budget, because the AVR's transmit ring
fills and simavr never drains it; the firmware keeps running, wedged in
HardwareSerial::write. The frame count is therefore a sample, not a rate.

That mattered: the CI threshold was 50 against an observed 51, so it had one
packet of margin and would have gone red for reasons having nothing to do with
this repo. It is 25 now, and the harness says plainly that the count is not a
rate. Comparing counts between two builds measures where the simulator stalls,
which is a trap I walked into before catching it.

@PonderForge PonderForge left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looking good, though I'm still not sure if it's best to calc speeds of the magnets during the loop or if it'd be better to do it each time the magnet is registered

@PonderForge
PonderForge merged commit 241067d into main Sep 27, 2026
4 checks passed
@PonderForge
PonderForge deleted the fix/temp-polling-and-atomic-speed branch September 27, 2026 23:17
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants