前言
本文從例子程序細節上(語法層面)去理解PCIe對于事物層數據的接收及解析。參考數據手冊:PG054;例子程序有Vivado生成;
為什么將這個內容寫出來?
通過寫博客,可以檢驗自己理解了這個設計沒有,這像是一個提問題并自我解讀的過程,如果你提出了問題,但發現自己解決不了,那問題就在這里。
例程是入門某一個IP核的最好途徑,它可以作為你進一步設計的基礎。你的后續設計都可以基于此。
正文
理解一個新的設計的最好方法是仿真,Aurora如此,PCIe也是如此,自己定制一個PCIe的IP核,之后右擊生成相應的例程。
該例程是一個PIO例程,所謂的PIO,其全稱為:The Programmed Input/Output (PIO) ,即可編程輸入輸出。
編程輸入/輸出(PIO)事務通常由PCI Express系統主機CPU用于訪問PCI Express邏輯中的內存映射輸入/輸出(MMIO)和配置映射輸入/輸出(CMIO)位置。PCI Express的端點接受內存和I/O寫事務,并以帶有數據的完成事務來響應內存和I/O讀事務。
FPGA端作為Endpoint,PC端作為Root,其對FPGA的存儲空間進行讀寫,讀寫分為很多類別,可以是存儲器讀寫,也可以是I/O讀寫,細節可在數據手冊上進行學習。
仿真平臺
仿真平臺的結構圖如下:
上面的部分為用戶邏輯,我們這里面接收并解析PCIe IP核收到的請求,例如讀請求包,我們就會返還一個完成包,或者是一個寫請求包,我們負責將寫數據寫入FPGA RAM空間等等。如:
下面部分是PCIe IP經過例程包裝后的部分,它與Root端進行高速串行通信。通信速率以及數據帶寬根據PCIe IP的配置有關,例如是Gen2 X1就是單通道且鏈路速率是5Gbps的PCIe;Gen1 X1則是單通道且鏈路速率為2.5Gbps的 PCIe.
下圖是例程中的用戶邏輯部分的模塊結構圖:
我們常常看到EP作為前綴的命名,其意思就是Endpoint,指的就是FPGA這端。我們都知道PCIe是端對端通信,協議規定的就是PC端為Root,而FPGA端為Endpoint。
可見,上面有如下幾個模塊:EP_RX:該模塊是接收來自PCIe IP核收到的請求,該請求肯定是來自于PC端或者叫Root端,讀請求或者是寫請求;一般而言,收到一個請求包之后,RX會對其進行解析,如果是讀請求,則需要通過另一個發送模塊回復一個讀完成包。
EP_TX:該模塊用來向Root端發送數據包,該包在這個模塊組裝,然后通過AXI-S協議發送給IP核,進而與Root進行通信。
EP_MEM:該模塊的作用很簡單,就是一個存儲結構,由于Root向EP發送讀寫請求,例如讀,從哪里讀數據呢?就在這個模塊里呀,寫到哪里去呢?也是從這個模塊里呀。
PIO_TO_CTRL:這個模塊的作用呢?是管理cfg_turnoff_ok這個信號的,具體什么用?需要斟酌!
例程手冊程序概括
PIO設計是一個簡單的只針對目標的應用程序,它與PCIe核心事務(AXI4-Stream)的端點接口相連接,并被提供作為構建自己設計的起點。
為了直觀地理解Root Complex與Endpoint之間的區別,我們以下面的PCIe系統結構圖為例,來說明數據的傳輸情況:
上圖中多了一個PCIe Switch結構,不過沒關系,我們可以把它當成中間的過渡結構,它不影響我們Endpoint端以及Root complex端的數據處理。
圖5-3說明了PCI Express系統結構組件,由一個Root Complex、一個PCI Express交換設備和一個PCIe的Endpoint組成。PIO操作將數據從Root Complex(CPU寄存器)向下游移動到Endpoint,和/或從Endpoint向上游移動到Root Complex(CPU寄存器)。在這兩種情況下,移動數據的PCI Express協議請求都是由主機CPU發起的。
當CPU向MMIO地址命令發出存儲寄存器時,數據將向下游移動。Root Complex通常會生成一個具有適當MMIO地址的存儲器寫TLP包和字節使能。當Endpoint接收到存儲器寫TLP并更新相應的本地寄存器時,事務終止。
當CPU通過MMIO地址命令發出加載寄存器時,數據將向上游移動。Root Complex通常會生成具有適當MMIO位置地址的存儲器讀TLP包和字節使能。Endpoint在收到“內存讀取” TLP后會生成“數據TLP完成包”。將完成操作引導到Root Complex,并將有效負載加載到目標寄存器中,從而完成事務。
此兩端較為生澀,放入英文原文:
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.
例程用戶邏輯包括如下文件:
應用程序內部數據寬度,即AXI-Stream數據總線寬度根據鏈路通道數不同而不同,其關系為:
則在程序里也有體現,例如我使用的是X1模式,因此:
該例程的所有模塊組件:
則從文件結構也能看出:
應用程序,也即用戶邏輯的接口關系為:
這里是以X1為例。
應用程序中的接收模塊:
接收來自于PCIe IP核的數據,該模塊與PCIe IP模塊之間的接口為AXI-Stream,后面就不在贅述,對來自Root Complex端的讀寫請求包(TLP)進行接收并解析。
假如接收到了Root端的讀請求TLP,則輸出信號如下:
這都是對接收的數據包進行解析出來的結果,我們都知道PCIe是以包的形式來發送數據或者接收數據。TLP包的結構可見PCIe的事務處包(TLP)的組成,則在數據手冊PG054上也是詳細描述的。對這個包的輸出發送給TX模塊,把讀出來的數據一同組成一個完成包,發送給PCIe IP核進而發送給Root Complex,這個過程是一個響應,對讀請求的一個響應,這需要另一個模塊,也即TX模塊進行配合。下面會講到。
如果EP接收到的包是寫請求包,則EP_RX會生成另外一些信號:
輸出給存儲器訪問模塊,對存儲器模塊進行寫數據。
發送模塊的接口示意圖:
右端為輸出的接口,為AXI-stream接口,與PCIe IP核連接,送出IP核需要的完成包。
其輸入與RX的輸入對應:
無論是讀還是寫,總得有個存儲器寫進入或者讀出來才行,這就是這個模塊:
其輸入輸出關系一目了然,不在話下。
按照數據手冊得說法就是:
這個模塊就是處理來自于存儲器以及IO寫得TLP包得數據,將其寫入存儲器,或者呢?用來響應存儲器或者IO讀TLP包,從存儲器中讀出數據;
對于寫請求包,其接口如下:
對與讀,其接口如下:
下面講下對于讀請求事務包及其響應完成包的時序關系:
如圖所示,先是接收到一個讀請求事務包,但第一個TLP包完成接收的時候,立刻令ready無效,并響應一個完成包。等完成包響應完成之后,拉高信號compl_done,表明響應完成,之后再接收下一個事務包。
下面是寫事物請求TLP的時序關系:
首先接收一個寫請求事務包,然后寫入存儲器,寫入的過程中,拉高wr_busy,表明正在寫。寫入完成之后,令wr_busy無效,表明寫入完成。之后再接收另一個寫事務包。
這個例程的用戶程序消耗的資源為:
這表明使用了4個BRAM,就是用來寫入以及讀出來自Root請求的數據的存儲器。
例程仿真分析
PIO_RX_ENGINE.v 分析:
首先,定義了一個變量in_packet_q,高有效,用來表示接收一個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表示包的開始,sop有效的條件自然是in_packet_q無效且valid有效;即:
assign sop = !in_packet_q && m_axis_rx_tvalid;
包什么時候有效呢?可以看出是sop有效且ready有效,這時候有人可能就有點暈了,到底是in_packet_q決定sop呢?還是sop決定in_packet_q呢?那必然是in_packet_q決定sop呀,因為sop的含義是包的開始呀。將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
這就很明白了,其實這段程序的作用(請允許我用程序來代表硬件描述語言)就是判斷包有效的標志。valid和ready有效,這packet有效,一直持續到valid,ready,以及last都有效,last表示最后一個數據。可以從仿真圖中來觀察:
有了包的起始標志,就可以通過判斷這個信號有效,進入了包的解析狀態機;這里使用了一個狀態機來處理接收的TLP,對其進行解析,解析數據:
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];