Skip to content

Add asynchronous SPI target support - #3158

Draft
floitsch wants to merge 2 commits into
floitsch/i2c.30-async-controllerfrom
floitsch/spi.10-target
Draft

Add asynchronous SPI target support#3158
floitsch wants to merge 2 commits into
floitsch/i2c.30-async-controllerfrom
floitsch/spi.10-target

Conversation

@floitsch

@floitsch floitsch commented Aug 6, 2026

Copy link
Copy Markdown
Member

Stacked on #3156.

This introduces an ESP32 spi.Target API for transaction-sized controller/target exchanges. Native primitives only allocate, queue with a zero timeout, copy completed data, or tear down resources; waiting happens through ResourceState_ and suspends only the calling Toit task.

Highlights:

  • independent MOSI/MISO, modes 0 through 3, independent RX/TX bit order, DMA selection, fill byte, early controller termination, and configurable maximum transaction size
  • preallocated native buffers with retryable OOM cleanup before dispatch and allocation-free ISR callbacks
  • explicit PendingExchange so applications can assert a ready GPIO after the peripheral is armed
  • classic ESP32 DMA restrictions validated at both the Toit and native API boundaries
  • controller CS setup/hold options needed for target timing tests
  • two-board hardware coverage for modes, direction, bit order, DMA/non-DMA, 50 kHz through 2 MHz, sizes through 4092 bytes, fill behavior, and early termination

The required ESP-IDF fixes are in toitware/esp-idf#125, stacked on the I2C ESP-IDF series. The target API intentionally exposes complete transactions only; it does not add half-buffer streaming.

@floitsch
floitsch force-pushed the floitsch/spi.10-target branch from 2fe33e8 to 023ed83 Compare August 6, 2026 21:47
@floitsch
floitsch force-pushed the floitsch/spi.10-target branch from be56f0c to a423a38 Compare August 10, 2026 12:46
@floitsch
floitsch force-pushed the floitsch/spi.10-target branch 2 times, most recently from 73af96b to 398621d Compare August 10, 2026 14:29
@floitsch
floitsch force-pushed the floitsch/spi.10-target branch from 398621d to be780ca Compare August 10, 2026 16:11
@floitsch
floitsch force-pushed the floitsch/spi.10-target branch from be780ca to 9955d82 Compare August 10, 2026 17:59
@floitsch
floitsch force-pushed the floitsch/spi.10-target branch from 9955d82 to 707faf8 Compare August 10, 2026 20:13
@floitsch
floitsch force-pushed the floitsch/spi.10-target branch 2 times, most recently from 561deb0 to 3970550 Compare August 10, 2026 21:51
@floitsch
floitsch force-pushed the floitsch/spi.10-target branch from e566eb0 to fe266ce Compare August 11, 2026 01:18
@floitsch

Copy link
Copy Markdown
Member Author

@CodeRabbit review

@coderabbitai

coderabbitai Bot commented Aug 11, 2026

Copy link
Copy Markdown
✅ Action performed

Review finished.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@coderabbitai

coderabbitai Bot commented Aug 11, 2026

Copy link
Copy Markdown

Review Change Stack

Walkthrough

The PR adds ESP32 SPI target support with asynchronous exchanges, DMA configuration, transfer validation, lifecycle handling, chip-select timing parameters, native resource primitives, IRAM settings, and board-level coverage.

Changes

ESP32 SPI target support

Layer / File(s) Summary
Public SPI target contract
lib/spi.toit, src/compiler/propagation/type_primitive_spi.cc, src/primitive.h
Adds the Target and PendingExchange APIs, transfer-size constants, chip-select timing parameters, and target primitive registrations.
Native target resource lifecycle
src/resources/spi_esp32.h, src/resources/spi_esp32.cc, src/tags.h
Adds ESP32 SPI target resources, event handling, DMA buffers, initialization, GPIO validation, cleanup, and resource registration.
Transfer execution and device timing
src/resources/spi_esp32.cc
Adds asynchronous transaction setup and completion, receive-size handling, DMA alignment behavior, and chip-select setup and hold cycle configuration.
Hardware validation and toolchain support
tests/hw/esp32/*, toolchains/*/sdkconfig, third_party/esp-idf
Adds board tests for SPI target transfers and enables SPI slave ISR and HAL execution from IRAM across ESP32 toolchains.
Estimated code review effort: 5 (Critical) ~120 minutes

Possibly related PRs

  • toitlang/toit#3018: Adds similar asynchronous ESP32 resource operations with lifecycle primitives and event handling.
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly and concisely identifies the main change: asynchronous SPI target support.
Description check ✅ Passed The description directly explains the SPI target API, implementation details, supported features, and hardware tests.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches 💡 2
📝 Generate docstrings 💡
  • Create stacked PR
  • Commit on current branch
🛠️ Fix failing CI checks 💡
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch floitsch/spi.10-target

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 4

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@lib/spi.toit`:
- Around line 234-238: Update the finalization lifecycle around Target.finalize_
and exchange-in-flight_ so an in-flight exchange cannot cause permanent resource
retention: either allow target_close to tear down in-flight operations and make
finalize_ always call close, or retain a strong Target reference until
finish-exchange_ completes. Ensure the peripheral, host slot, and GPIO pins are
released without relying on a second finalizer invocation.

In `@src/resources/spi_esp32.cc`:
- Around line 378-385: In the result-size calculation, apply the classic ESP32
DMA word-alignment mask to transferred_bytes immediately after it is computed,
before clamping with resource->receive_size(). Remove the later mask on
result_size so requested sizes such as six bytes are preserved when the DMA
transferred a complete word-aligned amount.

In `@src/resources/spi_esp32.h`:
- Around line 159-166: Update signal_from_isr to call portYIELD_FROM_ISR when
xQueueSendFromISR sets higher_was_woken to pdTRUE, placing the yield before the
function returns while preserving the existing critical-section and queue-send
flow.

In `@toolchains/esp32p4/sdkconfig`:
- Line 1004: Regenerate the ESP-IDF Kconfig settings so
CONFIG_HAL_SPI_SLAVE_FUNC_IN_IRAM is enabled alongside
CONFIG_SPI_SLAVE_ISR_IN_IRAM in toolchains/esp32p4/sdkconfig at lines 1004-1004
and toolchains/esp32s2/sdkconfig at lines 745-745; use ESP-IDF 5.4.2 for the S2
configuration. Also apply the same generated HAL IRAM setting to
toolchains/esp32c3/sdkconfig at lines 983-983 and toolchains/esp32c6/sdkconfig
at lines 1117-1117.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: a2f9dd24-83d7-41db-a133-79ed7237b8ce

📥 Commits

Reviewing files that changed from the base of the PR and between 6b39845 and fe266ce.

📒 Files selected for processing (16)
  • lib/spi.toit
  • src/compiler/propagation/type_primitive_spi.cc
  • src/primitive.h
  • src/resources/spi_esp32.cc
  • src/resources/spi_esp32.h
  • src/tags.h
  • tests/hw/esp32/spi-target-board1.toit
  • tests/hw/esp32/spi-target-board2.toit
  • tests/hw/esp32/spi-target-shared.toit
  • third_party/esp-idf
  • toolchains/esp32/sdkconfig
  • toolchains/esp32c3/sdkconfig
  • toolchains/esp32c6/sdkconfig
  • toolchains/esp32p4/sdkconfig
  • toolchains/esp32s2/sdkconfig
  • toolchains/esp32s3/sdkconfig

Comment thread lib/spi.toit
Comment on lines +234 to +238
finalize_ -> none:
// An armed ESP-IDF transaction cannot be canceled. Leave the native
// resource registered so process teardown can release it safely.
if exchange-in-flight_: return
close

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 Stability & Availability | 🟠 Major | 🏗️ Heavy lift

The finalizer can leak the peripheral, the host slot, and the GPIO pins.

Toit unregisters a finalizer after it runs. When finalize_ returns early because exchange-in-flight_ is true, no later finalizer call happens. The Target object is then collected and close is never called. The SPI host slot and the reserved pins stay taken until the process exits.

The native destructor already handles this case. SpiTargetResource::~SpiTargetResource in src/resources/spi_esp32.cc frees the slave driver before it releases the buffers, and it calls finish_operation when an operation is in flight. Only the target_close primitive rejects an in-flight operation with INVALID_STATE.

Consider one of the following:

  • Let target_close tear the resource down even when an operation is in flight, and let finalize_ always close.
  • Keep a strong reference to the Target while an exchange is in flight, so the object cannot be collected before finish-exchange_ runs.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@lib/spi.toit` around lines 234 - 238, Update the finalization lifecycle
around Target.finalize_ and exchange-in-flight_ so an in-flight exchange cannot
cause permanent resource retention: either allow target_close to tear down
in-flight operations and make finalize_ always call close, or retain a strong
Target reference until finish-exchange_ completes. Ensure the peripheral, host
slot, and GPIO pins are released without relying on a second finalizer
invocation.

Comment on lines +378 to +385
size_t transferred_bytes = (resource->transferred_bits() + 7) / 8;
size_t result_size = Utils::min(transferred_bytes, resource->receive_size());
#if CONFIG_IDF_TARGET_ESP32
// Classic ESP32 target DMA only commits complete words to its receive
// buffer. ESP-IDF documents that a controller's trailing bytes are
// discarded when its transaction length is not a multiple of four.
if (resource->dma()) result_size &= ~static_cast<size_t>(3);
#endif

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

The classic-ESP32 word truncation is applied after the clamp and drops valid bytes.

The mask at line 384 applies to result_size, which is already clamped to receive_size. The hardware limitation affects how many bytes the DMA committed, which is transferred_bytes, not how many bytes the caller requested.

Consider a caller that requests 6 bytes while the controller clocks 64 bytes. transferred_bytes is 64 and the DMA committed all 64 bytes. result_size becomes 6, and the mask reduces it to 4. Bytes 4 and 5 are valid but are discarded.

Apply the mask to transferred_bytes before the clamp.

🐛 Proposed fix
   size_t transferred_bytes = (resource->transferred_bits() + 7) / 8;
-  size_t result_size = Utils::min(transferred_bytes, resource->receive_size());
 `#if` CONFIG_IDF_TARGET_ESP32
   // Classic ESP32 target DMA only commits complete words to its receive
   // buffer. ESP-IDF documents that a controller's trailing bytes are
   // discarded when its transaction length is not a multiple of four.
-  if (resource->dma()) result_size &= ~static_cast<size_t>(3);
+  if (resource->dma()) transferred_bytes &= ~static_cast<size_t>(3);
 `#endif`
+  size_t result_size = Utils::min(transferred_bytes, resource->receive_size());
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
size_t transferred_bytes = (resource->transferred_bits() + 7) / 8;
size_t result_size = Utils::min(transferred_bytes, resource->receive_size());
#if CONFIG_IDF_TARGET_ESP32
// Classic ESP32 target DMA only commits complete words to its receive
// buffer. ESP-IDF documents that a controller's trailing bytes are
// discarded when its transaction length is not a multiple of four.
if (resource->dma()) result_size &= ~static_cast<size_t>(3);
#endif
size_t transferred_bytes = (resource->transferred_bits() + 7) / 8;
`#if` CONFIG_IDF_TARGET_ESP32
// Classic ESP32 target DMA only commits complete words to its receive
// buffer. ESP-IDF documents that a controller's trailing bytes are
// discarded when its transaction length is not a multiple of four.
if (resource->dma()) transferred_bytes &= ~static_cast<size_t>(3);
`#endif`
size_t result_size = Utils::min(transferred_bytes, resource->receive_size());
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/resources/spi_esp32.cc` around lines 378 - 385, In the result-size
calculation, apply the classic ESP32 DMA word-alignment mask to
transferred_bytes immediately after it is computed, before clamping with
resource->receive_size(). Remove the later mask on result_size so requested
sizes such as six bytes are preserved when the DMA transferred a complete
word-aligned amount.

Comment thread src/resources/spi_esp32.h
Comment on lines +159 to +166
IRAM_ATTR void signal_from_isr(word event) {
BaseType_t higher_was_woken = pdFALSE;
portENTER_CRITICAL_ISR(&spinlock_);
pending_event_ |= event;
portEXIT_CRITICAL_ISR(&spinlock_);
word payload = 0;
xQueueSendFromISR(queue(), &payload, &higher_was_woken);
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🚀 Performance & Scalability | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Compare with the yield convention used by other ESP32 ISR senders in this repository.
rg -n -C3 'xQueueSendFromISR' src/ | rg -n -C3 'YIELD|higher|woken'

Repository: toitlang/toit

Length of output: 3755


🏁 Script executed:

#!/bin/bash
set -eu
printf '%s\n' '--- spi_esp32.h relevant structure and usages ---'
rg -n -C8 'signal_from_isr|higher_was_woken|queue\(\)|return .*pdTRUE|portYIELD_FROM_ISR|portEND_SWITCHING_ISR' src/resources/spi_esp32.h src/resources/*.cc
printf '%s\n' '--- focused file outline ---'
ast-grep outline src/resources/spi_esp32.h --match 'signal_from_isr' --view expanded || true
printf '%s\n' '--- nearby SPI source ---'
sed -n '1,230p' src/resources/spi_esp32.h

Repository: toitlang/toit

Length of output: 30524


🏁 Script executed:

#!/bin/bash
set -eu
rg -n -C8 'signal_from_isr|higher_was_woken|queue\(\)|return .*pdTRUE|portYIELD_FROM_ISR|portEND_SWITCHING_ISR' src/resources/spi_esp32.h src/resources/*.cc
sed -n '1,230p' src/resources/spi_esp32.h

Repository: toitlang/toit

Length of output: 30231


🏁 Script executed:

#!/bin/bash
set -eu
printf '%s\n' '--- all SPI ISR callback call sites ---'
rg -n -C12 'ready_from_isr|complete_from_isr|spi_slave|post_trans|pre_trans|isr' src/resources/spi_esp32.cc src/resources/spi_esp32.h
printf '%s\n' '--- event-source dispatch and ISR return handling ---'
rg -n -C10 'portYIELD_FROM_ISR|portEND_SWITCHING_ISR|higher_was_woken|BaseType_t|bool.*callback|callback.*bool' src
printf '%s\n' '--- SPI implementation outline ---'
ast-grep outline src/resources/spi_esp32.cc

Repository: toitlang/toit

Length of output: 50369


🏁 Script executed:

#!/bin/bash
set -eu
rg -n -C12 'ready_from_isr|complete_from_isr|spi_slave|post_trans|pre_trans|isr' src/resources/spi_esp32.cc src/resources/spi_esp32.h
rg -n -C10 'portYIELD_FROM_ISR|portEND_SWITCHING_ISR|higher_was_woken|BaseType_t|bool.*callback|callback.*bool' src
ast-grep outline src/resources/spi_esp32.cc

Repository: toitlang/toit

Length of output: 50369


🌐 Web query:

ESP-IDF spi_slave post_setup_cb post_trans_cb ISR FreeRTOS portYIELD_FROM_ISR callback documentation

💡 Result:

In the ESP-IDF SPI Slave driver, the post_setup_cb and post_trans_cb callbacks are executed within the SPI peripheral's interrupt service routine (ISR) context [1][2]. Key considerations for these callbacks include: 1. Interrupt Context Constraints: Because these callbacks run in an ISR, they must be concise and execute quickly to avoid starving the SPI driver [1][2]. It is highly recommended to place these callback functions in IRAM (using the IRAM_ATTR attribute) to prevent potential crashes caused by flash access during interrupt processing, especially if the driver is initialized with the ESP_INTR_FLAG_IRAM flag [2][3]. 2. FreeRTOS Yielding: The standard SPI Slave driver's post_setup_cb and post_trans_cb do not receive an explicit awoken parameter to manage task yielding [2][3]. If you need to trigger a task-level action from these callbacks, standard inter-task communication (such as queues or task notifications) should be used. 3. SPI Slave Half Duplex (HD) Driver Distinction: Do not confuse the standard SPI Slave driver with the SPI Slave Half Duplex (HD) driver. The HD driver explicitly provides an awoken parameter in its callback signature [4][5]. When using the HD driver, if a callback unblocks a task (e.g., via xSemaphoreGiveFromISR), you must pass the corresponding BaseType_t variable to the awoken parameter and then call portYIELD_FROM_ISR within the ISR context if the value is pdTRUE [4][5]. 4. Thread Safety and ISR Core: The SPI ISR is executed on the same core that initialized the SPI peripheral [2]. To ensure thread safety, it is recommended to register the SPI ISR to the same core where tasks accessing the SPI peripheral are running [1][6]. In summary, for the standard SPI Slave driver, these callbacks are direct ISR hooks [2]. If your goal is to minimize latency or integrate closely with FreeRTOS tasks, follow the guidance on executing minimal code within the ISR and using efficient inter-task communication mechanisms [1][2][7]. If you require explicit, managed task yielding within the callback itself, evaluate if the SPI Slave Half Duplex driver is a more appropriate architectural choice for your application [4][5].

Citations:


🌐 Web query:

FreeRTOS xQueueSendFromISR pxHigherPriorityTaskWoken portYIELD_FROM_ISR documentation

💡 Result:

In FreeRTOS, xQueueSendFromISR, pxHigherPriorityTaskWoken, and portYIELD_FROM_ISR work together to safely manage task synchronization and context switching within an Interrupt Service Routine (ISR) [1][2][3]. xQueueSendFromISR is the version of the queue send API designed for use inside an ISR [1][3]. Because ISRs cannot block, standard API functions are unsuitable; xQueueSendFromISR ensures that if a queue is full, the function simply returns an error code instead of attempting to wait [4][5][1]. pxHigherPriorityTaskWoken is a pointer to a BaseType_t variable that acts as a flag to notify the ISR that a task of higher priority than the currently running task has been unblocked by the queue operation [1][6][2]. 1. Initialization: You must initialize this variable to pdFALSE before calling any "FromISR" API function within the ISR [2][3]. 2. Function Call: You pass the address of this variable to the API function (e.g., xQueueSendFromISR(..., &xHigherPriorityTaskWoken)) [1][7]. 3. Mechanism: If the operation unblocks a task with a priority higher than the currently executing one, the kernel sets the variable to pdTRUE [1][2]. If it is not NULL, this informs the application that a context switch should occur [6][8]. portYIELD_FROM_ISR is an interrupt-safe macro used to request a context switch [7][3]. After calling your "FromISR" API functions, you pass the pxHigherPriorityTaskWoken variable to portYIELD_FROM_ISR [7][2]. If the variable is pdTRUE, the macro triggers a context switch, ensuring the highest-priority task (the one just unblocked) starts running as soon as possible after the ISR finishes [1][7][3]. If it is pdFALSE, the macro does nothing [7][2]. Note that some FreeRTOS ports provide an identical alternative macro called portEND_SWITCHING_ISR [7][2]. Always check your specific port's documentation, as certain architectures require the yield macro to be called at the very end of the ISR [2][9][10].

Citations:


🏁 Script executed:

#!/bin/bash
set -eu
printf '%s\n' '--- event queue implementation ---'
fd -i 'ev_queue_esp32' src
rg -n -C12 'class EventQueueEventSource|xQueueReceive|event queue|EventQueueResource|queue_size|queue.*1' src/event_sources src/resources/resource* src
printf '%s\n' '--- callback declarations in available headers ---'
rg -n -C5 'post_setup_cb|post_trans_cb|spi_slave_interface_config_t' . -g '*.h' -g '*.c' -g '*.cc' -g '*.cpp' | head -160

Repository: toitlang/toit

Length of output: 50370


🏁 Script executed:

#!/bin/bash
set -eu
fd -i 'ev_queue_esp32' src
rg -n -C12 'class EventQueueEventSource|xQueueReceive|EventQueueResource|queue_size|queue.*1' src/event_sources src/resources
rg -n -C5 'post_setup_cb|post_trans_cb|spi_slave_interface_config_t' . -g '*.h' -g '*.c' -g '*.cc' -g '*.cpp' | head -160

Repository: toitlang/toit

Length of output: 50369


🏁 Script executed:

#!/bin/bash
set -eu
printf '%s\n' '--- event-source task setup ---'
sed -n '1,135p' src/event_sources/ev_queue_esp32.cc
printf '%s\n' '--- thread priority defaults and yield macros in repository ---'
rg -n -C5 'EventQueueEventSource|xTaskCreatePinnedToCore|tskIDLE_PRIORITY|portYIELD_FROM_ISR|portEND_SWITCHING_ISR' src/event_sources src/os* src
printf '%s\n' '--- read-only structural verifier ---'
python3 - <<'PY'
from pathlib import Path

spi_h = Path("src/resources/spi_esp32.h").read_text()
spi_cc = Path("src/resources/spi_esp32.cc").read_text()
ev_cc = Path("src/event_sources/ev_queue_esp32.cc").read_text()

checks = {
    "SPI ISR helper records wake flag": "xQueueSendFromISR(queue(), &payload, &higher_was_woken);" in spi_h,
    "SPI ISR helper ignores wake flag": "portYIELD_FROM_ISR" not in spi_h and "portEND_SWITCHING_ISR" not in spi_h,
    "ready callback invokes helper": "resource->ready_from_isr();" in spi_cc,
    "complete callback invokes helper": "resource->complete_from_isr();" in spi_cc,
    "callbacks configured as standard SPI callbacks": ".post_setup_cb = spi_target_ready_callback," in spi_cc and ".post_trans_cb = spi_target_done_callback," in spi_cc,
    "event source receives from resource queue": "if (receiver->receive_event(&data))" in ev_cc,
}
for name, value in checks.items():
    print(f"{name}: {'PASS' if value else 'FAIL'}")
if not all(checks.values()):
    raise SystemExit(1)
PY

Repository: toitlang/toit

Length of output: 24688


🏁 Script executed:

#!/bin/bash
set -eu
sed -n '1,135p' src/event_sources/ev_queue_esp32.cc
rg -n -C5 'EventQueueEventSource|xTaskCreatePinnedToCore|tskIDLE_PRIORITY|portYIELD_FROM_ISR|portEND_SWITCHING_ISR' src/event_sources src/os* src
python3 - <<'PY'
from pathlib import Path

spi_h = Path("src/resources/spi_esp32.h").read_text()
spi_cc = Path("src/resources/spi_esp32.cc").read_text()
ev_cc = Path("src/event_sources/ev_queue_esp32.cc").read_text()

checks = {
    "SPI ISR helper records wake flag": "xQueueSendFromISR(queue(), &payload, &higher_was_woken);" in spi_h,
    "SPI ISR helper ignores wake flag": "portYIELD_FROM_ISR" not in spi_h and "portEND_SWITCHING_ISR" not in spi_h,
    "ready callback invokes helper": "resource->ready_from_isr();" in spi_cc,
    "complete callback invokes helper": "resource->complete_from_isr();" in spi_cc,
    "callbacks configured as standard SPI callbacks": ".post_setup_cb = spi_target_ready_callback," in spi_cc and ".post_trans_cb = spi_target_done_callback," in spi_cc,
    "event source receives from resource queue": "if (receiver->receive_event(&data))" in ev_cc,
}
for name, value in checks.items():
    print(f"{name}: {'PASS' if value else 'FAIL'}")
if not all(checks.values()):
    raise SystemExit(1)
PY

Repository: toitlang/toit

Length of output: 24554


Yield from the SPI callbacks

If higher_was_woken == pdTRUE, call portYIELD_FROM_ISR() before signal_from_isr returns. The SPI callbacks wake EventQueueEventSource, which otherwise may not process the event until a tick or another ISR yield, adding latency to both ready and done events.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/resources/spi_esp32.h` around lines 159 - 166, Update signal_from_isr to
call portYIELD_FROM_ISR when xQueueSendFromISR sets higher_was_woken to pdTRUE,
placing the yield before the function returns while preserving the existing
critical-section and queue-send flow.

CONFIG_SPI_MASTER_ISR_IN_IRAM=y
# CONFIG_SPI_SLAVE_IN_IRAM is not set
# CONFIG_SPI_SLAVE_ISR_IN_IRAM is not set
CONFIG_SPI_SLAVE_ISR_IN_IRAM=y

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -eu

printf '%s\n' '--- target files and nearby configuration ---'
for f in toolchains/esp32p4/sdkconfig toolchains/esp32s2/sdkconfig; do
  echo "### $f"
  sed -n '985,1015p' "$f" 2>/dev/null || true
  sed -n '728,758p' "$f" 2>/dev/null || true
done

printf '%s\n' '--- SPI and HAL IRAM symbols in tracked files ---'
rg -n --hidden -g '!node_modules' \
  'SPI_SLAVE_ISR_IN_IRAM|HAL_SPI_SLAVE_FUNC_IN_IRAM|esp_driver_spi|ESP-IDF|IDF_VERSION' \
  toolchains components 2>/dev/null || true

printf '%s\n' '--- candidate Kconfig and version files ---'
git ls-files | rg '(^|/)(Kconfig|sdkconfig|idf|esp-idf|version|manifest)' | head -200

Repository: toitlang/toit

Length of output: 7354


🏁 Script executed:

#!/bin/bash
set -eu

printf '%s\n' '--- vendored ESP-IDF tree ---'
git ls-files third_party/esp-idf | head -80
find third_party/esp-idf -type f \( -name Kconfig -o -name 'Kconfig.*' \) -print 2>/dev/null | head -120

printf '%s\n' '--- SPI Kconfig definitions and references ---'
rg -n -C 8 \
  'SPI_SLAVE_ISR_IN_IRAM|HAL_SPI_SLAVE_FUNC_IN_IRAM|SPI_SLAVE_IN_IRAM' \
  third_party/esp-idf toolchains 2>/dev/null || true

printf '%s\n' '--- all generated SPI symbols ---'
for f in toolchains/*/sdkconfig; do
  printf '### %s\n' "$f"
  rg -n 'CONFIG_(SPI_SLAVE_ISR_IN_IRAM|HAL_SPI_SLAVE_FUNC_IN_IRAM|SPI_SLAVE_IN_IRAM)=' "$f" || true
done

printf '%s\n' '--- target SDK headers ---'
for f in toolchains/*/sdkconfig; do
  printf '### %s\n' "$f"
  sed -n '1,8p' "$f"
done

Repository: toitlang/toit

Length of output: 18729


🏁 Script executed:

#!/bin/bash
set -eu

base='https://raw.githubusercontent.com/espressif/esp-idf/v5.4.2'

printf '%s\n' '--- ESP-Driver SPI Kconfig ---'
curl -fsSL "$base/components/esp_driver_spi/Kconfig" | \
  rg -n -C 12 'SPI_SLAVE_ISR_IN_IRAM|SPI_SLAVE_IN_IRAM|HAL_SPI_SLAVE_FUNC_IN_IRAM' || true

printf '%s\n' '--- HAL Kconfig candidates ---'
for path in components/hal/Kconfig components/hal/Kconfig.projbuild components/esp_hal_spi/Kconfig components/esp_driver_spi/Kconfig.projbuild; do
  echo "### $path"
  curl -fsSL "$base/$path" 2>/dev/null | \
    rg -n -C 12 'HAL_SPI_SLAVE_FUNC_IN_IRAM|SPI_SLAVE_ISR_IN_IRAM|SPI_SLAVE_IN_IRAM' || true
done

printf '%s\n' '--- submodule metadata ---'
git submodule status third_party/esp-idf 2>/dev/null || true
git ls-tree HEAD third_party/esp-idf

Repository: toitlang/toit

Length of output: 3827


🏁 Script executed:

#!/bin/bash
set -eu

printf '%s\n' '--- exact submodule revision Kconfig contract ---'
base='https://raw.githubusercontent.com/espressif/esp-idf/4a8004d3ec393c1020bd82373705c5b210eba17d'
curl -fsSL "$base/components/esp_driver_spi/Kconfig" | \
  rg -n -C 5 'SPI_SLAVE_ISR_IN_IRAM|SPI_SLAVE_IN_IRAM' || true
curl -fsSL "$base/components/hal/Kconfig" | \
  rg -n -C 5 'HAL_SPI_SLAVE_FUNC_IN_IRAM' || true

printf '%s\n' '--- deterministic inventory of generated configurations ---'
python3 - <<'PY'
from pathlib import Path
import re

for path in sorted(Path("toolchains").glob("*/sdkconfig")):
    text = path.read_text()
    isr = re.search(r"^CONFIG_SPI_SLAVE_ISR_IN_IRAM=(.*)$", text, re.M)
    hal = re.search(r"^CONFIG_HAL_SPI_SLAVE_FUNC_IN_IRAM=(.*)$", text, re.M)
    print(f"{path}: ISR={isr.group(1) if isr else '<unset>'}; "
          f"HAL={hal.group(1) if hal else '<unset>'}")
PY

printf '%s\n' '--- configuration-generation and sdkconfig usage ---'
rg -n -C 4 \
  'sdkconfig|idf.py|IDF_PATH|ESP-IDF|SDKCONFIG' \
  Makefile CMakeLists.txt tools toolchains .github 2>/dev/null | head -300 || true

printf '%s\n' '--- repository status summary ---'
git status --short
git diff --stat

Repository: toitlang/toit

Length of output: 18812


Synchronize the SPI slave ISR and HAL IRAM settings.

ESP-IDF 5.4.2 selects CONFIG_HAL_SPI_SLAVE_FUNC_IN_IRAM when CONFIG_SPI_SLAVE_ISR_IN_IRAM=y. Regenerate these configurations with the vendored ESP-IDF Kconfig:

  • toolchains/esp32p4/sdkconfig#L1004
  • toolchains/esp32s2/sdkconfig#L745
  • toolchains/esp32c3/sdkconfig#L983
  • toolchains/esp32c6/sdkconfig#L1117

The S2 configuration also uses ESP-IDF 5.4.2, not 5.3.1. Missing HAL IRAM placement can cause SPI slave operations to access flash while the cache is disabled.

📍 Affects 2 files
  • toolchains/esp32p4/sdkconfig#L1004-L1004 (this comment)
  • toolchains/esp32s2/sdkconfig#L745-L745
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@toolchains/esp32p4/sdkconfig` at line 1004, Regenerate the ESP-IDF Kconfig
settings so CONFIG_HAL_SPI_SLAVE_FUNC_IN_IRAM is enabled alongside
CONFIG_SPI_SLAVE_ISR_IN_IRAM in toolchains/esp32p4/sdkconfig at lines 1004-1004
and toolchains/esp32s2/sdkconfig at lines 745-745; use ESP-IDF 5.4.2 for the S2
configuration. Also apply the same generated HAL IRAM setting to
toolchains/esp32c3/sdkconfig at lines 983-983 and toolchains/esp32c6/sdkconfig
at lines 1117-1117.

@floitsch
floitsch force-pushed the floitsch/spi.10-target branch 2 times, most recently from 0e63013 to 9e0c4bb Compare August 12, 2026 21:18
@floitsch
floitsch force-pushed the floitsch/spi.10-target branch from 9e0c4bb to b43a439 Compare August 12, 2026 21:44
@floitsch
floitsch force-pushed the floitsch/spi.10-target branch from b43a439 to 3387cb2 Compare August 12, 2026 21:49
@floitsch
floitsch force-pushed the floitsch/spi.10-target branch from 3387cb2 to b1f78a1 Compare August 12, 2026 22:18
@floitsch
floitsch force-pushed the floitsch/spi.10-target branch from b1f78a1 to 3b956db Compare August 12, 2026 22:31
@floitsch
floitsch force-pushed the floitsch/spi.10-target branch from 3b956db to 4cc7c1d Compare August 12, 2026 23:18
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Development

Successfully merging this pull request may close these issues.

1 participant