Linux下PCI设备驱动开发详解(五)

本章及其以后的几章,我们将从用户态软件、内核态驱动、FPGA逻辑介绍一个通过PCI Express总线实现CPU和FPGA数据通信的简单框架。

这个框架就是开源界非常有名的RIFFA(reuseable integration framework for FPGA accelerators),它是一个FPGA加速器的一种可重用性集成框架,是一个第三方开源PCIe框架。

该框架要求具备一个支持PCIe的工作站和一个带有PCIe连接器的FPGA板卡。RIFFA支持windows、linux,altera和xilinx,可以通过c/c++、python、matlab、java驱动来实现数据的发送和接收。驱动程序可以在linux或windows上运行,每一个系统最多支持5个FPGA device。

在用户端有独立的发送和接收端口,用户只需要编写几行简单代码即可实现和FPGA IP内核通信。

riffa使用直接存储器访问(DMA)传输和中断信号传输数据。这实现了PCIe链路上的高带宽,运行速率可以达到PCIe链路饱和点。

开源地址:https://github.com/KastnerRG/riffa

一、riffa体系结构

在硬件方面,简化了接口,以便通过FIFO简便的将数据取出或存入。数据的传输由riffa的rx和tx DMA engine模块用分散聚合方法来实现。rx engine模块收集上位机来的有效数据,收集完成发给channel模块,tx engine收集channel模块传送来的数据,打包发给PCI express端点。

在软件方面,PC接收FPGA板卡数据是用户应用程序调用库函数fpga_recv,然后由FPGA端启动。用户应用程序线程进入内核驱动程序,然后开始接收上游FPGA的读请求,将数据分包发送,如果没收到请求,将会等待它达到。

启动发送函数后,服务器将建立一个散列收集元素的列表,将数据存储地址和长度等信息放入其中,将其写入共享缓冲区。用户应用程序将缓冲区地址和数据长度等信息发送给FPGA。FPGA读取散射收集数据,然后发出相应地址的数据写入请求,如果散列收集元素列表的地址有多个,FPGA将通过中断发出多次请求。

TX搬移的数据全部写入缓存区后,驱动程序读取FPGA写入的字节数,确认是否与发送数据长度一致。这样就完成了传输。其过程如下图所示:

PC机发送数据到FPGA板卡过程与PC机接收FPGA板卡数据过程相似,下图说明了该流程。 刚开始也是用户应用程序调用库函数fpga_send,传输线程进入内核驱动程序,然后FPGA启动传输。

启动fpga_send,服务器将申请一些空间,将要发送的数据写入其中,然后建立一个分散收集列表,将存储数据的地址和长度放入其中,并将分散收集列表的地址和要发生的数据长度等信息发给FPGA。FPGA 收到列表地址后,读取该列表的信息,然后发出相应地址和长度的读请求,然后将数据存储,最后一起发给FPGA板卡。

驱动程序首先调用pci_present()检查PCI总线是否被linux内核支持,如果系统支持PCI总线结构,这个函数的返回值为0,如果驱动程序在调用这个函数时得到一个非0的返回值,那么驱动程序就必须得中止自己的任务。调用pci_register_driver()函数来注册PCI设备的驱动程序,此时需要提供一个“demo_pci_driver”结构,在该结构中给出的probe探测例程负责完成对硬件的检测工作。

下面将从user logic -> PCIe IP -> 驱动层 -> library-> 用户态C++/C按照实例一一进行分析,包括理论基础、实际操作、源代码分析。

首先我们先从FPGA xilinx integrated block for PCI express分析,因为这个有承上启下的作用。

二、PCIe hard IP分析

1. PCIE IP 核配置

AXI总线时钟选择62.5M,AXI总线接口位宽设置为64bit。

在IDs界面是PCIe设备的相关信息,主机在上电时BIOS系统中识别到的PCIe设备,就是通过这些ID号来进行识别的。

在本实验中,关于ID的设定全部保持为默认值即可,若用户对ID进行了更改,可能导致计算机在启动时不能正确识别设备从而导致蓝屏死机。

Vendor ID是厂商ID,本实验中的厂商ID代值的就是Xilinx;

Device ID代表了PCIe设备,其中7指的是Xilinx 7 系列FPGA,02指的是使用的PCIe 2.0的协议,1指的是含有一个PCIe的传输lane;

Base class Menu指的是PCIe设备的种类,常见的有声卡、显卡、网卡等,各种不同种类的设备都有其对应的驱动,若驱动与其PCIe的种类不对应,就会导致系统的内存访问错误,从而导致蓝屏。

在PCIe的bars配置界面对PCIe的bar进行设置,BAR空间对应的就是在内存中开辟一段空间用于存放PCIe设备的信息。只使用到一个bar0一个bar且将其内存空间的大小设置为1k。

IP核的负载选择最大负载长度为512字节,勾选对数据进行缓冲。

在中断配置界面,取消勾选传统类型的中断,只选择消息类型的中断。

在share logic界面取消勾选包含其他逻辑,这样在PCIe的IP核中就包含了全部功能。

在IP核接口参数配置界面,只选择其中用于配置核控制的参数,这是由于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数据common接口信号、tx/rx数据面信号、FC流控信号、configuration(CFG)interface、EP中断信号、系统时钟/复位信号。

详细使用参考xilinx PCIe spec官方文档。

PCIe IP位于整个设计架构的这个位置:

2. tx_engine和rx_engine

下面我们分析一下tx_engine和rx_engine这两个核心模块,这两个模块负责转换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配置通道个数,C_PCI_DATA_WIDTH配置PCIe data的位宽,C_LOG_NUM_TAGS配置PCIe tag的个数。

module riffa_wrapper_ac701负责将xilinx PCIe IP的TX、RX、Configuration、flow control、中断信号转换为riffa的输入输出信号。

阅读riffa_wrapper_ac701.v源代码发现这个模块分为两个模块:

· translation_xilinx · engine_layer · riffa

transtion_xilinx:负责提供统一的PCIe接口信息,比如altera、xilinx;

engine_layer:负责封装了tx_engine和rx_engine。其中tx engine负责上传DMA通道(写通道),即Interface: TX Classic;rx engine负责下发DMA通道(读通道),即Interface: RX Classic;

riffa:负责将tx/rx engine的信号转换为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

经过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

这个模块的用于执行RIFFA TX 和 RX 接口。在RX接口上接收数据并保存最后接收的值。在TX接口上发送回相同数量的数据。返回的数据从接收到的最后一个值开始,重置并递增,直到等于TX接口上发回的(4字节)字数的值结束。

三、总结

这篇文章通过经典开源项目RIIFA的FPGA部分,结合源代码详细分析了框架部分的Xilinx PCIe hard IP、TX/RX engine和RIFFA模块,最后结合了user logic部分的chnl_tester,介绍了如何使用CHNL_TX_*和CHNL_RX_*接口。

四、未完待续

Linux下PCI设备驱动开发详解(六),将结合经典开源项目RIIFA,详细介绍内核态驱动的设计、开发、安装等。

五、参考资料

https://blog.csdn.net/qq_41332806/article/details/105864632

文章来源

评论可见,请评论后查看内容,谢谢!!!评论后请刷新页面。