好久没有认真的写一些技术博客了,工作半年了,最近两个月好像才慢慢的恢复过来了,不能摸鱼了,2020年,自己的生活中会有很多可见的变化,要快速成长啊,具备与之匹配的技术和能力啊。
由于项目的需要,利用一周的时间测试了zcu104开发板DMA的实际带宽。之前就用过AXI DMA做过图像处理方面的东西,还以为这次两天驾轻就熟,两天就能做好呢,结果细细的研究了一下,才发现还是有很多的坑的,学无止境啊,自己也记录一下。
工程block design的整体设计如下:
AXI DMA的配置如下:
DMA没有开启Scatter/Gather模式,只使用了DMA的简单模式,width of buffer length register 选择了24bits,因为本次测试,最大传输的数据是634*1600个,传输的数据数不能大于2^24。
zcu104使用的是zynq Ultrascale MPsoc的芯片,不同于zynq7000系列AXI_HP,AXI_GP,AXI_ACP的接口类型,AXI接口更加细化。选中与DMA相连的接口。
综合,布局布线,generate bitstream。
PS程序也写的挺简单的,直接上代码吧。
#include
#include "platform.h"
#include "xil_printf.h"
#include "xaxidma.h"
#include "xparameters.h"
#include "xscugic.h"
#include "Xtime_l.h"
#include "sleep.h"
#define DMA_DEV_ID XPAR_AXIDMA_0_DEVICE_ID
#define wt_INTR_ID XPAR_FABRIC_AXIDMA_0_S2MM_INTROUT_VEC_ID
#define rd_INTR_ID XPAR_FABRIC_AXIDMA_0_MM2S_INTROUT_VEC_ID
#define INTC XScuGic
#define INTC_DEVICE_ID XPAR_SCUGIC_SINGLE_DEVICE_ID
#define DMA_0_BASE_ADDR 0x1000000
#define Trans_Len (634*1600)*4 //这里要乘以4,因为一个32位的数据占用4个地址空间
#define CPU_COUNTS_PER_SECOND COUNTS_PER_SECOND
static int SetupIntcSystem(INTC * IntcInstancePtr,
XAxiDma * AxiDmaPtr, u16 TxIntrId, u16 RxIntrId);
static int TxIntrHandler(void *param);//read
static int RxIntrHandler(void *param);//write
XTime Tsetup_wr,Tend_wr,Tsetup_rd,Tend_rd;
static XAxiDma AxiDma;
u32 *wr_dma_ptr;
u32 *rd_dma_ptr;
int main()
{
//====================================
XAxiDma_Config *config = NULL;
int status;
static INTC intc;
wr_dma_ptr = (u32 *)DMA_0_BASE_ADDR;
rd_dma_ptr = (u32 *)DMA_0_BASE_ADDR;
printf("zcu104 DMA test begin!!!\n\r");
config = XAxiDma_LookupConfig(DMA_DEV_ID);
if(!config){
printf("ther is no DMA Dev found!!!\n\r");
return XST_FAILURE;
}
status = XAxiDma_CfgInitialize(&AxiDma,config);//config->AxiDma
if(status != XST_SUCCESS){
printf("Initialization failed \n");
return XST_FAILURE;
}
if(XAxiDma_HasSg(&AxiDma)== TRUE){
printf("Device configured as SG mode \n");
}
else{
printf("Device configured is NOT SG mode \n");
}
//设置 DMA的中断
status = SetupIntcSystem(&intc,&AxiDma,rd_INTR_ID,wt_INTR_ID);
if(status != XST_SUCCESS){
printf("DMA intc failed!!!\n");
return XST_FAILURE;
}
//=====================================================================test time
XTime T1,T2;
u32 T3;
XTime_GetTime(&T1);
usleep(1314);
XTime_GetTime(&T2);
T3 = ((T2-T1)*1000000)/CPU_COUNTS_PER_SECOND;
printf("elapsed time is %d us \n\r",T3);
//=======================================================================
/* Disable all interrupts before setup */
XAxiDma_IntrDisable(&AxiDma,XAXIDMA_IRQ_ALL_MASK,XAXIDMA_DMA_TO_DEVICE);
XAxiDma_IntrDisable(&AxiDma,XAXIDMA_IRQ_ALL_MASK,XAXIDMA_DEVICE_TO_DMA);
/* Enable all interrupts */
XAxiDma_IntrEnable(&AxiDma,XAXIDMA_IRQ_ALL_MASK,XAXIDMA_DMA_TO_DEVICE);
XAxiDma_IntrEnable(&AxiDma,XAXIDMA_IRQ_ALL_MASK,XAXIDMA_DEVICE_TO_DMA);
//----------------------------flush cathe before setup DMA trans
XTime_GetTime(&Tsetup_wr);
Xil_DCacheFlushRange((INTPTR)wr_dma_ptr,Trans_Len);
status = XAxiDma_SimpleTransfer(&AxiDma,(INTPTR)wr_dma_ptr,Trans_Len,XAXIDMA_DEVICE_TO_DMA);
if(status != XST_SUCCESS){
printf("setup data write to DMA failed!!!\n");
return XST_FAILURE;
}
else{
printf("setup data write to DMA success !!!\n");
}
sleep(1);
//after trans data to DDR,start reading from DDR
XTime_GetTime(&Tsetup_rd);
status = XAxiDma_SimpleTransfer(&AxiDma,(INTPTR)rd_dma_ptr,Trans_Len,XAXIDMA_DMA_TO_DEVICE);
if(status != XST_SUCCESS){
printf("setup read data from DMA failed!!!\n");
return XST_FAILURE;
}
else{
printf("setup read data from DMA success!!!\n");
}
while(1){
};
return 0;
}
static int SetupIntcSystem(INTC * IntcInstancePtr,
XAxiDma * AxiDmaPtr, u16 TxIntrId, u16 RxIntrId){
int Status;
XScuGic_Config *IntcConfig;
/*
* Initialize the interrupt controller driver so that it is ready to
* use.
*/
IntcConfig = XScuGic_LookupConfig(INTC_DEVICE_ID);
if (IntcConfig == NULL) {
print("INTC device ID is wrong!!!\n");
return XST_FAILURE;
}
Status = XScuGic_CfgInitialize(IntcInstancePtr, IntcConfig,
IntcConfig->CpuBaseAddress);
if (Status != XST_SUCCESS) {
return XST_FAILURE;
}
XScuGic_SetPriorityTriggerType(IntcInstancePtr, RxIntrId, 0xA0, 0x3);
XScuGic_SetPriorityTriggerType(IntcInstancePtr, TxIntrId, 0xA0, 0x3);//MM2S 03为上升沿触发中断
/*
* Connect the device driver handler that will be called when an
* interrupt for the device occurs, the handler defined above performs
* the specific interrupt processing for the device.
*/
Status = XScuGic_Connect(IntcInstancePtr, RxIntrId,
(Xil_InterruptHandler)RxIntrHandler,
AxiDmaPtr);
if (Status != XST_SUCCESS) {
return Status;
}
Status = XScuGic_Connect(IntcInstancePtr, TxIntrId,
(Xil_InterruptHandler)TxIntrHandler,
AxiDmaPtr);
if (Status != XST_SUCCESS) {
return Status;
}
XScuGic_Enable(IntcInstancePtr, RxIntrId);
XScuGic_Enable(IntcInstancePtr, TxIntrId);
/* Enable interrupts from the hardware */
Xil_ExceptionInit();
Xil_ExceptionRegisterHandler(XIL_EXCEPTION_ID_INT,
(Xil_ExceptionHandler)XScuGic_InterruptHandler,
(void *)IntcInstancePtr);
Xil_ExceptionEnable();
//====================================
return XST_SUCCESS;
}
static int RxIntrHandler(void *param){
u32 Tuse;
int Speed_wr_DMA;
printf("into RxIntr process!!!\n");
XTime_GetTime(&Tend_wr);
Tuse = ((Tend_wr-Tsetup_wr)*1000000)/CPU_COUNTS_PER_SECOND;
printf("wr data elapsed time is %d us \n",Tuse);
Speed_wr_DMA = (634*1600*32/8/1024/1024*1000*1000)/Tuse;
printf("data translate to DMA DDR is %d MB/s \n\r",Speed_wr_DMA);
// XTime_GetTime(&Tsetup_rd);
return XST_SUCCESS;
}
static int TxIntrHandler(void *param){
u32 Tuse;
int Speed_rd_DMA;
printf("into TXIntr process!!!\n");
XTime_GetTime(&Tend_rd);
Tuse = ((Tend_rd-Tsetup_rd)*1000000)/CPU_COUNTS_PER_SECOND;
printf("read data elapsed time is %d us \n",Tuse);
Speed_rd_DMA = (634*1600*32/8/1024/1024*1000*1000)/Tuse;
printf("read data from DMA DDR is %d MB/s \n\r",Speed_rd_DMA);
// printf("read data from DMA DDR is %d \n\r",Tsetup_rd);
// printf("read data from DMA DDR is %d \n\r",Tend_rd);
return XST_SUCCESS;
}
关于cache一致性,CPU在访问内存时,首先判断所要访问的内容是否在Cache中,如果在,就称为“命中(hit)”,此时CPU直接从Cache中调用该内容;否则,就 称为“ 不命中”,CPU只好去内存中调用所需的子程序或指令了。CPU不但可以直接从Cache中读出内容,也可以直接往其中写入内容。由于Cache的存取速 率相当快,使得CPU的利用率大大提高,进而使整个系统的性能得以提升。
Cache的一致性就是直Cache中的数据,与对应的内存中的数据是一致的。DMA是直接操作总线地址的,这里先当作物理地址来看待吧(系统总线地址和物理地址只是观察内存的角度不同)。如果cache缓存的内存区域不包括DMA分配到的区域,那么就没有一致性的问题。但是如果cache缓存包括了DMA目的地址的话,会出现什么什么问题呢?
问题出在,经过DMA操作,cache缓存对应的内存数据已经被修改了,而CPU本身不知道(DMA传输是不通过CPU的),它仍然认为cache中的数 据就是内存中的数据,以后访问Cache映射的内存时,它仍然使用旧的Cache数据。这样就发生Cache与内存的数据“不一致性”错误。所以,在使用DMA写入/读取内存中的一段区域的时候,应该告诉CPU这段区域不能的cache无效了,也就是把cache中的数据送还到内存中,使用的函数是
:
Xil_DCacheFlushRange((INTPTR)wr_dma_ptr,Trans_Len);
要注意DMA操作DDR3,CPU是不知道的,要用dcacheflush及时刷新。这个刷新速度很慢的,要注意控制刷新的长度。
关于中断,硬件工程师还是有点没完全弄懂啊,在这里有个提醒吧,可千万别配置完DMA传输就直接return 0了,return 0之后就再也不会进去中断了TT
配置完成AXI DMA开始进行测试的时候,发现DMA的时序和我想象中的有些不一样。
(1)关于DMA的tready信号
之前一直脑补的是,在PS配置完DMA IP核的相关寄存器后,tready信号才拉高,数据传输开始进行。但是用逻辑分析仪抓取信号之后,发现不是这样的。
上图是上电后,PS进行debug,烧写bit文件之后的相关时序。注意,还没有开始执行main.cpp程序。刚烧写进去,还没有DMA进行配置,由上面数据的计数可知,tready已经有了4个clock的高电平了。但是这4个数据并没有被读入DDR中,而是在配置DMA传输中丢失了。个人分析原因,是因为PL端的配置速度比PS端快的多,当烧写工程文件进行整个系统的初始化时,PL很快就完成了初始化配置,但是PS较慢。
在PS对DMA进行配置后,还会有4个clock 的tready高电平信号,此时这4个数据可以被正确地写入到DDR中去。发送数据tlast后,也会有额外的4个clock 的tready高电平信号,也可以被正确的写入,所以理论上应该每次发送时,都会有4个数据被提前写入。
解决 AXI DMA发送丢失4个数据的方式,可以通过s2mm_introut或者PS驱动GPIO的方式,告诉上一级数据源,PS已经完成对AXI DMA的配置,然后开始发送数据。
(2)读写数据的实际数据长度
在每次DMA IP进行数据传输之前,都需要配置一次DMA相关的寄存器,告诉本次传输的起始地址、数据长度和方向。在MM2S通道,配置要读取的数据长度,很好理解。DMA 需要根据用户配置的数据长度来产生tlast信号。但是在S2MM通道,配置要写入内存的数据长度,在用户看来,其实并不太必要,因为一方面,每一帧传输的数据量可能并不固定,在传输之前未知;另一方面,DMA IP核完全可以根据PL端送来的tlast信号来判断数据传输完成。所以,DMA S2MM通道配置的数据长度,其实并不一定是本次传输的准确值,只需要大于等于本次数据量,DMA根据tlast信号判断本帧数据是否结束,完成本数据传输后,DMA会将本次传输真实的数据量写入数据长度配置寄存器S2MM_LENGTH中。
详细的说明如下:
Xilinx的例程代码向来都是异常复杂,调到最后不免都要去看寄存器,翻阅手册我们看到DMA简单模式如何使用。要注意,MM2S是说 memory map to stream,S2MM是stream to memory map,memory map当然是指DDR3内存了,根据名称我们就知道MM2S是说DMA把数据从DDR3搬移到stream FIFO中。 从下面手册中我们看到了寄存器偏移地址从00h-28h 是mm2s的,30h-58h是s2mm的。30h是CR寄存器,有RS,reset等寄存器位,后面都有说明。
判断DMA发送的SR寄存器idle是否为1,为1则idle,说明发送结束。
while (XAxiDma_Busy(&AxiDma,XAXIDMA_DMA_TO_DEVICE))
a.不使能scatter gather和miro DMA,使用简单模式,前者可以配置多个地址,一次开启多次传输。width of buffer length register这个是指接收或发送的最长长度,14位最大就是16384,实际上在函数中判断是16383,该参数设置过长, XAxiDma_SimpleTransfer函数就会返回一个错误。
b.address width是32位,就是对应到DDR3上,一次操作是32bit。read channel 和write channel很不好记,要理解DMA工作时是独立于CPU的,DMA读写都是指对DDR3的操作,由于我的应用是从底层产生数据存到FIFO中,等DMA来读取,因此只需要开启写DDR3通道即可。
c.max burst size,突发传输长度。突发的意思,就是传送一次地址,取多个数据的长度。这一点不好理解:当CPU通过AXI lite写DMA寄存器开启DMA接收后,DMA的tready拉高,开始传输数据,每接收到突发长度后,拉低一次tready,把数据写到DDR3中,写完之后再拉高再接收数据。
d.allow unaligned transfers。地址对齐,这个很重要,发送和接收通道都有,不开启,你发送和接收都必须要从4byte对齐的位置开始,必须从0x00,0x04,0x08等位置发送或接收,否则DMA不会正常工作。
我们再回去看接收的编程说明。
a.写S2MM_DMACR.RS =1,然后DMASR.Halted = 0 ,DMA表示在运行了。
b.写目的地地址到DA寄存器中。
c.写LENGTH寄存器,必须要最后写。写完这个寄存器后,DMA就开始传输了。传输完毕后,该寄存器的值是实际接收到的字节数。
d.判断DMASR.idle = 1,后表示接收结束。
DMA何时结束?
DMA的读写接口都是axis stream,是强制有tlast的,就是靠tlast信号来判断接收结束的,而不是填写的length值。发送接收值,最后一个tdata会同时伴随一个tlast高电平。接收时,设置接收5000字节,并不是说接收满5000个字节就结束了,而是开启接收后,接收到1个tlast高电平就结束了,也就是说,你可能会只接收到2000个字节,手册中明确说了,当设置的length = 0,或者接收到的数据大于设置的length,就会引起DMAIntErr置1,接收错误。
所以,当设置每次接收5000字节时,tlast就应该每隔<5000字节时插入。我的应用中,DMA前是一个AXIS DATA FIFO,写入是自定义的时序,但接收时还是报错,接收得太多了。为什么呢?因为DMA是自己拉高tready,而fifo的valid也为高就开始计数了,跟FIFO随后的valid 无关(我瞎猜的),DMA读的速度远高于我写入速度,我还没写到tlast,DMA就计数到16383了,DMA就报错,怎么办呢,开启fifo的packet模式。然后DMA接收就ok了。