Skip to content

perf: smaller relay code, relay pin writes no longer clobber the load pins - #175

Merged
FredM67 merged 5 commits into
devfrom
perf/relay-code
Oct 1, 2026
Merged

FredM67 merged 5 commits into
devfrom
perf/relay-code

Conversation

@FredM67

@FredM67 FredM67 commented Oct 1, 2026 •

Copy link
Copy Markdown
Owner

Summary

The relay code is 396 bytes smaller, and a relay pin write can no longer clobber the load pins written by the ADC ISR.

With two relays (RELAY_DIVERSION{ true }, relays on D8 and D9), basic: 11566 → 11170 bytes flash, RAM unchanged. Builds without relays are unchanged.

Change Flash
proceed_relays() reads the TEMA average once, not before every relay; the two identical import-threshold branches of proceed_relay() are one −138
try_turnON() and try_turnOFF() are one try_switch(bool) −28
setPinState() works out the port and the mask once, instead of two inlined 3-way branches with variable shifts −90
EWMA_average::addValue() and getAverageT() not inlined: getAverageT() was copied to every call site. addValue() has a single caller, but inlined it reaches the static filter's six int32 with absolute lds/sts (4 bytes per byte accessed); out of line it goes through this with ld/st/ldd/std (2 bytes) −140

Relay pin writes

setPinState() did a read-modify-write of PORTx from loop() (in/or/out - a run-time mask can't use sbi/cbi). Had the ADC ISR changed a load pin of the same port in between, loop() would have written the old load state back, until the next decision rewrote it (up to one mains cycle).

The relay's bit is now toggled through PINx: writing a 1 there toggles that bit of PORTx, writing a 0 leaves the others alone. That is a single write, so interrupts stay enabled. Only loop() changes a relay's bit, so reading its state first is safe. togglePin() already works this way for the watchdog pin.

This could only happen with a relay on the loads' port (D2-D7), so not with the default pins.

Where the rest of the relay cost goes

With two relays, about 1.5 KB remains:

  • ~550 bytes: the start-up configuration printout (printRelayConfiguration() and its strings) - user-facing text, left as is;
  • ~210 bytes: the TEMA filter (three cascaded int32 stages) - a plain EMA would change the relays' cloud immunity;
  • ~540 bytes: the relay logic itself, and the relay values in the datalog output.

Test plan

  • test_utils_relay and test_utils_pins (simavr): 47/47, including test_setPinState
  • native: 170/170
  • disassembly of try_switch(): one load from PORTx, one store to PINx, no cli, no store to PORTx
  • on hardware: a relay switches on and off with its thresholds and minimum durations

🤖 Generated with Claude Code

FredM67 and others added 5 commits October 1, 2026 07:50
… pins

With two relays, basic is 396 bytes smaller (11566 -> 11170):
- proceed_relays() reads the TEMA average once, not before every relay;
  proceed_relay() had two identical branches for the import threshold
- try_turnON() and try_turnOFF() are one try_switch(bool)
- setPinState() works out the port and the mask once
- EWMA_average::addValue() and getAverageT() are no longer inlined

setPinState() used a read-modify-write of PORTx from loop(). Had the ADC
ISR changed a load pin of the same port in between, the old load state
would have been written back, until the next decision. It now toggles
the relay's bit through PINx: a single write, which leaves the other
bits alone, with no need to disable interrupts. Only possible with a
relay on the loads' port (D2-D7), so not with the default pins.

Builds without relays are unchanged.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
setPinsON() and setPinsOFF() read-modify-write the ports: only safe from
the ADC ISR, which cannot be interrupted. setPinON() and setPinOFF() are
atomic only for a pin known at compile time. From loop(), setPinState()
and togglePin() write through PINx.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Inlined, a static instance's state is reached with absolute lds/sts,
4 bytes per byte accessed; out of line, through the this pointer with
ld/st/ldd/std, 2 bytes. Not register spills, as the comment said.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Two callers in a basic build, and each inlined copy would read the
static filter's averages with absolute lds (4 bytes per byte), against
ld/ldd through this (2 bytes): 70 bytes less out of line.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@FredM67
FredM67 merged commit c9c07df into dev Oct 1, 2026
11 checks passed
@FredM67
FredM67 deleted the perf/relay-code branch October 1, 2026 10:10
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.

1 participant