|
【参赛手记】System
Generator 与XPS连接的方法
本文是AXI-Stream IP调试日记的终结篇。
看到这里,可能大家都还对Stream没有一个直观的认识。其实Stream并不陌生,在我们学c++编程时,一定会包含,这样就可以完成控制终端对程序的输入输出了。如果还是不够直观,想象一下水流,是连续不断的,向某一方向,以固定的速度输送的接口。以我们看视频为例,视频文件本来是保存在硬盘里的,怎么播放呢,不能一下子把整个文件都显示到屏幕上,而是以一定速度,连续不断地输出到屏幕上(每秒若干帧),这个过程就是流Stream接口完成的。
Xilinx提供的流式IP核有很多用途,可以实现音频流、视频流、数据流到内存或者相反方向的传输。有人问了,内存是PS控制的,怎么才能把PS里DDR2的内容以Stream形式发出去呢(例如以固定速度送往DA,完成信号发生器的设计)?答案就是利用AXI总线做转换。ZYNQ的PS部分是arm Cortex A9系列,支持AXI4,AXI-Lite总线。PL部分也有相应AXI总线接口,这样就能完成PS到PL的互联。仅仅这样还不够,需要PL部分实现流式转换,即AXI-Stream接口实现。Xilinx提供的从AXI到AXI-Stream转换的IP核有:AXI-DMA,AXI-Datamover,AXI-fifo-MM2S以及AXI-vDMA等。这些IP核可以在XPS中看到。
这里要和大家说明白一点,就是AXI总线和接口的区别。总线是一种标准化接口,由数据线、地址线、控制线等构成,具有一定的强制性。接口是其物理实现,即在硬件上的分配。在ZYNQ中,支持AXI-Lite,AXI4和AXI-Stream三种总线,但PS与PL之间的接口却只支持前两种,AXI-Stream只能在PL中实现,不能直接和PS相连,必须通过AXI-Lite或AXI4转接。PS与PL之间的物理接口有9个,包括4个AXI-GP接口和4个AXI-HP接口、1个AXI-ACP接口。
AXI-DMA:实现从PS内存到PL高速传输高速通道AXI-HP<---->AXI-Stream的转换
AXI-FIFO-MM2S:实现从PS内存到PL通用传输通道AXI-GP<----->AXI-Stream的转换
AXI-Datamover:实现从PS内存到PL高速传输高速通道AXI-HP<---->AXI-Stream的转换,只不过这次是完全由PL控制的,PS是完全被动的。
AXI-VDMA:实现从PS内存到PL高速传输高速通道AXI-HP<---->AXI-Stream的转换,只不过是专门针对视频、图像等二维数据的。
除了上面的还有一个AXI-CDMA IP核,这个是由PL完成的将数据从内存的一个位置搬移到另一个位置,无需cpu来插手。这个和我们这里用的Stream没有关系,所以不表。
上面的IP是完成总线协议转换,如果需要做某些处理(如变换、迭代、训练……),则需要生成一个自定义Stream类型IP,与上面的Stream接口连接起来,实现数据输入输出。用户的功能在自定义Stream类型IP中实现。
下面讲一个例子,来加深对上面介绍内容的理解。
软件:ISE 14.2
1.先建立PlanAhead工程,一直做到进入XPS,具体流程见官方文档CTT。
2.在XPS中,添加一个AXI-DMA模块,配置界面如下图所示
其余参数默认。SG模块如果选上,那么后面软件控制会相对复杂一些。这里不选,采用Simple模式。
3.选菜单Hardware->Create
or Import Peripheral。。。,设计自定义IP。名称起为my_stream_ip,自动版本为1.00a。遇到Bus Interface选择AXI4-Stream类型,一直点下一步到最后结束。该类型IP的生成过程比AXI4-Lite和AXI4都要简单。
4.添加一个my_stream_ip到系统中,连接图见下。
my_stream_ip实现了先接受8个字的数据,求和,然后将和发送回去(发送8次)。上图连接方式说明是AXI-DMA发送给my_stream_ip,然后my_stream_ip又发回AXI-DMA。同时看到AXI-DMA和PS连接是通过HP0传输数据,由GP0控制其传输的进行。
5.以上连接是有问题的(主要是XPS的bug),需要一项项修改。
首先是HP0的地址区间报错,可以先点Zynq标签,然后单击HP0绿线,在弹出的配置对话框中将HP0的地址区间改为我们ZED Board 上DDR2区间
0x00000000~0x1FFFFFFF,像下图这样:
之后就是AXI-DMA和my_stream_ip的连线问题。本来都是Stream 接口,按理说是标准接口,不应该有差异。但事实就是这样,XPS界面掩饰之下,问题层出不穷。我们右击my_stream_ip,选择View MPD,将内容改为:
##############################################################################
##
Filename:
E:\work\FPGA\ZynqProjects\axi_dma_final\axi_dma_final.srcs\sources_1\edk\system\pcores/my_stream_ip_v1_00_a/data/my_stream_ip_v2_1_0.mpd
## Description:
Microprocessor Peripheral Description
##
Date:
Sat May 11 19:51:06 2013 (by Create and Import Peripheral Wizard)
##############################################################################
BEGIN my_stream_ip
## Peripheral Options
OPTION IPTYPE = PERIPHERAL
OPTION IMP_NETLIST = TRUE
OPTION hdl = verilog
## Bus Interfaces
BUS_INTERFACE BUS=M_AXIS, BUS_STD=AXIS, BUS_TYPE=INITIATOR
BUS_INTERFACE BUS=S_AXIS, BUS_STD=AXIS, BUS_TYPE=TARGET
## Parameters
PARAMETER C_S_AXIS_PROTOCOL = GENERIC, DT = string, TYPE =
NON_HDL, ASSIGNMENT = CONSTANT, BUS = S_AXIS
PARAMETER C_S_AXIS_TDATA_WIDTH = 32, DT = integer, TYPE =
NON_HDL, ASSIGNMENT = CONSTANT, BUS = S_AXIS
PARAMETER C_M_AXIS_PROTOCOL = GENERIC, DT = string, TYPE =
NON_HDL, ASSIGNMENT = CONSTANT, BUS = M_AXIS
PARAMETER C_M_AXIS_TDATA_WIDTH = 32, DT = integer, TYPE =
NON_HDL, ASSIGNMENT = CONSTANT, BUS = M_AXIS
## Peripheral ports
PORT ACLK = "", DIR=I, SIGIS=CLK, BUS=M_AXIS:S_AXIS
PORT
ARESETN = ARESETN, DIR=I, INITIALVAL = VCC
PORT S_AXIS_TREADY = TREADY, DIR=O, BUS=S_AXIS
PORT S_AXIS_TDATA = TDATA, DIR=I, VEC=[31:0], BUS=S_AXIS
PORT S_AXIS_TLAST = TLAST, DIR=I, BUS=S_AXIS
PORT S_AXIS_TVALID = TVALID, DIR=I, BUS=S_AXIS
PORT M_AXIS_TVALID = TVALID, DIR=O, BUS=M_AXIS
PORT M_AXIS_TDATA = TDATA, DIR=O, VEC=[31:0], BUS=M_AXIS
PORT M_AXIS_TLAST = TLAST, DIR=O, BUS=M_AXIS
PORT M_AXIS_TREADY = TREADY, DIR=I, BUS=M_AXIS
PORT
M_AXIS_TKEEP = TKEEP, DIR=O, VEC=[3:0], BUS=M_AXIS
END
这里存在两个问题:一个是ARESETN,在连接时AXI-DMA上没有合适的引脚与之相连,于是默认就接地了。接地不就复位了嘛!所以要显式声明接VCC。另一个问题是TKEEP信号,在上一篇文章AXI-Stream调试日记(三)里说过了,这里加上这个引脚,才能准确地将数据发回AXI-DMA。
保存MPD文件,关闭。
再次右击my_stream_ip,选择Browse HDL Sources。。。,打开my_stream_ip.v(或者my_stream_ip.vhd),内容改为:
module my_stream_ip
(
// ADD USER PORTS BELOW THIS LINE
// -- USER ports added here
// ADD USER PORTS ABOVE THIS LINE
// DO NOT EDIT BELOW THIS
LINE ////////////////////
// Bus protocol ports, do not add or delete.
ACLK,
ARESETN,
S_AXIS_TREADY,
S_AXIS_TDATA,
S_AXIS_TLAST,
S_AXIS_TVALID,
M_AXIS_TVALID,
M_AXIS_TDATA,
M_AXIS_TLAST,
M_AXIS_TREADY,
M_AXIS_TKEEP
// DO NOT EDIT ABOVE THIS LINE
////////////////////
);
// ADD USER PORTS BELOW THIS LINE
// -- USER ports added here
// ADD USER PORTS ABOVE THIS LINE
input
ACLK;
input
ARESETN;
output
S_AXIS_TREADY;
input [31 :
0]
S_AXIS_TDATA;
input
S_AXIS_TLAST;
input
S_AXIS_TVALID;
output
M_AXIS_TVALID;
output [31 :
0]
M_AXIS_TDATA;
output
M_AXIS_TLAST;
input
M_AXIS_TREADY;
output
[3:0] M_AXIS_TKEEP;
// ADD USER PARAMETERS BELOW THIS LINE
// --USER parameters added here
// ADD USER PARAMETERS ABOVE THIS LINE
//----------------------------------------
// Implementation Section
//----------------------------------------
// In this section, we povide an example implementation of MODULE my_stream_ip
// that does the following:
//
// 1. Read all inputs
// 2. Add each input to the contents of register 'sum' which
// acts as an accumulator
// 3. After all the inputs have been read, write out the
// content of 'sum' into the output stream
NUMBER_OF_OUTPUT_WORDS times
//
// You will need to modify this example for
// MODULE my_stream_ip to implement your coprocessor
// Total number of input data.
localparam NUMBER_OF_INPUT_WORDS = 8;
// Total number of output data
localparam NUMBER_OF_OUTPUT_WORDS = 8;
// Define the states of state machine
localparam Idle = 3'b100;
localparam Read_Inputs = 3'b010;
localparam Write_Outputs = 3'b001;
reg [2:0] state;
// Accumulator to hold sum of inputs read at any
point in time
reg [31:0] sum;
// Counters to store the number inputs read &
outputs written
reg [NUMBER_OF_INPUT_WORDS - 1:0] nr_of_reads;
reg [NUMBER_OF_OUTPUT_WORDS - 1:0] nr_of_writes;
// CAUTION:
// The sequence in which data are read in should be
// consistent with the sequence they are written in the
// driver's my_stream_ip.c file
assign S_AXIS_TREADY = (state ==
Read_Inputs);
assign M_AXIS_TVALID = (state == Write_Outputs);
assign M_AXIS_TDATA = sum;
assign M_AXIS_TLAST = (nr_of_writes == 1);
assign
M_AXIS_TKEEP = 4'b1111;
always @(posedge ACLK)
begin // process The_SW_accelerator
if
(!ARESETN)
// Synchronous reset (active low)
begin
// CAUTION: make
sure your reset polarity is consistent with the
// system reset
polarity
state <= Idle;
nr_of_reads
<= 0;
nr_of_writes <=
0;
sum <= 0;
end
else
case (state)
Idle:
if
(S_AXIS_TVALID == 1)
begin
state <= Read_Inputs;
nr_of_reads <= NUMBER_OF_INPUT_WORDS - 1;
sum <= 0;
end
Read_Inputs:
if
(S_AXIS_TVALID == 1)
begin
// Coprocessor function (Adding) happens here
sum <= sum + S_AXIS_TDATA;
if (nr_of_reads == 0)
begin
state <= Write_Outputs;
nr_of_writes <= NUMBER_OF_OUTPUT_WORDS - 1;
end
else
nr_of_reads <= nr_of_reads - 1;
end
Write_Outputs:
if
(M_AXIS_TREADY == 1)
begin
if (nr_of_writes == 0)
state <= Idle;
else
nr_of_writes <= nr_of_writes - 1;
end
endcase
end
endmodule
这里修正了bug。
VHDL代码见后面附件1.
完成上述更改后,点XPS菜单Project->Rescan
User Repositories,实现用户配置更新。
6.点Port标签,引脚连接。这里重点是将所有带CLK字样的都连接到PS7_FCLK_CLK0.如下图所示。
7.点击Addresses标签,看看AXI-DMA是否分配了控制端口地址
注意,如果你的axi-dma地址和途中不一样,那么在后面软件编写时一定要修改成你的地址。
8.点Project->Design Rule Check;没错时,点Hardware->Generate
Netlist,完成后关闭XPS。
9.在PlanAhead中完成综合、实现、出Bit步骤。其实上一步已经完成了综合,所以这一步速度就会非常快。
10 导出SDK工程。建立Helloworld工程。将Helloworld.c里面的内容改为如下代码:
#include
#include
#include "platform.h"
#include "xil_cache.h"
#define sendram ((int *)0x10000000)
#define recvram ((int *)0x10001000)
#define sizeofbuffer 32
void print(char *str);
#define WITH_SG 0
#define AXI_DMA_BASE 0x40400000
#define MM2S_DMACR 0
#define MM2S_DMASR 1
#if WITH_SG
#define MM2S_CURDESC 2
#define MM2S_TAILDESC 4
#else
#define MM2S_SA 6
#define MM2S_LENGTH 10
#endif
#define S2MM_DMACR 12
#define S2MM_DMASR 13
#if WITH_SG
#define S2MM_CURDESC 14
#define S2MM_TAILDESC 16
#else
#define S2MM_DA 18
#define S2MM_LENGTH 22
#endif
void debug_axi_dma_register(unsigned int * p)
{
printf("MM2S_DMACR = 0x%x\n",*(p+MM2S_DMACR));
printf("MM2S_DMASR = 0x%x\n",*(p+MM2S_DMASR));
#if WITH_SG
printf("MM2S_CURDESC = 0x%x\n",*(p+MM2S_CURDESC));
printf("MM2S_TAILDESC = 0x%x\n",*(p+MM2S_TAILDESC));
#else
printf("MM2S_SA = 0x%x\n",*(p+MM2S_SA));
printf("MM2S_LENGTH = 0x%x\n",*(p+MM2S_LENGTH));
#endif
printf("S2MM_DMACR = 0x%x\n",*(p+S2MM_DMACR));
printf("S2MM_DMACSR = 0x%x\n",*(p+S2MM_DMASR));
#if WITH_SG
printf("S2MM_CURDESC = 0x%x\n",*(p+S2MM_CURDESC));
printf("S2MM_TAILDESC = 0x%x\n",*(p+S2MM_TAILDESC));
#else
printf("S2MM_DA = 0x%x\n",*(p+S2MM_DA));
printf("S2MM_LENGTH = 0x%x\n",*(p+S2MM_LENGTH));
#endif
}
void init_axi_dma_simple(unsigned int * p)
{
*(p+MM2S_DMACR) = 0x04; //reset send axi dma
while(*(p+MM2S_DMACR)&0x04);
*(p+S2MM_DMACR) = 0x04; //reset send axi dma
while(*(p+S2MM_DMACR)&0x04);
*(p+MM2S_DMACR)=1;
while((*(p+MM2S_DMASR)&0x01));
*(p+S2MM_DMACR)=1;
while((*(p+S2MM_DMASR)&0x01));
*(p+MM2S_SA) = (unsigned int )sendram;
*(p+S2MM_DA) = (unsigned int )recvram;
Xil_DCacheFlushRange((u32)sendram,sizeofbuffer);
*(p+S2MM_LENGTH) = sizeofbuffer;//sizeof(recvram);
*(p+MM2S_LENGTH) = sizeofbuffer;//sizeof(sendram);
while(!(*(p+MM2S_DMASR)&0x1000)); //wait for send ok
}
void init_sendbuffer()
{
int i;
for(i=0;i<100;i++)
{
sendram[i]=i*2;//50*sinf(2*3.14*i/10);
}
}
void show_recvbuffer()
{
int i;
printf("Recv contents are:\n");
for(i=0;i<100;i++)
{
printf("%d\t",recvram[i]);
}
printf("\r\n");
}
void show_sendbuffer()
{
int i;
printf("Send contents are:\n");
for(i=0;i<100;i++)
{
printf("%d\t",sendram[i]);
}
printf("\r\n");
}
int main()
{
unsigned int status=0;
int rxlen;
init_platform();
init_sendbuffer();
init_axi_dma_simple((unsigned int
*)AXI_DMA_BASE);
printf("Hello World\n\rPlease input data:");
while(1)
{
scanf("%x",&status);
printf("Got 0x%x\n",status);
debug_axi_dma_register((unsigned int *)AXI_DMA_BASE);
if(status==0)
{
break;
}
}
show_sendbuffer();
show_recvbuffer();
cleanup_platform();
return 0;
}
保存,等待生成elf。然后连接板子,下载bit文件,Run App,打开串口终端,等待输出。
发送的内容为连续的偶数0~14,接收到数据为56,只是个数为7个而不是8个。这个是因为我们的my_stream_ip的TLAST信号提前到达造成的(是故意这么做的,没问题)。
好了,到这里,Stream接口例子就完成了。希望本文对正在调试stream接口的童鞋们有帮助。