Linux驱动之DMA(二)
DMA和中断是不同层次的协作机制,DMA用于高效数据搬运,解放CPU;中断用于事件通知。纯中断方式每个字节都需CPU处理,适合小数据量;DMA+中断方式则批量处理数据,大幅减少CPU开销。两者在串口等外设中常协同工作,DMA负责数据传输,中断负责事件通知。DMA通道数量有限,需合理分配外设资源。
一、使用中断和DMA传输数据的区别
DMA(直接内存访问)和中断传输数据的方式经常被放在一起讨论,甚至有时被看作“替代品”,但它们其实是在不同层次解决不同问题的机制。它们在串口等外设应用中常常协同工作。
🔧 1. 核心概念:它们解决什么问题?
-
中断:
- 核心问题: CPU如何知道外设(如串口)什么时候“有事”需要它处理?CPU不能永远轮询(死循环检查)外设状态,那样效率极低。
- 解决方案: 中断是一种事件通知机制。当外设事件发生(如:串口收到一个字节、发送缓冲区空、传输完成、出错等),外设会主动向CPU发送一个中断请求信号,打断CPU正在执行的任务,迫使CPU立刻跳转到预先设定的中断服务程序去处理这个事件。处理完后再返回被打断的地方继续执行。
- 关键点: 中断让CPU从“轮询等待”中解放出来,提高了CPU的利用率,让它能去做其他事情,只在真正需要时服务外设。
-
DMA:
- 核心问题: 如何高效地在内存和外设之间或内存不同区域之间移动成块的数据,同时最大限度地解放CPU?
- 解决方案: DMA是一个专用硬件控制器。CPU只需要告诉DMA控制器:
- 源地址(内存地址或外设数据寄存器地址)
- 目标地址(内存地址或外设数据寄存器地址)
- 要传输的数据量(字节数或字数)
- 传输方向(外设->内存、内存->外设、内存->内存)
- 传输模式(单次、循环等)
- 然后,DMA控制器独立于CPU,接管系统总线,负责将数据从源地址搬运到目标地址,一个字节/字一个字节/字地搬,直到完成指定数量的传输。
- 关键点: DMA的核心价值是数据搬运的“自动化”和“解放CPU”。在DMA传输期间,CPU不需要参与每一次数据移动(不像中断方式那样),可以继续执行其他任务(只要不访问被DMA占用的总线)。这大大减少了CPU的开销,尤其对大数据量传输。
🔄 2. 它们的关系:协作而非对立
你可能经常看到“使用DMA vs 使用中断传输数据”的说法,这里的“中断传输”通常指的是 “使用中断驱动的方式进行数据收发”。让我们在串口这个具体场景下对比这两种常见的数据传输方式:
方式一:纯中断驱动(不使用DMA)
- 配置:
- 配置串口,使能接收中断(
USART_IT_RXNE)。 - 编写串口接收中断服务程序。
- 配置串口,使能接收中断(
- 工作流程:
- CPU空闲,执行主程序或其他任务。
- 事件发生: 串口收到一个字节,数据到达接收数据寄存器,硬件自动将
RXNE标志位置1。 - 中断请求: 串口硬件检测到
RXNE置1,向CPU发出接收中断请求。 - CPU响应: CPU暂停当前任务,保存上下文,跳转到串口接收中断服务程序。
- ISR操作:
- ISR读取接收数据寄存器(读取操作自动清除
RXNE标志)。 - 将读取到的字节存入内存中某个缓冲区(比如一个全局数组)。
- 可能进行一些简单处理(比如更新接收计数器)。
- ISR读取接收数据寄存器(读取操作自动清除
- 中断返回: ISR执行完毕,CPU恢复之前暂停的任务。
- 特点:
- CPU参与度高: 每一个字节的接收都会触发一次中断,CPU必须亲自读取数据寄存器并搬运到内存缓冲区。
- 开销大: 频繁的中断(尤其波特率高时)和上下文切换会消耗大量CPU时间。
- 实时性: 理论上可以尽快处理每个字节(但对大块数据,频繁中断反而可能影响其他高优先级任务)。
- 代码简单: 逻辑直观,易于理解和实现。
方式二:DMA + 中断(或DMA + 轮询)
- 配置:
- 配置串口。
- 配置DMA控制器:
- 源:串口接收数据寄存器。
- 目标:内存中的接收缓冲区。
- 传输方向:外设(串口) -> 内存。
- 传输数据量:比如预期接收多少字节(如64字节)。
- 配置DMA中断(可选但推荐):使能DMA传输完成中断(
DMA_IT_TC)或半完成中断(DMA_IT_HT)。 - 使能串口的DMA接收请求(
USART_CR3_DMAR)。 - 启动DMA传输。
- 工作流程:
- 启动后: CPU配置好DMA就不管了,去执行主程序或其他任务。
- 数据接收:
- 串口每收到一个字节,放入接收数据寄存器。
- DMA控制器自动检测到“接收缓冲区非空”(硬件信号连接),无需CPU干预。
- DMA控制器发起总线请求,获取总线控制权。
- DMA控制器将数据从串口数据寄存器自动搬运到配置好的内存缓冲区的当前指针位置。
- DMA内部计数器减1。
- 搬运结束,释放总线。
- 串口继续接收下一个字节,重复上述过程…(CPU此时完全没参与)
- 传输完成/事件通知(关键!):
- 当DMA搬运完预先设定的所有数据量后(比如64字节),硬件自动设置DMA传输完成标志(
DMA_ISR_TCIF)。 - 如果配置了DMA传输完成中断:
- DMA控制器向CPU发出中断请求。
- CPU响应,跳转到DMA传输完成中断服务程序。
- ISR操作:
- 清除DMA传输完成标志。
- 处理这完整一块64字节的数据(比如解析协议、存入队列、通知应用层等)。
- 可能重新配置DMA(比如将缓冲区指针复位,数据量重设,再次启动DMA准备接收下一批64字节)。
- 如果不配DMA中断(轮询):
- CPU需要在主程序中定期检查DMA的状态寄存器,看传输完成标志是否置位。
- 如果置位,则进行数据处理和后续配置。
- 当DMA搬运完预先设定的所有数据量后(比如64字节),硬件自动设置DMA传输完成标志(
- 特点:
- CPU解放: CPU只在配置DMA和处理传输完成事件(通过中断或轮询)时参与。在数据搬运(字节到字节)的整个过程中,CPU完全空闲,可以干其他事情。
- 开销极低: 对于大数据块传输,DMA中断次数远少于纯中断方式(64字节可能只触发1次中断,而非64次)。
- 效率高: 特别适合连续、大批量数据传输(如串口Modem通信、大文件传输、高速ADC采样)。
- 复杂度稍高: 需要正确配置DMA通道、中断、内存缓冲区管理(双缓冲、循环缓冲等技巧更复杂)。
🎯 总结关系与对比(串口接收为例)
| 特性 | 纯中断驱动 (不使用DMA) | DMA + 中断 (或轮询) |
|---|---|---|
| 核心机制 | 中断通知 + CPU直接搬运 (每字节) | DMA自动搬运 (每字节) + 中断通知事件 (块完成) |
| CPU参与度 | 极高:每个字节都需CPU读寄存器、搬运 | 极低:仅配置起点和终点、处理整块数据完成事件 |
| 中断频率 | 极高:每接收一个字节触发一次中断 | 极低:仅当整块数据接收完成才触发一次中断 |
| 数据搬运 | 靠CPU指令 (通过ISR) 实现 | 靠DMA硬件控制器 自动实现 |
| 适用场景 | 数据量小、波特率低、简单应用 | 数据量大、波特率高、要求CPU实时性高、复杂应用 |
| 实时性 | 每个字节能尽快处理 | 整块数据处理有延迟,但CPU处理其他任务实时性更好 |
| 效率 | 低 (频繁中断开销) | 高 (解放CPU,总线利用率高) |
| 代码复杂度 | 低 | 中到高 (需处理缓冲区、DMA状态、同步) |
| 内存占用 | 通常一个字节缓冲区 (或小缓冲区) | 需要足够大的内存缓冲区存放DMA传输的数据块 |
🧩 关键结论:它们不是互斥的,而是不同层次的协作
📌 形象比喻
- 纯中断驱动 (像快递员送货到家):
- 你(CPU)在家里工作。
- 每当有快递(一个字节数据)送到你家门口,快递员(外设硬件)按门铃(触发中断)。
- 你必须立刻放下工作(上下文切换),开门(进入ISR),签收快递(读取数据寄存器),把快递拿进屋(存入内存缓冲区),然后回到工作岗位(中断返回)。
- 如果一天有100个快递(100字节),你要被打断100次。
- DMA + 中断 (像快递柜集中收货):
- 你(CPU)在家门口的快递柜(DMA控制器)设定好:“今天预计会有100件快递(数据量),请收货后统一放进我指定的柜子(内存缓冲区)”。
- 快递员(外设硬件)把快递(数据)一件件投进快递柜指定的收货口(串口数据寄存器)。
- 快递柜(DMA控制器)自动识别,把每件快递从收货口搬运到指定的大柜子(内存缓冲区)里(自动搬运)。
- 你(CPU)完全不用管这个过程,在家安心工作。
- 当100件快递全部投递完毕(DMA传输完成),快递柜(DMA控制器)给你手机发个短信(触发DMA完成中断):“你的100件快递已到齐”。
- 你看到短信后(进入DMA完成ISR),一次性把快递柜里的100件快递搬回家(处理整块数据),或者设定快递柜准备接收下一批100件(重新启动DMA)。
- 一天下来,你只被打断了一次(处理100件快递)。
二、dma通道是否够多个外设使用?
这是一个非常核心且重要的问题,答案是:**这完全取决于硬件设计,但Linux内核的DMA Engine框架提供了强大的抽象层,使得在多外设场景下管理和共享DMA资源成为可能。**下面从多个方面详细解析这个问题。
1. 硬件层面:DMA控制器的类型
首先,我们需要了解硬件提供了什么。SoC中的DMA控制器主要有两种架构:
| 类型 | 描述 | 通道资源特点 | 典型例子 |
|---|---|---|---|
| 专用外设DMA | 外设内置了简单的DMA逻辑,与自身FIFO紧密耦合。 | 通道专用于该外设,不能被其他外设使用。一个外设可能拥有独立的TX和RX通道。 | 很多UART、SPI控制器内置的DMA。 |
| 通用DMA控制器 | 一个独立的、集中的DMA控制器,像是一个“数据搬运工”,可以被系统中多个外设请求使用。 | 通道是共享资源。控制器有固定数量的物理通道(如8通道、16通道),所有外设竞争使用。 | ARM的PL330、TI的EDMA、Intel的IOAT。 |
结论一: 如果您的多个外设都是使用自己专用的内置DMA,那么它们之间通常没有冲突,因为通道是物理上隔离的。冲突主要发生在共享通用DMA控制器的外设之间。
2. 软件层面:Linux DMA Engine框架的资源管理
Linux的DMA Engine框架的核心作用就是抽象并管理这些有限的、共享的物理通道,为驱动开发者提供一套统一的API。其工作模式如下图所示:
如上图所示,DMA Engine框架扮演了“交通指挥官”的角色,它的资源管理策略包括:
-
通道分配:
- 当驱动调用
dma_request_channel()时,框架会根据驱动指定的能力掩码(dma_cap_mask_t)和可选的过滤器函数,动态地从通用DMA控制器中找到一个空闲的、合适的物理通道分配给该驱动。 - 重要:通道是在驱动初始化或打开设备时动态申请的,而不是在系统启动时就固定死的。传输完成后,驱动可以(但通常不会)释放通道,以避免重复申请的开销。
- 当驱动调用
-
通道复用:
- 物理通道的数量是有限的(例如8个),但系统中外设的数量可能超过8个。这并不代表只有8个外设能使用DMA。
- 框架允许分时复用物理通道。一个通道在同一时间只能被一个外设占用,但当该外设的DMA传输完成并释放通道后,另一个外设就可以申请使用它。
- 这意味着,只要不是所有外设都在同一时刻持续不断地进行DMA传输,有限的物理通道就足以服务多个外设。对于间歇性传输的外设(如UART、键盘),这非常有效。
-
Scatter-Gather (SG) 支持:
- 这是一个巨大的帮助。一个支持SG的DMA通道可以处理一个包含多个不连续内存块的传输链表。
- 这意味着,一次DMA传输设置就可以完成大量数据的搬运,极大地减少了申请通道、配置传输的次数,降低了通道资源的占用时间,从而提升了整体效率。
3. 如何查看和评估系统DMA资源?
在嵌入式Linux系统上,你可以通过以下方式查看可用的DMA资源:
# 1. 查看系统注册了哪些DMA控制器
ls /sys/class/dma/
# 可能输出: dma0chan0 dma0chan1 dma1chan0 dma1chan1 ...
# 2. 查看某个通道的信息
cat /sys/class/dma/dma0chan0/in_use # 查看是否被占用
cat /sys/class/dma/dma0chan0/device/device/num_channels # 查看该控制器总通道数
# 3. 在内核启动日志中查找DMA控制器初始化信息
dmesg | grep -i dma
# 例如: dma-pl330 f8003000.dma: Loaded driver for PL330 DMAC-241330
# dma-pl330 f8003000.dma: DBUFF-32x8bytes Num_Chans-8 Num_Peri-32 Num_Events-16
4. 实践建议:在多外设系统中使用DMA
-
设计时评估:
- 仔细阅读SoC数据手册,弄清楚每个外设的DMA连接方式:是专用还是共享通用控制器?
- 统计所有外设的并发DMA需求和带宽要求。例如,摄像头和网络可能同时需要高带宽,而UART和I2C则需求较低。
-
在驱动中优雅地处理:
- 总是检查
dma_request_channel()的返回值。申请失败是可能的,你的驱动必须有回退方案(例如,降级为PIO模式——用CPU搬运数据)。 - 使用
dmaengine_prep_slave_sg()而不是多次dmaengine_prep_slave_single(),以减少通道占用时间。 - 传输完成后,如果驱动长时间不再使用DMA,可以考虑调用
dma_release_channel()主动释放,但这在大多数情况下不是必须的。
- 总是检查
-
设备树配置:
- 在设备树中正确声明外设对DMA通道的需求,内核会利用这些信息进行正确的资源映射。
&uart0 { dmas = <&dmac0 5>, <&dmac0 6>; /* 指定使用dmac0的控制器的5和6号通道 */ dma-names = "tx", "rx"; };
5. 总结
- 硬件上,DMA通道资源可能是有限的,尤其是共享的通用DMA控制器。
- 软件上,Linux DMA Engine框架通过动态分配和分时复用机制,高效地管理这些有限资源,使得多个外设能够“合作共用”物理通道。
- 只要不是所有外设都在极限带宽下持续并行工作,通常不需要担心通道不够用。
- 作为驱动开发者,你的任务是正确地请求和使用通道,并妥善处理申请失败的情况,而不是担心硬件到底有多少个通道。框架会帮你处理底层的复杂性。
三、5.10.110 内核源码实例分析:drivers/tty/serial/8250/8250_dma.c
/home/kernel-develop-5.10/drivers/tty/serial/8250/8250_dma.c#L1-L415
// SPDX-License-Identifier: GPL-2.0+
/*
* 8250_dma.c - DMA Engine API support for 8250.c
*
* Copyright (C) 2013 Intel Corporation
*/
#include <linux/tty.h> // TTY子系统头文件
#include <linux/tty_flip.h> // TTY翻转缓冲区接口
#include <linux/serial_reg.h> // 8250寄存器定义
#include <linux/dma-mapping.h> // DMA映射API
#include "8250.h" // 8250驱动核心头文件
// Rockchip平台特定配置
#if defined(CONFIG_ARCH_ROCKCHIP) && defined(CONFIG_NO_GKI)
#define MAX_TX_BYTES 64 // 最大单次传输字节数
#define MAX_FIFO_SIZE 64 // FIFO缓冲区大小
#define UART_RFL_16550A 0x21 // RX FIFO级别寄存器偏移
#define DW_UART_DMASA 0x2a // DMA起始寄存器偏移
#endif
/**
* __dma_tx_complete - DMA传输完成回调函数(TX方向)
* @param: uart_8250_port结构指针,包含DMA传输上下文
*
* 处理DMA传输完成后的清理工作:
* 1. 同步DMA缓冲区数据
* 2. 更新传输计数器
* 3. 唤醒等待的写入进程
* 4. 重新启动DMA传输(如有待发送数据)
*/
static void __dma_tx_complete(void *param)
{
struct uart_8250_port *p = param;
struct uart_8250_dma *dma = p->dma;
struct circ_buf *xmit = &p->port.state->xmit;
unsigned long flags;
int ret;
// 同步DMA缓冲区数据给CPU
dma_sync_single_for_cpu(dma->txchan->device->dev, dma->tx_addr,
UART_XMIT_SIZE, DMA_TO_DEVICE);
spin_lock_irqsave(&p->port.lock, flags);
dma->tx_running = 0; // 标记传输完成
// 更新发送缓冲区尾指针
xmit->tail += dma->tx_size;
xmit->tail &= UART_XMIT_SIZE - 1;
p->port.icount.tx += dma->tx_size; // 更新发送计数
// 如果待发送字符数低于唤醒阈值,唤醒写入进程
if (uart_circ_chars_pending(xmit) < WAKEUP_CHARS)
uart_write_wakeup(&p->port);
// 尝试启动新的DMA传输
ret = serial8250_tx_dma(p);
if (ret)
serial8250_set_THRI(p); // 启用传统中断模式
spin_unlock_irqrestore(&p->port.lock, flags);
}
// Rockchip平台特定DMA接收实现
#if defined(CONFIG_ARCH_ROCKCHIP) && defined(CONFIG_NO_GKI)
static void __dma_rx_complete(void *param)
{
struct uart_8250_port *p = param;
struct uart_8250_dma *dma = p->dma;
struct tty_port *tty_port = &p->port.state->port;
struct dma_tx_state state;
unsigned int count = 0, cur_index = 0;
// 获取当前DMA传输状态
dmaengine_tx_status(dma->rxchan, dma->rx_cookie, &state);
cur_index = dma->rx_size - state.residue; // 计算当前接收位置
// 如果没有新数据接收,直接返回
if (cur_index == dma->rx_index)
return;
else if (cur_index > dma->rx_index)
count = cur_index - dma->rx_index;
else
count = dma->rx_size - dma->rx_index;
// 将新接收的数据插入TTY缓冲区
tty_insert_flip_string(tty_port, dma->rx_buf + dma->rx_index, count);
// 处理环形缓冲区回绕情况
if (cur_index < dma->rx_index) {
tty_insert_flip_string(tty_port, dma->rx_buf, cur_index);
count += cur_index;
}
// 更新接收计数和缓冲区索引
p->port.icount.rx += count;
dma->rx_index = cur_index;
}
#else
// 通用DMA接收实现
static void __dma_rx_complete(void *param)
{
struct uart_8250_port *p = param;
struct uart_8250_dma *dma = p->dma;
struct tty_port *tty_port = &p->port.state->port;
struct dma_tx_state state;
enum dma_status dma_status;
int count;
// 检查DMA状态(进行中则退出)
dma_status = dmaengine_tx_status(dma->rxchan, dma->rx_cookie, &state);
if (dma_status == DMA_IN_PROGRESS)
return;
// 计算实际接收数据量
count = dma->rx_size - state.residue;
// 将数据插入TTY缓冲区
tty_insert_flip_string(tty_port, dma->rx_buf, count);
// 更新接收计数
p->port.icount.rx += count;
dma->rx_running = 0;
// 提交接收缓冲区
tty_flip_buffer_push(tty_port);
}
/**
* dma_rx_complete - DMA接收完成中断处理函数
* @param: uart_8250_port结构指针
*
* 中断上下文中的DMA接收处理:
* 1. 获取端口锁
* 2. 条件触发实际的DMA完成处理
* 3. 释放端口锁
*/
static void dma_rx_complete(void *param)
{
struct uart_8250_port *p = param;
struct uart_8250_dma *dma = p->dma;
unsigned long flags;
// 在中断上下文中安全地处理DMA完成
spin_lock_irqsave(&p->port.lock, flags);
if (dma->rx_running)
__dma_rx_complete(p);
spin_unlock_irqrestore(&p->port.lock, flags);
}
#endif
/**
* serial8250_tx_dma - 启动DMA传输
* @p: uart_8250_port结构指针
*
* 返回值:
* 0: 成功或无待发送数据
* 负错误码: 启动失败
*
* 功能:
* 1. 处理XON/XOFF字符发送
* 2. 准备DMA描述符
* 3. 配置DMA传输参数
* 4. 提交并启动DMA传输
*/
int serial8250_tx_dma(struct uart_8250_port *p)
{
struct uart_8250_dma *dma = p->dma;
struct circ_buf *xmit = &p->port.state->xmit;
struct dma_async_tx_descriptor *desc;
struct uart_port *up = &p->port;
int ret;
// 如果传输正在进行
if (dma->tx_running) {
if (up->x_char) {
// 暂停DMA传输处理XON/XOFF字符
dmaengine_pause(dma->txchan);
uart_xchar_out(up, UART_TX);
dmaengine_resume(dma->txchan);
}
return 0;
} else if (up->x_char) {
// 无DMA传输时直接发送XON/XOFF字符
uart_xchar_out(up, UART_TX);
}
// 检查传输条件
if (uart_tx_stopped(&p->port) || uart_circ_empty(xmit)) {
serial8250_rpm_put_tx(p);
return 0;
}
// 计算待传输数据长度
dma->tx_size = CIRC_CNT_TO_END(xmit->head, xmit->tail, UART_XMIT_SIZE);
// Rockchip平台特定检查
#if defined(CONFIG_ARCH_ROCKCHIP) && defined(CONFIG_NO_GKI)
if (dma->tx_size < MAX_TX_BYTES) {
ret = -EBUSY;
goto err;
}
#endif
// 准备DMA描述符
desc = dmaengine_prep_slave_single(dma->txchan,
dma->tx_addr + xmit->tail,
dma->tx_size, DMA_MEM_TO_DEV,
DMA_PREP_INTERRUPT | DMA_CTRL_ACK);
if (!desc) {
ret = -EBUSY;
goto err;
}
// 配置DMA传输参数
dma->tx_running = 1;
desc->callback = __dma_tx_complete; // 设置完成回调
desc->callback_param = p; // 传递私有参数
// 提交DMA传输
dma->tx_cookie = dmaengine_submit(desc);
// 同步DMA缓冲区
dma_sync_single_for_device(dma->txchan->device->dev, dma->tx_addr,
UART_XMIT_SIZE, DMA_TO_DEVICE);
// Rockchip平台特定寄存器配置
#if defined(CONFIG_ARCH_ROCKCHIP) && defined(CONFIG_NO_GKI)
serial_port_out(&p->port, DW_UART_DMASA, 0x1);
#endif
// 启动DMA传输
dma_async_issue_pending(dma->txchan);
// 处理传输错误情况
if (dma->tx_err) {
dma->tx_err = 0;
serial8250_clear_THRI(p); // 清除中断标志
}
return 0;
err:
dma->tx_err = 1;
return ret;
}
// Rockchip平台特定DMA接收实现
#if defined(CONFIG_ARCH_ROCKCHIP) && defined(CONFIG_NO_GKI)
int serial8250_rx_dma(struct uart_8250_port *p)
{
unsigned int rfl, i = 0, fcr = 0, cur_index = 0;
unsigned char buf[MAX_FIFO_SIZE];
struct uart_port *port = &p->port;
struct tty_port *tty_port = &p->port.state->port;
struct dma_tx_state state;
struct uart_8250_dma *dma = p->dma;
// 配置FIFO触发级别
fcr = UART_FCR_ENABLE_FIFO | UART_FCR_T_TRIG_10 | UART_FCR_R_TRIG_11;
serial_port_out(port, UART_FCR, fcr);
// 等待DMA传输完成
do {
dmaengine_tx_status(dma->rxchan, dma->rx_cookie, &state);
cur_index = dma->rx_size - state.residue;
} while (cur_index % dma->rxconf.src_maxburst);
// 读取FIFO数据
rfl = serial_port_in(port, UART_RFL_16550A);
while (i < rfl)
buf[i++] = serial_port_in(port, UART_RX);
// 处理DMA接收完成
__dma_rx_complete(p);
// 将数据插入TTY缓冲区
tty_insert_flip_string(tty_port, buf, i);
// 更新接收计数
p->port.icount.rx += i;
// 提交接收缓冲区
tty_flip_buffer_push(tty_port);
// 恢复FIFO配置
if (fcr)
serial_port_out(port, UART_FCR, p->fcr);
return 0;
}
int serial8250_start_rx_dma(struct uart_8250_port *p)
{
struct uart_8250_dma *dma = p->dma;
struct dma_async_tx_descriptor *desc;
// 准备循环DMA接收
desc = dmaengine_prep_dma_cyclic(dma->rxchan, dma->rx_addr,
dma->rx_size, dma->rx_size,
DMA_DEV_TO_MEM, DMA_PREP_INTERRUPT |
DMA_CTRL_ACK);
if (!desc)
return -EBUSY;
// 配置DMA参数
dma->rx_running = 1;
desc->callback = NULL;
desc->callback_param = NULL;
// 提交并启动DMA
dma->rx_cookie = dmaengine_submit(desc);
dma_async_issue_pending(dma->rxchan);
dma->rx_index = 0;
return 0;
}
#else
// 通用DMA接收实现
int serial8250_rx_dma(struct uart_8250_port *p)
{
struct uart_8250_dma *dma = p->dma;
struct dma_async_tx_descriptor *desc;
// 如果已有传输在运行
if (dma->rx_running)
return 0;
// 准备单次DMA接收
desc = dmaengine_prep_slave_single(dma->rxchan, dma->rx_addr,
dma->rx_size, DMA_DEV_TO_MEM,
DMA_PREP_INTERRUPT | DMA_CTRL_ACK);
if (!desc)
return -EBUSY;
// 配置DMA参数
dma->rx_running = 1;
desc->callback = dma_rx_complete; // 设置完成回调
desc->callback_param = p; // 传递私有参数
// 提交DMA传输
dma->rx_cookie = dmaengine_submit(desc);
// 启动DMA传输
dma_async_issue_pending(dma->rxchan);
return 0;
}
#endif
/**
* serial8250_rx_dma_flush - DMA接收缓冲区刷新
* @p: uart_8250_port结构指针
*
* 功能:
* 1. 暂停DMA传输
* 2. 完成剩余数据处理
* 3. 终止DMA传输
*/
void serial8250_rx_dma_flush(struct uart_8250_port *p)
{
struct uart_8250_dma *dma = p->dma;
if (dma->rx_running) {
dmaengine_pause(dma->rxchan); // 暂停DMA
__dma_rx_complete(p); // 处理剩余数据
dmaengine_terminate_async(dma->rxchan); // 终止传输
}
}
EXPORT_SYMBOL_GPL(serial8250_rx_dma_flush);
/**
* serial8250_request_dma - DMA通道请求
* @p: uart_8250_port结构指针
*
* 返回值:
* 0: 成功
* 负错误码: 失败
*
* 功能:
* 1. 配置DMA从设备参数
* 2. 请求DMA通道
* 3. 分配DMA缓冲区
* 4. 配置DMA传输参数
*/
int serial8250_request_dma(struct uart_8250_port *p)
{
struct uart_8250_dma *dma = p->dma;
phys_addr_t rx_dma_addr = dma->rx_dma_addr ?
dma->rx_dma_addr : p->port.mapbase;
phys_addr_t tx_dma_addr = dma->tx_dma_addr ?
dma->tx_dma_addr : p->port.mapbase;
dma_cap_mask_t mask;
struct dma_slave_caps caps;
int ret;
// 配置RX DMA参数
dma->rxconf.direction = DMA_DEV_TO_MEM;
dma->rxconf.src_addr_width = DMA_SLAVE_BUSWIDTH_1_BYTE;
dma->rxconf.src_addr = rx_dma_addr + UART_RX;
// Rockchip平台特定配置
#if defined(CONFIG_ARCH_ROCKCHIP) && defined(CONFIG_NO_GKI)
if ((p->port.fifosize / 4) < 16)
dma->rxconf.src_maxburst = p->port.fifosize / 4;
else
dma->rxconf.src_maxburst = 16;
#endif
// 配置TX DMA参数
dma->txconf.direction = DMA_MEM_TO_DEV;
dma->txconf.dst_addr_width = DMA_SLAVE_BUSWIDTH_1_BYTE;
dma->txconf.dst_addr = tx_dma_addr + UART_TX;
// Rockchip平台特定配置
#if defined(CONFIG_ARCH_ROCKCHIP) && defined(CONFIG_NO_GKI)
dma->txconf.dst_maxburst = 16;
#endif
// 初始化DMA能力掩码
dma_cap_zero(mask);
dma_cap_set(DMA_SLAVE, mask);
// 请求RX DMA通道
dma->rxchan = dma_request_slave_channel_compat(mask,
dma->fn, dma->rx_param,
p->port.dev, "rx");
if (!dma->rxchan)
return -ENODEV;
// 获取DMA通道能力
ret = dma_get_slave_caps(dma->rxchan, &caps);
if (ret)
goto release_rx;
// 检查必需的功能支持
if (!caps.cmd_pause || !caps.cmd_terminate ||
caps.residue_granularity == DMA_RESIDUE_GRANULARITY_DESCRIPTOR) {
ret = -EINVAL;
goto release_rx;
}
// 配置DMA通道
dmaengine_slave_config(dma->rxchan, &dma->rxconf);
// 分配RX缓冲区
#if defined(CONFIG_ARCH_ROCKCHIP) && defined(CONFIG_NO_GKI)
if (!dma->rx_size)
dma->rx_size = PAGE_SIZE * 2;
#else
if (!dma->rx_size)
dma->rx_size = PAGE_SIZE;
#endif
// 分配DMA一致性内存
dma->rx_buf = dma_alloc_coherent(dma->rxchan->device->dev, dma->rx_size,
&dma->rx_addr, GFP_KERNEL);
if (!dma->rx_buf) {
ret = -ENOMEM;
goto release_rx;
}
// 请求TX DMA通道
dma->txchan = dma_request_slave_channel_compat(mask,
dma->fn, dma->tx_param,
p->port.dev, "tx");
if (dma->txchan) {
// 配置TX DMA通道
dmaengine_slave_config(dma->txchan, &dma->txconf);
// 映射TX缓冲区
dma->tx_addr = dma_map_single(dma->txchan->device->dev,
p->port.state->xmit.buf,
UART_XMIT_SIZE,
DMA_TO_DEVICE);
if (dma_mapping_error(dma->txchan->device->dev, dma->tx_addr)) {
dma_free_coherent(dma->rxchan->device->dev,
dma->rx_size, dma->rx_buf,
dma->rx_addr);
dma_release_channel(dma->txchan);
dma->txchan = NULL;
}
// 日志输出
dev_info_ratelimited(p->port.dev, "got rx and tx dma channels\n");
} else {
dev_info_ratelimited(p->port.dev, "got rx dma channels only\n");
}
// Rockchip平台特定操作
#if defined(CONFIG_ARCH_ROCKCHIP) && defined(CONFIG_NO_GKI)
serial8250_start_rx_dma(p);
#endif
return 0;
release_rx:
dma_release_channel(dma->rxchan);
return ret;
}
EXPORT_SYMBOL_GPL(serial8250_request_dma);
/**
* serial8250_release_dma - DMA资源释放
* @p: uart_8250_port结构指针
*
* 功能:
* 1. 终止DMA传输
* 2. 释放DMA缓冲区
* 3. 释放DMA通道
*/
void serial8250_release_dma(struct uart_8250_port *p)
{
struct uart_8250_dma *dma = p->dma;
if (!dma)
return;
// 终止RX DMA传输
dmaengine_terminate_sync(dma->rxchan);
// 释放RX缓冲区
dma_free_coherent(dma->rxchan->device->dev, dma->rx_size, dma->rx_buf,
dma->rx_addr);
// 释放RX通道
dma_release_channel(dma->rxchan);
dma->rxchan = NULL;
// Rockchip平台特定操作
#if defined(CONFIG_ARCH_ROCKCHIP) && defined(CONFIG_NO_GKI)
dma->rx_running = 0;
#endif
// 处理TX资源
if (dma->txchan) {
// 终止TX DMA传输
dmaengine_terminate_all(dma->txchan);
// 解除TX缓冲区映射
dma_unmap_single(dma->txchan->device->dev, dma->tx_addr,
UART_XMIT_SIZE, DMA_TO_DEVICE);
// 释放TX通道
dma_release_channel(dma->txchan);
dma->txchan = NULL;
dma->tx_running = 0;
}
// 调试日志
dev_dbg_ratelimited(p->port.dev, "dma channels released\n");
}
EXPORT_SYMBOL_GPL(serial8250_release_dma);
1. 文件概述
- 路径:[/drivers/tty/serial/8250/8250_dma.c]
- 功能:为8250串口驱动实现DMA(直接内存访问)支持,优化数据传输效率。
- 核心目标:通过DMA引擎API减少CPU中断开销,提升串口通信性能。
2. 关键函数解析

发送DMA处理
- __dma_tx_complete
- 作用:DMA发送完成后更新环形缓冲区状态,唤醒等待进程。
- 流程:
(1)同步DMA内存(dma_sync_single_for_cpu)。
(2)更新发送尾指针(xmit->tail)并减少待发送数据量。
(3)若剩余数据不足阈值(WAKEUP_CHARS),触发uart_write_wakeup通知上层。
(4)调用serial8250_tx_dma继续发送后续数据。
- serial8250_tx_dma
- 作用:启动DMA发送操作。
- 关键逻辑:
(1)检查是否已有DMA在运行或发送被暂停。
(2)配置DMA描述符(dmaengine_prep_slave_single)并提交任务。
(3)Rockchip平台特殊处理:通过写入DW_UART_DMASA寄存器清除DMA请求。
接收DMA处理
- __dma_rx_complete(条件编译分支)
- Rockchip特定实现:
(1)支持环形缓冲区多段数据拼接(处理DMA缓冲区回绕)。
(2)通过dmaengine_tx_status获取当前DMA位置,计算新接收数据量。 - 通用实现:
单次DMA传输完成后将数据插入TTY缓冲区(tty_insert_flip_string),并调用tty_flip_buffer_push通知上层。
- Rockchip特定实现:
- serial8250_rx_dma
- 作用:启动DMA接收操作。
- 差异:
(1)Rockchip使用dmaengine_prep_dma_cyclic配置循环DMA以持续接收。
(2)通用实现使用单次DMA传输,完成回调为dma_rx_complete。
DMA资源管理
-
serial8250_request_dma
- 作用:请求并配置DMA通道(RX和TX)。
- 流程:
(1)设置DMA从设备配置(方向、地址宽度、突发长度)。
(2)通过dma_request_slave_channel_compat获取DMA通道。
(3)分配DMA缓冲区(Rockchip使用2倍页大小)。
(4)导出DMA符号供其他模块调用(如EXPORT_SYMBOL_GPL)。
-
serial8250_release_dma
- 作用:释放DMA资源(终止通道、释放内存)。
3. 平台适配
- Rockchip平台特殊逻辑
- 通过
CONFIG_ARCH_ROCKCHIP && CONFIG_NO_GKI启用: - 定义
MAX_TX_BYTES和MAX_FIFO_SIZE为64字节(匹配硬件特性)。 - 使用特定寄存器
DW_UART_DMASA控制DMA请求。 - 自定义
serial8250_start_rx_dma启动循环DMA。 - 潜在风险:条件编译可能导致代码维护复杂,建议通过
Kconfig明确依赖关系。
- 通过
4. 同步与错误处理
- DMA内存同步:
- 在DMA传输前后调用
dma_sync_single_for_cpu/device确保数据一致性(如__dma_tx_complete和serial8250_tx_dma)。
- 在DMA传输前后调用
- 错误处理:
- 资源分配失败时(如
dma_alloc_coherent),正确释放已分配资源(如goto release_rx)。 - DMA操作失败(如
dmaengine_prep_slave_single返回NULL)时返回错误码并清理状态。
- 资源分配失败时(如
欢迎阅读下一篇:Linux驱动之DMA(三)
更多推荐



所有评论(0)