Skip to content
Merged
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
83 changes: 73 additions & 10 deletions ports/zephyr-cp/supervisor/port.c
Original file line number Diff line number Diff line change
Expand Up @@ -22,8 +22,10 @@

#if defined(CONFIG_ARCH_POSIX)
#include <limits.h>
#include <fcntl.h>

#include "cmdline.h"
#include "nsi_host_trampolines.h"
#include "posix_board_if.h"
#include "posix_native_task.h"
#endif
Expand Down Expand Up @@ -65,7 +67,13 @@ static struct k_timer tick_timer;
static int32_t native_sim_vm_runs = INT32_MAX;
static uint32_t native_sim_reset_port_count = 0;

static struct args_struct_t native_sim_reset_port_args[] = {
// Path to a file used to preserve retained memory across the execv reboot, or
// NULL if disabled. Set with --retained-memory=<path> (see
// cp_saved_word_save/restore()). Currently persists the safe-mode saved word;
// intended to also preserve sleep RAM in the future.
static const char *native_sim_retained_memory;

static struct args_struct_t native_sim_port_args[] = {
{
.option = "vm-runs",
.name = "count",
Expand All @@ -74,11 +82,20 @@ static struct args_struct_t native_sim_reset_port_args[] = {
.descript = "Exit native_sim after this many VM runs. "
"Example: --vm-runs=2"
},
{
.option = "retained-memory",
.name = "path",
.type = 's',
.dest = (void *)&native_sim_retained_memory,
.descript = "File used to preserve retained memory (e.g. the safe-mode "
"saved word) across the process re-exec reboot. "
"Example: --retained-memory=/tmp/cp_retained.bin"
},
ARG_TABLE_ENDMARKER
};

static void native_sim_register_cmdline_opts(void) {
native_add_command_line_opts(native_sim_reset_port_args);
native_add_command_line_opts(native_sim_port_args);
}

NATIVE_TASK(native_sim_register_cmdline_opts, PRE_BOOT_1, 0);
Expand Down Expand Up @@ -135,7 +152,56 @@ static void _tick_function(struct k_timer *timer_id) {
supervisor_tick();
}

// Save and retrieve a word from memory that is preserved over reset. Used for safe mode.
static __noinit uint32_t cp_saved_word;

void port_set_saved_word(uint32_t value) {
cp_saved_word = value;
}

uint32_t port_get_saved_word(void) {
return cp_saved_word;
}

// Save and restore retained memory across the native_sim/bsim reboot.
// Opt in with --retained-memory=<path>.
#if defined(CONFIG_ARCH_POSIX)
static void cp_saved_word_save(void) {
const char *path = native_sim_retained_memory;
if (path == NULL || path[0] == '\0') {
return;
}
int fd = nsi_host_open(path, O_WRONLY | O_CREAT | O_TRUNC, 0644);
if (fd < 0) {
return;
}
uint32_t value = cp_saved_word;
(void)nsi_host_write(fd, &value, sizeof(value));
(void)nsi_host_close(fd);
}

static void cp_saved_word_restore(void) {
const char *path = native_sim_retained_memory;
if (path == NULL || path[0] == '\0') {
return;
}
int fd = nsi_host_open(path, O_RDONLY, 0 /* unused */);
if (fd < 0) {
return; // First boot: no save file yet.
}
uint32_t value = 0;
(void)nsi_host_read(fd, &value, sizeof(value));
(void)nsi_host_close(fd);
cp_saved_word = value;
}
#endif

safe_mode_t port_init(void) {
#if defined(CONFIG_ARCH_POSIX)
// Restore the saved word (if any) before wait_for_safe_mode_reset reads it.
cp_saved_word_restore();
#endif

// We run CircuitPython at the lowest priority (just higher than idle.)
// This allows networking and USB to preempt us.
k_thread_priority_set(k_current_get(), CONFIG_NUM_PREEMPT_PRIORITIES - 1);
Expand All @@ -146,6 +212,11 @@ safe_mode_t port_init(void) {

// Reset the microcontroller completely.
void reset_cpu(void) {
#if defined(CONFIG_ARCH_POSIX)
// Persist the saved word across the process re-exec reboot.
cp_saved_word_save();
#endif

// Try a warm reboot first. It won't return if it works but isn't always
// implemented.
sys_reboot(SYS_REBOOT_WARM);
Expand Down Expand Up @@ -206,14 +277,6 @@ uint32_t *port_stack_get_top(void) {
return (uint32_t *)(stack_info.start + stack_info.size - stack_info.delta);
}

// Save and retrieve a word from memory that is preserved over reset. Used for safe mode.
void port_set_saved_word(uint32_t) {

}
uint32_t port_get_saved_word(void) {
return 0;
}

uint64_t port_get_raw_ticks(uint8_t *subticks) {
// Make sure time advances in the simulator.
#if defined(CONFIG_ARCH_POSIX)
Expand Down
37 changes: 37 additions & 0 deletions ports/zephyr-cp/tests/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -138,6 +138,43 @@ def __init__(self, cmd, timeout=5, trace_file=None, env=None, flash_file=None):
self.debug_serial = SerialSaver(
StdSerial(self._proc.stdin, self._proc.stdout), name="debug"
)
# Offset into debug_serial output for finding the next UART PTY path.
self._pty_search_offset = 0

def reconnect_serial(self, timeout=30.0):
"""Wait for the simulator to reboot (execv) and reopen the UART.

native_sim/bsim reboot by re-executing the process; the UART PTY master
fd is O_CLOEXEC so it closes on execv and a new PTY is opened. The new
"connected to pseudotty: <path>" line is printed to the process stdout
(which survives execv), so it shows up in debug_serial. This method waits
for that line and reopens ``self.serial`` on the new PTY.
"""
import time

marker = "connected to pseudotty:"
deadline = time.monotonic() + timeout
while time.monotonic() < deadline:
if self._proc.poll() is not None:
return False
out = self.debug_serial.all_output
idx = out.find(marker, self._pty_search_offset)
newline = out.find("\n", idx) if idx >= 0 else -1
if idx >= 0 and newline >= 0:
line = out[idx:newline]
pty_path = line.strip().rsplit(":", maxsplit=1)[1].strip()
self._pty_search_offset = newline + 1
try:
self.serial.close()
except Exception:
pass
self.serial = SerialSaver(
serial.Serial(pty_path, baudrate=115200, timeout=0.05, write_timeout=0),
name="uart0",
)
return True
time.sleep(0.05)
return False

def shutdown(self):
if self._proc.poll() is None:
Expand Down
4 changes: 4 additions & 0 deletions ports/zephyr-cp/tests/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -342,6 +342,10 @@ def circuitpython(request, board, sim_id, native_sim_binary, native_sim_env, tmp
)
)

# Always preserve retained memory (e.g. the safe-mode saved word) in
# in case of reboot.
cmd.append(f"--retained-memory={tmp_path / f'retained-{i}.bin'}")

if flash_erase_block_size is not None:
cmd.append(f"--flash_erase_block_size={flash_erase_block_size}")
if flash_write_block_size is not None:
Expand Down
42 changes: 42 additions & 0 deletions ports/zephyr-cp/tests/test_saved_word.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
# SPDX-FileCopyrightText: 2025 Scott Shawcroft for Adafruit Industries
# SPDX-License-Identifier: MIT

"""Test that the safe-mode saved word survives a hard reboot on native_sim/bsim.

native_sim and the bsim boards reboot by re-executing the process (execv),
which wipes RAM (including .noinit). supervisor/port.c works around that by
persisting retained memory to a file across the reboot (the --retained-memory
flag); currently that holds the safe-mode saved word.

The saved word drives safe-mode detection: ``microcontroller.on_next_reset(
RunMode.SAFE_MODE)`` arms the sentinel in the saved word, and a subsequent
``microcontroller.reset()`` reboots. If the word persisted, the next boot reads
the sentinel and enters safe mode (printed as "Running in safe mode!").
"""

import pytest


SAFE_MODE_RESET_CODE = """\
import microcontroller
microcontroller.on_next_reset(microcontroller.RunMode.SAFE_MODE)
print("resetting")
microcontroller.reset()
"""


@pytest.mark.circuitpy_drive({"code.py": SAFE_MODE_RESET_CODE})
@pytest.mark.duration(30)
def test_saved_word_survives_reboot_into_safe_mode(circuitpython):
"""The saved word persists across a hard reboot and triggers safe mode."""
circuitpython.serial.wait_for("resetting", timeout=20)

# microcontroller.reset() re-execs the process; the UART PTY is O_CLOEXEC so
# a new one is opened after reboot. Reconnect to it.
assert circuitpython.reconnect_serial(timeout=20), "simulator did not reboot"

# The next boot restores the saved word (the SAFE_MODE sentinel) and enters
# safe mode instead of running code.py.
circuitpython.serial.wait_for("Running in safe mode", timeout=20)

assert "Running in safe mode" in circuitpython.serial.all_output
2 changes: 1 addition & 1 deletion ports/zephyr-cp/zephyr-config/west.yml
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,6 @@ manifest:
path: modules/bsim_hw_models/nrf_hw_models
- name: zephyr
url: https://github.com/adafruit/zephyr
revision: 62e7a3764b652fff733cee43f23f82217403a51d
revision: 499310df6e4da5ca54165c4b4c4c714771acb84c
clone-depth: 100
import: true
Loading