You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Linux下DMA内存与设备间传输的代码修改及故障排查求助

问题描述

现有可运行的内存到内存DMA内核模块

我有一段能正常运行的Linux内核模块代码,实现内存到内存的DMA传输:

#include <linux/module.h>
#include <linux/init.h>
#include <linux/completion.h>
#include <linux/slab.h>
#include <linux/dmaengine.h>
#include <linux/dma-mapping.h>

void my_dma_transfer_completed(void *param)
{
    struct completion *cmp=(struct completion *)param;
    complete(cmp);
    printk("dma transfer completed callback\n");
}

static int __init dma_init (void)
{
    struct dma_device *dma_dev;
    dma_cap_mask_t mask;
    struct dma_chan *chan;
    struct dma_async_tx_descriptor *chan_desc;
    dma_cookie_t cookie;
    dma_addr_t src_addr,dst_addr;
    u8 *src_buf, *dst_buf;
    struct completion cmp;
    int status;
    
    printk(KERN_INFO "Loading module \n");
    
    dma_cap_zero(mask);
    dma_cap_set(DMA_MEMCPY,mask);
    chan=dma_request_channel(mask,NULL,NULL);
    if(!chan)
    {
        printk("chan request error\n");
        return ENODEV;
    }
    dma_dev = chan->device;
    
    src_buf=dma_alloc_coherent(chan->device->dev,1024,&src_addr,GFP_KERNEL);
    dst_buf=dma_alloc_coherent(chan->device->dev,1024,&dst_addr,GFP_KERNEL);

    memset(src_buf,0x12,1024);
    memset(dst_buf,0x00,1024);
    
    printk("Before %x\n",src_buf[0]);
    printk("Before %x\n",dst_buf[0]);

    chan_desc=dma_dev->device_prep_dma_memcpy(chan,dst_addr,src_addr,1024,DMA_MEM_TO_MEM);
    if(!chan_desc)
    {
        printk("chan desc request error\n");
        status=-1;
        goto free;
    }
    init_completion(&cmp);
    chan_desc->callback=my_dma_transfer_completed;
    chan_desc->callback_param=&cmp;
    cookie=dmaengine_submit(chan_desc);
    
    dma_async_issue_pending(chan);
    if(wait_for_completion_timeout(&cmp,msecs_to_jiffies(3000)) <= 0)
    {
        printk("timout\n");
        status=-1;

    }
    status=dma_async_is_tx_complete(chan,cookie,NULL,NULL); 
    if(status==DMA_SUCCESS)
    {
        printk("complete %d\n",status);
        status=0;
        printk("After %x\n",src_buf[0]);
        printk("After %x\n",dst_buf[0]);
    }
    else
    {
        printk("transfer error\n");
    }
    dmaengine_terminate_all(chan);
    
free:
    dma_free_coherent(chan->device->dev,1024,src_buf,src_addr);
    dma_free_coherent(chan->device->dev,1024,dst_buf,dst_addr);
    
    dma_release_channel(chan);
    return 0; 
} 


static void __exit dma_exit (void) 
{
    printk(KERN_INFO "Exitingg module \n");
}

module_init(dma_init);
module_exit(dma_exit);

MODULE_AUTHOR("Saeed Setareh");
MODULE_DESCRIPTION("DMA");
MODULE_LICENSE("GPL");

需求与问题

我需要修改这段代码实现内存到设备(如SPI、UART)或设备到内存的DMA传输,硬件平台是Orange Pi One(Allwinner H3 SOC,Linux 3.4.113内核)。

另外,我从Allwinner H3文档的DMA章节找到一段内存到设备的DMA代码,编译通过但运行时提示transfer error:

#include <linux/module.h>
#include <linux/init.h>
#include <linux/completion.h>
#include <linux/slab.h>
#include <linux/dmaengine.h>
#include <linux/dma-mapping.h>
#include <linux/dma/sunxi-dma.h>


void my_dma_transfer_completed(void *param)
{
    struct completion *cmp=(struct completion *)param;
    complete(cmp);
    printk("dma transfer completed callback\n");
}

static int __init dma_init (void)
{
    struct dma_chan *chan;
    dma_cap_mask_t mask; 
    dma_cookie_t cookie;
    struct dma_slave_config config; 
    struct dma_async_tx_descriptor *tx = NULL; 
    void *src_buf;
    struct completion cmp;
    dma_addr_t src_dma;
    int status;

    printk(KERN_INFO "Loading module \n");

    dma_cap_zero(mask); 
    dma_cap_set(DMA_SLAVE, mask); 
    dma_cap_set(DMA_CYCLIC, mask);

    chan = dma_request_channel(mask, NULL, NULL); 
    if (!chan)
    {
        printk("Request channel error\n");
        return -EINVAL;
    }

    src_buf = kmalloc(1024*4, GFP_KERNEL); 
    if (!src_buf) 
    {
        printk("kmalloc error\n");
        dma_release_channel(chan); 
        return -EINVAL;
    }

    src_dma = dma_map_single(NULL, src_buf, 1024*4, DMA_TO_DEVICE);

    config.direction = DMA_MEM_TO_DEV; 
    config.src_addr = src_dma; 
    config.dst_addr = 0x01c;
    config.src_addr_width = DMA_SLAVE_BUSWIDTH_2_BYTES; 
    config.dst_addr_width = DMA_SLAVE_BUSWIDTH_2_BYTES; 
    config.src_maxburst = 1;
    config.dst_maxburst = 1;
    config.slave_id = sunxi_slave_id(DRQDST_AUDIO_CODEC, DRQSRC_SDRAM);

    dmaengine_slave_config(chan, &config);

    tx = dmaengine_prep_dma_cyclic(chan, src_dma, 1024*4, 1024, DMA_MEM_TO_DEV, DMA_PREP_INTERRUPT | DMA_CTRL_ACK);
    if(!tx)
    {
        printk("chan desc request error\n");
        status=-1;
    }
    init_completion(&cmp);
    tx->callback = my_dma_transfer_completed; 
    tx->callback = NULL;

    cookie = dmaengine_submit(tx); 
    dma_async_issue_pending(chan);
    if(wait_for_completion_timeout(&cmp,msecs_to_jiffies(3000)) <= 0)
    {
        printk("timout\n");
        status=-1;

    }
    status=dma_async_is_tx_complete(chan,cookie,NULL,NULL); 
    if(status==DMA_SUCCESS)
    {
        printk("complete %d\n",status);
        status=0;
    }
    else
    {
        printk("transfer error\n");
    }
    dmaengine_terminate_all(chan);
    return 0; 
} 


static void __exit dma_exit (void) 
{
    printk(KERN_INFO "Exitingg module \n");
}

module_init(dma_init);
module_exit(dma_exit);

MODULE_AUTHOR("Saeed Setareh");
MODULE_DESCRIPTION("Low Level Driver");
MODULE_LICENSE("GPL");

请帮忙解决这个运行报错的问题,并给出正确的内存到设备DMA实现方案。


解决方案

针对Allwinner H3平台的DMA问题,先分析错误代码中的问题点,再给出修正后的实现:

错误代码中的关键问题

  1. 回调函数被覆盖:代码中先设置tx->callback = my_dma_transfer_completed;,随后又赋值tx->callback = NULL;,导致传输完成后无法触发完成信号,wait_for_completion_timeout会超时,最终判定传输失败。
  2. 目标地址错误:config.dst_addr = 0x01c;是无效的设备物理地址,需要使用对应外设(如UART、SPI)的实际DMA寄存器物理地址(例如H3的UART0 TX寄存器地址是0x01C28000 + 0x00)。
  3. DMA映射参数错误:dma_map_single(NULL, ...)中第一个参数应为设备指针(chan->device->dev),而不是NULL,否则DMA映射可能失败。
  4. 循环DMA使用不当:如果只是单次内存到设备传输,不需要使用DMA_CYCLIC,改用普通的DMA_SLAVE传输即可;循环DMA会重复传输,需要手动终止,否则会一直运行。
  5. 资源未释放:代码中没有调用dma_unmap_single和kfree释放分配的内存和DMA映射,会造成内存泄漏。

修正后的内存到设备(UART0 TX)DMA代码

以UART0为例,实现内存到设备的DMA传输,适配Allwinner H3平台:

#include <linux/module.h>
#include <linux/init.h>
#include <linux/completion.h>
#include <linux/slab.h>
#include <linux/dmaengine.h>
#include <linux/dma-mapping.h>
#include <linux/dma/sunxi-dma.h>
#include <mach/platform.h>

// Allwinner H3 UART0 物理基地址
#define UART0_BASE 0x01C28000
// UART0 TX寄存器偏移
#define UART_TX_REG 0x00

void my_dma_transfer_completed(void *param)
{
    struct completion *cmp = (struct completion *)param;
    complete(cmp);
    printk(KERN_INFO "DMA transfer completed callback\n");
}

static int __init dma_init(void)
{
    struct dma_chan *chan;
    dma_cap_mask_t mask;
    dma_cookie_t cookie;
    struct dma_slave_config config;
    struct dma_async_tx_descriptor *tx = NULL;
    void *src_buf;
    struct completion cmp;
    dma_addr_t src_dma;
    int status;
    int ret;

    printk(KERN_INFO "Loading DMA MEM-to-DEV module\n");

    // 初始化DMA能力掩码,仅请求SLAVE模式(非循环)
    dma_cap_zero(mask);
    dma_cap_set(DMA_SLAVE, mask);

    // 请求DMA通道,指定H3的DMA控制器
    chan = dma_request_channel(mask, NULL, NULL);
    if (!chan) {
        printk(KERN_ERR "Failed to request DMA channel\n");
        return -ENODEV;
    }

    // 分配可DMA的内存(用dma_alloc_coherent更可靠,避免手动映射)
    src_buf = dma_alloc_coherent(chan->device->dev, 1024, &src_dma, GFP_KERNEL);
    if (!src_buf) {
        printk(KERN_ERR "Failed to allocate DMA buffer\n");
        ret = -ENOMEM;
        goto release_chan;
    }

    // 填充测试数据
    memset(src_buf, 'A', 1024);

    // 配置DMA slave参数
    memset(&config, 0, sizeof(config));
    config.direction = DMA_MEM_TO_DEV;
    config.src_addr = src_dma;
    // 设置UART0 TX寄存器的物理地址
    config.dst_addr = UART0_BASE + UART_TX_REG;
    // 总线宽度匹配UART(1字节)
    config.src_addr_width = DMA_SLAVE_BUSWIDTH_1_BYTE;
    config.dst_addr_width = DMA_SLAVE_BUSWIDTH_1_BYTE;
    config.src_maxburst = 1;
    config.dst_maxburst = 1;
    // 指定UART0的DMA请求ID(H3中UART0 TX的DRQ是DRQDST_UART0_TX)
    config.slave_id = sunxi_slave_id(DRQDST_UART0_TX, DRQSRC_SDRAM);

    // 应用DMA配置
    ret = dmaengine_slave_config(chan, &config);
    if (ret) {
        printk(KERN_ERR "Failed to configure DMA slave: %d\n", ret);
        goto free_buf;
    }

    // 准备单次DMA传输描述符
    tx = dmaengine_prep_slave_single(
        chan,
        config.dst_addr,
        src_dma,
        1024,
        DMA_MEM_TO_DEV,
        DMA_PREP_INTERRUPT | DMA_CTRL_ACK
    );
    if (!tx) {
        printk(KERN_ERR "Failed to prepare DMA descriptor\n");
        ret = -ENOMEM;
        goto free_buf;
    }

    // 初始化完成量,设置回调函数
    init_completion(&cmp);
    tx->callback = my_dma_transfer_completed;
    tx->callback_param = &cmp;

    // 提交DMA传输
    cookie = dmaengine_submit(tx);
    if (dma_submit_error(cookie)) {
        printk(KERN_ERR "Failed to submit DMA transfer\n");
        ret = -EIO;
        goto free_buf;
    }

    // 启动DMA传输
    dma_async_issue_pending(chan);

    // 等待传输完成,超时3秒
    if (wait_for_completion_timeout(&cmp, msecs_to_jiffies(3000)) <= 0) {
        printk(KERN_ERR "DMA transfer timed out\n");
        ret = -ETIMEDOUT;
        goto terminate_chan;
    }

    // 检查传输状态
    status = dma_async_is_tx_complete(chan, cookie, NULL, NULL);
    if (status == DMA_SUCCESS) {
        printk(KERN_INFO "DMA transfer completed successfully\n");
        ret = 0;
    } else {
        printk(KERN_ERR "DMA transfer failed with status: %d\n", status);
        ret = -EIO;
    }

terminate_chan:
    // 终止DMA通道上的所有传输
    dmaengine_terminate_all(chan);

free_buf:
    // 释放DMA内存
    dma_free_coherent(chan->device->dev, 1024, src_buf, src_dma);

release_chan:
    // 释放DMA通道
    dma_release_channel(chan);

    return ret;
}

static void __exit dma_exit(void)
{
    printk(KERN_INFO "Unloading DMA MEM-to-DEV module\n");
}

module_init(dma_init);
module_exit(dma_exit);

MODULE_AUTHOR("Modified for Allwinner H3");
MODULE_DESCRIPTION("DMA MEM-to-DEV (UART0) for Orange Pi One");
MODULE_LICENSE("GPL");

关键注意事项

  1. 外设地址与DRQ ID:不同外设的物理地址和DMA请求ID需要参考Allwinner H3的官方手册,例如:
    • UART0 TX: DRQDST_UART0_TX,物理地址0x01C28000 + 0x00
    • SPI0 TX: DRQDST_SPI0_TX,物理地址0x01C05000 + 0x00
  2. 内存分配:优先使用dma_alloc_coherent分配DMA兼容内存,比kmalloc+dma_map_single更可靠,避免缓存一致性问题。
  3. 回调函数:必须正确设置回调函数和参数,确保传输完成后触发完成信号,避免超时。
  4. 资源释放:所有分配的DMA通道、内存都要在退出时释放,避免内存泄漏。
  5. 内核版本适配:Linux 3.4.113属于较老内核,部分DMA API可能与新内核不同,需参考对应内核版本的DMA驱动代码(如sunxi-dma.c)。

内容的提问来源于stack exchange,提问作者pirozy

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.03 13:00:11