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.