Skip to content

ports: Add RTC.memory() backed by hardware registers.#19084

Open
andrewleech wants to merge 10 commits into
micropython:masterfrom
andrewleech:rtc-memory
Open

ports: Add RTC.memory() backed by hardware registers.#19084
andrewleech wants to merge 10 commits into
micropython:masterfrom
andrewleech:rtc-memory

Conversation

@andrewleech
Copy link
Copy Markdown
Contributor

Summary

Adds RTC.memory() to mimxrt, stm32, rp2, and alif ports. On read it returns a writable uint32 memoryview backed directly by the hardware registers / memory, so users get direct register access, slicing, and uctypes integration without any copying. On write, RTC.memory(data) does a word-aligned bulk write that only touches the registers covered by the input data.

Each port auto-sizes to the available hardware: SNVS LPGPR on mimxrt (16-32 bytes), RTC backup registers on stm32 (20-128 bytes), watchdog scratch on rp2 (32 bytes), and 4KB battery-backed SRAM on alif. Boards can override MICROPY_HW_RTC_USER_MEM_MAX if needed.

Some registers are used by system firmware (UF2 bootloader on mimxrt, CLK_LAST_FREQ / mboot / rfcore on stm32, watchdog_reboot on rp2). These are documented in the RST but not hidden, the full register array is always exposed so users can address specific registers for purposes like mboot status reporting or bootloader flags.

On rp2 the watchdog scratch registers aren't battery-backed, data only survives soft resets.

Testing

Tested on hardware across all four ports:

  • MIMXRT1010-EVK (RT1011, 4 LPGPR, no UF2): 16 bytes, all tests pass
  • Seeed ARCH MIX (RT1052, 8 LPGPR, UF2): 32 bytes, all tests pass
  • NUCLEO-F429ZI (STM32F4, 20 BKP regs): 80 bytes, all tests pass
  • NUCLEO-H563ZI (STM32H5/TAMP, 32 BKP regs): 128 bytes, all tests pass
  • NUCLEO-WB55 (STM32WB, 20 BKP regs): 80 bytes, all tests pass
  • Pico 2 W (RP2350, 8 scratch regs): 32 bytes, all tests pass
  • OpenMV AE3 (Alif AE722, 4KB backup SRAM): 4096 bytes, all tests pass

Tests cover direct memoryview register write/read, bulk write with untouched register preservation, partial trailing word read-modify-write, buffer-too-long ValueError, and persistence across soft reset.

Also build-tested on MIMXRT1170_EVK, NUCLEO_G0B1RE, NUCLEO_H7A3ZI_Q, and PYBV10.

Trade-offs and Alternatives

The read path returns a uint32 memoryview rather than bytes (as esp32/esp8266 do) or a bytearray. A bytes copy is safe but doesn't allow direct register writes. A bytearray by reference allows byte-level access but STM32 backup registers require 32-bit aligned writes, so byte stores through the bytearray silently corrupt data. The uint32 memoryview forces word-aligned access at the type level which matches the hardware constraint, and works well with uctypes for structured register layouts.

The full register array is exposed including system-reserved locations, rather than hiding them with skip logic (an earlier revision did skip LPGPR[3] on mimxrt for the UF2 bootloader). Hiding registers creates confusing offset mapping when users need to address specific registers for things like mboot status or bootloader flags, and the same approach would be impractical on stm32 where reserved registers are scattered across the array.

The write path only modifies registers covered by the input data rather than zero-filling the remainder. This is important when the register space is shared with system firmware, a bulk write of a couple of bytes shouldn't clobber the clock frequency register at the other end of the array.

The esp32/esp8266 ports still return bytes with length tracking. Updating those to match the memoryview approach would be a separate change.

Closes #18960.

Generative AI

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

@github-actions
Copy link
Copy Markdown

github-actions Bot commented Apr 11, 2026

Code size report:

Reference:  tools/mpremote: Add tests for array readinto and write. [670d775]
Comparison: github/workflows: Upload 32-bit unix coverage to codecov. [merge of 5d38d73]
  mpy-cross:   +32 +0.008% 
   bare-arm:    +0 +0.000% 
minimal x86:    +0 +0.000% 
   unix x64:  +248 +0.029% standard[incl +32(data)]
      stm32:  +228 +0.057% PYBV10
      esp32:  +140 +0.008% ESP32_GENERIC[incl +64(data)]
     mimxrt:  +112 +0.029% TEENSY40
        rp2:  +128 +0.014% RPI_PICO_W
       samd:  +120 +0.043% ADAFRUIT_ITSYBITSY_M4_EXPRESS
  qemu rv32:  +105 +0.023% VIRT_RV32

@andrewleech
Copy link
Copy Markdown
Contributor Author

This would be cleaner as a new api machine.backup_memory which directly exposes the memoryview directly.
When this is enabled it should enable the item_size attribute for memoryview too so that users can check both len (number of entries) and item_size (size of each entry) that gives all the detail.

@dpgeorge
Copy link
Copy Markdown
Member

See related #7133.

@kwagyeman
Copy link
Copy Markdown
Contributor

kwagyeman commented Apr 17, 2026

@andrewleech - Please use the STM32 backup RAM versus the RTC register. You should aim to use the largest possible backup-related memory on the device (also initialize the clocks, etc.).

The IMXRT doesn't have a lot of backup RAM, so, it's the odd duck here. But, Alif and ST do.

For the IMXRT, you are correct though with that those registers have to be accessed as 32-bit at a time. Byte writes don't work.

@kwagyeman
Copy link
Copy Markdown
Contributor

kwagyeman commented Apr 17, 2026

Here's usign the backup RAM (4KB) on the STM32H7. The N6 has 8KB.
h7_backupram.py

Here's how I contolled the backup ram (32-bytes on the RT1062)
rt1062_backupram.py

@andrewleech
Copy link
Copy Markdown
Contributor Author

andrewleech commented Apr 19, 2026

I've reworked this PR as machine.backup_memory, a module-level singleton memoryview (like machine.mem32). The implementation is shared in extmod (~50 lines), each port just provides four config macros.

Thanks @kwagyeman for the suggestion to use STM32 backup SRAM instead of the BKP registers. The stm32 commit now auto-detects BKPSRAM availability at compile time via the CMSIS header macros. On F4/F7/H5/H7/U5/N6 this gives 4-8KB of byte-addressable battery-backed SRAM (itemsize=1). Families without BKPSRAM (L0, L1, L4, G0, G4, WB, WL) fall back to the RTC BKP registers with word-level access (itemsize=4). BKPSRAM clock enable and backup regulator init are done during boot after rtc_init_start().

The Alif backup SRAM at 0x4902C000 turned out to be in peripheral space, not real SRAM, so byte writes don't work there either. It's exposed as itemsize=4 with a comment explaining why.

During hardware testing I found a pre-existing issue in py/binary.c where both mp_binary_set_val_array (memoryview/array subscript) and mp_binary_set_val (struct.pack_into) use mp_obj_int_to_bytes_impl for big-int values, which writes individual bytes via mpz_as_bytes. On 32-bit targets any value >= 0x40000000 takes this path. The byte-wise stores silently corrupt word-only hardware registers.

The last commit fixes both functions by routing big-int values through mp_obj_int_get_truncated and then through the same typed-store paths that small-int values already use. This doesn't change behaviour on normal RAM since both approaches produce identical bytes, it just uses the more natural C typed store instead of an unnecessary byte-decomposition loop. Also slightly faster since it skips the mpz byte extraction. The byte-wise path is only retained for element sizes exceeding mp_int_t (e.g. int64 on 32-bit). There's a test that confirms the failure on NUCLEO-WB55 before the fix, and passes after.

Changes from the original PR:

  • machine.backup_memory replaces per-port RTC.memory() on mimxrt, stm32, rp2, alif
  • Static const singleton, no heap allocation on access
  • memoryview.itemsize auto-enabled so users can discover access granularity
  • ESP32 gets machine.backup_memory alongside existing RTC.memory() (not removed)
  • STM32 uses BKPSRAM where available (4-8KB), BKP registers on the rest
  • Per-port itemsize: 4 (word-only registers) or 1 (byte-addressable SRAM)

Original commits preserved at backup/rtc-memory-v1.

Tested on:

  • STM32H747I-DISCO (H7, BKPSRAM 4KB, itemsize=1): pass, persistence across soft_reset confirmed
  • PYBD-SF6 (F767, BKPSRAM 4KB, itemsize=1): pass
  • NUCLEO-WB55 (WB55, BKP regs 80B, itemsize=4): pass, including big-int regression test
  • Pico 2 W (RP2350, 8 scratch regs, 32B, itemsize=4): pass
  • OpenMV AE3 (Alif, 4KB backup SRAM, itemsize=4): pass, both subscript and struct.pack_into paths
  • MIMXRT1010-EVK (4 LPGPR, 16B, itemsize=4): pass

On H7 and N6, BKPSRAM falls in the 0x20000000-0x3FFFFFFF SRAM region which is cacheable by default, so an MPU region marks it non-cacheable during init. This was validated on the H747I-DISCO. Other families (F4, F7, H5, U5) have BKPSRAM in the peripheral address space where caching isn't an issue.

@codecov
Copy link
Copy Markdown

codecov Bot commented Apr 19, 2026

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 98.51%. Comparing base (8a56be6) to head (5d38d73).
⚠️ Report is 159 commits behind head on master.

Additional details and impacted files
@@            Coverage Diff             @@
##           master   #19084      +/-   ##
==========================================
+ Coverage   98.46%   98.51%   +0.05%     
==========================================
  Files         176      178       +2     
  Lines       22811    23074     +263     
==========================================
+ Hits        22460    22731     +271     
+ Misses        351      343       -8     
Flag Coverage Δ
unix-coverage-32bit 98.51% <100.00%> (?)
unix-coverage-64bit 98.45% <55.55%> (?)

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@andrewleech andrewleech force-pushed the rtc-memory branch 2 times, most recently from d9fc7c1 to 8b0a588 Compare April 19, 2026 05:26
@kwagyeman
Copy link
Copy Markdown
Contributor

@andrewleech - Need an N6/H7? Email me.

@andrewleech
Copy link
Copy Markdown
Contributor Author

@andrewleech - Need an N6/H7? Email me.

Thanks for the offer! I'll get my openmv N6 back on Tuesday, I just loaned it for testing some other image quality stuff at work.
I've got a H7 disco board I'll be able to test with too, just need to get the test environment set up for it.

@andrewleech andrewleech force-pushed the rtc-memory branch 4 times, most recently from a7c7e6a to c2e1b6d Compare April 19, 2026 13:39
@robert-hh
Copy link
Copy Markdown
Contributor

I tried that with a Arch Mix board and battery backup. Works in that the content survives a power off period, when a battery is connected. A few questions:

  • there is a mem.hex() method, which seems to dump the backup memory content. Maybe included just for testing.
  • len(mem) should probably return the lent of the memory in bytes? Or is it the amount of the memory items? For the tested device, the situations is confusing. The data sheet shows of 4 backup registers. The symbol SNVS_LPGPR_COUNT of the library has the value 8. len(mem) reports 8. mem[4] is writable and keeps it's value during a power cycle, so it seems that the data sheet is wrong.

@robert-hh
Copy link
Copy Markdown
Contributor

robert-hh commented Apr 20, 2026

Note: The SAMD51 RTC has 8 words of backup memory as well, which survives hard reset. Address 0x40002480, or &RTC->MODE2.BKUP[0].reg. And it has 8k backup memory at Address 0x47000000, symbol BKUPRAM_ADDR.

edit: A suitable code snippet for samd/mcu/samd51/mpconfigmcu following the style of the other patches would be:

#ifndef MICROPY_PY_MACHINE_BACKUP_MEMORY
#define MICROPY_PY_MACHINE_BACKUP_MEMORY        (1)
#define MICROPY_HW_BACKUP_MEMORY_BYTES          (BKUPRAM_SIZE)
#define MICROPY_HW_BACKUP_MEMORY_ITEMSIZE       (1)
#define MICROPY_HW_BACKUP_MEMORY_ADDR           ((void *)BKUPRAM_ADDR)
#endif

Tested with ITSYBITSY_M4 board.

@andrewleech andrewleech force-pushed the rtc-memory branch 2 times, most recently from 39d543f to 3c8d730 Compare April 20, 2026 22:44
@andrewleech
Copy link
Copy Markdown
Contributor Author

Thanks for testing @robert-hh, good to hear it survives power cycling with battery backup.

On your questions:

mem.hex() is just the standard memoryview.hex() method, not something added by this PR. It returns a hex string of the raw buffer contents, same as bytes(mem).hex().

len(mem) returns the number of elements, so with itemsize=4 and 8 LPGPR registers that's 8 elements (32 bytes total). You can get the byte count with len(mem) * mem.itemsize. The RT1052 does have 8 LPGPR in the CMSIS headers (SNVS_LPGPR_COUNT = 8) even though some datasheets only document 4, so the SDK definition and your test results are consistent.

Re SAMD51, I've added it as a new commit using your suggested config with the 8KB backup RAM. Builds clean on ADAFRUIT_ITSYBITSY_M4_EXPRESS though I don't have hardware to test on, so it'd be great if you could verify it on your board.

@robert-hh
Copy link
Copy Markdown
Contributor

robert-hh commented Apr 21, 2026

I don't have hardware to test on, so it'd be great if you could verify it on your board.

Thanks. I tested that before posting. Tested again with your last commit. The content of the backup memory survives hard reset. Since no board I have has the MCU battery pin exposed, testing with a small backup battery like a coin cell is not possible. The battery pin on the boards is designed to completely feed the board.
I have a question on style. You bracket every setting item with #ifndef. But the hardware usually does not change and the values for the set-up are defined for most MCUs in the header files. So it seems to be sufficient to make the whole block only depend on MICROPY_PY_MACHINE_BACKUP_MEMORY-

@andrewleech
Copy link
Copy Markdown
Contributor Author

Thanks @robert-hh for the feedback. I'd originally thought boards might want to override individual values to avoid certain registers used by bootloaders etc, but on reflection it's probably better for the app to just avoid those as needed. I've simplified all ports to just gate the block on MICROPY_PY_MACHINE_BACKUP_MEMORY (which boards can still set to 0 to disable the feature). The ESP32 config already did it this way.

Thanks for retesting on SAMD51.

Comment thread docs/library/machine.rst Outdated

.. note::

On esp32 and rp2, data persists across soft resets but is lost on
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

From #17521 -

For, now I am suggesting that the docs point out that EN will clear RTC memory, just a heads-up for beginners.

[Nice RAG tool Andrew ]

@andrewleech
Copy link
Copy Markdown
Contributor Author

Got the N6 BKPSRAM tested via OpenMV N6 (STM32N657X0), 44/44 pass. Byte writes, struct.pack_into across the full uint32 range, uctypes overlays, persistence across soft_reset, all good.

Couple of things this confirmed:

The MPU non-cacheable region works correctly on Cortex-M55, not just M7. Writes reach BKPSRAM and survive soft_reset.

N6 BKPSRAM is real byte-addressable SRAM so itemsize=1 works fine, no sub-word write hazard like we saw on Alif. The Alif region at 0x4902C000 is in peripheral space and zeros unaddressed bytes of the containing word, which is why those two ports use different itemsize values despite both being called "backup SRAM" on paper.

The auto-detect in ports/stm32/backup_memory.h picked the right BKPSRAM_BASE_NS fallback for N6 since it has no unsuffixed alias.

Full hardware test list now:

  • STM32H747I-DISCO (H7, BKPSRAM 4KB, itemsize=1): pass, persistence confirmed
  • OpenMV N6 (N657, BKPSRAM 8KB, itemsize=1): pass, 44/44
  • PYBD-SF6 (F767, BKPSRAM 4KB, itemsize=1): pass
  • NUCLEO-WB55 (WB55, BKP regs 80B, itemsize=4): pass, including big-int regression test
  • Pico 2 W (RP2350, 8 scratch regs, 32B, itemsize=4): pass
  • OpenMV AE3 (Alif, 4KB backup SRAM, itemsize=4): pass, 28/28
  • MIMXRT1010-EVK (4 LPGPR, 16B, itemsize=4): pass

Comment thread py/mpconfig.h Outdated
// Whether to support memoryview.itemsize attribute
#ifndef MICROPY_PY_BUILTINS_MEMORYVIEW_ITEMSIZE
#define MICROPY_PY_BUILTINS_MEMORYVIEW_ITEMSIZE (MICROPY_CONFIG_ROM_LEVEL_AT_LEAST_EVERYTHING)
#define MICROPY_PY_BUILTINS_MEMORYVIEW_ITEMSIZE (MICROPY_PY_MACHINE_BACKUP_MEMORY || MICROPY_CONFIG_ROM_LEVEL_AT_LEAST_EVERYTHING)
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggest changing this to MICROPY_CONFIG_ROM_LEVEL_AT_LEAST_BASIC_FEATURES. It's a pretty basic feature, and since 6443130 mpremote will take advantage of it.

pi-anl added 10 commits May 9, 2026 14:13
Add a static const memoryview object that exposes battery-backed or
persistent hardware memory as machine.backup_memory. Each port
provides four config macros (enable, byte count, itemsize, address)
and the shared implementation handles the rest.

Automatically enables MICROPY_PY_BUILTINS_MEMORYVIEW_ITEMSIZE so
users can discover access granularity at runtime.

Signed-off-by: Andrew Leech <[email protected]>
Expose SNVS LP General Purpose Registers as machine.backup_memory
with word-level access (itemsize=4). Register count varies by chip
(4 on RT1011/RT1176, 8 on RT1015/1021/1052/1062/1064).

Signed-off-by: Andrew Leech <[email protected]>
On families with dedicated backup SRAM (F4, F7, H5, H7, U5, N6),
expose 4-8 KB of byte-addressable battery-backed SRAM as
machine.backup_memory with itemsize=1. The BKPSRAM clock and
backup regulator are enabled during boot after RTC init. On H7
and N6, an MPU region marks the BKPSRAM non-cacheable since its
address falls in the default-cacheable SRAM range.

On families without BKPSRAM (L0, L1, L4, G0, G4, WB, WL), fall
back to RTC backup registers (BKPxR / TAMP BKPxR) with word-level
access (itemsize=4, 20-128 bytes). Gated on MICROPY_HW_ENABLE_RTC.

Signed-off-by: Andrew Leech <[email protected]>
Expose the 8 watchdog scratch registers as machine.backup_memory
with word-level access (itemsize=4, 32 bytes). Data persists
across soft resets but not power-off (no battery backing).

Signed-off-by: Andrew Leech <[email protected]>
Expose the 4KB battery-backed backup SRAM at 0x4902C000 as
machine.backup_memory with word-level access (itemsize=4). The
region lives in peripheral space and does not support sub-word
writes, so the memoryview is exposed as uint32 to enforce
word-aligned access from Python.

Signed-off-by: Andrew Leech <[email protected]>
Expose the RTC user memory (2048 bytes default) as
machine.backup_memory with byte-level access alongside the
existing RTC.memory() method. The two APIs share the same
backing buffer but have independent semantics: backup_memory
is a raw memoryview while RTC.memory() tracks written length.

Signed-off-by: Andrew Leech <[email protected]>
Expose the 8KB backup RAM at 0x47000000 as machine.backup_memory
with byte-level access (itemsize=1) on SAMD51 boards.

Signed-off-by: Andrew Leech <[email protected]>
Add documentation for the machine.backup_memory memoryview attribute
including per-port storage sizes, reserved register table, uctypes
integration example, and availability.

Signed-off-by: Andrew Leech <[email protected]>
When writing a big-int value to an array, memoryview, or struct
buffer, mp_binary_set_val_array and mp_binary_set_val previously
used mp_obj_int_to_bytes_impl which writes individual bytes. On
hardware registers and peripheral-backed memory that only supports
word-sized stores (e.g. STM32 RTC backup registers, NXP SNVS
LPGPR, RP2 watchdog scratch), byte-wise writes silently corrupt
the target word.

For element sizes that fit in mp_int_t, route big-int values
through mp_obj_int_get_truncated and then through the same typed
store paths that small-int values already use. This is also
slightly faster since it avoids the byte decomposition loop in
mpz_as_bytes. The byte-wise path is retained for element sizes
exceeding mp_int_t (e.g. int64 on 32-bit targets).

The bug only manifests on 32-bit targets where values >= 0x40000000
exceed the small-int range and the destination rejects sub-word
stores. On 64-bit hosts all uint32 values are small ints and
already take the typed-store path.

Signed-off-by: Andrew Leech <[email protected]>
The coverage_32bit job was building with coverage flags but never
running gcov or uploading to codecov, so all reported coverage was
from the 64-bit build only.  Add the same gcov + codecov-action
steps that the 64-bit job has, with distinct flags so codecov can
merge the data correctly.

This closes coverage gaps in code paths that only execute on 32-bit
targets (e.g. py/binary.c byte-write fallbacks for typecodes where
size > sizeof(mp_int_t)).

Signed-off-by: Andrew Leech <[email protected]>
@kwagyeman
Copy link
Copy Markdown
Contributor

@dpgeorge - Any progress on this?

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

Projects

None yet

Development

Successfully merging this pull request may close these issues.

OpenMV Feature: RTC.memory()

6 participants