Skip to content

rtc: write the RV3028 time with an A0h year mark, and only 2000-2099 - #103

Closed
ptr727 wants to merge 1 commit into
liquidraver:devfrom
ptr727:rv3028-year-marker
Closed

ptr727 wants to merge 1 commit into
liquidraver:devfrom
ptr727:rv3028-year-marker

Conversation

@ptr727

@ptr727 ptr727 commented Oct 6, 2026 •

Copy link
Copy Markdown
Contributor

A follow-up to #98. The Zephyr and MeshCore counterparts are zephyrproject-rtos/zephyr#121389 and meshcore-dev/MeshCore#3421.

Code links are permalinks: dev at a2e8d5f, this PR's commit 535a37c.

Cross-project consistency. This change is part of a coordinated update to RV3028 time handling across three codebases, kept consistent in behaviour: Zephyr (zephyrproject-rtos/zephyr#121389), MeshCore (meshcore-dev/MeshCore#3421) and ZephCore (#103, a follow-up to #98). It replaces the rule all three adopted on 2026-10-05: zero the year, write 00h-05h, then write the year. That rule split the time write into separate accesses against the manual's one-access rule (RV-3028-C7 Application Manual Rev 1.4, §4.5), with the argument that the Seconds write restarts the prescaler, so no tick lands before the year write. It also used year 00 as the "not set" marker, which made 2000-01-01 unusable as a date. Hardware tests on an RV-3028-C7 then showed three things: a write cut short by an interface reset keeps the bytes already acknowledged and leaves the rest unchanged (tested with the 950 ms bus timeout, §4.5.1); the year register keeps a non-BCD value while the clock counts; and a New Year rollover counts a left mark up like a BCD value (A0h to A1h, A9h to B0h), so it stays a non-BCD year. The shared rule is now: write 06h := A0h, then one 7-byte burst 00h-06h, and read a year of A0h-FEh as not set and FFh as a failed read. Retry policy and never-set detection (PORF) are left to each project.

All three projects also take only years 2000-2099 on a write, refusing any other year before touching the chip, since a BCD-encoded year outside that range either aliases into it or, for 2100, equals the A0h mark.

The change

At a descriptor with rv3028-eeprom-config (the RAK4631's RAK12002):

  • Write. rv3028_write_time() writes 06h := A0h on its own, then one 7-byte burst to 00h-06h, year last. It replaces year 00h, a 6-byte burst, then the year. The BSF bracket around it and the retry (every 5 s, 13 attempts) are unchanged.
  • Read. In rtc_probe(), a year of A0h-FEh logs time not yet set (year A0h) and takes no time. The chip stays the write-back target. FFh still falls to "time unreadable". Before this change, any non-BCD year logged "time unreadable", so behaviour is the same and only the log line differs.

For every chip:

  • Year range. rtc_time_block() now fails outside 2000-2099, and zephcore_rtc_save() writes nothing then. ZephCore writes the year as two BCD digits and no century bit, and it encoded y % 100. So a time before 2000, accepted by a node whose clock had no time yet, was stored as a year near the end of the century: time 900000000 (1998) was restored at the next boot as 2098, past the 2025 floor. Other chips keep their older time on a refusal.
  • Refusal at an RV3028. At an rv3028-eeprom-config descriptor, rv3028_mark_year() writes the A0h mark instead, so the next boot takes no time rather than the chip's older one. The mark gets the BSF bracket and the same 5 s repeat as a time write, sharing its 13-attempt cap, and a repeat whose run-on time leaves 2000-2099 marks instead. Marking on a refusal is ZephCore's own choice: Zephyr returns -EINVAL with no side effects, and MeshCore keeps the old time.

Unchanged: the 2025 floor, so a power-on-reset chip (2000-01-01) still gives no time, and PORF: no time is taken while it is set, and only a confirmed time write clears it.

The header contract and the zephcore,rtc-i2c binding describe both, kept to the length of the RTC comments after 9d6b21a (comment diet vol.2). The Kconfig help and docs/ARCHITECTURE.md now say the RTC is written only for a time within 2000-2099, and ARCHITECTURE.md that an RV3028 with rv3028-eeprom-config marks its year unset instead.

Testing

RAK4631 + RAK12002, repeater debug builds on dev 9bb7ccf (Zephyr 74b7173), with this PR's code; rebased onto a2e8d5f since, with comment-only changes and a clean build:

  • Restore. Flashed over a chip holding a valid time: RTC rtc-rv3028@52: restored 2026-10-06 15:53:27 UTC. time <now> then reboot restored the set time.
  • A cut write. A throwaway build whose write stops after the A0h mark and a 3-byte burst, then reports failure (the shape a bus timeout produced on this chip). time <now> gave time write not confirmed, repeating every 5 s, then not confirmed after 13 tries. After reboot: rtc-rv3028@52 present, time not yet set (year A0h).
  • Recovery. This PR flashed over that chip: time not yet set (year A0h) at boot. Then time <now> and reboot: restored 2026-10-06 15:59:40 UTC, so the one-burst write clears the mark.
  • Year range. A throwaway build that saves 1998, then 2100, three seconds after boot. The node's own clock stayed in 2026, so nothing went on air with a wrong date. Both logged time outside 2000-2099 not written, year marked unset. This PR then booted with time not yet set (year A0h), and a real time <now> plus reboot restored 16:43:03 UTC.
  • Not run here: a real power-loss cut, an FFh read, and a failed mark write. The cut-write storage and the rollover were measured on this same board by the Zephyr-side hardware test (results in the paragraph above).

Known wording point: the read-side comment says "A0h-FEh holds the A0h mark". Only A0h is the mark as written; A1h-F9h are it counted on by a New Year, and the code rejects the whole range either way.

Iteration history and review: ptr727#50.

🤖 Generated with Claude Code

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Copilot review overview

🟢 Approval recommended

No unresolved blocking issues were identified.

Review effort: Lite
Findings: None

What changed in this PR

Updates RV3028 persistence to use an A0h unset marker and restrict writes to years 2000–2099.

Changes:

  • Uses a marker followed by one seven-byte time burst.
  • Rejects out-of-range years and handles unset markers.
  • Updates RTC API and devicetree documentation.
File Description
zephcore/​dts/​bindings/​rtc/​zephcore,rtc-i2c.yaml Documents RV3028 marker and year limits.
zephcore/​adapters/​clock/​ZephyrRTCDiscover.h Updates the RTC contract documentation.
zephcore/​adapters/​clock/​ZephyrRTCDiscover.c Implements marker writes, detection, retries, and year validation.

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

The RV3028 time write set year 00h, burst-wrote 00h-05h, then wrote the
real year, so a write cut short left year 2000 and the next boot took no
time from it. That split the write into separate accesses against the
manual's one-access rule (Application Manual Rev 1.4, 4.5), relying on
the Seconds write restarting the prescaler, and used a real date as the
"not set" mark.

The write now sets 06h to A0h, which is not BCD, then writes all seven
registers 00h-06h in one burst, year last, inside the existing BSF
bracket. The chip keeps each byte as it is acknowledged, so a write cut
between the mark and the year byte leaves the mark. On read at an
rv3028-eeprom-config descriptor, a year of A0h-FEh gives no time: a New
Year counts a left mark on (A0h to A1h, A9h to B0h). FFh stays a failed
read, since a read cut short returns 1s and the year is the last byte.

The save also refuses a year outside 2000-2099, for every chip, because
ZephCore writes the year as two BCD digits and no century bit (the
DS3231 and PCF8563 century bits are written as 0 and masked off on
read). It encoded y % 100, so a time before 2000, accepted by a node
whose clock had no time yet, was stored as a year near the end of the
century: `time 900000000` (1998) was restored at the next boot as 2098,
past the 2025 floor. At an rv3028-eeprom-config descriptor a refused
year is marked A0h instead, so the next boot takes no time rather than
the older time the chip still holds; the mark gets the BSF bracket and
the same 5 s repeat as a time write, sharing its 13-attempt cap, and a
repeat whose run-on time leaves 2000-2099 marks instead. Other chips
keep their older time.

The header contract and the binding describe both; the Kconfig help and
ARCHITECTURE.md describe the 2000-2099 range, and ARCHITECTURE.md the
refused-year mark.

The same write order, year handling and 2000-2099 range are used by
Zephyr and MeshCore for the RV3028; the three are kept consistent in
behaviour.

Link: zephyrproject-rtos/zephyr#121389
Link: meshcore-dev/MeshCore#3421
Link: liquidraver#98

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@liquidraver

Copy link
Copy Markdown
Owner

Thanks for the work on these. I've gone through #103 and #104 and I'd like to take much less of both, to keep this file as small as it can be.

#103: keep only the year-range check

  • rtc_time_block() returns false outside 2000-2099.
  • zephcore_rtc_save() logs and writes nothing in that case, for every chip, including an rv3028-eeprom-config descriptor. The chip keeps the time it has.
  • rv3028_save_retry_fn() just stops if the run-on time leaves the range.
  • The matching one-line doc updates (Kconfig help, header, docs/ARCHITECTURE.md), without the "marked unset" part.

This is the real bug: a pre-2000 time is stored as y % 100 and comes back as 20xx at the next boot, above the 2025 floor.

#103: drop

  • The A0h year mark and the one-burst write. The current sequence (year 00h, six bytes, real year) already makes a cut write read as year 2000, which is below the 2025 floor, so the outcome here is the same.
  • rv3028_mark_year(), s_save_mark and the A0h read branch. I don't want a refused write to erase a chip time that is still counting correctly.

#104: replace with a much smaller version

I'd take what MeshCore does: clear the RV3028's 12_24 bit once at boot, before the time is read, and let the chip convert its own hours.

  • One optional devicetree property for the register and the bit, set on the two RV3028 nodes.
  • In rtc_probe(), after the chip is identified: read that register, write it back with the bit cleared only if the bit is set, and never if the register reads FFh. Then re-read the time block.
  • No change to the write path, no build asserts, no read-back retry.
  • DS3231/DS1307 can stay as they are; the first sync already fixes those.

Could you cut #103 down to the above and rework #104 this way (or close it and open a fresh one)?

@ptr727

ptr727 commented Oct 7, 2026

Copy link
Copy Markdown
Contributor Author

Thanks. Done as two new PRs, so this one stays as the record of the version coordinated with Zephyr and MeshCore, whose PRs link here:

Closing.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants