linux驱动之DMA

转载自:https://www.jianshu.com/p/e1b622234d13

一、前言

在 嵌入式Linux 的内核及驱动中,DMA 常常被人提起。我们也许清楚它的原理且很明白它非常重要,但在某种程度上,对于 DMA 的使用者来说,我们一般使用其接口,而很少去了解整个 DMA 的运作方式。那么本文就从头到尾,简单地说一下 DMA 吧
注意:本文对DMA的概念不做讲述,请各位读者自行了解DMA的概念。

二、正文

2.1 高端内存

2.1.1 内核虚拟内存

在了解 DMA 之前,我们需要先了解一下 高端内存 的相关内容。这有助于我们理解 DMA
在 32位 的操作系统上,常常把程序的 0-3G(即PAGE_OFFSET) 作为 用户空间,而 3-4G 作为 内核空间

用户空间与内核空间


每个进程的 用户空间 是完全独立、互不相干 的,因为 用户进程 各自有不同的 页表 。
但 内核空间 是由 内核负责映射,它并 不会随进程切换而切换内核空间 的 虚拟地址 到 物理地址 映射是 所有进程 共享的。内核的 虚拟空间 独立其他程序。

我们重点关注一下 内核的虚拟内存空间,笔者将文档 Documentation/arm/memory.txt 中的 内核内存分布 列表如下(有所删减):

起始地址结束地址用处
0xffff80000xffffffff用于 copy_user_page 和 clear_user_page
0xffff10000xffff7fff保留,任何平台都不能使用该段 虚拟内存空间
0xffff00000xffff0fff异常向量表 所在的内存区域
0xffc000000xffefffff专用页面映射区(固定页面映射区),使用 fix_to_vir()可以获取该区域的逻辑地址
0xfee000000xfeffffffPCI技术的IO映射空间
VMALLOC_STARTVMALLOC_END使用 vmlloac() 和 ioremap() 获取的地址都处于该内存段
PAGE_OFFSEThigh_memory-1内存直接映射区域(常规内存映射区域),该段内存地址也称为 逻辑地址 ,可以用于 DMA寻址
PKMAP_BASEPAGE_OFFSET-1持久映射区域,一般用于映射 高端内存

2.1.2 逻辑地址及高端内存

我们知道内核的虚拟地址空间为 3G-4G。在这个范围内,有一段大小为 896M 的虚拟内存是直接映射到 0-896M 的 物理地址空间逻辑地址 与 物理地址 之间的转换是通过加上一个偏移 PAGE_OFFSET 来实现的。
按照笔者的理解:逻辑地址也是虚拟地址。其与一般虚拟地址不同的是,逻辑地址采用了线性映射,直接造成了逻辑地址与物理地址一一对应的关系

举个例子我们可以按照一般情况来假设 PAGE_OFFSET 为 0xc0000000,那么内核虚拟空间就是 0xc0000000-0xffffffff 的 1G 空间,这会造成一个问题。如果物理地址的 0-1G 地址空间映射到虚拟地址的 0xc0000000-0xffffffff,高于物理地址 0-1G 的地址范围我们就无法访问。那么此时就产生了 高端内存

通常在 32bit的内核 ,将 0-896M 的物理地址空间直接映射到 内存直接映射区域。将高于 896M 的物理地址映射到 高端内存区域,即 PKMAP_BASE ~ PAGE_OFFSET-1 这段空间。
在访问高于 896M 的 物理空间时,先从 高端内存区域 申请一段虚拟地址,并把需要访问的物理地址映射到该段虚拟地址,这样就可以访问高于 896M 大小的物理地址了。这种方式往往是使用 alloc_page 来获取内存。

按照上面的理解,当我们使用 vmlloc 时,也可以在 vmalloc区域 分配一段虚拟地址空间来映射到 物理高端内存,通过这种方式也可以访问 物理高端内存

高端内存映射

总结访问高端内存的 2 种方式:

  • alloc_page
  • vmalloc

2.1.3 DMA寻址

在 ARM架构 的 Soc 上,可能会存在 DMA寻址问题,即 DMA 无法访问所有的物理地址空间,只能访问特定的物理地址空间。上面说了 内存直接映射区域 中可以用于 DMA,意思是说如果 Soc 只能访问特定的物理空间,该段特定物理空间常常位于 内存直接映射区域

DMA区域和常规区域

2.2 总线地址

笔者在 蜗窝科技 中发现关于 总线地址 解释的好文章 Dynamic DMA mapping Guide。笔者将根据文章中的内容并按照自己的理解描述 总线地址,有兴趣的读者请访问原文。

总线地址 的使用笔者目前仅在 PCI技术 中见到过,所以笔者也将使用 PCI技术 并结合文章来描述。

2.2.1 MMIO

MM IO 即 内存映射I/O(Memory mapping I/O),有资料说它是 PCI规范 的一部分。但是按照笔者理解,在 ARM架构 的 Soc 中我们经常见到。
按照笔者理解,MMIO 可以理解为一段内存地址,我们通过这段 内存地址 就可以直接访问对应的 控制器 的 寄存器。举个例子,以 SPI控制器 为例,我们可以在一些 ARM架构Soc的 数据手册 中见到 SPI控制器 的 寄存器地址 在某一段内存地址中,我们可以直接通过访问这些地址来访问寄存器,这就是 MMIO。这样做的好处就是我们可以使用一套 汇编语言 即可访问 外围IO设备 的寄存器。
而在 x86 中,内存空间 和 IO空间 不是共享一段内存地址,所以需要使用另外一套 IO汇编语言 来访问 IO空间,这种方式也称为 port IO

2.2.2 例子说明

1. 访问MMIO上的寄存器
假设某 Soc 的 PCI设备 在内存中有一段 MMIO空间,我们需要通过对 PCI设备 的寄存器进行访问才能相应的控制 PCI 设备。
在一般情况下,在驱动中访问 寄存器 往往也是通过 虚拟地址 进行访问的。通常是使用 ioremap 对一个 寄存器 地址进行映射,将其映射到 虚拟地址,我们通过访问这个 虚拟地址 即可访问 寄存器
但在 PCI 设备中,PCI桥 将这些 PCI设备 和 系统(按笔者理解,此处可以理解为CPU) 连接在一起。PCI设备 会有基地址寄存器BAR(base address register),该寄存器表示 PCI设备 在 PCI总线 上的地址,即 总线地址。这样做之后,就不能直接通过访问虚拟地址来访问 PCI设备 的寄存器,需要使用 总线地址 才能访问到 PCI设备的MMIO

如下图所示,红圈 代表的是访问 PCI设备 的过程:

  1. CPU 并不能通过 总线地址A直接访问 PCI设备
  2. PCI桥(PCI host bridge) 会在 MMIO 的 地址B(物理地址) 和 总线地址A 之间进行映射。
  3. 映射完成后,可以通过 物理地址B(处于MMIO) 访问 PCI设备,访问是会通过 PCI桥 对地址进行翻译。
  4. 驱动通过 ioremap 把 物理地址B 映射成 虚拟地址C
  5. 通过 虚拟地址C 访问 PCI总线地址A

MMIO访问

2. PCI总线访问内存
假设 PCI设备 支持 DMA,那么在传输数据的时候,我们需要一块 DMA buffer 用于 接收或者发送数据,这块 DMA buffer 存在于 RAM内存区域 中。但我们之前说了,PCI 在 MMIO区域 有规定的 总线地址,那么在 RAM内存区域 也是一样,PCI设备 无法通过方位 RAM内存区域中的虚拟地址 来 获取或存放数据。但与 MMIO 不同的是,MMIO 通过 PCI桥 将 虚拟地址 映射为 总线地址RAM内存 则是通过 IOMMU 将 虚拟地址 映射为 总线地址
上面说的 IOMMU 与 MMU 的工作机理类似,但不同的是 MMU 是映射 物理地址到虚拟地址IOMMU 是映射 总线地址到物理地址 。
那么 PCI设备DMA 和 CPU 是如何在 同一块内存 中进行交互的呢?
回答这个问题,我们需要清楚以下几点:

  • PCI设备 使用 DMA 传输 的是数据时需要使用的是 总线地址,即 DMA 是使用 总线地址 作为 源地址 或者 目的地址
  • DMA 传输数据时,*IOMMU 可以将 总线地址 转换 物理地址 。
  • DMA 传输完成后,CPU 使用 虚拟地址 访问该内存块。

其步骤如下:

  1. 内存块 由 CPU 创建,此时 CPU 获取到的是 内存块的虚拟地址X
  2. 调用接口,将该内存块的 虚拟地址X 对应的 物理地址Y 映射为 总线地址Z 并返回给 CPU
  3. CPU 拿到的地址有 内存块 的 虚拟地址 和 总线地址,其 物理地址 对于 CPU 来说没有意义。
  4. 将 总线地址 写入 DMA 对应的寄存器,接着就可以执行相关的 DMA操作 了。

内存访问

PS:注意如果DMA的工作不是在PCI这种有规范的设备上,那么总线地址可以认为是普通内存地址

2.3 IOMMU

上面粗略讲了 IOMMU 在 DMA 工作过程中的应用,但其实 IOMMU 的用处不止这些,下面简单地描述 IOMMI 的另外一个作用。

我们都知道,在带有 MMU 的 Soc 上,对于程序来说,虚拟地址空间 是 可连续访问的
因为 MMU 帮我们完成了从 虚拟地址空间 到 物理地址空间 的映射,这样做固然对于程序来说可以大大提高 内存管理 的效率,但同时也带来了 物理内存空间碎片化 的结果,找到 可连续访问 的 物理地址空间 的难度将大大增加。
而当 Soc 上的 设备 使用 DMA 访问内存时,需要 可连续访问 的 物理地址空间

一般情况下,有 2 种办法可以让 DMA 访问 连续的物理地址空间

  1. 在初始化 内核时,将 一部分物理空间 保留下来,不进行虚拟空间的映射。当使用到 DMA 的时候,将所需要的数据放置到 内存空间。再让DMA去访问这段 物理内存。这种方法简单直接,但会使得 内存空间的使用率并不高
  2. DMA 带上 MMU,让其在访问 虚拟空间 时 自动完成虚拟地址到物理地址的映射,此时 DMA 可以在不保留 连续物理地址空间 的情况下 访问连续的虚拟空间 。

ARM 使用了第二种方法,增加了一个特殊的 MMU,即 IOMMUIOMMU 在 ARM架构 中称为 SMMUSMMU 和 MMU 一样,在配置后可以进行 translation table walk

总结 IOMMU 的 2 个用处:

  1. 映射总线地址到物理地址
  2. 提高物理内存的使用率

2.4 DMA控制器硬件

2.4.1 DMA寄存器

按照笔者理解,DMA控制器 一般都会包含以下寄存器:

  • DMA硬件描述符地址寄存器:存放 DMA描述符 的地址。
  • DMA配置寄存器:配置 DMA 的 burst 、 width 、 传输方向 等属性。
  • DMA使能寄存器:使能 DMA通道
  • DMA中断状态寄存器:获取 DMA 传输中断状态
  • DMA中断使能寄存器:使能 DMA 通道中断

2.4.2 DMA描述符

DMA控制器 在工作时需要读取 DMA描述符,这个描述符如下图所示:

image.png


一般情况下,它一共包含以下信息:

  • src_addrDMA源地址
  • dst_addrDMA目的地址
  • byte_count传输数量
  • link下一个描述符地址,如果为最后一个描述符则该值为某一个特定的值。

PS:上面的信息是指在一般情况,有些厂家会根据需要调整包含信息的内容。

需要使用 DMA控制器 进行传输时,我们需要在开辟一块内存,这块内存存放的就是 DMA描述符。当 DMA控制器 进行工作时,需要程序将 DMA描述符 的地址设置到 DMA硬件描述符地址寄存器 中。这样,当使能 DMA控制器 开始工作后,会读取 DMA硬件描述符地址寄存器 中的内存地址并读取相应的 DMA描述符,根据 DMA描述符 的所描述的地址跟大小进行传输。当完成一个 DMA描述符 的传输后会根据情况读取下一个 DMA描述符

2.4.3 LLI

上面说过驱动会创建 内存块 用于存放 DMA描述符 ,这些 内存块 我们称之为 LLILLI 全称为 Link List Item,一般在驱动代码中都可以看到其结构体。以笔者的学习代码,其代码如下,可以看到有几个成员与图中所描绘的一致:

 
  1. /*

  2. * Hardware representation of the LLI

  3. *

  4. * The hardware will be fed the physical address of this structure,

  5. * and read its content in order to start the transfer.

  6. */

  7. struct sun6i_dma_lli {

  8. u32 cfg;

  9. u32 src;

  10. u32 dst;

  11. u32 len;

  12. u32 para;

  13. u32 p_lli_next;

  14. /*

  15. * This field is not used by the DMA controller, but will be

  16. * used by the CPU to go through the list (mostly for dumping

  17. * or freeing it).

  18. */

  19. struct sun6i_dma_lli *v_lli_next;

  20. };

2.4.4 DMA request

一般情况下,当 外设驱动 准备好传输数据及任务配置后,需要向 DMA控制器 发送 DRQ信号(DMA request)。所以需要有物理线连接 DMA控制器 和 外设,这条物理线称为 DMA request line。。发送这个信号往往是向 DMA配置寄存器 中写入 DRQ值。每种 外设驱动 都有自己的 DRQ值,当启动 DMA传输 后,会查询 DRQ值,如果当前的 DRQ值 能够进行传输,则启动 DMA传输
有时 DMA request (line) 又称为 DMA port

2.4.5 DMA通道

DMA控制器 可以 同时 进行的传输个数是有限的,每一个传输都需要使用到 DMA物理通道DMA物理通道 的数量决定了 DMA控制器 能够同时传输的任务量。
在软件上,DMA控制器 会为 外设 分配一个 DMA虚拟通道,这个虚拟通道是根据 DMA request信号 来区分。
通常来讲,DMA物理通道 是 DMA控制器 提供的服务,外设通过申请 DMA通道 ,如果申请成功将返回 DMA虚拟通道,该 DMA虚拟通道 绑定了一个 DMA物理通道。这样 DMA控制器 为 外设 提供了 DMA服务,当 外设 需要传输数据时,对 虚拟通道 进行操作即可,但本质上的工作由 物理通道 来完成。

看完了这些以后,对于 DMA硬件及其工作流程 都应该有了一定的了解。

2.5 DMA驱动讲解

2.5.1 DMA设备树

下面 2 段设备树代码例程是关于 DMA控制器 和 DMA客户端 的

 
  1. /* DMA控制器设备树节点 */

  2. dma: dma-controller@01c02000 {

  3. compatible = "allwinner,sun8i-v3s-dma";

  4. reg = <0x01c02000 0x1000>;

  5. interrupts = <GIC_SPI 50 IRQ_TYPE_LEVEL_HIGH>;

  6. clocks = <&ccu CLK_BUS_DMA>;

  7. resets = <&ccu RST_BUS_DMA>;

  8. #dma-cells = <1>;

  9. };

  10. /* DMA客户端设备树节点,以SPI为例 */

  11. spi2: spi@01c6a000 {

  12. compatible = "allwinner,sun6i-a31-spi";

  13. reg = <0x01c6a000 0x1000>;

  14. interrupts = <0 67 4>;

  15. clocks = <&ahb1_gates 22>, <&spi2_clk>;

  16. clock-names = "ahb", "mod";

  17. dmas = <&dma 25>, <&dma 25>;

  18. dma-names = "rx", "tx";

  19. resets = <&ahb1_rst 22>;

  20. };

DMA控制器 的设备树节点属性我们这里不多讲,有兴趣的读者可以阅读内核文档 。
DMA客户端 我们主要关注下面 2 个属性:

  • dmas:该属性一共有 2 个,第一个 DMA控制器 的 节点名,第二个为该驱动的 DMA port
  • dma-names:该属性用于 dma_request_chan 接口,传入该接口的参数中的 name参数 需要与设备树中的 dma-names 一致,这样才能申请到 DMA通道

PS:DMA port 在每个Soc的datasheet中有说明,使用DMA时需要将DMA port设置到DMA配置寄存器中。DMA port一般如下图所示:

image.png

更多详情可以在文档 Documentation/devicetree/bindings/dma/dma.txt 中查看

2.5.2 dmaengine框架

在驱动中,有多种使用 DMA 的方式及接口框架,本文将重点说明 dmaengine框架 的代码及使用。下面按照使用流程进行描述。
重要事情说三遍:
各个平台的实现可能有所不同,代码过程仅供参考,重在学习流程及机制,下面将按照笔者手中的学习代码为例!!!!
各个平台的实现可能有所不同,代码过程仅供参考,重在学习流程及机制,下面将按照笔者手中的学习代码为例!!!!
各个平台的实现可能有所不同,代码过程仅供参考,重在学习流程及机制,下面将按照笔者手中的学习代码为例!!!!

2.5.2.1 dmaengine初始化

当 DMA控制器驱动 匹配到对应的 设备树节点 时,将调用 probe函数 对 DMA控制器 进行初始化,代码如下:

 
  1. struct sun6i_pchan {

  2. u32 idx;

  3. void __iomem *base;

  4. struct sun6i_vchan *vchan;

  5. struct sun6i_desc *desc;

  6. struct sun6i_desc *done;

  7. };

  8. struct sun6i_vchan {

  9. struct virt_dma_chan vc;

  10. struct list_head node;

  11. struct dma_slave_config cfg;

  12. struct sun6i_pchan *phy;

  13. u8 port;

  14. u8 irq_type;

  15. bool cyclic;

  16. };

  17. struct sun6i_dma_dev {

  18. struct dma_device slave;

  19. void __iomem *base;

  20. struct clk *clk;

  21. int irq;

  22. spinlock_t lock;

  23. struct reset_control *rstc;

  24. struct tasklet_struct task;

  25. atomic_t tasklet_shutdown;

  26. struct list_head pending;

  27. struct dma_pool *pool;

  28. struct sun6i_pchan *pchans;

  29. struct sun6i_vchan *vchans;

  30. const struct sun6i_dma_config *cfg;

  31. };

  32. static struct sun6i_dma_config sun8i_v3s_dma_cfg = {

  33. .nr_max_channels = 8,

  34. .nr_max_requests = 23,

  35. .nr_max_vchans = 24,

  36. .gate_needed = true,

  37. };

  38. static const struct of_device_id sun6i_dma_match[] = {

  39. { .compatible = "allwinner,sun6i-a31-dma", .data = &sun6i_a31_dma_cfg },

  40. { .compatible = "allwinner,sun8i-a23-dma", .data = &sun8i_a23_dma_cfg },

  41. { .compatible = "allwinner,sun8i-a83t-dma", .data = &sun8i_a83t_dma_cfg },

  42. { .compatible = "allwinner,sun8i-h3-dma", .data = &sun8i_h3_dma_cfg },

  43. { .compatible = "allwinner,sun8i-v3s-dma", .data = &sun8i_v3s_dma_cfg },

  44. { /* sentinel */ }

  45. };

  46. static int sun6i_dma_probe(struct platform_device *pdev)

  47. {

  48. const struct of_device_id *device;

  49. struct sun6i_dma_dev *sdc;

  50. struct resource *res;

  51. int ret, i;

  52. /* 申请dma控制器所需内存 */

  53. sdc = devm_kzalloc(&pdev->dev, sizeof(*sdc), GFP_KERNEL);

  54. /* 从设备树中获取device */

  55. device = of_match_device(sun6i_dma_match, &pdev->dev);

  56. /* device->data即为sun8i_v3s_dma_cfg */

  57. sdc->cfg = device->data;

  58. /* 获取设备树中的寄存器地址 */

  59. res = platform_get_resource(pdev, IORESOURCE_MEM, 0);

  60. /* 将寄存器地址映射为虚拟地址以供CPU使用 */

  61. sdc->base = devm_ioremap_resource(&pdev->dev, res);

  62. /* 获取DMA中断 */

  63. sdc->irq = platform_get_irq(pdev, 0);

  64. /* 获取DMA时钟 */

  65. sdc->clk = devm_clk_get(&pdev->dev, NULL);

  66. /* 创建dma内存池,以存放LLI,后续使用会从该池中取出LLI并填充 */

  67. sdc->pool = dmam_pool_create(dev_name(&pdev->dev), &pdev->dev,

  68. sizeof(struct sun6i_dma_lli), 4, 0);

  69. /*初始化DMA控制器的pending链表,所有提交的传输通道最终都会被挂在这对链表上面 */

  70. INIT_LIST_HEAD(&sdc->pending);

  71. spin_lock_init(&sdc->lock);

  72. /* 初始化DMA能力,可以在内核文档中查阅,下附文档所在目录 */

  73. dma_cap_set(DMA_PRIVATE, sdc->slave.cap_mask);

  74. dma_cap_set(DMA_MEMCPY, sdc->slave.cap_mask);

  75. dma_cap_set(DMA_SLAVE, sdc->slave.cap_mask);

  76. dma_cap_set(DMA_CYCLIC, sdc->slave.cap_mask);

  77. /* 初始化DMA控制器的struct dma_device 结构 */

  78. INIT_LIST_HEAD(&sdc->slave.channels);

  79. sdc->slave.device_free_chan_resources = sun6i_dma_free_chan_resources;

  80. sdc->slave.device_tx_status = sun6i_dma_tx_status;

  81. sdc->slave.device_issue_pending = sun6i_dma_issue_pending;

  82. sdc->slave.device_prep_slave_sg = sun6i_dma_prep_slave_sg;

  83. sdc->slave.device_prep_dma_memcpy = sun6i_dma_prep_dma_memcpy;

  84. sdc->slave.device_prep_dma_cyclic = sun6i_dma_prep_dma_cyclic;

  85. sdc->slave.copy_align = DMAENGINE_ALIGN_4_BYTES;

  86. sdc->slave.device_config = sun6i_dma_config;

  87. sdc->slave.device_pause = sun6i_dma_pause;

  88. sdc->slave.device_resume = sun6i_dma_resume;

  89. sdc->slave.device_terminate_all = sun6i_dma_terminate_all;

  90. sdc->slave.src_addr_widths = BIT(DMA_SLAVE_BUSWIDTH_1_BYTE) |

  91. BIT(DMA_SLAVE_BUSWIDTH_2_BYTES) |

  92. BIT(DMA_SLAVE_BUSWIDTH_4_BYTES);

  93. sdc->slave.dst_addr_widths = BIT(DMA_SLAVE_BUSWIDTH_1_BYTE) |

  94. BIT(DMA_SLAVE_BUSWIDTH_2_BYTES) |

  95. BIT(DMA_SLAVE_BUSWIDTH_4_BYTES);

  96. sdc->slave.directions = BIT(DMA_DEV_TO_MEM) |

  97. BIT(DMA_MEM_TO_DEV);

  98. sdc->slave.residue_granularity = DMA_RESIDUE_GRANULARITY_BURST;

  99. sdc->slave.dev = &pdev->dev;

  100. /* 申请内存存放DMA物理通道信息 */

  101. sdc->pchans = devm_kcalloc(&pdev->dev, sdc->cfg->nr_max_channels,

  102. sizeof(struct sun6i_pchan), GFP_KERNEL);

  103. /* 申请内存存放DMA虚拟通道信息 */

  104. sdc->vchans = devm_kcalloc(&pdev->dev, sdc->cfg->nr_max_vchans,

  105. sizeof(struct sun6i_vchan), GFP_KERNEL);

  106. /* 初始化tasklet队列,DMA中断任务在这个tasklet中执行 */

  107. tasklet_init(&sdc->task, sun6i_dma_tasklet, (unsigned long)sdc);

  108. /* 初始化DMA物理通道信息,包括通道号及通道所属寄存器 */

  109. for (i = 0; i < sdc->cfg->nr_max_channels; i++) {

  110. struct sun6i_pchan *pchan = &sdc->pchans[i];

  111. pchan->idx = i;

  112. pchan->base = sdc->base + 0x100 + i * 0x40;

  113. }

  114. /* 初始化DMA虚拟通道信息 */

  115. for (i = 0; i < sdc->cfg->nr_max_vchans; i++) {

  116. struct sun6i_vchan *vchan = &sdc->vchans[i];

  117. /* 初始化虚拟通道的node成员,后续工作中还会用到 */

  118. INIT_LIST_HEAD(&vchan->node);

  119. /* 注册描述符销毁回调 */

  120. vchan->vc.desc_free = sun6i_dma_free_desc;

  121. /* 初始化虚拟通道,将虚拟通道挂在到slave的channel成员上,以后就可以直接通过dma_device找到所有的虚拟通道 */

  122. vchan_init(&vchan->vc, &sdc->slave);

  123. }

  124. /* 注册DMA中断 */

  125. ret = devm_request_irq(&pdev->dev, sdc->irq, sun6i_dma_interrupt, 0,

  126. dev_name(&pdev->dev), sdc);

  127. /* 注册DMA控制器到 dmaengine*/

  128. ret = dma_async_device_register(&sdc->slave);

  129. /* 注册DMA回调sun6i_dma_of_xlate到DMA控制器,该回调会在申请DMA通道时用到 */

  130. ret = of_dma_controller_register(pdev->dev.of_node, sun6i_dma_of_xlate, sdc);

  131. }

  132. void vchan_init(struct virt_dma_chan *vc, struct dma_device *dmadev)

  133. {

  134. spin_lock_init(&vc->lock);

  135. /* 初始化虚拟通道的各个工作链表,每个链表代表节点的状态。虚拟通道的每个传输任务将会在这些链表上流转,以执行不同状态时所需要的操作 */

  136. INIT_LIST_HEAD(&vc->desc_allocated);

  137. INIT_LIST_HEAD(&vc->desc_submitted);

  138. INIT_LIST_HEAD(&vc->desc_issued);

  139. INIT_LIST_HEAD(&vc->desc_completed);

  140. /* 初始化虚拟通道的tasklet,每当虚拟通道完成一次传输任务,就会调用一次vchan_complete */

  141. tasklet_init(&vc->task, vchan_complete, (unsigned long)vc);

  142. vc->chan.device = dmadev;

  143. /* 将虚拟通道挂到dma_device的channels上 */

  144. list_add_tail(&vc->chan.device_node, &dmadev->channels);

  145. }

PS:DMA能力详情可以在文档中 Documentation/dmaengine/provider.txt 中查阅。

2.5.2.2 申请DMA通道

进行 DMA操作 首先需要申请 DMA通道,其接口原型为:

struct dma_chan *dma_request_chan(struct device *dev, const char *name);
  • dev:设备的 device成员
  • name:设备树 dma-names 属性值
    先看看 dma_request_chan 的调用图谱

 
  1. dma_request_chan

  2. ->of_dma_request_slave_channel(请求到对应的channel)

  3. ->of_property_count_strings(找到设备树中dma-names属性的值的个数)

  4. ->of_dma_match_channel(根据dma-names值的个数,为每个值找到该值对应的phandle,即dma_spec)

  5. ->of_dma_find_controller(根据phandle找到对应的dma controller)

  6. ->sun6i_dma_of_xlate(调用of_dma_xlate回调, dma_spec中包含该设备所在的DMA port,据此找到一个可用的dma_chan并返回)

  7. ->dma_get_any_slave_channel

下面是各个调用层次的简化代码及讲述:

 
  1. struct dma_chan *dma_request_chan(struct device *dev, const char *name)

  2. {

  3. struct dma_device *d, *_d;

  4. struct dma_chan *chan = NULL;

  5. /* 根据设备树节点属性获取DMA虚拟通道 */

  6. if (dev->of_node)

  7. chan = of_dma_request_slave_channel(dev->of_node, name);

  8. ......

  9. return chan ? chan : ERR_PTR(-EPROBE_DEFER);

  10. }

  11. struct dma_chan *of_dma_request_slave_channel(struct device_node *np,

  12. const char *name)

  13. {

  14. struct of_phandle_args dma_spec;

  15. struct of_dma *ofdma;

  16. struct dma_chan *chan;

  17. int count, i, start;

  18. int ret_no_channel = -ENODEV;

  19. static atomic_t last_index;

  20. /* 判断设备的设备树节点属性是否有 dmas 属性 */

  21. if (!of_find_property(np, "dmas", NULL))

  22. return ERR_PTR(-ENODEV);

  23. /* 获取设备树节点的 dma-names属性值 的个数 */

  24. count = of_property_count_strings(np, "dma-names");

  25. start = atomic_inc_return(&last_index);

  26. /*

  27. 根据属性值的个数进行遍历,因为有多少个属性值就说明有该设备需要多少个DMA通道 。

  28. 也就是说申请的通道只在设备树节点的描述范围内

  29. */

  30. for (i = 0; i < count; i++) {

  31. /*

  32. dma_spec是设备节点dmas属性的struct of_phandle_args结构体,

  33. 该函数根据设备树节点和通道名找到对应的 struct of_phandle_args结构体

  34. */

  35. if (of_dma_match_channel(np, name,

  36. (i + start) % count,

  37. &dma_spec))

  38. continue;

  39. /*

  40. 根据dma_spec找到DMA控制器的struct of_dma结构体,

  41. 在设备树中dmas描述了DMA控制器节点的信息,所以可以获取到DMA控制器的信息

  42. */

  43. ofdma = of_dma_find_controller(&dma_spec);

  44. if (ofdma) {

  45. /*

  46. 如果结构体有效则调用of_dma_xlate回调,

  47. 可以根据probe函数得知,该回调为sun6i_dma_of_xlate

  48. 使用该回调找到对应的DMA通道并返回

  49. */

  50. chan = ofdma->of_dma_xlate(&dma_spec, ofdma);

  51. } else {

  52. ret_no_channel = -EPROBE_DEFER;

  53. chan = NULL;

  54. }

  55. if (chan)

  56. return chan;

  57. }

  58. return ERR_PTR(ret_no_channel);

  59. }

  60. static int of_dma_match_channel(struct device_node *np, const char *name,

  61. int index, struct of_phandle_args *dma_spec)

  62. {

  63. const char *s;

  64. /* 根据index(即属性下标)读取dma-names属性中的值 */

  65. if (of_property_read_string_index(np, "dma-names", index, &s))

  66. return -ENODEV;

  67. /* 比较传入的 name 是否和设备树中所描述的一致 */

  68. if (strcmp(name, s))

  69. return -ENODEV;

  70. /*

  71. 如果一致则使用np,index来找到dma控制器的struct of_phandle_args结构体,

  72. 并把值赋给dma_spec用于返回

  73. */

  74. if (of_parse_phandle_with_args(np, "dmas", "#dma-cells", index,

  75. dma_spec))

  76. return -ENODEV;

  77. return 0;

  78. }

  79. static struct dma_chan *sun6i_dma_of_xlate(struct of_phandle_args *dma_spec,

  80. struct of_dma *ofdma)

  81. {

  82. /* 找到Soc的DMA控制器描述结构体sdev */

  83. struct sun6i_dma_dev *sdev = ofdma->of_dma_data;

  84. struct sun6i_vchan *vchan;

  85. struct dma_chan *chan;

  86. u8 port = dma_spec->args[0];

  87. /* 判断sdev的nr_max_requests是否有效 */

  88. if (port > sdev->cfg->nr_max_requests)

  89. return NULL;

  90. /* 判断sdev的nr_max_requests是否有效 */

  91. chan = dma_get_any_slave_channel(&sdev->slave);

  92. if (!chan)

  93. return NULL;

  94. /*

  95. 获取chan所在的struct sun6i_vchan结构体,并设置其DRQ信号值,

  96. DRQ信号值是使用设备树中的dmas属性来描述的,所以port的值为dma_spec的arg[0]成员

  97. */

  98. vchan = to_sun6i_vchan(chan);

  99. vchan->port = port;

  100. return chan;

  101. }

  102. struct dma_chan *dma_get_any_slave_channel(struct dma_device *device)

  103. {

  104. dma_cap_mask_t mask;

  105. struct dma_chan *chan;

  106. dma_cap_zero(mask);

  107. dma_cap_set(DMA_SLAVE, mask);

  108. chan = find_candidate(device, &mask, NULL, NULL);

  109. return IS_ERR(chan) ? NULL : chan;

  110. }

  111. static struct dma_chan *find_candidate(struct dma_device *device,

  112. const dma_cap_mask_t *mask,

  113. dma_filter_fn fn, void *fn_param)

  114. {

  115. struct dma_chan *chan = private_candidate(mask, device, fn, fn_param);

  116. int err;

  117. if (chan) {

  118. /* Found a suitable channel, try to grab, prep, and return it.

  119. * We first set DMA_PRIVATE to disable balance_ref_count as this

  120. * channel will not be published in the general-purpose

  121. * allocator

  122. */

  123. dma_cap_set(DMA_PRIVATE, device->cap_mask);

  124. device->privatecnt++;

  125. err = dma_chan_get(chan);

  126. if (err) {

  127. if (err == -ENODEV) {

  128. dev_dbg(device->dev, "%s: %s module removed\n",

  129. __func__, dma_chan_name(chan));

  130. list_del_rcu(&device->global_node);

  131. } else

  132. dev_dbg(device->dev,

  133. "%s: failed to get %s: (%d)\n",

  134. __func__, dma_chan_name(chan), err);

  135. if (--device->privatecnt == 0)

  136. dma_cap_clear(DMA_PRIVATE, device->cap_mask);

  137. chan = ERR_PTR(err);

  138. }

  139. }

  140. return chan ? chan : ERR_PTR(-EPROBE_DEFER);

  141. }

  142. static struct dma_chan *private_candidate(const dma_cap_mask_t *mask,

  143. struct dma_device *dev,

  144. dma_filter_fn fn, void *fn_param)

  145. {

  146. struct dma_chan *chan;

  147. /*

  148. 在vchan_init中,我们已经将多个vchan挂在了dma_device的channel链表上 、

  149. 这里遍历所有vchan的chan成员,并判断其client_count,

  150. 如果已经被申请了(client_count不为0),则遍历下一个。

  151. */

  152. list_for_each_entry(chan, &dev->channels, device_node) {

  153. if (chan->client_count) {

  154. dev_dbg(dev->dev, "%s: %s busy\n",

  155. __func__, dma_chan_name(chan));

  156. continue;

  157. }

  158. ......

  159. /* 如果该chan没有被申请,则返回 */

  160. return chan;

  161. }

  162. return NULL;

  163. }

2.5.2.3 配置DMA通道的参数

申请完 DMA通道 后则需要对其进行配置,其接口原型为:

static inline int dmaengine_slave_config(struct dma_chan *chan, struct dma_slave_config *config)
  • chanDMA通道 描述结构体指针
  • configDMA通道配置 结构体指针

这里需要说名一下 *struct dma_slave_config config 结构体,如下所示:

 
  1. struct dma_slave_config {

  2. enum dma_transfer_direction direction;

  3. phys_addr_t src_addr;

  4. phys_addr_t dst_addr;

  5. enum dma_slave_buswidth src_addr_width;

  6. enum dma_slave_buswidth dst_addr_width;

  7. u32 src_maxburst;

  8. u32 dst_maxburst;

  9. u32 src_port_window_size;

  10. u32 dst_port_window_size;

  11. bool device_fc;

  12. unsigned int slave_id;

  13. };

其重要成员如下:

  • direction:配置 DMA通道 的 传输方向,包括:
    DMA_MEM_TO_MEM(内存到内存)
    DMA_MEM_TO_DEV(内存到设备)
    DMA_DEV_TO_MEM(设备到内存)
    DMA_DEV_TO_DEV(设备到设备)
  • src_addr传输方向 为 DMA_DEV_TO_MEM 或 DMA_DEV_TO_DEV 时,读取数据的地址(通常外设的fifo寄存器地址)。对 DMA_MEM_TO_DEV 的 DMA通道,不需配置该参数。
  • dst_addr传输方向 为 DMA_MEM_TO_DEV 或 DMA_DEV_TO_DEV时,写入数据的地址(通常外设的fifo寄存器地址)。对 DMA_DEV_TO_MEM 类型的 DMA通道,不需配置该参数。
    slave_id:在有些情况下,驱动需要告诉 DMA控制器 自己的 外设类型,该成员的作用取决于 DMA控制器的实现

PS:对于未说明到的成员,感兴趣的读者可以前往内核源码查看结构体说明

先看看 dmaengine_slave_config 的调用图谱

 
  1. dmaengine_slave_config

  2. ->device_config(执行sun6i_dma_config回调)

可以看到其调用图谱比较简单, sun6i_dma_config 已经在 probe函数 初始化时注册了,可以回顾前面的代码进行了解。
下面的简化diam

 
  1. static int sun6i_dma_config(struct dma_chan *chan,

  2. struct dma_slave_config *config)

  3. {

  4. struct sun6i_vchan *vchan = to_sun6i_vchan(chan);

  5. /* 拷贝config到vchan(虚拟通道)的cfg成员,该成员会在后面使用到 */

  6. memcpy(&vchan->cfg, config, sizeof(*config));

  7. return 0;

  8. }

2.5.2.4 获取通道的传输描述符

使用 DMA 可能存在多种需求不同的场景,针对这些场景 dmaengine 给出了多种不同的 传输描述符,常用的接口如下:

  • dmaengine_prep_slave_sg:执行 scatter gather传输 ,即 分散/聚合传输
  • dmaengine_prep_dma_cyclic:循环执行 DMA操作,直到操作明确停止为止。常用语音频驱动
  • dmaengine_prep_interleaved_dma:执行 不连续的、交叉的DMA传输,通常用在图像处理、显示等场景中。
  • dmaengine_prep_slave_single:执行一次 DMA传输,与 dmaengine_prep_slave_sg 相比不需要连续传输多次。
  • dmaengine_prep_dma_memcpy:执行内存拷贝传输,即 内存到内存的DMA传输

蜗窝科技的描述非常形象的描述了 分散/聚合传输,如下:
一般情况下,DMA传输一般只能处理在物理上连续的buffer。但在有些场景下,我们需要将一些非连续的buffer拷贝到一个连续buffer中。
对于这种非连续的传输,大多时候都是通过软件,将传输分成多个连续的小块(chunk)。但为了提高传输效率(特别是在图像、视频等场景中),有些DMA controller从硬件上支持了这种操作。

具体各个函数的作用可以参考文档 Documentation/dmaengine/client.txt
下面

下面以 dmaengine_prep_dma_memcpy 为例来说明代码,首先看一下调用图谱

 
  1. dmaengine_prep_dma_memcpy

  2. ->device_prep_dma_memcpy(即sun6i_dma_prep_dma_memcpy回调)

代码及讲述如下:

 
  1. static struct dma_async_tx_descriptor *sun6i_dma_prep_dma_memcpy(

  2. struct dma_chan *chan, dma_addr_t dest, dma_addr_t src,

  3. size_t len, unsigned long flags)

  4. {

  5. /* 获取DMA控制器sdev */

  6. struct sun6i_dma_dev *sdev = to_sun6i_dma_dev(chan->device);

  7. /* 获取chan成员所在的vchan结构体 */

  8. struct sun6i_vchan *vchan = to_sun6i_vchan(chan);

  9. struct sun6i_dma_lli *v_lli;

  10. struct sun6i_desc *txd;

  11. dma_addr_t p_lli;

  12. s8 burst, width;

  13. /* 创建传输描述符txd */

  14. txd = kzalloc(sizeof(*txd), GFP_NOWAIT);

  15. /* 从dma内存池中获取一个lli结构体内存,lli的物理地址为p_lli,虚拟地址为v_lli */

  16. v_lli = dma_pool_alloc(sdev->pool, GFP_NOWAIT, &p_lli);

  17. /* 使用虚拟地址初始化lli */

  18. v_lli->src = src;

  19. v_lli->dst = dest;

  20. v_lli->len = len;

  21. v_lli->para = NORMAL_WAIT;

  22. /*

  23. 设置传输通道的burst和width属性。

  24. 这里需要注意,一般情况下会使用dmaengine_slave_config传进来的配置,

  25. 但在memcpy类型中,配置是固定的,所以不需要进行配置。

  26. 其他类型的传输就需要使用configs进行配置

  27. */

  28. burst = convert_burst(8);

  29. width = convert_buswidth(DMA_SLAVE_BUSWIDTH_4_BYTES);

  30. v_lli->cfg = DMA_CHAN_CFG_SRC_DRQ(DRQ_SDRAM) |

  31. DMA_CHAN_CFG_DST_DRQ(DRQ_SDRAM) |

  32. DMA_CHAN_CFG_DST_LINEAR_MODE |

  33. DMA_CHAN_CFG_SRC_LINEAR_MODE |

  34. DMA_CHAN_CFG_SRC_BURST(burst) |

  35. DMA_CHAN_CFG_SRC_WIDTH(width) |

  36. DMA_CHAN_CFG_DST_BURST(burst) |

  37. DMA_CHAN_CFG_DST_WIDTH(width);

  38. /* 将lli链入传输描述符的v_lli和p_lli成员,形成v_lli链表和p_lli链表 */

  39. sun6i_dma_lli_add(NULL, v_lli, p_lli, txd);

  40. /* 初始化传输描述符并返回 */

  41. return vchan_tx_prep(&vchan->vc, &txd->vd, flags);

  42. }

  43. static void *sun6i_dma_lli_add(struct sun6i_dma_lli *prev,

  44. struct sun6i_dma_lli *next,

  45. dma_addr_t next_phy,

  46. struct sun6i_desc *txd)

  47. {

  48. /* 如果prev为空则说明当前是空链表,链入传输描述符的p_lli成员和v_lli成员 */

  49. if (!prev) {

  50. txd->p_lli = next_phy;

  51. txd->v_lli = next;

  52. } else {

  53. /* 如果不是则链入指定位置,该位置在prev之后,next为需要链入的lli */

  54. prev->p_lli_next = next_phy;

  55. prev->v_lli_next = next;

  56. }

  57. /* 设置最后一个lli的next成员为指定值,说明该lli是最后一个 */

  58. next->p_lli_next = LLI_LAST_ITEM;

  59. next->v_lli_next = NULL;

  60. return next;

  61. }

  62. static inline struct dma_async_tx_descriptor *vchan_tx_prep(struct virt_dma_chan *vc,

  63. struct virt_dma_desc *vd, unsigned long tx_flags)

  64. {

  65. unsigned long flags;

  66. /* 初始化描述符,将vc->chan的值赋给vd->tx->chan */

  67. dma_async_tx_descriptor_init(&vd->tx, &vc->chan);

  68. /* 对vd->tx的各个值进行赋值 */

  69. vd->tx.flags = tx_flags;

  70. vd->tx.tx_submit = vchan_tx_submit;

  71. /* vchan_tx_desc_free回答会在销毁传输描述符vd时使用,因为vd是使用alloc创建的 */

  72. vd->tx.desc_free = vchan_tx_desc_free;

  73. spin_lock_irqsave(&vc->lock, flags);

  74. /* 将传输描述符加入通道的desc_allocated链表 */

  75. list_add_tail(&vd->node, &vc->desc_allocated);

  76. spin_unlock_irqrestore(&vc->lock, flags);

  77. return &vd->tx;

  78. }

2.5.2.5 提交传输请求

在获取完 DMA描述符 后,就可提交传输任务了,接口原型如下:

dma_cookie_t dmaengine_submit(struct dma_async_tx_descriptor *desc)
  • desc:申请的DMA传输描述符

其调用图谱如下:

 
  1. dmaengine_submit

  2. ->tx_submit(即vchan_tx_submit回调)

回顾 申请传输描述符 ,在申请前需要对 DMA描述符 进行初始化,初始化的时候就注册了回调 vchan_tx_submit

函数描述如下:

 
  1. static inline dma_cookie_t dmaengine_submit(struct dma_async_tx_descriptor *desc)

  2. {

  3. return desc->tx_submit(desc);

  4. }

  5. dma_cookie_t vchan_tx_submit(struct dma_async_tx_descriptor *tx)

  6. {

  7. /* 获取DMA虚拟通道 */

  8. struct virt_dma_chan *vc = to_virt_chan(tx->chan);

  9. /* 获取虚拟通道的DMA */

  10. struct virt_dma_desc *vd = to_virt_desc(tx);

  11. unsigned long flags;

  12. dma_cookie_t cookie;

  13. spin_lock_irqsave(&vc->lock, flags);

  14. /* 为传输描述符分配cookie */

  15. cookie = dma_cookie_assign(tx);

  16. /* 将传输描述符转移到虚拟通道的desc_submitted链表上 */

  17. list_move_tail(&vd->node, &vc->desc_submitted);

  18. spin_unlock_irqrestore(&vc->lock, flags);

  19. dev_dbg(vc->chan.device->dev, "vchan %p: txd %p[%x]: submitted\n",

  20. vc, vd, cookie);

  21. /* 返回cookie */

  22. return cookie;

  23. }

2.5.2.6 启动传输

做好上面的准备工作并提交好传输任务后即可进行传输了。接口原型如下:

 
  1. static inline void dma_async_issue_pending(struct dma_chan *chan)

  2. {

  3. chan->device->device_issue_pending(chan);

  4. }

  • chan:启动传输的 DMA通道

根据上面所说,提交好的传输任务都挂在了 DMA通道 的 desc_submitted 链表上。启动传输后,dmaengine 会在该 DMA虚体通道 上获取 传输任务(传输描述符),并根据所配置的信息启动传输。

其调用图谱如下:

 
  1. dma_async_issue_pending

  2. ->device_issue_pending(即sun6i_dma_issue_pending回调)

代码描述如下:

 
  1. static inline void dma_async_issue_pending(struct dma_chan *chan)

  2. {

  3. chan->device->device_issue_pending(chan);

  4. }

  5. static void sun6i_dma_issue_pending(struct dma_chan *chan)

  6. {

  7. /* 获取DMA控制器sdev */

  8. struct sun6i_dma_dev *sdev = to_sun6i_dma_dev(chan->device);

  9. /* 获取DMA虚拟通道 */

  10. struct sun6i_vchan *vchan = to_sun6i_vchan(chan);

  11. unsigned long flags;

  12. spin_lock_irqsave(&vchan->vc.lock, flags);

  13. /* 将所有在desc_submitted链表上的传输描述符转移到desc_issued链表 */

  14. if (vchan_issue_pending(&vchan->vc)) {

  15. spin_lock(&sdev->lock);

  16. /* 如果当前虚拟通道已经绑定了物理通道,

  17. 且虚拟通道vchan没有挂载到DMA控制器sdev的pending链表上。

  18. 则将虚拟通道vchan挂载到pending链表上,并调度执行tasklet。

  19. 虚拟通道vchan就是我们需要启动传输的通道

  20. */

  21. if (!vchan->phy && list_empty(&vchan->node)) {

  22. list_add_tail(&vchan->node, &sdev->pending);

  23. tasklet_schedule(&sdev->task);

  24. dev_dbg(chan2dev(chan), "vchan %p: issued\n",

  25. &vchan->vc);

  26. }

  27. spin_unlock(&sdev->lock);

  28. } else {

  29. dev_dbg(chan2dev(chan), "vchan %p: nothing to issue\n",

  30. &vchan->vc);

  31. }

  32. spin_unlock_irqrestore(&vchan->vc.lock, flags);

  33. }

  34. static inline bool vchan_issue_pending(struct virt_dma_chan *vc)

  35. {

  36. /* 将所有在desc_submitted链表上的传输描述符转移到desc_issued链表 */

  37. list_splice_tail_init(&vc->desc_submitted, &vc->desc_issued);

  38. return !list_empty(&vc->desc_issued);

  39. }

2.5.2.7 传输tasklet

使用 dma_async_issue_pending 启动传输后,真正执行传输任务的是 tasklet。其执行函数在 probe函数 初始化时已经注册,就是 sun6i_dma_tasklet

 
  1. static void sun6i_dma_tasklet(unsigned long data)

  2. {

  3. /* 获取DMA控制器sdev */

  4. struct sun6i_dma_dev *sdev = (struct sun6i_dma_dev *)data;

  5. /* DMA控制器配置 */

  6. const struct sun6i_dma_config *cfg = sdev->cfg;

  7. struct sun6i_vchan *vchan;

  8. struct sun6i_pchan *pchan;

  9. unsigned int pchan_alloc = 0;

  10. unsigned int pchan_idx;

  11. /* DMA controller的channel链表上的vchan进行遍历 */

  12. list_for_each_entry(vchan, &sdev->slave.channels, vc.chan.device_node) {

  13. spin_lock_irq(&vchan->vc.lock);

  14. pchan = vchan->phy;

  15. /* 如果vchan具有pchan并且该pchan处于工作状态 */

  16. if (pchan && pchan->done) {

  17. /* 尝试启动该vchan上的传输sun6i_dma_start_desc */

  18. if (sun6i_dma_start_desc(vchan)) {

  19. /*

  20. 如果返回非0值,则说明当前遍历到的vchan已经完成了所有的传输任务 。

  21. 按照笔者理解,这个循环应该是释放掉所有完成任务的pchan,方便分配给即将需要传输的vchan

  22. */

  23. dev_dbg(sdev->slave.dev, "pchan %u: free\n",

  24. pchan->idx);

  25. /* Mark this channel free */

  26. vchan->phy = NULL;

  27. pchan->vchan = NULL;

  28. }

  29. }

  30. spin_unlock_irq(&vchan->vc.lock);

  31. }

  32. spin_lock_irq(&sdev->lock);

  33. /* 遍历所有pchan,找到空闲的pchan并绑定到需要进行传输的vchan上 */

  34. for (pchan_idx = 0; pchan_idx < cfg->nr_max_channels; pchan_idx++) {

  35. pchan = &sdev->pchans[pchan_idx];

  36. /*

  37. 如果pchan的vchan成员不为NULL,则说明该pchan已经被占用。继续下一个ptchan

  38. 如果sdev的pending链表为空,则说明当前DMA控制器没有需要进行传输的通道,继续遍历。按照笔者理解,这里应该可以直接返回

  39. */

  40. if (pchan->vchan || list_empty(&sdev->pending))

  41. continue;

  42. /*

  43. 如果pchan的vchan成员为空,则说明该pchan是空闲的,可以使用。

  44. 取出DMA控制器pengding链表上需要进行传输的第一个虚拟通道vchan

  45. */

  46. vchan = list_first_entry(&sdev->pending,

  47. struct sun6i_vchan, node);

  48. /* 并且将将虚拟通道从pengding链表上移除 */

  49. list_del_init(&vchan->node);

  50. /* 将当前的pchan申请情况记录下来 */

  51. pchan_alloc |= BIT(pchan_idx);

  52. /* 绑定pchan和vchan,防止pchan被其他虚拟通道申请去 */

  53. pchan->vchan = vchan;

  54. vchan->phy = pchan;

  55. dev_dbg(sdev->slave.dev, "pchan %u: alloc vchan %p\n",

  56. pchan->idx, &vchan->vc);

  57. }

  58. spin_unlock_irq(&sdev->lock);

  59. /* 遍历所有pchan */

  60. for (pchan_idx = 0; pchan_idx < cfg->nr_max_channels; pchan_idx++) {

  61. /* 如果当前pchan已经没被申请,即为空闲pchan,则遍历下一个pchan */

  62. if (!(pchan_alloc & BIT(pchan_idx)))

  63. continue;

  64. /* 当前pchan已经被申请,是在工作中。获取该pchan以及其绑定的vchan */

  65. pchan = sdev->pchans + pchan_idx;

  66. vchan = pchan->vchan;

  67. /* 如果pchan的vchan成员有效,则说明该通道有需要进行传输的任务 */

  68. if (vchan) {

  69. spin_lock_irq(&vchan->vc.lock);

  70. /* 进行传输 */

  71. sun6i_dma_start_desc(vchan);

  72. spin_unlock_irq(&vchan->vc.lock);

  73. }

  74. }

  75. }

  76. static int sun6i_dma_start_desc(struct sun6i_vchan *vchan)

  77. {

  78. /* 获取DMA控制器 */

  79. struct sun6i_dma_dev *sdev = to_sun6i_dma_dev(vchan->vc.chan.device);

  80. /* 弹出DMA虚拟通道上的传输描述符 */

  81. struct virt_dma_desc *desc = vchan_next_desc(&vchan->vc);

  82. struct sun6i_pchan *pchan = vchan->phy;

  83. u32 irq_val, irq_reg, irq_offset;

  84. if (!pchan)

  85. return -EAGAIN;

  86. /* 如果传输描述符为空,则说明当前通道没有需要进行传输的任务 */

  87. if (!desc) {

  88. pchan->desc = NULL;

  89. pchan->done = NULL;

  90. return -EAGAIN;

  91. }

  92. /*

  93. 经过前面的流程,当前传输描述符已经在desc_issued链表上。

  94. 现在将传输描述符从 desc_issued链表上删除。

  95. */

  96. list_del(&desc->node);

  97. /*

  98. 获取传输描述符的lli。

  99. 启动DMA中断。

  100. 并将lli写入控制寄存器。

  101. 启动传输

  102. */

  103. pchan->desc = to_sun6i_desc(&desc->tx);

  104. pchan->done = NULL;

  105. irq_reg = pchan->idx / DMA_IRQ_CHAN_NR;

  106. irq_offset = pchan->idx % DMA_IRQ_CHAN_NR;

  107. vchan->irq_type = vchan->cyclic ? DMA_IRQ_PKG : DMA_IRQ_QUEUE;

  108. irq_val = readl(sdev->base + DMA_IRQ_EN(irq_reg));

  109. irq_val &= ~((DMA_IRQ_HALF | DMA_IRQ_PKG | DMA_IRQ_QUEUE) <<

  110. (irq_offset * DMA_IRQ_CHAN_WIDTH));

  111. irq_val |= vchan->irq_type << (irq_offset * DMA_IRQ_CHAN_WIDTH);

  112. writel(irq_val, sdev->base + DMA_IRQ_EN(irq_reg));

  113. writel(pchan->desc->p_lli, pchan->base + DMA_CHAN_LLI_ADDR);

  114. writel(DMA_CHAN_ENABLE_START, pchan->base + DMA_CHAN_ENABLE);

  115. return 0;

  116. }

总结一下tasklet任务:tasklet对每一个vchan进行遍历,如果当前vchan有传输任务但有分配pchan,则启动传输任务。如果没有pchan,则找出一个空闲pchan并分配给给vchan,并启动传输任务。

2.5.2.8 传输完成

当完成一次 传输任务后,DMA控制器硬件 会出发中断,执行中断处理函数

 
  1. static irqreturn_t sun6i_dma_interrupt(int irq, void *dev_id)

  2. {

  3. struct sun6i_dma_dev *sdev = dev_id;

  4. struct sun6i_vchan *vchan;

  5. struct sun6i_pchan *pchan;

  6. int i, j, ret = IRQ_NONE;

  7. u32 status;

  8. /* 遍历所有的物理通道的中断标志位 */

  9. for (i = 0; i < sdev->cfg->nr_max_channels / DMA_IRQ_CHAN_NR; i++) {

  10. /* 如果当前的物理通道的标志位无效,则说明不是该通道完成,继续遍历下一个 */

  11. status = readl(sdev->base + DMA_IRQ_STAT(i));

  12. if (!status)

  13. continue;

  14. /* 如果当前的物理通道的标志位有效,则进行中断应答,将标志位置0 */

  15. writel(status, sdev->base + DMA_IRQ_STAT(i));

  16. for (j = 0; (j < DMA_IRQ_CHAN_NR) && status; j++) {

  17. /* 获取触发中断的DMA物理通道及其虚拟通道 */

  18. pchan = sdev->pchans + j;

  19. vchan = pchan->vchan;

  20. /* 如果vchan有效,则可以进行收尾工作 */

  21. if (vchan && (status & vchan->irq_type)) {

  22. if (vchan->cyclic) {

  23. /* 如果当前通道的传输类型为cyclic类型,则调用其回调,在本例中不是该类型 */

  24. vchan_cyclic_callback(&pchan->desc->vd);

  25. } else {

  26. /*

  27. 如果当前通道的传输类型为不cyclic类型,

  28. 则设置传输描述符的cookiet,通知驱动传输完成

  29. */

  30. spin_lock(&vchan->vc.lock);

  31. vchan_cookie_complete(&pchan->desc->vd);

  32. pchan->done = pchan->desc;

  33. spin_unlock(&vchan->vc.lock);

  34. }

  35. }

  36. status = status >> DMA_IRQ_CHAN_WIDTH;

  37. }

  38. /* 如果DMA传输没有被取消,则再执行一次tasklet,完成下一个传输描述符 */

  39. if (!atomic_read(&sdev->tasklet_shutdown))

  40. tasklet_schedule(&sdev->task);

  41. ret = IRQ_HANDLED;

  42. }

  43. return ret;

  44. }

  45. static inline void vchan_cookie_complete(struct virt_dma_desc *vd)

  46. {

  47. /* 获取完成传输的虚拟通道 */

  48. struct virt_dma_chan *vc = to_virt_chan(vd->tx.chan);

  49. dma_cookie_t cookie;

  50. cookie = vd->tx.cookie;

  51. dma_cookie_complete(&vd->tx);

  52. dev_vdbg(vc->chan.device->dev, "txd %p[%x]: marked complete\n",

  53. vd, cookie);

  54. /*

  55. 前面说了传输描述符已经从desc_issue链表上删除掉了,所以现在它不属于任何链表

  56. 将传输描述符从添加到desc_completed链表

  57. */

  58. list_add_tail(&vd->node, &vc->desc_completed);

  59. /*

  60. 在vchan_init中我们提到过会给虚拟通道注册一个tasklet,其执行函数为vchan_complete

  61. 调用虚拟通道的tasklet。

  62. */

  63. tasklet_schedule(&vc->task);

  64. }

  65. static void vchan_complete(unsigned long arg)

  66. {

  67. struct virt_dma_chan *vc = (struct virt_dma_chan *)arg;

  68. struct virt_dma_desc *vd, *_vd;

  69. struct dmaengine_desc_callback cb;

  70. LIST_HEAD(head);

  71. /* 将desc_completed链表上的所有传输描述符移动到head链表上 */

  72. list_splice_tail_init(&vc->desc_completed, &head);

  73. ......

  74. /* 遍历当前head上的所有传输描述符 */

  75. list_for_each_entry_safe(vd, _vd, &head, node) {

  76. /* 将传输描述符去head对链表上移除 */

  77. list_del(&vd->node);

  78. /* 如果当前传输描述符不需要重复使用,则调用 */

  79. if (dmaengine_desc_test_reuse(&vd->tx))

  80. list_add(&vd->node, &vc->desc_allocated);

  81. else

  82. /*

  83. 如果当前传输描述符不需要重复使用,则调用desc_free回调

  84. desc_free回到是在probe函数中注册的,是sun6i_dma_free_desc函数

  85. 很明显,是用于销毁传输描述符的,以为传输描述符是从dma_pool中申请的,用完要还回去

  86. */

  87. vc->desc_free(vd);

  88. }

  89. }

  90. static void sun6i_dma_free_desc(struct virt_dma_desc *vd)

  91. {

  92. struct sun6i_desc *txd = to_sun6i_desc(&vd->tx);

  93. struct sun6i_dma_dev *sdev = to_sun6i_dma_dev(vd->tx.chan->device);

  94. struct sun6i_dma_lli *v_lli, *v_next;

  95. dma_addr_t p_lli, p_next;

  96. if (unlikely(!txd))

  97. return;

  98. p_lli = txd->p_lli;

  99. v_lli = txd->v_lli;

  100. while (v_lli) {

  101. v_next = v_lli->v_lli_next;

  102. p_next = v_lli->p_lli_next;

  103. /* 归还传输描述符 */

  104. dma_pool_free(sdev->pool, v_lli, p_lli);

  105. v_lli = v_next;

  106. p_lli = p_next;

  107. }

  108. kfree(txd);

  109. }

总结一下中断:中断遍历所有的物理通道,并找出触发中断的物理通道。对该物理通道上的传输描述符进行销毁,并再继续下一个传输任务

DMA传输描述符 的销毁流程如下:

 
  1. sun6i_dma_interrupt

  2. ->vchan_cookie_complete

  3. ->vchan_complete

  4. ->sun6i_dma_free_desc

最后再总结一下 DMA传输符 的流转过程:

流转过程

2.5.3 DMA映射

了解完 dmaengine框架 后,对于其工作机制及原理有了一定的了解。现在还有一个问题,传输用的内存空间从哪里来?有没有什么限制。本节将为读者讲述 DMA缓冲区 。

DMA缓冲区 用于存放 读取/写入 的数据,DMA控制器 一般支持多种类型的缓冲区,常见的有 单一缓冲区(sigle) 和 分散/聚合缓冲区(scatter gather/sg)

  • sigle类型:一块连续可访问的缓冲区
  • sg类型:多块离散的可访问缓冲区,将它们串成链表进行操作

前面我们说了,DMA缓冲区 地址需要完成从 虚拟地址(一般情况下) 到 总线地址 的映射。
为什么需要进行映射呢?

  • 因为 DMA控制器 未必是使用 虚拟地址 或 物理地址,常用的是 总线地址
  • DMA硬件 和 CPU 存在内存的一致性问题。因为 DMA 是脱离 CPU 对内存进行访问的,所以 DMA 有可能访问到的是 脏数据。所以需要对缓冲区进行处理(有关内存/cache一致性的问题欢迎阅读笔者前面的文章)

DMA映射 分为 一致性映射 和 流式映射,下面均以 sigle类型缓冲区 进行举例说明。

2.5.3.1 一致性映射

一致性映射 是使用专门的接口分配一块 DMA缓冲区,这块 DMA缓冲区 是关闭了 cache机制 的。也就是数据直接写入内存,这样就不存在一致性问题。此类接口分配的缓冲区没有 sg类型,可以理解为单纯的一块缓冲区。

下面我们看一下接口原型:

 
  1. /* 该接口可以在中断上下文调用 */

  2. void *dma_alloc_coherent(struct device *dev, size_t size,dma_addr_t *dma_handle, gfp_t flag)

dev:该参数是设备的 struct device对象

  • size 该参数指明 DMA缓冲区 的大小,单位为 byte
    flag:该参数可以传入 GFP_ATOMIC标记,此参数为 内存分配 的 flag,接口不使用该参数,但透传参数到 内存管理模块

该函数的调用图谱如下:

 
  1. dma_alloc_coherent

  2. ->dma_alloc_attrs

  3. /* 这个分支从设备的dma_mem中去分配 */

  4. ->dma_alloc_from_dev_coherent

  5. ->dev_get_coherent_memory

  6. ->__dma_alloc_from_coherent

  7. /* 这个分支是从内存中去分配 */

  8. ->arm_dma_alloc(dma_ops->alloc(dma-mapping.c(arm)->arm_dma_ops))

  9. ->__dma_alloc

  10. ->simple_allocator_alloc(simple_allocator)

  11. ->__alloc_simple_buffer

当然了,使用完缓冲去后,需要使用 dma_free_coherent 进行释放。

2.5.3.2 流式映射

流式映射 则简单很多,在有些场景下我们无法使用 一致性映射缓冲区。只能使用类似 kmallloc/vmalloc 等接口分配的内存。那么内核也提供了映射这种缓冲区的机制。

一致性映射 和 流式映射 的区别总结如下:
流式映射:映射已有的缓冲区。
一致性映射:直接开辟一个一致性缓冲区。

流式映射 的原理是刷新 缓冲区的cache ,保证 cache 中的数据回写到 内存 中,不会与 一致性映射 一样关闭 cache机制

流式映射 就支持多种类型的缓冲区,下面以 sigle类型 为例说明部分代码。
函数接口原型为:

 
  1. #define dma_map_single(d, a, s, r) dma_map_single_attrs(d, a, s, r, 0)

  2. dma_addr_t dma_map_single_attrs(struct device *dev, void *ptr,

  3. size_t size,

  4. enum dma_data_direction dir,

  5. unsigned long attrs)

其调用图谱如下:

 
  1. dma_map_single

  2. ->dma_map_single_attrs

  3. ->arm_dma_map_page(dma_ops->alloc(dma-mapping.c(arm)->arm_dma_ops))

  4. ->__dma_page_cpu_to_dev

下面这段代码展示了 刷新缓冲区 的主要部分

 
  1. static void __dma_page_cpu_to_dev(struct page *page, unsigned long off,

  2. size_t size, enum dma_data_direction dir)

  3. {

  4. phys_addr_t paddr;

  5. dma_cache_maint_page(page, off, size, dir, dmac_map_area);

  6. paddr = page_to_phys(page) + off;

  7. if (dir == DMA_FROM_DEVICE) {

  8. /* invalidate cache */

  9. outer_inv_range(paddr, paddr + size);

  10. } else {

  11. /* clean cache */

  12. outer_clean_range(paddr, paddr + size);

  13. }

  14. /* FIXME: non-speculating: flush on bidirectional mappings? */

  15. }

2.5.3.3 sync操作

如果我们需要对一块 流式映射 的 DMA缓冲区 频繁进行操作,之后需要小心地对 流式缓冲区 进行 sync操作,以保证在内存中看到的数据都是有效的。
一般可以使用下面的接口

 
  1. /* 当DMA完成操作后使用该接口以保证CPU可以拿到最新的有效数据 */

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

  3. /* 当CPU对流式缓冲区进行操作后,使用该接口保证DMA可以拿到最新的有效数据 */

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

2.5.4 代码示例

下面是笔者自己手动实践的 代码片段 ,仅供参考:

 
  1. /* 申请dma通道,在此之前请确保设备树中的dma相关属性编写正确,否则会引发oops */

  2. test_device->dma_chan = dma_request_chan(&pdev->dev, "sdram");

  3. if(NULL == test_device->dma_chan)

  4. {

  5. printk(KERN_INFO"request dma channel error\n");

  6. goto DEVICE_FAILE;

  7. }

  8. /* 开辟缓冲区并填充 */

  9. int buf_size = 128;

  10. void* dma_src = NULL;

  11. void* dma_dst = NULL;

  12. dma_addr_t dma_bus_src;

  13. dma_addr_t dma_bus_dst;

  14. #if 0

  15. /* 一致性映射 */

  16. dma_src = dma_alloc_coherent(&pdev->dev, buf_size, &dma_bus_src, GFP_KERNEL|GFP_DMA);

  17. if(NULL == dma_src)

  18. {

  19. printk(KERN_INFO"alloc src buffer error\n");

  20. goto DMA_SRC_FAILED;

  21. }

  22. printk(KERN_INFO"dma_src = %p, dma_bus_src = %#x\n", dma_src, dma_bus_src);

  23. dma_dst = dma_alloc_coherent(&pdev->dev, buf_size, &dma_bus_dst, GFP_KERNEL|GFP_DMA);

  24. if(NULL == dma_src)

  25. {

  26. printk(KERN_INFO"alloc src buffer error\n");

  27. goto DMA_DST_FAILED;

  28. }

  29. printk(KERN_INFO"dma_dst = %p, dma_bus_dst = %#x\n", dma_dst, dma_bus_dst);

  30. for(int i = 0; i < buf_size; i++)

  31. {

  32. ((char*)dma_src)[i] = i;

  33. printk(KERN_INFO"dma_src[%d] = %d, dma_dst[%d] = %d\n", i, ((char*)dma_src)[i], i, ((char*)dma_dst)[i]);

  34. }

  35. #else

  36. dma_src = devm_kzalloc(&pdev->dev, buf_size, GFP_KERNEL);

  37. if(NULL == dma_src)

  38. {

  39. printk(KERN_INFO"alloc src buffer error\n");

  40. goto DEVICE_FAILE;

  41. }

  42. dma_dst = devm_kzalloc(&pdev->dev, buf_size, GFP_KERNEL);

  43. if(NULL == dma_src)

  44. {

  45. printk(KERN_INFO"alloc src buffer error\n");

  46. goto DEVICE_FAILE;

  47. }

  48. #if 0

  49. /*

  50. 错误的流式映射

  51. 在进行映射后不能对缓冲区进行操作,不然DMA拿到的数据与真正的数据不一致

  52. */

  53. dma_bus_src = dma_map_single(&pdev->dev, dma_src, buf_size, DMA_BIDIRECTIONAL);

  54. dma_bus_dst = dma_map_single(&pdev->dev, dma_dst, buf_size, DMA_BIDIRECTIONAL);

  55. printk(KERN_INFO"dma_src = %p, dma_bus_src = %#x\n", dma_src, dma_bus_src);

  56. printk(KERN_INFO"dma_dst = %p, dma_bus_dst = %#x\n", dma_dst, dma_bus_dst);

  57. for(int i = 0; i < buf_size; i++)

  58. {

  59. ((char*)dma_src)[i] = i;

  60. printk(KERN_INFO"dma_src[%d] = %d, dma_dst[%d] = %d\n", i, ((char*)dma_src)[i], i, ((char*)dma_dst)[i]);

  61. }

  62. #else

  63. /*

  64. 正确的流式映射

  65. 将数据放入缓冲区后在进行映射,确保DMA拿到正确的数据

  66. */

  67. for(int i = 0; i < buf_size; i++)

  68. {

  69. ((char*)dma_src)[i] = i;

  70. printk(KERN_INFO"dma_src[%d] = %d, dma_dst[%d] = %d\n", i, ((char*)dma_src)[i], i, ((char*)dma_dst)[i]);

  71. }

  72. dma_bus_src = dma_map_single(&pdev->dev, dma_src, buf_size, DMA_BIDIRECTIONAL);

  73. dma_bus_dst = dma_map_single(&pdev->dev, dma_dst, buf_size, DMA_BIDIRECTIONAL);

  74. printk(KERN_INFO"dma_src = %p, dma_bus_src = %#x\n", dma_src, dma_bus_src);

  75. printk(KERN_INFO"dma_dst = %p, dma_bus_dst = %#x\n", dma_dst, dma_bus_dst);

  76. #endif

  77. #endif

  78. /* 获取传输描述符 */

  79. struct dma_async_tx_descriptor* dma_tx = NULL;

  80. dma_tx = dmaengine_prep_dma_memcpy(test_device->dma_chan, dma_bus_dst, dma_bus_src, buf_size, DMA_PREP_INTERRUPT);

  81. if(NULL == dma_tx)

  82. {

  83. printk(KERN_INFO"prepare dma error\n");

  84. goto DEVICE_FAILE;

  85. }

  86. /* 获取dma cookie */

  87. dma_cookie_t dma_cookie;

  88. dma_cookie = dmaengine_submit(dma_tx);

  89. if (dma_submit_error(dma_cookie))

  90. {

  91. printk(KERN_INFO"submit dma error\n");

  92. goto DEVICE_FAILE;

  93. }

  94. /* 开始传输 */

  95. dma_async_issue_pending(test_device->dma_chan);

  96. /* 等待传输完成 */

  97. enum dma_status dma_status = DMA_ERROR;

  98. struct dma_tx_state tx_state = {0};

  99. while(DMA_COMPLETE!= dma_status)

  100. {

  101. dma_status = test_device->dma_chan->device->device_tx_status(test_device->dma_chan, dma_cookie, &tx_state);

  102. schedule();

  103. }

  104. printk(KERN_INFO"dma_status = %d\n", dma_status);

  105. for(int i = 0; i < buf_size; i++)

  106. {

  107. printk(KERN_INFO"dma_src[%d] = %d, dma_dst[%d] = %d\n", i, ((char*)dma_src)[i], i, ((char*)dma_dst)[i]);

  108. }

  109. printk(KERN_INFO"dma finished\n");

  110. #if 1

  111. /* 如果是流式映射,在使用完以后需要去映射 */

  112. dma_unmap_single(&pdev->dev, dma_bus_src, buf_size, DMA_BIDIRECTIONAL);

  113. dma_unmap_single(&pdev->dev, dma_bus_dst, buf_size, DMA_BIDIRECTIONAL);

  114. #endif

参考链接

蜗窝科技DMA:http://www.wowotech.net/tag/dma
scatterlist && DMA:https://blog.csdn.net/iamlbccc/article/details/8002159
Linux内核scatterlist用法:https://blog.csdn.net/scarecrow_byr/article/details/79676482
DMA中的四种控制信号:https://blog.csdn.net/imred/article/details/50357819
Linux 4.0的dmaengine编程:https://blog.csdn.net/were0415/article/details/54095899
Linux内核中DMA分析:https://blog.csdn.net/jun_8018/article/details/77841606
linux内核之dmaengine:https://blog.csdn.net/dragon101788/article/details/100150385
linux dmaengine编程:https://blog.csdn.net/u012247418/article/details/82313959
Linux 4.0的dmaengine编程:https://blog.csdn.net/were0415/article/details/54095899
详解ARM的AMBA设备中的DMA设备:https://blog.csdn.net/qq_21792169/article/details/51277975
Linux高端内存的由来:https://blog.csdn.net/lwj103862095/article/details/38705593
内核逻辑地址和内核虚拟地址的区别:http://blog.chinaunix.net/uid-13245160-id-84373.html
物理地址与总线地址:http://freearth.blog.chinaunix.net/uid-24125210-id-2627080.html
linux物理地址和总线地址:http://blog.sina.com.cn/s/blog_4cd5d2bb01014vn9.html
linux中的物理地址,虚拟地址,总线地址的区别:https://blog.csdn.net/u014379540/article/details/52502470
PCIe扫盲——Memory & IO 地址空间:https://www.jianshu.com/p/8ca541612dd5
Kmalloc和Vmalloc的区别:https://www.cnblogs.com/wuchanming/p/4465155.html
对流式DMA和一致性DMA的认识:http://blog.chinaunix.net/uid-24153750-id-5127064.html
一致性DMA与流式DMA:https://blog.csdn.net/xiaolubk/article/details/46501987
linux之DMA API:https://blog.csdn.net/bugouyonggan/article/details/8921623
Arm linux dma mapping操作:https://blog.csdn.net/Windgs_YF/article/details/104259193
IOMMU的简单介绍:https://www.dazhuanlan.com/2019/10/28/5db651819008b/
什么是IOMMU?:https://nanxiao.me/iommu-introduction/
理解SMMU基本原理和基本概念:https://blog.csdn.net/FinicsWang/article/details/96107339

  • 5
    点赞
  • 47
    收藏
    觉得还不错? 一键收藏
  • 2
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值