Introduction
In our last lesson, we established the crucial theoretical difference between coherent and streaming DMA mappings, framing them as a "shared whiteboard" versus "passing a notebook." We covered the why—address translation and cache coherency—and the core concepts of ownership and synchronization. Your background in embedded development means you understand that theory is only useful when it translates into working code.
This lesson delivers on that promise. We will now focus on the how, directly addressing the learning outcome: Use the kernel's DMA API to allocate and map coherent and streaming DMA buffers within a device driver. We will examine the specific C functions you will use, see how they are implemented in a real driver, and explore different DMA frameworks within the kernel.
1. Implementing Coherent DMA Mappings
As we recapped, coherent mappings are long-lived buffers, allocated at initialization, that guarantee the CPU and device always see the same data. They are ideal for shared control structures.
The primary functions for this are dma_alloc_coherent() and dma_free_coherent().
void *dma_alloc_coherent(struct device *dev,
size_t size,
dma_addr_t *dma_handle,
gfp_t flag);
void dma_free_coherent(struct device *dev,
size_t size,
void *cpu_addr,
dma_addr_t dma_handle);
dma_alloc_coherentdoes two things: it allocates a physically contiguous block of memory and creates a mapping for it.- It returns a kernel virtual address (
void *), which your driver code uses to access the buffer. - It populates
dma_handlewith the bus address (dma_addr_t), which you program into your device's DMA registers.
- It returns a kernel virtual address (
dma_free_coherentcleans up both the mapping and the memory. Note that you must provide all the original parameters.
For a deeper look at the API, including the useful dma_pool interface for allocating many small coherent buffers, please review the following text.
Memory Mapping and DMA - An excerpt from Linux Device Drivers, 3rd Edition
This excerpt from 'Linux Device Drivers, 3rd Edition' details the practical use of the coherent DMA API. Pay special attention to the function signatures and the description of DMA pools, which are a best-practice for managing small, frequently used control blocks.
Please read the section titled 'Setting up coherent DMA mappings' and the following subsection 'DMA pools' (pages 446-447).
Example: Allocating a Control Block
Here’s how you might allocate a control structure in your driver's probe function:
#include <linux/dma-mapping.h>
/* In your device's private data structure */
struct my_device {
// ... other members
struct device *dev; // Pointer to the generic device struct
void *control_block_cpu_va;
dma_addr_t control_block_dma_handle;
};
static int my_driver_probe(struct pci_dev *pdev, const struct pci_device_id *id)
{
struct my_device *my_dev;
// ... initialization code to allocate my_dev ...
my_dev->dev = &pdev->dev;
// Allocate a 1024-byte coherent buffer for control structures
my_dev->control_block_cpu_va = dma_alloc_coherent(my_dev->dev,
1024,
&my_dev->control_block_dma_handle,
GFP_KERNEL);
if (!my_dev->control_block_cpu_va) {
dev_err(my_dev->dev, "Failed to allocate coherent DMA buffer\n");
return -ENOMEM;
}
dev_info(my_dev->dev, "Coherent buffer allocated. CPU VA: %p, DMA Handle: %pad\n",
my_dev->control_block_cpu_va, &my_dev->control_block_dma_handle);
// Now, you can write to control_block_cpu_va and program
// control_block_dma_handle into your device's registers.
return 0;
}
static void my_driver_remove(struct pci_dev *pdev)
{
struct my_device *my_dev = pci_get_drvdata(pdev);
if (my_dev->control_block_cpu_va) {
dma_free_coherent(my_dev->dev,
1024,
my_dev->control_block_cpu_va,
my_dev->control_block_dma_handle);
}
// ... other cleanup ...
}
2. Implementing Streaming DMA Mappings
Streaming mappings are for transient, one-shot data transfers. The process is more involved because your driver must manage buffer ownership and cache synchronization. The full cycle is:
- Map the buffer, transferring ownership to the device.
- Instruct the device to start the transfer.
- Wait for an interrupt signaling completion.
- Synchronize the buffer for CPU access (if necessary).
- Unmap the buffer, returning ownership to the CPU.
The following video provides an excellent, end-to-end demonstration of implementing this cycle for a custom PCI device emulated in QEMU. It will serve as our guide for this section.
Using DMA in a Linux PCI or PCI Express Driver
The video 'Using DMA in a Linux PCI or PCI Express Driver' by Johannes 4GNU_Linux is a practical walkthrough of everything we've discussed. We'll watch it in segments to analyze the key API calls.
There's no need to watch anything just yet. We will proceed step-by-step.
Step 1: Enabling DMA and Setting the Mask
Before any DMA can occur, you must tell the PCI subsystem that your device will be a "bus master"—that is, it can initiate transactions on the bus. You also need to inform the kernel of your device's addressing capabilities.
pci_set_master(pdev): This sets the Bus Master Enable (BME) bit in the device's PCI command configuration register.dma_set_mask_and_coherent(dev, mask): This function checks if the system can support DMA with the given addressmaskand configures the DMA subsystem accordingly. For a typical 32-bit device, you'd useDMA_BIT_MASK(32). For a 64-bit device, you'd first tryDMA_BIT_MASK(64)and fall back to 32-bit if that fails.
Watch this short segment to see pci_set_master being called in the driver's probe function.
Using DMA in a Linux PCI or PCI Express Driver
This clip shows where to enable bus mastering in the probe function.
Watch from 15:12 to 15:43. Notice the call to pci_set_master(pdev).
Step 2: The Core Transfer Function
Now let's look at the function that actually implements a streaming DMA transfer. We will focus on the dma_map_single and dma_unmap_single calls.
dma_addr_t dma_map_single(struct device *dev,
void *cpu_addr,
size_t size,
enum dma_data_direction dir);
void dma_unmap_single(struct device *dev,
dma_addr_t dma_handle,
size_t size,
enum dma_data_direction dir);
The video's dma_transfer function is a perfect illustration. It maps a buffer, programs the device's registers with the resulting DMA address and transfer size, triggers the transfer, and then unmaps the buffer.
Using DMA in a Linux PCI or PCI Express Driver
This segment shows the implementation of the main transfer function. It's the heart of our lesson.
Watch from 15:44 to 20:21. Pay close attention to: The explanation of virtual vs. physical addresses (15:54). The call to dma_map_single to get the DMA address (16:23). The corresponding dma_unmap_single at the end (16:57). How the dma_addr_t variable (buffer_dma_address) is used to program the device's DMA source/destination registers (18:04).
Let's break down the key lines from the video:
-
Get the DMA address:
dma_addr_t buffer_dma_address; buffer_dma_address = dma_map_single(&dev->pdev->dev, buffer, count, direction); // You should ALWAYS check for errors here in a real driver! // if (dma_mapping_error(&dev->pdev->dev, buffer_dma_address)) { /* handle error */ }Here, the driver passes in a kernel virtual pointer (
buffer) and gets back an opaque bus address (buffer_dma_address). Ownership ofbufferis now transferred to the device. -
Program the device:
// Writing to the device's MMIO registers // In this case, for a DMA transfer FROM memory TO device iowrite64(buffer_dma_address, dev->bar0 + DMA_SOURCE_OFFSET); iowrite64(device_offset_in_bar1, dev->bar0 + DMA_DEST_OFFSET); iowrite32(count, dev->bar0 + DMA_COUNT_OFFSET);The
dma_addr_tis programmed directly into the hardware. -
Start the transfer:
// Set the command register to start the transfer iowrite32(DMA_RUN_BIT | direction_flag, dev->bar0 + DMA_COMMAND_OFFSET);At this point, the device starts reading from or writing to system memory.
-
Clean up:
// After the transfer is complete (e.g., in an IRQ or after polling) dma_unmap_single(&dev->pdev->dev, buffer_dma_address, count, direction);This returns ownership to the CPU and frees the mapping resources. It is now safe for your driver code to access the buffer's contents again.
3. Alternative: The DMA Engine Subsystem
The previous example showed a driver managing a device's own DMA controller. However, many embedded SoCs have general-purpose platform DMA engines that multiple device drivers can use. The Linux kernel provides a generic dmaengine subsystem for this.
The API is more abstract: you request a channel from the engine, describe the transfer you want (e.g., memcpy), submit it, and get a callback upon completion.
The next video demonstrates this dmaengine API to perform a memory-to-memory copy. This is highly relevant for your background in embedded systems, as it's a common pattern on SoCs without a dedicated PCI bus.
Let's code a Linux Driver - 30 DMA (Direct Memory Access) Memcopy
This video from Johannes 4GNU_Linux, 'Let's code a Linux Driver - 30 DMA (Direct Memory Access) Memcopy', shows a different but equally important DMA framework.
Watch from 06:40 to 17:25. This covers the entire process. Focus on understanding the sequence of operations: Requesting a channel with dma_request_channel. Allocating a buffer (note the use of dma_alloc_coherent). Preparing a 'descriptor' for the transfer with dma_engine_prepare_dma_memcopy. Setting a callback to be notified on completion. Submitting and launching the transfer with dma_engine_submit and dma_async_issue_pending. Waiting for completion using a completion variable.
While the API calls are different (dma_engine_prepare_*, dma_engine_submit), the underlying concepts remain the same. You are still dealing with DMA addresses, buffer management, and asynchronous completion.
4. Interview Scenario
You are designing a driver for a networking device. The device uses two distinct memory regions for its operation:
- A set of Rx/Tx ring descriptors (16 KB total). The CPU writes new Tx descriptors, and the device writes status back into Rx descriptors. This happens continuously.
- Large data buffers (many kilobytes each) that hold the actual packet data for transmit and receive operations. Each packet transfer is a discrete event.
Question:
For each of these memory regions, would you use a coherent or streaming DMA mapping? Justify your choice and name the key kernel API function(s) you would use for the allocation and mapping of each.
Click for Analysis
-
1. Ring Descriptors (16 KB):
- Mapping Type: Coherent mapping.
- Justification: The ring descriptors represent a shared control structure. Both the CPU (posting new work) and the device (reporting completion status) need to access this memory region concurrently and frequently. A coherent mapping ensures that updates from one are immediately visible to the other without manual synchronization, perfectly fitting this "shared whiteboard" model.
- API Functions:
dma_alloc_coherent()would be called once in the driver'sprobefunction to allocate the entire 16 KB region.dma_free_coherent()would be called in theremovefunction to release it.
-
2. Packet Data Buffers:
- Mapping Type: Streaming mapping.
- Justification: Each packet transfer is a distinct, one-way bulk data transfer. For a transmit operation, the CPU prepares the packet, and then the device reads it. For a receive operation, the device writes the packet, and then the CPU reads it. There is no need for simultaneous access. Streaming mappings are optimized for this "pass the notebook" model and are more performant as they allow the system to make assumptions based on the transfer direction.
- API Functions:
- For transmit (
DMA_TO_DEVICE): Before telling the device to send a packet, the driver would calldma_map_single()(ordma_map_pageif it's a page from the networking stack) on the packet's buffer. After the interrupt confirms the transmission is complete, it would calldma_unmap_single(). - For receive (
DMA_FROM_DEVICE): The driver would provide the device with a buffer viadma_map_single. After the interrupt indicates a packet has been received, the driver might calldma_sync_single_for_cpu()to make the data visible to the CPU, pass the packet up the network stack, and finally calldma_unmap_single().
- For transmit (
Conclusion
In this lesson, you have translated the theory of DMA mappings into practice. You are now equipped to implement both major types of DMA transfers in a Linux device driver.
Key Takeaways:
- Coherent Mappings: Use for long-lived, shared control structures.
- Allocate & Map:
dma_alloc_coherent() - Free:
dma_free_coherent() - For small objects: Use DMA pools via
dma_pool_create()anddma_pool_alloc().
- Allocate & Map:
- Streaming Mappings: Use for transient, one-shot bulk data transfers.
- Enable HW:
pci_set_master() - Map:
dma_map_single(),dma_map_page(), ordma_map_sg()(for scatter-gather). Remember to specify the direction. - Unmap:
dma_unmap_single(),dma_unmap_page(), ordma_unmap_sg(). - Cache Sync:
dma_sync_single_for_cpu()anddma_sync_single_for_device()are used to manually manage cache coherency when the CPU needs to access a mapped buffer.
- Enable HW:
- DMA Frameworks: The kernel provides multiple ways to interact with DMA hardware, from device-specific register programming (common in PCI) to the abstract
dmaenginesubsystem (common in SoCs).
In our next lesson, we will shift our focus from the data path (DMA) to the signaling path. We'll dive deep into interrupt handling and what happens when things go wrong, preparing you to diagnose an interrupt storm scenario, identifying root causes and mitigation strategies.