Linux DMA Streaming Mapping Guide- Free Linux Device Drivers Tutorial

Linux DMA Streaming Mapping Guide
Free Linux Device Drivers Course — Chapter 12, Lecture 2
Kernel 6.x Ready
Original Driver Code
Free & Open

← PREV_LEC  |  NEXT_LEC →

In the previous lecture of this free Linux kernel development course, we studied coherent DMA mapping using dma_alloc_coherent(). In this lecture we cover Linux DMA streaming mapping, the technique real network and block drivers use to move ordinary kernel buffers to and from a device without keeping a permanent, always-coherent memory region. You will learn single buffer streaming mapping and scatter/gather DMA mapping, with original, kernel 6.x-correct driver examples you can build and run on your own system.

What You Will Learn

  • What streaming DMA mapping is
  • dma_map_single() and dma_unmap_single()
  • struct scatterlist internals
  • dma_map_sg() and dma_unmap_sg()
  • dma_sync_*_for_cpu / device
  • A working platform driver example

Prerequisites

Before this lecture, you should be comfortable with:

  • Coherent DMA mapping (previous lecture)
  • Platform driver basics
  • kmalloc-based kernel memory allocation
  • Basic C pointers and structures

Streaming vs Coherent DMA Mapping

Coherent mapping allocates a buffer that stays mapped for the entire life of the driver, so both the CPU and the device always see the same data automatically. Streaming mapping is the opposite: you take a buffer that already exists (for example one you got from kmalloc()), map it just before a single transfer, and unmap it right after. This is cheaper to set up per-call and is the normal choice for one-shot or per-packet transfers such as network sk_buffs or block I/O requests.

Coherent vs Streaming Mapping
Coherent Mapping
Allocated once → stays mapped forever → freed on driver exit
Streaming Mapping
Existing buffer → map before transfer → unmap right after
Aspect Coherent Mapping Streaming Mapping
Setup cost High (once) Low (per transfer)
Lifetime Driver lifetime Single transfer
Typical use Descriptor rings, control blocks Network packets, block I/O buffers
Needs manual sync? No Yes, if CPU touches buffer mid-transfer

Single Buffer Streaming Mapping

The simplest form of streaming DMA mapping is a single buffer. You hand the kernel a virtual address and a size, and it returns a bus address the device can use directly.

dma_addr_t dma_map_single(struct device *dev, void *ptr,
                           size_t size, enum dma_data_direction dir);

void dma_unmap_single(struct device *dev, dma_addr_t addr,
                       size_t size, enum dma_data_direction dir);

The dir argument must accurately describe the transfer direction. Getting it wrong on architectures with non-coherent caches can silently corrupt data.

enum dma_data_direction {
    DMA_BIDIRECTIONAL = 0,
    DMA_TO_DEVICE     = 1,
    DMA_FROM_DEVICE   = 2,
    DMA_NONE          = 3,
};
Single Buffer Mapping Flow
kmalloc buffer
dma_map_single()
device DMA transfer
dma_unmap_single()

Original Driver Example: Single Buffer Mapping

Below is an original demo platform driver (not copied from any book) that allocates a buffer, maps it as a streaming DMA region, and unmaps it. It targets kernel 6.x and uses dma_set_mask_and_coherent() before any mapping call, which is required on modern kernels.

#include <linux/module.h>
#include <linux/platform_device.h>
#include <linux/dma-mapping.h>
#include <linux/slab.h>

#define EP_BUF_SIZE 4096

struct ep_stream_dev {
    struct device *dev;
    void *cpu_buf;
    dma_addr_t dma_addr;
};

static int ep_stream_probe(struct platform_device *pdev)
{
    struct ep_stream_dev *edev;
    int ret;

    edev = devm_kzalloc(&pdev->dev, sizeof(*edev), GFP_KERNEL);
    if (!edev)
        return -ENOMEM;

    edev->dev = &pdev->dev;

    ret = dma_set_mask_and_coherent(edev->dev, DMA_BIT_MASK(32));
    if (ret) {
        dev_err(edev->dev, "no usable DMA mask\n");
        return ret;
    }

    edev->cpu_buf = kmalloc(EP_BUF_SIZE, GFP_KERNEL);
    if (!edev->cpu_buf)
        return -ENOMEM;

    memset(edev->cpu_buf, 0xAA, EP_BUF_SIZE);

    edev->dma_addr = dma_map_single(edev->dev, edev->cpu_buf,
                                     EP_BUF_SIZE, DMA_TO_DEVICE);
    if (dma_mapping_error(edev->dev, edev->dma_addr)) {
        dev_err(edev->dev, "dma_map_single failed\n");
        kfree(edev->cpu_buf);
        return -EIO;
    }

    dev_info(edev->dev, "mapped buffer at bus address %pad\n",
             &edev->dma_addr);

    platform_set_drvdata(pdev, edev);
    return 0;
}

static void ep_stream_remove(struct platform_device *pdev)
{
    struct ep_stream_dev *edev = platform_get_drvdata(pdev);

    dma_unmap_single(edev->dev, edev->dma_addr,
                      EP_BUF_SIZE, DMA_TO_DEVICE);
    kfree(edev->cpu_buf);
    dev_info(edev->dev, "unmapped and freed buffer\n");
}

static struct platform_driver ep_stream_driver = {
    .probe  = ep_stream_probe,
    .remove = ep_stream_remove,
    .driver = {
        .name = "ep_dma_stream_demo",
    },
};
module_platform_driver(ep_stream_driver);

MODULE_LICENSE("GPL");
MODULE_AUTHOR("EmbeddedPathashala");
MODULE_DESCRIPTION("EmbeddedPathashala streaming DMA single buffer demo");

Build and run, using a platform_device registration stub the same way you did in the coherent mapping lecture, or via a small init function calling platform_device_register_simple():

$ make
$ sudo insmod ep_dma_stream_demo.ko
$ dmesg | tail -5

Expected output:

ep_dma_stream_demo ep_dma_stream_demo.0: mapped buffer at bus address 0x3a1f2000
$ sudo rmmod ep_dma_stream_demo
$ dmesg | tail -2
ep_dma_stream_demo ep_dma_stream_demo.0: unmapped and freed buffer

Scatter/Gather DMA Mapping

Real transfers are rarely a single contiguous buffer. A block I/O request or a large network send may touch several physically separate pages. Instead of mapping each piece one at a time, the kernel lets you describe them all as one scatterlist and map them together.

struct scatterlist {
    unsigned long   page_link;
    unsigned int    offset;
    unsigned int    length;
    dma_addr_t      dma_address;
    unsigned int    dma_length;
};

Building a scatterlist has four steps: initialise the table, attach each buffer, map it, and later unmap it.

int dma_map_sg(struct device *dev, struct scatterlist *sg,
               int nents, enum dma_data_direction dir);

void dma_unmap_sg(struct device *dev, struct scatterlist *sg,
                   int nents, enum dma_data_direction dir);

dma_map_sg() returns the number of DMA segments actually mapped, which can be lower than the number of entries you passed in — an IOMMU may merge adjacent entries into one segment. Always loop using the returned count, not the original nents, when reading back addresses. When unmapping, pass the original nents you used for mapping, not the returned count.

Scatter/Gather Mapping Flow
buffer 1
buffer 2
buffer 3
↓↓↓ sg_set_buf() into one scatterlist
dma_map_sg()
device receives one list of DMA segments

Original Driver Example: Scatter/Gather Mapping

#include <linux/module.h>
#include <linux/platform_device.h>
#include <linux/dma-mapping.h>
#include <linux/scatterlist.h>
#include <linux/slab.h>

#define EP_SG_COUNT   3
#define EP_SG_BUFSIZE 2048

struct ep_sg_dev {
    struct device *dev;
    void *bufs[EP_SG_COUNT];
    struct scatterlist sg[EP_SG_COUNT];
    int mapped_count;
};

static int ep_sg_probe(struct platform_device *pdev)
{
    struct ep_sg_dev *edev;
    int i, ret;

    edev = devm_kzalloc(&pdev->dev, sizeof(*edev), GFP_KERNEL);
    if (!edev)
        return -ENOMEM;

    edev->dev = &pdev->dev;

    ret = dma_set_mask_and_coherent(edev->dev, DMA_BIT_MASK(32));
    if (ret)
        return ret;

    sg_init_table(edev->sg, EP_SG_COUNT);

    for (i = 0; i < EP_SG_COUNT; i++) {
        edev->bufs[i] = kmalloc(EP_SG_BUFSIZE, GFP_KERNEL);
        if (!edev->bufs[i])
            return -ENOMEM;
        sg_set_buf(&edev->sg[i], edev->bufs[i], EP_SG_BUFSIZE);
    }

    edev->mapped_count = dma_map_sg(edev->dev, edev->sg,
                                     EP_SG_COUNT, DMA_TO_DEVICE);
    if (edev->mapped_count == 0) {
        dev_err(edev->dev, "dma_map_sg failed\n");
        return -EIO;
    }

    dev_info(edev->dev, "mapped %d scatter/gather segments\n",
             edev->mapped_count);

    platform_set_drvdata(pdev, edev);
    return 0;
}

static void ep_sg_remove(struct platform_device *pdev)
{
    struct ep_sg_dev *edev = platform_get_drvdata(pdev);
    int i;

    dma_unmap_sg(edev->dev, edev->sg, EP_SG_COUNT, DMA_TO_DEVICE);

    for (i = 0; i < EP_SG_COUNT; i++)
        kfree(edev->bufs[i]);

    dev_info(edev->dev, "unmapped scatter/gather list\n");
}

static struct platform_driver ep_sg_driver = {
    .probe  = ep_sg_probe,
    .remove = ep_sg_remove,
    .driver = {
        .name = "ep_dma_sg_demo",
    },
};
module_platform_driver(ep_sg_driver);

MODULE_LICENSE("GPL");
MODULE_AUTHOR("EmbeddedPathashala");
MODULE_DESCRIPTION("EmbeddedPathashala scatter/gather DMA demo");

Expected dmesg output:

$ sudo insmod ep_dma_sg_demo.ko
$ dmesg | tail -3
ep_dma_sg_demo ep_dma_sg_demo.0: mapped 3 scatter/gather segments

Keeping CPU and Device In Sync

Once a buffer is mapped, ownership belongs to the device until you unmap it. If your driver genuinely needs to read or write the buffer from the CPU while the mapping is still active, you must explicitly sync it — skipping this step is a common bug on non-coherent architectures.

void dma_sync_single_for_cpu(struct device *dev, dma_addr_t addr,
                              size_t size, enum dma_data_direction dir);

void dma_sync_single_for_device(struct device *dev, dma_addr_t addr,
                                 size_t size, enum dma_data_direction dir);

void dma_sync_sg_for_cpu(struct device *dev, struct scatterlist *sg,
                          int nents, enum dma_data_direction dir);

void dma_sync_sg_for_device(struct device *dev, struct scatterlist *sg,
                             int nents, enum dma_data_direction dir);

Call the _for_cpu variant right before the CPU reads the buffer, and the _for_device variant right before handing control back to the device. There is no need to call either function after the final unmap — unmapping already guarantees the CPU can read the data safely.

Common Mistakes and Troubleshooting

  • Forgetting dma_set_mask_and_coherent()
  • Unmapping with wrong nents
  • Wrong DMA direction
  • Touching buffer without syncing
  • Not checking dma_mapping_error()
  • Double mapping same sg list

Best Practices

  • Map as late, unmap as early as possible
  • Always check for mapping failure
  • Use the exact direction the transfer needs
  • Enable CONFIG_DMA_API_DEBUG while testing

Performance and Security Considerations

Streaming mappings are cheap individually but add up under high transfer rates, since each map/unmap pair may involve cache maintenance or IOMMU page table updates. Batch small buffers into a single scatterlist where possible instead of issuing many single-buffer mappings back to back. On the security side, never let userspace supply a raw physical or DMA address directly to hardware; always route userspace buffers through the DMA API so IOMMU protections stay in effect.

Real-World Use Cases

Network interface drivers map each outgoing sk_buff with dma_map_single() before handing it to the NIC. Block drivers and NVMe controllers build a scatterlist per request with dma_map_sg() since a single I/O request commonly spans several non-contiguous pages in the page cache.

Summary / Key Takeaways

  • Streaming mapping is per-transfer, not permanent
  • dma_map_single() for one buffer
  • dma_map_sg() for multiple non-contiguous buffers
  • Always sync before CPU access mid-transfer
  • Match unmap nents to the original mapping call

Conclusion

Linux DMA streaming mapping is the workhorse mechanism behind almost every high-throughput driver in the kernel. Once you are comfortable with dma_map_single() and dma_map_sg(), you can read and write real network, storage, and multimedia drivers with confidence. In the next lecture of this free Linux device drivers course, we move on to completions — the kernel’s mechanism for one thread to wait for a DMA transfer to finish.

FAQ: Linux DMA Streaming Mapping

What is the difference between coherent and streaming DMA mapping?

Coherent mapping stays valid for the driver’s whole lifetime and needs no manual sync. Streaming mapping is set up right before one transfer and torn down right after, and may need explicit CPU/device sync calls.

When should I use dma_map_sg() instead of dma_map_single()?

Use dma_map_sg() whenever your data spans multiple, possibly non-contiguous, buffers or pages in a single transfer, such as a block I/O request or a large network packet made of several fragments.

Why does dma_map_sg() sometimes return fewer segments than I passed in?

An IOMMU can merge physically or virtually adjacent scatterlist entries into a single DMA segment, so the returned count can be smaller than the entry count you supplied.

Do I need to call dma_sync functions after dma_unmap_single()?

No. Unmapping already guarantees the CPU can safely read the buffer, so a separate sync call after unmap is unnecessary.

What happens if I skip dma_set_mask_and_coherent()?

Mapping calls can fail or the device may receive invalid addresses on systems where the default DMA mask does not match your hardware’s addressing capability.

How do I check if dma_map_single() failed?

Always pass the returned address to dma_mapping_error() and handle the error path if it returns non-zero, instead of assuming the mapping succeeded.

Can I map the same scatterlist twice without unmapping it first?

No. A scatterlist must be unmapped with dma_unmap_sg() before it can be mapped again; mapping it twice in a row is not supported.

Is this free Linux kernel development course updated for modern kernels?

Yes, every lecture in this free Linux device drivers course, including this one, is verified against current kernel 6.x DMA API documentation.

 

free linux kernel development course
free linux device drivers course
free embedded systems course
free linux development course
linux dma streaming mapping
scatter gather dma linux

Continue the Free Linux Device Drivers Course

Next up: Completions — waiting for a DMA transfer to finish.

← PREV_LEC  |  NEXT_LEC →

Leave a Reply

Your email address will not be published. Required fields are marked *