本文介绍ZYNQ AXI DMA的简单模式使用方法,查询模式(poll),不使用中断,32bit。
1.有关DMA的函数调用,去参照DMA的官方例程。所有的外设都是有ID的,先建立一个结构体,初始化外设,把外设的基地址赋值给结构体,对结构体进行赋值就是写相应的寄存器,控制DMA工作。所有的外设都有寄存器手册,自己去下载,直接看寄存器空间register space就可以了,例如DMA的寄存器手册。DMA有两种方式,我只学习了使用简单模式,Scatter / Gather Mode可以看其他博主的介绍。下面的代码是DMA的初始化,非常简单,以及开启一次DMA接收,数据从axis stream fifo到DMA到DDR3。
Xil_In32 和Xil_Out32直接读和写寄存器,还有Xil_In8和Xil_Out8,是按byte操作。
void DMA_INIT(void)
{
//初始化
int Status;
XAxiDma_Config *Config;
XScuGic_Config *IntcConfig;
Config = XAxiDma_LookupConfig(XPAR_AXIDMA_0_DEVICE_ID);
Status = XAxiDma_CfgInitialize(&AxiDma, Config);
//不使能中断
XAxiDma_IntrDisable(&AxiDma, XAXIDMA_IRQ_ALL_MASK,XAXIDMA_DMA_TO_DEVICE);
XAxiDma_IntrDisable(&AxiDma, XAXIDMA_IRQ_ALL_MASK,XAXIDMA_DEVICE_TO_DMA);
//初始化DMA,初不初始化无所谓,跟进去就知道本质都在写寄存器
XAxiDma_Reset(&AxiDma);
while(!XAxiDma_ResetIsDone(&AxiDma));
//打印寄存器
//0x4040 0000是DMA的基地址,在block design的address中可以看到,后面是偏移地址,
//window->address editor
xil_printf("CR is %d\r\n", Xil_In32(0x40400000 + 0x30));
xil_printf("SR is %d\r\n", Xil_In32(0x40400000 + 0x34));
xil_printf("LENGTH is %d\r\n", Xil_In32(0x40400000 + 0x58));
//开始传输
Xil_Out32((0x40400000 + 0x30),1); //start dma
Xil_Out32((0x40400000 + 0x48),base_addr); //set da address
Xil_Out32((0x40400000 + 0x58),16383); //set length
//再次打印,看寄存器的状态
xil_printf("CR is %d\r\n", Xil_In32(0x40400000 + 0x30));
xil_printf("SR is %d\r\n", Xil_In32(0x40400000 + 0x34));
xil_printf("LENGTH is %d\r\n", Xil_In32(0x40400000 + 0x58));
}
2. 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等寄存器位,后面都有说明。
3.DMA的使用方法也在手册中直接说明了。
A.DMA发送在例程里也有说明,xaxidma_example_simple_poll.c,
XAxiDma_SimpleTransfer(&AxiDma,(UINTPTR) TxBufferPtr,MAX_PKT_LEN,XAXIDMA_DMA_TO_DEVICE);
注意,XAXIDMA_DMA_TO_DEVICE就是DDR3 到 FIFO,从TxBufferPtr发送MAX_PKT_LEN个字节到FIFO(不一定是FIFO),
判断DMA发送的SR寄存器idle是否为1,为1则idle,说明发送结束。
while (XAxiDma_Busy(&AxiDma,XAXIDMA_DMA_TO_DEVICE))
B.发送很简单,接收有点复杂,主要是tlast信号的问题,要先了解DMA的接收过程。下图是AXI DMA的配置图。
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,后表示接收结束。
Xil_Out32((0x40400000 + 0x30),1); //start dma
Xil_Out32((0x40400000 + 0x48),base_addr); //set da address
Xil_Out32((0x40400000 + 0x58),16383); //set length
对照代码看,是不是很简单,看到这里,要是以为你就会用DMA发送和接收,扭头就去写代码了,小伙子你还是太年轻了。
4. 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了。
5. 实际应用
DMA的使用上我遇到的坑,实际上就是地址对齐,tlast的给定上,因为我每次接收完后都会reset下,导致看不出来dma哪儿有问题,但数据总是不对。
DMA的发送一般没有什么问题,有个小技巧,可以用DMA的reset_out引脚去初始化fifo,我有一个应用就2个dma交替发送数据到pl的2个fifo,fifo的存取上老有问题,写768个字节,读768个字节,fifo有时还会有留存数据,导致下一次取数据多了一个,也没细想过问题,DMA发送前先复位一下,顺便把fifo复位一下,就解决这个问题。
另一个应用是pl采集spi的数据,但数据不知道什么时候来,每一包也不知道有多少个字节。我在ps中开了一个定时器,1ms一次,开定时器前,先开启DMA接收,在定时器中断中查询是否接收完成(idle),idle后就把接收地址base_addr加上1个length,再开启下一次传输。而pl端,每隔4096个字节就插一个tlast。又有一个问题,要是有5000个字节呢?剩下的4个怎么办?我又加了个定时器,超过2s,没有数据来,且fifo的data_count不等于0,我就加入aa aa aa数据,并在最后一个aa时拉高一次tlast,表示一次传输完成。在ps端,开启freertos多线程,设置一个send_addr和base_addr初始值相同,当send_addr小于base_addr时,用udp把数据发送出去。又有一个问题,base_addr不能一直往上上加啊,加到大于某个值的时候,且send_addr = base_addr,把数据拷贝到初始值去,重置这2个指针。
void Timer_ISR(void *CallBackRef)
{
lock = 1;
if(Xil_In32(0x40400000 + 0x34) & 0x02) //idle = 10
{
Xil_DCacheFlushRange(base_addr,Xil_In32(0x40400000 + 0x58));
// xil_printf("length is %d\r\n",Xil_In32(0x40400000 + 0x58));
unsigned int len = Xil_In32(0x40400000 + 0x58);
if((send_addr == base_addr) &&(send_addr != 0x1000000) && (base_addr >= 0x1F400000))
{
memcpy(0x1000000,base_addr,len);// dest,src,len
base_addr = 0x1000000;
send_addr = 0x1000000;
base_addr = base_addr + len;
Xil_Out32((0x40400000 + 0x48),base_addr); //set da address
Xil_Out32((0x40400000 + 0x58),8192); //set da address
}
else
{
base_addr = base_addr + len;
Xil_Out32((0x40400000 + 0x48),base_addr); //set da address
Xil_Out32((0x40400000 + 0x58),8192); //set da address
}
}
lock = 0;
// xil_printf("count is %d\r\n",XGpio_DiscreteRead(&COUNT,1));
// xil_printf("CR is %d\r\n", Xil_In32(0x40400000 + 0x30));
// xil_printf("SR is %d\r\n", Xil_In32(0x40400000 + 0x34));
// xil_printf("LENGTH is %d\r\n", Xil_In32(0x40400000 + 0x58));
}
要注意DMA操作DDR3,CPU是不知道的,要用dcacheflush及时刷新。这个刷新速度很慢的,要注意控制刷新的长度。
while(1)
{
if(lock == 0)
{
if(send_addr < base_addr)
{
unsigned int len = base_addr - send_addr;
if(len > 16384)
len = 16384;
msg_udp_send((UINTPTR)send_addr,len);
send_addr = send_addr + len;
}
}
vTaskDelay(10);
}
always@(posedge clk)
if((!en) ||(delay_over == 32'd400_000_000))
byte_cnt <= 1'd0;
else if(tvalid)
begin
if(byte_cnt == 32'd4095)
byte_cnt <= 1'd0;
else
byte_cnt <= byte_cnt + 1'd1;
end
assign m_axis_tlast = (tvalid && (byte_cnt == 32'd4095 || i == 8'd13))?1'd1:1'd0;
reg [31:0] delay_over = 1'd0;
assign mode = reg_mode;
always@(posedge clk)
if(!en)
begin
i <= 1'd0;
reg_mode <= 1'd0;
tvalid <= 1'd0;
tdata <= 1'd0;
end
else
case(i)
0:
if(cs_fall_edge && m_axis_tready&&(fifo_count <= 32'd36700) ) //fifo ready && cs fall
begin
i <= i + 1'd1;
delay_over = 1'd0;
end
else if(fifo_count != 1'd0)
begin
if(delay_over == 32'd400_000_000) //超时2s
begin
delay_over = 1'd0;
i <= 8'd10;
end
else
delay_over = delay_over + 1'd1;
end
1:
if(data_cs)
i <= 1'd0;
else if(valid)
begin
case (data)
8'h03,8'h0b:
reg_mode <= 4'd0;
8'h05,8'h9f:
reg_mode <= 4'd1;
default:
reg_mode <= 4'd1;
endcase
i <= i + 1'd1;
end
else
i <= i;
2:
begin
tvalid<= 1'd1;
tdata <= 8'hee;
if(m_axis_tready&&(fifo_count <= 32'd36700))
i <= i + 1'd1;
else
i <= 8'd7;
end
3:
begin
tvalid<= 1'd1;
tdata <= 8'h55;
if(m_axis_tready&&(fifo_count <= 32'd36700))
i <= i + 1'd1;
else
i <= 8'd7;
end
4:
begin
tvalid<= 1'd1;
tdata <= data;
if(m_axis_tready&&(fifo_count <= 32'd36700))
i <= i + 1'd1;
else
i <= 8'd7;
end
5:
begin
tvalid<= 1'd0;
i <= i + 1'd1;
end
6:
if(data_cs)
i <= i + 1'd1;
else if(valid)
begin
tvalid<= 1'd1;
tdata <= data;
if(m_axis_tready&&(fifo_count <= 32'd36700))
i <= 8'd5;
else
i <= 8'd7;
end
else
i <= i;
7:
begin
tvalid<= 1'd1;
tdata <= 8'haa;
i <= i + 1'd1;
end
8:
begin
tvalid<= 1'd0;
i <= i + 1'd1;
if(isr_en)
isr_start <= 1'd1;
end
9:
begin
i <= 1'd0;
isr_start <= 1'd0;
end
10,11,12:
begin
tvalid<= 1'd1;
tdata <= 8'haa;
i <= i + 1'd1;
end
13:
begin
tvalid<= 1'd0;
i <= 1'd0;
end
default:
i <= 1'd0;
endcase
assign m_axis_tvalid = tvalid;
assign m_axis_tdata = tdata;
坑有无数,愿你抗住!