我的硬件平台采用的是AM335x,外部接了一片MAX10系列FPGA,再连接一片AD7616,采样并打入内部FIFO。FPGA和CPU之间采用16位GPMC总线连接,GPMC接口为最简单的异步读写,地址数据线不复用。经过几天的调试,实现了DMA+中断方式读取FIFO内的数据,现在将其共享出来,给大家使用。按照我写的调试文档,应该可以直接用起来GPMC的DMA了,如果还有问题,可以直接邮件联系:walei163@hotmail.com。下面是具体内容:
1、GPMC的DMA通道号为52,可以在手册中查到,但是默认在DTS中,被NAND FLash给占用了,如果要使用,则需要将DTS GPMC关于NAND这块修改为:
//ti,nand-xfer-type = "prefetch-dma";
ti,nand-xfer-type = "prefetch-polled";
即将prefetch-dma改为prefetch-polled,这样才能腾出52号DMA给GPMC使用。
2、在DTS中配置DMA一般为如下方式:
dmas = <&edma 52 0>;
dma-names = "rxtx";
并在程序中采用:
dma->chan = dma_request_chan(dma->dev, "rxtx");
来实现申请。内核代码会根据dma-names来搜索并匹配DMA通道号。不过奇怪的是,我按照这个方式申请可以成功,但是最后始终无法产生DMA的回调结束函数。
我采用如下方式申请(申请和初始化可以在probe函数里完成,也可以在open函数中完成):
//第一步:分配EDMA slave通道
dma_cap_zero(mask);
dma_cap_set(DMA_SLAVE, mask); //direction:device to memory
dma->channel = 52;
dma->chan = dma_request_channel(mask, fpga_dma_filter_fn, (void *)(long)dma->channel);
而回调函数 fpga_dma_filter_fn()可以如下实现:
static bool fpga_dma_filter_fn(struct dma_chan *chan, void *filter_param)
{
if (chan->chan_id == (int)filter_param) {
printk("DMA channel finded: %d\n", chan->chan_id);
return true;
}
return false;
}
3、申请成功后,需要再映射一段内存供DMA使用:
/* RX buffer */
dma->buf = dma_alloc_coherent(dma->chan->device->dev, DMA_BUFFER_SIZE, &dma->addr, GFP_KERNEL);
if (!dma->buf) {
printk("request DMA memory failed.\n");
goto err_dma_out;
}
注意此处申请的DMA_BUFFER_SIZE应按照字节数来申请大小。
4、然后需要设置slave_config:
//设定Source
dma->conf.direction = DMA_DEV_TO_MEM; //从设备到内存
dma->conf.src_addr_width = DMA_SLAVE_BUSWIDTH_2_BYTES; //16位数据,应按照2个字节传输
dma->conf.src_addr = CTRL_BANK_ADDR + ADC_DATA_CTRL; //FIFO的物理地址
dma->conf.src_maxburst = FGPA_M_NUMS; //burst应该按照21的整倍数或者能够被21整除的数
dmaengine_slave_config(dma->chan, &dma->conf);
注意这里的dma->conf.src_maxburst设置方法,需要按照你DMA一次传输的整帧数的整数倍或者能够被整除的数量来设置。比如我一包帧数是21个字,则此处可以设置为1,3,7,21等等。否则如果不能被整除,则余出来的将会被DMA丢弃掉。另外此处的设置数量不是字节数,而是要根据dma->conf.src_addr_width的设置来,比如我的FIFO传输的是16位的字,那么这里我设置的21就表示的是字的数量。
5、最后可以启动DMA传输(启动DMA传输我是在中断中启动的,即当FPGA的FIFO达到一定容量时,产生一个电平中断,然后在中断里启动一次DMA传输,将FIFO里的数据一次性全部取完):
//生成DMA描述符
desc = dmaengine_prep_slave_single(dma->chan, dma->addr,
//注意这里的data->data_num是要取的字节数量,如果要取10个字,这里需要填20
dma->data_num, DMA_DEV_TO_MEM,
(DMA_PREP_INTERRUPT | DMA_CTRL_ACK));
if (!desc) {
esam_prt("dmaengine_prep_slave_single failed.\n");
return -EBUSY;
}
init_completion(&dma->comp1);
desc->callback = fpga_dma_callback_func;
desc->callback_param = &dma->comp1;
dma->cookie = dmaengine_submit(desc);
if (dma_submit_error(dma->cookie)){
esam_prt("Failed to do dmaengine_submit.\n");
return -EBUSY;
}
dma_async_issue_pending(dma->chan);
wait_for_completion(&dma->comp1);
DMA传输如果顺利完成,则会调用我们设置的回调函数:
static void fpga_dma_callback_func(void *dma_async_param)
{
struct fpga_edma_dev *comp = dma_async_param;
complete(comp);
}
6、传输完成后,再将DMA内存中的数据复制到我们通过kmalloc映射的内存空间中,并向应用层发送异步通知:
int len = dma->data_num;
int i = 0;
//uint16_t *data = esam_dev.mem_start;
if ((fpga_dma_offset + len) > RECORD_BUF_SIZE) {
fpga_dma_offset = 0;
}
memcpy((esam_dev.mem_start + fpga_dma_offset), dma->buf, len);
fpga_dma_offset += len;
dma->sw->appdata.page_pos = (fpga_dma_offset / 2); //注意传递的都是word的位置
dma->sw->appdata.page_sum = (len / 2); //注意传递的都是word的数量
kill_fasync(&dma->sw->fasync, SIGIO, POLL_IN); //发送异步通知
当然也可以通过dma_maple_single函数将前面采用kmalloc映射的内存直接作为DMA内存来使用,但是考虑到CPU和DMA以及应用层都要争用这段内存空间,保险起见,我没有这样设计。
7、非常重要的一点,我采用的函数dmaengine_prep_slave_single如果要顺利工作,需要修改内核代码/drivers/dma/edma.c中的static struct dma_async_tx_descriptor *edma_prep_slave_sg函数,需要给edesc->pset[i].param.opt设置ITCCHEN属性。
原来的代码:
if (i == sg_len – 1)
/* Enable completion interrupt */
edesc->pset[i].param.opt |= TCINTEN;
else if (!((i+1) % MAX_NR_SG))
/*
* Enable early completion interrupt for the
* intermediateset. In this case the driver will be
* notified when the paRAM set is submitted to TC. This
* will allow more time to set up the next set of slots.
*/
edesc->pset[i].param.opt |= (TCINTEN | TCCMODE);
修改后的代码:
if (i == sg_len – 1)
/* Enable completion interrupt */
edesc->pset[i].param.opt |= (TCINTEN | ITCCHEN);
else if (!((i+1) % MAX_NR_SG))
/*
* Enable early completion interrupt for the
* intermediateset. In this case the driver will be
* notified when the paRAM set is submitted to TC. This
* will allow more time to set up the next set of slots.
*/
edesc->pset[i].param.opt |= (TCINTEN | TCCMODE | ITCCHEN);
这样GPMC的DMA就可以正常工作了。
8、GPMC cs段片选的读写时序就按照正常时序读写即可,具体可参照相关文档实现。
Jason.Wang:
忘了说一点,之前网上的一些文章,都是基于3.2内核的,过于老旧,现在我的这个是基于4.9内核的,是采用dmaengine模式实现。
yongqing wang:
回复 Jason.Wang:
感谢分享
Nancy Wang:
回复 Jason.Wang:
感谢分享!