1. 嵌入式总线DMA处理的必要性
在嵌入式系统开发中,UART、I2C和SPI这三种总线常被误认为是"低速"接口,但实际上它们在现代应用中经常需要处理高并发的数据流。以典型的传感器数据采集场景为例,当数据包格式为55 AA + 数据头 + 数据内容 + 校验码 + 结束符时,每个包虽然只有几十字节,但积少成多就会形成可观的数据量。
1.1 中断模式的瓶颈分析
传统的中断驱动模式(PIO)在低速率下尚可应付,但随着速率提升就会暴露出严重问题:
- UART在115200bps时:每接收一个字节约需86μs,如果每个字节都触发中断,CPU负载已经很高
- 提升到4Mbps时:中断间隔缩短到80μs,CPU根本无法及时响应所有中断
- I2C在400kHz时:主控需要逐字节处理时钟拉伸和ACK/NACK响应
- SPI在50MHz时:虽然传输速度快,但频繁中断仍会消耗大量CPU资源
实际测试数据显示,在IMX6平台4Mbps UART传输时,中断负载下DMA启动延迟可达4400μs,而32字节的硬件FIFO仅需80μs就会被填满,这必然导致持续的溢出错误(overrun)。
1.2 DMA的优势体现
DMA(Direct Memory Access)技术通过硬件直接管理数据传输,解放了CPU资源:
- 降低CPU中断负载:DMA引擎自动处理数据传输,仅在完成时通知CPU
- 提高系统吞吐量:DMA可以充分利用总线带宽,实现更高的传输速率
- 减少数据丢失风险:特别适合处理连续数据流,如音频、视频等实时数据
2. UART的DMA处理机制
2.1 循环DMA模式设计
UART的特殊性在于其数据流是连续的、以字节为单位的。Linux 4.19内核中IMX UART驱动采用了循环DMA(Cyclic DMA)方案来解决高速率下的数据丢失问题。
2.1.1 核心设计思路
c复制/**
* @brief 循环DMA工作流程
*
* 1. DMA引擎在第一次数据传输时启动
* 2. 持续循环运行直到串口关闭
* 3. 每个周期(period)完成后触发回调
* 4. 回调函数处理接收到的数据
*/
static void imx_uart_dma_start_rx(struct imx_port *sport)
{
struct dma_async_tx_descriptor *desc;
desc = dmaengine_prep_dma_cyclic(chan,
sport->rx_dma, /* DMA地址 */
PAGE_SIZE, /* 总缓冲区大小 */
sport->rx_period_len, /* 周期长度(如32字节) */
DMA_DEV_TO_MEM,
DMA_PREP_INTERRUPT);
desc->callback = imx_uart_dma_rx_callback;
dmaengine_submit(desc);
dma_async_issue_pending(chan);
}
2.1.2 关键参数设置
- 缓冲区大小:通常为一页(PAGE_SIZE,如4KB)
- 周期长度:应与硬件FIFO深度匹配(如32字节)
- 传输方向:设备到内存(DMA_DEV_TO_MEM)
2.2 数据位置计算与处理
在循环DMA模式下,硬件会循环写入缓冲区,因此需要特殊处理数据位置:
c复制static void imx_uart_dma_rx_callback(void *param)
{
/* 获取当前DMA传输状态 */
dmaengine_tx_status(chan, sport->rx_cookie, &state);
/* 计算新数据位置 */
count = sport->rx_period_len - residue;
count = (count + sport->rx_period_len) % sport->rx_period_len;
if (count > sport->rx_last_pos) {
/* 数据连续 */
count = count - sport->rx_last_pos;
} else {
/* 数据绕回,分两段处理 */
count = (sport->rx_period_len - sport->rx_last_pos) + count;
}
/* 将数据推送到tty层 */
tty_insert_flip_string(port, sport->rx_buf + sport->rx_last_pos, count);
tty_flip_buffer_push(port);
}
2.3 PL011 UART的DMA同步问题
ARM PL011 UART驱动中存在一个典型的DMA同步问题:
c复制static void pl011_dma_flush_buffer(struct uart_port *port)
{
/* 错误实现:释放锁后终止DMA */
spin_unlock(&uap->port.lock);
dmaengine_terminate_all(uap->dmatx.chan);
spin_lock(&uap->port.lock);
/* 正确实现:不释放锁,使用异步终止 */
dmaengine_terminate_async(uap->dmatx.chan);
}
错误实现会导致竞态条件:在锁释放期间,可能有新数据写入环形缓冲区,但DMA已终止,数据无法发送,最终导致tcdrain()死等。
3. I2C的DMA处理机制
3.1 DMA安全缓冲区问题
I2C面临的核心挑战是:用户提供的缓冲区可能来自vmalloc区域,不能直接用于DMA。
3.1.1 I2C消息结构
c复制struct i2c_msg {
__u16 addr; /* 从设备地址 */
__u16 flags; /* 标志位 */
__u16 len; /* 消息长度 */
__u8 *buf; /* 缓冲区指针 */
};
#define I2C_M_DMA_SAFE 0x0200 /* 缓冲区已适配DMA要求 */
3.1.2 DMA安全缓冲区API
Linux 4.19引入了专门的API来处理这个问题:
c复制u8 *i2c_get_dma_safe_msg_buf(struct i2c_msg *msg, unsigned int threshold)
{
/* 1. 已标记为DMA安全的直接使用 */
if (msg->flags & I2C_M_DMA_SAFE)
return msg->buf;
/* 2. 小数据量用PIO更高效 */
if (msg->len < threshold)
return NULL;
/* 3. 分配bounce buffer并拷贝数据 */
return kmemdup(msg->buf, msg->len, GFP_KERNEL);
}
void i2c_put_dma_safe_msg_buf(u8 *buf, struct i2c_msg *msg, bool xferred)
{
/* 如果是读操作且传输成功,需要将数据拷贝回原缓冲区 */
if (xferred && (msg->flags & I2C_M_RD))
memcpy(msg->buf, buf, msg->len);
kfree(buf);
}
3.2 QCOM GENI I2C的DMA实现
高通GENI I2C控制器展示了完整的DMA处理流程:
c复制static int geni_i2c_tx_one_msg(struct geni_i2c_dev *gi2c, struct i2c_msg *msg)
{
/* 1. 获取DMA安全缓冲区 */
dma_buf = i2c_get_dma_safe_msg_buf(msg, 32);
if (!dma_buf)
return geni_i2c_pio_tx(gi2c, msg);
/* 2. 映射DMA缓冲区 */
dma_addr = dma_map_single(gi2c->dev->parent, dma_buf,
msg->len, DMA_TO_DEVICE);
/* 3. 准备DMA描述符 */
tx_desc = dmaengine_prep_slave_single(gi2c->tx_chan, dma_addr,
msg->len, DMA_MEM_TO_DEV,
DMA_PREP_INTERRUPT);
/* 4. 提交并等待完成 */
dmaengine_submit(tx_desc);
dma_async_issue_pending(gi2c->tx_chan);
wait_for_completion(&gi2c->tx_complete);
/* 5. 释放资源 */
dma_unmap_single(gi2c->dev->parent, dma_addr, msg->len, DMA_TO_DEVICE);
i2c_put_dma_safe_msg_buf(dma_buf, msg, true);
}
3.3 SH Mobile I2C的bounce buffer泄漏修复
早期实现中常见的资源泄漏问题:
c复制static void sh_mobile_i2c_dma_callback(void *data)
{
/* 修复前:只unmap了DMA,没有释放bounce buffer */
dma_unmap_single(pd->chan_tx->device->dev, pd->dma_addr,
msg->len, DMA_TO_DEVICE);
/* 修复后:正确释放DMA安全缓冲区 */
i2c_put_dma_safe_msg_buf(pd->dma_buf, msg, true);
}
4. SPI的DMA处理机制
4.1 SPI控制器架构
SPI的DMA处理比UART/I2C更复杂,因为它支持全双工传输:
c复制struct spi_controller {
struct device *dev;
/* DMA通道 */
struct dma_chan *dma_tx; /* 发送通道 */
struct dma_chan *dma_rx; /* 接收通道 */
/* 传输函数 */
int (*transfer_one)(struct spi_controller *ctlr,
struct spi_device *spi,
struct spi_transfer *transfer);
/* DMA决策函数 */
bool (*can_dma)(struct spi_controller *ctlr,
struct spi_device *spi,
struct spi_transfer *xfer);
};
4.2 数据传输生命周期
SPI数据传输采用消息队列机制:
c复制int spi_async_locked(struct spi_device *spi, struct spi_message *message)
{
/* 1. 初始化消息 */
message->status = -EINPROGRESS;
INIT_LIST_HEAD(&message->queue);
/* 2. 加入控制器队列 */
list_add_tail(&message->queue, &ctlr->queue);
/* 3. 启动队列处理 */
queue_work(ctlr->kworker, &ctlr->pump_messages);
}
static void spi_pump_messages(struct work_struct *work)
{
/* 1. 从队列取出消息 */
message = list_first_entry_or_null(&ctlr->queue,
struct spi_message, queue);
/* 2. 决定使用DMA还是PIO */
if (ctlr->can_dma && ctlr->can_dma(ctlr, message->spi, message)) {
spi_map_buf(ctlr, message); /* DMA模式 */
}
/* 3. 调用控制器传输函数 */
ctlr->transfer_one_message(ctlr, message);
}
4.3 DMA决策机制
Rockchip SPI控制器展示了典型的DMA决策逻辑:
c复制static bool rockchip_spi_can_dma(struct spi_controller *ctlr,
struct spi_device *spi,
struct spi_transfer *xfer)
{
/* 1. 检查硬件是否支持DMA */
if (!rs->dma_tx || !rs->dma_rx)
return false;
/* 2. 根据长度决策:小于32字节用PIO */
if (xfer->len < 32)
return false;
return true;
}
4.4 全双工DMA传输实现
c复制static int rockchip_spi_prepare_dma(struct rockchip_spi *rs,
struct spi_transfer *xfer)
{
/* 1. 映射tx缓冲区 */
if (xfer->tx_buf) {
tx_addr = dma_map_single(rs->dev, (void *)xfer->tx_buf,
xfer->len, DMA_TO_DEVICE);
tx_desc = dmaengine_prep_slave_single(rs->dma_tx, tx_addr,
xfer->len, DMA_MEM_TO_DEV,
DMA_PREP_INTERRUPT);
}
/* 2. 映射rx缓冲区 */
if (xfer->rx_buf) {
rx_addr = dma_map_single(rs->dev, xfer->rx_buf,
xfer->len, DMA_FROM_DEVICE);
rx_desc = dmaengine_prep_slave_single(rs->dma_rx, rx_addr,
xfer->len, DMA_DEV_TO_MEM,
DMA_PREP_INTERRUPT);
}
/* 3. 提交并启动DMA */
if (xfer->tx_buf) dmaengine_submit(tx_desc);
if (xfer->rx_buf) dmaengine_submit(rx_desc);
if (xfer->tx_buf) dma_async_issue_pending(rs->dma_tx);
if (xfer->rx_buf) dma_async_issue_pending(rs->dma_rx);
}
5. 三种总线DMA设计对比
5.1 架构差异
| 特性 | UART | I2C | SPI |
|---|---|---|---|
| DMA类型 | cyclic DMA | single DMA | single DMA(双通道) |
| 数据方向 | 全双工(独立) | 半双工 | 全双工(同步) |
| 缓冲区要求 | DMA连续 | 必须DMA安全 | DMA连续 |
| 阈值决策 | 始终DMA(cyclic) | 动态(可配置) | 动态(如>32字节) |
| 典型场景 | 连续数据流 | 寄存器读写 | 传感器数据 |
5.2 优化建议
针对"55 AA数据头+数据内容+校验码+结束符"的典型场景:
-
UART优化:
c复制sport->rx_period_len = 32; /* 与FIFO深度匹配 */ -
I2C优化:
c复制/* 对小于32字节的指令用PIO */ if (msg->len < 32) return NULL; -
SPI优化:
c复制/* 利用全双工特性,一次传输完成命令+数据交换 */ spi_transfer.tx_buf = cmd_buf; /* 55 AA + 命令 + 数据 */ spi_transfer.rx_buf = resp_buf; /* 响应数据 */
6. Linux内核DMA设计哲学
- 分层抽象:DMA引擎层提供统一API,总线驱动层实现具体逻辑
- 动态决策:根据传输长度动态选择DMA/PIO,平衡性能与延迟
- 安全性优先:I2C引入DMA安全缓冲区机制,解决vmalloc问题
- 持续演进:UART从manual restart到cyclic DMA,解决启动延迟问题
- 兼容性设计:SPI同时支持master和slave模式,适应不同场景
在实际项目中,我曾遇到一个案例:IMX6平台UART在4Mbps速率下持续丢数据。通过分析发现是DMA启动延迟导致,最终采用cyclic DMA方案解决了问题。这个经验告诉我,理解底层机制对于解决实际问题至关重要。