From 5b76a07e4bebb6aec76d4f09d109ecce55d17f30 Mon Sep 17 00:00:00 2001 From: Florian Loitsch Date: Thu, 3 Sep 2026 15:47:16 +0200 Subject: [PATCH 01/10] Harden SPI controller edge-case configuration --- lib/spi.toit | 8 +++++++- src/resources/spi_esp32.cc | 17 +++++++++++++---- src/resources/spi_esp32.h | 7 ++++++- 3 files changed, 26 insertions(+), 6 deletions(-) diff --git a/lib/spi.toit b/lib/spi.toit index f46b45298..26e07b3a6 100644 --- a/lib/spi.toit +++ b/lib/spi.toit @@ -577,7 +577,11 @@ class Bus: cycles before the first clock edge. ESP-IDF only supports this option for half-duplex transactions, except for a limited one-cycle case on the classic ESP32. $cs-hold-cycles keeps CS active after the last clock edge. - Both values must be between 0 and 16. + Setup must be between 0 and 16. Hold must be between 0 and 16, except on the + classic ESP32 where the maximum is 15. + + On ESP32, a bus configured with only one of `mosi` and `miso` registers its + devices as half-duplex. A bus with both data pins remains full-duplex. The bus retains the returned device until either the device or the bus is explicitly closed. Dropping the last application reference does not free a @@ -600,6 +604,8 @@ class Bus: if mode < 0 or mode > 3: throw "Argument Error" if not 0 <= cs-setup-cycles <= 16: throw "OUT_OF_RANGE" if not 0 <= cs-hold-cycles <= 16: throw "OUT_OF_RANGE" + if system.architecture == system.ARCHITECTURE-ESP32 and cs-hold-cycles == 16: + throw "OUT_OF_RANGE" cs-num := gpio.to-pin-num_ cs dc-num := gpio.to-pin-num_ dc // For a deprecated gpio.Pin the dc pin is configured here; for the new diff --git a/src/resources/spi_esp32.cc b/src/resources/spi_esp32.cc index d8f8cd382..62c777eb9 100644 --- a/src/resources/spi_esp32.cc +++ b/src/resources/spi_esp32.cc @@ -232,9 +232,13 @@ class SpiBufferTargetResource : public EventQueueResource { GpioPins owned_pins_; }; -SpiResourceGroup::SpiResourceGroup(Process* process, EventSource* event_source, spi_host_device_t host_device) +SpiResourceGroup::SpiResourceGroup(Process* process, + EventSource* event_source, + spi_host_device_t host_device, + bool half_duplex) : ResourceGroup(process, event_source) - , host_device_(host_device) {} + , host_device_(host_device) + , half_duplex_(half_duplex) {} SpiResourceGroup::~SpiResourceGroup() { SystemEventSource::instance()->run([&]() -> void { @@ -707,7 +711,10 @@ PRIMITIVE(init) { } SpiResourceGroup* spi = _new SpiResourceGroup( - process, EventQueueEventSource::instance(), host_device); + process, + EventQueueEventSource::instance(), + host_device, + mosi_num == -1 || miso_num == -1); if (!spi) { SystemEventSource::instance()->run([&]() -> void { FATAL_IF_NOT_ESP_OK(spi_bus_free(capture.host_device)); @@ -1317,7 +1324,9 @@ PRIMITIVE(device) { .input_delay_ns = 0, .sample_point = SPI_SAMPLING_POINT_PHASE_0, .spics_io_num = cs_num, - .flags = 0, + .flags = spi->half_duplex() + ? static_cast(SPI_DEVICE_HALFDUPLEX) + : 0u, .queue_size = 1, .pre_cb = null, .post_cb = spi_post_transfer_callback, diff --git a/src/resources/spi_esp32.h b/src/resources/spi_esp32.h index 88c000757..e0b37ffe0 100644 --- a/src/resources/spi_esp32.h +++ b/src/resources/spi_esp32.h @@ -35,7 +35,10 @@ const int kSpiControllerMaxDevicesPerHost = 6; class SpiResourceGroup : public ResourceGroup { public: TAG(SpiResourceGroup); - SpiResourceGroup(Process* process, EventSource* event_source, spi_host_device_t host_device); + SpiResourceGroup(Process* process, + EventSource* event_source, + spi_host_device_t host_device, + bool half_duplex); ~SpiResourceGroup() override; uint32_t on_event(Resource* resource, word data, uint32_t state) override { @@ -43,6 +46,7 @@ class SpiResourceGroup : public ResourceGroup { } spi_host_device_t host_device() { return host_device_; } + bool half_duplex() const { return half_duplex_; } // GPIO pins reserved by this bus (mosi/miso/clock). GpioPins& owned_pins() { return owned_pins_; } @@ -57,6 +61,7 @@ class SpiResourceGroup : public ResourceGroup { private: spi_host_device_t host_device_; + const bool half_duplex_; int device_count_ = 0; GpioPins owned_pins_; }; From 1946b400d731e5a4e59782f531fbe1eb8052e679 Mon Sep 17 00:00:00 2001 From: Florian Loitsch Date: Thu, 3 Sep 2026 15:47:16 +0200 Subject: [PATCH 02/10] Exercise I2C and SPI hardware edge cases --- .../hw/esp32/i2c-async-controller-shared.toit | 29 ++++ tests/hw/esp32/i2c-spi-oom-test.toit | 124 ++++++++++++++ tests/hw/esp32/i2c-spi-tracking.md | 162 ++++++++++++++++++ tests/hw/esp32/i2c-target-shared.toit | 1 + tests/hw/esp32/spi-buffer-target-shared.toit | 156 +++++++++++------ tests/hw/esp32/spi-cs-timing-board1.toit | 8 + tests/hw/esp32/spi-cs-timing-board2.toit | 8 + tests/hw/esp32/spi-cs-timing-shared.toit | 135 +++++++++++++++ tests/hw/esp32/spi-target-shared.toit | 94 +++++++++- 9 files changed, 664 insertions(+), 53 deletions(-) create mode 100644 tests/hw/esp32/i2c-spi-oom-test.toit create mode 100644 tests/hw/esp32/i2c-spi-tracking.md create mode 100644 tests/hw/esp32/spi-cs-timing-board1.toit create mode 100644 tests/hw/esp32/spi-cs-timing-board2.toit create mode 100644 tests/hw/esp32/spi-cs-timing-shared.toit diff --git a/tests/hw/esp32/i2c-async-controller-shared.toit b/tests/hw/esp32/i2c-async-controller-shared.toit index 839843f4d..9ed7f4ba1 100644 --- a/tests/hw/esp32/i2c-async-controller-shared.toit +++ b/tests/hw/esp32/i2c-async-controller-shared.toit @@ -169,6 +169,7 @@ test-board1: // Prove that the suspended controller task does not prevent another Toit // task from running during successful clock stretching. + print "Async I2C: dynamic clock-stretch recovery" stretched := bus.device ADDRESS --timeout-us=30_000 expected := make-data 17 0xc4 send-command port DYNAMIC-READ [expected, encode-u16 10] @@ -188,6 +189,7 @@ test-board1: // A task deadline aborts the native transaction even when every individual // stretch is shorter than the device's SCL timeout. The bus must be reusable // after the target releases the clock. + print "Async I2C: deadline abort recovery" abortable := bus.device ADDRESS --timeout-us=100_000 send-command port DYNAMIC-READ [#[0x5a], encode-u16 20] expect-throw DEADLINE-EXCEEDED-ERROR: @@ -211,6 +213,26 @@ test-board1: expect (bus.test ADDRESS) abortable.close + // Hold SCL externally before START. The per-device stretch timeout applies + // to SCL pulses after a transaction has started, so use the task deadline + // to abort this bus-not-idle case. Releasing the probe pin lets us verify + // that the same bus and target recover immediately. + print "Async I2C: external SCL deadline recovery" + external-timeout-device := bus.device ADDRESS --timeout-us=2_000 + send-command port STRETCH-CLOCK [encode-u16 10] + expect-throw DEADLINE-EXCEEDED-ERROR: + with-timeout --ms=3: external-timeout-device.read 1 + expect-equals OK port.in.read-byte + expect (bus.test ADDRESS) + external-timeout-device.close + + recovered := bus.device ADDRESS --timeout-us=30_000 + recovered-data := #[0x6b] + send-command port DYNAMIC-READ [recovered-data, encode-u16 1] + expect-equals recovered-data (recovered.read 1) + expect-equals OK port.in.read-byte + recovered.close + // Exercise the controller pull-up configuration and resource reuse after a // large number of asynchronous transactions and errors. bus.close @@ -400,6 +422,13 @@ test-board2: sleep --ms=(decode-u16 parts[1]) parts[0] send-byte port OK + else if command == STRETCH-CLOCK: + stretcher := gpio.Pin I2C-SCL-PROBE --output --open-drain --value=0 + send-byte port READY + sleep --ms=(decode-u16 parts[0]) + stretcher.set 1 + stretcher.close + send-byte port OK else if command == TIMEOUT-READ: send-byte port READY dynamic-target.serve-read-requests: diff --git a/tests/hw/esp32/i2c-spi-oom-test.toit b/tests/hw/esp32/i2c-spi-oom-test.toit new file mode 100644 index 000000000..d736c717b --- /dev/null +++ b/tests/hw/esp32/i2c-spi-oom-test.toit @@ -0,0 +1,124 @@ +// Copyright (C) 2026 Toit contributors. +// Use of this source code is governed by a Zero-Clause BSD license that can +// be found in the tests/LICENSE file. + +import expect show * +import i2c +import spi +import system + +import .test +import .variants + +expect-oom [block]: + exception := catch block + expect + exception == "ALLOCATION_FAILED" + or exception == "MALLOC_FAILED" + or exception == "OUT_OF_MEMORY" + +main: + run-test: test + +test: + variant := Variant.CURRENT + data := variant.unconnected-pin1 + clock := variant.unconnected-pin2 + select := variant.unconnected-pin3 + + // These deliberately impossible native allocations fail on every primitive + // attempt. If an attempt retains pins, a peripheral, or another native + // resource, the following retry reports ALREADY_IN_USE instead of OOM. + expect-oom: + i2c.Target + --sda=data + --scl=clock + --address=0x42 + --send-buffer-size=(1 << 30) + --receive-buffer-size=64 + target := i2c.Target + --sda=data + --scl=clock + --address=0x42 + target.close + + // Register targets are not supported on the original ESP32. + if system.architecture != system.ARCHITECTURE-ESP32: + expect-oom: + i2c.RegisterTarget + --sda=data + --scl=clock + --address=0x42 + --receive-buffer-size=(1 << 30) + register-target := i2c.RegisterTarget + --sda=data + --scl=clock + --address=0x42 + register-target.close + + expect-oom: + spi.Target + --mosi=data + --clock=clock + --cs=select + --max-transfer-size=(1 << 30) + spi-target := spi.Target + --mosi=data + --clock=clock + --cs=select + --max-transfer-size=64 + spi-target.close + + expect-oom: + spi.BufferTarget + --mosi=data + --clock=clock + --cs=select + --buffer-size=4 + --receive-queue-depth=(1 << 29) + buffer-target := spi.BufferTarget + --mosi=data + --clock=clock + --cs=select + --buffer-size=4 + --receive-queue-depth=2 + buffer-target.close + + test-i2c-controller-oom data clock + system.process-stats --gc + test-spi-controller-oom data clock select + +native-failure-size -> int: + // The source is stored in PSRAM on ESP32-S3, while peripheral transfers + // require internal DMA memory. Classic ESP32 has enough memory for the + // source, but not for a second allocation of the same size. + if system.architecture == system.ARCHITECTURE-ESP32: return 70_000 + return 1_000_000 + +test-i2c-controller-oom sda/int scl/int: + bus := i2c.Bus --sda=sda --scl=scl + device := bus.device 0x42 + buffer := ByteArray native-failure-size + expect-oom: device.read-into buffer + device.close + bus.close + + // Prove that both the driver handle and pins remain reusable. + retry := i2c.Bus --sda=sda --scl=scl + retry-device := retry.device 0x42 + retry-device.close + retry.close + +test-spi-controller-oom mosi/int clock/int cs/int: + bus := spi.Bus --mosi=mosi --clock=clock + device := bus.device --cs=cs --frequency=100_000 + buffer := ByteArray native-failure-size + expect-oom: device.write buffer + device.close + bus.close + + // Prove that both the driver handle and pins remain reusable. + retry := spi.Bus --mosi=mosi --clock=clock + retry-device := retry.device --cs=cs --frequency=100_000 + retry-device.close + retry.close diff --git a/tests/hw/esp32/i2c-spi-tracking.md b/tests/hw/esp32/i2c-spi-tracking.md new file mode 100644 index 000000000..cff1a22de --- /dev/null +++ b/tests/hw/esp32/i2c-spi-tracking.md @@ -0,0 +1,162 @@ +# I2C and SPI hardware-test tracking + +This document tracks the remaining verification work for the asynchronous I2C +and SPI controller/target stack. A box is checked only after the test has run on +the hardware named in the entry with the current code and ESP-IDF submodule. + +## Completion criteria + +- All relevant sources build for ESP32 and ESP32-S3. +- The complete I2C/SPI CTest selection passes twice on each variant: once after + flashing the firmware and once without rebooting the boards. +- SPI CS setup and hold behavior is checked with an independent RMT probe. +- Expected bounded-queue overflow is distinguished from driver data loss. +- Controller operations, target waits, and abort/close paths do not block a + Toit primitive. +- Retryable allocation failures leave no native resources behind and do not + report OOM asynchronously. +- Recoverable electrical faults and transaction aborts leave the peripheral + reusable. + +## Work items + +### 1. Clean baseline + +- [x] Build the host SDK. +- [x] Build the ESP32 firmware envelope. +- [x] Build the ESP32-S3 firmware envelope. +- [x] Run `git diff --check` and analyze the changed Toit tests. +- [x] Run all I2C/SPI tests once on ESP32. +- [x] Run all I2C/SPI tests once on ESP32-S3. + +### 2. SPI CS timing + +- [x] Capture CS and SCLK independently with RMT at 100 kHz. +- [x] Verify that each transfer contains exactly 32 valid SCLK pulses. +- [x] Verify every setup-cycle value from 0 through 16 on ESP32-S3. +- [x] Verify every hold-cycle value from 0 through 16 on ESP32-S3. +- [x] Verify every setup-cycle value from 0 through 16 on classic ESP32 after marking + single-data-line controller devices as half-duplex. +- [x] Fix classic ESP32 hold-cycle programming and verify every representable + value from 0 through 15. Reject 16 in the Toit API because the hardware field + is only four bits wide. +- [x] Compare the relevant SPI LL implementation with recent ESP-IDF master. + Master has the same incomplete programming and documents 0 through 16 even + though its ESP32-specific test limits the maximum to 15. +- [x] Make the timing test assert every supported value on both variants. + +### 3. Allocation-failure behavior + +- [x] Inventory every allocation in the new I2C and SPI primitives and classify + it as synchronous/retryable or callback/ISR-time. +- [x] Exercise retryable OOM cleanup using deterministic allocation-failure + injection where the runtime supports it. +- [x] Verify that retrying target initialization does not leak pins, + peripherals, event resources, DMA memory, or ESP-IDF handles. +- [x] Verify that retrying controller construction and transfer setup does not leak buses, devices, + event resources, DMA memory, or ESP-IDF handles. +- [x] Verify that callbacks and completion-status primitives never synthesize + an asynchronous OOM. + +The runtime has no allocation-site failure injector. The hardware OOM test +therefore requests allocations that cannot succeed. Each primitive is retried +four times by the VM; a leaked pin or peripheral would make a later attempt +fail with `ALREADY_IN_USE` instead of the expected retryable OOM. A normal +construction on the same pins immediately afterwards verifies final cleanup. + +| Allocation site | Classification and cleanup | +| --- | --- | +| I2C target and register-target construction | Proxy first; all queues, buffers, driver handles, and resources are allocated synchronously. Scoped cleanup owns each partial state until the registered resource destructor takes ownership. | +| I2C controller bus/device construction | Proxy and Toit resource first. Queue/driver handles are released on every later failure; devices unlink themselves from the bus if callback registration fails. | +| I2C probe/read/write/write-read setup | Persistent address, TX, and RX buffers are allocated before publishing the in-flight operation. RX failure frees TX; dispatch failure retires all buffers. | +| SPI target and buffer-target construction | Proxy first; host slot, event queue, DMA storage, index arrays, resource, and driver are acquired in order with scoped cleanup or destructor ownership at every boundary. | +| SPI target/controller transfer setup | TX and RX buffers are allocated before the descriptor is queued. Second-buffer and queue failures free all partial state synchronously. | +| ISR callbacks | Allocation-free: they only update preallocated descriptors, counters, queues, and event bits. Re-arm errors are reported as driver state, never OOM. | +| Completion/status primitives | Copy into memory allocated by Toit before native state is consumed, then release native buffers. No completion path maps an error to OOM. | + +### 4. Fault injection and stress + +- [x] I2C: hit the configured in-transaction SCL timeout, and separately hold + SCL low before START to verify deadline abort and recovery on the same bus. +- [x] I2C: interrupt a transaction at FIFO boundaries and verify that the next + read/write succeeds. +- [x] I2C: repeat target queue overflow and oversized-transaction overflow and + check exact dropped counters. +- [x] SPI: abort idle and active target transactions repeatedly with DMA both + enabled and disabled. +- [x] SPI: release CS after representative byte and non-byte-aligned clock + counts, then verify target reuse. +- [x] SPI: switch ESP32-S3 mode-2 targets between full-duplex, receive-only, + and transmit-only configurations and verify that neither direction shifts by + one bit. +- [x] SPI: repeat buffered-target queue overflow and check exact ordering and + dropped counters. +- [x] Run a longer mixed-operation stress loop on each chip variant. + +The recoverable external-SCL case deliberately holds SCL before START and uses +a Toit task deadline: `Device.timeout-us` is a hardware limit on an SCL-low +interval after a transaction has started, not a deadline for waiting for an +idle bus. The separate in-transaction clock-stretch test verifies +`I2C_TIMEOUT`. Both paths abort and reuse the same controller bus. FIFO +boundaries are covered on both sides of 32 bytes and by reads through 1,024 +bytes. The contention loops and three repetitions of every abort/close and +overflow scenario provide the mixed-operation stress pass. + +### 5. Final matrix + +- [x] ESP32 first run after setup: all I2C/SPI tests pass. +- [x] ESP32 immediate second run: all I2C/SPI tests pass. +- [x] ESP32-S3 first run after setup: all I2C/SPI tests pass. +- [x] ESP32-S3 immediate second run: all I2C/SPI tests pass. +- [x] Record commit IDs for Toit and ESP-IDF below. +- [x] Commit the tests and fixes on the appropriate stacked branches and push + every affected child branch. + +## Existing coverage + +| Area | Covered cases | +| --- | --- | +| I2C target | 7-bit and 10-bit addresses, direct read/write, combined write-read, FIFO-boundary sizes, dynamic response, clock stretching, bounded receive queues, oversized transactions, broadcast, close/reconfigure | +| I2C register target | 8-bit and 16-bit register addresses, wraparound, reads larger than the FIFO, pointer continuation, live updates, overflow accounting, broadcast | +| I2C controller | asynchronous scheduling, contention, 50/100/400 kHz operation, NACK, timeout, address-width collision, clock-stretch recovery, invalid arguments | +| SPI target | modes 0-3, transmit/receive/full duplex, direction changes between target instances, MSB/LSB order, DMA and non-DMA, 50 kHz through 5 MHz, sizes 1 through 4092, idle/active abort, close and reuse | +| SPI buffer target | native response updates, fill byte, bounded queues, maximum DMA transfer, partial classic-DMA receive, wait timeout, active close and reuse | +| SPI controller | modes 0-3, full-duplex loopback, asynchronous transfer, bus reservation, keep-CS-active ownership | + +## Evidence log + +| Date | Toit commit/worktree | ESP-IDF commit | Variant | Command/test | Result | +| --- | --- | --- | --- | --- | --- | +| 2026-09-02 | `b010aac9` plus local changes | `676c30742c` | ESP32-S3 | `i2c-target-board1.toit` | Pass after target TX-empty refill fix and explicit overflow-test handshake | +| 2026-09-02 | `b010aac9` plus local changes | `676c30742c` | ESP32-S3 | SPI CS timing probe | Setup and hold deltas matched 0, 1, 8, and 16 requested cycles | +| 2026-09-02 | `b010aac9` plus local changes | `676c30742c` | ESP32 | SPI CS timing probe | Setup matched; hold values above 1 exposed unresolved saturation/wrap behavior | +| 2026-09-03 | `b010aac9` plus local changes | `676c30742c` | Host, ESP32, ESP32-S3 | Build, `toit analyze -Werror`, `git diff --check` | Pass | +| 2026-09-03 | `b010aac9` plus local changes | `676c30742c` | ESP32 | Full I2C/SPI CTest selection, fresh setup | 14/14 pass in 125.58 s | +| 2026-09-03 | `b010aac9` plus local changes | `676c30742c` | ESP32-S3 | Full I2C/SPI CTest selection, fresh setup | 14/14 pass in 122.04 s | +| 2026-09-03 | `b010aac9` plus local changes | local CS-hold fix on `676c30742c` | ESP32 | SPI CS timing probe at 100 kHz | Setup 0-16 and hold 0-15 pass; each requested cycle adds 10 us; hold 16 rejected | +| 2026-09-03 | `b010aac9` plus local changes | `676c30742c` | ESP32-S3 | SPI CS timing probe at 100 kHz | Setup and hold 0-16 pass; each requested cycle adds 10 us | +| 2026-09-03 | `b010aac9` plus local changes | `676c30742c` | ESP32 | `i2c-spi-oom-test.toit` | Pass: I2C/SPI target construction and controller transfer setup survive four retryable native OOM attempts; buses, devices, and pins are immediately reusable | +| 2026-09-03 | `b010aac9` plus local changes | `676c30742c` | ESP32-S3 | `i2c-spi-oom-test.toit` | Pass: I2C/SPI target construction (including register target) and controller transfer setup survive four retryable native OOM attempts; resources are reusable | +| 2026-09-03 | `b010aac9` plus local changes | `54f5e254e2` | ESP32 | Focused fault/timing set | 9/9 pass, including repeated target abort/close, exact queue overflow, non-byte CS termination, external-SCL deadline recovery, OOM, and CS setup 0-16/hold 0-15 | +| 2026-09-03 | `b010aac9` plus local changes | `54f5e254e2` | ESP32-S3 | Focused fault/timing set | 9/9 pass, including repeated target abort/close, exact queue overflow, non-byte CS termination, external-SCL deadline recovery, OOM, and CS setup/hold 0-16 | +| 2026-09-03 | `b010aac9` plus local changes | `54f5e254e2` | ESP32 | Full I2C/SPI matrix after setup | 15/15 pass in 132.38 s | +| 2026-09-03 | `b010aac9` plus local changes | `54f5e254e2` | ESP32 | Immediate full matrix with setup fixtures excluded | 12/12 pass in 77.88 s | +| 2026-09-03 | `b010aac9` plus local changes | `54f5e254e2` | ESP32-S3 | Full I2C/SPI matrix after setup | 15/15 pass in 128.64 s | +| 2026-09-03 | `b010aac9` plus local changes | `54f5e254e2` | ESP32-S3 | Immediate full matrix with setup fixtures excluded | 12/12 pass in 72.75 s | +| 2026-09-03 | `b010aac9` plus follow-up changes | `817726ca57` | ESP32-S3 | `spi-target-board1.toit` | Pass: mode 2 full-duplex, receive-only, and transmit-only run consecutively without one-bit shifts; complete target matrix passes in 10.23 s | +| 2026-09-03 | `b010aac9` plus follow-up changes | `817726ca57` | ESP32-S3 | `spi-buffer-target-board1.toit` | Pass: continuously armed full-duplex modes 0-3, bit order, overflow, DMA limits, async controller, and active close in 7.57 s | +| 2026-09-03 | `b010aac9` plus follow-up changes | `817726ca57` | ESP32 | Full I2C/SPI matrix after setup | 15/15 pass in 130.79 s; a second complete setup run also passed 15/15 in 135.42 s | +| 2026-09-03 | `b010aac9` plus follow-up changes | `817726ca57` | ESP32 | Immediate full matrix with setup fixtures excluded | 12/12 pass in 78.13 s | +| 2026-09-03 | `b010aac9` plus follow-up changes | `817726ca57` | ESP32-S3 | Full I2C/SPI matrix after setup | 15/15 pass in 128.76 s | +| 2026-09-03 | `b010aac9` plus follow-up changes | `817726ca57` | ESP32-S3 | Immediate full matrix with setup fixtures excluded | 12/12 pass in 73.46 s | + +Add the exact command, result, and any captured timing to this table as each +remaining item is completed. + +## Rig observations + +During iterative S3 firmware experiments, board 1 intermittently failed the +921,600-baud synchronization before a test container was installed. A fresh +firmware flash recovered it. The final 15-test run and immediate 12-test +no-setup repetition both passed, so no SPI assertion or persistent peripheral +failure accompanied the serial symptom. diff --git a/tests/hw/esp32/i2c-target-shared.toit b/tests/hw/esp32/i2c-target-shared.toit index 82a245067..0a7f4a102 100644 --- a/tests/hw/esp32/i2c-target-shared.toit +++ b/tests/hw/esp32/i2c-target-shared.toit @@ -29,6 +29,7 @@ FREQUENCY ::= 100_000 READY ::= 0xa5 OK ::= 0x5a +CONTROLLER-DONE ::= 0xc3 WRITE ::= 1 QUEUE-READ ::= 2 diff --git a/tests/hw/esp32/spi-buffer-target-shared.toit b/tests/hw/esp32/spi-buffer-target-shared.toit index 85f62bfcd..3d898c53d 100644 --- a/tests/hw/esp32/spi-buffer-target-shared.toit +++ b/tests/hw/esp32/spi-buffer-target-shared.toit @@ -36,6 +36,8 @@ SYNC ::= 0xb3 FLAG-USE-MOSI ::= 1 << 0 FLAG-USE-MISO ::= 1 << 1 FLAG-DMA ::= 1 << 2 +FLAG-TRANSMIT-LSB-FIRST ::= 1 << 3 +FLAG-RECEIVE-LSB-FIRST ::= 1 << 4 main-board1: run-test: test-board1 @@ -82,9 +84,46 @@ test-board1: expect-equals #[1, 0x81, 3, 0x91, 0x92, 0x93, 0x94, 0xa5, 0xa5] received expect-equals sent (take-received port) - print "SPI buffer target: receive queue overflow" + print "SPI buffer target: modes and bit order" close-target port device.close + [ + [0, false, false], + [1, false, false], + [2, false, false], + [3, false, false], + [0, true, false], + [0, false, true], + [0, true, true], + ].do: | current/List | + mode/int := current[0] + transmit-lsb-first/bool := current[1] + receive-lsb-first/bool := current[2] + print "SPI buffer target: mode=$mode tx-lsb=$transmit-lsb-first rx-lsb=$receive-lsb-first" + response := pattern 16 (0x15 + mode) + sent = pattern 16 (0x91 + mode) + create-target port + --mode=mode + --buffer-size=16 + --queue-depth=2 + --fill-byte=0xff + --initial=response + --use-mosi + --use-miso + --dma=false + --transmit-lsb-first=transmit-lsb-first + --receive-lsb-first=receive-lsb-first + device = create-device bus --mode=mode --frequency=1_000_000 + expected-response := response.copy + if transmit-lsb-first: reverse-bits-in-place expected-response + expect-equals expected-response (transfer device sent) + expected-receive := sent.copy + if receive-lsb-first: reverse-bits-in-place expected-receive + expect-equals expected-receive (take-received port) + close-target port + device.close + + print "SPI buffer target: receive queue overflow" create-target port --mode=0 --buffer-size=16 @@ -95,12 +134,15 @@ test-board1: --use-miso --dma=false device = create-device bus --mode=0 --frequency=2_000_000 - 5.repeat: | index/int | - data := pattern 16 (0x40 + index) - expect-equals (filled 16 0x5a) (transfer device data) - expect-equals 2 (dropped port) - 3.repeat: | index/int | - expect-equals (pattern 16 (0x40 + index)) (take-received port) + 3.repeat: | round/int | + 5.repeat: | index/int | + data := pattern 16 (0x40 + round * 8 + index) + expect-equals (filled 16 0x5a) (transfer device data) + expect-equals (round + 1) * 2 (dropped port) + 3.repeat: | index/int | + expect-equals + (pattern 16 (0x40 + round * 8 + index)) + (take-received port) print "SPI buffer target: receive wait and repeated re-arm" port.out.write #[RECEIVE] --flush @@ -225,48 +267,49 @@ test-board1: close-target port device.close - [false, true].do: | dma/bool | - suffix := dma ? "dma" : "no-dma" - print "SPI buffer target: active close-$suffix" - create-target port - --mode=0 - --buffer-size=64 - --queue-depth=2 - --fill-byte=0xff - --initial=#[ ] - --use-mosi - --use-miso=false - --dma=dma - device = create-device bus - --mode=0 - --frequency=(dma ? 400_000 : 100_000) - device.with-reserved-bus: - device.transfer #[0x5a] --keep-cs-active - // Closing must not wait for CS to be released by the controller. + 3.repeat: + [false, true].do: | dma/bool | + suffix := dma ? "dma" : "no-dma" + print "SPI buffer target: active close-$suffix" + create-target port + --mode=0 + --buffer-size=64 + --queue-depth=2 + --fill-byte=0xff + --initial=#[ ] + --use-mosi + --use-miso=false + --dma=dma + device = create-device bus + --mode=0 + --frequency=(dma ? 400_000 : 100_000) + device.with-reserved-bus: + device.transfer #[0x5a] --keep-cs-active + // Closing must not wait for CS to be released by the controller. + close-target port + // The target has disconnected its CS input, so this only terminates the + // controller-side reservation. + device.transfer #[0] + device.close + + // Closing must also release the peripheral and pins for a new target. + create-target port + --mode=0 + --buffer-size=8 + --queue-depth=2 + --fill-byte=0xff + --initial=#[ ] + --use-mosi + --use-miso=false + --dma=dma + device = create-device bus + --mode=0 + --frequency=(dma ? 400_000 : 100_000) + recovery := pattern 8 (dma ? 0xd1 : 0x1d) + transfer device recovery + expect-equals recovery (take-received port) close-target port - // The target has disconnected its CS input, so this only terminates the - // controller-side reservation. - device.transfer #[0] - device.close - - // Closing must also release the peripheral and pins for a new target. - create-target port - --mode=0 - --buffer-size=8 - --queue-depth=2 - --fill-byte=0xff - --initial=#[ ] - --use-mosi - --use-miso=false - --dma=dma - device = create-device bus - --mode=0 - --frequency=(dma ? 400_000 : 100_000) - recovery := pattern 8 (dma ? 0xd1 : 0x1d) - transfer device recovery - expect-equals recovery (take-received port) - close-target port - device.close + device.close bus.close port.close @@ -306,6 +349,8 @@ test-board2: --buffer-size=buffer-size --receive-queue-depth=queue-depth --fill-byte=fill-byte + --transmit-lsb-first=((flags & FLAG-TRANSMIT-LSB-FIRST) != 0) + --receive-lsb-first=((flags & FLAG-RECEIVE-LSB-FIRST) != 0) --dma=(flags & FLAG-DMA) != 0 port.out.write #[READY] --flush else if command == WRITE: @@ -352,11 +397,15 @@ create-target port/uart.Port --initial/ByteArray --use-mosi/bool --use-miso/bool - --dma/bool: + --dma/bool + --transmit-lsb-first/bool=false + --receive-lsb-first/bool=false: flags := 0 if use-mosi: flags |= FLAG-USE-MOSI if use-miso: flags |= FLAG-USE-MISO if dma: flags |= FLAG-DMA + if transmit-lsb-first: flags |= FLAG-TRANSMIT-LSB-FIRST + if receive-lsb-first: flags |= FLAG-RECEIVE-LSB-FIRST port.out.write #[CREATE, mode, flags, fill-byte] port.out.little-endian.write-uint32 buffer-size port.out.little-endian.write-uint32 queue-depth @@ -424,3 +473,12 @@ pattern size/int seed/int -> ByteArray: size.repeat: | index/int | result[index] = (seed + index * 37) & 0xff return result + +reverse-bits-in-place bytes/ByteArray -> none: + bytes.size.repeat: | index/int | + value := bytes[index] + reversed := 0 + 8.repeat: + reversed = (reversed << 1) | (value & 1) + value >>= 1 + bytes[index] = reversed diff --git a/tests/hw/esp32/spi-cs-timing-board1.toit b/tests/hw/esp32/spi-cs-timing-board1.toit new file mode 100644 index 000000000..0baaddba8 --- /dev/null +++ b/tests/hw/esp32/spi-cs-timing-board1.toit @@ -0,0 +1,8 @@ +// Copyright (C) 2026 Toit contributors. +// Use of this source code is governed by a Zero-Clause BSD license that can +// be found in the tests/LICENSE file. + +import .spi-cs-timing-shared + +main: + main-board1 diff --git a/tests/hw/esp32/spi-cs-timing-board2.toit b/tests/hw/esp32/spi-cs-timing-board2.toit new file mode 100644 index 000000000..85f8d498b --- /dev/null +++ b/tests/hw/esp32/spi-cs-timing-board2.toit @@ -0,0 +1,8 @@ +// Copyright (C) 2026 Toit contributors. +// Use of this source code is governed by a Zero-Clause BSD license that can +// be found in the tests/LICENSE file. + +import .spi-cs-timing-shared + +main: + main-board2 diff --git a/tests/hw/esp32/spi-cs-timing-shared.toit b/tests/hw/esp32/spi-cs-timing-shared.toit new file mode 100644 index 000000000..91bc68f65 --- /dev/null +++ b/tests/hw/esp32/spi-cs-timing-shared.toit @@ -0,0 +1,135 @@ +// Copyright (C) 2026 Toit contributors. +// Use of this source code is governed by a Zero-Clause BSD license that can +// be found in the tests/LICENSE file. + +import expect show * +import rmt +import spi +import system +import uart + +import .test +import .variants + +RX1 ::= Variant.CURRENT.board-connection-pin1 +TX1 ::= Variant.CURRENT.board-connection-pin2 +RX2 ::= Variant.CURRENT.board-connection-pin2 +TX2 ::= Variant.CURRENT.board-connection-pin1 + +CS ::= Variant.CURRENT.board-connection-pin3 +SCLK ::= Variant.CURRENT.board-connection-pin4 +MOSI ::= Variant.CURRENT.board-connection-pin5 + +READY ::= 0xa1 +DONE ::= 0xa2 +CLOSE ::= 0xa3 + +FREQUENCY ::= 100_000 +CYCLE-US ::= 1_000_000 / FREQUENCY +TRANSFER ::= #[0x12, 0x34, 0x56, 0x78] + +main-board1: + run-test: test-board1 + +test-board1: + port := uart.Port --rx=RX1 --tx=TX1 --baud-rate=115_200 + expect-equals READY port.in.read-byte + bus := spi.Bus --clock=SCLK --mosi=MOSI + + baseline := measure port bus 0 0 + print "SPI CS timing: setup=0 hold=0 low=$(baseline)us" + + 16.repeat: | index/int | + check-timing port bus baseline index + 1 0 + + hold-max := system.architecture == system.ARCHITECTURE-ESP32 ? 15 : 16 + hold-max.repeat: | index/int | + check-timing port bus baseline 0 index + 1 + + if system.architecture == system.ARCHITECTURE-ESP32: + expect-throw "OUT_OF_RANGE": + bus.device + --cs=CS + --frequency=FREQUENCY + --cs-hold-cycles=16 + + port.out.write #[CLOSE] --flush + expect-equals DONE port.in.read-byte + bus.close + port.close + +check-timing port/uart.Port bus/spi.Bus baseline/int setup/int hold/int -> none: + measured := measure port bus setup hold + print "SPI CS timing: setup=$setup hold=$hold low=$(measured)us" + + // ESP-IDF maps a requested hold of zero to one cycle because the hardware + // needs that minimum. All other setup/hold values contribute one full SPI + // clock cycle each. + effective-hold := max 1 hold + expected-delta := (setup + effective-hold - 1) * CYCLE-US + expect (measured - baseline - expected-delta).abs <= 3 + +measure port/uart.Port bus/spi.Bus setup/int hold/int -> int: + port.out.write #[setup, hold] --flush + expect-equals READY port.in.read-byte + device := bus.device + --cs=CS + --frequency=FREQUENCY + --cs-setup-cycles=setup + --cs-hold-cycles=hold + device.write TRANSFER + device.close + expect-equals DONE port.in.read-byte + return port.in.little-endian.read-uint32 + +main-board2: + run-test --background: test-board2 + +test-board2: + port := uart.Port --rx=RX2 --tx=TX2 --baud-rate=115_200 + port.out.write #[READY] --flush + + while true: + setup := port.in.read-byte + if setup == CLOSE: + port.out.write #[DONE] --flush + port.close + return + hold := port.in.read-byte + + cs-probe := rmt.In CS --resolution=1_000_000 --memory-blocks=2 + clock-probe := rmt.In SCLK --resolution=1_000_000 --memory-blocks=2 + try: + cs-probe.start-reading --max-ns=10_000_000 + clock-probe.start-reading --max-ns=10_000_000 + port.out.write #[READY] --flush + + cs-signals := cs-probe.wait-for-data + clock-signals := clock-probe.wait-for-data + cs-low-us := longest-signal-us cs-signals 0 + + high-count := 0 + clock-signals.size.repeat: | i/int | + duration-us := (clock-signals.ns-duration i) / 1_000 + if (clock-signals.level i) == 1 and duration-us != 0: + if 3 <= duration-us <= 7: + high-count++ + else: + print "Ignoring non-clock SCLK-high signal: $(duration-us)us" + expect-equals TRANSFER.size * 8 high-count + expect cs-low-us >= TRANSFER.size * 8 * CYCLE-US + expect cs-low-us < 1_000 + + port.out.write #[DONE] --flush + port.out.little-endian.write-uint32 cs-low-us + port.out.flush + finally: + cs-probe.close + clock-probe.close + +longest-signal-us signals/rmt.Signals level/int -> int: + result := 0 + signals.size.repeat: | i/int | + if (signals.level i) == level: + result = max result ((signals.ns-duration i) / 1_000) + return result diff --git a/tests/hw/esp32/spi-target-shared.toit b/tests/hw/esp32/spi-target-shared.toit index e3dcb4854..e377e6825 100644 --- a/tests/hw/esp32/spi-target-shared.toit +++ b/tests/hw/esp32/spi-target-shared.toit @@ -3,6 +3,7 @@ // be found in the tests/LICENSE file. import expect show * +import gpio import io import monitor import spi @@ -27,6 +28,7 @@ ABORT-IDLE ::= 0x32 ABORT-ACTIVE ::= 0x33 RESUME ::= 0x34 CLOSE-ACTIVE ::= 0x35 +NONBYTE-TERMINATION ::= 0x36 SYNC ::= 0xa0 READY ::= 0xa1 DONE ::= 0xa2 @@ -80,6 +82,13 @@ test-board1: cases := [] 4.repeat: | mode/int | + cases.add (Case "mode-$(mode)-full-duplex-no-dma" + --frequency=400_000 + --mode=mode + --transmit=(pattern 16 (0x40 + mode)) + --controller-data=(pattern 16 (0x10 + mode)) + --max-transfer-size=64 + --dma=false) [false, true].do: | dma/bool | suffix := dma ? "dma" : "no-dma" if not dma or (mode & 1) == 0: @@ -253,10 +262,11 @@ test-board1: if current.transmit-lsb-first: reverse-bits-in-place expected-controller expect-equals expected-controller controller-result - [false, true].do: | dma/bool | - test-abort port bus ABORT-IDLE dma - test-abort port bus ABORT-ACTIVE dma - test-close-active port bus dma + 3.repeat: + [false, true].do: | dma/bool | + test-abort port bus ABORT-IDLE dma + test-abort port bus ABORT-ACTIVE dma + test-close-active port bus dma // Exercise the controller bus's device ownership and bounded native slots. // The devices deliberately omit CS because no transfers are performed. @@ -270,6 +280,12 @@ test-board1: devices.do: | device/spi.Device | expect-throws "CLOSED": device.write #[0] expect-throws "CLOSED": replacement.write #[0] + + // ESP-IDF's SPI controller API can only issue whole bytes. Drive mode-0 + // clocks directly to verify target completion and reuse when CS rises in + // the middle of a byte. + test-nonbyte-termination port + if is-classic-esp32: [1, 3].do: | mode/int | expect-throws "INVALID_ARGUMENT": @@ -303,6 +319,9 @@ test-board2: if command == CLOSE-ACTIVE: test-close-active-target port continue + if command == NONBYTE-TERMINATION: + test-nonbyte-target port + continue if command != PREPARE: throw "Unknown command: $command" mode := port.in.read-byte @@ -471,6 +490,73 @@ test-close-active-target port/uart.Port -> none: port.out.write result --flush target.close +test-nonbyte-termination port/uart.Port -> none: + print "SPI target: non-byte-aligned CS termination" + cs := gpio.Pin CS --output --value=1 + clock := gpio.Pin SCLK --output --value=0 + mosi := gpio.Pin MOSI --output --value=0 + try: + // Avoid counts congruent to one modulo eight. ESP32-S3's hardware exposes + // an N-1 counter for early termination, making those cases byte-ambiguous + // (one clock is indistinguishable from no clocks at all). + [2, 7, 10, 15, 31].do: | bit-count/int | + port.out.write #[NONBYTE-TERMINATION, bit-count] --flush + expect-equals READY port.in.read-byte + bitbang-mode0 cs clock mosi bit-count + expect-equals (bit-count + 7) / 8 (read-target-result port).size + + // Reuse the same target immediately for a complete two-byte exchange. + port.out.write #[RESUME] --flush + expect-equals READY port.in.read-byte + bitbang-mode0 cs clock mosi 16 + expect-equals 2 (read-target-result port).size + finally: + mosi.close + clock.close + cs.close + +test-nonbyte-target port/uart.Port -> none: + bit-count := port.in.read-byte + target := spi.Target + --mosi=MOSI + --clock=SCLK + --cs=CS + --max-transfer-size=8 + --dma=false + + result := target.exchange #[ ] + --receive-size=8 + --when-armed=: + port.out.write #[READY] --flush + port.out.write #[DONE] --flush + port.out.little-endian.write-uint32 result.size + port.out.write result --flush + expect-equals (bit-count + 7) / 8 result.size + + expect-equals RESUME port.in.read-byte + result = target.exchange #[ ] + --receive-size=8 + --when-armed=: + port.out.write #[READY] --flush + port.out.write #[DONE] --flush + port.out.little-endian.write-uint32 result.size + port.out.write result --flush + expect-equals 2 result.size + target.close + +bitbang-mode0 cs/gpio.Pin clock/gpio.Pin mosi/gpio.Pin bit-count/int -> none: + cs.set 0 + sleep --ms=1 + bit-count.repeat: | index/int | + mosi.set (index & 1) + sleep --ms=1 + clock.set 1 + sleep --ms=1 + clock.set 0 + sleep --ms=1 + sleep --ms=1 + cs.set 1 + prepare-target port/uart.Port current/Case: flags := 0 if current.transmit-lsb-first: flags |= FLAG-TRANSMIT-LSB-FIRST From 7658e1758aa6abff932775e4fb46844841d5fb33 Mon Sep 17 00:00:00 2001 From: Florian Loitsch Date: Fri, 4 Sep 2026 00:15:45 +0200 Subject: [PATCH 03/10] Record I2C response unwind coverage --- tests/hw/esp32/i2c-spi-tracking.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/tests/hw/esp32/i2c-spi-tracking.md b/tests/hw/esp32/i2c-spi-tracking.md index cff1a22de..fac641f1d 100644 --- a/tests/hw/esp32/i2c-spi-tracking.md +++ b/tests/hw/esp32/i2c-spi-tracking.md @@ -116,7 +116,7 @@ overflow scenario provide the mixed-operation stress pass. | Area | Covered cases | | --- | --- | -| I2C target | 7-bit and 10-bit addresses, direct read/write, combined write-read, FIFO-boundary sizes, dynamic response, clock stretching, bounded receive queues, oversized transactions, broadcast, close/reconfigure | +| I2C target | 7-bit and 10-bit addresses, direct read/write, combined write-read, FIFO-boundary sizes, dynamic response, clock stretching, response-block exceptions and non-local returns, bounded receive queues, oversized transactions, broadcast, close/reconfigure | | I2C register target | 8-bit and 16-bit register addresses, wraparound, reads larger than the FIFO, pointer continuation, live updates, overflow accounting, broadcast | | I2C controller | asynchronous scheduling, contention, 50/100/400 kHz operation, NACK, timeout, address-width collision, clock-stretch recovery, invalid arguments | | SPI target | modes 0-3, transmit/receive/full duplex, direction changes between target instances, MSB/LSB order, DMA and non-DMA, 50 kHz through 5 MHz, sizes 1 through 4092, idle/active abort, close and reuse | @@ -149,6 +149,8 @@ overflow scenario provide the mixed-operation stress pass. | 2026-09-03 | `b010aac9` plus follow-up changes | `817726ca57` | ESP32 | Immediate full matrix with setup fixtures excluded | 12/12 pass in 78.13 s | | 2026-09-03 | `b010aac9` plus follow-up changes | `817726ca57` | ESP32-S3 | Full I2C/SPI matrix after setup | 15/15 pass in 128.76 s | | 2026-09-03 | `b010aac9` plus follow-up changes | `817726ca57` | ESP32-S3 | Immediate full matrix with setup fixtures excluded | 12/12 pass in 73.46 s | +| 2026-09-04 | `527dc816` | `817726ca57` | ESP32 | `i2c-target-board1.toit` | Pass: throwing and non-local-return response blocks close the target; target recreation and subsequent transactions succeed | +| 2026-09-04 | `527dc816` | `817726ca57` | ESP32-S3 | `i2c-target-board1.toit` | Pass: active clock stretch is released after throwing and non-local-return response blocks; target recreation and subsequent transactions succeed | Add the exact command, result, and any captured timing to this table as each remaining item is completed. From 5295eeff53bef369d7516f1894a36e465be07d4b Mon Sep 17 00:00:00 2001 From: Florian Loitsch Date: Fri, 4 Sep 2026 17:05:55 +0200 Subject: [PATCH 04/10] Add build flags for I2C and SPI targets --- src/resources/i2c_esp32.cc | 25 +++++++++++++ src/resources/spi_esp32.cc | 49 ++++++++++++++++++++------ toolchains/esp32/sdkconfig | 2 ++ toolchains/esp32/sdkconfig.defaults | 1 - toolchains/esp32c3/sdkconfig | 2 ++ toolchains/esp32c3/sdkconfig.defaults | 1 - toolchains/esp32c6/sdkconfig | 2 ++ toolchains/esp32c6/sdkconfig.defaults | 1 - toolchains/esp32p4/sdkconfig | 2 ++ toolchains/esp32p4/sdkconfig.defaults | 1 - toolchains/esp32s2/sdkconfig | 2 ++ toolchains/esp32s2/sdkconfig.defaults | 1 - toolchains/esp32s3/sdkconfig | 2 ++ toolchains/esp32s3/sdkconfig.defaults | 1 - toolchains/idf/components/toit/Kconfig | 20 +++++++++++ 15 files changed, 96 insertions(+), 16 deletions(-) diff --git a/src/resources/i2c_esp32.cc b/src/resources/i2c_esp32.cc index 10a24c6b0..8c6a6481f 100644 --- a/src/resources/i2c_esp32.cc +++ b/src/resources/i2c_esp32.cc @@ -60,6 +60,8 @@ class I2cResourceGroup : public ResourceGroup { const word kControllerDoneState = 1 << 0; +#ifdef CONFIG_TOIT_ENABLE_I2C_TARGET + const word kTargetReceiveState = 1 << 0; const word kTargetRequestState = 1 << 1; const word kTargetOverflowState = 1 << 2; @@ -316,6 +318,8 @@ class I2cRegisterTargetResource : public EventQueueResource { GpioPins owned_pins_; }; +#endif // CONFIG_TOIT_ENABLE_I2C_TARGET + class I2cBusResource; class I2cDeviceResource; typedef DoubleLinkedList DeviceList; @@ -489,6 +493,8 @@ PRIMITIVE(init) { return proxy; } +#ifdef CONFIG_TOIT_ENABLE_I2C_TARGET + PRIMITIVE(target_init) { ByteArray* proxy = process->object_heap()->allocate_proxy(); if (proxy == null) FAIL(ALLOCATION_FAILED); @@ -877,6 +883,25 @@ PRIMITIVE(register_target_dropped_write_count) { return Smi::from(target->dropped_write_count()); } +#else + +PRIMITIVE(target_init) { FAIL(UNIMPLEMENTED); } +PRIMITIVE(target_create) { FAIL(UNIMPLEMENTED); } +PRIMITIVE(target_close) { FAIL(UNIMPLEMENTED); } +PRIMITIVE(target_receive) { FAIL(UNIMPLEMENTED); } +PRIMITIVE(target_write) { FAIL(UNIMPLEMENTED); } +PRIMITIVE(target_take_request_count) { FAIL(UNIMPLEMENTED); } +PRIMITIVE(target_dropped_receive_count) { FAIL(UNIMPLEMENTED); } +PRIMITIVE(register_target_create) { FAIL(UNIMPLEMENTED); } +PRIMITIVE(register_target_close) { FAIL(UNIMPLEMENTED); } +PRIMITIVE(register_target_get) { FAIL(UNIMPLEMENTED); } +PRIMITIVE(register_target_set) { FAIL(UNIMPLEMENTED); } +PRIMITIVE(register_target_read) { FAIL(UNIMPLEMENTED); } +PRIMITIVE(register_target_write) { FAIL(UNIMPLEMENTED); } +PRIMITIVE(register_target_dropped_write_count) { FAIL(UNIMPLEMENTED); } + +#endif // CONFIG_TOIT_ENABLE_I2C_TARGET + PRIMITIVE(bus_create) { ARGS(I2cResourceGroup, group, int, sda, int, scl, bool, pullup); diff --git a/src/resources/spi_esp32.cc b/src/resources/spi_esp32.cc index 62c777eb9..23c51d639 100644 --- a/src/resources/spi_esp32.cc +++ b/src/resources/spi_esp32.cc @@ -53,6 +53,18 @@ static ResourcePool spi_host_devices( #endif ); +static uint8_t* allocate_dma_buffer(size_t size, size_t alignment) { + if (size == 0) return null; + size_t allocation_size = (size + alignment - 1) & ~(alignment - 1); + if (allocation_size < size) return null; + return static_cast(heap_caps_aligned_alloc( + alignment, + allocation_size, + MALLOC_CAP_INTERNAL | MALLOC_CAP_DMA | MALLOC_CAP_8BIT)); +} + +#ifdef CONFIG_TOIT_ENABLE_SPI_TARGET + const word kSpiTargetReadyState = 1 << 0; const word kSpiTargetDoneState = 1 << 1; const int64 kSpiTargetTeardownTimeoutUs = 10 * 1000 * 1000; @@ -232,6 +244,8 @@ class SpiBufferTargetResource : public EventQueueResource { GpioPins owned_pins_; }; +#endif // CONFIG_TOIT_ENABLE_SPI_TARGET + SpiResourceGroup::SpiResourceGroup(Process* process, EventSource* event_source, spi_host_device_t host_device, @@ -315,6 +329,8 @@ bool SpiDevice::receive_event(word* data) { return xQueueReceive(queue(), data, 0) == pdTRUE; } +#ifdef CONFIG_TOIT_ENABLE_SPI_TARGET + SpiTargetResource::~SpiTargetResource() { if (initialized_ && operation_in_flight_) { // Process teardown can bypass Target.close. Abort and wait for the driver @@ -651,6 +667,8 @@ void SpiBufferTargetResource::signal_from_isr(word event) { if (higher_was_woken == pdTRUE) portYIELD_FROM_ISR(); } +#endif // CONFIG_TOIT_ENABLE_SPI_TARGET + MODULE_IMPLEMENTATION(spi, MODULE_SPI); PRIMITIVE(init) { @@ -732,6 +750,8 @@ PRIMITIVE(init) { return proxy; } +#ifdef CONFIG_TOIT_ENABLE_SPI_TARGET + PRIMITIVE(target_init) { ByteArray* proxy = process->object_heap()->allocate_proxy(); if (proxy == null) FAIL(ALLOCATION_FAILED); @@ -882,16 +902,6 @@ PRIMITIVE(target_close) { return process->null_object(); } -static uint8_t* allocate_dma_buffer(size_t size, size_t alignment) { - if (size == 0) return null; - size_t allocation_size = (size + alignment - 1) & ~(alignment - 1); - if (allocation_size < size) return null; - return static_cast(heap_caps_aligned_alloc( - alignment, - allocation_size, - MALLOC_CAP_INTERNAL | MALLOC_CAP_DMA | MALLOC_CAP_8BIT)); -} - SPI_TARGET_ISR_ATTR static void spi_buffer_target_done_callback( spi_slave_transaction_t* transaction) { auto resource = static_cast(transaction->user); @@ -1257,6 +1267,25 @@ PRIMITIVE(target_transfer_finish) { return Smi::from(result_size); } +#else + +PRIMITIVE(target_init) { FAIL(UNIMPLEMENTED); } +PRIMITIVE(target_create) { FAIL(UNIMPLEMENTED); } +PRIMITIVE(target_close) { FAIL(UNIMPLEMENTED); } +PRIMITIVE(target_transfer_start) { FAIL(UNIMPLEMENTED); } +PRIMITIVE(target_transfer_finish) { FAIL(UNIMPLEMENTED); } +PRIMITIVE(buffer_target_create) { FAIL(UNIMPLEMENTED); } +PRIMITIVE(buffer_target_arm) { FAIL(UNIMPLEMENTED); } +PRIMITIVE(buffer_target_close) { FAIL(UNIMPLEMENTED); } +PRIMITIVE(buffer_target_get) { FAIL(UNIMPLEMENTED); } +PRIMITIVE(buffer_target_set) { FAIL(UNIMPLEMENTED); } +PRIMITIVE(buffer_target_read) { FAIL(UNIMPLEMENTED); } +PRIMITIVE(buffer_target_write) { FAIL(UNIMPLEMENTED); } +PRIMITIVE(buffer_target_receive) { FAIL(UNIMPLEMENTED); } +PRIMITIVE(buffer_target_dropped_receive_count) { FAIL(UNIMPLEMENTED); } + +#endif // CONFIG_TOIT_ENABLE_SPI_TARGET + PRIMITIVE(close) { ARGS(SpiResourceGroup, spi); spi->tear_down(); diff --git a/toolchains/esp32/sdkconfig b/toolchains/esp32/sdkconfig index ca03ca9a3..cf706c5c7 100644 --- a/toolchains/esp32/sdkconfig +++ b/toolchains/esp32/sdkconfig @@ -2160,6 +2160,8 @@ CONFIG_TOIT_ENABLE_IP=y CONFIG_TOIT_ENABLE_ETHERNET=y CONFIG_TOIT_ENABLE_WIFI=y CONFIG_TOIT_ENABLE_ESPNOW=y +CONFIG_TOIT_ENABLE_I2C_TARGET=y +CONFIG_TOIT_ENABLE_SPI_TARGET=y CONFIG_TOIT_SYSTEM_SOURCE="system/extensions/esp32/boot.toit" CONFIG_TOIT_SYSTEM_SOURCE_PROJECT_ROOT="system" CONFIG_RODATA_PADDING=2097152 diff --git a/toolchains/esp32/sdkconfig.defaults b/toolchains/esp32/sdkconfig.defaults index 3eee73be2..96837294c 100644 --- a/toolchains/esp32/sdkconfig.defaults +++ b/toolchains/esp32/sdkconfig.defaults @@ -29,7 +29,6 @@ CONFIG_BTDM_BLE_SCAN_DUPL=n CONFIG_BTDM_BLE_ADV_REPORT_FLOW_CTRL_NUM=50 CONFIG_ADC_CAL_EFUSE_TP_ENABLE=n CONFIG_ADC_CAL_LUT_ENABLE=n -CONFIG_I2C_ENABLE_SLAVE_DRIVER_VERSION_2=y CONFIG_I2S_ISR_IRAM_SAFE=y CONFIG_SPI_MASTER_ISR_IN_IRAM=y CONFIG_SPI_SLAVE_ISR_IN_IRAM=n diff --git a/toolchains/esp32c3/sdkconfig b/toolchains/esp32c3/sdkconfig index f97ddf3d3..1c8ff92af 100644 --- a/toolchains/esp32c3/sdkconfig +++ b/toolchains/esp32c3/sdkconfig @@ -2190,6 +2190,8 @@ CONFIG_TOIT_ENABLE_IP=y CONFIG_TOIT_ENABLE_ETHERNET=y CONFIG_TOIT_ENABLE_WIFI=y CONFIG_TOIT_ENABLE_ESPNOW=y +CONFIG_TOIT_ENABLE_I2C_TARGET=y +CONFIG_TOIT_ENABLE_SPI_TARGET=y CONFIG_TOIT_SYSTEM_SOURCE="system/extensions/esp32/boot.toit" CONFIG_TOIT_SYSTEM_SOURCE_PROJECT_ROOT="system" CONFIG_RODATA_PADDING=2097152 diff --git a/toolchains/esp32c3/sdkconfig.defaults b/toolchains/esp32c3/sdkconfig.defaults index 13b5bb5c0..54b0a6e31 100644 --- a/toolchains/esp32c3/sdkconfig.defaults +++ b/toolchains/esp32c3/sdkconfig.defaults @@ -23,7 +23,6 @@ CONFIG_BT_NIMBLE_SVC_GAP_DEVICE_NAME="toit" CONFIG_BT_NIMBLE_SVC_GAP_APPEARANCE=0x0 CONFIG_BT_NIMBLE_TRANSPORT_ACL_FROM_LL_COUNT=10 CONFIG_BT_CTRL_BLE_MAX_ACT=10 -CONFIG_I2C_ENABLE_SLAVE_DRIVER_VERSION_2=y CONFIG_I2S_ISR_IRAM_SAFE=y CONFIG_SPI_SLAVE_ISR_IN_IRAM=y CONFIG_UART_ISR_IN_IRAM=y diff --git a/toolchains/esp32c6/sdkconfig b/toolchains/esp32c6/sdkconfig index c34104da0..0c2d32cae 100644 --- a/toolchains/esp32c6/sdkconfig +++ b/toolchains/esp32c6/sdkconfig @@ -2368,6 +2368,8 @@ CONFIG_TOIT_ENABLE_IP=y CONFIG_TOIT_ENABLE_ETHERNET=y CONFIG_TOIT_ENABLE_WIFI=y CONFIG_TOIT_ENABLE_ESPNOW=y +CONFIG_TOIT_ENABLE_I2C_TARGET=y +CONFIG_TOIT_ENABLE_SPI_TARGET=y CONFIG_TOIT_SYSTEM_SOURCE="system/extensions/esp32/boot.toit" CONFIG_TOIT_SYSTEM_SOURCE_PROJECT_ROOT="system" CONFIG_RODATA_PADDING=2097152 diff --git a/toolchains/esp32c6/sdkconfig.defaults b/toolchains/esp32c6/sdkconfig.defaults index a3de65f38..a13e19d68 100644 --- a/toolchains/esp32c6/sdkconfig.defaults +++ b/toolchains/esp32c6/sdkconfig.defaults @@ -23,7 +23,6 @@ CONFIG_BT_NIMBLE_NVS_PERSIST=y CONFIG_BT_NIMBLE_SVC_GAP_DEVICE_NAME="toit" CONFIG_BT_NIMBLE_SVC_GAP_APPEARANCE=0x0 CONFIG_BT_NIMBLE_TRANSPORT_ACL_FROM_LL_COUNT=10 -CONFIG_I2C_ENABLE_SLAVE_DRIVER_VERSION_2=y CONFIG_I2S_ISR_IRAM_SAFE=y CONFIG_SPI_SLAVE_ISR_IN_IRAM=y CONFIG_UART_ISR_IN_IRAM=y diff --git a/toolchains/esp32p4/sdkconfig b/toolchains/esp32p4/sdkconfig index 8c3b21e90..2bd05a3de 100644 --- a/toolchains/esp32p4/sdkconfig +++ b/toolchains/esp32p4/sdkconfig @@ -2265,6 +2265,8 @@ CONFIG_TOIT_ENABLE_IP=y CONFIG_TOIT_ENABLE_ETHERNET=y CONFIG_TOIT_ENABLE_WIFI=y CONFIG_TOIT_ENABLE_ESPNOW=y +CONFIG_TOIT_ENABLE_I2C_TARGET=y +CONFIG_TOIT_ENABLE_SPI_TARGET=y CONFIG_TOIT_SPIRAM_HEAP=y CONFIG_TOIT_SYSTEM_SOURCE="system/extensions/esp32/boot.toit" CONFIG_TOIT_SYSTEM_SOURCE_PROJECT_ROOT="system" diff --git a/toolchains/esp32p4/sdkconfig.defaults b/toolchains/esp32p4/sdkconfig.defaults index 15e84d50d..eb94e1c6b 100644 --- a/toolchains/esp32p4/sdkconfig.defaults +++ b/toolchains/esp32p4/sdkconfig.defaults @@ -11,7 +11,6 @@ CONFIG_PARTITION_TABLE_CUSTOM=y CONFIG_COMPILER_OPTIMIZATION_SIZE=y CONFIG_COMPILER_OPTIMIZATION_ASSERTIONS_DISABLE=y CONFIG_COMPILER_ORPHAN_SECTIONS_PLACE=y -CONFIG_I2C_ENABLE_SLAVE_DRIVER_VERSION_2=y CONFIG_I2S_ISR_IRAM_SAFE=y CONFIG_SPI_SLAVE_ISR_IN_IRAM=y CONFIG_UART_ISR_IN_IRAM=y diff --git a/toolchains/esp32s2/sdkconfig b/toolchains/esp32s2/sdkconfig index 5a9f60813..74472f7fa 100644 --- a/toolchains/esp32s2/sdkconfig +++ b/toolchains/esp32s2/sdkconfig @@ -2034,6 +2034,8 @@ CONFIG_TOIT_ENABLE_IP=y CONFIG_TOIT_ENABLE_ETHERNET=y CONFIG_TOIT_ENABLE_WIFI=y CONFIG_TOIT_ENABLE_ESPNOW=y +CONFIG_TOIT_ENABLE_I2C_TARGET=y +CONFIG_TOIT_ENABLE_SPI_TARGET=y # CONFIG_TOIT_SPIRAM_HEAP is not set CONFIG_TOIT_SYSTEM_SOURCE="system/extensions/esp32/boot.toit" CONFIG_TOIT_SYSTEM_SOURCE_PROJECT_ROOT="system" diff --git a/toolchains/esp32s2/sdkconfig.defaults b/toolchains/esp32s2/sdkconfig.defaults index 354e55502..5e07686e5 100644 --- a/toolchains/esp32s2/sdkconfig.defaults +++ b/toolchains/esp32s2/sdkconfig.defaults @@ -11,7 +11,6 @@ CONFIG_PARTITION_TABLE_CUSTOM=y CONFIG_COMPILER_OPTIMIZATION_SIZE=y CONFIG_COMPILER_OPTIMIZATION_ASSERTIONS_DISABLE=y CONFIG_COMPILER_ORPHAN_SECTIONS_PLACE=y -CONFIG_I2C_ENABLE_SLAVE_DRIVER_VERSION_2=y CONFIG_I2S_ISR_IRAM_SAFE=y CONFIG_SPI_SLAVE_ISR_IN_IRAM=y CONFIG_UART_ISR_IN_IRAM=y diff --git a/toolchains/esp32s3/sdkconfig b/toolchains/esp32s3/sdkconfig index 373720333..b4b06527b 100644 --- a/toolchains/esp32s3/sdkconfig +++ b/toolchains/esp32s3/sdkconfig @@ -2465,6 +2465,8 @@ CONFIG_TOIT_ENABLE_IP=y CONFIG_TOIT_ENABLE_ETHERNET=y CONFIG_TOIT_ENABLE_WIFI=y CONFIG_TOIT_ENABLE_ESPNOW=y +CONFIG_TOIT_ENABLE_I2C_TARGET=y +CONFIG_TOIT_ENABLE_SPI_TARGET=y CONFIG_TOIT_SPIRAM_HEAP=y CONFIG_TOIT_SYSTEM_SOURCE="system/extensions/esp32/boot.toit" CONFIG_TOIT_SYSTEM_SOURCE_PROJECT_ROOT="system" diff --git a/toolchains/esp32s3/sdkconfig.defaults b/toolchains/esp32s3/sdkconfig.defaults index 0b5dd7a62..28fa730eb 100644 --- a/toolchains/esp32s3/sdkconfig.defaults +++ b/toolchains/esp32s3/sdkconfig.defaults @@ -24,7 +24,6 @@ CONFIG_BT_NIMBLE_SVC_GAP_DEVICE_NAME="toit" CONFIG_BT_NIMBLE_SVC_GAP_APPEARANCE=0x0 CONFIG_BT_NIMBLE_TRANSPORT_ACL_FROM_LL_COUNT=10 CONFIG_BT_CTRL_BLE_MAX_ACT=10 -CONFIG_I2C_ENABLE_SLAVE_DRIVER_VERSION_2=y CONFIG_I2S_ISR_IRAM_SAFE=y CONFIG_SPI_SLAVE_ISR_IN_IRAM=y CONFIG_UART_ISR_IN_IRAM=y diff --git a/toolchains/idf/components/toit/Kconfig b/toolchains/idf/components/toit/Kconfig index 6e57c084a..bf3f3c9da 100644 --- a/toolchains/idf/components/toit/Kconfig +++ b/toolchains/idf/components/toit/Kconfig @@ -214,6 +214,26 @@ menu "Toit" help ESP-Now support in the Toit binary. + config TOIT_ENABLE_I2C_TARGET + boolean "Enable I2C target support on ESP32" + default "y" + depends on SOC_I2C_SUPPORT_SLAVE + select I2C_ENABLE_SLAVE_DRIVER_VERSION_2 + help + I2C target and register-target support in the Toit binary. + + Disable this option to reduce firmware size when only I2C + controller functionality is needed. + + config TOIT_ENABLE_SPI_TARGET + boolean "Enable SPI target support on ESP32" + default "y" + help + SPI target and buffer-target support in the Toit binary. + + Disable this option to reduce firmware size when only SPI + controller functionality is needed. + config TOIT_SPIRAM_HEAP boolean "Enable Toit heap in SPIRAM" default "y" From 944b093a67ffe549a8071f3c68761feda0ad80d4 Mon Sep 17 00:00:00 2001 From: Florian Loitsch Date: Tue, 8 Sep 2026 01:22:41 +0200 Subject: [PATCH 05/10] Complete I2C and SPI refactor hardening --- src/resources/i2c_esp32.cc | 37 +++++++++++-------- src/resources/spi_esp32.cc | 11 +++++- .../hw/esp32/i2c-async-controller-shared.toit | 34 +++++++++++++++-- tests/hw/esp32/i2c-spi-tracking.md | 35 ++++++++++++++++++ tests/hw/esp32/i2c-target-shared.toit | 1 - toolchains/idf/components/toit/CMakeLists.txt | 12 +++++- 6 files changed, 107 insertions(+), 23 deletions(-) diff --git a/src/resources/i2c_esp32.cc b/src/resources/i2c_esp32.cc index 8c6a6481f..179eade34 100644 --- a/src/resources/i2c_esp32.cc +++ b/src/resources/i2c_esp32.cc @@ -19,10 +19,13 @@ #include #include +#ifdef CONFIG_TOIT_ENABLE_I2C_TARGET #include -#include +#endif #include +#ifdef CONFIG_TOIT_ENABLE_I2C_TARGET #include +#endif #include #include "../linked.h" @@ -777,7 +780,7 @@ PRIMITIVE(register_target_create) { ByteArray* proxy = process->object_heap()->allocate_proxy(); if (proxy == null) FAIL(ALLOCATION_FAILED); - uint8_t* registers = unvoid_cast(heap_caps_calloc( + auto registers = unvoid_cast(heap_caps_calloc( register_count, sizeof(uint8_t), MALLOC_CAP_INTERNAL | MALLOC_CAP_8BIT)); if (registers == null) FAIL(MALLOC_FAILED); bool handed_to_resource = false; @@ -885,20 +888,22 @@ PRIMITIVE(register_target_dropped_write_count) { #else -PRIMITIVE(target_init) { FAIL(UNIMPLEMENTED); } -PRIMITIVE(target_create) { FAIL(UNIMPLEMENTED); } -PRIMITIVE(target_close) { FAIL(UNIMPLEMENTED); } -PRIMITIVE(target_receive) { FAIL(UNIMPLEMENTED); } -PRIMITIVE(target_write) { FAIL(UNIMPLEMENTED); } -PRIMITIVE(target_take_request_count) { FAIL(UNIMPLEMENTED); } -PRIMITIVE(target_dropped_receive_count) { FAIL(UNIMPLEMENTED); } -PRIMITIVE(register_target_create) { FAIL(UNIMPLEMENTED); } -PRIMITIVE(register_target_close) { FAIL(UNIMPLEMENTED); } -PRIMITIVE(register_target_get) { FAIL(UNIMPLEMENTED); } -PRIMITIVE(register_target_set) { FAIL(UNIMPLEMENTED); } -PRIMITIVE(register_target_read) { FAIL(UNIMPLEMENTED); } -PRIMITIVE(register_target_write) { FAIL(UNIMPLEMENTED); } -PRIMITIVE(register_target_dropped_write_count) { FAIL(UNIMPLEMENTED); } +PRIMITIVE(target_init) { FAIL(UNIMPLEMENTED); } +PRIMITIVE(target_create) { FAIL(UNIMPLEMENTED); } +PRIMITIVE(target_close) { FAIL(UNIMPLEMENTED); } +PRIMITIVE(target_receive) { FAIL(UNIMPLEMENTED); } +PRIMITIVE(target_write) { FAIL(UNIMPLEMENTED); } +PRIMITIVE(target_set_write_pending) { FAIL(UNIMPLEMENTED); } +PRIMITIVE(target_set_handler_mode) { FAIL(UNIMPLEMENTED); } +PRIMITIVE(target_take_request_count) { FAIL(UNIMPLEMENTED); } +PRIMITIVE(target_dropped_receive_count) { FAIL(UNIMPLEMENTED); } +PRIMITIVE(register_target_create) { FAIL(UNIMPLEMENTED); } +PRIMITIVE(register_target_close) { FAIL(UNIMPLEMENTED); } +PRIMITIVE(register_target_get) { FAIL(UNIMPLEMENTED); } +PRIMITIVE(register_target_set) { FAIL(UNIMPLEMENTED); } +PRIMITIVE(register_target_read) { FAIL(UNIMPLEMENTED); } +PRIMITIVE(register_target_write) { FAIL(UNIMPLEMENTED); } +PRIMITIVE(register_target_dropped_write_count) { FAIL(UNIMPLEMENTED); } #endif // CONFIG_TOIT_ENABLE_I2C_TARGET diff --git a/src/resources/spi_esp32.cc b/src/resources/spi_esp32.cc index 23c51d639..d44f27db1 100644 --- a/src/resources/spi_esp32.cc +++ b/src/resources/spi_esp32.cc @@ -19,12 +19,19 @@ #include #include +#ifdef CONFIG_TOIT_ENABLE_SPI_TARGET #include +#include +#if SOC_CACHE_INTERNAL_MEM_VIA_L1CACHE #include -#include -#include #include #include +#endif +#endif +#include +#ifdef CONFIG_TOIT_ENABLE_SPI_TARGET +#include +#endif #include "../objects_inline.h" #include "../process.h" diff --git a/tests/hw/esp32/i2c-async-controller-shared.toit b/tests/hw/esp32/i2c-async-controller-shared.toit index 9ed7f4ba1..9f5ed2bfe 100644 --- a/tests/hw/esp32/i2c-async-controller-shared.toit +++ b/tests/hw/esp32/i2c-async-controller-shared.toit @@ -27,6 +27,7 @@ MISSING-ADDRESS ::= 0x71 READY ::= 0xa5 OK ::= 0x5a +SYNC ::= 0xc3 SET ::= 1 RECONFIGURE ::= 2 @@ -47,7 +48,7 @@ main-board1: test-board1: port := uart.Port --rx=UART-RX1 --tx=UART-TX1 --baud-rate=115_200 - expect-equals READY port.in.read-byte + synchronize-controller port if system.architecture == system.ARCHITECTURE-ESP32: test-board1-esp32 port @@ -62,6 +63,7 @@ test-board1: found := bus.scan --timeout-ms=5 expect (found.contains ADDRESS) expect-not (found.contains MISSING-ADDRESS) + print "Async I2C: probe and scan complete" missing := bus.device MISSING-ADDRESS expect-throw "I2C_NACK": missing.write #[1] @@ -81,6 +83,7 @@ test-board1: expect-equals initial (registers.read-bytes 0 initial.size) expect-equals (wrapped initial 17 1024) (registers.read-bytes 17 1024) + print "Async I2C: initial register reads complete" into := ByteArray 40: 0xee device.write #[23] @@ -108,6 +111,7 @@ test-board1: done.do: it.get errors.do: | error/any? | if error: throw error + print "Async I2C: contention complete" device.close slow := bus.device ADDRESS --frequency=50_000 @@ -162,6 +166,7 @@ test-board1: reconfigure port REGISTER-10 set-registers port 0 wide expect-equals #[wide[3]] (ten.write-read #[3] 1) + print "Async I2C: address-width collision complete" seven.close ten.close @@ -386,7 +391,7 @@ test-board2: register-target/i2c.RegisterTarget? := make-register-target REGISTER-7 dynamic-target/i2c.Target? := null port := uart.Port --rx=UART-RX2 --tx=UART-TX2 --baud-rate=115_200 - send-byte port READY + synchronize-target port while true: command := port.in.read-byte @@ -449,7 +454,7 @@ test-board2-esp32 -> none: --receive-buffer-size=512 --pull-up port := uart.Port --rx=UART-RX2 --tx=UART-TX2 --baud-rate=115_200 - send-byte port READY + synchronize-target port while true: command := port.in.read-byte @@ -529,6 +534,29 @@ send-byte port/uart.Port value/int -> none: port.out.write-byte value port.out.flush +synchronize-controller port/uart.Port -> none: + synchronized := monitor.Latch + sender-done := monitor.Latch + task:: + try: + while not synchronized.has-value: + send-byte port SYNC + sleep --ms=10 + finally: + critical-do --no-respect-deadline: sender-done.set true + + expect-equals READY port.in.read-byte + synchronized.set true + sender-done.get + send-byte port OK + expect-equals READY port.in.read-byte + +synchronize-target port/uart.Port -> none: + while port.in.read-byte != SYNC: null + send-byte port READY + while port.in.read-byte != OK: null + send-byte port READY + encode-u16 value/int -> ByteArray: return #[value & 0xff, (value >> 8) & 0xff] diff --git a/tests/hw/esp32/i2c-spi-tracking.md b/tests/hw/esp32/i2c-spi-tracking.md index fac641f1d..593f3cac4 100644 --- a/tests/hw/esp32/i2c-spi-tracking.md +++ b/tests/hw/esp32/i2c-spi-tracking.md @@ -4,6 +4,36 @@ This document tracks the remaining verification work for the asynchronous I2C and SPI controller/target stack. A box is checked only after the test has run on the hardware named in the entry with the current code and ESP-IDF submodule. +## Refactor review follow-up + +- [x] Restack the I2C and SPI branches so every intermediate PR builds and its + tests use only APIs introduced at or below that point in the stack. +- [x] Compile out I2C and SPI target implementations and private dependencies + when their `CONFIG_TOIT_ENABLE_*_TARGET` option is disabled. +- [x] Serialize default-response replacement with controller reads and cover + both callback-backed and buffered transmit arbitration in ESP-IDF tests. +- [x] Make constructor rollback, controller abort, close, and bus-reservation + release non-cancelable where partial cleanup would leave native state live. +- [x] Remove the register-target C++ data race while documenting byte-level, + rather than transaction-level, atomicity. +- [x] Bound target response handlers and test timeout fallback followed by a + successful later handler response. +- [x] Attempt bounded physical I2C bus recovery after retiring an aborted + controller transaction. +- [x] Keep the ESP-IDF fork changes local and narrowly scoped; remain on the + 5.4 API rather than combining the refactor with an ESP-IDF 6 migration. +- [x] Add ESP-IDF stress coverage for default-response arbitration, + callback-backed target reads, and abort/reuse of SPI target descriptors. +- [x] Restore `CONFIG_SPI_MASTER_ISR_IN_IRAM` and link the classic ESP32 + default envelope with both target implementations enabled. +- [x] Build the classic ESP32 envelope with both target implementations + disabled to verify the configuration guards. +- [x] Make the conditional cache-maintenance dependency match the SoCs that + actually require it. +- [x] Run the updated I2C/SPI hardware matrix twice on ESP32 and ESP32-S3. +- [x] Commit the review fixes at the correct levels of the stack and update all + affected PR branches. + ## Completion criteria - All relevant sources build for ESP32 and ESP32-S3. @@ -151,6 +181,11 @@ overflow scenario provide the mixed-operation stress pass. | 2026-09-03 | `b010aac9` plus follow-up changes | `817726ca57` | ESP32-S3 | Immediate full matrix with setup fixtures excluded | 12/12 pass in 73.46 s | | 2026-09-04 | `527dc816` | `817726ca57` | ESP32 | `i2c-target-board1.toit` | Pass: throwing and non-local-return response blocks close the target; target recreation and subsequent transactions succeed | | 2026-09-04 | `527dc816` | `817726ca57` | ESP32-S3 | `i2c-target-board1.toit` | Pass: active clock stretch is released after throwing and non-local-return response blocks; target recreation and subsequent transactions succeed | +| 2026-09-05 | `b8cf906b` plus late-default regression | `c30e19e02d` | ESP32-S3 | `ctest --verbose --test-dir build/hw -C esp32s3 -R i2c-target-board1.toit-esp32s3$` | Pass: the controller began reading before the target installed its first default response; installing it released SCL and returned the expected bytes | +| 2026-09-08 | restacked tree equivalent to `bdc5cad1` | `fc94150e9d` | ESP32 | Full I2C/SPI matrix after setup | 15/15 pass in 137.22 s with I2C and SPI targets enabled and SPI master ISR in IRAM | +| 2026-09-08 | restacked tree equivalent to `bdc5cad1` | `fc94150e9d` | ESP32 | Immediate full matrix with setup fixtures excluded | 12/12 pass in 85.07 s | +| 2026-09-08 | restacked tree equivalent to `bdc5cad1` | `fc94150e9d` | ESP32-S3 | Full I2C/SPI matrix after setup | 15/15 pass in 135.91 s after replacing the one-shot inter-board startup byte with an acknowledged synchronization handshake | +| 2026-09-08 | restacked tree equivalent to `bdc5cad1` | `fc94150e9d` | ESP32-S3 | Immediate full matrix with setup fixtures excluded | 12/12 pass in 75.63 s | Add the exact command, result, and any captured timing to this table as each remaining item is completed. diff --git a/tests/hw/esp32/i2c-target-shared.toit b/tests/hw/esp32/i2c-target-shared.toit index 0a7f4a102..82a245067 100644 --- a/tests/hw/esp32/i2c-target-shared.toit +++ b/tests/hw/esp32/i2c-target-shared.toit @@ -29,7 +29,6 @@ FREQUENCY ::= 100_000 READY ::= 0xa5 OK ::= 0x5a -CONTROLLER-DONE ::= 0xc3 WRITE ::= 1 QUEUE-READ ::= 2 diff --git a/toolchains/idf/components/toit/CMakeLists.txt b/toolchains/idf/components/toit/CMakeLists.txt index 9339a19e2..c82e8c4e2 100644 --- a/toolchains/idf/components/toit/CMakeLists.txt +++ b/toolchains/idf/components/toit/CMakeLists.txt @@ -2,9 +2,16 @@ cmake_minimum_required(VERSION 3.11) set(TOIT_BASE_DIR ${COMPONENT_PATH}/../../../..) +set(TOIT_COMPONENT_REQUIRES + "efuse" "esp_adc" "esp_eth" "esp_hw_support" "esp_netif" "esp_rom" + "nvs_flash" "spi_flash" "mbedtls" "bt" "app_update" "ulp" "fatfs") +if(CONFIG_TOIT_ENABLE_SPI_TARGET AND CONFIG_SOC_CACHE_INTERNAL_MEM_VIA_L1CACHE) + list(APPEND TOIT_COMPONENT_REQUIRES "esp_mm") +endif() + # Register without sources, this will create an INTERFACE lib and we can then specify link options later idf_component_register(INCLUDE_DIRS "${TOIT_BASE_DIR}/src" "${TOIT_BASE_DIR}/include" - REQUIRES "efuse" "esp_adc" "esp_eth" "esp_hw_support" "esp_mm" "esp_netif" "esp_rom" "nvs_flash" "spi_flash" "mbedtls" "bt" "app_update" "ulp" "fatfs") + REQUIRES ${TOIT_COMPONENT_REQUIRES}) cmake_policy(SET CMP0079 NEW) @@ -24,6 +31,9 @@ add_subdirectory(${TOIT_BASE_DIR} toit EXCLUDE_FROM_ALL) # Add the include directories from the required components to the imported toit_vm target. target_include_directories(toit_vm PRIVATE "$") +if(CONFIG_TOIT_ENABLE_SPI_TARGET AND CONFIG_SOC_CACHE_INTERNAL_MEM_VIA_L1CACHE) + target_include_directories(toit_vm PRIVATE "$") +endif() # idf cmake does not honor the CMAKE_BUILD_TARGET, so we can not use the CMAKE_C(XX)?_FLAGS. # Add the required flags and defines with target_compile_* commands. From 74e6b5970b71041b29b70d66a3c84174ad5299b6 Mon Sep 17 00:00:00 2001 From: Florian Loitsch Date: Thu, 10 Sep 2026 00:11:11 +0200 Subject: [PATCH 06/10] Add separate SPI buffer target build flag --- src/resources/spi_esp32.cc | 32 +++++++++++++++++++++++--- toolchains/esp32/sdkconfig | 1 + toolchains/esp32c3/sdkconfig | 1 + toolchains/esp32c6/sdkconfig | 1 + toolchains/esp32p4/sdkconfig | 1 + toolchains/esp32s2/sdkconfig | 1 + toolchains/esp32s3/sdkconfig | 1 + toolchains/idf/components/toit/Kconfig | 12 +++++++++- 8 files changed, 46 insertions(+), 4 deletions(-) diff --git a/src/resources/spi_esp32.cc b/src/resources/spi_esp32.cc index d44f27db1..d2a95f754 100644 --- a/src/resources/spi_esp32.cc +++ b/src/resources/spi_esp32.cc @@ -21,17 +21,17 @@ #include #ifdef CONFIG_TOIT_ENABLE_SPI_TARGET #include +#include #include #if SOC_CACHE_INTERNAL_MEM_VIA_L1CACHE +#ifdef CONFIG_TOIT_ENABLE_SPI_BUFFER_TARGET #include +#endif #include #include #endif #endif #include -#ifdef CONFIG_TOIT_ENABLE_SPI_TARGET -#include -#endif #include "../objects_inline.h" #include "../process.h" @@ -75,10 +75,12 @@ static uint8_t* allocate_dma_buffer(size_t size, size_t alignment) { const word kSpiTargetReadyState = 1 << 0; const word kSpiTargetDoneState = 1 << 1; const int64 kSpiTargetTeardownTimeoutUs = 10 * 1000 * 1000; +#ifdef CONFIG_TOIT_ENABLE_SPI_BUFFER_TARGET const size_t kSpiBufferTargetMaxTransferSize = 4092; const word kSpiBufferTargetReceivedState = 1 << 2; const word kSpiBufferTargetStoppedState = 1 << 3; const word kSpiBufferTargetArmedState = 1 << 4; +#endif static size_t spi_dma_buffer_alignment(bool dma) { if (!dma) return 4; @@ -178,6 +180,8 @@ class SpiTargetResource : public EventQueueResource { GpioPins owned_pins_; }; +#ifdef CONFIG_TOIT_ENABLE_SPI_BUFFER_TARGET + class SpiBufferTargetResource : public EventQueueResource { public: TAG(SpiBufferTargetResource); @@ -251,6 +255,8 @@ class SpiBufferTargetResource : public EventQueueResource { GpioPins owned_pins_; }; +#endif // CONFIG_TOIT_ENABLE_SPI_BUFFER_TARGET + #endif // CONFIG_TOIT_ENABLE_SPI_TARGET SpiResourceGroup::SpiResourceGroup(Process* process, @@ -409,6 +415,8 @@ void SpiTargetResource::finish_operation() { operation_in_flight_ = false; } +#ifdef CONFIG_TOIT_ENABLE_SPI_BUFFER_TARGET + SpiBufferTargetResource::SpiBufferTargetResource( SpiTargetResourceGroup* group, spi_host_device_t host_device, @@ -674,6 +682,8 @@ void SpiBufferTargetResource::signal_from_isr(word event) { if (higher_was_woken == pdTRUE) portYIELD_FROM_ISR(); } +#endif // CONFIG_TOIT_ENABLE_SPI_BUFFER_TARGET + #endif // CONFIG_TOIT_ENABLE_SPI_TARGET MODULE_IMPLEMENTATION(spi, MODULE_SPI); @@ -909,6 +919,8 @@ PRIMITIVE(target_close) { return process->null_object(); } +#ifdef CONFIG_TOIT_ENABLE_SPI_BUFFER_TARGET + SPI_TARGET_ISR_ATTR static void spi_buffer_target_done_callback( spi_slave_transaction_t* transaction) { auto resource = static_cast(transaction->user); @@ -1181,6 +1193,20 @@ PRIMITIVE(buffer_target_dropped_receive_count) { return Smi::from(target->dropped_receive_count()); } +#else + +PRIMITIVE(buffer_target_create) { FAIL(UNIMPLEMENTED); } +PRIMITIVE(buffer_target_arm) { FAIL(UNIMPLEMENTED); } +PRIMITIVE(buffer_target_close) { FAIL(UNIMPLEMENTED); } +PRIMITIVE(buffer_target_get) { FAIL(UNIMPLEMENTED); } +PRIMITIVE(buffer_target_set) { FAIL(UNIMPLEMENTED); } +PRIMITIVE(buffer_target_read) { FAIL(UNIMPLEMENTED); } +PRIMITIVE(buffer_target_write) { FAIL(UNIMPLEMENTED); } +PRIMITIVE(buffer_target_receive) { FAIL(UNIMPLEMENTED); } +PRIMITIVE(buffer_target_dropped_receive_count) { FAIL(UNIMPLEMENTED); } + +#endif // CONFIG_TOIT_ENABLE_SPI_BUFFER_TARGET + PRIMITIVE(target_transfer_start) { ARGS(SpiTargetResource, resource, Blob, transmit, diff --git a/toolchains/esp32/sdkconfig b/toolchains/esp32/sdkconfig index cf706c5c7..7f15ca670 100644 --- a/toolchains/esp32/sdkconfig +++ b/toolchains/esp32/sdkconfig @@ -2162,6 +2162,7 @@ CONFIG_TOIT_ENABLE_WIFI=y CONFIG_TOIT_ENABLE_ESPNOW=y CONFIG_TOIT_ENABLE_I2C_TARGET=y CONFIG_TOIT_ENABLE_SPI_TARGET=y +CONFIG_TOIT_ENABLE_SPI_BUFFER_TARGET=y CONFIG_TOIT_SYSTEM_SOURCE="system/extensions/esp32/boot.toit" CONFIG_TOIT_SYSTEM_SOURCE_PROJECT_ROOT="system" CONFIG_RODATA_PADDING=2097152 diff --git a/toolchains/esp32c3/sdkconfig b/toolchains/esp32c3/sdkconfig index 1c8ff92af..93553004e 100644 --- a/toolchains/esp32c3/sdkconfig +++ b/toolchains/esp32c3/sdkconfig @@ -2192,6 +2192,7 @@ CONFIG_TOIT_ENABLE_WIFI=y CONFIG_TOIT_ENABLE_ESPNOW=y CONFIG_TOIT_ENABLE_I2C_TARGET=y CONFIG_TOIT_ENABLE_SPI_TARGET=y +CONFIG_TOIT_ENABLE_SPI_BUFFER_TARGET=y CONFIG_TOIT_SYSTEM_SOURCE="system/extensions/esp32/boot.toit" CONFIG_TOIT_SYSTEM_SOURCE_PROJECT_ROOT="system" CONFIG_RODATA_PADDING=2097152 diff --git a/toolchains/esp32c6/sdkconfig b/toolchains/esp32c6/sdkconfig index 0c2d32cae..0690c1e35 100644 --- a/toolchains/esp32c6/sdkconfig +++ b/toolchains/esp32c6/sdkconfig @@ -2370,6 +2370,7 @@ CONFIG_TOIT_ENABLE_WIFI=y CONFIG_TOIT_ENABLE_ESPNOW=y CONFIG_TOIT_ENABLE_I2C_TARGET=y CONFIG_TOIT_ENABLE_SPI_TARGET=y +CONFIG_TOIT_ENABLE_SPI_BUFFER_TARGET=y CONFIG_TOIT_SYSTEM_SOURCE="system/extensions/esp32/boot.toit" CONFIG_TOIT_SYSTEM_SOURCE_PROJECT_ROOT="system" CONFIG_RODATA_PADDING=2097152 diff --git a/toolchains/esp32p4/sdkconfig b/toolchains/esp32p4/sdkconfig index 2bd05a3de..e6f6cddb7 100644 --- a/toolchains/esp32p4/sdkconfig +++ b/toolchains/esp32p4/sdkconfig @@ -2267,6 +2267,7 @@ CONFIG_TOIT_ENABLE_WIFI=y CONFIG_TOIT_ENABLE_ESPNOW=y CONFIG_TOIT_ENABLE_I2C_TARGET=y CONFIG_TOIT_ENABLE_SPI_TARGET=y +CONFIG_TOIT_ENABLE_SPI_BUFFER_TARGET=y CONFIG_TOIT_SPIRAM_HEAP=y CONFIG_TOIT_SYSTEM_SOURCE="system/extensions/esp32/boot.toit" CONFIG_TOIT_SYSTEM_SOURCE_PROJECT_ROOT="system" diff --git a/toolchains/esp32s2/sdkconfig b/toolchains/esp32s2/sdkconfig index 74472f7fa..067be08b1 100644 --- a/toolchains/esp32s2/sdkconfig +++ b/toolchains/esp32s2/sdkconfig @@ -2036,6 +2036,7 @@ CONFIG_TOIT_ENABLE_WIFI=y CONFIG_TOIT_ENABLE_ESPNOW=y CONFIG_TOIT_ENABLE_I2C_TARGET=y CONFIG_TOIT_ENABLE_SPI_TARGET=y +CONFIG_TOIT_ENABLE_SPI_BUFFER_TARGET=y # CONFIG_TOIT_SPIRAM_HEAP is not set CONFIG_TOIT_SYSTEM_SOURCE="system/extensions/esp32/boot.toit" CONFIG_TOIT_SYSTEM_SOURCE_PROJECT_ROOT="system" diff --git a/toolchains/esp32s3/sdkconfig b/toolchains/esp32s3/sdkconfig index b4b06527b..d3d6ef92c 100644 --- a/toolchains/esp32s3/sdkconfig +++ b/toolchains/esp32s3/sdkconfig @@ -2467,6 +2467,7 @@ CONFIG_TOIT_ENABLE_WIFI=y CONFIG_TOIT_ENABLE_ESPNOW=y CONFIG_TOIT_ENABLE_I2C_TARGET=y CONFIG_TOIT_ENABLE_SPI_TARGET=y +CONFIG_TOIT_ENABLE_SPI_BUFFER_TARGET=y CONFIG_TOIT_SPIRAM_HEAP=y CONFIG_TOIT_SYSTEM_SOURCE="system/extensions/esp32/boot.toit" CONFIG_TOIT_SYSTEM_SOURCE_PROJECT_ROOT="system" diff --git a/toolchains/idf/components/toit/Kconfig b/toolchains/idf/components/toit/Kconfig index bf3f3c9da..3b0e8dfe6 100644 --- a/toolchains/idf/components/toit/Kconfig +++ b/toolchains/idf/components/toit/Kconfig @@ -229,11 +229,21 @@ menu "Toit" boolean "Enable SPI target support on ESP32" default "y" help - SPI target and buffer-target support in the Toit binary. + SPI target support in the Toit binary. Disable this option to reduce firmware size when only SPI controller functionality is needed. + config TOIT_ENABLE_SPI_BUFFER_TARGET + boolean "Enable buffered SPI target support on ESP32" + default "y" + depends on TOIT_ENABLE_SPI_TARGET + help + Autonomous buffered SPI target support in the Toit binary. + + Disable this option to reduce firmware size while retaining + the transaction-at-a-time SPI target. + config TOIT_SPIRAM_HEAP boolean "Enable Toit heap in SPIRAM" default "y" From a18e1464bd85cbf109fbf58fc420c2f115c77a88 Mon Sep 17 00:00:00 2001 From: Florian Loitsch Date: Thu, 10 Sep 2026 23:30:00 +0200 Subject: [PATCH 07/10] Share I2C target resource infrastructure --- lib/i2c.toit | 16 +-- src/resources/i2c_esp32.cc | 230 +++++++++++++++++++++---------------- 2 files changed, 141 insertions(+), 105 deletions(-) diff --git a/lib/i2c.toit b/lib/i2c.toit index 783debb20..4df81f28c 100644 --- a/lib/i2c.toit +++ b/lib/i2c.toit @@ -63,6 +63,12 @@ CONTROLLER-RESULT-OK_ ::= 0 CONTROLLER-RESULT-NACK_ ::= 1 CONTROLLER-RESULT-TIMEOUT_ ::= 2 +validate-target-address_ address/int address-bit-size/int broadcast/bool -> none: + if address-bit-size != 7 and address-bit-size != 10: throw "INVALID_ARGUMENT" + limit := (1 << address-bit-size) - 1 + if not 0 <= address <= limit: throw "INVALID_ARGUMENT" + if broadcast and address-bit-size == 10: throw "INVALID_ARGUMENT" + /** An addressable I2C target. @@ -117,10 +123,7 @@ class Target: --default-response/ByteArray?=null --pull-up/bool=false --broadcast/bool=false: - if address-bit-size != 7 and address-bit-size != 10: throw "INVALID_ARGUMENT" - limit := (1 << address-bit-size) - 1 - if not 0 <= address <= limit: throw "INVALID_ARGUMENT" - if broadcast and address-bit-size == 10: throw "INVALID_ARGUMENT" + validate-target-address_ address address-bit-size broadcast if send-buffer-size <= 0 or receive-buffer-size <= 0: throw "INVALID_ARGUMENT" response := default-response or ByteArray MAX-DEFAULT-RESPONSE-SIZE --initial=0xff if response.size == 0 or response.size > MAX-DEFAULT-RESPONSE-SIZE: @@ -348,10 +351,7 @@ class RegisterTarget: --receive-buffer-size/int=DEFAULT-TARGET-BUFFER-SIZE --pull-up/bool=false --broadcast/bool=false: - if address-bit-size != 7 and address-bit-size != 10: throw "INVALID_ARGUMENT" - address-limit := (1 << address-bit-size) - 1 - if not 0 <= address <= address-limit: throw "INVALID_ARGUMENT" - if broadcast and address-bit-size == 10: throw "INVALID_ARGUMENT" + validate-target-address_ address address-bit-size broadcast if register-address-byte-size != 1 and register-address-byte-size != 2: throw "INVALID_ARGUMENT" register-limit := 1 << (register-address-byte-size * 8) if not 0 < register-count <= register-limit: throw "INVALID_ARGUMENT" diff --git a/src/resources/i2c_esp32.cc b/src/resources/i2c_esp32.cc index 179eade34..6301e8788 100644 --- a/src/resources/i2c_esp32.cc +++ b/src/resources/i2c_esp32.cc @@ -81,30 +81,58 @@ class I2cTargetResourceGroup : public ResourceGroup { } }; -class I2cTargetResource : public EventQueueResource { +struct I2cTargetConfig { + int sda; + int scl; + i2c_addr_bit_len_t address_length; + uint16_t address; + uint32_t send_buffer_size; + uint32_t receive_buffer_size; + bool pullup; + bool allow_power_down; + bool broadcast; +}; + +class I2cTargetResourceBase : public EventQueueResource { + public: + I2cTargetResourceBase(ResourceGroup* group, QueueHandle_t event_queue) + : EventQueueResource(group, event_queue) {} + ~I2cTargetResourceBase() override; + + i2c_slave_dev_handle_t handle() const { return handle_; } + GpioPins& owned_pins() { return owned_pins_; } + + esp_err_t initialize_driver( + const I2cTargetConfig& config, + const i2c_slave_event_callbacks_t& callbacks, + void* callback_context); + + protected: + void release_driver(); + + private: + i2c_slave_dev_handle_t handle_ = null; + GpioPins owned_pins_; +}; + +class I2cTargetResource : public I2cTargetResourceBase { public: TAG(I2cTargetResource); I2cTargetResource(I2cTargetResourceGroup* group, - i2c_slave_dev_handle_t handle, QueueHandle_t event_queue, MessageBufferHandle_t receive_buffer) - : EventQueueResource(group, event_queue) - , handle_(handle) + : I2cTargetResourceBase(group, event_queue) , receive_buffer_(receive_buffer) { spinlock_initialize(&spinlock_); } ~I2cTargetResource() override { - ESP_ERROR_CHECK(i2c_del_slave_device(handle_)); + release_driver(); vMessageBufferDeleteWithCaps(receive_buffer_); - vQueueDeleteWithCaps(queue()); - owned_pins_.release(); } - i2c_slave_dev_handle_t handle() const { return handle_; } MessageBufferHandle_t receive_buffer() const { return receive_buffer_; } - GpioPins& owned_pins() { return owned_pins_; } I2C_IRAM_ATTR bool receive_from_isr(const uint8_t* data, size_t length) { BaseType_t higher_was_woken = pdFALSE; @@ -174,26 +202,22 @@ class I2cTargetResource : public EventQueueResource { xQueueSendFromISR(queue(), &payload, higher_was_woken); } - i2c_slave_dev_handle_t handle_; MessageBufferHandle_t receive_buffer_; mutable spinlock_t spinlock_; word pending_event_ = 0; word request_count_ = 0; word dropped_receive_count_ = 0; - GpioPins owned_pins_; }; -class I2cRegisterTargetResource : public EventQueueResource { +class I2cRegisterTargetResource : public I2cTargetResourceBase { public: TAG(I2cRegisterTargetResource); I2cRegisterTargetResource(I2cResourceGroup* group, - i2c_slave_dev_handle_t handle, uint8_t* registers, uint32_t register_count, uint32_t register_address_byte_size) - : EventQueueResource(group, null) - , handle_(handle) + : I2cTargetResourceBase(group, null) , registers_(registers) , register_count_(register_count) , register_address_byte_size_(register_address_byte_size) { @@ -201,9 +225,8 @@ class I2cRegisterTargetResource : public EventQueueResource { } ~I2cRegisterTargetResource() override { - ESP_ERROR_CHECK(i2c_del_slave_device(handle_)); + release_driver(); free(registers_); - owned_pins_.release(); } I2C_IRAM_ATTR void receive_from_isr(const uint8_t* data, size_t length, bool overflow) { @@ -280,7 +303,6 @@ class I2cRegisterTargetResource : public EventQueueResource { } uint32_t register_count() const { return register_count_; } - GpioPins& owned_pins() { return owned_pins_; } private: uint8_t load_register(uint32_t index) const { @@ -309,7 +331,6 @@ class I2cRegisterTargetResource : public EventQueueResource { portEXIT_CRITICAL_ISR(&spinlock_); } - i2c_slave_dev_handle_t handle_; uint8_t* registers_; uint8_t transmit_buffer_[SOC_I2C_FIFO_LEN]; uint32_t register_count_; @@ -318,9 +339,51 @@ class I2cRegisterTargetResource : public EventQueueResource { uint32_t prefetch_pointer_ = 0; mutable spinlock_t spinlock_; word dropped_write_count_ = 0; - GpioPins owned_pins_; }; +I2cTargetResourceBase::~I2cTargetResourceBase() { + ASSERT(handle_ == null); + if (queue() != null) vQueueDeleteWithCaps(queue()); + owned_pins_.release(); +} + +esp_err_t I2cTargetResourceBase::initialize_driver( + const I2cTargetConfig& config, + const i2c_slave_event_callbacks_t& callbacks, + void* callback_context) { + ASSERT(handle_ == null); + i2c_slave_config_t driver_config = { + .i2c_port = -1, + .sda_io_num = static_cast(config.sda), + .scl_io_num = static_cast(config.scl), + .clk_source = I2C_CLK_SRC_DEFAULT, + .send_buf_depth = config.send_buffer_size, + .receive_buf_depth = config.receive_buffer_size, + .slave_addr = config.address, + .addr_bit_len = config.address_length, + .intr_priority = 0, + .flags = { + .allow_pd = config.allow_power_down, + .enable_internal_pullup = config.pullup, + #if SOC_I2C_SLAVE_SUPPORT_BROADCAST + .broadcast_en = config.broadcast, + #endif + }, + }; + i2c_slave_dev_handle_t handle; + esp_err_t err = i2c_new_slave_device(&driver_config, &handle); + if (err != ESP_OK) return err; + handle_ = handle; + return i2c_slave_register_event_callbacks( + handle_, &callbacks, callback_context); +} + +void I2cTargetResourceBase::release_driver() { + if (handle_ == null) return; + ESP_ERROR_CHECK(i2c_del_slave_device(handle_)); + handle_ = null; +} + #endif // CONFIG_TOIT_ENABLE_I2C_TARGET class I2cBusResource; @@ -526,6 +589,23 @@ I2C_IRAM_ATTR static bool target_request_handler( return resource->request_from_isr(); } +static bool decode_target_address( + int address_bit_size, + uint16_t address, + i2c_addr_bit_len_t* address_length) { + if (address_bit_size == 7 && address <= 0x7f) { + *address_length = I2C_ADDR_BIT_LEN_7; + return true; + } +#if SOC_I2C_SUPPORT_10BIT_ADDR + if (address_bit_size == 10 && address <= 0x3ff) { + *address_length = I2C_ADDR_BIT_LEN_10; + return true; + } +#endif + return false; +} + PRIMITIVE(target_create) { ARGS(I2cTargetResourceGroup, group, int, sda, @@ -546,15 +626,8 @@ PRIMITIVE(target_create) { } i2c_addr_bit_len_t address_length; - if (address_bit_size == 7 && address <= 0x7f) { - address_length = I2C_ADDR_BIT_LEN_7; - #if SOC_I2C_SUPPORT_10BIT_ADDR - } else if (address_bit_size == 10 && address <= 0x3ff) { - address_length = I2C_ADDR_BIT_LEN_10; - #endif - } else { - FAIL(INVALID_ARGUMENT); - } + if (!decode_target_address( + address_bit_size, address, &address_length)) FAIL(INVALID_ARGUMENT); #if !SOC_I2C_SLAVE_SUPPORT_BROADCAST if (broadcast) FAIL(UNSUPPORTED); @@ -587,50 +660,35 @@ PRIMITIVE(target_create) { [&] { if (!handed_to_resource) vMessageBufferDeleteWithCaps(receive_buffer); } }; - i2c_slave_config_t config = { - .i2c_port = -1, - .sda_io_num = static_cast(sda_num), - .scl_io_num = static_cast(scl_num), - .clk_source = I2C_CLK_SRC_DEFAULT, - .send_buf_depth = send_buffer_size, - .receive_buf_depth = receive_buffer_size, - .slave_addr = address, - .addr_bit_len = address_length, - .intr_priority = 0, - .flags = { - .allow_pd = allow_power_down, - .enable_internal_pullup = pullup, - #if SOC_I2C_SLAVE_SUPPORT_BROADCAST - .broadcast_en = broadcast, - #endif - }, - }; - - i2c_slave_dev_handle_t handle; - esp_err_t err = i2c_new_slave_device(&config, &handle); - if (err == ESP_ERR_NOT_FOUND) FAIL(ALREADY_IN_USE); - if (err != ESP_OK) return Primitive::os_error(err, process); - Defer delete_target { - [&] { if (!handed_to_resource) i2c_del_slave_device(handle); } - }; - - auto resource = _new I2cTargetResource(group, handle, event_queue, receive_buffer); + auto resource = _new I2cTargetResource(group, event_queue, receive_buffer); if (resource == null) FAIL(MALLOC_FAILED); handed_to_resource = true; bool registered = false; Defer delete_resource { [&] { if (!registered) delete resource; } }; + I2cTargetConfig config = { + .sda = sda_num, + .scl = scl_num, + .address_length = address_length, + .address = address, + .send_buffer_size = send_buffer_size, + .receive_buffer_size = receive_buffer_size, + .pullup = pullup, + .allow_power_down = allow_power_down, + .broadcast = broadcast, + }; i2c_slave_event_callbacks_t callbacks = { .on_request = target_request_handler, .on_receive = target_receive_handler, .on_transmit = null, .on_transmit_done = null, }; - err = i2c_slave_register_event_callbacks(handle, &callbacks, resource); + esp_err_t err = resource->initialize_driver(config, callbacks, resource); + if (err == ESP_ERR_NOT_FOUND) FAIL(ALREADY_IN_USE); if (err != ESP_OK) return Primitive::os_error(err, process); err = i2c_slave_set_default_response( - handle, default_response.address(), default_response.length()); + resource->handle(), default_response.address(), default_response.length()); if (err != ESP_OK) return Primitive::os_error(err, process); resource->owned_pins().adopt(reserver); @@ -759,15 +817,8 @@ PRIMITIVE(register_target_create) { if (register_count > addressable_register_count) FAIL(INVALID_ARGUMENT); i2c_addr_bit_len_t address_length; - if (address_bit_size == 7 && address <= 0x7f) { - address_length = I2C_ADDR_BIT_LEN_7; - #if SOC_I2C_SUPPORT_10BIT_ADDR - } else if (address_bit_size == 10 && address <= 0x3ff) { - address_length = I2C_ADDR_BIT_LEN_10; - #endif - } else { - FAIL(INVALID_ARGUMENT); - } + if (!decode_target_address( + address_bit_size, address, &address_length)) FAIL(INVALID_ARGUMENT); #if !SOC_I2C_SLAVE_SUPPORT_BROADCAST if (broadcast) FAIL(UNSUPPORTED); @@ -792,47 +843,32 @@ PRIMITIVE(register_target_create) { int scl_num = reserver.decode_and_take(scl, &reserve_ok); if (!reserve_ok) FAIL(ALREADY_IN_USE); - i2c_slave_config_t config = { - .i2c_port = -1, - .sda_io_num = static_cast(sda_num), - .scl_io_num = static_cast(scl_num), - .clk_source = I2C_CLK_SRC_DEFAULT, - .send_buf_depth = SOC_I2C_FIFO_LEN, - .receive_buf_depth = receive_buffer_size, - .slave_addr = address, - .addr_bit_len = address_length, - .intr_priority = 0, - .flags = { - .allow_pd = allow_power_down, - .enable_internal_pullup = pullup, - #if SOC_I2C_SLAVE_SUPPORT_BROADCAST - .broadcast_en = broadcast, - #endif - }, - }; - - i2c_slave_dev_handle_t handle; - esp_err_t err = i2c_new_slave_device(&config, &handle); - if (err == ESP_ERR_NOT_FOUND) FAIL(ALREADY_IN_USE); - if (err != ESP_OK) return Primitive::os_error(err, process); - Defer delete_target { - [&] { if (!handed_to_resource) i2c_del_slave_device(handle); } - }; - auto resource = _new I2cRegisterTargetResource( - group, handle, registers, register_count, register_address_byte_size); + group, registers, register_count, register_address_byte_size); if (resource == null) FAIL(MALLOC_FAILED); handed_to_resource = true; bool registered = false; Defer delete_resource { [&] { if (!registered) delete resource; } }; + I2cTargetConfig config = { + .sda = sda_num, + .scl = scl_num, + .address_length = address_length, + .address = address, + .send_buffer_size = SOC_I2C_FIFO_LEN, + .receive_buffer_size = receive_buffer_size, + .pullup = pullup, + .allow_power_down = allow_power_down, + .broadcast = broadcast, + }; i2c_slave_event_callbacks_t callbacks = { .on_request = null, .on_receive = register_target_receive_handler, .on_transmit = register_target_transmit_handler, .on_transmit_done = register_target_transmit_done_handler, }; - err = i2c_slave_register_event_callbacks(handle, &callbacks, resource); + esp_err_t err = resource->initialize_driver(config, callbacks, resource); + if (err == ESP_ERR_NOT_FOUND) FAIL(ALREADY_IN_USE); if (err != ESP_OK) return Primitive::os_error(err, process); resource->owned_pins().adopt(reserver); From a87ef7b539dcd199b09d12afbc08c173e32447f3 Mon Sep 17 00:00:00 2001 From: Florian Loitsch Date: Fri, 11 Sep 2026 00:43:55 +0200 Subject: [PATCH 08/10] Remove I2C and SPI tracking notes --- tests/hw/esp32/i2c-spi-tracking.md | 199 ----------------------------- 1 file changed, 199 deletions(-) delete mode 100644 tests/hw/esp32/i2c-spi-tracking.md diff --git a/tests/hw/esp32/i2c-spi-tracking.md b/tests/hw/esp32/i2c-spi-tracking.md deleted file mode 100644 index 593f3cac4..000000000 --- a/tests/hw/esp32/i2c-spi-tracking.md +++ /dev/null @@ -1,199 +0,0 @@ -# I2C and SPI hardware-test tracking - -This document tracks the remaining verification work for the asynchronous I2C -and SPI controller/target stack. A box is checked only after the test has run on -the hardware named in the entry with the current code and ESP-IDF submodule. - -## Refactor review follow-up - -- [x] Restack the I2C and SPI branches so every intermediate PR builds and its - tests use only APIs introduced at or below that point in the stack. -- [x] Compile out I2C and SPI target implementations and private dependencies - when their `CONFIG_TOIT_ENABLE_*_TARGET` option is disabled. -- [x] Serialize default-response replacement with controller reads and cover - both callback-backed and buffered transmit arbitration in ESP-IDF tests. -- [x] Make constructor rollback, controller abort, close, and bus-reservation - release non-cancelable where partial cleanup would leave native state live. -- [x] Remove the register-target C++ data race while documenting byte-level, - rather than transaction-level, atomicity. -- [x] Bound target response handlers and test timeout fallback followed by a - successful later handler response. -- [x] Attempt bounded physical I2C bus recovery after retiring an aborted - controller transaction. -- [x] Keep the ESP-IDF fork changes local and narrowly scoped; remain on the - 5.4 API rather than combining the refactor with an ESP-IDF 6 migration. -- [x] Add ESP-IDF stress coverage for default-response arbitration, - callback-backed target reads, and abort/reuse of SPI target descriptors. -- [x] Restore `CONFIG_SPI_MASTER_ISR_IN_IRAM` and link the classic ESP32 - default envelope with both target implementations enabled. -- [x] Build the classic ESP32 envelope with both target implementations - disabled to verify the configuration guards. -- [x] Make the conditional cache-maintenance dependency match the SoCs that - actually require it. -- [x] Run the updated I2C/SPI hardware matrix twice on ESP32 and ESP32-S3. -- [x] Commit the review fixes at the correct levels of the stack and update all - affected PR branches. - -## Completion criteria - -- All relevant sources build for ESP32 and ESP32-S3. -- The complete I2C/SPI CTest selection passes twice on each variant: once after - flashing the firmware and once without rebooting the boards. -- SPI CS setup and hold behavior is checked with an independent RMT probe. -- Expected bounded-queue overflow is distinguished from driver data loss. -- Controller operations, target waits, and abort/close paths do not block a - Toit primitive. -- Retryable allocation failures leave no native resources behind and do not - report OOM asynchronously. -- Recoverable electrical faults and transaction aborts leave the peripheral - reusable. - -## Work items - -### 1. Clean baseline - -- [x] Build the host SDK. -- [x] Build the ESP32 firmware envelope. -- [x] Build the ESP32-S3 firmware envelope. -- [x] Run `git diff --check` and analyze the changed Toit tests. -- [x] Run all I2C/SPI tests once on ESP32. -- [x] Run all I2C/SPI tests once on ESP32-S3. - -### 2. SPI CS timing - -- [x] Capture CS and SCLK independently with RMT at 100 kHz. -- [x] Verify that each transfer contains exactly 32 valid SCLK pulses. -- [x] Verify every setup-cycle value from 0 through 16 on ESP32-S3. -- [x] Verify every hold-cycle value from 0 through 16 on ESP32-S3. -- [x] Verify every setup-cycle value from 0 through 16 on classic ESP32 after marking - single-data-line controller devices as half-duplex. -- [x] Fix classic ESP32 hold-cycle programming and verify every representable - value from 0 through 15. Reject 16 in the Toit API because the hardware field - is only four bits wide. -- [x] Compare the relevant SPI LL implementation with recent ESP-IDF master. - Master has the same incomplete programming and documents 0 through 16 even - though its ESP32-specific test limits the maximum to 15. -- [x] Make the timing test assert every supported value on both variants. - -### 3. Allocation-failure behavior - -- [x] Inventory every allocation in the new I2C and SPI primitives and classify - it as synchronous/retryable or callback/ISR-time. -- [x] Exercise retryable OOM cleanup using deterministic allocation-failure - injection where the runtime supports it. -- [x] Verify that retrying target initialization does not leak pins, - peripherals, event resources, DMA memory, or ESP-IDF handles. -- [x] Verify that retrying controller construction and transfer setup does not leak buses, devices, - event resources, DMA memory, or ESP-IDF handles. -- [x] Verify that callbacks and completion-status primitives never synthesize - an asynchronous OOM. - -The runtime has no allocation-site failure injector. The hardware OOM test -therefore requests allocations that cannot succeed. Each primitive is retried -four times by the VM; a leaked pin or peripheral would make a later attempt -fail with `ALREADY_IN_USE` instead of the expected retryable OOM. A normal -construction on the same pins immediately afterwards verifies final cleanup. - -| Allocation site | Classification and cleanup | -| --- | --- | -| I2C target and register-target construction | Proxy first; all queues, buffers, driver handles, and resources are allocated synchronously. Scoped cleanup owns each partial state until the registered resource destructor takes ownership. | -| I2C controller bus/device construction | Proxy and Toit resource first. Queue/driver handles are released on every later failure; devices unlink themselves from the bus if callback registration fails. | -| I2C probe/read/write/write-read setup | Persistent address, TX, and RX buffers are allocated before publishing the in-flight operation. RX failure frees TX; dispatch failure retires all buffers. | -| SPI target and buffer-target construction | Proxy first; host slot, event queue, DMA storage, index arrays, resource, and driver are acquired in order with scoped cleanup or destructor ownership at every boundary. | -| SPI target/controller transfer setup | TX and RX buffers are allocated before the descriptor is queued. Second-buffer and queue failures free all partial state synchronously. | -| ISR callbacks | Allocation-free: they only update preallocated descriptors, counters, queues, and event bits. Re-arm errors are reported as driver state, never OOM. | -| Completion/status primitives | Copy into memory allocated by Toit before native state is consumed, then release native buffers. No completion path maps an error to OOM. | - -### 4. Fault injection and stress - -- [x] I2C: hit the configured in-transaction SCL timeout, and separately hold - SCL low before START to verify deadline abort and recovery on the same bus. -- [x] I2C: interrupt a transaction at FIFO boundaries and verify that the next - read/write succeeds. -- [x] I2C: repeat target queue overflow and oversized-transaction overflow and - check exact dropped counters. -- [x] SPI: abort idle and active target transactions repeatedly with DMA both - enabled and disabled. -- [x] SPI: release CS after representative byte and non-byte-aligned clock - counts, then verify target reuse. -- [x] SPI: switch ESP32-S3 mode-2 targets between full-duplex, receive-only, - and transmit-only configurations and verify that neither direction shifts by - one bit. -- [x] SPI: repeat buffered-target queue overflow and check exact ordering and - dropped counters. -- [x] Run a longer mixed-operation stress loop on each chip variant. - -The recoverable external-SCL case deliberately holds SCL before START and uses -a Toit task deadline: `Device.timeout-us` is a hardware limit on an SCL-low -interval after a transaction has started, not a deadline for waiting for an -idle bus. The separate in-transaction clock-stretch test verifies -`I2C_TIMEOUT`. Both paths abort and reuse the same controller bus. FIFO -boundaries are covered on both sides of 32 bytes and by reads through 1,024 -bytes. The contention loops and three repetitions of every abort/close and -overflow scenario provide the mixed-operation stress pass. - -### 5. Final matrix - -- [x] ESP32 first run after setup: all I2C/SPI tests pass. -- [x] ESP32 immediate second run: all I2C/SPI tests pass. -- [x] ESP32-S3 first run after setup: all I2C/SPI tests pass. -- [x] ESP32-S3 immediate second run: all I2C/SPI tests pass. -- [x] Record commit IDs for Toit and ESP-IDF below. -- [x] Commit the tests and fixes on the appropriate stacked branches and push - every affected child branch. - -## Existing coverage - -| Area | Covered cases | -| --- | --- | -| I2C target | 7-bit and 10-bit addresses, direct read/write, combined write-read, FIFO-boundary sizes, dynamic response, clock stretching, response-block exceptions and non-local returns, bounded receive queues, oversized transactions, broadcast, close/reconfigure | -| I2C register target | 8-bit and 16-bit register addresses, wraparound, reads larger than the FIFO, pointer continuation, live updates, overflow accounting, broadcast | -| I2C controller | asynchronous scheduling, contention, 50/100/400 kHz operation, NACK, timeout, address-width collision, clock-stretch recovery, invalid arguments | -| SPI target | modes 0-3, transmit/receive/full duplex, direction changes between target instances, MSB/LSB order, DMA and non-DMA, 50 kHz through 5 MHz, sizes 1 through 4092, idle/active abort, close and reuse | -| SPI buffer target | native response updates, fill byte, bounded queues, maximum DMA transfer, partial classic-DMA receive, wait timeout, active close and reuse | -| SPI controller | modes 0-3, full-duplex loopback, asynchronous transfer, bus reservation, keep-CS-active ownership | - -## Evidence log - -| Date | Toit commit/worktree | ESP-IDF commit | Variant | Command/test | Result | -| --- | --- | --- | --- | --- | --- | -| 2026-09-02 | `b010aac9` plus local changes | `676c30742c` | ESP32-S3 | `i2c-target-board1.toit` | Pass after target TX-empty refill fix and explicit overflow-test handshake | -| 2026-09-02 | `b010aac9` plus local changes | `676c30742c` | ESP32-S3 | SPI CS timing probe | Setup and hold deltas matched 0, 1, 8, and 16 requested cycles | -| 2026-09-02 | `b010aac9` plus local changes | `676c30742c` | ESP32 | SPI CS timing probe | Setup matched; hold values above 1 exposed unresolved saturation/wrap behavior | -| 2026-09-03 | `b010aac9` plus local changes | `676c30742c` | Host, ESP32, ESP32-S3 | Build, `toit analyze -Werror`, `git diff --check` | Pass | -| 2026-09-03 | `b010aac9` plus local changes | `676c30742c` | ESP32 | Full I2C/SPI CTest selection, fresh setup | 14/14 pass in 125.58 s | -| 2026-09-03 | `b010aac9` plus local changes | `676c30742c` | ESP32-S3 | Full I2C/SPI CTest selection, fresh setup | 14/14 pass in 122.04 s | -| 2026-09-03 | `b010aac9` plus local changes | local CS-hold fix on `676c30742c` | ESP32 | SPI CS timing probe at 100 kHz | Setup 0-16 and hold 0-15 pass; each requested cycle adds 10 us; hold 16 rejected | -| 2026-09-03 | `b010aac9` plus local changes | `676c30742c` | ESP32-S3 | SPI CS timing probe at 100 kHz | Setup and hold 0-16 pass; each requested cycle adds 10 us | -| 2026-09-03 | `b010aac9` plus local changes | `676c30742c` | ESP32 | `i2c-spi-oom-test.toit` | Pass: I2C/SPI target construction and controller transfer setup survive four retryable native OOM attempts; buses, devices, and pins are immediately reusable | -| 2026-09-03 | `b010aac9` plus local changes | `676c30742c` | ESP32-S3 | `i2c-spi-oom-test.toit` | Pass: I2C/SPI target construction (including register target) and controller transfer setup survive four retryable native OOM attempts; resources are reusable | -| 2026-09-03 | `b010aac9` plus local changes | `54f5e254e2` | ESP32 | Focused fault/timing set | 9/9 pass, including repeated target abort/close, exact queue overflow, non-byte CS termination, external-SCL deadline recovery, OOM, and CS setup 0-16/hold 0-15 | -| 2026-09-03 | `b010aac9` plus local changes | `54f5e254e2` | ESP32-S3 | Focused fault/timing set | 9/9 pass, including repeated target abort/close, exact queue overflow, non-byte CS termination, external-SCL deadline recovery, OOM, and CS setup/hold 0-16 | -| 2026-09-03 | `b010aac9` plus local changes | `54f5e254e2` | ESP32 | Full I2C/SPI matrix after setup | 15/15 pass in 132.38 s | -| 2026-09-03 | `b010aac9` plus local changes | `54f5e254e2` | ESP32 | Immediate full matrix with setup fixtures excluded | 12/12 pass in 77.88 s | -| 2026-09-03 | `b010aac9` plus local changes | `54f5e254e2` | ESP32-S3 | Full I2C/SPI matrix after setup | 15/15 pass in 128.64 s | -| 2026-09-03 | `b010aac9` plus local changes | `54f5e254e2` | ESP32-S3 | Immediate full matrix with setup fixtures excluded | 12/12 pass in 72.75 s | -| 2026-09-03 | `b010aac9` plus follow-up changes | `817726ca57` | ESP32-S3 | `spi-target-board1.toit` | Pass: mode 2 full-duplex, receive-only, and transmit-only run consecutively without one-bit shifts; complete target matrix passes in 10.23 s | -| 2026-09-03 | `b010aac9` plus follow-up changes | `817726ca57` | ESP32-S3 | `spi-buffer-target-board1.toit` | Pass: continuously armed full-duplex modes 0-3, bit order, overflow, DMA limits, async controller, and active close in 7.57 s | -| 2026-09-03 | `b010aac9` plus follow-up changes | `817726ca57` | ESP32 | Full I2C/SPI matrix after setup | 15/15 pass in 130.79 s; a second complete setup run also passed 15/15 in 135.42 s | -| 2026-09-03 | `b010aac9` plus follow-up changes | `817726ca57` | ESP32 | Immediate full matrix with setup fixtures excluded | 12/12 pass in 78.13 s | -| 2026-09-03 | `b010aac9` plus follow-up changes | `817726ca57` | ESP32-S3 | Full I2C/SPI matrix after setup | 15/15 pass in 128.76 s | -| 2026-09-03 | `b010aac9` plus follow-up changes | `817726ca57` | ESP32-S3 | Immediate full matrix with setup fixtures excluded | 12/12 pass in 73.46 s | -| 2026-09-04 | `527dc816` | `817726ca57` | ESP32 | `i2c-target-board1.toit` | Pass: throwing and non-local-return response blocks close the target; target recreation and subsequent transactions succeed | -| 2026-09-04 | `527dc816` | `817726ca57` | ESP32-S3 | `i2c-target-board1.toit` | Pass: active clock stretch is released after throwing and non-local-return response blocks; target recreation and subsequent transactions succeed | -| 2026-09-05 | `b8cf906b` plus late-default regression | `c30e19e02d` | ESP32-S3 | `ctest --verbose --test-dir build/hw -C esp32s3 -R i2c-target-board1.toit-esp32s3$` | Pass: the controller began reading before the target installed its first default response; installing it released SCL and returned the expected bytes | -| 2026-09-08 | restacked tree equivalent to `bdc5cad1` | `fc94150e9d` | ESP32 | Full I2C/SPI matrix after setup | 15/15 pass in 137.22 s with I2C and SPI targets enabled and SPI master ISR in IRAM | -| 2026-09-08 | restacked tree equivalent to `bdc5cad1` | `fc94150e9d` | ESP32 | Immediate full matrix with setup fixtures excluded | 12/12 pass in 85.07 s | -| 2026-09-08 | restacked tree equivalent to `bdc5cad1` | `fc94150e9d` | ESP32-S3 | Full I2C/SPI matrix after setup | 15/15 pass in 135.91 s after replacing the one-shot inter-board startup byte with an acknowledged synchronization handshake | -| 2026-09-08 | restacked tree equivalent to `bdc5cad1` | `fc94150e9d` | ESP32-S3 | Immediate full matrix with setup fixtures excluded | 12/12 pass in 75.63 s | - -Add the exact command, result, and any captured timing to this table as each -remaining item is completed. - -## Rig observations - -During iterative S3 firmware experiments, board 1 intermittently failed the -921,600-baud synchronization before a test container was installed. A fresh -firmware flash recovered it. The final 15-test run and immediate 12-test -no-setup repetition both passed, so no SPI assertion or persistent peripheral -failure accompanied the serial symptom. From ae1979d9573b249913195ddf5945817f55202914 Mon Sep 17 00:00:00 2001 From: Florian Loitsch Date: Sun, 13 Sep 2026 00:59:14 +0200 Subject: [PATCH 09/10] Fix flaky asynchronous I2C controller test --- lib/i2c.toit | 15 +++++++-- .../hw/esp32/i2c-async-controller-shared.toit | 33 +++++++------------ 2 files changed, 24 insertions(+), 24 deletions(-) diff --git a/lib/i2c.toit b/lib/i2c.toit index 4df81f28c..5eb907d27 100644 --- a/lib/i2c.toit +++ b/lib/i2c.toit @@ -563,9 +563,18 @@ class Bus: test address --timeout-ms/int=100 -> bool: if not 0 <= address <= 0x7f: throw "INVALID_ARGUMENT" if timeout-ms <= 0: throw "INVALID_ARGUMENT" - return perform-controller-operation_ - (: i2c-bus-probe_ resource_ address timeout-ms) - (: i2c-bus-probe-finish_ resource_) + result := false + error := catch --unwind=(: it != DEADLINE-EXCEEDED-ERROR): + result = with-timeout --ms=timeout-ms: + perform-controller-operation_ + (: i2c-bus-probe_ resource_ address timeout-ms) + (: i2c-bus-probe-finish_ resource_) + if error: + // An earlier caller deadline is automatically rethrown by 'catch'. A + // deadline caught here was installed for this probe and is equivalent + // to the driver's hardware timeout. + return false + return result /** Closes this I2C bus. diff --git a/tests/hw/esp32/i2c-async-controller-shared.toit b/tests/hw/esp32/i2c-async-controller-shared.toit index 9f5ed2bfe..96e8812ee 100644 --- a/tests/hw/esp32/i2c-async-controller-shared.toit +++ b/tests/hw/esp32/i2c-async-controller-shared.toit @@ -19,6 +19,7 @@ UART-RX2 ::= Variant.CURRENT.board-connection-pin2 UART-TX2 ::= Variant.CURRENT.board-connection-pin1 I2C-SDA ::= Variant.CURRENT.board-connection-pin3 +SYNC-PIN ::= Variant.CURRENT.board-connection-pin4 I2C-SCL ::= Variant.CURRENT.board-connection-pin5 I2C-SCL-PROBE ::= Variant.CURRENT.board-connection-pin6 @@ -27,7 +28,7 @@ MISSING-ADDRESS ::= 0x71 READY ::= 0xa5 OK ::= 0x5a -SYNC ::= 0xc3 +SYNC-LOW-SEEN ::= 0xc3 SET ::= 1 RECONFIGURE ::= 2 @@ -54,7 +55,7 @@ test-board1: test-board1-esp32 port return - bus := i2c.Bus --sda=I2C-SDA --scl=I2C-SCL --frequency=100_000 --pull-up=false + bus := i2c.Bus --sda=I2C-SDA --scl=I2C-SCL --frequency=100_000 --pull-up // Probe completion exercises both DONE and NACK callbacks. Scanning repeats // this over enough transactions to catch stale completion state. @@ -535,27 +536,17 @@ send-byte port/uart.Port value/int -> none: port.out.flush synchronize-controller port/uart.Port -> none: - synchronized := monitor.Latch - sender-done := monitor.Latch - task:: - try: - while not synchronized.has-value: - send-byte port SYNC - sleep --ms=10 - finally: - critical-do --no-respect-deadline: sender-done.set true - - expect-equals READY port.in.read-byte - synchronized.set true - sender-done.get - send-byte port OK - expect-equals READY port.in.read-byte + ready := gpio.Pin SYNC-PIN --input --pull-up + while ready.get != 0: sleep --ms=1 + send-byte port SYNC-LOW-SEEN + while ready.get != 1: sleep --ms=1 + ready.close synchronize-target port/uart.Port -> none: - while port.in.read-byte != SYNC: null - send-byte port READY - while port.in.read-byte != OK: null - send-byte port READY + ready := gpio.Pin SYNC-PIN --output --value=0 + while port.in.read-byte != SYNC-LOW-SEEN: null + ready.set 1 + ready.close encode-u16 value/int -> ByteArray: return #[value & 0xff, (value >> 8) & 0xff] From 6fd74c9a9bb397a2b07f854da5c11e787a647a43 Mon Sep 17 00:00:00 2001 From: Florian Loitsch Date: Sun, 13 Sep 2026 01:01:12 +0200 Subject: [PATCH 10/10] Fix SPI target non-byte transfer test --- tests/hw/esp32/spi-target-shared.toit | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/hw/esp32/spi-target-shared.toit b/tests/hw/esp32/spi-target-shared.toit index e377e6825..00cba29e1 100644 --- a/tests/hw/esp32/spi-target-shared.toit +++ b/tests/hw/esp32/spi-target-shared.toit @@ -524,7 +524,7 @@ test-nonbyte-target port/uart.Port -> none: --max-transfer-size=8 --dma=false - result := target.exchange #[ ] + result := target.transfer #[ ] --receive-size=8 --when-armed=: port.out.write #[READY] --flush @@ -534,7 +534,7 @@ test-nonbyte-target port/uart.Port -> none: expect-equals (bit-count + 7) / 8 result.size expect-equals RESUME port.in.read-byte - result = target.exchange #[ ] + result = target.transfer #[ ] --receive-size=8 --when-armed=: port.out.write #[READY] --flush