Skip to content

Day 110 — Continuous DMA Streaming and Ring Buffer Ownership

Today's Goal

Extend the one-shot DMA client model from Day109 into a continuous RX streaming architecture.

The main goal is to understand how a persistent DMA buffer can be divided into reusable slots and shared safely between a DMA producer and a CPU consumer without copying completed data into a second software ring buffer.

The main topics are:

  • Continuous DMA streaming architecture
  • Cyclic DMA vs request-based one-shot DMA
  • Persistent DMA mapping
  • DMA buffer slot organization
  • Producer and consumer positions
  • head, tail, count, and dma_idx
  • Per-slot ownership
  • DMA completion and slot publication
  • CPU acquire and release
  • Partial DMA synchronization
  • Consumer lag
  • DMA overrun detection
  • Stream termination with outstanding CPU data
  • Continuous ADC vs FIFO-triggered sensor acquisition
  • Continuous DMA stream simulation

The implementation goal is to build a simplified continuous RX DMA simulator with a persistent DMA mapping, cyclic buffer slots, explicit ownership transitions, and overrun detection.


1. From One-Shot DMA to Continuous Streaming

Day109 modeled a request-based one-shot DMA lifecycle:

Request
Map Buffer
Prepare DMA
Submit
Issue Pending
Start Peripheral
DMA Transfer
Completion
Sync / Unmap
Consume

This model works well when one logical operation corresponds to one DMA transaction.

Continuous acquisition has a different lifetime.

Instead of repeatedly mapping and unmapping a buffer for every block:

Map
Transfer
Unmap
Map
Transfer
Unmap

a streaming design may keep one DMA buffer mapped across the entire stream:

Map DMA Buffer
Start Stream
DMA fills Slot 0
DMA fills Slot 1
DMA fills Slot 2
DMA fills Slot 3
wrap
DMA reuses Slot 0
...
Stop Stream
Unmap DMA Buffer

This changes the lifetime relationship:

One-Shot DMA

mapping lifetime
transaction lifetime


Continuous Streaming

mapping lifetime
    >
individual slot completion lifetime

The mapping becomes a stream-level resource rather than a transaction-level resource.


2. Avoiding an Extra Software Ring Copy

A continuous DMA design does not necessarily need:

DMA Buffer
memcpy()
Software Ring Buffer
Consumer

For high-throughput streaming, this extra copy may add:

  • CPU utilization
  • memory bandwidth consumption
  • cache traffic
  • latency

A more direct architecture can divide the DMA buffer itself into reusable slots:

Persistent DMA Buffer

┌────────┬────────┬────────┬────────┐
│ Slot 0 │ Slot 1 │ Slot 2 │ Slot 3 │
└────────┴────────┴────────┴────────┘
     └── completed slots become directly available
         to the CPU consumer

The DMA buffer therefore also acts as the storage behind the producer-consumer ring.

The ring metadata tracks which slots are available rather than storing another copy of the payload.

This does not mean an additional software buffer is always wrong. Subsystem architecture, userspace interfaces, lifetime requirements, data transformation, and hardware constraints may still require another buffering layer.

The important optimization principle is:

Avoid an additional payload copy when the DMA buffer itself
can safely provide the required producer-consumer storage.

3. DMA Buffer Slots

The Day110 simulator divides one persistent DMA mapping into fixed-size slots.

Conceptually:

DMA Buffer

offset 0
┌──────────────┐
│ Slot 0       │
├──────────────┤
│ Slot 1       │
├──────────────┤
│ Slot 2       │
├──────────────┤
│ Slot 3       │
└──────────────┘

Each slot contains metadata describing its position and current ownership.

The simplified slot descriptor contains information such as:

index
offset
length
sequence
state

The slot does not contain another payload array.

Instead:

CPU address of slot
    =
stream buffer base
    +
slot offset

and:

DMA range of slot
    =
DMA mapping base
    +
slot offset

This keeps the payload inside the persistent DMA mapping.


4. Ring Descriptor

The simulator uses a ring descriptor to track completed slots waiting for the CPU consumer.

The important runtime values are:

Field Meaning
head Position where the next completed DMA slot is published
tail Position of the next READY slot to be acquired by the CPU
count Number of completed slots waiting in the ready ring
capacity Number of slots available in the ring
slots[] Metadata describing each DMA buffer slot

The distinction between head, tail, and count is important.

For example:

head == tail

by itself is ambiguous.

It may mean:

EMPTY

head = 0
tail = 0
count = 0

or:

FULL

head = 0
tail = 0
count = capacity

Tracking count removes this ambiguity.

Therefore:

empty:
    count == 0

full:
    count == capacity

5. dma_idx vs Ring Positions

The DMA producer position is tracked separately from the ready-ring positions.

The simulator uses:

dma_idx

to identify the slot currently owned by DMA.

The roles are therefore:

dma_idx
    = current DMA producer slot

ring.head
    = next completion publication position

ring.tail
    = next CPU acquisition position

ring.count
    = number of READY slots waiting for the CPU

During normal streaming, dma_idx and head may often advance together.

However, they describe different concepts.

dma_idx belongs to the DMA producer state.

head and tail belong to the completed-data queue.

Keeping them separate makes the ownership model explicit and allows invalid state transitions to be detected.


6. Slot Ownership Lifecycle

Each DMA buffer slot moves through an explicit ownership lifecycle.

The normal sequence is:

FREE
DMA_OWNED
READY
CPU_OWNED
FREE

The meanings are:

State Meaning
FREE The slot is available for future DMA reuse
DMA_OWNED DMA may currently write the slot
READY DMA completed the slot and published it to the consumer
CPU_OWNED The CPU consumer acquired the slot and may access it

This ownership model separates DMA activity from ready-ring bookkeeping.

A slot becomes READY when DMA completion publishes it:

DMA_OWNED
DMA completion
READY

The CPU then acquires it:

READY
acquire
CPU_OWNED

After processing:

CPU_OWNED
release
FREE

Only a FREE slot may safely become the next DMA_OWNED slot.


7. Publishing DMA Completion

When the simulated DMA completes the current slot, the client performs several operations.

Conceptually:

DMA completes current slot
write simulated device data
assign sequence number
publish current slot
DMA_OWNED → READY
advance ring.head
increment ring.count
find next cyclic DMA slot

The simulator also verifies:

completed slot index == ring.head

because cyclic completion is expected to occur in ring order in this simplified model.

The sequence number is independent of the physical slot index.

For example:

Slot 0:
    sequence 0
    sequence 4
    sequence 8
    ...

Slot 1:
    sequence 1
    sequence 5
    sequence 9
    ...

The slot index identifies storage location.

The sequence number identifies which generation of streamed data occupies that slot.


8. CPU Acquire and Release

The ready ring contains only completed slots that have not yet been acquired by the CPU.

When the CPU acquires a slot:

READY
CPU_OWNED

tail++
count--

Advancing tail during acquire means the slot is removed from the ready queue immediately.

The slot itself remains protected by:

state == CPU_OWNED

until the CPU explicitly releases it.

Therefore the ready-ring state and slot ownership state have different responsibilities:

head / tail / count
    → completed-data queue

slot state
    → actual ownership of each DMA buffer region

When the consumer finishes:

CPU_OWNED
release
FREE

The release operation does not modify:

  • head
  • tail
  • count
  • dma_idx

It only returns the slot to the pool of DMA-reusable storage.


9. Persistent DMA Mapping

Unlike the Day109 one-shot model, Day110 keeps the complete streaming buffer mapped while the stream is active.

Conceptually:

CPU Buffer
dma_map_single()
Persistent Streaming Mapping

┌────────┬────────┬────────┬────────┐
│ Slot 0 │ Slot 1 │ Slot 2 │ Slot 3 │
└────────┴────────┴────────┴────────┘

mapping remains valid
        ├── Slot 0 completion
        ├── Slot 1 completion
        ├── Slot 2 completion
        ├── Slot 3 completion
        └── cyclic reuse

The buffer is unmapped only after the stream has stopped and all outstanding CPU-visible slots have been released.

This gives the mapping a longer lifetime than any individual slot transaction.


10. Partial DMA Synchronization

A persistent mapping creates a new synchronization requirement.

The complete DMA buffer may remain mapped while only one completed slot is transferred to CPU ownership.

Therefore synchronizing the entire mapping for every slot would not accurately model per-slot ownership.

The Day110 simulator extends the existing DMA-mapping simulator with range-based synchronization helpers:

dma_sync_single_range_for_cpu()
dma_sync_single_range_for_device()

These are simulator APIs, not Linux kernel API names.

The range is described relative to the base DMA mapping:

DMA mapping base
┌────────┬────────┬────────┬────────┐
│ Slot 0 │ Slot 1 │ Slot 2 │ Slot 3 │
└────────┴────────┴────────┴────────┘
             offset + size

For CPU access:

persistent mapping
      +
slot offset
      +
slot length
sync requested subrange for CPU

Before the slot becomes reusable by DMA:

CPU finishes slot
sync requested subrange for device
slot becomes FREE

The original whole-mapping simulator APIs remain unchanged.

This preserves the previous simulator behavior while adding a separate capability for persistent streaming mappings.


11. Consumer Lag

The DMA producer and CPU consumer do not need to operate at exactly the same instant.

For example:

DMA:
S0 complete
S1 complete
S2 complete

CPU:
                  acquire S0
                  acquire S1
                  acquire S2

During this period:

ring.count = 3

The consumer is behind the producer, but this is not yet an overrun.

The ring exists specifically to tolerate a bounded amount of producer-consumer timing difference.

The important condition is whether the DMA producer reaches a slot that has not yet been released.


12. DMA Overrun

An overrun occurs when cyclic DMA needs to reuse the next slot but that slot is not FREE.

For example:

Slot 0 = READY
Slot 1 = READY
Slot 2 = READY
Slot 3 = DMA_OWNED

DMA completes Slot 3:

Slot 3
DMA_OWNED
READY

The completed slot is valid and remains published.

The DMA producer then wraps:

next slot = Slot 0

but:

Slot 0 == READY

The consumer has not released the old data.

Therefore:

next slot != FREE
overrun detected
overrun_count++
stream cannot safely advance

The simulator returns failure for this completion operation after preserving the successfully completed current slot.

The resulting full-ring state is:

head = 0
tail = 0
count = capacity

and every slot is READY.

The Day110 simulator treats this overrun as a fatal streaming condition for the current run rather than silently overwriting unread data.


13. Callback-Side Ring Updates

A DMA completion callback should not perform operations that may sleep.

Small ring metadata updates can be performed in the completion path when protected by synchronization appropriate to the execution context.

Possible mechanisms include:

  • spinlocks
  • atomic operations
  • lockless single-producer/single-consumer designs when their assumptions are valid

A mutex is not appropriate in a callback context that cannot sleep.

The simulator itself is single-threaded and therefore does not implement real callback locking, but the ownership model is designed so that synchronization requirements remain visible.

In a real driver, the exact locking strategy depends on:

  • callback execution context
  • number of producers
  • number of consumers
  • process-context access
  • interrupt-context access
  • subsystem buffering architecture

14. Cyclic DMA and Peripheral Trigger Models

Cyclic DMA does not mean that the DMA controller independently decides when peripheral data should be transferred.

For slave DMA, actual data movement normally depends on DMA requests generated by the peripheral.

Conceptually:

DMA descriptor ready
DMA controller armed
peripheral generates DMA request
DMA controller transfers data

The peripheral data-production model therefore affects whether cyclic DMA is a natural fit.


15. Continuous ADC Sampling

A continuously clocked ADC may produce samples at a regular rate.

Conceptually:

Sample Clock
    ├── sample
    ├── sample
    ├── sample
    ├── sample
Peripheral DMA Requests
Cyclic DMA Buffer

For example, a continuously operating ADC may generate samples at 1 kHz while DMA continuously transfers those samples into a cyclic buffer.

The stream has no natural request boundary for each individual block.

Period completion therefore means:

another region of the continuous stream is ready

rather than:

the entire acquisition request has finished

This is a natural use case for persistent cyclic DMA.


16. FIFO-Threshold Sensor Acquisition

A sensor such as an accelerometer may instead accumulate samples in an internal FIFO and assert a GPIO interrupt when a threshold is reached.

For an external SPI sensor:

Accelerometer
    │ samples internally
Sensor FIFO
    │ threshold reached
GPIO IRQ
Driver
Start SPI FIFO Read
One-Shot DMA Transfer
Block Complete

In this architecture, the GPIO interrupt creates a natural request boundary.

Each interrupt may correspond to one bounded SPI FIFO read.

Therefore one-shot DMA may be simpler than a permanently running cyclic DMA stream.

The distinction is not simply:

ADC → cyclic
sensor → one-shot

Instead, the important question is:

Does the peripheral expose a continuous DMA request stream,
or does software initiate bounded transfers in response to events?

Hardware architecture and peripheral interface behavior determine the appropriate DMA model.


17. Continuous DMA Stream Simulator

The Day110 simulator extends the previous DMA-mapping simulator into a persistent streaming model.

The architecture is:

Simulated Peripheral Data
DMA Stream Producer
Persistent DMA Mapping
┌────────┬────────┬────────┬────────┐
│ Slot 0 │ Slot 1 │ Slot 2 │ Slot 3 │
└────────┴────────┴────────┴────────┘
Ready Ring Metadata
CPU Consumer

The stream state lifecycle is:

IDLE
map
IDLE + mapped
prepare
PREPARED
start
RUNNING
terminate_sync()
TERMINATED
drain / release outstanding slots
unmap
IDLE

The first slot becomes DMA_OWNED when the stream starts.

Each completion publishes the current slot and attempts to acquire the next cyclic slot for DMA.


18. Case 1 — Normal Producer / Consumer

The first test immediately consumes each completed DMA slot.

The sequence is:

DMA completes Slot 0
Slot 0 READY
CPU acquire
Slot 0 CPU_OWNED
sync range for CPU
verify payload
release
Slot 0 FREE

The same sequence repeats for all four slots.

The observed result was:

[TEST] consumed slot=0 sequence=0
[TEST] consumed slot=1 sequence=1
[TEST] consumed slot=2 sequence=2
[TEST] consumed slot=3 sequence=3
[TEST] CASE 1 PASS

No overrun occurs because each slot is released before DMA wraps around to it.


19. Case 2 — Consumer Lag Without Overrun

The second test allows DMA to complete three slots before the CPU begins consuming them.

The intermediate state is:

Slot 0 = READY
Slot 1 = READY
Slot 2 = READY
Slot 3 = DMA_OWNED

ring.count = 3

The consumer then acquires and releases the three completed slots in order.

The observed result was:

[TEST] queued slots=3
[TEST] consumed slot=0 sequence=0
[TEST] consumed slot=1 sequence=1
[TEST] consumed slot=2 sequence=2
[TEST] CASE 2 PASS

This verifies that consumer lag alone is not an error.

The error occurs only when the producer exhausts the available reusable slots.


20. Case 3 — Cyclic DMA Overrun

The third test deliberately does not consume any completed slot.

After all four slots complete:

Slot 0 = READY
Slot 1 = READY
Slot 2 = READY
Slot 3 = READY

head = 0
tail = 0
count = 4
capacity = 4

DMA then needs to reuse Slot 0, but Slot 0 is still READY.

The simulator detects:

next slot != FREE

and increments the overrun counter.

The observed state was:

state              : RUNNING
mapped             : yes
peripheral_enabled : yes
dma_idx            : 3
ring head          : 0
ring tail          : 0
ring count         : 4
ring capacity      : 4
next_sequence      : 4
overrun_count      : 1

All four completed slots remain available to the consumer.

The test then terminates the stream, drains the remaining completed slots, releases them, and safely removes the persistent DMA mapping.


21. Stream Termination

Synchronous stream termination stops DMA-side activity but does not automatically revoke CPU ownership of completed data.

The simulator uses the following policy:

DMA_OWNED
    → FREE

READY
    → preserve READY

CPU_OWNED
    → preserve CPU_OWNED

FREE
    → remain FREE

The important distinction is:

DMA synchronization complete
CPU consumer lifetime complete

After termination, completed READY slots may still be acquired and consumed.

A CPU_OWNED slot remains owned by the CPU until explicitly released.

This prevents stream teardown from silently invalidating memory still in use by the consumer.


22. Safe Unmapping

The persistent mapping may be removed directly from:

IDLE + mapped

when streaming never started.

After a running stream has been terminated, unmapping requires every slot to be FREE.

Therefore:

TERMINATED
check all slots
    ├── READY      → reject
    ├── CPU_OWNED  → reject
    ├── DMA_OWNED  → invalid after synchronized termination
    └── all FREE   → safe
dma_unmap_single()
IDLE + unmapped

This ensures that neither DMA nor the CPU consumer still depends on the persistent mapping.


23. Important Observations

Persistent Mapping Changes the Ownership Granularity

A one-shot mapping can often be considered as one ownership unit.

A streaming mapping may contain several regions with different runtime states.

Therefore:

mapping lifetime
slot ownership lifetime

Ring Metadata Does Not Replace Slot Ownership

head, tail, and count describe the ready queue.

They do not completely describe who currently owns every slot.

CPU_OWNED slots have already left the ready queue but still cannot be reused by DMA.

Consumer Lag Is Expected

A ring buffer exists to absorb bounded producer-consumer timing differences.

Therefore:

consumer behind producer
overrun

Overrun occurs only when the producer needs storage that has not yet been released.

Completion and Next-Slot Acquisition Are Separate Events

The current slot may complete successfully even when DMA cannot safely continue to the next slot.

Therefore an overrun can occur after the current slot has already transitioned to READY.

DMA Synchronization May Need Subrange Granularity

With a persistent multi-slot mapping, CPU and device ownership may transition one slot at a time.

The simulator therefore requires range-based synchronization instead of synchronizing the entire mapping for every completion.

Peripheral Architecture Determines the DMA Model

A continuously clocked peripheral naturally fits a persistent streaming model.

A FIFO-based SPI sensor triggered by a GPIO threshold interrupt may instead expose a sequence of bounded read requests.

The DMA architecture should follow the peripheral's actual data-production and request model.


Summary

Day110 extended the Day109 one-shot DMA client architecture into a continuous streaming model.

The central architecture is:

Persistent DMA Mapping
DMA Buffer Slots
DMA_OWNED
Completion
READY
CPU Acquire
CPU_OWNED
Partial DMA Sync
CPU Consume
Release
FREE
DMA Reuse

The ready ring is tracked using:

head
tail
count

while the DMA producer position is tracked separately using:

dma_idx

The most important ownership rule is:

DMA may reuse a slot
        only when
slot state == FREE

If cyclic DMA reaches a slot that is still READY or CPU_OWNED, the consumer has not released the storage quickly enough and the stream has overrun.

The simulator verified three cases:

  1. Normal producer-consumer operation
  2. Temporary consumer lag without overrun
  3. Cyclic DMA overrun when all slots remain unread

The resulting model connects DMA Engine streaming behavior with the producer-consumer and buffer-ownership problems that appear in real continuous acquisition drivers.


Next Plan

Continue from the continuous DMA streaming model toward integration with a real kernel buffering architecture.

The next topic should build on:

Continuous DMA Acquisition
Completed DMA Blocks
Kernel Buffering Framework
Producer / Consumer Synchronization
Userspace Data Access

This provides the foundation for studying how subsystem frameworks such as IIO manage continuous buffered acquisition between a hardware producer and userspace consumer.