Skip to content
Draft
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
27 changes: 23 additions & 4 deletions ports/espressif/common-hal/alarm/SleepMemory.c
Original file line number Diff line number Diff line change
Expand Up @@ -12,15 +12,34 @@
#include "shared-bindings/alarm/SleepMemory.h"

#include "esp_sleep.h"
#include "esp_system.h"

// Data storage for singleton instance of SleepMemory.
// Might be RTC_SLOW_MEM or RTC_FAST_MEM, depending on setting of CONFIG_ESP32S2_RTCDATA_IN_FAST_MEM.
static RTC_DATA_ATTR uint8_t _sleep_mem[SLEEP_MEMORY_LENGTH];
static RTC_NOINIT_ATTR uint8_t _sleep_mem[SLEEP_MEMORY_LENGTH];

void alarm_sleep_memory_reset(void) {
// ESP-IDF build system takes care of doing esp_sleep_pd_config() or the equivalent with
// the correct settings, depending on which RTC mem we are using.
// https://docs.espressif.com/projects/esp-idf/en/latest/esp32s2/api-reference/system/sleep_modes.html#power-down-of-rtc-peripherals-and-memories
// With RTC_NOINIT_ATTR, the bootloader does not initialize sleep memory.
// Preserve contents on resets where the RTC domain stays powered (software
// reset, watchdog, panic, deep sleep wake). Clear on everything else —
// after power-on, SRAM contents are undefined; after brown-out, the RTC
// domain was reset by hardware (System Reset scope per TRM Figure 6.1-1).
esp_reset_reason_t reason = esp_reset_reason();
switch (reason) {
case ESP_RST_SW: // microcontroller.reset() / esp_restart()
case ESP_RST_DEEPSLEEP: // deep sleep wake
case ESP_RST_PANIC: // unhandled exception
case ESP_RST_INT_WDT: // interrupt watchdog
case ESP_RST_TASK_WDT: // task watchdog
case ESP_RST_WDT: // other watchdog
// RTC domain was not reset — sleep memory is intact.
break;
default:
// Power-on, brown-out, unknown, or any other reason where
// RTC SRAM contents may be undefined. Clear to zero.
memset(_sleep_mem, 0, sizeof(_sleep_mem));
break;
}
}

uint32_t common_hal_alarm_sleep_memory_get_length(alarm_sleep_memory_obj_t *self) {
Expand Down
12 changes: 10 additions & 2 deletions shared-bindings/alarm/SleepMemory.c
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,17 @@
#include "shared-bindings/alarm/SleepMemory.h"

//| class SleepMemory:
//| """Store raw bytes in RAM that persists during deep sleep.
//| """Store raw bytes in RAM that persists across deep sleep and software resets.
//| The class acts as a ``bytearray``.
//| If power is lost, the memory contents are lost.
//| Contents are preserved across ``microcontroller.reset()``, watchdog resets,
//| and deep sleep wake. Contents are lost when power is removed and restored,
//| or on brown-out reset.
//|
//| .. note::
//| Programs that call ``microcontroller.reset()`` should wait at least
//| one second after boot before resetting, otherwise CircuitPython's
//| double-reset safe mode detector may activate. See
//| ``supervisor/shared/safe_mode.c``.
//|
//| Note that this class can't be imported and used directly. The sole
//| instance of :class:`SleepMemory` is available at
Expand Down
100 changes: 100 additions & 0 deletions tests/circuitpython-manual/alarm/sleep_memory_persist.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
"""Reproducer for https://github.com/adafruit/circuitpython/issues/10896

Copy to code.py. Sequences through test steps using sleep_memory to
track state.

The test scoreboard is printed to the magtag e-ink display after every
run so the final display shows cumulative results.
"""

import alarm
import binascii
import microcontroller
import struct
import supervisor
import time

# Safe mode avoidance
# On boot CircuitPython writes a SAFE_MODE_USER guard to an RTC register,
# If a second boot occurs before 1000ms elapses then we enter safe mode.
# So, wait for 1000+1ms before calling microcontroller.reset().
_SAFE_MODE_WINDOW_MS = 1000


def _wait_for_safe_mode_window():
elapsed_ms = time.monotonic() * 1000
remaining = _SAFE_MODE_WINDOW_MS + 1 - elapsed_ms
if remaining > 0:
time.sleep(remaining / 1000)


# Test result enum
_UNTESTED = 0
_PASS = 1
_FAIL = 2
_LABEL = {_UNTESTED: "-", _PASS: "PASS", _FAIL: "FAIL"}

# CRC32-protected state in sleep_memory
# [magic:2][step:1][r_reset:1][r_reload:1][pad:1][crc32:4]
_MAGIC = 0xBE01
_FMT = "<HBBBx"
_DATA_SZ = struct.calcsize(_FMT) # 6
_TOTAL = _DATA_SZ + 4 # 10


def _write(step, r_reset=_UNTESTED, r_reload=_UNTESTED):
data = struct.pack(_FMT, _MAGIC, step, r_reset, r_reload)
crc = struct.pack("<I", binascii.crc32(data) & 0xFFFFFFFF)
alarm.sleep_memory[0:_TOTAL] = data + crc


def _read():
raw = bytes(alarm.sleep_memory[0:_TOTAL])
data = raw[:_DATA_SZ]
stored_crc = struct.unpack_from("<I", raw, _DATA_SZ)[0]
if (binascii.crc32(data) & 0xFFFFFFFF) != stored_crc:
return (0, _UNTESTED, _UNTESTED, False)
magic, step, r_reset, r_reload = struct.unpack(_FMT, data)
if magic != _MAGIC:
return (0, _UNTESTED, _UNTESTED, False)
return (step, r_reset, r_reload, True)


def _scoreboard(r_reset, r_reload):
print(f" reset: {_LABEL[r_reset]}")
print(f" reload: {_LABEL[r_reload]}")


# Main
reason = microcontroller.cpu.reset_reason
step, r_reset, r_reload, valid = _read()

if reason == microcontroller.ResetReason.POWER_ON:
_scoreboard(_UNTESTED, _UNTESTED)
print("writing marker, resetting...")
_write(0)
_wait_for_safe_mode_window()
microcontroller.reset()

elif valid and step == 0:
r_reset = _PASS
_scoreboard(r_reset, _UNTESTED)
print("testing supervisor.reload()...")
_write(1, r_reset)
_wait_for_safe_mode_window()
supervisor.reload()

elif not valid and step == 0:
r_reset = _FAIL
_scoreboard(r_reset, _UNTESTED)
print("Install patched FW, power cycle")

elif valid and step == 1:
r_reload = _PASS
_scoreboard(r_reset, r_reload)
_write(2, r_reset, r_reload)

else:
_scoreboard(r_reset, r_reload)
print(f"unexpected: step={step} valid={valid}")
print("Power cycle to restart tests")
Loading