Skip to content

Day110 Lab — Continuous DMA Streaming and Ring Buffer Ownership

Goal

Build a simplified continuous RX DMA streaming simulator and use it to verify the interaction between:

  • persistent DMA mapping
  • cyclic DMA buffer slots
  • DMA producer position
  • ready-ring producer and consumer positions
  • per-slot ownership
  • DMA completion
  • CPU synchronization
  • CPU acquire and release
  • consumer lag
  • DMA overrun
  • synchronous termination
  • safe DMA unmapping

The lab extends the one-shot DMA client lifecycle from Day109 into a persistent streaming model.

The central safety rule is:

DMA may reuse a buffer slot only after the CPU consumer
has released that slot back to the DMA producer.

The simulator intentionally models only the client-side streaming lifecycle. It does not reproduce a complete DMA controller driver or DMA Engine provider.


1. Simulator Architecture

The lab combines the existing simulation layers with a new DMA streaming layer:

Test Application
DMA Stream Simulator
       ├── stream state
       ├── cyclic producer position
       ├── ready-ring metadata
       ├── slot ownership
       ├── sequence tracking
       └── overrun detection
DMA Mapping Simulator
       ├── persistent streaming mapping
       ├── CPU-visible view
       ├── device-visible view
       └── range synchronization
Kernel Memory Simulator
       └── kzalloc() / kfree()

Each layer has a separate responsibility.

The kernel-memory simulator owns allocation lifetime.

The DMA-mapping simulator owns mapping lifetime and simulated CPU/device visibility.

The DMA-stream simulator owns the continuous producer-consumer lifecycle.


2. Stream State Machine

The DMA stream uses the following simplified states:

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

State Meaning

State Meaning
SIM_DMA_STREAM_IDLE Stream is inactive; the buffer may be mapped or unmapped
SIM_DMA_STREAM_PREPARED Ring state is initialized and ready to start
SIM_DMA_STREAM_RUNNING Peripheral and cyclic DMA producer are active
SIM_DMA_STREAM_TERMINATED DMA-side activity has been synchronously stopped

The mapping lifetime is independent of the stream state.

For example:

state  = IDLE
mapped = true

is valid after mapping but before preparation.


3. Persistent DMA Buffer

The simulator maps one complete RX buffer and keeps that mapping valid for the streaming lifetime.

The buffer is divided into fixed-size slots:

Persistent DMA Buffer

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

Each slot refers to a subrange of the same mapping.

For slot n:

offset = n × slot size
length = slot size

The slot payload is not copied into a second software ring.

Instead, the ring tracks metadata describing which DMA-buffer regions are ready for the CPU.


4. DMA Buffer Slot Descriptor

Each slot contains metadata similar to:

index
offset
length
sequence
state

The fields have different purposes.

Field Meaning
index Physical slot position inside the cyclic DMA buffer
offset Byte offset from the persistent buffer base
length Number of bytes in the slot
sequence Generation number assigned when DMA completes the slot
state Current ownership state

The slot index is reused cyclically.

The sequence number continues increasing across reuse.

For example:

Slot 0:
    sequence 0
    sequence 4
    sequence 8

This separates physical storage position from stream ordering.


5. Slot Ownership

Continuous DMA buffer ownership

The DMA ring combines cyclic producer movement with explicit per-slot ownership. The ready-ring metadata and slot ownership state describe different parts of the producer-consumer lifecycle.

Each slot follows this lifecycle:

FREE
DMA_OWNED
READY
CPU_OWNED
FREE

FREE

The slot is available for DMA reuse.

DMA_OWNED

DMA may write the slot.

The CPU consumer must not treat the payload as completed data.

READY

DMA completed the slot and published it to the ready ring.

The CPU has not acquired it yet.

CPU_OWNED

The CPU consumer acquired the completed slot.

The slot cannot be reused by DMA until the consumer releases it.

The central reuse rule is:

next slot state == FREE

Anything else indicates that the storage is still in use.


6. Ready Ring Descriptor

The simulator uses a ring descriptor containing:

head
tail
count
capacity
slots[]

The runtime positions mean:

Field Meaning
head Position where the next DMA completion is published
tail Position where the CPU acquires the next READY slot
count Number of READY slots waiting for the CPU
capacity Total number of slots

The ready ring is empty when:

count == 0

It is full when:

count == capacity

Tracking count avoids ambiguity when:

head == tail

because that condition may represent either an empty or full ring.


7. DMA Producer Position

The DMA producer uses a separate index:

dma_idx

Its meaning is:

dma_idx
    = slot currently owned by DMA

This is separate from:

ring.head
    = next completion publication position

ring.tail
    = next CPU acquisition position

Although dma_idx and head often advance together during normal streaming, they represent different runtime responsibilities.


8. Stream Initialization

The caller allocates the complete streaming buffer:

dma_buffer = kzalloc(SIM_DMA_BUFFER_SIZE);

The stream initialization API stores:

  • CPU buffer
  • buffer length
  • initial mapping state
  • initial stream state
  • DMA producer position
  • sequence counter
  • overrun counter
  • peripheral state

The ring descriptor is also initialized.

The initial state is:

stream state       = IDLE
mapped             = false
peripheral_enabled = false
dma_idx            = 0

head               = 0
tail               = 0
count              = 0

all slots           = FREE

9. Persistent RX Mapping

The complete stream buffer is mapped once:

dma_map_single(buffer,
               buffer_length,
               DMA_FROM_DEVICE)

The mapping remains active across multiple slot completions.

Conceptually:

map complete buffer
Slot 0 DMA
Slot 1 DMA
Slot 2 DMA
Slot 3 DMA
wrap and reuse
...
terminate
unmap complete buffer

This differs from the one-shot model where mapping lifetime closely follows one transaction.


10. Stream Preparation

Preparation requires:

mapped == true
state  == IDLE

Preparation resets runtime streaming state:

  • ring descriptor
  • dma_idx
  • sequence counter
  • overrun counter
  • peripheral state

The transition is:

IDLE + mapped
prepare
PREPARED

Reinitializing the ring during preparation ensures that a new streaming session does not inherit stale slot ownership from an earlier run.


11. Starting the Stream

Starting requires:

state == PREPARED
mapped == true
peripheral_enabled == false

The first DMA slot is selected by:

ring.slots[dma_idx]

That slot must be:

FREE

The start operation then performs:

FREE
DMA_OWNED

and:

peripheral_enabled = true
state              = RUNNING

The resulting initial runtime state is:

              DMA
┌───────────┬──────┬──────┬──────┐
│ DMA_OWNED │ FREE │ FREE │ FREE │
│  Slot 0   │ S1   │ S2   │ S3   │
└───────────┴──────┴──────┴──────┘

12. Simulating DMA Completion

sim_dma_complete_slot() models completion of the current DMA-owned slot.

The operation validates:

  • stream exists
  • persistent mapping exists
  • peripheral is enabled
  • stream is RUNNING
  • input length matches the slot size
  • dma_idx is valid
  • current slot is DMA_OWNED

The simulated device then writes into the current slot range.

Conceptually:

stream DMA base
      +
slot offset
device-visible slot

After the write succeeds, the slot receives the next sequence number.

The current slot is then published.


13. Publishing a Completed Slot

Publishing performs:

DMA_OWNED
READY

and updates the ready ring:

head = next(head)
count++

The simulator also verifies:

slot index == head

before publication.

This check verifies the simplified assumption that cyclic DMA completions occur in ring order.

The ready slot now contains completed data but has not yet been acquired by the CPU.


14. Selecting the Next DMA Slot

After publishing the current completion:

next_dma_idx = next(dma_idx)

The simulator checks the state of that next slot.

If:

next slot == FREE

DMA may safely acquire it:

FREE
DMA_OWNED

and:

dma_idx = next_dma_idx

The stream then continues.

If the next slot is not FREE, the producer has caught the consumer.

This is an overrun.


15. CPU Acquire

The CPU acquires the next completed slot from:

ring.slots[ring.tail]

The ring must not be empty, and the selected slot must be:

READY

Acquisition performs:

READY
CPU_OWNED

and removes the slot from the ready queue:

tail = next(tail)
count--

The slot remains unavailable to DMA because its ownership state is still:

CPU_OWNED

The ready-ring metadata therefore tracks queued completions, while slot state tracks actual buffer ownership.


16. Range Synchronization for CPU Access

The persistent DMA mapping contains multiple slots.

Only the completed slot needs to become visible to the CPU consumer.

The DMA-mapping simulator therefore adds:

dma_sync_single_range_for_cpu()

This is a simulator extension, not the name of a Linux kernel DMA API.

The API identifies:

  • persistent mapping base
  • offset inside the mapping
  • synchronization size
  • DMA direction

For one slot:

mapping base
    +
slot offset
    +
slot length

Conceptually:

┌────────┬────────┬────────┬────────┐
│ Slot 0 │ Slot 1 │ Slot 2 │ Slot 3 │
└────────┴────────┴────────┴────────┘
          synchronize only
          this completed range

This allows the simulator to model per-slot visibility while keeping the complete buffer mapped.


17. CPU Release

After the consumer finishes processing a slot, the slot is synchronized back for device access using:

dma_sync_single_range_for_device()

This is also a simulator extension.

The ownership transition is:

CPU_OWNED
sync range for device
FREE

The release operation does not modify:

  • head
  • tail
  • count
  • dma_idx

The slot simply becomes eligible for future cyclic DMA reuse.


18. Normal Producer / Consumer Test

The first test consumes each slot immediately after completion.

The lifecycle is:

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

The same sequence is repeated for all four slots.

Observed output:

[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.


19. Consumer Lag Test

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

The intermediate state is:

┌───────┬───────┬───────┬───────────┐
│ READY │ READY │ READY │ DMA_OWNED │
│  S0   │  S1   │  S2   │    S3     │
└───────┴───────┴───────┴───────────┘

head  = 3
tail  = 0
count = 3

The consumer is behind the producer, but the producer still has one available DMA slot.

The consumer then drains:

S0
S1
S2

Observed output:

[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:

consumer lag != overrun

A ring exists to tolerate bounded producer-consumer timing differences.


20. Overrun Test

The third test deliberately leaves every completed slot unread.

Before the final completion:

┌───────┬───────┬───────┬───────────┐
│ READY │ READY │ READY │ DMA_OWNED │
│  S0   │  S1   │  S2   │    S3     │
└───────┴───────┴───────┴───────────┘

Slot 3 completes successfully:

Slot 3
DMA_OWNED
READY

The ring becomes full:

┌───────┬───────┬───────┬───────┐
│ READY │ READY │ READY │ READY │
│  S0   │  S1   │  S2   │  S3   │
└───────┴───────┴───────┴───────┘

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

The DMA producer then wraps to Slot 0.

However:

Slot 0 != FREE

Therefore the simulator detects an overrun:

overrun_count++
return false

The completed Slot 3 is not rolled back.

Its DMA completion already occurred and the data remains valid.

Observed output:

[DMA] next_slot state mismatched, expected=FREE actual=READY
[DMA] overrun count=1
[TEST] overrun detected correctly

The final stream state is:

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

This verifies that a full ring is distinguishable from an empty ring even though:

head == tail

because:

count == capacity

21. Synchronous Stream Termination

sim_dma_stream_terminate_sync() stops DMA-side activity.

For a running stream:

peripheral_enabled = false

The slot policy is:

Slot State Termination Result
DMA_OWNED Change to FREE after DMA-side activity is stopped
READY Preserve completed data
CPU_OWNED Preserve CPU ownership
FREE Remain FREE

This models an important lifetime distinction:

DMA stopped
CPU finished using completed data

A synchronous DMA termination does not automatically release data still owned by the consumer.


22. Draining After Termination

The simulator allows the CPU to acquire completed slots while the stream is:

RUNNING

or:

TERMINATED

This allows teardown to preserve completed data.

For example:

terminate stream
READY slots remain
CPU acquire
sync for CPU
process
release
FREE

Only after all outstanding slots are released is the persistent mapping safe to remove.


23. Safe Unmapping

Direct unmapping is allowed from:

IDLE + mapped

because streaming has not started.

Unmapping is rejected from:

PREPARED
RUNNING

because the stream lifecycle has not been safely terminated.

For a terminated stream, every slot must be:

FREE

before unmapping.

Therefore:

TERMINATED
check all slots
    ├── READY     → reject
    ├── CPU_OWNED → reject
    └── all FREE  → continue
dma_unmap_single()
mapped = false
state  = IDLE

This prevents the mapping from being destroyed while completed or CPU-owned data still depends on it.


24. Test Result

The final test result was:

Day110 - Continuous DMA Streaming Simulator

========================================
CASE 1 - Normal Producer / Consumer
========================================
[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

========================================
CASE 2 - Consumer Lag Without Overrun
========================================
[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

========================================
CASE 3 - Cyclic DMA Overrun
========================================
[DMA] next_slot state mismatched, expected=FREE actual=READY
[DMA] overrun count=1
[TEST] overrun detected correctly
[DMA STREAM]
  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
  slot[0]            : READY     seq=0 offset=0 len=16
  slot[1]            : READY     seq=1 offset=16 len=16
  slot[2]            : READY     seq=2 offset=32 len=16
  slot[3]            : READY     seq=3 offset=48 len=16
[TEST] CASE 3 PASS

========================================
Test Summary
========================================
CASE 1 - Normal Producer / Consumer : PASS
CASE 2 - Consumer Lag              : PASS
CASE 3 - Cyclic DMA Overrun        : PASS

[TEST] All Day110 tests passed

All three scenarios passed.


25. Key Takeaways

A Persistent Mapping Can Contain Multiple Ownership Regions

The complete DMA buffer remains mapped while individual slots independently transition between DMA and CPU use.

Therefore:

mapping lifetime
slot ownership lifetime

Ready-Ring State and Slot Ownership Are Different

The ready ring tracks completed slots waiting for acquisition.

A CPU_OWNED slot has already left the ready ring but is still unavailable to DMA.

Therefore both mechanisms are required.

count Resolves Ring Ambiguity

When:

head == tail

the ring may be empty or full.

count makes the state explicit.

Consumer Lag Is Normal Until Storage Is Exhausted

Several READY slots do not automatically indicate an error.

Overrun occurs only when DMA needs to reuse a slot that is not FREE.

Current Completion Can Succeed Before Overrun Is Detected

The current slot may become READY successfully.

The failure occurs when the producer tries to acquire the next cyclic slot.

Therefore the completed slot must not be rolled back.

DMA Termination Does Not End CPU Ownership

Stopping DMA prevents future device access.

It does not mean that the CPU has finished processing previously completed data.

Unmapping Is the Final Lifetime Boundary

The persistent mapping may be removed only after:

DMA stopped
    +
all READY data drained
    +
all CPU_OWNED slots released

This keeps DMA mapping lifetime, DMA execution lifetime, and CPU consumer lifetime explicitly separated.


Summary

This lab extends the one-shot DMA client model into a continuous streaming architecture.

The complete lifecycle is:

Allocate Buffer
Persistent DMA Map
Prepare Stream
Start
DMA_OWNED
DMA Completion
READY
CPU Acquire
CPU_OWNED
Range Sync for CPU
Process Data
Range Sync for Device
Release
FREE
Cyclic DMA Reuse

If the DMA producer reaches a slot that has not returned to FREE:

Producer catches Consumer
Overrun

The resulting ownership model provides a foundation for understanding how real continuous acquisition drivers coordinate DMA buffers, completion callbacks, kernel consumers, and subsystem buffering frameworks.