diff --git a/ports/zephyr-cp/boards/adafruit/feather_nrf52840_zephyr/autogen_board_info.toml b/ports/zephyr-cp/boards/adafruit/feather_nrf52840_zephyr/autogen_board_info.toml index 1672ab0b461c7..762f1166996da 100644 --- a/ports/zephyr-cp/boards/adafruit/feather_nrf52840_zephyr/autogen_board_info.toml +++ b/ports/zephyr-cp/boards/adafruit/feather_nrf52840_zephyr/autogen_board_info.toml @@ -73,7 +73,7 @@ microcontroller = true mipidsi = false msgpack = false neopixel_write = false -nvm = false +nvm = true # Zephyr board has nvm onewireio = false os = true paralleldisplaybus = false diff --git a/ports/zephyr-cp/boards/adafruit_feather_nrf52840_uf2.overlay b/ports/zephyr-cp/boards/adafruit_feather_nrf52840_uf2.overlay index a61cbf2047de2..e01ebd6df1f51 100644 --- a/ports/zephyr-cp/boards/adafruit_feather_nrf52840_uf2.overlay +++ b/ports/zephyr-cp/boards/adafruit_feather_nrf52840_uf2.overlay @@ -17,6 +17,28 @@ /delete-node/ partitions; }; +/delete-node/ &storage_partition; +/delete-node/ &code_partition; + +&flash0 { + partitions { + code_partition: partition@26000 { + label = "Application"; + reg = <0x00026000 0x000c4000>; + }; + + storage_partition: partition@ea000 { + label = "storage"; + reg = <0x000ea000 0x00008000>; + }; + + nvm_partition: partition@f2000 { + label = "nvm"; + reg = <0x000f2000 0x00002000>; + }; + }; +}; + &uart0 { status = "okay"; }; diff --git a/ports/zephyr-cp/boards/native/native_sim/autogen_board_info.toml b/ports/zephyr-cp/boards/native/native_sim/autogen_board_info.toml index 51a9f1b347621..7224381ec28e4 100644 --- a/ports/zephyr-cp/boards/native/native_sim/autogen_board_info.toml +++ b/ports/zephyr-cp/boards/native/native_sim/autogen_board_info.toml @@ -73,7 +73,7 @@ microcontroller = true mipidsi = false msgpack = false neopixel_write = false -nvm = false +nvm = true # Zephyr board has nvm onewireio = false os = true paralleldisplaybus = false diff --git a/ports/zephyr-cp/boards/native_sim.overlay b/ports/zephyr-cp/boards/native_sim.overlay index 2a07108627fc9..aee9d17f9d099 100644 --- a/ports/zephyr-cp/boards/native_sim.overlay +++ b/ports/zephyr-cp/boards/native_sim.overlay @@ -31,7 +31,12 @@ circuitpy_partition: partition@0 { label = "circuitpy"; - reg = <0x00000000 DT_SIZE_K(2048)>; + reg = <0x00000000 DT_SIZE_K(2040)>; + }; + + nvm_partition: partition@1fe000 { + label = "nvm"; + reg = <0x001fe000 0x00002000>; }; }; }; diff --git a/ports/zephyr-cp/common-hal/microcontroller/__init__.c b/ports/zephyr-cp/common-hal/microcontroller/__init__.c index 6ebf5f4c36818..be33cd26c9ace 100644 --- a/ports/zephyr-cp/common-hal/microcontroller/__init__.c +++ b/ports/zephyr-cp/common-hal/microcontroller/__init__.c @@ -11,7 +11,7 @@ #include "common-hal/microcontroller/Pin.h" #include "common-hal/microcontroller/Processor.h" -// #include "shared-bindings/nvm/ByteArray.h" +#include "shared-bindings/nvm/ByteArray.h" #include "shared-bindings/microcontroller/__init__.h" #include "shared-bindings/microcontroller/Pin.h" #include "shared-bindings/microcontroller/Processor.h" @@ -93,14 +93,12 @@ const mcu_processor_obj_t common_hal_mcu_processor_obj = { }, }; -#if CIRCUITPY_NVM && CIRCUITPY_INTERNAL_NVM_SIZE > 0 +#if CIRCUITPY_NVM // The singleton nvm.ByteArray object. const nvm_bytearray_obj_t common_hal_mcu_nvm_obj = { .base = { .type = &nvm_bytearray_type, }, - .start_address = (uint8_t *)CIRCUITPY_INTERNAL_NVM_START_ADDR, - .len = CIRCUITPY_INTERNAL_NVM_SIZE, }; #endif diff --git a/ports/zephyr-cp/common-hal/nvm/ByteArray.c b/ports/zephyr-cp/common-hal/nvm/ByteArray.c new file mode 100644 index 0000000000000..b8f552d677389 --- /dev/null +++ b/ports/zephyr-cp/common-hal/nvm/ByteArray.c @@ -0,0 +1,103 @@ +// This file is part of the CircuitPython project: https://circuitpython.org +// +// SPDX-FileCopyrightText: Copyright (c) 2025 Scott Shawcroft for Adafruit Industries +// +// SPDX-License-Identifier: MIT + +#include "py/runtime.h" +#include "common-hal/nvm/ByteArray.h" +#include "shared-bindings/nvm/ByteArray.h" + +#include + +#include +#include + +#define NVM_PARTITION nvm_partition + +#if FIXED_PARTITION_EXISTS(NVM_PARTITION) + +static const struct flash_area *nvm_area = NULL; +static size_t nvm_erase_size = 0; + +static bool ensure_nvm_open(void) { + if (nvm_area != NULL) { + return true; + } + int rc = flash_area_open(FIXED_PARTITION_ID(NVM_PARTITION), &nvm_area); + if (rc != 0) { + return false; + } + + const struct device *dev = flash_area_get_device(nvm_area); + struct flash_pages_info info; + flash_get_page_info_by_offs(dev, nvm_area->fa_off, &info); + nvm_erase_size = info.size; + + return true; +} + +uint32_t common_hal_nvm_bytearray_get_length(const nvm_bytearray_obj_t *self) { + if (!ensure_nvm_open()) { + return 0; + } + return nvm_area->fa_size; +} + +bool common_hal_nvm_bytearray_set_bytes(const nvm_bytearray_obj_t *self, + uint32_t start_index, uint8_t *values, uint32_t len) { + if (!ensure_nvm_open()) { + return false; + } + + uint32_t address = start_index; + while (len > 0) { + uint32_t page_offset = address % nvm_erase_size; + uint32_t page_start = address - page_offset; + uint32_t write_len = MIN(len, nvm_erase_size - page_offset); + + uint8_t *buffer = m_malloc(nvm_erase_size); + if (buffer == NULL) { + return false; + } + + // Read the full erase page. + int rc = flash_area_read(nvm_area, page_start, buffer, nvm_erase_size); + if (rc != 0) { + m_free(buffer); + return false; + } + + // Modify the relevant bytes. + memcpy(buffer + page_offset, values, write_len); + + // Erase the page. + rc = flash_area_erase(nvm_area, page_start, nvm_erase_size); + if (rc != 0) { + m_free(buffer); + return false; + } + + // Write the page back. + rc = flash_area_write(nvm_area, page_start, buffer, nvm_erase_size); + m_free(buffer); + if (rc != 0) { + return false; + } + + address += write_len; + values += write_len; + len -= write_len; + } + return true; +} + +void common_hal_nvm_bytearray_get_bytes(const nvm_bytearray_obj_t *self, + uint32_t start_index, uint32_t len, uint8_t *values) { + if (!ensure_nvm_open()) { + return; + } + flash_area_read(nvm_area, start_index, values, len); +} + +#endif // FIXED_PARTITION_EXISTS(NVM_PARTITION) diff --git a/ports/zephyr-cp/common-hal/nvm/ByteArray.h b/ports/zephyr-cp/common-hal/nvm/ByteArray.h new file mode 100644 index 0000000000000..9c771aaa3a950 --- /dev/null +++ b/ports/zephyr-cp/common-hal/nvm/ByteArray.h @@ -0,0 +1,13 @@ +// This file is part of the CircuitPython project: https://circuitpython.org +// +// SPDX-FileCopyrightText: Copyright (c) 2025 Scott Shawcroft for Adafruit Industries +// +// SPDX-License-Identifier: MIT + +#pragma once + +#include "py/obj.h" + +typedef struct { + mp_obj_base_t base; +} nvm_bytearray_obj_t; diff --git a/ports/zephyr-cp/cptools/zephyr2cp.py b/ports/zephyr-cp/cptools/zephyr2cp.py index c123d90ce7816..d8372919f299d 100644 --- a/ports/zephyr-cp/cptools/zephyr2cp.py +++ b/ports/zephyr-cp/cptools/zephyr2cp.py @@ -893,4 +893,9 @@ def zephyr_dts_to_cp_board(board_id, portdir, builddir, zephyrbuilddir): # noqa board_info["flash_count"] = len(flashes) board_info["rotaryio"] = bool(ioports) board_info["usb_num_endpoint_pairs"] = usb_num_endpoint_pairs + + # Detect NVM partition from the device tree. + nvm_node = device_tree.label2node.get("nvm_partition") + board_info["nvm"] = nvm_node is not None + return board_info diff --git a/ports/zephyr-cp/mpconfigport.h b/ports/zephyr-cp/mpconfigport.h index 491b5293e2ebc..491c03592c01d 100644 --- a/ports/zephyr-cp/mpconfigport.h +++ b/ports/zephyr-cp/mpconfigport.h @@ -17,6 +17,9 @@ #define CIRCUITPY_DEBUG_TINYUSB 0 +// NVM size is determined at runtime from the Zephyr partition table. +#define CIRCUITPY_INTERNAL_NVM_SIZE 1 + // Disable native _Float16 handling for host builds. #define MICROPY_FLOAT_USE_NATIVE_FLT16 (0) diff --git a/ports/zephyr-cp/tests/test_nvm.py b/ports/zephyr-cp/tests/test_nvm.py new file mode 100644 index 0000000000000..5de304afc1bdc --- /dev/null +++ b/ports/zephyr-cp/tests/test_nvm.py @@ -0,0 +1,73 @@ +# SPDX-FileCopyrightText: 2025 Scott Shawcroft for Adafruit Industries +# SPDX-License-Identifier: MIT + +"""Test NVM functionality on native_sim.""" + +import pytest + + +NVM_BASIC_CODE = """\ +import microcontroller + +nvm = microcontroller.nvm +print(f"nvm length: {len(nvm)}") + +# Write some bytes +nvm[0] = 42 +nvm[1] = 99 +print(f"nvm[0]: {nvm[0]}") +print(f"nvm[1]: {nvm[1]}") + +# Write a slice +nvm[2:5] = b"\\x01\\x02\\x03" +print(f"nvm[2:5]: {list(nvm[2:5])}") + +print("done") +""" + + +@pytest.mark.circuitpy_drive({"code.py": NVM_BASIC_CODE}) +def test_nvm_read_write(circuitpython): + """Test basic NVM read and write operations.""" + circuitpython.wait_until_done() + + output = circuitpython.serial.all_output + assert "nvm length: 8192" in output + assert "nvm[0]: 42" in output + assert "nvm[1]: 99" in output + assert "nvm[2:5]: [1, 2, 3]" in output + assert "done" in output + + +NVM_PERSIST_CODE = """\ +import microcontroller + +nvm = microcontroller.nvm +value = nvm[0] +print(f"nvm[0]: {value}") + +if value == 255: + # First run: write a marker + nvm[0] = 123 + print("wrote marker") +else: + print(f"marker found: {value}") + +print("done") +""" + + +@pytest.mark.circuitpy_drive({"code.py": NVM_PERSIST_CODE}) +@pytest.mark.code_py_runs(2) +def test_nvm_persists_across_reload(circuitpython): + """Test that NVM data persists across soft reloads.""" + circuitpython.serial.wait_for("wrote marker") + # Trigger soft reload + circuitpython.serial.write("\x04") + circuitpython.wait_until_done() + + output = circuitpython.serial.all_output + assert "nvm[0]: 255" in output + assert "wrote marker" in output + assert "marker found: 123" in output + assert "done" in output