前言
本文分析哪吒版中的 DMA Controller 和 SPI 驱动和以太网驱动使用 DMAC 的方式
先把结论放前头
Linux DMAC | 模块硬件内置 DMAC | |
---|---|---|
申请 DMA channel | 需要申请 | 无需申请 |
配置 channel 并获取描述符 | 需要申请 | 无需申请 |
DMA MAPPING | 需要 Buffer | 需要 Buffer 描述符 |
启动传输 | 交给 DMAC dmaengine_submitBuffer | 控制 DMA 寄存器位开启 |
等待传输完成 | 等待 DMA 中断 | 读 DMA 寄存器状态位 |
中断处理 | 硬件中断基础上,使用 DMAC 收发数据都将多产生一次中断 | 都有 |
数据 BUFFER | 自己处理 | 自己处理 |
描述符 | 需要申请 | 自己管理 |
最重要的概念是:谁是 DMA 传输的控制者,谁就是 DMA MASTER。在 Linux DMAC 框架下,Linux DMAC 是 DMA MASTER ,SPI UART I2C 这些外设试图用 DMAC 传输,则是 DMA SLAVE。在 ETH PCIE USB 中,可能自己包含 DMA Controller,此时,外设中的这个 DMAC(或者说外设本身)就是 DMA MASTER。
MASTER & SLVAE 概念清晰之后,才能进一步辨别清楚 DMA 描述符,DMA 描述符指向的 buffer 等概念
Provider Consumer Core
内核中的外设相关的子系统,如中断、Clock、Reset、Pinctrl 等都有类似的架构,即 Provider Consumer Core
- Core 向其它 driver 提供操作 clocks 的通用 API,其他 driver 就是 Consumer
- Core 实现 clock 控制的通用逻辑,这部分和硬件无关
- Provider 将和硬件相关的 clock 控制逻辑封装成操作函数集,交由底层的 platform 开发者实现,由通用逻辑调用
根据具体硬件不同,最大的区别在 Provider 方面,新思以太网、PCIE 都内置 DMA Controller,承担了 Provider 的用途,但低速外设没有内置 DMA Controller, 故之后应该需要用新思 DMAC IP,写对应的 DMA Controller,下文的 DMA Controller 意义等同 Provider
本文主要说明 Consumer 如何使用 Provider,对 Linux 子系统而言就是 SPI 怎么使用 DMAC,对 ETH MAC 而言就是以太网相关功能如何使用 ETH IP 内部的的 DMAC。
DMA 作用的内存空间示意
本节文字全部引用自:www.wowotech.net/memory_mana…
在嵌入式系统中,从需求的角度看内存,是非常简单的,可以总结为两大类(参考上面的图片 1):
1)CPU 有访问内存的需求,包括从内存中取指、从内存中取数据、向内存中写数据。相应的数据流为:
CPU<-------------->MMU(Optional)<----------->Memory
2)其它外部设备有访问内存的需求,包括从内存“读取”数据、向内存“写入”数据。这里的“读取”和“写入”加了引号,是因为在大部分情况下,设备不像 CPU 那样有智能,不具备直接访问内存的能力。总结来说,设备有三种途径访问内存:
a)由 CPU 从中中转,数据流如下(本质上是 CPU 访问内存):
Device<---------------->CPU<--------------------Memory
b)由第三方具有智能的模块(如 DMA 控制器)中转,数据流如下:
Device<----------->DMA Controller<--------->Memory
c)直接访问内存,数据流如下:
Device<---------->IOMMU(Optional)--------->Memory
内核通常使用的地址是虚拟地址。我们调用 kmalloc()、vmalloc()或者类似的接口返回的地址都是虚拟地址,保存在 "void *" 的变量中。
虚拟内存系统(TLB、页表等)将虚拟地址(程序角度)翻译成物理地址(CPU 角度),物理地址保存在“phys_addr_t”或“resource_size_t”的变量中。对于一个硬件设备上的寄存器等设备资源,内核是按照物理地址来管理的。通过 /proc/iomem,你可以看到这些和设备 IO 相关的物理地址。当然,驱动并不能直接使用这些物理地址,必须首先通过 ioremap()接口将这些物理地址映射到内核虚拟地址空间上去。
I/O 设备使用第三种地址:“总线地址”。如果设备在 MMIO 地址空间中有若干的寄存器,或者该设备足够的智能,它可以通过 DMA 执行读写系统内存的操作,这些情况下,设备使用的地址就是总线地址。在某些系统中,总线地址与 CPU 物理地址相同,但一般来说它们不是。iommus 和 host bridge 可以在物理地址和总线地址之间进行映射。
BUFFER 和 DESCRIPTOR
对于使用 Linux DMAC 的外设而言,descriptor 是要去申请的,buffer 是自己管理的,把描述符申请回来,自己填充,再告诉 DMAC
对于含有 DMAC 的外设而言,descriptor 和 buffer 都要自己管理,要把描述符自己填充后,发给外设中的 DMAC,外设中的 DMAC 根据描述符再完成 buffer 的传输
在看以太网 DMA 资料过程中,经常看到一个说法,建立了环形缓冲区,经过 DMA 后,把环形缓冲区的数据完成了传输,这个表述会让人有点迷惑,以为 DMA 作用于环形缓冲区。正确的表述应该是,~~DMA 从环形缓冲区获得 DMA BUFFER 的 source 起始地址和长度和 destination 的起始地址和长度信息,并根据信息完成信息中指向的数据区域的转移。Descriptor 是为了描述一次 DMA 传输产生的结构,~~以太网的 DMAC 通过 DMA 过程获取描述符后,再自行完成了 skb->data 的 DMA 传输过程。
软件层面研究 DMA 的核心是 DMA Buffer 和 DMA Descripor 的分配、初始化、管理方式和回收。DMA 只能接收物理地址,故对 Buffer 的管理本身就涉及到虚拟地址、物理地址和设备的映射
CHANNEL
CHANNEL PORT 都是偏硬件相关的,软件除了配置外,可以注意的地方不多
哪吒设备树
哪吒中对 DMA PORT 的描述
中断
参考文档:
外设传输都涉及中断,走 Linux DMAC 也会涉及中断,目前接触的外设中都是软硬中断共同配合完成中断处理,对软硬中断的概念做个说明
中断处理模块是任何 OS 中最重要的一个模块,对系统的性能会有直接的影响。想像一下:如果在通过 U 盘进行大量数据拷贝的时候,你按下一个 key,需要半秒的时间才显示出来,这个场景是否让你崩溃?因此,对于那些复杂的、需要大量数据处理的硬件中断,我们不能让 handler 中处理完一切再恢复现场(handler 是全程关闭中断的),而是仅仅在 handler 中处理一部分,具体包括: (1)有实时性要求的 (2)和硬件相关的。例如 ack 中断,read HW FIFO to ram 等 (3)如果是共享中断,那么获取硬件中断状态以便判断是否是本中断发生 除此之外,其他的内容都是放到 bottom half 中处理。在把中断处理过程划分成 top half 和 bottom half 之后,关中断的 top half 被瘦身,可以非常快速的执行完毕,大大减少了系统关中断的时间,提高了系统的性能。
仅有硬中断:
上半下半中断:
softirq 和 tasklet
以太网用的 softirq,spi 用的 tasklet,其区别在于 softirq 是静态定义的,定义如上,可以看到 tasklet 是一个特殊的 softirq,tasklet 可以动态定义。
为了性能,同一类型的 softirq 有可能在不同的 CPU 上并发执行,这给使用者带来了极大的痛苦,因为驱动工程师在撰写 softirq 的回调函数的时候要考虑重入。
Tasklet 不会在两个 CPU 上被调度
spi-sunxi.c
使用 linux 内核 dma engine 的步骤是,spi-sunxi 遵循这几个步骤,是每次传输的时候才申请的 DMA channel 相比网络驱动来说,没有事先获取的描述符队列,描述符由 dma engine 分发
- 申请 DMA channel
- 配置 DMA channel 参数
- 获取描述符
- 启动传输
- 等待传输结束
分别找一下对应的代码
申请 DMA channel
从 DMA ENGINE 返回一个 dma_chan
static int sunxi_spi_prepare_dma(struct device *dev, spi_dma_info_t *_info,
enum spi_dma_dir _dir, const char *name)
{
dprintk(DEBUG_INFO, "Init DMA, dir %d\n", _dir);
if (_info->chan == NULL) {
_info->chan = dma_request_chan(dev, name);
if (IS_ERR(_info->chan)) {
SPI_ERR("Request DMA(dir %d) failed!\n", _dir);
return -EINVAL;
}
}
_info->dir = _dir;
return 0;
}
配置 DMA channel 参数和获取描述符
对 DMA channel 进行配置,并获取描述符,通用的步骤是获取描述符后,对描述符进行配置,再通过一个特定结构提交这次 DMA 传输,这个特定结构是 struct dma_async_tx_descriptor,它不是 dma descriptor
static int sunxi_spi_config_dma_tx(struct sunxi_spi *sspi, struct spi_transfer *t)
{
int ret = 0;
int nents = 0;
struct dma_slave_config dma_conf = {0};
struct dma_async_tx_descriptor *dma_desc = NULL;
unsigned int i, j;
u8 buf[64], cnt = 0;
if (debug_mask & DEBUG_INFO4) {
dprintk(DEBUG_INIT, "t->len = %d\n", t->len);
if (debug_mask & DEBUG_DATA) {
for (i = 0; i < t->len; i += 16) {
cnt = 0;
cnt += sprintf(buf + cnt, "%03x: ", i);
for (j = 0; ((i + j) < t->len) && (j < 16); j++)
cnt += sprintf(buf + cnt, "%02x ",
((unsigned char *)(t->tx_buf))[i+j]);
pr_warn("%s\n", buf);
}
}
}
// 会根据 buf 情况组织 sg
ret = sunxi_spi_dma_init_sg(&sspi->dma_tx, (void *)t->tx_buf,
t->len);
if (ret != 0)
return ret;
dma_conf.direction = DMA_MEM_TO_DEV;
src
dma_conf.dst_addr = sspi->base_addr_phy + SPI_TXDATA_REG;
if (t->len%DMA_SLAVE_BUSWIDTH_4_BYTES) {
dma_conf.src_addr_width = DMA_SLAVE_BUSWIDTH_1_BYTE;
dma_conf.dst_addr_width = DMA_SLAVE_BUSWIDTH_1_BYTE;
} else {
dma_conf.src_addr_width = DMA_SLAVE_BUSWIDTH_4_BYTES;
dma_conf.dst_addr_width = DMA_SLAVE_BUSWIDTH_4_BYTES;
}
dma_conf.src_maxburst = 4;
dma_conf.dst_maxburst = 4;
dmaengine_slave_config(sspi->dma_tx.chan, &dma_conf);
// 重点
nents = dma_map_sg(&sspi->pdev->dev, sspi->dma_tx.sg, sspi->dma_tx.nents, DMA_TO_DEVICE);
if (!nents) {
SPI_ERR("[spi%d] dma_map_sg(%d) failed! return %d\n",
sspi->master->bus_num, sspi->dma_tx.nents, nents);
return -ENOMEM;
}
dprintk(DEBUG_INFO, "[spi%d] npages = %d, nents = %d\n",
sspi->master->bus_num, sspi->dma_tx.nents, nents);
//会形成 sg address 到 dma 的映射,主要是物理地址的映射
dma_desc = dmaengine_prep_slave_sg(sspi->dma_tx.chan, sspi->dma_tx.sg,
nents, DMA_MEM_TO_DEV,
DMA_PREP_INTERRUPT|DMA_CTRL_ACK);
if (!dma_desc) {
SPI_ERR("[spi%d] dmaengine_prep_slave_sg() failed!\n",
sspi->master->bus_num);
dma_unmap_sg(&sspi->pdev->dev, sspi->dma_tx.sg,
sspi->dma_tx.nents, DMA_TO_DEVICE);
return -1;
}
dma_desc->callback = sunxi_spi_dma_cb_tx;
dma_desc->callback_param = (void *)sspi;
dmaengine_submit(dma_desc);
return 0;
}
Scatter List 将过程封装到函数中,不是很直观,只需知道其核心作用即可,即获取描述符 -> 配置描述符
着重讲一下涉及了映射的操作函数 dma_map_sg 和 Linux 提供的映射机制,这个映射连接了内存空间的相互关系
DMA MAPPING
DMA MAPPING 不包含于 Linux DMAC 框架,其是内核为框架提供的映射功能
一共有两种类型的 DMA mapping
- 一致性 DMA 映射(Consistent DMA mappings )
Consistent DMA mapping 有下面两种特点:
(1)持续使用该 DMA buffer(不是一次性的),因此 Consistent DMA 总是在初始化的时候进行 map,在 shutdown 的时候 unmap。
(2)CPU 和 DMA controller 在发起对 DMA buffer 的并行访问的时候不需要考虑 cache 的影响,也就是说不需要软件进行 cache 操作,CPU 和 DMA controller 都可以看到对方对 DMA buffer 的更新。实际上一致性 DMA 映射中的那个 Consistent 实际上可以称为 coherent,即 cache coherent。
虚拟地址 DMA 地址
- 流式 DMA 映射(streaming DMA mapping)
流式 DMA 映射是一次性的,一般是需要进行 DMA 传输的时候才进行 mapping,一旦 DMA 传输完成,就立刻 ummap(除非你使用 dma_sync_*的接口,下面会描述)。并且硬件可以为顺序化访问进行优化
Coherent 映射的传入参数是设备,映射的空间大小,用来接返回值的物理地址值和 flag,返回参数是开辟空间的指向虚拟地址的指针,一致性 dma 映射包含了内存空间的分配
流式映射一般思路是传入要做映射的空间和 size, 返回 dma 需要的物理地址
DMA 地址
启动传输
client driver 可以通过 dmaengine_submit 接口将该描述符放到传输队列上,然后调用 dma_async_issue_pending 接口,启动传输
static int sunxi_spi_dma_tx_config(struct spi_device *spi, struct spi_transfer *t)
{
struct sunxi_spi *sspi = spi_master_get_devdata(spi->master);
void __iomem *base_addr = sspi->base_addr;
int ret = 0;
spi_enable_dma_irq(SPI_FIFO_CTL_TX_DRQEN, base_addr);
ret = sunxi_spi_prepare_dma(&sspi->pdev->dev, &sspi->dma_tx,
SPI_DMA_WDEV, "tx");
if (ret < 0) {
spi_disable_dma_irq(SPI_FIFO_CTL_TX_DRQEN, base_addr);
spi_disable_irq(SPI_INTEN_TC|SPI_INTEN_ERR, base_addr);
return -EINVAL;
}
// 在这里组织 DMA 信息并 submit
sunxi_spi_config_dma(sspi, SPI_DMA_WDEV, t);
// 调用 issue_pending
sunxi_spi_start_dma(&sspi->dma_tx);
return ret;
}
等待传输结束
在 DMA Controller 中注册了中断处理函数,DMA 传输完成后会引起对应中断,执行中断处理函数
DMAC 的 probe 中
ret = devm_request_irq(&pdev->dev, sdc->irq, sun6i_dma_interrupt, 0,
dev_name(&pdev->dev), sdc);
static irqreturn_t sun6i_dma_interrupt(int irq, void *dev_id)
{
vchan_cookie_complete(&pchan->desc->vd);
}
static inline void vchan_cookie_complete(struct virt_dma_desc *vd)
{
tasklet_schedule(&vc->task);
}
void vchan_init(struct virt_dma_chan *vc, struct dma_device *dmadev)
{
tasklet_init(&vc->task, vchan_complete, (unsigned long)vc);
}
static void vchan_complete(unsigned long arg)
{
// 有对 callback 的调用
dmaengine_desc_callback_invoke(&cb, &vd->tx_result);
}
中断处理函数最后会调用 spi-sunxi.c 中 dma 结束后的 callback,callback 的设置是这个
static int sunxi_spi_config_dma_rx(struct sunxi_spi *sspi, struct spi_transfer *t)
{
dma_desc->callback = sunxi_spi_dma_cb_rx;
dma_desc->callback_param = (void *)sspi;
dmaengine_submit(dma_desc);
}
static void sunxi_spi_dma_cb_rx(void *data)
{
struct sunxi_spi *sspi = (struct sunxi_spi *)data;
unsigned long flags = 0;
void __iomem *base_addr = sspi->base_addr;
spin_lock_irqsave(&sspi->lock, flags);
dprintk(DEBUG_INFO, "[spi%d] dma read data end\n", sspi->master->bus_num);
if (spi_query_rxfifo(base_addr) > 0) {
SPI_ERR("[spi%d] DMA end, but RxFIFO isn't empty! FSR: %#x\n",
sspi->master->bus_num, spi_query_rxfifo(base_addr));
sspi->result = -1; /* failed */
} else {
sspi->result = 0;
}
complete(&sspi->done);
spin_unlock_irqrestore(&sspi->lock, flags);
}
/* dma full done callback for spi tx */
static void sunxi_spi_dma_cb_tx(void *data)
{
struct sunxi_spi *sspi = (struct sunxi_spi *)data;
unsigned long flags = 0;
spin_lock_irqsave(&sspi->lock, flags);
dprintk(DEBUG_INFO, "[spi%d] dma write data end\n", sspi->master->bus_num);
spin_unlock_irqrestore(&sspi->lock, flags);
}
描述一下从提交本次传输给 dma engine 到完成传输的全过程
- 首先调用 dmaengine_submit 函数将 传输描述符(tx->vd)加到 vchan 的 desc_submited 链表。
上面如图所示,两个 tx 中的 vd 分别加到对应的 vchan 中的 desc_submited 链表。
- 调用 sunxi_issue_pending 函数将各自 vchan 中的 desc_submited 链表的 job 拿出来放到 desc_issued 链表中
- 将两个需要执行 job 的 schan 加入到 sun6i_dma_dev(DMA controller)中的 pending 链表中进行统一管理,之后启动 tasklet 来处理 pending 链表。
- tasklet 启动后,调用 sun6i_dma_tasklet 读取 pending 链表,取出需要处理的 schan(sunxi_chan),然后读取 schan 所对应的 vchan 中的 desc_issued 链表,从中取出 job, 之后调用 sun6i_dma_start_desc 函数 enable job 所绑定的 channel。
- 数据传输处理完后,DMA 会产生中断
- 中断函数会通过 vchan_cookie_complete 函数唤醒对应 vchan 中的 tasklet:vchan_complete
- tasklet:vchan_complete 会调用 tasklet 软中断
- 软中断执行
Provider
这里在公司内部分享时候用了非常复杂且精妙的图片,详细描述了一个 specific DMA provider 和框架一同合作完成 DMA 的流程,但是图片来自 CSDN 的收费博客且笔者发现完全找不到这篇博客了,故尽可能文字叙述一下图片的重点内容。
DMA 的关键是描述符和通道,描述符和通道上都存在子父类关系
描述符:sun6i_desc(specific DMA provider's desc)包含 virt_dma_desc 包含 dma_desc(这个结构在 Linux DMAC 中叫 dma_async_tx_descriptor,这个也是 DMA consumer 唯一可以拿到的关于 DMAC 子系统的数据结构)
通道:sun6i_vchan(specific DMA provider's channel)包含 virt_dma_chan 包含 dma_chan
描述符填充完就没啥了,在 DMA 子系统中,virtual channel 负责管理和处理提交描述符的 issue ,进而管理异步的 DMA 提交。
dma_async_tx_descriptor 为啥不命名成 dma_desc,笔者这里感觉这个命名非常精妙,它完全说明了使用一次 DMA 子系统的必要步骤,就是把描述符异步的提交给 DMA 子系统,这个命名是个动词。
sunxi-gmac.c
以太网内部集成了 dma 控制器,相比使用 linux 内核中提供的 dma 机制而言,~~简化了一些步骤,~~增加了一些步骤,在整体梳理完后再做梳理。
底下是特定网卡驱动的 DMA 工作流程,可以用作参考
网卡内置 DMAC,不走 Linux DMAC 逻辑,需要自己在驱动代码中管理描述符的内存
rx 和 tx 的传输数据内存空间分配通过 kzalloc, dma 映射通过 dma_map 系列 api ,desc 通过 dma_alloc_coherent 分配内存同时完成映射,数据的传输空间通过 dma_map_single 映射。
在以太网的 DMAC 中,会使用两个环(Ring)来管理数据的收发:接收环(Rx Ring)和发送环(Tx Ring),接收环用于将数据从网络设备复制到接收缓冲区,而发送环用于将数据从发送缓冲区发送到网络设备。此处的复制是通过读取 DMA 描述符中的数据地址和长度来实现数据的复制。
描述符分配和映射
dma_alloc_coherent 返回两个值,函数的返回值是从 cpu 角度访问 dma buffer 的虚拟地址,在参数中返回的值,是从设备看到的地址,没有 IOMMU 过程的话这个地址就是物理地址,这里的网卡就是物理地址,而在 PCIE 中是总线地址。这个 API 返回的内存地址都是按 page 对齐的连续的物理内存。在设备侧的内存区域和物理地址的内存区域都被分配了
static int geth_dma_desc_init(struct net_device *ndev)
{
struct geth_priv *priv = netdev_priv(ndev);
unsigned int buf_sz;
priv->rx_sk = kzalloc(sizeof(struct sk_buff *) * dma_desc_rx,
GFP_KERNEL);
if (!priv->rx_sk)
return -ENOMEM;
priv->tx_sk = kzalloc(sizeof(struct sk_buff *) * dma_desc_tx,
GFP_KERNEL);
if (!priv->tx_sk)
goto tx_sk_err;
/* Set the size of buffer depend on the MTU & max buf size */
buf_sz = MAX_BUF_SZ;
priv->dma_tx = dma_alloc_coherent(priv->dev,
dma_desc_tx *
sizeof(struct dma_desc),
&priv->dma_tx_phy,
GFP_KERNEL);
if (!priv->dma_tx)
goto dma_tx_err;
1 priv->dma_rx = dma_alloc_coherent(priv->dev,
dma_desc_rx *
sizeof(struct dma_desc),
&priv->dma_rx_phy,
GFP_KERNEL);
if (!priv->dma_rx)
goto dma_rx_err;
priv->buf_sz = buf_sz;
return 0;
}
传输数据的分配和映射
以 TX 为例,此处已确定好的待传输数据内容,已放置在 SKB 数据结构中。若想使用相应的 DMA 传输,需使用了 dma_map_single()
函数将待传输数据分片映射到 DMA 地址,并设置相应的 DMA 描述符,进而通过发送环实现数据通过 DMA 通道的发送。
数据的传输函数如下:
static netdev_tx_t geth_xmit(struct sk_buff *skb, struct net_device *ndev)
{
......
while (len != 0) {
desc = priv->dma_tx + entry;
tmp_len = ((len > MAX_BUF_SZ) ? MAX_BUF_SZ : len);
2 paddr = dma_map_single(priv->dev, skb->data, tmp_len, DMA_TO_DEVICE);
3 desc_buf_set(desc, paddr, tmp_len);
/* Don't set the first's own bit, here */
if (first != desc) {
priv->tx_sk[entry] = NULL;
desc_set_own(desc);
}
entry = circ_inc(entry, dma_desc_tx);
len -= tmp_len;
}
......
//根据发送环(Ring)的空闲空间大小,控制网络设备队列的启用或停用
if (circ_space(priv->tx_dirty, priv->tx_clean, dma_desc_tx) <=
(MAX_SKB_FRAGS + 1)) {
netif_stop_queue(ndev);
if (circ_space(priv->tx_dirty, priv->tx_clean, dma_desc_tx) >
TX_THRESH)
netif_wake_queue(ndev);
}
sunxi_tx_poll(priv->base);
geth_tx_complete(priv);
return NETDEV_TX_OK;
}
void desc_buf_set(struct dma_desc *desc, unsigned long paddr, int size)
{
desc->desc1.all &= (~((1 << 11) - 1));
desc->desc1.all |= (size & ((1 << 11) - 1));
desdesc2 = paddr;
}
通过 sunxi_tx_poll 函数的 write1 设置启动 DMA 数据发送位,该函数触发以太网控制器开始发送已经准备好的数据帧。
void sunxi_tx_poll(void *iobase)
{
unsigned int value;
value = readl(iobase + GETH_TX_CTL1);
writel(value | 0x80000000, iobase + GETH_TX_CTL1);//0x80000000转化为二进制为31位
}
相应的,若以 RX 为例,则需要在网卡中填充接收环的空闲位置,进而可以通过 DMA 通道接收相应的数据。geth_rx_refill 函数主要在网卡启动阶段(geth_open)和数据传输上层后(geth_rx)中进行调用。
static void geth_rx_refill(struct net_device *ndev)
{
struct geth_priv *priv = netdev_priv(ndev);
struct dma_desc *desc;
struct sk_buff *sk = NULL;
dma_addr_t paddr;
while (circ_space(priv->rx_clean, priv->rx_dirty, dma_desc_rx) > 0) {
int entry = priv->rx_clean;
/* Find the dirty's desc and clean it */
desc = priv->dma_rx + entry;
if (priv->rx_sk[entry] == NULL) {
sk = netdev_alloc_skb_ip_align(ndev, priv->buf_sz);
if (unlikely(sk == NULL))
break;
priv->rx_sk[entry] = sk;
coherent
paddr = dma_map_single(priv->dev, sk->data,
priv->buf_sz, DMA_FROM_DEVICE);
desc_buf_set(desc, paddr, priv->buf_sz);
}
/* sync memery */
wmb();
desc_set_own(desc);
priv->rx_clean = circ_inc(priv->rx_clean, dma_desc_rx);
}
}
此时以太网若启用 RX,即通过对应的寄存器使能即可。
void sunxi_rx_poll(void *iobase)
{
unsigned int value;
value = readl(iobase + GETH_RX_CTL1);
writel(value | 0x80000000, iobase + GETH_RX_CTL1);
}
等待传输结束
tx 传输出去,状态正常就结束了,rx 环节有数据包进来后产生硬中断,硬中断调用 napi 相关 api ,在 softirq 中处理相关逻辑。如以太网会启用轮询功能通过中断实现对 geth_rx 的调用,将数据发送至应用层。同时调用 geth_rx_refill 将 RX ring 重新进行填充。
static int geth_poll(struct napi_struct *napi, int budget)
{
......
geth_tx_complete(priv);
work_done = geth_rx(priv, budget);
......
}
static int geth_rx(struct geth_priv *priv, int limit)
{
......
skb_put(skb, frame_len);
dma_unmap_single(priv->dev, (u32)desc_buf_get_addr(desc),
desc_buf_get_len(desc), DMA_FROM_DEVICE);
skb->protocol = eth_type_trans(skb, priv->ndev);
skb->ip_summed = CHECKSUM_UNNECESSARY;
napi_gro_receive(&priv->napi, skb);
priv->ndev->stats.rx_packets++;
priv->ndev->stats.rx_bytes += frame_len;
}
geth_rx_refill(priv->ndev);
return rxcount;
}
总结
对比使用 Linux DMAC 和模块内置 DMAC 的 Consumer 驱动编写情况
Linux DMAC | 模块硬件内置 DMAC | |
---|---|---|
申请 DMA channel | 需要申请 | 无需申请 |
配置 channel 并获取描述符 | 需要申请 | 无需申请 |
DMA MAPPING | 需要 Buffer | 需要 Buffer 描述符 |
启动传输 | 交给 DMAC dmaengine_submitBuffer | 控制 DMA 寄存器位开启 |
等待传输完成 | 等待 DMA 中断 | 读 DMA 寄存器状态位 |
中断处理 | 硬件中断基础上,使用 DMAC 收发数据都将多产生一次中断 | 都有 |
数据 BUFFER | 自己处理 | 自己处理 |
描述符 | 需要申请 | 自己管理 |
这个也是刚来工作的时候写的,文章目的就是区分下 dma master & slave 的概念,目前要做 DMA Provider 的裸板驱动,对如何把 DMA 功能从 DMA 子系统框架下剥离有了更进一步认识,具体内容后续整理吧 |