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问题,先分析错误代码中的问题点,再给出修正后的实现:
错误代码中的关键问题
- 回调函数被覆盖:代码中先设置
tx->callback = my_dma_transfer_completed;,随后又赋值tx->callback = NULL;,导致传输完成后无法触发完成信号,wait_for_completion_timeout会超时,最终判定传输失败。 - 目标地址错误:
config.dst_addr = 0x01c;是无效的设备物理地址,需要使用对应外设(如UART、SPI)的实际DMA寄存器物理地址(例如H3的UART0 TX寄存器地址是0x01C28000 + 0x00)。 - DMA映射参数错误:
dma_map_single(NULL, ...)中第一个参数应为设备指针(chan->device->dev),而不是NULL,否则DMA映射可能失败。 - 循环DMA使用不当:如果只是单次内存到设备传输,不需要使用
DMA_CYCLIC,改用普通的DMA_SLAVE传输即可;循环DMA会重复传输,需要手动终止,否则会一直运行。 - 资源未释放:代码中没有调用
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");
关键注意事项
- 外设地址与DRQ ID:不同外设的物理地址和DMA请求ID需要参考Allwinner H3的官方手册,例如:
- UART0 TX:
DRQDST_UART0_TX,物理地址0x01C28000 + 0x00 - SPI0 TX:
DRQDST_SPI0_TX,物理地址0x01C05000 + 0x00
- UART0 TX:
- 内存分配:优先使用
dma_alloc_coherent分配DMA兼容内存,比kmalloc+dma_map_single更可靠,避免缓存一致性问题。 - 回调函数:必须正确设置回调函数和参数,确保传输完成后触发完成信号,避免超时。
- 资源释放:所有分配的DMA通道、内存都要在退出时释放,避免内存泄漏。
- 内核版本适配:Linux 3.4.113属于较老内核,部分DMA API可能与新内核不同,需参考对应内核版本的DMA驱动代码(如sunxi-dma.c)。
内容的提问来源于stack exchange,提问作者pirozy
相关产品推荐
相关产品推荐

