Self-learning holdover temperature compensation#9
Draft
peterlewis wants to merge 16 commits into
Draft
Conversation
peterlewis
added a commit
to peterlewis/clock4
that referenced
this pull request
Jul 6, 2026
…ignificance-fade + tc_seed) Conflicted files (EOL-divergent whole-file conflicts) resolved to the validated rollup integration; see the following integration-delta commit for the exact residue of the union against the PR branches. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
peterlewis
added a commit
to peterlewis/clock4
that referenced
this pull request
Jul 6, 2026
The exact residue between the pure union of the five draft PRs (mitxela#5 $PMTXTS, mitxela#6 astro pack, mitxela#7 hardening, mitxela#8 sidereal/solar, mitxela#9 tempcomp + significance-fade + tc_seed — mitxela#5 and mitxela#6 arrive as ancestors of mitxela#9 and mitxela#8) and the rollup tree that has been emulator-verified and hardware-run: - astro.c/astro.h/test_astro.c: rollup astro refinements not yet folded back into the refreshed PR mitxela#6/mitxela#8 heads (candidates for a future PR update) - version.c: rollup version string - qspi/output/*.bin: rollup's built firmware images - .settings IDE noise After this commit the megapack tree is BYTE-IDENTICAL to the previously validated rollup (feaf970) while the merge ancestry proves containment of all five PR heads. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This was referenced Jul 7, 2026
peterlewis
added a commit
to peterlewis/clock4
that referenced
this pull request
Jul 7, 2026
…xela#9) The source said 0.0.5 (misidentifying a rollup build as stock) while the committed image embedded 0.0.6 from an earlier build lineage. Source and freshly built, CRC-gated image now both say 0.0.6. No cuckoo — that work is parked on its own branch and versions later. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Author
|
Rebased onto the updated #5 ( |
peterlewis
added a commit
to peterlewis/clock4
that referenced
this pull request
Jul 10, 2026
…ts dim out, not dash PR mitxela#9 computes each sub-second digit's live significance (digit_bright, 0..FADE_MAX, from the 3-sigma holdover uncertainty) but the display driver could only show-or-dash. PR mitxela#10 built the missing per-digit dimmer. This connects them: - segbal_poll scales the ds/cs/ms columns' mirror duty by their digit_bright, and the seconds column's decimal point by digit_bright[3] (it dies with the 0.1 s digit). While any digit is mid-fade the mirror runs even with seg_balance off (identity duty), so the fade works standalone; with balance on the two multiply. - setPrecision's significance_fade branch now retires digits to BLANK instead of a dash glyph — the digit fades to black, as the feature's own comment always intended. - Stock behaviour (significance_fade off) unchanged: the Tolerance dash ladder remains. Verified in the WASM build of this source: partial fade tracked exactly (holdover age 11 s -> ms digit at 14/16 significance -> lit in exactly 14 of 16 cycles, DP tracking), zero-significance digits blank in the masters, relock restores full digits with auto balance resumed. ARM Release 0 errors. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
With `pps = on` in config.txt, emit one proprietary NMEA sentence per PPS edge over the existing CDC stream, carrying the sub-second phase captured at the edge plus calibration / holdover / die-temperature telemetry. Time-critical fields are snapshotted in the PPS ISR; formatting and CDC submission happen in the main loop, serialised against the NMEA passthrough. With no enumerated host the record is dropped before any formatting. mk4-time/Core/Src/main.c: capturePPS(), emitPPSTimestamp(), measure_temp(), a `pps` config key and one main-loop hook. qspi/config.txt documents the key. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…ver USB Experimental attack on the ~6ms host-driven USB read jitter that swamps the clock's true ~180us precision — without a hardware PPS wire. The OTG_FS core rules out mitxela's last-microsecond FIFO injection, so instead we let the host anchor each PPS to a USB Start-Of-Frame, whose own arrival time the host can read in hardware (macOS IOKit GetBusFrameNumberWithTime). - Enable DWT->CYCCNT (free-running 12.5ns core-cycle counter) as the timebase both the PPS edge and each SOF are latched against. Unaffected by tempcomp SysTick steering (counts raw core clock), so it's a stable monotonic ruler. - Enable the USB SOF interrupt; PCD_SOFCallback latches (DWT, 11-bit frame from DSTS[13:8]) every 1ms, as its first act for minimal latency. - capturePPS() latches DWT at the edge; emitPPSTimestamp() appends the tail ,dwt_pps,sof_frame,dwt_sof to $PMTXTS (read under __disable_irq, no torn read). - Bump NMEA_BUF_SIZE 90->128 for the longer sentence. Host places the edge at hostTime(sof_frame) + (dwt_pps-dwt_sof)/f_dwt, immune to delivery lateness; dwt_pps deltas self-calibrate f_dwt (no core-clock assumption). Backward-compatible: lenient $PMTXTS parsers read the first 9 fields. Bench-only; +1kHz SOF ISR (~0.05% CPU) is the cost to watch. Builds clean (0 errors). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Adversarial audit of the SOF-correlation change surfaced two real issues: - Stale first anchor: pps_sof_dwt/frame init to 0, so the first PPS emitted after enumeration (or with pps just toggled on, or on the emulator which has no USB SOF) carried a (0,0) anchor → a ~53s/−729ms host error on that record. Now a pps_sof_valid flag gates the tail: emit the plain 9-field sentence until a real SOF has latched. Backward-compatible; also makes the emulator correct. - SOF work ran unconditionally. Gate the latch on pps_ts_enabled (the SOF IRQ still fires — cheap, below the priority-0 display DMA — but does nothing when the feature is off); clear valid when off so a re-enable can't reuse a stale anchor. config.txt documents that pps=on enables the SOF tail. Builds clean (0 errors). Refuted findings (torn read, 16-bit atomicity, buffer truncation, display preemption, scope) left as-is. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Five GPS-derived astronomy read-outs, each opt-in via a MODE_* config key and shown on the 10-char date row while the live clock keeps running on the time row (SATVIEW-style, COUNT_NORMAL): MODE_SUN sunrise / sunset / solar noon (local), auto-paged MODE_SUN_AZEL sun azimuth & elevation, now MODE_MOON moon phase index (0-7) + illuminated % MODE_GRID Maidenhead grid locator MODE_LATLON latitude / longitude, auto-paged The astronomy maths is a self-contained Core/Src/astro.c (+ astro.h): low- precision NOAA/Meeus sun alt-az and event times, moon phase, equation of time, and Maidenhead. It has no firmware dependencies, so it unit-tests natively (test/test_astro.c, 45 reference vectors) and was validated to ~1e-5 before touching the firmware. The L4 has only a single-precision FPU, so the double maths would be slow soft- float. It is therefore computed in the main loop (astro_update(), gated on the active mode exactly as measure_vbat() is for MODE_VBAT) and swapped into a cache under a brief __disable_irq() mask; the sendDate() cases that run inside the SysTick ISR only format the cached scalars -- no trig in the interrupt. Modes are disabled by default; with no GPS fix they use fake_latitude/longitude if set, else show "----". config.txt documents the keys and the moon-phase index legend. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
"LAT 51.48" vs "LAT-51.48" let the minus swallow the separator. Add a sign
slot after the label space — "LAT 51.48" / "LAT -51.48" — so the digits
start in the same column either way, matching the RISE/SET layout. A 3-digit
longitude can't fit both the separator and the slot in 10 characters, so the
separator alone is dropped there ("LON-179.99").
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The paged modes (SUN, LATLON) previously flipped sub-screen every 2 s hard-coded. Make the dwell a config key (page_ms, ms; unset -> 5500, floored at 250 so a tiny value can't flood the date-board UART), driven off uwTick, and repaint the moment a page flips rather than waiting for the 1 Hz date-row refresh (guarded by decisec!=9 to avoid racing the SysTick sendDate). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
peterlewis
added a commit
to peterlewis/clock4
that referenced
this pull request
Jul 12, 2026
…xela#9) The source said 0.0.5 (misidentifying a rollup build as stock) while the committed image embedded 0.0.6 from an earlier build lineage. Source and freshly built, CRC-gated image now both say 0.0.6. No cuckoo — that work is parked on its own branch and versions later. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- loadRules: reject untrusted rowLength/numEntries from TZRULES.BIN (RAM overflow) - MODE_TEXT: clamp snprintf untruncated-return index (uart2_tx_buffer OOB) - mk4-date latchDisplay: bound dp_pos per orientation (buffer_a/b OOB) - readConfigFile: don't treat a zero FAT timestamp as a cache hit (first-boot hang) - config-over-USB: defer postConfigCleanup out of the USB ISR (sendDate/nextMode races) - firmwareCheckOnEject: fatfs_busy guard for FATFS reentrancy on USB eject Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Two further pre-existing bugs from the same read-through. - mk4-time NMEA-over-CDC: CDC_Copy_Transmit() / CDC_Transmit_FS() dereference hUsbDeviceFS.pClassDataCDC unconditionally, but it is NULL until a USB host enumerates. The default nmea_cdc_level = NMEA_ALL forwards every GPS sentence to CDC, so on charger-only power the first forwarded sentence faults in ISR context. NULL guard on both, plus a length clamp before the memcpy. - mk4-date CMD_LOAD_TEXT: 'if (text_idx > MAX_TEXT_LEN) return;' passes at text_idx == MAX_TEXT_LEN and writes text[32], one past the 32-byte array. '>' -> '>='. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
MODE_LST and MODE_SOLAR turn the big HH:MM:SS digits into a live ticking Local Sidereal Time or apparent-solar clock. Both reuse the MODE_COUNTDOWN takeover pattern: the heavy GMST/EoT double maths runs once per second in the main loop, staging pre-rendered digits that the SysTick handlers latch at the true GPS PPS boundary -- so accuracy and the P3..P0 sub-second digit-hiding are inherited from the disciplined civil second, and civil timekeeping (currentTime, PPS, the date row) is untouched. The display is quantised to civil second boundaries and reseeded every second, so sidereal's 1.00273791x rate makes the seconds double-step once every ~6 min -- the honest signature of a GPS-disciplined sidereal clock. A dedicated colon animation (alt_colon_mode, default alt_sawtooth, kept distinct from the civil colon) marks the mode so it can never be mistaken for civil time; apparent solar especially can sit within minutes of it. With no position the row shows dashes -- never GMST-as-LST. astro.c regains local_sidereal_time()/local_solar_time() with the GMST series factored into one helper (gmst_hours, +T^2 term); native suite 53/53 incl. the IAU J2000 anchor and Meeus example 12.b. All default-off: stock behaviour unchanged. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
sun_subsolar(t, &lat, &lon) returns the point where the sun is at the zenith: latitude = solar declination, longitude where the local hour angle is zero (lon = alpha - gmst*15). It reuses the same gmst_hours series as sun_az_el and local_sidereal_time, so the three can never drift apart. Library + tests only — no display mode consumes it yet (a companion app plots it). Tests: the sun must sit at elevation 90 deg over its own subsolar point at every existing test instant (self-consistent but not circular — az/el and subsolar take different paths to the sky), plus declination anchors at the solstices and the absolute GMST anchors (IAU J2000, Meeus ex. 12.b) recorded in comments. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Builds on the $PMTXTS telemetry: while GPS-locked the clock learns each oscillator's frequency error vs die temperature (binned, per-oscillator), then during a GPS-loss holdover it steers the SysTick timebase from the HSE model to cancel temperature-driven drift, and optionally trims RTC->CALR from the LSE model so the battery RTC hands over better time across a power loss. - tc_learn / tc_apply / tc_rtc config keys (all default off = stock behaviour) - steering is a Bresenham SysTick->LOAD stretch inside the tick ISR (integer only; all model maths is main-loop); origin-free HSE model so only the temperature DIFFERENCE since GPS loss is applied, and the per-edge PPS phase snap makes re-lock instantly neutral - 'tc_dump = on' over serial prints the learned coefficients as paste-ready config lines (+ a $PMTXTC sentence); pasting them back freezes the model - MODE_TEMPCOMP diagnostic display: die temp / model offset / sample count - ticks-per-ppm derived from the runtime SysTick period (no core-clock assumption) Hardware-validated on an Mk IV: the LSE model learned 18.68 ppm vs 18.9 ppm measured directly (~1%). Survived an adversarial review pass (ISR-safety, integer math, sign conventions, re-lock neutrality, off-means-identical). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Two coupled additions on top of the self-learning tempcomp, both opt-in and byte-identical to stock behaviour when off. significance_fade: during GPS-loss holdover, retire each sub-second digit by its remaining SIGNIFICANCE rather than by the fixed dash timeouts. A live 3-sigma time-interval-error bound U(tau) = k*sigma*tau (sigma = RSS of the calibration, the MEASURED temp-model residual, and aging terms) drives setPrecision's P3/P2/P1/P0 ladder: a digit dashes the moment U passes its place value — the same dash glyph as the Tolerance_time_* ladder, but driven by measured significance instead of fixed timeouts. digit_bright[] carries the per-digit intensity for a future smooth-dimming display change (and for emulators). tc_fit now also reports the weighted-RMS fit residual, mean-centred for HSE whose model origin is arbitrary. tc_seed (warm start): reload a previously-learned model -- the last tc_dump, persisted in config.txt by the host -- as an EVOLVING prior rather than a freeze. The clock is temperature-compensated from the first second and keeps refining: tc_fit_one returns the achieved model order and tc_fit preserves the seed until real data supports a fit at least as rich, then hands over. New keys tc_seed / tc_seed_lo / tc_seed_hi (documented in qspi/config.txt); tc_dump emits the coverage so a paste round-trips. Over serial the "tc_seed = on" line is the trigger -- it arms tc_seed_pending (a single-word ISR write) and tc_housekeeping applies the seed from the MAIN LOOP, serialized with tc_fit/tc_governor, after the whole paste has parsed. The learned state keeps its main-loop-only contract; the USB ISR never touches the model. Survived two adversarial review passes (the feature, then this port), with fixes re-verified in the emulator against this exact tree: boot-time tc_nom_load capture (ticks-per-ppm is never 0), origin-invariant HSE residual, main-loop-serialized serial seeding with a whole-paste trigger, tc_reset clears the held prior, significance_fade drives the dash ladder (no regression vs stock when enabled), and measure_temp's cadence gate includes the fade so die_temp_c is always real. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…, not FADE_MAX
On a warm boot with significance_fade on, digit_bright[] defaulted to
{FADE_MAX,...} ("certainly significant"), so setPrecision() at boot (and each
PendSV second) showed the ms digit ticking — a ~1 s flash of numbers — until the
first per-second computeHoldoverFade() saw the huge holdover age (last_pps_time=0,
no PPS yet) and dashed them. Default to 0 (not significant): the sub-second digits
start dashed and only light once computeHoldoverFade proves significance after
lock (the same PendSV setPrecision path applies it, symmetric). Pre-existing PR mitxela#9
issue, not the SOF change. Builds clean.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…ing arrives with seg_balance) PR mitxela#9 computes each sub-second digit's live significance (digit_bright, 0..FADE_MAX, from the 3-sigma holdover uncertainty) but the display driver could only show-or-dash. PR mitxela#10 built the missing per-digit dimmer. This connects them: - segbal_poll scales the ds/cs/ms columns' mirror duty by their digit_bright, and the seconds column's decimal point by digit_bright[3] (it dies with the 0.1 s digit). While any digit is mid-fade the mirror runs even with seg_balance off (identity duty), so the fade works standalone; with balance on the two multiply. - setPrecision's significance_fade branch now retires digits to BLANK instead of a dash glyph — the digit fades to black, as the feature's own comment always intended. - Stock behaviour (significance_fade off) unchanged: the Tolerance dash ladder remains. Verified in the WASM build of this source: partial fade tracked exactly (holdover age 11 s -> ms digit at 14/16 significance -> lit in exactly 14 of 16 cycles, DP tracking), zero-significance digits blank in the masters, relock restores full digits with auto balance resumed. ARM Release 0 errors. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The MODE_TEMPCOMP date-row pages honour the configurable page_ms dwell instead of a hardcoded 5.5 s, and the mode's enum entry moves to its final position (after the astro read-outs, before the alternate-timebase modes) so later tiers append ordinals without reshuffling. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
peterlewis
added a commit
to peterlewis/clock4
that referenced
this pull request
Jul 12, 2026
…xela#9) The source said 0.0.5 (misidentifying a rollup build as stock) while the committed image embedded 0.0.6 from an earlier build lineage. Source and freshly built, CRC-gated image now both say 0.0.6. No cuckoo — that work is parked on its own branch and versions later. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
peterlewis
added a commit
to peterlewis/clock4
that referenced
this pull request
Jul 12, 2026
Opt-in `tc_persist = on`: while GPS-locked the clock learns its oscillator
tempco, and a well-supported model is saved to a dedicated 2-page flash store
so the first holdover after a power-up is already compensated instead of
re-learning from cold. On boot it loads back as an EVOLVING seed through the
existing tc_seed path; config.txt still wins per-key.
Store (ee2) is parallel to the menu store, reusing its address helpers and
append-only ping-pong framing (magic/gen/schema, CRC16 in the last doubleword
so a torn write fails CRC and the prior generation wins). Runtime-derived base
lands in bank 2 on the RG (read-while-write; ~20 ms erase never stalls PPS or
SysTick) and disables itself below 0x8040000 (RC -> RAM-only, no brick).
Pre-mortem fixes folded in:
- persist gate: only a well-supported fit reaches flash (>=300 HSE samples,
>=4 C coverage, believable residual) -> a shaky fit can never auto-seed a
bad steer next boot.
- load sanity: a non-finite / physically-implausible stored model is rejected
before it can seed the timebase.
- break the permanent-hold trap: cap the seeded prior order at 2 so the first
genuine linear fit supersedes the seed.
- wear: commit at most once per 30 min on a MONOTONIC clock (uwTick), gated on
L0 + UART-idle + !pps_record_pending, and only when the model moved
meaningfully (ppm-at-{lo,mid,hi} change detector). A few writes/day -> the
10k-cycle flash lasts for decades.
- config precedence: tc_seed/coefficient/tc_t0 lines in config.txt override the
stored model (cfg_tc_defined bitmask).
Serial: `tc_forget = on` erases the stored copy (live model untouched);
`tc_reset = on` clears both.
Also bake the measured VTT9812FH (R11=470K) brightness curve as the compiled-in
brightnessCurve[] default, so the clock needs no config.txt BS lines (a BS line
still overrides its stop). config.txt documents tc_persist and marks the BS
lines optional.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Gradual significance fade: digit_bright drives the duty mirror — digits dim out, not dash
PR mitxela#9 computes each sub-second digit's live significance (digit_bright, 0..FADE_MAX,
from the 3-sigma holdover uncertainty) but the display driver could only show-or-dash.
PR mitxela#10 built the missing per-digit dimmer. This connects them:
- segbal_poll scales the ds/cs/ms columns' mirror duty by their digit_bright, and the
seconds column's decimal point by digit_bright[3] (it dies with the 0.1 s digit).
While any digit is mid-fade the mirror runs even with seg_balance off (identity duty),
so the fade works standalone; with balance on the two multiply.
- setPrecision's significance_fade branch now retires digits to BLANK instead of a dash
glyph — the digit fades to black, as the feature's own comment always intended.
- Stock behaviour (significance_fade off) unchanged: the Tolerance dash ladder remains.
Verified in the WASM build of this source: partial fade tracked exactly (holdover age
11 s -> ms digit at 14/16 significance -> lit in exactly 14 of 16 cycles, DP tracking),
zero-significance digits blank in the masters, relock restores full digits with auto
balance resumed. ARM Release 0 errors.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Fix significance_fade boot flash: digit_bright defaults to 0 (dashed), not FADE_MAX
On a warm boot with significance_fade on, digit_bright[] defaulted to
{FADE_MAX,...} ("certainly significant"), so setPrecision() at boot (and each
PendSV second) showed the ms digit ticking — a ~1 s flash of numbers — until the
first per-second computeHoldoverFade() saw the huge holdover age (last_pps_time=0,
no PPS yet) and dashed them. Default to 0 (not significant): the sub-second digits
start dashed and only light once computeHoldoverFade proves significance after
lock (the same PendSV setPrecision path applies it, symmetric). Pre-existing PR mitxela#9
issue, not the SOF change. Builds clean.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Holdover significance-fade + persistent, evolving warm-start temp model
Two coupled additions on top of the self-learning tempcomp, both opt-in and
byte-identical to stock behaviour when off.
significance_fade: during GPS-loss holdover, retire each sub-second digit by its
remaining SIGNIFICANCE rather than by the fixed dash timeouts. A live 3-sigma
time-interval-error bound U(tau) = k*sigma*tau (sigma = RSS of the calibration, the
MEASURED temp-model residual, and aging terms) drives setPrecision's P3/P2/P1/P0
ladder: a digit dashes the moment U passes its place value — the same dash glyph as
the Tolerance_time_* ladder, but driven by measured significance instead of fixed
timeouts. digit_bright[] carries the per-digit intensity for a future smooth-dimming
display change (and for emulators). tc_fit now also reports the weighted-RMS fit
residual, mean-centred for HSE whose model origin is arbitrary.
tc_seed (warm start): reload a previously-learned model -- the last tc_dump, persisted
in config.txt by the host -- as an EVOLVING prior rather than a freeze. The clock is
temperature-compensated from the first second and keeps refining: tc_fit_one returns
the achieved model order and tc_fit preserves the seed until real data supports a fit
at least as rich, then hands over. New keys tc_seed / tc_seed_lo / tc_seed_hi
(documented in qspi/config.txt); tc_dump emits the coverage so a paste round-trips.
Over serial the "tc_seed = on" line is the trigger -- it arms tc_seed_pending (a
single-word ISR write) and tc_housekeeping applies the seed from the MAIN LOOP,
serialized with tc_fit/tc_governor, after the whole paste has parsed. The learned
state keeps its main-loop-only contract; the USB ISR never touches the model.
Survived two adversarial review passes (the feature, then this port), with fixes
re-verified in the emulator against this exact tree: boot-time tc_nom_load capture
(ticks-per-ppm is never 0), origin-invariant HSE residual, main-loop-serialized
serial seeding with a whole-paste trigger, tc_reset clears the held prior,
significance_fade drives the dash ladder (no regression vs stock when enabled), and
measure_temp's cadence gate includes the fade so die_temp_c is always real.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Add self-learning holdover temperature compensation
Builds on the $PMTXTS telemetry: while GPS-locked the clock learns each
oscillator's frequency error vs die temperature (binned, per-oscillator), then
during a GPS-loss holdover it steers the SysTick timebase from the HSE model to
cancel temperature-driven drift, and optionally trims RTC->CALR from the LSE
model so the battery RTC hands over better time across a power loss.
- tc_learn / tc_apply / tc_rtc config keys (all default off = stock behaviour)
- steering is a Bresenham SysTick->LOAD stretch inside the tick ISR (integer
only; all model maths is main-loop); origin-free HSE model so only the
temperature DIFFERENCE since GPS loss is applied, and the per-edge PPS phase
snap makes re-lock instantly neutral
- 'tc_dump = on' over serial prints the learned coefficients as paste-ready
config lines (+ a $PMTXTC sentence); pasting them back freezes the model
- MODE_TEMPCOMP diagnostic display: die temp / model offset / sample count
- ticks-per-ppm derived from the runtime SysTick period (no core-clock assumption)
Hardware-validated on an Mk IV: the LSE model learned 18.68 ppm vs 18.9 ppm
measured directly (~1%). Survived an adversarial review pass (ISR-safety,
integer math, sign conventions, re-lock neutrality, off-means-identical).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
sidereal: rename config key alt_colon_mode -> colon_alt_mode
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Add sidereal (LST) and apparent-solar time-row modes
MODE_LST and MODE_SOLAR turn the big HH:MM:SS digits into a live ticking
Local Sidereal Time or apparent-solar clock. Both reuse the MODE_COUNTDOWN
takeover pattern: the heavy GMST/EoT double maths runs once per second in the
main loop, staging pre-rendered digits that the SysTick handlers latch at the
true GPS PPS boundary -- so accuracy and the P3..P0 sub-second digit-hiding are
inherited from the disciplined civil second, and civil timekeeping (currentTime,
PPS, the date row) is untouched.
The display is quantised to civil second boundaries and reseeded every second,
so sidereal's 1.00273791x rate makes the seconds double-step once every ~6 min
-- the honest signature of a GPS-disciplined sidereal clock. A dedicated colon
animation (alt_colon_mode, default alt_sawtooth, kept distinct from the civil
colon) marks the mode so it can never be mistaken for civil time; apparent solar
especially can sit within minutes of it. With no position the row shows dashes
-- never GMST-as-LST.
astro.c regains local_sidereal_time()/local_solar_time() with the GMST series
factored into one helper (gmst_hours, +T^2 term); native suite 53/53 incl. the
IAU J2000 anchor and Meeus example 12.b. All default-off: stock behaviour
unchanged.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Fix charger-only-power CDC hard-fault and date-board CMD_LOAD_TEXT OOB
Two further pre-existing bugs from the same read-through.
- mk4-time NMEA-over-CDC: CDC_Copy_Transmit() / CDC_Transmit_FS() dereference
hUsbDeviceFS.pClassDataCDC unconditionally, but it is NULL until a USB host
enumerates. The default nmea_cdc_level = NMEA_ALL forwards every GPS sentence
to CDC, so on charger-only power the first forwarded sentence faults in ISR
context. NULL guard on both, plus a length clamp before the memcpy.
- mk4-date CMD_LOAD_TEXT: 'if (text_idx > MAX_TEXT_LEN) return;' passes at
text_idx == MAX_TEXT_LEN and writes text[32], one past the 32-byte array.
'>' -> '>='.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Harden firmware: bounds + ISR-safety fixes from broad audit
- loadRules: reject untrusted rowLength/numEntries from TZRULES.BIN (RAM overflow)
- MODE_TEXT: clamp snprintf untruncated-return index (uart2_tx_buffer OOB)
- mk4-date latchDisplay: bound dp_pos per orientation (buffer_a/b OOB)
- readConfigFile: don't treat a zero FAT timestamp as a cache hit (first-boot hang)
- config-over-USB: defer postConfigCleanup out of the USB ISR (sendDate/nextMode races)
- firmwareCheckOnEject: fatfs_busy guard for FATFS reentrancy on USB eject
astro: subsolar-point helper (sun_subsolar) + shared-series comments
sun_subsolar(t, &lat, &lon) returns the point where the sun is at the zenith:
latitude = solar declination, longitude where the local hour angle is zero
(lon = alpha - gmst*15). It reuses the same gmst_hours series as sun_az_el and
local_sidereal_time, so the three can never drift apart. Library + tests only —
no display mode consumes it yet (a companion app plots it).
Tests: the sun must sit at elevation 90 deg over its own subsolar point at every
existing test instant (self-consistent but not circular — az/el and subsolar
take different paths to the sky), plus declination anchors at the solstices and
the absolute GMST anchors (IAU J2000, Meeus ex. 12.b) recorded in comments.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
astro: configurable per-screen page dwell (page_ms, default 5500 ms)
The paged modes (SUN, LATLON) previously flipped sub-screen every 2 s hard-coded.
Make the dwell a config key (page_ms, ms; unset -> 5500, floored at 250 so a tiny
value can't flood the date-board UART), driven off uwTick, and repaint the moment a
page flips rather than waiting for the 1 Hz date-row refresh (guarded by decisec!=9
to avoid racing the SysTick sendDate).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
astro: give LAT/LON a sign slot so negatives keep the label gap
"LAT 51.48" vs "LAT-51.48" let the minus swallow the separator. Add a sign
slot after the label space — "LAT 51.48" / "LAT -51.48" — so the digits
start in the same column either way, matching the RISE/SET layout. A 3-digit
longitude can't fit both the separator and the slot in 10 characters, so the
separator alone is dropped there ("LON-179.99").
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Add astro pack: sun / moon / Maidenhead / lat-lon display modes
Five GPS-derived astronomy read-outs, each opt-in via a MODE_* config key and
shown on the 10-char date row while the live clock keeps running on the time
row (SATVIEW-style, COUNT_NORMAL):
MODE_SUN sunrise / sunset / solar noon (local), auto-paged
MODE_SUN_AZEL sun azimuth & elevation, now
MODE_MOON moon phase index (0-7) + illuminated %
MODE_GRID Maidenhead grid locator
MODE_LATLON latitude / longitude, auto-paged
The astronomy maths is a self-contained Core/Src/astro.c (+ astro.h): low-
precision NOAA/Meeus sun alt-az and event times, moon phase, equation of time,
and Maidenhead. It has no firmware dependencies, so it unit-tests natively
(test/test_astro.c, 45 reference vectors) and was validated to ~1e-5 before
touching the firmware.
The L4 has only a single-precision FPU, so the double maths would be slow soft-
float. It is therefore computed in the main loop (astro_update(), gated on the
active mode exactly as measure_vbat() is for MODE_VBAT) and swapped into a cache
under a brief __disable_irq() mask; the sendDate() cases that run inside the
SysTick ISR only format the cached scalars -- no trig in the interrupt.
Modes are disabled by default; with no GPS fix they use fake_latitude/longitude
if set, else show "----". config.txt documents the keys and the moon-phase
index legend.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
SOF timestamping: audit fixes — valid-gated tail, latch only when pps=on
Adversarial audit of the SOF-correlation change surfaced two real issues:
- Stale first anchor: pps_sof_dwt/frame init to 0, so the first PPS emitted
after enumeration (or with pps just toggled on, or on the emulator which has
no USB SOF) carried a (0,0) anchor → a ~53s/−729ms host error on that record.
Now a pps_sof_valid flag gates the tail: emit the plain 9-field sentence until
a real SOF has latched. Backward-compatible; also makes the emulator correct.
- SOF work ran unconditionally. Gate the latch on pps_ts_enabled (the SOF IRQ
still fires — cheap, below the priority-0 display DMA — but does nothing when
the feature is off); clear valid when off so a re-enable can't reuse a stale
anchor. config.txt documents that pps=on enables the SOF tail.
Builds clean (0 errors). Refuted findings (torn read, 16-bit atomicity, buffer
truncation, display preemption, scope) left as-is.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
SOF-correlation timestamping: latch (USB frame, DWT) for sub-ms PPS over USB
Experimental attack on the ~6ms host-driven USB read jitter that swamps the
clock's true ~180us precision — without a hardware PPS wire. The OTG_FS core
rules out mitxela's last-microsecond FIFO injection, so instead we let the host
anchor each PPS to a USB Start-Of-Frame, whose own arrival time the host can
read in hardware (macOS IOKit GetBusFrameNumberWithTime).
- Enable DWT->CYCCNT (free-running 12.5ns core-cycle counter) as the timebase
both the PPS edge and each SOF are latched against. Unaffected by tempcomp
SysTick steering (counts raw core clock), so it's a stable monotonic ruler.
- Enable the USB SOF interrupt; PCD_SOFCallback latches (DWT, 11-bit frame from
DSTS[13:8]) every 1ms, as its first act for minimal latency.
- capturePPS() latches DWT at the edge; emitPPSTimestamp() appends the tail
,dwt_pps,sof_frame,dwt_sof to $PMTXTS (read under __disable_irq, no torn read).
- Bump NMEA_BUF_SIZE 90->128 for the longer sentence.
Host places the edge at hostTime(sof_frame) + (dwt_pps-dwt_sof)/f_dwt, immune to
delivery lateness; dwt_pps deltas self-calibrate f_dwt (no core-clock assumption).
Backward-compatible: lenient $PMTXTS parsers read the first 9 fields. Bench-only;
+1kHz SOF ISR (~0.05% CPU) is the cost to watch. Builds clean (0 errors).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Add $PMTXTS per-PPS host timestamping (opt-in)
With `pps = on` in config.txt, emit one proprietary NMEA sentence per PPS edge
over the existing CDC stream, carrying the sub-second phase captured at the edge
plus calibration / holdover / die-temperature telemetry. Time-critical fields are
snapshotted in the PPS ISR; formatting and CDC submission happen in the main loop,
serialised against the NMEA passthrough. With no enumerated host the record is
dropped before any formatting.
mk4-time/Core/Src/main.c: capturePPS(), emitPPSTimestamp(), measure_temp(), a
`pps` config key and one main-loop hook. qspi/config.txt documents the key.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
adjust hole sizes on acrylic support parts
tap drills to 2.6mm, switch holes to 4.0mm, lightsensor to 6.02mm
inc fwt version
add MODE_WDY_MM_DD, truncated weekday with MM-DD
add MODE_WEEKDA_DD
the missing Y is a reminder that some of the weekday names are truncated
Fix errant extra mode
This typo was simple enough to spot once we went looking for it, but the curious thing is that we hadn't seen any symptoms until now.
When cycling through modes with the buttons, going in one direction works fine but the other requires an extra button press to loop. By a quirk of the memory layout it only affects some clocks, and only when they've been powered on long enough to have a full fix for a while.
When cycling downwards, the loop erroneously attempted to set the mode to NUM_DISPLAY_MODES, which is one beyond the end of the array. It continues cycling until it finds an enabled mode. Looking at the memory map, the next value in memory happened to be debug_rtc_val:
*fill* 0x200005a5 0x3
.bss.config 0x200005a8 0x40 ./Core/Src/main.o
0x200005a8 config
.bss.debug_rtc_val
0x200005e8 0x4 ./Core/Src/main.o
0x200005e8 debug_rtc_val
.bss.nmea_cdc_level
0x200005ec 0x1 ./Core/Src/main.o
0x200005ec nmea_cdc_level
.bss.colonMode
0x200005ed 0x1 ./Core/Src/main.o
0x200005ed colonMode
This integer value is zero at powerup and later contains the remainder after calibrating the RTC crystal over a 63 second period. So it's only ever non-zero if the clock has had PPS for more than 63 seconds, and then depending on the unique crystal characteristics of that hardware, the value may be positive, meaning the high byte is zero. Only if the remainder is negative would the errant extra mode appear. The first batch of clocks in 2026 are apparently the first ones to meet the criteria.
The fallback in sendDate() for the unknown mode is just ISO8601 standard, so this doesn't really matter but it's interesting it flew under the radar for so long.
add wider versions of shelfstand for acrylic
split wallhanger4 into separate files, move keyhole to top
reduce wallhanger4 spring pressure to 3 degrees
New wallhanger that clips only onto PCB
cleaner way of grabbing the versions
update github release script
Merge branch 'ttff'
Release script for zip file with disk image and bootloader
if enabled, always start with mode_ttff
mode to display time till first fix
inc firmware versions
improve hinge-tile-gen.py
Hold both buttons to reset
smoked acrylic svg with 50 micron kerf baked in
simplify acrylic case svg
bootloader: skip erase if not needed
Erasing a page is much, much slower than writing a page.
Chips arrive erased from the factory, so this significantly speeds up the initial flashing process.
additional support for acrylic case threaded holes
lasercut pieces tile generator
hinge.svg threaded holes dia
hinge.svg: start and stop paths on corners, not at the arc
add acrylic case cad files
appease demands for noncompliant date formats
...but hide them behind an #ifdef
fwd protect stack when jumping to system loader
can't be too careful eh
fwt version 0.0.2
Add display test mode
Countdown: set decimal point when epoch has past
Once the moment has gone, always display 0.000, regardless of GPS/RTC precision
Resend date in pendSV during count_hidden
Enables text such as "utc offset" to still flip over when clock is folded/unfolded
Double-sided tape wallhanger by Kevin Thomasson
slim wallhanger (mitxela#3)
* slim wallhanger
* rename
* added midpoint locker
typo in hinge.svg
Black 6mm rectangle should have a height of 22.56, with the lower hole 6mm from the bottom, and the top hole 5.28 from the top. It was off by 0.28mm so hardly noticable with the kerf of the laser, but we may as well make it exact.
Add redrawn STEP files
These don't exactly match the STL files although both will work. STL files were generated from the OpenSCAD file. STEP files were redrawn with some fillets and other tweaks, moving forwards the step files may be the main reference
Move main STL files into folders for rev C, rev D
Set brightness curve in config file for phototransistor
The default curve in the firmware, with nothing specified in the config file (or this commented out) matches the LDR.
add STL for phototransistor (VTT9812FH)
bootloader user request on PA2/hinge TX
update config.txt template
inc fwt version
wipe vbat measurement on leaving mode_vbat
Reduce power-on precision if coin cell below 2.7V
oversampling vbat is substantially more stable
mode_vbat
initial vbat reading using adc3
monitor beidou, plot gps and the highest non-gps satview
further complication of GSV parsing
signal ID is not always present, number of fields is variable, what was meant to be a quick way of showing number of satellites is becoming a mess
correctly interpret multiple GSV messages
M10 gps module sends GPGSV multiple times in separate groups, even though it's only listening to a single band.
latch satview
ublox 10 series output GSV only every 5 seconds
update qspi.md
tzmap 2025b (full)
tzrules to 2025b
update timezone-names to 2025b
Bootloader updates to explicitly protect the stack
Using both noreturn and __builting_unreachable() is superfluous but hopefully makes it twice as unlikely to optimise the jump into a tail call. Not sure if this is better than -O0 but hopefully makes some people less unhappy.
add physical dimensions to hinge svg
generate either version in openscad
cad for vtt9812fh
a dubiously helpful overlay for brightness curve
method to convert a bunch of brightness values
basic support for touchscreen devices
allow pasting in brightness curve values
generate binary files
compatibility with toolchain 12.3.rel1
Ignore env hashes using clean/smudge filter
Add following to .git/config:
[filter "eclipse_env_hash"]
clean = sed 's/env-hash=\"[-,0-9]*\"/env-hash=\"0000000000000000000\"/g'
smudge = cat
Revert "Don't track these IDE files that keep changing"
This reverts commit 8e4d62c.
add shelfstand cad files
Add handcrafted SVG version of moshidraw hinge
add readme
CAD files for antenna case
Specifically for the patch antenna with SMA connector crimped as close as possible. Top half (caset.stl) needs to be printed upside down with support for the cable shroud. Last modified 2023-04-19
Add cad files for wall hangers
lasercut files update for easier tiling
Add moshidraw file for hinge (convert to DXF or SVG later)
make colon cover a teeny bit taller
add side cutouts to colons for better pliability
flat-top LEDs have 3.15 dia
commit STL files
cad updates from 2023-06-15
start adding cad files to this repo
Don't track these IDE files that keep changing
Add known firmware images to repo (2024-05-29)
Config file defaults, most modes off
IDE settings (unsure what changed)
slightly smaller hammer (re: b91a100)
sledgehammer to prevent -Os trashing launch_app()
suppress maybe-uninitialised warning
Pad bootloader to exactly 64K
Bootloader version string
env hash stuff - should probably ignore these files
partial changes to bootloader - unfinished
f_mount doesn't actually do anything until first read (unless forced) so it's not really possible to error out at the mount stage, but we don't want any possible way to hang without enabling usb
Clear colons in bootloader
They are off when tristated too but need to be explicitly high to remain off on the xl
and volatile again in the bootloader
This was working without it, so the fix is hypothetical. Might be because bootloader is -Os and time is -O3
another volatile optimisation victim
set volume label to CLOCK
blank date when updating on eject
delay setDisplayFrequency if needed
Don't write RTC during USB read/writes, pendSV could be delayed
fix countdown mode entering/exiting
Replace delayedButtonPress with resendDate
Not sure if this is better than the alternative, that is to delay CDC commands and reloaded config too.
Todo: countdown mode fixes
Measure lookup time macro
Takes 3x longer if launched via swd instead of bootloader. QSPI quad read seems to make it slower. Definitely something odd going on
possible compiler optimisation problems
enable config mtime checking
ftime field has a resolution of 2 seconds, so fast changes can be missed.
But this has the benefit of not wiping out CDC config when the OS does random other writes we might not have supressed (system volume information, DS_Store, etc)
remount filesystem, skip config mtime check
The remount does fix some occasional fs problems, which seems to just be stale data in fatfs window. This can be invalidated by doing USERFatFS.winsect = -1 but safest to remount properly.
The config ftime still sometimes misses changes if they're in quick succession. This might even be the OS not updating the value, or the low resolution (to the second). But realistically the config file is normally the only thing that gets changed so I don't think always reloading it is a problem.
manually manage Etc/ zones again
zonedetect backoff on usb read too
Only zonedetect once per nmea
Priority rewrite, avoid f_read in usb context
more typos
Typo
dos2unix generate-tzrules.py
Fix tzrules for zone names out of order
single script for flashing and creating disk image
mount as loop device is auto determined
create dummy 'system volume information'
Set qspi_write_time even if write fails
Only reload config if modified
update config template
Accept fake long/lat from config
day-of-week mode for no reason
if rtc valid but no tzrules file, copy out rtc rules string too
enum the loadrules error codes
reload map file each time
only copy rules string if rules valid
configure debug/release builds for date
same treatment for bootloader
fix linux eject sequence
util-linux eject sends a scsi start immediately before scsi stop
QSPI_STATUS consistency in bootloader
hacky delayed-check-on-eject
This is pretty much impossible to test anyway
Revert "Add fatfs read qspi timeout"
This reverts commit 70553aa.
Add fatfs read qspi timeout
Protection against qspi collisions
Not convinced the lock stuff is bullet proof. The zonedetect backoff is enough to fix it but there's still a remote chance something could get corrupted.
Also the chainloader ejection check runs in the usb interrupt context, if qspi was locked at ejection this could hang
fix typo (DP on error)
Don't connect USB during date update, unless an error occurs
force update version string on every build
reboot on eject if new firmware present
corrupted filesystem could cause f_crc to hang
Flashing script
rollover fix (hypothetical, from earlier)
we want at least the option of separate version strings
linker alignment fix and give bootloader same treatment
share version string method
ISO8601 build string, worth it
propagate changes to linker script
There's only one line difference between the scripts, we should probably try passing a parameter from the ide build options
Add build datetime string to firmware image
One disadvantage to this linker method is the error message is nonsensical if you overflow the pad region
update create-image.sh
fixup! Commit release script
Show firmware crc (date)
Firmware CRC display mode (for time)
Commit release script
Add firmware CRC script
Add notes about QSPI flash contents
Add example config.txt
Create disk image using loop device
Add example tzmap.bin (2023b)
Add example tzrules output
Add example timezone-names file
Specifically needs to match the shapefile version we're using
curl -L https://github.com/evansiroky/timezone-boundary-builder/releases/download/2023b/timezone-names.json | jq '.' > timezone-names.json
Newer tzrules generator, uses zdump and subprocess
Add original tzrules generator, depends on external csv
Add reboot CDC command
manual vbus detection for self powered version
Enable PA8 input and update IOC
Fix inverted date trailing dot error
In the inverted position, the decimal point is shown on the following digit. If the DP is the last character of the message, it would not set the cathodes and multiple dots would light up.
Pullup on usart2 rx
Without it and without date connected, frame error interrupt is called continuously
update default brightness curve for LDR
Enabling a mode via CDC should switch to it
Rip out io retarget stuff
USB Eject bootloader should retry update
Debug RTC mode + theoretical improvements to RTC cal
Critical fix to RTC calibration
When launched from ST-LINK or without bootloader, the calibration routine works fine. When launched by the bootloader, even if we jump out before anything is initialised, the calibration was way off. The LPTIM1 interrupt was running twice for each counter reset, because the interrupt ended before the flag had cleared. Clearing the flag at the start of the interrupt instead of the end fixes it.
Can't figure out why the launch method changes this, it must be something to do with the pipeline acceleration. Both bootloader and app have the same startup file, and it still happens if we skip out all of HAL.
display fault type (could make this more helpful at some point)
can have GPS satellites but no GLGSV message
suppress snprintf warning
"fix" hardfault on usb scsi command
When launched after firmware update, first boot causes hardfault when scsi command happens. The GetCapacity function pointer in USBD_Storage_Interface_fops_FS is corrupted.
Launching the older non-CDC firmware is fine. Disabling usb in bootloader has no effect. Zeroing ram or adding delays has no effect, but breakpointing with the debugger stops it happening.
Calling reset after a firmware update seems hacky but appears to fix everything.
bootloader: only init USB on error
prevents double-connect when booting normally
Alternate show tz name
Fix show offset
brightness curve in config file
prepare to rewrite brightness curve
pointless attention to detail
1.0 or +1.0 should be max brightness, 1 should be 1/4095
brightness override float optional
send full text buffer
dots are valid characters that don't take up a digit, so a valid message could be 20 bytes long. Might as well send it all
Brightness override
fix likely glitch with uart handler
was checking against UART_IT_RXNE instead of USART_ISR_RXNE
also explicitly hand errors over to the HAL routine
button delayed auto shift
date: don't disable swclk if button held at power on
iso week mode
Merge branch 'cdc-msc'
expand falsey
satview be explicit if there's no data
nmea output level, cleanup cdc transmit
add generic text display mode
rx config value can be empty
cope with config file not present
config cleanup on cdc commands
parse config from cdc input
fix MSC eject
fix descriptors for bus powered, 500mA
update build config with alt linker script
rip out unused descriptors
USB CDC MSC
CDC MSC Almost there
initial steps on usb msc+cdc composite
Add basic satview mode
countdown tolerance; handle on-the-fly changes
allow countdowns to the year 9999
Countdown enter/exit
Starting on countdown mode
Update eclipse env-hash nonsense
At some point we should clean/smudge this out of the repo
DP debug signals
update manufacturer/product/scsi description
Add brightness debug mode
Delay enabling uart2 interrupt until after doDateUpdate
swap buttons when inverted
proobably shouldn't be tracking these files
code generation using workspace 1.8.0
update ioc for code gen
update file names
code generated workspace 1.8.0
update .ioc for working code generation
update file name
Code generation using workspace 1.8.0
Update .ioc for correct code generation
update env hash
update project name
remove .launch files
Merge repos
Merge repos
Move everything into subdir
disable power on test message
Move everything into subfolder
Move everything into subfolder
Configure PC12 correctly to hide DP at power-on
Update to work with chips bigger than 256K
adjust animation speed to compensate for slower erase of the bank we're running our code from
don't light decimal points during bootloader
button 2 scrolls modes in reverse
swap colons, don't show DP during date update
env hash update
Add second button
Add second button
Colon flashing modes
Colons independently fading
Separate PWM for each colon pair
Fix .ioc, culprit was ADC1.DMAContinuousRequests=ENABLE
Interim checkin before .ioc corruption
Forget using DMA for ADC reading
Show offset mode and hook into USB write
Support for modes that don't display time
Use hyphens for reduced precision
Standby mode
Load tolerances from config file
Load modes from config file
sendDate cleanup
Reduce precision to align with accuracy
Rewire colons
Ordinal date
Julian date
Rudimentary support for decimal places
Debouncing
Button press changes date display
Initial button press mechanism
Move sTimeDPR to PC12
Calibrate RTC
Use LPTIM clocked from LSE
Better latch mechanism
Better latch mechanism
Improve rtc subsecond stuff
Load frequency from config
set frequency fixes
cmd set frequency
Cleanup
RTC subseconds somewhat accurate
Fixes to enable code generation
Fiddling with rtc timings
Smooth progress again
alternate progress bar
Smooth progress
Look ma no magic numbers
Progress bar progress
chainloadin' is a happenin'
Use 8E1 always to avoid having to switch
Recovery logic in place
Chainloader progress
Chainloader progress
Move display matrix to TIM7 to free up UART2 TX DMA channel
More RTC stuff
RTC basics
Ticking
Further fiddling with pointless parts
Buffer wheel thing
Mostly lame progress bar
We has a bootloader
Main structure in place
Checks CRC of firmware image on fatfs
Possibly better way of filling the flash, still lets us see usage
One method of filling the flash
Initial commit with working display, QSPI FATFS and USB
Safely shift code section to make room for bootloader
Power up at lowest brightness
Interim checkin
Enable RTC, fiddle with brightness stuff
Use 10MHz HSE, MSI for USB
Initial commit
Use timer and DMA to smooth DAC output, add brightness curve
DAC controlled brightness circuit, needs linearity correction
In-phase ADC apparently functioning
PWM generation on PA0 from timer 2 ch1
Use timer4 for display to free up timer2
Use coords from nmea
Use int32 for offset
Almost there
Apply rules to time
Change to little-endian, 6 byte format
Use TCXO, read binary rules file
PPS ticking
BCD routines
output unix time
DMA read line and check checksum
Trigger uart interrupt on character match
config stuff
Start on parsing config
USB Preemption priority
error handler
Move vectors to ram and swap systick
basic ticking
Matrix stuff
Zonedetect working
Add zonedetect files
Add locking to potentially avoid concurrency issues
Update .ioc, remove QSPI DMA
FatFS
USB MSC working
cleanup
more junk
junk
More work on qspi driver
Starting on QSPI driver
Enable printf
Initial commit
peterlewis
added a commit
to peterlewis/clock4
that referenced
this pull request
Jul 12, 2026
…k (PPS-latch)
The menu-settings commit gate (and the tempcomp-persist gate) deferred the ~20 ms
page erase around a pending $PMTXTS emit by gating on `!pps_record_pending`. But
pps_record_pending is set on EVERY PPS edge (capturePPS also feeds ADEV/RTC/
tempcomp) and is CLEARED only by emitPPSTimestamp, which runs only when
pps_ts_enabled. So on a GPS-locked clock with PPS-out off (the default) the flag
latches high after the first pulse and never clears — the gate never opens,
menu_dirty stays 1, and NO menu edit (or learned tempcomp model) is ever written
to flash. It works on a bench with no antenna (flag stays 0) and silently fails
in the field the moment there's a fix — exactly the reported "modes cycle fine
but don't survive a reboot."
Fix: gate on a REAL pending emit via a shared pps_emit_pending() helper
(pps_ts_enabled && pps_record_pending), used by both the menu commit gate
(menu_poll) and tc_persist_step. When PPS-out is off there is no emit to protect,
so the gate opens normally; when it's on, behaviour is unchanged.
This class of bug was invisible to the emulator: it has no PPS EXTI hardware
(pps_record_pending stays 0) and the persistence tests called ee_commit directly,
bypassing the gate entirely. Found by an adversarial silicon-path audit.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
emu: auto-persist learned tempcomp model to flash retained memory
Opt-in `tc_persist = on`: while GPS-locked the clock learns its oscillator
tempco, and a well-supported model is saved to a dedicated 2-page flash store
so the first holdover after a power-up is already compensated instead of
re-learning from cold. On boot it loads back as an EVOLVING seed through the
existing tc_seed path; config.txt still wins per-key.
Store (ee2) is parallel to the menu store, reusing its address helpers and
append-only ping-pong framing (magic/gen/schema, CRC16 in the last doubleword
so a torn write fails CRC and the prior generation wins). Runtime-derived base
lands in bank 2 on the RG (read-while-write; ~20 ms erase never stalls PPS or
SysTick) and disables itself below 0x8040000 (RC -> RAM-only, no brick).
Pre-mortem fixes folded in:
- persist gate: only a well-supported fit reaches flash (>=300 HSE samples,
>=4 C coverage, believable residual) -> a shaky fit can never auto-seed a
bad steer next boot.
- load sanity: a non-finite / physically-implausible stored model is rejected
before it can seed the timebase.
- break the permanent-hold trap: cap the seeded prior order at 2 so the first
genuine linear fit supersedes the seed.
- wear: commit at most once per 30 min on a MONOTONIC clock (uwTick), gated on
L0 + UART-idle + !pps_record_pending, and only when the model moved
meaningfully (ppm-at-{lo,mid,hi} change detector). A few writes/day -> the
10k-cycle flash lasts for decades.
- config precedence: tc_seed/coefficient/tc_t0 lines in config.txt override the
stored model (cfg_tc_defined bitmask).
Serial: `tc_forget = on` erases the stored copy (live model untouched);
`tc_reset = on` clears both.
Also bake the measured VTT9812FH (R11=470K) brightness curve as the compiled-in
brightnessCurve[] default, so the clock needs no config.txt BS lines (a BS line
still overrides its stop). config.txt documents tc_persist and marks the BS
lines optional.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Gradual significance fade: digit_bright drives the duty mirror — digits dim out, not dash
PR mitxela#9 computes each sub-second digit's live significance (digit_bright, 0..FADE_MAX,
from the 3-sigma holdover uncertainty) but the display driver could only show-or-dash.
PR mitxela#10 built the missing per-digit dimmer. This connects them:
- segbal_poll scales the ds/cs/ms columns' mirror duty by their digit_bright, and the
seconds column's decimal point by digit_bright[3] (it dies with the 0.1 s digit).
While any digit is mid-fade the mirror runs even with seg_balance off (identity duty),
so the fade works standalone; with balance on the two multiply.
- setPrecision's significance_fade branch now retires digits to BLANK instead of a dash
glyph — the digit fades to black, as the feature's own comment always intended.
- Stock behaviour (significance_fade off) unchanged: the Tolerance dash ladder remains.
Verified in the WASM build of this source: partial fade tracked exactly (holdover age
11 s -> ms digit at 14/16 significance -> lit in exactly 14 of 16 cycles, DP tracking),
zero-significance digits blank in the masters, relock restores full digits with auto
balance resumed. ARM Release 0 errors.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Fix significance_fade boot flash: digit_bright defaults to 0 (dashed), not FADE_MAX
On a warm boot with significance_fade on, digit_bright[] defaulted to
{FADE_MAX,...} ("certainly significant"), so setPrecision() at boot (and each
PendSV second) showed the ms digit ticking — a ~1 s flash of numbers — until the
first per-second computeHoldoverFade() saw the huge holdover age (last_pps_time=0,
no PPS yet) and dashed them. Default to 0 (not significant): the sub-second digits
start dashed and only light once computeHoldoverFade proves significance after
lock (the same PendSV setPrecision path applies it, symmetric). Pre-existing PR mitxela#9
issue, not the SOF change. Builds clean.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Holdover significance-fade + persistent, evolving warm-start temp model
Two coupled additions on top of the self-learning tempcomp, both opt-in and
byte-identical to stock behaviour when off.
significance_fade: during GPS-loss holdover, retire each sub-second digit by its
remaining SIGNIFICANCE rather than by the fixed dash timeouts. A live 3-sigma
time-interval-error bound U(tau) = k*sigma*tau (sigma = RSS of the calibration, the
MEASURED temp-model residual, and aging terms) drives setPrecision's P3/P2/P1/P0
ladder: a digit dashes the moment U passes its place value — the same dash glyph as
the Tolerance_time_* ladder, but driven by measured significance instead of fixed
timeouts. digit_bright[] carries the per-digit intensity for a future smooth-dimming
display change (and for emulators). tc_fit now also reports the weighted-RMS fit
residual, mean-centred for HSE whose model origin is arbitrary.
tc_seed (warm start): reload a previously-learned model -- the last tc_dump, persisted
in config.txt by the host -- as an EVOLVING prior rather than a freeze. The clock is
temperature-compensated from the first second and keeps refining: tc_fit_one returns
the achieved model order and tc_fit preserves the seed until real data supports a fit
at least as rich, then hands over. New keys tc_seed / tc_seed_lo / tc_seed_hi
(documented in qspi/config.txt); tc_dump emits the coverage so a paste round-trips.
Over serial the "tc_seed = on" line is the trigger -- it arms tc_seed_pending (a
single-word ISR write) and tc_housekeeping applies the seed from the MAIN LOOP,
serialized with tc_fit/tc_governor, after the whole paste has parsed. The learned
state keeps its main-loop-only contract; the USB ISR never touches the model.
Survived two adversarial review passes (the feature, then this port), with fixes
re-verified in the emulator against this exact tree: boot-time tc_nom_load capture
(ticks-per-ppm is never 0), origin-invariant HSE residual, main-loop-serialized
serial seeding with a whole-paste trigger, tc_reset clears the held prior,
significance_fade drives the dash ladder (no regression vs stock when enabled), and
measure_temp's cadence gate includes the fade so die_temp_c is always real.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Add self-learning holdover temperature compensation
Builds on the $PMTXTS telemetry: while GPS-locked the clock learns each
oscillator's frequency error vs die temperature (binned, per-oscillator), then
during a GPS-loss holdover it steers the SysTick timebase from the HSE model to
cancel temperature-driven drift, and optionally trims RTC->CALR from the LSE
model so the battery RTC hands over better time across a power loss.
- tc_learn / tc_apply / tc_rtc config keys (all default off = stock behaviour)
- steering is a Bresenham SysTick->LOAD stretch inside the tick ISR (integer
only; all model maths is main-loop); origin-free HSE model so only the
temperature DIFFERENCE since GPS loss is applied, and the per-edge PPS phase
snap makes re-lock instantly neutral
- 'tc_dump = on' over serial prints the learned coefficients as paste-ready
config lines (+ a $PMTXTC sentence); pasting them back freezes the model
- MODE_TEMPCOMP diagnostic display: die temp / model offset / sample count
- ticks-per-ppm derived from the runtime SysTick period (no core-clock assumption)
Hardware-validated on an Mk IV: the LSE model learned 18.68 ppm vs 18.9 ppm
measured directly (~1%). Survived an adversarial review pass (ISR-safety,
integer math, sign conventions, re-lock neutrality, off-means-identical).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
sidereal: rename config key alt_colon_mode -> colon_alt_mode
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Add sidereal (LST) and apparent-solar time-row modes
MODE_LST and MODE_SOLAR turn the big HH:MM:SS digits into a live ticking
Local Sidereal Time or apparent-solar clock. Both reuse the MODE_COUNTDOWN
takeover pattern: the heavy GMST/EoT double maths runs once per second in the
main loop, staging pre-rendered digits that the SysTick handlers latch at the
true GPS PPS boundary -- so accuracy and the P3..P0 sub-second digit-hiding are
inherited from the disciplined civil second, and civil timekeeping (currentTime,
PPS, the date row) is untouched.
The display is quantised to civil second boundaries and reseeded every second,
so sidereal's 1.00273791x rate makes the seconds double-step once every ~6 min
-- the honest signature of a GPS-disciplined sidereal clock. A dedicated colon
animation (alt_colon_mode, default alt_sawtooth, kept distinct from the civil
colon) marks the mode so it can never be mistaken for civil time; apparent solar
especially can sit within minutes of it. With no position the row shows dashes
-- never GMST-as-LST.
astro.c regains local_sidereal_time()/local_solar_time() with the GMST series
factored into one helper (gmst_hours, +T^2 term); native suite 53/53 incl. the
IAU J2000 anchor and Meeus example 12.b. All default-off: stock behaviour
unchanged.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Fix charger-only-power CDC hard-fault and date-board CMD_LOAD_TEXT OOB
Two further pre-existing bugs from the same read-through.
- mk4-time NMEA-over-CDC: CDC_Copy_Transmit() / CDC_Transmit_FS() dereference
hUsbDeviceFS.pClassDataCDC unconditionally, but it is NULL until a USB host
enumerates. The default nmea_cdc_level = NMEA_ALL forwards every GPS sentence
to CDC, so on charger-only power the first forwarded sentence faults in ISR
context. NULL guard on both, plus a length clamp before the memcpy.
- mk4-date CMD_LOAD_TEXT: 'if (text_idx > MAX_TEXT_LEN) return;' passes at
text_idx == MAX_TEXT_LEN and writes text[32], one past the 32-byte array.
'>' -> '>='.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Harden firmware: bounds + ISR-safety fixes from broad audit
- loadRules: reject untrusted rowLength/numEntries from TZRULES.BIN (RAM overflow)
- MODE_TEXT: clamp snprintf untruncated-return index (uart2_tx_buffer OOB)
- mk4-date latchDisplay: bound dp_pos per orientation (buffer_a/b OOB)
- readConfigFile: don't treat a zero FAT timestamp as a cache hit (first-boot hang)
- config-over-USB: defer postConfigCleanup out of the USB ISR (sendDate/nextMode races)
- firmwareCheckOnEject: fatfs_busy guard for FATFS reentrancy on USB eject
astro: subsolar-point helper (sun_subsolar) + shared-series comments
sun_subsolar(t, &lat, &lon) returns the point where the sun is at the zenith:
latitude = solar declination, longitude where the local hour angle is zero
(lon = alpha - gmst*15). It reuses the same gmst_hours series as sun_az_el and
local_sidereal_time, so the three can never drift apart. Library + tests only —
no display mode consumes it yet (a companion app plots it).
Tests: the sun must sit at elevation 90 deg over its own subsolar point at every
existing test instant (self-consistent but not circular — az/el and subsolar
take different paths to the sky), plus declination anchors at the solstices and
the absolute GMST anchors (IAU J2000, Meeus ex. 12.b) recorded in comments.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
astro: configurable per-screen page dwell (page_ms, default 5500 ms)
The paged modes (SUN, LATLON) previously flipped sub-screen every 2 s hard-coded.
Make the dwell a config key (page_ms, ms; unset -> 5500, floored at 250 so a tiny
value can't flood the date-board UART), driven off uwTick, and repaint the moment a
page flips rather than waiting for the 1 Hz date-row refresh (guarded by decisec!=9
to avoid racing the SysTick sendDate).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
astro: give LAT/LON a sign slot so negatives keep the label gap
"LAT 51.48" vs "LAT-51.48" let the minus swallow the separator. Add a sign
slot after the label space — "LAT 51.48" / "LAT -51.48" — so the digits
start in the same column either way, matching the RISE/SET layout. A 3-digit
longitude can't fit both the separator and the slot in 10 characters, so the
separator alone is dropped there ("LON-179.99").
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Add astro pack: sun / moon / Maidenhead / lat-lon display modes
Five GPS-derived astronomy read-outs, each opt-in via a MODE_* config key and
shown on the 10-char date row while the live clock keeps running on the time
row (SATVIEW-style, COUNT_NORMAL):
MODE_SUN sunrise / sunset / solar noon (local), auto-paged
MODE_SUN_AZEL sun azimuth & elevation, now
MODE_MOON moon phase index (0-7) + illuminated %
MODE_GRID Maidenhead grid locator
MODE_LATLON latitude / longitude, auto-paged
The astronomy maths is a self-contained Core/Src/astro.c (+ astro.h): low-
precision NOAA/Meeus sun alt-az and event times, moon phase, equation of time,
and Maidenhead. It has no firmware dependencies, so it unit-tests natively
(test/test_astro.c, 45 reference vectors) and was validated to ~1e-5 before
touching the firmware.
The L4 has only a single-precision FPU, so the double maths would be slow soft-
float. It is therefore computed in the main loop (astro_update(), gated on the
active mode exactly as measure_vbat() is for MODE_VBAT) and swapped into a cache
under a brief __disable_irq() mask; the sendDate() cases that run inside the
SysTick ISR only format the cached scalars -- no trig in the interrupt.
Modes are disabled by default; with no GPS fix they use fake_latitude/longitude
if set, else show "----". config.txt documents the keys and the moon-phase
index legend.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
SOF timestamping: audit fixes — valid-gated tail, latch only when pps=on
Adversarial audit of the SOF-correlation change surfaced two real issues:
- Stale first anchor: pps_sof_dwt/frame init to 0, so the first PPS emitted
after enumeration (or with pps just toggled on, or on the emulator which has
no USB SOF) carried a (0,0) anchor → a ~53s/−729ms host error on that record.
Now a pps_sof_valid flag gates the tail: emit the plain 9-field sentence until
a real SOF has latched. Backward-compatible; also makes the emulator correct.
- SOF work ran unconditionally. Gate the latch on pps_ts_enabled (the SOF IRQ
still fires — cheap, below the priority-0 display DMA — but does nothing when
the feature is off); clear valid when off so a re-enable can't reuse a stale
anchor. config.txt documents that pps=on enables the SOF tail.
Builds clean (0 errors). Refuted findings (torn read, 16-bit atomicity, buffer
truncation, display preemption, scope) left as-is.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
SOF-correlation timestamping: latch (USB frame, DWT) for sub-ms PPS over USB
Experimental attack on the ~6ms host-driven USB read jitter that swamps the
clock's true ~180us precision — without a hardware PPS wire. The OTG_FS core
rules out mitxela's last-microsecond FIFO injection, so instead we let the host
anchor each PPS to a USB Start-Of-Frame, whose own arrival time the host can
read in hardware (macOS IOKit GetBusFrameNumberWithTime).
- Enable DWT->CYCCNT (free-running 12.5ns core-cycle counter) as the timebase
both the PPS edge and each SOF are latched against. Unaffected by tempcomp
SysTick steering (counts raw core clock), so it's a stable monotonic ruler.
- Enable the USB SOF interrupt; PCD_SOFCallback latches (DWT, 11-bit frame from
DSTS[13:8]) every 1ms, as its first act for minimal latency.
- capturePPS() latches DWT at the edge; emitPPSTimestamp() appends the tail
,dwt_pps,sof_frame,dwt_sof to $PMTXTS (read under __disable_irq, no torn read).
- Bump NMEA_BUF_SIZE 90->128 for the longer sentence.
Host places the edge at hostTime(sof_frame) + (dwt_pps-dwt_sof)/f_dwt, immune to
delivery lateness; dwt_pps deltas self-calibrate f_dwt (no core-clock assumption).
Backward-compatible: lenient $PMTXTS parsers read the first 9 fields. Bench-only;
+1kHz SOF ISR (~0.05% CPU) is the cost to watch. Builds clean (0 errors).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Add $PMTXTS per-PPS host timestamping (opt-in)
With `pps = on` in config.txt, emit one proprietary NMEA sentence per PPS edge
over the existing CDC stream, carrying the sub-second phase captured at the edge
plus calibration / holdover / die-temperature telemetry. Time-critical fields are
snapshotted in the PPS ISR; formatting and CDC submission happen in the main loop,
serialised against the NMEA passthrough. With no enumerated host the record is
dropped before any formatting.
mk4-time/Core/Src/main.c: capturePPS(), emitPPSTimestamp(), measure_temp(), a
`pps` config key and one main-loop hook. qspi/config.txt documents the key.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
adjust hole sizes on acrylic support parts
tap drills to 2.6mm, switch holes to 4.0mm, lightsensor to 6.02mm
inc fwt version
add MODE_WDY_MM_DD, truncated weekday with MM-DD
add MODE_WEEKDA_DD
the missing Y is a reminder that some of the weekday names are truncated
Fix errant extra mode
This typo was simple enough to spot once we went looking for it, but the curious thing is that we hadn't seen any symptoms until now.
When cycling through modes with the buttons, going in one direction works fine but the other requires an extra button press to loop. By a quirk of the memory layout it only affects some clocks, and only when they've been powered on long enough to have a full fix for a while.
When cycling downwards, the loop erroneously attempted to set the mode to NUM_DISPLAY_MODES, which is one beyond the end of the array. It continues cycling until it finds an enabled mode. Looking at the memory map, the next value in memory happened to be debug_rtc_val:
*fill* 0x200005a5 0x3
.bss.config 0x200005a8 0x40 ./Core/Src/main.o
0x200005a8 config
.bss.debug_rtc_val
0x200005e8 0x4 ./Core/Src/main.o
0x200005e8 debug_rtc_val
.bss.nmea_cdc_level
0x200005ec 0x1 ./Core/Src/main.o
0x200005ec nmea_cdc_level
.bss.colonMode
0x200005ed 0x1 ./Core/Src/main.o
0x200005ed colonMode
This integer value is zero at powerup and later contains the remainder after calibrating the RTC crystal over a 63 second period. So it's only ever non-zero if the clock has had PPS for more than 63 seconds, and then depending on the unique crystal characteristics of that hardware, the value may be positive, meaning the high byte is zero. Only if the remainder is negative would the errant extra mode appear. The first batch of clocks in 2026 are apparently the first ones to meet the criteria.
The fallback in sendDate() for the unknown mode is just ISO8601 standard, so this doesn't really matter but it's interesting it flew under the radar for so long.
add wider versions of shelfstand for acrylic
split wallhanger4 into separate files, move keyhole to top
reduce wallhanger4 spring pressure to 3 degrees
New wallhanger that clips only onto PCB
cleaner way of grabbing the versions
update github release script
Merge branch 'ttff'
Release script for zip file with disk image and bootloader
if enabled, always start with mode_ttff
mode to display time till first fix
inc firmware versions
improve hinge-tile-gen.py
Hold both buttons to reset
smoked acrylic svg with 50 micron kerf baked in
simplify acrylic case svg
bootloader: skip erase if not needed
Erasing a page is much, much slower than writing a page.
Chips arrive erased from the factory, so this significantly speeds up the initial flashing process.
additional support for acrylic case threaded holes
lasercut pieces tile generator
hinge.svg threaded holes dia
hinge.svg: start and stop paths on corners, not at the arc
add acrylic case cad files
appease demands for noncompliant date formats
...but hide them behind an #ifdef
fwd protect stack when jumping to system loader
can't be too careful eh
fwt version 0.0.2
Add display test mode
Countdown: set decimal point when epoch has past
Once the moment has gone, always display 0.000, regardless of GPS/RTC precision
Resend date in pendSV during count_hidden
Enables text such as "utc offset" to still flip over when clock is folded/unfolded
Double-sided tape wallhanger by Kevin Thomasson
slim wallhanger (mitxela#3)
* slim wallhanger
* rename
* added midpoint locker
typo in hinge.svg
Black 6mm rectangle should have a height of 22.56, with the lower hole 6mm from the bottom, and the top hole 5.28 from the top. It was off by 0.28mm so hardly noticable with the kerf of the laser, but we may as well make it exact.
Add redrawn STEP files
These don't exactly match the STL files although both will work. STL files were generated from the OpenSCAD file. STEP files were redrawn with some fillets and other tweaks, moving forwards the step files may be the main reference
Move main STL files into folders for rev C, rev D
Set brightness curve in config file for phototransistor
The default curve in the firmware, with nothing specified in the config file (or this commented out) matches the LDR.
add STL for phototransistor (VTT9812FH)
bootloader user request on PA2/hinge TX
update config.txt template
inc fwt version
wipe vbat measurement on leaving mode_vbat
Reduce power-on precision if coin cell below 2.7V
oversampling vbat is substantially more stable
mode_vbat
initial vbat reading using adc3
monitor beidou, plot gps and the highest non-gps satview
further complication of GSV parsing
signal ID is not always present, number of fields is variable, what was meant to be a quick way of showing number of satellites is becoming a mess
correctly interpret multiple GSV messages
M10 gps module sends GPGSV multiple times in separate groups, even though it's only listening to a single band.
latch satview
ublox 10 series output GSV only every 5 seconds
update qspi.md
tzmap 2025b (full)
tzrules to 2025b
update timezone-names to 2025b
Bootloader updates to explicitly protect the stack
Using both noreturn and __builting_unreachable() is superfluous but hopefully makes it twice as unlikely to optimise the jump into a tail call. Not sure if this is better than -O0 but hopefully makes some people less unhappy.
add physical dimensions to hinge svg
generate either version in openscad
cad for vtt9812fh
a dubiously helpful overlay for brightness curve
method to convert a bunch of brightness values
basic support for touchscreen devices
allow pasting in brightness curve values
generate binary files
compatibility with toolchain 12.3.rel1
Ignore env hashes using clean/smudge filter
Add following to .git/config:
[filter "eclipse_env_hash"]
clean = sed 's/env-hash=\"[-,0-9]*\"/env-hash=\"0000000000000000000\"/g'
smudge = cat
Revert "Don't track these IDE files that keep changing"
This reverts commit 8e4d62c.
add shelfstand cad files
Add handcrafted SVG version of moshidraw hinge
add readme
CAD files for antenna case
Specifically for the patch antenna with SMA connector crimped as close as possible. Top half (caset.stl) needs to be printed upside down with support for the cable shroud. Last modified 2023-04-19
Add cad files for wall hangers
lasercut files update for easier tiling
Add moshidraw file for hinge (convert to DXF or SVG later)
make colon cover a teeny bit taller
add side cutouts to colons for better pliability
flat-top LEDs have 3.15 dia
commit STL files
cad updates from 2023-06-15
start adding cad files to this repo
Don't track these IDE files that keep changing
Add known firmware images to repo (2024-05-29)
Config file defaults, most modes off
IDE settings (unsure what changed)
slightly smaller hammer (re: b91a100)
sledgehammer to prevent -Os trashing launch_app()
suppress maybe-uninitialised warning
Pad bootloader to exactly 64K
Bootloader version string
env hash stuff - should probably ignore these files
partial changes to bootloader - unfinished
f_mount doesn't actually do anything until first read (unless forced) so it's not really possible to error out at the mount stage, but we don't want any possible way to hang without enabling usb
Clear colons in bootloader
They are off when tristated too but need to be explicitly high to remain off on the xl
and volatile again in the bootloader
This was working without it, so the fix is hypothetical. Might be because bootloader is -Os and time is -O3
another volatile optimisation victim
set volume label to CLOCK
blank date when updating on eject
delay setDisplayFrequency if needed
Don't write RTC during USB read/writes, pendSV could be delayed
fix countdown mode entering/exiting
Replace delayedButtonPress with resendDate
Not sure if this is better than the alternative, that is to delay CDC commands and reloaded config too.
Todo: countdown mode fixes
Measure lookup time macro
Takes 3x longer if launched via swd instead of bootloader. QSPI quad read seems to make it slower. Definitely something odd going on
possible compiler optimisation problems
enable config mtime checking
ftime field has a resolution of 2 seconds, so fast changes can be missed.
But this has the benefit of not wiping out CDC config when the OS does random other writes we might not have supressed (system volume information, DS_Store, etc)
remount filesystem, skip config mtime check
The remount does fix some occasional fs problems, which seems to just be stale data in fatfs window. This can be invalidated by doing USERFatFS.winsect = -1 but safest to remount properly.
The config ftime still sometimes misses changes if they're in quick succession. This might even be the OS not updating the value, or the low resolution (to the second). But realistically the config file is normally the only thing that gets changed so I don't think always reloading it is a problem.
manually manage Etc/ zones again
zonedetect backoff on usb read too
Only zonedetect once per nmea
Priority rewrite, avoid f_read in usb context
more typos
Typo
dos2unix generate-tzrules.py
Fix tzrules for zone names out of order
single script for flashing and creating disk image
mount as loop device is auto determined
create dummy 'system volume information'
Set qspi_write_time even if write fails
Only reload config if modified
update config template
Accept fake long/lat from config
day-of-week mode for no reason
if rtc valid but no tzrules file, copy out rtc rules string too
enum the loadrules error codes
reload map file each time
only copy rules string if rules valid
configure debug/release builds for date
same treatment for bootloader
fix linux eject sequence
util-linux eject sends a scsi start immediately before scsi stop
QSPI_STATUS consistency in bootloader
hacky delayed-check-on-eject
This is pretty much impossible to test anyway
Revert "Add fatfs read qspi timeout"
This reverts commit 70553aa.
Add fatfs read qspi timeout
Protection against qspi collisions
Not convinced the lock stuff is bullet proof. The zonedetect backoff is enough to fix it but there's still a remote chance something could get corrupted.
Also the chainloader ejection check runs in the usb interrupt context, if qspi was locked at ejection this could hang
fix typo (DP on error)
Don't connect USB during date update, unless an error occurs
force update version string on every build
reboot on eject if new firmware present
corrupted filesystem could cause f_crc to hang
Flashing script
rollover fix (hypothetical, from earlier)
we want at least the option of separate version strings
linker alignment fix and give bootloader same treatment
share version string method
ISO8601 build string, worth it
propagate changes to linker script
There's only one line difference between the scripts, we should probably try passing a parameter from the ide build options
Add build datetime string to firmware image
One disadvantage to this linker method is the error message is nonsensical if you overflow the pad region
update create-image.sh
fixup! Commit release script
Show firmware crc (date)
Firmware CRC display mode (for time)
Commit release script
Add firmware CRC script
Add notes about QSPI flash contents
Add example config.txt
Create disk image using loop device
Add example tzmap.bin (2023b)
Add example tzrules output
Add example timezone-names file
Specifically needs to match the shapefile version we're using
curl -L https://github.com/evansiroky/timezone-boundary-builder/releases/download/2023b/timezone-names.json | jq '.' > timezone-names.json
Newer tzrules generator, uses zdump and subprocess
Add original tzrules generator, depends on external csv
Add reboot CDC command
manual vbus detection for self powered version
Enable PA8 input and update IOC
Fix inverted date trailing dot error
In the inverted position, the decimal point is shown on the following digit. If the DP is the last character of the message, it would not set the cathodes and multiple dots would light up.
Pullup on usart2 rx
Without it and without date connected, frame error interrupt is called continuously
update default brightness curve for LDR
Enabling a mode via CDC should switch to it
Rip out io retarget stuff
USB Eject bootloader should retry update
Debug RTC mode + theoretical improvements to RTC cal
Critical fix to RTC calibration
When launched from ST-LINK or without bootloader, the calibration routine works fine. When launched by the bootloader, even if we jump out before anything is initialised, the calibration was way off. The LPTIM1 interrupt was running twice for each counter reset, because the interrupt ended before the flag had cleared. Clearing the flag at the start of the interrupt instead of the end fixes it.
Can't figure out why the launch method changes this, it must be something to do with the pipeline acceleration. Both bootloader and app have the same startup file, and it still happens if we skip out all of HAL.
display fault type (could make this more helpful at some point)
can have GPS satellites but no GLGSV message
suppress snprintf warning
"fix" hardfault on usb scsi command
When launched after firmware update, first boot causes hardfault when scsi command happens. The GetCapacity function pointer in USBD_Storage_Interface_fops_FS is corrupted.
Launching the older non-CDC firmware is fine. Disabling usb in bootloader has no effect. Zeroing ram or adding delays has no effect, but breakpointing with the debugger stops it happening.
Calling reset after a firmware update seems hacky but appears to fix everything.
bootloader: only init USB on error
prevents double-connect when booting normally
Alternate show tz name
Fix show offset
brightness curve in config file
prepare to rewrite brightness curve
pointless attention to detail
1.0 or +1.0 should be max brightness, 1 should be 1/4095
brightness override float optional
send full text buffer
dots are valid characters that don't take up a digit, so a valid message could be 20 bytes long. Might as well send it all
Brightness override
fix likely glitch with uart handler
was checking against UART_IT_RXNE instead of USART_ISR_RXNE
also explicitly hand errors over to the HAL routine
button delayed auto shift
date: don't disable swclk if button held at power on
iso week mode
Merge branch 'cdc-msc'
expand falsey
satview be explicit if there's no data
nmea output level, cleanup cdc transmit
add generic text display mode
rx config value can be empty
cope with config file not present
config cleanup on cdc commands
parse config from cdc input
fix MSC eject
fix descriptors for bus powered, 500mA
update build config with alt linker script
rip out unused descriptors
USB CDC MSC
CDC MSC Almost there
initial steps on usb msc+cdc composite
Add basic satview mode
countdown tolerance; handle on-the-fly changes
allow countdowns to the year 9999
Countdown enter/exit
Starting on countdown mode
Update eclipse env-hash nonsense
At some point we should clean/smudge this out of the repo
DP debug signals
update manufacturer/product/scsi description
Add brightness debug mode
Delay enabling uart2 interrupt until after doDateUpdate
swap buttons when inverted
proobably shouldn't be tracking these files
code generation using workspace 1.8.0
update ioc for code gen
update file names
code generated workspace 1.8.0
update .ioc for working code generation
update file name
Code generation using workspace 1.8.0
Update .ioc for correct code generation
update env hash
update project name
remove .launch files
Merge repos
Merge repos
Move everything into subdir
disable power on test message
Move everything into subfolder
Move everything into subfolder
Configure PC12 correctly to hide DP at power-on
Update to work with chips bigger than 256K
adjust animation speed to compensate for slower erase of the bank we're running our code from
don't light decimal points during bootloader
button 2 scrolls modes in reverse
swap colons, don't show DP during date update
env hash update
Add second button
Add second button
Colon flashing modes
Colons independently fading
Separate PWM for each colon pair
Fix .ioc, culprit was ADC1.DMAContinuousRequests=ENABLE
Interim checkin before .ioc corruption
Forget using DMA for ADC reading
Show offset mode and hook into USB write
Support for modes that don't display time
Use hyphens for reduced precision
Standby mode
Load tolerances from config file
Load modes from config file
sendDate cleanup
Reduce precision to align with accuracy
Rewire colons
Ordinal date
Julian date
Rudimentary support for decimal places
Debouncing
Button press changes date display
Initial button press mechanism
Move sTimeDPR to PC12
Calibrate RTC
Use LPTIM clocked from LSE
Better latch mechanism
Better latch mechanism
Improve rtc subsecond stuff
Load frequency from config
set frequency fixes
cmd set frequency
Cleanup
RTC subseconds somewhat accurate
Fixes to enable code generation
Fiddling with rtc timings
Smooth progress again
alternate progress bar
Smooth progress
Look ma no magic numbers
Progress bar progress
chainloadin' is a happenin'
Use 8E1 always to avoid having to switch
Recovery logic in place
Chainloader progress
Chainloader progress
Move display matrix to TIM7 to free up UART2 TX DMA channel
More RTC stuff
RTC basics
Ticking
Further fiddling with pointless parts
Buffer wheel thing
Mostly lame progress bar
We has a bootloader
Main structure in place
Checks CRC of firmware image on fatfs
Possibly better way of filling the flash, still lets us see usage
One method of filling the flash
Initial commit with working display, QSPI FATFS and USB
Safely shift code section to make room for bootloader
Power up at lowest brightness
Interim checkin
Enable RTC, fiddle with brightness stuff
Use 10MHz HSE, MSI for USB
Initial commit
Use timer and DMA to smooth DAC output, add brightness curve
DAC controlled brightness circuit, needs linearity correction
In-phase ADC apparently functioning
PWM generation on PA0 from timer 2 ch1
Use timer4 for display to free up timer2
Use coords from nmea
Use int32 for offset
Almost there
Apply rules to time
Change to little-endian, 6 byte format
Use TCXO, read binary rules file
PPS ticking
BCD routines
output unix time
DMA read line and check checksum
Trigger uart interrupt on character match
config stuff
Start on parsing config
USB Preemption priority
error handler
Move vectors to ram and swap systick
basic ticking
Matrix stuff
Zonedetect working
Add zonedetect files
Add locking to potentially avoid concurrency issues
Update .ioc, remove QSPI DMA
FatFS
USB MSC working
cleanup
more junk
junk
More work on qspi driver
Starting on QSPI driver
Enable printf
Initial commit
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Adds opt-in temperature compensation: the clock learns ppm-vs-die-temperature for both oscillators while GPS-locked (
tc_learn), steers the timebase in holdover (tc_apply), trims the battery RTC (tc_rtc), round-trips the model viatc_dump/$PMTXTC/tc_seed, optionally retains it in flash (tc_persist), and drives thesignificance_fadeuncertainty display and theMODE_TEMPCOMPread-out.Stack: based on
sidereal-modes(#8).Try it now: fwt.bin · fwd.bin — the union of this PR stack, byte-identical to the merged result.
Docs:
docs/firmware-additions.md, shipped in the final (menu) PR.Note
You can try this PR without hardware: the PCC web app runs the real clock4 firmware compiled to WebAssembly, and its clock is built from a rollup of the whole PR series together, so this PR's behaviour is live there alongside the others. The app's DEVICE → UPDATES panel shows the exact commit it was built from. Fair warning: the app around the firmware is beta and still has bugs — the firmware behaviour is the real thing, the app chrome less so.
Draft — self-learning holdover temperature compensation, built on the
$PMTXTStelemetry from #5.Stacking note (please read first)
The series is a linear stack; this PR is based on #8 (sidereal-modes), so the GitHub diff vs
mastershows the lower PRs' commits too — it consumes the per-PPSpps_capsnapshot and die temperature that #5 adds. The temp-comp changes are the branch's own commits — the compensator itself, then significance-fade + the evolving warm-start model, plus their fixes (the branch's diff against #8's head). Note #5's own text says "no compensation shipped" — this is the deliberate follow-up. (Flash retention of the learned model —tc_persist— arrives with the settings store in the final menu PR.)What it does
While GPS-locked the clock learns each oscillator's frequency error vs die temperature (binned, weighted least-squares). During a GPS-loss holdover it then steers the SysTick display timebase from the HSE model to cancel temperature-driven drift (
tc_apply), and optionally trimsRTC->CALRfrom the LSE model so the battery RTC hands over better time across a power loss (tc_rtc). All enables default off — stock behaviour unchanged.Design notes
calibrateRTCfloat precedent); the tick ISR does only an integer BresenhamSysTick->LOADstretch — a fractional-ppm rate steer, no phase steps.model(T_now) − model(T_at_loss), so the origin cancels and the per-edge phase snap makes re-lock instantly neutral. (It corrects temperature-driven drift; the absolute TCXO offset at loss is its spec — re-lock anchoring is a possible follow-up.)tc_dump = onover serial prints the learned coefficients as paste-readyconfig.txtlines (+ a$PMTXTCsentence); pasting them back freezes the model. Serial-only, guarded against config-file reload.MODE_TEMPCOMPdiagnostic display pages die temp / model offset / sample count.calibrateRTC); the WPR sequence runs IRQ-off against PendSV'swrite_rtc.The follow-on commit (both opt-in, off = stock)
digit_bright[]carries the per-digit significance levels; withseg_balanceactive the sub-second digits dim gradually through the duty mirror rather than snapping to dashes.tc_seed— pastedtc_dumpcoefficients currently freeze the model. Withtc_seed = onthey load as a starting point instead: the clock is temperature-compensated immediately after power-up and keeps learning; once its own data supports a fit of equal or higher order, the learned fit replaces the seed.tc_dumpnow also printstc_seed_lo/tc_seed_hi(the temperature range the model covers) so its output round-trips. Over serial, send the coefficients first andtc_seed = onlast — that line triggers the load, which runs from the main loop (tc_housekeeping), so the USB ISR never writes the model.Status
Hardware-validated on an Mk IV — the LSE model learned 18.68 ppm vs 18.9 ppm measured directly (~1%). Builds clean; the base commit passed an adversarial review (ISR-safety, integer math, sign conventions, re-lock neutrality, off-means-identical), and the follow-on commit was reviewed twice more and behaviour-tested in a WASM build of this exact branch (seeding, seed-to-learned handover, dash thresholds). Draft pending broader holdover-steering validation.
🤖 Generated with Claude Code