本文轉載自FPGA之家
12.1前言
ZYNQ擁有ARM+FPGA這個神奇的架構,那麼ARM和FPGA究竟是如何進行通信的呢?本章通過剖析AXI總線源碼,來一探其中的秘密。
12.2 AXI總線與ZYNQ的關系
AXI(Advanced eXtensible Interface)本是由ARM公司提出的一種總線協定,Xilinx從6系列的FPGA開始對AXI總線提供支援,此時AXI已經發展到了AXI4這個版本,是以當你用到Xilinx的軟體的時候看到的都是“AIX4”的IP,如Vivado打包一個AXI IP的時候,看到的都是Create a new AXI4 peripheral。
到了ZYNQ就更不必說了,AXI總線更是應用廣泛,輕按兩下檢視ZYNQ的IP核的内部配置,随處可見AXI的身影。
12.3 AXI總線和AXI接口以及AXI協定
總線、接口和協定,這三個詞常常被聯系在一起,但是我們心裡要明白他們的差別。
總線是一組傳輸通道,是各種邏輯器件構成的傳輸資料的通道,一般由由資料線、位址線、控制線等構成。接口是一種連接配接标準,又常常被稱之為實體接口。
協定就是傳輸資料的規則。
12.3.1 AXI總線概述
在ZYNQ中有支援三種AXI總線,擁有三種AXI接口,當然用的都是AXI協定。其中三種AXI總線分别為:
AXI4:(For high-performance memory-mapped requirements.)主要面向高性能位址映射通信的需求,是面向位址映射的接口,允許最大256輪的資料突發傳輸;
AXI4-Lite:(For simple, low-throughput memory-mapped communication )是一個輕量級的位址映射單次傳輸接口,占用很少的邏輯單元。
AXI4-Stream:(For high-speed streaming data.)面向高速流資料傳輸;去掉了位址項,允許無限制的資料突發傳輸規模。
首先說AXI4總線和AXI4-Lite總線具有相同的組成部分:
(1)讀位址通道,包含ARVALID, ARADDR, ARREADY信号;
(2)讀資料通道,包含RVALID, RDATA, RREADY, RRESP信号;
(3)寫位址通道,包含AWVALID,AWADDR, AWREADY信号;
(4)寫資料通道,包含WVALID, WDATA,WSTRB, WREADY信号;
(5)寫應答通道,包含BVALID, BRESP, BREADY信号;
(6)系統通道,包含:ACLK,ARESETN信号。
AXI4總線和AXI4-Lite總線的信号也有他的命名特點:
讀位址信号都是以AR開頭(A:address;R:read)
寫位址信号都是以AW開頭(A:address;W:write)
讀資料信号都是以R開頭(R:read)
寫資料信号都是以W開頭(W:write)
應答型号都是以B開頭(B:back(answer back))
了解到總線的組成部分以及命名特點,那麼在後續的實驗中您将逐漸看到他們的身影。每個信号的作用暫停不表,放在後面一一介紹。
而AXI4-Stream總線的組成有:
(1)ACLK信号:總線時鐘,上升沿有效;
(2)ARESETN信号:總線複位,低電平有效
(3)TREADY信号:從機告訴主機做好傳輸準備;
(4)TDATA信号:資料,可選寬度32,64,128,256bit
(5)TSTRB信号:每一bit對應TDATA的一個有效位元組,寬度為TDATA/8
(6)TLAST信号:主機告訴從機該次傳輸為突發傳輸的結尾;
(7)TVALID信号:主機告訴從機資料本次傳輸有效;
(8)TUSER信号 :使用者定義信号,寬度為128bit。
對于AXI4-Stream總線命名而言,除了總線時鐘和總線複位,其他的信号線都是以T字母開頭,後面跟上一個有意義的單詞,看清這一點後,能幫助讀者記憶每個信号線的意義。如TVALID = T+單詞Valid(有效),那麼讀者就應該立刻反應該信号的作用。每個信号的具體作用,在後面分析源碼時再做分析
12.3.2 AXI接口介紹
三種AXI接口分别是:
AXI-GP接口(4個):是通用的AXI接口,包括兩個32位主裝置接口和兩個32位從裝置接口,用過改接口可以通路PS中的片内外設。
AXI-HP接口(4個):是高性能/帶寬的标準的接口,PL子產品作為主裝置連接配接(從下圖中箭頭可以看出)。主要用于PL通路PS上的存儲器(DDR和On-Chip RAM)
AXI-ACP接口(1個):是ARM多核架構下定義的一種接口,中文翻譯為加速器一緻性端口,用來管理DMA之類的不帶緩存的AXI外設,PS端是Slave接口。
我們可以輕按兩下檢視ZYNQ的IP核的内部配置,就能發現上述的三種接口,圖中已用紅色方框标記出來,我們可以清楚的看出接口連接配接與總線的走向:
12.3.3 AXI協定概述
講到協定不可能說是撇開總線單講協定,因為協定的制定也是要建立在總線構成之上的。雖然說AXI4,AXI4-Lite,AXI4-Stream都是AXI4協定,但是各自細節上還是不同的。
總的來說,AXI總線協定的兩端可以分為分為主(master)、從(slave)兩端,他們之間一般需要通過一個AXI Interconnect相連接配接,作用是提供将一個或多個AXI主裝置連接配接到一個或多個AXI從裝置的一種交換機制。當我們添加了zynq以及帶AXI的IP後再進行自動連線時vivado會自動幫我們添加上這個IP,大家應該是不陌生了。
AXI Interconnect的主要作用是,當存在多個主機以及從機器時,AXI Interconnect負責将它們聯系并管理起來。由于AXI支援亂序發送,亂序發送需要主機的ID信号支撐,而不同的主機發送的ID可能相同,而AXI Interconnect解決了這一問題,他會對不同主機的ID信号進行處理讓ID變得唯一。
AXI協定将讀位址通道,讀資料通道,寫位址通道,寫資料通道,寫響應通道分開,各自通道都有自己的握手協定。每個通道互不幹擾卻又彼此依賴。這也是AXI高效的原因之一。
12.3.4 AXI協定之握手協定
AXI4所采用的是一種READY,VALID握手通信機制,簡單來說主從雙方進行資料通信前,有一個握手的過程。傳輸源産生VLAID信号來指明何時資料或控制資訊有效。而目地源産生READY信号來指明已經準備好接受資料或控制資訊。傳輸發生在VALID和READY信号同時為高的時候。VALID和READY信号的出現有三種關系。
(1) VALID先變高READY後變高。時序圖如下:
在箭頭處資訊傳輸發生。
(2) READY先變高VALID後變高。時序圖如下:
同樣在箭頭處資訊傳輸發生。
(3) VALID和READY信号同時變高。時序圖如下:
在這種情況下,資訊傳輸立馬發生,如圖箭頭處指明資訊傳輸發生。
需要強調的是,AXI的五個通道,每個通道都有握手機制,接下來我們就來分析一下AXI-Lite的源碼來更深入的了解AXI機制。
12.3.5 突發式讀寫
1、突發式讀的時序圖如下:
當位址出現在位址總線後,傳輸的資料将出現在讀資料通道上。裝置保持VALID為低直到讀資料有效。為了表明一次突發式讀寫的完成,裝置用RLAST信号來表示最後一個被傳輸的資料。
2、 突發式寫時序圖如下:
這一過程的開始時,主機發送位址和控制資訊到寫位址通道中,然後主機發送每一個寫資料到寫資料通道中。當主機發送最後一個資料時,WLAST信号就變為高。當裝置接收完所有資料之後他将一個寫響應發送回主機來表明寫事務完成。
12.4 AXI4-Lite詳解
12.4.1 AXI4-Lite源碼檢視
Step1:要看到AXI-Lite的源碼,我們先要自定義一個AXI-Lite的IP,建立工程之後,選擇,菜單欄->Tools->Creat and Package IP:
Step2:選擇Next
Step3:選擇Create AXI4 Peripheral,然後Next:
Step4:預設,選擇Next
Step5:注意這裡接口類型選擇Lite,選擇Next:
Step6:選擇Edit IP,點選Finish:
Step7:此後,Vivado會建立一個工程,專門編輯該IP,通過該工程,我們就可以看到Vivado為我們生成的AXI-Lite的操作源碼:
12.4.2 AXI-Lite 源碼分析
當打開頂層檔案的時,映入眼簾的是一堆AXI的信号,這些信号是否似曾相識?
input wire s00_axi_aclk, input wire s00_axi_aresetn, input wire [C_S00_AXI_ADDR_WIDTH-1 : 0] s00_axi_awaddr, input wire [2 : 0] s00_axi_awprot, input wire s00_axi_awvalid, output wire s00_axi_awready, input wire [C_S00_AXI_DATA_WIDTH-1 : 0] s00_axi_wdata, input wire [(C_S00_AXI_DATA_WIDTH/8)-1 : 0] s00_axi_wstrb, input wire s00_axi_wvalid, output wire s00_axi_wready, output wire [1 : 0] s00_axi_bresp, output wire s00_axi_bvalid, input wire s00_axi_bready, input wire [C_S00_AXI_ADDR_WIDTH-1 : 0] s00_axi_araddr, input wire [2 : 0] s00_axi_arprot, input wire s00_axi_arvalid, output wire s00_axi_arready, output wire [C_S00_AXI_DATA_WIDTH-1 : 0] s00_axi_rdata, output wire [1 : 0] s00_axi_rresp, output wire s00_axi_rvalid, input wire s00_axi_rready |
沒錯筆者曾在《AXI總線概述》這節中提到了他們,這次通過源碼分析再次隆重介紹它們。
讀 通 道 | 位址通道 | 資料通道 | |||
ARVALID | 讀位址有效。此信号表明該信道此時能有效讀出位址和控制資訊 | RVALID | 讀資料有效。此信号表明該信道此時能有效讀出資料 | ||
ARADDR | 讀位址 | RDATA | 讀資料 | ||
ARREADY | 讀位址準備好了。該信号訓示從器件準備好接受一個位址和相關聯的控制信号 | RREADY | 讀資料準備好了。該信号訓示從器件準備好接收資料 | ||
ARPROT | 保護類型。這個信号表示該事務的特權和安全級别,并确定是否該事務是一個資料存取或指令的通路 | RRESP | 讀取響應。這個信号表明讀事務處理的狀态。 | ||
寫 通 道 | 位址通道 | 資料通道 | 應答通道 | ||
AWVALID | 寫位址有效。這個信号表示該主信令有效的寫位址和控制資訊。 | WVALID | 寫有效。這個信号表示有效的寫資料和選通信号都可用。 | BVALID | 寫響應有效。此信号表明寫指令的有效寫入響應。 |
AWADDR | 寫位址 | WDATA | 寫資料 | BREADY | 響應準備。該信号訓示在主主機可以接受一個響應信号 |
AWREADY | 寫位址準備好了。該信号訓示從器件準備好接受一個位址和相關聯的控制信号 | WSTRB | 寫選通。這個信号表明該位元組通道持有效資料。每一bit對應WDATA一個位元組 | BRESP | 寫響應。這個信号表示寫事務處理的狀态。 |
AWPROT | 寫通道保護類型。這個信号表示該事務的特權和安全級别,并确定是否該事務是一個資料存取或指令的通路 | WREADY | 寫準備好了。該信号訓示從器件可以接受寫資料。 |
Vivado為我們生成的AXI-Lite的操作源碼,是一個例子,我隻需要讀懂他,然後稍加修改,就可以為我們所用。
我們先來看一段WDATA相關的代碼:
always @( posedge S_AXI_ACLK ) begin if ( S_AXI_ARESETN == 1'b0 ) begin slv_reg0 <= 0; slv_reg1 <= 0; slv_reg2 <= 0; slv_reg3 <= 0; end else begin if (slv_reg_wren) begin case ( axi_awaddr[ADDR_LSB+OPT_MEM_ADDR_BITS:ADDR_LSB] ) 2'h0: for ( byte_index = 0; byte_index <= (C_S_AXI_DATA_WIDTH/8)-1; byte_index = byte_index+1 ) if ( S_AXI_WSTRB[byte_index] == 1 ) begin // Respective byte enables are asserted as per write strobes // Slave register 0 slv_reg0[(byte_index*8) +: 8] <= S_AXI_WDATA[(byte_index*8) +: 8]; end 2'h1: for ( byte_index = 0; byte_index <= (C_S_AXI_DATA_WIDTH/8)-1; byte_index = byte_index+1 ) if ( S_AXI_WSTRB[byte_index] == 1 ) begin // Respective byte enables are asserted as per write strobes // Slave register 1 slv_reg1[(byte_index*8) +: 8] <= S_AXI_WDATA[(byte_index*8) +: 8]; end 2'h2: for ( byte_index = 0; byte_index <= (C_S_AXI_DATA_WIDTH/8)-1; byte_index = byte_index+1 ) if ( S_AXI_WSTRB[byte_index] == 1 ) begin // Respective byte enables are asserted as per write strobes // Slave register 2 slv_reg2[(byte_index*8) +: 8] <= S_AXI_WDATA[(byte_index*8) +: 8]; end 2'h3: for ( byte_index = 0; byte_index <= (C_S_AXI_DATA_WIDTH/8)-1; byte_index = byte_index+1 ) if ( S_AXI_WSTRB[byte_index] == 1 ) begin // Respective byte enables are asserted as per write strobes // Slave register 3 slv_reg3[(byte_index*8) +: 8] <= S_AXI_WDATA[(byte_index*8) +: 8]; end default : begin slv_reg0 <= slv_reg0; slv_reg1 <= slv_reg1; slv_reg2 <= slv_reg2; slv_reg3 <= slv_reg3; end endcase end end end |
這段程式的作用是,當PS那邊向AXI4-Lite總線寫資料時,PS這邊負責将資料接收到寄存器slv_reg。而slv_reg寄存器有0~3共4個。至于指派給哪一個由
axi_awaddr[ADDR_LSB+OPT_MEM_ADDR_BITS:ADDR_LSB]決定,根據宏定義其實就是由axi_awaddr[3:2] (寫位址中不僅包含位址,而且包含了控制位,這裡的[3:2]就是控制位)決定指派給哪個slv_reg。
PS調用寫函數時,如果不做位址偏移的話,axi_awaddr[3:2]的值預設是為0的,舉個例子,如果我們自定義的IP的位址被映射為0x43C00000,那麼我們Xil_Out32(0x43C00000,Value)寫的就是slv_reg0的值。如果位址偏移4位,如
Xil_Out32(0x43C00000 + 4,Value) 寫的就是slv_reg1的值,依次類推。
分析時隻關注slv_reg0(其他結構上也是一模一樣的):
for ( byte_index = 0; byte_index <= (C_S_AXI_DATA_WIDTH/8)-1; byte_index = byte_index+1 ) if ( S_AXI_WSTRB[byte_index] == 1 ) begin slv_reg0[(byte_index*8) +: 8] <= S_AXI_WDATA[(byte_index*8) +: 8]; end |
其中,C_S_AXI_DATA_WIDTH的宏定義的值為32,也就是資料位寬,S_AXI_WSTRB就是寫選通信号,S_AXI_WDATA就是寫資料信号。
存在于for循環中的最關鍵的一句:
slv_reg0[(byte_index*8) +: 8] <= S_AXI_WDATA[(byte_index*8) +: 8];
當byte_index = 0的時候這句話就等價于:
slv_reg0[7:0] <= S_AXI_WDATA[7:0];
當byte_index = 1的時候這句話就等價于:
slv_reg0[15:8] <= S_AXI_WDATA[15:8];
當byte_index = 2的時候這句話就等價于:
slv_reg0[23:16] <= S_AXI_WDATA[23:16];
當byte_index = 3的時候這句話就等價于:
slv_reg0[31:24] <= S_AXI_WDATA[31:24];
也就是說,隻有當寫選通信号為1時,它所對應S_AXI_WDATA的位元組才會被讀取。
讀懂了這段話之後,我們就知道了,如果我們想得到PS寫到總線上的資料,我們隻需要讀取slv_reg0的值即可。
那如果,我們想寫資料到總線讓PS讀取該資料,我們該怎麼做呢?我們繼續來看有關RADTA讀資料代碼:
// Output register or memory read data always @( posedge S_AXI_ACLK ) begin if ( S_AXI_ARESETN == 1'b0 ) begin axi_rdata <= 0; end else begin // When there is a valid read address (S_AXI_ARVALID) with // acceptance of read address by the slave (axi_arready), // output the read dada if (slv_reg_rden) begin axi_rdata <= reg_data_out; // register read data end end end |
觀察可知,當PS讀取資料時,程式會把reg_data_out複制給axi_rdata(RADTA讀資料)。我們繼續追蹤reg_data_out:
always @(*) begin // Address decoding for reading registers case ( axi_araddr[ADDR_LSB+OPT_MEM_ADDR_BITS:ADDR_LSB] ) 2'h0 : reg_data_out <= slv_reg0; 2'h1 : reg_data_out <= slv_reg1; 2'h2 : reg_data_out <= slv_reg2; 2'h3 : reg_data_out <= slv_reg3; default : reg_data_out <= 0; endcase end |
和前面分析的一樣此時通過判斷axi_awaddr[3:2]的值來判斷将那個值給reg_data_out上,同樣當PS調用讀取函數時,這裡axi_awaddr[3:2]預設是0,是以我們隻需要把slv_reg0替換成我們自己資料,就可以讓PS通過總線讀到我們提供的資料。
這裡可能有的讀者會問了,slv_reg0不是總線寫過來的資料嗎?因為筆者說過這個程式是Vivado為我們提供的例子,它這麼做無非是想驗證我寫出去的值和我讀進入的值相等。但是他怎麼寫确實會對初看代碼的人造成困擾。
最後筆者提出一個問題,為什麼寫通道要比讀通道多了一列應答通道,這是為什麼呢?
首先,你要知道這個應答信号是幹什麼用的?
寫應答,主要是回複主機你這個寫過程是沒有問題的,那讀為什麼不需要這個過程呢?
這時因為主機在讀取資料時,從機可以直接通過讀資料通道給主機回報資訊,是以就沒有必要再來開辟一個單獨的應答通道了。
小結:
如果我們想讀AXI4_Lite總線上的資料時,隻需關注slv_reg的資料,我們可自行添加一段代碼,如:
reg [11:0]rlcd_rgb; always @( posedge S_AXI_ACLK ) begin if ( S_AXI_ARESETN == 1'b0 ) begin rlcd_rgb <= 12'd0; end else begin rlcd_rgb <= slv_reg0[11:0]; end end assign lcd_rgb = rlcd_rgb; |
如果我們想對AXI4_Lite信号寫資料時,我們隻需修改對reg_data_out的指派,如:
//寫總線測試修改!!!!!!!!! wire[31:0]wlcd_xy;// = {10'd0,lcd_xy}; assign wlcd_xy = {10'd0,lcd_xy}; assign slv_reg_rden = axi_arready & S_AXI_ARVALID & ~axi_rvalid; always @(*) begin // Address decoding for reading registers case ( axi_araddr[ADDR_LSB+OPT_MEM_ADDR_BITS:ADDR_LSB] ) 2'h0 : reg_data_out <= wlcd_xy;//slv_reg0; 2'h1 : reg_data_out <= slv_reg1; 2'h2 : reg_data_out <= slv_reg2; 2'h3 : reg_data_out <= slv_reg3; default : reg_data_out <= 0; endcase end |
最後強調下如果我們自定義的IP的位址被映射為0x43C00000,那麼我們Xil_Out32(0x43C00000,Value)寫的就是slv_reg0的值。如果位址偏移4位,如
Xil_Out32(0x43C00000 + 4,Value) 寫的就是slv_reg1的值,依次類推。
目前這裡隻有4個寄存器,那是因為之前選擇的是4個,其實我們可以定義的更多:
在ps的頭檔案裡可以看到我們自定義的IP的位址是有個範圍的
#define XPAR_MYIPFREQUENCY_0_S00_AXI_BASEADDR 0x43C00000
#define XPAR_MYIPFREQUENCY_0_S00_AXI_HIGHADDR 0x43C0FFFF
理論上隻要基位址 + 偏移量不要超過HIGHADDR即可。
12.5 觀察AXI4-Lite總線信号
在第十章,我們封裝了一個AXI_Lite的GPIO,通過本章的分析,我們在第十章工程的基礎上通過添加一個ila核的方式,來具體看看AXI_Lite總線的信号。
Step1:做好第十章工程的備份,然後直接打開第十章的工程。
Step2:單擊IP icon
添加 ila CORE
Step3:輕按兩下打開ILA CORE
Step4:輕按兩下打開ILA CORE
General Options設定如下
Probe_Ports設定如下,之後單擊OK
Step5:連接配接Probe0到GPIO_LED。
Step6:連接配接CLK接口到FCLK_CLK0接口
Step7:選中Processing_System7_0_axi_periph和GPIO_LITE_ML_0之間的S_AXI總線。
Step8:右擊選擇Mark Debug
Step9:接下來依然是,右鍵單擊Block檔案,檔案選擇Generate the Output Products。
Step10:繼續右鍵單擊Block檔案,選擇Create a HDL wrapper,根據Block檔案内容産生一個HDL 的頂層檔案,并選擇讓vivado自動完成。
Setp11:單擊Run Synthesis,如果有 Save 對話框彈出選擇儲存。
Setp12:綜合結束後選擇Synthesized Design option單擊 OK。
Step13:在如下對話框中找到Unassigned debug nets(如果對話框沒有出現選擇 菜單->Window > Debug)
Step14:右擊 Unassigned Debug Nets 選擇Set up Debug… 之後單擊 Next
Step15:删除紅色錯誤的信号然後單擊Next 到結束
Step16:生成Bit檔案。
12.6 加載到SDK
Step1:導出硬體。
Step2:右擊工程,選擇Debug as ->Debug configuration。
Step3:選中system Debugger,輕按兩下建立一個系統調試。
Step4:設定系統調試。
Step5:回到VIVADO單擊Open Target->Auto Connect
Step6:加載完成後的界面
Step7:選擇菜單->window->Debugprobes 選擇AXI_WVALID和AXI_AWVALID做為觸發信号
Step8:設定觸發條件為1
Step9:設定觸發位置為512
Step10:單擊箭頭所指向啟動觸發
Step11:進入等待觸發狀态
Step12:單擊運作
後VIVADO HW_ILA2 視窗采集到波形輸出,可以看到AXI總線的工作時序。
Step13:HW_ILA1 視窗采集到的資料是GPIO_LED的值為0x02,同時可觀察到開發闆上的LED2亮起。
12.7 本章小結
通過本章的學習,我們首先得認識到總線和接口以及協定的差別,其次通過分析AXI4-Lite,AXI4-Stream,AXI4總線的從機代碼,對AXI協定有一定的認識,那麼在後面學習AXI的一些IP時就不會有恐懼的心理。
最後,我們再理一理AXI總線和AXI接口的關系。在ZYNQ中,支援AXI4-Lite,AXI4和AXI4-Stream三種總線協定,這前面已經說過了,要注意的是PS與PL之間的接口(AXI-GP接口,AXI-HP接口以及AXI-ACP接口)卻隻支援AXI-Lite和AXI協定這兩種總線協定。也就是說PL這邊的AXI-Stream的接口是不能直接與PS對接的,需要經過AXI4或者AXI4-Lite的轉換。比如後面将用到的VDMA IP ,它就實作了在PL内部AXI4到AXI-Stream的轉換,VDMA利用的接口就是AXI-HP接口。