From 9336f6b826572702072766631a9c91dba4f16de8 Mon Sep 17 00:00:00 2001 From: foamyguy Date: Tue, 18 Aug 2026 14:39:13 -0500 Subject: [PATCH 01/14] TE SP-1 initial code --- extmod/vfs_blockdev.c | 23 + ports/nordic/Makefile | 66 +- ports/nordic/background.c | 13 + .../boards/teenage_engineering_sp1/board.c | 380 +++++++ .../teenage_engineering_sp1/mpconfigboard.h | 84 ++ .../teenage_engineering_sp1/mpconfigboard.mk | 87 ++ .../boards/teenage_engineering_sp1/pins.c | 89 ++ ports/nordic/common-hal/alarm/pin/PinAlarm.c | 2 + ports/nordic/common-hal/analogio/AnalogIn.c | 52 +- ports/nordic/common-hal/audiobusio/I2SOut.c | 35 +- ports/nordic/common-hal/audiobusio/I2SOut.h | 1 + ports/nordic/common-hal/busio/SPI.c | 9 + ports/nordic/common-hal/microcontroller/Pin.c | 6 + .../common-hal/microcontroller/__init__.c | 44 +- .../common-hal/neopixel_write/__init__.c | 2 + ports/nordic/device/nrf52/startup_nrf52840.c | 15 + ports/nordic/mpconfigport.h | 71 +- ports/nordic/mpconfigport.mk | 17 + ports/nordic/peripherals/nrf/nrf52833/power.c | 2 + ports/nordic/peripherals/nrf/nrf52840/power.c | 2 + ports/nordic/peripherals/nrf/nvm.c | 54 + ports/nordic/peripherals/nrf/nvm.h | 11 +- ports/nordic/peripherals/nrf/wdt.h | 53 + ports/nordic/power_off.c | 173 ++++ ports/nordic/power_off.h | 52 + ports/nordic/sp1_emmc/automount.c | 71 ++ ports/nordic/sp1_emmc/automount.h | 29 + ports/nordic/sp1_emmc/bindings.c | 772 +++++++++++++++ ports/nordic/sp1_emmc/blockdev.h | 31 + ports/nordic/sp1_emmc/sp1_emmc.c | 933 ++++++++++++++++++ ports/nordic/sp1_emmc/sp1_emmc.h | 159 +++ ports/nordic/sp1_emmc/sp1_emmc_hw.h | 251 +++++ ports/nordic/supervisor/internal_flash.c | 15 + ports/nordic/supervisor/port.c | 70 +- supervisor/board.h | 9 + supervisor/shared/board.c | 8 + supervisor/shared/filesystem.c | 12 + supervisor/shared/usb/usb_msc_flash.c | 41 +- 38 files changed, 3714 insertions(+), 30 deletions(-) create mode 100644 ports/nordic/boards/teenage_engineering_sp1/board.c create mode 100644 ports/nordic/boards/teenage_engineering_sp1/mpconfigboard.h create mode 100644 ports/nordic/boards/teenage_engineering_sp1/mpconfigboard.mk create mode 100644 ports/nordic/boards/teenage_engineering_sp1/pins.c create mode 100644 ports/nordic/peripherals/nrf/wdt.h create mode 100644 ports/nordic/power_off.c create mode 100644 ports/nordic/power_off.h create mode 100644 ports/nordic/sp1_emmc/automount.c create mode 100644 ports/nordic/sp1_emmc/automount.h create mode 100644 ports/nordic/sp1_emmc/bindings.c create mode 100644 ports/nordic/sp1_emmc/blockdev.h create mode 100644 ports/nordic/sp1_emmc/sp1_emmc.c create mode 100644 ports/nordic/sp1_emmc/sp1_emmc.h create mode 100644 ports/nordic/sp1_emmc/sp1_emmc_hw.h diff --git a/extmod/vfs_blockdev.c b/extmod/vfs_blockdev.c index 74d1262364e..092a87ed940 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_SP1_EMMC) && CIRCUITPY_SP1_EMMC +#include "sp1_emmc/blockdev.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_SP1_EMMC) && CIRCUITPY_SP1_EMMC + 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/ports/nordic/Makefile b/ports/nordic/Makefile index 1e283fc699c..9460d7ec63a 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 += \ @@ -112,14 +144,42 @@ endif SRC_C += \ background.c \ + power_off.c \ boards/$(BOARD)/board.c \ boards/$(BOARD)/pins.c \ device/$(MCU_VARIANT)/startup_$(MCU_SUB_VARIANT).c \ + nrfx/mdk/system_$(MCU_SUB_VARIANT).c \ + + +CIRCUITPY_SP1_EMMC ?= 0 +CFLAGS += -DCIRCUITPY_SP1_EMMC=$(CIRCUITPY_SP1_EMMC) +# HS_TIMING switch +CIRCUITPY_SP1_EMMC_HS ?= 0 +CFLAGS += -DSP1_EMMC_HS_TIMING=$(CIRCUITPY_SP1_EMMC_HS) +# write switch +CIRCUITPY_SP1_EMMC_WRITE ?= 0 +CFLAGS += -DSP1_EMMC_WRITE=$(CIRCUITPY_SP1_EMMC_WRITE) +# USB mass storage automount switch +CIRCUITPY_SP1_EMMC_USB ?= 0 +CFLAGS += -DSP1_EMMC_AUTOMOUNT=$(CIRCUITPY_SP1_EMMC_USB) +ifeq ($(CIRCUITPY_SP1_EMMC),1) +SRC_C += \ + sp1_emmc/sp1_emmc.c \ + sp1_emmc/bindings.c \ + +ifeq ($(CIRCUITPY_SP1_EMMC_USB),1) +SRC_C += sp1_emmc/automount.c +endif +endif + +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 +237,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/background.c b/ports/nordic/background.c index 9afade89136..2b74b478a5b 100644 --- a/ports/nordic/background.c +++ b/ports/nordic/background.c @@ -9,6 +9,13 @@ #include "py/runtime.h" #include "supervisor/port.h" +#include "nrf/wdt.h" +#include "power_off.h" + +#ifdef PROVING_GROUND_WEDGE +#include "supervisor/shared/safe_mode.h" +#endif + #if CIRCUITPY_DISPLAYIO #include "shared-module/displayio/__init__.h" #endif @@ -41,5 +48,11 @@ MP_WEAK void board_background_task(void) { } void port_background_task(void) { + bootloader_wdt_feed(); + + #ifdef BOARD_POWER_OFF_BUTTON_PIN + power_off_tick(); + #endif + board_background_task(); } 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..df2a18dd529 --- /dev/null +++ b/ports/nordic/boards/teenage_engineering_sp1/board.c @@ -0,0 +1,380 @@ +// 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 TE 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 "power_off.h" +#include "py/misc.h" +#include "nrf/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_SP1_EMMC +#include "sp1_emmc/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 + // actually charges. The current-set pin P1.00 is deliberately left alone, + // since the resistor configuration it selects has not been established. + 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); +} + +// -- 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..3ab2914b929 --- /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 peripherals/nrf/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..fdac1d57797 --- /dev/null +++ b/ports/nordic/boards/teenage_engineering_sp1/mpconfigboard.mk @@ -0,0 +1,87 @@ +# Placeholder USB IDs: pid.codes 0x1209/0x0001 is the "test" PID, valid for +# development only. A real allocation is needed before any release. Do NOT +# ship Teenage Engineering's 0x2367 -- this is not their firmware. +USB_VID = 0x1209 +USB_PID = 0x0001 +USB_PRODUCT = "SP-1 (CircuitPython)" +USB_MANUFACTURER = "Teenage Engineering" + +MCU_CHIP = nrf52840 + +# No UICR write may survive into this board's image (plan §4.5.a). UICR is +# undoable only by ERASEALL, which needs SWD; the SP-1's SWD pads are not +# reachable, and an ERASEALL would take TE's bootloader with it — the only copy +# in existence, since the USB protocol has no read command. So each of the +# three conditional UICR writes on the boot path is turned off here, and the +# acceptance test is the disassembly: every reference to 0x10001000 in +# firmware.elf must be a read. +# +# 1. PSELRESET. The SP-1 has no reset pin and P0.18's wiring is unknown. With +# CONFIG_GPIO_AS_PINRESET on, SystemInit would burn PSELRESET[0..1] = 18 +# on the first boot (PSELRESET is almost certainly erased on this device), +# turning P0.18 into nRESET forever — if anything ever pulls that net low, +# the chip is held in reset and unreachable even over SWD. +NRF_GPIO_AS_PINRESET = 0 + +# 2. REGOUT0. Fires only in high-voltage mode (VDDH supplied) with REGOUT0 +# unprogrammed. The proving ground never reached it — a USB-powered board +# reads MAINREGSTATUS = 0 — but the SP-1 runs from a LiPo and may well be +# VDDH-supplied, which is exactly the case that writes UICR. Safe to remove +# either way: the stock firmware drives 3.3 V logic (eMMC, codecs, LEDs) on +# this hardware today, so REGOUT0 is already whatever it needs to be. +NRF_REGOUT0_3V3 = 0 + +# 3. NFCPINS. P0.09/P0.10 are the NFC pins and are TAS_RESET and BT_RESET here. +# Stock firmware drives both as GPIO, so NFCPINS.PROTECT must already be +# cleared on this device and the write cannot fire — but "must already be" +# is not the bar for a one-way change, and if the assumption is wrong the +# right outcome is two dead reset lines to diagnose, not a UICR burn. The +# actual value is read from the REPL at first light (plan §5.2). +NRF_NFCT_PINS_AS_GPIOS = 0 + +# No SoftDevice. It would have to live at 0x1000, which is inside the TE +# 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 TE 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 +# audiomp3 would otherwise come along for free with audiocore on a full build. +# It is 7.1's §6 A8 item -- decode CPU headroom on this chip is unassessed and +# it costs ~26 KB of flash -- so it stays off until it is deliberately assessed. +CIRCUITPY_AUDIOMP3 = 0 + +# eMMC +CIRCUITPY_SP1_EMMC = 1 + +# HS_TIMING for EMMC +CIRCUITPY_SP1_EMMC_HS = 1 + +# EMMC writing +CIRCUITPY_SP1_EMMC_WRITE = 1 + +# auto mount EMMC as /sd +CIRCUITPY_SP1_EMMC_USB = 1 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..20182bfa0e5 --- /dev/null +++ b/ports/nordic/boards/teenage_engineering_sp1/pins.c @@ -0,0 +1,89 @@ +// 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 TE 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_DATA), 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_BCLK), MP_ROM_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/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..661e909b829 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 CIRCUITPY_SP1_EMMC +#include "sp1_emmc/sp1_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 CIRCUITPY_SP1_EMMC + 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..7f6b8ccc7f1 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,79 @@ // 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 + #if BOOTLOADER_START_ADDR % FLASH_ERASE_SIZE != 0 #error BOOTLOADER_START_ADDR must be on a flash erase boundary. #endif @@ -159,7 +221,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..f7a7fb65463 100644 --- a/ports/nordic/mpconfigport.mk +++ b/ports/nordic/mpconfigport.mk @@ -86,7 +86,16 @@ 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. +# +# This is one of the three UICR writes on the boot path (see the Makefile): 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 -- but +# then P0.09/P0.10 are usable as GPIO only if NFCPINS is *already* programmed, +# which is a claim about that specific device, not about the design. +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 +133,11 @@ ifeq ($(INTERNAL_FLASH_FILESYSTEM),1) endif endif endif + +# Cannot have BLEIO without SoftDevice, and +# without _bleio the BLE workflow services have nothing to run on. +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..865f21aabfd 100644 --- a/ports/nordic/peripherals/nrf/nvm.c +++ b/ports/nordic/peripherals/nrf/nvm.c @@ -12,8 +12,32 @@ #include "nrfx_nvmc.h" +#include "nrf/wdt.h" +#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 +92,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 +120,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 +163,9 @@ bool nrf_nvm_safe_flash_page_write(uint32_t page_addr, uint8_t *data) { } #endif + // feed bootloader watchdog per page write + bootloader_wdt_feed(); + 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/peripherals/nrf/wdt.h b/ports/nordic/peripherals/nrf/wdt.h new file mode 100644 index 00000000000..df7c838f8e1 --- /dev/null +++ b/ports/nordic/peripherals/nrf/wdt.h @@ -0,0 +1,53 @@ +// 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 a watchdog that was started by the board's bootloader, 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. +// +// A board opts in with CIRCUITPY_BOOTLOADER_ARMED_WDT in its mpconfigboard.h, +// and should normally also set CIRCUITPY_WATCHDOG = 0, since +// common_hal_watchdog_set_mode() would call nrfx_wdt_init() on the running +// peripheral, where the timeout write is silently ignored. + +// 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" + +#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). +#ifndef CIRCUITPY_SAFE_MODE_ON_WATCHDOG_REQUIRES_USB +#define CIRCUITPY_SAFE_MODE_ON_WATCHDOG_REQUIRES_USB (!CIRCUITPY_BOOTLOADER_ARMED_WDT) +#endif + +// 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/power_off.c b/ports/nordic/power_off.c new file mode 100644 index 00000000000..7633ab70d37 --- /dev/null +++ b/ports/nordic/power_off.c @@ -0,0 +1,173 @@ +// 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 "nrf/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 -- which is the point. The way out of a + // running program must not depend on that program's cooperation. + 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/power_off.h b/ports/nordic/power_off.h new file mode 100644 index 00000000000..3be8be42b2b --- /dev/null +++ b/ports/nordic/power_off.h @@ -0,0 +1,52 @@ +// 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 reset default, and where a digitalio deinit leaves it -- +// the poll reconnects it with a pull-up. A board is still free to configure it +// itself, and anything already configured is left alone. This is deliberate +// belt-and-braces: an unconfigured pin reads as permanently held, which would +// disable the gesture silently and for good. +// +// This lives in the supervisor rather than in Python on purpose. On a device +// with no reset pin and no removable battery it is half of the safety design: +// wherever the watchdog is being fed, the way out has to be live too -- +// including at the REPL, in safe mode, and while a program that has stopped +// listening is running. + +#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 port_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. Called with the button still held, before the wake-up is armed, so it +// may take as long as it needs. Weak; the default does nothing. +void board_power_off_prepare(void); + +#endif diff --git a/ports/nordic/sp1_emmc/automount.c b/ports/nordic/sp1_emmc/automount.c new file mode 100644 index 00000000000..daf9c9d2ec5 --- /dev/null +++ b/ports/nordic/sp1_emmc/automount.c @@ -0,0 +1,71 @@ +// 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 "sp1_emmc/automount.h" + +#if defined(SP1_EMMC_AUTOMOUNT) && SP1_EMMC_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" + + +static mp_vfs_mount_t _emmc_vfs; +static fs_user_mount_t _emmc_usermount; + +static bool _tried; + +void sp1emmc_automount(void) { + if (_tried) { + return; + } + _tried = true; + + if (get_safe_mode() != SAFE_MODE_NONE) { + return; + } + + bool enabled = true; + (void)settings_get_bool("CIRCUITPY_EMMC_USB", &enabled); + if (!enabled) { + return; + } + + mp_obj_t dev = sp1emmc_automount_construct(true, true); + if (dev == MP_OBJ_NULL) { + 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) { + 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 = SP1_EMMC_AUTOMOUNT_PATH; + emmc_vfs->len = sizeof(SP1_EMMC_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; +} + +#endif // SP1_EMMC_AUTOMOUNT diff --git a/ports/nordic/sp1_emmc/automount.h b/ports/nordic/sp1_emmc/automount.h new file mode 100644 index 00000000000..7414861c271 --- /dev/null +++ b/ports/nordic/sp1_emmc/automount.h @@ -0,0 +1,29 @@ +// 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(SP1_EMMC_AUTOMOUNT) && SP1_EMMC_AUTOMOUNT + +#ifndef SP1_EMMC_AUTOMOUNT_PATH +#define SP1_EMMC_AUTOMOUNT_PATH "/sd" +#endif + +void sp1emmc_automount(void); + +bool sp1emmc_is_automounted(void); + +mp_obj_t sp1emmc_automount_construct(bool high_speed, bool write_enabled); + +#else + +static inline bool sp1emmc_is_automounted(void) { + return false; +} + +#endif diff --git a/ports/nordic/sp1_emmc/bindings.c b/ports/nordic/sp1_emmc/bindings.c new file mode 100644 index 00000000000..56dc2ceaeb0 --- /dev/null +++ b/ports/nordic/sp1_emmc/bindings.c @@ -0,0 +1,772 @@ +// 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 + +// `sp1emmc` -- Python access to the SP-1's 4 GB eMMC as a block device. +// +// The card is FAT-formatted (7.3.a) and the songs on it are WAV files, so this +// module is a block device and nothing more. It used to carry two pieces of +// the stock firmware's custom album format -- the TE sector decode (plan 7.2 +// D8) and the stem loader's COBS packet layer (7.2.w W6) -- both removed on +// 2026-08-17; te_song_decode/ in the repo root keeps them if they are ever +// wanted back. +// +// import sp1emmc +// e = sp1emmc.EMMC() # VCCQ on, reset released, CMD0..16, EXT_CSD +// e = sp1emmc.EMMC(high_speed=True) # ... then HS_TIMING=1 + 32 MHz (D11) +// e.count # 7_733_248 blocks of 512 bytes +// buf = bytearray(512) +// e.readblocks(0, buf) +// e.deinit() +// +// Writing takes BOTH gates (7.2.w W2): SP1_EMMC_WRITE in the build, and +// +// e = sp1emmc.EMMC(write_enabled=True) +// e.writeblocks(0, buf) # 512-byte multiples, no address floor +// +// A default EMMC() is exactly the read-only object 7.2 measured -- writeblocks +// raises on it. The two keywords compose: +// +// e = sp1emmc.EMMC(high_speed=True, write_enabled=True) # 7.2.w.g +// +// which is the read-write M32 object a FAT mount wants. 7.2.w's W3 refused +// that pairing until the write direction's launch edge had been modelled; it +// has been, and the answer was that the card's input edge never moved. +// +// There is no filesystem on this chip and this module does not invent one: +// read/writeblocks() only match the native block-device protocol's shape so a +// future FAT-on-eMMC could hand the object to storage.VfsFat unchanged. + +#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 "sp1_emmc/automount.h" +#include "sp1_emmc/blockdev.h" +#include "sp1_emmc/sp1_emmc.h" +#include "sp1_emmc/sp1_emmc_hw.h" + +static bool s_constructed; + +typedef struct { + mp_obj_base_t base; + bool deinited; + bool write_enabled; +} sp1emmc_emmc_obj_t; + +#if SP1_EMMC_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 SP1_EMMC_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_diag.cmd0_sent) { + return "cmd0"; + } + if (g_emmc_diag.cmd1_retries < 0) { + return "cmd1 (card never ready)"; + } + if (!g_emmc_diag.cmd2_resp) { + return "cmd2 (no CID)"; + } + if (!g_emmc_diag.cmd3_resp) { + return "cmd3"; + } + if (!g_emmc_diag.cmd7_resp) { + return "cmd7 (select)"; + } + if (!g_emmc_diag.cmd16_resp) { + return "cmd16 (blocklen)"; + } + return "ext_csd"; +} + + +#if SP1_EMMC_HS_TIMING +static const char *hs_failure_stage(void) {. + switch (g_emmc_diag.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_diag.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 "M32 smoke test (fell back to 16 MHz)"; + } +} +#endif + +// 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 SP1_EMMC_HS_TIMING + if (high_speed && !emmc_set_high_speed()) { + const char *stage = hs_failure_stage(); + release_hardware(); + *hs_failed = true; + return stage; + } + #else + (void)high_speed; + #endif + emmc_prof_reset(); + return NULL; +} + +#if SP1_EMMC_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; + #if SP1_EMMC_WRITE + s_automount_obj.write_enabled = write_enabled; + #else + (void)write_enabled; + s_automount_obj.write_enabled = false; + #endif + s_automounted = true; + return MP_OBJ_FROM_PTR(&s_automount_obj); +} + +bool sp1emmc_is_automounted(void) { + return s_automounted; +} +#endif + +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 !SP1_EMMC_HS_TIMING + if (high_speed) { + mp_raise_ValueError(MP_ERROR_TEXT("high_speed not compiled in")); + } + #endif + #if !SP1_EMMC_WRITE + if (write_enabled) { + mp_raise_ValueError(MP_ERROR_TEXT("write_enabled not compiled in")); + } + #endif + + #if SP1_EMMC_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); +} + +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); + +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__); + +// readblocks(start_block, buf) -- len(buf) must be a multiple of 512. +#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; +} + +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(MP_ERROR_TEXT("Buffer length must be a multiple of 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("Block 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); + +// writeblocks(start_block, buf) -- len(buf) must be a multiple of 512. +#if SP1_EMMC_WRITE + +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++) { + if (attempt) { + g_emmc_diag.wr_retries++; + } + 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; +} + +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: construct EMMC(write_enabled=True) to write")); + } + + 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(MP_ERROR_TEXT("Buffer length must be a multiple of 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("Block 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); +#endif + +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); + +// The op numbers are restated in sp1_emmc.h because the driver also compiles +// for the host harness, which has no extmod headers. If upstream ever +// renumbers them, fail here rather than on the card. +MP_STATIC_ASSERT(EMMC_IOCTL_INIT == MP_BLOCKDEV_IOCTL_INIT); +MP_STATIC_ASSERT(EMMC_IOCTL_DEINIT == MP_BLOCKDEV_IOCTL_DEINIT); +MP_STATIC_ASSERT(EMMC_IOCTL_SYNC == MP_BLOCKDEV_IOCTL_SYNC); +MP_STATIC_ASSERT(EMMC_IOCTL_BLOCK_COUNT == MP_BLOCKDEV_IOCTL_BLOCK_COUNT); +MP_STATIC_ASSERT(EMMC_IOCTL_BLOCK_SIZE == MP_BLOCKDEV_IOCTL_BLOCK_SIZE); +MP_STATIC_ASSERT(EMMC_IOCTL_BLOCK_ERASE == MP_BLOCKDEV_IOCTL_BLOCK_ERASE); + +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) { + #if SP1_EMMC_WRITE + 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); + #else + (void)self_in; + (void)buf; + (void)start_block; + (void)nblocks; + return -MP_EROFS; + #endif +} + +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); + #if SP1_EMMC_WRITE + return !self->deinited && self->write_enabled; + #else + (void)self; + return false; + #endif +} + +// read_ext_csd() -> bytes, the card's 512-byte EXT_CSD (CMD8). Read-only; +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); + +// status() -> int, the card status register (CMD13 SEND_STATUS), R1 bits [39:8] of the response. +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); + +// reset_profile() zeros the per-block cycle accumulators. +static mp_obj_t sp1emmc_emmc_reset_profile(mp_obj_t self_in) { + sp1emmc_emmc_obj_t *self = MP_OBJ_TO_PTR(self_in); + check_for_deinit(self); + emmc_prof_reset(); + return mp_const_none; +} +static MP_DEFINE_CONST_FUN_OBJ_1(sp1emmc_emmc_reset_profile_obj, sp1emmc_emmc_reset_profile); + +// ---- properties ----------------------------------------------------------- + +static void diag_store(mp_obj_t dict, qstr key, mp_obj_t value) { + mp_obj_dict_store(dict, MP_OBJ_NEW_QSTR(key), value); +} + +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); + +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: the 16-byte CID from CMD2. MID 0x11 (Toshiba) +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_diag.cid, sizeof(g_emmc_diag.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); + +static mp_obj_t sp1emmc_emmc_get_crc_errors(mp_obj_t self_in) { + return mp_obj_new_int_from_uint(g_emmc_diag.crc_rd_errs); +} +static MP_DEFINE_CONST_FUN_OBJ_1(sp1emmc_emmc_get_crc_errors_obj, sp1emmc_emmc_get_crc_errors); +MP_PROPERTY_GETTER(sp1emmc_emmc_crc_errors_obj, (mp_obj_t)&sp1emmc_emmc_get_crc_errors_obj); + +static mp_obj_t sp1emmc_emmc_get_retries(mp_obj_t self_in) { + return mp_obj_new_int_from_uint(g_emmc_diag.cmd_retries); +} +static MP_DEFINE_CONST_FUN_OBJ_1(sp1emmc_emmc_get_retries_obj, sp1emmc_emmc_get_retries); +MP_PROPERTY_GETTER(sp1emmc_emmc_retries_obj, (mp_obj_t)&sp1emmc_emmc_get_retries_obj); + +static mp_obj_t sp1emmc_emmc_get_busy_timeouts(mp_obj_t self_in) { + return mp_obj_new_int_from_uint(g_emmc_diag.busy_timeouts); +} +static MP_DEFINE_CONST_FUN_OBJ_1(sp1emmc_emmc_get_busy_timeouts_obj, sp1emmc_emmc_get_busy_timeouts); +MP_PROPERTY_GETTER(sp1emmc_emmc_busy_timeouts_obj, (mp_obj_t)&sp1emmc_emmc_get_busy_timeouts_obj); + +static mp_obj_t sp1emmc_emmc_get_rd_wait_us_max(mp_obj_t self_in) { + return mp_obj_new_int_from_uint(g_emmc_diag.rd_wait_us_max); +} +static MP_DEFINE_CONST_FUN_OBJ_1(sp1emmc_emmc_get_rd_wait_us_max_obj, sp1emmc_emmc_get_rd_wait_us_max); +MP_PROPERTY_GETTER(sp1emmc_emmc_rd_wait_us_max_obj, (mp_obj_t)&sp1emmc_emmc_get_rd_wait_us_max_obj); + +// write_enabled: whether this object may write at all. False by default. +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); + +static mp_obj_t sp1emmc_emmc_get_wr_busy_us_max(mp_obj_t self_in) { + return mp_obj_new_int_from_uint(g_emmc_diag.wr_busy_us_max); +} +static MP_DEFINE_CONST_FUN_OBJ_1(sp1emmc_emmc_get_wr_busy_us_max_obj, sp1emmc_emmc_get_wr_busy_us_max); +MP_PROPERTY_GETTER(sp1emmc_emmc_wr_busy_us_max_obj, (mp_obj_t)&sp1emmc_emmc_get_wr_busy_us_max_obj); + +// write_errors: CRC-status tokens that were not 010. +static mp_obj_t sp1emmc_emmc_get_write_errors(mp_obj_t self_in) { + return mp_obj_new_int_from_uint(g_emmc_diag.werr); +} +static MP_DEFINE_CONST_FUN_OBJ_1(sp1emmc_emmc_get_write_errors_obj, sp1emmc_emmc_get_write_errors); +MP_PROPERTY_GETTER(sp1emmc_emmc_write_errors_obj, (mp_obj_t)&sp1emmc_emmc_get_write_errors_obj); + +// high_speed: True once HS_TIMING = 1 has been read back off the card AND the +// host clock is at M32. False means the bus is at 16 MHz +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_diag.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: the SPIM data-phase clock 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); + +// profile: where a block's time actually goes, in microseconds, averaged over +// every block read since the last reset_profile(). 7.2.c could only infer the +// split between the DMA and everything else; this measures it, which is what +// decides whether M32 can help (the DMA halves; the rest may not). +static mp_obj_t sp1emmc_emmc_get_profile(mp_obj_t self_in) { + sp1emmc_emmc_obj_t *self = MP_OBJ_TO_PTR(self_in); + check_for_deinit(self); + mp_obj_t d = mp_obj_new_dict(9); + uint32_t blocks = g_emmc_diag.prof_blocks; + uint32_t calls = g_emmc_diag.prof_calls; + diag_store(d, MP_QSTR_ok, mp_obj_new_bool(g_emmc_diag.prof_ok)); + diag_store(d, MP_QSTR_blocks, mp_obj_new_int_from_uint(blocks)); + diag_store(d, MP_QSTR_calls, mp_obj_new_int_from_uint(calls)); + // Per-block averages in microseconds. Integer division is fine at these + // magnitudes (hundreds of us) and keeps floats out of the hot path. + #define PROF_US(field, n) mp_obj_new_int_from_uint( \ + (n) ? (uint32_t)((field) / (n) / (EMMC_CYCLES_HZ / 1000000u)) : 0u) + diag_store(d, MP_QSTR_hunt_us, PROF_US(g_emmc_diag.prof_hunt_cyc, blocks)); + diag_store(d, MP_QSTR_dma_us, PROF_US(g_emmc_diag.prof_dma_cyc, blocks)); + diag_store(d, MP_QSTR_verify_us, PROF_US(g_emmc_diag.prof_verify_cyc, blocks)); + diag_store(d, MP_QSTR_block_us, PROF_US(g_emmc_diag.prof_block_cyc, blocks)); + diag_store(d, MP_QSTR_call_us, PROF_US(g_emmc_diag.prof_call_cyc, calls)); + // The command half: whole-call time minus the blocks inside it, per block. + // This is the CMD18/CMD12 handshake plus the bounds check -- the part M32 + // does nothing for. + uint64_t cmd_cyc = g_emmc_diag.prof_call_cyc > g_emmc_diag.prof_block_cyc + ? g_emmc_diag.prof_call_cyc - g_emmc_diag.prof_block_cyc : 0; + diag_store(d, MP_QSTR_cmd_us, PROF_US(cmd_cyc, blocks)); + #undef PROF_US + return d; +} +static MP_DEFINE_CONST_FUN_OBJ_1(sp1emmc_emmc_get_profile_obj, sp1emmc_emmc_get_profile); +MP_PROPERTY_GETTER(sp1emmc_emmc_profile_obj, (mp_obj_t)&sp1emmc_emmc_get_profile_obj); + +// The whole diagnostics struct, for the bring-up harness. +static mp_obj_t sp1emmc_emmc_get_diagnostics(mp_obj_t self_in) { + mp_obj_t d = mp_obj_new_dict(20); + diag_store(d, MP_QSTR_cmd0_sent, mp_obj_new_bool(g_emmc_diag.cmd0_sent)); + diag_store(d, MP_QSTR_cmd1_retries, mp_obj_new_int(g_emmc_diag.cmd1_retries)); + diag_store(d, MP_QSTR_cmd2_resp, mp_obj_new_bool(g_emmc_diag.cmd2_resp)); + diag_store(d, MP_QSTR_cmd2_tries, mp_obj_new_int(g_emmc_diag.cmd2_tries)); + diag_store(d, MP_QSTR_cmd2_clocks, mp_obj_new_int(g_emmc_diag.cmd2_clocks)); + diag_store(d, MP_QSTR_cmd3_resp, mp_obj_new_bool(g_emmc_diag.cmd3_resp)); + diag_store(d, MP_QSTR_cmd7_resp, mp_obj_new_bool(g_emmc_diag.cmd7_resp)); + diag_store(d, MP_QSTR_cmd16_resp, mp_obj_new_bool(g_emmc_diag.cmd16_resp)); + diag_store(d, MP_QSTR_ocr, mp_obj_new_bytes(g_emmc_diag.ocr, sizeof(g_emmc_diag.ocr))); + diag_store(d, MP_QSTR_r1, mp_obj_new_bytes(g_emmc_diag.r1, sizeof(g_emmc_diag.r1))); + diag_store(d, MP_QSTR_cid, mp_obj_new_bytes(g_emmc_diag.cid, sizeof(g_emmc_diag.cid))); + diag_store(d, MP_QSTR_init_us, mp_obj_new_int_from_uint(g_emmc_diag.init_us)); + diag_store(d, MP_QSTR_last_cmd_resp, mp_obj_new_bool(g_emmc_diag.last_cmd_resp)); + diag_store(d, MP_QSTR_resp_clocks, mp_obj_new_int(g_emmc_diag.resp_clocks)); + diag_store(d, MP_QSTR_rd_crc, mp_obj_new_int_from_uint(g_emmc_diag.rd_crc)); + diag_store(d, MP_QSTR_crc_errors, mp_obj_new_int_from_uint(g_emmc_diag.crc_rd_errs)); + diag_store(d, MP_QSTR_retries, mp_obj_new_int_from_uint(g_emmc_diag.cmd_retries)); + diag_store(d, MP_QSTR_busy_timeouts, mp_obj_new_int_from_uint(g_emmc_diag.busy_timeouts)); + diag_store(d, MP_QSTR_burst_aborts, mp_obj_new_int_from_uint(g_emmc_diag.burst_aborts)); + diag_store(d, MP_QSTR_rd_wait_us_max, mp_obj_new_int_from_uint(g_emmc_diag.rd_wait_us_max)); + diag_store(d, MP_QSTR_blocks_read, mp_obj_new_int_from_uint(g_emmc_diag.blocks_read)); + // The write path (7.2.w W2). These read 0/-1 on a build or a run that + // never wrote, which is how the bring-up harness proves a read-only image + // stayed read-only without inspecting the build flags. + diag_store(d, MP_QSTR_werr, mp_obj_new_int_from_uint(g_emmc_diag.werr)); + diag_store(d, MP_QSTR_wr_retries, mp_obj_new_int_from_uint(g_emmc_diag.wr_retries)); + diag_store(d, MP_QSTR_wr_busy_us_max, mp_obj_new_int_from_uint(g_emmc_diag.wr_busy_us_max)); + diag_store(d, MP_QSTR_blocks_written, mp_obj_new_int_from_uint(g_emmc_diag.blocks_written)); + diag_store(d, MP_QSTR_wr_status, mp_obj_new_int(g_emmc_diag.wr_status)); + // The HS_TIMING switch (plan D11). These read 0/False on a build or a run + // that never asked for it, which is how the harness proves no CMD6 went out. + diag_store(d, MP_QSTR_hs_requested, mp_obj_new_bool(g_emmc_diag.hs_requested)); + diag_store(d, MP_QSTR_cmd6_sent, mp_obj_new_bool(g_emmc_diag.cmd6_sent)); + diag_store(d, MP_QSTR_cmd6_resp, mp_obj_new_bool(g_emmc_diag.cmd6_resp)); + diag_store(d, MP_QSTR_hs_switch_error, mp_obj_new_bool(g_emmc_diag.hs_switch_error)); + diag_store(d, MP_QSTR_hs_verified, mp_obj_new_bool(g_emmc_diag.hs_verified)); + diag_store(d, MP_QSTR_hs_active, mp_obj_new_bool(g_emmc_diag.hs_active)); + diag_store(d, MP_QSTR_hs_dat_phase, mp_obj_new_bool(g_emmc_diag.hs_dat_phase)); + diag_store(d, MP_QSTR_hs_timing_at_init, mp_obj_new_int_from_uint(g_emmc_diag.hs_timing_at_init)); + diag_store(d, MP_QSTR_hs_timing_readback, mp_obj_new_int_from_uint(g_emmc_diag.hs_timing_readback)); + diag_store(d, MP_QSTR_device_type, mp_obj_new_int_from_uint(g_emmc_diag.device_type)); + diag_store(d, MP_QSTR_cmd6_busy_us, mp_obj_new_int_from_uint(g_emmc_diag.cmd6_busy_us)); + diag_store(d, MP_QSTR_cmd6_tran_us, mp_obj_new_int_from_uint(g_emmc_diag.cmd6_tran_us)); + diag_store(d, MP_QSTR_hs_stage, mp_obj_new_int_from_uint(g_emmc_diag.hs_stage)); + diag_store(d, MP_QSTR_frequency, mp_obj_new_int_from_uint(emmc_bus_hz())); + return d; +} +static MP_DEFINE_CONST_FUN_OBJ_1(sp1emmc_emmc_get_diagnostics_obj, sp1emmc_emmc_get_diagnostics); +MP_PROPERTY_GETTER(sp1emmc_emmc_diagnostics_obj, (mp_obj_t)&sp1emmc_emmc_get_diagnostics_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) }, + #if SP1_EMMC_WRITE + { MP_ROM_QSTR(MP_QSTR_writeblocks), MP_ROM_PTR(&sp1emmc_emmc_writeblocks_obj) }, + #endif + { 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_reset_profile), MP_ROM_PTR(&sp1emmc_emmc_reset_profile_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_profile), MP_ROM_PTR(&sp1emmc_emmc_profile_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_crc_errors), MP_ROM_PTR(&sp1emmc_emmc_crc_errors_obj) }, + { MP_ROM_QSTR(MP_QSTR_retries), MP_ROM_PTR(&sp1emmc_emmc_retries_obj) }, + { MP_ROM_QSTR(MP_QSTR_busy_timeouts), MP_ROM_PTR(&sp1emmc_emmc_busy_timeouts_obj) }, + { MP_ROM_QSTR(MP_QSTR_rd_wait_us_max), MP_ROM_PTR(&sp1emmc_emmc_rd_wait_us_max_obj) }, + { MP_ROM_QSTR(MP_QSTR_write_enabled), MP_ROM_PTR(&sp1emmc_emmc_write_enabled_obj) }, + { MP_ROM_QSTR(MP_QSTR_write_errors), MP_ROM_PTR(&sp1emmc_emmc_write_errors_obj) }, + { MP_ROM_QSTR(MP_QSTR_wr_busy_us_max), MP_ROM_PTR(&sp1emmc_emmc_wr_busy_us_max_obj) }, + { MP_ROM_QSTR(MP_QSTR_diagnostics), MP_ROM_PTR(&sp1emmc_emmc_diagnostics_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 + ); + +// ---- module --------------------------------------------------------------- + +// Module-level diagnostics and the automount flag (7.3.f). With the supervisor +// holding the card there is no EMMC object to ask, and the integrity counters +// are exactly what a USB transfer has to be graded on -- `crc_errors` after a +// desktop copies a file is the whole acceptance test. Cheap to expose: the +// getter never looked at `self` in the first place, because every counter it +// reports lives in g_emmc_diag. +static mp_obj_t sp1emmc_diagnostics(void) { + return sp1emmc_emmc_get_diagnostics(mp_const_none); +} +static MP_DEFINE_CONST_FUN_OBJ_0(sp1emmc_diagnostics_obj, sp1emmc_diagnostics); + +// True when the card belongs to the supervisor's /sd mount, false otherwise +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); + +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_diagnostics), MP_ROM_PTR(&sp1emmc_diagnostics_obj) }, + { MP_ROM_QSTR(MP_QSTR_automounted), MP_ROM_PTR(&sp1emmc_automounted_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/sp1_emmc/blockdev.h b/ports/nordic/sp1_emmc/blockdev.h new file mode 100644 index 00000000000..f5630eab633 --- /dev/null +++ b/ports/nordic/sp1_emmc/blockdev.h @@ -0,0 +1,31 @@ +// 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 native block-device face of sp1emmc.EMMC, split out of +// bindings.c so extmod/vfs_blockdev.c can reach it without dragging the rest of +// the module's headers. The same shape sdcardio and sdioio present. + +#pragma once + +#include "py/obj.h" + +extern const mp_obj_type_t sp1emmc_emmc_type; + +// 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); diff --git a/ports/nordic/sp1_emmc/sp1_emmc.c b/ports/nordic/sp1_emmc/sp1_emmc.c new file mode 100644 index 00000000000..68a36d4dbe1 --- /dev/null +++ b/ports/nordic/sp1_emmc/sp1_emmc.c @@ -0,0 +1,933 @@ +// 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 path always; the write path only with SP1_EMMC_WRITE = 1. +// ============================================================================ +// 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 "sp1_emmc.h" +#include "sp1_emmc_hw.h" + +#include + +#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; + +sp1_emmc_diag_t g_emmc_diag; + +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]; + +#define HALF(hd) do { if (hd) { EMMC_DELAY_US(hd); } } while (0) + +// ---- bounded-wait helpers (D6) --------------------------------------------- +// 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 (emmc_ticks() - t0) & EMMC_TICK_MASK; +} + +static inline uint32_t ticks_to_us(uint32_t t) { + return (uint32_t)(((uint64_t)t * 1000000u) / EMMC_TICKS_HZ); +} + +static inline void half_delay(uint32_t us) { + if (us) { + EMMC_DELAY_US(us); + } +} + +// 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; + } +} + +__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; +} + +#if SP1_EMMC_HOST_TEST +// Hooks so tools/test_sp1_emmc.py checks the CRCs that actually ship, rather +// than a copy of them. Not built for the device. +uint8_t sp1_emmc_test_crc7(const uint8_t *data, uint8_t len) { + return crc7(data, len); +} +uint16_t sp1_emmc_test_crc16(const uint8_t *data, uint32_t len) { + crc16_tab_init(); + return crc16(data, len); +} +#endif + +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(); + g_emmc_diag.resp_clocks = -1; + for (int t = 0; t < 200; t++) { + clk_pulse(); + if (!READ_CMD()) { + g_emmc_diag.resp_clocks = t; + break; + } + } + if (g_emmc_diag.resp_clocks < 0) { + 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 (send_command(cmd, arg, r1_out)) { + return true; + } + g_emmc_diag.cmd_retries++; + 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 { + EMMC_SLEEP_MS(2); + } + } + return false; +} + +// DATA read: per-bit CLK toggle uses the configurable (possibly 0) half-period. +__attribute__((optimize("O2"))) // read path only: -O2 safe for reads, NOT writes +static bool read_data_block(uint8_t *buf) { + const uint32_t hd = g_emmc_clk_half_us; + const uint32_t pc0 = emmc_cycles(); + + 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(hd); + EDGE_SETTLE(); + if (!RDAT_GET()) { + got_start = true; // leave with RCLK HIGH (as before) + break; + } + RCLK_LOW(); + HALF(hd); + } + uint32_t el = ticks_since(t0); + if (got_start) { + uint32_t us = ticks_to_us(el); + if (us > g_emmc_diag.rd_wait_us_max) { + g_emmc_diag.rd_wait_us_max = us; + } + break; + } + if (el >= lim) { + g_emmc_diag.busy_timeouts++; + return false; + } + if (el >= yield_at) { + emmc_yield(); + } + } + } + RCLK_LOW(); + HALF(hd); + const uint32_t pc1 = emmc_cycles(); + + // The start bit was just consumed by the bit-bang hunt above, so the + // remaining 512 data bytes + CRC16 are exactly byte-aligned. + sp1_emmc_spim_xfer(NULL, 0, s_dma_rx, sizeof(s_dma_rx)); + const uint32_t pc2 = emmc_cycles(); + memcpy(buf, s_dma_rx, EMMC_BLOCK_SIZE); + g_emmc_diag.rd_crc = (uint16_t)(((uint16_t)s_dma_rx[EMMC_BLOCK_SIZE] << 8) | + s_dma_rx[EMMC_BLOCK_SIZE + 1]); + RCLK_HIGH(); + HALF(hd); + RCLK_LOW(); + HALF(hd); // end bit + bool crc_ok = crc16(buf, EMMC_BLOCK_SIZE) == g_emmc_diag.rd_crc; + const uint32_t pc3 = emmc_cycles(); + g_emmc_diag.prof_hunt_cyc += pc1 - pc0; + g_emmc_diag.prof_dma_cyc += pc2 - pc1; + g_emmc_diag.prof_verify_cyc += pc3 - pc2; + g_emmc_diag.prof_block_cyc += pc3 - pc0; + g_emmc_diag.prof_blocks++; + if (!crc_ok) { + g_emmc_diag.crc_rd_errs++; // corrupt read: caller retries + DAT0_OUT(); + DAT0_HIGH(); + return false; + } + + DAT0_OUT(); + DAT0_HIGH(); + g_emmc_diag.blocks_read++; + return true; +} + +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))); + } +} + +bool emmc_init(void) { + uint32_t t_init0 = emmc_ticks(); + + 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_diag, 0, sizeof(g_emmc_diag)); + g_emmc_diag.cmd1_retries = -1; + g_emmc_diag.resp_clocks = -1; + g_emmc_diag.cmd2_clocks = -1; + + sp1_emmc_pins_init(); + sp1_emmc_spim_init(); // hardware-clocked data path, at M16 + g_emmc_diag.prof_ok = emmc_prof_init(); + crc16_tab_init(); + + CLK_LOW(); + CMD_HIGH(); + DAT0_HIGH(); + + VCCQ_ON(); + EMMC_SLEEP_MS(10); + + RST_ASSERT(); + EMMC_SLEEP_MS(1); + RST_RELEASE(); + EMMC_SLEEP_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_diag.cmd0_sent = true; + EMMC_SLEEP_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); + emmc_feed(); + EMMC_SLEEP_MS(1); + if (ok && (r3[1] & 0x80)) { // response seen AND busy bit set = ready + g_emmc_diag.cmd1_retries = retry; + break; + } + } + memcpy(g_emmc_diag.ocr, r3, 6); + if (g_emmc_diag.cmd1_retries < 0) { // card never responded ready -> stop + g_emmc_diag.init_us = ticks_to_us(ticks_since(t_init0)); + return false; + } + + for (int t = 0; t < 8; t++) { + g_emmc_diag.cmd2_tries = t + 1; + g_emmc_diag.cmd2_resp = send_command(2, 0, NULL); + g_emmc_diag.cmd2_clocks = g_emmc_diag.resp_clocks; + if (g_emmc_diag.cmd2_resp) { + drain_r2_cid(g_emmc_diag.cid); + break; + } + EMMC_SLEEP_MS(2); + } + EMMC_SLEEP_MS(1); + + uint8_t r6[6] = {0}; + s_rca = 0x0001u << 16; + g_emmc_diag.cmd3_resp = send_command_retry(3, s_rca, r6, 8); // SET_RELATIVE_ADDR + EMMC_SLEEP_MS(1); + + uint8_t r1[6] = {0}; + g_emmc_diag.cmd7_resp = send_command_retry(7, s_rca, r1, 8); // SELECT_CARD + memcpy(g_emmc_diag.r1, r1, 6); + EMMC_SLEEP_MS(1); + g_emmc_diag.cmd16_resp = send_command_retry(16, EMMC_BLOCK_SIZE, r1, 8); // SET_BLOCKLEN + EMMC_SLEEP_MS(1); + + // strict: ready only if the card actually selected AND accepted block length + s_ready = g_emmc_diag.cmd7_resp && g_emmc_diag.cmd16_resp; + if (s_ready) { + s_cmd_half_us = 0u; // identification done: full-speed commands + g_emmc_clk_half_us = 0u; + } + g_emmc_diag.init_us = ticks_to_us(ticks_since(t_init0)); + return s_ready; +} + +bool emmc_is_ready(void) { + 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 EMMC_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 EMMC_IOCTL_DEINIT: + case EMMC_IOCTL_SYNC: + case EMMC_IOCTL_BLOCK_ERASE: + break; + case EMMC_IOCTL_BLOCK_COUNT: + *out_value = s_block_count; + break; + case EMMC_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. With no write path +// and the card's volatile cache never enabled there is nothing to flush first, +// which is why 6.3's board_power_off_prepare() ordering stays correct +// unmodified (plan §4). The card is gone until the next emmc_init(). +void emmc_power_down(void) { + s_ready = false; + s_block_count = 0; + sp1_emmc_spim_deinit(); + RST_ASSERT(); + sp1_emmc_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]; + g_emmc_diag.device_type = buf[196]; + if (g_emmc_diag.hs_requested) { + g_emmc_diag.hs_timing_readback = buf[185]; + } else { + g_emmc_diag.hs_timing_at_init = buf[185]; + } + return true; +} + +#if SP1_EMMC_HS_TIMING || SP1_EMMC_WRITE +// ---- 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 -- emmc_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, uint32_t *elapsed_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) { + *elapsed_us = ticks_to_us(el); + DAT0_OUT(); // back to the read path's resting state + DAT0_HIGH(); + return true; + } + if (el >= lim) { + *elapsed_us = ticks_to_us(el); + g_emmc_diag.busy_timeouts++; + // DAT0 STAYS AN INPUT on a timeout + return false; + } + if (run_bg) { + emmc_yield(); + } else { + emmc_feed(); + } + } +} +#endif // SP1_EMMC_HS_TIMING || SP1_EMMC_WRITE + +#if SP1_EMMC_HS_TIMING + +// 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 *elapsed_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_diag.hs_switch_error = true; + *elapsed_us = ticks_to_us(ticks_since(t0)); + return false; + } + if (((status >> 9) & 0xFu) == 4u && ((status >> 8) & 1u)) { + *elapsed_us = ticks_to_us(ticks_since(t0)); + return true; // tran + ready_for_data + } + } + uint32_t el = ticks_since(t0); + if (el >= lim) { + g_emmc_diag.busy_timeouts++; + *elapsed_us = ticks_to_us(el); + return false; + } + emmc_yield(); + EMMC_SLEEP_MS(1); + } +} + +bool emmc_set_high_speed(void) { + if (!s_ready) { + return false; + } + g_emmc_diag.hs_requested = true; + // Gate on the card's own capability byte. + if (!(s_device_type & EMMC_DEVICE_TYPE_HS52)) { + return false; + } + g_emmc_diag.hs_stage = 1; + + uint8_t r1[6]; + g_emmc_diag.cmd6_sent = true; + g_emmc_diag.cmd6_resp = send_command_retry(6, EMMC_SWITCH_HS_TIMING_ARG, r1, 8); + if (!g_emmc_diag.cmd6_resp) { + return false; + } + g_emmc_diag.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, &g_emmc_diag.cmd6_busy_us, true)) { + return false; + } + g_emmc_diag.hs_stage = 3; + if (!wait_tran_after_switch(EMMC_CMD6_BUSY_US, &g_emmc_diag.cmd6_tran_us)) { + return false; + } + g_emmc_diag.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. + sp1_emmc_spim_set_config(SPIM_CONFIG_MODE1); + g_emmc_diag.hs_dat_phase = true; + + // 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) { + sp1_emmc_spim_set_config(SPIM_CONFIG_MODE0); + g_emmc_diag.hs_dat_phase = false; + return false; // still at M16, card still readable + } + g_emmc_diag.hs_verified = true; + g_emmc_diag.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. + sp1_emmc_spim_set_freq(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. + sp1_emmc_spim_set_freq(SPIM_FREQ_M16); + return false; + } + g_emmc_diag.hs_active = true; + g_emmc_diag.hs_stage = 6; + return true; +} +#endif // SP1_EMMC_HS_TIMING + +static bool read_blocks_inner(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) { + g_emmc_diag.last_cmd_resp = send_command_retry(17, block_addr, r1, 8); + if (!g_emmc_diag.last_cmd_resp) { + 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 + g_emmc_diag.last_cmd_resp = send_command_retry(18, block_addr, r1, 4); + if (!g_emmc_diag.last_cmd_resp) { + 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) { + (void)send_command_retry(12, 0, r1, 3); + g_emmc_diag.busy_timeouts++; + g_emmc_diag.burst_aborts++; + 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 public entry point exists only to time the whole call: subtract the +// per-block total from it and what is left is the CMD18 + CMD12 handshake plus +// the bounds check, which is the half of the ~465 us/block that 7.2.c could +// only infer (results file, "Streaming follow-ups"). +bool emmc_read_blocks(uint32_t block_addr, uint8_t *buf, uint32_t count) { + const uint32_t c0 = emmc_cycles(); + bool ok = read_blocks_inner(block_addr, buf, count); + g_emmc_diag.prof_call_cyc += emmc_cycles() - c0; + g_emmc_diag.prof_calls++; + return ok; +} + +#if SP1_EMMC_WRITE + +// 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 + +__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 = sp1_emmc_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 (sp1_emmc_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 = sp1_emmc_spim_config(); + if (saved_cfg != SPIM_CONFIG_MODE0) { + sp1_emmc_spim_set_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. + sp1_emmc_spim_xfer(tx, 2u + EMMC_BLOCK_SIZE + 2u, NULL, 0); + if (saved_cfg != SPIM_CONFIG_MODE0) { + sp1_emmc_spim_set_config(saved_cfg); + } + // END bit: DAT0 is back at its GPIO latch (output HIGH) -- clock it. + HALF(hd); + EDGE_SETTLE(); + RCLK_HIGH(); + HALF(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(); + g_emmc_diag.wr_status = -1; + for (int i = 0; i < 16; i++) { + RCLK_HIGH(); + HALF(hd); + EDGE_SETTLE(); + int start = (int)RDAT_GET(); + RCLK_LOW(); + HALF(hd); + if (!start) { + int st = 0; + for (int k = 0; k < 3; k++) { + RCLK_HIGH(); + HALF(hd); + EDGE_SETTLE(); + st = (st << 1) | (int)RDAT_GET(); + RCLK_LOW(); + HALF(hd); + } + g_emmc_diag.wr_status = st; + break; + } + } + + // Programming busy on DAT0 + uint32_t busy_us = 0; + if (!dat0_busy_wait(EMMC_WR_BUSY_US, &busy_us, false)) { + if (busy_us > g_emmc_diag.wr_busy_us_max) { + g_emmc_diag.wr_busy_us_max = busy_us; + } + return false; // DAT0 left an INPUT -- see the wait + } + if (busy_us > g_emmc_diag.wr_busy_us_max) { + g_emmc_diag.wr_busy_us_max = busy_us; + } + + // 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 (g_emmc_diag.wr_status != 0x2) { + g_emmc_diag.werr++; + return false; + } + g_emmc_diag.blocks_written++; + 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) { + g_emmc_diag.last_cmd_resp = send_command_retry(24, block_addr, r1, 8); + if (!g_emmc_diag.last_cmd_resp) { + 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. + g_emmc_diag.last_cmd_resp = send_command_retry(25, block_addr, r1, 4); + if (!g_emmc_diag.last_cmd_resp) { + 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) { + (void)send_command_retry(12, 0, r1, 3); + g_emmc_diag.busy_timeouts++; + g_emmc_diag.burst_aborts++; + 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); + + uint32_t busy_us = 0; + if (dat0_busy_wait(EMMC_WR_BUSY_US, &busy_us, false) && + busy_us > g_emmc_diag.wr_busy_us_max) { + g_emmc_diag.wr_busy_us_max = busy_us; + } + return true; +} +#endif // SP1_EMMC_WRITE + +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 sp1_emmc_spim_freq() == SPIM_FREQ_M32 ? 32000000u : 16000000u; +} + +void emmc_prof_reset(void) { + g_emmc_diag.prof_blocks = 0; + g_emmc_diag.prof_calls = 0; + g_emmc_diag.prof_hunt_cyc = 0; + g_emmc_diag.prof_dma_cyc = 0; + g_emmc_diag.prof_verify_cyc = 0; + g_emmc_diag.prof_block_cyc = 0; + g_emmc_diag.prof_call_cyc = 0; +} + +// ============================================================================ +// STILL NOT COMPILED, in either build (plan 7.2 D3, 7.2.w §4) +// ============================================================================ +// CMD6's dangerous clients (CACHE_CTRL, FLUSH_CACHE, HPI_MGMT, BKOPS +// AUTO_EN, POWER_OFF_NOTIFICATION), CMD35/36/38 TRIM and the abortable HPI +// machinery all stay in the looper file and are deliberately absent here +// rather than merely unexposed. The write path above did not need one of +// them: they exist to serve a live 4-stream recorder (W1). +// +// (CMD6 itself is compiled in for exactly one volatile byte -- see the +// HS_TIMING block above. That is a hard-coded argument with no caller input, +// not a general SWITCH: none of the clients listed above became reachable.) +// +// The lessons block that used to sit here has been replaced by the ported +// code carrying the same comments inline, which was its stated purpose. Two +// of them are now behavioural tests in tools/test_sp1_emmc.py, so a +// "cleanup" that reintroduces them fails on a laptop rather than on Device A: +// the TX frame that must end at the CRC's last bit, and DAT0 that must stay +// an input after a busy timeout. The -Os attribute on write_data_block() is +// the third; only a build flag can undo that one, which is why it is stated +// per-function. +// +// Left open, and still open: HPI_FEATURES bit 1 = 0 on this part means JEDEC +// wants HPI signalled via CMD13, yet the looper fires CMD12+HPI and works. +// Nothing here uses HPI. +// ============================================================================ diff --git a/ports/nordic/sp1_emmc/sp1_emmc.h b/ports/nordic/sp1_emmc/sp1_emmc.h new file mode 100644 index 00000000000..1b8233a3379 --- /dev/null +++ b/ports/nordic/sp1_emmc/sp1_emmc.h @@ -0,0 +1,159 @@ +// 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 + +#ifndef SP1_EMMC_WRITE +#define SP1_EMMC_WRITE (0) +#endif + +// The HS_TIMING switch. Off unless the build asks for it. +#ifndef SP1_EMMC_HS_TIMING +#define SP1_EMMC_HS_TIMING (0) +#endif + +#define EMMC_BLOCK_SIZE 512u + +// Where init got to / how the bus is behaving. +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 ocr[6]; // CMD1 R3 response bytes + uint8_t r1[6]; // CMD7 R1 response bytes + uint8_t cid[16]; // CMD2 R2 payload (CID[127:0]) + uint32_t init_us; // wall-clock cost of the last emmc_init() + // ---- last command ---- + bool last_cmd_resp; + int32_t resp_clocks; // clocks until last response start bit (-1 = none) + int32_t cmd2_clocks; // same, captured for CMD2 + int32_t cmd2_tries; // CMD2 attempts before a response + uint16_t rd_crc; // CRC16 the card appended to the last read block + // ---- integrity / stalls ---- + uint32_t crc_rd_errs; // verified-read CRC catches (acceptance: stays 0) + uint32_t cmd_retries; // first-try response misses recovered + uint32_t busy_timeouts; // busy/hunt expiries (SEPARATE from CRC errors) + uint32_t rd_wait_us_max; // worst read start-bit access wait, us + uint32_t burst_aborts; // CMD18 bursts cut short by the burst deadline + uint32_t blocks_read; // blocks successfully read this session + // ---- writing ---- + uint32_t werr; // CRC-status tokens that were not 010 (accepted) + uint32_t wr_retries; // failed write attempts the caller retried + uint32_t wr_busy_us_max; // worst post-write program busy, us (E3/W-R9) + uint32_t blocks_written; // blocks the card accepted this session + int32_t wr_status; // last CRC-status token: 2 ok, 5 CRC, 6 write, -1 none + // ---- high-speed timing ---- + // All of these stay at their zero values unless emmc_set_high_speed() is + // called, which is what makes "did anything issue a CMD6?" answerable from + // Python rather than by reading the build flags. + bool hs_requested; // a caller asked for the switch + bool cmd6_sent; // CMD6 SWITCH actually went out on the wire + bool cmd6_resp; // ... and the card answered R1b + bool hs_switch_error; // CMD13 reported SWITCH_ERROR after the CMD6 + bool hs_verified; // EXT_CSD[185] read back as 1 at the OLD clock + bool hs_active; // verified AND the host clock is now at M32 + bool hs_dat_phase; // SPIM is sampling DAT0 on the TRAILING edge, + // i.e. following the card into high-speed + // timing, where it launches on the rising edge + uint8_t hs_timing_at_init; // EXT_CSD[185] as found, before any CMD6 + uint8_t hs_timing_readback;// EXT_CSD[185] after the CMD6 + uint8_t device_type; // EXT_CSD[196]; bit 1 = 52 MHz supported + uint32_t cmd6_busy_us; // how long the card held DAT0 low after CMD6 + uint32_t cmd6_tran_us; // ... plus the CMD13 poll back to tran + // 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; + // ---- per-block profile ---- + // Cycle counts from the CPU's DWT counter (64 MHz). prof_ok says whether + // that counter is actually running; if it is not, every accumulator below + // stays 0 rather than lying. Reset with emmc_prof_reset(). + bool prof_ok; + uint32_t prof_blocks; // blocks measured + uint32_t prof_calls; // emmc_read_blocks() calls measured + uint64_t prof_hunt_cyc; // bit-banged start-bit hunt + uint64_t prof_dma_cyc; // SPIM3 RX DMA of 512 B + CRC16 + uint64_t prof_verify_cyc; // memcpy to the caller + CRC16 check + uint64_t prof_block_cyc; // whole read_data_block(), the three above + edges + uint64_t prof_call_cyc; // whole emmc_read_blocks(): blocks + CMD18/CMD12 +} sp1_emmc_diag_t; + +extern sp1_emmc_diag_t g_emmc_diag; + +// 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; + +bool emmc_init(void); +bool emmc_is_ready(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 + +// The block-protocol ioctl ops (extmod/vfs.h's MP_BLOCKDEV_IOCTL_*), restated +// here because this driver also compiles for the host harness, which has no +// MicroPython headers. bindings.c static-asserts the two sets agree, so a +// future upstream renumbering stops the build rather than the card. +#define EMMC_IOCTL_INIT 1u +#define EMMC_IOCTL_DEINIT 2u +#define EMMC_IOCTL_SYNC 3u +#define EMMC_IOCTL_BLOCK_COUNT 4u +#define EMMC_IOCTL_BLOCK_SIZE 5u +#define EMMC_IOCTL_BLOCK_ERASE 6u + + +bool emmc_blockdev_ioctl(uint32_t op, uint32_t arg, uint32_t *out_value); + +#if SP1_EMMC_WRITE +// 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); +#endif + +uint32_t emmc_bus_hz(void); // the SPIM data-phase clock, Hz +void emmc_prof_reset(void); // zero the per-block profile + +#if SP1_EMMC_HS_TIMING +// 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); +#endif + +// ---- bindings-side hooks (bindings.c) -------------------------------------- + +// 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/sp1_emmc/sp1_emmc_hw.h b/ports/nordic/sp1_emmc/sp1_emmc_hw.h new file mode 100644 index 00000000000..21d540c9806 --- /dev/null +++ b/ports/nordic/sp1_emmc/sp1_emmc_hw.h @@ -0,0 +1,251 @@ +// 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 + +// Every hardware touch sp1_emmc.c makes goes through this header: the five +// pins, the clock/tick sources, the WDT feed and the SPIM3 data engine. The +// protocol file itself has no nRF or CircuitPython includes, which is what +// lets tools/test_sp1_emmc.py compile the *same* .c on the host against a fake +// bus (SP1_EMMC_HOST_TEST=1) instead of a reimplementation that could drift +// away from the thing that ships. + + +#pragma once + +#include +#include + +// Pins (SP-1 wiring; identical to the looper's stemplayer_pins.h values). +#define SP1_EMMC_PIN_CLK 6u // P0.06 +#define SP1_EMMC_PIN_DAT0 7u // P0.07 +#define SP1_EMMC_PIN_CMD 8u // P0.08 +#define SP1_EMMC_PIN_RST 40u // P1.08, active low +#define SP1_EMMC_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 + +// 1 only in the host harness build (tools/test_sp1_emmc.py). +#ifndef SP1_EMMC_HOST_TEST +#define SP1_EMMC_HOST_TEST (0) +#endif + +// The write path's TX frame: FF gap | FE start token | 512 data | CRC16 = 516 +// bytes, rounded up. It must live in the port's reserved 8 KiB low-RAM SPIM3 +// buffer. +#define SP1_EMMC_TX_FRAME_SIZE 520u + +#if SP1_EMMC_HOST_TEST + +// Host build: the fake bus supplies all of the below (tools/emmc_host/). +#include "sp1_emmc_hostshim.h" + +#else + +#include "nrf_gpio.h" +#include "nrf.h" + +#include "py/mphal.h" +#include "py/runtime.h" // RUN_BACKGROUND_TASKS +#include "shared-bindings/microcontroller/__init__.h" +#include "nrf/wdt.h" + +// ---- 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 what +// pinned the looper's bit-bang at ~58 KB/s). +#define CLK_HIGH() nrf_gpio_pin_set(SP1_EMMC_PIN_CLK) +#define CLK_LOW() nrf_gpio_pin_clear(SP1_EMMC_PIN_CLK) +#define CMD_HIGH() nrf_gpio_pin_set(SP1_EMMC_PIN_CMD) +#define CMD_LOW() nrf_gpio_pin_clear(SP1_EMMC_PIN_CMD) +#define DAT0_HIGH() nrf_gpio_pin_set(SP1_EMMC_PIN_DAT0) +#define DAT0_LOW() nrf_gpio_pin_clear(SP1_EMMC_PIN_DAT0) +#define DAT0_IN() nrf_gpio_cfg_input(SP1_EMMC_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(SP1_EMMC_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(SP1_EMMC_PIN_CMD, NRF_GPIO_PIN_PULLUP) +#define CMD_OUT() nrf_gpio_cfg_output(SP1_EMMC_PIN_CMD) +#define READ_CMD() nrf_gpio_pin_read(SP1_EMMC_PIN_CMD) +#define READ_DAT0() nrf_gpio_pin_read(SP1_EMMC_PIN_DAT0) + +#define P0_CLK_BIT (1u << SP1_EMMC_PIN_CLK) +#define P0_DAT_BIT (1u << SP1_EMMC_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 >> SP1_EMMC_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(SP1_EMMC_PIN_RST) +#define RST_RELEASE() nrf_gpio_pin_set(SP1_EMMC_PIN_RST) +#define VCCQ_ON() nrf_gpio_pin_set(SP1_EMMC_PIN_VCCQ) +#define VCCQ_OFF() nrf_gpio_pin_clear(SP1_EMMC_PIN_VCCQ) + +static inline void sp1_emmc_pins_init(void) { + nrf_gpio_cfg(SP1_EMMC_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(SP1_EMMC_PIN_CMD); + DAT0_OUT(); // high-drive DAT0 + nrf_gpio_cfg_output(SP1_EMMC_PIN_RST); + // VCCQ: standard drive, exactly as the looper (and stock firmware) run it. + // E7 in the plan -- do NOT "improve" this to H0H1 without evidence. + nrf_gpio_cfg_output(SP1_EMMC_PIN_VCCQ); +} + +static inline void sp1_emmc_pins_release(void) { + nrf_gpio_cfg_default(SP1_EMMC_PIN_CLK); + nrf_gpio_cfg_default(SP1_EMMC_PIN_CMD); + nrf_gpio_cfg_default(SP1_EMMC_PIN_DAT0); + nrf_gpio_cfg_default(SP1_EMMC_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 (D4: the 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. +// +// This is a better fix than the looper's CRC-status-retry, and it is the one +// the write path uses; the status token is still enforced as the backstop. +#if SP1_EMMC_WRITE +static inline uint8_t *sp1_emmc_tx_frame(void) { + return (uint8_t *)SPIM3_BUFFER_RAM_START_ADDR; +} +#endif + +// ---- time ---------------------------------------------------------------- +#define EMMC_DELAY_US(us) common_hal_mcu_delay_us(us) +#define EMMC_SLEEP_MS(ms) mp_hal_delay_ms(ms) + +// 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 +static inline uint32_t emmc_ticks(void) { + return NRF_RTC2->COUNTER; +} + +// Long-wait service: feed the bootloader's dog and run background tasks (USB, +// the power-off gesture). Never sleeps -- see the looper's retry-without-sleep +// lesson; this is a few microseconds, not a nap. +static inline void emmc_yield(void) { + RUN_BACKGROUND_TASKS; + bootloader_wdt_feed(); +} +static inline void emmc_feed(void) { + bootloader_wdt_feed(); +} + +// ---- cycle counter (7.2.f profiling) ------------------------------------- +// The 32768 Hz RTC above resolves ~30 us, which is useless against a ~465 us +// block split three ways. The Cortex-M4's DWT cycle counter resolves 15.6 ns +// and costs two cycles to read, so the per-block breakdown is free. It needs +// TRCENA in DEMCR and CYCCNTENA in DWT->CTRL; on some parts/debug states the +// counter refuses to run, so emmc_prof_init() reports whether it actually +// ticked instead of assuming (a stuck counter would report every phase as +// taking zero time, which is worse than reporting nothing). +#define EMMC_CYCLES_HZ 64000000u +static inline uint32_t emmc_cycles(void) { + return DWT->CYCCNT; +} +static inline bool emmc_prof_init(void) { + CoreDebug->DEMCR |= CoreDebug_DEMCR_TRCENA_Msk; + DWT->CTRL |= DWT_CTRL_CYCCNTENA_Msk; + uint32_t a = DWT->CYCCNT; + __asm__ volatile ("nop\nnop\nnop\nnop\nnop\nnop\nnop\nnop"); + return DWT->CYCCNT != a; +} + +// ---- 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). D2 fixes the bus 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 (plan D11). +// There is still no free-floating "speed knob": the two codes at the top of +// this file are the only values ever written. +// +// The peripheral register IS the state -- no shadow copy to drift, and it +// survives ENABLE=0 between transfers. sp1_emmc_spim_init() puts it back to +// M16 on every init, so a fresh object always starts at compat speed even if +// the previous one ran high. +static inline uint32_t sp1_emmc_spim_freq(void) { + return NRF_SPIM3->FREQUENCY; +} + +static inline void sp1_emmc_spim_set_freq(uint32_t freq) { + NRF_SPIM3->FREQUENCY = freq; +} + +// CONFIG: SPIM_CONFIG_MODE0/MODE1 at the top of this file, and the reason the +// bit has to move at all. +static inline uint32_t sp1_emmc_spim_config(void) { + return NRF_SPIM3->CONFIG; +} + +static inline void sp1_emmc_spim_set_config(uint32_t config) { + NRF_SPIM3->CONFIG = config; +} + +static inline void sp1_emmc_spim_init(void) { + NRF_SPIM3->ENABLE = 0; + NRF_SPIM3->PSEL.SCK = SP1_EMMC_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 sp1_emmc_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. That handoff is the +// whole trick and it is proven on this hardware. +// +// With SP1_EMMC_WRITE 0 this is rx-only, so SPIM3 anomaly 198 (TX corruption) +// cannot bite at all (plan 7.2 E5). The write path makes TX real again, which +// is why its frame is built in sp1_emmc_tx_frame() above (7.2.w W-R5). +static inline void sp1_emmc_spim_xfer(const uint8_t *tx, uint32_t txlen, uint8_t *rx, uint32_t rxlen) { + NRF_SPIM3->PSEL.MOSI = tx ? SP1_EMMC_PIN_DAT0 : 0xFFFFFFFFu; + NRF_SPIM3->PSEL.MISO = rx ? SP1_EMMC_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; + while (!NRF_SPIM3->EVENTS_END) { + // ~260 us for a full block at 16 MHz -- far too short to be worth a + // background-task round trip, and any yield here would risk the + // caller's framing. + } + NRF_SPIM3->ENABLE = 0; +} + +#endif // SP1_EMMC_HOST_TEST 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..bba0a79a409 100644 --- a/ports/nordic/supervisor/port.c +++ b/ports/nordic/supervisor/port.c @@ -19,10 +19,19 @@ #include "nrf/cache.h" #include "nrf/clocks.h" +#include "nrf/nvm.h" #include "nrf/power.h" #include "nrf/timers.h" +#include "nrf/wdt.h" +#if CIRCUITPY_SP1_EMMC +#include "sp1_emmc/sp1_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 +43,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 +67,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 +107,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 +154,17 @@ void tick_set_prescaler(uint32_t prescaler_val) { } safe_mode_t port_init(void) { + // Feed bootloader wdt before anything else + bootloader_wdt_feed(); + + // 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 +209,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 +244,12 @@ void reset_port(void) { rtc_reset(); #endif + #if CIRCUITPY_SP1_EMMC + // 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 +264,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 +366,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 +385,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..01fc9b13502 100644 --- a/supervisor/shared/filesystem.c +++ b/supervisor/shared/filesystem.c @@ -19,6 +19,10 @@ #include "shared-module/sdcardio/__init__.h" #endif +#if defined(SP1_EMMC_AUTOMOUNT) && SP1_EMMC_AUTOMOUNT +#include "sp1_emmc/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(SP1_EMMC_AUTOMOUNT) && SP1_EMMC_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..06fc9cf760c 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(SP1_EMMC_AUTOMOUNT) && SP1_EMMC_AUTOMOUNT +#include "sp1_emmc/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(SP1_EMMC_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)); } From 3b8c97cf44a34715829b76c61d67fd92df6a531f Mon Sep 17 00:00:00 2001 From: foamyguy Date: Tue, 18 Aug 2026 17:19:36 -0500 Subject: [PATCH 02/14] refactor sp1_emmc bindings, remove diagnostics, profiling, and host harness stuff, cleanup more comments --- extmod/vfs_blockdev.c | 2 +- ports/nordic/Makefile | 9 +- ports/nordic/background.c | 4 - .../bindings.c => bindings/sp1emmc/EMMC.c} | 400 +++++------------- .../blockdev.h => bindings/sp1emmc/EMMC.h} | 18 +- ports/nordic/bindings/sp1emmc/__init__.c | 48 +++ .../teenage_engineering_sp1/mpconfigboard.mk | 39 +- ports/nordic/sp1_emmc/sp1_emmc.c | 249 +++-------- ports/nordic/sp1_emmc/sp1_emmc.h | 88 +--- ports/nordic/sp1_emmc/sp1_emmc_hw.h | 73 +--- ports/nordic/supervisor/port.c | 2 +- 11 files changed, 286 insertions(+), 646 deletions(-) rename ports/nordic/{sp1_emmc/bindings.c => bindings/sp1emmc/EMMC.c} (53%) rename ports/nordic/{sp1_emmc/blockdev.h => bindings/sp1emmc/EMMC.h} (63%) create mode 100644 ports/nordic/bindings/sp1emmc/__init__.c diff --git a/extmod/vfs_blockdev.c b/extmod/vfs_blockdev.c index 092a87ed940..b7d61f42c63 100644 --- a/extmod/vfs_blockdev.c +++ b/extmod/vfs_blockdev.c @@ -39,7 +39,7 @@ #endif #if defined(CIRCUITPY_SP1_EMMC) && CIRCUITPY_SP1_EMMC -#include "sp1_emmc/blockdev.h" +#include "bindings/sp1emmc/EMMC.h" #endif diff --git a/ports/nordic/Makefile b/ports/nordic/Makefile index 9460d7ec63a..07f7d1df746 100755 --- a/ports/nordic/Makefile +++ b/ports/nordic/Makefile @@ -153,19 +153,14 @@ SRC_C += \ CIRCUITPY_SP1_EMMC ?= 0 CFLAGS += -DCIRCUITPY_SP1_EMMC=$(CIRCUITPY_SP1_EMMC) -# HS_TIMING switch -CIRCUITPY_SP1_EMMC_HS ?= 0 -CFLAGS += -DSP1_EMMC_HS_TIMING=$(CIRCUITPY_SP1_EMMC_HS) -# write switch -CIRCUITPY_SP1_EMMC_WRITE ?= 0 -CFLAGS += -DSP1_EMMC_WRITE=$(CIRCUITPY_SP1_EMMC_WRITE) # USB mass storage automount switch CIRCUITPY_SP1_EMMC_USB ?= 0 CFLAGS += -DSP1_EMMC_AUTOMOUNT=$(CIRCUITPY_SP1_EMMC_USB) ifeq ($(CIRCUITPY_SP1_EMMC),1) SRC_C += \ + bindings/sp1emmc/__init__.c \ + bindings/sp1emmc/EMMC.c \ sp1_emmc/sp1_emmc.c \ - sp1_emmc/bindings.c \ ifeq ($(CIRCUITPY_SP1_EMMC_USB),1) SRC_C += sp1_emmc/automount.c diff --git a/ports/nordic/background.c b/ports/nordic/background.c index 2b74b478a5b..28a89bb0d5f 100644 --- a/ports/nordic/background.c +++ b/ports/nordic/background.c @@ -12,10 +12,6 @@ #include "nrf/wdt.h" #include "power_off.h" -#ifdef PROVING_GROUND_WEDGE -#include "supervisor/shared/safe_mode.h" -#endif - #if CIRCUITPY_DISPLAYIO #include "shared-module/displayio/__init__.h" #endif diff --git a/ports/nordic/sp1_emmc/bindings.c b/ports/nordic/bindings/sp1emmc/EMMC.c similarity index 53% rename from ports/nordic/sp1_emmc/bindings.c rename to ports/nordic/bindings/sp1emmc/EMMC.c index 56dc2ceaeb0..7664d2b6298 100644 --- a/ports/nordic/sp1_emmc/bindings.c +++ b/ports/nordic/bindings/sp1emmc/EMMC.c @@ -4,41 +4,6 @@ // // SPDX-License-Identifier: MIT -// `sp1emmc` -- Python access to the SP-1's 4 GB eMMC as a block device. -// -// The card is FAT-formatted (7.3.a) and the songs on it are WAV files, so this -// module is a block device and nothing more. It used to carry two pieces of -// the stock firmware's custom album format -- the TE sector decode (plan 7.2 -// D8) and the stem loader's COBS packet layer (7.2.w W6) -- both removed on -// 2026-08-17; te_song_decode/ in the repo root keeps them if they are ever -// wanted back. -// -// import sp1emmc -// e = sp1emmc.EMMC() # VCCQ on, reset released, CMD0..16, EXT_CSD -// e = sp1emmc.EMMC(high_speed=True) # ... then HS_TIMING=1 + 32 MHz (D11) -// e.count # 7_733_248 blocks of 512 bytes -// buf = bytearray(512) -// e.readblocks(0, buf) -// e.deinit() -// -// Writing takes BOTH gates (7.2.w W2): SP1_EMMC_WRITE in the build, and -// -// e = sp1emmc.EMMC(write_enabled=True) -// e.writeblocks(0, buf) # 512-byte multiples, no address floor -// -// A default EMMC() is exactly the read-only object 7.2 measured -- writeblocks -// raises on it. The two keywords compose: -// -// e = sp1emmc.EMMC(high_speed=True, write_enabled=True) # 7.2.w.g -// -// which is the read-write M32 object a FAT mount wants. 7.2.w's W3 refused -// that pairing until the write direction's launch edge had been modelled; it -// has been, and the answer was that the card's input edge never moved. -// -// There is no filesystem on this chip and this module does not invent one: -// read/writeblocks() only match the native block-device protocol's shape so a -// future FAT-on-eMMC could hand the object to storage.VfsFat unchanged. - #include #include "py/obj.h" @@ -55,8 +20,8 @@ #include "common-hal/microcontroller/Pin.h" #include "peripherals/nrf/nrf52840/pins.h" +#include "bindings/sp1emmc/EMMC.h" #include "sp1_emmc/automount.h" -#include "sp1_emmc/blockdev.h" #include "sp1_emmc/sp1_emmc.h" #include "sp1_emmc/sp1_emmc_hw.h" @@ -111,31 +76,29 @@ static void check_for_deinit(sp1emmc_emmc_obj_t *self) { } static const char *init_failure_stage(void) { - if (!g_emmc_diag.cmd0_sent) { + if (!g_emmc_state.cmd0_sent) { return "cmd0"; } - if (g_emmc_diag.cmd1_retries < 0) { + if (g_emmc_state.cmd1_retries < 0) { return "cmd1 (card never ready)"; } - if (!g_emmc_diag.cmd2_resp) { + if (!g_emmc_state.cmd2_resp) { return "cmd2 (no CID)"; } - if (!g_emmc_diag.cmd3_resp) { + if (!g_emmc_state.cmd3_resp) { return "cmd3"; } - if (!g_emmc_diag.cmd7_resp) { + if (!g_emmc_state.cmd7_resp) { return "cmd7 (select)"; } - if (!g_emmc_diag.cmd16_resp) { + if (!g_emmc_state.cmd16_resp) { return "cmd16 (blocklen)"; } return "ext_csd"; } - -#if SP1_EMMC_HS_TIMING -static const char *hs_failure_stage(void) {. - switch (g_emmc_diag.hs_stage) { +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: @@ -143,16 +106,15 @@ static const char *hs_failure_stage(void) {. case 2: return "cmd6 busy (card never released DAT0)"; case 3: - return g_emmc_diag.hs_switch_error + 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 "M32 smoke test (fell back to 16 MHz)"; + return "32 MHz smoke test (fell back to 16 MHz)"; } } -#endif // Power the card up and (optionally) switch it to high speed. Shared by // make_new() and the automount, deliberately never raises. Returns NULL @@ -174,17 +136,12 @@ static const char *emmc_power_up(bool high_speed, bool *hs_failed) { release_hardware(); return "ext_csd"; } - #if SP1_EMMC_HS_TIMING if (high_speed && !emmc_set_high_speed()) { const char *stage = hs_failure_stage(); release_hardware(); *hs_failed = true; return stage; } - #else - (void)high_speed; - #endif - emmc_prof_reset(); return NULL; } @@ -217,12 +174,7 @@ mp_obj_t sp1emmc_automount_construct(bool high_speed, bool write_enabled) { } s_automount_obj.base.type = &sp1emmc_emmc_type; s_automount_obj.deinited = false; - #if SP1_EMMC_WRITE s_automount_obj.write_enabled = write_enabled; - #else - (void)write_enabled; - s_automount_obj.write_enabled = false; - #endif s_automounted = true; return MP_OBJ_FROM_PTR(&s_automount_obj); } @@ -232,6 +184,34 @@ bool sp1emmc_is_automounted(void) { } #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[] = { @@ -242,16 +222,6 @@ static mp_obj_t sp1emmc_emmc_make_new(const mp_obj_type_t *type, size_t n_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 !SP1_EMMC_HS_TIMING - if (high_speed) { - mp_raise_ValueError(MP_ERROR_TEXT("high_speed not compiled in")); - } - #endif - #if !SP1_EMMC_WRITE - if (write_enabled) { - mp_raise_ValueError(MP_ERROR_TEXT("write_enabled not compiled in")); - } - #endif #if SP1_EMMC_AUTOMOUNT if (s_automounted) { @@ -285,6 +255,11 @@ static mp_obj_t sp1emmc_emmc_make_new(const mp_obj_type_t *type, size_t n_args, 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) { @@ -295,12 +270,20 @@ static mp_obj_t sp1emmc_emmc_deinit(mp_obj_t self_in) { } 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__); -// readblocks(start_block, buf) -- len(buf) must be a multiple of 512. #define CHUNK_BLOCKS 64u static int emmc_read_chunked(uint8_t *out, mp_uint_t start, mp_uint_t count, bool from_vm) { @@ -326,6 +309,18 @@ static int emmc_read_chunked(uint8_t *out, mp_uint_t start, mp_uint_t count, boo 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); @@ -350,9 +345,6 @@ static mp_obj_t sp1emmc_emmc_readblocks(mp_obj_t self_in, mp_obj_t start_in, mp_ } static MP_DEFINE_CONST_FUN_OBJ_3(sp1emmc_emmc_readblocks_obj, sp1emmc_emmc_readblocks); -// writeblocks(start_block, buf) -- len(buf) must be a multiple of 512. -#if SP1_EMMC_WRITE - 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) { @@ -362,9 +354,6 @@ static int emmc_write_chunked(const uint8_t *src, mp_uint_t start, mp_uint_t cou mp_uint_t run = MIN(CHUNK_BLOCKS, count - done); bool ok = false; for (int attempt = 0; attempt < 3 && !ok; attempt++) { - if (attempt) { - g_emmc_diag.wr_retries++; - } ok = emmc_write_blocks(start + done, src + done * EMMC_BLOCK_SIZE, run); } if (!ok) { @@ -379,6 +368,20 @@ static int emmc_write_chunked(const uint8_t *src, mp_uint_t start, mp_uint_t cou 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); @@ -406,8 +409,13 @@ static mp_obj_t sp1emmc_emmc_writeblocks(mp_obj_t self_in, mp_obj_t start_in, mp return mp_const_none; } static MP_DEFINE_CONST_FUN_OBJ_3(sp1emmc_emmc_writeblocks_obj, sp1emmc_emmc_writeblocks); -#endif +//| 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); @@ -420,9 +428,9 @@ static mp_obj_t sp1emmc_emmc_ioctl(mp_obj_t self_in, mp_obj_t op_in, mp_obj_t ar } static MP_DEFINE_CONST_FUN_OBJ_3(sp1emmc_emmc_ioctl_obj, sp1emmc_emmc_ioctl); -// The op numbers are restated in sp1_emmc.h because the driver also compiles -// for the host harness, which has no extmod headers. If upstream ever -// renumbers them, fail here rather than on the card. +// The op numbers are restated in sp1_emmc.h so the driver can also be compiled +// away from MicroPython, where the extmod headers are not available. If they +// are ever renumbered, fail here rather than on the card. MP_STATIC_ASSERT(EMMC_IOCTL_INIT == MP_BLOCKDEV_IOCTL_INIT); MP_STATIC_ASSERT(EMMC_IOCTL_DEINIT == MP_BLOCKDEV_IOCTL_DEINIT); MP_STATIC_ASSERT(EMMC_IOCTL_SYNC == MP_BLOCKDEV_IOCTL_SYNC); @@ -441,7 +449,6 @@ mp_uint_t sp1emmc_emmc_readblocks_native(mp_obj_t self_in, uint8_t *buf, mp_uint_t sp1emmc_emmc_writeblocks_native(mp_obj_t self_in, const uint8_t *buf, uint32_t start_block, uint32_t nblocks) { - #if SP1_EMMC_WRITE sp1emmc_emmc_obj_t *self = MP_OBJ_TO_PTR(self_in); if (self->deinited) { return -MP_ENODEV; @@ -451,13 +458,6 @@ mp_uint_t sp1emmc_emmc_writeblocks_native(mp_obj_t self_in, const uint8_t *buf, return -MP_EROFS; } return emmc_write_chunked(buf, start_block, nblocks, false); - #else - (void)self_in; - (void)buf; - (void)start_block; - (void)nblocks; - return -MP_EROFS; - #endif } bool sp1emmc_emmc_ioctl_native(mp_obj_t self_in, uint32_t cmd, uint32_t arg, @@ -472,15 +472,13 @@ bool sp1emmc_emmc_ioctl_native(mp_obj_t self_in, uint32_t cmd, uint32_t arg, bool sp1emmc_emmc_is_write_enabled(mp_obj_t self_in) { sp1emmc_emmc_obj_t *self = MP_OBJ_TO_PTR(self_in); - #if SP1_EMMC_WRITE return !self->deinited && self->write_enabled; - #else - (void)self; - return false; - #endif } -// read_ext_csd() -> bytes, the card's 512-byte EXT_CSD (CMD8). Read-only; +//| 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); @@ -493,7 +491,10 @@ static mp_obj_t sp1emmc_emmc_read_ext_csd(mp_obj_t self_in) { } static MP_DEFINE_CONST_FUN_OBJ_1(sp1emmc_emmc_read_ext_csd_obj, sp1emmc_emmc_read_ext_csd); -// status() -> int, the card status register (CMD13 SEND_STATUS), R1 bits [39:8] of the response. +//| 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); @@ -508,21 +509,11 @@ static mp_obj_t sp1emmc_emmc_status(mp_obj_t self_in) { } static MP_DEFINE_CONST_FUN_OBJ_1(sp1emmc_emmc_status_obj, sp1emmc_emmc_status); -// reset_profile() zeros the per-block cycle accumulators. -static mp_obj_t sp1emmc_emmc_reset_profile(mp_obj_t self_in) { - sp1emmc_emmc_obj_t *self = MP_OBJ_TO_PTR(self_in); - check_for_deinit(self); - emmc_prof_reset(); - return mp_const_none; -} -static MP_DEFINE_CONST_FUN_OBJ_1(sp1emmc_emmc_reset_profile_obj, sp1emmc_emmc_reset_profile); - // ---- properties ----------------------------------------------------------- -static void diag_store(mp_obj_t dict, qstr key, mp_obj_t value) { - mp_obj_dict_store(dict, MP_OBJ_NEW_QSTR(key), value); -} - +//| 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); @@ -531,46 +522,29 @@ static mp_obj_t sp1emmc_emmc_get_count(mp_obj_t self_in) { 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: the 16-byte CID from CMD2. MID 0x11 (Toshiba) +//| 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_diag.cid, sizeof(g_emmc_diag.cid)); + 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); -static mp_obj_t sp1emmc_emmc_get_crc_errors(mp_obj_t self_in) { - return mp_obj_new_int_from_uint(g_emmc_diag.crc_rd_errs); -} -static MP_DEFINE_CONST_FUN_OBJ_1(sp1emmc_emmc_get_crc_errors_obj, sp1emmc_emmc_get_crc_errors); -MP_PROPERTY_GETTER(sp1emmc_emmc_crc_errors_obj, (mp_obj_t)&sp1emmc_emmc_get_crc_errors_obj); - -static mp_obj_t sp1emmc_emmc_get_retries(mp_obj_t self_in) { - return mp_obj_new_int_from_uint(g_emmc_diag.cmd_retries); -} -static MP_DEFINE_CONST_FUN_OBJ_1(sp1emmc_emmc_get_retries_obj, sp1emmc_emmc_get_retries); -MP_PROPERTY_GETTER(sp1emmc_emmc_retries_obj, (mp_obj_t)&sp1emmc_emmc_get_retries_obj); - -static mp_obj_t sp1emmc_emmc_get_busy_timeouts(mp_obj_t self_in) { - return mp_obj_new_int_from_uint(g_emmc_diag.busy_timeouts); -} -static MP_DEFINE_CONST_FUN_OBJ_1(sp1emmc_emmc_get_busy_timeouts_obj, sp1emmc_emmc_get_busy_timeouts); -MP_PROPERTY_GETTER(sp1emmc_emmc_busy_timeouts_obj, (mp_obj_t)&sp1emmc_emmc_get_busy_timeouts_obj); - -static mp_obj_t sp1emmc_emmc_get_rd_wait_us_max(mp_obj_t self_in) { - return mp_obj_new_int_from_uint(g_emmc_diag.rd_wait_us_max); -} -static MP_DEFINE_CONST_FUN_OBJ_1(sp1emmc_emmc_get_rd_wait_us_max_obj, sp1emmc_emmc_get_rd_wait_us_max); -MP_PROPERTY_GETTER(sp1emmc_emmc_rd_wait_us_max_obj, (mp_obj_t)&sp1emmc_emmc_get_rd_wait_us_max_obj); - -// write_enabled: whether this object may write at all. False by default. +//| 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); @@ -579,30 +553,20 @@ static mp_obj_t sp1emmc_emmc_get_write_enabled(mp_obj_t self_in) { 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); -static mp_obj_t sp1emmc_emmc_get_wr_busy_us_max(mp_obj_t self_in) { - return mp_obj_new_int_from_uint(g_emmc_diag.wr_busy_us_max); -} -static MP_DEFINE_CONST_FUN_OBJ_1(sp1emmc_emmc_get_wr_busy_us_max_obj, sp1emmc_emmc_get_wr_busy_us_max); -MP_PROPERTY_GETTER(sp1emmc_emmc_wr_busy_us_max_obj, (mp_obj_t)&sp1emmc_emmc_get_wr_busy_us_max_obj); - -// write_errors: CRC-status tokens that were not 010. -static mp_obj_t sp1emmc_emmc_get_write_errors(mp_obj_t self_in) { - return mp_obj_new_int_from_uint(g_emmc_diag.werr); -} -static MP_DEFINE_CONST_FUN_OBJ_1(sp1emmc_emmc_get_write_errors_obj, sp1emmc_emmc_get_write_errors); -MP_PROPERTY_GETTER(sp1emmc_emmc_write_errors_obj, (mp_obj_t)&sp1emmc_emmc_get_write_errors_obj); - -// high_speed: True once HS_TIMING = 1 has been read back off the card AND the -// host clock is at M32. False means the bus is at 16 MHz +//| 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_diag.hs_active); + 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: the SPIM data-phase clock in Hz +//| 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); @@ -611,121 +575,23 @@ static mp_obj_t sp1emmc_emmc_get_frequency(mp_obj_t self_in) { 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); -// profile: where a block's time actually goes, in microseconds, averaged over -// every block read since the last reset_profile(). 7.2.c could only infer the -// split between the DMA and everything else; this measures it, which is what -// decides whether M32 can help (the DMA halves; the rest may not). -static mp_obj_t sp1emmc_emmc_get_profile(mp_obj_t self_in) { - sp1emmc_emmc_obj_t *self = MP_OBJ_TO_PTR(self_in); - check_for_deinit(self); - mp_obj_t d = mp_obj_new_dict(9); - uint32_t blocks = g_emmc_diag.prof_blocks; - uint32_t calls = g_emmc_diag.prof_calls; - diag_store(d, MP_QSTR_ok, mp_obj_new_bool(g_emmc_diag.prof_ok)); - diag_store(d, MP_QSTR_blocks, mp_obj_new_int_from_uint(blocks)); - diag_store(d, MP_QSTR_calls, mp_obj_new_int_from_uint(calls)); - // Per-block averages in microseconds. Integer division is fine at these - // magnitudes (hundreds of us) and keeps floats out of the hot path. - #define PROF_US(field, n) mp_obj_new_int_from_uint( \ - (n) ? (uint32_t)((field) / (n) / (EMMC_CYCLES_HZ / 1000000u)) : 0u) - diag_store(d, MP_QSTR_hunt_us, PROF_US(g_emmc_diag.prof_hunt_cyc, blocks)); - diag_store(d, MP_QSTR_dma_us, PROF_US(g_emmc_diag.prof_dma_cyc, blocks)); - diag_store(d, MP_QSTR_verify_us, PROF_US(g_emmc_diag.prof_verify_cyc, blocks)); - diag_store(d, MP_QSTR_block_us, PROF_US(g_emmc_diag.prof_block_cyc, blocks)); - diag_store(d, MP_QSTR_call_us, PROF_US(g_emmc_diag.prof_call_cyc, calls)); - // The command half: whole-call time minus the blocks inside it, per block. - // This is the CMD18/CMD12 handshake plus the bounds check -- the part M32 - // does nothing for. - uint64_t cmd_cyc = g_emmc_diag.prof_call_cyc > g_emmc_diag.prof_block_cyc - ? g_emmc_diag.prof_call_cyc - g_emmc_diag.prof_block_cyc : 0; - diag_store(d, MP_QSTR_cmd_us, PROF_US(cmd_cyc, blocks)); - #undef PROF_US - return d; -} -static MP_DEFINE_CONST_FUN_OBJ_1(sp1emmc_emmc_get_profile_obj, sp1emmc_emmc_get_profile); -MP_PROPERTY_GETTER(sp1emmc_emmc_profile_obj, (mp_obj_t)&sp1emmc_emmc_get_profile_obj); - -// The whole diagnostics struct, for the bring-up harness. -static mp_obj_t sp1emmc_emmc_get_diagnostics(mp_obj_t self_in) { - mp_obj_t d = mp_obj_new_dict(20); - diag_store(d, MP_QSTR_cmd0_sent, mp_obj_new_bool(g_emmc_diag.cmd0_sent)); - diag_store(d, MP_QSTR_cmd1_retries, mp_obj_new_int(g_emmc_diag.cmd1_retries)); - diag_store(d, MP_QSTR_cmd2_resp, mp_obj_new_bool(g_emmc_diag.cmd2_resp)); - diag_store(d, MP_QSTR_cmd2_tries, mp_obj_new_int(g_emmc_diag.cmd2_tries)); - diag_store(d, MP_QSTR_cmd2_clocks, mp_obj_new_int(g_emmc_diag.cmd2_clocks)); - diag_store(d, MP_QSTR_cmd3_resp, mp_obj_new_bool(g_emmc_diag.cmd3_resp)); - diag_store(d, MP_QSTR_cmd7_resp, mp_obj_new_bool(g_emmc_diag.cmd7_resp)); - diag_store(d, MP_QSTR_cmd16_resp, mp_obj_new_bool(g_emmc_diag.cmd16_resp)); - diag_store(d, MP_QSTR_ocr, mp_obj_new_bytes(g_emmc_diag.ocr, sizeof(g_emmc_diag.ocr))); - diag_store(d, MP_QSTR_r1, mp_obj_new_bytes(g_emmc_diag.r1, sizeof(g_emmc_diag.r1))); - diag_store(d, MP_QSTR_cid, mp_obj_new_bytes(g_emmc_diag.cid, sizeof(g_emmc_diag.cid))); - diag_store(d, MP_QSTR_init_us, mp_obj_new_int_from_uint(g_emmc_diag.init_us)); - diag_store(d, MP_QSTR_last_cmd_resp, mp_obj_new_bool(g_emmc_diag.last_cmd_resp)); - diag_store(d, MP_QSTR_resp_clocks, mp_obj_new_int(g_emmc_diag.resp_clocks)); - diag_store(d, MP_QSTR_rd_crc, mp_obj_new_int_from_uint(g_emmc_diag.rd_crc)); - diag_store(d, MP_QSTR_crc_errors, mp_obj_new_int_from_uint(g_emmc_diag.crc_rd_errs)); - diag_store(d, MP_QSTR_retries, mp_obj_new_int_from_uint(g_emmc_diag.cmd_retries)); - diag_store(d, MP_QSTR_busy_timeouts, mp_obj_new_int_from_uint(g_emmc_diag.busy_timeouts)); - diag_store(d, MP_QSTR_burst_aborts, mp_obj_new_int_from_uint(g_emmc_diag.burst_aborts)); - diag_store(d, MP_QSTR_rd_wait_us_max, mp_obj_new_int_from_uint(g_emmc_diag.rd_wait_us_max)); - diag_store(d, MP_QSTR_blocks_read, mp_obj_new_int_from_uint(g_emmc_diag.blocks_read)); - // The write path (7.2.w W2). These read 0/-1 on a build or a run that - // never wrote, which is how the bring-up harness proves a read-only image - // stayed read-only without inspecting the build flags. - diag_store(d, MP_QSTR_werr, mp_obj_new_int_from_uint(g_emmc_diag.werr)); - diag_store(d, MP_QSTR_wr_retries, mp_obj_new_int_from_uint(g_emmc_diag.wr_retries)); - diag_store(d, MP_QSTR_wr_busy_us_max, mp_obj_new_int_from_uint(g_emmc_diag.wr_busy_us_max)); - diag_store(d, MP_QSTR_blocks_written, mp_obj_new_int_from_uint(g_emmc_diag.blocks_written)); - diag_store(d, MP_QSTR_wr_status, mp_obj_new_int(g_emmc_diag.wr_status)); - // The HS_TIMING switch (plan D11). These read 0/False on a build or a run - // that never asked for it, which is how the harness proves no CMD6 went out. - diag_store(d, MP_QSTR_hs_requested, mp_obj_new_bool(g_emmc_diag.hs_requested)); - diag_store(d, MP_QSTR_cmd6_sent, mp_obj_new_bool(g_emmc_diag.cmd6_sent)); - diag_store(d, MP_QSTR_cmd6_resp, mp_obj_new_bool(g_emmc_diag.cmd6_resp)); - diag_store(d, MP_QSTR_hs_switch_error, mp_obj_new_bool(g_emmc_diag.hs_switch_error)); - diag_store(d, MP_QSTR_hs_verified, mp_obj_new_bool(g_emmc_diag.hs_verified)); - diag_store(d, MP_QSTR_hs_active, mp_obj_new_bool(g_emmc_diag.hs_active)); - diag_store(d, MP_QSTR_hs_dat_phase, mp_obj_new_bool(g_emmc_diag.hs_dat_phase)); - diag_store(d, MP_QSTR_hs_timing_at_init, mp_obj_new_int_from_uint(g_emmc_diag.hs_timing_at_init)); - diag_store(d, MP_QSTR_hs_timing_readback, mp_obj_new_int_from_uint(g_emmc_diag.hs_timing_readback)); - diag_store(d, MP_QSTR_device_type, mp_obj_new_int_from_uint(g_emmc_diag.device_type)); - diag_store(d, MP_QSTR_cmd6_busy_us, mp_obj_new_int_from_uint(g_emmc_diag.cmd6_busy_us)); - diag_store(d, MP_QSTR_cmd6_tran_us, mp_obj_new_int_from_uint(g_emmc_diag.cmd6_tran_us)); - diag_store(d, MP_QSTR_hs_stage, mp_obj_new_int_from_uint(g_emmc_diag.hs_stage)); - diag_store(d, MP_QSTR_frequency, mp_obj_new_int_from_uint(emmc_bus_hz())); - return d; -} -static MP_DEFINE_CONST_FUN_OBJ_1(sp1emmc_emmc_get_diagnostics_obj, sp1emmc_emmc_get_diagnostics); -MP_PROPERTY_GETTER(sp1emmc_emmc_diagnostics_obj, (mp_obj_t)&sp1emmc_emmc_get_diagnostics_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) }, - #if SP1_EMMC_WRITE { MP_ROM_QSTR(MP_QSTR_writeblocks), MP_ROM_PTR(&sp1emmc_emmc_writeblocks_obj) }, - #endif { 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_reset_profile), MP_ROM_PTR(&sp1emmc_emmc_reset_profile_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_profile), MP_ROM_PTR(&sp1emmc_emmc_profile_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_crc_errors), MP_ROM_PTR(&sp1emmc_emmc_crc_errors_obj) }, - { MP_ROM_QSTR(MP_QSTR_retries), MP_ROM_PTR(&sp1emmc_emmc_retries_obj) }, - { MP_ROM_QSTR(MP_QSTR_busy_timeouts), MP_ROM_PTR(&sp1emmc_emmc_busy_timeouts_obj) }, - { MP_ROM_QSTR(MP_QSTR_rd_wait_us_max), MP_ROM_PTR(&sp1emmc_emmc_rd_wait_us_max_obj) }, { MP_ROM_QSTR(MP_QSTR_write_enabled), MP_ROM_PTR(&sp1emmc_emmc_write_enabled_obj) }, - { MP_ROM_QSTR(MP_QSTR_write_errors), MP_ROM_PTR(&sp1emmc_emmc_write_errors_obj) }, - { MP_ROM_QSTR(MP_QSTR_wr_busy_us_max), MP_ROM_PTR(&sp1emmc_emmc_wr_busy_us_max_obj) }, - { MP_ROM_QSTR(MP_QSTR_diagnostics), MP_ROM_PTR(&sp1emmc_emmc_diagnostics_obj) }, }; static MP_DEFINE_CONST_DICT(sp1emmc_emmc_locals_dict, sp1emmc_emmc_locals_dict_table); @@ -736,37 +602,3 @@ MP_DEFINE_CONST_OBJ_TYPE( locals_dict, &sp1emmc_emmc_locals_dict, make_new, sp1emmc_emmc_make_new ); - -// ---- module --------------------------------------------------------------- - -// Module-level diagnostics and the automount flag (7.3.f). With the supervisor -// holding the card there is no EMMC object to ask, and the integrity counters -// are exactly what a USB transfer has to be graded on -- `crc_errors` after a -// desktop copies a file is the whole acceptance test. Cheap to expose: the -// getter never looked at `self` in the first place, because every counter it -// reports lives in g_emmc_diag. -static mp_obj_t sp1emmc_diagnostics(void) { - return sp1emmc_emmc_get_diagnostics(mp_const_none); -} -static MP_DEFINE_CONST_FUN_OBJ_0(sp1emmc_diagnostics_obj, sp1emmc_diagnostics); - -// True when the card belongs to the supervisor's /sd mount, false otherwise -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); - -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_diagnostics), MP_ROM_PTR(&sp1emmc_diagnostics_obj) }, - { MP_ROM_QSTR(MP_QSTR_automounted), MP_ROM_PTR(&sp1emmc_automounted_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/sp1_emmc/blockdev.h b/ports/nordic/bindings/sp1emmc/EMMC.h similarity index 63% rename from ports/nordic/sp1_emmc/blockdev.h rename to ports/nordic/bindings/sp1emmc/EMMC.h index f5630eab633..e303af55240 100644 --- a/ports/nordic/sp1_emmc/blockdev.h +++ b/ports/nordic/bindings/sp1emmc/EMMC.h @@ -4,16 +4,17 @@ // // SPDX-License-Identifier: MIT -// The native block-device face of sp1emmc.EMMC, split out of -// bindings.c so extmod/vfs_blockdev.c can reach it without dragging the rest of -// the module's headers. The same shape sdcardio and sdioio present. - #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); @@ -29,3 +30,12 @@ bool sp1emmc_emmc_ioctl_native(mp_obj_t self_in, uint32_t cmd, uint32_t arg, // 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/bindings/sp1emmc/__init__.c b/ports/nordic/bindings/sp1emmc/__init__.c new file mode 100644 index 00000000000..0f755b1c8e4 --- /dev/null +++ b/ports/nordic/bindings/sp1emmc/__init__.c @@ -0,0 +1,48 @@ +// 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 "py/obj.h" +#include "py/runtime.h" + +#include "bindings/sp1emmc/EMMC.h" +#include "sp1_emmc/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); + +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) }, +}; +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/mpconfigboard.mk b/ports/nordic/boards/teenage_engineering_sp1/mpconfigboard.mk index fdac1d57797..6fefebc39be 100644 --- a/ports/nordic/boards/teenage_engineering_sp1/mpconfigboard.mk +++ b/ports/nordic/boards/teenage_engineering_sp1/mpconfigboard.mk @@ -8,13 +8,12 @@ USB_MANUFACTURER = "Teenage Engineering" MCU_CHIP = nrf52840 -# No UICR write may survive into this board's image (plan §4.5.a). UICR is -# undoable only by ERASEALL, which needs SWD; the SP-1's SWD pads are not -# reachable, and an ERASEALL would take TE's bootloader with it — the only copy -# in existence, since the USB protocol has no read command. So each of the -# three conditional UICR writes on the boot path is turned off here, and the -# acceptance test is the disassembly: every reference to 0x10001000 in -# firmware.elf must be a read. +# 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 reachable, and an +# ERASEALL would take TE's bootloader with it — the only copy in existence, +# since the USB protocol has no read command. So each of the three conditional +# UICR writes on the boot path is turned off here, and the acceptance test is +# the disassembly: every reference to 0x10001000 in firmware.elf must be a read. # # 1. PSELRESET. The SP-1 has no reset pin and P0.18's wiring is unknown. With # CONFIG_GPIO_AS_PINRESET on, SystemInit would burn PSELRESET[0..1] = 18 @@ -24,19 +23,19 @@ MCU_CHIP = nrf52840 NRF_GPIO_AS_PINRESET = 0 # 2. REGOUT0. Fires only in high-voltage mode (VDDH supplied) with REGOUT0 -# unprogrammed. The proving ground never reached it — a USB-powered board -# reads MAINREGSTATUS = 0 — but the SP-1 runs from a LiPo and may well be -# VDDH-supplied, which is exactly the case that writes UICR. Safe to remove -# either way: the stock firmware drives 3.3 V logic (eMMC, codecs, LEDs) on -# this hardware today, so REGOUT0 is already whatever it needs to be. +# unprogrammed. A USB-powered board never reaches it — MAINREGSTATUS reads +# 0 — but the SP-1 runs from a LiPo and may well be VDDH-supplied, which is +# exactly the case that writes UICR. Safe to remove either way: this board +# runs 3.3 V logic (eMMC, codecs, LEDs) as shipped, so REGOUT0 is already +# whatever it needs to be. NRF_REGOUT0_3V3 = 0 # 3. NFCPINS. P0.09/P0.10 are the NFC pins and are TAS_RESET and BT_RESET here. -# Stock firmware drives both as GPIO, so NFCPINS.PROTECT must already be -# cleared on this device and the write cannot fire — but "must already be" +# Both are driven as GPIO on this device as shipped, so NFCPINS.PROTECT +# must already be cleared and the write cannot fire — but "must already be" # is not the bar for a one-way change, and if the assumption is wrong the # right outcome is two dead reset lines to diagnose, not a UICR burn. The -# actual value is read from the REPL at first light (plan §5.2). +# actual value can be read back from the REPL. NRF_NFCT_PINS_AS_GPIOS = 0 # No SoftDevice. It would have to live at 0x1000, which is inside the TE @@ -70,18 +69,12 @@ CIRCUITPY_AUDIOPWMIO = 0 CIRCUITPY_SYNTHIO = 1 CIRCUITPY_AUDIOEFFECTS = 1 # audiomp3 would otherwise come along for free with audiocore on a full build. -# It is 7.1's §6 A8 item -- decode CPU headroom on this chip is unassessed and -# it costs ~26 KB of flash -- so it stays off until it is deliberately assessed. +# Decode CPU headroom on this chip is unassessed and it costs ~26 KB of flash, +# so it stays off until it is deliberately assessed. CIRCUITPY_AUDIOMP3 = 0 # eMMC CIRCUITPY_SP1_EMMC = 1 -# HS_TIMING for EMMC -CIRCUITPY_SP1_EMMC_HS = 1 - -# EMMC writing -CIRCUITPY_SP1_EMMC_WRITE = 1 - # auto mount EMMC as /sd CIRCUITPY_SP1_EMMC_USB = 1 diff --git a/ports/nordic/sp1_emmc/sp1_emmc.c b/ports/nordic/sp1_emmc/sp1_emmc.c index 68a36d4dbe1..b62bef9d320 100644 --- a/ports/nordic/sp1_emmc/sp1_emmc.c +++ b/ports/nordic/sp1_emmc/sp1_emmc.c @@ -6,7 +6,8 @@ // ============================================================================ // SP-1 eMMC flash driver (1-bit MMC protocol over the nRF52840) -// Read path always; the write path only with SP1_EMMC_WRITE = 1. +// Read and write paths; writing is gated at runtime by +// EMMC(write_enabled=True). // ============================================================================ // Two layers: // @@ -38,7 +39,7 @@ static uint32_t s_cmd_half_us = CMD_SAFE_HALF_US; volatile uint32_t g_emmc_clk_half_us = CMD_SAFE_HALF_US; -sp1_emmc_diag_t g_emmc_diag; +sp1_emmc_state_t g_emmc_state; static bool s_ready; static uint32_t s_rca; @@ -50,7 +51,7 @@ static uint8_t s_dma_rx[EMMC_BLOCK_SIZE + 2]; #define HALF(hd) do { if (hd) { EMMC_DELAY_US(hd); } } while (0) -// ---- bounded-wait helpers (D6) --------------------------------------------- +// ---- 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)) @@ -58,10 +59,6 @@ static inline uint32_t ticks_since(uint32_t t0) { return (emmc_ticks() - t0) & EMMC_TICK_MASK; } -static inline uint32_t ticks_to_us(uint32_t t) { - return (uint32_t)(((uint64_t)t * 1000000u) / EMMC_TICKS_HZ); -} - static inline void half_delay(uint32_t us) { if (us) { EMMC_DELAY_US(us); @@ -142,18 +139,6 @@ static uint16_t crc16(const uint8_t *data, uint32_t len) { return crc; } -#if SP1_EMMC_HOST_TEST -// Hooks so tools/test_sp1_emmc.py checks the CRCs that actually ship, rather -// than a copy of them. Not built for the device. -uint8_t sp1_emmc_test_crc7(const uint8_t *data, uint8_t len) { - return crc7(data, len); -} -uint16_t sp1_emmc_test_crc16(const uint8_t *data, uint32_t len) { - crc16_tab_init(); - return crc16(data, len); -} -#endif - 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); @@ -185,15 +170,15 @@ static bool send_command(uint8_t cmd_index, uint32_t arg, uint8_t *r1_out) { cmd_send_bit(1); CMD_IN(); - g_emmc_diag.resp_clocks = -1; + bool responded = false; for (int t = 0; t < 200; t++) { clk_pulse(); if (!READ_CMD()) { - g_emmc_diag.resp_clocks = t; + responded = true; break; } } - if (g_emmc_diag.resp_clocks < 0) { + if (!responded) { return false; } @@ -219,7 +204,6 @@ static bool send_command_retry(uint8_t cmd, uint32_t arg, uint8_t *r1_out, int t if (send_command(cmd, arg, r1_out)) { return true; } - g_emmc_diag.cmd_retries++; if (t == 0) { // First miss = the card still settling after the previous burst: a // handful of idle clocks is all it needs. @@ -237,7 +221,6 @@ static bool send_command_retry(uint8_t cmd, uint32_t arg, uint8_t *r1_out, int t __attribute__((optimize("O2"))) // read path only: -O2 safe for reads, NOT writes static bool read_data_block(uint8_t *buf) { const uint32_t hd = g_emmc_clk_half_us; - const uint32_t pc0 = emmc_cycles(); DAT0_IN(); // START-BIT HUNT @@ -262,14 +245,9 @@ static bool read_data_block(uint8_t *buf) { } uint32_t el = ticks_since(t0); if (got_start) { - uint32_t us = ticks_to_us(el); - if (us > g_emmc_diag.rd_wait_us_max) { - g_emmc_diag.rd_wait_us_max = us; - } break; } if (el >= lim) { - g_emmc_diag.busy_timeouts++; return false; } if (el >= yield_at) { @@ -279,37 +257,21 @@ static bool read_data_block(uint8_t *buf) { } RCLK_LOW(); HALF(hd); - const uint32_t pc1 = emmc_cycles(); // The start bit was just consumed by the bit-bang hunt above, so the // remaining 512 data bytes + CRC16 are exactly byte-aligned. sp1_emmc_spim_xfer(NULL, 0, s_dma_rx, sizeof(s_dma_rx)); - const uint32_t pc2 = emmc_cycles(); memcpy(buf, s_dma_rx, EMMC_BLOCK_SIZE); - g_emmc_diag.rd_crc = (uint16_t)(((uint16_t)s_dma_rx[EMMC_BLOCK_SIZE] << 8) | + 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(hd); RCLK_LOW(); HALF(hd); // end bit - bool crc_ok = crc16(buf, EMMC_BLOCK_SIZE) == g_emmc_diag.rd_crc; - const uint32_t pc3 = emmc_cycles(); - g_emmc_diag.prof_hunt_cyc += pc1 - pc0; - g_emmc_diag.prof_dma_cyc += pc2 - pc1; - g_emmc_diag.prof_verify_cyc += pc3 - pc2; - g_emmc_diag.prof_block_cyc += pc3 - pc0; - g_emmc_diag.prof_blocks++; - if (!crc_ok) { - g_emmc_diag.crc_rd_errs++; // corrupt read: caller retries - DAT0_OUT(); - DAT0_HIGH(); - return false; - } - + bool crc_ok = crc16(buf, EMMC_BLOCK_SIZE) == card_crc; DAT0_OUT(); DAT0_HIGH(); - g_emmc_diag.blocks_read++; - return true; + return crc_ok; // a mismatch: caller retries } bool emmc_cmd13(uint8_t *r1_out) { @@ -332,21 +294,16 @@ static void drain_r2_cid(uint8_t *cid_out) { } bool emmc_init(void) { - uint32_t t_init0 = emmc_ticks(); - 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_diag, 0, sizeof(g_emmc_diag)); - g_emmc_diag.cmd1_retries = -1; - g_emmc_diag.resp_clocks = -1; - g_emmc_diag.cmd2_clocks = -1; + memset(&g_emmc_state, 0, sizeof(g_emmc_state)); + g_emmc_state.cmd1_retries = -1; sp1_emmc_pins_init(); sp1_emmc_spim_init(); // hardware-clocked data path, at M16 - g_emmc_diag.prof_ok = emmc_prof_init(); crc16_tab_init(); CLK_LOW(); @@ -367,7 +324,7 @@ bool emmc_init(void) { } send_command(0, 0x00000000, NULL); // CMD0 GO_IDLE (no response expected) - g_emmc_diag.cmd0_sent = true; + g_emmc_state.cmd0_sent = true; EMMC_SLEEP_MS(1); // CMD1 SEND_OP_COND, arg 0x40FF8000: HCS=1 @@ -377,22 +334,18 @@ bool emmc_init(void) { emmc_feed(); EMMC_SLEEP_MS(1); if (ok && (r3[1] & 0x80)) { // response seen AND busy bit set = ready - g_emmc_diag.cmd1_retries = retry; + g_emmc_state.cmd1_retries = retry; break; } } - memcpy(g_emmc_diag.ocr, r3, 6); - if (g_emmc_diag.cmd1_retries < 0) { // card never responded ready -> stop - g_emmc_diag.init_us = ticks_to_us(ticks_since(t_init0)); + if (g_emmc_state.cmd1_retries < 0) { // card never responded ready -> stop return false; } for (int t = 0; t < 8; t++) { - g_emmc_diag.cmd2_tries = t + 1; - g_emmc_diag.cmd2_resp = send_command(2, 0, NULL); - g_emmc_diag.cmd2_clocks = g_emmc_diag.resp_clocks; - if (g_emmc_diag.cmd2_resp) { - drain_r2_cid(g_emmc_diag.cid); + g_emmc_state.cmd2_resp = send_command(2, 0, NULL); + if (g_emmc_state.cmd2_resp) { + drain_r2_cid(g_emmc_state.cid); break; } EMMC_SLEEP_MS(2); @@ -401,23 +354,21 @@ bool emmc_init(void) { uint8_t r6[6] = {0}; s_rca = 0x0001u << 16; - g_emmc_diag.cmd3_resp = send_command_retry(3, s_rca, r6, 8); // SET_RELATIVE_ADDR + g_emmc_state.cmd3_resp = send_command_retry(3, s_rca, r6, 8); // SET_RELATIVE_ADDR EMMC_SLEEP_MS(1); uint8_t r1[6] = {0}; - g_emmc_diag.cmd7_resp = send_command_retry(7, s_rca, r1, 8); // SELECT_CARD - memcpy(g_emmc_diag.r1, r1, 6); + g_emmc_state.cmd7_resp = send_command_retry(7, s_rca, r1, 8); // SELECT_CARD EMMC_SLEEP_MS(1); - g_emmc_diag.cmd16_resp = send_command_retry(16, EMMC_BLOCK_SIZE, r1, 8); // SET_BLOCKLEN + g_emmc_state.cmd16_resp = send_command_retry(16, EMMC_BLOCK_SIZE, r1, 8); // SET_BLOCKLEN EMMC_SLEEP_MS(1); // strict: ready only if the card actually selected AND accepted block length - s_ready = g_emmc_diag.cmd7_resp && g_emmc_diag.cmd16_resp; + 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; } - g_emmc_diag.init_us = ticks_to_us(ticks_since(t_init0)); return s_ready; } @@ -460,8 +411,8 @@ bool emmc_blockdev_ioctl(uint32_t op, uint32_t arg, uint32_t *out_value) { // Power-off: release the bus pins and cut the VCCQ I/O rail. With no write path // and the card's volatile cache never enabled there is nothing to flush first, -// which is why 6.3's board_power_off_prepare() ordering stays correct -// unmodified (plan §4). The card is gone until the next emmc_init(). +// so board_power_off_prepare() needs no eMMC step of its own. The card is gone +// until the next emmc_init(). void emmc_power_down(void) { s_ready = false; s_block_count = 0; @@ -492,16 +443,9 @@ bool emmc_read_ext_csd(uint8_t *buf) { // DEVICE_TYPE[196] gates the HS_TIMING switch (bit 1 = 52 MHz supported; // this part reads 0x57). s_device_type = buf[196]; - g_emmc_diag.device_type = buf[196]; - if (g_emmc_diag.hs_requested) { - g_emmc_diag.hs_timing_readback = buf[185]; - } else { - g_emmc_diag.hs_timing_at_init = buf[185]; - } return true; } -#if SP1_EMMC_HS_TIMING || SP1_EMMC_WRITE // ---- 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 @@ -518,7 +462,7 @@ bool emmc_read_ext_csd(uint8_t *buf) { // 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, uint32_t *elapsed_us, bool run_bg) { +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(); @@ -536,14 +480,11 @@ static bool dat0_busy_wait(uint32_t timeout_us, uint32_t *elapsed_us, bool run_b } uint32_t el = ticks_since(t0); if (released) { - *elapsed_us = ticks_to_us(el); DAT0_OUT(); // back to the read path's resting state DAT0_HIGH(); return true; } if (el >= lim) { - *elapsed_us = ticks_to_us(el); - g_emmc_diag.busy_timeouts++; // DAT0 STAYS AN INPUT on a timeout return false; } @@ -554,9 +495,6 @@ static bool dat0_busy_wait(uint32_t timeout_us, uint32_t *elapsed_us, bool run_b } } } -#endif // SP1_EMMC_HS_TIMING || SP1_EMMC_WRITE - -#if SP1_EMMC_HS_TIMING // CMD6 SWITCH argument: access 0b11 (WRITE_BYTE) | index 185 | value 1 | // cmd_set 0 -> 0x03 B9 01 00. @@ -571,7 +509,7 @@ static bool dat0_busy_wait(uint32_t timeout_us, uint32_t *elapsed_us, bool run_b // 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 *elapsed_us) { +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 (;;) { @@ -580,19 +518,14 @@ static bool wait_tran_after_switch(uint32_t timeout_us, uint32_t *elapsed_us) { 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_diag.hs_switch_error = true; - *elapsed_us = ticks_to_us(ticks_since(t0)); + g_emmc_state.hs_switch_error = true; return false; } if (((status >> 9) & 0xFu) == 4u && ((status >> 8) & 1u)) { - *elapsed_us = ticks_to_us(ticks_since(t0)); return true; // tran + ready_for_data } } - uint32_t el = ticks_since(t0); - if (el >= lim) { - g_emmc_diag.busy_timeouts++; - *elapsed_us = ticks_to_us(el); + if (ticks_since(t0) >= lim) { return false; } emmc_yield(); @@ -604,31 +537,28 @@ bool emmc_set_high_speed(void) { if (!s_ready) { return false; } - g_emmc_diag.hs_requested = true; // Gate on the card's own capability byte. if (!(s_device_type & EMMC_DEVICE_TYPE_HS52)) { return false; } - g_emmc_diag.hs_stage = 1; + g_emmc_state.hs_stage = 1; uint8_t r1[6]; - g_emmc_diag.cmd6_sent = true; - g_emmc_diag.cmd6_resp = send_command_retry(6, EMMC_SWITCH_HS_TIMING_ARG, r1, 8); - if (!g_emmc_diag.cmd6_resp) { + if (!send_command_retry(6, EMMC_SWITCH_HS_TIMING_ARG, r1, 8)) { return false; } - g_emmc_diag.hs_stage = 2; + 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, &g_emmc_diag.cmd6_busy_us, true)) { + if (!dat0_busy_wait(EMMC_CMD6_BUSY_US, true)) { return false; } - g_emmc_diag.hs_stage = 3; - if (!wait_tran_after_switch(EMMC_CMD6_BUSY_US, &g_emmc_diag.cmd6_tran_us)) { + g_emmc_state.hs_stage = 3; + if (!wait_tran_after_switch(EMMC_CMD6_BUSY_US)) { return false; } - g_emmc_diag.hs_stage = 4; + 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 @@ -640,7 +570,6 @@ bool emmc_set_high_speed(void) { // This happens BEFORE the readback, because the readback is itself a block // read off a card that has already switched. sp1_emmc_spim_set_config(SPIM_CONFIG_MODE1); - g_emmc_diag.hs_dat_phase = true; // 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, @@ -649,11 +578,9 @@ bool emmc_set_high_speed(void) { if (!emmc_read_ext_csd(ext_csd) || ext_csd[EMMC_EXT_CSD_HS_TIMING] != 1u) { sp1_emmc_spim_set_config(SPIM_CONFIG_MODE0); - g_emmc_diag.hs_dat_phase = false; return false; // still at M16, card still readable } - g_emmc_diag.hs_verified = true; - g_emmc_diag.hs_stage = 5; + 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 @@ -668,13 +595,12 @@ bool emmc_set_high_speed(void) { sp1_emmc_spim_set_freq(SPIM_FREQ_M16); return false; } - g_emmc_diag.hs_active = true; - g_emmc_diag.hs_stage = 6; + g_emmc_state.hs_active = true; + g_emmc_state.hs_stage = 6; return true; } -#endif // SP1_EMMC_HS_TIMING -static bool read_blocks_inner(uint32_t block_addr, uint8_t *buf, uint32_t count) { +bool emmc_read_blocks(uint32_t block_addr, uint8_t *buf, uint32_t count) { if (!s_ready || count == 0) { return false; } @@ -685,16 +611,14 @@ static bool read_blocks_inner(uint32_t block_addr, uint8_t *buf, uint32_t count) } uint8_t r1[6]; if (count == 1) { - g_emmc_diag.last_cmd_resp = send_command_retry(17, block_addr, r1, 8); - if (!g_emmc_diag.last_cmd_resp) { + 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 - g_emmc_diag.last_cmd_resp = send_command_retry(18, block_addr, r1, 4); - if (!g_emmc_diag.last_cmd_resp) { + if (!send_command_retry(18, block_addr, r1, 4)) { return false; } @@ -703,8 +627,6 @@ static bool read_blocks_inner(uint32_t block_addr, uint8_t *buf, uint32_t count) for (uint32_t i = 0; i < count; i++) { if (i && ticks_since(bt0) >= blim) { (void)send_command_retry(12, 0, r1, 3); - g_emmc_diag.busy_timeouts++; - g_emmc_diag.burst_aborts++; return false; } if (!read_data_block(buf + i * EMMC_BLOCK_SIZE)) { @@ -716,20 +638,6 @@ static bool read_blocks_inner(uint32_t block_addr, uint8_t *buf, uint32_t count) return true; } -// The public entry point exists only to time the whole call: subtract the -// per-block total from it and what is left is the CMD18 + CMD12 handshake plus -// the bounds check, which is the half of the ~465 us/block that 7.2.c could -// only infer (results file, "Streaming follow-ups"). -bool emmc_read_blocks(uint32_t block_addr, uint8_t *buf, uint32_t count) { - const uint32_t c0 = emmc_cycles(); - bool ok = read_blocks_inner(block_addr, buf, count); - g_emmc_diag.prof_call_cyc += emmc_cycles() - c0; - g_emmc_diag.prof_calls++; - return ok; -} - -#if SP1_EMMC_WRITE - // The card declares MIN_PERF_W_* = 0x00: no minimum write performance #define EMMC_WR_BUSY_US 500000u // Same shape as the read side @@ -795,7 +703,7 @@ static bool write_data_block(const uint8_t *buf) { // 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(); - g_emmc_diag.wr_status = -1; + int wr_status = -1; for (int i = 0; i < 16; i++) { RCLK_HIGH(); HALF(hd); @@ -813,31 +721,22 @@ static bool write_data_block(const uint8_t *buf) { RCLK_LOW(); HALF(hd); } - g_emmc_diag.wr_status = st; + wr_status = st; break; } } // Programming busy on DAT0 - uint32_t busy_us = 0; - if (!dat0_busy_wait(EMMC_WR_BUSY_US, &busy_us, false)) { - if (busy_us > g_emmc_diag.wr_busy_us_max) { - g_emmc_diag.wr_busy_us_max = busy_us; - } + if (!dat0_busy_wait(EMMC_WR_BUSY_US, false)) { return false; // DAT0 left an INPUT -- see the wait } - if (busy_us > g_emmc_diag.wr_busy_us_max) { - g_emmc_diag.wr_busy_us_max = busy_us; - } // 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 (g_emmc_diag.wr_status != 0x2) { - g_emmc_diag.werr++; + if (wr_status != 0x2) { return false; } - g_emmc_diag.blocks_written++; return true; } @@ -851,16 +750,14 @@ bool emmc_write_blocks(uint32_t block_addr, const uint8_t *buf, uint32_t count) } uint8_t r1[6]; if (count == 1) { - g_emmc_diag.last_cmd_resp = send_command_retry(24, block_addr, r1, 8); - if (!g_emmc_diag.last_cmd_resp) { + 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. - g_emmc_diag.last_cmd_resp = send_command_retry(25, block_addr, r1, 4); - if (!g_emmc_diag.last_cmd_resp) { + if (!send_command_retry(25, block_addr, r1, 4)) { return false; } uint32_t bt0 = emmc_ticks(); @@ -868,8 +765,6 @@ bool emmc_write_blocks(uint32_t block_addr, const uint8_t *buf, uint32_t count) for (uint32_t i = 0; i < count; i++) { if (i && ticks_since(bt0) >= blim) { (void)send_command_retry(12, 0, r1, 3); - g_emmc_diag.busy_timeouts++; - g_emmc_diag.burst_aborts++; return false; } if (!write_data_block(buf + i * EMMC_BLOCK_SIZE)) { @@ -879,14 +774,9 @@ bool emmc_write_blocks(uint32_t block_addr, const uint8_t *buf, uint32_t count) } (void)send_command_retry(12, 0, r1, 3); - uint32_t busy_us = 0; - if (dat0_busy_wait(EMMC_WR_BUSY_US, &busy_us, false) && - busy_us > g_emmc_diag.wr_busy_us_max) { - g_emmc_diag.wr_busy_us_max = busy_us; - } + (void)dat0_busy_wait(EMMC_WR_BUSY_US, false); return true; } -#endif // SP1_EMMC_WRITE uint32_t emmc_bus_hz(void) { // SPIM3's M16/M32 codes are special values, NOT points on the linear scale @@ -895,39 +785,24 @@ uint32_t emmc_bus_hz(void) { return sp1_emmc_spim_freq() == SPIM_FREQ_M32 ? 32000000u : 16000000u; } -void emmc_prof_reset(void) { - g_emmc_diag.prof_blocks = 0; - g_emmc_diag.prof_calls = 0; - g_emmc_diag.prof_hunt_cyc = 0; - g_emmc_diag.prof_dma_cyc = 0; - g_emmc_diag.prof_verify_cyc = 0; - g_emmc_diag.prof_block_cyc = 0; - g_emmc_diag.prof_call_cyc = 0; -} - // ============================================================================ -// STILL NOT COMPILED, in either build (plan 7.2 D3, 7.2.w §4) +// Deliberately not implemented // ============================================================================ -// CMD6's dangerous clients (CACHE_CTRL, FLUSH_CACHE, HPI_MGMT, BKOPS -// AUTO_EN, POWER_OFF_NOTIFICATION), CMD35/36/38 TRIM and the abortable HPI -// machinery all stay in the looper file and are deliberately absent here -// rather than merely unexposed. The write path above did not need one of -// them: they exist to serve a live 4-stream recorder (W1). +// CMD6's dangerous clients (CACHE_CTRL, FLUSH_CACHE, HPI_MGMT, BKOPS AUTO_EN, +// POWER_OFF_NOTIFICATION), CMD35/36/38 TRIM and the abortable HPI machinery +// are absent rather than merely unexposed. Nothing in the read or write path +// needs them, and each one can put the card into a state a block device has no +// way to recover from. // -// (CMD6 itself is compiled in for exactly one volatile byte -- see the +// CMD6 itself is compiled in for exactly one volatile byte -- see the // HS_TIMING block above. That is a hard-coded argument with no caller input, -// not a general SWITCH: none of the clients listed above became reachable.) +// not a general SWITCH, so none of the clients listed above become reachable. // -// The lessons block that used to sit here has been replaced by the ported -// code carrying the same comments inline, which was its stated purpose. Two -// of them are now behavioural tests in tools/test_sp1_emmc.py, so a -// "cleanup" that reintroduces them fails on a laptop rather than on Device A: -// the TX frame that must end at the CRC's last bit, and DAT0 that must stay -// an input after a busy timeout. The -Os attribute on write_data_block() is -// the third; only a build flag can undo that one, which is why it is stated -// per-function. +// Three invariants here look like cleanup targets and are not: the TX frame +// must end at the CRC's last bit, DAT0 must stay an input after a busy +// timeout, and write_data_block() must keep its -Os attribute (only a build +// flag can undo that one, which is why it is stated per-function). // -// Left open, and still open: HPI_FEATURES bit 1 = 0 on this part means JEDEC -// wants HPI signalled via CMD13, yet the looper fires CMD12+HPI and works. -// Nothing here uses HPI. +// Open question: HPI_FEATURES bit 1 = 0 on this part means JEDEC wants HPI +// signalled via CMD13 rather than CMD12. Nothing here uses HPI. // ============================================================================ diff --git a/ports/nordic/sp1_emmc/sp1_emmc.h b/ports/nordic/sp1_emmc/sp1_emmc.h index 1b8233a3379..aff2aa6572c 100644 --- a/ports/nordic/sp1_emmc/sp1_emmc.h +++ b/ports/nordic/sp1_emmc/sp1_emmc.h @@ -17,18 +17,10 @@ #include #include -#ifndef SP1_EMMC_WRITE -#define SP1_EMMC_WRITE (0) -#endif - -// The HS_TIMING switch. Off unless the build asks for it. -#ifndef SP1_EMMC_HS_TIMING -#define SP1_EMMC_HS_TIMING (0) -#endif - #define EMMC_BLOCK_SIZE 512u -// Where init got to / how the bus is behaving. +// 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; @@ -37,67 +29,19 @@ typedef struct { bool cmd3_resp; bool cmd7_resp; bool cmd16_resp; - uint8_t ocr[6]; // CMD1 R3 response bytes - uint8_t r1[6]; // CMD7 R1 response bytes uint8_t cid[16]; // CMD2 R2 payload (CID[127:0]) - uint32_t init_us; // wall-clock cost of the last emmc_init() - // ---- last command ---- - bool last_cmd_resp; - int32_t resp_clocks; // clocks until last response start bit (-1 = none) - int32_t cmd2_clocks; // same, captured for CMD2 - int32_t cmd2_tries; // CMD2 attempts before a response - uint16_t rd_crc; // CRC16 the card appended to the last read block - // ---- integrity / stalls ---- - uint32_t crc_rd_errs; // verified-read CRC catches (acceptance: stays 0) - uint32_t cmd_retries; // first-try response misses recovered - uint32_t busy_timeouts; // busy/hunt expiries (SEPARATE from CRC errors) - uint32_t rd_wait_us_max; // worst read start-bit access wait, us - uint32_t burst_aborts; // CMD18 bursts cut short by the burst deadline - uint32_t blocks_read; // blocks successfully read this session - // ---- writing ---- - uint32_t werr; // CRC-status tokens that were not 010 (accepted) - uint32_t wr_retries; // failed write attempts the caller retried - uint32_t wr_busy_us_max; // worst post-write program busy, us (E3/W-R9) - uint32_t blocks_written; // blocks the card accepted this session - int32_t wr_status; // last CRC-status token: 2 ok, 5 CRC, 6 write, -1 none // ---- high-speed timing ---- - // All of these stay at their zero values unless emmc_set_high_speed() is - // called, which is what makes "did anything issue a CMD6?" answerable from - // Python rather than by reading the build flags. - bool hs_requested; // a caller asked for the switch - bool cmd6_sent; // CMD6 SWITCH actually went out on the wire - bool cmd6_resp; // ... and the card answered R1b + // 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_verified; // EXT_CSD[185] read back as 1 at the OLD clock - bool hs_active; // verified AND the host clock is now at M32 - bool hs_dat_phase; // SPIM is sampling DAT0 on the TRAILING edge, - // i.e. following the card into high-speed - // timing, where it launches on the rising edge - uint8_t hs_timing_at_init; // EXT_CSD[185] as found, before any CMD6 - uint8_t hs_timing_readback;// EXT_CSD[185] after the CMD6 - uint8_t device_type; // EXT_CSD[196]; bit 1 = 52 MHz supported - uint32_t cmd6_busy_us; // how long the card held DAT0 low after CMD6 - uint32_t cmd6_tran_us; // ... plus the CMD13 poll back to tran + 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; - // ---- per-block profile ---- - // Cycle counts from the CPU's DWT counter (64 MHz). prof_ok says whether - // that counter is actually running; if it is not, every accumulator below - // stays 0 rather than lying. Reset with emmc_prof_reset(). - bool prof_ok; - uint32_t prof_blocks; // blocks measured - uint32_t prof_calls; // emmc_read_blocks() calls measured - uint64_t prof_hunt_cyc; // bit-banged start-bit hunt - uint64_t prof_dma_cyc; // SPIM3 RX DMA of 512 B + CRC16 - uint64_t prof_verify_cyc; // memcpy to the caller + CRC16 check - uint64_t prof_block_cyc; // whole read_data_block(), the three above + edges - uint64_t prof_call_cyc; // whole emmc_read_blocks(): blocks + CMD18/CMD12 -} sp1_emmc_diag_t; +} sp1_emmc_state_t; -extern sp1_emmc_diag_t g_emmc_diag; +extern sp1_emmc_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 @@ -113,9 +57,9 @@ 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 // The block-protocol ioctl ops (extmod/vfs.h's MP_BLOCKDEV_IOCTL_*), restated -// here because this driver also compiles for the host harness, which has no -// MicroPython headers. bindings.c static-asserts the two sets agree, so a -// future upstream renumbering stops the build rather than the card. +// here so the protocol layer needs no MicroPython headers. EMMC.c +// static-asserts the two sets agree, so a future upstream renumbering stops +// the build rather than the card. #define EMMC_IOCTL_INIT 1u #define EMMC_IOCTL_DEINIT 2u #define EMMC_IOCTL_SYNC 3u @@ -126,18 +70,14 @@ void emmc_power_down(void); // reset asserted, pins released, bool emmc_blockdev_ioctl(uint32_t op, uint32_t arg, uint32_t *out_value); -#if SP1_EMMC_WRITE // 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); -#endif uint32_t emmc_bus_hz(void); // the SPIM data-phase clock, Hz -void emmc_prof_reset(void); // zero the per-block profile -#if SP1_EMMC_HS_TIMING // 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 @@ -147,13 +87,3 @@ void emmc_prof_reset(void); // zero the per-block profile // // Requires emmc_read_ext_csd() to have run. bool emmc_set_high_speed(void); -#endif - -// ---- bindings-side hooks (bindings.c) -------------------------------------- - -// 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/sp1_emmc/sp1_emmc_hw.h b/ports/nordic/sp1_emmc/sp1_emmc_hw.h index 21d540c9806..191ff46082f 100644 --- a/ports/nordic/sp1_emmc/sp1_emmc_hw.h +++ b/ports/nordic/sp1_emmc/sp1_emmc_hw.h @@ -6,10 +6,7 @@ // Every hardware touch sp1_emmc.c makes goes through this header: the five // pins, the clock/tick sources, the WDT feed and the SPIM3 data engine. The -// protocol file itself has no nRF or CircuitPython includes, which is what -// lets tools/test_sp1_emmc.py compile the *same* .c on the host against a fake -// bus (SP1_EMMC_HOST_TEST=1) instead of a reimplementation that could drift -// away from the thing that ships. +// protocol file itself has no nRF or CircuitPython includes of its own. #pragma once @@ -17,7 +14,7 @@ #include #include -// Pins (SP-1 wiring; identical to the looper's stemplayer_pins.h values). +// Pins (SP-1 wiring). #define SP1_EMMC_PIN_CLK 6u // P0.06 #define SP1_EMMC_PIN_DAT0 7u // P0.07 #define SP1_EMMC_PIN_CMD 8u // P0.08 @@ -32,23 +29,11 @@ #define SPIM_CONFIG_MODE0 0u // MSB first, CPOL0/CPHA0 #define SPIM_CONFIG_MODE1 (1u << 1) // MSB first, CPOL0/CPHA1 -// 1 only in the host harness build (tools/test_sp1_emmc.py). -#ifndef SP1_EMMC_HOST_TEST -#define SP1_EMMC_HOST_TEST (0) -#endif - // The write path's TX frame: FF gap | FE start token | 512 data | CRC16 = 516 // bytes, rounded up. It must live in the port's reserved 8 KiB low-RAM SPIM3 // buffer. #define SP1_EMMC_TX_FRAME_SIZE 520u -#if SP1_EMMC_HOST_TEST - -// Host build: the fake bus supplies all of the below (tools/emmc_host/). -#include "sp1_emmc_hostshim.h" - -#else - #include "nrf_gpio.h" #include "nrf.h" @@ -59,8 +44,8 @@ // ---- 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 what -// pinned the looper's bit-bang at ~58 KB/s). +// 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(SP1_EMMC_PIN_CLK) #define CLK_LOW() nrf_gpio_pin_clear(SP1_EMMC_PIN_CLK) #define CMD_HIGH() nrf_gpio_pin_set(SP1_EMMC_PIN_CMD) @@ -99,8 +84,8 @@ static inline void sp1_emmc_pins_init(void) { nrf_gpio_cfg_output(SP1_EMMC_PIN_CMD); DAT0_OUT(); // high-drive DAT0 nrf_gpio_cfg_output(SP1_EMMC_PIN_RST); - // VCCQ: standard drive, exactly as the looper (and stock firmware) run it. - // E7 in the plan -- do NOT "improve" this to H0H1 without evidence. + // 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(SP1_EMMC_PIN_VCCQ); } @@ -118,16 +103,14 @@ static inline void sp1_emmc_pins_release(void) { // 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 (D4: the 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. +// 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. // -// This is a better fix than the looper's CRC-status-retry, and it is the one -// the write path uses; the status token is still enforced as the backstop. -#if SP1_EMMC_WRITE +// The write path relies on this buffer rather than on retrying a bad CRC +// status; the status token is still enforced as the backstop. static inline uint8_t *sp1_emmc_tx_frame(void) { return (uint8_t *)SPIM3_BUFFER_RAM_START_ADDR; } -#endif // ---- time ---------------------------------------------------------------- #define EMMC_DELAY_US(us) common_hal_mcu_delay_us(us) @@ -142,8 +125,8 @@ static inline uint32_t emmc_ticks(void) { } // Long-wait service: feed the bootloader's dog and run background tasks (USB, -// the power-off gesture). Never sleeps -- see the looper's retry-without-sleep -// lesson; this is a few microseconds, not a nap. +// the power-off gesture). Never sleeps -- the waits this serves are a few +// microseconds, not a nap. static inline void emmc_yield(void) { RUN_BACKGROUND_TASKS; bootloader_wdt_feed(); @@ -152,32 +135,12 @@ static inline void emmc_feed(void) { bootloader_wdt_feed(); } -// ---- cycle counter (7.2.f profiling) ------------------------------------- -// The 32768 Hz RTC above resolves ~30 us, which is useless against a ~465 us -// block split three ways. The Cortex-M4's DWT cycle counter resolves 15.6 ns -// and costs two cycles to read, so the per-block breakdown is free. It needs -// TRCENA in DEMCR and CYCCNTENA in DWT->CTRL; on some parts/debug states the -// counter refuses to run, so emmc_prof_init() reports whether it actually -// ticked instead of assuming (a stuck counter would report every phase as -// taking zero time, which is worse than reporting nothing). -#define EMMC_CYCLES_HZ 64000000u -static inline uint32_t emmc_cycles(void) { - return DWT->CYCCNT; -} -static inline bool emmc_prof_init(void) { - CoreDebug->DEMCR |= CoreDebug_DEMCR_TRCENA_Msk; - DWT->CTRL |= DWT_CTRL_CYCCNTENA_Msk; - uint32_t a = DWT->CYCCNT; - __asm__ volatile ("nop\nnop\nnop\nnop\nnop\nnop\nnop\nnop"); - return DWT->CYCCNT != a; -} - // ---- 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). D2 fixes the bus there; the ONE way it moves +// 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 (plan D11). +// 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. // @@ -227,9 +190,9 @@ static inline void sp1_emmc_spim_deinit(void) { // the surrounding bit-bang phases continue seamlessly. That handoff is the // whole trick and it is proven on this hardware. // -// With SP1_EMMC_WRITE 0 this is rx-only, so SPIM3 anomaly 198 (TX corruption) -// cannot bite at all (plan 7.2 E5). The write path makes TX real again, which -// is why its frame is built in sp1_emmc_tx_frame() above (7.2.w W-R5). +// 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 sp1_emmc_tx_frame() above. static inline void sp1_emmc_spim_xfer(const uint8_t *tx, uint32_t txlen, uint8_t *rx, uint32_t rxlen) { NRF_SPIM3->PSEL.MOSI = tx ? SP1_EMMC_PIN_DAT0 : 0xFFFFFFFFu; NRF_SPIM3->PSEL.MISO = rx ? SP1_EMMC_PIN_DAT0 : 0xFFFFFFFFu; @@ -247,5 +210,3 @@ static inline void sp1_emmc_spim_xfer(const uint8_t *tx, uint32_t txlen, uint8_t } NRF_SPIM3->ENABLE = 0; } - -#endif // SP1_EMMC_HOST_TEST diff --git a/ports/nordic/supervisor/port.c b/ports/nordic/supervisor/port.c index bba0a79a409..336dfd702e5 100644 --- a/ports/nordic/supervisor/port.c +++ b/ports/nordic/supervisor/port.c @@ -25,7 +25,7 @@ #include "nrf/wdt.h" #if CIRCUITPY_SP1_EMMC -#include "sp1_emmc/sp1_emmc.h" +#include "bindings/sp1emmc/EMMC.h" #endif // The SoftDevice headers (nrf_sdm.h / nrf_soc.h) come in via mpconfigport.h. From 03b490913f9e580043c82f2b184371c26df03ea9 Mon Sep 17 00:00:00 2001 From: foamyguy Date: Tue, 18 Aug 2026 17:29:35 -0500 Subject: [PATCH 03/14] update VID/PID --- .../nordic/boards/teenage_engineering_sp1/mpconfigboard.mk | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/ports/nordic/boards/teenage_engineering_sp1/mpconfigboard.mk b/ports/nordic/boards/teenage_engineering_sp1/mpconfigboard.mk index 6fefebc39be..7fcbf4344c3 100644 --- a/ports/nordic/boards/teenage_engineering_sp1/mpconfigboard.mk +++ b/ports/nordic/boards/teenage_engineering_sp1/mpconfigboard.mk @@ -1,8 +1,5 @@ -# Placeholder USB IDs: pid.codes 0x1209/0x0001 is the "test" PID, valid for -# development only. A real allocation is needed before any release. Do NOT -# ship Teenage Engineering's 0x2367 -- this is not their firmware. -USB_VID = 0x1209 -USB_PID = 0x0001 +USB_VID = 0x239A +USB_PID = 0x817A USB_PRODUCT = "SP-1 (CircuitPython)" USB_MANUFACTURER = "Teenage Engineering" From 25432a111ba603238b81827c9d614a9afc9a5135 Mon Sep 17 00:00:00 2001 From: foamyguy Date: Tue, 18 Aug 2026 17:33:28 -0500 Subject: [PATCH 04/14] fix SPI include --- ports/nordic/common-hal/busio/SPI.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ports/nordic/common-hal/busio/SPI.c b/ports/nordic/common-hal/busio/SPI.c index 661e909b829..ad9bb459e57 100644 --- a/ports/nordic/common-hal/busio/SPI.c +++ b/ports/nordic/common-hal/busio/SPI.c @@ -15,7 +15,7 @@ #include "nrf_gpio.h" #if CIRCUITPY_SP1_EMMC -#include "sp1_emmc/sp1_emmc.h" +#include "bindings/sp1emmc/EMMC.h" #endif #ifndef NRFX_SPIM3_ENABLED From 5a66dd48aadf0e5a4a1e55f5453a7a78c7a0ab58 Mon Sep 17 00:00:00 2001 From: foamyguy Date: Wed, 19 Aug 2026 10:52:31 -0500 Subject: [PATCH 05/14] remove host harness stuff, refine comments. --- ports/nordic/bindings/sp1emmc/EMMC.c | 13 +- .../boards/teenage_engineering_sp1/board.c | 5 +- .../teenage_engineering_sp1/mpconfigboard.mk | 28 ++-- ports/nordic/sp1_emmc/sp1_emmc.c | 129 ++++++++++-------- ports/nordic/sp1_emmc/sp1_emmc.h | 14 +- ports/nordic/sp1_emmc/sp1_emmc_hw.h | 69 ++-------- 6 files changed, 99 insertions(+), 159 deletions(-) diff --git a/ports/nordic/bindings/sp1emmc/EMMC.c b/ports/nordic/bindings/sp1emmc/EMMC.c index 7664d2b6298..79159567834 100644 --- a/ports/nordic/bindings/sp1emmc/EMMC.c +++ b/ports/nordic/bindings/sp1emmc/EMMC.c @@ -23,7 +23,8 @@ #include "bindings/sp1emmc/EMMC.h" #include "sp1_emmc/automount.h" #include "sp1_emmc/sp1_emmc.h" -#include "sp1_emmc/sp1_emmc_hw.h" + +#include "nrf.h" static bool s_constructed; @@ -428,16 +429,6 @@ static mp_obj_t sp1emmc_emmc_ioctl(mp_obj_t self_in, mp_obj_t op_in, mp_obj_t ar } static MP_DEFINE_CONST_FUN_OBJ_3(sp1emmc_emmc_ioctl_obj, sp1emmc_emmc_ioctl); -// The op numbers are restated in sp1_emmc.h so the driver can also be compiled -// away from MicroPython, where the extmod headers are not available. If they -// are ever renumbered, fail here rather than on the card. -MP_STATIC_ASSERT(EMMC_IOCTL_INIT == MP_BLOCKDEV_IOCTL_INIT); -MP_STATIC_ASSERT(EMMC_IOCTL_DEINIT == MP_BLOCKDEV_IOCTL_DEINIT); -MP_STATIC_ASSERT(EMMC_IOCTL_SYNC == MP_BLOCKDEV_IOCTL_SYNC); -MP_STATIC_ASSERT(EMMC_IOCTL_BLOCK_COUNT == MP_BLOCKDEV_IOCTL_BLOCK_COUNT); -MP_STATIC_ASSERT(EMMC_IOCTL_BLOCK_SIZE == MP_BLOCKDEV_IOCTL_BLOCK_SIZE); -MP_STATIC_ASSERT(EMMC_IOCTL_BLOCK_ERASE == MP_BLOCKDEV_IOCTL_BLOCK_ERASE); - 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); diff --git a/ports/nordic/boards/teenage_engineering_sp1/board.c b/ports/nordic/boards/teenage_engineering_sp1/board.c index df2a18dd529..d4b5a8a6420 100644 --- a/ports/nordic/boards/teenage_engineering_sp1/board.c +++ b/ports/nordic/boards/teenage_engineering_sp1/board.c @@ -181,8 +181,9 @@ void board_reset_pin_defaults(void) { nrf_gpio_cfg_input(PIN_FUNCTION_BUTTON, NRF_GPIO_PIN_PULLUP); // BQ24232 charge enable, active low: drive it low so a plugged-in device - // actually charges. The current-set pin P1.00 is deliberately left alone, - // since the resistor configuration it selects has not been established. + // 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); } diff --git a/ports/nordic/boards/teenage_engineering_sp1/mpconfigboard.mk b/ports/nordic/boards/teenage_engineering_sp1/mpconfigboard.mk index 7fcbf4344c3..1ea34bdf174 100644 --- a/ports/nordic/boards/teenage_engineering_sp1/mpconfigboard.mk +++ b/ports/nordic/boards/teenage_engineering_sp1/mpconfigboard.mk @@ -12,27 +12,21 @@ MCU_CHIP = nrf52840 # UICR writes on the boot path is turned off here, and the acceptance test is # the disassembly: every reference to 0x10001000 in firmware.elf must be a read. # -# 1. PSELRESET. The SP-1 has no reset pin and P0.18's wiring is unknown. With -# CONFIG_GPIO_AS_PINRESET on, SystemInit would burn PSELRESET[0..1] = 18 -# on the first boot (PSELRESET is almost certainly erased on this device), -# turning P0.18 into nRESET forever — if anything ever pulls that net low, -# the chip is held in reset and unreachable even over SWD. +# 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. Fires only in high-voltage mode (VDDH supplied) with REGOUT0 -# unprogrammed. A USB-powered board never reaches it — MAINREGSTATUS reads -# 0 — but the SP-1 runs from a LiPo and may well be VDDH-supplied, which is -# exactly the case that writes UICR. Safe to remove either way: this board -# runs 3.3 V logic (eMMC, codecs, LEDs) as shipped, so REGOUT0 is already -# whatever it needs to be. +# 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. P0.09/P0.10 are the NFC pins and are TAS_RESET and BT_RESET here. -# Both are driven as GPIO on this device as shipped, so NFCPINS.PROTECT -# must already be cleared and the write cannot fire — but "must already be" -# is not the bar for a one-way change, and if the assumption is wrong the -# right outcome is two dead reset lines to diagnose, not a UICR burn. The -# actual value can be read back from the REPL. +# 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 TE diff --git a/ports/nordic/sp1_emmc/sp1_emmc.c b/ports/nordic/sp1_emmc/sp1_emmc.c index b62bef9d320..f0590cd511a 100644 --- a/ports/nordic/sp1_emmc/sp1_emmc.c +++ b/ports/nordic/sp1_emmc/sp1_emmc.c @@ -31,6 +31,12 @@ #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 "nrf/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 @@ -49,22 +55,26 @@ 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]; -#define HALF(hd) do { if (hd) { EMMC_DELAY_US(hd); } } while (0) - // ---- 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 (emmc_ticks() - t0) & EMMC_TICK_MASK; + return (EMMC_TICKS() - t0) & EMMC_TICK_MASK; } static inline void half_delay(uint32_t us) { if (us) { - EMMC_DELAY_US(us); + common_hal_mcu_delay_us(us); } } +// 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(); @@ -130,6 +140,8 @@ static void crc16_tab_init(void) { } } +// 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; @@ -211,21 +223,24 @@ static bool send_command_retry(uint8_t cmd, uint32_t arg, uint8_t *r1_out, int t clk_pulse(); } } else { - EMMC_SLEEP_MS(2); + mp_hal_delay_ms(2); } } return false; } // DATA read: per-bit CLK toggle uses the configurable (possibly 0) half-period. -__attribute__((optimize("O2"))) // read path only: -O2 safe for reads, NOT writes +// -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(); + 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; @@ -234,14 +249,14 @@ static bool read_data_block(uint8_t *buf) { // timing modes for (int burst = 0; burst < 64 && !got_start; burst++) { RCLK_HIGH(); - HALF(hd); + half_delay(hd); EDGE_SETTLE(); if (!RDAT_GET()) { got_start = true; // leave with RCLK HIGH (as before) break; } RCLK_LOW(); - HALF(hd); + half_delay(hd); } uint32_t el = ticks_since(t0); if (got_start) { @@ -256,7 +271,7 @@ static bool read_data_block(uint8_t *buf) { } } RCLK_LOW(); - HALF(hd); + 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. @@ -265,9 +280,9 @@ static bool read_data_block(uint8_t *buf) { 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(hd); + half_delay(hd); RCLK_LOW(); - HALF(hd); // end bit + half_delay(hd); // end bit bool crc_ok = crc16(buf, EMMC_BLOCK_SIZE) == card_crc; DAT0_OUT(); DAT0_HIGH(); @@ -311,12 +326,12 @@ bool emmc_init(void) { DAT0_HIGH(); VCCQ_ON(); - EMMC_SLEEP_MS(10); + mp_hal_delay_ms(10); RST_ASSERT(); - EMMC_SLEEP_MS(1); + mp_hal_delay_ms(1); RST_RELEASE(); - EMMC_SLEEP_MS(2); + mp_hal_delay_ms(2); CMD_HIGH(); for (int i = 0; i < 80; i++) { // 74+ clocks before the first command @@ -325,14 +340,14 @@ bool emmc_init(void) { send_command(0, 0x00000000, NULL); // CMD0 GO_IDLE (no response expected) g_emmc_state.cmd0_sent = true; - EMMC_SLEEP_MS(1); + 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); - emmc_feed(); - EMMC_SLEEP_MS(1); + 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; @@ -348,20 +363,20 @@ bool emmc_init(void) { drain_r2_cid(g_emmc_state.cid); break; } - EMMC_SLEEP_MS(2); + mp_hal_delay_ms(2); } - EMMC_SLEEP_MS(1); + 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 - EMMC_SLEEP_MS(1); + 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 - EMMC_SLEEP_MS(1); + mp_hal_delay_ms(1); g_emmc_state.cmd16_resp = send_command_retry(16, EMMC_BLOCK_SIZE, r1, 8); // SET_BLOCKLEN - EMMC_SLEEP_MS(1); + 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; @@ -372,10 +387,6 @@ bool emmc_init(void) { return s_ready; } -bool emmc_is_ready(void) { - return s_ready; -} - uint32_t emmc_block_count(void) { return s_block_count; } @@ -385,7 +396,7 @@ bool emmc_blockdev_ioctl(uint32_t op, uint32_t arg, uint32_t *out_value) { (void)arg; *out_value = 0; switch (op) { - case EMMC_IOCTL_INIT: + 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 @@ -393,14 +404,14 @@ bool emmc_blockdev_ioctl(uint32_t op, uint32_t arg, uint32_t *out_value) { // than at the first read. *out_value = s_ready ? 0u : 1u; break; - case EMMC_IOCTL_DEINIT: - case EMMC_IOCTL_SYNC: - case EMMC_IOCTL_BLOCK_ERASE: + case MP_BLOCKDEV_IOCTL_DEINIT: + case MP_BLOCKDEV_IOCTL_SYNC: + case MP_BLOCKDEV_IOCTL_BLOCK_ERASE: break; - case EMMC_IOCTL_BLOCK_COUNT: + case MP_BLOCKDEV_IOCTL_BLOCK_COUNT: *out_value = s_block_count; break; - case EMMC_IOCTL_BLOCK_SIZE: + case MP_BLOCKDEV_IOCTL_BLOCK_SIZE: *out_value = EMMC_BLOCK_SIZE; break; default: @@ -409,10 +420,8 @@ bool emmc_blockdev_ioctl(uint32_t op, uint32_t arg, uint32_t *out_value) { return true; } -// Power-off: release the bus pins and cut the VCCQ I/O rail. With no write path -// and the card's volatile cache never enabled there is nothing to flush first, -// so board_power_off_prepare() needs no eMMC step of its own. The card is gone -// until the next emmc_init(). +// 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; @@ -457,7 +466,7 @@ bool emmc_read_ext_csd(uint8_t *buf) { // 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 -- emmc_feed(): feed the dog ONLY. Used by every wait inside a +// 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 @@ -467,7 +476,7 @@ static bool dat0_busy_wait(uint32_t timeout_us, bool run_bg) { for (int i = 0; i < EMMC_BUSY_LEADIN_CLOCKS; i++) { clk_pulse(); } - uint32_t t0 = emmc_ticks(); + uint32_t t0 = EMMC_TICKS(); const uint32_t lim = US_TO_TICKS(timeout_us); for (;;) { bool released = false; @@ -491,7 +500,7 @@ static bool dat0_busy_wait(uint32_t timeout_us, bool run_bg) { if (run_bg) { emmc_yield(); } else { - emmc_feed(); + bootloader_wdt_feed(); } } } @@ -510,7 +519,7 @@ static bool dat0_busy_wait(uint32_t timeout_us, bool run_bg) { // 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(); + uint32_t t0 = EMMC_TICKS(); const uint32_t lim = US_TO_TICKS(timeout_us); for (;;) { uint8_t r1[6]; @@ -529,7 +538,7 @@ static bool wait_tran_after_switch(uint32_t timeout_us) { return false; } emmc_yield(); - EMMC_SLEEP_MS(1); + mp_hal_delay_ms(1); } } @@ -569,7 +578,7 @@ bool emmc_set_high_speed(void) { // // This happens BEFORE the readback, because the readback is itself a block // read off a card that has already switched. - sp1_emmc_spim_set_config(SPIM_CONFIG_MODE1); + 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, @@ -577,7 +586,7 @@ bool emmc_set_high_speed(void) { uint8_t ext_csd[EMMC_BLOCK_SIZE]; if (!emmc_read_ext_csd(ext_csd) || ext_csd[EMMC_EXT_CSD_HS_TIMING] != 1u) { - sp1_emmc_spim_set_config(SPIM_CONFIG_MODE0); + NRF_SPIM3->CONFIG = SPIM_CONFIG_MODE0; return false; // still at M16, card still readable } g_emmc_state.hs_stage = 5; @@ -586,13 +595,13 @@ bool emmc_set_high_speed(void) { // 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. - sp1_emmc_spim_set_freq(SPIM_FREQ_M32); + 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. - sp1_emmc_spim_set_freq(SPIM_FREQ_M16); + NRF_SPIM3->FREQUENCY = SPIM_FREQ_M16; return false; } g_emmc_state.hs_active = true; @@ -622,7 +631,7 @@ bool emmc_read_blocks(uint32_t block_addr, uint8_t *buf, uint32_t count) { return false; } - uint32_t bt0 = emmc_ticks(); + 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) { @@ -643,6 +652,8 @@ bool emmc_read_blocks(uint32_t block_addr, uint8_t *buf, uint32_t count) { // 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; @@ -657,7 +668,7 @@ static bool write_data_block(const uint8_t *buf) { DAT0_OUT(); RDAT_HIGH(); - uint8_t *tx = sp1_emmc_tx_frame(); // the reserved low-RAM SPIM3 buffer + uint8_t *tx = SP1_EMMC_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 @@ -682,22 +693,22 @@ static bool write_data_block(const uint8_t *buf) { // register IS the state" true for the read path (sp1_emmc_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 = sp1_emmc_spim_config(); + const uint32_t saved_cfg = NRF_SPIM3->CONFIG; if (saved_cfg != SPIM_CONFIG_MODE0) { - sp1_emmc_spim_set_config(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. sp1_emmc_spim_xfer(tx, 2u + EMMC_BLOCK_SIZE + 2u, NULL, 0); if (saved_cfg != SPIM_CONFIG_MODE0) { - sp1_emmc_spim_set_config(saved_cfg); + NRF_SPIM3->CONFIG = saved_cfg; } // END bit: DAT0 is back at its GPIO latch (output HIGH) -- clock it. - HALF(hd); + half_delay(hd); EDGE_SETTLE(); RCLK_HIGH(); - HALF(hd); + half_delay(hd); RCLK_LOW(); // CRC-status token: the card drives DAT0 low (start bit), then 3 status @@ -706,20 +717,20 @@ static bool write_data_block(const uint8_t *buf) { int wr_status = -1; for (int i = 0; i < 16; i++) { RCLK_HIGH(); - HALF(hd); + half_delay(hd); EDGE_SETTLE(); int start = (int)RDAT_GET(); RCLK_LOW(); - HALF(hd); + half_delay(hd); if (!start) { int st = 0; for (int k = 0; k < 3; k++) { RCLK_HIGH(); - HALF(hd); + half_delay(hd); EDGE_SETTLE(); st = (st << 1) | (int)RDAT_GET(); RCLK_LOW(); - HALF(hd); + half_delay(hd); } wr_status = st; break; @@ -760,7 +771,7 @@ bool emmc_write_blocks(uint32_t block_addr, const uint8_t *buf, uint32_t count) if (!send_command_retry(25, block_addr, r1, 4)) { return false; } - uint32_t bt0 = emmc_ticks(); + 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) { @@ -782,7 +793,7 @@ 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 sp1_emmc_spim_freq() == SPIM_FREQ_M32 ? 32000000u : 16000000u; + return NRF_SPIM3->FREQUENCY == SPIM_FREQ_M32 ? 32000000u : 16000000u; } // ============================================================================ diff --git a/ports/nordic/sp1_emmc/sp1_emmc.h b/ports/nordic/sp1_emmc/sp1_emmc.h index aff2aa6572c..1e7403f50c6 100644 --- a/ports/nordic/sp1_emmc/sp1_emmc.h +++ b/ports/nordic/sp1_emmc/sp1_emmc.h @@ -49,25 +49,13 @@ extern sp1_emmc_state_t g_emmc_state; extern volatile uint32_t g_emmc_clk_half_us; bool emmc_init(void); -bool emmc_is_ready(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 -// The block-protocol ioctl ops (extmod/vfs.h's MP_BLOCKDEV_IOCTL_*), restated -// here so the protocol layer needs no MicroPython headers. EMMC.c -// static-asserts the two sets agree, so a future upstream renumbering stops -// the build rather than the card. -#define EMMC_IOCTL_INIT 1u -#define EMMC_IOCTL_DEINIT 2u -#define EMMC_IOCTL_SYNC 3u -#define EMMC_IOCTL_BLOCK_COUNT 4u -#define EMMC_IOCTL_BLOCK_SIZE 5u -#define EMMC_IOCTL_BLOCK_ERASE 6u - - +// 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 diff --git a/ports/nordic/sp1_emmc/sp1_emmc_hw.h b/ports/nordic/sp1_emmc/sp1_emmc_hw.h index 191ff46082f..79943e3f4b5 100644 --- a/ports/nordic/sp1_emmc/sp1_emmc_hw.h +++ b/ports/nordic/sp1_emmc/sp1_emmc_hw.h @@ -4,9 +4,8 @@ // // SPDX-License-Identifier: MIT -// Every hardware touch sp1_emmc.c makes goes through this header: the five -// pins, the clock/tick sources, the WDT feed and the SPIM3 data engine. The -// protocol file itself has no nRF or CircuitPython includes of its own. +// The hardware sp1_emmc.c drives: the five pins, the RTC2 tick source and the +// SPIM3 data engine. #pragma once @@ -14,6 +13,9 @@ #include #include +#include "nrf.h" +#include "nrf_gpio.h" + // Pins (SP-1 wiring). #define SP1_EMMC_PIN_CLK 6u // P0.06 #define SP1_EMMC_PIN_DAT0 7u // P0.07 @@ -29,19 +31,6 @@ #define SPIM_CONFIG_MODE0 0u // MSB first, CPOL0/CPHA0 #define SPIM_CONFIG_MODE1 (1u << 1) // MSB first, CPOL0/CPHA1 -// The write path's TX frame: FF gap | FE start token | 512 data | CRC16 = 516 -// bytes, rounded up. It must live in the port's reserved 8 KiB low-RAM SPIM3 -// buffer. -#define SP1_EMMC_TX_FRAME_SIZE 520u - -#include "nrf_gpio.h" -#include "nrf.h" - -#include "py/mphal.h" -#include "py/runtime.h" // RUN_BACKGROUND_TASKS -#include "shared-bindings/microcontroller/__init__.h" -#include "nrf/wdt.h" - // ---- 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 @@ -108,32 +97,14 @@ static inline void sp1_emmc_pins_release(void) { // // The write path relies on this buffer rather than on retrying a bad CRC // status; the status token is still enforced as the backstop. -static inline uint8_t *sp1_emmc_tx_frame(void) { - return (uint8_t *)SPIM3_BUFFER_RAM_START_ADDR; -} +#define SP1_EMMC_TX_FRAME ((uint8_t *)SPIM3_BUFFER_RAM_START_ADDR) // ---- time ---------------------------------------------------------------- -#define EMMC_DELAY_US(us) common_hal_mcu_delay_us(us) -#define EMMC_SLEEP_MS(ms) mp_hal_delay_ms(ms) - // 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 -static inline uint32_t emmc_ticks(void) { - return NRF_RTC2->COUNTER; -} - -// Long-wait service: feed the bootloader's dog and run background tasks (USB, -// the power-off gesture). Never sleeps -- the waits this serves are a few -// microseconds, not a nap. -static inline void emmc_yield(void) { - RUN_BACKGROUND_TASKS; - bootloader_wdt_feed(); -} -static inline void emmc_feed(void) { - bootloader_wdt_feed(); -} +#define EMMC_TICKS() (NRF_RTC2->COUNTER) // ---- SPIM3 data engine --------------------------------------------------- // SPIM3 is the only instance that runs above 8 MHz. M16 = 16 MHz, the fastest @@ -144,27 +115,11 @@ static inline void emmc_feed(void) { // There is still no free-floating "speed knob": the two codes at the top of // this file are the only values ever written. // -// The peripheral register IS the state -- no shadow copy to drift, and it -// survives ENABLE=0 between transfers. sp1_emmc_spim_init() puts it back to -// M16 on every init, so a fresh object always starts at compat speed even if -// the previous one ran high. -static inline uint32_t sp1_emmc_spim_freq(void) { - return NRF_SPIM3->FREQUENCY; -} - -static inline void sp1_emmc_spim_set_freq(uint32_t freq) { - NRF_SPIM3->FREQUENCY = freq; -} - -// CONFIG: SPIM_CONFIG_MODE0/MODE1 at the top of this file, and the reason the -// bit has to move at all. -static inline uint32_t sp1_emmc_spim_config(void) { - return NRF_SPIM3->CONFIG; -} - -static inline void sp1_emmc_spim_set_config(uint32_t config) { - NRF_SPIM3->CONFIG = config; -} +// NRF_SPIM3->FREQUENCY and ->CONFIG are read and written directly: the +// peripheral register IS the state -- no shadow copy to drift, and it survives +// ENABLE=0 between transfers. sp1_emmc_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 sp1_emmc_spim_init(void) { NRF_SPIM3->ENABLE = 0; From 82c470a487392577fcb476d1c1be879f5816d6bd Mon Sep 17 00:00:00 2001 From: foamyguy Date: Wed, 19 Aug 2026 13:12:41 -0500 Subject: [PATCH 06/14] power cycle emmc before automount, enforce automount deadline with graceful fallback --- ports/nordic/bindings/sp1emmc/EMMC.c | 9 ++++ ports/nordic/bindings/sp1emmc/__init__.c | 44 +++++++++++++++++++ ports/nordic/sp1_emmc/automount.c | 46 ++++++++++++++++++++ ports/nordic/sp1_emmc/automount.h | 18 ++++++++ ports/nordic/sp1_emmc/sp1_emmc.c | 54 +++++++++++++++++++++--- ports/nordic/sp1_emmc/sp1_emmc.h | 9 ++++ ports/nordic/sp1_emmc/sp1_emmc_hw.h | 23 ++++++++-- 7 files changed, 192 insertions(+), 11 deletions(-) diff --git a/ports/nordic/bindings/sp1emmc/EMMC.c b/ports/nordic/bindings/sp1emmc/EMMC.c index 79159567834..6b361396d33 100644 --- a/ports/nordic/bindings/sp1emmc/EMMC.c +++ b/ports/nordic/bindings/sp1emmc/EMMC.c @@ -183,6 +183,15 @@ mp_obj_t sp1emmc_automount_construct(bool high_speed, bool write_enabled) { 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: diff --git a/ports/nordic/bindings/sp1emmc/__init__.c b/ports/nordic/bindings/sp1emmc/__init__.c index 0f755b1c8e4..14ffbb2fa80 100644 --- a/ports/nordic/bindings/sp1emmc/__init__.c +++ b/ports/nordic/bindings/sp1emmc/__init__.c @@ -4,6 +4,8 @@ // // SPDX-License-Identifier: MIT +#include + #include "py/obj.h" #include "py/runtime.h" @@ -33,10 +35,52 @@ static mp_obj_t sp1emmc_automounted(void) { } 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(SP1_EMMC_AUTOMOUNT) && SP1_EMMC_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); diff --git a/ports/nordic/sp1_emmc/automount.c b/ports/nordic/sp1_emmc/automount.c index daf9c9d2ec5..624dbc93b9c 100644 --- a/ports/nordic/sp1_emmc/automount.c +++ b/ports/nordic/sp1_emmc/automount.c @@ -18,11 +18,34 @@ #include "supervisor/shared/safe_mode.h" #include "supervisor/shared/settings.h" +#include "sp1_emmc/sp1_emmc.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) { @@ -31,17 +54,30 @@ void sp1emmc_automount(void) { _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; } @@ -53,6 +89,7 @@ void sp1emmc_automount(void) { mp_vfs_blockdev_init(&vfs->blockdev, dev); if (f_mount(&vfs->fatfs) != FR_OK) { + automount_give_up(SP1EMMC_AUTOMOUNT_NO_FILESYSTEM); return; } @@ -66,6 +103,15 @@ void sp1emmc_automount(void) { 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 // SP1_EMMC_AUTOMOUNT diff --git a/ports/nordic/sp1_emmc/automount.h b/ports/nordic/sp1_emmc/automount.h index 7414861c271..951ee429432 100644 --- a/ports/nordic/sp1_emmc/automount.h +++ b/ports/nordic/sp1_emmc/automount.h @@ -14,14 +14,32 @@ #define SP1_EMMC_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; } diff --git a/ports/nordic/sp1_emmc/sp1_emmc.c b/ports/nordic/sp1_emmc/sp1_emmc.c index f0590cd511a..d94bcc16080 100644 --- a/ports/nordic/sp1_emmc/sp1_emmc.c +++ b/ports/nordic/sp1_emmc/sp1_emmc.c @@ -60,7 +60,7 @@ static uint8_t s_dma_rx[EMMC_BLOCK_SIZE + 2]; #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 (EMMC_TICKS() - t0) & EMMC_TICK_MASK; + return ticks_since_raw(t0); } static inline void half_delay(uint32_t us) { @@ -69,6 +69,24 @@ static inline void half_delay(uint32_t 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; @@ -213,6 +231,9 @@ static bool send_command(uint8_t cmd_index, uint32_t arg, uint8_t *r1_out) { // (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; } @@ -262,7 +283,7 @@ static bool read_data_block(uint8_t *buf) { if (got_start) { break; } - if (el >= lim) { + if (el >= lim || emmc_deadline_expired()) { return false; } if (el >= yield_at) { @@ -308,6 +329,21 @@ static void drain_r2_cid(uint8_t *cid_out) { } } +#define EMMC_POWER_OFF_MS 50u + +void emmc_power_cycle(void) { + sp1_emmc_spim_deinit(); // SPIM3 must not drive DAT0 either + sp1_emmc_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; @@ -317,7 +353,8 @@ bool emmc_init(void) { memset(&g_emmc_state, 0, sizeof(g_emmc_state)); g_emmc_state.cmd1_retries = -1; - sp1_emmc_pins_init(); + emmc_power_cycle(); + sp1_emmc_spim_init(); // hardware-clocked data path, at M16 crc16_tab_init(); @@ -352,6 +389,9 @@ bool emmc_init(void) { 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; @@ -493,7 +533,7 @@ static bool dat0_busy_wait(uint32_t timeout_us, bool run_bg) { DAT0_HIGH(); return true; } - if (el >= lim) { + if (el >= lim || emmc_deadline_expired()) { // DAT0 STAYS AN INPUT on a timeout return false; } @@ -534,7 +574,7 @@ static bool wait_tran_after_switch(uint32_t timeout_us) { return true; // tran + ready_for_data } } - if (ticks_since(t0) >= lim) { + if (ticks_since(t0) >= lim || emmc_deadline_expired()) { return false; } emmc_yield(); @@ -634,7 +674,7 @@ bool emmc_read_blocks(uint32_t block_addr, uint8_t *buf, uint32_t count) { 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) { + if (i && (ticks_since(bt0) >= blim || emmc_deadline_expired())) { (void)send_command_retry(12, 0, r1, 3); return false; } @@ -774,7 +814,7 @@ bool emmc_write_blocks(uint32_t block_addr, const uint8_t *buf, uint32_t count) 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) { + if (i && (ticks_since(bt0) >= blim || emmc_deadline_expired())) { (void)send_command_retry(12, 0, r1, 3); return false; } diff --git a/ports/nordic/sp1_emmc/sp1_emmc.h b/ports/nordic/sp1_emmc/sp1_emmc.h index 1e7403f50c6..3533e011ebf 100644 --- a/ports/nordic/sp1_emmc/sp1_emmc.h +++ b/ports/nordic/sp1_emmc/sp1_emmc.h @@ -48,6 +48,15 @@ extern sp1_emmc_state_t g_emmc_state; // 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 diff --git a/ports/nordic/sp1_emmc/sp1_emmc_hw.h b/ports/nordic/sp1_emmc/sp1_emmc_hw.h index 79943e3f4b5..e7464263e72 100644 --- a/ports/nordic/sp1_emmc/sp1_emmc_hw.h +++ b/ports/nordic/sp1_emmc/sp1_emmc_hw.h @@ -106,6 +106,13 @@ static inline void sp1_emmc_pins_release(void) { #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 SP1_EMMC_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 @@ -158,10 +165,18 @@ static inline void sp1_emmc_spim_xfer(const uint8_t *tx, uint32_t txlen, uint8_t NRF_SPIM3->RXD.MAXCNT = rx ? rxlen : 0; NRF_SPIM3->EVENTS_END = 0; NRF_SPIM3->TASKS_START = 1; - while (!NRF_SPIM3->EVENTS_END) { - // ~260 us for a full block at 16 MHz -- far too short to be worth a - // background-task round trip, and any yield here would risk the - // caller's framing. + { + uint32_t t0 = EMMC_TICKS(); + while (!NRF_SPIM3->EVENTS_END) { + if (ticks_since_raw(t0) >= SP1_EMMC_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; } From b072414d2fb81c13b6a919a938868290bb4c33b8 Mon Sep 17 00:00:00 2001 From: foamyguy Date: Wed, 19 Aug 2026 14:22:35 -0500 Subject: [PATCH 07/14] refactor sp1_emmc to sp1emmc --- extmod/vfs_blockdev.c | 4 +- ports/nordic/Makefile | 16 ++-- ports/nordic/bindings/sp1emmc/EMMC.c | 12 +-- ports/nordic/bindings/sp1emmc/__init__.c | 4 +- .../boards/teenage_engineering_sp1/board.c | 4 +- .../teenage_engineering_sp1/mpconfigboard.mk | 7 +- ports/nordic/common-hal/busio/SPI.c | 4 +- .../nordic/{sp1_emmc => sp1emmc}/automount.c | 12 +-- .../nordic/{sp1_emmc => sp1emmc}/automount.h | 6 +- .../sp1_emmc.c => sp1emmc/sp1emmc.c} | 24 ++--- .../sp1_emmc.h => sp1emmc/sp1emmc.h} | 4 +- .../sp1_emmc_hw.h => sp1emmc/sp1emmc_hw.h} | 92 +++++++++---------- ports/nordic/supervisor/port.c | 4 +- supervisor/shared/filesystem.c | 6 +- supervisor/shared/usb/usb_msc_flash.c | 6 +- 15 files changed, 102 insertions(+), 103 deletions(-) rename ports/nordic/{sp1_emmc => sp1emmc}/automount.c (92%) rename ports/nordic/{sp1_emmc => sp1emmc}/automount.h (90%) rename ports/nordic/{sp1_emmc/sp1_emmc.c => sp1emmc/sp1emmc.c} (98%) rename ports/nordic/{sp1_emmc/sp1_emmc.h => sp1emmc/sp1emmc.h} (98%) rename ports/nordic/{sp1_emmc/sp1_emmc_hw.h => sp1emmc/sp1emmc_hw.h} (68%) diff --git a/extmod/vfs_blockdev.c b/extmod/vfs_blockdev.c index b7d61f42c63..d839ce00ea3 100644 --- a/extmod/vfs_blockdev.c +++ b/extmod/vfs_blockdev.c @@ -38,7 +38,7 @@ #include "shared-bindings/sdioio/SDCard.h" #endif -#if defined(CIRCUITPY_SP1_EMMC) && CIRCUITPY_SP1_EMMC +#if defined(CIRCUITPY_SP1EMMC) && CIRCUITPY_SP1EMMC #include "bindings/sp1emmc/EMMC.h" #endif @@ -80,7 +80,7 @@ void mp_vfs_blockdev_init(mp_vfs_blockdev_t *self, mp_obj_t bdev) { } #endif - #if defined(CIRCUITPY_SP1_EMMC) && CIRCUITPY_SP1_EMMC + #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; diff --git a/ports/nordic/Makefile b/ports/nordic/Makefile index 07f7d1df746..d320a702775 100755 --- a/ports/nordic/Makefile +++ b/ports/nordic/Makefile @@ -151,19 +151,19 @@ SRC_C += \ nrfx/mdk/system_$(MCU_SUB_VARIANT).c \ -CIRCUITPY_SP1_EMMC ?= 0 -CFLAGS += -DCIRCUITPY_SP1_EMMC=$(CIRCUITPY_SP1_EMMC) +CIRCUITPY_SP1EMMC ?= 0 +CFLAGS += -DCIRCUITPY_SP1EMMC=$(CIRCUITPY_SP1EMMC) # USB mass storage automount switch -CIRCUITPY_SP1_EMMC_USB ?= 0 -CFLAGS += -DSP1_EMMC_AUTOMOUNT=$(CIRCUITPY_SP1_EMMC_USB) -ifeq ($(CIRCUITPY_SP1_EMMC),1) +CIRCUITPY_SP1EMMC_USB ?= 0 +CFLAGS += -DSP1EMMC_AUTOMOUNT=$(CIRCUITPY_SP1EMMC_USB) +ifeq ($(CIRCUITPY_SP1EMMC),1) SRC_C += \ bindings/sp1emmc/__init__.c \ bindings/sp1emmc/EMMC.c \ - sp1_emmc/sp1_emmc.c \ + sp1emmc/sp1emmc.c \ -ifeq ($(CIRCUITPY_SP1_EMMC_USB),1) -SRC_C += sp1_emmc/automount.c +ifeq ($(CIRCUITPY_SP1EMMC_USB),1) +SRC_C += sp1emmc/automount.c endif endif diff --git a/ports/nordic/bindings/sp1emmc/EMMC.c b/ports/nordic/bindings/sp1emmc/EMMC.c index 6b361396d33..23c652a23a0 100644 --- a/ports/nordic/bindings/sp1emmc/EMMC.c +++ b/ports/nordic/bindings/sp1emmc/EMMC.c @@ -21,8 +21,8 @@ #include "peripherals/nrf/nrf52840/pins.h" #include "bindings/sp1emmc/EMMC.h" -#include "sp1_emmc/automount.h" -#include "sp1_emmc/sp1_emmc.h" +#include "sp1emmc/automount.h" +#include "sp1emmc/sp1emmc.h" #include "nrf.h" @@ -34,7 +34,7 @@ typedef struct { bool write_enabled; } sp1emmc_emmc_obj_t; -#if SP1_EMMC_AUTOMOUNT +#if SP1EMMC_AUTOMOUNT static sp1emmc_emmc_obj_t s_automount_obj; static bool s_automounted; #endif @@ -60,7 +60,7 @@ static void release_hardware(void) { } void sp1emmc_reset(void) { - #if SP1_EMMC_AUTOMOUNT + #if SP1EMMC_AUTOMOUNT if (s_automounted) { return; } @@ -146,7 +146,7 @@ static const char *emmc_power_up(bool high_speed, bool *hs_failed) { return NULL; } -#if SP1_EMMC_AUTOMOUNT +#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. // @@ -233,7 +233,7 @@ static mp_obj_t sp1emmc_emmc_make_new(const mp_obj_type_t *type, size_t n_args, bool high_speed = args[ARG_high_speed].u_bool; bool write_enabled = args[ARG_write_enabled].u_bool; - #if SP1_EMMC_AUTOMOUNT + #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")); } diff --git a/ports/nordic/bindings/sp1emmc/__init__.c b/ports/nordic/bindings/sp1emmc/__init__.c index 14ffbb2fa80..c794e012637 100644 --- a/ports/nordic/bindings/sp1emmc/__init__.c +++ b/ports/nordic/bindings/sp1emmc/__init__.c @@ -10,7 +10,7 @@ #include "py/runtime.h" #include "bindings/sp1emmc/EMMC.h" -#include "sp1_emmc/automount.h" +#include "sp1emmc/automount.h" //| """Block device access to the on-board eMMC //| @@ -51,7 +51,7 @@ static MP_DEFINE_CONST_FUN_OBJ_0(sp1emmc_automounted_obj, sp1emmc_automounted); //| static mp_obj_t sp1emmc_automount_status(void) { const char *s = "disabled"; - #if defined(SP1_EMMC_AUTOMOUNT) && SP1_EMMC_AUTOMOUNT + #if defined(SP1EMMC_AUTOMOUNT) && SP1EMMC_AUTOMOUNT switch (sp1emmc_automount_get_status()) { case SP1EMMC_AUTOMOUNT_OK: s = "ok"; diff --git a/ports/nordic/boards/teenage_engineering_sp1/board.c b/ports/nordic/boards/teenage_engineering_sp1/board.c index d4b5a8a6420..4a5af986f6b 100644 --- a/ports/nordic/boards/teenage_engineering_sp1/board.c +++ b/ports/nordic/boards/teenage_engineering_sp1/board.c @@ -23,8 +23,8 @@ // 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_SP1_EMMC -#include "sp1_emmc/automount.h" +#if CIRCUITPY_SP1EMMC +#include "sp1emmc/automount.h" #else static inline bool sp1emmc_is_automounted(void) { return false; diff --git a/ports/nordic/boards/teenage_engineering_sp1/mpconfigboard.mk b/ports/nordic/boards/teenage_engineering_sp1/mpconfigboard.mk index 1ea34bdf174..f0c5eaf4168 100644 --- a/ports/nordic/boards/teenage_engineering_sp1/mpconfigboard.mk +++ b/ports/nordic/boards/teenage_engineering_sp1/mpconfigboard.mk @@ -60,12 +60,11 @@ CIRCUITPY_AUDIOPWMIO = 0 CIRCUITPY_SYNTHIO = 1 CIRCUITPY_AUDIOEFFECTS = 1 # audiomp3 would otherwise come along for free with audiocore on a full build. -# Decode CPU headroom on this chip is unassessed and it costs ~26 KB of flash, -# so it stays off until it is deliberately assessed. +# but doesn't reliably decode mp3s off of emmc CIRCUITPY_AUDIOMP3 = 0 # eMMC -CIRCUITPY_SP1_EMMC = 1 +CIRCUITPY_SP1EMMC = 1 # auto mount EMMC as /sd -CIRCUITPY_SP1_EMMC_USB = 1 +CIRCUITPY_SP1EMMC_USB = 1 diff --git a/ports/nordic/common-hal/busio/SPI.c b/ports/nordic/common-hal/busio/SPI.c index ad9bb459e57..7fb535b0d3e 100644 --- a/ports/nordic/common-hal/busio/SPI.c +++ b/ports/nordic/common-hal/busio/SPI.c @@ -14,7 +14,7 @@ #include "nrfx_spim.h" #include "nrf_gpio.h" -#if CIRCUITPY_SP1_EMMC +#if CIRCUITPY_SP1EMMC #include "bindings/sp1emmc/EMMC.h" #endif @@ -126,7 +126,7 @@ 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 CIRCUITPY_SP1_EMMC + #if CIRCUITPY_SP1EMMC if (spim_peripherals[i].spim.p_reg == NRF_SPIM3 && sp1emmc_spim3_in_use()) { continue; } diff --git a/ports/nordic/sp1_emmc/automount.c b/ports/nordic/sp1emmc/automount.c similarity index 92% rename from ports/nordic/sp1_emmc/automount.c rename to ports/nordic/sp1emmc/automount.c index 624dbc93b9c..4298978cdbd 100644 --- a/ports/nordic/sp1_emmc/automount.c +++ b/ports/nordic/sp1emmc/automount.c @@ -4,9 +4,9 @@ // // SPDX-License-Identifier: MIT -#include "sp1_emmc/automount.h" +#include "sp1emmc/automount.h" -#if defined(SP1_EMMC_AUTOMOUNT) && SP1_EMMC_AUTOMOUNT +#if defined(SP1EMMC_AUTOMOUNT) && SP1EMMC_AUTOMOUNT #include "py/mpstate.h" @@ -18,7 +18,7 @@ #include "supervisor/shared/safe_mode.h" #include "supervisor/shared/settings.h" -#include "sp1_emmc/sp1_emmc.h" +#include "sp1emmc/sp1emmc.h" static mp_vfs_mount_t _emmc_vfs; @@ -98,8 +98,8 @@ void sp1emmc_automount(void) { filesystem_set_writable_by_usb(vfs, true); mp_vfs_mount_t *emmc_vfs = &_emmc_vfs; - emmc_vfs->str = SP1_EMMC_AUTOMOUNT_PATH; - emmc_vfs->len = sizeof(SP1_EMMC_AUTOMOUNT_PATH) - 1; + 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; @@ -114,4 +114,4 @@ sp1emmc_automount_status_t sp1emmc_automount_get_status(void) { return _status; } -#endif // SP1_EMMC_AUTOMOUNT +#endif // SP1EMMC_AUTOMOUNT diff --git a/ports/nordic/sp1_emmc/automount.h b/ports/nordic/sp1emmc/automount.h similarity index 90% rename from ports/nordic/sp1_emmc/automount.h rename to ports/nordic/sp1emmc/automount.h index 951ee429432..faa9800d697 100644 --- a/ports/nordic/sp1_emmc/automount.h +++ b/ports/nordic/sp1emmc/automount.h @@ -8,10 +8,10 @@ #include "py/obj.h" -#if defined(SP1_EMMC_AUTOMOUNT) && SP1_EMMC_AUTOMOUNT +#if defined(SP1EMMC_AUTOMOUNT) && SP1EMMC_AUTOMOUNT -#ifndef SP1_EMMC_AUTOMOUNT_PATH -#define SP1_EMMC_AUTOMOUNT_PATH "/sd" +#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 diff --git a/ports/nordic/sp1_emmc/sp1_emmc.c b/ports/nordic/sp1emmc/sp1emmc.c similarity index 98% rename from ports/nordic/sp1_emmc/sp1_emmc.c rename to ports/nordic/sp1emmc/sp1emmc.c index d94bcc16080..662b260dacf 100644 --- a/ports/nordic/sp1_emmc/sp1_emmc.c +++ b/ports/nordic/sp1emmc/sp1emmc.c @@ -26,8 +26,8 @@ // // ============================================================================ -#include "sp1_emmc.h" -#include "sp1_emmc_hw.h" +#include "sp1emmc.h" +#include "sp1emmc_hw.h" #include @@ -45,7 +45,7 @@ static uint32_t s_cmd_half_us = CMD_SAFE_HALF_US; volatile uint32_t g_emmc_clk_half_us = CMD_SAFE_HALF_US; -sp1_emmc_state_t g_emmc_state; +sp1emmc_state_t g_emmc_state; static bool s_ready; static uint32_t s_rca; @@ -296,7 +296,7 @@ static bool read_data_block(uint8_t *buf) { // The start bit was just consumed by the bit-bang hunt above, so the // remaining 512 data bytes + CRC16 are exactly byte-aligned. - sp1_emmc_spim_xfer(NULL, 0, s_dma_rx, sizeof(s_dma_rx)); + 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]); @@ -332,8 +332,8 @@ static void drain_r2_cid(uint8_t *cid_out) { #define EMMC_POWER_OFF_MS 50u void emmc_power_cycle(void) { - sp1_emmc_spim_deinit(); // SPIM3 must not drive DAT0 either - sp1_emmc_pins_init(); + sp1emmc_spim_deinit(); // SPIM3 must not drive DAT0 either + sp1emmc_pins_init(); RST_ASSERT(); CLK_LOW(); @@ -355,7 +355,7 @@ bool emmc_init(void) { emmc_power_cycle(); - sp1_emmc_spim_init(); // hardware-clocked data path, at M16 + sp1emmc_spim_init(); // hardware-clocked data path, at M16 crc16_tab_init(); CLK_LOW(); @@ -465,9 +465,9 @@ bool emmc_blockdev_ioctl(uint32_t op, uint32_t arg, uint32_t *out_value) { void emmc_power_down(void) { s_ready = false; s_block_count = 0; - sp1_emmc_spim_deinit(); + sp1emmc_spim_deinit(); RST_ASSERT(); - sp1_emmc_pins_release(); + sp1emmc_pins_release(); VCCQ_OFF(); // rail off (pin stays an output) } @@ -708,7 +708,7 @@ static bool write_data_block(const uint8_t *buf) { DAT0_OUT(); RDAT_HIGH(); - uint8_t *tx = SP1_EMMC_TX_FRAME; // the reserved low-RAM SPIM3 buffer + 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 @@ -730,7 +730,7 @@ static bool write_data_block(const uint8_t *buf) { // 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 (sp1_emmc_hw.h): this + // 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; @@ -740,7 +740,7 @@ static bool write_data_block(const uint8_t *buf) { // 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. - sp1_emmc_spim_xfer(tx, 2u + EMMC_BLOCK_SIZE + 2u, NULL, 0); + sp1emmc_spim_xfer(tx, 2u + EMMC_BLOCK_SIZE + 2u, NULL, 0); if (saved_cfg != SPIM_CONFIG_MODE0) { NRF_SPIM3->CONFIG = saved_cfg; } diff --git a/ports/nordic/sp1_emmc/sp1_emmc.h b/ports/nordic/sp1emmc/sp1emmc.h similarity index 98% rename from ports/nordic/sp1_emmc/sp1_emmc.h rename to ports/nordic/sp1emmc/sp1emmc.h index 3533e011ebf..2891c70d58f 100644 --- a/ports/nordic/sp1_emmc/sp1_emmc.h +++ b/ports/nordic/sp1emmc/sp1emmc.h @@ -39,9 +39,9 @@ typedef struct { // 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; -} sp1_emmc_state_t; +} sp1emmc_state_t; -extern sp1_emmc_state_t g_emmc_state; +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 diff --git a/ports/nordic/sp1_emmc/sp1_emmc_hw.h b/ports/nordic/sp1emmc/sp1emmc_hw.h similarity index 68% rename from ports/nordic/sp1_emmc/sp1_emmc_hw.h rename to ports/nordic/sp1emmc/sp1emmc_hw.h index e7464263e72..45c9b3932f2 100644 --- a/ports/nordic/sp1_emmc/sp1_emmc_hw.h +++ b/ports/nordic/sp1emmc/sp1emmc_hw.h @@ -4,7 +4,7 @@ // // SPDX-License-Identifier: MIT -// The hardware sp1_emmc.c drives: the five pins, the RTC2 tick source and the +// The hardware sp1emmc.c drives: the five pins, the RTC2 tick source and the // SPIM3 data engine. @@ -17,11 +17,11 @@ #include "nrf_gpio.h" // Pins (SP-1 wiring). -#define SP1_EMMC_PIN_CLK 6u // P0.06 -#define SP1_EMMC_PIN_DAT0 7u // P0.07 -#define SP1_EMMC_PIN_CMD 8u // P0.08 -#define SP1_EMMC_PIN_RST 40u // P1.08, active low -#define SP1_EMMC_PIN_VCCQ 14u // P0.14, I/O rail gate +#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 @@ -35,54 +35,54 @@ // 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(SP1_EMMC_PIN_CLK) -#define CLK_LOW() nrf_gpio_pin_clear(SP1_EMMC_PIN_CLK) -#define CMD_HIGH() nrf_gpio_pin_set(SP1_EMMC_PIN_CMD) -#define CMD_LOW() nrf_gpio_pin_clear(SP1_EMMC_PIN_CMD) -#define DAT0_HIGH() nrf_gpio_pin_set(SP1_EMMC_PIN_DAT0) -#define DAT0_LOW() nrf_gpio_pin_clear(SP1_EMMC_PIN_DAT0) -#define DAT0_IN() nrf_gpio_cfg_input(SP1_EMMC_PIN_DAT0, NRF_GPIO_PIN_PULLUP) +#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(SP1_EMMC_PIN_DAT0, NRF_GPIO_PIN_DIR_OUTPUT, \ +#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(SP1_EMMC_PIN_CMD, NRF_GPIO_PIN_PULLUP) -#define CMD_OUT() nrf_gpio_cfg_output(SP1_EMMC_PIN_CMD) -#define READ_CMD() nrf_gpio_pin_read(SP1_EMMC_PIN_CMD) -#define READ_DAT0() nrf_gpio_pin_read(SP1_EMMC_PIN_DAT0) +#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 << SP1_EMMC_PIN_CLK) -#define P0_DAT_BIT (1u << SP1_EMMC_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 >> SP1_EMMC_PIN_DAT0) & 1u) +#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(SP1_EMMC_PIN_RST) -#define RST_RELEASE() nrf_gpio_pin_set(SP1_EMMC_PIN_RST) -#define VCCQ_ON() nrf_gpio_pin_set(SP1_EMMC_PIN_VCCQ) -#define VCCQ_OFF() nrf_gpio_pin_clear(SP1_EMMC_PIN_VCCQ) +#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 sp1_emmc_pins_init(void) { - nrf_gpio_cfg(SP1_EMMC_PIN_CLK, NRF_GPIO_PIN_DIR_OUTPUT, NRF_GPIO_PIN_INPUT_DISCONNECT, +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(SP1_EMMC_PIN_CMD); + nrf_gpio_cfg_output(SP1EMMC_PIN_CMD); DAT0_OUT(); // high-drive DAT0 - nrf_gpio_cfg_output(SP1_EMMC_PIN_RST); + 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(SP1_EMMC_PIN_VCCQ); + nrf_gpio_cfg_output(SP1EMMC_PIN_VCCQ); } -static inline void sp1_emmc_pins_release(void) { - nrf_gpio_cfg_default(SP1_EMMC_PIN_CLK); - nrf_gpio_cfg_default(SP1_EMMC_PIN_CMD); - nrf_gpio_cfg_default(SP1_EMMC_PIN_DAT0); - nrf_gpio_cfg_default(SP1_EMMC_PIN_RST); +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. } @@ -97,7 +97,7 @@ static inline void sp1_emmc_pins_release(void) { // // 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 SP1_EMMC_TX_FRAME ((uint8_t *)SPIM3_BUFFER_RAM_START_ADDR) +#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, @@ -111,7 +111,7 @@ static inline uint32_t ticks_since_raw(uint32_t t0) { } // 20 ms, ~75x the 260 us a full block takes at M16. -#define SP1_EMMC_SPIM_TIMEOUT_TICKS ((EMMC_TICKS_HZ * 20u) / 1000u) +#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 @@ -124,13 +124,13 @@ static inline uint32_t ticks_since_raw(uint32_t t0) { // // NRF_SPIM3->FREQUENCY and ->CONFIG are read and written directly: the // peripheral register IS the state -- no shadow copy to drift, and it survives -// ENABLE=0 between transfers. sp1_emmc_spim_init() puts both back to M16 / +// 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 sp1_emmc_spim_init(void) { +static inline void sp1emmc_spim_init(void) { NRF_SPIM3->ENABLE = 0; - NRF_SPIM3->PSEL.SCK = SP1_EMMC_PIN_CLK; + 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; @@ -139,7 +139,7 @@ static inline void sp1_emmc_spim_init(void) { NRF_SPIM3->ORC = 0xFF; // idle-high filler } -static inline void sp1_emmc_spim_deinit(void) { +static inline void sp1emmc_spim_deinit(void) { NRF_SPIM3->ENABLE = 0; NRF_SPIM3->PSEL.SCK = 0xFFFFFFFFu; NRF_SPIM3->PSEL.MOSI = 0xFFFFFFFFu; @@ -154,10 +154,10 @@ static inline void sp1_emmc_spim_deinit(void) { // // 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 sp1_emmc_tx_frame() above. -static inline void sp1_emmc_spim_xfer(const uint8_t *tx, uint32_t txlen, uint8_t *rx, uint32_t rxlen) { - NRF_SPIM3->PSEL.MOSI = tx ? SP1_EMMC_PIN_DAT0 : 0xFFFFFFFFu; - NRF_SPIM3->PSEL.MISO = rx ? SP1_EMMC_PIN_DAT0 : 0xFFFFFFFFu; +// 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; @@ -168,7 +168,7 @@ static inline void sp1_emmc_spim_xfer(const uint8_t *tx, uint32_t txlen, uint8_t { uint32_t t0 = EMMC_TICKS(); while (!NRF_SPIM3->EVENTS_END) { - if (ticks_since_raw(t0) >= SP1_EMMC_SPIM_TIMEOUT_TICKS) { + 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. diff --git a/ports/nordic/supervisor/port.c b/ports/nordic/supervisor/port.c index 336dfd702e5..940b67b5835 100644 --- a/ports/nordic/supervisor/port.c +++ b/ports/nordic/supervisor/port.c @@ -24,7 +24,7 @@ #include "nrf/timers.h" #include "nrf/wdt.h" -#if CIRCUITPY_SP1_EMMC +#if CIRCUITPY_SP1EMMC #include "bindings/sp1emmc/EMMC.h" #endif @@ -244,7 +244,7 @@ void reset_port(void) { rtc_reset(); #endif - #if CIRCUITPY_SP1_EMMC + #if CIRCUITPY_SP1EMMC // board_reset_pin_defaults() will already have asserted the card's reset // and dropped its VCCQ rail. sp1emmc_reset(); diff --git a/supervisor/shared/filesystem.c b/supervisor/shared/filesystem.c index 01fc9b13502..49ed97f40a5 100644 --- a/supervisor/shared/filesystem.c +++ b/supervisor/shared/filesystem.c @@ -19,8 +19,8 @@ #include "shared-module/sdcardio/__init__.h" #endif -#if defined(SP1_EMMC_AUTOMOUNT) && SP1_EMMC_AUTOMOUNT -#include "sp1_emmc/automount.h" +#if defined(SP1EMMC_AUTOMOUNT) && SP1EMMC_AUTOMOUNT +#include "sp1emmc/automount.h" #endif static mp_vfs_mount_t _circuitpy_vfs; @@ -238,7 +238,7 @@ bool filesystem_init(bool create_allowed, bool force_create) { // 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(SP1_EMMC_AUTOMOUNT) && SP1_EMMC_AUTOMOUNT + #if defined(SP1EMMC_AUTOMOUNT) && SP1EMMC_AUTOMOUNT sp1emmc_automount(); #endif diff --git a/supervisor/shared/usb/usb_msc_flash.c b/supervisor/shared/usb/usb_msc_flash.c index 06fc9cf760c..bff4f24346b 100644 --- a/supervisor/shared/usb/usb_msc_flash.c +++ b/supervisor/shared/usb/usb_msc_flash.c @@ -38,8 +38,8 @@ #define SDCARD_COUNT 0 #endif -#if defined(SP1_EMMC_AUTOMOUNT) && SP1_EMMC_AUTOMOUNT -#include "sp1_emmc/automount.h" +#if defined(SP1EMMC_AUTOMOUNT) && SP1EMMC_AUTOMOUNT +#include "sp1emmc/automount.h" #define EMMC_COUNT 1 #define EMMC_LUN (1 + SAVES_COUNT + SDCARD_COUNT) @@ -178,7 +178,7 @@ static fs_user_mount_t *get_vfs(int lun) { if (lun == EMMC_LUN) { const char *path_under_mount; - fs_user_mount_t *emmc = filesystem_for_path(SP1_EMMC_AUTOMOUNT_PATH, &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 From 7a4e901ca538a911304d61aca0edb9ed7af8a330 Mon Sep 17 00:00:00 2001 From: foamyguy Date: Wed, 19 Aug 2026 15:53:29 -0500 Subject: [PATCH 08/14] pre-commit fixes and code format --- locale/circuitpython.pot | 40 +++++++++++++++++-- ports/nordic/bindings/sp1emmc/EMMC.c | 4 +- .../boards/teenage_engineering_sp1/board.c | 2 +- .../teenage_engineering_sp1/mpconfigboard.mk | 4 +- .../boards/teenage_engineering_sp1/pins.c | 2 +- 5 files changed, 42 insertions(+), 10 deletions(-) diff --git a/locale/circuitpython.pot b/locale/circuitpython.pot index 543a6ecdfa3..1519fbd2b15 100644 --- a/locale/circuitpython.pot +++ b/locale/circuitpython.pot @@ -1703,6 +1703,42 @@ msgid "" "Frequency must be 24, 150, 396, 450, 528, 600, 720, 816, 912, 960 or 1008 Mhz" msgstr "" +#: ports/nordic/bindings/sp1emmc/EMMC.c +msgid "" +"eMMC owned by the USB drive; set CIRCUITPY_EMMC_USB = false in settings.toml" +msgstr "" + +#: ports/nordic/bindings/sp1emmc/EMMC.c +msgid "eMMC already in use" +msgstr "" + +#: ports/nordic/bindings/sp1emmc/EMMC.c +#: ports/raspberrypi/common-hal/busio/SPI.c +msgid "SPI peripheral in use" +msgstr "" + +#: ports/nordic/bindings/sp1emmc/EMMC.c +#, c-format +msgid "eMMC high-speed switch failed at %s" +msgstr "" + +#: ports/nordic/bindings/sp1emmc/EMMC.c +#, c-format +msgid "eMMC init failed at %s" +msgstr "" + +#: ports/nordic/bindings/sp1emmc/EMMC.c +msgid "Buffer length must be a multiple of 512" +msgstr "" + +#: ports/nordic/bindings/sp1emmc/EMMC.c +msgid "Block address out of range" +msgstr "" + +#: ports/nordic/bindings/sp1emmc/EMMC.c +msgid "Read-only: construct EMMC(write_enabled=True) to write" +msgstr "" + #: ports/nordic/boards/aramcon2_badge/mpconfigboard.h msgid "You pressed the left button at start up." msgstr "" @@ -1902,10 +1938,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 "" diff --git a/ports/nordic/bindings/sp1emmc/EMMC.c b/ports/nordic/bindings/sp1emmc/EMMC.c index 23c652a23a0..e347771908e 100644 --- a/ports/nordic/bindings/sp1emmc/EMMC.c +++ b/ports/nordic/bindings/sp1emmc/EMMC.c @@ -301,7 +301,7 @@ static int emmc_read_chunked(uint8_t *out, mp_uint_t start, mp_uint_t count, boo if (count == 0 || start >= total || count > total - start) { return -MP_EINVAL; } - for (mp_uint_t done = 0; done < count; ) { + 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++) { @@ -360,7 +360,7 @@ static int emmc_write_chunked(const uint8_t *src, mp_uint_t start, mp_uint_t cou if (count == 0 || start >= total || count > total - start) { return -MP_EINVAL; } - for (mp_uint_t done = 0; done < count; ) { + 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++) { diff --git a/ports/nordic/boards/teenage_engineering_sp1/board.c b/ports/nordic/boards/teenage_engineering_sp1/board.c index 4a5af986f6b..07e5ff87162 100644 --- a/ports/nordic/boards/teenage_engineering_sp1/board.c +++ b/ports/nordic/boards/teenage_engineering_sp1/board.c @@ -4,7 +4,7 @@ // // SPDX-License-Identifier: MIT -// Early-boot hygiene for the TE SP-1. +// 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 diff --git a/ports/nordic/boards/teenage_engineering_sp1/mpconfigboard.mk b/ports/nordic/boards/teenage_engineering_sp1/mpconfigboard.mk index f0c5eaf4168..e6c76af03c3 100644 --- a/ports/nordic/boards/teenage_engineering_sp1/mpconfigboard.mk +++ b/ports/nordic/boards/teenage_engineering_sp1/mpconfigboard.mk @@ -29,7 +29,7 @@ NRF_REGOUT0_3V3 = 0 # UICR burn is not. NRF_NFCT_PINS_AS_GPIOS = 0 -# No SoftDevice. It would have to live at 0x1000, which is inside the TE +# 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 = @@ -48,7 +48,7 @@ CIRCUITPY_SHARPDISPLAY = 0 CIRCUITPY_IS31FL3741 = 0 CIRCUITPY_VECTORIO = 0 -# The watchdog is started by the TE bootloader before our first instruction and +# The watchdog is started by the bootloader before our first instruction and # its config registers are locked. CIRCUITPY_WATCHDOG = 0 diff --git a/ports/nordic/boards/teenage_engineering_sp1/pins.c b/ports/nordic/boards/teenage_engineering_sp1/pins.c index 20182bfa0e5..a21d2f4fefa 100644 --- a/ports/nordic/boards/teenage_engineering_sp1/pins.c +++ b/ports/nordic/boards/teenage_engineering_sp1/pins.c @@ -4,7 +4,7 @@ // // SPDX-License-Identifier: MIT -// Pin map from Tim Knapen's reverse-engineering of the TE SP-1 +// 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" From 96e81b4991c910e2a4299952a51a7ec498f1999a Mon Sep 17 00:00:00 2001 From: foamyguy Date: Wed, 19 Aug 2026 16:29:59 -0500 Subject: [PATCH 09/14] more comment cleanup --- .../teenage_engineering_sp1/mpconfigboard.mk | 8 +++---- ports/nordic/mpconfigport.mk | 11 ++++------ ports/nordic/power_off.c | 3 +-- ports/nordic/power_off.h | 17 ++++---------- ports/nordic/sp1emmc/sp1emmc.c | 22 ------------------- ports/nordic/sp1emmc/sp1emmc_hw.h | 5 ++--- 6 files changed, 14 insertions(+), 52 deletions(-) diff --git a/ports/nordic/boards/teenage_engineering_sp1/mpconfigboard.mk b/ports/nordic/boards/teenage_engineering_sp1/mpconfigboard.mk index e6c76af03c3..14c9e647057 100644 --- a/ports/nordic/boards/teenage_engineering_sp1/mpconfigboard.mk +++ b/ports/nordic/boards/teenage_engineering_sp1/mpconfigboard.mk @@ -6,11 +6,9 @@ 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 reachable, and an -# ERASEALL would take TE's bootloader with it — the only copy in existence, -# since the USB protocol has no read command. So each of the three conditional -# UICR writes on the boot path is turned off here, and the acceptance test is -# the disassembly: every reference to 0x10001000 in firmware.elf must be a read. +# 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 diff --git a/ports/nordic/mpconfigport.mk b/ports/nordic/mpconfigport.mk index f7a7fb65463..9f70c778368 100644 --- a/ports/nordic/mpconfigport.mk +++ b/ports/nordic/mpconfigport.mk @@ -87,11 +87,9 @@ NRF_DEFINES += -DNRF52840_XXAA -DNRF52840 # See https://github.com/adafruit/circuitpython/issues/1300 # Defined here because system_nrf52840.c doesn't #include any of our own include files. # -# This is one of the three UICR writes on the boot path (see the Makefile): 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 -- but -# then P0.09/P0.10 are usable as GPIO only if NFCPINS is *already* programmed, -# which is a claim about that specific device, not about the design. +# 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 @@ -134,8 +132,7 @@ endif endif endif -# Cannot have BLEIO without SoftDevice, and -# without _bleio the BLE workflow services have nothing to run on. +# Cannot have BLEIO without SoftDevice ifeq ($(SD), ) CIRCUITPY_BLEIO_NATIVE = 0 CIRCUITPY_BLE_FILE_SERVICE = 0 diff --git a/ports/nordic/power_off.c b/ports/nordic/power_off.c index 7633ab70d37..920b1234a0f 100644 --- a/ports/nordic/power_off.c +++ b/ports/nordic/power_off.c @@ -140,8 +140,7 @@ void power_off_tick(void) { ensure_input_buffer_connected(); // Reading IN never disturbs the pin, so the gesture still works if user - // code has claimed the button -- which is the point. The way out of a - // running program must not depend on that program's cooperation. + // 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 diff --git a/ports/nordic/power_off.h b/ports/nordic/power_off.h index 3be8be42b2b..63fa8e981ad 100644 --- a/ports/nordic/power_off.h +++ b/ports/nordic/power_off.h @@ -22,17 +22,9 @@ // that was a moment too short. // // The board does not have to configure that pin. If its input buffer is found -// disconnected -- the reset default, and where a digitalio deinit leaves it -- -// the poll reconnects it with a pull-up. A board is still free to configure it -// itself, and anything already configured is left alone. This is deliberate -// belt-and-braces: an unconfigured pin reads as permanently held, which would -// disable the gesture silently and for good. -// -// This lives in the supervisor rather than in Python on purpose. On a device -// with no reset pin and no removable battery it is half of the safety design: -// wherever the watchdog is being fed, the way out has to be live too -- -// including at the REPL, in safe mode, and while a program that has stopped -// listening is running. +// 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" @@ -45,8 +37,7 @@ 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. Called with the button still held, before the wake-up is armed, so it -// may take as long as it needs. Weak; the default does nothing. +// off. void board_power_off_prepare(void); #endif diff --git a/ports/nordic/sp1emmc/sp1emmc.c b/ports/nordic/sp1emmc/sp1emmc.c index 662b260dacf..8d312c0f643 100644 --- a/ports/nordic/sp1emmc/sp1emmc.c +++ b/ports/nordic/sp1emmc/sp1emmc.c @@ -835,25 +835,3 @@ uint32_t emmc_bus_hz(void) { // this is a lookup and not arithmetic. Only two values are ever written. return NRF_SPIM3->FREQUENCY == SPIM_FREQ_M32 ? 32000000u : 16000000u; } - -// ============================================================================ -// Deliberately not implemented -// ============================================================================ -// CMD6's dangerous clients (CACHE_CTRL, FLUSH_CACHE, HPI_MGMT, BKOPS AUTO_EN, -// POWER_OFF_NOTIFICATION), CMD35/36/38 TRIM and the abortable HPI machinery -// are absent rather than merely unexposed. Nothing in the read or write path -// needs them, and each one can put the card into a state a block device has no -// way to recover from. -// -// CMD6 itself is compiled in for exactly one volatile byte -- see the -// HS_TIMING block above. That is a hard-coded argument with no caller input, -// not a general SWITCH, so none of the clients listed above become reachable. -// -// Three invariants here look like cleanup targets and are not: the TX frame -// must end at the CRC's last bit, DAT0 must stay an input after a busy -// timeout, and write_data_block() must keep its -Os attribute (only a build -// flag can undo that one, which is why it is stated per-function). -// -// Open question: HPI_FEATURES bit 1 = 0 on this part means JEDEC wants HPI -// signalled via CMD13 rather than CMD12. Nothing here uses HPI. -// ============================================================================ diff --git a/ports/nordic/sp1emmc/sp1emmc_hw.h b/ports/nordic/sp1emmc/sp1emmc_hw.h index 45c9b3932f2..6d63b6da2c0 100644 --- a/ports/nordic/sp1emmc/sp1emmc_hw.h +++ b/ports/nordic/sp1emmc/sp1emmc_hw.h @@ -123,7 +123,7 @@ static inline uint32_t ticks_since_raw(uint32_t t0) { // this file are the only values ever written. // // NRF_SPIM3->FREQUENCY and ->CONFIG are read and written directly: the -// peripheral register IS the state -- no shadow copy to drift, and it survives +// 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. @@ -149,8 +149,7 @@ static inline void sp1emmc_spim_deinit(void) { // 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. That handoff is the -// whole trick and it is proven on this hardware. +// 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 From f42ac6f979933a15b1552d6d1e1af4e2f0ab7bff Mon Sep 17 00:00:00 2001 From: foamyguy Date: Thu, 20 Aug 2026 14:46:36 -0500 Subject: [PATCH 10/14] move wdt, power_off, and sp1emmc into board folder. --- ports/nordic/Makefile | 17 - ports/nordic/background.c | 9 - .../bindings/sp1emmc/EMMC.c | 604 +++++++++++++ .../bindings/sp1emmc/EMMC.h | 41 + .../bindings/sp1emmc/__init__.c | 92 ++ .../boards/teenage_engineering_sp1/board.c | 12 +- .../teenage_engineering_sp1/mpconfigboard.h | 2 +- .../teenage_engineering_sp1/mpconfigboard.mk | 26 +- .../teenage_engineering_sp1}/power_off.c | 2 +- .../teenage_engineering_sp1}/power_off.h | 2 +- .../sp1emmc/automount.c | 117 +++ .../sp1emmc/automount.h | 47 + .../teenage_engineering_sp1/sp1emmc/sp1emmc.c | 837 ++++++++++++++++++ .../teenage_engineering_sp1/sp1emmc/sp1emmc.h | 86 ++ .../sp1emmc/sp1emmc_hw.h | 181 ++++ .../teenage_engineering_sp1}/wdt.h | 21 +- ports/nordic/common-hal/busio/SPI.c | 4 +- ports/nordic/mpconfigport.h | 17 + ports/nordic/peripherals/nrf/nvm.c | 6 +- ports/nordic/supervisor/port.c | 10 +- 20 files changed, 2079 insertions(+), 54 deletions(-) create mode 100644 ports/nordic/boards/teenage_engineering_sp1/bindings/sp1emmc/EMMC.c create mode 100644 ports/nordic/boards/teenage_engineering_sp1/bindings/sp1emmc/EMMC.h create mode 100644 ports/nordic/boards/teenage_engineering_sp1/bindings/sp1emmc/__init__.c rename ports/nordic/{ => boards/teenage_engineering_sp1}/power_off.c (99%) rename ports/nordic/{ => boards/teenage_engineering_sp1}/power_off.h (95%) create mode 100644 ports/nordic/boards/teenage_engineering_sp1/sp1emmc/automount.c create mode 100644 ports/nordic/boards/teenage_engineering_sp1/sp1emmc/automount.h create mode 100644 ports/nordic/boards/teenage_engineering_sp1/sp1emmc/sp1emmc.c create mode 100644 ports/nordic/boards/teenage_engineering_sp1/sp1emmc/sp1emmc.h create mode 100644 ports/nordic/boards/teenage_engineering_sp1/sp1emmc/sp1emmc_hw.h rename ports/nordic/{peripherals/nrf => boards/teenage_engineering_sp1}/wdt.h (65%) diff --git a/ports/nordic/Makefile b/ports/nordic/Makefile index d320a702775..cb2d32f74f8 100755 --- a/ports/nordic/Makefile +++ b/ports/nordic/Makefile @@ -144,29 +144,12 @@ endif SRC_C += \ background.c \ - power_off.c \ boards/$(BOARD)/board.c \ boards/$(BOARD)/pins.c \ device/$(MCU_VARIANT)/startup_$(MCU_SUB_VARIANT).c \ nrfx/mdk/system_$(MCU_SUB_VARIANT).c \ -CIRCUITPY_SP1EMMC ?= 0 -CFLAGS += -DCIRCUITPY_SP1EMMC=$(CIRCUITPY_SP1EMMC) -# USB mass storage automount switch -CIRCUITPY_SP1EMMC_USB ?= 0 -CFLAGS += -DSP1EMMC_AUTOMOUNT=$(CIRCUITPY_SP1EMMC_USB) -ifeq ($(CIRCUITPY_SP1EMMC),1) -SRC_C += \ - bindings/sp1emmc/__init__.c \ - bindings/sp1emmc/EMMC.c \ - sp1emmc/sp1emmc.c \ - -ifeq ($(CIRCUITPY_SP1EMMC_USB),1) -SRC_C += sp1emmc/automount.c -endif -endif - ifneq ($(SD), ) SRC_C += \ bluetooth/ble_drv.c \ diff --git a/ports/nordic/background.c b/ports/nordic/background.c index 28a89bb0d5f..9afade89136 100644 --- a/ports/nordic/background.c +++ b/ports/nordic/background.c @@ -9,9 +9,6 @@ #include "py/runtime.h" #include "supervisor/port.h" -#include "nrf/wdt.h" -#include "power_off.h" - #if CIRCUITPY_DISPLAYIO #include "shared-module/displayio/__init__.h" #endif @@ -44,11 +41,5 @@ MP_WEAK void board_background_task(void) { } void port_background_task(void) { - bootloader_wdt_feed(); - - #ifdef BOARD_POWER_OFF_BUTTON_PIN - power_off_tick(); - #endif - board_background_task(); } 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..e347771908e --- /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(MP_ERROR_TEXT("Buffer length must be a multiple of 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("Block 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: construct EMMC(write_enabled=True) to write")); + } + + 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(MP_ERROR_TEXT("Buffer length must be a multiple of 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("Block 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 index 07e5ff87162..54b0d44cd16 100644 --- a/ports/nordic/boards/teenage_engineering_sp1/board.c +++ b/ports/nordic/boards/teenage_engineering_sp1/board.c @@ -14,9 +14,10 @@ #include "supervisor/board.h" +#include "background.h" #include "power_off.h" #include "py/misc.h" -#include "nrf/wdt.h" +#include "wdt.h" #include "nrfx/hal/nrf_gpio.h" // For the eMMC's supervisor mount: board_reset_pin_defaults() leaves @@ -195,6 +196,15 @@ void board_init(void) { 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 diff --git a/ports/nordic/boards/teenage_engineering_sp1/mpconfigboard.h b/ports/nordic/boards/teenage_engineering_sp1/mpconfigboard.h index 3ab2914b929..cdde601b076 100644 --- a/ports/nordic/boards/teenage_engineering_sp1/mpconfigboard.h +++ b/ports/nordic/boards/teenage_engineering_sp1/mpconfigboard.h @@ -54,7 +54,7 @@ #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 peripherals/nrf/wdt.h. +// 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 diff --git a/ports/nordic/boards/teenage_engineering_sp1/mpconfigboard.mk b/ports/nordic/boards/teenage_engineering_sp1/mpconfigboard.mk index 14c9e647057..a3bd93b6542 100644 --- a/ports/nordic/boards/teenage_engineering_sp1/mpconfigboard.mk +++ b/ports/nordic/boards/teenage_engineering_sp1/mpconfigboard.mk @@ -57,12 +57,32 @@ CIRCUITPY_ALARM = 0 CIRCUITPY_AUDIOPWMIO = 0 CIRCUITPY_SYNTHIO = 1 CIRCUITPY_AUDIOEFFECTS = 1 -# audiomp3 would otherwise come along for free with audiocore on a full build. -# but doesn't reliably decode mp3s off of emmc -CIRCUITPY_AUDIOMP3 = 0 +CIRCUITPY_AUDIOMP3 = 1 + +# The eMMC driver, its `sp1emmc` module, and the hold-to-power-off gesture are +# all SP-1 hardware, boards/$(BOARD) is already on the include path, so their +# "sp1emmc/..." and "bindings/sp1emmc/..." includes resolve as written. + +# 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/power_off.c b/ports/nordic/boards/teenage_engineering_sp1/power_off.c similarity index 99% rename from ports/nordic/power_off.c rename to ports/nordic/boards/teenage_engineering_sp1/power_off.c index 920b1234a0f..7131073c63e 100644 --- a/ports/nordic/power_off.c +++ b/ports/nordic/boards/teenage_engineering_sp1/power_off.c @@ -12,7 +12,7 @@ #include "supervisor/flash.h" -#include "nrf/wdt.h" +#include "wdt.h" #include "nrfx/hal/nrf_gpio.h" #include "nrfx/hal/nrf_power.h" diff --git a/ports/nordic/power_off.h b/ports/nordic/boards/teenage_engineering_sp1/power_off.h similarity index 95% rename from ports/nordic/power_off.h rename to ports/nordic/boards/teenage_engineering_sp1/power_off.h index 63fa8e981ad..c99be1c44a3 100644 --- a/ports/nordic/power_off.h +++ b/ports/nordic/boards/teenage_engineering_sp1/power_off.h @@ -31,7 +31,7 @@ #ifdef BOARD_POWER_OFF_BUTTON_PIN // Poll the button and, if it has been held long enough, power off (never -// returns). Called from port_background_task(), i.e. from every +// returns). Called from the board's board_background_task(), i.e. from every // RUN_BACKGROUND_TASKS. void power_off_tick(void); 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/peripherals/nrf/wdt.h b/ports/nordic/boards/teenage_engineering_sp1/wdt.h similarity index 65% rename from ports/nordic/peripherals/nrf/wdt.h rename to ports/nordic/boards/teenage_engineering_sp1/wdt.h index df7c838f8e1..08e7f501662 100644 --- a/ports/nordic/peripherals/nrf/wdt.h +++ b/ports/nordic/boards/teenage_engineering_sp1/wdt.h @@ -6,7 +6,7 @@ #pragma once -// Support for a watchdog that was started by the board's bootloader, before +// 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 @@ -14,10 +14,11 @@ // registers (CRV, RREN, CONFIG) are locked, so the only thing the application // can do is reload it. // -// A board opts in with CIRCUITPY_BOOTLOADER_ARMED_WDT in its mpconfigboard.h, -// and should normally also set CIRCUITPY_WATCHDOG = 0, since -// common_hal_watchdog_set_mode() would call nrfx_wdt_init() on the running -// peripheral, where the timeout write is silently ignored. +// The board opts in with CIRCUITPY_BOOTLOADER_ARMED_WDT in its +// mpconfigboard.h. That flag, and the safe-mode policy derived from it, +// default to off for every other board in the port's mpconfigport.h, which is +// what keeps the generic feed sites (supervisor/port.c, +// peripherals/nrf/nvm.c) compiling without this header. // 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 @@ -26,16 +27,6 @@ #include "nrfx.h" -#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). -#ifndef CIRCUITPY_SAFE_MODE_ON_WATCHDOG_REQUIRES_USB -#define CIRCUITPY_SAFE_MODE_ON_WATCHDOG_REQUIRES_USB (!CIRCUITPY_BOOTLOADER_ARMED_WDT) -#endif - // Value that a reload request register must be written with, per the nRF52 // product specification. #define NRF_WDT_RELOAD_REQUEST_VALUE (0x6E524635UL) diff --git a/ports/nordic/common-hal/busio/SPI.c b/ports/nordic/common-hal/busio/SPI.c index 7fb535b0d3e..e75c5c66199 100644 --- a/ports/nordic/common-hal/busio/SPI.c +++ b/ports/nordic/common-hal/busio/SPI.c @@ -14,7 +14,7 @@ #include "nrfx_spim.h" #include "nrf_gpio.h" -#if CIRCUITPY_SP1EMMC +#if defined(CIRCUITPY_SP1EMMC) && CIRCUITPY_SP1EMMC #include "bindings/sp1emmc/EMMC.h" #endif @@ -126,7 +126,7 @@ 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 CIRCUITPY_SP1EMMC + #if defined(CIRCUITPY_SP1EMMC) && CIRCUITPY_SP1EMMC if (spim_peripherals[i].spim.p_reg == NRF_SPIM3 && sp1emmc_spim3_in_use()) { continue; } diff --git a/ports/nordic/mpconfigport.h b/ports/nordic/mpconfigport.h index 7f6b8ccc7f1..c0654587688 100644 --- a/ports/nordic/mpconfigport.h +++ b/ports/nordic/mpconfigport.h @@ -179,6 +179,23 @@ #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 diff --git a/ports/nordic/peripherals/nrf/nvm.c b/ports/nordic/peripherals/nrf/nvm.c index 865f21aabfd..74ef2529239 100644 --- a/ports/nordic/peripherals/nrf/nvm.c +++ b/ports/nordic/peripherals/nrf/nvm.c @@ -12,7 +12,9 @@ #include "nrfx_nvmc.h" -#include "nrf/wdt.h" +#if CIRCUITPY_BOOTLOADER_ARMED_WDT +#include "wdt.h" +#endif #include "supervisor/shared/safe_mode.h" #define FLASH_PAGE_SIZE (4096) @@ -163,8 +165,10 @@ 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); diff --git a/ports/nordic/supervisor/port.c b/ports/nordic/supervisor/port.c index 940b67b5835..b909f983d62 100644 --- a/ports/nordic/supervisor/port.c +++ b/ports/nordic/supervisor/port.c @@ -22,9 +22,11 @@ #include "nrf/nvm.h" #include "nrf/power.h" #include "nrf/timers.h" -#include "nrf/wdt.h" +#if CIRCUITPY_BOOTLOADER_ARMED_WDT +#include "wdt.h" +#endif -#if CIRCUITPY_SP1EMMC +#if defined(CIRCUITPY_SP1EMMC) && CIRCUITPY_SP1EMMC #include "bindings/sp1emmc/EMMC.h" #endif @@ -154,8 +156,10 @@ 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. @@ -244,7 +248,7 @@ void reset_port(void) { rtc_reset(); #endif - #if CIRCUITPY_SP1EMMC + #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(); From 44144acf0670921f5efd7be87a945f8898160733 Mon Sep 17 00:00:00 2001 From: foamyguy Date: Thu, 20 Aug 2026 16:32:17 -0500 Subject: [PATCH 11/14] cleanup comments --- ports/nordic/boards/teenage_engineering_sp1/mpconfigboard.mk | 4 ---- ports/nordic/boards/teenage_engineering_sp1/wdt.h | 5 +---- 2 files changed, 1 insertion(+), 8 deletions(-) diff --git a/ports/nordic/boards/teenage_engineering_sp1/mpconfigboard.mk b/ports/nordic/boards/teenage_engineering_sp1/mpconfigboard.mk index a3bd93b6542..9bfe06fce6e 100644 --- a/ports/nordic/boards/teenage_engineering_sp1/mpconfigboard.mk +++ b/ports/nordic/boards/teenage_engineering_sp1/mpconfigboard.mk @@ -59,10 +59,6 @@ CIRCUITPY_SYNTHIO = 1 CIRCUITPY_AUDIOEFFECTS = 1 CIRCUITPY_AUDIOMP3 = 1 -# The eMMC driver, its `sp1emmc` module, and the hold-to-power-off gesture are -# all SP-1 hardware, boards/$(BOARD) is already on the include path, so their -# "sp1emmc/..." and "bindings/sp1emmc/..." includes resolve as written. - # Hold-to-power-off, opted into by BOARD_POWER_OFF_BUTTON_PIN in # mpconfigboard.h. SRC_C += boards/$(BOARD)/power_off.c diff --git a/ports/nordic/boards/teenage_engineering_sp1/wdt.h b/ports/nordic/boards/teenage_engineering_sp1/wdt.h index 08e7f501662..31c0a338585 100644 --- a/ports/nordic/boards/teenage_engineering_sp1/wdt.h +++ b/ports/nordic/boards/teenage_engineering_sp1/wdt.h @@ -15,10 +15,7 @@ // can do is reload it. // // The board opts in with CIRCUITPY_BOOTLOADER_ARMED_WDT in its -// mpconfigboard.h. That flag, and the safe-mode policy derived from it, -// default to off for every other board in the port's mpconfigport.h, which is -// what keeps the generic feed sites (supervisor/port.c, -// peripherals/nrf/nvm.c) compiling without this header. +// 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 From ef39750a318557ef8114593b436ad807c225ddfc Mon Sep 17 00:00:00 2001 From: foamyguy Date: Thu, 20 Aug 2026 16:50:45 -0500 Subject: [PATCH 12/14] remove old sp1emmc bindings and implementation. --- ports/nordic/bindings/sp1emmc/EMMC.c | 604 ---------------- ports/nordic/bindings/sp1emmc/EMMC.h | 41 -- ports/nordic/bindings/sp1emmc/__init__.c | 92 --- ports/nordic/sp1emmc/automount.c | 117 ---- ports/nordic/sp1emmc/automount.h | 47 -- ports/nordic/sp1emmc/sp1emmc.c | 837 ----------------------- ports/nordic/sp1emmc/sp1emmc.h | 86 --- ports/nordic/sp1emmc/sp1emmc_hw.h | 181 ----- 8 files changed, 2005 deletions(-) delete mode 100644 ports/nordic/bindings/sp1emmc/EMMC.c delete mode 100644 ports/nordic/bindings/sp1emmc/EMMC.h delete mode 100644 ports/nordic/bindings/sp1emmc/__init__.c delete mode 100644 ports/nordic/sp1emmc/automount.c delete mode 100644 ports/nordic/sp1emmc/automount.h delete mode 100644 ports/nordic/sp1emmc/sp1emmc.c delete mode 100644 ports/nordic/sp1emmc/sp1emmc.h delete mode 100644 ports/nordic/sp1emmc/sp1emmc_hw.h diff --git a/ports/nordic/bindings/sp1emmc/EMMC.c b/ports/nordic/bindings/sp1emmc/EMMC.c deleted file mode 100644 index e347771908e..00000000000 --- a/ports/nordic/bindings/sp1emmc/EMMC.c +++ /dev/null @@ -1,604 +0,0 @@ -// 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(MP_ERROR_TEXT("Buffer length must be a multiple of 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("Block 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: construct EMMC(write_enabled=True) to write")); - } - - 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(MP_ERROR_TEXT("Buffer length must be a multiple of 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("Block 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/bindings/sp1emmc/EMMC.h b/ports/nordic/bindings/sp1emmc/EMMC.h deleted file mode 100644 index e303af55240..00000000000 --- a/ports/nordic/bindings/sp1emmc/EMMC.h +++ /dev/null @@ -1,41 +0,0 @@ -// 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/bindings/sp1emmc/__init__.c b/ports/nordic/bindings/sp1emmc/__init__.c deleted file mode 100644 index c794e012637..00000000000 --- a/ports/nordic/bindings/sp1emmc/__init__.c +++ /dev/null @@ -1,92 +0,0 @@ -// 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/sp1emmc/automount.c b/ports/nordic/sp1emmc/automount.c deleted file mode 100644 index 4298978cdbd..00000000000 --- a/ports/nordic/sp1emmc/automount.c +++ /dev/null @@ -1,117 +0,0 @@ -// 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/sp1emmc/automount.h b/ports/nordic/sp1emmc/automount.h deleted file mode 100644 index faa9800d697..00000000000 --- a/ports/nordic/sp1emmc/automount.h +++ /dev/null @@ -1,47 +0,0 @@ -// 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/sp1emmc/sp1emmc.c b/ports/nordic/sp1emmc/sp1emmc.c deleted file mode 100644 index 8d312c0f643..00000000000 --- a/ports/nordic/sp1emmc/sp1emmc.c +++ /dev/null @@ -1,837 +0,0 @@ -// 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 "nrf/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/sp1emmc/sp1emmc.h b/ports/nordic/sp1emmc/sp1emmc.h deleted file mode 100644 index 2891c70d58f..00000000000 --- a/ports/nordic/sp1emmc/sp1emmc.h +++ /dev/null @@ -1,86 +0,0 @@ -// 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/sp1emmc/sp1emmc_hw.h b/ports/nordic/sp1emmc/sp1emmc_hw.h deleted file mode 100644 index 6d63b6da2c0..00000000000 --- a/ports/nordic/sp1emmc/sp1emmc_hw.h +++ /dev/null @@ -1,181 +0,0 @@ -// 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; -} From 0a2aa8cb5ea373c7a0438f23da1b8097ddde3998 Mon Sep 17 00:00:00 2001 From: foamyguy Date: Fri, 21 Aug 2026 09:41:22 -0500 Subject: [PATCH 13/14] re-use error messages, update translations --- locale/circuitpython.pot | 47 ++++++++----------- .../bindings/sp1emmc/EMMC.c | 10 ++-- 2 files changed, 24 insertions(+), 33 deletions(-) diff --git a/locale/circuitpython.pot b/locale/circuitpython.pot index 1519fbd2b15..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 @@ -1703,44 +1704,45 @@ msgid "" "Frequency must be 24, 150, 396, 450, 528, 600, 720, 816, 912, 960 or 1008 Mhz" msgstr "" -#: ports/nordic/bindings/sp1emmc/EMMC.c +#: ports/nordic/boards/aramcon2_badge/mpconfigboard.h +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/bindings/sp1emmc/EMMC.c +#: ports/nordic/boards/teenage_engineering_sp1/bindings/sp1emmc/EMMC.c msgid "eMMC already in use" msgstr "" -#: ports/nordic/bindings/sp1emmc/EMMC.c +#: 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/bindings/sp1emmc/EMMC.c +#: ports/nordic/boards/teenage_engineering_sp1/bindings/sp1emmc/EMMC.c #, c-format msgid "eMMC high-speed switch failed at %s" msgstr "" -#: ports/nordic/bindings/sp1emmc/EMMC.c +#: ports/nordic/boards/teenage_engineering_sp1/bindings/sp1emmc/EMMC.c #, c-format msgid "eMMC init failed at %s" msgstr "" -#: ports/nordic/bindings/sp1emmc/EMMC.c -msgid "Buffer length must be a multiple of 512" -msgstr "" - -#: ports/nordic/bindings/sp1emmc/EMMC.c -msgid "Block address out of range" -msgstr "" - -#: ports/nordic/bindings/sp1emmc/EMMC.c -msgid "Read-only: construct EMMC(write_enabled=True) to write" +#: ports/nordic/boards/teenage_engineering_sp1/bindings/sp1emmc/EMMC.c +#: shared-bindings/i2cioexpander/IOExpander.c +msgid "address out of range" msgstr "" -#: ports/nordic/boards/aramcon2_badge/mpconfigboard.h -msgid "You pressed the left button at start up." +#: 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 @@ -3645,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 "" @@ -4044,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/boards/teenage_engineering_sp1/bindings/sp1emmc/EMMC.c b/ports/nordic/boards/teenage_engineering_sp1/bindings/sp1emmc/EMMC.c index e347771908e..eb6223ea834 100644 --- a/ports/nordic/boards/teenage_engineering_sp1/bindings/sp1emmc/EMMC.c +++ b/ports/nordic/boards/teenage_engineering_sp1/bindings/sp1emmc/EMMC.c @@ -338,13 +338,13 @@ static mp_obj_t sp1emmc_emmc_readblocks(mp_obj_t self_in, mp_obj_t start_in, mp_ 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(MP_ERROR_TEXT("Buffer length must be a multiple of 512")); + 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("Block address out of range")); + mp_raise_ValueError(MP_ERROR_TEXT("address out of range")); } int err = emmc_read_chunked(bufinfo.buf, start, count, true); @@ -397,19 +397,19 @@ static mp_obj_t sp1emmc_emmc_writeblocks(mp_obj_t self_in, mp_obj_t start_in, mp check_for_deinit(self); if (!self->write_enabled) { mp_raise_msg(&mp_type_RuntimeError, - MP_ERROR_TEXT("Read-only: construct EMMC(write_enabled=True) to write")); + 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(MP_ERROR_TEXT("Buffer length must be a multiple of 512")); + 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("Block address out of range")); + mp_raise_ValueError(MP_ERROR_TEXT("address out of range")); } int err = emmc_write_chunked(bufinfo.buf, start, count, true); From d53931f472daf2bdd66e5812140cfdd517f44466 Mon Sep 17 00:00:00 2001 From: foamyguy Date: Fri, 21 Aug 2026 10:32:31 -0500 Subject: [PATCH 14/14] match I2S pin names from other devices --- ports/nordic/boards/teenage_engineering_sp1/pins.c | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/ports/nordic/boards/teenage_engineering_sp1/pins.c b/ports/nordic/boards/teenage_engineering_sp1/pins.c index a21d2f4fefa..66897f7514d 100644 --- a/ports/nordic/boards/teenage_engineering_sp1/pins.c +++ b/ports/nordic/boards/teenage_engineering_sp1/pins.c @@ -59,9 +59,11 @@ static const mp_rom_map_elem_t board_module_globals_table[] = { { MP_ROM_QSTR(MP_QSTR_CS42_RESET), MP_ROM_PTR(&pin_P0_15) }, // I2S - { MP_ROM_QSTR(MP_QSTR_I2S_DATA), MP_ROM_PTR(&pin_P1_09) }, + { 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