From 68a7f4967fbb5627deb67a254fa44b8302e31622 Mon Sep 17 00:00:00 2001 From: foamyguy Date: Thu, 27 Aug 2026 16:22:58 -0500 Subject: [PATCH 1/5] sp1 board def, move as much as possible into board specific dir --- ports/nordic/board.h | 10 + .../boards/teenage_engineering_sp1/board.c | 478 ++++++++++++++++++ .../teenage_engineering_sp1/flash_protect.c | 52 ++ .../teenage_engineering_sp1/flash_protect.h | 11 + .../teenage_engineering_sp1/mpconfigboard.h | 71 +++ .../teenage_engineering_sp1/mpconfigboard.mk | 51 ++ .../boards/teenage_engineering_sp1/pins.c | 91 ++++ .../teenage_engineering_sp1/power_off.c | 172 +++++++ .../teenage_engineering_sp1/power_off.h | 43 ++ .../boards/teenage_engineering_sp1/wdt.h | 40 ++ ports/nordic/common-hal/microcontroller/Pin.c | 11 + ports/nordic/common-hal/microcontroller/Pin.h | 5 + ports/nordic/mpconfigport.h | 27 + ports/nordic/supervisor/port.c | 21 +- 14 files changed, 1079 insertions(+), 4 deletions(-) create mode 100644 ports/nordic/board.h create mode 100644 ports/nordic/boards/teenage_engineering_sp1/board.c create mode 100644 ports/nordic/boards/teenage_engineering_sp1/flash_protect.c create mode 100644 ports/nordic/boards/teenage_engineering_sp1/flash_protect.h 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/boards/teenage_engineering_sp1/power_off.c create mode 100644 ports/nordic/boards/teenage_engineering_sp1/power_off.h create mode 100644 ports/nordic/boards/teenage_engineering_sp1/wdt.h diff --git a/ports/nordic/board.h b/ports/nordic/board.h new file mode 100644 index 00000000000..560cafc6da8 --- /dev/null +++ b/ports/nordic/board.h @@ -0,0 +1,10 @@ +// 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 + +// Called from port_init() before any peripheral is initialized. +void board_early_init(void); 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..ede8e0d523e --- /dev/null +++ b/ports/nordic/boards/teenage_engineering_sp1/board.c @@ -0,0 +1,478 @@ +// This file is part of the CircuitPython project: https://circuitpython.org +// +// SPDX-FileCopyrightText: Copyright (c) 2026 Tim Cocks for Adafruit Industries +// +// SPDX-License-Identifier: MIT + +// Early-boot hygiene for the Teenage Engineering SP-1. +// +// Unlike a normal CircuitPython board, this one is entered from a bootloader +// that has already brought hardware up: it starts HFCLK and LFCLK, and leaves +// PWM2, PWM3 and the SAADC enabled. It also starts a watchdog that we cannot +// stop. So the app has to take the machine over from a *running* state rather +// than a reset state, and it has to do so quickly. +// +// The bootloader's watchdog is the reason for the feeds scattered through this +// file: it is running before we are, its configuration is locked, and nothing +// but a reload keeps it from resetting the board. See wdt.h. + +#include "supervisor/board.h" + +#include "background.h" +#include "board.h" +#include "common-hal/microcontroller/Pin.h" +#include "flash_protect.h" +#include "power_off.h" +#include "py/misc.h" +#include "supervisor/shared/safe_mode.h" +#include "wdt.h" +#include "nrfx/drivers/include/nrfx_rtc.h" +#include "nrfx/hal/nrf_gpio.h" + +// Pins this file drives directly. +#define PIN_EMMC_RESET NRF_GPIO_PIN_MAP(1, 8) // eMMC, active low +#define PIN_EMMC_VCCQ_EN NRF_GPIO_PIN_MAP(0, 14) // eMMC I/O rail +#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_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 uint8_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; + +// The bootloader's watchdog is fed from board_background_task(), so the CPU +// must not stay in WFI for longer than the bootloader will wait. The port owns +// RTC2 and this board builds no SoftDevice (which would own RTC0), so RTC1 is +// free to hand the main loop its turn back on a fixed period. +// +// Waking is the whole job. The feed itself deliberately stays in the main loop, +// so that a wedge there still becomes a reset. +static const nrfx_rtc_t wake_rtc = NRFX_RTC_INSTANCE(1); +#define WAKE_RTC_CHANNEL (0) +// Run the counter straight off the LFCLK, as the port's own RTC does. +#define WAKE_RTC_FREQUENCY_HZ (32768) +// How long the CPU may stay in WFI before the main loop must get another look +// in. +#define WAKE_RTC_PERIOD_TICKS (WAKE_RTC_FREQUENCY_HZ) + +static void arm_wake_rtc(void) { + nrfx_rtc_cc_set(&wake_rtc, WAKE_RTC_CHANNEL, + nrfx_rtc_counter_get(&wake_rtc) + WAKE_RTC_PERIOD_TICKS, true); +} + +static void wake_rtc_handler(nrfx_rtc_int_type_t int_type) { + (void)int_type; + arm_wake_rtc(); +} + +// 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) { + // Feed the bootloader's watchdog before anything else. This is the first + // CircuitPython code to run on the board: port_init() calls it before it + // touches a peripheral. + bootloader_wdt_feed(); + + // Lock the bootloader out of NVMC's reach for the rest of this boot. First, + // because from here on every line of CircuitPython that runs is one more + // thing that could get it wrong, and the bootloader is this board's only + // way back in. See flash_protect.h. + board_flash_protect(); + + // A watchdog reset can only mean the main loop stopped, and on this board + // that is worth safe mode with or without a host attached: there is no + // reset pin and no removable battery, so re-running the same wedging + // `code.py` is the one thing that can make the device unrecoverable. + // port_init() asks for safe mode itself when USB is attached; this covers + // the battery case. RESETREAS is still untouched here -- port_init() reads + // and clears it after we return -- and the request is picked up by + // wait_for_safe_mode_reset() later in this same boot, not after a reset. + if ((NRF_POWER->RESETREAS & POWER_RESETREAS_DOG_Msk) != 0 && + (NRF_POWER->USBREGSTATUS & POWER_USBREGSTATUS_VBUSDETECT_Msk) == 0) { + safe_mode_on_next_reset(SAFE_MODE_WATCHDOG); + } + + // 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; + } + + // Last, because the blanket NVIC clear above would undo it: the periodic + // wake-up that keeps the main loop feeding the watchdog. LFCLK is not + // running yet, so the counter starts when port_init() starts it, a few + // instructions from here. + static const nrfx_rtc_config_t wake_rtc_config = { + .prescaler = RTC_FREQ_TO_PRESCALER(WAKE_RTC_FREQUENCY_HZ), + .reliable = 0, + .tick_latency = 0, + .interrupt_priority = 6, + }; + nrfx_rtc_init(&wake_rtc, &wake_rtc_config, wake_rtc_handler); + arm_wake_rtc(); + nrfx_rtc_enable(&wake_rtc); +} + +// Pins that must not float. reset_all_pins() and reset_pin_number() ask the +// board for each pin, so this configuration is re-applied after every reset +// rather than the pin being left in its default (disconnected) state. +// +// None of these are marked never-reset, so Python can still claim them. This +// only makes the resting state between runs a defined, safe one. +static const uint8_t default_low_pins[] = { + // 3.072 MHz oscillator enable. Held low: it draws current straight through + // SYSTEM_OFF, so a floating pin here would drain battery. + 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; UICR NFCPINS reads with PROTECT + // already cleared on this board, so they are usable as GPIO. + PIN_TAS_RESET, + PIN_CS42_RESET, + PIN_BT_RESET, + + // eMMC held in reset with its VCCQ rail off. The contents of the chip are + // unaffected; this only keeps the rail from floating. + PIN_EMMC_RESET, + PIN_EMMC_VCCQ_EN, + + // Rail feeding the faders and both button ladders. Off unless something is + // actually reading them. + PIN_CONTROL_RAIL, + + // BQ24232 charge enable, active low: drive it low so a plugged-in device + // charges. P1.00 (CHARGE_ISET) is deliberately left untouched. It is the + // charge-current programming node (ICHG = 870 AΩ / RISET) and doubles as + // the current monitor. + PIN_CHARGE_ENABLE, +}; + +// Returns false for a pin this board has no opinion about. +static bool apply_pin_default(uint8_t pin_number) { + // 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. + if (pin_number == PIN_FUNCTION_BUTTON) { + nrf_gpio_cfg_input(PIN_FUNCTION_BUTTON, NRF_GPIO_PIN_PULLUP); + return true; + } + + // Both LED rows, off. + for (size_t i = 0; i < MP_ARRAY_SIZE(led_pins); i++) { + if (led_pins[i] == pin_number) { + nrf_gpio_cfg_output(pin_number); + nrf_gpio_pin_clear(pin_number); + return true; + } + } + + for (size_t i = 0; i < MP_ARRAY_SIZE(default_low_pins); i++) { + if (default_low_pins[i] == pin_number) { + nrf_gpio_cfg_output(pin_number); + nrf_gpio_pin_clear(pin_number); + return true; + } + } + + return false; +} + +// Put every pin this board has an opinion about into its resting state at once. +static void apply_all_pin_defaults(void) { + apply_pin_default(PIN_FUNCTION_BUTTON); + for (size_t i = 0; i < MP_ARRAY_SIZE(led_pins); i++) { + apply_pin_default(led_pins[i]); + } + for (size_t i = 0; i < MP_ARRAY_SIZE(default_low_pins); i++) { + apply_pin_default(default_low_pins[i]); + } +} + +bool board_reset_pin_number(uint8_t pin_number) { + // main() calls reset_all_pins() immediately after port_init(), so without + // this the boot up heartbeat blink would last microseconds and show + // nothing. board_init() hands the pin back. + if (heartbeat_lit && pin_number == PIN_LED_HEARTBEAT) { + return true; + } + + return apply_pin_default(pin_number); +} + +// Called once at start up, after the filesystem is mounted and immediately +// before the USB workflow starts. Where we end the heartbeat status blink. +void board_init(void) { + heartbeat_lit = false; + nrf_gpio_pin_clear(PIN_LED_HEARTBEAT); +} + +void board_background_task(void) { + bootloader_wdt_feed(); + + #ifdef BOARD_POWER_OFF_BUTTON_PIN + // Never returns if the hold completes. + power_off_tick(); + #endif +} + +// -- muting the codecs on the way out -------------------------------------- +// +// Dropping the reset lines and the oscillator (apply_all_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. + apply_all_pin_defaults(); +} diff --git a/ports/nordic/boards/teenage_engineering_sp1/flash_protect.c b/ports/nordic/boards/teenage_engineering_sp1/flash_protect.c new file mode 100644 index 00000000000..db87935dbb9 --- /dev/null +++ b/ports/nordic/boards/teenage_engineering_sp1/flash_protect.c @@ -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 + +#include "flash_protect.h" + +#include "py/mpconfig.h" + +#include "nrfx/hal/nrf_acl.h" + +// protect the MBR at 0x0 and the bootloader above it, up to the +// address the bootloader jumps to. +#define PROTECT_START_ADDR (MBR_START_ADDR) +#define PROTECT_SIZE (ISR_START_ADDR - MBR_START_ADDR) + +#if PROTECT_SIZE == 0 +#error The bootloader for this board is not in low flash; nothing for the ACL to protect. +#endif + +#if PROTECT_SIZE > NRF_ACL_REGION_SIZE_MAX +#error The bootloader region is larger than a single ACL region can cover. +#endif + +#if (PROTECT_START_ADDR % FLASH_PAGE_SIZE) != 0 || (PROTECT_SIZE % FLASH_PAGE_SIZE) != 0 +#error ACL regions must start and end on a flash page boundary. +#endif + +void board_flash_protect(void) { + for (uint32_t region = 0; region < ACL_REGIONS_COUNT; region++) { + // A region's registers only take their first write after a reset, so a + // region the bootloader has already configured has to be left alone. + if (nrf_acl_region_size_get(NRF_ACL, region) != 0 || + nrf_acl_region_perm_get(NRF_ACL, region) != 0) { + continue; + } + + // Read stays enabled. The MBR's vector table is read out of this + // region on every interrupt, and the bootloader is executed in place + // out of it on the way back in. + nrf_acl_region_set(NRF_ACL, region, PROTECT_START_ADDR, PROTECT_SIZE, + NRF_ACL_PERM_READ_NO_WRITE); + + // Read back rather than trust the write: an already-claimed region + // would have ignored it. + if (nrf_acl_region_address_get(NRF_ACL, region) == PROTECT_START_ADDR && + nrf_acl_region_size_get(NRF_ACL, region) == PROTECT_SIZE) { + return; + } + } +} diff --git a/ports/nordic/boards/teenage_engineering_sp1/flash_protect.h b/ports/nordic/boards/teenage_engineering_sp1/flash_protect.h new file mode 100644 index 00000000000..4a21bfb2758 --- /dev/null +++ b/ports/nordic/boards/teenage_engineering_sp1/flash_protect.h @@ -0,0 +1,11 @@ +// 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 + +// Hardware write-protection for this board's bootloader. + +void board_flash_protect(void); 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..703c8f9744b --- /dev/null +++ b/ports/nordic/boards/teenage_engineering_sp1/mpconfigboard.h @@ -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 + +#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 BLE on this board, so nothing to store for bonding. +#define CIRCUITPY_BLE_CONFIG_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) + +// 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..5f84a3031bb --- /dev/null +++ b/ports/nordic/boards/teenage_engineering_sp1/mpconfigboard.mk @@ -0,0 +1,51 @@ +USB_VID = 0x239A +USB_PID = 0x817A +USB_PRODUCT = "SP-1 (CircuitPython)" +USB_MANUFACTURER = "Teenage Engineering" + +MCU_CHIP = nrf52840 + +# SystemInit() burns UICR PSELRESET[0..1] = 18 and resets whenever it finds +# them unprogrammed, which is the case on this board. Opt out of it +$(BUILD)/nrfx/mdk/system_nrf52840.o: CFLAGS += -UCONFIG_GPIO_AS_PINRESET + +# No BLE. A SoftDevice would have to live at 0x1000, which is inside this +# board's bootloader, and the radio has no antenna. +CIRCUITPY_BLEIO_NATIVE = 0 +CIRCUITPY_BLE_FILE_SERVICE = 0 +CIRCUITPY_BLE_SERIAL_SERVICE = 0 + +# CIRCUITPY is in internal flash; there is no external flash chip. +INTERNAL_FLASH_FILESYSTEM = 1 + +# No UF2 bootloader on this device. +CIRCUITPY_BUILD_EXTENSIONS = bin,hex + +# displayio off no display. +CIRCUITPY_DISPLAYIO = 0 +CIRCUITPY_FRAMEBUFFERIO = 0 +CIRCUITPY_RGBMATRIX = 0 +CIRCUITPY_SHARPDISPLAY = 0 +CIRCUITPY_IS31FL3741 = 0 +CIRCUITPY_VECTORIO = 0 + +# The watchdog is started by the bootloader before our first instruction and +# its config registers are locked. +CIRCUITPY_WATCHDOG = 0 + +# alarm's idle paths need a WDT-feed audit before they are safe here. +CIRCUITPY_ALARM = 0 + +# Audio +CIRCUITPY_AUDIOPWMIO = 0 +CIRCUITPY_SYNTHIO = 1 +CIRCUITPY_AUDIOEFFECTS = 1 +CIRCUITPY_AUDIOMP3 = 1 + +# Hold-to-power-off, opted into by BOARD_POWER_OFF_BUTTON_PIN in +# mpconfigboard.h. +SRC_C += boards/$(BOARD)/power_off.c + +# ACL write-protection for the MBR and the bootloader, which this board's +# bootloader does not set up for itself. +SRC_C += boards/$(BOARD)/flash_protect.c diff --git a/ports/nordic/boards/teenage_engineering_sp1/pins.c b/ports/nordic/boards/teenage_engineering_sp1/pins.c new file mode 100644 index 00000000000..66897f7514d --- /dev/null +++ b/ports/nordic/boards/teenage_engineering_sp1/pins.c @@ -0,0 +1,91 @@ +// This file is part of the CircuitPython project: https://circuitpython.org +// +// SPDX-FileCopyrightText: Copyright (c) 2026 Tim Cocks for Adafruit Industries +// +// SPDX-License-Identifier: MIT + +// Pin map from Tim Knapen's reverse-engineering of the Teenage Engineering SP-1 +// https://github.com/timknapen/SP-1-dev/wiki + +#include "shared-bindings/board/__init__.h" + +static const mp_rom_map_elem_t board_module_globals_table[] = { + CIRCUITPYTHON_BOARD_DICT_STANDARD_ITEMS + + // Four track faders, in physical left-to-right order. + { MP_ROM_QSTR(MP_QSTR_FADER1), MP_ROM_PTR(&pin_P0_05) }, + { MP_ROM_QSTR(MP_QSTR_FADER2), MP_ROM_PTR(&pin_P0_30) }, + { MP_ROM_QSTR(MP_QSTR_FADER3), MP_ROM_PTR(&pin_P0_04) }, + { MP_ROM_QSTR(MP_QSTR_FADER4), MP_ROM_PTR(&pin_P0_31) }, + + // Two resistor-ladder button rows, read as analog voltages. Both need + // LADDER_POWER driven high to read anything; the faders need it too. + // + // Rungs in ascending voltage: + // LADDER1: TRACK1, TRACK2, TRACK3, TRACK4, PLAY + // LADDER2: ROCKER-, VOL-, ROCKER+, VOL+ + // Both rows are the same resistor network; LADDER2 is LADDER1 with the + // lowest rung unpopulated, so one threshold table serves both. + // + // "ROCKER" is the left-side rocker switch + { MP_ROM_QSTR(MP_QSTR_LADDER1), MP_ROM_PTR(&pin_P0_02) }, + { MP_ROM_QSTR(MP_QSTR_LADDER2), MP_ROM_PTR(&pin_P0_03) }, + { MP_ROM_QSTR(MP_QSTR_LADDER_POWER), MP_ROM_PTR(&pin_P1_10) }, + + // The Function button ("••") is the only GPIO button, active low with a + // pull-up, and the only wake source out of SYSTEM_OFF. + { MP_ROM_QSTR(MP_QSTR_BUTTON), MP_ROM_PTR(&pin_P0_27) }, + + // Playback LED row on the side of the device, active high. + { MP_ROM_QSTR(MP_QSTR_LED_PLAY1), MP_ROM_PTR(&pin_P1_13) }, + { MP_ROM_QSTR(MP_QSTR_LED_PLAY2), MP_ROM_PTR(&pin_P0_00) }, + { MP_ROM_QSTR(MP_QSTR_LED_PLAY3), MP_ROM_PTR(&pin_P1_12) }, + { MP_ROM_QSTR(MP_QSTR_LED_PLAY4), MP_ROM_PTR(&pin_P0_01) }, + + // Track LED row above the track buttons, active high. + { MP_ROM_QSTR(MP_QSTR_LED_TRACK1), MP_ROM_PTR(&pin_P0_29) }, + { MP_ROM_QSTR(MP_QSTR_LED_TRACK2), MP_ROM_PTR(&pin_P0_26) }, + { MP_ROM_QSTR(MP_QSTR_LED_TRACK3), MP_ROM_PTR(&pin_P1_15) }, + { MP_ROM_QSTR(MP_QSTR_LED_TRACK4), MP_ROM_PTR(&pin_P1_14) }, + + // I2C to both codecs: CS42L42 headphone amp at 0x48, TAS2505 speaker amp + // at 0x18. + { MP_ROM_QSTR(MP_QSTR_SCL), MP_ROM_PTR(&pin_P1_11) }, + { MP_ROM_QSTR(MP_QSTR_SDA), MP_ROM_PTR(&pin_P1_07) }, + { MP_ROM_QSTR(MP_QSTR_I2C), MP_ROM_PTR(&board_i2c_obj) }, + + // Codec resets, both active low. + { MP_ROM_QSTR(MP_QSTR_TAS_RESET), MP_ROM_PTR(&pin_P0_09) }, + { MP_ROM_QSTR(MP_QSTR_CS42_RESET), MP_ROM_PTR(&pin_P0_15) }, + + // I2S + { MP_ROM_QSTR(MP_QSTR_I2S_DOUT), MP_ROM_PTR(&pin_P1_09) }, + { MP_ROM_QSTR(MP_QSTR_I2S_LRCLK), MP_ROM_PTR(&pin_P0_11) }, + { MP_ROM_QSTR(MP_QSTR_I2S_WORD_SELECT), MP_OBJ_FROM_PTR(&pin_P0_11) }, + { MP_ROM_QSTR(MP_QSTR_I2S_BCLK), MP_ROM_PTR(&pin_P0_12) }, + { MP_ROM_QSTR(MP_QSTR_I2S_BIT_CLOCK), MP_OBJ_FROM_PTR(&pin_P0_12) }, + { MP_ROM_QSTR(MP_QSTR_OSC_EN), MP_ROM_PTR(&pin_P0_13) }, + + // 4 GB eMMC + { MP_ROM_QSTR(MP_QSTR_EMMC_CLK), MP_ROM_PTR(&pin_P0_06) }, + { MP_ROM_QSTR(MP_QSTR_EMMC_DAT0), MP_ROM_PTR(&pin_P0_07) }, + { MP_ROM_QSTR(MP_QSTR_EMMC_CMD), MP_ROM_PTR(&pin_P0_08) }, + { MP_ROM_QSTR(MP_QSTR_EMMC_RESET), MP_ROM_PTR(&pin_P1_08) }, + { MP_ROM_QSTR(MP_QSTR_EMMC_VCCQ), MP_ROM_PTR(&pin_P0_14) }, + + // BQ24232 charger. CHARGE_ENABLE and the two status lines are active low. + { MP_ROM_QSTR(MP_QSTR_CHARGE_ISET), MP_ROM_PTR(&pin_P1_00) }, + { MP_ROM_QSTR(MP_QSTR_CHARGE_ENABLE), MP_ROM_PTR(&pin_P0_21) }, + { MP_ROM_QSTR(MP_QSTR_CHARGE_STATUS), MP_ROM_PTR(&pin_P0_22) }, + { MP_ROM_QSTR(MP_QSTR_POWER_GOOD), MP_ROM_PTR(&pin_P0_24) }, + + // Battery sense, AIN4, through a divider. + { MP_ROM_QSTR(MP_QSTR_VBATT), MP_ROM_PTR(&pin_P0_28) }, + { MP_ROM_QSTR(MP_QSTR_BATTERY), MP_ROM_PTR(&pin_P0_28) }, + { MP_ROM_QSTR(MP_QSTR_VOLTAGE_MONITOR), MP_ROM_PTR(&pin_P0_28) }, + + // CYBT-353027-02 Bluetooth module reset, active low + { MP_ROM_QSTR(MP_QSTR_BT_RESET), MP_ROM_PTR(&pin_P0_10) }, +}; + +MP_DEFINE_CONST_DICT(board_module_globals, board_module_globals_table); diff --git a/ports/nordic/boards/teenage_engineering_sp1/power_off.c b/ports/nordic/boards/teenage_engineering_sp1/power_off.c new file mode 100644 index 00000000000..7131073c63e --- /dev/null +++ b/ports/nordic/boards/teenage_engineering_sp1/power_off.c @@ -0,0 +1,172 @@ +// This file is part of the CircuitPython project: https://circuitpython.org +// +// SPDX-FileCopyrightText: Copyright (c) 2026 Tim Cocks for Adafruit Industries +// +// SPDX-License-Identifier: MIT + +#include "power_off.h" + +#ifdef BOARD_POWER_OFF_BUTTON_PIN + +#include "py/misc.h" + +#include "supervisor/flash.h" + +#include "wdt.h" +#include "nrfx/hal/nrf_gpio.h" +#include "nrfx/hal/nrf_power.h" + +#ifndef BOARD_POWER_OFF_HOLD_SECONDS +#define BOARD_POWER_OFF_HOLD_SECONDS (3) +#endif + +// Timings in RTC subticks (32.768 kHz). The counter is 24 bits, so every +// comparison is masked; it wraps every 512s, longer than needed. +#define RTC_COUNTER_MASK (0xFFFFFF) +#define POLL_INTERVAL_SUBTICKS (1024) // ~31 ms +#define POWER_OFF_HOLD_SUBTICKS (BOARD_POWER_OFF_HOLD_SECONDS * 32768) +#define RELEASE_DEBOUNCE_SUBTICKS (1638) // ~50 ms + +// The RTC that port.c runs the tick from, read straight out of its counter. +#define POWER_OFF_RTC (NRF_RTC2) + +// Do-nothing default; a board with hardware to quiesce overrides this. +MP_WEAK void board_power_off_prepare(void) { +} + +#ifdef BOARD_POWER_OFF_CONFIRM_LED_PIN + +#ifndef BOARD_POWER_OFF_CONFIRM_LED_MS +#define BOARD_POWER_OFF_CONFIRM_LED_MS (200) +#endif +#define CONFIRM_BLINK_SUBTICKS ((BOARD_POWER_OFF_CONFIRM_LED_MS) * 32768 / 1000) + +// One flash to say the gesture landed. +static void power_off_confirm_blink(void) { + nrf_gpio_cfg_output(BOARD_POWER_OFF_CONFIRM_LED_PIN); + nrf_gpio_pin_set(BOARD_POWER_OFF_CONFIRM_LED_PIN); + // Busy-wait on the same RTC the release loop uses, feeding the watchdog: + // 200 ms is comfortably longer than a bootloader-armed dog's patience. + uint32_t started = POWER_OFF_RTC->COUNTER; + while (((POWER_OFF_RTC->COUNTER - started) & RTC_COUNTER_MASK) < CONFIRM_BLINK_SUBTICKS) { + bootloader_wdt_feed(); + } + nrf_gpio_pin_clear(BOARD_POWER_OFF_CONFIRM_LED_PIN); +} + +#endif // BOARD_POWER_OFF_CONFIRM_LED_PIN + +// Power-off is a sequence, not a register write, and the order matters. +static void power_off(void) { + // 0. Commit the filesystem. Hold-to-power-off is this device's normal + // "off", so the dirty page sitting in the flash cache is typically the + // last thing FAT wrote. + supervisor_flash_flush(); + + // 1. Let the board put its own hardware to bed first, while everything is + // still powered and predictable. + board_power_off_prepare(); + + // 1a. Confirm the gesture with one flash, before anything else changes. + // Deliberately after the prepare hook, so it is drawing on a board that + // is already quiesced and the LED it leaves behind is off. + #ifdef BOARD_POWER_OFF_CONFIRM_LED_PIN + power_off_confirm_blink(); + #endif + + // 2. Detach from USB + NRF_USBD->USBPULLUP = 0; + NRF_USBD->ENABLE = 0; + + // 3. Wait for the button to be released, feeding the watchdog meanwhile. + uint32_t released_since = POWER_OFF_RTC->COUNTER; + while (true) { + bootloader_wdt_feed(); + uint32_t now = POWER_OFF_RTC->COUNTER; + if (nrf_gpio_pin_read(BOARD_POWER_OFF_BUTTON_PIN) == 0) { + released_since = now; + } else if (((now - released_since) & RTC_COUNTER_MASK) >= RELEASE_DEBOUNCE_SUBTICKS) { + break; + } + } + + // 4. Clear RESETREAS so the next boot can tell a wake-from-off from a + // watchdog reset. + NRF_POWER->RESETREAS = NRF_POWER->RESETREAS; + + // 5. Arm the wake. Clear any latched DETECT first. + NRF_P0->LATCH = 0xFFFFFFFF; + NRF_P1->LATCH = 0xFFFFFFFF; + nrf_gpio_cfg_sense_input(BOARD_POWER_OFF_BUTTON_PIN, + NRF_GPIO_PIN_PULLUP, NRF_GPIO_PIN_SENSE_LOW); + + // 6. Off. Note that the spin below deliberately does not feed the + // watchdog. If SYSTEM_OFF does not take a bootloader-armed dog + // bites within seconds and the board comes back up normally. + __DSB(); + NRF_POWER->SYSTEMOFF = 1; + __DSB(); + while (true) { + } +} + +// Reading a pin whose input buffer is disconnected returns 0, which is +// indistinguishable from the button being held. power_off_tick() would see a +// button that is down on the very first poll and never released, so the gesture +// would arm itself off permanently and the board would silently lose its only +// way to power down. +// +// So check the buffer every poll and reconnect it if it +// has gone away. Anything already configured is left exactly as it is. +static void ensure_input_buffer_connected(void) { + uint32_t pin_number = BOARD_POWER_OFF_BUTTON_PIN; + NRF_GPIO_Type *reg = nrf_gpio_pin_port_decode(&pin_number); + if ((reg->PIN_CNF[pin_number] & GPIO_PIN_CNF_INPUT_Msk) == + (GPIO_PIN_CNF_INPUT_Disconnect << GPIO_PIN_CNF_INPUT_Pos)) { + // Pull-up, matching the active-low switch-to-ground the header + // documents. Without a pull the line floats and the read is noise. + nrf_gpio_cfg_input(BOARD_POWER_OFF_BUTTON_PIN, NRF_GPIO_PIN_PULLUP); + } +} + +void power_off_tick(void) { + uint32_t now = POWER_OFF_RTC->COUNTER; + static uint32_t last_poll_subticks; + if (((now - last_poll_subticks) & RTC_COUNTER_MASK) < POLL_INTERVAL_SUBTICKS) { + return; + } + last_poll_subticks = now; + + ensure_input_buffer_connected(); + + // Reading IN never disturbs the pin, so the gesture still works if user + // code has claimed the button. + bool pressed = nrf_gpio_pin_read(BOARD_POWER_OFF_BUTTON_PIN) == 0; + + // Waking from SYSTEM_OFF happens with the button still held, and the + // bootloader plus start up take far less than the hold time, so a fresh + // boot would otherwise see a hold already in progress and power straight + // back off. Require the button to be seen released once first. + static bool gesture_armed; + static bool was_pressed; + if (!pressed) { + gesture_armed = true; + was_pressed = false; + return; + } + if (!gesture_armed) { + return; + } + + static uint32_t press_started_subticks; + if (!was_pressed) { + was_pressed = true; + press_started_subticks = now; + return; + } + if (((now - press_started_subticks) & RTC_COUNTER_MASK) >= POWER_OFF_HOLD_SUBTICKS) { + power_off(); + } +} + +#endif // BOARD_POWER_OFF_BUTTON_PIN diff --git a/ports/nordic/boards/teenage_engineering_sp1/power_off.h b/ports/nordic/boards/teenage_engineering_sp1/power_off.h new file mode 100644 index 00000000000..c99be1c44a3 --- /dev/null +++ b/ports/nordic/boards/teenage_engineering_sp1/power_off.h @@ -0,0 +1,43 @@ +// This file is part of the CircuitPython project: https://circuitpython.org +// +// SPDX-FileCopyrightText: Copyright (c) 2026 Tim Cocks for Adafruit Industries +// +// SPDX-License-Identifier: MIT + +#pragma once + +// A supervisor-level power-off gesture, for a board whose only "off" is +// SYSTEM_OFF. +// +// A board opts in by defining BOARD_POWER_OFF_BUTTON_PIN in mpconfigboard.h to +// an active-low, switch-to-ground button that is also the wake source. Holding +// it for BOARD_POWER_OFF_HOLD_SECONDS then powers the board down; pressing it +// again wakes the chip through a reset, which on a board with a bootloader +// means the bootloader runs first. +// +// A board may also define BOARD_POWER_OFF_CONFIRM_LED_PIN to an active-high +// LED, which is flashed once (BOARD_POWER_OFF_CONFIRM_LED_MS, default 200) as +// soon as the hold completes. Without it the gesture is silent, which on a +// screen-less board leaves no way to tell a successful power-off from a hold +// that was a moment too short. +// +// The board does not have to configure that pin. If its input buffer is found +// disconnected the poll reconnects it with a pull-up. A board is still free to +// configure it, and anything already configured is left alone. + + +#include "py/mpconfig.h" + +#ifdef BOARD_POWER_OFF_BUTTON_PIN + +// Poll the button and, if it has been held long enough, power off (never +// returns). Called from the board's board_background_task(), i.e. from every +// RUN_BACKGROUND_TASKS. +void power_off_tick(void); + +// Put the board's own hardware into its off state: rails down, resets +// asserted, anything that would drain a battery through SYSTEM_OFF switched +// off. +void board_power_off_prepare(void); + +#endif diff --git a/ports/nordic/boards/teenage_engineering_sp1/wdt.h b/ports/nordic/boards/teenage_engineering_sp1/wdt.h new file mode 100644 index 00000000000..c0107e4e1ca --- /dev/null +++ b/ports/nordic/boards/teenage_engineering_sp1/wdt.h @@ -0,0 +1,40 @@ +// This file is part of the CircuitPython project: https://circuitpython.org +// +// SPDX-FileCopyrightText: Copyright (c) 2026 Tim Cocks for Adafruit Industries +// +// SPDX-License-Identifier: MIT + +#pragma once + +// Support for the watchdog that this board's bootloader starts, before +// CircuitPython's first instruction, and that cannot be stopped. +// +// This is not the `watchdog` module: that one owns the peripheral and can +// configure it. Here the WDT is already running and its configuration +// registers (CRV, RREN, CONFIG) are locked, so the only thing the application +// can do is reload it. +// +// 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. So the +// feed lives in board_background_task(), never in an interrupt handler, and +// mpconfigboard.mk keeps CIRCUITPY_WATCHDOG off so that user code cannot get +// at the peripheral. + +#include + +#include "nrfx.h" + +// Value that a reload request register must be written with, per the nRF52 +// product specification. +#define NRF_WDT_RELOAD_REQUEST_VALUE (0x6E524635UL) + +// Reload the bootloader's watchdog. +// +// Call this from the main loop, never from an interrupt handler. Feeding from +// an ISR would keep a wedged main loop "alive" indefinitely. +static inline void bootloader_wdt_feed(void) { + for (size_t channel = 0; channel < 8; channel++) { + NRF_WDT->RR[channel] = NRF_WDT_RELOAD_REQUEST_VALUE; + } +} diff --git a/ports/nordic/common-hal/microcontroller/Pin.c b/ports/nordic/common-hal/microcontroller/Pin.c index 8043d2dfd4d..ea495d32893 100644 --- a/ports/nordic/common-hal/microcontroller/Pin.c +++ b/ports/nordic/common-hal/microcontroller/Pin.c @@ -33,6 +33,10 @@ static void reset_speaker_enable_pin(void) { #endif } +MP_WEAK bool board_reset_pin_number(uint8_t pin_number) { + return false; +} + void reset_all_pins(void) { for (size_t i = 0; i < GPIO_COUNT; i++) { claimed_pins[i] = never_reset_pins[i]; @@ -42,6 +46,10 @@ void reset_all_pins(void) { if ((never_reset_pins[nrf_pin_port(pin)] & (1 << nrf_relative_pin_number(pin))) != 0) { continue; } + // Allow the board to override the reset state of any pin. + if (board_reset_pin_number(pin)) { + continue; + } nrf_gpio_cfg_default(pin); } @@ -64,6 +72,9 @@ void reset_pin_number(uint8_t pin_number) { reset_speaker_enable_pin(); } #endif + + // Allow the board to override the reset state of any pin. + board_reset_pin_number(pin_number); } diff --git a/ports/nordic/common-hal/microcontroller/Pin.h b/ports/nordic/common-hal/microcontroller/Pin.h index e0a7550ec3a..330a8ef1283 100644 --- a/ports/nordic/common-hal/microcontroller/Pin.h +++ b/ports/nordic/common-hal/microcontroller/Pin.h @@ -10,6 +10,11 @@ #include "peripherals/nrf/pins.h" +// If a board needs a different reset state for one or more pins, implement +// board_reset_pin_number so that it sets this state and returns `true` for those +// pin numbers, `false` for others. +bool board_reset_pin_number(uint8_t pin_number); + void reset_all_pins(void); // reset_pin_number takes the pin number instead of the pointer so that objects don't // need to store a full pointer. diff --git a/ports/nordic/mpconfigport.h b/ports/nordic/mpconfigport.h index d926da73596..a6995f9b885 100644 --- a/ports/nordic/mpconfigport.h +++ b/ports/nordic/mpconfigport.h @@ -95,7 +95,11 @@ #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 #define ISR_SIZE (4 * 1024) // 4kiB // Smallest unit of flash that can be erased. @@ -106,14 +110,37 @@ // 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) #define BOOTLOADER_SETTINGS_SIZE (4 * 1024) // 4kiB +// 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. A bootloader whose magic is wider than eight bits also +// defines BOOTLOADER_DFU_MAGIC2, which is written to GPREGRET2. +#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 + #define CIRCUITPY_INTERNAL_FLASH_FILESYSTEM_START_ADDR (BOOTLOADER_START_ADDR - CIRCUITPY_INTERNAL_FLASH_FILESYSTEM_SIZE) #if CIRCUITPY_INTERNAL_FLASH_FILESYSTEM_SIZE > 0 && CIRCUITPY_INTERNAL_FLASH_FILESYSTEM_START_ADDR != (BOOTLOADER_START_ADDR - CIRCUITPY_INTERNAL_FLASH_FILESYSTEM_SIZE) diff --git a/ports/nordic/supervisor/port.c b/ports/nordic/supervisor/port.c index 0bb23364081..944dd21dc73 100644 --- a/ports/nordic/supervisor/port.c +++ b/ports/nordic/supervisor/port.c @@ -11,6 +11,8 @@ #include "supervisor/background_callback.h" #include "supervisor/board.h" +#include "board.h" + #include "nrfx/hal/nrf_clock.h" #include "nrfx/hal/nrf_power.h" #include "nrfx/drivers/include/nrfx_gpiote.h" @@ -36,6 +38,7 @@ #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" @@ -53,6 +56,10 @@ extern void qspi_disable(void); #endif +// Do-nothing, not every board needs to provide this function. +MP_WEAK void board_early_init(void) { +} + static void power_warning_handler(void) { reset_into_safe_mode(SAFE_MODE_BROWNOUT); } @@ -132,6 +139,9 @@ void tick_set_prescaler(uint32_t prescaler_val) { } safe_mode_t port_init(void) { + // Before any peripheral is touched + board_early_init(); + nrf_peripherals_clocks_init(); // If GPIO voltage is set wrong in UICR, this will fix it, and @@ -176,10 +186,10 @@ 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 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; } @@ -219,9 +229,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 + // This bootloader's magic is 16 bits wide, split across both retention + // registers. + NRF_POWER->GPREGRET2 = BOOTLOADER_DFU_MAGIC2; + #endif reset_cpu(); } From 44122a254d7d72d07cd1638893702cfe47501330 Mon Sep 17 00:00:00 2001 From: foamyguy Date: Thu, 27 Aug 2026 17:37:26 -0500 Subject: [PATCH 2/5] board_wdt_feed() hook, feed watchdog during flash filesystem formatting, removed unused include from nordic/supervisor/port.c --- ports/nordic/background.c | 3 +++ ports/nordic/background.h | 3 +++ ports/nordic/boards/teenage_engineering_sp1/board.c | 5 +++++ ports/nordic/boards/teenage_engineering_sp1/mpconfigboard.h | 4 ++++ ports/nordic/mpconfigport.h | 4 ++++ ports/nordic/peripherals/nrf/nvm.c | 6 ++++++ ports/nordic/supervisor/port.c | 1 - 7 files changed, 25 insertions(+), 1 deletion(-) diff --git a/ports/nordic/background.c b/ports/nordic/background.c index 9afade89136..f461f3d165b 100644 --- a/ports/nordic/background.c +++ b/ports/nordic/background.c @@ -40,6 +40,9 @@ void port_background_tick(void) { MP_WEAK void board_background_task(void) { } +MP_WEAK void board_wdt_feed(void) { +} + void port_background_task(void) { board_background_task(); } diff --git a/ports/nordic/background.h b/ports/nordic/background.h index 5712b054193..c6bf6facedf 100644 --- a/ports/nordic/background.h +++ b/ports/nordic/background.h @@ -7,3 +7,6 @@ #pragma once void board_background_task(void); + +// Feed a watchdog that wasn't armed by CircuitPython i.e. a boards custom bootloader +void board_wdt_feed(void); diff --git a/ports/nordic/boards/teenage_engineering_sp1/board.c b/ports/nordic/boards/teenage_engineering_sp1/board.c index ede8e0d523e..ff4c0458158 100644 --- a/ports/nordic/boards/teenage_engineering_sp1/board.c +++ b/ports/nordic/boards/teenage_engineering_sp1/board.c @@ -283,6 +283,11 @@ void board_init(void) { nrf_gpio_pin_clear(PIN_LED_HEARTBEAT); } +// The feed on its own +void board_wdt_feed(void) { + bootloader_wdt_feed(); +} + void board_background_task(void) { bootloader_wdt_feed(); diff --git a/ports/nordic/boards/teenage_engineering_sp1/mpconfigboard.h b/ports/nordic/boards/teenage_engineering_sp1/mpconfigboard.h index 703c8f9744b..481f2251050 100644 --- a/ports/nordic/boards/teenage_engineering_sp1/mpconfigboard.h +++ b/ports/nordic/boards/teenage_engineering_sp1/mpconfigboard.h @@ -56,6 +56,10 @@ #define BOOTLOADER_UF2_MAGIC (0xB3) #define BOOTLOADER_UF2_MAGIC2 (0x7E) +// For a bootloader that starts a watchdog before our first instruction and +// locks its configuration. +#define CIRCUITPY_BOOTLOADER_ARMED_WDT (1) + // TWIM to the CS42L42 (0x48) and TAS2505 (0x18). #define DEFAULT_I2C_BUS_SCL (&pin_P1_11) #define DEFAULT_I2C_BUS_SDA (&pin_P1_07) diff --git a/ports/nordic/mpconfigport.h b/ports/nordic/mpconfigport.h index a6995f9b885..b3bbade0fc8 100644 --- a/ports/nordic/mpconfigport.h +++ b/ports/nordic/mpconfigport.h @@ -141,6 +141,10 @@ #define BOOTLOADER_UF2_MAGIC (0x57) #endif +// Whether this board's bootloader starts a watchdog before CircuitPython +#define CIRCUITPY_BOOTLOADER_ARMED_WDT (0) +#endif + #define CIRCUITPY_INTERNAL_FLASH_FILESYSTEM_START_ADDR (BOOTLOADER_START_ADDR - CIRCUITPY_INTERNAL_FLASH_FILESYSTEM_SIZE) #if CIRCUITPY_INTERNAL_FLASH_FILESYSTEM_SIZE > 0 && CIRCUITPY_INTERNAL_FLASH_FILESYSTEM_START_ADDR != (BOOTLOADER_START_ADDR - CIRCUITPY_INTERNAL_FLASH_FILESYSTEM_SIZE) diff --git a/ports/nordic/peripherals/nrf/nvm.c b/ports/nordic/peripherals/nrf/nvm.c index 61a517f1152..3f4f90ff6ac 100644 --- a/ports/nordic/peripherals/nrf/nvm.c +++ b/ports/nordic/peripherals/nrf/nvm.c @@ -12,6 +12,8 @@ #include "nrfx_nvmc.h" +#include "background.h" + #define FLASH_PAGE_SIZE (4096) #ifdef BLUETOOTH_SD @@ -112,6 +114,10 @@ bool nrf_nvm_safe_flash_page_write(uint32_t page_addr, uint8_t *data) { } #endif + #if CIRCUITPY_BOOTLOADER_ARMED_WDT + board_wdt_feed(); + #endif + nrfx_nvmc_page_erase(page_addr); nrfx_nvmc_bytes_write(page_addr, data, FLASH_PAGE_SIZE); return true; diff --git a/ports/nordic/supervisor/port.c b/ports/nordic/supervisor/port.c index 944dd21dc73..20d04e53803 100644 --- a/ports/nordic/supervisor/port.c +++ b/ports/nordic/supervisor/port.c @@ -38,7 +38,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" From fd526b0ba4dabeb9592aa9de9db344165f2bc5f0 Mon Sep 17 00:00:00 2001 From: foamyguy Date: Thu, 27 Aug 2026 19:25:48 -0500 Subject: [PATCH 3/5] fix mpconfigport.h, add microcontroller.on_next_reset bootloader support for 16 bit magic, disable CIRCUITPY_SDCARD_USB on SP-1 --- .../teenage_engineering_sp1/mpconfigboard.h | 3 +++ .../common-hal/microcontroller/__init__.c | 24 ++++++++++++++++--- ports/nordic/mpconfigport.h | 1 + 3 files changed, 25 insertions(+), 3 deletions(-) diff --git a/ports/nordic/boards/teenage_engineering_sp1/mpconfigboard.h b/ports/nordic/boards/teenage_engineering_sp1/mpconfigboard.h index 481f2251050..dcfbf0eeffd 100644 --- a/ports/nordic/boards/teenage_engineering_sp1/mpconfigboard.h +++ b/ports/nordic/boards/teenage_engineering_sp1/mpconfigboard.h @@ -60,6 +60,9 @@ // locks its configuration. #define CIRCUITPY_BOOTLOADER_ARMED_WDT (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) diff --git a/ports/nordic/common-hal/microcontroller/__init__.c b/ports/nordic/common-hal/microcontroller/__init__.c index d5752fb46b6..03746d242b8 100644 --- a/ports/nordic/common-hal/microcontroller/__init__.c +++ b/ports/nordic/common-hal/microcontroller/__init__.c @@ -80,20 +80,38 @@ void common_hal_mcu_enable_interrupts(void) { } 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 + // This bootloader's 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; + } + #endif #ifdef BLUETOOTH_SD - int err_code = sd_power_gpregret_set(0, DFU_MAGIC_UF2_RESET); + 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/mpconfigport.h b/ports/nordic/mpconfigport.h index b3bbade0fc8..388f329c86c 100644 --- a/ports/nordic/mpconfigport.h +++ b/ports/nordic/mpconfigport.h @@ -142,6 +142,7 @@ #endif // Whether this board's bootloader starts a watchdog before CircuitPython +#ifndef CIRCUITPY_BOOTLOADER_ARMED_WDT #define CIRCUITPY_BOOTLOADER_ARMED_WDT (0) #endif From 14179066a1187f987279a20213ce633dafaa01b0 Mon Sep 17 00:00:00 2001 From: foamyguy Date: Fri, 28 Aug 2026 13:49:23 -0500 Subject: [PATCH 4/5] use board_background_task() and remove board_wdt_feed. Move background tasks out of peripherals nvm.c to the callsites instead. --- ports/nordic/background.c | 3 --- ports/nordic/background.h | 3 --- ports/nordic/boards/teenage_engineering_sp1/board.c | 5 ----- ports/nordic/boards/teenage_engineering_sp1/mpconfigboard.h | 4 ---- ports/nordic/boards/teenage_engineering_sp1/power_off.c | 5 ++++- ports/nordic/common-hal/nvm/ByteArray.c | 2 ++ ports/nordic/mpconfigport.h | 5 ----- ports/nordic/peripherals/nrf/nvm.c | 6 ------ ports/nordic/supervisor/internal_flash.c | 2 ++ 9 files changed, 8 insertions(+), 27 deletions(-) diff --git a/ports/nordic/background.c b/ports/nordic/background.c index f461f3d165b..9afade89136 100644 --- a/ports/nordic/background.c +++ b/ports/nordic/background.c @@ -40,9 +40,6 @@ void port_background_tick(void) { MP_WEAK void board_background_task(void) { } -MP_WEAK void board_wdt_feed(void) { -} - void port_background_task(void) { board_background_task(); } diff --git a/ports/nordic/background.h b/ports/nordic/background.h index c6bf6facedf..5712b054193 100644 --- a/ports/nordic/background.h +++ b/ports/nordic/background.h @@ -7,6 +7,3 @@ #pragma once void board_background_task(void); - -// Feed a watchdog that wasn't armed by CircuitPython i.e. a boards custom bootloader -void board_wdt_feed(void); diff --git a/ports/nordic/boards/teenage_engineering_sp1/board.c b/ports/nordic/boards/teenage_engineering_sp1/board.c index ff4c0458158..ede8e0d523e 100644 --- a/ports/nordic/boards/teenage_engineering_sp1/board.c +++ b/ports/nordic/boards/teenage_engineering_sp1/board.c @@ -283,11 +283,6 @@ void board_init(void) { nrf_gpio_pin_clear(PIN_LED_HEARTBEAT); } -// The feed on its own -void board_wdt_feed(void) { - bootloader_wdt_feed(); -} - void board_background_task(void) { bootloader_wdt_feed(); diff --git a/ports/nordic/boards/teenage_engineering_sp1/mpconfigboard.h b/ports/nordic/boards/teenage_engineering_sp1/mpconfigboard.h index dcfbf0eeffd..76bfb957210 100644 --- a/ports/nordic/boards/teenage_engineering_sp1/mpconfigboard.h +++ b/ports/nordic/boards/teenage_engineering_sp1/mpconfigboard.h @@ -56,10 +56,6 @@ #define BOOTLOADER_UF2_MAGIC (0xB3) #define BOOTLOADER_UF2_MAGIC2 (0x7E) -// For a bootloader that starts a watchdog before our first instruction and -// locks its configuration. -#define CIRCUITPY_BOOTLOADER_ARMED_WDT (1) - // No SD card LUN. There is no card slot on this device. #define CIRCUITPY_SDCARD_USB (0) diff --git a/ports/nordic/boards/teenage_engineering_sp1/power_off.c b/ports/nordic/boards/teenage_engineering_sp1/power_off.c index 7131073c63e..27b1e477b4c 100644 --- a/ports/nordic/boards/teenage_engineering_sp1/power_off.c +++ b/ports/nordic/boards/teenage_engineering_sp1/power_off.c @@ -10,6 +10,7 @@ #include "py/misc.h" +#include "supervisor/filesystem.h" #include "supervisor/flash.h" #include "wdt.h" @@ -61,7 +62,9 @@ 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(); + if (filesystem_present()) { + supervisor_flash_flush(); + } // 1. Let the board put its own hardware to bed first, while everything is // still powered and predictable. diff --git a/ports/nordic/common-hal/nvm/ByteArray.c b/ports/nordic/common-hal/nvm/ByteArray.c index c846a61f751..f245207e83c 100644 --- a/ports/nordic/common-hal/nvm/ByteArray.c +++ b/ports/nordic/common-hal/nvm/ByteArray.c @@ -11,6 +11,7 @@ #include #include +#include "background.h" #include "peripherals/nrf/nvm.h" uint32_t common_hal_nvm_bytearray_get_length(const nvm_bytearray_obj_t *self) { @@ -40,6 +41,7 @@ bool common_hal_nvm_bytearray_set_bytes(const nvm_bytearray_obj_t *self, while (len) { uint32_t write_len = MIN(len, FLASH_PAGE_SIZE - offset); + board_background_task(); if (!write_page(page_addr, offset, write_len, values)) { return false; } diff --git a/ports/nordic/mpconfigport.h b/ports/nordic/mpconfigport.h index 388f329c86c..a6995f9b885 100644 --- a/ports/nordic/mpconfigport.h +++ b/ports/nordic/mpconfigport.h @@ -141,11 +141,6 @@ #define BOOTLOADER_UF2_MAGIC (0x57) #endif -// Whether this board's bootloader starts a watchdog before CircuitPython -#ifndef CIRCUITPY_BOOTLOADER_ARMED_WDT -#define CIRCUITPY_BOOTLOADER_ARMED_WDT (0) -#endif - #define CIRCUITPY_INTERNAL_FLASH_FILESYSTEM_START_ADDR (BOOTLOADER_START_ADDR - CIRCUITPY_INTERNAL_FLASH_FILESYSTEM_SIZE) #if CIRCUITPY_INTERNAL_FLASH_FILESYSTEM_SIZE > 0 && CIRCUITPY_INTERNAL_FLASH_FILESYSTEM_START_ADDR != (BOOTLOADER_START_ADDR - CIRCUITPY_INTERNAL_FLASH_FILESYSTEM_SIZE) diff --git a/ports/nordic/peripherals/nrf/nvm.c b/ports/nordic/peripherals/nrf/nvm.c index 3f4f90ff6ac..61a517f1152 100644 --- a/ports/nordic/peripherals/nrf/nvm.c +++ b/ports/nordic/peripherals/nrf/nvm.c @@ -12,8 +12,6 @@ #include "nrfx_nvmc.h" -#include "background.h" - #define FLASH_PAGE_SIZE (4096) #ifdef BLUETOOTH_SD @@ -114,10 +112,6 @@ bool nrf_nvm_safe_flash_page_write(uint32_t page_addr, uint8_t *data) { } #endif - #if CIRCUITPY_BOOTLOADER_ARMED_WDT - board_wdt_feed(); - #endif - nrfx_nvmc_page_erase(page_addr); nrfx_nvmc_bytes_write(page_addr, data, FLASH_PAGE_SIZE); return true; diff --git a/ports/nordic/supervisor/internal_flash.c b/ports/nordic/supervisor/internal_flash.c index 88b6a274d06..82a687d1dd8 100644 --- a/ports/nordic/supervisor/internal_flash.c +++ b/ports/nordic/supervisor/internal_flash.c @@ -16,6 +16,7 @@ #include "lib/oofatfs/ff.h" #include "supervisor/shared/safe_mode.h" +#include "background.h" #include "peripherals/nrf/nvm.h" #ifdef BLUETOOTH_SD @@ -54,6 +55,7 @@ void port_internal_flash_flush(void) { // Skip if data is the same if (memcmp(_flash_cache, (void *)_flash_page_addr, FLASH_PAGE_SIZE) != 0) { + board_background_task(); if (!nrf_nvm_safe_flash_page_write(_flash_page_addr, _flash_cache)) { reset_into_safe_mode(SAFE_MODE_FLASH_WRITE_FAIL); } From 19118d0fa34655558d634d09b18eba32600e71e7 Mon Sep 17 00:00:00 2001 From: foamyguy Date: Fri, 28 Aug 2026 14:33:41 -0500 Subject: [PATCH 5/5] defore power off button gesture during filesystem format --- .../teenage_engineering_sp1/power_off.c | 26 +++++++++++++++++++ .../teenage_engineering_sp1/power_off.h | 4 +++ 2 files changed, 30 insertions(+) diff --git a/ports/nordic/boards/teenage_engineering_sp1/power_off.c b/ports/nordic/boards/teenage_engineering_sp1/power_off.c index 27b1e477b4c..dd20d4293e4 100644 --- a/ports/nordic/boards/teenage_engineering_sp1/power_off.c +++ b/ports/nordic/boards/teenage_engineering_sp1/power_off.c @@ -28,6 +28,12 @@ #define POWER_OFF_HOLD_SUBTICKS (BOARD_POWER_OFF_HOLD_SECONDS * 32768) #define RELEASE_DEBOUNCE_SUBTICKS (1638) // ~50 ms +// How long the gesture will wait for a filesystem that is being built +#ifndef BOARD_POWER_OFF_FILESYSTEM_GRACE_SECONDS +#define BOARD_POWER_OFF_FILESYSTEM_GRACE_SECONDS (30) +#endif +#define FILESYSTEM_GRACE_SUBTICKS (BOARD_POWER_OFF_FILESYSTEM_GRACE_SECONDS * 32768) + // The RTC that port.c runs the tick from, read straight out of its counter. #define POWER_OFF_RTC (NRF_RTC2) @@ -132,6 +138,22 @@ static void ensure_input_buffer_connected(void) { } } +static bool power_off_deferred(uint32_t now) { + static uint32_t absent_since_subticks; + static bool was_absent; + + if (filesystem_present()) { + was_absent = false; + return false; + } + + if (!was_absent) { + was_absent = true; + absent_since_subticks = now; + } + return ((now - absent_since_subticks) & RTC_COUNTER_MASK) < FILESYSTEM_GRACE_SUBTICKS; +} + void power_off_tick(void) { uint32_t now = POWER_OFF_RTC->COUNTER; static uint32_t last_poll_subticks; @@ -140,6 +162,10 @@ void power_off_tick(void) { } last_poll_subticks = now; + if (power_off_deferred(now)) { + return; + } + ensure_input_buffer_connected(); // Reading IN never disturbs the pin, so the gesture still works if user diff --git a/ports/nordic/boards/teenage_engineering_sp1/power_off.h b/ports/nordic/boards/teenage_engineering_sp1/power_off.h index c99be1c44a3..d9eef76d611 100644 --- a/ports/nordic/boards/teenage_engineering_sp1/power_off.h +++ b/ports/nordic/boards/teenage_engineering_sp1/power_off.h @@ -24,6 +24,10 @@ // The board does not have to configure that pin. If its input buffer is found // disconnected the poll reconnects it with a pull-up. A board is still free to // configure it, and anything already configured is left alone. +// +// The gesture holds off while the supervisor is building the filesystem, so +// that a press during a format cannot leave a half-written FAT behind. The +// press is not lost: it takes effect as soon as the filesystem is mounted. #include "py/mpconfig.h"