【OpenHW參賽手記】AXI-Stream介面開發詳細流程
下面講一個例子,來加深對上面介紹內容的理解。筆者使用的軟體版本為ISE 14.2。
1.建立PlanAhead工程,一直到進入XPS,具體流程見官方文件CTT[1]。
2.在XPS中,新增一個AXI-DMA模組,配置介面如圖1所示。
圖1 AXI-DMA模組配置
其餘引數預設。SG模組如果選上,那麼後面軟體控制會相對複雜一些。這裡不選,採用Simple模式,實現較為簡單的傳輸。
3.選選單Hardware->Createor Import Peripheral…,設計自定義IP。名稱起為my_stream_ip,自動版本為1.00a。遇到Bus Interface選擇AXI4-Stream型別,一直點下一步到最後結束。該型別IP的生成過程比AXI4-Lite和AXI4都要簡單。
4.新增一個my_stream_ip到系統中,連線見圖2。
圖2 AXI Stream IP硬體連線
由XPS自動生成的my_stream_ip實現了先接收8個32bit字,然後求和,再將結果傳送回去(連續傳送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,像圖3一樣。
圖3 修正bug1
在較高版本軟體ISE14.5中,這個bug已經修復,不需要改。
第二個bug就是AXI-DMA和my_stream_ip的連線問題。本來都是Stream 介面,按理說是標準介面,不應該有差異。但事實就是這樣,XPS介面掩飾的問題層出不窮。我們右擊my_stream_ip,選擇View MPD,將內容改為:
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),新增TKEEP訊號並設定TLAST訊號。
module my_stream_ip
(
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
);
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;
localparamNUMBER_OF_INPUT_WORDS = 8;
localparamNUMBER_OF_OUTPUT_WORDS = 8;
localparam Idle =3'b100;
localparam Read_Inputs = 3'b010;
localparam Write_Outputs = 3'b001;
reg [2:0] state;
reg [31:0] sum;
reg [NUMBER_OF_INPUT_WORDS -1:0] nr_of_reads;
reg [NUMBER_OF_OUTPUT_WORDS - 1:0] nr_of_writes;
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
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
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程式碼見我的部落格文章http://www.eeforum.com/附件,或通過郵件聯絡我獲取。完成上述更改後,點XPS選單Project->Rescan User Repositories,實現使用者配置更新。
6.點Port標籤,引腳連線。這裡重點是將所有帶CLK字樣的都連線到PS7_FCLK_CLK0.如圖4所示。
圖4 PORT標籤訊號線連線
7.點選Addresses標籤,看看AXI-DMA是否分配了控制埠地址
圖5 地址分配
注意,如果你的axi_dma_0的地址和圖中不一樣,那麼在後面軟體編寫時一定要修改成你的地址。
8.點Project->DesignRule Check;沒錯時,點Hardware->Generate Netlist,完成後關閉XPS。
9.在PlanAhead中完成綜合、實現、生成Bit等步驟[12]。其實上一步已經完成了綜合,所以這一步速度就會非常快。
10 匯出SDK工程。建立Helloworld工程。將Helloworld.c裡面的內容改為如下程式碼。
#include <stdio.h>
#include <stdlib.h>
#include "platform.h"
#include "xil_cache.h" //必須包含該標頭檔案,實現cache操作
#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_CURDESC14
#define S2MM_TAILDESC16
#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); //將cache內容同步到DDR2
*(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< sizeofbuffer/4;i++)
{
sendram[i]=i*2;
}
}
void show_recvbuffer()
{
int i;
printf("Recv contents are:\n");
for(i=0;i< sizeofbuffer/4;i++)
{
printf("%d\t",recvram[i]);
}
printf("\r\n");
}
void show_sendbuffer()
{
int i;
printf("Send contents are:\n");
for(i=0;i< sizeofbuffer/4;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((unsignedint *)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();
Xil_DCacheInvalidateRange((u32)recvram,sizeofbuffer); //將DDR2內容同步到cache
show_recvbuffer();
cleanup_platform();
return 0;
}
儲存,等待生成elf。然後連線板子,下載bit檔案,Run App,開啟串列埠終端,等待輸出。由圖6可見結果正確。
圖6 程式輸出
最終實現的my_stream_ip對外介面如下圖所示。其中“M_AXIS”開頭的訊號線表示為AXI_Stream主機訊號線,而“S_AXIS”開頭的訊號線表示為AXI_Stream從機訊號線。自動生成的程式碼中沒有M_AXIS_TKEEP訊號,根據AXI4_Stream協議,這會導致該模組作為主機時傳送的資料一直處於無效狀態,影響資料傳輸。我們在my_stream_ip中添加了該訊號,並使之有效,從而能夠獲得正確的處理資料。
圖7 my_stream_ip對外介面
其中 Xil_DCacheFlushRange()和Xil_DCacheInvalidateRange()兩個函式均在"xil_cache.h"中宣告,用於將cache內容同步到DDR2或相反的操作。之前由於不瞭解cache,導致程式一直得不到正確的結果,總是懷疑硬體問題,後來通過forums.xilinx.com看到了相關的帖子才明白這一點,在此感謝論壇上國內外的技術大牛為社群提供的支援。