Add autonomous SPI buffer target - #3160
Conversation
36ef280 to
92a6e97
Compare
92a6e97 to
3d34fbc
Compare
3d34fbc to
47dc4d8
Compare
47dc4d8 to
007f05b
Compare
007f05b to
7cfbdf8
Compare
7cfbdf8 to
9452af1
Compare
9452af1 to
3903e57
Compare
3903e57 to
85bb56e
Compare
6dc9dd8 to
f56b49c
Compare
f56b49c to
2bbefb0
Compare
|
@CodeRabbit review |
✅ Action performedReview finished.
|
WalkthroughAdds ChangesBuffered SPI Target
Estimated code review effort: 4 (Complex) | ~60 minutes 🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✨ Finishing Touches 💡 1🛠️ Fix failing CI checks 💡
📝 Generate docstrings
🧪 Generate unit tests (beta)
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. Comment |
There was a problem hiding this comment.
Actionable comments posted: 1
🧹 Nitpick comments (1)
tests/hw/esp32/spi-buffer-target-shared.toit (1)
215-239: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low valueGuard the command handlers against a null
target.
WRITE,SET,READ,RECEIVE, andDROPPEDusetargetwithout a null check. If a command arrives beforeCREATE, or afterCLOSEsetstargetto null at Line 242, the test fails with a null dereference instead of a clear protocol error.Add one check at the top of the loop for the commands that require a target.
♻️ Proposed guard
else if command == WRITE: offset := port.in.little-endian.read-uint32 size := port.in.little-endian.read-uint32 + if not target: throw "No target for command: $command" target.write offset (port.in.read-bytes size) port.out.write #[DONE] --flush🤖 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 `@tests/hw/esp32/spi-buffer-target-shared.toit` around lines 215 - 239, Add a guard at the top of the command-processing loop for target-dependent commands WRITE, SET, READ, RECEIVE, and DROPPED, rejecting them with the test’s established protocol error when target is null before reaching target.write, target[], target.read, target.receive, or target.dropped-receive-count. Leave commands that do not require a target unchanged.
🤖 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 `@src/resources/spi_esp32.cc`:
- Around line 224-241: Update complete_from_isr to reserve an available
receive-ring slot under spinlock_, copy received bytes outside the critical
section, then reacquire spinlock_ to publish receive_lengths_ and increment
receive_count_. Add and maintain a separate receive_reserved_ counter so
concurrent completions cannot claim the same capacity, while preserving
dropped_receive_count_ handling when no slot can be reserved.
---
Nitpick comments:
In `@tests/hw/esp32/spi-buffer-target-shared.toit`:
- Around line 215-239: Add a guard at the top of the command-processing loop for
target-dependent commands WRITE, SET, READ, RECEIVE, and DROPPED, rejecting them
with the test’s established protocol error when target is null before reaching
target.write, target[], target.read, target.receive, or
target.dropped-receive-count. Leave commands that do not require a target
unchanged.
🪄 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: 0f922544-02a8-44c7-b499-97365e34dd3c
📒 Files selected for processing (10)
lib/spi.toitsrc/compiler/propagation/type_primitive_spi.ccsrc/primitive.hsrc/resources/spi_esp32.ccsrc/resources/spi_esp32.hsrc/tags.htests/hw/esp32/spi-buffer-target-board1.toittests/hw/esp32/spi-buffer-target-board2.toittests/hw/esp32/spi-buffer-target-shared.toittoolchains/esp32/sdkconfig
| portENTER_CRITICAL_ISR(&spinlock_); | ||
| // A floating or newly configured CS line can produce an interrupt without | ||
| // any clock edges. It carries no transaction data, so don't consume receive | ||
| // queue capacity or wake a receiver for it. | ||
| if (receive_buffer_ != null && received != 0) { | ||
| if (receive_count_ < receive_queue_depth_) { | ||
| uint32_t index = receive_head_ + receive_count_; | ||
| if (index >= receive_queue_depth_) index -= receive_queue_depth_; | ||
| uint8_t* destination = receive_ring_ + index * buffer_size_; | ||
| for (size_t i = 0; i < received; i++) destination[i] = receive_buffer_[i]; | ||
| receive_lengths_[index] = received; | ||
| receive_count_++; | ||
| enqueued = true; | ||
| } else if (dropped_receive_count_ != Smi::MAX_SMI_VALUE) { | ||
| dropped_receive_count_++; | ||
| } | ||
| } | ||
| portEXIT_CRITICAL_ISR(&spinlock_); |
There was a problem hiding this comment.
🚀 Performance & Scalability | 🟠 Major | 🏗️ Heavy lift
Reduce the interrupt-disabled copy in complete_from_isr.
The byte copy at Line 233 runs inside portENTER_CRITICAL_ISR. It can copy up to buffer_size_ bytes, which is 4092 in the DMA configuration. Interrupts stay disabled for the whole copy on that core.
take_receive deliberately avoids this pattern and documents the reason at Lines 192-194. Apply the same approach here: reserve the slot under the spinlock, copy outside it, then publish the length and increment receive_count_ under the spinlock.
♻️ Sketch of the reservation approach
portENTER_CRITICAL_ISR(&spinlock_);
- if (receive_buffer_ != null && received != 0) {
- if (receive_count_ < receive_queue_depth_) {
- uint32_t index = receive_head_ + receive_count_;
- if (index >= receive_queue_depth_) index -= receive_queue_depth_;
- uint8_t* destination = receive_ring_ + index * buffer_size_;
- for (size_t i = 0; i < received; i++) destination[i] = receive_buffer_[i];
- receive_lengths_[index] = received;
- receive_count_++;
- enqueued = true;
- } else if (dropped_receive_count_ != Smi::MAX_SMI_VALUE) {
+ uint32_t reserved = 0;
+ bool has_slot = false;
+ if (receive_buffer_ != null && received != 0) {
+ if (receive_count_ < receive_queue_depth_) {
+ reserved = receive_head_ + receive_count_;
+ if (reserved >= receive_queue_depth_) reserved -= receive_queue_depth_;
+ has_slot = true;
+ } else if (dropped_receive_count_ != Smi::MAX_SMI_VALUE) {
dropped_receive_count_++;
}
}
portEXIT_CRITICAL_ISR(&spinlock_);
+ if (has_slot) {
+ uint8_t* destination = receive_ring_ + reserved * buffer_size_;
+ for (size_t i = 0; i < received; i++) destination[i] = receive_buffer_[i];
+ portENTER_CRITICAL_ISR(&spinlock_);
+ receive_lengths_[reserved] = received;
+ receive_count_++;
+ portEXIT_CRITICAL_ISR(&spinlock_);
+ enqueued = true;
+ }Note that the slot must stay reserved against a second completion. Track a separate receive_reserved_ counter, or keep the current design and document the latency cost.
🤖 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 224 - 241, Update complete_from_isr
to reserve an available receive-ring slot under spinlock_, copy received bytes
outside the critical section, then reacquire spinlock_ to publish
receive_lengths_ and increment receive_count_. Add and maintain a separate
receive_reserved_ counter so concurrent completions cannot claim the same
capacity, while preserving dropped_receive_count_ handling when no slot can be
reserved.
2bbefb0 to
860f4f4
Compare
860f4f4 to
1bb524d
Compare
1bb524d to
7af779d
Compare
7af779d to
3282021
Compare
3282021 to
0217a71
Compare
0217a71 to
4d9d8a4
Compare
4d9d8a4 to
6e508ab
Compare
Stacked on #3158.
This adds an ESP32
spi.BufferTargetfor protocols that need the target to remain ready without waiting for a Toit task between transactions. It uses a fixed native response buffer and a bounded native queue of complete MOSI transactions. The completion callback performs no allocation and re-arms the same descriptor from the ISR; all primitives return without waiting.Highlights:
ResourceState_; native calls never wait for controller activity or queue spaceThe implementation uses ESP-IDF’s private
spi_slave_queue_trans_isrentry point. It is present with the same contract in the recent ESP-IDF checkout and is also used by ESP-IDF components. No new ESP-IDF patch is needed for this layer.This deliberately remains a complete-transaction API. It does not add half-buffer streaming or watermark callbacks.