Linux下PCI設備驅(qū)動開發(fā)詳解(五)
本章及其以后的幾章,我們將從用戶態(tài)軟件、內(nèi)核態(tài)驅(qū)動、FPGA邏輯介紹一個通過PCI Express總線實現(xiàn)CPU和FPGA數(shù)據(jù)通信的簡單框架。
這個框架就是開源界非常有名的RIFFA(reuseable integration framework for FPGA accelerators),它是一個FPGA加速器的一種可重用性集成框架,是一個第三方開源PCIe框架。
該框架要求具備一個支持PCIe的工作站和一個帶有PCIe連接器的FPGA板卡。RIFFA支持windows、linux,altera和xilinx,可以通過c/c++、python、matlab、java驅(qū)動來實現(xiàn)數(shù)據(jù)的發(fā)送和接收。驅(qū)動程序可以在linux或windows上運行,每一個系統(tǒng)最多支持5個FPGA device。
在用戶端有獨立的發(fā)送和接收端口,用戶只需要編寫幾行簡單代碼即可實現(xiàn)和FPGA IP內(nèi)核通信。
riffa使用直接存儲器訪問(DMA)傳輸和中斷信號傳輸數(shù)據(jù)。這實現(xiàn)了PCIe鏈路上的高帶寬,運行速率可以達到PCIe鏈路飽和點。
開源地址:https://github.com/KastnerRG/riffa
一、riffa體系結構

在硬件方面,簡化了接口,以便通過FIFO簡便的將數(shù)據(jù)取出或存入。數(shù)據(jù)的傳輸由riffa的rx和tx DMA engine模塊用分散聚合方法來實現(xiàn)。rx engine模塊收集上位機來的有效數(shù)據(jù),收集完成發(fā)給channel模塊,tx engine收集channel模塊傳送來的數(shù)據(jù),打包發(fā)給PCI express端點。
在軟件方面,PC接收FPGA板卡數(shù)據(jù)是用戶應用程序調(diào)用庫函數(shù)fpga_recv,然后由FPGA端啟動。用戶應用程序線程進入內(nèi)核驅(qū)動程序,然后開始接收上游FPGA的讀請求,將數(shù)據(jù)分包發(fā)送,如果沒收到請求,將會等待它達到。
啟動發(fā)送函數(shù)后,服務器將建立一個散列收集元素的列表,將數(shù)據(jù)存儲地址和長度等信息放入其中,將其寫入共享緩沖區(qū)。用戶應用程序?qū)⒕彌_區(qū)地址和數(shù)據(jù)長度等信息發(fā)送給FPGA。FPGA讀取散射收集數(shù)據(jù),然后發(fā)出相應地址的數(shù)據(jù)寫入請求,如果散列收集元素列表的地址有多個,F(xiàn)PGA將通過中斷發(fā)出多次請求。
TX搬移的數(shù)據(jù)全部寫入緩存區(qū)后,驅(qū)動程序讀取FPGA寫入的字節(jié)數(shù),確認是否與發(fā)送數(shù)據(jù)長度一致。這樣就完成了傳輸。其過程如下圖所示:

PC機發(fā)送數(shù)據(jù)到FPGA板卡過程與PC機接收FPGA板卡數(shù)據(jù)過程相似,下圖說明了該流程。 剛開始也是用戶應用程序調(diào)用庫函數(shù)fpga_send,傳輸線程進入內(nèi)核驅(qū)動程序,然后FPGA啟動傳輸。
啟動fpga_send,服務器將申請一些空間,將要發(fā)送的數(shù)據(jù)寫入其中,然后建立一個分散收集列表,將存儲數(shù)據(jù)的地址和長度放入其中,并將分散收集列表的地址和要發(fā)生的數(shù)據(jù)長度等信息發(fā)給FPGA。FPGA 收到列表地址后,讀取該列表的信息,然后發(fā)出相應地址和長度的讀請求,然后將數(shù)據(jù)存儲,最后一起發(fā)給FPGA板卡。

驅(qū)動程序首先調(diào)用pci_present()檢查PCI總線是否被linux內(nèi)核支持,如果系統(tǒng)支持PCI總線結構,這個函數(shù)的返回值為0,如果驅(qū)動程序在調(diào)用這個函數(shù)時得到一個非0的返回值,那么驅(qū)動程序就必須得中止自己的任務。調(diào)用pci_register_driver()函數(shù)來注冊PCI設備的驅(qū)動程序,此時需要提供一個“demo_pci_driver”結構,在該結構中給出的probe探測例程負責完成對硬件的檢測工作。
下面將從user logic -> PCIe IP -> 驅(qū)動層 -> library-> 用戶態(tài)C++/C按照實例一一進行分析,包括理論基礎、實際操作、源代碼分析。
首先我們先從FPGA xilinx integrated block for PCI express分析,因為這個有承上啟下的作用。
二、PCIe hard IP分析
1. PCIE IP 核配置
AXI總線時鐘選擇62.5M,AXI總線接口位寬設置為64bit。

在IDs界面是PCIe設備的相關信息,主機在上電時BIOS系統(tǒng)中識別到的PCIe設備,就是通過這些ID號來進行識別的。
在本實驗中,關于ID的設定全部保持為默認值即可,若用戶對ID進行了更改,可能導致計算機在啟動時不能正確識別設備從而導致藍屏死機。
Vendor ID是廠商ID,本實驗中的廠商ID代值的就是Xilinx;
Device ID代表了PCIe設備,其中7指的是Xilinx 7 系列FPGA,02指的是使用的PCIe 2.0的協(xié)議,1指的是含有一個PCIe的傳輸lane;
Base class Menu指的是PCIe設備的種類,常見的有聲卡、顯卡、網(wǎng)卡等,各種不同種類的設備都有其對應的驅(qū)動,若驅(qū)動與其PCIe的種類不對應,就會導致系統(tǒng)的內(nèi)存訪問錯誤,從而導致藍屏。

在PCIe的bars配置界面對PCIe的bar進行設置,BAR空間對應的就是在內(nèi)存中開辟一段空間用于存放PCIe設備的信息。只使用到一個bar0一個bar且將其內(nèi)存空間的大小設置為1k。

IP核的負載選擇最大負載長度為512字節(jié),勾選對數(shù)據(jù)進行緩沖。

在中斷配置界面,取消勾選傳統(tǒng)類型的中斷,只選擇消息類型的中斷。

在share logic界面取消勾選包含其他邏輯,這樣在PCIe的IP核中就包含了全部功能。

在IP核接口參數(shù)配置界面,只選擇其中用于配置核控制的參數(shù),這是由于riffa框架的特性所提供的。

下面代碼是實際的top level,
PCIeGen1x4If64 PCIeGen1x4If64_i
(//---------------------------------------------------------------------
// PCI Express (pci_exp) Interface
//---------------------------------------------------------------------
// Tx
.pci_exp_txn ( PCI_EXP_TXN ),
.pci_exp_txp ( PCI_EXP_TXP ),
// Rx
.pci_exp_rxn ( PCI_EXP_RXN ),
.pci_exp_rxp ( PCI_EXP_RXP ),
//---------------------------------------------------------------------
// AXI-S Interface
//---------------------------------------------------------------------
// Common
.user_clk_out ( user_clk ),
.user_reset_out ( user_reset ),
.user_lnk_up ( user_lnk_up ),
.user_app_rdy ( user_app_rdy ),
// TX
.s_axis_tx_tready ( s_axis_tx_tready ),
.s_axis_tx_tdata ( s_axis_tx_tdata ),
.s_axis_tx_tkeep ( s_axis_tx_tkeep ),
.s_axis_tx_tuser ( s_axis_tx_tuser ),
.s_axis_tx_tlast ( s_axis_tx_tlast ),
.s_axis_tx_tvalid ( s_axis_tx_tvalid ),
// Rx
.m_axis_rx_tdata ( m_axis_rx_tdata ),
.m_axis_rx_tkeep ( m_axis_rx_tkeep ),
.m_axis_rx_tlast ( m_axis_rx_tlast ),
.m_axis_rx_tvalid ( m_axis_rx_tvalid ),
.m_axis_rx_tready ( m_axis_rx_tready ),
.m_axis_rx_tuser ( m_axis_rx_tuser ),
.tx_cfg_gnt ( tx_cfg_gnt ),
.rx_np_ok ( rx_np_ok ),
.rx_np_req ( rx_np_req ),
.cfg_trn_pending ( cfg_trn_pending ),
.cfg_pm_halt_aspm_l0s ( cfg_pm_halt_aspm_l0s ),
.cfg_pm_halt_aspm_l1 ( cfg_pm_halt_aspm_l1 ),
.cfg_pm_force_state_en ( cfg_pm_force_state_en ),
.cfg_pm_force_state ( cfg_pm_force_state ),
.cfg_dsn ( cfg_dsn ),
.cfg_turnoff_ok ( cfg_turnoff_ok ),
.cfg_pm_wake ( cfg_pm_wake ),
.cfg_pm_send_pme_to ( 1'b0 ),
.cfg_ds_bus_number ( 8'b0 ),
.cfg_ds_device_number ( 5'b0 ),
.cfg_ds_function_number ( 3'b0 ),
//---------------------------------------------------------------------
// Flow Control Interface
//---------------------------------------------------------------------
.fc_cpld ( fc_cpld ),
.fc_cplh ( fc_cplh ),
.fc_npd ( fc_npd ),
.fc_nph ( fc_nph ),
.fc_pd ( fc_pd ),
.fc_ph ( fc_ph ),
.fc_sel ( fc_sel ),
//---------------------------------------------------------------------
// Configuration (CFG) Interface
//---------------------------------------------------------------------
.cfg_device_number ( cfg_device_number ),
.cfg_dcommand2 ( cfg_dcommand2 ),
.cfg_pmcsr_pme_status ( cfg_pmcsr_pme_status ),
.cfg_status ( cfg_status ),
.cfg_to_turnoff ( cfg_to_turnoff ),
.cfg_received_func_lvl_rst ( cfg_received_func_lvl_rst ),
.cfg_dcommand ( cfg_dcommand ),
.cfg_bus_number ( cfg_bus_number ),
.cfg_function_number ( cfg_function_number ),
.cfg_command ( cfg_command ),
.cfg_dstatus ( cfg_dstatus ),
.cfg_lstatus ( cfg_lstatus ),
.cfg_pcie_link_state ( cfg_pcie_link_state ),
.cfg_lcommand ( cfg_lcommand ),
.cfg_pmcsr_pme_en ( cfg_pmcsr_pme_en ),
.cfg_pmcsr_powerstate ( cfg_pmcsr_powerstate ),
//------------------------------------------------//
// EP Only //
//------------------------------------------------//
.cfg_interrupt ( cfg_interrupt ),
.cfg_interrupt_rdy ( cfg_interrupt_rdy ),
.cfg_interrupt_assert ( cfg_interrupt_assert ),
.cfg_interrupt_di ( cfg_interrupt_di ),
.cfg_interrupt_do ( cfg_interrupt_do ),
.cfg_interrupt_mmenable ( cfg_interrupt_mmenable ),
.cfg_interrupt_msienable ( cfg_interrupt_msienable ),
.cfg_interrupt_msixenable ( cfg_interrupt_msixenable ),
.cfg_interrupt_msixfm ( cfg_interrupt_msixfm ),
.cfg_interrupt_stat ( cfg_interrupt_stat ),
.cfg_pciecap_interrupt_msgnum ( cfg_pciecap_interrupt_msgnum ),
//---------------------------------------------------------------------
// System (SYS) Interface
//---------------------------------------------------------------------
.sys_clk ( pcie_refclk ),
.sys_rst_n ( pcie_reset_n )
);
從頂層代碼接口可以看出來,TX/RX差分信號、AXIS數(shù)據(jù)common接口信號、tx/rx數(shù)據(jù)面信號、FC流控信號、configuration(CFG)interface、EP中斷信號、系統(tǒng)時鐘/復位信號。
詳細使用參考xilinx PCIe spec官方文檔。
PCIe IP位于整個設計架構的這個位置:

2. tx_engine和rx_engine
下面我們分析一下tx_engine和rx_engine這兩個核心模塊,這兩個模塊負責轉(zhuǎn)換axis data和tlp data。
我們先看一下top level的源代碼:
riffa_wrapper_ac701
#(/*AUTOINSTPARAM*/
// Parameters
.C_LOG_NUM_TAGS (C_LOG_NUM_TAGS),
.C_NUM_CHNL (C_NUM_CHNL),
.C_PCI_DATA_WIDTH (C_PCI_DATA_WIDTH),
.C_MAX_PAYLOAD_BYTES (C_MAX_PAYLOAD_BYTES))
riffa
(
// Outputs
.CFG_INTERRUPT (cfg_interrupt),
.M_AXIS_RX_TREADY (m_axis_rx_tready),
.S_AXIS_TX_TDATA (s_axis_tx_tdata[C_PCI_DATA_WIDTH-1:0]),
.S_AXIS_TX_TKEEP (s_axis_tx_tkeep[(C_PCI_DATA_WIDTH/8)-1:0]),
.S_AXIS_TX_TLAST (s_axis_tx_tlast),
.S_AXIS_TX_TVALID (s_axis_tx_tvalid),
.S_AXIS_TX_TUSER (s_axis_tx_tuser[`SIG_XIL_TX_TUSER_W-1:0]),
.FC_SEL (fc_sel[`SIG_FC_SEL_W-1:0]),
.RST_OUT (rst_out),
.CHNL_RX (chnl_rx[C_NUM_CHNL-1:0]),
.CHNL_RX_LAST (chnl_rx_last[C_NUM_CHNL-1:0]),
.CHNL_RX_LEN (chnl_rx_len[(C_NUM_CHNL*`SIG_CHNL_LENGTH_W)-1:0]),
.CHNL_RX_OFF (chnl_rx_off[(C_NUM_CHNL*`SIG_CHNL_OFFSET_W)-1:0]),
.CHNL_RX_DATA (chnl_rx_data[(C_NUM_CHNL*C_PCI_DATA_WIDTH)-1:0]),
.CHNL_RX_DATA_VALID (chnl_rx_data_valid[C_NUM_CHNL-1:0]),
.CHNL_TX_ACK (chnl_tx_ack[C_NUM_CHNL-1:0]),
.CHNL_TX_DATA_REN (chnl_tx_data_ren[C_NUM_CHNL-1:0]),
// Inputs
.M_AXIS_RX_TDATA (m_axis_rx_tdata[C_PCI_DATA_WIDTH-1:0]),
.M_AXIS_RX_TKEEP (m_axis_rx_tkeep[(C_PCI_DATA_WIDTH/8)-1:0]),
.M_AXIS_RX_TLAST (m_axis_rx_tlast),
.M_AXIS_RX_TVALID (m_axis_rx_tvalid),
.M_AXIS_RX_TUSER (m_axis_rx_tuser[`SIG_XIL_RX_TUSER_W-1:0]),
.S_AXIS_TX_TREADY (s_axis_tx_tready),
.CFG_BUS_NUMBER (cfg_bus_number[`SIG_BUSID_W-1:0]),
.CFG_DEVICE_NUMBER (cfg_device_number[`SIG_DEVID_W-1:0]),
.CFG_FUNCTION_NUMBER (cfg_function_number[`SIG_FNID_W-1:0]),
.CFG_COMMAND (cfg_command[`SIG_CFGREG_W-1:0]),
.CFG_DCOMMAND (cfg_dcommand[`SIG_CFGREG_W-1:0]),
.CFG_LSTATUS (cfg_lstatus[`SIG_CFGREG_W-1:0]),
.CFG_LCOMMAND (cfg_lcommand[`SIG_CFGREG_W-1:0]),
.FC_CPLD (fc_cpld[`SIG_FC_CPLD_W-1:0]),
.FC_CPLH (fc_cplh[`SIG_FC_CPLH_W-1:0]),
.CFG_INTERRUPT_MSIEN (cfg_interrupt_msienable),// TODO: Rename
.CFG_INTERRUPT_RDY (cfg_interrupt_rdy),
.USER_CLK (user_clk),
.USER_RESET (user_reset),
.CHNL_RX_CLK (chnl_rx_clk[C_NUM_CHNL-1:0]),
.CHNL_RX_ACK (chnl_rx_ack[C_NUM_CHNL-1:0]),
.CHNL_RX_DATA_REN (chnl_rx_data_ren[C_NUM_CHNL-1:0]),
.CHNL_TX_CLK (chnl_tx_clk[C_NUM_CHNL-1:0]),
.CHNL_TX (chnl_tx[C_NUM_CHNL-1:0]),
.CHNL_TX_LAST (chnl_tx_last[C_NUM_CHNL-1:0]),
.CHNL_TX_LEN (chnl_tx_len[(C_NUM_CHNL*`SIG_CHNL_LENGTH_W)-1:0]),
.CHNL_TX_OFF (chnl_tx_off[(C_NUM_CHNL*`SIG_CHNL_OFFSET_W)-1:0]),
.CHNL_TX_DATA (chnl_tx_data[(C_NUM_CHNL*C_PCI_DATA_WIDTH)-1:0]),
.CHNL_TX_DATA_VALID (chnl_tx_data_valid[C_NUM_CHNL-1:0]),
.RX_NP_OK (rx_np_ok),
.TX_CFG_GNT (tx_cfg_gnt),
.RX_NP_REQ (rx_np_req)
/*AUTOINST*/);
這個模塊可以通過C_NUM_CHNL配置通道個數(shù),C_PCI_DATA_WIDTH配置PCIe data的位寬,C_LOG_NUM_TAGS配置PCIe tag的個數(shù)。
module riffa_wrapper_ac701負責將xilinx PCIe IP的TX、RX、Configuration、flow control、中斷信號轉(zhuǎn)換為riffa的輸入輸出信號。
閱讀riffa_wrapper_ac701.v源代碼發(fā)現(xiàn)這個模塊分為兩個模塊:
· translation_xilinx
· engine_layer
· riffa
transtion_xilinx:負責提供統(tǒng)一的PCIe接口信息,比如altera、xilinx;
engine_layer:負責封裝了tx_engine和rx_engine。其中tx engine負責上傳DMA通道(寫通道),即Interface: TX Classic;rx engine負責下發(fā)DMA通道(讀通道),即Interface: RX Classic;
riffa:負責將tx/rx engine的信號轉(zhuǎn)換為user logic的通道信號,方便我們使用,接口代碼如下:
input [C_NUM_CHNL-1:0] CHNL_RX_CLK,
output [C_NUM_CHNL-1:0] CHNL_RX,
input [C_NUM_CHNL-1:0] CHNL_RX_ACK,
output [C_NUM_CHNL-1:0] CHNL_RX_LAST,
output [(C_NUM_CHNL*32)-1:0] CHNL_RX_LEN,
output [(C_NUM_CHNL*31)-1:0] CHNL_RX_OFF,
output [(C_NUM_CHNL*C_PCI_DATA_WIDTH)-1:0] CHNL_RX_DATA,
output [C_NUM_CHNL-1:0] CHNL_RX_DATA_VALID,
input [C_NUM_CHNL-1:0] CHNL_RX_DATA_REN,
input [C_NUM_CHNL-1:0] CHNL_TX_CLK,
input [C_NUM_CHNL-1:0] CHNL_TX,
output [C_NUM_CHNL-1:0] CHNL_TX_ACK,
input [C_NUM_CHNL-1:0] CHNL_TX_LAST,
input [(C_NUM_CHNL*32)-1:0] CHNL_TX_LEN,
input [(C_NUM_CHNL*31)-1:0] CHNL_TX_OFF,
input [(C_NUM_CHNL*C_PCI_DATA_WIDTH)-1:0] CHNL_TX_DATA,
input [C_NUM_CHNL-1:0] CHNL_TX_DATA_VALID,
output [C_NUM_CHNL-1:0] CHNL_TX_DATA_REN
對應整個實際框架的這一部分,如下圖所示:

3. user logic
經(jīng)過tx engine和rx engine模塊,輸出CHNL_RX_和CHNL_TX_信號,下面我們看一下user如何使用的,源代碼如下:
generate
for (chnl = 0; chnl < C_NUM_CHNL; chnl = chnl + 1) begin : test_channels
chnl_tester
#(
.C_PCI_DATA_WIDTH(C_PCI_DATA_WIDTH)
)
module1
(.CLK(user_clk),
.RST(rst_out), // riffa_reset includes riffa_endpoint resets
// Rx interface
.CHNL_RX_CLK(chnl_rx_clk[chnl]),
.CHNL_RX(chnl_rx[chnl]),
.CHNL_RX_ACK(chnl_rx_ack[chnl]),
.CHNL_RX_LAST(chnl_rx_last[chnl]),
.CHNL_RX_LEN(chnl_rx_len[32*chnl +:32]),
.CHNL_RX_OFF(chnl_rx_off[31*chnl +:31]),
.CHNL_RX_DATA(chnl_rx_data[C_PCI_DATA_WIDTH*chnl +:C_PCI_DATA_WIDTH]),
.CHNL_RX_DATA_VALID(chnl_rx_data_valid[chnl]),
.CHNL_RX_DATA_REN(chnl_rx_data_ren[chnl]),
// Tx interface
.CHNL_TX_CLK(chnl_tx_clk[chnl]),
.CHNL_TX(chnl_tx[chnl]),
.CHNL_TX_ACK(chnl_tx_ack[chnl]),
.CHNL_TX_LAST(chnl_tx_last[chnl]),
.CHNL_TX_LEN(chnl_tx_len[32*chnl +:32]),
.CHNL_TX_OFF(chnl_tx_off[31*chnl +:31]),
.CHNL_TX_DATA(chnl_tx_data[C_PCI_DATA_WIDTH*chnl +:C_PCI_DATA_WIDTH]),
.CHNL_TX_DATA_VALID(chnl_tx_data_valid[chnl]),
.CHNL_TX_DATA_REN(chnl_tx_data_ren[chnl])
);
end
endgenerate
這個模塊的用于執(zhí)行RIFFA TX 和 RX 接口。在RX接口上接收數(shù)據(jù)并保存最后接收的值。在TX接口上發(fā)送回相同數(shù)量的數(shù)據(jù)。返回的數(shù)據(jù)從接收到的最后一個值開始,重置并遞增,直到等于TX接口上發(fā)回的(4字節(jié))字數(shù)的值結束。
三、總結
這篇文章通過經(jīng)典開源項目RIIFA的FPGA部分,結合源代碼詳細分析了框架部分的Xilinx PCIe hard IP、TX/RX engine和RIFFA模塊,最后結合了user logic部分的chnl_tester,介紹了如何使用CHNL_TX_和CHNL_RX_接口。

四、未完待續(xù)
Linux下PCI設備驅(qū)動開發(fā)詳解(六),將結合經(jīng)典開源項目RIIFA,詳細介紹內(nèi)核態(tài)驅(qū)動的設計、開發(fā)、安裝等。