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 中的 内核内存分布 列表如下(有所删减):
起始地址 | 结束地址 | 用处 |
---|---|---|
0xffff8000 | 0xffffffff | 用于 copy_user_page 和 clear_user_page |
0xffff1000 | 0xffff7fff | 保留,任何平台都不能使用该段 虚拟内存空间 |
0xffff0000 | 0xffff0fff | 异常向量表 所在的内存区域 |
0xffc00000 | 0xffefffff | 专用页面映射区(固定页面映射区),使用 fix_to_vir()可以获取该区域的逻辑地址 |
0xfee00000 | 0xfeffffff | PCI技术的IO映射空间 |
VMALLOC_START | VMALLOC_END | 使用 vmlloac() 和 ioremap() 获取的地址都处于该内存段 |
PAGE_OFFSET | high_memory-1 | 内存直接映射区域(常规内存映射区域),该段内存地址也称为 逻辑地址 ,可以用于 DMA寻址 |
PKMAP_BASE | PAGE_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设备 的过程:
- CPU 并不能通过 总线地址A直接访问 PCI设备
- PCI桥(PCI host bridge) 会在 MMIO 的 地址B(物理地址) 和 总线地址A 之间进行映射。
- 映射完成后,可以通过 物理地址B(处于MMIO) 访问 PCI设备,访问是会通过 PCI桥 对地址进行翻译。
- 驱动通过 ioremap 把 物理地址B 映射成 虚拟地址C
- 通过 虚拟地址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 使用 虚拟地址 访问该内存块。
其步骤如下:
- 内存块 由 CPU 创建,此时 CPU 获取到的是 内存块的虚拟地址X。
- 调用接口,将该内存块的 虚拟地址X 对应的 物理地址Y 映射为 总线地址Z 并返回给 CPU。
- CPU 拿到的地址有 内存块 的 虚拟地址 和 总线地址,其 物理地址 对于 CPU 来说没有意义。
- 将 总线地址 写入 DMA 对应的寄存器,接着就可以执行相关的 DMA操作 了。
内存访问
PS:注意如果DMA的工作不是在PCI这种有规范的设备上,那么总线地址可以认为是普通内存地址
2.3 IOMMU
上面粗略讲了 IOMMU 在 DMA 工作过程中的应用,但其实 IOMMU 的用处不止这些,下面简单地描述 IOMMI 的另外一个作用。
我们都知道,在带有 MMU 的 Soc 上,对于程序来说,虚拟地址空间 是 可连续访问的。
因为 MMU 帮我们完成了从 虚拟地址空间 到 物理地址空间 的映射,这样做固然对于程序来说可以大大提高 内存管理 的效率,但同时也带来了 物理内存空间碎片化 的结果,找到 可连续访问 的 物理地址空间 的难度将大大增加。
而当 Soc 上的 设备 使用 DMA 访问内存时,需要 可连续访问 的 物理地址空间。
一般情况下,有 2 种办法可以让 DMA 访问 连续的物理地址空间:
- 在初始化 内核时,将 一部分物理空间 保留下来,不进行虚拟空间的映射。当使用到 DMA 的时候,将所需要的数据放置到 内存空间。再让DMA去访问这段 物理内存。这种方法简单直接,但会使得 内存空间的使用率并不高。
- DMA 带上 MMU,让其在访问 虚拟空间 时 自动完成虚拟地址到物理地址的映射,此时 DMA 可以在不保留 连续物理地址空间 的情况下 访问连续的虚拟空间 。
ARM 使用了第二种方法,增加了一个特殊的 MMU,即 IOMMU。IOMMU 在 ARM架构 中称为 SMMU。SMMU 和 MMU 一样,在配置后可以进行 translation table walk。
总结 IOMMU 的 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_addr:DMA源地址
- dst_addr:DMA目的地址
- byte_count:传输数量
- link:下一个描述符地址,如果为最后一个描述符则该值为某一个特定的值。
PS:上面的信息是指在一般情况,有些厂家会根据需要调整包含信息的内容。
需要使用 DMA控制器 进行传输时,我们需要在开辟一块内存,这块内存存放的就是 DMA描述符。当 DMA控制器 进行工作时,需要程序将 DMA描述符 的地址设置到 DMA硬件描述符地址寄存器 中。这样,当使能 DMA控制器 开始工作后,会读取 DMA硬件描述符地址寄存器 中的内存地址并读取相应的 DMA描述符,根据 DMA描述符 的所描述的地址跟大小进行传输。当完成一个 DMA描述符 的传输后会根据情况读取下一个 DMA描述符。
2.4.3 LLI
上面说过驱动会创建 内存块 用于存放 DMA描述符 ,这些 内存块 我们称之为 LLI。LLI 全称为 Link List Item,一般在驱动代码中都可以看到其结构体。以笔者的学习代码,其代码如下,可以看到有几个成员与图中所描绘的一致:
- /*
- * Hardware representation of the LLI
- *
- * The hardware will be fed the physical address of this structure,
- * and read its content in order to start the transfer.
- */
- struct sun6i_dma_lli {
- u32 cfg;
- u32 src;
- u32 dst;
- u32 len;
- u32 para;
- u32 p_lli_next;
-
- /*
- * This field is not used by the DMA controller, but will be
- * used by the CPU to go through the list (mostly for dumping
- * or freeing it).
- */
- struct sun6i_dma_lli *v_lli_next;
- };
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客户端 的
- /* DMA控制器设备树节点 */
- dma: dma-controller@01c02000 {
- compatible = "allwinner,sun8i-v3s-dma";
- reg = <0x01c02000 0x1000>;
- interrupts = <GIC_SPI 50 IRQ_TYPE_LEVEL_HIGH>;
- clocks = <&ccu CLK_BUS_DMA>;
- resets = <&ccu RST_BUS_DMA>;
- #dma-cells = <1>;
- };
- /* DMA客户端设备树节点,以SPI为例 */
- spi2: spi@01c6a000 {
- compatible = "allwinner,sun6i-a31-spi";
- reg = <0x01c6a000 0x1000>;
- interrupts = <0 67 4>;
- clocks = <&ahb1_gates 22>, <&spi2_clk>;
- clock-names = "ahb", "mod";
- dmas = <&dma 25>, <&dma 25>;
- dma-names = "rx", "tx";
- resets = <&ahb1_rst 22>;
- };
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控制器 进行初始化,代码如下:
- struct sun6i_pchan {
- u32 idx;
- void __iomem *base;
- struct sun6i_vchan *vchan;
- struct sun6i_desc *desc;
- struct sun6i_desc *done;
- };
-
- struct sun6i_vchan {
- struct virt_dma_chan vc;
- struct list_head node;
- struct dma_slave_config cfg;
- struct sun6i_pchan *phy;
- u8 port;
- u8 irq_type;
- bool cyclic;
- };
- struct sun6i_dma_dev {
- struct dma_device slave;
- void __iomem *base;
- struct clk *clk;
- int irq;
- spinlock_t lock;
- struct reset_control *rstc;
- struct tasklet_struct task;
- atomic_t tasklet_shutdown;
- struct list_head pending;
- struct dma_pool *pool;
- struct sun6i_pchan *pchans;
- struct sun6i_vchan *vchans;
- const struct sun6i_dma_config *cfg;
- };
- static struct sun6i_dma_config sun8i_v3s_dma_cfg = {
- .nr_max_channels = 8,
- .nr_max_requests = 23,
- .nr_max_vchans = 24,
- .gate_needed = true,
- };
-
- static const struct of_device_id sun6i_dma_match[] = {
- { .compatible = "allwinner,sun6i-a31-dma", .data = &sun6i_a31_dma_cfg },
- { .compatible = "allwinner,sun8i-a23-dma", .data = &sun8i_a23_dma_cfg },
- { .compatible = "allwinner,sun8i-a83t-dma", .data = &sun8i_a83t_dma_cfg },
- { .compatible = "allwinner,sun8i-h3-dma", .data = &sun8i_h3_dma_cfg },
- { .compatible = "allwinner,sun8i-v3s-dma", .data = &sun8i_v3s_dma_cfg },
- { /* sentinel */ }
- };
- static int sun6i_dma_probe(struct platform_device *pdev)
- {
- const struct of_device_id *device;
- struct sun6i_dma_dev *sdc;
- struct resource *res;
- int ret, i;
-
- /* 申请dma控制器所需内存 */
- sdc = devm_kzalloc(&pdev->dev, sizeof(*sdc), GFP_KERNEL);
-
- /* 从设备树中获取device */
- device = of_match_device(sun6i_dma_match, &pdev->dev);
-
- /* device->data即为sun8i_v3s_dma_cfg */
- sdc->cfg = device->data;
-
- /* 获取设备树中的寄存器地址 */
- res = platform_get_resource(pdev, IORESOURCE_MEM, 0);
-
- /* 将寄存器地址映射为虚拟地址以供CPU使用 */
- sdc->base = devm_ioremap_resource(&pdev->dev, res);
-
- /* 获取DMA中断 */
- sdc->irq = platform_get_irq(pdev, 0);
-
- /* 获取DMA时钟 */
- sdc->clk = devm_clk_get(&pdev->dev, NULL);
-
- /* 创建dma内存池,以存放LLI,后续使用会从该池中取出LLI并填充 */
- sdc->pool = dmam_pool_create(dev_name(&pdev->dev), &pdev->dev,
- sizeof(struct sun6i_dma_lli), 4, 0);
-
- /*初始化DMA控制器的pending链表,所有提交的传输通道最终都会被挂在这对链表上面 */
- INIT_LIST_HEAD(&sdc->pending);
- spin_lock_init(&sdc->lock);
-
- /* 初始化DMA能力,可以在内核文档中查阅,下附文档所在目录 */
- dma_cap_set(DMA_PRIVATE, sdc->slave.cap_mask);
- dma_cap_set(DMA_MEMCPY, sdc->slave.cap_mask);
- dma_cap_set(DMA_SLAVE, sdc->slave.cap_mask);
- dma_cap_set(DMA_CYCLIC, sdc->slave.cap_mask);
-
- /* 初始化DMA控制器的struct dma_device 结构 */
- INIT_LIST_HEAD(&sdc->slave.channels);
- sdc->slave.device_free_chan_resources = sun6i_dma_free_chan_resources;
- sdc->slave.device_tx_status = sun6i_dma_tx_status;
- sdc->slave.device_issue_pending = sun6i_dma_issue_pending;
- sdc->slave.device_prep_slave_sg = sun6i_dma_prep_slave_sg;
- sdc->slave.device_prep_dma_memcpy = sun6i_dma_prep_dma_memcpy;
- sdc->slave.device_prep_dma_cyclic = sun6i_dma_prep_dma_cyclic;
- sdc->slave.copy_align = DMAENGINE_ALIGN_4_BYTES;
- sdc->slave.device_config = sun6i_dma_config;
- sdc->slave.device_pause = sun6i_dma_pause;
- sdc->slave.device_resume = sun6i_dma_resume;
- sdc->slave.device_terminate_all = sun6i_dma_terminate_all;
- sdc->slave.src_addr_widths = BIT(DMA_SLAVE_BUSWIDTH_1_BYTE) |
- BIT(DMA_SLAVE_BUSWIDTH_2_BYTES) |
- BIT(DMA_SLAVE_BUSWIDTH_4_BYTES);
- sdc->slave.dst_addr_widths = BIT(DMA_SLAVE_BUSWIDTH_1_BYTE) |
- BIT(DMA_SLAVE_BUSWIDTH_2_BYTES) |
- BIT(DMA_SLAVE_BUSWIDTH_4_BYTES);
- sdc->slave.directions = BIT(DMA_DEV_TO_MEM) |
- BIT(DMA_MEM_TO_DEV);
- sdc->slave.residue_granularity = DMA_RESIDUE_GRANULARITY_BURST;
- sdc->slave.dev = &pdev->dev;
-
- /* 申请内存存放DMA物理通道信息 */
- sdc->pchans = devm_kcalloc(&pdev->dev, sdc->cfg->nr_max_channels,
- sizeof(struct sun6i_pchan), GFP_KERNEL);
-
- /* 申请内存存放DMA虚拟通道信息 */
- sdc->vchans = devm_kcalloc(&pdev->dev, sdc->cfg->nr_max_vchans,
- sizeof(struct sun6i_vchan), GFP_KERNEL);
-
- /* 初始化tasklet队列,DMA中断任务在这个tasklet中执行 */
- tasklet_init(&sdc->task, sun6i_dma_tasklet, (unsigned long)sdc);
-
-
- /* 初始化DMA物理通道信息,包括通道号及通道所属寄存器 */
- for (i = 0; i < sdc->cfg->nr_max_channels; i++) {
- struct sun6i_pchan *pchan = &sdc->pchans[i];
-
- pchan->idx = i;
- pchan->base = sdc->base + 0x100 + i * 0x40;
- }
-
- /* 初始化DMA虚拟通道信息 */
- for (i = 0; i < sdc->cfg->nr_max_vchans; i++) {
- struct sun6i_vchan *vchan = &sdc->vchans[i];
- /* 初始化虚拟通道的node成员,后续工作中还会用到 */
- INIT_LIST_HEAD(&vchan->node);
- /* 注册描述符销毁回调 */
- vchan->vc.desc_free = sun6i_dma_free_desc;
- /* 初始化虚拟通道,将虚拟通道挂在到slave的channel成员上,以后就可以直接通过dma_device找到所有的虚拟通道 */
- vchan_init(&vchan->vc, &sdc->slave);
- }
-
- /* 注册DMA中断 */
- ret = devm_request_irq(&pdev->dev, sdc->irq, sun6i_dma_interrupt, 0,
- dev_name(&pdev->dev), sdc);
-
- /* 注册DMA控制器到 dmaengine*/
- ret = dma_async_device_register(&sdc->slave);
-
- /* 注册DMA回调sun6i_dma_of_xlate到DMA控制器,该回调会在申请DMA通道时用到 */
- ret = of_dma_controller_register(pdev->dev.of_node, sun6i_dma_of_xlate, sdc);
-
- }
-
- void vchan_init(struct virt_dma_chan *vc, struct dma_device *dmadev)
- {
-
- spin_lock_init(&vc->lock);
- /* 初始化虚拟通道的各个工作链表,每个链表代表节点的状态。虚拟通道的每个传输任务将会在这些链表上流转,以执行不同状态时所需要的操作 */
- INIT_LIST_HEAD(&vc->desc_allocated);
- INIT_LIST_HEAD(&vc->desc_submitted);
- INIT_LIST_HEAD(&vc->desc_issued);
- INIT_LIST_HEAD(&vc->desc_completed);
-
- /* 初始化虚拟通道的tasklet,每当虚拟通道完成一次传输任务,就会调用一次vchan_complete */
- tasklet_init(&vc->task, vchan_complete, (unsigned long)vc);
-
- vc->chan.device = dmadev;
- /* 将虚拟通道挂到dma_device的channels上 */
- list_add_tail(&vc->chan.device_node, &dmadev->channels);
- }
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 的调用图谱
dma_request_chan ->of_dma_request_slave_channel(请求到对应的channel) ->of_property_count_strings(找到设备树中dma-names属性的值的个数) ->of_dma_match_channel(根据dma-names值的个数,为每个值找到该值对应的phandle,即dma_spec) ->of_dma_find_controller(根据phandle找到对应的dma controller) ->sun6i_dma_of_xlate(调用of_dma_xlate回调, dma_spec中包含该设备所在的DMA port,据此找到一个可用的dma_chan并返回) ->dma_get_any_slave_channel
下面是各个调用层次的简化代码及讲述:
- struct dma_chan *dma_request_chan(struct device *dev, const char *name)
- {
- struct dma_device *d, *_d;
- struct dma_chan *chan = NULL;
-
- /* 根据设备树节点属性获取DMA虚拟通道 */
- if (dev->of_node)
- chan = of_dma_request_slave_channel(dev->of_node, name);
-
- ......
-
- return chan ? chan : ERR_PTR(-EPROBE_DEFER);
- }
-
- struct dma_chan *of_dma_request_slave_channel(struct device_node *np,
- const char *name)
- {
- struct of_phandle_args dma_spec;
- struct of_dma *ofdma;
- struct dma_chan *chan;
- int count, i, start;
- int ret_no_channel = -ENODEV;
- static atomic_t last_index;
-
- /* 判断设备的设备树节点属性是否有 dmas 属性 */
- if (!of_find_property(np, "dmas", NULL))
- return ERR_PTR(-ENODEV);
-
- /* 获取设备树节点的 dma-names属性值 的个数 */
- count = of_property_count_strings(np, "dma-names");
-
-
- start = atomic_inc_return(&last_index);
- /*
- 根据属性值的个数进行遍历,因为有多少个属性值就说明有该设备需要多少个DMA通道 。
- 也就是说申请的通道只在设备树节点的描述范围内
- */
- for (i = 0; i < count; i++) {
- /*
- dma_spec是设备节点dmas属性的struct of_phandle_args结构体,
- 该函数根据设备树节点和通道名找到对应的 struct of_phandle_args结构体
- */
- if (of_dma_match_channel(np, name,
- (i + start) % count,
- &dma_spec))
- continue;
- /*
- 根据dma_spec找到DMA控制器的struct of_dma结构体,
- 在设备树中dmas描述了DMA控制器节点的信息,所以可以获取到DMA控制器的信息
- */
- ofdma = of_dma_find_controller(&dma_spec);
-
- if (ofdma) {
- /*
- 如果结构体有效则调用of_dma_xlate回调,
- 可以根据probe函数得知,该回调为sun6i_dma_of_xlate
- 使用该回调找到对应的DMA通道并返回
- */
- chan = ofdma->of_dma_xlate(&dma_spec, ofdma);
- } else {
- ret_no_channel = -EPROBE_DEFER;
- chan = NULL;
- }
-
-
- if (chan)
- return chan;
- }
-
- return ERR_PTR(ret_no_channel);
- }
- static int of_dma_match_channel(struct device_node *np, const char *name,
- int index, struct of_phandle_args *dma_spec)
- {
- const char *s;
- /* 根据index(即属性下标)读取dma-names属性中的值 */
- if (of_property_read_string_index(np, "dma-names", index, &s))
- return -ENODEV;
- /* 比较传入的 name 是否和设备树中所描述的一致 */
- if (strcmp(name, s))
- return -ENODEV;
- /*
- 如果一致则使用np,index来找到dma控制器的struct of_phandle_args结构体,
- 并把值赋给dma_spec用于返回
- */
- if (of_parse_phandle_with_args(np, "dmas", "#dma-cells", index,
- dma_spec))
- return -ENODEV;
-
- return 0;
- }
-
- static struct dma_chan *sun6i_dma_of_xlate(struct of_phandle_args *dma_spec,
- struct of_dma *ofdma)
- {
- /* 找到Soc的DMA控制器描述结构体sdev */
- struct sun6i_dma_dev *sdev = ofdma->of_dma_data;
- struct sun6i_vchan *vchan;
- struct dma_chan *chan;
- u8 port = dma_spec->args[0];
-
- /* 判断sdev的nr_max_requests是否有效 */
- if (port > sdev->cfg->nr_max_requests)
- return NULL;
- /* 判断sdev的nr_max_requests是否有效 */
- chan = dma_get_any_slave_channel(&sdev->slave);
- if (!chan)
- return NULL;
-
- /*
- 获取chan所在的struct sun6i_vchan结构体,并设置其DRQ信号值,
- DRQ信号值是使用设备树中的dmas属性来描述的,所以port的值为dma_spec的arg[0]成员
- */
- vchan = to_sun6i_vchan(chan);
- vchan->port = port;
-
- return chan;
- }
- struct dma_chan *dma_get_any_slave_channel(struct dma_device *device)
- {
- dma_cap_mask_t mask;
- struct dma_chan *chan;
-
- dma_cap_zero(mask);
- dma_cap_set(DMA_SLAVE, mask);
-
- chan = find_candidate(device, &mask, NULL, NULL);
-
- return IS_ERR(chan) ? NULL : chan;
- }
- static struct dma_chan *find_candidate(struct dma_device *device,
- const dma_cap_mask_t *mask,
- dma_filter_fn fn, void *fn_param)
- {
- struct dma_chan *chan = private_candidate(mask, device, fn, fn_param);
- int err;
-
- if (chan) {
- /* Found a suitable channel, try to grab, prep, and return it.
- * We first set DMA_PRIVATE to disable balance_ref_count as this
- * channel will not be published in the general-purpose
- * allocator
- */
- dma_cap_set(DMA_PRIVATE, device->cap_mask);
- device->privatecnt++;
- err = dma_chan_get(chan);
-
- if (err) {
- if (err == -ENODEV) {
- dev_dbg(device->dev, "%s: %s module removed\n",
- __func__, dma_chan_name(chan));
- list_del_rcu(&device->global_node);
- } else
- dev_dbg(device->dev,
- "%s: failed to get %s: (%d)\n",
- __func__, dma_chan_name(chan), err);
-
- if (--device->privatecnt == 0)
- dma_cap_clear(DMA_PRIVATE, device->cap_mask);
-
- chan = ERR_PTR(err);
- }
- }
-
- return chan ? chan : ERR_PTR(-EPROBE_DEFER);
- }
- static struct dma_chan *private_candidate(const dma_cap_mask_t *mask,
- struct dma_device *dev,
- dma_filter_fn fn, void *fn_param)
- {
- struct dma_chan *chan;
-
-
- /*
- 在vchan_init中,我们已经将多个vchan挂在了dma_device的channel链表上 、
- 这里遍历所有vchan的chan成员,并判断其client_count,
- 如果已经被申请了(client_count不为0),则遍历下一个。
-
- */
- list_for_each_entry(chan, &dev->channels, device_node) {
- if (chan->client_count) {
- dev_dbg(dev->dev, "%s: %s busy\n",
- __func__, dma_chan_name(chan));
- continue;
- }
- ......
- /* 如果该chan没有被申请,则返回 */
- return chan;
- }
-
- return NULL;
- }
2.5.2.3 配置DMA通道的参数
申请完 DMA通道 后则需要对其进行配置,其接口原型为:
static inline int dmaengine_slave_config(struct dma_chan *chan, struct dma_slave_config *config)
- chan:DMA通道 描述结构体指针
- config:DMA通道配置 结构体指针
这里需要说名一下 *struct dma_slave_config config 结构体,如下所示:
- struct dma_slave_config {
- enum dma_transfer_direction direction;
- phys_addr_t src_addr;
- phys_addr_t dst_addr;
- enum dma_slave_buswidth src_addr_width;
- enum dma_slave_buswidth dst_addr_width;
- u32 src_maxburst;
- u32 dst_maxburst;
- u32 src_port_window_size;
- u32 dst_port_window_size;
- bool device_fc;
- unsigned int slave_id;
- };
其重要成员如下:
- 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 的调用图谱
dmaengine_slave_config ->device_config(执行sun6i_dma_config回调)
可以看到其调用图谱比较简单, sun6i_dma_config 已经在 probe函数 初始化时注册了,可以回顾前面的代码进行了解。
下面的简化diam
- static int sun6i_dma_config(struct dma_chan *chan,
- struct dma_slave_config *config)
- {
- struct sun6i_vchan *vchan = to_sun6i_vchan(chan);
- /* 拷贝config到vchan(虚拟通道)的cfg成员,该成员会在后面使用到 */
- memcpy(&vchan->cfg, config, sizeof(*config));
-
- return 0;
- }
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 为例来说明代码,首先看一下调用图谱
dmaengine_prep_dma_memcpy ->device_prep_dma_memcpy(即sun6i_dma_prep_dma_memcpy回调)
代码及讲述如下:
- static struct dma_async_tx_descriptor *sun6i_dma_prep_dma_memcpy(
- struct dma_chan *chan, dma_addr_t dest, dma_addr_t src,
- size_t len, unsigned long flags)
- {
- /* 获取DMA控制器sdev */
- struct sun6i_dma_dev *sdev = to_sun6i_dma_dev(chan->device);
- /* 获取chan成员所在的vchan结构体 */
- struct sun6i_vchan *vchan = to_sun6i_vchan(chan);
- struct sun6i_dma_lli *v_lli;
- struct sun6i_desc *txd;
- dma_addr_t p_lli;
- s8 burst, width;
-
- /* 创建传输描述符txd */
- txd = kzalloc(sizeof(*txd), GFP_NOWAIT);
-
- /* 从dma内存池中获取一个lli结构体内存,lli的物理地址为p_lli,虚拟地址为v_lli */
- v_lli = dma_pool_alloc(sdev->pool, GFP_NOWAIT, &p_lli);
-
- /* 使用虚拟地址初始化lli */
- v_lli->src = src;
- v_lli->dst = dest;
- v_lli->len = len;
- v_lli->para = NORMAL_WAIT;
-
- /*
- 设置传输通道的burst和width属性。
- 这里需要注意,一般情况下会使用dmaengine_slave_config传进来的配置,
- 但在memcpy类型中,配置是固定的,所以不需要进行配置。
- 其他类型的传输就需要使用configs进行配置
- */
- burst = convert_burst(8);
- width = convert_buswidth(DMA_SLAVE_BUSWIDTH_4_BYTES);
- v_lli->cfg = DMA_CHAN_CFG_SRC_DRQ(DRQ_SDRAM) |
- DMA_CHAN_CFG_DST_DRQ(DRQ_SDRAM) |
- DMA_CHAN_CFG_DST_LINEAR_MODE |
- DMA_CHAN_CFG_SRC_LINEAR_MODE |
- DMA_CHAN_CFG_SRC_BURST(burst) |
- DMA_CHAN_CFG_SRC_WIDTH(width) |
- DMA_CHAN_CFG_DST_BURST(burst) |
- DMA_CHAN_CFG_DST_WIDTH(width);
-
- /* 将lli链入传输描述符的v_lli和p_lli成员,形成v_lli链表和p_lli链表 */
- sun6i_dma_lli_add(NULL, v_lli, p_lli, txd);
-
- /* 初始化传输描述符并返回 */
- return vchan_tx_prep(&vchan->vc, &txd->vd, flags);
-
- }
-
- static void *sun6i_dma_lli_add(struct sun6i_dma_lli *prev,
- struct sun6i_dma_lli *next,
- dma_addr_t next_phy,
- struct sun6i_desc *txd)
- {
- /* 如果prev为空则说明当前是空链表,链入传输描述符的p_lli成员和v_lli成员 */
- if (!prev) {
- txd->p_lli = next_phy;
- txd->v_lli = next;
- } else {
- /* 如果不是则链入指定位置,该位置在prev之后,next为需要链入的lli */
- prev->p_lli_next = next_phy;
- prev->v_lli_next = next;
- }
- /* 设置最后一个lli的next成员为指定值,说明该lli是最后一个 */
- next->p_lli_next = LLI_LAST_ITEM;
- next->v_lli_next = NULL;
-
- return next;
- }
-
- static inline struct dma_async_tx_descriptor *vchan_tx_prep(struct virt_dma_chan *vc,
- struct virt_dma_desc *vd, unsigned long tx_flags)
- {
- unsigned long flags;
-
- /* 初始化描述符,将vc->chan的值赋给vd->tx->chan */
- dma_async_tx_descriptor_init(&vd->tx, &vc->chan);
-
- /* 对vd->tx的各个值进行赋值 */
- vd->tx.flags = tx_flags;
- vd->tx.tx_submit = vchan_tx_submit;
- /* vchan_tx_desc_free回答会在销毁传输描述符vd时使用,因为vd是使用alloc创建的 */
- vd->tx.desc_free = vchan_tx_desc_free;
-
- spin_lock_irqsave(&vc->lock, flags);
-
- /* 将传输描述符加入通道的desc_allocated链表 */
- list_add_tail(&vd->node, &vc->desc_allocated);
- spin_unlock_irqrestore(&vc->lock, flags);
-
- return &vd->tx;
- }
-
2.5.2.5 提交传输请求
在获取完 DMA描述符 后,就可提交传输任务了,接口原型如下:
- dma_cookie_t dmaengine_submit(struct dma_async_tx_descriptor *desc)
- desc:申请的DMA传输描述符
其调用图谱如下:
dmaengine_submit ->tx_submit(即vchan_tx_submit回调)
回顾 申请传输描述符 ,在申请前需要对 DMA描述符 进行初始化,初始化的时候就注册了回调 vchan_tx_submit。
函数描述如下:
- static inline dma_cookie_t dmaengine_submit(struct dma_async_tx_descriptor *desc)
- {
- return desc->tx_submit(desc);
- }
- dma_cookie_t vchan_tx_submit(struct dma_async_tx_descriptor *tx)
- {
- /* 获取DMA虚拟通道 */
- struct virt_dma_chan *vc = to_virt_chan(tx->chan);
- /* 获取虚拟通道的DMA */
- struct virt_dma_desc *vd = to_virt_desc(tx);
- unsigned long flags;
- dma_cookie_t cookie;
-
- spin_lock_irqsave(&vc->lock, flags);
- /* 为传输描述符分配cookie */
- cookie = dma_cookie_assign(tx);
-
- /* 将传输描述符转移到虚拟通道的desc_submitted链表上 */
- list_move_tail(&vd->node, &vc->desc_submitted);
- spin_unlock_irqrestore(&vc->lock, flags);
-
- dev_dbg(vc->chan.device->dev, "vchan %p: txd %p[%x]: submitted\n",
- vc, vd, cookie);
- /* 返回cookie */
- return cookie;
- }
2.5.2.6 启动传输
做好上面的准备工作并提交好传输任务后即可进行传输了。接口原型如下:
- static inline void dma_async_issue_pending(struct dma_chan *chan)
- {
- chan->device->device_issue_pending(chan);
- }
- chan:启动传输的 DMA通道
根据上面所说,提交好的传输任务都挂在了 DMA通道 的 desc_submitted 链表上。启动传输后,dmaengine 会在该 DMA虚体通道 上获取 传输任务(传输描述符),并根据所配置的信息启动传输。
其调用图谱如下:
dma_async_issue_pending ->device_issue_pending(即sun6i_dma_issue_pending回调)
代码描述如下:
- static inline void dma_async_issue_pending(struct dma_chan *chan)
- {
- chan->device->device_issue_pending(chan);
- }
- static void sun6i_dma_issue_pending(struct dma_chan *chan)
- {
- /* 获取DMA控制器sdev */
- struct sun6i_dma_dev *sdev = to_sun6i_dma_dev(chan->device);
- /* 获取DMA虚拟通道 */
- struct sun6i_vchan *vchan = to_sun6i_vchan(chan);
- unsigned long flags;
-
- spin_lock_irqsave(&vchan->vc.lock, flags);
-
- /* 将所有在desc_submitted链表上的传输描述符转移到desc_issued链表 */
- if (vchan_issue_pending(&vchan->vc)) {
- spin_lock(&sdev->lock);
-
- /* 如果当前虚拟通道已经绑定了物理通道,
- 且虚拟通道vchan没有挂载到DMA控制器sdev的pending链表上。
- 则将虚拟通道vchan挂载到pending链表上,并调度执行tasklet。
- 虚拟通道vchan就是我们需要启动传输的通道
- */
- if (!vchan->phy && list_empty(&vchan->node)) {
- list_add_tail(&vchan->node, &sdev->pending);
- tasklet_schedule(&sdev->task);
- dev_dbg(chan2dev(chan), "vchan %p: issued\n",
- &vchan->vc);
- }
-
- spin_unlock(&sdev->lock);
- } else {
- dev_dbg(chan2dev(chan), "vchan %p: nothing to issue\n",
- &vchan->vc);
- }
-
- spin_unlock_irqrestore(&vchan->vc.lock, flags);
- }
- static inline bool vchan_issue_pending(struct virt_dma_chan *vc)
- {
- /* 将所有在desc_submitted链表上的传输描述符转移到desc_issued链表 */
- list_splice_tail_init(&vc->desc_submitted, &vc->desc_issued);
- return !list_empty(&vc->desc_issued);
- }
2.5.2.7 传输tasklet
使用 dma_async_issue_pending 启动传输后,真正执行传输任务的是 tasklet。其执行函数在 probe函数 初始化时已经注册,就是 sun6i_dma_tasklet。
- static void sun6i_dma_tasklet(unsigned long data)
- {
- /* 获取DMA控制器sdev */
- struct sun6i_dma_dev *sdev = (struct sun6i_dma_dev *)data;
- /* DMA控制器配置 */
- const struct sun6i_dma_config *cfg = sdev->cfg;
- struct sun6i_vchan *vchan;
- struct sun6i_pchan *pchan;
- unsigned int pchan_alloc = 0;
- unsigned int pchan_idx;
-
- /* DMA controller的channel链表上的vchan进行遍历 */
- list_for_each_entry(vchan, &sdev->slave.channels, vc.chan.device_node) {
- spin_lock_irq(&vchan->vc.lock);
-
- pchan = vchan->phy;
- /* 如果vchan具有pchan并且该pchan处于工作状态 */
- if (pchan && pchan->done) {
- /* 尝试启动该vchan上的传输sun6i_dma_start_desc */
- if (sun6i_dma_start_desc(vchan)) {
- /*
- 如果返回非0值,则说明当前遍历到的vchan已经完成了所有的传输任务 。
- 按照笔者理解,这个循环应该是释放掉所有完成任务的pchan,方便分配给即将需要传输的vchan
- */
- dev_dbg(sdev->slave.dev, "pchan %u: free\n",
- pchan->idx);
-
- /* Mark this channel free */
- vchan->phy = NULL;
- pchan->vchan = NULL;
- }
- }
- spin_unlock_irq(&vchan->vc.lock);
- }
-
- spin_lock_irq(&sdev->lock);
-
- /* 遍历所有pchan,找到空闲的pchan并绑定到需要进行传输的vchan上 */
- for (pchan_idx = 0; pchan_idx < cfg->nr_max_channels; pchan_idx++) {
- pchan = &sdev->pchans[pchan_idx];
- /*
- 如果pchan的vchan成员不为NULL,则说明该pchan已经被占用。继续下一个ptchan
- 如果sdev的pending链表为空,则说明当前DMA控制器没有需要进行传输的通道,继续遍历。按照笔者理解,这里应该可以直接返回
- */
- if (pchan->vchan || list_empty(&sdev->pending))
- continue;
-
- /*
- 如果pchan的vchan成员为空,则说明该pchan是空闲的,可以使用。
- 取出DMA控制器pengding链表上需要进行传输的第一个虚拟通道vchan
- */
- vchan = list_first_entry(&sdev->pending,
- struct sun6i_vchan, node);
-
- /* 并且将将虚拟通道从pengding链表上移除 */
- list_del_init(&vchan->node);
- /* 将当前的pchan申请情况记录下来 */
- pchan_alloc |= BIT(pchan_idx);
-
- /* 绑定pchan和vchan,防止pchan被其他虚拟通道申请去 */
- pchan->vchan = vchan;
- vchan->phy = pchan;
- dev_dbg(sdev->slave.dev, "pchan %u: alloc vchan %p\n",
- pchan->idx, &vchan->vc);
- }
- spin_unlock_irq(&sdev->lock);
-
- /* 遍历所有pchan */
- for (pchan_idx = 0; pchan_idx < cfg->nr_max_channels; pchan_idx++) {
- /* 如果当前pchan已经没被申请,即为空闲pchan,则遍历下一个pchan */
- if (!(pchan_alloc & BIT(pchan_idx)))
- continue;
- /* 当前pchan已经被申请,是在工作中。获取该pchan以及其绑定的vchan */
- pchan = sdev->pchans + pchan_idx;
- vchan = pchan->vchan;
- /* 如果pchan的vchan成员有效,则说明该通道有需要进行传输的任务 */
- if (vchan) {
- spin_lock_irq(&vchan->vc.lock);
- /* 进行传输 */
- sun6i_dma_start_desc(vchan);
- spin_unlock_irq(&vchan->vc.lock);
- }
- }
- }
-
- static int sun6i_dma_start_desc(struct sun6i_vchan *vchan)
- {
- /* 获取DMA控制器 */
- struct sun6i_dma_dev *sdev = to_sun6i_dma_dev(vchan->vc.chan.device);
- /* 弹出DMA虚拟通道上的传输描述符 */
- struct virt_dma_desc *desc = vchan_next_desc(&vchan->vc);
- struct sun6i_pchan *pchan = vchan->phy;
- u32 irq_val, irq_reg, irq_offset;
-
- if (!pchan)
- return -EAGAIN;
- /* 如果传输描述符为空,则说明当前通道没有需要进行传输的任务 */
- if (!desc) {
- pchan->desc = NULL;
- pchan->done = NULL;
- return -EAGAIN;
- }
- /*
- 经过前面的流程,当前传输描述符已经在desc_issued链表上。
- 现在将传输描述符从 desc_issued链表上删除。
- */
- list_del(&desc->node);
- /*
- 获取传输描述符的lli。
- 启动DMA中断。
- 并将lli写入控制寄存器。
- 启动传输
- */
- pchan->desc = to_sun6i_desc(&desc->tx);
- pchan->done = NULL;
-
- irq_reg = pchan->idx / DMA_IRQ_CHAN_NR;
- irq_offset = pchan->idx % DMA_IRQ_CHAN_NR;
-
- vchan->irq_type = vchan->cyclic ? DMA_IRQ_PKG : DMA_IRQ_QUEUE;
-
- irq_val = readl(sdev->base + DMA_IRQ_EN(irq_reg));
- irq_val &= ~((DMA_IRQ_HALF | DMA_IRQ_PKG | DMA_IRQ_QUEUE) <<
- (irq_offset * DMA_IRQ_CHAN_WIDTH));
- irq_val |= vchan->irq_type << (irq_offset * DMA_IRQ_CHAN_WIDTH);
- writel(irq_val, sdev->base + DMA_IRQ_EN(irq_reg));
-
- writel(pchan->desc->p_lli, pchan->base + DMA_CHAN_LLI_ADDR);
- writel(DMA_CHAN_ENABLE_START, pchan->base + DMA_CHAN_ENABLE);
-
- return 0;
- }
总结一下tasklet任务:tasklet对每一个vchan进行遍历,如果当前vchan有传输任务但有分配pchan,则启动传输任务。如果没有pchan,则找出一个空闲pchan并分配给给vchan,并启动传输任务。
2.5.2.8 传输完成
当完成一次 传输任务后,DMA控制器硬件 会出发中断,执行中断处理函数
- static irqreturn_t sun6i_dma_interrupt(int irq, void *dev_id)
- {
- struct sun6i_dma_dev *sdev = dev_id;
- struct sun6i_vchan *vchan;
- struct sun6i_pchan *pchan;
- int i, j, ret = IRQ_NONE;
- u32 status;
-
- /* 遍历所有的物理通道的中断标志位 */
- for (i = 0; i < sdev->cfg->nr_max_channels / DMA_IRQ_CHAN_NR; i++) {
- /* 如果当前的物理通道的标志位无效,则说明不是该通道完成,继续遍历下一个 */
- status = readl(sdev->base + DMA_IRQ_STAT(i));
- if (!status)
- continue;
-
- /* 如果当前的物理通道的标志位有效,则进行中断应答,将标志位置0 */
- writel(status, sdev->base + DMA_IRQ_STAT(i));
-
- for (j = 0; (j < DMA_IRQ_CHAN_NR) && status; j++) {
- /* 获取触发中断的DMA物理通道及其虚拟通道 */
- pchan = sdev->pchans + j;
- vchan = pchan->vchan;
-
- /* 如果vchan有效,则可以进行收尾工作 */
- if (vchan && (status & vchan->irq_type)) {
- if (vchan->cyclic) {
- /* 如果当前通道的传输类型为cyclic类型,则调用其回调,在本例中不是该类型 */
- vchan_cyclic_callback(&pchan->desc->vd);
- } else {
- /*
- 如果当前通道的传输类型为不cyclic类型,
- 则设置传输描述符的cookiet,通知驱动传输完成
- */
- spin_lock(&vchan->vc.lock);
- vchan_cookie_complete(&pchan->desc->vd);
- pchan->done = pchan->desc;
- spin_unlock(&vchan->vc.lock);
- }
- }
-
- status = status >> DMA_IRQ_CHAN_WIDTH;
- }
- /* 如果DMA传输没有被取消,则再执行一次tasklet,完成下一个传输描述符 */
- if (!atomic_read(&sdev->tasklet_shutdown))
- tasklet_schedule(&sdev->task);
- ret = IRQ_HANDLED;
- }
-
- return ret;
- }
- static inline void vchan_cookie_complete(struct virt_dma_desc *vd)
- {
- /* 获取完成传输的虚拟通道 */
- struct virt_dma_chan *vc = to_virt_chan(vd->tx.chan);
- dma_cookie_t cookie;
-
- cookie = vd->tx.cookie;
- dma_cookie_complete(&vd->tx);
- dev_vdbg(vc->chan.device->dev, "txd %p[%x]: marked complete\n",
- vd, cookie);
- /*
- 前面说了传输描述符已经从desc_issue链表上删除掉了,所以现在它不属于任何链表
- 将传输描述符从添加到desc_completed链表
- */
- list_add_tail(&vd->node, &vc->desc_completed);
- /*
- 在vchan_init中我们提到过会给虚拟通道注册一个tasklet,其执行函数为vchan_complete
- 调用虚拟通道的tasklet。
- */
- tasklet_schedule(&vc->task);
- }
- static void vchan_complete(unsigned long arg)
- {
- struct virt_dma_chan *vc = (struct virt_dma_chan *)arg;
- struct virt_dma_desc *vd, *_vd;
- struct dmaengine_desc_callback cb;
- LIST_HEAD(head);
-
- /* 将desc_completed链表上的所有传输描述符移动到head链表上 */
- list_splice_tail_init(&vc->desc_completed, &head);
- ......
- /* 遍历当前head上的所有传输描述符 */
- list_for_each_entry_safe(vd, _vd, &head, node) {
- /* 将传输描述符去head对链表上移除 */
- list_del(&vd->node);
- /* 如果当前传输描述符不需要重复使用,则调用 */
- if (dmaengine_desc_test_reuse(&vd->tx))
- list_add(&vd->node, &vc->desc_allocated);
- else
- /*
- 如果当前传输描述符不需要重复使用,则调用desc_free回调
- desc_free回到是在probe函数中注册的,是sun6i_dma_free_desc函数
- 很明显,是用于销毁传输描述符的,以为传输描述符是从dma_pool中申请的,用完要还回去
- */
- vc->desc_free(vd);
- }
- }
- static void sun6i_dma_free_desc(struct virt_dma_desc *vd)
- {
- struct sun6i_desc *txd = to_sun6i_desc(&vd->tx);
- struct sun6i_dma_dev *sdev = to_sun6i_dma_dev(vd->tx.chan->device);
- struct sun6i_dma_lli *v_lli, *v_next;
- dma_addr_t p_lli, p_next;
-
- if (unlikely(!txd))
- return;
-
- p_lli = txd->p_lli;
- v_lli = txd->v_lli;
-
- while (v_lli) {
- v_next = v_lli->v_lli_next;
- p_next = v_lli->p_lli_next;
- /* 归还传输描述符 */
- dma_pool_free(sdev->pool, v_lli, p_lli);
-
- v_lli = v_next;
- p_lli = p_next;
- }
-
- kfree(txd);
- }
总结一下中断:中断遍历所有的物理通道,并找出触发中断的物理通道。对该物理通道上的传输描述符进行销毁,并再继续下一个传输任务。
DMA传输描述符 的销毁流程如下:
sun6i_dma_interrupt ->vchan_cookie_complete ->vchan_complete ->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类型,可以理解为单纯的一块缓冲区。
下面我们看一下接口原型:
- /* 该接口可以在中断上下文调用 */
- 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,接口不使用该参数,但透传参数到 内存管理模块。
该函数的调用图谱如下:
- dma_alloc_coherent
- ->dma_alloc_attrs
- /* 这个分支从设备的dma_mem中去分配 */
- ->dma_alloc_from_dev_coherent
- ->dev_get_coherent_memory
- ->__dma_alloc_from_coherent
- /* 这个分支是从内存中去分配 */
- ->arm_dma_alloc(dma_ops->alloc(dma-mapping.c(arm)->arm_dma_ops))
- ->__dma_alloc
- ->simple_allocator_alloc(simple_allocator)
- ->__alloc_simple_buffer
当然了,使用完缓冲去后,需要使用 dma_free_coherent 进行释放。
2.5.3.2 流式映射
流式映射 则简单很多,在有些场景下我们无法使用 一致性映射缓冲区。只能使用类似 kmallloc/vmalloc 等接口分配的内存。那么内核也提供了映射这种缓冲区的机制。
一致性映射 和 流式映射 的区别总结如下:
流式映射:映射已有的缓冲区。
一致性映射:直接开辟一个一致性缓冲区。
流式映射 的原理是刷新 缓冲区的cache ,保证 cache 中的数据回写到 内存 中,不会与 一致性映射 一样关闭 cache机制。
流式映射 就支持多种类型的缓冲区,下面以 sigle类型 为例说明部分代码。
函数接口原型为:
- #define dma_map_single(d, a, s, r) dma_map_single_attrs(d, a, s, r, 0)
- dma_addr_t dma_map_single_attrs(struct device *dev, void *ptr,
- size_t size,
- enum dma_data_direction dir,
- unsigned long attrs)
其调用图谱如下:
dma_map_single ->dma_map_single_attrs ->arm_dma_map_page(dma_ops->alloc(dma-mapping.c(arm)->arm_dma_ops)) ->__dma_page_cpu_to_dev
下面这段代码展示了 刷新缓冲区 的主要部分
- static void __dma_page_cpu_to_dev(struct page *page, unsigned long off,
- size_t size, enum dma_data_direction dir)
- {
- phys_addr_t paddr;
-
- dma_cache_maint_page(page, off, size, dir, dmac_map_area);
-
- paddr = page_to_phys(page) + off;
- if (dir == DMA_FROM_DEVICE) {
- /* invalidate cache */
- outer_inv_range(paddr, paddr + size);
- } else {
- /* clean cache */
- outer_clean_range(paddr, paddr + size);
- }
- /* FIXME: non-speculating: flush on bidirectional mappings? */
- }
2.5.3.3 sync操作
如果我们需要对一块 流式映射 的 DMA缓冲区 频繁进行操作,之后需要小心地对 流式缓冲区 进行 sync操作,以保证在内存中看到的数据都是有效的。
一般可以使用下面的接口
- /* 当DMA完成操作后使用该接口以保证CPU可以拿到最新的有效数据 */
- void dma_sync_single_for_cpu(struct device *dev, dma_addr_t addr, size_t size, enum dma_data_direction dir)
- /* 当CPU对流式缓冲区进行操作后,使用该接口保证DMA可以拿到最新的有效数据 */
- void dma_sync_single_for_device(struct device *dev, dma_addr_t addr, size_t size, enum dma_data_direction dir)
2.5.4 代码示例
下面是笔者自己手动实践的 代码片段 ,仅供参考:
- /* 申请dma通道,在此之前请确保设备树中的dma相关属性编写正确,否则会引发oops */
- test_device->dma_chan = dma_request_chan(&pdev->dev, "sdram");
- if(NULL == test_device->dma_chan)
- {
- printk(KERN_INFO"request dma channel error\n");
- goto DEVICE_FAILE;
- }
-
- /* 开辟缓冲区并填充 */
- int buf_size = 128;
- void* dma_src = NULL;
- void* dma_dst = NULL;
- dma_addr_t dma_bus_src;
- dma_addr_t dma_bus_dst;
- #if 0
- /* 一致性映射 */
- dma_src = dma_alloc_coherent(&pdev->dev, buf_size, &dma_bus_src, GFP_KERNEL|GFP_DMA);
- if(NULL == dma_src)
- {
- printk(KERN_INFO"alloc src buffer error\n");
- goto DMA_SRC_FAILED;
- }
- printk(KERN_INFO"dma_src = %p, dma_bus_src = %#x\n", dma_src, dma_bus_src);
-
- dma_dst = dma_alloc_coherent(&pdev->dev, buf_size, &dma_bus_dst, GFP_KERNEL|GFP_DMA);
- if(NULL == dma_src)
- {
- printk(KERN_INFO"alloc src buffer error\n");
- goto DMA_DST_FAILED;
- }
- printk(KERN_INFO"dma_dst = %p, dma_bus_dst = %#x\n", dma_dst, dma_bus_dst);
-
- for(int i = 0; i < buf_size; i++)
- {
- ((char*)dma_src)[i] = i;
- printk(KERN_INFO"dma_src[%d] = %d, dma_dst[%d] = %d\n", i, ((char*)dma_src)[i], i, ((char*)dma_dst)[i]);
- }
- #else
- dma_src = devm_kzalloc(&pdev->dev, buf_size, GFP_KERNEL);
- if(NULL == dma_src)
- {
- printk(KERN_INFO"alloc src buffer error\n");
如若内容造成侵权/违法违规/事实不符,请联系编程学习网邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
相关文章
- mysql-01基础操作
mysql-01基础操作...
2024/4/17 5:59:22 - MA5680T降级操作及注意事项
背景说明:使用MA5680T(OLT)设备,中兴厂商GPON光猫注册失败的问题。在OLT上可以发现光猫,但是注册时光猫的光纤指示灯常亮一下(正常情况下是注册成功后一直常亮)后又变为闪烁。说明光猫只是成功注册了一瞬间;降级的系统为:MA5600V800R008 ,目前我只发现此OLT版本可以正…...
2024/4/15 4:53:35 - DWZ框架添加自定义数据验证(手机号码,邮箱,数字等)
前言: DWZ前端框架,如同其他前端框架一般,都有Coder已经写好了的数据验证的方法,如验证手机号码格式、邮箱格式、日期格式等等。然而在项目过程中,框架给定的验证并不严格或者不符合实际项目中的需要,我们能不能在框架中自定义添加我们自己想要的验证方法呢?答案当然是…...
2024/4/15 4:53:40 - CentOS7 安装 7-zip 压缩工具
文章目录1. 下载 7-zip 的 rpm 包2. 安装 7-zip3. 安装依赖工具4. 解压命令1. 下载 7-zip 的 rpm 包 wget http://101.34.22.188/7-zip/p7zip-16.02-10.el7.x86_64.rpm -P /opt2. 安装 7-zip rpm -ivh /opt/p7zip-16.02-10.el7.x86_64.rpm3. 安装依赖工具 默认 Centos7 没…...
2024/4/7 0:49:48 - 逃离「大厂」回乡创业,用氚云攻克流量瓶颈,年入1300万
近两三年,短视频电商和直播带货火热,一定程度上提高了产品在线销售的说服力,但线上销售需要流量支撑,没有充足的营销投入,流量就无从保证,销路一样难以拓开。流量红利见顶,依靠传统电商模式按部…...
2024/4/7 0:49:47 - 刚毕业大学生考PMP用处大吗?
这个问题我觉得不能一概而论吧。 一般刚毕业的大学生就像是一张白纸一样,没有什么社会阅历,也没有什么拿的出手的社会成就。 现在很多大学生在学校就会考各种各样的证书来提升自己的含金量,因为现在国家施行的是任何行业都需要考资格证&…...
2024/4/17 12:59:38 - Linux中直接运行py文件
(1) 创建*.py文件* nano与vim创建文件时,如果文件不存在时,创建并打开;文件存在时,直接打开。 nano方式: [cogglei-7lo31rsr coggle]$ nano nano_hell_World.pyvim方式: [cogglei-7lo31rsr coggle]$ vim …...
2024/4/7 0:49:45 - 【问题记录】React使用connect后,ref.current为null
问题 在用React开发项目的过程中,遇到一个问题,使用connect连接低阶组件包装成高阶组件HOC后,父组件通用ref调用子组件方法时,提示xxxRef.current为null的错误。代码如下: // 子组件 // 通过connect方式连接为高阶组…...
2024/4/15 4:53:50 - 开店创业缺什么都不能缺水獭掌柜,外卖接单有它太省心
开店创业缺什么都不能缺水獭掌柜,外卖接单有它太省心 闺蜜辞职开了一家韩国料理店。店铺不大,但是味道很不错,平时一到饭点就很忙。我有一次去吃饭正好赶上最忙的时候,结果外卖接单设备居然出了问题,漏了好几单&#…...
2024/4/6 23:50:57 - 全站仪任意设站直线放样(方法)
说明 了解更多工程测量、仪器知识,关注”CORS测绘服务商“VX公众号 在工程建设中,经常会涉及到测量放样工作,它是保证建设项目工程质量的重要环节。施工放样方法有很多种,实践中需要因地制宜地选择合理的放样方法,如地…...
2024/4/15 4:54:00 - python操作MySQL数据库
一.python3.x是使用pymysql库访问,python2.x是使用mysqldb库访问 import pymysql conn pymysql.connect(host‘xxx.xxx.xxx.xxx’, user‘name’, passwd‘123456’, db‘xxx’) #创建连接 cursor conn.cursor() sql “SELECT * from *** WHERE app_names ‘健…...
2024/4/16 2:20:05 - vue 为element树形组件el-tree添加虚线,指示线
需求需要实现一个带有指示线的树形组件,项目用的Vue,树形组件使用的是element的el-tree,所以想直接在element的组件上直接改样式实现。 先看最终实现的效果图: 思路 在控制台查看 el-tree 渲染后的HTML结构,找到相应的…...
2024/4/16 16:57:14 - 2021-11-04 攻防世界-进阶题-MISC-024(stage1)
stage1 下载附件后,stegsolve打开,转换色道得到二维码 QR research扫描二维码 得到二进制 03F30D0AB6266A576300000000000000000100000040000000730D0000006400008400005A00006401005328020000006300000000030000000800000043000000734E00000064010…...
2024/4/16 22:25:19 - 微信小程序 跳转页面
页面返回 正常 A -> B -> C 都是通过 wx.navigateTo 跳转的,所以 wx.navigateBack 只能返回上一界面,如果要返回到A 界面就会出现 C -> B -> A 的效果。 如果想实现 A -> B -> C 当 C 点击返回时, 实现直接 C -> A 这…...
2024/4/17 23:19:40 - Java SpringBoot整合Elasticsearch
Java SpringBoot整合Elasticsearch 创建工程,pom.xml引入依赖: <!-- SpringBoot 核心包 --><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter</artifactId><version&g…...
2024/4/15 4:54:46 - Java实现获取两个字符串中最大相同子串。
如题 package com.mxh.java01;import org.junit.Test;/*** author MXHstrat* create 2021 - 11 - 04 9:33*/ public class StringDemo03 {/* 获取两个字符串中最大相同子串。比如: str1 "abcwerthel Loyuiodef";str2 "cvhellobnm" 提示:将短的那个串进…...
2024/4/7 0:49:40 - 记录:富文本图片显示问题
情景:微信小程序里面有一个富文本显示,两条数据前一条显示出来图片,后面的没有显示图片,图片操作相同,在后台查看可以看到图片 经检查:两张图片的域名相同 原因:在两张图片上传中间服务器更换过…...
2024/4/16 22:25:23 - 对Airtest报告的步骤标题做内容定制?实用速学
此文章来源于项目官方公众号:“AirtestProject” 版权声明:允许转载,但转载必须保留原链接;请勿用作商业或者非法用途 1. 前言 今天我们来聊一个非常实用的话题!有很多同学提过,我能不能修改Airtest报告显…...
2024/4/15 4:54:41 - 杰理之做蓝牙发射时,将立体声修改成单声道差分输出时,接收端出现卡音【篇】
...
2024/4/7 0:49:37 - 2021河南省ccpc欢度佳节题解+赛后总结
赛后总结:第一次距离金这么近,而且这道题几乎同类型的题,某acw网站算法基础课的 最短Hamilton路径,我还做过,但是当时没有想到两者一些相同的性质,还是练的少,明年省赛继续努力吧。 光棍节即将来…...
2024/4/7 0:49:36
最新文章
- 数据库的特点
前面讲了,数据库是有组织的,规范的把数据保存起来的。 怎么个组织的,规范的? 数据库的特点: 1.将数据放到数据表格(二维表)中,在将表格放到库中。 2.一个数据库中可以有多张表&am…...
2024/4/18 6:11:52 - 梯度消失和梯度爆炸的一些处理方法
在这里是记录一下梯度消失或梯度爆炸的一些处理技巧。全当学习总结了如有错误还请留言,在此感激不尽。 权重和梯度的更新公式如下: w w − η ⋅ ∇ w w w - \eta \cdot \nabla w ww−η⋅∇w 个人通俗的理解梯度消失就是网络模型在反向求导的时候出…...
2024/3/20 10:50:27 - C# 抽象类、接口
(1)、抽象类和抽象方法的定义和实现:abstract override abstract class Vehicle{ public abstract void Run(); } 继承抽象类并且实现抽象方法 class RaceCar : Vehicle{ public override void Run(){ } } (2)、接口的…...
2024/4/16 16:41:40 - vue中内置指令v-model的作用和常见使用方法介绍以及在自定义组件上支持
文章目录 一、v-model是什么二、什么是语法糖三、v-model常见的用法1、对于输入框(input):2、对于复选框(checkbox):3、对于选择框(select):4、对于组件(comp…...
2024/4/16 21:18:45 - 【外汇早评】美通胀数据走低,美元调整
原标题:【外汇早评】美通胀数据走低,美元调整昨日美国方面公布了新一期的核心PCE物价指数数据,同比增长1.6%,低于前值和预期值的1.7%,距离美联储的通胀目标2%继续走低,通胀压力较低,且此前美国一季度GDP初值中的消费部分下滑明显,因此市场对美联储后续更可能降息的政策…...
2024/4/18 0:33:31 - 【原油贵金属周评】原油多头拥挤,价格调整
原标题:【原油贵金属周评】原油多头拥挤,价格调整本周国际劳动节,我们喜迎四天假期,但是整个金融市场确实流动性充沛,大事频发,各个商品波动剧烈。美国方面,在本周四凌晨公布5月份的利率决议和新闻发布会,维持联邦基金利率在2.25%-2.50%不变,符合市场预期。同时美联储…...
2024/4/17 20:29:59 - 【外汇周评】靓丽非农不及疲软通胀影响
原标题:【外汇周评】靓丽非农不及疲软通胀影响在刚结束的周五,美国方面公布了新一期的非农就业数据,大幅好于前值和预期,新增就业重新回到20万以上。具体数据: 美国4月非农就业人口变动 26.3万人,预期 19万人,前值 19.6万人。 美国4月失业率 3.6%,预期 3.8%,前值 3…...
2024/4/18 3:56:15 - 【原油贵金属早评】库存继续增加,油价收跌
原标题:【原油贵金属早评】库存继续增加,油价收跌周三清晨公布美国当周API原油库存数据,上周原油库存增加281万桶至4.692亿桶,增幅超过预期的74.4万桶。且有消息人士称,沙特阿美据悉将于6月向亚洲炼油厂额外出售更多原油,印度炼油商预计将每日获得至多20万桶的额外原油供…...
2024/4/17 2:33:17 - 【外汇早评】日本央行会议纪要不改日元强势
原标题:【外汇早评】日本央行会议纪要不改日元强势近两日日元大幅走强与近期市场风险情绪上升,避险资金回流日元有关,也与前一段时间的美日贸易谈判给日本缓冲期,日本方面对汇率问题也避免继续贬值有关。虽然今日早间日本央行公布的利率会议纪要仍然是支持宽松政策,但这符…...
2024/4/17 7:50:46 - 【原油贵金属早评】欧佩克稳定市场,填补伊朗问题的影响
原标题:【原油贵金属早评】欧佩克稳定市场,填补伊朗问题的影响近日伊朗局势升温,导致市场担忧影响原油供给,油价试图反弹。此时OPEC表态稳定市场。据消息人士透露,沙特6月石油出口料将低于700万桶/日,沙特已经收到石油消费国提出的6月份扩大出口的“适度要求”,沙特将满…...
2024/4/18 3:56:01 - 【外汇早评】美欲与伊朗重谈协议
原标题:【外汇早评】美欲与伊朗重谈协议美国对伊朗的制裁遭到伊朗的抗议,昨日伊朗方面提出将部分退出伊核协议。而此行为又遭到欧洲方面对伊朗的谴责和警告,伊朗外长昨日回应称,欧洲国家履行它们的义务,伊核协议就能保证存续。据传闻伊朗的导弹已经对准了以色列和美国的航…...
2024/4/18 3:56:04 - 【原油贵金属早评】波动率飙升,市场情绪动荡
原标题:【原油贵金属早评】波动率飙升,市场情绪动荡因中美贸易谈判不安情绪影响,金融市场各资产品种出现明显的波动。随着美国与中方开启第十一轮谈判之际,美国按照既定计划向中国2000亿商品征收25%的关税,市场情绪有所平复,已经开始接受这一事实。虽然波动率-恐慌指数VI…...
2024/4/18 3:55:30 - 【原油贵金属周评】伊朗局势升温,黄金多头跃跃欲试
原标题:【原油贵金属周评】伊朗局势升温,黄金多头跃跃欲试美国和伊朗的局势继续升温,市场风险情绪上升,避险黄金有向上突破阻力的迹象。原油方面稍显平稳,近期美国和OPEC加大供给及市场需求回落的影响,伊朗局势并未推升油价走强。近期中美贸易谈判摩擦再度升级,美国对中…...
2024/4/18 3:55:54 - 【原油贵金属早评】市场情绪继续恶化,黄金上破
原标题:【原油贵金属早评】市场情绪继续恶化,黄金上破周初中国针对于美国加征关税的进行的反制措施引发市场情绪的大幅波动,人民币汇率出现大幅的贬值动能,金融市场受到非常明显的冲击。尤其是波动率起来之后,对于股市的表现尤其不安。隔夜美国股市出现明显的下行走势,这…...
2024/4/18 3:55:45 - 【外汇早评】美伊僵持,风险情绪继续升温
原标题:【外汇早评】美伊僵持,风险情绪继续升温昨日沙特两艘油轮再次发生爆炸事件,导致波斯湾局势进一步恶化,市场担忧美伊可能会出现摩擦生火,避险品种获得支撑,黄金和日元大幅走强。美指受中美贸易问题影响而在低位震荡。继5月12日,四艘商船在阿联酋领海附近的阿曼湾、…...
2024/4/17 21:50:30 - 【原油贵金属早评】贸易冲突导致需求低迷,油价弱势
原标题:【原油贵金属早评】贸易冲突导致需求低迷,油价弱势近日虽然伊朗局势升温,中东地区几起油船被袭击事件影响,但油价并未走高,而是出于调整结构中。由于市场预期局势失控的可能性较低,而中美贸易问题导致的全球经济衰退风险更大,需求会持续低迷,因此油价调整压力较…...
2024/4/15 13:53:08 - 氧生福地 玩美北湖(上)——为时光守候两千年
原标题:氧生福地 玩美北湖(上)——为时光守候两千年一次说走就走的旅行,只有一张高铁票的距离~ 所以,湖南郴州,我来了~ 从广州南站出发,一个半小时就到达郴州西站了。在动车上,同时改票的南风兄和我居然被分到了一个车厢,所以一路非常愉快地聊了过来。 挺好,最起…...
2024/4/15 9:16:52 - 氧生福地 玩美北湖(中)——永春梯田里的美与鲜
原标题:氧生福地 玩美北湖(中)——永春梯田里的美与鲜一觉醒来,因为大家太爱“美”照,在柳毅山庄去寻找龙女而错过了早餐时间。近十点,向导坏坏还是带着饥肠辘辘的我们去吃郴州最富有盛名的“鱼头粉”。说这是“十二分推荐”,到郴州必吃的美食之一。 哇塞!那个味美香甜…...
2024/4/18 3:56:13 - 氧生福地 玩美北湖(下)——奔跑吧骚年!
原标题:氧生福地 玩美北湖(下)——奔跑吧骚年!让我们红尘做伴 活得潇潇洒洒 策马奔腾共享人世繁华 对酒当歌唱出心中喜悦 轰轰烈烈把握青春年华 让我们红尘做伴 活得潇潇洒洒 策马奔腾共享人世繁华 对酒当歌唱出心中喜悦 轰轰烈烈把握青春年华 啊……啊……啊 两…...
2024/4/18 3:56:18 - 扒开伪装医用面膜,翻六倍价格宰客,小姐姐注意了!
原标题:扒开伪装医用面膜,翻六倍价格宰客,小姐姐注意了!扒开伪装医用面膜,翻六倍价格宰客!当行业里的某一品项火爆了,就会有很多商家蹭热度,装逼忽悠,最近火爆朋友圈的医用面膜,被沾上了污点,到底怎么回事呢? “比普通面膜安全、效果好!痘痘、痘印、敏感肌都能用…...
2024/4/18 3:55:57 - 「发现」铁皮石斛仙草之神奇功效用于医用面膜
原标题:「发现」铁皮石斛仙草之神奇功效用于医用面膜丽彦妆铁皮石斛医用面膜|石斛多糖无菌修护补水贴19大优势: 1、铁皮石斛:自唐宋以来,一直被列为皇室贡品,铁皮石斛生于海拔1600米的悬崖峭壁之上,繁殖力差,产量极低,所以古代仅供皇室、贵族享用 2、铁皮石斛自古民间…...
2024/4/18 3:55:50 - 丽彦妆\医用面膜\冷敷贴轻奢医学护肤引导者
原标题:丽彦妆\医用面膜\冷敷贴轻奢医学护肤引导者【公司简介】 广州华彬企业隶属香港华彬集团有限公司,专注美业21年,其旗下品牌: 「圣茵美」私密荷尔蒙抗衰,产后修复 「圣仪轩」私密荷尔蒙抗衰,产后修复 「花茵莳」私密荷尔蒙抗衰,产后修复 「丽彦妆」专注医学护…...
2024/4/15 23:28:22 - 广州械字号面膜生产厂家OEM/ODM4项须知!
原标题:广州械字号面膜生产厂家OEM/ODM4项须知!广州械字号面膜生产厂家OEM/ODM流程及注意事项解读: 械字号医用面膜,其实在我国并没有严格的定义,通常我们说的医美面膜指的应该是一种「医用敷料」,也就是说,医用面膜其实算作「医疗器械」的一种,又称「医用冷敷贴」。 …...
2024/4/18 3:56:20 - 械字号医用眼膜缓解用眼过度到底有无作用?
原标题:械字号医用眼膜缓解用眼过度到底有无作用?医用眼膜/械字号眼膜/医用冷敷眼贴 凝胶层为亲水高分子材料,含70%以上的水分。体表皮肤温度传导到本产品的凝胶层,热量被凝胶内水分子吸收,通过水分的蒸发带走大量的热量,可迅速地降低体表皮肤局部温度,减轻局部皮肤的灼…...
2024/4/18 3:56:11 - 配置失败还原请勿关闭计算机,电脑开机屏幕上面显示,配置失败还原更改 请勿关闭计算机 开不了机 这个问题怎么办...
解析如下:1、长按电脑电源键直至关机,然后再按一次电源健重启电脑,按F8健进入安全模式2、安全模式下进入Windows系统桌面后,按住“winR”打开运行窗口,输入“services.msc”打开服务设置3、在服务界面,选中…...
2022/11/19 21:17:18 - 错误使用 reshape要执行 RESHAPE,请勿更改元素数目。
%读入6幅图像(每一幅图像的大小是564*564) f1 imread(WashingtonDC_Band1_564.tif); subplot(3,2,1),imshow(f1); f2 imread(WashingtonDC_Band2_564.tif); subplot(3,2,2),imshow(f2); f3 imread(WashingtonDC_Band3_564.tif); subplot(3,2,3),imsho…...
2022/11/19 21:17:16 - 配置 已完成 请勿关闭计算机,win7系统关机提示“配置Windows Update已完成30%请勿关闭计算机...
win7系统关机提示“配置Windows Update已完成30%请勿关闭计算机”问题的解决方法在win7系统关机时如果有升级系统的或者其他需要会直接进入一个 等待界面,在等待界面中我们需要等待操作结束才能关机,虽然这比较麻烦,但是对系统进行配置和升级…...
2022/11/19 21:17:15 - 台式电脑显示配置100%请勿关闭计算机,“准备配置windows 请勿关闭计算机”的解决方法...
有不少用户在重装Win7系统或更新系统后会遇到“准备配置windows,请勿关闭计算机”的提示,要过很久才能进入系统,有的用户甚至几个小时也无法进入,下面就教大家这个问题的解决方法。第一种方法:我们首先在左下角的“开始…...
2022/11/19 21:17:14 - win7 正在配置 请勿关闭计算机,怎么办Win7开机显示正在配置Windows Update请勿关机...
置信有很多用户都跟小编一样遇到过这样的问题,电脑时发现开机屏幕显现“正在配置Windows Update,请勿关机”(如下图所示),而且还需求等大约5分钟才干进入系统。这是怎样回事呢?一切都是正常操作的,为什么开时机呈现“正…...
2022/11/19 21:17:13 - 准备配置windows 请勿关闭计算机 蓝屏,Win7开机总是出现提示“配置Windows请勿关机”...
Win7系统开机启动时总是出现“配置Windows请勿关机”的提示,没过几秒后电脑自动重启,每次开机都这样无法进入系统,此时碰到这种现象的用户就可以使用以下5种方法解决问题。方法一:开机按下F8,在出现的Windows高级启动选…...
2022/11/19 21:17:12 - 准备windows请勿关闭计算机要多久,windows10系统提示正在准备windows请勿关闭计算机怎么办...
有不少windows10系统用户反映说碰到这样一个情况,就是电脑提示正在准备windows请勿关闭计算机,碰到这样的问题该怎么解决呢,现在小编就给大家分享一下windows10系统提示正在准备windows请勿关闭计算机的具体第一种方法:1、2、依次…...
2022/11/19 21:17:11 - 配置 已完成 请勿关闭计算机,win7系统关机提示“配置Windows Update已完成30%请勿关闭计算机”的解决方法...
今天和大家分享一下win7系统重装了Win7旗舰版系统后,每次关机的时候桌面上都会显示一个“配置Windows Update的界面,提示请勿关闭计算机”,每次停留好几分钟才能正常关机,导致什么情况引起的呢?出现配置Windows Update…...
2022/11/19 21:17:10 - 电脑桌面一直是清理请关闭计算机,windows7一直卡在清理 请勿关闭计算机-win7清理请勿关机,win7配置更新35%不动...
只能是等着,别无他法。说是卡着如果你看硬盘灯应该在读写。如果从 Win 10 无法正常回滚,只能是考虑备份数据后重装系统了。解决来方案一:管理员运行cmd:net stop WuAuServcd %windir%ren SoftwareDistribution SDoldnet start WuA…...
2022/11/19 21:17:09 - 计算机配置更新不起,电脑提示“配置Windows Update请勿关闭计算机”怎么办?
原标题:电脑提示“配置Windows Update请勿关闭计算机”怎么办?win7系统中在开机与关闭的时候总是显示“配置windows update请勿关闭计算机”相信有不少朋友都曾遇到过一次两次还能忍但经常遇到就叫人感到心烦了遇到这种问题怎么办呢?一般的方…...
2022/11/19 21:17:08 - 计算机正在配置无法关机,关机提示 windows7 正在配置windows 请勿关闭计算机 ,然后等了一晚上也没有关掉。现在电脑无法正常关机...
关机提示 windows7 正在配置windows 请勿关闭计算机 ,然后等了一晚上也没有关掉。现在电脑无法正常关机以下文字资料是由(历史新知网www.lishixinzhi.com)小编为大家搜集整理后发布的内容,让我们赶快一起来看一下吧!关机提示 windows7 正在配…...
2022/11/19 21:17:05 - 钉钉提示请勿通过开发者调试模式_钉钉请勿通过开发者调试模式是真的吗好不好用...
钉钉请勿通过开发者调试模式是真的吗好不好用 更新时间:2020-04-20 22:24:19 浏览次数:729次 区域: 南阳 > 卧龙 列举网提醒您:为保障您的权益,请不要提前支付任何费用! 虚拟位置外设器!!轨迹模拟&虚拟位置外设神器 专业用于:钉钉,外勤365,红圈通,企业微信和…...
2022/11/19 21:17:05 - 配置失败还原请勿关闭计算机怎么办,win7系统出现“配置windows update失败 还原更改 请勿关闭计算机”,长时间没反应,无法进入系统的解决方案...
前几天班里有位学生电脑(windows 7系统)出问题了,具体表现是开机时一直停留在“配置windows update失败 还原更改 请勿关闭计算机”这个界面,长时间没反应,无法进入系统。这个问题原来帮其他同学也解决过,网上搜了不少资料&#x…...
2022/11/19 21:17:04 - 一个电脑无法关闭计算机你应该怎么办,电脑显示“清理请勿关闭计算机”怎么办?...
本文为你提供了3个有效解决电脑显示“清理请勿关闭计算机”问题的方法,并在最后教给你1种保护系统安全的好方法,一起来看看!电脑出现“清理请勿关闭计算机”在Windows 7(SP1)和Windows Server 2008 R2 SP1中,添加了1个新功能在“磁…...
2022/11/19 21:17:03 - 请勿关闭计算机还原更改要多久,电脑显示:配置windows更新失败,正在还原更改,请勿关闭计算机怎么办...
许多用户在长期不使用电脑的时候,开启电脑发现电脑显示:配置windows更新失败,正在还原更改,请勿关闭计算机。。.这要怎么办呢?下面小编就带着大家一起看看吧!如果能够正常进入系统,建议您暂时移…...
2022/11/19 21:17:02 - 还原更改请勿关闭计算机 要多久,配置windows update失败 还原更改 请勿关闭计算机,电脑开机后一直显示以...
配置windows update失败 还原更改 请勿关闭计算机,电脑开机后一直显示以以下文字资料是由(历史新知网www.lishixinzhi.com)小编为大家搜集整理后发布的内容,让我们赶快一起来看一下吧!配置windows update失败 还原更改 请勿关闭计算机&#x…...
2022/11/19 21:17:01 - 电脑配置中请勿关闭计算机怎么办,准备配置windows请勿关闭计算机一直显示怎么办【图解】...
不知道大家有没有遇到过这样的一个问题,就是我们的win7系统在关机的时候,总是喜欢显示“准备配置windows,请勿关机”这样的一个页面,没有什么大碍,但是如果一直等着的话就要两个小时甚至更久都关不了机,非常…...
2022/11/19 21:17:00 - 正在准备配置请勿关闭计算机,正在准备配置windows请勿关闭计算机时间长了解决教程...
当电脑出现正在准备配置windows请勿关闭计算机时,一般是您正对windows进行升级,但是这个要是长时间没有反应,我们不能再傻等下去了。可能是电脑出了别的问题了,来看看教程的说法。正在准备配置windows请勿关闭计算机时间长了方法一…...
2022/11/19 21:16:59 - 配置失败还原请勿关闭计算机,配置Windows Update失败,还原更改请勿关闭计算机...
我们使用电脑的过程中有时会遇到这种情况,当我们打开电脑之后,发现一直停留在一个界面:“配置Windows Update失败,还原更改请勿关闭计算机”,等了许久还是无法进入系统。如果我们遇到此类问题应该如何解决呢࿰…...
2022/11/19 21:16:58 - 如何在iPhone上关闭“请勿打扰”
Apple’s “Do Not Disturb While Driving” is a potentially lifesaving iPhone feature, but it doesn’t always turn on automatically at the appropriate time. For example, you might be a passenger in a moving car, but your iPhone may think you’re the one dri…...
2022/11/19 21:16:57