Skip to content

✨ (minor) Add for_each_run() to walk two scatter spans together - #19

Merged
kammce merged 2 commits into
mainfrom
for-each-run
Oct 26, 2026
Merged

kammce merged 2 commits into
mainfrom
for-each-run

Conversation

@kammce

@kammce kammce commented Oct 26, 2026

Copy link
Copy Markdown
Member

Adds mem::for_each_run(), which walks two scatter spans side by side, one contiguous run at a time.

std::array<int, 2> a = { 1, 2 };
std::array<int, 3> b = { 3, 4, 5 };
std::array<int, 4> c = { 6, 7, 8, 9 };
std::array<int, 1> d = { 10 };
std::array<int, 1> e = { 11 };

// Runs: ({1, 2}, {6, 7}), ({3, 4}, {8, 9}), ({5}, {10}), ({}, {11})
mem::for_each_run(mem::scatter_span<int>{ a, b },
                  mem::scatter_span<int>{ c, d, e },
                  [](std::span<int> p_first, std::span<int> p_second) {
                    // ...
                  });

Behavior

  • Each call gets the longest pair of spans that starts at the same logical position and is contiguous in both scatter spans. A run ends at whichever chunk boundary comes first.
  • Every element of both scatter spans is passed exactly once, in order. Empty chunks are skipped.
  • While both scatter spans have elements left, the two spans of a run are the same size. Once one runs out, its span is empty and the run is the rest of the other's current chunk.
  • The two scatter spans can have different element types, e.g. scatter_span<byte const> for data going out and scatter_span<byte> for data coming in.
  • Each run costs O(1), with no allocation. sub_scatter_span() would walk the chunks from the start on every call.

Why

A full duplex bus moves two caller-supplied buffers in lockstep, and their chunk boundaries rarely line up. The STM32 SPI driver in libhal-arm-mcu needs this to hand each run to DMA, and a hal::spi on any other platform would need the same walk. A single buffer is already covered by a range-for over its chunks.

Testing

Five new unit tests cover:

  • splitting at the first boundary;
  • empty spans once one side runs out;
  • skipping empty chunks;
  • writing through to the elements;
  • two empty scatter spans.

The build and all tests pass with LLVM 23, and pre-commit passes.

for_each_run() calls a function with the longest pair of runs, one from
each scatter span, that is contiguous in both. Each run ends at
whichever chunk boundary comes first, and once one scatter span runs
out its side of the run is empty. Full duplex drivers, such as an SPI
moving its transmit and receive buffers by DMA, need exactly this walk
and would otherwise each carry their own cursor type.
kammce added a commit to libhal/libhal-arm-mcu that referenced this pull request Oct 26, 2026
Both transfer paths walk the outgoing and incoming data together with
for_each_run(), one run that is contiguous on both sides at a time. The
polled path loops over each run's bytes and the DMA path splits each
run into transfers of at most 65,535 bytes. for_each_run() is a private
copy of the one proposed in libhal/scatter_span#19, until libhal
requires a scatter_span release that has it. scatter_cursor is removed.

@kammce kammce left a comment

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Lgtm except for the places where () should wrap up arithmetic

@kammce kammce left a comment

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Lgtm

@kammce
kammce merged commit d059f3d into main Oct 26, 2026
9 checks passed
@kammce
kammce deleted the for-each-run branch October 26, 2026 11:46
kammce added a commit to libhal/libhal-arm-mcu that referenced this pull request Oct 26, 2026
* ✨ v2 Move STM32F1 SPI data by DMA

SPI transfers on the STM32F1 now move their bytes by DMA when they can
claim both of the peripheral's channels, and the calling context sleeps
on its waiter while the bytes move. The channels are claimed for each
transfer, so drivers whose requests share a channel take turns. If
another driver holds one, such as a USART receiving on it, the transfer
polls as before.

The generic SPI driver accepts any channel that satisfies the new
stm32_generic::byte_dma_channel concept, keeping the F1's DMA controller
out of the generic code. scatter_cursor moves into its own partition and
gains contiguous() and advance(n), so a whole chunk can go to DMA.

* ♻️ v2 Walk SPI buffers in runs instead of scatter_cursor

Both transfer paths walk the outgoing and incoming data together with
for_each_run(), one run that is contiguous on both sides at a time. The
polled path loops over each run's bytes and the DMA path splits each
run into transfers of at most 65,535 bytes. for_each_run() is a private
copy of the one proposed in libhal/scatter_span#19, until libhal
requires a scatter_span release that has it. scatter_cursor is removed.

* 🎨 v2 Write logical operators as words, make precedence explicit

Covers the SPI driver and the STM32F1 DMA channel driver.

* ⚡ v2 Shrink the SPI DMA transfer

The DMA transfer() takes its channels as pointers and polls each run when
either is missing, so one chunk walk serves both paths and the compiler
emits stop_transfer once. dma_channel keeps a pointer to its channel's
registers, so stop(), remaining(), failed() and start() don't work out the
controller and channel from the index each time.

spi demo: 31,849 B -> 31,321 B of text. dma demo: 28,613 B -> 28,453 B.

* ♻️ v2 Use mem::for_each_run() from scatter_span 0.1.0

libhal 5.0.0-alpha.13 requires scatter_span 0.1.0, which has it, so the
private copy in the SPI driver goes. Also groups the arithmetic in
interrupt_for().
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant