From 905b81341cd306c6d8a9ad16463833250ab56f9a Mon Sep 17 00:00:00 2001 From: Peter Wittich Date: Tue, 20 Oct 2026 16:58:10 -0400 Subject: [PATCH] set RTC via progcom --- AGENTS.md | 4 +++ MCU_REGISTER_MAP.md | 27 ++++++++++++++++-- README.md | 4 +++ device/mcu.py | 47 +++++++++++++++++++++++++++++++ tests/test_mcu.py | 55 ++++++++++++++++++++++++++++++++++++- tests/test_wire_contract.py | 6 ++++ 6 files changed, 139 insertions(+), 4 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index 6478fd2..239a096 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -94,6 +94,10 @@ The Python `MCU` client for ProgCom device `MC 0` follows minor 0; - `read_persistent_log_info()` and `read_persistent_log_entries()` read the frozen (offsets final, not yet firmware-served) persistent-log pages; and +- `set_rtc(when=None)` writes the Control-page RTC set word (needs + `RTC_WRITE`, map minor >= 4): host local wall-clock time as seconds since + 1970-01-01 of that clock, not Unix time. Client and firmware refuse values + before 2026-01-01. `mcu_set_rtc.py` is the command-line wrapper; - `send_control(McuControlCommand...)` writes the one-byte write-only Control-page command (`ASSERT`/`RELEASE_PROGCOM_POWER_INHIBIT`, `CLEAR_POWER_FAULT`, `CLEAR_ALARM_LATCHES`, `ZYNQMON_ENABLE_TRANSMIT`, diff --git a/MCU_REGISTER_MAP.md b/MCU_REGISTER_MAP.md index b498114..da95619 100644 --- a/MCU_REGISTER_MAP.md +++ b/MCU_REGISTER_MAP.md @@ -23,7 +23,7 @@ Read-only. | --- | ---: | --- | | `0x00` | 4 | ASCII magic `CMCU` | | `0x04` | 1 | map major version (`1`) | -| `0x05` | 1 | map minor version (`3`) | +| `0x05` | 1 | map minor version (`4`) | | `0x06` | 1 | hardware revision | | `0x07` | 1 | ADC channel count (`21`) | | `0x08` | 4 | capability bitmap | @@ -62,6 +62,7 @@ Capability bits (bitmap at `0x08`): | 7 | `CONFIG` | yes (page `0x05` readable; map minor ≥ 2) | | 8 | `RUNTIME` | yes | | 9 | `CONFIG_WRITE` | yes (page `0x05` writable; map minor ≥ 3) | +| 10 | `RTC_WRITE` | yes (page `0x7f` offset `0x04`; map minor ≥ 4) | A capability bit is set only once its page is fully implemented; a client must not assume a page works just because its offsets are frozen here. @@ -311,7 +312,13 @@ count is published — scan all 64 and filter erased/empty sentinels. ## Page `0x7f` — Control — **implemented** -Write-only. One command byte at offset `0x00`. +Write-only. One command byte at offset `0x00`, and from map minor 4 the RTC +set word at offset `0x04`. + +| Offset | Size | Field | +| --- | ---: | --- | +| `0x00` | 1 | command code, table below | +| `0x04` | 4 | RTC set: local wall-clock seconds since 1970-01-01 00:00:00 (needs `RTC_WRITE`, bit 10) | | Code | Command | | ---: | --- | @@ -348,6 +355,20 @@ timeout): a full queue is real backpressure and should surface to the remote client immediately as `MCU_REG_QUEUE_FULL`, not stall the ProgCom UART link waiting for space. +**RTC set (`0x04`).** The RTC carries no time zone. The value is the setter's +local wall-clock time expressed as seconds since 1970-01-01 00:00:00 of that +clock, so it is *not* Unix time; `cm_interface` (`MCU.set_rtc`) computes it +with `calendar.timegm` on the host's local time. A DST change shifts the RTC +only when someone sets it again. Values below `1767225600` (2026-01-01 +00:00:00) return `invalid MCU value`. The write is synchronous, with no queue, +and is what the Runtime page's RTC fields read back. On map minor 3 firmware +the offset returns `invalid MCU address`; check bit 10 first. + +The RTC set is not serialised against the CLI `time set` command. Each loads +the calendar with two register writes (time, then date), so two concurrent +setters could leave one caller's time with the other's date. No lock is taken; do not set the clock from both +interfaces at once. + Each alarm-clear command targets exactly one alarm task's queue — there is no combined "clear all alarms" command, so no command ever needs atomic delivery across more than one queue. @@ -361,7 +382,7 @@ delivery across more than one queue. | address not inside a declared field | `invalid MCU address` | | read/write crosses the end of a field, or length outside 1-4 | `invalid MCU read span` | | write to a field with a length other than its width (pages `0x05`, `0x7f`) | `invalid MCU write span` | -| valid address and length, value outside the field's clamp (page `0x05`) | `invalid MCU value` | +| valid address and length, value outside the field's clamp (page `0x05`, page `0x7f` RTC set) | `invalid MCU value` | | write to a read-only page | `MCU register is read only` | | read from a write-only page | `MCU register is write only` | | valid request, queue-owned op not yet accepted | `MCU queue full` | diff --git a/README.md b/README.md index 1718d8d..1c840ad 100644 --- a/README.md +++ b/README.md @@ -133,6 +133,10 @@ rt = mcu.read_runtime() # page 0x06, map minor >= 1 firmware print(rt.heap_free_bytes, rt.system_stack_untouched_words, rt.fpga_done) print(rt.rtc.isoformat()) # "unset" when the RTC is not valid +# Needs map minor >= 4 (RTC_WRITE). Sets host local wall-clock time; the RTC +# has no time zone. Refuses times before 2026-01-01. CLI: mcu_set_rtc.py +mcu.set_rtc() + mcu.send_control(McuControlCommand.CLEAR_ALARM_LATCHES) ``` diff --git a/device/mcu.py b/device/mcu.py index 94e98ad..446d205 100644 --- a/device/mcu.py +++ b/device/mcu.py @@ -5,6 +5,8 @@ ``MCU_Reg.h`` — keep this module in sync with it, not the other way around. """ +import calendar +import datetime import math import struct from enum import IntEnum, IntFlag @@ -127,6 +129,22 @@ class ConfigReg(IntEnum): ALARM_VOLT_CPCT_MIN = 100 # 1 % ALARM_VOLT_CPCT_MAX = 1000 # 10 % +# Lowest RTC set value the firmware accepts (``RTC_SET_MIN_SECONDS``): +# 2026-01-01 00:00:00 local wall-clock time, in the units of rtc_local_seconds(). +RTC_SET_MIN_SECONDS = 1767225600 + + +def rtc_local_seconds(when: datetime.datetime) -> int: + """Wire value for the RTC set field: seconds since 1970-01-01 00:00:00 of + ``when``'s wall clock. Not Unix time: the RTC carries no time zone. + + A naive datetime is taken as wall-clock time as given; an aware one is + first converted to the host's local zone. Sub-second precision is dropped. + """ + if when.tzinfo is not None: + when = when.astimezone().replace(tzinfo=None) + return calendar.timegm(when.timetuple()) + class AlarmTempDevice(IntEnum): """Matches the firmware's ``enum device`` (``Tasks.h``): FF, DCDC, TM4C, FPGA.""" @@ -159,6 +177,7 @@ class RuntimeReg(IntEnum): class ControlReg(IntEnum): COMMAND = 0x00 # uint8, write-only + RTC_SET_LOCAL = 0x04 # uint32, write-only, see rtc_local_seconds() class McuControlCommand(IntEnum): @@ -180,6 +199,7 @@ class McuCapability(IntFlag): CONFIG = 1 << 7 RUNTIME = 1 << 8 CONFIG_WRITE = 1 << 9 + RTC_WRITE = 1 << 10 class McuBuildType(IntEnum): @@ -835,3 +855,30 @@ def send_control(self, command: McuControlCommand, page = McuPage.CONTROL self._require_page(page, check_capability) self.write_reg(self._reg(page, ControlReg.COMMAND), bytes([int(command)])) + + def set_rtc(self, when: Optional[datetime.datetime] = None, + check_capability: bool = True) -> int: + """Set the MCU RTC to a local wall-clock time; returns the value sent. + + ``when`` defaults to the host's local time now; see + :func:`rtc_local_seconds` for naive vs. aware datetimes. The RTC has + no time zone, so a DST change shifts it only when it is set again. + Values before 2026-01-01 raise ``ValueError`` and send nothing. + + Not serialised against the CLI ``time set``: two concurrent setters + can leave one's time with the other's date. + """ + if when is None: + when = datetime.datetime.now() + seconds = rtc_local_seconds(when) + if not RTC_SET_MIN_SECONDS <= seconds <= 0xFFFFFFFF: + raise ValueError( + "RTC time {} outside 2026-01-01..2106-02-07".format(when.isoformat())) + page = McuPage.CONTROL + self._require_page(page, check_capability) + if check_capability and not self.capabilities() & McuCapability.RTC_WRITE: + raise McuCapabilityUnavailable( + "firmware does not advertise {}".format(McuCapability.RTC_WRITE.name) + ) + self.write_reg(self._reg(page, ControlReg.RTC_SET_LOCAL), struct.pack("