前言
本文從例子程序細(xì)節(jié)上(語(yǔ)法層面)去理解PCIe對(duì)于事物層數(shù)據(jù)的接收及解析。參考數(shù)據(jù)手冊(cè):PG054;例子程序有Vivado生成;
為什么將這個(gè)內(nèi)容寫(xiě)出來(lái)?
通過(guò)寫(xiě)博客,可以檢驗(yàn)自己理解了這個(gè)設(shè)計(jì)沒(méi)有,這像是一個(gè)提問(wèn)題并自我解讀的過(guò)程,如果你提出了問(wèn)題,但發(fā)現(xiàn)自己解決不了,那問(wèn)題就在這里。
例程是入門(mén)某一個(gè)IP核的最好途徑,它可以作為你進(jìn)一步設(shè)計(jì)的基礎(chǔ)。你的后續(xù)設(shè)計(jì)都可以基于此。
正文
理解一個(gè)新的設(shè)計(jì)的最好方法是仿真,Aurora如此,PCIe也是如此,自己定制一個(gè)PCIe的IP核,之后右擊生成相應(yīng)的例程。
該例程是一個(gè)PIO例程,所謂的PIO,其全稱為:The Programmed Input/Output (PIO) ,即可編程輸入輸出。
編程輸入/輸出(PIO)事務(wù)通常由PCI Express系統(tǒng)主機(jī)CPU用于訪問(wèn)PCI Express邏輯中的內(nèi)存映射輸入/輸出(MMIO)和配置映射輸入/輸出(CMIO)位置。PCI Express的端點(diǎn)接受內(nèi)存和I/O寫(xiě)事務(wù),并以帶有數(shù)據(jù)的完成事務(wù)來(lái)響應(yīng)內(nèi)存和I/O讀事務(wù)。
FPGA端作為Endpoint,PC端作為Root,其對(duì)FPGA的存儲(chǔ)空間進(jìn)行讀寫(xiě),讀寫(xiě)分為很多類別,可以是存儲(chǔ)器讀寫(xiě),也可以是I/O讀寫(xiě),細(xì)節(jié)可在數(shù)據(jù)手冊(cè)上進(jìn)行學(xué)習(xí)。
仿真平臺(tái)
仿真平臺(tái)的結(jié)構(gòu)圖如下:
上面的部分為用戶邏輯,我們這里面接收并解析PCIe IP核收到的請(qǐng)求,例如讀請(qǐng)求包,我們就會(huì)返還一個(gè)完成包,或者是一個(gè)寫(xiě)請(qǐng)求包,我們負(fù)責(zé)將寫(xiě)數(shù)據(jù)寫(xiě)入FPGA RAM空間等等。如:
下面部分是PCIe IP經(jīng)過(guò)例程包裝后的部分,它與Root端進(jìn)行高速串行通信。通信速率以及數(shù)據(jù)帶寬根據(jù)PCIe IP的配置有關(guān),例如是Gen2 X1就是單通道且鏈路速率是5Gbps的PCIe;Gen1 X1則是單通道且鏈路速率為2.5Gbps的 PCIe.
下圖是例程中的用戶邏輯部分的模塊結(jié)構(gòu)圖:
我們常??吹紼P作為前綴的命名,其意思就是Endpoint,指的就是FPGA這端。我們都知道PCIe是端對(duì)端通信,協(xié)議規(guī)定的就是PC端為Root,而FPGA端為Endpoint。
可見(jiàn),上面有如下幾個(gè)模塊:EP_RX:該模塊是接收來(lái)自PCIe IP核收到的請(qǐng)求,該請(qǐng)求肯定是來(lái)自于PC端或者叫Root端,讀請(qǐng)求或者是寫(xiě)請(qǐng)求;一般而言,收到一個(gè)請(qǐng)求包之后,RX會(huì)對(duì)其進(jìn)行解析,如果是讀請(qǐng)求,則需要通過(guò)另一個(gè)發(fā)送模塊回復(fù)一個(gè)讀完成包。
EP_TX:該模塊用來(lái)向Root端發(fā)送數(shù)據(jù)包,該包在這個(gè)模塊組裝,然后通過(guò)AXI-S協(xié)議發(fā)送給IP核,進(jìn)而與Root進(jìn)行通信。
EP_MEM:該模塊的作用很簡(jiǎn)單,就是一個(gè)存儲(chǔ)結(jié)構(gòu),由于Root向EP發(fā)送讀寫(xiě)請(qǐng)求,例如讀,從哪里讀數(shù)據(jù)呢?就在這個(gè)模塊里呀,寫(xiě)到哪里去呢?也是從這個(gè)模塊里呀。
PIO_TO_CTRL:這個(gè)模塊的作用呢?是管理cfg_turnoff_ok這個(gè)信號(hào)的,具體什么用?需要斟酌!
例程手冊(cè)程序概括
PIO設(shè)計(jì)是一個(gè)簡(jiǎn)單的只針對(duì)目標(biāo)的應(yīng)用程序,它與PCIe核心事務(wù)(AXI4-Stream)的端點(diǎn)接口相連接,并被提供作為構(gòu)建自己設(shè)計(jì)的起點(diǎn)。
為了直觀地理解Root Complex與Endpoint之間的區(qū)別,我們以下面的PCIe系統(tǒng)結(jié)構(gòu)圖為例,來(lái)說(shuō)明數(shù)據(jù)的傳輸情況:
上圖中多了一個(gè)PCIe Switch結(jié)構(gòu),不過(guò)沒(méi)關(guān)系,我們可以把它當(dāng)成中間的過(guò)渡結(jié)構(gòu),它不影響我們Endpoint端以及Root complex端的數(shù)據(jù)處理。
圖5-3說(shuō)明了PCI Express系統(tǒng)結(jié)構(gòu)組件,由一個(gè)Root Complex、一個(gè)PCI Express交換設(shè)備和一個(gè)PCIe的Endpoint組成。PIO操作將數(shù)據(jù)從Root Complex(CPU寄存器)向下游移動(dòng)到Endpoint,和/或從Endpoint向上游移動(dòng)到Root Complex(CPU寄存器)。在這兩種情況下,移動(dòng)數(shù)據(jù)的PCI Express協(xié)議請(qǐng)求都是由主機(jī)CPU發(fā)起的。
當(dāng)CPU向MMIO地址命令發(fā)出存儲(chǔ)寄存器時(shí),數(shù)據(jù)將向下游移動(dòng)。Root Complex通常會(huì)生成一個(gè)具有適當(dāng)MMIO地址的存儲(chǔ)器寫(xiě)TLP包和字節(jié)使能。當(dāng)Endpoint接收到存儲(chǔ)器寫(xiě)TLP并更新相應(yīng)的本地寄存器時(shí),事務(wù)終止。
當(dāng)CPU通過(guò)MMIO地址命令發(fā)出加載寄存器時(shí),數(shù)據(jù)將向上游移動(dòng)。Root Complex通常會(huì)生成具有適當(dāng)MMIO位置地址的存儲(chǔ)器讀TLP包和字節(jié)使能。Endpoint在收到“內(nèi)存讀取” TLP后會(huì)生成“數(shù)據(jù)TLP完成包”。將完成操作引導(dǎo)到Root Complex,并將有效負(fù)載加載到目標(biāo)寄存器中,從而完成事務(wù)。
此兩端較為生澀,放入英文原文:
Data is moved downstream when the CPU issues a store register to a MMIO address command. The Root Complex typically generates a Memory Write TLP with the appropriate MMIO location address, byte enables, and the register contents. The transaction terminates when the Endpoint receives the Memory Write TLP and updates the corresponding local register.
Data is moved upstream when the CPU issues a load register from a MMIO address command. The Root Complex typically generates a Memory Read TLP with the appropriate MMIO location address and byte enables. The Endpoint generates a Completion with Data TLP after it receives the Memory Read TLP. The Completion is steered to the Root Complex and payload is loaded into the target register, completing the transaction.
例程用戶邏輯包括如下文件:
應(yīng)用程序內(nèi)部數(shù)據(jù)寬度,即AXI-Stream數(shù)據(jù)總線寬度根據(jù)鏈路通道數(shù)不同而不同,其關(guān)系為:
則在程序里也有體現(xiàn),例如我使用的是X1模式,因此:
該例程的所有模塊組件:
則從文件結(jié)構(gòu)也能看出:
應(yīng)用程序,也即用戶邏輯的接口關(guān)系為:
這里是以X1為例。
應(yīng)用程序中的接收模塊:
接收來(lái)自于PCIe IP核的數(shù)據(jù),該模塊與PCIe IP模塊之間的接口為AXI-Stream,后面就不在贅述,對(duì)來(lái)自Root Complex端的讀寫(xiě)請(qǐng)求包(TLP)進(jìn)行接收并解析。
假如接收到了Root端的讀請(qǐng)求TLP,則輸出信號(hào)如下:
這都是對(duì)接收的數(shù)據(jù)包進(jìn)行解析出來(lái)的結(jié)果,我們都知道PCIe是以包的形式來(lái)發(fā)送數(shù)據(jù)或者接收數(shù)據(jù)。TLP包的結(jié)構(gòu)可見(jiàn)PCIe的事務(wù)處包(TLP)的組成,則在數(shù)據(jù)手冊(cè)PG054上也是詳細(xì)描述的。對(duì)這個(gè)包的輸出發(fā)送給TX模塊,把讀出來(lái)的數(shù)據(jù)一同組成一個(gè)完成包,發(fā)送給PCIe IP核進(jìn)而發(fā)送給Root Complex,這個(gè)過(guò)程是一個(gè)響應(yīng),對(duì)讀請(qǐng)求的一個(gè)響應(yīng),這需要另一個(gè)模塊,也即TX模塊進(jìn)行配合。下面會(huì)講到。
如果EP接收到的包是寫(xiě)請(qǐng)求包,則EP_RX會(huì)生成另外一些信號(hào):
輸出給存儲(chǔ)器訪問(wèn)模塊,對(duì)存儲(chǔ)器模塊進(jìn)行寫(xiě)數(shù)據(jù)。
發(fā)送模塊的接口示意圖:
右端為輸出的接口,為AXI-stream接口,與PCIe IP核連接,送出IP核需要的完成包。
其輸入與RX的輸入對(duì)應(yīng):
無(wú)論是讀還是寫(xiě),總得有個(gè)存儲(chǔ)器寫(xiě)進(jìn)入或者讀出來(lái)才行,這就是這個(gè)模塊:
其輸入輸出關(guān)系一目了然,不在話下。
按照數(shù)據(jù)手冊(cè)得說(shuō)法就是:
這個(gè)模塊就是處理來(lái)自于存儲(chǔ)器以及IO寫(xiě)得TLP包得數(shù)據(jù),將其寫(xiě)入存儲(chǔ)器,或者呢?用來(lái)響應(yīng)存儲(chǔ)器或者IO讀TLP包,從存儲(chǔ)器中讀出數(shù)據(jù);
對(duì)于寫(xiě)請(qǐng)求包,其接口如下:
對(duì)與讀,其接口如下:
下面講下對(duì)于讀請(qǐng)求事務(wù)包及其響應(yīng)完成包的時(shí)序關(guān)系:
如圖所示,先是接收到一個(gè)讀請(qǐng)求事務(wù)包,但第一個(gè)TLP包完成接收的時(shí)候,立刻令ready無(wú)效,并響應(yīng)一個(gè)完成包。等完成包響應(yīng)完成之后,拉高信號(hào)compl_done,表明響應(yīng)完成,之后再接收下一個(gè)事務(wù)包。
下面是寫(xiě)事物請(qǐng)求TLP的時(shí)序關(guān)系:
首先接收一個(gè)寫(xiě)請(qǐng)求事務(wù)包,然后寫(xiě)入存儲(chǔ)器,寫(xiě)入的過(guò)程中,拉高wr_busy,表明正在寫(xiě)。寫(xiě)入完成之后,令wr_busy無(wú)效,表明寫(xiě)入完成。之后再接收另一個(gè)寫(xiě)事務(wù)包。
這個(gè)例程的用戶程序消耗的資源為:
這表明使用了4個(gè)BRAM,就是用來(lái)寫(xiě)入以及讀出來(lái)自Root請(qǐng)求的數(shù)據(jù)的存儲(chǔ)器。
例程仿真分析
PIO_RX_ENGINE.v 分析:
首先,定義了一個(gè)變量in_packet_q,高有效,用來(lái)表示接收一個(gè)TLP包。
如下:
wire sop; // Start of packet
reg in_packet_q;
always@(posedge clk)
begin
if(!rst_n)
in_packet_q <= # TCQ 1'b0;
else if (m_axis_rx_tvalid && m_axis_rx_tready && m_axis_rx_tlast)
in_packet_q <= # TCQ 1'b0;
else if (sop && m_axis_rx_tready)
in_packet_q <= # TCQ 1'b1;
end
assign sop = !in_packet_q && m_axis_rx_tvalid;
sop表示包的開(kāi)始,sop有效的條件自然是in_packet_q無(wú)效且valid有效;即:
assign sop = !in_packet_q && m_axis_rx_tvalid;
包什么時(shí)候有效呢?可以看出是sop有效且ready有效,這時(shí)候有人可能就有點(diǎn)暈了,到底是in_packet_q決定sop呢?還是sop決定in_packet_q呢?那必然是in_packet_q決定sop呀,因?yàn)閟op的含義是包的開(kāi)始呀。將sop代入in_packet_q有效的條件中去:
always@(posedge clk)
begin
if(!rst_n)
in_packet_q <= # TCQ 1'b0;
else if (m_axis_rx_tvalid && m_axis_rx_tready && m_axis_rx_tlast)
in_packet_q <= # TCQ 1'b0;
else if (!in_packet_q && m_axis_rx_valid && m_axis_rx_tready)
in_packet_q <= # TCQ 1'b1;
end
這就很明白了,其實(shí)這段程序的作用(請(qǐng)?jiān)试S我用程序來(lái)代表硬件描述語(yǔ)言)就是判斷包有效的標(biāo)志。valid和ready有效,這packet有效,一直持續(xù)到valid,ready,以及l(fā)ast都有效,last表示最后一個(gè)數(shù)據(jù)??梢詮姆抡鎴D中來(lái)觀察:
有了包的起始標(biāo)志,就可以通過(guò)判斷這個(gè)信號(hào)有效,進(jìn)入了包的解析狀態(tài)機(jī);這里使用了一個(gè)狀態(tài)機(jī)來(lái)處理接收的TLP,對(duì)其進(jìn)行解析,解析數(shù)據(jù):
always @ ( posedge clk ) begin
if (!rst_n )
begin
m_axis_rx_tready <= #TCQ 1'b0;
req_compl <= #TCQ 1'b0;
req_compl_wd <= #TCQ 1'b1;
req_tc <= #TCQ 3'b0;
req_td <= #TCQ 1'b0;
req_ep <= #TCQ 1'b0;
req_attr <= #TCQ 2'b0;
req_len <= #TCQ 10'b0;
req_rid <= #TCQ 16'b0;
req_tag <= #TCQ 8'b0;
req_be <= #TCQ 8'b0;
req_addr <= #TCQ 13'b0;
wr_be <= #TCQ 8'b0;
wr_addr <= #TCQ 11'b0;
wr_data <= #TCQ 32'b0;
wr_en <= #TCQ 1'b0;
state <= #TCQ PIO_RX_RST_STATE;
tlp_type <= #TCQ 8'b0;
end
else
begin
wr_en <= #TCQ 1'b0;
req_compl <= #TCQ 1'b0;
case (state)
PIO_RX_RST_STATE : begin
m_axis_rx_tready <= #TCQ 1'b1;
req_compl_wd <= #TCQ 1'b1;
if (sop)
begin
case (m_axis_rx_tdata[30:24])
PIO_RX_MEM_RD32_FMT_TYPE : begin
tlp_type <= #TCQ m_axis_rx_tdata[31:24];
req_len <= #TCQ m_axis_rx_tdata[9:0];
m_axis_rx_tready <= #TCQ 1'b0;
if (m_axis_rx_tdata[9:0] == 10'b1)
begin
req_tc <= #TCQ m_axis_rx_tdata[22:20];
req_td <= #TCQ m_axis_rx_tdata[15];
req_ep <= #TCQ m_axis_rx_tdata[14];
req_attr <= #TCQ m_axis_rx_tdata[13:12];
req_len <= #TCQ m_axis_rx_tdata[9:0];
req_rid <= #TCQ m_axis_rx_tdata[63:48];
req_tag <= #TCQ m_axis_rx_tdata[47:40];
req_be <= #TCQ m_axis_rx_tdata[39:32];
state <= #TCQ PIO_RX_MEM_RD32_DW1DW2;
end // if (m_axis_rx_tdata[9:0] == 10'b1)
else
begin
state <= #TCQ PIO_RX_RST_STATE;
end // if !(m_axis_rx_tdata[9:0] == 10'b1)
end // PIO_RX_MEM_RD32_FMT_TYPE
PIO_RX_MEM_WR32_FMT_TYPE : begin
tlp_type <= #TCQ m_axis_rx_tdata[31:24];
req_len <= #TCQ m_axis_rx_tdata[9:0];
m_axis_rx_tready <= #TCQ 1'b0;
if (m_axis_rx_tdata[9:0] == 10'b1)
begin
wr_be <= #TCQ m_axis_rx_tdata[39:32];
state <= #TCQ PIO_RX_MEM_WR32_DW1DW2;
end // if (m_axis_rx_tdata[9:0] == 10'b1)
else
begin
state <= #TCQ PIO_RX_RST_STATE;
end // if !(m_axis_rx_tdata[9:0] == 10'b1)
end // PIO_RX_MEM_WR32_FMT_TYPE
PIO_RX_MEM_RD64_FMT_TYPE : begin
tlp_type <= #TCQ m_axis_rx_tdata[31:24];
req_len <= #TCQ m_axis_rx_tdata[9:0];
m_axis_rx_tready <= #TCQ 1'b0;
if (m_axis_rx_tdata[9:0] == 10'b1)
begin
req_tc <= #TCQ m_axis_rx_tdata[22:20];
req_td <= #TCQ m_axis_rx_tdata[15];
req_ep <= #TCQ m_axis_rx_tdata[14];
req_attr <= #TCQ m_axis_rx_tdata[13:12];
req_len <= #TCQ m_axis_rx_tdata[9:0];
req_rid <= #TCQ m_axis_rx_tdata[63:48];
req_tag <= #TCQ m_axis_rx_tdata[47:40];
req_be <= #TCQ m_axis_rx_tdata[39:32];
state <= #TCQ PIO_RX_MEM_RD64_DW1DW2;
end // if (m_axis_rx_tdata[9:0] == 10'b1)
else
begin
state <= #TCQ PIO_RX_RST_STATE;
end // if !(m_axis_rx_tdata[9:0] == 10'b1)
end // PIO_RX_MEM_RD64_FMT_TYPE
PIO_RX_MEM_WR64_FMT_TYPE : begin
tlp_type <= #TCQ m_axis_rx_tdata[31:24];