diff --git a/extmod/vfs_blockdev.c b/extmod/vfs_blockdev.c index 74d1262364e..d839ce00ea3 100644 --- a/extmod/vfs_blockdev.c +++ b/extmod/vfs_blockdev.c @@ -38,6 +38,10 @@ #include "shared-bindings/sdioio/SDCard.h" #endif +#if defined(CIRCUITPY_SP1EMMC) && CIRCUITPY_SP1EMMC +#include "bindings/sp1emmc/EMMC.h" +#endif + #if MICROPY_VFS @@ -75,6 +79,25 @@ void mp_vfs_blockdev_init(mp_vfs_blockdev_t *self, mp_obj_t bdev) { self->u.ioctl[2] = (mp_obj_t)sdioio_sdcard_ioctl; // native version } #endif + + #if defined(CIRCUITPY_SP1EMMC) && CIRCUITPY_SP1EMMC + if (mp_obj_get_type(bdev) == &sp1emmc_emmc_type) { + self->flags |= MP_BLOCKDEV_FLAG_NATIVE | MP_BLOCKDEV_FLAG_HAVE_IOCTL; + self->readblocks[0] = mp_const_none; + self->readblocks[1] = bdev; + self->readblocks[2] = (mp_obj_t)sp1emmc_emmc_readblocks_native; + if (sp1emmc_emmc_is_write_enabled(bdev)) { + self->writeblocks[0] = mp_const_none; + self->writeblocks[1] = bdev; + self->writeblocks[2] = (mp_obj_t)sp1emmc_emmc_writeblocks_native; + } else { + self->writeblocks[0] = MP_OBJ_NULL; + } + self->u.ioctl[0] = mp_const_none; + self->u.ioctl[1] = bdev; + self->u.ioctl[2] = (mp_obj_t)sp1emmc_emmc_ioctl_native; + } + #endif if (self->u.ioctl[0] != MP_OBJ_NULL) { // Device supports new block protocol, so indicate it self->flags |= MP_BLOCKDEV_FLAG_HAVE_IOCTL; diff --git a/locale/circuitpython.pot b/locale/circuitpython.pot index 543a6ecdfa3..4c34ed852e9 100644 --- a/locale/circuitpython.pot +++ b/locale/circuitpython.pot @@ -1039,6 +1039,7 @@ msgstr "" #: ports/atmel-samd/common-hal/sdioio/SDCard.c #: ports/cxd56/common-hal/sdioio/SDCard.c #: ports/espressif/common-hal/sdioio/SDCard.c +#: ports/nordic/boards/teenage_engineering_sp1/bindings/sp1emmc/EMMC.c #: ports/raspberrypi/common-hal/sdioio/SDCard.c #: ports/stm/common-hal/sdioio/SDCard.c shared-bindings/floppyio/__init__.c #: shared-module/sdcardio/SDCard.c @@ -1707,6 +1708,43 @@ msgstr "" msgid "You pressed the left button at start up." msgstr "" +#: ports/nordic/boards/teenage_engineering_sp1/bindings/sp1emmc/EMMC.c +msgid "" +"eMMC owned by the USB drive; set CIRCUITPY_EMMC_USB = false in settings.toml" +msgstr "" + +#: ports/nordic/boards/teenage_engineering_sp1/bindings/sp1emmc/EMMC.c +msgid "eMMC already in use" +msgstr "" + +#: ports/nordic/boards/teenage_engineering_sp1/bindings/sp1emmc/EMMC.c +#: ports/raspberrypi/common-hal/busio/SPI.c +msgid "SPI peripheral in use" +msgstr "" + +#: ports/nordic/boards/teenage_engineering_sp1/bindings/sp1emmc/EMMC.c +#, c-format +msgid "eMMC high-speed switch failed at %s" +msgstr "" + +#: ports/nordic/boards/teenage_engineering_sp1/bindings/sp1emmc/EMMC.c +#, c-format +msgid "eMMC init failed at %s" +msgstr "" + +#: ports/nordic/boards/teenage_engineering_sp1/bindings/sp1emmc/EMMC.c +#: shared-bindings/i2cioexpander/IOExpander.c +msgid "address out of range" +msgstr "" + +#: ports/nordic/boards/teenage_engineering_sp1/bindings/sp1emmc/EMMC.c +#: shared-bindings/_bleio/__init__.c +#: shared-bindings/memorymonitor/AllocationSize.c +#: shared-bindings/pulseio/PulseIn.c shared-module/bitmaptools/__init__.c +#: shared-module/displayio/Bitmap.c shared-module/displayio/Group.c +msgid "Read-only" +msgstr "" + #: ports/nordic/common-hal/_bleio/Adapter.c msgid "timeout must be < 655.35 secs" msgstr "" @@ -1902,10 +1940,6 @@ msgstr "" msgid "I2C peripheral in use" msgstr "" -#: ports/raspberrypi/common-hal/busio/SPI.c -msgid "SPI peripheral in use" -msgstr "" - #: ports/raspberrypi/common-hal/busio/UART.c msgid "UART peripheral in use" msgstr "" @@ -3613,13 +3647,6 @@ msgstr "" msgid "not a 128-bit UUID" msgstr "" -#: shared-bindings/_bleio/__init__.c -#: shared-bindings/memorymonitor/AllocationSize.c -#: shared-bindings/pulseio/PulseIn.c shared-module/bitmaptools/__init__.c -#: shared-module/displayio/Bitmap.c shared-module/displayio/Group.c -msgid "Read-only" -msgstr "" - #: shared-bindings/_pew/PewPew.c msgid "Incorrect buffer size" msgstr "" @@ -4012,10 +4039,6 @@ msgstr "" msgid "Unsupported hash algorithm" msgstr "" -#: shared-bindings/i2cioexpander/IOExpander.c -msgid "address out of range" -msgstr "" - #: shared-bindings/i2cioexpander/IOExpander.c msgid "num_pins must be 8 or 16" msgstr "" diff --git a/ports/nordic/Makefile b/ports/nordic/Makefile index 1e283fc699c..cb2d32f74f8 100755 --- a/ports/nordic/Makefile +++ b/ports/nordic/Makefile @@ -6,9 +6,16 @@ include ../../py/circuitpy_mkenv.mk +# The SoftDevice paths are built from MCU_VARIANT and +# SOFTDEV_VERSION, which only exist once boards/$(BOARD)/mpconfigboard.mk has +# set MCU_CHIP. Skip when the rule requested is not board-specific (e.g. +# fetch-port-submodules), or the stack-missing check looks for an +# unexpanded path and fails on a tree where nothing is actually missing. +ifneq ($(VALID_BOARD),) ifneq ($(SD), ) include bluetooth/bluetooth_common.mk endif +endif CROSS_COMPILE = arm-none-eabi- @@ -65,7 +72,32 @@ endif # nrfx does casts that increase alignment requirements. CFLAGS += -Wno-cast-align +# Three places on the boot path write UICR, each conditionally, each with a +# forced reset afterwards, and each permanent: UICR can only be cleared by an +# ERASEALL, which needs a debugger and wipes the whole chip. On a board with a +# vendor bootloader and no SWD access that is a one-way change with no undo, so +# every one of them is a knob. See also NRF_NFCT_PINS_AS_GPIOS (mpconfigport.mk) +# and NRF_REGOUT0_3V3 below; teenage_engineering_sp1 turns off all three. +# +# CONFIG_GPIO_AS_PINRESET makes SystemInit write UICR PSELRESET[0..1] = 18 on +# any chip where it is unprogrammed. Boards where P0.18 is not a reset pad (or +# whose UICR must never be touched) opt out with NRF_GPIO_AS_PINRESET = 0. +NRF_GPIO_AS_PINRESET ?= 1 +ifeq ($(NRF_GPIO_AS_PINRESET),1) NRF_DEFINES += -DCONFIG_GPIO_AS_PINRESET +endif + +# nrf_peripherals_power_init() writes UICR REGOUT0 = 3.3 V on any nRF52840 that +# is running in high-voltage mode (VDDH supplied) with REGOUT0 unprogrammed, +# because the reset default there is 1.8 V and most boards want 3.3 V. A board +# that is externally regulated never reaches the write; a battery-powered, +# VDDH-supplied board reaches it on its first boot. Opt out with +# NRF_REGOUT0_3V3 = 0 -- but only if the board is known to work at whatever +# REGOUT0 already says, since nothing else sets that voltage. +NRF_REGOUT0_3V3 ?= 1 +ifeq ($(NRF_REGOUT0_3V3),1) +CFLAGS += -DCONFIG_REGOUT0_3V3 +endif CFLAGS += $(NRF_DEFINES) CFLAGS += \ @@ -115,11 +147,17 @@ SRC_C += \ boards/$(BOARD)/board.c \ boards/$(BOARD)/pins.c \ device/$(MCU_VARIANT)/startup_$(MCU_SUB_VARIANT).c \ + nrfx/mdk/system_$(MCU_SUB_VARIANT).c \ + + +ifneq ($(SD), ) +SRC_C += \ bluetooth/ble_drv.c \ common-hal/_bleio/bonding.c \ - nrfx/mdk/system_$(MCU_SUB_VARIANT).c \ sd_mutex.c \ +endif + SRC_PERIPHERALS := \ peripherals/nrf/cache.c \ peripherals/nrf/clocks.c \ @@ -177,7 +215,11 @@ UF2_FAMILY_ID_nrf52840 = 0xADA52840 UF2_FAMILY_ID_nrf52833 = 0x621E937A +ifneq ($(SD), ) all: $(BUILD)/firmware.bin $(BUILD)/firmware.uf2 $(BUILD)/firmware.combined.hex +else +all: $(BUILD)/firmware.bin $(BUILD)/firmware.uf2 $(BUILD)/firmware.hex +endif ifeq ($(VALID_BOARD),) $(BUILD)/firmware.elf: invalid-board diff --git a/ports/nordic/boards/teenage_engineering_sp1/bindings/sp1emmc/EMMC.c b/ports/nordic/boards/teenage_engineering_sp1/bindings/sp1emmc/EMMC.c new file mode 100644 index 00000000000..eb6223ea834 --- /dev/null +++ b/ports/nordic/boards/teenage_engineering_sp1/bindings/sp1emmc/EMMC.c @@ -0,0 +1,604 @@ +// This file is part of the CircuitPython project: https://circuitpython.org +// +// SPDX-FileCopyrightText: Copyright (c) 2026 Tim Cocks for Adafruit Industries +// +// SPDX-License-Identifier: MIT + +#include + +#include "py/obj.h" +#include "py/objproperty.h" +#include "py/runtime.h" +#include "py/mperrno.h" +#include "py/mphal.h" + +#include "extmod/vfs.h" + +#include "shared-bindings/microcontroller/Pin.h" +#include "shared-bindings/util.h" +#include "shared/runtime/context_manager_helpers.h" +#include "common-hal/microcontroller/Pin.h" +#include "peripherals/nrf/nrf52840/pins.h" + +#include "bindings/sp1emmc/EMMC.h" +#include "sp1emmc/automount.h" +#include "sp1emmc/sp1emmc.h" + +#include "nrf.h" + +static bool s_constructed; + +typedef struct { + mp_obj_base_t base; + bool deinited; + bool write_enabled; +} sp1emmc_emmc_obj_t; + +#if SP1EMMC_AUTOMOUNT +static sp1emmc_emmc_obj_t s_automount_obj; +static bool s_automounted; +#endif + +static const mcu_pin_obj_t *const emmc_pins[] = { + &pin_P0_06, // EMMC_CLK + &pin_P0_07, // EMMC_DAT0 + &pin_P0_08, // EMMC_CMD + &pin_P1_08, // EMMC_RESET + &pin_P0_14, // EMMC_VCCQ +}; + +bool sp1emmc_spim3_in_use(void) { + return s_constructed; +} + +static void release_hardware(void) { + emmc_power_down(); + for (size_t i = 0; i < MP_ARRAY_SIZE(emmc_pins); i++) { + reset_pin_number(emmc_pins[i]->number); + } + s_constructed = false; +} + +void sp1emmc_reset(void) { + #if SP1EMMC_AUTOMOUNT + if (s_automounted) { + return; + } + #endif + if (s_constructed) { + release_hardware(); + } +} + +static void check_for_deinit(sp1emmc_emmc_obj_t *self) { + if (self->deinited) { + raise_deinited_error(); + } +} + +static const char *init_failure_stage(void) { + if (!g_emmc_state.cmd0_sent) { + return "cmd0"; + } + if (g_emmc_state.cmd1_retries < 0) { + return "cmd1 (card never ready)"; + } + if (!g_emmc_state.cmd2_resp) { + return "cmd2 (no CID)"; + } + if (!g_emmc_state.cmd3_resp) { + return "cmd3"; + } + if (!g_emmc_state.cmd7_resp) { + return "cmd7 (select)"; + } + if (!g_emmc_state.cmd16_resp) { + return "cmd16 (blocklen)"; + } + return "ext_csd"; +} + +static const char *hs_failure_stage(void) { + switch (g_emmc_state.hs_stage) { + case 0: + return "DEVICE_TYPE (card does not advertise 52 MHz)"; + case 1: + return "cmd6 (no response)"; + case 2: + return "cmd6 busy (card never released DAT0)"; + case 3: + return g_emmc_state.hs_switch_error + ? "cmd13 SWITCH_ERROR (card rejected HS_TIMING)" + : "cmd13 (card never came back to tran)"; + case 4: + return "readback (EXT_CSD[185] did not take)"; + default: + return "32 MHz smoke test (fell back to 16 MHz)"; + } +} + +// Power the card up and (optionally) switch it to high speed. Shared by +// make_new() and the automount, deliberately never raises. Returns NULL +// on success, or a static stage description, *hs_failed tells the +// caller which of the two messages to use. On any failure the hardware has +// already been released, pin claims included. +static const char *emmc_power_up(bool high_speed, bool *hs_failed) { + *hs_failed = false; + s_constructed = true; + + if (!emmc_init()) { + const char *stage = init_failure_stage(); + release_hardware(); + return stage; + } + + uint8_t ext_csd[EMMC_BLOCK_SIZE]; + if (!emmc_read_ext_csd(ext_csd)) { + release_hardware(); + return "ext_csd"; + } + if (high_speed && !emmc_set_high_speed()) { + const char *stage = hs_failure_stage(); + release_hardware(); + *hs_failed = true; + return stage; + } + return NULL; +} + +#if SP1EMMC_AUTOMOUNT +// Construct the supervisor's EMMC object. Returns MP_OBJ_NULL if the +// card cannot be brought up, and raises nothing on any path. +// +// The pins are marked never-reset as well as claimed. Claiming keeps Python +// from taking them; never-reset keeps reset_all_pins() from reconfiguring them +// out from under a live volume between VM runs. +mp_obj_t sp1emmc_automount_construct(bool high_speed, bool write_enabled) { + if (s_constructed) { + return MP_OBJ_NULL; + } + if ((NRF_SPIM3->ENABLE & SPIM_ENABLE_ENABLE_Msk) != 0) { + return MP_OBJ_NULL; + } + for (size_t i = 0; i < MP_ARRAY_SIZE(emmc_pins); i++) { + if (!pin_number_is_free(emmc_pins[i]->number)) { + return MP_OBJ_NULL; + } + } + for (size_t i = 0; i < MP_ARRAY_SIZE(emmc_pins); i++) { + claim_pin(emmc_pins[i]); + never_reset_pin_number(emmc_pins[i]->number); + } + bool hs_failed = false; + if (emmc_power_up(high_speed, &hs_failed) != NULL) { + return MP_OBJ_NULL; + } + s_automount_obj.base.type = &sp1emmc_emmc_type; + s_automount_obj.deinited = false; + s_automount_obj.write_enabled = write_enabled; + s_automounted = true; + return MP_OBJ_FROM_PTR(&s_automount_obj); +} + +bool sp1emmc_is_automounted(void) { + return s_automounted; +} + +// The automount's undo. Safe to call from any of its failure paths +void sp1emmc_automount_abandon(void) { + s_automounted = false; + s_automount_obj.deinited = true; + if (s_constructed) { + release_hardware(); + } +} +#endif + +//| class EMMC: +//| """The on-board eMMC as a block device""" +//| +//| def __init__(self, *, high_speed: bool = False, write_enabled: bool = False) -> None: +//| """Power up the card and make it ready for block access. +//| +//| Only one `EMMC` object may exist at a time. Call `deinit()`, or use +//| the object as a context manager, to release the card and its pins. +//| +//| :param bool high_speed: Run the bus at its faster clock rate. Raises +//| an `OSError` if the card will not make the switch. +//| :param bool write_enabled: Allow `writeblocks()`. When `False`, the +//| object is read-only and every write path refuses. +//| +//| :raises ValueError: if the card is already in use, or is owned by +//| the USB drive. +//| :raises OSError: if the card does not come up. +//| +//| Mount the card's filesystem:: +//| +//| import sp1emmc +//| import storage +//| +//| emmc = sp1emmc.EMMC(high_speed=True, write_enabled=True) +//| storage.mount(storage.VfsFat(emmc), "/sd") +//| """ +//| ... +//| +static mp_obj_t sp1emmc_emmc_make_new(const mp_obj_type_t *type, size_t n_args, size_t n_kw, const mp_obj_t *all_args) { + enum { ARG_high_speed, ARG_write_enabled }; + static const mp_arg_t allowed_args[] = { + { MP_QSTR_high_speed, MP_ARG_KW_ONLY | MP_ARG_BOOL, {.u_bool = false} }, + { MP_QSTR_write_enabled, MP_ARG_KW_ONLY | MP_ARG_BOOL, {.u_bool = false} }, + }; + mp_arg_val_t args[MP_ARRAY_SIZE(allowed_args)]; + mp_arg_parse_all_kw_array(n_args, n_kw, all_args, MP_ARRAY_SIZE(allowed_args), allowed_args, args); + bool high_speed = args[ARG_high_speed].u_bool; + bool write_enabled = args[ARG_write_enabled].u_bool; + + #if SP1EMMC_AUTOMOUNT + if (s_automounted) { + mp_raise_ValueError(MP_ERROR_TEXT("eMMC owned by the USB drive; set CIRCUITPY_EMMC_USB = false in settings.toml")); + } + #endif + if (s_constructed) { + mp_raise_ValueError(MP_ERROR_TEXT("eMMC already in use")); + } + + if ((NRF_SPIM3->ENABLE & SPIM_ENABLE_ENABLE_Msk) != 0) { + mp_raise_ValueError(MP_ERROR_TEXT("SPI peripheral in use")); + } + for (size_t i = 0; i < MP_ARRAY_SIZE(emmc_pins); i++) { + assert_pin_free(emmc_pins[i]); + } + for (size_t i = 0; i < MP_ARRAY_SIZE(emmc_pins); i++) { + claim_pin(emmc_pins[i]); + } + bool hs_failed = false; + const char *stage = emmc_power_up(high_speed, &hs_failed); + if (stage != NULL) { + mp_raise_msg_varg(&mp_type_OSError, + hs_failed ? MP_ERROR_TEXT("eMMC high-speed switch failed at %s") + : MP_ERROR_TEXT("eMMC init failed at %s"), stage); + } + + sp1emmc_emmc_obj_t *self = mp_obj_malloc(sp1emmc_emmc_obj_t, &sp1emmc_emmc_type); + self->deinited = false; + self->write_enabled = write_enabled; + return MP_OBJ_FROM_PTR(self); +} + +//| def deinit(self) -> None: +//| """Release the card and the pins it uses. Any further use of this +//| object raises a `ValueError`.""" +//| ... +//| +static mp_obj_t sp1emmc_emmc_deinit(mp_obj_t self_in) { + sp1emmc_emmc_obj_t *self = MP_OBJ_TO_PTR(self_in); + if (!self->deinited) { + release_hardware(); + self->deinited = true; + } + return mp_const_none; +} +static MP_DEFINE_CONST_FUN_OBJ_1(sp1emmc_emmc_deinit_obj, sp1emmc_emmc_deinit); + +//| def __enter__(self) -> EMMC: +//| """No-op used by Context Managers.""" +//| ... +//| +//| def __exit__(self) -> None: +//| """Automatically deinitializes the hardware when exiting a context. See +//| :ref:`lifetime-and-contextmanagers` for more info.""" +//| ... +//| +static mp_obj_t sp1emmc_emmc_obj___exit__(size_t n_args, const mp_obj_t *args) { + return sp1emmc_emmc_deinit(args[0]); +} +static MP_DEFINE_CONST_FUN_OBJ_VAR_BETWEEN(sp1emmc_emmc___exit___obj, 4, 4, sp1emmc_emmc_obj___exit__); + +#define CHUNK_BLOCKS 64u + +static int emmc_read_chunked(uint8_t *out, mp_uint_t start, mp_uint_t count, bool from_vm) { + mp_uint_t total = emmc_block_count(); + if (count == 0 || start >= total || count > total - start) { + return -MP_EINVAL; + } + for (mp_uint_t done = 0; done < count;) { + mp_uint_t run = MIN(CHUNK_BLOCKS, count - done); + bool ok = false; + for (int attempt = 0; attempt < 3 && !ok; attempt++) { + ok = emmc_read_blocks(start + done, out + done * EMMC_BLOCK_SIZE, run); + } + if (!ok) { + return -MP_EIO; + } + done += run; + RUN_BACKGROUND_TASKS; + if (from_vm) { + mp_handle_pending(true); + } + } + return 0; +} + +//| def readblocks(self, start_block: int, buf: WriteableBuffer) -> None: +//| """Read into ``buf`` starting at ``start_block``. +//| +//| :param int start_block: the first block to read +//| :param WriteableBuffer buf: a buffer whose length is a non-zero +//| multiple of `block_size` +//| +//| :raises ValueError: if ``buf`` is the wrong length, or the requested +//| blocks run past the end of the card. +//| :raises OSError: if the card fails to deliver the data.""" +//| ... +//| +static mp_obj_t sp1emmc_emmc_readblocks(mp_obj_t self_in, mp_obj_t start_in, mp_obj_t buf_in) { + sp1emmc_emmc_obj_t *self = MP_OBJ_TO_PTR(self_in); + check_for_deinit(self); + + mp_buffer_info_t bufinfo; + mp_get_buffer_raise(buf_in, &bufinfo, MP_BUFFER_WRITE); + if (bufinfo.len == 0 || (bufinfo.len % EMMC_BLOCK_SIZE) != 0) { + mp_raise_ValueError_varg(MP_ERROR_TEXT("Buffer must be a multiple of %d bytes"), 512); + } + mp_uint_t start = mp_obj_get_int_truncated(start_in); + mp_uint_t count = bufinfo.len / EMMC_BLOCK_SIZE; + mp_uint_t total = emmc_block_count(); + if (start >= total || count > total - start) { + mp_raise_ValueError(MP_ERROR_TEXT("address out of range")); + } + + int err = emmc_read_chunked(bufinfo.buf, start, count, true); + if (err != 0) { + mp_raise_OSError(-err); + } + return mp_const_none; +} +static MP_DEFINE_CONST_FUN_OBJ_3(sp1emmc_emmc_readblocks_obj, sp1emmc_emmc_readblocks); + +static int emmc_write_chunked(const uint8_t *src, mp_uint_t start, mp_uint_t count, bool from_vm) { + mp_uint_t total = emmc_block_count(); + if (count == 0 || start >= total || count > total - start) { + return -MP_EINVAL; + } + for (mp_uint_t done = 0; done < count;) { + mp_uint_t run = MIN(CHUNK_BLOCKS, count - done); + bool ok = false; + for (int attempt = 0; attempt < 3 && !ok; attempt++) { + ok = emmc_write_blocks(start + done, src + done * EMMC_BLOCK_SIZE, run); + } + if (!ok) { + return -MP_EIO; + } + done += run; + RUN_BACKGROUND_TASKS; + if (from_vm) { + mp_handle_pending(true); + } + } + return 0; +} + +//| def writeblocks(self, start_block: int, buf: ReadableBuffer) -> None: +//| """Write ``buf`` to the card starting at ``start_block``. +//| +//| :param int start_block: the first block to write +//| :param ReadableBuffer buf: a buffer whose length is a non-zero +//| multiple of `block_size` +//| +//| :raises RuntimeError: if this object was not constructed with +//| ``write_enabled=True``. +//| :raises ValueError: if ``buf`` is the wrong length, or the requested +//| blocks run past the end of the card. +//| :raises OSError: if the write fails.""" +//| ... +//| +static mp_obj_t sp1emmc_emmc_writeblocks(mp_obj_t self_in, mp_obj_t start_in, mp_obj_t buf_in) { + sp1emmc_emmc_obj_t *self = MP_OBJ_TO_PTR(self_in); + check_for_deinit(self); + if (!self->write_enabled) { + mp_raise_msg(&mp_type_RuntimeError, + MP_ERROR_TEXT("Read-only")); + } + + mp_buffer_info_t bufinfo; + mp_get_buffer_raise(buf_in, &bufinfo, MP_BUFFER_READ); + if (bufinfo.len == 0 || (bufinfo.len % EMMC_BLOCK_SIZE) != 0) { + mp_raise_ValueError_varg(MP_ERROR_TEXT("Buffer must be a multiple of %d bytes"), 512); + } + mp_uint_t start = mp_obj_get_int_truncated(start_in); + mp_uint_t count = bufinfo.len / EMMC_BLOCK_SIZE; + mp_uint_t total = emmc_block_count(); + if (start >= total || count > total - start) { + mp_raise_ValueError(MP_ERROR_TEXT("address out of range")); + } + + int err = emmc_write_chunked(bufinfo.buf, start, count, true); + if (err != 0) { + mp_raise_OSError(-err); + } + return mp_const_none; +} +static MP_DEFINE_CONST_FUN_OBJ_3(sp1emmc_emmc_writeblocks_obj, sp1emmc_emmc_writeblocks); + +//| def ioctl(self, op: int, arg: int) -> Optional[int]: +//| """Perform a block-device control operation, as required by the +//| block-device protocol. Returns `None` for operations this device does +//| not implement.""" +//| ... +//| +static mp_obj_t sp1emmc_emmc_ioctl(mp_obj_t self_in, mp_obj_t op_in, mp_obj_t arg_in) { + sp1emmc_emmc_obj_t *self = MP_OBJ_TO_PTR(self_in); + check_for_deinit(self); + uint32_t out = 0; + if (!emmc_blockdev_ioctl(mp_obj_get_int_truncated(op_in), + mp_obj_get_int_truncated(arg_in), &out)) { + return mp_const_none; + } + return mp_obj_new_int_from_uint(out); +} +static MP_DEFINE_CONST_FUN_OBJ_3(sp1emmc_emmc_ioctl_obj, sp1emmc_emmc_ioctl); + +mp_uint_t sp1emmc_emmc_readblocks_native(mp_obj_t self_in, uint8_t *buf, + uint32_t start_block, uint32_t nblocks) { + sp1emmc_emmc_obj_t *self = MP_OBJ_TO_PTR(self_in); + if (self->deinited) { + return -MP_ENODEV; + } + return emmc_read_chunked(buf, start_block, nblocks, false); +} + +mp_uint_t sp1emmc_emmc_writeblocks_native(mp_obj_t self_in, const uint8_t *buf, + uint32_t start_block, uint32_t nblocks) { + sp1emmc_emmc_obj_t *self = MP_OBJ_TO_PTR(self_in); + if (self->deinited) { + return -MP_ENODEV; + } + + if (!self->write_enabled) { + return -MP_EROFS; + } + return emmc_write_chunked(buf, start_block, nblocks, false); +} + +bool sp1emmc_emmc_ioctl_native(mp_obj_t self_in, uint32_t cmd, uint32_t arg, + size_t *out_value) { + + (void)self_in; + uint32_t out = 0; + bool ok = emmc_blockdev_ioctl(cmd, arg, &out); + *out_value = out; + return ok; +} + +bool sp1emmc_emmc_is_write_enabled(mp_obj_t self_in) { + sp1emmc_emmc_obj_t *self = MP_OBJ_TO_PTR(self_in); + return !self->deinited && self->write_enabled; +} + +//| def read_ext_csd(self) -> bytes: +//| """Read the card's 512-byte extended CSD register.""" +//| ... +//| +static mp_obj_t sp1emmc_emmc_read_ext_csd(mp_obj_t self_in) { + sp1emmc_emmc_obj_t *self = MP_OBJ_TO_PTR(self_in); + check_for_deinit(self); + + uint8_t ext_csd[EMMC_BLOCK_SIZE]; + if (!emmc_read_ext_csd(ext_csd)) { + mp_raise_OSError(MP_EIO); + } + return mp_obj_new_bytes(ext_csd, sizeof(ext_csd)); +} +static MP_DEFINE_CONST_FUN_OBJ_1(sp1emmc_emmc_read_ext_csd_obj, sp1emmc_emmc_read_ext_csd); + +//| def status(self) -> int: +//| """Read the card's 32-bit status register.""" +//| ... +//| +static mp_obj_t sp1emmc_emmc_status(mp_obj_t self_in) { + sp1emmc_emmc_obj_t *self = MP_OBJ_TO_PTR(self_in); + check_for_deinit(self); + + uint8_t r1[6]; + if (!emmc_cmd13(r1)) { + mp_raise_OSError(MP_EIO); + } + uint32_t status = ((uint32_t)r1[1] << 24) | ((uint32_t)r1[2] << 16) | + ((uint32_t)r1[3] << 8) | (uint32_t)r1[4]; + return mp_obj_new_int_from_uint(status); +} +static MP_DEFINE_CONST_FUN_OBJ_1(sp1emmc_emmc_status_obj, sp1emmc_emmc_status); + +// ---- properties ----------------------------------------------------------- + +//| count: int +//| """The number of blocks on the card.""" +//| +static mp_obj_t sp1emmc_emmc_get_count(mp_obj_t self_in) { + sp1emmc_emmc_obj_t *self = MP_OBJ_TO_PTR(self_in); + check_for_deinit(self); + return mp_obj_new_int_from_uint(emmc_block_count()); +} +static MP_DEFINE_CONST_FUN_OBJ_1(sp1emmc_emmc_get_count_obj, sp1emmc_emmc_get_count); +MP_PROPERTY_GETTER(sp1emmc_emmc_count_obj, (mp_obj_t)&sp1emmc_emmc_get_count_obj); + +//| block_size: int +//| """The size of one block, in bytes.""" +//| +static mp_obj_t sp1emmc_emmc_get_block_size(mp_obj_t self_in) { + return MP_OBJ_NEW_SMALL_INT(EMMC_BLOCK_SIZE); +} +static MP_DEFINE_CONST_FUN_OBJ_1(sp1emmc_emmc_get_block_size_obj, sp1emmc_emmc_get_block_size); +MP_PROPERTY_GETTER(sp1emmc_emmc_block_size_obj, (mp_obj_t)&sp1emmc_emmc_get_block_size_obj); + +//| cid: bytes +//| """The card's 16-byte identification register.""" +//| +static mp_obj_t sp1emmc_emmc_get_cid(mp_obj_t self_in) { + sp1emmc_emmc_obj_t *self = MP_OBJ_TO_PTR(self_in); + check_for_deinit(self); + return mp_obj_new_bytes(g_emmc_state.cid, sizeof(g_emmc_state.cid)); +} +static MP_DEFINE_CONST_FUN_OBJ_1(sp1emmc_emmc_get_cid_obj, sp1emmc_emmc_get_cid); +MP_PROPERTY_GETTER(sp1emmc_emmc_cid_obj, (mp_obj_t)&sp1emmc_emmc_get_cid_obj); + +//| write_enabled: bool +//| """Whether `writeblocks()` is permitted on this object.""" +//| +static mp_obj_t sp1emmc_emmc_get_write_enabled(mp_obj_t self_in) { + sp1emmc_emmc_obj_t *self = MP_OBJ_TO_PTR(self_in); + check_for_deinit(self); + return mp_obj_new_bool(self->write_enabled); +} +static MP_DEFINE_CONST_FUN_OBJ_1(sp1emmc_emmc_get_write_enabled_obj, sp1emmc_emmc_get_write_enabled); +MP_PROPERTY_GETTER(sp1emmc_emmc_write_enabled_obj, (mp_obj_t)&sp1emmc_emmc_get_write_enabled_obj); + +//| high_speed: bool +//| """Whether the card is running at its faster clock rate.""" +//| +static mp_obj_t sp1emmc_emmc_get_high_speed(mp_obj_t self_in) { + sp1emmc_emmc_obj_t *self = MP_OBJ_TO_PTR(self_in); + check_for_deinit(self); + return mp_obj_new_bool(g_emmc_state.hs_active); +} +static MP_DEFINE_CONST_FUN_OBJ_1(sp1emmc_emmc_get_high_speed_obj, sp1emmc_emmc_get_high_speed); +MP_PROPERTY_GETTER(sp1emmc_emmc_high_speed_obj, (mp_obj_t)&sp1emmc_emmc_get_high_speed_obj); + +//| frequency: int +//| """The bus clock rate in Hz.""" +//| +static mp_obj_t sp1emmc_emmc_get_frequency(mp_obj_t self_in) { + sp1emmc_emmc_obj_t *self = MP_OBJ_TO_PTR(self_in); + check_for_deinit(self); + return mp_obj_new_int_from_uint(emmc_bus_hz()); +} +static MP_DEFINE_CONST_FUN_OBJ_1(sp1emmc_emmc_get_frequency_obj, sp1emmc_emmc_get_frequency); +MP_PROPERTY_GETTER(sp1emmc_emmc_frequency_obj, (mp_obj_t)&sp1emmc_emmc_get_frequency_obj); + +static const mp_rom_map_elem_t sp1emmc_emmc_locals_dict_table[] = { + { MP_ROM_QSTR(MP_QSTR_deinit), MP_ROM_PTR(&sp1emmc_emmc_deinit_obj) }, + { MP_ROM_QSTR(MP_QSTR___enter__), MP_ROM_PTR(&default___enter___obj) }, + { MP_ROM_QSTR(MP_QSTR___exit__), MP_ROM_PTR(&sp1emmc_emmc___exit___obj) }, + + { MP_ROM_QSTR(MP_QSTR_readblocks), MP_ROM_PTR(&sp1emmc_emmc_readblocks_obj) }, + { MP_ROM_QSTR(MP_QSTR_writeblocks), MP_ROM_PTR(&sp1emmc_emmc_writeblocks_obj) }, + { MP_ROM_QSTR(MP_QSTR_ioctl), MP_ROM_PTR(&sp1emmc_emmc_ioctl_obj) }, + { MP_ROM_QSTR(MP_QSTR_read_ext_csd), MP_ROM_PTR(&sp1emmc_emmc_read_ext_csd_obj) }, + { MP_ROM_QSTR(MP_QSTR_status), MP_ROM_PTR(&sp1emmc_emmc_status_obj) }, + + { MP_ROM_QSTR(MP_QSTR_count), MP_ROM_PTR(&sp1emmc_emmc_count_obj) }, + { MP_ROM_QSTR(MP_QSTR_high_speed), MP_ROM_PTR(&sp1emmc_emmc_high_speed_obj) }, + { MP_ROM_QSTR(MP_QSTR_frequency), MP_ROM_PTR(&sp1emmc_emmc_frequency_obj) }, + { MP_ROM_QSTR(MP_QSTR_block_size), MP_ROM_PTR(&sp1emmc_emmc_block_size_obj) }, + { MP_ROM_QSTR(MP_QSTR_cid), MP_ROM_PTR(&sp1emmc_emmc_cid_obj) }, + { MP_ROM_QSTR(MP_QSTR_write_enabled), MP_ROM_PTR(&sp1emmc_emmc_write_enabled_obj) }, +}; +static MP_DEFINE_CONST_DICT(sp1emmc_emmc_locals_dict, sp1emmc_emmc_locals_dict_table); + +MP_DEFINE_CONST_OBJ_TYPE( + sp1emmc_emmc_type, + MP_QSTR_EMMC, + MP_TYPE_FLAG_HAS_SPECIAL_ACCESSORS, + locals_dict, &sp1emmc_emmc_locals_dict, + make_new, sp1emmc_emmc_make_new + ); diff --git a/ports/nordic/boards/teenage_engineering_sp1/bindings/sp1emmc/EMMC.h b/ports/nordic/boards/teenage_engineering_sp1/bindings/sp1emmc/EMMC.h new file mode 100644 index 00000000000..e303af55240 --- /dev/null +++ b/ports/nordic/boards/teenage_engineering_sp1/bindings/sp1emmc/EMMC.h @@ -0,0 +1,41 @@ +// This file is part of the CircuitPython project: https://circuitpython.org +// +// SPDX-FileCopyrightText: Copyright (c) 2026 Tim Cocks for Adafruit Industries +// +// SPDX-License-Identifier: MIT + +#pragma once + +#include "py/obj.h" + +extern const mp_obj_type_t sp1emmc_emmc_type; + +// ---- native block-device protocol ----------------------------------------- +// +// The same shape sdcardio and sdioio present, so extmod/vfs_blockdev.c can +// bypass the Python method call. + +// 0 on success, negative errno on failure. Never raises. +mp_uint_t sp1emmc_emmc_readblocks_native(mp_obj_t self_in, uint8_t *buf, + uint32_t start_block, uint32_t nblocks); + +// 0 on success, -MP_EROFS on an object without write_enabled=True, other +// negative errno on failure. Never raises. +mp_uint_t sp1emmc_emmc_writeblocks_native(mp_obj_t self_in, const uint8_t *buf, + uint32_t start_block, uint32_t nblocks); + +// false = op not implemented, the caller turns that into None. +bool sp1emmc_emmc_ioctl_native(mp_obj_t self_in, uint32_t cmd, uint32_t arg, + size_t *out_value); + +// Whether this object may write at all +bool sp1emmc_emmc_is_write_enabled(mp_obj_t self_in); + +// ---- module and port hooks ------------------------------------------------- + +// True while a live sp1emmc.EMMC object owns SPIM3. +bool sp1emmc_spim3_in_use(void); + +// Clear module state on every VM reset. board_reset_pin_defaults() has already +// yanked the card's rail by then, so the state must not pretend to survive. +void sp1emmc_reset(void); diff --git a/ports/nordic/boards/teenage_engineering_sp1/bindings/sp1emmc/__init__.c b/ports/nordic/boards/teenage_engineering_sp1/bindings/sp1emmc/__init__.c new file mode 100644 index 00000000000..c794e012637 --- /dev/null +++ b/ports/nordic/boards/teenage_engineering_sp1/bindings/sp1emmc/__init__.c @@ -0,0 +1,92 @@ +// This file is part of the CircuitPython project: https://circuitpython.org +// +// SPDX-FileCopyrightText: Copyright (c) 2026 Tim Cocks for Adafruit Industries +// +// SPDX-License-Identifier: MIT + +#include + +#include "py/obj.h" +#include "py/runtime.h" + +#include "bindings/sp1emmc/EMMC.h" +#include "sp1emmc/automount.h" + +//| """Block device access to the on-board eMMC +//| +//| The `sp1emmc` module exposes the board's soldered-down eMMC chip as a block +//| device. It provides no filesystem of its own: to read files, hand an `EMMC` +//| object to `storage.VfsFat` and mount it. +//| +//| .. note:: This module is only available on boards with an eMMC wired to the +//| dedicated SPI peripheral, and only one `EMMC` object may exist at a time. +//| """ +//| + +//| def automounted() -> bool: +//| """`True` when the eMMC has been mounted as a filesystem for you at +//| startup, `False` when it is free for Python to open. +//| +//| While this is `True`, constructing `EMMC` raises a `ValueError`.""" +//| ... +//| +static mp_obj_t sp1emmc_automounted(void) { + return mp_obj_new_bool(sp1emmc_is_automounted()); +} +static MP_DEFINE_CONST_FUN_OBJ_0(sp1emmc_automounted_obj, sp1emmc_automounted); + +//| def automount_status() -> str: +//| """Why the eMMC is or is not mounted at ``/sd``, as one of: +//| +//| * ``"ok"`` -- mounted. +//| * ``"disabled"`` -- ``CIRCUITPY_EMMC_USB = 0`` in ``settings.toml``, or +//| the automount is not in this build. +//| * ``"safe mode"`` -- the board booted into safe mode. +//| * ``"no card"`` -- the card did not come up inside the boot budget. +//| * ``"no filesystem"`` -- the card came up but has no FAT volume. +//| * ``"skipped after fault"`` -- the *previous* boot did not come back out +//| of the automount, so this boot left the card alone to be sure USB came +//| up. The next boot tries again.""" +//| ... +//| +static mp_obj_t sp1emmc_automount_status(void) { + const char *s = "disabled"; + #if defined(SP1EMMC_AUTOMOUNT) && SP1EMMC_AUTOMOUNT + switch (sp1emmc_automount_get_status()) { + case SP1EMMC_AUTOMOUNT_OK: + s = "ok"; + break; + case SP1EMMC_AUTOMOUNT_SAFE_MODE: + s = "safe mode"; + break; + case SP1EMMC_AUTOMOUNT_NO_CARD: + s = "no card"; + break; + case SP1EMMC_AUTOMOUNT_NO_FILESYSTEM: + s = "no filesystem"; + break; + case SP1EMMC_AUTOMOUNT_SKIPPED_AFTER_FAULT: + s = "skipped after fault"; + break; + default: + break; + } + #endif + return mp_obj_new_str(s, strlen(s)); +} +static MP_DEFINE_CONST_FUN_OBJ_0(sp1emmc_automount_status_obj, sp1emmc_automount_status); + +static const mp_rom_map_elem_t sp1emmc_module_globals_table[] = { + { MP_ROM_QSTR(MP_QSTR___name__), MP_ROM_QSTR(MP_QSTR_sp1emmc) }, + { MP_ROM_QSTR(MP_QSTR_EMMC), MP_ROM_PTR(&sp1emmc_emmc_type) }, + { MP_ROM_QSTR(MP_QSTR_automounted), MP_ROM_PTR(&sp1emmc_automounted_obj) }, + { MP_ROM_QSTR(MP_QSTR_automount_status), MP_ROM_PTR(&sp1emmc_automount_status_obj) }, +}; +static MP_DEFINE_CONST_DICT(sp1emmc_module_globals, sp1emmc_module_globals_table); + +const mp_obj_module_t sp1emmc_module = { + .base = { &mp_type_module }, + .globals = (mp_obj_dict_t *)&sp1emmc_module_globals, +}; + +MP_REGISTER_MODULE(MP_QSTR_sp1emmc, sp1emmc_module); diff --git a/ports/nordic/boards/teenage_engineering_sp1/board.c b/ports/nordic/boards/teenage_engineering_sp1/board.c new file mode 100644 index 00000000000..54b0d44cd16 --- /dev/null +++ b/ports/nordic/boards/teenage_engineering_sp1/board.c @@ -0,0 +1,391 @@ +// This file is part of the CircuitPython project: https://circuitpython.org +// +// SPDX-FileCopyrightText: Copyright (c) 2026 Tim Cocks for Adafruit Industries +// +// SPDX-License-Identifier: MIT + +// Early-boot hygiene for the Teenage Engineering SP-1. +// +// Unlike a normal CircuitPython board, this one is entered from a bootloader +// that has already brought hardware up: it starts HFCLK and LFCLK, and leaves +// PWM2, PWM3 and the SAADC enabled. It also starts a watchdog that we cannot +// stop. So the app has to take the machine over from a *running* state rather +// than a reset state, and it has to do so quickly. + +#include "supervisor/board.h" + +#include "background.h" +#include "power_off.h" +#include "py/misc.h" +#include "wdt.h" +#include "nrfx/hal/nrf_gpio.h" + +// For the eMMC's supervisor mount: board_reset_pin_defaults() leaves +// the card's reset and rail alone while it is up. The header stubs +// sp1emmc_is_automounted() to false when the automount is not built, so the +// call site needs no #if of its own. +#if CIRCUITPY_SP1EMMC +#include "sp1emmc/automount.h" +#else +static inline bool sp1emmc_is_automounted(void) { + return false; +} +#endif + +// Pins this file drives directly. +#define PIN_OSC_EN NRF_GPIO_PIN_MAP(0, 13) // 3.072 MHz oscillator +#define PIN_TAS_RESET NRF_GPIO_PIN_MAP(0, 9) // TAS2505, active low +#define PIN_CS42_RESET NRF_GPIO_PIN_MAP(0, 15) // CS42L42, active low +#define PIN_BT_RESET NRF_GPIO_PIN_MAP(0, 10) // CYBT-353027-02, active low +#define PIN_EMMC_RESET NRF_GPIO_PIN_MAP(1, 8) // active low +#define PIN_EMMC_VCCQ_EN NRF_GPIO_PIN_MAP(0, 14) // eMMC I/O rail +#define PIN_CONTROL_RAIL NRF_GPIO_PIN_MAP(1, 10) // feeds faders + ladders +#define PIN_FUNCTION_BUTTON NRF_GPIO_PIN_MAP(0, 27) // active low, only GPIO button +#define PIN_CHARGE_ENABLE NRF_GPIO_PIN_MAP(0, 21) // BQ24232, active low +#define PIN_I2C_SCL NRF_GPIO_PIN_MAP(1, 11) // shared by both codecs +#define PIN_I2C_SDA NRF_GPIO_PIN_MAP(1, 7) + +// Both LED rows, active high. PIN_LED_HEARTBEAT is the first track LED, which +// is also MICROPY_HW_LED_STATUS and BOARD_POWER_OFF_CONFIRM_LED_PIN +#define PIN_LED_HEARTBEAT NRF_GPIO_PIN_MAP(0, 29) +static const uint32_t led_pins[] = { + NRF_GPIO_PIN_MAP(1, 13), NRF_GPIO_PIN_MAP(0, 0), // playback row (side) + NRF_GPIO_PIN_MAP(1, 12), NRF_GPIO_PIN_MAP(0, 1), + PIN_LED_HEARTBEAT, NRF_GPIO_PIN_MAP(0, 26), // track row (front) + NRF_GPIO_PIN_MAP(1, 15), NRF_GPIO_PIN_MAP(1, 14), +}; + +// Whether the boot up blink heartbeat still owns PIN_LED_HEARTBEAT. +static bool heartbeat_lit; + +// Bounded wait for a peripheral to acknowledge a STOP. +#define STOP_TIMEOUT_ITERATIONS (20000) + +static void wait_for_event(volatile uint32_t *event) { + for (uint32_t i = 0; i < STOP_TIMEOUT_ITERATIONS && *event == 0; i++) { + __NOP(); + } + *event = 0; + // Read back to flush the write buffer, per the nRF52 errata guidance for + // clearing events. + (void)*event; +} + +static void stop_pwm(NRF_PWM_Type *pwm) { + if (pwm->ENABLE == 0) { + return; + } + pwm->EVENTS_STOPPED = 0; + pwm->TASKS_STOP = 1; + wait_for_event(&pwm->EVENTS_STOPPED); + pwm->INTENCLR = 0xFFFFFFFF; + pwm->ENABLE = 0; +} + +void board_early_init(void) { + // First light boot up status blink. + // + // never lights the bootloader did not jump here, or we died in + // the reset handler / SystemInit + // lights and stays on we are running, but did not reach board_init(): + // suspect the filesystem format or something + // before the workflow starts + // lights, then goes out board_init() reached; USB is next, so from here + // on the absence of a tty is a USB problem + // + nrf_gpio_cfg_output(PIN_LED_HEARTBEAT); + nrf_gpio_pin_set(PIN_LED_HEARTBEAT); + heartbeat_lit = true; + + for (size_t i = 0; i < 8; i++) { + NVIC->ICER[i] = 0xFFFFFFFF; + NVIC->ICPR[i] = 0xFFFFFFFF; + } + __DSB(); + __ISB(); + + // Break any PPI wiring before stopping peripherals, so nothing we stop can + // be restarted by a leftover event->task connection. + NRF_PPI->CHENCLR = 0xFFFFFFFF; + + // The bootloader drives the LEDs with PWM2 and PWM3. PWM0/PWM1 are stopped + // too so that pwmio starts from a known state. + stop_pwm(NRF_PWM0); + stop_pwm(NRF_PWM1); + stop_pwm(NRF_PWM2); + stop_pwm(NRF_PWM3); + + // The bootloader reads the button ladders with the SAADC and leaves it + // enabled, so clear it. + if (NRF_SAADC->ENABLE != 0) { + NRF_SAADC->EVENTS_STOPPED = 0; + NRF_SAADC->TASKS_STOP = 1; + wait_for_event(&NRF_SAADC->EVENTS_STOPPED); + NRF_SAADC->INTENCLR = 0xFFFFFFFF; + NRF_SAADC->EVENTS_END = 0; + NRF_SAADC->EVENTS_STARTED = 0; + NRF_SAADC->EVENTS_CALIBRATEDONE = 0; + NRF_SAADC->ENABLE = 0; + } +} + +// Pins that must not float, re-applied after every reset_all_pins(). +// None of these are marked never-reset, so Python can +// still claim them. Done so that the resting +// state between runs is a defined, safe one. +void board_reset_pin_defaults(void) { + // 3.072 MHz oscillator enable. Held low: it draws current straight through + // SYSTEM_OFF, so a floating pin here would drain battery + nrf_gpio_cfg_output(PIN_OSC_EN); + nrf_gpio_pin_clear(PIN_OSC_EN); + + // Codecs and the Bluetooth module held in reset (all active low) so that + // nothing downstream of us starts making noise or driving a shared bus on + // its own. P0.09/P0.10 are the NFC pins, and this board deliberately does + // *not* build with CONFIG_NFCT_PINS_AS_GPIOS (mpconfigboard.mk), because + // that option writes UICR. + nrf_gpio_cfg_output(PIN_TAS_RESET); + nrf_gpio_pin_clear(PIN_TAS_RESET); + nrf_gpio_cfg_output(PIN_CS42_RESET); + nrf_gpio_pin_clear(PIN_CS42_RESET); + nrf_gpio_cfg_output(PIN_BT_RESET); + nrf_gpio_pin_clear(PIN_BT_RESET); + + // eMMC held in reset with its VCCQ rail off. The albums live on the chip + // itself and are unaffected; this only keeps the rail from floating. + if (!sp1emmc_is_automounted()) { + nrf_gpio_cfg_output(PIN_EMMC_RESET); + nrf_gpio_pin_clear(PIN_EMMC_RESET); + nrf_gpio_cfg_output(PIN_EMMC_VCCQ_EN); + nrf_gpio_pin_clear(PIN_EMMC_VCCQ_EN); + } + + // Rail feeding the faders and both button ladders. Off unless something is + // actually reading them. + nrf_gpio_cfg_output(PIN_CONTROL_RAIL); + nrf_gpio_pin_clear(PIN_CONTROL_RAIL); + + // Both LED rows off except the first-light heartbeat status blink, if it is still + // running. main() calls reset_all_pins() immediately after port_init(), so + // without this the heartbeat would last microseconds and show nothing. + for (size_t i = 0; i < MP_ARRAY_SIZE(led_pins); i++) { + if (heartbeat_lit && led_pins[i] == PIN_LED_HEARTBEAT) { + continue; + } + nrf_gpio_cfg_output(led_pins[i]); + nrf_gpio_pin_clear(led_pins[i]); + } + + // Function button: the only GPIO button, active low, and the only wake + // source out of SYSTEM_OFF. Keep it readable at all times, the + // supervisor's power-off gesture depends on it. + nrf_gpio_cfg_input(PIN_FUNCTION_BUTTON, NRF_GPIO_PIN_PULLUP); + + // BQ24232 charge enable, active low: drive it low so a plugged-in device + // charges. P1.00 (CHARGE_ISET) is deliberately left untouched. + // It is the charge-current programming node (ICHG = 870 AΩ / RISET) and + // doubles as the current monitor. + nrf_gpio_cfg_output(PIN_CHARGE_ENABLE); + nrf_gpio_pin_clear(PIN_CHARGE_ENABLE); +} + +// Called once at start up, after the filesystem is mounted and immediately +// before the USB workflow starts. Where we end the heartbeat status blink. +void board_init(void) { + heartbeat_lit = false; + nrf_gpio_pin_clear(PIN_LED_HEARTBEAT); +} + +void board_background_task(void) { + bootloader_wdt_feed(); + + #ifdef BOARD_POWER_OFF_BUTTON_PIN + // Never returns if the hold completes. + power_off_tick(); + #endif +} + +// -- muting the codecs on the way out -------------------------------------- +// +// Dropping the reset lines and the oscillator (board_reset_pin_defaults()) is +// enough to make the board quiet and to save the battery, but it cuts both +// codecs off mid-signal: the CS42L42 loses its clock and the TAS2505's class-D +// driver loses its reset with whatever was on the output still on it. +// +// Bit-banged rather than driven through TWIM. + +#define I2C_HALF_PERIOD_ITERATIONS (100) // ~8 us at 64 MHz; slow is fine +#define I2C_STRETCH_TIMEOUT_ITERATIONS (20000) + +static void i2c_delay(void) { + for (volatile uint32_t i = 0; i < I2C_HALF_PERIOD_ITERATIONS; i++) { + } +} + +// Open drain, input buffer connected so ACK and clock stretching are readable. +// The internal pull-up is additional safety next to the board's own; it is +// removed again by i2c_release() so nothing pulls current in SYSTEM_OFF. +static void i2c_cfg_pin(uint32_t pin) { + nrf_gpio_cfg(pin, NRF_GPIO_PIN_DIR_OUTPUT, NRF_GPIO_PIN_INPUT_CONNECT, + NRF_GPIO_PIN_PULLUP, NRF_GPIO_PIN_S0D1, NRF_GPIO_PIN_NOSENSE); +} + +static void i2c_claim(void) { + // Whatever owned these pins through TWIM keeps driving them while the + // peripheral is enabled, PIN_CNF notwithstanding. TWIM0/TWIM1 share their + // base addresses with SPIM0/SPIM1, so both are disabled here. + // + // On the power-off path nothing after this returns to user code, so that + // is free. On the soft-reset path, reset_board(), the VM + // *does* come back -- and it is still free, because a busio object cannot + // survive that reset. i2c_release() below puts the + // pins back to their reset configuration, internal pull-ups and all. + NRF_TWIM0->ENABLE = 0; + NRF_TWIM1->ENABLE = 0; + __DSB(); + nrf_gpio_pin_set(PIN_I2C_SDA); + nrf_gpio_pin_set(PIN_I2C_SCL); + i2c_cfg_pin(PIN_I2C_SDA); + i2c_cfg_pin(PIN_I2C_SCL); + i2c_delay(); +} + +static void i2c_release(void) { + nrf_gpio_cfg_default(PIN_I2C_SDA); + nrf_gpio_cfg_default(PIN_I2C_SCL); +} + +// Raise SCL and wait for it to actually read high, so a codec stretching the +// clock is honoured. A device holding SCL down forever must not be +// able to hold the whole power-off sequence, so we give up and carry on. The +// transfer is then garbage, which the caller finds out about at the next ACK. +static void i2c_scl_high(void) { + nrf_gpio_pin_set(PIN_I2C_SCL); + for (uint32_t i = 0; i < I2C_STRETCH_TIMEOUT_ITERATIONS && + nrf_gpio_pin_read(PIN_I2C_SCL) == 0; i++) { + __NOP(); + } + i2c_delay(); +} + +static void i2c_scl_low(void) { + nrf_gpio_pin_clear(PIN_I2C_SCL); + i2c_delay(); +} + +static void i2c_start(void) { + nrf_gpio_pin_set(PIN_I2C_SDA); + i2c_scl_high(); + nrf_gpio_pin_clear(PIN_I2C_SDA); + i2c_delay(); + i2c_scl_low(); +} + +static void i2c_stop(void) { + nrf_gpio_pin_clear(PIN_I2C_SDA); + i2c_delay(); + i2c_scl_high(); + nrf_gpio_pin_set(PIN_I2C_SDA); + i2c_delay(); +} + +// Returns true if the slave ACKed. +static bool i2c_write_byte(uint8_t value) { + for (uint8_t bit = 0; bit < 8; bit++) { + if (value & 0x80) { + nrf_gpio_pin_set(PIN_I2C_SDA); + } else { + nrf_gpio_pin_clear(PIN_I2C_SDA); + } + value <<= 1; + i2c_delay(); + i2c_scl_high(); + i2c_scl_low(); + } + nrf_gpio_pin_set(PIN_I2C_SDA); // release for the ACK bit + i2c_delay(); + i2c_scl_high(); + bool acked = nrf_gpio_pin_read(PIN_I2C_SDA) == 0; + i2c_scl_low(); + return acked; +} + +// two-byte write +static bool i2c_write2(uint8_t address, uint8_t first, uint8_t second) { + i2c_start(); + bool ok = i2c_write_byte(address << 1) && + i2c_write_byte(first) && + i2c_write_byte(second); + i2c_stop(); + return ok; +} + +// Register addresses +#define PAGE_SELECT_REG (0x00) // register 0 selects the page, on both + +#define CS42L42_ADDRESS (0x48) +#define CS_HP_CTL_PAGE (0x20) // CS_HP_CTL = 0x2001, page = high byte +#define CS_HP_CTL_REG (0x01) +#define CS_HP_MUTE (0x0D) + +#define TAS2505_ADDRESS (0x18) +#define TAS_SW_RESET (0x01) // page 0 +#define TAS_DAC_MUTE (0x40) // page 0 +#define TAS_MUTED (0x0C) +#define TAS_SPK_POWER (0x2D) // page 1 + +// Mute the CS42L42, then mute the TAS2505, power its class-D driver down and +// soft-reset it. +static void quiesce_codecs(void) { + bootloader_wdt_feed(); + i2c_claim(); + + if (i2c_write2(CS42L42_ADDRESS, PAGE_SELECT_REG, CS_HP_CTL_PAGE)) { + i2c_write2(CS42L42_ADDRESS, CS_HP_CTL_REG, CS_HP_MUTE); + } + + if (i2c_write2(TAS2505_ADDRESS, PAGE_SELECT_REG, 0x00)) { + i2c_write2(TAS2505_ADDRESS, TAS_DAC_MUTE, TAS_MUTED); + if (i2c_write2(TAS2505_ADDRESS, PAGE_SELECT_REG, 0x01)) { + i2c_write2(TAS2505_ADDRESS, TAS_SPK_POWER, 0x00); + } + // Back to page 0 for the software reset + if (i2c_write2(TAS2505_ADDRESS, PAGE_SELECT_REG, 0x00)) { + i2c_write2(TAS2505_ADDRESS, TAS_SW_RESET, 0x01); + } + } + + i2c_release(); + bootloader_wdt_feed(); +} + +void reset_board(void) { + quiesce_codecs(); +} + +// The board half of the power-off sequence +// Runs with the Function button still held, before the wake is armed. +void board_power_off_prepare(void) { + quiesce_codecs(); + + // Codecs into reset, CS42L42 first: it drives the I2S frames, so + // stopping it stops the bus the TAS2505 is listening to. + nrf_gpio_cfg_output(PIN_CS42_RESET); + nrf_gpio_pin_clear(PIN_CS42_RESET); + nrf_gpio_cfg_output(PIN_TAS_RESET); + nrf_gpio_pin_clear(PIN_TAS_RESET); + + // eMMC I/O rail, before the oscillator, so nothing is left half-powered + // against a clock that has stopped. + nrf_gpio_cfg_output(PIN_EMMC_RESET); + nrf_gpio_pin_clear(PIN_EMMC_RESET); + nrf_gpio_cfg_output(PIN_EMMC_VCCQ_EN); + nrf_gpio_pin_clear(PIN_EMMC_VCCQ_EN); + + // 3.072 MHz oscillator + nrf_gpio_cfg_output(PIN_OSC_EN); + nrf_gpio_pin_clear(PIN_OSC_EN); + + // Everything else, LEDs included. + board_reset_pin_defaults(); +} diff --git a/ports/nordic/boards/teenage_engineering_sp1/mpconfigboard.h b/ports/nordic/boards/teenage_engineering_sp1/mpconfigboard.h new file mode 100644 index 00000000000..cdde601b076 --- /dev/null +++ b/ports/nordic/boards/teenage_engineering_sp1/mpconfigboard.h @@ -0,0 +1,84 @@ +// This file is part of the CircuitPython project: https://circuitpython.org +// +// SPDX-FileCopyrightText: Copyright (c) 2026 Tim Cocks for Adafruit Industries +// +// SPDX-License-Identifier: MIT + +#pragma once + +#include "nrfx/hal/nrf_gpio.h" + +#define MICROPY_HW_BOARD_NAME "Teenage Engineering SP-1" +#define MICROPY_HW_MCU_NAME "nRF52840" + +// Flash map. The bootloader owns 0x00000-0x20000 and jumps to 0x20000, so +// the vector table goes there. +// +// There is no bootloader in *high* flash either, so both bootloader sizes are +// zero and BOOTLOADER_START_ADDR collapses onto the bootloader settings page +// at 0xFF000. That page belongs to the bootloader and must never be +// written. +// +// 0x00000-0x20000 bootloader (never touched) +// 0x20000-0x21000 ISR/vector table +// 0x21000-0xBD000 firmware (624 KiB) +// 0xBD000-0xBF000 microcontroller.nvm (8 KiB) +// 0xBF000-0xFF000 CIRCUITPY internal-flash FAT (256 KiB) +// 0xFF000-0x100000 bootloader settings page (RESERVED) +#define ISR_START_ADDR (0x20000) +#define BOOTLOADER_SIZE (0) +#define BOOTLOADER_MBR_SIZE (0) + +// No 32.768 kHz crystal: P0.00 and P0.01 are playback LEDs. Use the RC +// oscillator for LFCLK. +#define BOARD_HAS_32KHZ_XTAL (0) + +// Power off. There is no reset pin, no power switch and no removable battery, +// so SYSTEM_OFF is the only "off" this device has. Waking from off is one +// of only two routes back to the bootloader. P0.27 (Function) is the only +// GPIO button and the only wake source; it is a plain switch to ground. +#define BOARD_POWER_OFF_BUTTON_PIN NRF_GPIO_PIN_MAP(0, 27) + +// The bootloader's DFU magic. The gate at 0x6b2 is 16 bits wide and +// split across both retention registers, +// +// GPREGRET | (GPREGRET2 << 8) == 0x7EB3 +// +// +// This bootloader has no separate UF2/OTA and serial-DFU requests. There is +// one gate, so both magics are the same pair and, reset_to_bootloader() and +// microcontroller.on_next_reset(RunMode.BOOTLOADER) both land in boot mode. +#define BOOTLOADER_DFU_MAGIC (0xB3) +#define BOOTLOADER_DFU_MAGIC2 (0x7E) +#define BOOTLOADER_UF2_MAGIC (0xB3) +#define BOOTLOADER_UF2_MAGIC2 (0x7E) + +// The bootloader starts a watchdog before our first instruction and locks +// its configuration, so the app can only reload it. See wdt.h. +// This is the device's escape hatch. With no reset pin and no +// way to remove the battery, a wedge that stops the main loop has to become a +// reset, because a reset is what runs the bootloader and re-opens the +// reflashing window. mpconfigboard.mk keeps CIRCUITPY_WATCHDOG off to match. +#define CIRCUITPY_BOOTLOADER_ARMED_WDT (1) + +// Hardware-write-protect everything outside the nvm and CIRCUITPY regions for +// the life of each boot. The FAT ends at 0xBF000 + 256 KiB = +// 0xFF000, which is exactly where the bootloader settings page begins. +#define CIRCUITPY_NRF_FLASH_PROTECT (1) + +// No SD card LUN. There is no card slot on this device. +#define CIRCUITPY_SDCARD_USB (0) + +// TWIM to the CS42L42 (0x48) and TAS2505 (0x18). +#define DEFAULT_I2C_BUS_SCL (&pin_P1_11) +#define DEFAULT_I2C_BUS_SDA (&pin_P1_07) + +// The CDC REPL is normally the only place status is visible. Borrow the first +// track LED for the supervisor status LED so that safe mode is legible on the +// device itself. It is claimed only while the supervisor is showing status and +// is released before user code runs, so board.LED_TRACK1 stays usable. +#define MICROPY_HW_LED_STATUS (&pin_P0_29) + +// Blink that same LED once, ~200 ms, the moment the power-off hold completes. +// The gesture is otherwise silent. +#define BOARD_POWER_OFF_CONFIRM_LED_PIN NRF_GPIO_PIN_MAP(0, 29) diff --git a/ports/nordic/boards/teenage_engineering_sp1/mpconfigboard.mk b/ports/nordic/boards/teenage_engineering_sp1/mpconfigboard.mk new file mode 100644 index 00000000000..9bfe06fce6e --- /dev/null +++ b/ports/nordic/boards/teenage_engineering_sp1/mpconfigboard.mk @@ -0,0 +1,84 @@ +USB_VID = 0x239A +USB_PID = 0x817A +USB_PRODUCT = "SP-1 (CircuitPython)" +USB_MANUFACTURER = "Teenage Engineering" + +MCU_CHIP = nrf52840 + +# No UICR write may survive into this board's image. UICR is undoable only by +# ERASEALL, which needs SWD; the SP-1's SWD pads are not easily reachable, and an +# ERASEALL would take the bootloader with it. So each of the three conditional +# UICR writes on the boot path is turned off here. +# +# 1. PSELRESET reads erased here, so CONFIG_GPIO_AS_PINRESET would burn +# PSELRESET[0..1] = 18 on the first boot, making P0.18 nRESET forever. This +# board has no reset pin and P0.18's wiring is unknown; if anything pulls +# that net low the chip is held in reset and unreachable even over SWD. +NRF_GPIO_AS_PINRESET = 0 + +# 2. REGOUT0 is unprogrammed but MAINREGSTATUS reads 0 (normal-voltage mode), +# so the write cannot fire. Off anyway: VDD is externally regulated to +# 3.3 V, which is what the eMMC, codecs and LEDs already run at. +NRF_REGOUT0_3V3 = 0 + +# 3. NFCPINS reads 0xFFFFFFFE, i.e. PROTECT already cleared, so the write +# cannot fire and P0.09/P0.10 work as TAS_RESET and BT_RESET. Off anyway: +# if that ever fails to hold, two dead reset lines are diagnosable and a +# UICR burn is not. +NRF_NFCT_PINS_AS_GPIOS = 0 + +# No SoftDevice. It would have to live at 0x1000, which is inside the +# bootloader, and the radio has no antenna. Empty SD also forces +# CIRCUITPY_BLEIO_NATIVE and the BLE workflow services to 0 (mpconfigport.mk). +SD = + +# CIRCUITPY is in internal flash; there is no external flash chip. +INTERNAL_FLASH_FILESYSTEM = 1 + +# No UF2 bootloader on this device. +CIRCUITPY_BUILD_EXTENSIONS = bin,hex + +# displayio off no display. +CIRCUITPY_DISPLAYIO = 0 +CIRCUITPY_FRAMEBUFFERIO = 0 +CIRCUITPY_RGBMATRIX = 0 +CIRCUITPY_SHARPDISPLAY = 0 +CIRCUITPY_IS31FL3741 = 0 +CIRCUITPY_VECTORIO = 0 + +# The watchdog is started by the bootloader before our first instruction and +# its config registers are locked. +CIRCUITPY_WATCHDOG = 0 + +# alarm's idle paths need a WDT-feed audit before they are safe here. +CIRCUITPY_ALARM = 0 + +# Audio +CIRCUITPY_AUDIOPWMIO = 0 +CIRCUITPY_SYNTHIO = 1 +CIRCUITPY_AUDIOEFFECTS = 1 +CIRCUITPY_AUDIOMP3 = 1 + +# Hold-to-power-off, opted into by BOARD_POWER_OFF_BUTTON_PIN in +# mpconfigboard.h. +SRC_C += boards/$(BOARD)/power_off.c + +# eMMC +CIRCUITPY_SP1EMMC = 1 + +# auto mount EMMC as /sd +CIRCUITPY_SP1EMMC_USB = 1 + +ifeq ($(CIRCUITPY_SP1EMMC),1) +CFLAGS += -DCIRCUITPY_SP1EMMC=1 +SRC_C += \ + boards/$(BOARD)/sp1emmc/sp1emmc.c \ + boards/$(BOARD)/bindings/sp1emmc/__init__.c \ + boards/$(BOARD)/bindings/sp1emmc/EMMC.c \ + +# USB mass storage automount switch +ifeq ($(CIRCUITPY_SP1EMMC_USB),1) +CFLAGS += -DSP1EMMC_AUTOMOUNT=1 +SRC_C += boards/$(BOARD)/sp1emmc/automount.c +endif +endif diff --git a/ports/nordic/boards/teenage_engineering_sp1/pins.c b/ports/nordic/boards/teenage_engineering_sp1/pins.c new file mode 100644 index 00000000000..66897f7514d --- /dev/null +++ b/ports/nordic/boards/teenage_engineering_sp1/pins.c @@ -0,0 +1,91 @@ +// This file is part of the CircuitPython project: https://circuitpython.org +// +// SPDX-FileCopyrightText: Copyright (c) 2026 Tim Cocks for Adafruit Industries +// +// SPDX-License-Identifier: MIT + +// Pin map from Tim Knapen's reverse-engineering of the Teenage Engineering SP-1 +// https://github.com/timknapen/SP-1-dev/wiki + +#include "shared-bindings/board/__init__.h" + +static const mp_rom_map_elem_t board_module_globals_table[] = { + CIRCUITPYTHON_BOARD_DICT_STANDARD_ITEMS + + // Four track faders, in physical left-to-right order. + { MP_ROM_QSTR(MP_QSTR_FADER1), MP_ROM_PTR(&pin_P0_05) }, + { MP_ROM_QSTR(MP_QSTR_FADER2), MP_ROM_PTR(&pin_P0_30) }, + { MP_ROM_QSTR(MP_QSTR_FADER3), MP_ROM_PTR(&pin_P0_04) }, + { MP_ROM_QSTR(MP_QSTR_FADER4), MP_ROM_PTR(&pin_P0_31) }, + + // Two resistor-ladder button rows, read as analog voltages. Both need + // LADDER_POWER driven high to read anything; the faders need it too. + // + // Rungs in ascending voltage: + // LADDER1: TRACK1, TRACK2, TRACK3, TRACK4, PLAY + // LADDER2: ROCKER-, VOL-, ROCKER+, VOL+ + // Both rows are the same resistor network; LADDER2 is LADDER1 with the + // lowest rung unpopulated, so one threshold table serves both. + // + // "ROCKER" is the left-side rocker switch + { MP_ROM_QSTR(MP_QSTR_LADDER1), MP_ROM_PTR(&pin_P0_02) }, + { MP_ROM_QSTR(MP_QSTR_LADDER2), MP_ROM_PTR(&pin_P0_03) }, + { MP_ROM_QSTR(MP_QSTR_LADDER_POWER), MP_ROM_PTR(&pin_P1_10) }, + + // The Function button ("••") is the only GPIO button, active low with a + // pull-up, and the only wake source out of SYSTEM_OFF. + { MP_ROM_QSTR(MP_QSTR_BUTTON), MP_ROM_PTR(&pin_P0_27) }, + + // Playback LED row on the side of the device, active high. + { MP_ROM_QSTR(MP_QSTR_LED_PLAY1), MP_ROM_PTR(&pin_P1_13) }, + { MP_ROM_QSTR(MP_QSTR_LED_PLAY2), MP_ROM_PTR(&pin_P0_00) }, + { MP_ROM_QSTR(MP_QSTR_LED_PLAY3), MP_ROM_PTR(&pin_P1_12) }, + { MP_ROM_QSTR(MP_QSTR_LED_PLAY4), MP_ROM_PTR(&pin_P0_01) }, + + // Track LED row above the track buttons, active high. + { MP_ROM_QSTR(MP_QSTR_LED_TRACK1), MP_ROM_PTR(&pin_P0_29) }, + { MP_ROM_QSTR(MP_QSTR_LED_TRACK2), MP_ROM_PTR(&pin_P0_26) }, + { MP_ROM_QSTR(MP_QSTR_LED_TRACK3), MP_ROM_PTR(&pin_P1_15) }, + { MP_ROM_QSTR(MP_QSTR_LED_TRACK4), MP_ROM_PTR(&pin_P1_14) }, + + // I2C to both codecs: CS42L42 headphone amp at 0x48, TAS2505 speaker amp + // at 0x18. + { MP_ROM_QSTR(MP_QSTR_SCL), MP_ROM_PTR(&pin_P1_11) }, + { MP_ROM_QSTR(MP_QSTR_SDA), MP_ROM_PTR(&pin_P1_07) }, + { MP_ROM_QSTR(MP_QSTR_I2C), MP_ROM_PTR(&board_i2c_obj) }, + + // Codec resets, both active low. + { MP_ROM_QSTR(MP_QSTR_TAS_RESET), MP_ROM_PTR(&pin_P0_09) }, + { MP_ROM_QSTR(MP_QSTR_CS42_RESET), MP_ROM_PTR(&pin_P0_15) }, + + // I2S + { MP_ROM_QSTR(MP_QSTR_I2S_DOUT), MP_ROM_PTR(&pin_P1_09) }, + { MP_ROM_QSTR(MP_QSTR_I2S_LRCLK), MP_ROM_PTR(&pin_P0_11) }, + { MP_ROM_QSTR(MP_QSTR_I2S_WORD_SELECT), MP_OBJ_FROM_PTR(&pin_P0_11) }, + { MP_ROM_QSTR(MP_QSTR_I2S_BCLK), MP_ROM_PTR(&pin_P0_12) }, + { MP_ROM_QSTR(MP_QSTR_I2S_BIT_CLOCK), MP_OBJ_FROM_PTR(&pin_P0_12) }, + { MP_ROM_QSTR(MP_QSTR_OSC_EN), MP_ROM_PTR(&pin_P0_13) }, + + // 4 GB eMMC + { MP_ROM_QSTR(MP_QSTR_EMMC_CLK), MP_ROM_PTR(&pin_P0_06) }, + { MP_ROM_QSTR(MP_QSTR_EMMC_DAT0), MP_ROM_PTR(&pin_P0_07) }, + { MP_ROM_QSTR(MP_QSTR_EMMC_CMD), MP_ROM_PTR(&pin_P0_08) }, + { MP_ROM_QSTR(MP_QSTR_EMMC_RESET), MP_ROM_PTR(&pin_P1_08) }, + { MP_ROM_QSTR(MP_QSTR_EMMC_VCCQ), MP_ROM_PTR(&pin_P0_14) }, + + // BQ24232 charger. CHARGE_ENABLE and the two status lines are active low. + { MP_ROM_QSTR(MP_QSTR_CHARGE_ISET), MP_ROM_PTR(&pin_P1_00) }, + { MP_ROM_QSTR(MP_QSTR_CHARGE_ENABLE), MP_ROM_PTR(&pin_P0_21) }, + { MP_ROM_QSTR(MP_QSTR_CHARGE_STATUS), MP_ROM_PTR(&pin_P0_22) }, + { MP_ROM_QSTR(MP_QSTR_POWER_GOOD), MP_ROM_PTR(&pin_P0_24) }, + + // Battery sense, AIN4, through a divider. + { MP_ROM_QSTR(MP_QSTR_VBATT), MP_ROM_PTR(&pin_P0_28) }, + { MP_ROM_QSTR(MP_QSTR_BATTERY), MP_ROM_PTR(&pin_P0_28) }, + { MP_ROM_QSTR(MP_QSTR_VOLTAGE_MONITOR), MP_ROM_PTR(&pin_P0_28) }, + + // CYBT-353027-02 Bluetooth module reset, active low + { MP_ROM_QSTR(MP_QSTR_BT_RESET), MP_ROM_PTR(&pin_P0_10) }, +}; + +MP_DEFINE_CONST_DICT(board_module_globals, board_module_globals_table); diff --git a/ports/nordic/boards/teenage_engineering_sp1/power_off.c b/ports/nordic/boards/teenage_engineering_sp1/power_off.c new file mode 100644 index 00000000000..7131073c63e --- /dev/null +++ b/ports/nordic/boards/teenage_engineering_sp1/power_off.c @@ -0,0 +1,172 @@ +// This file is part of the CircuitPython project: https://circuitpython.org +// +// SPDX-FileCopyrightText: Copyright (c) 2026 Tim Cocks for Adafruit Industries +// +// SPDX-License-Identifier: MIT + +#include "power_off.h" + +#ifdef BOARD_POWER_OFF_BUTTON_PIN + +#include "py/misc.h" + +#include "supervisor/flash.h" + +#include "wdt.h" +#include "nrfx/hal/nrf_gpio.h" +#include "nrfx/hal/nrf_power.h" + +#ifndef BOARD_POWER_OFF_HOLD_SECONDS +#define BOARD_POWER_OFF_HOLD_SECONDS (3) +#endif + +// Timings in RTC subticks (32.768 kHz). The counter is 24 bits, so every +// comparison is masked; it wraps every 512s, longer than needed. +#define RTC_COUNTER_MASK (0xFFFFFF) +#define POLL_INTERVAL_SUBTICKS (1024) // ~31 ms +#define POWER_OFF_HOLD_SUBTICKS (BOARD_POWER_OFF_HOLD_SECONDS * 32768) +#define RELEASE_DEBOUNCE_SUBTICKS (1638) // ~50 ms + +// The RTC that port.c runs the tick from, read straight out of its counter. +#define POWER_OFF_RTC (NRF_RTC2) + +// Do-nothing default; a board with hardware to quiesce overrides this. +MP_WEAK void board_power_off_prepare(void) { +} + +#ifdef BOARD_POWER_OFF_CONFIRM_LED_PIN + +#ifndef BOARD_POWER_OFF_CONFIRM_LED_MS +#define BOARD_POWER_OFF_CONFIRM_LED_MS (200) +#endif +#define CONFIRM_BLINK_SUBTICKS ((BOARD_POWER_OFF_CONFIRM_LED_MS) * 32768 / 1000) + +// One flash to say the gesture landed. +static void power_off_confirm_blink(void) { + nrf_gpio_cfg_output(BOARD_POWER_OFF_CONFIRM_LED_PIN); + nrf_gpio_pin_set(BOARD_POWER_OFF_CONFIRM_LED_PIN); + // Busy-wait on the same RTC the release loop uses, feeding the watchdog: + // 200 ms is comfortably longer than a bootloader-armed dog's patience. + uint32_t started = POWER_OFF_RTC->COUNTER; + while (((POWER_OFF_RTC->COUNTER - started) & RTC_COUNTER_MASK) < CONFIRM_BLINK_SUBTICKS) { + bootloader_wdt_feed(); + } + nrf_gpio_pin_clear(BOARD_POWER_OFF_CONFIRM_LED_PIN); +} + +#endif // BOARD_POWER_OFF_CONFIRM_LED_PIN + +// Power-off is a sequence, not a register write, and the order matters. +static void power_off(void) { + // 0. Commit the filesystem. Hold-to-power-off is this device's normal + // "off", so the dirty page sitting in the flash cache is typically the + // last thing FAT wrote. + supervisor_flash_flush(); + + // 1. Let the board put its own hardware to bed first, while everything is + // still powered and predictable. + board_power_off_prepare(); + + // 1a. Confirm the gesture with one flash, before anything else changes. + // Deliberately after the prepare hook, so it is drawing on a board that + // is already quiesced and the LED it leaves behind is off. + #ifdef BOARD_POWER_OFF_CONFIRM_LED_PIN + power_off_confirm_blink(); + #endif + + // 2. Detach from USB + NRF_USBD->USBPULLUP = 0; + NRF_USBD->ENABLE = 0; + + // 3. Wait for the button to be released, feeding the watchdog meanwhile. + uint32_t released_since = POWER_OFF_RTC->COUNTER; + while (true) { + bootloader_wdt_feed(); + uint32_t now = POWER_OFF_RTC->COUNTER; + if (nrf_gpio_pin_read(BOARD_POWER_OFF_BUTTON_PIN) == 0) { + released_since = now; + } else if (((now - released_since) & RTC_COUNTER_MASK) >= RELEASE_DEBOUNCE_SUBTICKS) { + break; + } + } + + // 4. Clear RESETREAS so the next boot can tell a wake-from-off from a + // watchdog reset. + NRF_POWER->RESETREAS = NRF_POWER->RESETREAS; + + // 5. Arm the wake. Clear any latched DETECT first. + NRF_P0->LATCH = 0xFFFFFFFF; + NRF_P1->LATCH = 0xFFFFFFFF; + nrf_gpio_cfg_sense_input(BOARD_POWER_OFF_BUTTON_PIN, + NRF_GPIO_PIN_PULLUP, NRF_GPIO_PIN_SENSE_LOW); + + // 6. Off. Note that the spin below deliberately does not feed the + // watchdog. If SYSTEM_OFF does not take a bootloader-armed dog + // bites within seconds and the board comes back up normally. + __DSB(); + NRF_POWER->SYSTEMOFF = 1; + __DSB(); + while (true) { + } +} + +// Reading a pin whose input buffer is disconnected returns 0, which is +// indistinguishable from the button being held. power_off_tick() would see a +// button that is down on the very first poll and never released, so the gesture +// would arm itself off permanently and the board would silently lose its only +// way to power down. +// +// So check the buffer every poll and reconnect it if it +// has gone away. Anything already configured is left exactly as it is. +static void ensure_input_buffer_connected(void) { + uint32_t pin_number = BOARD_POWER_OFF_BUTTON_PIN; + NRF_GPIO_Type *reg = nrf_gpio_pin_port_decode(&pin_number); + if ((reg->PIN_CNF[pin_number] & GPIO_PIN_CNF_INPUT_Msk) == + (GPIO_PIN_CNF_INPUT_Disconnect << GPIO_PIN_CNF_INPUT_Pos)) { + // Pull-up, matching the active-low switch-to-ground the header + // documents. Without a pull the line floats and the read is noise. + nrf_gpio_cfg_input(BOARD_POWER_OFF_BUTTON_PIN, NRF_GPIO_PIN_PULLUP); + } +} + +void power_off_tick(void) { + uint32_t now = POWER_OFF_RTC->COUNTER; + static uint32_t last_poll_subticks; + if (((now - last_poll_subticks) & RTC_COUNTER_MASK) < POLL_INTERVAL_SUBTICKS) { + return; + } + last_poll_subticks = now; + + ensure_input_buffer_connected(); + + // Reading IN never disturbs the pin, so the gesture still works if user + // code has claimed the button. + bool pressed = nrf_gpio_pin_read(BOARD_POWER_OFF_BUTTON_PIN) == 0; + + // Waking from SYSTEM_OFF happens with the button still held, and the + // bootloader plus start up take far less than the hold time, so a fresh + // boot would otherwise see a hold already in progress and power straight + // back off. Require the button to be seen released once first. + static bool gesture_armed; + static bool was_pressed; + if (!pressed) { + gesture_armed = true; + was_pressed = false; + return; + } + if (!gesture_armed) { + return; + } + + static uint32_t press_started_subticks; + if (!was_pressed) { + was_pressed = true; + press_started_subticks = now; + return; + } + if (((now - press_started_subticks) & RTC_COUNTER_MASK) >= POWER_OFF_HOLD_SUBTICKS) { + power_off(); + } +} + +#endif // BOARD_POWER_OFF_BUTTON_PIN diff --git a/ports/nordic/boards/teenage_engineering_sp1/power_off.h b/ports/nordic/boards/teenage_engineering_sp1/power_off.h new file mode 100644 index 00000000000..c99be1c44a3 --- /dev/null +++ b/ports/nordic/boards/teenage_engineering_sp1/power_off.h @@ -0,0 +1,43 @@ +// This file is part of the CircuitPython project: https://circuitpython.org +// +// SPDX-FileCopyrightText: Copyright (c) 2026 Tim Cocks for Adafruit Industries +// +// SPDX-License-Identifier: MIT + +#pragma once + +// A supervisor-level power-off gesture, for a board whose only "off" is +// SYSTEM_OFF. +// +// A board opts in by defining BOARD_POWER_OFF_BUTTON_PIN in mpconfigboard.h to +// an active-low, switch-to-ground button that is also the wake source. Holding +// it for BOARD_POWER_OFF_HOLD_SECONDS then powers the board down; pressing it +// again wakes the chip through a reset, which on a board with a bootloader +// means the bootloader runs first. +// +// A board may also define BOARD_POWER_OFF_CONFIRM_LED_PIN to an active-high +// LED, which is flashed once (BOARD_POWER_OFF_CONFIRM_LED_MS, default 200) as +// soon as the hold completes. Without it the gesture is silent, which on a +// screen-less board leaves no way to tell a successful power-off from a hold +// that was a moment too short. +// +// The board does not have to configure that pin. If its input buffer is found +// disconnected the poll reconnects it with a pull-up. A board is still free to +// configure it, and anything already configured is left alone. + + +#include "py/mpconfig.h" + +#ifdef BOARD_POWER_OFF_BUTTON_PIN + +// Poll the button and, if it has been held long enough, power off (never +// returns). Called from the board's board_background_task(), i.e. from every +// RUN_BACKGROUND_TASKS. +void power_off_tick(void); + +// Put the board's own hardware into its off state: rails down, resets +// asserted, anything that would drain a battery through SYSTEM_OFF switched +// off. +void board_power_off_prepare(void); + +#endif diff --git a/ports/nordic/boards/teenage_engineering_sp1/sp1emmc/automount.c b/ports/nordic/boards/teenage_engineering_sp1/sp1emmc/automount.c new file mode 100644 index 00000000000..4298978cdbd --- /dev/null +++ b/ports/nordic/boards/teenage_engineering_sp1/sp1emmc/automount.c @@ -0,0 +1,117 @@ +// This file is part of the CircuitPython project: https://circuitpython.org +// +// SPDX-FileCopyrightText: Copyright (c) 2026 Tim Cocks for Adafruit Industries +// +// SPDX-License-Identifier: MIT + +#include "sp1emmc/automount.h" + +#if defined(SP1EMMC_AUTOMOUNT) && SP1EMMC_AUTOMOUNT + +#include "py/mpstate.h" + +#include "extmod/vfs.h" +#include "extmod/vfs_fat.h" +#include "lib/oofatfs/ff.h" + +#include "supervisor/filesystem.h" +#include "supervisor/shared/safe_mode.h" +#include "supervisor/shared/settings.h" + +#include "sp1emmc/sp1emmc.h" + + +static mp_vfs_mount_t _emmc_vfs; +static fs_user_mount_t _emmc_usermount; + +static bool _tried; +static sp1emmc_automount_status_t _status = SP1EMMC_AUTOMOUNT_NOT_TRIED; + +#define AUTOMOUNT_BUDGET_US 5000000u + +// One word of RAM that survives a reset but not a power cycle. If it is still +// set when we get here, the previous boot faulted. Skip the card for this +// boot so the board enumerates, and clear the crumb so the next boot tries again. +#define AUTOMOUNT_CRUMB_MAGIC 0x5350314du // 'SP1M' + +static struct { + uint32_t magic; + uint32_t in_progress; +} _crumb __attribute__((section(".uninitialized"))); + +static void automount_give_up(sp1emmc_automount_status_t status) { + emmc_deadline_clear(); + // Leave the card powered down and the pins released + sp1emmc_automount_abandon(); + _crumb.in_progress = 0; + _status = status; +} + +void sp1emmc_automount(void) { + if (_tried) { + return; + } + _tried = true; + + if (get_safe_mode() != SAFE_MODE_NONE) { + _status = SP1EMMC_AUTOMOUNT_SAFE_MODE; + return; + } + + bool enabled = true; + (void)settings_get_bool("CIRCUITPY_EMMC_USB", &enabled); + if (!enabled) { + _status = SP1EMMC_AUTOMOUNT_DISABLED; + return; + } + + if (_crumb.magic == AUTOMOUNT_CRUMB_MAGIC && _crumb.in_progress != 0) { + _crumb.in_progress = 0; + _status = SP1EMMC_AUTOMOUNT_SKIPPED_AFTER_FAULT; + return; + } + _crumb.magic = AUTOMOUNT_CRUMB_MAGIC; + _crumb.in_progress = 1; + + emmc_deadline_set(AUTOMOUNT_BUDGET_US); + + mp_obj_t dev = sp1emmc_automount_construct(true, true); + if (dev == MP_OBJ_NULL) { + automount_give_up(SP1EMMC_AUTOMOUNT_NO_CARD); + return; + } + + fs_user_mount_t *vfs = &_emmc_usermount; + vfs->base.type = &mp_fat_vfs_type; + vfs->fatfs.drv = vfs; + // Initialise underlying block device. + vfs->blockdev.block_size = FF_MIN_SS; + mp_vfs_blockdev_init(&vfs->blockdev, dev); + + if (f_mount(&vfs->fatfs) != FR_OK) { + automount_give_up(SP1EMMC_AUTOMOUNT_NO_FILESYSTEM); + return; + } + + // Same as CIRCUITPY: while a host has the drive, the host owns writing. + filesystem_set_concurrent_write_protection(vfs, true); + filesystem_set_writable_by_usb(vfs, true); + + mp_vfs_mount_t *emmc_vfs = &_emmc_vfs; + emmc_vfs->str = SP1EMMC_AUTOMOUNT_PATH; + emmc_vfs->len = sizeof(SP1EMMC_AUTOMOUNT_PATH) - 1; + emmc_vfs->obj = MP_OBJ_FROM_PTR(&_emmc_usermount); + emmc_vfs->next = MP_STATE_VM(vfs_mount_table); + MP_STATE_VM(vfs_mount_table) = emmc_vfs; + + // The budget covers bring-up and the mount only + emmc_deadline_clear(); + _crumb.in_progress = 0; + _status = SP1EMMC_AUTOMOUNT_OK; +} + +sp1emmc_automount_status_t sp1emmc_automount_get_status(void) { + return _status; +} + +#endif // SP1EMMC_AUTOMOUNT diff --git a/ports/nordic/boards/teenage_engineering_sp1/sp1emmc/automount.h b/ports/nordic/boards/teenage_engineering_sp1/sp1emmc/automount.h new file mode 100644 index 00000000000..faa9800d697 --- /dev/null +++ b/ports/nordic/boards/teenage_engineering_sp1/sp1emmc/automount.h @@ -0,0 +1,47 @@ +// This file is part of the CircuitPython project: https://circuitpython.org +// +// SPDX-FileCopyrightText: Copyright (c) 2026 Tim Cocks for Adafruit Industries +// +// SPDX-License-Identifier: MIT + +#pragma once + +#include "py/obj.h" + +#if defined(SP1EMMC_AUTOMOUNT) && SP1EMMC_AUTOMOUNT + +#ifndef SP1EMMC_AUTOMOUNT_PATH +#define SP1EMMC_AUTOMOUNT_PATH "/sd" +#endif + +// Why /sd is or is not there, so a boot that skipped the card can say so +// instead of leaving the user to guess. Reported by sp1emmc.automount_status(). +typedef enum { + SP1EMMC_AUTOMOUNT_NOT_TRIED = 0, + SP1EMMC_AUTOMOUNT_OK, + SP1EMMC_AUTOMOUNT_DISABLED, // CIRCUITPY_EMMC_USB = 0 + SP1EMMC_AUTOMOUNT_SAFE_MODE, + SP1EMMC_AUTOMOUNT_NO_CARD, // bring-up failed or timed out + SP1EMMC_AUTOMOUNT_NO_FILESYSTEM, // card came up, f_mount refused it + SP1EMMC_AUTOMOUNT_SKIPPED_AFTER_FAULT, // last boot died in here +} sp1emmc_automount_status_t; + +void sp1emmc_automount(void); + +bool sp1emmc_is_automounted(void); + +sp1emmc_automount_status_t sp1emmc_automount_get_status(void); + +mp_obj_t sp1emmc_automount_construct(bool high_speed, bool write_enabled); + +void sp1emmc_automount_abandon(void); + +#else + +#include + +static inline bool sp1emmc_is_automounted(void) { + return false; +} + +#endif diff --git a/ports/nordic/boards/teenage_engineering_sp1/sp1emmc/sp1emmc.c b/ports/nordic/boards/teenage_engineering_sp1/sp1emmc/sp1emmc.c new file mode 100644 index 00000000000..e9fc0f00706 --- /dev/null +++ b/ports/nordic/boards/teenage_engineering_sp1/sp1emmc/sp1emmc.c @@ -0,0 +1,837 @@ +// This file is part of the CircuitPython project: https://circuitpython.org +// +// SPDX-FileCopyrightText: Copyright (c) 2026 Tim Cocks for Adafruit Industries +// +// SPDX-License-Identifier: MIT + +// ============================================================================ +// SP-1 eMMC flash driver (1-bit MMC protocol over the nRF52840) +// Read and write paths; writing is gated at runtime by +// EMMC(write_enabled=True). +// ============================================================================ +// Two layers: +// +// * COMMAND / control phases (init, CMD17/18 headers, busy polling) are +// bit-banged on GPIO. They are short and timing-insensitive. +// +// * The 512-byte DATA payloads ride SPIM3 + EasyDMA at 16 MHz. eMMC DAT0 at +// default speed is SPI-mode-0 compatible: the host launches data while +// CLK is low, the card samples (and launches) on the rising edge, MSB +// first. The start-bit hunt is bit-banged, then the payload + CRC16 is +// exactly byte-aligned for one RX DMA; on ENABLE=0 the pins fall back to +// their GPIO latches, so the surrounding bit-bang continues seamlessly. +// +// INTEGRITY: every block read is verified against the card's CRC16 and the +// caller retries on a mismatch. +// +// ============================================================================ + +#include "sp1emmc.h" +#include "sp1emmc_hw.h" + +#include + +#include "extmod/vfs.h" // MP_BLOCKDEV_IOCTL_* +#include "py/mphal.h" +#include "py/runtime.h" // RUN_BACKGROUND_TASKS +#include "shared-bindings/microcontroller/__init__.h" +#include "wdt.h" + +#define CMD_SAFE_HALF_US 1u // slow clock for the IDENTIFICATION phase only + +// Command-phase half-period: starts safe (eMMC identification requires a slow +// clock), switched to 0 (full-speed bit-bang, ~1-2 MHz) once init completes. +static uint32_t s_cmd_half_us = CMD_SAFE_HALF_US; + +volatile uint32_t g_emmc_clk_half_us = CMD_SAFE_HALF_US; + +sp1emmc_state_t g_emmc_state; + +static bool s_ready; +static uint32_t s_rca; +static uint32_t s_block_count; // from EXT_CSD SEC_COUNT; 0 = not read yet +static uint8_t s_device_type; // from EXT_CSD[196]; 0 = not read yet + +// One 512-byte block + its CRC16, byte-aligned, for the RX DMA. +static uint8_t s_dma_rx[EMMC_BLOCK_SIZE + 2]; + +// ---- bounded-wait helpers -------------------------------------------------- +// The 32768 Hz counter is 24-bit, so every elapsed calculation masks. +#define US_TO_TICKS(us) ((uint32_t)(((uint64_t)(us) * EMMC_TICKS_HZ + 999999u) / 1000000u)) + +static inline uint32_t ticks_since(uint32_t t0) { + return ticks_since_raw(t0); +} + +static inline void half_delay(uint32_t us) { + if (us) { + common_hal_mcu_delay_us(us); + } +} + +static bool s_deadline_armed; +static uint32_t s_deadline_t0; +static uint32_t s_deadline_lim; + +void emmc_deadline_set(uint32_t timeout_us) { + s_deadline_t0 = EMMC_TICKS(); + s_deadline_lim = US_TO_TICKS(timeout_us); + s_deadline_armed = true; +} + +void emmc_deadline_clear(void) { + s_deadline_armed = false; +} + +bool emmc_deadline_expired(void) { + return s_deadline_armed && ticks_since(s_deadline_t0) >= s_deadline_lim; +} + +// Long-wait service: feed the bootloader's dog and run background tasks +static inline void emmc_yield(void) { + RUN_BACKGROUND_TASKS; + bootloader_wdt_feed(); +} + +// Safe clock pulse for command/CRC phases. +static inline void clk_pulse(void) { + CLK_HIGH(); + half_delay(s_cmd_half_us); + CLK_LOW(); + half_delay(s_cmd_half_us); +} + +static void cmd_send_bit(uint8_t bit) { + // caller (send_command) sets CMD_OUT() once. + if (bit) { + CMD_HIGH(); + } else { + CMD_LOW(); + } + clk_pulse(); +} + +// SAMPLE POINT: read the line at the END of the low phase, i.e. before this +// bit's clock pulse, not in the middle of it. That is the one point in the +// cycle where BOTH of the card's timing modes hold valid data, which is what +// makes this path work either side of an HS_TIMING switch: +// +// * backward-compatible timing: the card launches on the FALLING edge and +// holds the bit until the next one, so the whole low phase is valid. +// tOSU(min) = tWL(min) - tODLY, data good from ~8 ns after the edge. +// We read a full low phase later. +// * high-speed timing: the card launches on the RISING edge (tODLY, 13.7 ns +// max, referenced to it) and holds until the next rising edge, so the low +// phase is again inside the window. +// +static uint8_t cmd_recv_bit(void) { + // caller sets CMD_IN() once before the response read + uint8_t b = (uint8_t)READ_CMD(); + clk_pulse(); + return b; +} + +static uint8_t crc7(const uint8_t *data, uint8_t len) { + uint8_t crc = 0; + for (uint8_t i = 0; i < len; i++) { + uint8_t v = data[i]; + for (int b = 7; b >= 0; b--) { + crc <<= 1; + if (((v >> b) & 1) ^ ((crc >> 7) & 1)) { + crc ^= 0x09; + } + crc &= 0x7F; + } + } + return (crc << 1) | 1; +} + +// Table-driven CRC16-CCITT +static uint16_t s_crc16_tab[256]; +static void crc16_tab_init(void) { + for (uint32_t i = 0; i < 256; i++) { + uint16_t crc = (uint16_t)(i << 8); + for (int b = 0; b < 8; b++) { + crc = (crc & 0x8000) ? (uint16_t)((crc << 1) ^ 0x1021) : (uint16_t)(crc << 1); + } + s_crc16_tab[i] = crc; + } +} + +// The port builds at -Os, so this is an opt-up. crc16 is pure computation, +// the level can only change its speed, never its value. +__attribute__((optimize("O2"))) +static uint16_t crc16(const uint8_t *data, uint32_t len) { + uint16_t crc = 0; + for (uint32_t i = 0; i < len; i++) { + crc = (uint16_t)((crc << 8) ^ s_crc16_tab[(crc >> 8) ^ data[i]]); + } + return crc; +} + +static bool send_command(uint8_t cmd_index, uint32_t arg, uint8_t *r1_out) { + uint8_t frame[6]; + frame[0] = 0x40 | (cmd_index & 0x3F); + frame[1] = (uint8_t)(arg >> 24); + frame[2] = (uint8_t)(arg >> 16); + frame[3] = (uint8_t)(arg >> 8); + frame[4] = (uint8_t)(arg); + frame[5] = crc7(frame, 5); + + // PRE-COMMAND GAP on an UNDRIVEN line + CMD_IN(); + for (int i = 0; i < 24; i++) { + clk_pulse(); + } + CMD_OUT(); + cmd_send_bit(0); + cmd_send_bit(1); + for (int b = 5; b >= 0; b--) { + cmd_send_bit((frame[0] >> b) & 1); + } + for (int i = 1; i <= 4; i++) { + for (int b = 7; b >= 0; b--) { + cmd_send_bit((frame[i] >> b) & 1); + } + } + for (int b = 7; b >= 1; b--) { + cmd_send_bit((frame[5] >> b) & 1); + } + cmd_send_bit(1); + + CMD_IN(); + bool responded = false; + for (int t = 0; t < 200; t++) { + clk_pulse(); + if (!READ_CMD()) { + responded = true; + break; + } + } + if (!responded) { + return false; + } + + if (!r1_out) { + return true; + } + + uint8_t resp[6] = {0}; + for (int i = 0; i < 38; i++) { + uint8_t bit = cmd_recv_bit(); + resp[i / 8] |= (bit << (7 - (i % 8))); + } + memcpy(r1_out, resp, 6); + + // Leave CMD as an INPUT (pulled up) + return true; +} + +// Bit-banged MMC commands intermittently miss the response on the first try +// (settling after the previous command); retry until the card answers. +static bool send_command_retry(uint8_t cmd, uint32_t arg, uint8_t *r1_out, int tries) { + for (int t = 0; t < tries; t++) { + if (emmc_deadline_expired()) { + return false; + } + if (send_command(cmd, arg, r1_out)) { + return true; + } + if (t == 0) { + // First miss = the card still settling after the previous burst: a + // handful of idle clocks is all it needs. + for (int c = 0; c < 16; c++) { + clk_pulse(); + } + } else { + mp_hal_delay_ms(2); + } + } + return false; +} + +// DATA read: per-bit CLK toggle uses the configurable (possibly 0) half-period. +// -O2 opts up from the port's -Os default and is load-bearing: at -Os the GPIO +// and delay helpers stop being inlined and become calls inside the per-bit +// loop, where the instruction count is the bit timing. +__attribute__((optimize("O2"))) +static bool read_data_block(uint8_t *buf) { + const uint32_t hd = g_emmc_clk_half_us; + + DAT0_IN(); + // START-BIT HUNT + { + uint32_t t0 = EMMC_TICKS(); + const uint32_t lim = US_TO_TICKS(80000u); // 80 ms bound + const uint32_t yield_at = US_TO_TICKS(500u); + bool got_start = false; + for (;;) { + // This hunt samples in the HIGH phase and stays there in both + // timing modes + for (int burst = 0; burst < 64 && !got_start; burst++) { + RCLK_HIGH(); + half_delay(hd); + EDGE_SETTLE(); + if (!RDAT_GET()) { + got_start = true; // leave with RCLK HIGH (as before) + break; + } + RCLK_LOW(); + half_delay(hd); + } + uint32_t el = ticks_since(t0); + if (got_start) { + break; + } + if (el >= lim || emmc_deadline_expired()) { + return false; + } + if (el >= yield_at) { + emmc_yield(); + } + } + } + RCLK_LOW(); + half_delay(hd); + + // The start bit was just consumed by the bit-bang hunt above, so the + // remaining 512 data bytes + CRC16 are exactly byte-aligned. + sp1emmc_spim_xfer(NULL, 0, s_dma_rx, sizeof(s_dma_rx)); + memcpy(buf, s_dma_rx, EMMC_BLOCK_SIZE); + uint16_t card_crc = (uint16_t)(((uint16_t)s_dma_rx[EMMC_BLOCK_SIZE] << 8) | + s_dma_rx[EMMC_BLOCK_SIZE + 1]); + RCLK_HIGH(); + half_delay(hd); + RCLK_LOW(); + half_delay(hd); // end bit + bool crc_ok = crc16(buf, EMMC_BLOCK_SIZE) == card_crc; + DAT0_OUT(); + DAT0_HIGH(); + return crc_ok; // a mismatch: caller retries +} + +bool emmc_cmd13(uint8_t *r1_out) { + return send_command_retry(13, s_rca, r1_out, 8); +} + +// Clock out an R2 response and reassemble the CID. R2 framing: start(0) + +// transmission(0) + 6 reserved ones + CID[127:1] + end(1) = 136 bits. +static void drain_r2_cid(uint8_t *cid_out) { + uint8_t bits[136]; + for (int i = 0; i < 136; i++) { + bits[i] = cmd_recv_bit(); + } + memset(cid_out, 0, 16); + for (int i = 0; i < 128; i++) { + // bits[0] start, bits[1] transmission, bits[2..7] six reserved ones, + // bits[8..134] CID[127:1], bits[135] end bit + cid_out[i / 8] |= (uint8_t)(bits[8 + i] << (7 - (i % 8))); + } +} + +#define EMMC_POWER_OFF_MS 50u + +void emmc_power_cycle(void) { + sp1emmc_spim_deinit(); // SPIM3 must not drive DAT0 either + sp1emmc_pins_init(); + + RST_ASSERT(); + CLK_LOW(); + CMD_LOW(); + DAT0_OUT(); + DAT0_LOW(); + VCCQ_OFF(); + mp_hal_delay_ms(EMMC_POWER_OFF_MS); +} + +bool emmc_init(void) { + s_ready = false; + s_block_count = 0; + s_device_type = 0; + g_emmc_clk_half_us = CMD_SAFE_HALF_US; + s_cmd_half_us = CMD_SAFE_HALF_US; + memset(&g_emmc_state, 0, sizeof(g_emmc_state)); + g_emmc_state.cmd1_retries = -1; + + emmc_power_cycle(); + + sp1emmc_spim_init(); // hardware-clocked data path, at M16 + crc16_tab_init(); + + CLK_LOW(); + CMD_HIGH(); + DAT0_HIGH(); + + VCCQ_ON(); + mp_hal_delay_ms(10); + + RST_ASSERT(); + mp_hal_delay_ms(1); + RST_RELEASE(); + mp_hal_delay_ms(2); + + CMD_HIGH(); + for (int i = 0; i < 80; i++) { // 74+ clocks before the first command + clk_pulse(); + } + + send_command(0, 0x00000000, NULL); // CMD0 GO_IDLE (no response expected) + g_emmc_state.cmd0_sent = true; + mp_hal_delay_ms(1); + + // CMD1 SEND_OP_COND, arg 0x40FF8000: HCS=1 + uint8_t r3[6] = {0}; + for (int retry = 0; retry < 1000; retry++) { + bool ok = send_command(1, 0x40FF8000, r3); + bootloader_wdt_feed(); + mp_hal_delay_ms(1); + if (ok && (r3[1] & 0x80)) { // response seen AND busy bit set = ready + g_emmc_state.cmd1_retries = retry; + break; + } + if (emmc_deadline_expired()) { + break; + } + } + if (g_emmc_state.cmd1_retries < 0) { // card never responded ready -> stop + return false; + } + + for (int t = 0; t < 8; t++) { + g_emmc_state.cmd2_resp = send_command(2, 0, NULL); + if (g_emmc_state.cmd2_resp) { + drain_r2_cid(g_emmc_state.cid); + break; + } + mp_hal_delay_ms(2); + } + mp_hal_delay_ms(1); + + uint8_t r6[6] = {0}; + s_rca = 0x0001u << 16; + g_emmc_state.cmd3_resp = send_command_retry(3, s_rca, r6, 8); // SET_RELATIVE_ADDR + mp_hal_delay_ms(1); + + uint8_t r1[6] = {0}; + g_emmc_state.cmd7_resp = send_command_retry(7, s_rca, r1, 8); // SELECT_CARD + mp_hal_delay_ms(1); + g_emmc_state.cmd16_resp = send_command_retry(16, EMMC_BLOCK_SIZE, r1, 8); // SET_BLOCKLEN + mp_hal_delay_ms(1); + + // strict: ready only if the card actually selected AND accepted block length + s_ready = g_emmc_state.cmd7_resp && g_emmc_state.cmd16_resp; + if (s_ready) { + s_cmd_half_us = 0u; // identification done: full-speed commands + g_emmc_clk_half_us = 0u; + } + return s_ready; +} + +uint32_t emmc_block_count(void) { + return s_block_count; +} + +// The block-device ioctl +bool emmc_blockdev_ioctl(uint32_t op, uint32_t arg, uint32_t *out_value) { + (void)arg; + *out_value = 0; + switch (op) { + case MP_BLOCKDEV_IOCTL_INIT: + // The constructor already did the whole CMD0..CMD16 + EXT_CSD + // walk, or raised. 0 means "initialised"; a card that has since + // been deinited answers with the error the callers check for + // (s_ready), so a mount over a dead object fails at INIT rather + // than at the first read. + *out_value = s_ready ? 0u : 1u; + break; + case MP_BLOCKDEV_IOCTL_DEINIT: + case MP_BLOCKDEV_IOCTL_SYNC: + case MP_BLOCKDEV_IOCTL_BLOCK_ERASE: + break; + case MP_BLOCKDEV_IOCTL_BLOCK_COUNT: + *out_value = s_block_count; + break; + case MP_BLOCKDEV_IOCTL_BLOCK_SIZE: + *out_value = EMMC_BLOCK_SIZE; + break; + default: + return false; + } + return true; +} + +// Power-off: release the bus pins and cut the VCCQ I/O rail. +// The card is gone until the next emmc_init(). +void emmc_power_down(void) { + s_ready = false; + s_block_count = 0; + sp1emmc_spim_deinit(); + RST_ASSERT(); + sp1emmc_pins_release(); + VCCQ_OFF(); // rail off (pin stays an output) +} + +// CMD8 SEND_EXT_CSD: an ADTC (read) command -- the card responds R1, then +// sends a single 512-byte EXT_CSD data block on DAT0 exactly like CMD17. +// Read-only and safe. buf must be >= EMMC_BLOCK_SIZE. +bool emmc_read_ext_csd(uint8_t *buf) { + if (!s_ready) { + return false; + } + uint8_t r1[6]; + if (!send_command_retry(8, 0, r1, 8)) { + return false; + } + if (!read_data_block(buf)) { + return false; + } + // SEC_COUNT[215:212], little-endian. 0x00760000 on this part = 7,733,248 + // blocks; the value is the software LBA bound for every later read. + s_block_count = (uint32_t)buf[212] | ((uint32_t)buf[213] << 8) | + ((uint32_t)buf[214] << 16) | ((uint32_t)buf[215] << 24); + // DEVICE_TYPE[196] gates the HS_TIMING switch (bit 1 = 52 MHz supported; + // this part reads 0x57). + s_device_type = buf[196]; + return true; +} + +// ---- R1b / program busy on DAT0 -------------------------------------------- +// Shared by the CMD6 switch (below) and the write path (further down): the +// card pulls DAT0 low while it programs and releases it high when done, and it +// only advances on OUR clock, so the host must keep clocking for the card to +// get anywhere. + +#define EMMC_BUSY_LEADIN_CLOCKS 16 + +// run_bg picks the service call for a long stall +// true -- emmc_yield(): feed the dog AND run background tasks. That is +// where the power-off gesture lives. +// false -- bootloader_wdt_feed(): feed the dog ONLY. Used by every wait inside a +// write, so a gesture can never drop the rail around a card that +// is mid-program. Detection is deferred by at most one bounded +// wait (<=500 ms) against a 3 s hold; between blocks and between +// calls the gesture is live as usual. +static bool dat0_busy_wait(uint32_t timeout_us, bool run_bg) { + DAT0_IN(); // never drive against a busy card + for (int i = 0; i < EMMC_BUSY_LEADIN_CLOCKS; i++) { + clk_pulse(); + } + uint32_t t0 = EMMC_TICKS(); + const uint32_t lim = US_TO_TICKS(timeout_us); + for (;;) { + bool released = false; + for (int i = 0; i < 64 && !released; i++) { + CLK_HIGH(); + half_delay(s_cmd_half_us); + released = READ_DAT0() != 0; + CLK_LOW(); + half_delay(s_cmd_half_us); + } + uint32_t el = ticks_since(t0); + if (released) { + DAT0_OUT(); // back to the read path's resting state + DAT0_HIGH(); + return true; + } + if (el >= lim || emmc_deadline_expired()) { + // DAT0 STAYS AN INPUT on a timeout + return false; + } + if (run_bg) { + emmc_yield(); + } else { + bootloader_wdt_feed(); + } + } +} + +// CMD6 SWITCH argument: access 0b11 (WRITE_BYTE) | index 185 | value 1 | +// cmd_set 0 -> 0x03 B9 01 00. +#define EMMC_SWITCH_HS_TIMING_ARG 0x03B90100u +#define EMMC_EXT_CSD_HS_TIMING 185u +#define EMMC_EXT_CSD_DEVICE_TYPE 196u +#define EMMC_DEVICE_TYPE_HS52 0x02u + +// GENERIC_CMD6_TIME on this part is 0x05 = 50 ms. Ten times that is the bound. +#define EMMC_CMD6_BUSY_US 500000u + +// Poll CMD13 until the card is back in tran and ready for data. This is the +// authoritative "the switch finished" test, and it is also where SWITCH_ERROR +// (status bit 7) shows up if the card rejected the write. +static bool wait_tran_after_switch(uint32_t timeout_us) { + uint32_t t0 = EMMC_TICKS(); + const uint32_t lim = US_TO_TICKS(timeout_us); + for (;;) { + uint8_t r1[6]; + if (emmc_cmd13(r1)) { + uint32_t status = ((uint32_t)r1[1] << 24) | ((uint32_t)r1[2] << 16) | + ((uint32_t)r1[3] << 8) | (uint32_t)r1[4]; + if (status & (1u << 7)) { // SWITCH_ERROR: the card said no + g_emmc_state.hs_switch_error = true; + return false; + } + if (((status >> 9) & 0xFu) == 4u && ((status >> 8) & 1u)) { + return true; // tran + ready_for_data + } + } + if (ticks_since(t0) >= lim || emmc_deadline_expired()) { + return false; + } + emmc_yield(); + mp_hal_delay_ms(1); + } +} + +bool emmc_set_high_speed(void) { + if (!s_ready) { + return false; + } + // Gate on the card's own capability byte. + if (!(s_device_type & EMMC_DEVICE_TYPE_HS52)) { + return false; + } + g_emmc_state.hs_stage = 1; + + uint8_t r1[6]; + if (!send_command_retry(6, EMMC_SWITCH_HS_TIMING_ARG, r1, 8)) { + return false; + } + g_emmc_state.hs_stage = 2; + // run_bg = true: a CMD6 on a volatile byte has no in-flight card state a + // power-off gesture could damage, so this wait services them as the read + // path does. + if (!dat0_busy_wait(EMMC_CMD6_BUSY_US, true)) { + return false; + } + g_emmc_state.hs_stage = 3; + if (!wait_tran_after_switch(EMMC_CMD6_BUSY_US)) { + return false; + } + g_emmc_state.hs_stage = 4; + + // THE DATA PATH'S HALF OF THE SWITCH. HS_TIMING moves the edge the card + // launches DAT0 on, from falling to rising, so SPIM has to move its sample + // edge with it (CPHA=1) or every block after this point comes back shifted + // by a bit and fails its CRC16. The command path needs no such flag, + // cmd_recv_bit() reads at a point that is valid in both timings. But, + // SPIM samples on an edge, and an edge has to pick one. + // + // This happens BEFORE the readback, because the readback is itself a block + // read off a card that has already switched. + NRF_SPIM3->CONFIG = SPIM_CONFIG_MODE1; + + // Read the byte back AT THE OLD CLOCK. A card that ACKed the switch but did + // not take it would otherwise be met with a 32 MHz bus it never agreed to, + // and the only symptom would be CRC noise that looks like a wiring fault. + uint8_t ext_csd[EMMC_BLOCK_SIZE]; + if (!emmc_read_ext_csd(ext_csd) || + ext_csd[EMMC_EXT_CSD_HS_TIMING] != 1u) { + NRF_SPIM3->CONFIG = SPIM_CONFIG_MODE0; + return false; // still at M16, card still readable + } + g_emmc_state.hs_stage = 5; + + // Only now does the host clock move. The re-read is a smoke test of the + // faster bus with the integrity layer watching: if the first fast transfer + // cannot even fetch a block the card just served correctly, fall straight + // back. + NRF_SPIM3->FREQUENCY = SPIM_FREQ_M32; + if (!emmc_read_ext_csd(ext_csd) || ext_csd[EMMC_EXT_CSD_HS_TIMING] != 1u) { + // Back to the old CLOCK but NOT to the old phase: the card is in + // high-speed timing and stays there until the rail drops, and + // high-speed timing is specified from 0 Hz up. Mode 1 is how we talk + // to it at M16 now. + NRF_SPIM3->FREQUENCY = SPIM_FREQ_M16; + return false; + } + g_emmc_state.hs_active = true; + g_emmc_state.hs_stage = 6; + return true; +} + +bool emmc_read_blocks(uint32_t block_addr, uint8_t *buf, uint32_t count) { + if (!s_ready || count == 0) { + return false; + } + // Reject an out-of-range LBA before any command reaches the card + if (s_block_count != 0 && + (block_addr >= s_block_count || count > s_block_count - block_addr)) { + return false; + } + uint8_t r1[6]; + if (count == 1) { + if (!send_command_retry(17, block_addr, r1, 8)) { + return false; + } + return read_data_block(buf); + } + // RETRY like CMD17 above: at high bus duty the card intermittently misses + // the first command after the previous burst's CMD12 + if (!send_command_retry(18, block_addr, r1, 4)) { + return false; + } + + uint32_t bt0 = EMMC_TICKS(); + const uint32_t blim = US_TO_TICKS(150000u); + for (uint32_t i = 0; i < count; i++) { + if (i && (ticks_since(bt0) >= blim || emmc_deadline_expired())) { + (void)send_command_retry(12, 0, r1, 3); + return false; + } + if (!read_data_block(buf + i * EMMC_BLOCK_SIZE)) { + (void)send_command_retry(12, 0, r1, 3); + return false; + } + } + (void)send_command_retry(12, 0, r1, 3); + return true; +} + +// The card declares MIN_PERF_W_* = 0x00: no minimum write performance +#define EMMC_WR_BUSY_US 500000u +// Same shape as the read side +#define EMMC_WR_BURST_US 250000u + +// -Os states the intent for this bit-bang, but matches the port default and so +// changes nothing today. +__attribute__((optimize("Os"))) +static bool write_data_block(const uint8_t *buf) { + const uint32_t hd = g_emmc_clk_half_us; + + // Write convention: change DAT0 while CLK is LOW, then a full half-period + // of setup before the rising edge where the card latches it. DAT0 is a + // HIGH-DRIVE (H0H1) output. + // + // The frame opens with DAT0 idle-HIGH for a whole byte (the Nwr gap) so + // the card cannot mistake a stray low for an early start bit and misframe + // the token. + DAT0_OUT(); + RDAT_HIGH(); + + uint8_t *tx = SP1EMMC_TX_FRAME; // the reserved low-RAM SPIM3 buffer + uint16_t crc = crc16(buf, EMMC_BLOCK_SIZE); + tx[0] = 0xFF; // Nwr idle gap + tx[1] = 0xFE; // 7 idle bits + START 0 + memcpy(&tx[2], buf, EMMC_BLOCK_SIZE); + tx[2 + EMMC_BLOCK_SIZE] = (uint8_t)(crc >> 8); + tx[2 + EMMC_BLOCK_SIZE + 1] = (uint8_t)crc; + RCLK_LOW(); + // Launch edge is mode 0's, always + // + // HS_TIMING moved the card's OUTPUT edge, and only that. Its input timing + // is unchanged: both of the datasheet's tables (p.18 high-speed, p.19 + // backward-compatible) give tISU = tIH = 3 ns for CMD/DAT "referenced to + // CLK", i.e. the card latches the host on the rising edge in either mode. + // So the read path has to follow the card to CPHA=1 and the write path + // must NOT: in mode 1 SPIM shifts MOSI on the leading edge, which is the + // very edge the card samples -- zero setup against a 3 ns requirement, + // and the card takes the previous bit. Mode 0 shifts on the trailing + // edge and hands the card a whole half period of setup: 31 ns at M16, + // 15.6 ns at M32, both an order of magnitude over tISU. + // + // Saving and restoring rather than assuming keeps "the peripheral + // register is the state" true for the read path (sp1emmc_hw.h): this + // function borrows the phase for one DMA and gives it back. Two register + // writes against a ~130 us transfer. + const uint32_t saved_cfg = NRF_SPIM3->CONFIG; + if (saved_cfg != SPIM_CONFIG_MODE0) { + NRF_SPIM3->CONFIG = SPIM_CONFIG_MODE0; + } + // The TX frame ends exactly at the crc's last bit, no trailing idle + // byte. The card emits its CRC-status token a couple of clocks after the + // end bit. + sp1emmc_spim_xfer(tx, 2u + EMMC_BLOCK_SIZE + 2u, NULL, 0); + if (saved_cfg != SPIM_CONFIG_MODE0) { + NRF_SPIM3->CONFIG = saved_cfg; + } + // END bit: DAT0 is back at its GPIO latch (output HIGH) -- clock it. + half_delay(hd); + EDGE_SETTLE(); + RCLK_HIGH(); + half_delay(hd); + RCLK_LOW(); + + // CRC-status token: the card drives DAT0 low (start bit), then 3 status + // bits -- 010 accepted, 101 CRC error, 110 write error -- then releases. + DAT0_IN(); + int wr_status = -1; + for (int i = 0; i < 16; i++) { + RCLK_HIGH(); + half_delay(hd); + EDGE_SETTLE(); + int start = (int)RDAT_GET(); + RCLK_LOW(); + half_delay(hd); + if (!start) { + int st = 0; + for (int k = 0; k < 3; k++) { + RCLK_HIGH(); + half_delay(hd); + EDGE_SETTLE(); + st = (st << 1) | (int)RDAT_GET(); + RCLK_LOW(); + half_delay(hd); + } + wr_status = st; + break; + } + } + + // Programming busy on DAT0 + if (!dat0_busy_wait(EMMC_WR_BUSY_US, false)) { + return false; // DAT0 left an INPUT -- see the wait + } + + // ENFORCE the token: 0b010 = accepted. Anything else -- including "never + // saw one" -- means the card did not take the block, and returning false + // makes the caller retry instead of believing a glitch was stored. + if (wr_status != 0x2) { + return false; + } + return true; +} + +bool emmc_write_blocks(uint32_t block_addr, const uint8_t *buf, uint32_t count) { + if (!s_ready || count == 0) { + return false; + } + if (s_block_count != 0 && + (block_addr >= s_block_count || count > s_block_count - block_addr)) { + return false; + } + uint8_t r1[6]; + if (count == 1) { + if (!send_command_retry(24, block_addr, r1, 8)) { + return false; + } + return write_data_block(buf); + } + // Settle-miss retry, exactly as CMD18: at high bus duty the card + // intermittently misses the first command after the previous burst. + if (!send_command_retry(25, block_addr, r1, 4)) { + return false; + } + uint32_t bt0 = EMMC_TICKS(); + const uint32_t blim = US_TO_TICKS(EMMC_WR_BURST_US); + for (uint32_t i = 0; i < count; i++) { + if (i && (ticks_since(bt0) >= blim || emmc_deadline_expired())) { + (void)send_command_retry(12, 0, r1, 3); + return false; + } + if (!write_data_block(buf + i * EMMC_BLOCK_SIZE)) { + (void)send_command_retry(12, 0, r1, 3); + return false; + } + } + (void)send_command_retry(12, 0, r1, 3); + + (void)dat0_busy_wait(EMMC_WR_BUSY_US, false); + return true; +} + +uint32_t emmc_bus_hz(void) { + // SPIM3's M16/M32 codes are special values, NOT points on the linear scale + // the K125..M8 codes sit on (0x0A000000 would decode to 156 MHz there), so + // this is a lookup and not arithmetic. Only two values are ever written. + return NRF_SPIM3->FREQUENCY == SPIM_FREQ_M32 ? 32000000u : 16000000u; +} diff --git a/ports/nordic/boards/teenage_engineering_sp1/sp1emmc/sp1emmc.h b/ports/nordic/boards/teenage_engineering_sp1/sp1emmc/sp1emmc.h new file mode 100644 index 00000000000..2891c70d58f --- /dev/null +++ b/ports/nordic/boards/teenage_engineering_sp1/sp1emmc/sp1emmc.h @@ -0,0 +1,86 @@ +// This file is part of the CircuitPython project: https://circuitpython.org +// +// SPDX-FileCopyrightText: Copyright (c) 2026 Tim Cocks for Adafruit Industries +// +// SPDX-License-Identifier: MIT + +// SP-1 eMMC: 1-bit MMC protocol over the nRF52840 (CLK/CMD/DAT0 + RST_n and a +// VCCQ rail gate). +// +// Card: Toshiba THGBMNG5D1LBAIL, e-MMC 5.0, SEC_COUNT 0x00760000 (7,733,248 +// blocks = 3.69 GiB), TRAN_SPEED 0x32 -> 26 MHz in backwards-compatible mode. +// It stays there unless the caller explicitly asks for high-speed timing. + + +#pragma once + +#include +#include + +#define EMMC_BLOCK_SIZE 512u + +// How far bring-up got, so a failure names the step it stopped at instead of +// leaving the caller to guess. Zeroed by emmc_init(). +typedef struct { + // ---- init progress ---- + bool cmd0_sent; + int32_t cmd1_retries; // retries until ready; -1 = never ready + bool cmd2_resp; + bool cmd3_resp; + bool cmd7_resp; + bool cmd16_resp; + uint8_t cid[16]; // CMD2 R2 payload (CID[127:0]) + // ---- high-speed timing ---- + // These stay at their zero values unless emmc_set_high_speed() is called. + bool hs_switch_error; // CMD13 reported SWITCH_ERROR after the CMD6 + bool hs_active; // EXT_CSD[185] verified AND the host clock is at M32 + // How far the switch got, so the failure message names a step instead of + // inferring one from timings that are legitimately 0 on a fast card: + // 0 not attempted, 1 DEVICE_TYPE ok, 2 CMD6 answered, 3 DAT0 released, + // 4 back in tran, 5 EXT_CSD[185] verified, 6 running at M32. + uint8_t hs_stage; +} sp1emmc_state_t; + +extern sp1emmc_state_t g_emmc_state; + +// DATA-transfer clk half-period in microseconds. 0 = fastest (no busy-wait, +// just GPIO register toggles); set to 0 by emmc_init() on success. Commands +// use a fixed safe clock during identification and 0 afterwards. +extern volatile uint32_t g_emmc_clk_half_us; + +// Drop VCCQ, hold RST_n asserted and park the signal pins low for long enough +// that the card comes back from a true power-on. +void emmc_power_cycle(void); + +// A wall-clock budget spanning a whole sequence of driver calls. +void emmc_deadline_set(uint32_t timeout_us); +void emmc_deadline_clear(void); +bool emmc_deadline_expired(void); + +bool emmc_init(void); +uint32_t emmc_block_count(void); // 0 until EXT_CSD has been read +bool emmc_cmd13(uint8_t *r1_out); // SEND_STATUS -- card status R1 +bool emmc_read_ext_csd(uint8_t *buf); // CMD8 -> 512-byte EXT_CSD (read-only) +bool emmc_read_blocks(uint32_t block_addr, uint8_t *buf, uint32_t count); +void emmc_power_down(void); // reset asserted, pins released, VCCQ off + +// Block-device ioctl, taking extmod/vfs.h's MP_BLOCKDEV_IOCTL_* ops. +bool emmc_blockdev_ioctl(uint32_t op, uint32_t arg, uint32_t *out_value); + +// CMD24 (count == 1) / CMD25 + CMD12 (count > 1), each block followed by the +// card's CRC-status token and its programming busy. Direct +// writes only: the card's volatile cache is never enabled, so when this +// returns true the data is in NAND and there is nothing to flush. +bool emmc_write_blocks(uint32_t block_addr, const uint8_t *buf, uint32_t count); + +uint32_t emmc_bus_hz(void); // the SPIM data-phase clock, Hz + +// CMD6 SWITCH: EXT_CSD[185] HS_TIMING = 1, then SPIM3 to M32. +// +// Volatile byte, no user data, one hard-coded argument. Refuses to send the +// command at all unless EXT_CSD[196] DEVICE_TYPE says the part supports 52 MHz, +// and refuses to raise the host clock unless the card's own EXT_CSD reads back +// HS_TIMING = 1 at the OLD clock. +// +// Requires emmc_read_ext_csd() to have run. +bool emmc_set_high_speed(void); diff --git a/ports/nordic/boards/teenage_engineering_sp1/sp1emmc/sp1emmc_hw.h b/ports/nordic/boards/teenage_engineering_sp1/sp1emmc/sp1emmc_hw.h new file mode 100644 index 00000000000..6d63b6da2c0 --- /dev/null +++ b/ports/nordic/boards/teenage_engineering_sp1/sp1emmc/sp1emmc_hw.h @@ -0,0 +1,181 @@ +// This file is part of the CircuitPython project: https://circuitpython.org +// +// SPDX-FileCopyrightText: Copyright (c) 2026 Tim Cocks for Adafruit Industries +// +// SPDX-License-Identifier: MIT + +// The hardware sp1emmc.c drives: the five pins, the RTC2 tick source and the +// SPIM3 data engine. + + +#pragma once + +#include +#include + +#include "nrf.h" +#include "nrf_gpio.h" + +// Pins (SP-1 wiring). +#define SP1EMMC_PIN_CLK 6u // P0.06 +#define SP1EMMC_PIN_DAT0 7u // P0.07 +#define SP1EMMC_PIN_CMD 8u // P0.08 +#define SP1EMMC_PIN_RST 40u // P1.08, active low +#define SP1EMMC_PIN_VCCQ 14u // P0.14, I/O rail gate + +// SPIM3 clock codes. +#define SPIM_FREQ_M16 0x0A000000u +#define SPIM_FREQ_M32 0x14000000u + +// SPIM3 CONFIG codes +#define SPIM_CONFIG_MODE0 0u // MSB first, CPOL0/CPHA0 +#define SPIM_CONFIG_MODE1 (1u << 1) // MSB first, CPOL0/CPHA1 + +// ---- pin control --------------------------------------------------------- +// The command/init path uses the HAL macros; the data path uses the direct +// port-0 register accesses below (~3 cycles vs ~130 for the HAL, which is the +// difference between a usable bit-bang clock and a useless one). +#define CLK_HIGH() nrf_gpio_pin_set(SP1EMMC_PIN_CLK) +#define CLK_LOW() nrf_gpio_pin_clear(SP1EMMC_PIN_CLK) +#define CMD_HIGH() nrf_gpio_pin_set(SP1EMMC_PIN_CMD) +#define CMD_LOW() nrf_gpio_pin_clear(SP1EMMC_PIN_CMD) +#define DAT0_HIGH() nrf_gpio_pin_set(SP1EMMC_PIN_DAT0) +#define DAT0_LOW() nrf_gpio_pin_clear(SP1EMMC_PIN_DAT0) +#define DAT0_IN() nrf_gpio_cfg_input(SP1EMMC_PIN_DAT0, NRF_GPIO_PIN_PULLUP) +// DAT0 as a HIGH-DRIVE output (H0H1) so edges are fast and clean. +#define DAT0_OUT() nrf_gpio_cfg(SP1EMMC_PIN_DAT0, NRF_GPIO_PIN_DIR_OUTPUT, \ + NRF_GPIO_PIN_INPUT_DISCONNECT, NRF_GPIO_PIN_NOPULL, \ + NRF_GPIO_PIN_H0H1, NRF_GPIO_PIN_NOSENSE) +#define CMD_IN() nrf_gpio_cfg_input(SP1EMMC_PIN_CMD, NRF_GPIO_PIN_PULLUP) +#define CMD_OUT() nrf_gpio_cfg_output(SP1EMMC_PIN_CMD) +#define READ_CMD() nrf_gpio_pin_read(SP1EMMC_PIN_CMD) +#define READ_DAT0() nrf_gpio_pin_read(SP1EMMC_PIN_DAT0) + +#define P0_CLK_BIT (1u << SP1EMMC_PIN_CLK) +#define P0_DAT_BIT (1u << SP1EMMC_PIN_DAT0) +#define RCLK_HIGH() (NRF_P0->OUTSET = P0_CLK_BIT) +#define RCLK_LOW() (NRF_P0->OUTCLR = P0_CLK_BIT) +#define RDAT_HIGH() (NRF_P0->OUTSET = P0_DAT_BIT) +#define RDAT_LOW() (NRF_P0->OUTCLR = P0_DAT_BIT) +#define RDAT_GET() ((NRF_P0->IN >> SP1EMMC_PIN_DAT0) & 1u) +// A few NOPs of settle after a clock edge for the delay-free (hd==0) path: +// covers the card's data-output valid time without throttling to a busy-wait. +#define EDGE_SETTLE() __asm__ volatile ("nop\nnop\nnop") + +#define RST_ASSERT() nrf_gpio_pin_clear(SP1EMMC_PIN_RST) +#define RST_RELEASE() nrf_gpio_pin_set(SP1EMMC_PIN_RST) +#define VCCQ_ON() nrf_gpio_pin_set(SP1EMMC_PIN_VCCQ) +#define VCCQ_OFF() nrf_gpio_pin_clear(SP1EMMC_PIN_VCCQ) + +static inline void sp1emmc_pins_init(void) { + nrf_gpio_cfg(SP1EMMC_PIN_CLK, NRF_GPIO_PIN_DIR_OUTPUT, NRF_GPIO_PIN_INPUT_DISCONNECT, + NRF_GPIO_PIN_NOPULL, NRF_GPIO_PIN_H0H1, NRF_GPIO_PIN_NOSENSE); // high-drive CLK + nrf_gpio_cfg_output(SP1EMMC_PIN_CMD); + DAT0_OUT(); // high-drive DAT0 + nrf_gpio_cfg_output(SP1EMMC_PIN_RST); + // VCCQ: standard drive. Do NOT "improve" this to H0H1 without evidence -- + // the rail gate does not need the extra drive and the card came up on it. + nrf_gpio_cfg_output(SP1EMMC_PIN_VCCQ); +} + +static inline void sp1emmc_pins_release(void) { + nrf_gpio_cfg_default(SP1EMMC_PIN_CLK); + nrf_gpio_cfg_default(SP1EMMC_PIN_CMD); + nrf_gpio_cfg_default(SP1EMMC_PIN_DAT0); + nrf_gpio_cfg_default(SP1EMMC_PIN_RST); + // VCCQ stays an output, driven low: the rail must stay off, not float. +} + +// ---- the write path's DMA buffer (anomaly 198) ---------------------------- +// SPIM3 on the nRF52840 corrupts TX bytes when EasyDMA reads them out of the +// upper RAM regions while the CPU is busy elsewhere (errata 198). The port +// already reserves 8 KiB of low RAM for exactly this (mpconfigport.h:36, +// SPIM3_BUFFER_RAM_START_ADDR), and busio's SPI uses it for the same reason -- +// which is safe to share because an sp1emmc.EMMC object owns SPIM3 outright +// while it lives: busio's allocator asks sp1emmc_spim3_in_use() and falls back +// to SPIM0/1/2, so the two can never have a transfer in flight at once. +// +// The write path relies on this buffer rather than on retrying a bad CRC +// status; the status token is still enforced as the backstop. +#define SP1EMMC_TX_FRAME ((uint8_t *)SPIM3_BUFFER_RAM_START_ADDR) + +// ---- time ---------------------------------------------------------------- +// Free-running 32768 Hz counter (RTC2, the supervisor's tick source). 24-bit, +// so differences must be masked; it wraps every 512 s. +#define EMMC_TICKS_HZ 32768u +#define EMMC_TICK_MASK 0x00FFFFFFu +#define EMMC_TICKS() (NRF_RTC2->COUNTER) + +static inline uint32_t ticks_since_raw(uint32_t t0) { + return (EMMC_TICKS() - t0) & EMMC_TICK_MASK; +} + +// 20 ms, ~75x the 260 us a full block takes at M16. +#define SP1EMMC_SPIM_TIMEOUT_TICKS ((EMMC_TICKS_HZ * 20u) / 1000u) + +// ---- SPIM3 data engine --------------------------------------------------- +// SPIM3 is the only instance that runs above 8 MHz. M16 = 16 MHz, the fastest +// in-spec step for this card at power-on timing (TRAN_SPEED 0x32 -> 26 MHz cap +// in backwards-compatible mode). The bus is fixed there; the ONE way it moves +// is emmc_set_high_speed(), which first gets the card's own EXT_CSD to read +// back HS_TIMING = 1 (52 MHz limit) and only then steps to M32. +// There is still no free-floating "speed knob": the two codes at the top of +// this file are the only values ever written. +// +// NRF_SPIM3->FREQUENCY and ->CONFIG are read and written directly: the +// peripheral register is the state, and it survives +// ENABLE=0 between transfers. sp1emmc_spim_init() puts both back to M16 / +// mode 0 on every init, so a fresh object always starts at compat speed even +// if the previous one ran high. + +static inline void sp1emmc_spim_init(void) { + NRF_SPIM3->ENABLE = 0; + NRF_SPIM3->PSEL.SCK = SP1EMMC_PIN_CLK; + NRF_SPIM3->PSEL.MOSI = 0xFFFFFFFFu; // attached per-transfer (write path only) + NRF_SPIM3->PSEL.MISO = 0xFFFFFFFFu; + NRF_SPIM3->PSEL.CSN = 0xFFFFFFFFu; + NRF_SPIM3->FREQUENCY = SPIM_FREQ_M16; + NRF_SPIM3->CONFIG = SPIM_CONFIG_MODE0; // the card comes up in compat timing + NRF_SPIM3->ORC = 0xFF; // idle-high filler +} + +static inline void sp1emmc_spim_deinit(void) { + NRF_SPIM3->ENABLE = 0; + NRF_SPIM3->PSEL.SCK = 0xFFFFFFFFu; + NRF_SPIM3->PSEL.MOSI = 0xFFFFFFFFu; + NRF_SPIM3->PSEL.MISO = 0xFFFFFFFFu; +} + +// One blocking DMA transfer with the wires temporarily owned by SPIM. While +// ENABLED the peripheral drives SCK (+MOSI for TX) / samples MISO; on disable +// the pins fall back to their GPIO latches (CLK low, DAT0 as configured), so +// the surrounding bit-bang phases continue seamlessly. +// +// A read is rx-only (MOSI unselected), so SPIM3 anomaly 198 (TX corruption) +// cannot bite there at all. Writes make TX real, which is why their frame is +// built in sp1emmc_tx_frame() above. +static inline void sp1emmc_spim_xfer(const uint8_t *tx, uint32_t txlen, uint8_t *rx, uint32_t rxlen) { + NRF_SPIM3->PSEL.MOSI = tx ? SP1EMMC_PIN_DAT0 : 0xFFFFFFFFu; + NRF_SPIM3->PSEL.MISO = rx ? SP1EMMC_PIN_DAT0 : 0xFFFFFFFFu; + NRF_SPIM3->ENABLE = 7; + NRF_SPIM3->TXD.PTR = (uint32_t)tx; + NRF_SPIM3->TXD.MAXCNT = tx ? txlen : 0; + NRF_SPIM3->RXD.PTR = (uint32_t)rx; + NRF_SPIM3->RXD.MAXCNT = rx ? rxlen : 0; + NRF_SPIM3->EVENTS_END = 0; + NRF_SPIM3->TASKS_START = 1; + { + uint32_t t0 = EMMC_TICKS(); + while (!NRF_SPIM3->EVENTS_END) { + if (ticks_since_raw(t0) >= SP1EMMC_SPIM_TIMEOUT_TICKS) { + NRF_SPIM3->EVENTS_STOPPED = 0; + NRF_SPIM3->TASKS_STOP = 1; + // Let EasyDMA stop writing before the buffer is handed back. + for (uint32_t i = 0; i < 10000u && !NRF_SPIM3->EVENTS_STOPPED; i++) { + } + break; + } + } + } + NRF_SPIM3->ENABLE = 0; +} diff --git a/ports/nordic/boards/teenage_engineering_sp1/wdt.h b/ports/nordic/boards/teenage_engineering_sp1/wdt.h new file mode 100644 index 00000000000..31c0a338585 --- /dev/null +++ b/ports/nordic/boards/teenage_engineering_sp1/wdt.h @@ -0,0 +1,41 @@ +// This file is part of the CircuitPython project: https://circuitpython.org +// +// SPDX-FileCopyrightText: Copyright (c) 2026 Tim Cocks for Adafruit Industries +// +// SPDX-License-Identifier: MIT + +#pragma once + +// Support for the watchdog that this board's bootloader starts, before +// CircuitPython's first instruction, and that cannot be stopped. +// +// This is not the `watchdog` module: that one owns the peripheral and can +// configure it. Here the WDT is already running and its configuration +// registers (CRV, RREN, CONFIG) are locked, so the only thing the application +// can do is reload it. +// +// The board opts in with CIRCUITPY_BOOTLOADER_ARMED_WDT in its +// mpconfigboard.h. + +// mpconfigboard.h arrives via mpconfigport.h. Included here rather than left to +// the caller so that the feed can never be silently compiled out by an include +// order that omitted it. +#include "py/mpconfig.h" + +#include "nrfx.h" + +// Value that a reload request register must be written with, per the nRF52 +// product specification. +#define NRF_WDT_RELOAD_REQUEST_VALUE (0x6E524635UL) + +// Reload the bootloader's watchdog. +// +// Call this from the main loop, never from an interrupt handler. Feeding from +// an ISR would keep a wedged main loop "alive" indefinitely. +static inline void bootloader_wdt_feed(void) { + #if CIRCUITPY_BOOTLOADER_ARMED_WDT + for (size_t channel = 0; channel < 8; channel++) { + NRF_WDT->RR[channel] = NRF_WDT_RELOAD_REQUEST_VALUE; + } + #endif +} diff --git a/ports/nordic/common-hal/alarm/pin/PinAlarm.c b/ports/nordic/common-hal/alarm/pin/PinAlarm.c index f43eb1ed251..3359faa74e0 100644 --- a/ports/nordic/common-hal/alarm/pin/PinAlarm.c +++ b/ports/nordic/common-hal/alarm/pin/PinAlarm.c @@ -17,7 +17,9 @@ #include "nrfx.h" #include "nrf_gpio.h" #include "nrfx_gpiote.h" +#ifdef BLUETOOTH_SD #include "nrf_soc.h" +#endif #include #define WPIN_UNUSED 0xFF diff --git a/ports/nordic/common-hal/analogio/AnalogIn.c b/ports/nordic/common-hal/analogio/AnalogIn.c index e48c9dd2e28..9d899ac3ffe 100644 --- a/ports/nordic/common-hal/analogio/AnalogIn.c +++ b/ports/nordic/common-hal/analogio/AnalogIn.c @@ -23,6 +23,12 @@ void analogin_init(void) { while (nrf_saadc_event_check(NRF_SAADC, NRF_SAADC_EVENT_CALIBRATEDONE) == 0) { } nrf_saadc_event_clear(NRF_SAADC, NRF_SAADC_EVENT_CALIBRATEDONE); + // Calibration also raises END, DONE and RESULTDONE. Leaving them set makes the + // first conversion in common_hal_analogio_analogin_get_value() fall straight + // through its wait on END and return 0; see the comment there. + nrf_saadc_event_clear(NRF_SAADC, NRF_SAADC_EVENT_END); + nrf_saadc_event_clear(NRF_SAADC, NRF_SAADC_EVENT_DONE); + nrf_saadc_event_clear(NRF_SAADC, NRF_SAADC_EVENT_RESULTDONE); nrf_saadc_disable(NRF_SAADC); } @@ -56,7 +62,20 @@ uint16_t common_hal_analogio_analogin_get_value(analogio_analogin_obj_t *self) { // Something else might have used the ADC in a different way, // so we completely re-initialize it. - nrf_saadc_value_t value = 0; + // The SAADC fills this in over EasyDMA, so the compiler must not be allowed + // to keep it in a register or assume it is unchanged across the conversion. + // `static volatile` forces a real memory location that is re-read after the + // conversion, and the explicit alignment keeps the EasyDMA pointer + // word-aligned. Not reentrant, but neither is the single, shared ADC channel + // that this function reconfigures on every call. + // + // This replaces an older `asm volatile ("" : : : "memory")` fence, added for + // a suspected gcc13 miscompile that made `value` always zero. That diagnosis + // looks mistaken: on gcc 14.2.1 the generated code was verified correct with + // and without the fence, and the real cause of the always-zero reading was + // the stale EVENTS_END handled below. + static volatile nrf_saadc_value_t value __attribute__((aligned(4))); + value = 0; const nrf_saadc_channel_config_t config = { .resistor_p = NRF_SAADC_RESISTOR_DISABLED, @@ -78,7 +97,21 @@ uint16_t common_hal_analogio_analogin_get_value(analogio_analogin_obj_t *self) { nrf_saadc_channel_init(NRF_SAADC, CHANNEL_NO, &config); nrf_saadc_channel_input_set(NRF_SAADC, CHANNEL_NO, self->pin->adc_channel, self->pin->adc_channel); - nrf_saadc_buffer_init(NRF_SAADC, &value, 1); + nrf_saadc_buffer_init(NRF_SAADC, (nrf_saadc_value_t *)&value, 1); + + // Clear stale events before triggering anything. EVENTS_END in particular is + // left set by the *previous* call's TASKS_STOP, and by the offset calibration + // in analogin_init(). If it is still set when we get to the wait below, that + // wait falls through immediately, TASKS_STOP then aborts the conversion that + // has only just started, and EasyDMA never writes the buffer: RESULT.AMOUNT + // stays 0 and `value` keeps whatever it was. That is the "AnalogIn.value is + // always zero" failure, and it is self-perpetuating once the first stale + // EVENTS_END appears. + nrf_saadc_event_clear(NRF_SAADC, NRF_SAADC_EVENT_STARTED); + nrf_saadc_event_clear(NRF_SAADC, NRF_SAADC_EVENT_END); + nrf_saadc_event_clear(NRF_SAADC, NRF_SAADC_EVENT_DONE); + nrf_saadc_event_clear(NRF_SAADC, NRF_SAADC_EVENT_RESULTDONE); + nrf_saadc_event_clear(NRF_SAADC, NRF_SAADC_EVENT_STOPPED); nrf_saadc_task_trigger(NRF_SAADC, NRF_SAADC_TASK_START); while (nrf_saadc_event_check(NRF_SAADC, NRF_SAADC_EVENT_STARTED) == 0) { @@ -100,23 +133,22 @@ uint16_t common_hal_analogio_analogin_get_value(analogio_analogin_obj_t *self) { nrf_saadc_disable(NRF_SAADC); - // Adding the "asm volatile" memory fence here or anywhere after the declaration of `value` - // fixes an issue with gcc13 which causes `value` to always be zero. - // Compiling with gcc10 or gcc12 is fine. - // It can also be fixed by declaring `value` to be static. - // I think I'd like to declare `value` as volatile, but that causes type errors. + // Belt and braces alongside the `volatile` above; costs nothing to keep. asm volatile ("" : : : "memory"); // Disconnect ADC from pin. nrf_saadc_channel_input_set(NRF_SAADC, CHANNEL_NO, NRF_SAADC_INPUT_DISABLED, NRF_SAADC_INPUT_DISABLED); + // Read the DMA'd result exactly once. + int32_t result = value; + // value is signed and might be (slightly) < 0, even on single-ended conversions, so force to 0. - if (value < 0) { - value = 0; + if (result < 0) { + result = 0; } // Stretch 14-bit ADC reading to 16-bit range - return (value << 2) | (value >> 12); + return (result << 2) | (result >> 12); } float common_hal_analogio_analogin_get_reference_voltage(analogio_analogin_obj_t *self) { diff --git a/ports/nordic/common-hal/audiobusio/I2SOut.c b/ports/nordic/common-hal/audiobusio/I2SOut.c index fbdaa93cfa4..a26702574c5 100644 --- a/ports/nordic/common-hal/audiobusio/I2SOut.c +++ b/ports/nordic/common-hal/audiobusio/I2SOut.c @@ -16,6 +16,8 @@ #include "py/obj.h" #include "py/runtime.h" +#include "nrf_gpio.h" + static audiobusio_i2sout_obj_t *instance; struct { int16_t l, r; @@ -191,9 +193,6 @@ void common_hal_audiobusio_i2sout_construct(audiobusio_i2sout_obj_t *self, const mcu_pin_obj_t *bit_clock, const mcu_pin_obj_t *word_select, const mcu_pin_obj_t *data, const mcu_pin_obj_t *main_clock, bool left_justified, bool external_clock) { - if (external_clock) { - mp_raise_NotImplementedError_varg(MP_ERROR_TEXT("%q"), MP_QSTR_external_clock); - } if (main_clock != NULL) { mp_raise_NotImplementedError_varg(MP_ERROR_TEXT("%q"), MP_QSTR_main_clock); } @@ -206,14 +205,26 @@ void common_hal_audiobusio_i2sout_construct(audiobusio_i2sout_obj_t *self, claim_pin(word_select); claim_pin(data); + self->external_clock = external_clock; + NRF_I2S->PSEL.SCK = self->bit_clock_pin_number = bit_clock->number; NRF_I2S->PSEL.LRCK = self->word_select_pin_number = word_select->number; NRF_I2S->PSEL.SDOUT = self->data_pin_number = data->number; - NRF_I2S->CONFIG.MODE = I2S_CONFIG_MODE_MODE_Master; + if (external_clock) { + // External clock mode: SCK and LRCK are driven by something else. + nrf_gpio_cfg_input(bit_clock->number, NRF_GPIO_PIN_NOPULL); + nrf_gpio_cfg_input(word_select->number, NRF_GPIO_PIN_NOPULL); + } + + NRF_I2S->CONFIG.MODE = external_clock ? I2S_CONFIG_MODE_MODE_Slave + : I2S_CONFIG_MODE_MODE_Master; NRF_I2S->CONFIG.RXEN = I2S_CONFIG_RXEN_RXEN_Disabled; NRF_I2S->CONFIG.TXEN = I2S_CONFIG_TXEN_TXEN_Enabled; - NRF_I2S->CONFIG.MCKEN = I2S_CONFIG_MCKEN_MCKEN_Enabled; + // MCK generation is only meaningful in internal clock mode; MCKFREQ and RATIO are + // ignored by the peripheral when MODE = external_clock. + NRF_I2S->CONFIG.MCKEN = external_clock ? I2S_CONFIG_MCKEN_MCKEN_Disabled + : I2S_CONFIG_MCKEN_MCKEN_Enabled; NRF_I2S->CONFIG.SWIDTH = I2S_CONFIG_SWIDTH_SWIDTH_16Bit; NRF_I2S->CONFIG.ALIGN = I2S_CONFIG_ALIGN_ALIGN_Left; @@ -233,6 +244,12 @@ void common_hal_audiobusio_i2sout_deinit(audiobusio_i2sout_obj_t *self) { } NRF_I2S->TASKS_STOP = 1; NRF_I2S->ENABLE = I2S_ENABLE_ENABLE_Disabled; + if (self->external_clock) { + // Disconnect the input buffers construct() connected, as + // nrfx_i2s_uninit() does. + nrf_gpio_cfg_default(self->bit_clock_pin_number); + nrf_gpio_cfg_default(self->word_select_pin_number); + } reset_pin_number(self->bit_clock_pin_number); self->bit_clock_pin_number = NO_PIN; reset_pin_number(self->word_select_pin_number); @@ -272,7 +289,13 @@ void common_hal_audiobusio_i2sout_play(audiobusio_i2sout_obj_t *self, ? I2S_CONFIG_CHANNELS_CHANNELS_Left : I2S_CONFIG_CHANNELS_CHANNELS_Stereo; - choose_i2s_clocking(self, sample_rate); + if (self->external_clock) { + // The frame rate is whatever the external word select line runs at; + // there is nothing to choose. + self->sample_rate = sample_rate; + } else { + choose_i2s_clocking(self, sample_rate); + } /* Allocate buffers based on a maximum duration * This duration was chosen empirically based on what would * cause os.listdir('') to cause stuttering. It seems like a diff --git a/ports/nordic/common-hal/audiobusio/I2SOut.h b/ports/nordic/common-hal/audiobusio/I2SOut.h index 33ec917eb64..27e077c0649 100644 --- a/ports/nordic/common-hal/audiobusio/I2SOut.h +++ b/ports/nordic/common-hal/audiobusio/I2SOut.h @@ -28,6 +28,7 @@ typedef struct { uint8_t bytes_per_sample; bool left_justified : 1; + bool external_clock : 1; bool playing : 1; bool stopping : 1; bool last_buffer : 1; diff --git a/ports/nordic/common-hal/busio/SPI.c b/ports/nordic/common-hal/busio/SPI.c index de54dd08a27..e75c5c66199 100644 --- a/ports/nordic/common-hal/busio/SPI.c +++ b/ports/nordic/common-hal/busio/SPI.c @@ -14,6 +14,10 @@ #include "nrfx_spim.h" #include "nrf_gpio.h" +#if defined(CIRCUITPY_SP1EMMC) && CIRCUITPY_SP1EMMC +#include "bindings/sp1emmc/EMMC.h" +#endif + #ifndef NRFX_SPIM3_ENABLED #define NRFX_SPIM3_ENABLED (0) #endif @@ -122,6 +126,11 @@ void common_hal_busio_spi_construct(busio_spi_obj_t *self, const mcu_pin_obj_t * // Find a free instance, with most desirable (highest freq and not shared) allocated first. self->spim_peripheral = NULL; for (size_t i = 0; i < MP_ARRAY_SIZE(spim_peripherals); i++) { + #if defined(CIRCUITPY_SP1EMMC) && CIRCUITPY_SP1EMMC + if (spim_peripherals[i].spim.p_reg == NRF_SPIM3 && sp1emmc_spim3_in_use()) { + continue; + } + #endif if ((spim_peripherals[i].spim.p_reg->ENABLE & SPIM_ENABLE_ENABLE_Msk) == 0) { self->spim_peripheral = &spim_peripherals[i]; break; diff --git a/ports/nordic/common-hal/microcontroller/Pin.c b/ports/nordic/common-hal/microcontroller/Pin.c index 8043d2dfd4d..cc37a63130b 100644 --- a/ports/nordic/common-hal/microcontroller/Pin.c +++ b/ports/nordic/common-hal/microcontroller/Pin.c @@ -6,6 +6,7 @@ #include "shared-bindings/microcontroller/Pin.h" #include "shared-bindings/digitalio/DigitalInOut.h" +#include "supervisor/board.h" #include "nrf_gpio.h" #include "py/mphal.h" @@ -47,6 +48,11 @@ void reset_all_pins(void) { // After configuring SWD because it may be shared. reset_speaker_enable_pin(); + + // Last, so it wins: a board may need some pins held in a safe state rather + // than left floating between VM runs. Configuration only, these pins are + // not claimed. Python can still use them. + board_reset_pin_defaults(); } // Mark pin as free and return it to a quiescent state. diff --git a/ports/nordic/common-hal/microcontroller/__init__.c b/ports/nordic/common-hal/microcontroller/__init__.c index fbb40afe044..1508d1ade72 100644 --- a/ports/nordic/common-hal/microcontroller/__init__.c +++ b/ports/nordic/common-hal/microcontroller/__init__.c @@ -24,7 +24,9 @@ #include "supervisor/port.h" #include "supervisor/shared/safe_mode.h" #include "nrfx_glue.h" +#ifdef BLUETOOTH_SD #include "nrf_nvic.h" +#endif #include "nrf_power.h" // This routine should work even when interrupts are disabled. Used by OneWire @@ -45,7 +47,14 @@ void common_hal_mcu_disable_interrupts(void) { // This only disables interrupts of priority 2 through 7; levels 0, 1, // and 4, are exclusive to softdevice and should never be used, so // this limitation is not important. + #ifdef BLUETOOTH_SD sd_nvic_critical_region_enter(&is_nested_critical_region); + #else + // With no SoftDevice to leave room for, mask everything. Record whether + // interrupts were already masked so the matching exit leaves them so. + is_nested_critical_region = __get_PRIMASK() ? 1 : 0; + __disable_irq(); + #endif } __DMB(); nesting_count++; @@ -61,20 +70,49 @@ void common_hal_mcu_enable_interrupts(void) { return; } __DMB(); + #ifdef BLUETOOTH_SD sd_nvic_critical_region_exit(is_nested_critical_region); + #else + if (!is_nested_critical_region) { + __enable_irq(); + } + #endif } void common_hal_mcu_on_next_reset(mcu_runmode_t runmode) { - enum { DFU_MAGIC_UF2_RESET = 0x57 }; uint8_t new_value = 0; if (runmode == RUNMODE_BOOTLOADER || runmode == RUNMODE_UF2) { - new_value = DFU_MAGIC_UF2_RESET; + new_value = BOOTLOADER_UF2_MAGIC; + } + #ifdef BOOTLOADER_UF2_MAGIC2 + // bootloader magic is 16 bits wide, split across both retention + // registers, so GPREGRET2 is written as a pair with GPREGRET + uint8_t new_value2 = 0; + if (runmode == RUNMODE_BOOTLOADER || runmode == RUNMODE_UF2) { + new_value2 = BOOTLOADER_UF2_MAGIC2; } - int err_code = sd_power_gpregret_set(0, DFU_MAGIC_UF2_RESET); + #endif + #ifdef BLUETOOTH_SD + int err_code = sd_power_gpregret_set(0, BOOTLOADER_UF2_MAGIC); + #ifdef BOOTLOADER_UF2_MAGIC2 + if (err_code == NRF_SUCCESS) { + err_code = sd_power_gpregret_set(1, BOOTLOADER_UF2_MAGIC2); + } + #endif if (err_code != NRF_SUCCESS) { // Set it without the soft device if the SD failed. (It may be off.) nrf_power_gpregret_set(NRF_POWER, new_value); + #ifdef BOOTLOADER_UF2_MAGIC2 + nrf_power_gpregret2_set(NRF_POWER, new_value2); + #endif } + #else + // No SoftDevice, so write GPREGRET directly. + nrf_power_gpregret_set(NRF_POWER, new_value); + #ifdef BOOTLOADER_UF2_MAGIC2 + nrf_power_gpregret2_set(NRF_POWER, new_value2); + #endif + #endif if (runmode == RUNMODE_SAFE_MODE) { safe_mode_on_next_reset(SAFE_MODE_PROGRAMMATIC); } diff --git a/ports/nordic/common-hal/neopixel_write/__init__.c b/ports/nordic/common-hal/neopixel_write/__init__.c index 66a1d70e869..e9f3a0fdacc 100644 --- a/ports/nordic/common-hal/neopixel_write/__init__.c +++ b/ports/nordic/common-hal/neopixel_write/__init__.c @@ -125,7 +125,9 @@ void common_hal_neopixel_write(const digitalio_digitalinout_obj_t *digitalinout, pixels_pattern = (uint16_t *)stack_pixels; } else { uint8_t sd_en = 0; + #ifdef BLUETOOTH_SD (void)sd_softdevice_is_enabled(&sd_en); + #endif if (pixels_pattern_heap_size < pattern_size) { // Current heap buffer is too small. diff --git a/ports/nordic/device/nrf52/startup_nrf52840.c b/ports/nordic/device/nrf52/startup_nrf52840.c index 6b81fac2a9c..f16902fb30a 100644 --- a/ports/nordic/device/nrf52/startup_nrf52840.c +++ b/ports/nordic/device/nrf52/startup_nrf52840.c @@ -27,8 +27,23 @@ void Default_Handler(void) { } } +extern const func __Vectors[]; + +#ifndef BLUETOOTH_SD +// SCB->VTOR, addressed directly so the startup file need not pull in CMSIS. +#define SCB_VTOR (*(volatile uint32_t *)0xE000ED08UL) +#endif + extern void Reset_Handler(void); void Reset_Handler(void) { + #ifndef BLUETOOTH_SD + // With a SoftDevice, the MBR at 0x0 owns the vector table and forwards + // interrupts to us, so VTOR is already pointing at our table by the time we + // run. Without one, nothing has set it. + SCB_VTOR = (uint32_t)&__Vectors[0]; + __asm volatile ("dsb 0xF" ::: "memory"); + #endif + uint32_t *p_src = &_sidata; uint32_t *p_dest = &_sdata; diff --git a/ports/nordic/mpconfigport.h b/ports/nordic/mpconfigport.h index 33fcfa371e0..c0654587688 100644 --- a/ports/nordic/mpconfigport.h +++ b/ports/nordic/mpconfigport.h @@ -7,10 +7,18 @@ #pragma once +#ifdef BLUETOOTH_SD #include "ble_drv.h" #include "nrf_mbr.h" // for MBR_SIZE #include "nrf_sdm.h" // for SD_FLASH_SIZE +#else +// No SoftDevice: its headers are not on the include path at all, and neither the +// MBR nor the SoftDevice occupy any flash. +#define MBR_SIZE (0) +#define SD_FLASH_SIZE (0) +#endif + #include "peripherals/nrf/nvm.h" // for FLASH_PAGE_SIZE #define MICROPY_PY_SYS_STDIO_BUFFER (1) @@ -81,8 +89,14 @@ #define SD_FLASH_START_ADDR (MBR_START_ADDR + MBR_SIZE) // SD_FLASH_SIZE is from nrf_sdm.h +// A board whose bootloader lives in low flash and jumps to a fixed address +// overrides this in mpconfigboard.h. +#ifndef ISR_START_ADDR #define ISR_START_ADDR (SD_FLASH_START_ADDR + SD_FLASH_SIZE) +#endif +#ifndef ISR_SIZE #define ISR_SIZE (4 * 1024) // 4kiB +#endif // Smallest unit of flash that can be erased. #define FLASH_ERASE_SIZE FLASH_PAGE_SIZE @@ -92,31 +106,96 @@ // Define these regions starting down from the bootloader: // Bootloader values from https://github.com/adafruit/Adafruit_nRF52_Bootloader/blob/master/src/linker/s140_v6.ld +// A board with no bootloader in high flash sets BOOTLOADER_SIZE and +// BOOTLOADER_MBR_SIZE to 0; BOOTLOADER_START_ADDR then collapses onto the +// settings page, which stays the top anchor everything else grows down from. #define BOOTLOADER_START_ADDR (FLASH_SIZE - BOOTLOADER_SIZE - BOOTLOADER_SETTINGS_SIZE - BOOTLOADER_MBR_SIZE) +#ifndef BOOTLOADER_MBR_SIZE #define BOOTLOADER_MBR_SIZE (4 * 1024) // 4kib +#endif #ifndef BOOTLOADER_SIZE #define BOOTLOADER_SIZE (40 * 1024) // 40kiB #endif #define BOOTLOADER_SETTINGS_START_ADDR (FLASH_SIZE - BOOTLOADER_SETTINGS_SIZE) +#ifndef BOOTLOADER_SETTINGS_SIZE #define BOOTLOADER_SETTINGS_SIZE (4 * 1024) // 4kiB +#endif + +// Value left in GPREGRET to ask the bootloader to stay in DFU mode after the +// reset that reset_to_bootloader() performs. The default is the Adafruit nRF52 +// bootloader's serial-DFU magic; a board with a different bootloader overrides +// it in mpconfigboard.h. +#ifndef BOOTLOADER_DFU_MAGIC +#define BOOTLOADER_DFU_MAGIC (0x4e) +#endif +// Value left in GPREGRET by common_hal_mcu_on_next_reset() for RunMode.UF2 and +// RunMode.BOOTLOADER. The default is the Adafruit nRF52 bootloader's UF2/OTA +// magic. A board whose bootloader gates on something else overrides it in +// mpconfigboard.h; if that bootloader has no separate UF2 and serial-DFU +// requests, it sets this to the same value as BOOTLOADER_DFU_MAGIC. +#ifndef BOOTLOADER_UF2_MAGIC +#define BOOTLOADER_UF2_MAGIC (0x57) +#endif + +#ifndef CIRCUITPY_INTERNAL_FLASH_FILESYSTEM_START_ADDR #define CIRCUITPY_INTERNAL_FLASH_FILESYSTEM_START_ADDR (BOOTLOADER_START_ADDR - CIRCUITPY_INTERNAL_FLASH_FILESYSTEM_SIZE) +#endif +// The filesystem is meant to be adjacent to the bootloader so that its location +// does not change when other regions do. A board that overrides the start +// address above and breaks that adjacency gets told about it. #if CIRCUITPY_INTERNAL_FLASH_FILESYSTEM_SIZE > 0 && CIRCUITPY_INTERNAL_FLASH_FILESYSTEM_START_ADDR != (BOOTLOADER_START_ADDR - CIRCUITPY_INTERNAL_FLASH_FILESYSTEM_SIZE) #warning Internal flash filesystem location has moved! #endif +#ifndef CIRCUITPY_INTERNAL_NVM_START_ADDR #define CIRCUITPY_INTERNAL_NVM_START_ADDR (CIRCUITPY_INTERNAL_FLASH_FILESYSTEM_START_ADDR - CIRCUITPY_INTERNAL_NVM_SIZE) +#endif -// 32kiB for bonding, etc. +// 32kiB for bonding, etc. Nothing to store without a SoftDevice. #ifndef CIRCUITPY_BLE_CONFIG_SIZE +#ifdef BLUETOOTH_SD #define CIRCUITPY_BLE_CONFIG_SIZE (32 * 1024) +#else +#define CIRCUITPY_BLE_CONFIG_SIZE (0) +#endif #endif #define CIRCUITPY_BLE_CONFIG_START_ADDR (CIRCUITPY_INTERNAL_NVM_START_ADDR - CIRCUITPY_BLE_CONFIG_SIZE) // The firmware space is the space left over between the fixed lower and upper regions. #define CIRCUITPY_FIRMWARE_SIZE (CIRCUITPY_BLE_CONFIG_START_ADDR - CIRCUITPY_FIRMWARE_START_ADDR) +// Use the nRF52's access control lists to hardware-write-protect every part of +// internal flash that CircuitPython does not own: everything below the BLE +// config / nvm regions (a bootloader, a SoftDevice, our own firmware) and +// everything from BOOTLOADER_START_ADDR up (a bootloader in high flash and its +// settings page). +// +// Off by default. Worth turning on where a bad erase is +// unrecoverable, a board whose bootloader cannot be reflashed, or has no +// backup. +#ifndef CIRCUITPY_NRF_FLASH_PROTECT +#define CIRCUITPY_NRF_FLASH_PROTECT (0) +#endif + +// Whether this board's bootloader starts a watchdog before CircuitPython's +// first instruction and leaves it running with its configuration registers +// locked, so that the application can only reload it. A board that sets this +// supplies its own wdt.h, defining bootloader_wdt_feed(). Such a board should +// normally also set CIRCUITPY_WATCHDOG = 0, so user code cannot interfere. +#ifndef CIRCUITPY_BOOTLOADER_ARMED_WDT +#define CIRCUITPY_BOOTLOADER_ARMED_WDT (0) +#endif + +// Whether entering safe mode after a watchdog reset requires USB to be +// connected (port_init(), supervisor/port.c). When the bootloader owns the +// watchdog it can only have bitten because the main loop stopped, which is +// worth reporting whether or not a host is attached. +#ifndef CIRCUITPY_SAFE_MODE_ON_WATCHDOG_REQUIRES_USB +#define CIRCUITPY_SAFE_MODE_ON_WATCHDOG_REQUIRES_USB (!CIRCUITPY_BOOTLOADER_ARMED_WDT) +#endif + #if BOOTLOADER_START_ADDR % FLASH_ERASE_SIZE != 0 #error BOOTLOADER_START_ADDR must be on a flash erase boundary. #endif @@ -159,7 +238,14 @@ // high enough to work and then check the mutation of the value done by sd_ble_enable(). // See common.template.ld. #ifndef SOFTDEVICE_RAM_SIZE +#ifdef BLUETOOTH_SD #define SOFTDEVICE_RAM_SIZE (56 * 1024) +#else +// No SoftDevice, so none of the low RAM is reserved for it. The SPIM3 buffer +// then sits at the very bottom of RAM, still inside the first 64kB as the +// hardware workaround requires. +#define SOFTDEVICE_RAM_SIZE (0) +#endif #endif diff --git a/ports/nordic/mpconfigport.mk b/ports/nordic/mpconfigport.mk index 028ceca9fcd..9f70c778368 100644 --- a/ports/nordic/mpconfigport.mk +++ b/ports/nordic/mpconfigport.mk @@ -86,7 +86,14 @@ NRF_DEFINES += -DNRF52840_XXAA -DNRF52840 # CircuitPython doesn't yet support NFC so force the NFC antenna pins to be GPIO. # See https://github.com/adafruit/circuitpython/issues/1300 # Defined here because system_nrf52840.c doesn't #include any of our own include files. +# +# One of the three UICR writes on the boot path. if NFCPINS.PROTECT +# still says NFC, SystemInit clears it and resets, permanently. +# A board whose UICR must not be touched sets NRF_NFCT_PINS_AS_GPIOS = 0 +NRF_NFCT_PINS_AS_GPIOS ?= 1 +ifeq ($(NRF_NFCT_PINS_AS_GPIOS),1) CFLAGS += -DCONFIG_NFCT_PINS_AS_GPIOS +endif ifeq ($(INTERNAL_FLASH_FILESYSTEM),1) OPTIMIZATION_FLAGS ?= -Os @@ -124,3 +131,10 @@ ifeq ($(INTERNAL_FLASH_FILESYSTEM),1) endif endif endif + +# Cannot have BLEIO without SoftDevice +ifeq ($(SD), ) +CIRCUITPY_BLEIO_NATIVE = 0 +CIRCUITPY_BLE_FILE_SERVICE = 0 +CIRCUITPY_BLE_SERIAL_SERVICE = 0 +endif diff --git a/ports/nordic/peripherals/nrf/nrf52833/power.c b/ports/nordic/peripherals/nrf/nrf52833/power.c index 4f084e71649..f345a93fef7 100644 --- a/ports/nordic/peripherals/nrf/nrf52833/power.c +++ b/ports/nordic/peripherals/nrf/nrf52833/power.c @@ -8,6 +8,7 @@ #include "hal/nrf_nvmc.h" void nrf_peripherals_power_init(void) { + #if defined(CONFIG_REGOUT0_3V3) // Set GPIO reference voltage to 3.3V if it isn't already. REGOUT0 will get reset to 0xfffffff // if flash is erased, which sets the default to 1.8V // This matters only when "high voltage mode" is enabled, which is true on the PCA10059, @@ -29,4 +30,5 @@ void nrf_peripherals_power_init(void) { // Must reset to enable change. NVIC_SystemReset(); } + #endif } diff --git a/ports/nordic/peripherals/nrf/nrf52840/power.c b/ports/nordic/peripherals/nrf/nrf52840/power.c index e3a7e4135cf..b0efd1952fd 100644 --- a/ports/nordic/peripherals/nrf/nrf52840/power.c +++ b/ports/nordic/peripherals/nrf/nrf52840/power.c @@ -9,6 +9,7 @@ #include "peripherals/nrf/power.h" void nrf_peripherals_power_init(void) { + #if defined(CONFIG_REGOUT0_3V3) // Set GPIO reference voltage to 3.3V if it isn't already. REGOUT0 will get reset to 0xfffffff // if flash is erased, which sets the default to 1.8V // This matters only when "high voltage mode" is enabled, which is true on the PCA10059, @@ -30,4 +31,5 @@ void nrf_peripherals_power_init(void) { // Must reset to enable change. NVIC_SystemReset(); } + #endif } diff --git a/ports/nordic/peripherals/nrf/nvm.c b/ports/nordic/peripherals/nrf/nvm.c index 61a517f1152..74ef2529239 100644 --- a/ports/nordic/peripherals/nrf/nvm.c +++ b/ports/nordic/peripherals/nrf/nvm.c @@ -12,8 +12,34 @@ #include "nrfx_nvmc.h" +#if CIRCUITPY_BOOTLOADER_ARMED_WDT +#include "wdt.h" +#endif +#include "supervisor/shared/safe_mode.h" + #define FLASH_PAGE_SIZE (4096) +// The only two regions of internal flash CircuitPython owns: the nvm byte array +// and the internal-flash filesystem. +static bool flash_page_is_ours(uint32_t page_addr) { + if ((page_addr & (FLASH_PAGE_SIZE - 1)) != 0) { + return false; + } + #if CIRCUITPY_INTERNAL_NVM_SIZE > 0 + if (page_addr >= CIRCUITPY_INTERNAL_NVM_START_ADDR && + page_addr < CIRCUITPY_INTERNAL_NVM_START_ADDR + CIRCUITPY_INTERNAL_NVM_SIZE) { + return true; + } + #endif + #if CIRCUITPY_INTERNAL_FLASH_FILESYSTEM_SIZE > 0 + if (page_addr >= CIRCUITPY_INTERNAL_FLASH_FILESYSTEM_START_ADDR && + page_addr < CIRCUITPY_INTERNAL_FLASH_FILESYSTEM_START_ADDR + CIRCUITPY_INTERNAL_FLASH_FILESYSTEM_SIZE) { + return true; + } + #endif + return false; +} + #ifdef BLUETOOTH_SD #include "ble_drv.h" #include "nrf_sdm.h" @@ -68,6 +94,27 @@ bool sd_flash_write_sync(uint32_t *dest_words, uint32_t *src_words, uint32_t num #endif +void nrf_nvm_protect_init(void) { + #if CIRCUITPY_NRF_FLASH_PROTECT + const struct { + uint32_t addr; + uint32_t size; + } regions[] = { + { 0, CIRCUITPY_BLE_CONFIG_START_ADDR }, + { BOOTLOADER_START_ADDR, FLASH_SIZE - BOOTLOADER_START_ADDR }, + }; + + for (size_t i = 0; i < MP_ARRAY_SIZE(regions); i++) { + if (regions[i].size == 0) { + continue; + } + NRF_ACL->ACL[i].ADDR = regions[i].addr; + NRF_ACL->ACL[i].SIZE = regions[i].size; + NRF_ACL->ACL[i].PERM = ACL_ACL_PERM_WRITE_Disable << ACL_ACL_PERM_WRITE_Pos; + } + #endif +} + // The nRF52840 datasheet specifies a maximum of two writes to a flash // location before an erase is necessary, even if the write is all // ones (erased state). So we can't avoid erases even if the page @@ -75,6 +122,12 @@ bool sd_flash_write_sync(uint32_t *dest_words, uint32_t *src_words, uint32_t num // writes to a page. bool nrf_nvm_safe_flash_page_write(uint32_t page_addr, uint8_t *data) { + if (!flash_page_is_ours(page_addr)) { + // Out of bounds write that should never have been asked for, + // reset into safe mode + reset_into_safe_mode(SAFE_MODE_FLASH_WRITE_FAIL); + } + #ifdef BLUETOOTH_SD if (sd_is_enabled()) { uint32_t err_code; @@ -112,6 +165,11 @@ bool nrf_nvm_safe_flash_page_write(uint32_t page_addr, uint8_t *data) { } #endif + #if CIRCUITPY_BOOTLOADER_ARMED_WDT + // feed bootloader watchdog per page write + bootloader_wdt_feed(); + #endif + nrfx_nvmc_page_erase(page_addr); nrfx_nvmc_bytes_write(page_addr, data, FLASH_PAGE_SIZE); return true; diff --git a/ports/nordic/peripherals/nrf/nvm.h b/ports/nordic/peripherals/nrf/nvm.h index aff98094469..fa3517cba1d 100644 --- a/ports/nordic/peripherals/nrf/nvm.h +++ b/ports/nordic/peripherals/nrf/nvm.h @@ -7,11 +7,20 @@ #pragma once +// Without a SoftDevice these do not arrive via nrf_sdm.h's include chain. +#include +#include + #define FLASH_PAGE_SIZE (4096) -#if BLUETOOTH_SD +#ifdef BLUETOOTH_SD bool sd_flash_page_erase_sync(uint32_t page_number); bool sd_flash_write_sync(uint32_t *dest_words, uint32_t *src_words, uint32_t num_words); #endif bool nrf_nvm_safe_flash_page_write(uint32_t page_addr, uint8_t *data); + +// Hardware-write-protect the parts of internal flash CircuitPython does not +// own, using the ACL peripheral. Call as early in start up as possible, the +// configuration cannot be changed again until the next reset. +void nrf_nvm_protect_init(void); diff --git a/ports/nordic/supervisor/internal_flash.c b/ports/nordic/supervisor/internal_flash.c index 88b6a274d06..b6fdbccf4d7 100644 --- a/ports/nordic/supervisor/internal_flash.c +++ b/ports/nordic/supervisor/internal_flash.c @@ -36,6 +36,13 @@ static inline uint32_t lba2addr(uint32_t block) { return CIRCUITPY_INTERNAL_FLASH_FILESYSTEM_START_ADDR + block * FILESYSTEM_BLOCK_SIZE; } +// Block numbers reach here from FAT structures on the media and, whenever USB +// MSC is enabled, straight from the host. +static bool blocks_in_range(uint32_t lba, uint32_t num_blocks) { + uint32_t block_count = supervisor_flash_get_block_count(); + return lba <= block_count && num_blocks <= block_count - lba; +} + void supervisor_flash_init(void) { } @@ -61,6 +68,10 @@ void port_internal_flash_flush(void) { } mp_uint_t supervisor_flash_read_blocks(uint8_t *dest, uint32_t block, uint32_t num_blocks) { + if (!blocks_in_range(block, num_blocks)) { + return 1; // failure + } + // Must write out anything in cache before trying to read. supervisor_flash_flush(); @@ -70,6 +81,10 @@ mp_uint_t supervisor_flash_read_blocks(uint8_t *dest, uint32_t block, uint32_t n } mp_uint_t supervisor_flash_write_blocks(const uint8_t *src, uint32_t lba, uint32_t num_blocks) { + if (!blocks_in_range(lba, num_blocks)) { + return 1; // failure + } + while (num_blocks) { uint32_t const addr = lba2addr(lba); uint32_t const page_addr = addr & ~(FLASH_PAGE_SIZE - 1); diff --git a/ports/nordic/supervisor/port.c b/ports/nordic/supervisor/port.c index 1eabfcbe216..b909f983d62 100644 --- a/ports/nordic/supervisor/port.c +++ b/ports/nordic/supervisor/port.c @@ -19,10 +19,21 @@ #include "nrf/cache.h" #include "nrf/clocks.h" +#include "nrf/nvm.h" #include "nrf/power.h" #include "nrf/timers.h" +#if CIRCUITPY_BOOTLOADER_ARMED_WDT +#include "wdt.h" +#endif + +#if defined(CIRCUITPY_SP1EMMC) && CIRCUITPY_SP1EMMC +#include "bindings/sp1emmc/EMMC.h" +#endif +// The SoftDevice headers (nrf_sdm.h / nrf_soc.h) come in via mpconfigport.h. +#ifdef BLUETOOTH_SD #include "nrf_nvic.h" +#endif #include "common-hal/microcontroller/Pin.h" #include "common-hal/alarm/time/TimeAlarm.h" @@ -34,7 +45,6 @@ #include "common-hal/watchdog/WatchDogTimer.h" #include "common-hal/alarm/__init__.h" -#include "shared-bindings/_bleio/__init__.h" #include "shared-bindings/microcontroller/__init__.h" #include "shared-bindings/rtc/__init__.h" @@ -59,6 +69,15 @@ static void power_warning_handler(void) { uint32_t reset_reason_saved = 0; const nrfx_rtc_t rtc_instance = NRFX_RTC_INSTANCE(2); +#if CIRCUITPY_BOOTLOADER_ARMED_WDT +// Channels 0 and 1 of this RTC are taken (deadline waits and light sleep); +// RTC2 has four. +#define BOOTLOADER_WDT_WAKE_RTC_CHANNEL (3) +// How long the CPU may stay in WFI before the main loop must get another look +// in. +#define BOOTLOADER_WDT_WAKE_TICKS (1024) +#endif + nrfx_rtc_config_t rtc_config = { .prescaler = RTC_FREQ_TO_PRESCALER(0x8000), .reliable = 0, @@ -90,6 +109,12 @@ static void rtc_handler(nrfx_rtc_int_type_t int_type) { sleepmem_wakeup_event = SLEEPMEM_WAKEUP_BY_TIMER; #endif nrfx_rtc_cc_set(&rtc_instance, 1, 0, false); + #if CIRCUITPY_BOOTLOADER_ARMED_WDT + } else if (int_type == NRFX_RTC_INT_COMPARE3) { + // The watchdog wake timer armed in port_idle_until_interrupt(). Waking + // is the entire job; the main loop does the feeding. + nrfx_rtc_cc_set(&rtc_instance, BOOTLOADER_WDT_WAKE_RTC_CHANNEL, 0, false); + #endif } } @@ -131,6 +156,19 @@ void tick_set_prescaler(uint32_t prescaler_val) { } safe_mode_t port_init(void) { + #if CIRCUITPY_BOOTLOADER_ARMED_WDT + // Feed bootloader wdt before anything else + bootloader_wdt_feed(); + #endif + + // Next, before any code that could go wrong has run: lock the flash regions + // we do not own out of reach of NVMC for the rest of this boot. + nrf_nvm_protect_init(); + + // Then, before any peripheral is touched: let a board quiesce + // whatever its bootloader left running. + board_early_init(); + nrf_peripherals_clocks_init(); // If GPIO voltage is set wrong in UICR, this will fix it, and @@ -175,13 +213,19 @@ safe_mode_t port_init(void) { // next time we reboot. if (reset_reason_saved & POWER_RESETREAS_DOG_Msk) { NRF_POWER->RESETREAS = POWER_RESETREAS_DOG_Msk; - uint32_t usb_reg = NRF_POWER->USBREGSTATUS; + #if CIRCUITPY_SAFE_MODE_ON_WATCHDOG_REQUIRES_USB // If USB is connected, then the user might be editing `code.py`, // in which case we should reboot into Safe Mode. + uint32_t usb_reg = NRF_POWER->USBREGSTATUS; if (usb_reg & POWER_USBREGSTATUS_VBUSDETECT_Msk) { return SAFE_MODE_WATCHDOG; } + #else + // The bootloader owns the watchdog, so it can only have bitten because + // the main loop stopped. Report it whether or not a host is attached. + return SAFE_MODE_WATCHDOG; + #endif } return SAFE_MODE_NONE; @@ -204,6 +248,12 @@ void reset_port(void) { rtc_reset(); #endif + #if defined(CIRCUITPY_SP1EMMC) && CIRCUITPY_SP1EMMC + // board_reset_pin_defaults() will already have asserted the card's reset + // and dropped its VCCQ rail. + sp1emmc_reset(); + #endif + timers_reset(); #if CIRCUITPY_WATCHDOG @@ -218,9 +268,12 @@ void reset_port(void) { } void reset_to_bootloader(void) { - enum { DFU_MAGIC_SERIAL = 0x4e }; - - NRF_POWER->GPREGRET = DFU_MAGIC_SERIAL; + NRF_POWER->GPREGRET = BOOTLOADER_DFU_MAGIC; + #ifdef BOOTLOADER_DFU_MAGIC2 + // bootloader's magic is 16 bits wide, split across both retention + // registers. + NRF_POWER->GPREGRET2 = BOOTLOADER_DFU_MAGIC2; + #endif reset_cpu(); } @@ -317,12 +370,18 @@ void port_idle_until_interrupt(void) { qspi_disable(); #endif + #if CIRCUITPY_BOOTLOADER_ARMED_WDT + bootloader_wdt_feed(); + port_interrupt_after_ticks_ch(BOOTLOADER_WDT_WAKE_RTC_CHANNEL, BOOTLOADER_WDT_WAKE_TICKS); + #endif + // Clear the FPU interrupt because it can prevent us from sleeping. if (NVIC_GetPendingIRQ(FPU_IRQn)) { __set_FPSCR(__get_FPSCR() & ~(0x9f)); (void)__get_FPSCR(); NVIC_ClearPendingIRQ(FPU_IRQn); } + #ifdef BLUETOOTH_SD uint8_t sd_enabled; sd_softdevice_is_enabled(&sd_enabled); @@ -330,7 +389,10 @@ void port_idle_until_interrupt(void) { if (!background_callback_pending()) { sd_app_evt_wait(); } - } else { + return; + } + #endif + { // Call wait for interrupt ourselves if the SD isn't enabled. // Note that `wfi` should be called with interrupts disabled, // to ensure that the queue is properly drained. The `wfi` diff --git a/supervisor/board.h b/supervisor/board.h index 0920bf4e577..7672e47009b 100644 --- a/supervisor/board.h +++ b/supervisor/board.h @@ -15,9 +15,18 @@ // way. bool board_requests_safe_mode(void); +// Called from port_init() before any peripheral is initialized, for boards +// whose bootloader hands over live peripherals and enabled interrupts. Nothing +// is available yet: no heap, no filesystem, no serial. Keep it to register +// writes. +void board_early_init(void); + // Initializes board related state once on start up. void board_init(void); +// Re-apply the board's quiescent pin configuration. +void board_reset_pin_defaults(void); + // Reset the state of off MCU components such as neopixels. void reset_board(void); diff --git a/supervisor/shared/board.c b/supervisor/shared/board.c index 192bf7a65c8..81227f91913 100644 --- a/supervisor/shared/board.c +++ b/supervisor/shared/board.c @@ -33,10 +33,18 @@ MP_WEAK bool board_requests_safe_mode(void) { return false; } +// Do-nothing so not all boards need to provide this function. +MP_WEAK void board_early_init(void) { +} + // Do-nothing so not all boards need to provide this function. MP_WEAK void board_init(void) { } +// Do-nothing so not all boards need to provide this function. +MP_WEAK void board_reset_pin_defaults(void) { +} + // Do-nothing so not all boards need to provide this function. MP_WEAK void board_deinit(void) { } diff --git a/supervisor/shared/filesystem.c b/supervisor/shared/filesystem.c index 3998c304b7f..49ed97f40a5 100644 --- a/supervisor/shared/filesystem.c +++ b/supervisor/shared/filesystem.c @@ -19,6 +19,10 @@ #include "shared-module/sdcardio/__init__.h" #endif +#if defined(SP1EMMC_AUTOMOUNT) && SP1EMMC_AUTOMOUNT +#include "sp1emmc/automount.h" +#endif + static mp_vfs_mount_t _circuitpy_vfs; static fs_user_mount_t _circuitpy_usermount; @@ -230,6 +234,14 @@ bool filesystem_init(bool create_allowed, bool force_create) { #endif #endif + // Same reason as the SD card above, mount it before USB enumerates rather than + // lazily from tud_msc_test_unit_ready_cb() -- and the same requirement, + // that settings.toml (just mounted, a few lines up) is readable, because + // this is where CIRCUITPY_EMMC_USB is honoured. + #if defined(SP1EMMC_AUTOMOUNT) && SP1EMMC_AUTOMOUNT + sp1emmc_automount(); + #endif + return true; } diff --git a/supervisor/shared/usb/usb_msc_flash.c b/supervisor/shared/usb/usb_msc_flash.c index 163019b7328..bff4f24346b 100644 --- a/supervisor/shared/usb/usb_msc_flash.c +++ b/supervisor/shared/usb/usb_msc_flash.c @@ -38,7 +38,16 @@ #define SDCARD_COUNT 0 #endif -#define LUN_COUNT (1 + SAVES_COUNT + SDCARD_COUNT) +#if defined(SP1EMMC_AUTOMOUNT) && SP1EMMC_AUTOMOUNT +#include "sp1emmc/automount.h" + +#define EMMC_COUNT 1 +#define EMMC_LUN (1 + SAVES_COUNT + SDCARD_COUNT) +#else +#define EMMC_COUNT 0 +#endif + +#define LUN_COUNT (1 + SAVES_COUNT + SDCARD_COUNT + EMMC_COUNT) // The ellipsis range in the designated initializer of `ejected` is not standard C, // but it works in both gcc and clang. @@ -165,6 +174,26 @@ static fs_user_mount_t *get_vfs(int lun) { } } #endif + #ifdef EMMC_LUN + if (lun == EMMC_LUN) { + const char *path_under_mount; + + fs_user_mount_t *emmc = filesystem_for_path(SP1EMMC_AUTOMOUNT_PATH, &path_under_mount); + // Unlike the SD card there is no heap-mount case to allow: the eMMC's + // drive exists only when the supervisor mounted it (automount.c), and + // that mount is static. A user mount made by code.py stays a Python + // filesystem and never becomes a LUN. + if (emmc != root && + ((emmc->blockdev.flags & MP_BLOCKDEV_FLAG_NATIVE) != 0) && + !gc_ptr_on_heap(emmc)) { + return emmc; + } else { + // Clear any ejected state so that a remount causes it to reappear. + ejected[EMMC_LUN] = false; + locked[EMMC_LUN] = false; + } + } + #endif return NULL; } @@ -362,7 +391,15 @@ void tud_msc_inquiry_cb(uint8_t lun, uint8_t vendor_id[8], uint8_t product_id[16 (void)lun; memcpy(vendor_id, CFG_TUD_MSC_VENDOR, strlen(CFG_TUD_MSC_VENDOR)); - memcpy(product_id, CFG_TUD_MSC_PRODUCT, strlen(CFG_TUD_MSC_PRODUCT)); + #ifdef EMMC_LUN + if (lun == EMMC_LUN) { + static const char emmc_product_id[] = "SP-1 eMMC"; + memcpy(product_id, emmc_product_id, strlen(emmc_product_id)); + } else + #endif + { + memcpy(product_id, CFG_TUD_MSC_PRODUCT, strlen(CFG_TUD_MSC_PRODUCT)); + } memcpy(product_rev, CFG_TUD_MSC_PRODUCT_REV, strlen(CFG_TUD_MSC_PRODUCT_REV)); }