Skip to content

Day109 Lab — One-Shot DMA Client Lifecycle

Goal

Build a simplified one-shot RX DMA client simulator and use it to verify the ordering between:

  • DMA buffer allocation
  • DMA mapping
  • descriptor preparation
  • descriptor submission
  • issue pending
  • peripheral start
  • DMA completion
  • CPU synchronization
  • DMA unmapping
  • DMA termination
  • buffer release

The lab focuses on one central safety rule:

A DMA mapping and its backing buffer must remain valid
while the DMA transaction may still access them.

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


1. Simulator Architecture

The lab combines previously implemented simulation layers:

Test Application
       │
       ▼
DMA Client Simulator
       │
       ├── transaction state
       ├── peripheral state
       └── completion callback
       │
       ▼
DMA Mapping Simulator
       │
       ├── CPU-visible view
       ├── device-visible view
       └── ownership synchronization
       │
       ▼
Kernel Memory Simulator
       │
       └── kzalloc() / kfree()

Each layer has a separate responsibility.

The kernel-memory simulator owns allocation lifetime.

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

The DMA-client simulator owns the transaction and peripheral lifecycle.


2. Simplified DMA State Machine

The DMA client simulator uses the following states:

IDLE
  ↓
PREPARED
  ↓
SUBMITTED
  ↓
ARMED
  ↓
ACTIVE
  ├────────────► COMPLETED
  │
  └────────────► TERMINATED

State Meaning

State Meaning
SIM_DMA_IDLE No descriptor has been prepared
SIM_DMA_PREPARED Descriptor contains the DMA address, length, and direction
SIM_DMA_SUBMITTED Descriptor has been submitted
SIM_DMA_ARMED Transaction has been issued and is ready for peripheral DMA requests
SIM_DMA_ACTIVE Peripheral is enabled and the transfer is active
SIM_DMA_COMPLETED Transfer completed normally
SIM_DMA_TERMINATED Outstanding transfer was synchronously terminated

The distinction between ARMED and ACTIVE is intentional:

issue_pending()
      ↓
    ARMED
      │
      │ start peripheral
      ▼
    ACTIVE

Issuing the DMA transaction does not itself mean that the peripheral has started producing DMA requests.


3. Request Object

The simulated request tracks three different categories of state:

sim_dma_request
    │
    ├── CPU buffer
    ├── buffer length
    │
    ├── DMA address
    ├── mapping state
    │
    ├── descriptor state
    │
    └── peripheral enabled state

This separation is important because:

mapping exists
        ≠
DMA transaction is active
        ≠
peripheral is enabled

For example, after normal DMA completion:

DMA state         : COMPLETED
Mapping state     : true
Peripheral enabled: false

The transaction has finished, but the mapping may still exist until the client releases it.


4. Initialization

The caller allocates the RX buffer:

rx_buffer = kzalloc(TEST_RX_SIZE, GFP_KERNEL);
if (!rx_buffer)
    goto test_out;

The request initialization API stores the caller-owned buffer:

sim_dma_request_init(&request,
                     rx_buffer,
                     TEST_RX_SIZE);

The DMA client simulator does not own the backing allocation.

Therefore:

caller
  │
  ├── kzalloc()
  │
  ├── initialize DMA request
  │
  ├── execute DMA lifecycle
  │
  └── kfree()

The request object only references the buffer.


5. DMA Mapping

The RX buffer is mapped using a streaming DMA_FROM_DEVICE mapping:

dma_addr = dma_map_single(request->buffer,
                          request->length,
                          DMA_FROM_DEVICE);

The successful mapping is stored in the request:

CPU buffer
    │
    ▼
dma_map_single()
    │
    ▼
DMA address

Descriptor preparation is rejected unless the mapping exists.

This enforces:

Buffer
  ↓
DMA Mapping
  ↓
Descriptor Preparation

6. Descriptor Preparation

The simulated descriptor records:

DMA address
length
direction
callback
callback context
state

For the RX path:

DMA Engine direction
    SIM_DMA_DEV_TO_MEM

DMA mapping direction
    DMA_FROM_DEVICE

These describe the same physical data direction:

Peripheral
    ↓
Memory

but belong to different API domains and should not be treated as interchangeable constants.


7. Submit and Issue

Submission performs:

PREPARED
    ↓
SUBMITTED

Issue pending performs:

SUBMITTED
    ↓
ARMED

At this point the test dumps the request.

Expected state:

DMA state         : ARMED
Mapping state     : true
Peripheral enabled: false

This verifies that the DMA transaction can be ready before the peripheral starts.


8. Peripheral Start

Starting the simulated peripheral requires:

SIM_DMA_ARMED

The operation then performs:

peripheral_enabled = true
        ↓
SIM_DMA_ACTIVE

The resulting state is:

DMA state         : ACTIVE
Mapping state     : true
Peripheral enabled: true

The simulated DMA transaction may now access the mapping.


9. Device-Side RX Write

DMA completion is simulated using the device-side access API from the DMA-mapping simulator.

Conceptually:

Peripheral Data
      │
      ▼
dma_device_write()
      │
      ▼
Device-visible DMA memory

The test does not directly write to the CPU-visible RX buffer.

This preserves the distinction between:

CPU-visible view
        ≠
device-visible view

and allows the lab to exercise the synchronization boundary explicitly.


10. Completion Ordering

Normal completion follows:

ACTIVE
   ↓
device-side write
   ↓
disable peripheral
   ↓
COMPLETED
   ↓
callback

The state transition occurs before callback invocation.

Therefore the callback observes:

[CALLBACK] DMA completed: state=COMPLETED

This models the callback as notification of a completed transaction rather than the operation that causes the transaction to become complete.


11. Synchronization for CPU Access

After a DMA_FROM_DEVICE transfer, the simulator requires an explicit CPU synchronization step before the CPU consumes the received data.

The lab performs:

COMPLETED
    ↓
sync for CPU
    ↓
CPU-visible RX data becomes valid

This is performed before destroying the mapping.

The sequence is therefore:

DMA completion
    ↓
sync for CPU
    ↓
unmap
    ↓
verify data

The lab intentionally exposes this boundary because the DMA-mapping simulator keeps CPU-visible and device-visible memory views separate.


12. Safe Unmapping

The DMA client simulator checks whether the current transaction state may still access the mapping.

The protected states are:

PREPARED
SUBMITTED
ARMED
ACTIVE

These states reject unmapping.

After:

COMPLETED

or:

TERMINATED

the DMA transaction itself no longer owns the mapping.

The mapping may then be released.

After successful unmapping:

mapped   = false
dma_addr = 0

13. Synchronous Termination Model

The lab provides a simplified:

sim_dma_terminate_sync()

The supported outstanding states are:

SUBMITTED
ARMED
ACTIVE

Termination performs:

disable peripheral
       ↓
terminate / synchronize
       ↓
TERMINATED

After the function returns successfully, the simulator guarantees:

the DMA transaction can no longer access the mapping

The lab intentionally does not implement a separate asynchronous termination state.

A more detailed model would require something similar to:

ACTIVE
  ↓
terminate_async()
  ↓
TERMINATING
  ↓
synchronize()
  ↓
TERMINATED

That behavior is outside the scope of this one-shot client lab.


14. Test Case 1 — Normal Completion

The first test exercises the complete RX happy path:

Allocate RX Buffer
        ↓
Initialize Request
        ↓
DMA Map
        ↓
Prepare
        ↓
Submit
        ↓
Issue Pending
        ↓
ARMED
        ↓
Start Peripheral
        ↓
ACTIVE
        ↓
Device Write
        ↓
COMPLETED
        ↓
Callback
        ↓
Sync for CPU
        ↓
Unmap
        ↓
Verify Data
        ↓
Free Buffer

The test data is generated in a simulated peripheral buffer and transferred through the device-visible DMA mapping.

The CPU verifies the RX buffer only after the ownership synchronization step.

Expected result:

[CASE 1] Normal completion

DMA state         : ARMED
Mapping state     : true
Peripheral enabled: false

[CALLBACK] DMA completed: state=COMPLETED

[PASS] CASE 1

This case verifies both the normal DMA lifecycle and RX data visibility.


15. Test Case 2 — Abort While DMA Is Active

The second test deliberately terminates an active transaction.

Before the error is injected:

DMA state         : ACTIVE
Mapping state     : true
Peripheral enabled: true

The simulated error path is:

ACTIVE
    ↓
Peripheral Error
    ↓
terminate_sync()
    ↓
TERMINATED
    ↓
Unmap
    ↓
Free Buffer

Expected result:

[TEST] Simulating peripheral error
[PASS] CASE 2

The test verifies that the mapping is released only after the DMA transaction has reached a synchronized termination point.


16. Test Case 3 — Invalid Cleanup Ordering

The third test deliberately violates the DMA ownership rule.

It first reaches:

ACTIVE

and then attempts:

sim_dma_unmap_rx_buffer()

The operation must fail because DMA may still access the mapping.

Expected output:

[TEST] Attempting unsafe unmap while DMA is active
[DMA] dma may still access the mapping: state=ACTIVE
[TEST] Unsafe unmap correctly rejected

The test then performs the correct recovery sequence:

ACTIVE
    ↓
terminate_sync()
    ↓
TERMINATED
    ↓
unmap

Expected output:

[TEST] Safe unmap succeeded after termination
[PASS] CASE 3

This is the central safety test of the lab.


17. Cleanup Pattern

The test implementation uses reverse-order cleanup labels.

Conceptually:

allocate buffer
    ↓
map
    ↓
prepare / submit / issue
    ↓
start peripheral

Failure cleanup unwinds in reverse:

terminate if required
    ↓
unmap if mapped
    ↓
free buffer
    ↓
destroy memory simulator

The key ordering is:

DMA may access memory
        ↓
terminate / complete
        ↓
DMA safe
        ↓
unmap
        ↓
free

The buffer must never be freed while the DMA transaction may still reference its mapping.


18. Expected Test Summary

A successful run should finish with all three cases passing:

=== Day109 One-Shot DMA Client Lab ===

[CASE 1] Normal completion
...
[PASS] CASE 1

[CASE 2] Abort while DMA active
...
[PASS] CASE 2

[CASE 3] Invalid cleanup
...
[DMA] dma may still access the mapping: state=ACTIVE
[TEST] Unsafe unmap correctly rejected
[TEST] Safe unmap succeeded after termination
[PASS] CASE 3

What This Lab Demonstrates

The lab connects several previously independent concepts:

Kernel Memory
      +
DMA Mapping
      +
DMA Descriptor Lifecycle
      +
DMA Completion
      +
DMA Termination
      +
Peripheral Coordination

The most important result is not the simulated data transfer itself.

The important result is the ownership model:

Buffer allocated
      │
      ▼
Mapping exists
      │
      ▼
DMA may access mapping
      │
      ▼
Complete or terminate
      │
      ▼
DMA no longer accesses mapping
      │
      ▼
Synchronize ownership if required
      │
      ▼
Unmap
      │
      ▼
Free buffer

A correct DMA client must preserve this ordering on both normal and error paths.


Key Takeaways

  • dma_async_issue_pending() makes a transaction ready but does not necessarily mean the peripheral has started transferring.
  • Peripheral start and DMA transaction state must be coordinated explicitly.
  • DMA completion and DMA mapping lifetime are separate.
  • A completion callback should observe an already-completed DMA transaction.
  • RX data ownership must be synchronized before CPU consumption when required by the mapping model.
  • dmaengine_terminate_sync() provides a safe teardown point for outstanding DMA work.
  • A DMA mapping must not be released while DMA may still access it.
  • A backing buffer must not be freed until its DMA mapping has been safely released.
  • Normal completion and error cleanup must obey the same ownership rules.

Next Step

The one-shot client model provides the foundation for continuous acquisition.

The next evolution is:

One-Shot Buffer
      ↓
Persistent Mapping
      ↓
Cyclic / Repeated DMA
      ↓
Period or Block Completion
      ↓
Kernel Buffering
      ↓
Userspace Consumer

This introduces a different lifetime model in which DMA completion may indicate that a block is ready rather than that the overall stream has ended.