- 1:前言
- 2:明确数据交互方式
- 3:DataMover
- 3.1:命令接口与接口时序
- 3.2:状态接口与接口时序
- 3.3:DataMoverS2MM通道实操
- 3.4:DMA_FRM_GEN设计
- 3.5:写操作
- 3.6:PS访问DMA发送的数据
- 3.7:DataMover吞吐计算
- 4:总结
- 5:祝君好运,乐享其中。 Good Luck & Have Fun
- 6:参考文档
本文章来自网友朔望,我为代发
前言
ZYNQ开发过程中 PL与PS DDR数据交互属于老生常谈的问题了,国内销售开发板的通常是介绍一些开源的DMA实现,这种方式在使用VIVADO BLOCK DESIGN 时会比较麻烦,为此本文介绍XILINX提供的官方底层DMA AXI DataMover的S2MM通道使用流程。
明确数据交互方式
在正式逻辑设计开始前,需要嵌入式工程师与FPGA工程师共同确定数据的交互方式,具体根据数据路径和数据率量大小决定。
- 对于PS到PL侧的少量数据交互的情况,通常考虑直接外挂PL侧AXI FULL/LITE总线从机,PS通过AXI GP接口直接对特定地址区域进行读写。
- 对于PS到PL侧的大量数据通常考虑使用使用ZYNQ内部内建DMA将数据直接从DDR搬运到PL侧。
对于PS到PL的数据路径通常是由PS进行控制,对于嵌入式开发较为友好,整体开发难度不高。但由于ZYNQ使用的CORTEX-A9处理器是一颗多级流水的应用处理器,因此在响应延迟差于传统的嵌入式处理(CORTEX-M3,M4)。
在大多数PL访问PS侧DDR应用场景下,都是PL读写DDR中连续的内存区域,有高带宽的特性。由于这个特性PL到PS的数据交互通常都是使用PL侧DMA来进行,在这种应用场景下DMA通常由PL侧逻辑控制,不过在对响应要求不苛刻的情况下也可由PS控制DMA。
DataMover
本文主要目的是介绍XILINX提供的底层DMA IP:AXI DataMover,该DMA是XILINX提供的一系列复杂DMA IP的基础设施。
- AXI MCDMA底层是DataMover搭配额外控制逻辑。
- 同样的AXI VDMA也是在DataMover上实现的。
AXI DataMover拥有两个数据通道,分别是:
- MM2S 即内存映射数据转换为流式数据,实现AXI-FULL协议到AXI-STREAM协议的转换;
- S2MM 即流式数据转换为内存映射数据,实现AXI-STREAM协议到AXI-FULL协议的转换。
本文主要介绍S2MM通道,从图1可以看出DataMover的S2MM通道是其内部的写通道。
写通道主要由命令、状态逻辑与写引擎构成,其中:
- 命令逻辑是一个FIFO,也就是说里面可以实现一个DMA传输队列;
- 状态逻辑是一个FIFO,里面放有上次DMA传输的状态报告;
- 写引擎,写引擎负责执行命令、协议转换。
命令接口与接口时序
在字节传输模式(BTT MODE)下,DataMover的命令由如下图2所示
在一般应用场景下通常不使用xCACHE与xUSER,故该部分无效。
在不使用xCACHE和xUSER的情况下DataMover的指令长度为N+40位。根据手册描述,N是内存映射总线总线地址的宽度,无论如何都必须是8的倍数,若不为8的倍数时需要向上取整,手册上举例:当总线宽度配置位33位时需要向上取整拓展到40位,多出来的高位在IP内部会忽略。
命令字节描述:
- TAG :命令标识,用于标记当前命令,便于后续出问题时在状态总线上排查问题。
- SADDR:起始地址,指示数据从哪个具体的地址开始。
- DRR:MM2S时需要,本文讲S2MM,该部分不做描述。
- EOF:指示本次传输是否是最后一次传输。
- DSA:字节对齐功能,S2MM应用不需要使用。
- TYPE:通常为1,实现递增,不然DMA只会向起始地址写数据。
- BTT:在字节传输模式下指示一次发送多少数据。最小为1,最大为8MiB。
从图3可以看出命令接口时序是一种简化的AXIS接口,去掉了tkeep与tlast信号。当主从握手信号就绪时总线上的命令送入命令FIFO,下面代码是对该接口的一种简单实现。
//send command to command interface
always_ff@(posedge clk) begin
if(srstn == 1'b0) begin
m_axis_s2mm_cmd_tvalid <= 1'b0;
m_axis_s2mm_cmd_tdata <= 72'd0;
end
else if(trans_start_pos) begin
m_axis_s2mm_cmd_tvalid <= 1'b1;
//enable increase,enable eof,disable DSA and DRR,no TAG
m_axis_s2mm_cmd_tdata <= {9'd0,DEST_ADDR,2'b01,6'd0,1'b1,trans_byte};
end
else if(m_axis_s2mm_cmd_tready) begin
m_axis_s2mm_cmd_tvalid <= 1'b0;
m_axis_s2mm_cmd_tdata <= 72'd0;
end
end
状态接口与接口时序
状态接口用于指示上次DMA传输事务的结果,可供外部用户逻辑对DMA状态将进行监控。
状态字节描述:
- OKAY:拉高指示一次成功的DMA传输,拉低表示失败。
- SLVERR:从机错误,拉高从机内部错误,拉低表示没有错误。
- DECERR:解码错误,通常是地址错误,拉低表示正常,拉高表示错误。
- INTERR:内部错误,要求DMA发0个数据,tlast信号到来的时间不对等均会引发内部错误,拉低表示正常,拉高表示错误。
- TAG:对应命令中的TAG。
图5是状态接口的时序,同样也是AXIS总线接口,除了上图中的信号,DataMover还额外提供s2mm_err/mm2s_err信号,当DMA出现错误时对应信号就会拉高。
需要额外注意的是当DataMover发生错误后,只能通过对DataMover进行复位才能进行后续数据传输操作。
DataMoverS2MM通道实操
理论部分讲完了下面开始DataMover的实操,本章节以一个简单的dma单向吞吐测试工程为例进行实操讲解。
顶层互联概述
图6是整个设计的顶层互联,其中:
- VIO负责设置DMA_FRM_GEN模块的初值和提供DMA传输信号。
- DMA_FRM_GEN负责生成AXIS数据流和DataMover命令流送入DataMover对应接口。
- design1_wrapper是block design的对外封装文件。
block design 概述
图7是图6中wrapper的内部,其为一个简单的DataMoverS2MM IP测试设计。其数据路径为:
- DMA_FRM_GEN模块生成的数据通过axis总线向外输出。
- DMA_FRM_GEN的AXIS接口接到图7的s_axis_fifo端口随后由位宽转换模块将DMA_FRM_GEN模块的32位AXIS总线转换为64位。
- 位宽拓展后的数据送入到FIFO模块缓存。
- 当DataMover收到命令后将 FIFO数据通过PS的AXI HP接口送入到PS侧DDR中。
通常PL DMA都接在PS的AXI HP端口,为啥不用AXI GP端口呢?要问AXI GP端口支持不支持访问PS DDR控制器,这当然是支持的,但两个接口以下差别让高速数据传输任务更加青睐AXI HP接口:
- AXI HP接口数据位宽可选为32/64bit,而AXI GP最大只有32bit,也就是说在相同工作时钟下,AXI HP的带宽最大可以是AXI GP接口的一倍。
- 为了提高吞吐每个AXI HP接口内部均有带有1KB的读写FIFO,而AXI GP没有带FIFO。在高速数据流到来时且DDR控制器被其它设备使用时,AXI GP接口由于没有缓存能力,会出现阻塞,AXI HP带有FIFO可以先缓存,等待DDR空闲后将数据写进去。
DataMoverIP设置。
- 由于只测试s2mm故只选择enable s2mm;
- channel type选择full,以支持更大的猝发传输次数;
- memory map data width 与 stream data width 均选择64位,便于测试dma最大吞吐;
- maximum burst size选择256,以提高总线效率;
- width of btt field根据需求选择,本次选择16即可;
- advanced页面设置不需要修改,保持默认。
DMA_FRM_GEN设计
本次设计是为了快速测试DataMoverS2MM吞吐,故DMA_FRM_GEN模块在收到VIO发出的起始信号后向DMA发送指定数量的数据即可,下面是该模块的代码。
module dma_frm_gen
#(
parameter TRANS_BEATS_NUM = 2048,
parameter TDATA_WIDTH = 32,
parameter DEST_ADDR = 32'h1FFF_0000
)
(
input logic clk,
input logic srstn,
input logic [7:0] init_val,
input logic trans_start,
output logic [TDATA_WIDTH-1:0]m_axis_fifo_tdata,
output logic [(TDATA_WIDTH/8)-1:0]m_axis_fifo_tkeep,
output logic m_axis_fifo_tlast,
input logic m_axis_fifo_tready,
output logic m_axis_fifo_tvalid,
output logic [71:0]m_axis_s2mm_cmd_tdata,
input logic m_axis_s2mm_cmd_tready,
output logic m_axis_s2mm_cmd_tvalid
);
localparam IDLE = 0;
localparam EXE = 1;
localparam DONE = 2;
localparam BYTE_NUM = (TDATA_WIDTH/8);
logic [15:0] trans_cnt;
logic [1:0] trans_state;
logic [1:0] trans_start_ff;
logic trans_start_pos;
logic [22:0] trans_byte;
logic [7:0] trans_data;
always_ff@(posedge clk) begin
trans_byte <= TRANS_BEATS_NUM*BYTE_NUM;
end
// generate start posedge signal
always_ff@(posedge clk) begin
if(srstn == 1'b0) begin
trans_start_ff <= 2'b00;
end
else begin
trans_start_ff <= {trans_start_ff[0],trans_start};
end
end
always_ff@(posedge clk) begin
if(srstn == 1'b0) begin
trans_start_pos <= 1'b0;
end
else begin
trans_start_pos <= (~trans_start_ff[1]) && trans_start_ff[0];
end
end
always_ff@(posedge clk) begin
if(srstn == 1'b0) begin
trans_cnt <= 16'b0;
trans_state <= IDLE;
trans_cnt <= 16'd0;
m_axis_fifo_tdata <= 32'd0;
end
else begin
case (trans_state)
IDLE: begin
if(trans_start_pos) begin
m_axis_fifo_tdata <= {4{init_val}};
trans_state <= EXE;
trans_cnt <= 16'd0;
end
else begin
trans_state <= IDLE;
trans_cnt <= 16'd0;
end
end
EXE: begin //when slv ready
if(trans_cnt < TRANS_BEATS_NUM) begin
if(m_axis_fifo_tready) begin
if(trans_cnt == 0) begin
m_axis_fifo_tdata <= {BYTE_NUM{trans_data}};
end
else begin
m_axis_fifo_tdata <= {BYTE_NUM{trans_data}};
end
trans_cnt <= trans_cnt + 1'b1;
end
end
else begin
trans_state <= DONE;
end
end
DONE: begin
trans_state <= IDLE;
trans_cnt <= 16'd0;
end
default: trans_state <= IDLE;
endcase
end
end
always_ff@(posedge clk) begin
if(srstn == 1'b0) begin
trans_data <= 8'd0;
end
else if(trans_start_pos) begin
trans_data <= init_val;
end
else if(trans_state == EXE && m_axis_fifo_tready) begin
trans_data <= trans_data + 1'b1;
end
end
// generate tvalid signal for axis bus
always_ff @(posedge clk ) begin
if(srstn == 1'b0) begin
m_axis_fifo_tvalid <= 1'b0;
end
else begin
if(trans_state == EXE) begin
if(trans_cnt < TRANS_BEATS_NUM) begin
m_axis_fifo_tvalid <= 1'b1;
end
else begin
m_axis_fifo_tvalid <= 1'b0;
end
end
else begin
m_axis_fifo_tvalid <= 1'b0;
end
end
end
//generate tlast signal for last axis transaction event
always_ff @( posedge clk ) begin
if(srstn == 1'b0) begin
m_axis_fifo_tlast <= 1'b0;
end
else begin
if(trans_state == EXE) begin
if(trans_cnt == TRANS_BEATS_NUM-1) begin
m_axis_fifo_tlast <= 1'b1;
end
else begin
m_axis_fifo_tlast <= 1'b0;
end
end
else begin
m_axis_fifo_tlast <= 1'b0;
end
end
end
always_ff @( posedge clk ) begin
if(srstn == 1'b0) begin
m_axis_fifo_tkeep <= 4'b0000;
end
else begin
m_axis_fifo_tkeep <= 4'b1111;
end
end
//send command to command interface
always_ff@(posedge clk) begin
if(srstn == 1'b0) begin
m_axis_s2mm_cmd_tvalid <= 1'b0;
m_axis_s2mm_cmd_tdata <= 72'd0;
end
else if(trans_start_pos) begin
m_axis_s2mm_cmd_tvalid <= 1'b1;
//enable increase,enable eof,disable DSA and DRR,no TAG
m_axis_s2mm_cmd_tdata <= {9'd0,DEST_ADDR,2'b01,6'd0,1'b1,trans_byte};
end
else if(m_axis_s2mm_cmd_tready) begin
m_axis_s2mm_cmd_tvalid <= 1'b0;
m_axis_s2mm_cmd_tdata <= 72'd0;
end
end
endmodule
写操作
在block design中将相关AXI总线信号添加到ILA即可观察总线行为。
测试中设置DMA传输8KiB数据,则dma_frm_gen需要输出2048拍数据。
图10为dma_frm_gen模块对外输出的情况,由于逻辑资源有限只能抓取一部分接口波形。
图11展示了DataMover向AXI HP接口写数据的情况,由于前面将总线从32bit拓宽到64bit,此处总线带宽翻倍,相应的传输相同大小的数据所需时间为32bit总线的一半,故ILA可以抓全信号。
我们可以看到DataMover将8KiB数据拆分为4次burst进行传输,根据之前DataMoverIP设置,单次burst为256拍数据,数据位宽为64位,则一次burst可以传输256x64/8 = 2048字节数据=2KiB,2048x4=8KiB,与ila抓到的信号情况一致。
图12展示了状态总线报告的DMA传输状态,可以看到传输正常,错误信号没有拉高。
PS访问DMA发送的数据
相较于逻辑端费力的操作,PS侧访问DDR数据较为容易,使用for循环加库函数即可访问,但需要注意:
- 缓存一致性问题;
对于缓存一致性问题,请使用Xil_DCacheInvalidate();函数更新cache。
下面上PS裸机代码。
#include <stdio.h>
#include "platform.h"
#include "xil_printf.h"
#include "xil_io.h"
#include "xil_cache.h"
volatile unsigned int rd;
volatile unsigned int rd_cnt;
int main()
{
init_platform();
while(1) {
Xil_DCacheInvalidate();
for (rd_cnt = 0; rd_cnt < 2048;rd_cnt = rd_cnt + 1) {
rd = Xil_In32(0x1FFF0000 + rd_cnt*4);
xil_printf("Address offset 0x%08x data in hex is 0x%08x \n\r",rd_cnt*4,rd);
}
}
cleanup_platform();
return 0;
}
DataMover吞吐计算
在DataMover到PS DDR之间传输8KiB数据花费了1026个时钟周期,而理论最少时钟周期为1024。由于整个系统运行在125MHz,一个时钟周期为8ns,则DataMover理论数据率为8KiB/(1024x8)=1000MiB/S,实际速率为8KiB/(1026x8)=998MiB/S,效率为99.8%。当然以上估计存在数据量过少,测试环境过于理想的问题。实际DMA使用过程中并不会有这么理想的流式数据以及大容量fifo作为dma缓存。
总结
本文简单介绍了使用AXI DataMover作为DMA将PL侧数据送到PS DDR侧的方法,相较于xilinx提供的配置繁杂的AXI DMA,AXI CDMA等DMA,DataMover使用简单且快速,适合简单流式数据传输如(高速ADC采样数据,SDR上下行数据流)。对于需要DMA完成复杂地址映射的场景, DataMover使用起来就较为别扭,此时推荐使用其它DMA。
此外AXI DataMover适合逻辑侧直接控制,对于PS部分来讲该部分是完全透明的,只需要定时访问DDR特定区域即可,能够降低嵌入式的工作量,不过这种方式对于逻辑端来讲就不太友好了。
最后在ZYNQ应用过程中应该在设计起始阶段就应该约定好PS与PL数据交互方式并以文档方式记录在案,这样设计更加清晰,更重要是可以减少无所谓的时间浪费。