Skip to content

stm32/boards: Set the RTC prescalers for the LSI on Arduino boards. - #19650

Open
kwagyeman wants to merge 1 commit into
micropython:masterfrom
kwagyeman:kwabena/arduino_rtc_prescaler
Open

stm32/boards: Set the RTC prescalers for the LSI on Arduino boards.#19650
kwagyeman wants to merge 1 commit into
micropython:masterfrom
kwagyeman:kwabena/arduino_rtc_prescaler

Conversation

@kwagyeman

Copy link
Copy Markdown
Contributor

Summary

The Arduino Giga, Nicla Vision, Opta and Portenta H7 run the RTC from the
~32kHz LSI, but with the RTC prescalers set for a 32768Hz source — so the
RTC keeps time about 2.3% slow (over half an hour per day), far beyond what
rtc.calibration() can correct. Reported with measurements in
https://forums.openmv.io/t/rtc-behind-on-arduino-nicla-vision/11861.
This changes the prescalers to divide by the LSI's nominal 32000.

These boards do carry an accurate external 32768Hz oscillator (a SiT1532,
±20ppm) on OSC32_IN and configure MICROPY_HW_RTC_USE_BYPASS, and it does
work — switched to LSE bypass the RTC held ±0ppm on the bench. But it can't
be shipped: the boards' stock Arduino MCUboot bootloader (built with Mbed's
lse_available=false) reinitialises the RTC onto the LSI at every hard
reset when any other clock source is selected, and the backup-domain reset
that requires wipes the calendar. Verified on a Nicla Vision: with the RTC
on LSE, every machine.reset() and deep-sleep wake came back on the LSI
with the calendar reset to the bootloader's 2021-01-01 epoch. So the RTC
clock source stays LSI (matching the bootloader, which then leaves the RTC
completely alone), and only the divider changes.

Testing

Measured on an Arduino Nicla Vision against the HSE-derived SysTick over
240s windows: -22300ppm before, -684ppm after (the remainder is
part-specific LSI tolerance). Time is preserved through
machine.deepsleep() and machine.reset(), including the prescaler value
itself. The microsecond subsecond fields still divide exactly
(32000/64 = 500). Build-tested on all four boards.

Generative AI

I used generative AI tools when creating this PR, but a human has checked the
code and is responsible for the code and the description above.

@kwagyeman kwagyeman moved this to In progress in OpenMV Features Aug 25, 2026
@kwagyeman

Copy link
Copy Markdown
Contributor Author

@github-actions

github-actions Bot commented Aug 25, 2026

Copy link
Copy Markdown

Code size report:

Reference:  esp32/modmachine: Use popcount to count wake pins. [8cf130d]
Comparison: stm32/boards: Set the RTC prescalers for the LSI on Arduino boards. [merge of 5065be8]
  mpy-cross:    +0 +0.000% 
   bare-arm:    +0 +0.000% 
minimal x86:    +0 +0.000% 
   unix x64:    +0 +0.000% standard
      stm32:    +0 +0.000% PYBV10
      esp32:    +0 +0.000% ESP32_GENERIC
     mimxrt:    +0 +0.000% TEENSY40
        rp2:    +0 +0.000% RPI_PICO_W
       samd:    +0 +0.000% ADAFRUIT_ITSYBITSY_M4_EXPRESS
  qemu rv32:    +0 +0.000% VIRT_RV32

@dpgeorge

Copy link
Copy Markdown
Member

These boards do carry an accurate external 32768Hz oscillator (a SiT1532,
±20ppm) on OSC32_IN and configure MICROPY_HW_RTC_USE_BYPASS, and it does
work — switched to LSE bypass the RTC held ±0ppm on the bench. But it can't
be shipped: the boards' stock Arduino MCUboot bootloader (built with Mbed's
lse_available=false) reinitialises the RTC onto the LSI at every hard
reset when any other clock source is selected, and the backup-domain reset
that requires wipes the calendar.

Oh wow! That's an oversight. The SiT1532 is a really nice time source, so it's a shame it's not usable.

I guess it's not easy to update the MCUboot bootloader to configure it with LSE=true?

I think the config here should explicitly mention this limitation, and explicitly enable LSI. Suggest all Arduino boards use the following:

// There is an external 32kHz oscillator but the stock MCUboot bootloader is built
// with lse_available=false and resets the RTC if it's not using LSI.  So use LSI.
#define RTC_ASYNCH_PREDIV           (0)
#define RTC_SYNCH_PREDIV            (31999)
#define MICROPY_HW_RTC_USE_LSE      (0)
#define MICROPY_HW_RTC_USE_US       (1)
#define MICROPY_HW_RTC_USE_CALOUT   (1)

@kwagyeman

Copy link
Copy Markdown
Contributor Author

Yeah, it's quite bad. And we cannot update the bootloader, though. So, it's a locked-in issue. Updating...

The Arduino Giga, Nicla Vision, Opta and Portenta H7 run the RTC from
the ~32kHz LSI, but with the prescalers set for a 32768Hz source, so the
RTC keeps time about 2.3% slow -- over half an hour per day, far beyond
what rtc.calibration() can correct.  Divide by the LSI's nominal 32000
instead.  Measured on a Nicla Vision this takes the error from -22300ppm
to -684ppm, and the remaining part-specific offset is small enough that
rtc.calibration() can trim most of it.

These boards do have an accurate external 32768Hz oscillator (a SiT1532
wired to OSC32_IN, +-20ppm), but it cannot be used: the boards' stock
Arduino MCUboot bootloader (built with Mbed's lse_available=false)
reinitialises the RTC onto the LSI at every hard reset when it finds any
other clock source selected, and the backup-domain reset that requires
wipes the calendar.  Verified on a Nicla Vision -- with the RTC switched
to LSE bypass (which does start and keep +-0ppm time while running),
every machine.reset() and deep-sleep wake came back on the LSI with the
calendar reset to the bootloader's 2021-01-01 epoch.  The RTC config is
therefore now explicitly LSI (MICROPY_HW_RTC_USE_LSE disabled, replacing
the never-reachable MICROPY_HW_RTC_USE_BYPASS setting), with a comment
recording the limitation; with the sources agreeing the bootloader
leaves the RTC alone and the time survives reset and deep sleep.

Tested on an Arduino Nicla Vision: -684ppm over a 240s measurement
against the HSE-derived SysTick (previously -22300ppm measured over two
hours against NTP), time preserved through machine.deepsleep(20000) and
machine.reset(), and the 31.25us subsecond resolution still divides
exactly for the microsecond datetime fields.

Signed-off-by: Kwabena W. Agyeman <kwagyeman@live.com>
@kwagyeman
kwagyeman force-pushed the kwabena/arduino_rtc_prescaler branch from c94f67d to 5065be8 Compare August 26, 2026 01:52
@dpgeorge

Copy link
Copy Markdown
Member

And we cannot update the bootloader

Is it at all possible to update the bootloader, eg use mboot? (Just out of curiosity.)

@kwagyeman

Copy link
Copy Markdown
Contributor Author

No, it would need to be done via jtag which is not something you can count on users having. Arduino boards aren't meant to be flashed by the end user.

@dpgeorge

Copy link
Copy Markdown
Member

It might be possible to make a bootloader-updater app, eg running in MicroPython "user space" which rewrites the bootloader. We already have such code in ports/stm32/mboot/fwupdate.py:update_mboot().

Anyway, I'm not suggesting to do that, just noting that it could be an option if someone really wants to use the LSE.

@kwagyeman

Copy link
Copy Markdown
Contributor Author

Very dangerous. Could brick niclas. Wouldn't suggest.

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

Projects

Status: In progress

Development

Successfully merging this pull request may close these issues.

2 participants