Linux之DMA驱动详解(2)-sun6i-dma.c 驱动为例

一、DMA控制器硬件

1.1 DMA寄存器

DMA控制器 一般都会包含以下寄存器:

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

1.2  DMA描述符

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

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

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

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

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

1.3 LLI

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

路径:drivers/dma/sun6i-dma.c

/*
 * 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;
};
 

1.4 DMA request

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

1.5 DMA通道

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

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

二、DMA 驱动详解

2.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一般如下图所示:

 更多详情可以在文档 Documentation/devicetree/bindings/dma/dma-controller.ymal 中查看,展示如下:

 2.2 sun6i-dma.c 控制器驱动分析

驱动代码路经:drivers/dma/sun6i-dma.c

在驱动中,有多种使用 DMA 的方式及接口框架,本文将重点说明 dmaengine框架 的代码及使用。下面按照使用流程进行描述。

2.2.1 dmaengine初始化

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

/* forward declaration */
struct sun6i_dma_dev;

/*
 * Hardware channels / ports representation
 *
 * The hardware is used in several SoCs, with differing numbers
 * of channels and endpoints. This structure ties those numbers
 * to a certain compatible string.
 */
struct sun6i_dma_config {
        u32 nr_max_channels;
        u32 nr_max_requests;
        u32 nr_max_vchans;
        /*
         * In the datasheets/user manuals of newer Allwinner SoCs, a special
         * bit (bit 2 at register 0x20) is present.
         * It's named "DMA MCLK interface circuit auto gating bit" in the
         * documents, and the footnote of this register says that this bit
         * should be set up when initializing the DMA controller.
         * Allwinner A23/A33 user manuals do not have this bit documented,
         * however these SoCs really have and need this bit, as seen in the
         * BSP kernel source code.
         */
        void (*clock_autogate_enable)(struct sun6i_dma_dev *);
        void (*set_burst_length)(u32 *p_cfg, s8 src_burst, s8 dst_burst);
        void (*set_drq)(u32 *p_cfg, s8 src_drq, s8 dst_drq);
        void (*set_mode)(u32 *p_cfg, s8 src_mode, s8 dst_mode);
        u32 src_burst_lengths;
        u32 dst_burst_lengths;
        u32 src_addr_widths;
        u32 dst_addr_widths;
        bool has_mbus_clk;
};

/*
 * 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;
};


struct sun6i_desc {
        struct virt_dma_desc    vd;
        dma_addr_t              p_lli;
        struct sun6i_dma_lli    *v_lli;
};

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;
        struct clk              *clk_mbus;
        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;
        u32                     num_pchans;
        u32                     num_vchans;
        u32                     max_request;
};
....

/*
 * The V3s have only 8 physical channels, a maximum DRQ port id of 23,
 * and a total of 24 usable source and destination endpoints.
 */

static struct sun6i_dma_config sun8i_v3s_dma_cfg = {
        .nr_max_channels = 8,
        .nr_max_requests = 23,
        .nr_max_vchans   = 24,
        .clock_autogate_enable = sun6i_enable_clock_autogate_a23,
        .set_burst_length = sun6i_set_burst_length_a31,
        .set_drq          = sun6i_set_drq_a31,
        .set_mode         = sun6i_set_mode_a31,
        .src_burst_lengths = BIT(1) | BIT(8),
        .dst_burst_lengths = BIT(1) | BIT(8),
        .src_addr_widths   = BIT(DMA_SLAVE_BUSWIDTH_1_BYTE) |
                             BIT(DMA_SLAVE_BUSWIDTH_2_BYTES) |
                             BIT(DMA_SLAVE_BUSWIDTH_4_BYTES),
        .dst_addr_widths   = BIT(DMA_SLAVE_BUSWIDTH_1_BYTE) |
                             BIT(DMA_SLAVE_BUSWIDTH_2_BYTES) |
                             BIT(DMA_SLAVE_BUSWIDTH_4_BYTES),
};

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 },
        { .compatible = "allwinner,sun50i-a64-dma", .data = &sun50i_a64_dma_cfg },
        { .compatible = "allwinner,sun50i-h6-dma", .data = &sun50i_h6_dma_cfg },
        { /* sentinel */ }
};

MODULE_DEVICE_TABLE(of, sun6i_dma_match);

static int sun6i_dma_probe(struct platform_device *pdev)
{
        struct device_node *np = pdev->dev.of_node;
        struct sun6i_dma_dev *sdc;
        struct resource *res;
        int ret, i;

        /* 申请dma控制器所需内存 */
        sdc = devm_kzalloc(&pdev->dev, sizeof(*sdc), GFP_KERNEL);
        if (!sdc)
                return -ENOMEM;

        /* 从设备树中获取device cfg */
        sdc->cfg = of_device_get_match_data(&pdev->dev);
        if (!sdc->cfg)
                return -ENODEV;

        /* 获取设备树中的寄存器地址 */
        res = platform_get_resource(pdev, IORESOURCE_MEM, 0);

        /* 将寄存器地址映射为虚拟地址以供CPU使用 */
        sdc->base = devm_ioremap_resource(&pdev->dev, res);
        if (IS_ERR(sdc->base))
                return PTR_ERR(sdc->base);

        /* 获取DMA中断 */
        sdc->irq = platform_get_irq(pdev, 0);
        if (sdc->irq < 0)
                return sdc->irq;

         /* 获取DMA时钟 */
        sdc->clk = devm_clk_get(&pdev->dev, NULL);
        if (IS_ERR(sdc->clk)) {
                dev_err(&pdev->dev, "No clock specified\n");
                return PTR_ERR(sdc->clk);
        }

        if (sdc->cfg->has_mbus_clk) {
                sdc->clk_mbus = devm_clk_get(&pdev->dev, "mbus");
                if (IS_ERR(sdc->clk_mbus)) {
                        dev_err(&pdev->dev, "No mbus clock specified\n");
                        return PTR_ERR(sdc->clk_mbus);
                }
        }

        sdc->rstc = devm_reset_control_get(&pdev->dev, NULL);
        if (IS_ERR(sdc->rstc)) {
                dev_err(&pdev->dev, "No reset controller specified\n");
                return PTR_ERR(sdc->rstc);
        }

        /* 创建dma内存池,以存放LLI,后续使用会从该池中取出LLI并填充 */
        sdc->pool = dmam_pool_create(dev_name(&pdev->dev), &pdev->dev,
                                     sizeof(struct sun6i_dma_lli), 4, 0);
        if (!sdc->pool) {
                dev_err(&pdev->dev, "No memory for descriptors dma pool\n");
                return -ENOMEM;
        }

        platform_set_drvdata(pdev, sdc);


        /*初始化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              = sdc->cfg->src_addr_widths;
        sdc->slave.dst_addr_widths              = sdc->cfg->dst_addr_widths;
        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;

        sdc->num_pchans = sdc->cfg->nr_max_channels;
        sdc->num_vchans = sdc->cfg->nr_max_vchans;
        sdc->max_request = sdc->cfg->nr_max_requests;

        ret = of_property_read_u32(np, "dma-channels", &sdc->num_pchans);
        if (ret && !sdc->num_pchans) {
                dev_err(&pdev->dev, "Can't get dma-channels.\n");
                return ret;
        }

        ret = of_property_read_u32(np, "dma-requests", &sdc->max_request);
        if (ret && !sdc->max_request) {
                dev_info(&pdev->dev, "Missing dma-requests, using %u.\n",
                         DMA_CHAN_MAX_DRQ_A31);
                sdc->max_request = DMA_CHAN_MAX_DRQ_A31;
        }

        /*
         * If the number of vchans is not specified, derive it from the
         * highest port number, at most one channel per port and direction.
         */
        if (!sdc->num_vchans)
                sdc->num_vchans = 2 * (sdc->max_request + 1);
        
        /* 申请内存存放DMA物理通道信息 */
        sdc->pchans = devm_kcalloc(&pdev->dev, sdc->num_pchans,
                                   sizeof(struct sun6i_pchan), GFP_KERNEL);
        if (!sdc->pchans)
                return -ENOMEM;
        /* 申请内存存放DMA虚拟通道信息 */

        sdc->vchans = devm_kcalloc(&pdev->dev, sdc->num_vchans,
                                   sizeof(struct sun6i_vchan), GFP_KERNEL);
        if (!sdc->vchans)
                return -ENOMEM;
        
        * 初始化tasklet队列,DMA传输任务在这个tasklet中执行 */
        tasklet_setup(&sdc->task, sun6i_dma_tasklet);

        
        /* 初始化DMA物理通道信息,包括通道号及通道所属寄存器 */
        for (i = 0; i < sdc->num_pchans; i++) {
                struct sun6i_pchan *pchan = &sdc->pchans[i];

                pchan->idx = i;
                pchan->base = sdc->base + 0x100 + i * 0x40;
        }
        
        /* 初始化DMA虚拟通道信息 */
        for (i = 0; i < sdc->num_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);
        }

        ret = reset_control_deassert(sdc->rstc);
        if (ret) {
                dev_err(&pdev->dev, "Couldn't deassert the device from reset\n");
                goto err_chan_free;
        }

        ret = clk_prepare_enable(sdc->clk);
        if (ret) {
                dev_err(&pdev->dev, "Couldn't enable the clock\n");
                goto err_reset_assert;
        }

        if (sdc->cfg->has_mbus_clk) {
                ret = clk_prepare_enable(sdc->clk_mbus);
                if (ret) {
                        dev_err(&pdev->dev, "Couldn't enable mbus clock\n");
                        goto err_clk_disable;
                }
        }

        /* 注册DMA中断 */
        ret = devm_request_irq(&pdev->dev, sdc->irq, sun6i_dma_interrupt, 0,
                               dev_name(&pdev->dev), sdc);
        if (ret) {
                dev_err(&pdev->dev, "Cannot request IRQ\n");
                goto err_mbus_clk_disable;
        }
        /* 注册DMA控制器到 dmaengine,这个函数是dma core 的api*/
        ret = dma_async_device_register(&sdc->slave);
        if (ret) {
                dev_warn(&pdev->dev, "Failed to register DMA engine device\n");
                goto err_irq_disable;
        }
        
        /* 注册DMA回调sun6i_dma_of_xlate到DMA控制器,该回调会在申请DMA通道时用到 */

        ret = of_dma_controller_register(pdev->dev.of_node, sun6i_dma_of_xlate,
                                         sdc);
        if (ret) {
                dev_err(&pdev->dev, "of_dma_controller_register failed\n");
                goto err_dma_unregister;
        }

        if (sdc->cfg->clock_autogate_enable)
                sdc->cfg->clock_autogate_enable(sdc);

        return 0;

err_dma_unregister:
        dma_async_device_unregister(&sdc->slave);
err_irq_disable:
        sun6i_kill_tasklet(sdc);
err_mbus_clk_disable:
        clk_disable_unprepare(sdc->clk_mbus);
err_clk_disable:
        clk_disable_unprepare(sdc->clk);
err_reset_assert:
        reset_control_assert(sdc->rstc);
err_chan_free:
        sun6i_dma_free(sdc);
        return ret;
}

vchan_init()函数 是virt-dma.c 提供的接口,实现虚拟通道的作用

void vchan_init(struct virt_dma_chan *vc, struct dma_device *dmadev)
{
        dma_cookie_init(&vc->chan);

        spin_lock_init(&vc->lock);

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

        INIT_LIST_HEAD(&vc->desc_allocated);
        INIT_LIST_HEAD(&vc->desc_submitted);
        INIT_LIST_HEAD(&vc->desc_issued);
        INIT_LIST_HEAD(&vc->desc_completed);
        INIT_LIST_HEAD(&vc->desc_terminated);

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

        tasklet_setup(&vc->task, vchan_complete);

        vc->chan.device = dmadev;

/* 将虚拟通道挂到dma_device的channels上 */
        list_add_tail(&vc->chan.device_node, &dmadev->channels);
}
EXPORT_SYMBOL_GPL(vchan_init);

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

cap_mask: 表示 controller 的传输能力,需要和后面 device_prep_dma_xxx 形式的回调函数对应。常见取值如下 :
DMA_MEMCPY: 可进行 memory copy 。
DMA_MEMSET: 可进行 memory set 。
DMA_SG: 可进行 scatter list 传输。
DMA_CYCLIC: 可进行 cyclic 类的传输。
DMA_INTERLEAVE: 可进行交叉传输。

2.2.2 DMA cookie 

DMA cookie 表示 DMA engine 在数据传送中使用的一段连续内存。

static inline void dma_cookie_init(struct dma_chan *chan)
static inline dma_cookie_t dma_cookie_assign(struct dma_async_tx_descriptor *tx)
static inline void dma_cookie_complete(struct dma_async_tx_descriptor *tx)
static inline enum dma_status dma_cookie_status(struct dma_chan *chan, dma_cookie_t cookie, struct dma_tx_state *state)

dma_cookie_init:初 始化 channel 中的 cookie 、 completed_cookie 。
dma_cookie_assign: 为指针的传输描述分配一个 cookie 。
dma_cookie_complete:当 一个传输完成时,可调用该接口更新该传输所对应 channel 的
completed_cookie 字段。
dma_cookie_status: 获取指定 channel 指 定 cookie 的传输状态。
2.2.3注册DMA

int dma_async_device_register(struct dma_device *device)

注 册成功后 dma_device 会放在一个名称为 dma_device_list 的全局链表上,以便后面使用。 dma_async_device_unregister 是相对的注销函数。

2.3  DMA client 驱动框架

上面的DMA控制器驱动加载后,可以供DMA slave ,也就是DMA client 使用。

2.3.1 申请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.3.2  配置DMA通道的参数

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

static inline int dmaengine_slave_config(struct dma_chan *chan, struct dma_slave_config *config)

  • chanDMA通道 描述结构体指针
  • configDMA通道配置 结构体指针

这里需要说名一下 *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函数 初始化时注册了,可以回顾前面的代码进行了解;

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.3.3  获取通道的传输描述符

使用 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;
}
 

transfer size、transfer wide、burst size
       1. transfer wide 可以理解为单次传输数据的大小,和串口来比较的话,串口一次只能传一个字节,而 DMA 则可以选择一次能传输的数据大小。在此基础上的 transfer size 则是传输的次数,而不是单纯的总大小,也就是说 DMA 传输总长度实际上是 transfer size 乘上 transfer wide 。
        2.burst size 是指  DMAC 内部缓存大小。当  DMA 传输的源或目的是内存 memory 时, DMAC 会先读取数据到缓存,再传入或传出。

留一个问题:这上面的p_chan 链表和 v_chan 链表的作用和关系是什么样的?

2.3.4  提交传输请求

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

dma_cookie_t dmaengine_submit(struct dma_async_tx_descriptor *desc)

 其调用:

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.3.5 启动传输

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

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.3.6 传输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已经被占用。继续下一个pchan
          如果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.3.7 传输完成

当完成一次 传输任务后,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

2.4 dma buffer

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

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

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

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

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

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

2.4.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
    /* 这个分支是从内存中去分配 */
    ->dma_alloc_direct
   

 dma_alloc_attrs 实现 在:kernel/dma/mapping.c 

void *dma_alloc_attrs(struct device *dev, size_t size, dma_addr_t *dma_handle,
                gfp_t flag, unsigned long attrs)
{
        const struct dma_map_ops *ops = get_dma_ops(dev);
        void *cpu_addr;

        WARN_ON_ONCE(!dev->coherent_dma_mask);

        if (dma_alloc_from_dev_coherent(dev, size, dma_handle, &cpu_addr))
                return cpu_addr;

        /* let the implementation decide on the zone to allocate from: */
        flag &= ~(__GFP_DMA | __GFP_DMA32 | __GFP_HIGHMEM);

        if (dma_alloc_direct(dev, ops))
                cpu_addr = dma_direct_alloc(dev, size, dma_handle, flag, attrs);
        else if (ops->alloc)
                cpu_addr = ops->alloc(dev, size, dma_handle, flag, attrs);
        else
                return NULL;

        debug_dma_alloc_coherent(dev, size, *dma_handle, cpu_addr);
        return cpu_addr;
}
EXPORT_SYMBOL(dma_alloc_attrs);
 

 

dma_alloc_cohrent通过几种不同的方式分配DMA内存:

优先从Device Cohorent Pool申请内存,这是设备专用的DMA内存池;
若设备使用直接映射,通过dma_direct_alloc分配内存;
若设备使用IOMMU,通过IOMMU提供的操作接口分配内存。

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

2.4.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

 

dma_map_single支持两种方式映射DMA内存:

  • 直接映射方式,底层使用SWIOTLB机制实现;
  • 设备支持IOMMU,通过IOMMU提供的操作接口映射内存。

数据同步:

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

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? */
}

 如果我们需要对一块 流式映射 的 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)

 1、dma_sync_single_for_cpu->dma_direct_sync_single_for_cpu

oid dma_sync_single_for_cpu(struct device *dev, dma_addr_t addr, size_t size,
                enum dma_data_direction dir)
{
        const struct dma_map_ops *ops = get_dma_ops(dev);

        BUG_ON(!valid_dma_direction(dir));
        if (dma_map_direct(dev, ops))
                dma_direct_sync_single_for_cpu(dev, addr, size, dir);
        else if (ops->sync_single_for_cpu)
                ops->sync_single_for_cpu(dev, addr, size, dir);
        debug_dma_sync_single_for_cpu(dev, addr, size, dir);
}
EXPORT_SYMBOL(dma_sync_single_for_cpu);
 


如果你需要多次访问同一个流式映射DMA缓冲区,并且在DMA传输之间读写DMA缓冲区上的数据,这时候你需要使用dma_sync_single_for_cpu进行DMA缓冲区的sync操作,以便CPU和设备可以看到最新的、正确的数据,。

2、 dma_sync_single_for_device

void dma_sync_single_for_device(struct device *dev, dma_addr_t addr,
                size_t size, enum dma_data_direction dir)
{
        const struct dma_map_ops *ops = get_dma_ops(dev);

        BUG_ON(!valid_dma_direction(dir));
        if (dma_map_direct(dev, ops))
                dma_direct_sync_single_for_device(dev, addr, size, dir);
        else if (ops->sync_single_for_device)
                ops->sync_single_for_device(dev, addr, size, dir);
        debug_dma_sync_single_for_device(dev, addr, size, dir);
}
EXPORT_SYMBOL(dma_sync_single_for_device);
 

dma_direct_sync_single_for_device
如果CPU操作了DMA缓冲区的数据,然后你又想让硬件设备访问DMA缓冲区,这时候,在真正让硬件设备去访问DMA缓冲区之前,你需要调用dma_direct_sync_single_for_device接口以便让硬件设备可以看到cpu更新后的数据。

Linux之DMA驱动(3)是写一个client驱动代码,看如何使用DMA 。

  • 24
    点赞
  • 12
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
W9825G6KH-6I是一种外部SDRAM芯片,用于提供较大的运行内存,特别适用于需要缓存显示屏的情况。它具有1280*800分辨率的显示屏,每帧缓存需要1Mb~3Mb的内存。在STM32F4xx内部无法运行,因此需要使用外部缓存。需要注意的是,该芯片在掉电后数据会迅速丢失,重启后需要特别注意这一特性。SDRAM和SRAM有所区别,引脚和程序也不通用。一般情况下,我们使用SDRAM,因为它的性价比高且有丰富的资料支持。SDRAM的结构复杂,但使用起来相对简单。初始化后,只需要记住一个地址即可,程序上可以忘记它的存在。W9825G6KH-6I是一种常用的SDRAM芯片,市场上有很多可替代的芯片,价格在15元左右。在硬件连接方面,可以参考原理图和引脚说明进行连接。在程序使用方面,需要进行初始化和基本函数的调用,然后进行正常使用。总结起来,对于W9825G6KH-6I的使用,需要将SDRAM模式中的CAS latency设置为3,并将SDCLK周期设置为2个FMC周期。此外,还需要注意突发读使能等相关设置。在主函数中调用SDRAM_Cmd_Init()和SDRAM_Test()函数即可。如果初始化指令出现问题,可以通过修改下载方式为under reset,然后按住复位键,点击下载键,复位后立即松开来解决问题。如果有其他问题或需要源码,请在评论区下方留言或留下邮箱联系。\[1\]\[2\]\[3\] #### 引用[.reference_title] - *1* [STM32F4xx SDRAM(更新中)](https://blog.csdn.net/zhouml_msn/article/details/90052820)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v91^control_2,239^v3^insert_chatgpt"}} ] [.reference_item] - *2* [STM32H750 SDRAM W9825G6KH-6](https://blog.csdn.net/smallerlang/article/details/128061724)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v91^control_2,239^v3^insert_chatgpt"}} ] [.reference_item] - *3* [STM32F429IGT6项目准备5——使用STM32CubeMX配置SDRAM](https://blog.csdn.net/qq_42039294/article/details/112221396)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v91^control_2,239^v3^insert_chatgpt"}} ] [.reference_item] [ .reference_list ]
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值