Skip to content

Commit 35210c2

Browse files
authored
Merge pull request adafruit#11491 from tannewt/zephyr_rtc
Implement RTC on Zephyr
2 parents eec31fe + 3156883 commit 35210c2

8 files changed

Lines changed: 194 additions & 0 deletions

File tree

‎ports/zephyr-cp/boards/st/stm32wba65i_dk1/board.overlay‎

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,14 @@
33
status = "okay";
44
};
55

6+
/* The board's &rtc node is already status-okay in the upstream devicetree, so
7+
* alias it so the rtc module picks it up like other RTC boards do. */
8+
/ {
9+
aliases {
10+
rtc = &rtc;
11+
};
12+
};
13+
614
/* The USB device controller (zephyr_udc0, including the OTG HS PHY and the
715
* HSE/VOS clocking it needs) and the joystick-selector boot button are
816
* defined in the mcuboot fork's dts/st/stm32wba65i_dk1.dtsi, which sysbuild
Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
// This file is part of the CircuitPython project: https://circuitpython.org
2+
//
3+
// SPDX-FileCopyrightText: Copyright (c) 2026 Scott Shawcroft for Adafruit Industries
4+
//
5+
// SPDX-License-Identifier: MIT
6+
7+
#include <errno.h>
8+
9+
#include "py/obj.h"
10+
#include "py/runtime.h"
11+
#include "shared/timeutils/timeutils.h"
12+
#include "shared-bindings/rtc/RTC.h"
13+
#include "common-hal/rtc/RTC.h"
14+
15+
#include "bindings/zephyr_kernel/__init__.h"
16+
17+
#include <zephyr/devicetree.h>
18+
#include <zephyr/device.h>
19+
#include <zephyr/drivers/rtc.h>
20+
21+
// The RTC driver already initializes its device at Zephyr boot time, so there
22+
// is nothing for the port to start up: time is simply read from and written
23+
// to the devicetree `rtc` device.
24+
static const struct device *const rtc_dev = DEVICE_DT_GET(DT_ALIAS(rtc));
25+
26+
// Zephyr stores calibration in parts per billion. CircuitPython's calibration
27+
// unit is approximately one part per million.
28+
#define CALIBRATION_PPM_TO_ZEPHYR (1000)
29+
30+
void common_hal_rtc_get_time(timeutils_struct_time_t *tm) {
31+
struct rtc_time zephyr_tm;
32+
int ret = rtc_get_time(rtc_dev, &zephyr_tm);
33+
if (ret == -ENODATA) {
34+
// The RTC has never been set (no battery-backed hardware and no
35+
// seeded time). Report the start of the CircuitPython epoch,
36+
// 2000-01-01, instead of failing; users set it with
37+
// rtc.RTC().datetime.
38+
timeutils_seconds_since_2000_to_struct_time(0, tm);
39+
return;
40+
}
41+
CHECK_ZEPHYR_RESULT(ret);
42+
43+
tm->tm_year = zephyr_tm.tm_year + 1900;
44+
tm->tm_mon = zephyr_tm.tm_mon + 1;
45+
tm->tm_mday = zephyr_tm.tm_mday;
46+
tm->tm_hour = zephyr_tm.tm_hour;
47+
tm->tm_min = zephyr_tm.tm_min;
48+
tm->tm_sec = zephyr_tm.tm_sec;
49+
// tm_wday and tm_yday are filled in by struct_time_from_tm.
50+
}
51+
52+
void common_hal_rtc_set_time(timeutils_struct_time_t *tm) {
53+
struct rtc_time zephyr_tm = {
54+
.tm_year = tm->tm_year - 1900,
55+
.tm_mon = tm->tm_mon - 1,
56+
.tm_mday = tm->tm_mday,
57+
.tm_hour = tm->tm_hour,
58+
.tm_min = tm->tm_min,
59+
.tm_sec = tm->tm_sec,
60+
};
61+
CHECK_ZEPHYR_RESULT(rtc_set_time(rtc_dev, &zephyr_tm));
62+
}
63+
64+
int common_hal_rtc_get_calibration(void) {
65+
#ifdef CONFIG_RTC_CALIBRATION
66+
int32_t zephyr_calibration;
67+
int ret = rtc_get_calibration(rtc_dev, &zephyr_calibration);
68+
if (ret == -ENOSYS || ret == -ENOTSUP) {
69+
mp_raise_NotImplementedError_varg(MP_ERROR_TEXT("%q"), MP_QSTR_calibration);
70+
}
71+
CHECK_ZEPHYR_RESULT(ret);
72+
return zephyr_calibration / CALIBRATION_PPM_TO_ZEPHYR;
73+
#else
74+
// The Zephyr RTC calibration syscall only exists with
75+
// CONFIG_RTC_CALIBRATION, which is off unless a driver needs it.
76+
mp_raise_NotImplementedError_varg(MP_ERROR_TEXT("%q"), MP_QSTR_calibration);
77+
#endif
78+
}
79+
80+
void common_hal_rtc_set_calibration(int calibration) {
81+
#ifdef CONFIG_RTC_CALIBRATION
82+
int ret = rtc_set_calibration(rtc_dev, (int32_t)calibration * CALIBRATION_PPM_TO_ZEPHYR);
83+
if (ret == -ENOSYS || ret == -ENOTSUP) {
84+
mp_raise_NotImplementedError_varg(MP_ERROR_TEXT("%q"), MP_QSTR_calibration);
85+
}
86+
CHECK_ZEPHYR_RESULT(ret);
87+
#else
88+
mp_raise_NotImplementedError_varg(MP_ERROR_TEXT("%q"), MP_QSTR_calibration);
89+
#endif
90+
}
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
// This file is part of the CircuitPython project: https://circuitpython.org
2+
//
3+
// SPDX-FileCopyrightText: Copyright (c) 2026 Scott Shawcroft for Adafruit Industries
4+
//
5+
// SPDX-License-Identifier: MIT
6+
7+
#pragma once
8+
9+
// The RTC device is selected by the devicetree `rtc` alias, following
10+
// Zephyr's own RTC sample (samples/drivers/rtc). The rtc module is only
11+
// enabled (CIRCUITPY_RTC=1) when the alias exists and CONFIG_RTC is on, so
12+
// common-hal/rtc/RTC.c can always DEVICE_DT_GET(DT_ALIAS(rtc)).
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
// This file is part of the CircuitPython project: https://circuitpython.org
2+
//
3+
// SPDX-FileCopyrightText: Copyright (c) 2026 Scott Shawcroft for Adafruit Industries
4+
//
5+
// SPDX-License-Identifier: MIT

‎ports/zephyr-cp/cptools/zephyr2cp.py‎

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -731,6 +731,7 @@ def zephyr_dts_to_cp_board(board_id, portdir, builddir, zephyrbuilddir, mpconfig
731731
config_bt_found = False
732732
config_adc_enabled = False
733733
config_dac_enabled = False
734+
config_rtc_enabled = False
734735
config_present = True
735736
config = zephyrbuilddir / ".config"
736737
if not config.exists():
@@ -747,6 +748,8 @@ def zephyr_dts_to_cp_board(board_id, portdir, builddir, zephyrbuilddir, mpconfig
747748
config_adc_enabled = line.strip().endswith("=y")
748749
elif line.startswith("CONFIG_DAC="):
749750
config_dac_enabled = line.strip().endswith("=y")
751+
elif line.startswith("CONFIG_RTC="):
752+
config_rtc_enabled = line.strip().endswith("=y")
750753

751754
runners = zephyrbuilddir / "runners.yaml"
752755
runners = yaml.safe_load(runners.read_text())
@@ -1693,6 +1696,21 @@ def zephyr_dts_to_cp_board(board_id, portdir, builddir, zephyrbuilddir, mpconfig
16931696
board_info["analogio"] = (adc_present and config_adc_enabled) or (
16941697
dac_present and config_dac_enabled
16951698
)
1699+
board_info["rtc"] = False
1700+
# RTC needs both a devicetree `rtc` alias pointing at the RTC device
1701+
# (the same selection Zephyr's own RTC sample uses) and the Zephyr RTC
1702+
# driver layer enabled (CONFIG_RTC).
1703+
rtc_node = device_tree.alias2node.get("rtc")
1704+
if rtc_node is not None:
1705+
rtc_status = rtc_node.props.get("status")
1706+
rtc_present = rtc_status is None or rtc_status.to_string() == "okay"
1707+
if rtc_present and config_rtc_enabled:
1708+
board_info["rtc"] = True
1709+
elif rtc_present:
1710+
logger.warning(
1711+
"Devicetree has a status-okay 'rtc' alias but CONFIG_RTC is "
1712+
"not enabled, so the rtc module is disabled."
1713+
)
16961714
board_info["usb_num_endpoint_pairs"] = usb_num_endpoint_pairs
16971715

16981716
# Detect NVM partition size from the device tree.

‎ports/zephyr-cp/prj.conf‎

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,12 @@ CONFIG_LOG_BLOCK_IN_THREAD=y
4141

4242
CONFIG_EVENTS=y
4343

44+
# Enable the RTC driver layer so boards with a status-okay `rtc` devicetree
45+
# alias (e.g. native_sim's rtc-emul, STM32, RP2040, SmartBond) get a working
46+
# rtc module via common-hal/rtc. Boards without the alias only pay for the
47+
# tiny RTC core (syscall table + rtc_utils).
48+
CONFIG_RTC=y
49+
4450
CONFIG_SERIAL=y
4551

4652
# Fail quickly on hung I2C devices (probe/scan also uses this timeout)

‎ports/zephyr-cp/supervisor/port.c‎

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,10 @@
1414
#include "common-hal/audiobusio/I2SOut.h"
1515
#endif
1616

17+
#if CIRCUITPY_RTC
18+
#include "shared-bindings/rtc/__init__.h"
19+
#endif
20+
1721
#include <stdlib.h>
1822

1923
#include <zephyr/autoconf.h>
@@ -261,6 +265,10 @@ void reset_port(void) {
261265
i2sout_reset();
262266
#endif
263267

268+
#if CIRCUITPY_RTC
269+
rtc_reset();
270+
#endif
271+
264272
#if defined(CONFIG_ARCH_POSIX)
265273
native_sim_reset_port_count++;
266274
if (native_sim_port_resets != INT32_MAX &&

‎ports/zephyr-cp/tests/test_rtc.py‎

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
# SPDX-FileCopyrightText: 2026 Scott Shawcroft for Adafruit Industries
2+
# SPDX-License-Identifier: MIT
3+
4+
"""Test the rtc module against native_sim's emulated RTC.
5+
6+
On a fresh boot the RTC has never been set (the emulated driver has no
7+
battery backup), so reading it must give the start of the CircuitPython
8+
epoch, 2000-01-01, rather than an error. Setting the datetime must update
9+
what time.localtime() reports, and the RTC must advance on its own.
10+
"""
11+
12+
import pytest
13+
14+
15+
RTC_SET_CODE = """\
16+
import rtc
17+
import time
18+
19+
r = rtc.RTC()
20+
21+
t = time.localtime()
22+
print("UNSET:", t.tm_year, t.tm_mon, t.tm_mday, t.tm_hour, t.tm_min, t.tm_sec)
23+
24+
r.datetime = time.struct_time((2025, 7, 4, 12, 34, 56, 4, -1, -1))
25+
26+
t = time.localtime()
27+
print("SET:", t.tm_year, t.tm_mon, t.tm_mday, t.tm_hour, t.tm_min, t.tm_sec)
28+
29+
time.sleep(2)
30+
t = time.localtime()
31+
print("ADVANCED:", t.tm_year, (t.tm_hour, t.tm_min, t.tm_sec) != (12, 34, 56))
32+
33+
try:
34+
r.calibration = 1
35+
except NotImplementedError:
36+
print("CALIBRATION NOT SUPPORTED")
37+
"""
38+
39+
40+
@pytest.mark.circuitpy_drive({"code.py": RTC_SET_CODE})
41+
@pytest.mark.duration(30)
42+
def test_rtc_set_and_get(circuitpython):
43+
"""Setting rtc.RTC().datetime updates time.localtime() and advances."""
44+
circuitpython.serial.wait_for("UNSET: 2000 1 1", timeout=30)
45+
circuitpython.serial.wait_for("SET: 2025 7 4 12 34 56", timeout=30)
46+
circuitpython.serial.wait_for("ADVANCED: 2025 True", timeout=30)
47+
circuitpython.serial.wait_for("CALIBRATION NOT SUPPORTED", timeout=30)

0 commit comments

Comments
 (0)