Skip to content

fix: modules held rather than moved drift one step backwards per move - #46

Open
socquique wants to merge 3 commits into
jhoff:mainfrom
socquique:stepper-drift
Open

fix: modules held rather than moved drift one step backwards per move#46
socquique wants to merge 3 commits into
jhoff:mainfrom
socquique:stepper-drift

Conversation

@socquique

Copy link
Copy Markdown

Problem

SplitFlapModule::start() re-energizes the coil the rotor is resting on before a
move, but it also rewinds stepNumber and leaves it rewound:

void SplitFlapModule::start() {
    stepNumber = (stepNumber + 3) % 4;  // mutates state
    step(false);
}

step() leaves stepNumber pointing at the next pattern, so the rotor sits on
stepNumber - 1. The first start() writes that and nothing moves. The second
writes stepNumber - 2 — a pattern the rotor is not on — and drags the drum one
full step backwards.

moveTo() calls startMotors() on every module on every move, so any module that
is energized and released without stepping loses one step each time. In clock mode
moveTo() runs once a minute, and an hour digit sits still for 59 of those 60
updates:

upstream            after 1h idle: -59 steps (-1.38 flaps on a 48 flap drum)
patched             after 1h idle:  +0 steps

Nothing corrects it, because a module that never turns never passes its hall
sensor. Minute digits pass theirs every ~48 minutes and look fine — which is why
this shows up as "the hours drift and the minutes are OK".

Modules that do step also waste their first commanded step rewriting the pattern
they are already on, so position runs one step ahead of the drum per move.

Changes

  • start() writes the holding pattern without touching stepNumber. Coil patterns
    moved to a named table so start() and step() cannot disagree about them.
  • moveTo() tracks the steps each module still owes instead of comparing positions
    for equality. A mid-move magnet correction could set position past the target,
    which the equality test then never matched, sending the module round for another
    full revolution.
  • moveTo() only energizes modules that actually have to move. Holding all eight
    costs ~200mA each for nothing, and on a shared supply that sag is what makes the
    modules that are moving lose steps.
  • homeToChar() passed moveTo() its arguments in the wrong order (true as
    speed), so homing to a character always ran at the minimum speed.
  • An i2c error latched hasErrored forever, permanently disabling that module's
    magnet correction after a single transient glitch. It now clears when the module
    answers again.
  • Dropped the undefined motorPins[] declaration and the dead file-scope
    hasErrored.

Also here: coil current at power-on

The PCF8575 powers up with every output high, which on this hardware means all four
coils of every module are energized. Nothing wrote them low until
SplitFlapDisplay::init(), which setup() only reaches after mounting LittleFS,
starting the web server and running connectToWifi() — and that blocks for up to
20 seconds. So every boot held the whole display at roughly double its running
current for seconds, longer when the network was down.

setup() now calls releaseAll() first, before STARTUP_DELAY. One i2c write per
module. This is the same root cause diagnosed in #45 against an 11 module display
whose 5V rail collapsed below 3V at power-on.

Testing

Verified on an 8 module ESP32-C3 display, charset 48, 2052 steps/rotation.
Reproduced the drift by cycling 26 words that differ only in the last character at
1s intervals — 60 moveTo() calls per minute, the same count the clock makes in an
hour. Before, the seven idle modules visibly walked backwards; after, they hold.

socquique and others added 3 commits September 1, 2026 14:00
start() re-energizes the coil pattern the rotor is resting on before a
move, but it also rewound stepNumber and left it rewound. Two consequences:

  - a module that is energized and then released without stepping (every
    module that is not changing character) gets the *previous* pattern
    written on the next start(), which physically drags the drum one full
    step backwards. In clock mode moveTo() runs once a minute, so an hour
    digit that sits still for 59 of 60 updates loses ~59 steps/hour -
    about 1.4 flaps on a 48-flap drum - and nothing corrects it, because a
    module that never turns never passes its hall sensor.

  - a module that does step wastes its first commanded step rewriting the
    pattern it is already on, so position runs one step ahead of the drum
    on every move.

start() now writes the holding pattern without touching stepNumber, and
step() keeps position and stepNumber in lockstep.

Also in moveTo():

  - track the steps each module still owes instead of comparing positions
    for equality. A mid-move magnet correction could set position past the
    target, which the equality test then never matched, sending the module
    round for another full revolution.
  - only energize the modules that actually have to move. Holding all
    eight costs ~200mA each for nothing, and that sag is what makes the
    modules that *are* moving lose steps on a shared supply.
  - re-test the finish condition every iteration rather than only inside
    the 20ms sensor-check window.

homeToChar() passed moveTo() its arguments in the wrong order, so homing
to a character always ran at the minimum speed.

Drop checkAllFalse()/startMotors(), now unused, along with the undefined
motorPins[] declaration and the dead file-scope hasErrored. An i2c error
now clears itself once the module answers again, instead of silently
disabling that module's magnet correction forever.

Separately, in the sketch: {mm} maps to %m (month), so the default
timeFormat "{HH}:{mm}" rendered hour:month, and the renderTime fallback
"HH:mm" had no braces at all so strftime passed it through literally.
Both now use {HH}:{MM}. {SS} is documented in the settings help modal but
was never translated; added.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The early return added with the only-energize-what-moves change skipped
stopMotors(), so a moveTo() that found every module already on target
would leave coils energized from a previous moveTo(.., releaseMotors=false).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The PCF8575 powers up with every output high, and on this hardware high
means energized, so at power-on all four coils of all eight modules are
driven at once - roughly double the current a module draws while turning,
on the whole display simultaneously.

Nothing wrote them low until SplitFlapDisplay::init(), which setup() only
reaches after mounting LittleFS, starting the web server and running
connectToWifi() - and that blocks for up to 20 seconds before giving up.
So every boot held the display at full stall current for seconds, longer
still when the network was down.

setup() now calls releaseAll() as its first act, before STARTUP_DELAY.
It needs nothing but the pin and address settings out of NVS and costs
one i2c write per module. Same idea as upstream PR jhoff#45, which diagnosed
this against an 11 module display whose 5V rail collapsed below 3V at
power-on; it was closed without merging.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
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