Appearance
DMA子系统
直接内存访问(DMA)是一种硬件机制,允许外设直接访问系统内存而无需CPU干预。在现代计算机系统中,大多数I/O设备都使用DMA来提高数据传输效率。本章将详细介绍Linux内核中的DMA子系统。
DMA基本概念
什么是DMA
DMA(Direct Memory Access,直接内存访问)是一种数据传输方式,它允许某些硬件子系统直接读写系统内存,而不需要中央处理器(CPU)的介入。这种机制大大提高了数据传输效率,减轻了CPU负担。
DMA控制器
DMA控制器是专门负责DMA传输的硬件组件。它通常包含以下寄存器:
- 源地址寄存器
- 目标地址寄存器
- 传输计数寄存器
- 控制寄存器
Linux DMA子系统架构
DMA引擎框架
Linux内核提供了统一的DMA引擎框架,为驱动开发者提供了一套标准的API来使用DMA功能。DMA引擎框架的主要优势包括:
- 异步传输能力
- 统一的API接口
- 支持多种DMA控制器
- 提供通道管理机制
DMA通道
每个DMA控制器通常包含多个DMA通道,每个通道可以独立地执行DMA传输操作。驱动程序需要申请DMA通道才能进行DMA操作。
DMA映射类型
一致性DMA映射
一致性DMA映射用于需要在CPU和设备之间共享的内存缓冲区。这类映射具有以下特点:
- CPU和设备可以同时访问同一块内存
- 内存缓存一致性得到保证
- 映射在整个设备生命周期内有效
c
#include <linux/dma-mapping.h>
// 分配一致性DMA内存
void *cpu_addr;
dma_addr_t dma_handle;
size_t size = 1024;
cpu_addr = dma_alloc_coherent(dev, size, &dma_handle, GFP_KERNEL);
if (!cpu_addr) {
pr_err("Failed to allocate coherent DMA memory\n");
return -ENOMEM;
}
// 使用内存...
// cpu_addr用于CPU访问
// dma_handle用于设备访问
// 释放一致性DMA内存
dma_free_coherent(dev, size, cpu_addr, dma_handle);流式DMA映射
流式DMA映射用于临时的、单向的数据传输。这类映射具有以下特点:
- 适用于单次数据传输
- 需要在每次传输前建立映射
- 传输完成后必须解除映射
c
#include <linux/dma-mapping.h>
// 建立流式DMA映射(设备到内存)
dma_addr_t dma_handle;
size_t size = 1024;
void *buffer; // 已分配的缓冲区
dma_handle = dma_map_single(dev, buffer, size, DMA_FROM_DEVICE);
if (dma_mapping_error(dev, dma_handle)) {
pr_err("DMA mapping error\n");
return -ENOMEM;
}
// 发起DMA传输...
// 同步DMA缓冲区
dma_sync_single_for_cpu(dev, dma_handle, size, DMA_FROM_DEVICE);
// 访问数据...
// 同步回设备
dma_sync_single_for_device(dev, dma_handle, size, DMA_FROM_DEVICE);
// 解除DMA映射
dma_unmap_single(dev, dma_handle, size, DMA_FROM_DEVICE);DMA引擎API详解
申请DMA通道
c
#include <linux/dmaengine.h>
struct dma_chan *chan;
// 根据能力掩码查找DMA通道
dma_cap_mask_t mask;
dma_cap_zero(mask);
dma_cap_set(DMA_MEMCPY, mask);
chan = dma_request_channel(mask, NULL, NULL);
if (!chan) {
pr_err("Failed to request DMA channel\n");
return -ENODEV;
}准备DMA传输描述符
c
struct dma_async_tx_descriptor *tx;
dma_cookie_t cookie;
// 准备memcpy类型的DMA传输
tx = dmaengine_prep_dma_memcpy(chan, dest_addr, src_addr, len,
DMA_PREP_INTERRUPT | DMA_CTRL_ACK);
if (!tx) {
pr_err("Failed to prepare DMA memcpy\n");
dma_release_channel(chan);
return -ENOMEM;
}
// 设置回调函数
tx->callback = dma_transfer_complete;
tx->callback_param = &my_data;
// 提交传输
cookie = dmaengine_submit(tx);
if (dma_submit_error(cookie)) {
pr_err("Failed to submit DMA transfer\n");
dma_release_channel(chan);
return -EIO;
}发起DMA传输
c
// 发起DMA传输
dma_async_issue_pending(chan);
// 等待传输完成(可选)
unsigned long timeout = msecs_to_jiffies(1000);
unsigned long expires = jiffies + timeout;
while (time_before(jiffies, expires)) {
enum dma_status status = dma_async_is_tx_complete(chan, cookie, NULL, NULL);
if (status != DMA_IN_PROGRESS) {
if (status == DMA_COMPLETE) {
pr_info("DMA transfer completed successfully\n");
} else {
pr_err("DMA transfer failed\n");
}
break;
}
msleep(10);
}设备树中的DMA配置
在设备树中配置DMA相关信息:
dts
my_device: my-device@10000000 {
compatible = "vendor,my-device";
reg = <0x10000000 0x1000>;
// DMA相关属性
dmas = <&dma_controller 0 1>, <&dma_controller 1 2>;
dma-names = "tx", "rx";
};在驱动程序中获取DMA资源:
c
struct device_node *np = dev->of_node;
struct dma_chan *tx_chan, *rx_chan;
if (np) {
// 获取发送DMA通道
tx_chan = dma_request_slave_channel(dev, "tx");
if (IS_ERR(tx_chan)) {
pr_err("Failed to request TX DMA channel\n");
return PTR_ERR(tx_chan);
}
// 获取接收DMA通道
rx_chan = dma_request_slave_channel(dev, "rx");
if (IS_ERR(rx_chan)) {
pr_err("Failed to request RX DMA channel\n");
dma_release_channel(tx_chan);
return PTR_ERR(rx_chan);
}
}DMA驱动开发实例
下面是一个完整的DMA驱动开发示例:
c
#include <linux/module.h>
#include <linux/platform_device.h>
#include <linux/dmaengine.h>
#include <linux/dma-mapping.h>
#include <linux/interrupt.h>
#include <linux/of.h>
#define BUFFER_SIZE 4096
struct my_dma_device {
struct platform_device *pdev;
struct dma_chan *tx_chan;
struct dma_chan *rx_chan;
void *tx_buf;
void *rx_buf;
dma_addr_t tx_phys;
dma_addr_t rx_phys;
};
static void dma_tx_callback(void *data)
{
struct my_dma_device *mydev = data;
pr_info("TX DMA transfer completed\n");
}
static void dma_rx_callback(void *data)
{
struct my_dma_device *mydev = data;
pr_info("RX DMA transfer completed\n");
// 处理接收到的数据
// ...
}
static int setup_dma_channels(struct my_dma_device *mydev)
{
struct device *dev = &mydev->pdev->dev;
struct dma_async_tx_descriptor *tx_desc, *rx_desc;
dma_cookie_t tx_cookie, rx_cookie;
// 分配DMA缓冲区
mydev->tx_buf = dma_alloc_coherent(dev, BUFFER_SIZE, &mydev->tx_phys, GFP_KERNEL);
if (!mydev->tx_buf) {
dev_err(dev, "Failed to allocate TX buffer\n");
return -ENOMEM;
}
mydev->rx_buf = dma_alloc_coherent(dev, BUFFER_SIZE, &mydev->rx_phys, GFP_KERNEL);
if (!mydev->rx_buf) {
dev_err(dev, "Failed to allocate RX buffer\n");
dma_free_coherent(dev, BUFFER_SIZE, mydev->tx_buf, mydev->tx_phys);
return -ENOMEM;
}
// 初始化发送缓冲区
memset(mydev->tx_buf, 0xAA, BUFFER_SIZE);
// 请求DMA通道
mydev->tx_chan = dma_request_slave_channel(dev, "tx");
if (IS_ERR(mydev->tx_chan)) {
dev_err(dev, "Failed to request TX DMA channel\n");
goto err_free_buffers;
}
mydev->rx_chan = dma_request_slave_channel(dev, "rx");
if (IS_ERR(mydev->rx_chan)) {
dev_err(dev, "Failed to request RX DMA channel\n");
goto err_release_tx;
}
// 准备发送传输
tx_desc = dmaengine_prep_slave_single(mydev->tx_chan, mydev->tx_phys,
BUFFER_SIZE, DMA_MEM_TO_DEV,
DMA_PREP_INTERRUPT | DMA_CTRL_ACK);
if (!tx_desc) {
dev_err(dev, "Failed to prepare TX descriptor\n");
goto err_release_channels;
}
tx_desc->callback = dma_tx_callback;
tx_desc->callback_param = mydev;
tx_cookie = dmaengine_submit(tx_desc);
if (dma_submit_error(tx_cookie)) {
dev_err(dev, "Failed to submit TX transfer\n");
goto err_release_channels;
}
// 准备接收传输
rx_desc = dmaengine_prep_slave_single(mydev->rx_chan, mydev->rx_phys,
BUFFER_SIZE, DMA_DEV_TO_MEM,
DMA_PREP_INTERRUPT | DMA_CTRL_ACK);
if (!rx_desc) {
dev_err(dev, "Failed to prepare RX descriptor\n");
goto err_release_channels;
}
rx_desc->callback = dma_rx_callback;
rx_desc->callback_param = mydev;
rx_cookie = dmaengine_submit(rx_desc);
if (dma_submit_error(rx_cookie)) {
dev_err(dev, "Failed to submit RX transfer\n");
goto err_release_channels;
}
// 发起传输
dma_async_issue_pending(mydev->tx_chan);
dma_async_issue_pending(mydev->rx_chan);
return 0;
err_release_channels:
dma_release_channel(mydev->rx_chan);
err_release_tx:
dma_release_channel(mydev->tx_chan);
err_free_buffers:
dma_free_coherent(dev, BUFFER_SIZE, mydev->rx_buf, mydev->rx_phys);
dma_free_coherent(dev, BUFFER_SIZE, mydev->tx_buf, mydev->tx_phys);
return -EIO;
}
static int my_dma_probe(struct platform_device *pdev)
{
struct my_dma_device *mydev;
int ret;
mydev = devm_kzalloc(&pdev->dev, sizeof(*mydev), GFP_KERNEL);
if (!mydev)
return -ENOMEM;
mydev->pdev = pdev;
platform_set_drvdata(pdev, mydev);
ret = setup_dma_channels(mydev);
if (ret) {
dev_err(&pdev->dev, "Failed to setup DMA channels\n");
return ret;
}
dev_info(&pdev->dev, "DMA device initialized successfully\n");
return 0;
}
static int my_dma_remove(struct platform_device *pdev)
{
struct my_dma_device *mydev = platform_get_drvdata(pdev);
struct device *dev = &pdev->dev;
if (mydev->tx_chan) {
dmaengine_terminate_all(mydev->tx_chan);
dma_release_channel(mydev->tx_chan);
}
if (mydev->rx_chan) {
dmaengine_terminate_all(mydev->rx_chan);
dma_release_channel(mydev->rx_chan);
}
if (mydev->tx_buf) {
dma_free_coherent(dev, BUFFER_SIZE, mydev->tx_buf, mydev->tx_phys);
}
if (mydev->rx_buf) {
dma_free_coherent(dev, BUFFER_SIZE, mydev->rx_buf, mydev->rx_phys);
}
dev_info(dev, "DMA device removed\n");
return 0;
}
static const struct of_device_id my_dma_of_match[] = {
{ .compatible = "vendor,my-dma-device", },
{ /* sentinel */ }
};
MODULE_DEVICE_TABLE(of, my_dma_of_match);
static struct platform_driver my_dma_driver = {
.probe = my_dma_probe,
.remove = my_dma_remove,
.driver = {
.name = "my-dma-device",
.of_match_table = my_dma_of_match,
},
};
module_platform_driver(my_dma_driver);
MODULE_AUTHOR("Your Name");
MODULE_DESCRIPTION("DMA Device Driver Example");
MODULE_LICENSE("GPL");DMA调试技巧
启用DMA调试
在内核配置中启用DMA调试选项:
Device Drivers --->
DMA Engine support --->
[*] DMA Engine debugging interface使用DMA测试工具
bash
# 查看系统DMA通道信息
cat /sys/kernel/debug/dmaengine/dmatest
# 运行DMA测试
echo 1 > /sys/kernel/debug/dmaengine/run内核日志分析
bash
# 查看DMA相关的内核消息
dmesg | grep -i dma
# 实时监控DMA事件
tail -f /var/log/kern.log | grep -i dma性能优化建议
DMA缓冲区大小优化
- 选择合适的缓冲区大小以平衡内存使用和传输效率
- 对于频繁的小数据传输,考虑使用环形缓冲区
- 避免过度分片,尽量使用较大的连续缓冲区
缓冲区对齐
c
// 确保缓冲区按CACHE_LINE_SIZE对齐
#define CACHE_LINE_SIZE 64
void *buffer = kmalloc(size, GFP_KERNEL | __GFP_ZERO);
void *aligned_buffer = PTR_ALIGN(buffer, CACHE_LINE_SIZE);减少DMA映射次数
- 对于频繁使用的缓冲区,使用一致性DMA映射
- 批量处理数据以减少映射/解映射操作
- 重用DMA描述符以减少开销
故障排除
常见问题
- DMA映射失败:检查设备地址空间和内存限制
- 传输超时:验证DMA控制器配置和中断处理
- 数据损坏:确认缓冲区对齐和缓存一致性
调试方法
- 使用
dma_mapping_error()检查映射错误 - 添加详细的日志记录跟踪DMA操作
- 使用逻辑分析仪监控DMA信号
通过掌握Linux内核DMA子系统的原理和使用方法,您可以开发出高效、稳定的设备驱动程序,充分利用硬件DMA功能提升系统性能。