Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 6 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -99,7 +99,12 @@ Device files implement `DMDRVI_IOCTL_RTC_GET_TIME`, `SET_TIME`, `GET_STATUS`,
`GET_INFO`, and optional `INVALIDATE`. The shared calendar, capabilities and
status structures belong to dmdrvi. Read commands require a readable handle;
SET_TIME/INVALIDATE require write access. Private ioctl numbers are removed.
There is no byte-stream encoding. See the [shared contract](https://github.com/choco-technologies/dmdrvi/blob/main/docs/rtc.md).
`cat /dev/rtc` reads a 20-byte diagnostic line, `YYYY-MM-DDTHH:MM:SS\n`,
with no NUL terminator or implicit timezone. The first nonempty read captures a
calendar snapshot for that open handle; partial reads and seeks reuse it, and
byte offset 20 or later returns EOF. Reopen the device for fresh time. An unset
calendar returns `-ENODATA` without caching a failed read. Writes remain
unsupported; use the standard ioctl contract to set time. See the [shared contract](https://github.com/choco-technologies/dmdrvi/blob/main/docs/rtc.md).

GET_INFO reports 2000–2099, one-second resolution, SET_TIME and INVALIDATE.
This driver does not advertise hardware continuity-loss detection; the console
Expand Down
20 changes: 20 additions & 0 deletions docs/testing.md
Original file line number Diff line number Diff line change
Expand Up @@ -96,3 +96,23 @@ Validated STM32F7 compressed packages:
- `dmrtc_port.dmfc`: `6f8f416592d6a7a8eccfe794d909536824fc5ff02bad47dd434a9ae84a26390c`
- `rtc.dmfc`: `d5c120a682f5fab69631444fc6edc45d2df42b8eeea38302acc21afb59962810`
- `test_dmrtc.dmfc`: `2106b08b510d912fa778fd67eb44a9903808cac87eb42214d0295dcae43ab10a`

## Diagnostic text reads — 2026-10-16

`cat /dev/rtc` now prints a bounded 20-byte civil calendar line and reaches EOF.
The first nonempty read snapshots time per open handle. Partial reads and seeks
use that snapshot, even if the calendar changes; a new open obtains fresh time.
An unset calendar returns ENODATA and can be retried after setting time.

- Native ctest: 3/3 targets, including 14/14 driver steps. New checks cover
partial reads across a calendar change, buffer guards, rewind, EOF, fresh
opens, invalid offset/buffer/handle, write-only access and unset-clock retry.
- STM32F4/F7 builds passed.
- STM32F746G-DISCO: RTC 12/12 through VFS, dmdrvi 6/6 and RNG 6/6; actual
`cat /dev/rtc` checked before and after restoring PC time.
- Firmware flashed with OpenOCD Verified OK.

[Console evidence](validation/text-read-2026-10-16.txt).

- `dmrtc.dmfc`: `ec3ff4189437e1967a158d74aba065379513608321e08e66b48694e6ee08ea39`
- `test_dmrtc.dmfc`: `224de0720fb9239b9a0b4d7aed0833abac163ec29cfd0b889656abc019bc6957`
109 changes: 109 additions & 0 deletions docs/validation/text-read-2026-10-16.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
rtc info
years=2000..2099 resolution_ns=1000000000 writable=1 invalidatable=1 loss_detection=0
stm32f746xg@/> echo RTC_COMMAND_DONE_473
RTC_COMMAND_DONE_473
stm32f746xg@/> rtc status
running=1 calendar_set=1 time_lost=unknown
stm32f746xg@/> echo RTC_COMMAND_DONE_473
RTC_COMMAND_DONE_473
stm32f746xg@/> rtc get
2026-10-16T07:53:29 weekday=5
stm32f746xg@/> echo RTC_COMMAND_DONE_473
RTC_COMMAND_DONE_473
stm32f746xg@/> cat /dev/rtc
2026-10-16T07:53:30
stm32f746xg@/> echo RTC_COMMAND_DONE_473
RTC_COMMAND_DONE_473
stm32f746xg@/> cat /dev/rtc
2026-10-16T07:53:30
stm32f746xg@/> echo RTC_COMMAND_DONE_473
RTC_COMMAND_DONE_473
stm32f746xg@/> test_dmrtc
=== DMOD Test Runner ===
[ RUN ] year_rollover
[ OK ] year_rollover
[ RUN ] leap_day_rollover
[ OK ] leap_day_rollover
[ RUN ] vfs_ioctl_and_access_permissions
[ OK ] vfs_ioctl_and_access_permissions
[ RUN ] single_owner_and_bad_configuration
[ OK ] single_owner_and_bad_configuration
[ RUN ] year_2000_is_not_an_uninitialized_calendar
[ OK ] year_2000_is_not_an_uninitialized_calendar
[ RUN ] set_read_and_reject_invalid
[ OK ] set_read_and_reject_invalid
[ RUN ] unset_calendar
[ OK ] unset_calendar
[ RUN ] calendar_validation
[ OK ] calendar_validation
[ RUN ] private_rtc_command_numbers_are_not_supported
[ OK ] private_rtc_command_numbers_are_not_supported
[ RUN ] standard_rtc_range_and_precision_are_enforced
[ OK ] standard_rtc_range_and_precision_are_enforced
[ RUN ] standard_rtc_capabilities_and_unset_state
[ OK ] standard_rtc_capabilities_and_unset_state
[ RUN ] text_read_is_a_bounded_per_open_calendar_snapshot
[ OK ] text_read_is_a_bounded_per_open_calendar_snapshot

=== Results: 12/12 passed ===
stm32f746xg@/> echo RTC_COMMAND_DONE_473
RTC_COMMAND_DONE_473
stm32f746xg@/> test_dmdrvi
=== DMOD Test Runner ===
[ RUN ] dmdrvi_input_states_compare_every_field
[ OK ] dmdrvi_input_states_compare_every_field
[ RUN ] dmdrvi_monitor_policy_holds_longest_handler_name
[ OK ] dmdrvi_monitor_policy_holds_longest_handler_name
[ RUN ] dmdrvi_monitor_commands_are_distinct
[ OK ] dmdrvi_monitor_commands_are_distinct
[ RUN ] dmdrvi_errors_are_distinct_from_eof
[ OK ] dmdrvi_errors_are_distinct_from_eof
[ RUN ] dmdrvi_sizes_round_trip_above_four_gib
[ OK ] dmdrvi_sizes_round_trip_above_four_gib
[ RUN ] dmdrvi_v2_signatures_are_registered
[ OK ] dmdrvi_v2_signatures_are_registered

=== Results: 6/6 passed ===
stm32f746xg@/> echo RTC_COMMAND_DONE_473
RTC_COMMAND_DONE_473
stm32f746xg@/> test_dmrng
=== DMOD Test Runner ===
[ RUN ] vfs_device
dmdevfs: [ERROR] Driver failed to open device: /rng
[ERROR] Failed to open file '/dev/rng'
[ OK ] vfs_device
[ RUN ] sustained_generation
RNG_SAMPLE bytes=65536 checksum=b6acd4c9
[ OK ] sustained_generation
[ RUN ] single_owner_and_configuration_validation
[ OK ] single_owner_and_configuration_validation
[ RUN ] independent_blocks
[ OK ] independent_blocks
[ RUN ] unaligned_and_guarded_buffer
[ OK ] unaligned_and_guarded_buffer
[ RUN ] zero_and_invalid_reads
[ OK ] zero_and_invalid_reads

=== Results: 6/6 passed ===
stm32f746xg@/> echo RTC_COMMAND_DONE_473
RTC_COMMAND_DONE_473
stm32f746xg@/> rtc set 2026-10-16T07:53:35
stm32f746xg@/> echo RTC_COMMAND_DONE_473
RTC_COMMAND_DONE_473
stm32f746xg@/> rtc get
2026-10-16T07:53:35 weekday=5
stm32f746xg@/> echo RTC_COMMAND_DONE_473
RTC_COMMAND_DONE_473
stm32f746xg@/> cat /dev/rtc
2026-10-16T07:53:35
stm32f746xg@/> echo RTC_COMMAND_DONE_473
RTC_COMMAND_DONE_473
stm32f746xg@/> rtc status
running=1 calendar_set=1 time_lost=unknown
stm32f746xg@/> echo RTC_COMMAND_DONE_473
RTC_COMMAND_DONE_473
stm32f746xg@/> ip addr
eth0: 192.168.50.184/24
stm32f746xg@/> echo RTC_COMMAND_DONE_473
RTC_COMMAND_DONE_473
stm32f746xg@/>
47 changes: 43 additions & 4 deletions src/dmrtc.c
Original file line number Diff line number Diff line change
Expand Up @@ -180,7 +180,18 @@ dmod_dmdrvi_dif_api_declaration(2.0, dmrtc, void, _free, (dmdrvi_context_t ctx))
int rc = dmrtc_destroy((dmrtc_t)ctx);
if (rc != 0) DMOD_LOG_ERROR("dmrtc: release failed (%d)\n", rc);
}
struct rtc_file { dmrtc_t owner; int flags; };
enum { RTC_TEXT_LENGTH = 20 };
struct rtc_file {
dmrtc_t owner;
int flags;
bool text_ready;
char text[RTC_TEXT_LENGTH];
};

static void text_number(char* out, unsigned value, unsigned width)
{
while (width != 0) { out[--width] = (char)('0' + value % 10); value /= 10; }
}
dmod_dmdrvi_dif_api_declaration(2.0, dmrtc, void*, _open,
(dmdrvi_context_t context, int flags, const dmdrvi_dev_num_t* number))
{
Expand All @@ -191,7 +202,7 @@ dmod_dmdrvi_dif_api_declaration(2.0, dmrtc, void*, _open,
return NULL;
if (dmosi_mutex_lock(ctx->lock) != 0) return NULL;
struct rtc_file* file = ctx->opens == UINT32_MAX ? NULL : Dmod_Malloc(sizeof(*file));
if (file != NULL) { file->owner = ctx; file->flags = access; ++ctx->opens; }
if (file != NULL) { file->owner = ctx; file->flags = access; file->text_ready = false; ++ctx->opens; }
dmosi_mutex_unlock(ctx->lock);
return file;
}
Expand All @@ -208,8 +219,36 @@ dmod_dmdrvi_dif_api_declaration(2.0, dmrtc, void, _close,
dmod_dmdrvi_dif_api_declaration(2.0, dmrtc, dmdrvi_ssize_t, _read,
(dmdrvi_context_t ctx, void* handle, void* buffer, size_t size, dmdrvi_offset_t offset))
{
(void)ctx; (void)handle; (void)buffer; (void)size; (void)offset;
return -ENOTSUP;
dmrtc_t owner = (dmrtc_t)ctx;
struct rtc_file* file = handle;
if (!dmrtc_is_valid(owner) || file == NULL || file->owner != owner) return -EBADF;
if (!(file->flags & (DMDRVI_O_RDONLY | DMDRVI_O_RDWR))) return -EACCES;
if (offset < 0) return -EINVAL;
if (size == 0) return 0;
if (buffer == NULL) return -EINVAL;
if (offset >= RTC_TEXT_LENGTH) return 0;
int rc = dmosi_mutex_lock(owner->lock);
if (rc != 0) return rc;
if (!file->text_ready) {
dmrtc_calendar_t c;
rc = dmrtc_port_get(&c);
if (rc == 0 && dmrtc_normalize(&c) != 0) rc = -EIO;
if (rc == 0) {
memcpy(file->text, "0000-00-00T00:00:00\n", RTC_TEXT_LENGTH);
text_number(file->text, c.year, 4);
text_number(file->text + 5, c.month, 2);
text_number(file->text + 8, c.day, 2);
text_number(file->text + 11, c.hour, 2);
text_number(file->text + 14, c.minute, 2);
text_number(file->text + 17, c.second, 2);
file->text_ready = true;
}
}
size_t count = RTC_TEXT_LENGTH - (size_t)offset;
if (count > size) count = size;
if (rc == 0) memcpy(buffer, file->text + (size_t)offset, count);
dmosi_mutex_unlock(owner->lock);
return rc == 0 ? (dmdrvi_ssize_t)count : rc;
}
dmod_dmdrvi_dif_api_declaration(2.0, dmrtc, dmdrvi_ssize_t, _write,
(dmdrvi_context_t ctx, void* handle, const void* buffer, size_t size, dmdrvi_offset_t offset))
Expand Down
70 changes: 69 additions & 1 deletion tests/dmrtc_test.c
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ void dmod_test_teardown(void)
owned = NULL; ctx = NULL;
}
#ifdef DMRTC_MOCK
static dmod_dmdrvi_read_t device_read;
static dmod_dmdrvi_ioctl_t device_ioctl;
static dmod_dmdrvi_close_t device_close;
#endif
Expand All @@ -56,9 +57,10 @@ static void* open_device(const char* mode)
Dmod_Context_t* module = Dmod_LoadModuleByName("dmrtc");
if (module == NULL) return NULL;
dmod_dmdrvi_open_t open = (dmod_dmdrvi_open_t)Dmod_GetDifFunction(module, dmod_dmdrvi_open_sig);
device_read = (dmod_dmdrvi_read_t)Dmod_GetDifFunction(module, dmod_dmdrvi_read_sig);
device_ioctl = (dmod_dmdrvi_ioctl_t)Dmod_GetDifFunction(module, dmod_dmdrvi_ioctl_sig);
device_close = (dmod_dmdrvi_close_t)Dmod_GetDifFunction(module, dmod_dmdrvi_close_sig);
if (open == NULL || device_ioctl == NULL || device_close == NULL) return NULL;
if (open == NULL || device_read == NULL || device_ioctl == NULL || device_close == NULL) return NULL;
dmdrvi_dev_num_t num = {0};
int flags = strcmp(mode,"rb") == 0 ? DMDRVI_O_RDONLY :
strcmp(mode,"wb") == 0 ? DMDRVI_O_WRONLY : DMDRVI_O_RDWR;
Expand All @@ -83,6 +85,72 @@ static void close_device(void* file)
Dmod_FileClose(file);
#endif
}
/* Host exercises the DIF directly; hardware exercises the same read via VFS. */
static dmdrvi_ssize_t read_device(void* file, void* buffer, size_t size, dmdrvi_offset_t offset)
{
#ifdef DMRTC_MOCK
return device_read((dmdrvi_context_t)ctx, file, buffer, size, offset);
#else
if (Dmod_FileSeek(file, offset, 0) != 0) return -EIO;
return (dmdrvi_ssize_t)Dmod_FileRead(buffer, 1, size, file);
#endif
}
DMOD_TEST_STEP(text_read_is_a_bounded_per_open_calendar_snapshot)
{
dmrtc_calendar_t c = {2024, 2, 29, 12, 34, 56, 0};
DMOD_TEST_EXPECT_EQ(dmrtc_set(ctx, &c), 0);
void* file = open_device("rb"); DMOD_TEST_EXPECT_NOT_NULL(file);
if (file == NULL) return;
char text[22]; memset(text, '#', sizeof(text));
DMOD_TEST_EXPECT_EQ(read_device(file, text + 1, 17, 0), 17);
DMOD_TEST_EXPECT_EQ(memcmp(text + 1, "2024-02-29T12:34:", 17), 0);
c = (dmrtc_calendar_t){2000, 1, 1, 0, 0, 0, 0};
DMOD_TEST_EXPECT_EQ(dmrtc_set(ctx, &c), 0);
for (int offset = 17; offset < 20; ++offset)
DMOD_TEST_EXPECT_EQ(read_device(file, text + 1 + offset, 1, offset), 1);
DMOD_TEST_EXPECT_EQ(text[0], '#'); DMOD_TEST_EXPECT_EQ(text[21], '#');
DMOD_TEST_EXPECT_EQ(text[20], '\n');
DMOD_TEST_EXPECT_TRUE(text[18] >= '0' && text[18] <= '5');
DMOD_TEST_EXPECT_TRUE(text[19] >= '0' && text[19] <= '9');
char again[32]; memset(again, '#', sizeof(again));
DMOD_TEST_EXPECT_EQ(read_device(file, again, sizeof(again), 0), 20);
DMOD_TEST_EXPECT_EQ(memcmp(again, text + 1, 20), 0);
DMOD_TEST_EXPECT_EQ(again[20], '#');
DMOD_TEST_EXPECT_EQ(read_device(file, again, sizeof(again), 20), 0);
DMOD_TEST_EXPECT_EQ(read_device(file, again, sizeof(again), 1000), 0);
close_device(file);
file = open_device("rb"); DMOD_TEST_EXPECT_NOT_NULL(file);
if (file != NULL) {
DMOD_TEST_EXPECT_EQ(read_device(file, again, sizeof(again), 0), 20);
DMOD_TEST_EXPECT_EQ(memcmp(again, "2000-01-01T00:00:", 17), 0);
close_device(file);
}
}
#ifdef DMRTC_MOCK
DMOD_TEST_STEP(text_read_validates_access_and_retries_unset_clock)
{
void* file = open_device("rb"); DMOD_TEST_EXPECT_NOT_NULL(file);
if (file == NULL) return;
char out[24]; memset(out, '#', sizeof(out));
DMOD_TEST_EXPECT_EQ(dmrtc_invalidate(ctx), 0);
DMOD_TEST_EXPECT_EQ(read_device(file, NULL, 0, 0), 0);
DMOD_TEST_EXPECT_EQ(read_device(file, NULL, 1, 0), -EINVAL);
DMOD_TEST_EXPECT_EQ(read_device(file, out, sizeof(out), -1), -EINVAL);
DMOD_TEST_EXPECT_EQ(read_device(file, out, sizeof(out), 0), -ENODATA);
DMOD_TEST_EXPECT_EQ(out[0], '#');
dmrtc_calendar_t c = {2026, 10, 16, 1, 2, 3, 0};
DMOD_TEST_EXPECT_EQ(dmrtc_set(ctx, &c), 0);
DMOD_TEST_EXPECT_EQ(read_device(file, out, sizeof(out), 0), 20);
DMOD_TEST_EXPECT_EQ(memcmp(out, "2026-10-16T01:02:03\n", 20), 0);
DMOD_TEST_EXPECT_EQ(device_read((dmdrvi_context_t)ctx, NULL, out, 1, 0), -EBADF);
close_device(file);
file = open_device("wb"); DMOD_TEST_EXPECT_NOT_NULL(file);
if (file != NULL) {
DMOD_TEST_EXPECT_EQ(read_device(file, out, 1, 0), -EACCES);
close_device(file);
}
}
#endif
DMOD_TEST_STEP(standard_rtc_capabilities_and_unset_state)
{
void* file = open_device("r+b"); DMOD_TEST_EXPECT_NOT_NULL(file);
Expand Down
Loading