diff --git a/extmod/modasyncio.c b/extmod/modasyncio.c index f73d32baf6d..28ca14290b2 100644 --- a/extmod/modasyncio.c +++ b/extmod/modasyncio.c @@ -31,8 +31,8 @@ #if MICROPY_PY_ASYNCIO -// CIRCUITPY-CHANGE -#if CIRCUITPY && !(defined(__unix__) || defined(__APPLE__)) +// CIRCUITPY-CHANGE: the unix port has no supervisor. +#if CIRCUITPY && CIRCUITPY_SUPERVISOR #include "shared-bindings/supervisor/__init__.h" #endif @@ -76,7 +76,7 @@ static mp_obj_t task_queue_make_new(const mp_obj_type_t *type, size_t n_args, si #define _TICKS_MAX (_TICKS_PERIOD - 1) #define _TICKS_HALFPERIOD (_TICKS_PERIOD >> 1) -#if !CIRCUITPY || (defined(__unix__) || defined(__APPLE__)) +#if !CIRCUITPY || !CIRCUITPY_SUPERVISOR static mp_obj_t ticks(void) { return MP_OBJ_NEW_SMALL_INT(mp_hal_ticks_ms() & _TICKS_MAX); } diff --git a/ports/zephyr-cp/CMakeLists.txt b/ports/zephyr-cp/CMakeLists.txt index 37b5f9e2648..3075b9a401a 100644 --- a/ports/zephyr-cp/CMakeLists.txt +++ b/ports/zephyr-cp/CMakeLists.txt @@ -22,7 +22,7 @@ target_sources(app PRIVATE zephyr_main.c) # Add native_sim test support helpers. if(CONFIG_BOARD_NATIVE_SIM) - target_sources(app PRIVATE native_sim_i2c_emul_control.c) + target_sources(app PRIVATE native_sim_i2c_emul_control.c native_sim_spi_loopback_emul.c) endif() if(CONFIG_TRACING_PERFETTO) diff --git a/ports/zephyr-cp/boards/native/native_sim/board.overlay b/ports/zephyr-cp/boards/native/native_sim/board.overlay index f8f03ec867f..909b1bff5dc 100644 --- a/ports/zephyr-cp/boards/native/native_sim/board.overlay +++ b/ports/zephyr-cp/boards/native/native_sim/board.overlay @@ -67,4 +67,13 @@ }; +/* An SPI loopback for testing: MOSI wired to MISO. */ +&spi0 { + loopback: loopback@0 { + compatible = "circuitpython,spi-loopback"; + reg = <0>; + spi-max-frequency = <50000000>; + }; +}; + #include "../../../app.overlay" diff --git a/ports/zephyr-cp/boards/native/native_sim_lfs/board.overlay b/ports/zephyr-cp/boards/native/native_sim_lfs/board.overlay index 45eeb7cf294..9c5cd4aa14d 100644 --- a/ports/zephyr-cp/boards/native/native_sim_lfs/board.overlay +++ b/ports/zephyr-cp/boards/native/native_sim_lfs/board.overlay @@ -97,3 +97,12 @@ #include "../../../app.overlay" + +/* An SPI loopback for testing: MOSI wired to MISO. */ +&spi0 { + loopback: loopback@0 { + compatible = "circuitpython,spi-loopback"; + reg = <0>; + spi-max-frequency = <50000000>; + }; +}; diff --git a/ports/zephyr-cp/common-hal/busio/SPI.c b/ports/zephyr-cp/common-hal/busio/SPI.c index 0bfda074218..78a0943354c 100644 --- a/ports/zephyr-cp/common-hal/busio/SPI.c +++ b/ports/zephyr-cp/common-hal/busio/SPI.c @@ -250,6 +250,15 @@ bool common_hal_busio_spi_read(busio_spi_obj_t *self, uint8_t *data, size_t len, int ret = spi_transceive_signal(self->spi_device, &self->config[self->active_config], &tx, &rx, &self->signal); + // Wait for the transfer to complete while running background tasks. The driver may read the + // fill buffer until then. + int signaled = 0; + int result = 0; + while (ret == 0 && !signaled && !mp_hal_is_interrupted()) { + RUN_BACKGROUND_TASKS; + k_poll_signal_check(&self->signal, &signaled, &result); + } + if (need_free) { if (used_port_malloc) { port_free(tx_data); @@ -258,19 +267,7 @@ bool common_hal_busio_spi_read(busio_spi_obj_t *self, uint8_t *data, size_t len, } } - if (ret != 0) { - return false; - } - - // Wait for the transfer to complete while running background tasks - int signaled = 0; - int result = 0; - while (!signaled && !mp_hal_is_interrupted()) { - RUN_BACKGROUND_TASKS; - k_poll_signal_check(&self->signal, &signaled, &result); - } - - return signaled && result == 0; + return ret == 0 && signaled && result == 0; } bool common_hal_busio_spi_transfer(busio_spi_obj_t *self, const uint8_t *data_out, uint8_t *data_in, size_t len) { diff --git a/ports/zephyr-cp/dts/bindings/circuitpython,spi-loopback.yaml b/ports/zephyr-cp/dts/bindings/circuitpython,spi-loopback.yaml new file mode 100644 index 00000000000..e74e499215f --- /dev/null +++ b/ports/zephyr-cp/dts/bindings/circuitpython,spi-loopback.yaml @@ -0,0 +1,8 @@ +# SPDX-FileCopyrightText: 2026 Vladimir Smitka +# SPDX-License-Identifier: MIT + +description: SPI loopback for tests, MOSI wired to MISO + +compatible: "circuitpython,spi-loopback" + +include: spi-device.yaml diff --git a/ports/zephyr-cp/internal-modules/iobroker/src/iobroker.c b/ports/zephyr-cp/internal-modules/iobroker/src/iobroker.c index 8871d27f46b..9fb88ba3e11 100644 --- a/ports/zephyr-cp/internal-modules/iobroker/src/iobroker.c +++ b/ports/zephyr-cp/internal-modules/iobroker/src/iobroker.c @@ -14,6 +14,8 @@ #include #include +#include +#include #include #include @@ -112,13 +114,28 @@ int iobroker_i2c_allocate(package_pin_t sda, package_pin_t scl, return -ENOSYS; } +#if defined(CONFIG_BOARD_NATIVE_SIM) && DT_HAS_COMPAT_STATUS_OKAY(zephyr_spi_emul_controller) +// native_sim has no pin routing: any pins get the emulated SPI controller. +#define NATIVE_SIM_SPI DEVICE_DT_GET_ONE(zephyr_spi_emul_controller) +static bool native_sim_spi_in_use; +#endif + int iobroker_spi_allocate(package_pin_t clock, package_pin_t mosi, package_pin_t miso, const struct device **dev_out) { (void)clock; (void)mosi; (void)miso; + #ifdef NATIVE_SIM_SPI + if (native_sim_spi_in_use) { + return -ENODEV; + } + native_sim_spi_in_use = true; + *dev_out = NATIVE_SIM_SPI; + return 0; + #else (void)dev_out; return -ENOSYS; + #endif } int iobroker_uart_allocate(package_pin_t tx, package_pin_t rx, @@ -138,6 +155,11 @@ int iobroker_pwm_allocate(package_pin_t pin, const struct device **dev_out) { } bool iobroker_release(const struct device *dev) { + #ifdef NATIVE_SIM_SPI + if (dev == NATIVE_SIM_SPI) { + native_sim_spi_in_use = false; + } + #endif (void)dev; LOG_DBG("release: no routing support on this SoC, nothing to release"); return false; diff --git a/ports/zephyr-cp/native_sim_spi_loopback_emul.c b/ports/zephyr-cp/native_sim_spi_loopback_emul.c new file mode 100644 index 00000000000..2736f8929fd --- /dev/null +++ b/ports/zephyr-cp/native_sim_spi_loopback_emul.c @@ -0,0 +1,63 @@ +// This file is part of the CircuitPython project: https://circuitpython.org +// +// SPDX-FileCopyrightText: Copyright (c) 2026 Vladimir Smitka +// +// SPDX-License-Identifier: MIT + +// SPI loopback for native_sim: every byte sent is read back, and zeros when nothing is sent. + +#define DT_DRV_COMPAT circuitpython_spi_loopback + +#include +#include +#include +#include + +static int loopback_io(const struct emul *target, const struct spi_config *config, + const struct spi_buf_set *tx_bufs, const struct spi_buf_set *rx_bufs) { + ARG_UNUSED(target); + ARG_UNUSED(config); + if (rx_bufs == NULL) { + return 0; + } + // A buffer without data is that many idle or dropped bytes, as in Zephyr's drivers. + size_t tx_i = 0; + size_t tx_pos = 0; + for (size_t i = 0; i < rx_bufs->count; i++) { + uint8_t *rx = rx_bufs->buffers[i].buf; + for (size_t j = 0; j < rx_bufs->buffers[i].len; j++) { + uint8_t value = 0; + while (tx_bufs != NULL && tx_i < tx_bufs->count && tx_pos >= tx_bufs->buffers[tx_i].len) { + tx_i++; + tx_pos = 0; + } + if (tx_bufs != NULL && tx_i < tx_bufs->count) { + const uint8_t *tx = tx_bufs->buffers[tx_i].buf; + if (tx != NULL) { + value = tx[tx_pos]; + } + tx_pos++; + } + if (rx != NULL) { + rx[j] = value; + } + } + } + return 0; +} + +static int loopback_init(const struct emul *target, const struct device *parent) { + ARG_UNUSED(target); + ARG_UNUSED(parent); + return 0; +} + +static const struct spi_emul_api loopback_api = { + .io = loopback_io, +}; + +#define LOOPBACK_DEFINE(n) \ + DEVICE_DT_INST_DEFINE(n, NULL, NULL, NULL, NULL, POST_KERNEL, CONFIG_SPI_INIT_PRIORITY, NULL); \ + EMUL_DT_INST_DEFINE(n, loopback_init, NULL, NULL, &loopback_api, NULL); + +DT_INST_FOREACH_STATUS_OKAY(LOOPBACK_DEFINE) diff --git a/ports/zephyr-cp/tests/test_spi.py b/ports/zephyr-cp/tests/test_spi.py new file mode 100644 index 00000000000..6ccfd4ec1a0 --- /dev/null +++ b/ports/zephyr-cp/tests/test_spi.py @@ -0,0 +1,54 @@ +# SPDX-FileCopyrightText: 2026 Vladimir Smitka +# SPDX-License-Identifier: MIT + +"""busio.SPI on native_sim, through the SPI loopback device.""" + +import pytest + +LOOPBACK_CODE = """\ +import board + +spi = board.SPI() +while not spi.try_lock(): + pass +spi.configure(baudrate=1_000_000) +bad = 0 +for n in (1, 31, 32, 33, 4096): + out = bytes(i * 7 & 0xFF for i in range(n)) + inb = bytearray(n) + spi.write_readinto(out, inb) + if inb != out: + bad += 1 + print("write_readinto", n, "differs") + spi.readinto(inb, write_value=0xA5) + if inb != b"\\xa5" * n: + bad += 1 + print("readinto 0xA5", n, "differs") + spi.readinto(inb) + if inb != bytes(n): + bad += 1 + print("readinto 0", n, "differs") + spi.write(out) +out = bytes(range(64)) +inb = bytearray(64) +spi.write_readinto(out, inb, out_start=8, out_end=40, in_start=8, in_end=40) +if inb[8:40] != out[8:40] or any(inb[:8]) or any(inb[40:]): + bad += 1 + print("slices differ") +spi.unlock() +spi.deinit() +print("frequency", spi.frequency if False else "ok") +print("bad", bad) +print("done") +""" + + +@pytest.mark.circuitpy_drive({"code.py": LOOPBACK_CODE}) +def test_spi_loopback(circuitpython): + """Blocking transfers come back through the loopback, including slices and reads.""" + circuitpython.wait_until_done() + + output = circuitpython.serial.all_output + assert "differs" not in output + assert "bad 0" in output + assert "done" in output diff --git a/ports/zephyr-cp/zephyr-config/west.yml b/ports/zephyr-cp/zephyr-config/west.yml index 2d7b3d95d53..afdd38ae54d 100644 --- a/ports/zephyr-cp/zephyr-config/west.yml +++ b/ports/zephyr-cp/zephyr-config/west.yml @@ -14,7 +14,7 @@ manifest: path: bootloader/mcuboot - name: zephyr url: https://github.com/adafruit/zephyr - revision: a4d81519a1affe11254b049ba565f124fa53671d + revision: 3ab36efea64edda80e45ece016753e3686bb763f clone-depth: 100 import: # Skip what no CircuitPython board can use, so west update fetches 37