From 6fa8becff50f5f588719e1dcd7113d7acb6ab977 Mon Sep 17 00:00:00 2001 From: Christian Walther Date: Mon, 1 Jan 2024 18:25:04 +0100 Subject: [PATCH 1/3] nrf: Allow boards to choose LFCLK source. Board definitions could already define BLUETOOTH_LFCLK_RC or not to choose whether to use the internal RC oscillator or an external 32kHz crystal as the source for LFCLK, but that setting was only applied when the softdevice was enabled by ble.enable(). Apply it from the start, so that the functions of the time module always have the specified accuracy and run without interruption when the softdevice is enabled. Also add an option for the third LFCLK source, synthesized from the HFCLK, by defining BLUETOOTH_LFCLK_SYNTH. Signed-off-by: Christian Walther --- ports/nrf/drivers/bluetooth/ble_drv.c | 7 +++++++ ports/nrf/mphalport.c | 18 ++++++++++++++++-- 2 files changed, 23 insertions(+), 2 deletions(-) diff --git a/ports/nrf/drivers/bluetooth/ble_drv.c b/ports/nrf/drivers/bluetooth/ble_drv.c index b1caa187d5fc5..1c150b5d7157e 100644 --- a/ports/nrf/drivers/bluetooth/ble_drv.c +++ b/ports/nrf/drivers/bluetooth/ble_drv.c @@ -150,6 +150,13 @@ uint32_t ble_drv_stack_enable(void) { .rc_temp_ctiv = 2, .accuracy = NRF_CLOCK_LF_ACCURACY_250_PPM }; + #elif BLUETOOTH_LFCLK_SYNTH + nrf_clock_lf_cfg_t clock_config = { + .source = NRF_CLOCK_LF_SRC_SYNTH, + .rc_ctiv = 0, + .rc_temp_ctiv = 0, + .accuracy = NRF_CLOCK_LF_ACCURACY_50_PPM + }; #else nrf_clock_lf_cfg_t clock_config = { .source = NRF_CLOCK_LF_SRC_XTAL, diff --git a/ports/nrf/mphalport.c b/ports/nrf/mphalport.c index 06c6ba5cc2e76..a8be2bd6e94c6 100644 --- a/ports/nrf/mphalport.c +++ b/ports/nrf/mphalport.c @@ -55,10 +55,24 @@ void mp_nrf_start_lfclk(void) { // Check if the clock was recently stopped but is still running. #if USE_WORKAROUND_FOR_ANOMALY_132 bool was_running = nrf_clock_lf_is_running(NRF_CLOCK); - // If so, wait for it to stop. This ensures that the delay for anomaly 132 workaround does - // not land us in the middle of the forbidden interval. + #endif + // If so, wait for it to stop, otherwise the source cannot be changed. This also ensures + // that the delay for anomaly 132 workaround does not land us in the middle of the forbidden + // interval. while (nrf_clock_lf_is_running(NRF_CLOCK)) { } + // Use the same LFCLK source as for bluetooth so that enabling the softdevice will not cause + // an interruption. + nrf_clock_lf_src_set(NRF_CLOCK, + #if BLUETOOTH_LFCLK_RC + NRF_CLOCK_LFCLK_RC + #elif BLUETOOTH_LFCLK_SYNTH + NRF_CLOCK_LFCLK_Synth + #else + NRF_CLOCK_LFCLK_Xtal + #endif + ); + #if USE_WORKAROUND_FOR_ANOMALY_132 // If the clock just stopped, we can start it again right away as we are certainly before // the forbidden 66-138us interval. Otherwise, apply a delay of 138us to make sure we are // after the interval. From 4cef128793b52356bb93a66ab752f4c5053d5cb4 Mon Sep 17 00:00:00 2001 From: Christian Walther Date: Fri, 29 Mar 2024 17:07:41 +0100 Subject: [PATCH 2/3] nrf: Start HFXO when LFCLK is synthesized from it. A board that uses BLUETOOTH_LFCLK_SYNTH is expected to have a 32MHz crystal (Do any without one exist at all? I guess not because it's required for Bluetooth, which nRF microcontrollers are usually chosen for.), otherwise BLUETOOTH_LFCLK_RC would be more appropriate, so start the crystal oscillator together with LFCLK to have accurate timekeeping from the start. Signed-off-by: Christian Walther --- ports/nrf/mphalport.c | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/ports/nrf/mphalport.c b/ports/nrf/mphalport.c index a8be2bd6e94c6..899c8318aba86 100644 --- a/ports/nrf/mphalport.c +++ b/ports/nrf/mphalport.c @@ -82,6 +82,14 @@ void mp_nrf_start_lfclk(void) { #endif nrf_clock_task_trigger(NRF_CLOCK, NRF_CLOCK_TASK_LFCLKSTART); } + // When synthesizing LFCLK from HFCLK, start HFXO if it hasn't been started yet, otherwise we + // would be synthesizing from HFINT, which is pointless as it's even less accurate than LFRC. + // Must come after starting LFCLK, otherwise the LFCLK source reverts to RC. + #if BLUETOOTH_LFCLK_SYNTH + if (!nrf_clock_hf_start_task_status_get(NRF_CLOCK)) { + nrf_clock_task_trigger(NRF_CLOCK, NRF_CLOCK_TASK_HFCLKSTART); + } + #endif } #if MICROPY_PY_TIME_TICKS From e1a6da0f9d0dc5d122055779f8a27f0f34ce8f59 Mon Sep 17 00:00:00 2001 From: Christian Walther Date: Tue, 2 Jan 2024 00:02:14 +0100 Subject: [PATCH 3/3] nrf: Implement time.time() and machine.RTC. Optionally adds time() and time_ns() to the time module, as well as a machine.RTC class that only implements the datetime() method (following the example of the rp2 port), whose sole purpose is to provide the ability to set the time. Also provides the basis for enabling gmtime(), localtime(), mktime() in the time module. The nRF52 does not have a dedicated real-time clock peripheral, but timekeeping can be done by the same real-time counter that already powers the time.ticks_* and related functions. For reasonable accuracy, a suitable LFCLK source is required: The internal RC oscillator (BLUETOOTH_LFCLK_RC) by itself is insufficient, but any of the following work fine: - external 32kHz crystal (default) - synthesis from HFCLK (BLUETOOTH_LFCLK_SYNTH) when HFXO (external 32MHz crystal) is enabled - BLUETOOTH_LFCLK_RC + periodical calibration from HFXO (automatically done by the SoftDevice while enabled using ble.enable()) Boards can enable this by defining both configuration options MICROPY_PY_TIME_TICKS and MICROPY_PY_TIME_TIME_TIME_NS. Additionally, they may want to enable MICROPY_PY_TIME_GMTIME_LOCALTIME_MKTIME. This includes a generic implementation of mp_time_localtime_get() and mp_time_time_get() in terms of mp_hal_time_ns(), which could also be used by other ports. In particular by the embed port, for which I originally wrote it, noting the following: "I'm unsure where to put modtime_mphal.h, it could also be in extmod. The important thing is that for MICROPY_PY_TIME_INCLUDEFILE to work it must be at the same path in both the port build (original source tree) and the application build (micropython_embed distribution), therefore not in ports/embed/port. It is named .h, mismatching the corresponding ports/*/modtime.c, because it must not be compiled separately, which naming it .c would make harder for users of the embed port - they would need to explicitly exclude it, whereas this way they can continue to just compile all the .c files found in the micropython_embed distribution except those in lib." Signed-off-by: Christian Walther --- ports/nrf/Makefile | 1 + ports/nrf/modules/machine/machine_rtc.c | 100 ++++++++++++++++++++++++ ports/nrf/modules/machine/modmachine.c | 7 ++ ports/nrf/mphalport.c | 25 ++++++ ports/nrf/mphalport.h | 4 + shared/timeutils/modtime_mphal.h | 55 +++++++++++++ 6 files changed, 192 insertions(+) create mode 100644 ports/nrf/modules/machine/machine_rtc.c create mode 100644 shared/timeutils/modtime_mphal.h diff --git a/ports/nrf/Makefile b/ports/nrf/Makefile index 4fe921f0fad88..5f93d5b45eca7 100644 --- a/ports/nrf/Makefile +++ b/ports/nrf/Makefile @@ -264,6 +264,7 @@ endif DRIVERS_SRC_C += $(addprefix modules/,\ machine/spi.c \ machine/i2c.c \ + machine/machine_rtc.c \ machine/pin.c \ machine/timer.c \ machine/rtcounter.c \ diff --git a/ports/nrf/modules/machine/machine_rtc.c b/ports/nrf/modules/machine/machine_rtc.c new file mode 100644 index 0000000000000..2b2f8b2dc5137 --- /dev/null +++ b/ports/nrf/modules/machine/machine_rtc.c @@ -0,0 +1,100 @@ +/* + * This file is part of the MicroPython project, http://micropython.org/ + * + * The MIT License (MIT) + * + * Copyright (c) 2021 "Krzysztof Adamski" + * Copyright (c) 2024 Christian Walther + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +#include "py/runtime.h" + +// Timekeeping is handled by the ticks machinery, so only enable the +// machine.RTC type (whose sole purpose is to provide the ability to set the +// time) if that is enabled. +#if MICROPY_PY_TIME_TIME_TIME_NS && MICROPY_PY_TIME_TICKS + +#include "extmod/modmachine.h" +#include "shared/timeutils/timeutils.h" + +typedef struct _machine_rtc_obj_t { + mp_obj_base_t base; +} machine_rtc_obj_t; + +// singleton RTC object +static const machine_rtc_obj_t machine_rtc_obj = {{&machine_rtc_type}}; + +static mp_obj_t machine_rtc_make_new(const mp_obj_type_t *type, size_t n_args, size_t n_kw, const mp_obj_t *args) { + // check arguments + mp_arg_check_num(n_args, n_kw, 0, 0, false); + // return constant object + return (mp_obj_t)&machine_rtc_obj; +} + +static mp_obj_t machine_rtc_datetime(mp_uint_t n_args, const mp_obj_t *args) { + if (n_args == 1) { + uint64_t nanoseconds = mp_hal_time_ns(); + uint64_t seconds = nanoseconds / 1000000000; + nanoseconds -= seconds * 1000000000; + timeutils_struct_time_t tm; + timeutils_seconds_since_epoch_to_struct_time(seconds, &tm); + mp_obj_t tuple[8] = { + mp_obj_new_int(tm.tm_year), + mp_obj_new_int(tm.tm_mon), + mp_obj_new_int(tm.tm_mday), + mp_obj_new_int(tm.tm_wday), + mp_obj_new_int(tm.tm_hour), + mp_obj_new_int(tm.tm_min), + mp_obj_new_int(tm.tm_sec), + mp_obj_new_int(nanoseconds) + }; + return mp_obj_new_tuple(8, tuple); + } else { + mp_obj_t *items; + mp_obj_get_array_fixed_n(args[1], 8, &items); + uint64_t t = 1000000000ULL * timeutils_seconds_since_epoch( + mp_obj_get_int(items[0]), // year + mp_obj_get_int(items[1]), // month + mp_obj_get_int(items[2]), // date + mp_obj_get_int(items[4]), // hour + mp_obj_get_int(items[5]), // minute + mp_obj_get_int(items[6]) // second + ) + mp_obj_get_int(items[7]); // nanoseconds + mp_hal_set_time_ns(t); + } + return mp_const_none; +} +static MP_DEFINE_CONST_FUN_OBJ_VAR_BETWEEN(machine_rtc_datetime_obj, 1, 2, machine_rtc_datetime); + +static const mp_rom_map_elem_t machine_rtc_locals_dict_table[] = { + { MP_ROM_QSTR(MP_QSTR_datetime), MP_ROM_PTR(&machine_rtc_datetime_obj) }, +}; +static MP_DEFINE_CONST_DICT(machine_rtc_locals_dict, machine_rtc_locals_dict_table); + +MP_DEFINE_CONST_OBJ_TYPE( + machine_rtc_type, + MP_QSTR_RTC, + MP_TYPE_FLAG_NONE, + make_new, machine_rtc_make_new, + locals_dict, &machine_rtc_locals_dict + ); + +#endif diff --git a/ports/nrf/modules/machine/modmachine.c b/ports/nrf/modules/machine/modmachine.c index de1d0e31246b4..8317ec84c2f0a 100644 --- a/ports/nrf/modules/machine/modmachine.c +++ b/ports/nrf/modules/machine/modmachine.c @@ -65,6 +65,12 @@ #define MICROPY_PY_MACHINE_RTCOUNTER_ENTRY #endif +#if MICROPY_PY_TIME_TIME_TIME_NS && MICROPY_PY_TIME_TICKS +#define MICROPY_PY_MACHINE_RTC_ENTRY { MP_ROM_QSTR(MP_QSTR_RTC), MP_ROM_PTR(&machine_rtc_type) }, +#else +#define MICROPY_PY_MACHINE_RTC_ENTRY +#endif + #if MICROPY_PY_MACHINE_TIMER_NRF #define MICROPY_PY_MACHINE_TIMER_ENTRY { MP_ROM_QSTR(MP_QSTR_Timer), MP_ROM_PTR(&machine_timer_type) }, #else @@ -91,6 +97,7 @@ { MP_ROM_QSTR(MP_QSTR_Pin), MP_ROM_PTR(&pin_type) }, \ \ MICROPY_PY_MACHINE_RTCOUNTER_ENTRY \ + MICROPY_PY_MACHINE_RTC_ENTRY \ MICROPY_PY_MACHINE_TIMER_ENTRY \ MICROPY_PY_MACHINE_TEMP_ENTRY \ { MP_ROM_QSTR(MP_QSTR_HARD_RESET), MP_ROM_INT(PYB_RESET_HARD) }, \ diff --git a/ports/nrf/mphalport.c b/ports/nrf/mphalport.c index 899c8318aba86..efa53ef312dd4 100644 --- a/ports/nrf/mphalport.c +++ b/ports/nrf/mphalport.c @@ -200,10 +200,35 @@ mp_uint_t mp_hal_ticks_ms(void) { #endif +#if MICROPY_PY_TIME_TIME_TIME_NS && MICROPY_PY_TIME_TICKS + +// nanoseconds between 2000-01-01 and tick counter zero, to adjust clock +static uint64_t epoch_offset = 0; + +uint64_t mp_hal_time_ns(void) { + // Range of `overflows` is sufficient: nanoseconds overflow 64 bits before `overflows` overflows + // 32 bits. + // Same logic as in mp_hal_ticks_ms, no need to worry about intermediate result overflows if we + // do everything in 64-bit (this probably needn't be fast). + uint32_t overflows; + uint32_t counter; + // guard against overflow irq + RTC1_GET_TICKS_ATOMIC(rtc1, overflows, counter) + return (((uint64_t)overflows << 18) * 1953125) + (((uint64_t)counter * 1953125) >> 6) + epoch_offset; +} + +void mp_hal_set_time_ns(uint64_t ns_since_epoch) { + epoch_offset += ns_since_epoch - mp_hal_time_ns(); +} + +#else + uint64_t mp_hal_time_ns(void) { return 0; } +#endif + // this table converts from HAL_StatusTypeDef to POSIX errno const byte mp_hal_status_to_errno_table[4] = { [HAL_OK] = 0, diff --git a/ports/nrf/mphalport.h b/ports/nrf/mphalport.h index 7efe05a15fc8e..21d78485846ba 100644 --- a/ports/nrf/mphalport.h +++ b/ports/nrf/mphalport.h @@ -75,6 +75,10 @@ mp_uint_t mp_hal_ticks_ms(void); #define mp_hal_ticks_us() (0) #endif +#if MICROPY_PY_TIME_TIME_TIME_NS && MICROPY_PY_TIME_TICKS +void mp_hal_set_time_ns(uint64_t ns_since_epoch); +#endif + // TODO: empty implementation for now. Used by machine_spi.c:69 #define mp_hal_delay_us_fast(p) #define mp_hal_ticks_cpu() (0) diff --git a/shared/timeutils/modtime_mphal.h b/shared/timeutils/modtime_mphal.h new file mode 100644 index 0000000000000..78832506b412b --- /dev/null +++ b/shared/timeutils/modtime_mphal.h @@ -0,0 +1,55 @@ +/* + * This file is part of the MicroPython project, http://micropython.org/ + * + * The MIT License (MIT) + * + * Copyright (c) 2013-2023 Damien P. George + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +// This file is never compiled standalone, it's included directly from +// extmod/modtime.c via MICROPY_PY_TIME_INCLUDEFILE. + +#include "py/obj.h" +#include "py/mphal.h" +#include "shared/timeutils/timeutils.h" + +// Return the localtime as an 8-tuple. +static mp_obj_t mp_time_localtime_get(void) { + mp_int_t seconds = mp_hal_time_ns() / 1000000000; + timeutils_struct_time_t tm; + timeutils_seconds_since_epoch_to_struct_time(seconds, &tm); + mp_obj_t tuple[8] = { + tuple[0] = mp_obj_new_int(tm.tm_year), + tuple[1] = mp_obj_new_int(tm.tm_mon), + tuple[2] = mp_obj_new_int(tm.tm_mday), + tuple[3] = mp_obj_new_int(tm.tm_hour), + tuple[4] = mp_obj_new_int(tm.tm_min), + tuple[5] = mp_obj_new_int(tm.tm_sec), + tuple[6] = mp_obj_new_int(tm.tm_wday), + tuple[7] = mp_obj_new_int(tm.tm_yday), + }; + return mp_obj_new_tuple(8, tuple); +} + +// Returns the number of seconds, as an integer, since the Epoch. +static mp_obj_t mp_time_time_get(void) { + return mp_obj_new_int(mp_hal_time_ns() / 1000000000); +}