MIZ7035官方提供了两种pcie的demo,一个就是普通的PIO测试,一个是BMD测试。我只是试验了PIO功能,可以对板卡直接进行IO寄存器读写。而另外一个BMD功能使用了DMA来加速数据读写速度。
我也是第一次接触PCIe,BMD确实也可以完成应用的需求,搞起来应该没有问题。准备用这个开始做实验呢,问了一下学校之前的第二导师,他直接给我说了5个字母:R-I-F-F-A。说让我去查一下,底层FPGA和上层软件都已经是一个完整的架构了,可以直接使用。既然老师说了,一定是这个方法可能更为方便吧,而且他说一直在用这个架构。
先百度了一下CPU与FPGA的通信方式,找到一个知乎问题:
CPU与FPGA进行数据通信有哪些方法?
有回答者直接贴了两个可用的框架:
XILLYBUS:
An FPGA IP core for easy DMA over PCIe with Windows and Linux
RIFFA2:
RIFFA: Home | RIFFA: A Reusable Integration Framework For FPGA Accelerators
还有一个北大无线可重构体系结构课题小组的框架:
EPEE:
EPEE – An Efficient and Flexible Host-FPGA PCIe Communication Library
XILLYBUS最早接触是在参加OpenHW2014比赛时候看到过的,当时真是不知道它是干什么的。当时在用zybo和zedboard做比赛项目,XILLYBUS提供了一个在这两个开发板上可以运行的Ubuntu操作系统,叫做Xillinux,很强大。虽然说这个Ubuntu的界面在没有GPU加持的Zynq器件上运行起来稍微有些卡顿,但是它毕竟是一个完整的桌面操作系统,我们很多参加比赛的人都是在这个上面搞起来的,还有一部分原因就是很多人不会PetaLinux,而且自己去添加桌面的难度更是高。查了一下,已经推出了Xillinux 2.0 beta版本,有空了去试一下。
在Xillinux中,官方提供了一个AXI接口的Xillybus,当时还没想明白是干啥用的。现在终于明白了。。原来是为了兼容它的PCIe接口驱动。Xillybus还是主打他的PCIe接口驱动的。
下图是Xillybus的结构
Xillybus是用到了Altera或Xilinux的PCIe接口core,接入它自己的Xillybus IP core,最后通过FIFO接口将TX、RX通道映射出来。结构很清晰,我们需要的只是在FIFO接口后接入我们自己的IP core,按照自己的协议来进行通信即可。
上层代码直接通过打开名为xillybus的设备,然后进行文件读写即可实现对底层FIFO收发数据的控制
这样看来PCIe接口的Xillybus是独立于PCIe接口FPGA的另外一个嵌入式Linux系统或是桌面的Windows、Linux操作系统。
而AXI接口的Xillybus对应的HOST就是本身Zynq器件上的ARM处理器。
RIFFA是第一次接触,他的结构如下图
FPGA部分首先是Xilinx的PCIe core,然后使用TX、RX引擎,在经过通道仲裁映射到最多12个TX、RX通道上。结构与PIO、BMD的结构类似,增加了通道扩展的功能,这样就不用我们自己去写仲裁代码了。
PC部分就是基本的驱动、应用结构,PCIe的访问空间映射到了PC的内存上,RIFFA的驱动负责内存以及PCIe接口的管理,RIFFA库可以被用户空间程序调用来实现对底层RIFFA驱动的调用,并最终实现PCIe的数据通信。
RIFFA的读写函数名为fpga_recv和fpga_send,函数接口也类似于一个文件读写命令,只不过可以在参数中指定PCIe的通道编号、数据的大小等信息。
fpga_send的流程
fpga_recv的流程
里面都有有一个叫做build scatter gather list的步骤,我的理解是在PC上malloc申请内存时候容易申请到非连续的物理内存,可能被分为好几块,所以要将这些空间进行分离并生成这个list,然后将list信息发送给PCIe的endpoint,EP再根据这个list来依次使用DMA直接操作PC上的内存空间。
看到上面两个图,我真是庆幸自己没有去尝试自己搞,根本没那么多时间去搞啊,而且感觉也没那能力···囧。
以上说到的三个框架,肯定是都可以使用的,但是听老师的建议,直接去搞RIFFA吧,也没有为什么。
去网上下载官方提供的RIFFA源文件,需要填写一些信息即可拿到下载地址。http://riffa.ucsd.edu/download
我直接下载了最新release的版本RIFFA 2.2.2
解压后的文件结构:
./source/fpga/xilinx目录下可以看到xilinx开发板的demo,MIZ7035的zynq芯片最为接近zc706开发板,我们就用这个demo来进行参考。
使用vivado直接打开工程./source/fpga/xilinx/zc706/ZC706_Gen2x4If128/prj/ZC706_Gen2x4If128.xpr,点击自动升级到Vivado 2017.4的版本。
软件更新依赖关系后,发现工程中缺失了一些文件,这些文件是riffa相关的hdl代码,我们在工程中直接添加目录./source/fpga/riffa_hdl下的所有文件,并勾选红框位置的两个选项
点击Finish,等待文件结构的更新,可以看到已经没有错误警告了
看到多了一个altera相关的verilog文件,可以删除掉。因为我没有ZC706的开发板,就不综合了,直接去在MIZ7035上去实验。
继续将前一节测试过的HDMI、MIG工程拿来添加PCIe功能
点击IP Catalog->搜索pcie->双击7 Series Integrated Block for PCI Express->选择Customize IP,这里不能将IP加入到BD中去,因为这个IP Core是包含在另外一个hdl文件中的。
为了方便移植,同时打开ZC706 Demo和MIZ7035工程中的PCIe IP,参照demo的配置来配置miz7035的IP。贴图:
X4,5.0GT/s,100MHz Ref clock
先不去管各个配置的意义了,照着demo做就应该没问题了。
在自己的工程中新建文件MIZ7035_Gen2x4lf128.v,然后复制./source/fpga/xilinx/zc706/ZC706_Gen2x4If128/hdl/ZC706_Gen2x4If128.v内的代码并粘贴进来。将ZC706字符都改为MIZ7035,其它不用修改。
在自己的工程中新建文件riffa_wrapper_miz7035.v,然后复制./source/fpga/xilinx/zc706/riffa_wrapper_zc706.v内的代码并粘贴进来。将ZC706字符都改为MIZ7035,其它不用修改。
然后文件目录更新就会提示文件缺失,然后按照刚才demo的方法,将./source/fpga/riffa_hdl下的所有文件添加到工程,等待更新,可以删除不需要的文件。
最终目录如下
因为我之前的工程中有BD,为了例化一个MIZ7035_Gen2x4lf128模块,需要将BD和这个模块重新例化然后作为顶层文件。
新建一个MIZ7035_PCIE_RIFFA.v文件,复制之前BD生成的wrapper代码并复制进来。删除原先BD的wrapper。下面对MIZ7035_Gen2x4lf128进行例化。
module MIZ7035_PCIE_RIFFA(
//pcie
PCI_EXP_TXP,
PCI_EXP_TXN,
PCI_EXP_RXP,
PCI_EXP_RXN,
PCIE_REFCLK_P,
PCIE_REFCLK_N,
PCIE_RESET_N,
...
...
parameter C_NUM_LANES = 4;
output [(C_NUM_LANES - 1) : 0] PCI_EXP_TXP;
output [(C_NUM_LANES - 1) : 0] PCI_EXP_TXN;
input [(C_NUM_LANES - 1) : 0] PCI_EXP_RXP;
input [(C_NUM_LANES - 1) : 0] PCI_EXP_RXN;
input PCIE_REFCLK_P;
input PCIE_REFCLK_N;
input PCIE_RESET_N;
MIZ7035_Gen2x4If128 MIZ7035_Gen2x4If128_inst
(
.PCI_EXP_TXP(PCI_EXP_TXP),
.PCI_EXP_TXN(PCI_EXP_TXN),
.PCI_EXP_RXP(PCI_EXP_RXP),
.PCI_EXP_RXN(PCI_EXP_RXN),
.LED(),
.PCIE_REFCLK_P(PCIE_REFCLK_P),
.PCIE_REFCLK_N(PCIE_REFCLK_N),
.PCIE_RESET_N(PCIE_RESET_N)
);
endmodule
将新建的MIZ7035_PCIE_RIFFA.v文件作为顶层文件。
最后的目录如下:
其中有一个chnl_tester的例化,代码如下:
`timescale 1ns/1ns
module chnl_tester #(
parameter C_PCI_DATA_WIDTH = 9'd32
)
(
input CLK,
input RST,
output CHNL_RX_CLK,
input CHNL_RX,
output CHNL_RX_ACK,
input CHNL_RX_LAST,
input [31:0] CHNL_RX_LEN,
input [30:0] CHNL_RX_OFF,
input [C_PCI_DATA_WIDTH-1:0] CHNL_RX_DATA,
input CHNL_RX_DATA_VALID,
output CHNL_RX_DATA_REN,
output CHNL_TX_CLK,
output CHNL_TX,
input CHNL_TX_ACK,
output CHNL_TX_LAST,
output [31:0] CHNL_TX_LEN,
output [30:0] CHNL_TX_OFF,
output [C_PCI_DATA_WIDTH-1:0] CHNL_TX_DATA,
output CHNL_TX_DATA_VALID,
input CHNL_TX_DATA_REN
);
reg [C_PCI_DATA_WIDTH-1:0] rData={C_PCI_DATA_WIDTH{1'b0}};
reg [31:0] rLen=0;
reg [31:0] rCount=0;
reg [1:0] rState=0;
assign CHNL_RX_CLK = CLK;
assign CHNL_RX_ACK = (rState == 2'd1);
assign CHNL_RX_DATA_REN = (rState == 2'd1);
assign CHNL_TX_CLK = CLK;
assign CHNL_TX = (rState == 2'd3);
assign CHNL_TX_LAST = 1'd1;
assign CHNL_TX_LEN = rLen; // in words
assign CHNL_TX_OFF = 0;
assign CHNL_TX_DATA = rData;
assign CHNL_TX_DATA_VALID = (rState == 2'd3);
always @(posedge CLK or posedge RST) begin
if (RST) begin
rLen <= #1 0;
rCount <= #1 0;
rState <= #1 0;
rData <= #1 0;
end
else begin
case (rState)
2'd0: begin // Wait for start of RX, save length
if (CHNL_RX) begin
rLen <= #1 CHNL_RX_LEN;
rCount <= #1 0;
rState <= #1 2'd1;
end
end
2'd1: begin // Wait for last data in RX, save value
if (CHNL_RX_DATA_VALID) begin
rData <= #1 CHNL_RX_DATA;
rCount <= #1 rCount + (C_PCI_DATA_WIDTH/32);
end
if (rCount >= rLen)
rState <= #1 2'd2;
end
2'd2: begin // Prepare for TX
rCount <= #1 (C_PCI_DATA_WIDTH/32);
rState <= #1 2'd3;
end
2'd3: begin // Start TX with save length and data value
if (CHNL_TX_DATA_REN & CHNL_TX_DATA_VALID) begin
rData <= #1 {rCount + 4, rCount + 3, rCount + 2, rCount + 1};
rCount <= #1 rCount + (C_PCI_DATA_WIDTH/32);
if (rCount >= rLen)
rState <= #1 2'd0;
end
end
endcase
end
end
CHNL就是RIFFA实现的一个类似FIFO或是AXIS的接口,这个文件等于是做了一个回环。RX将收到的最后一个数据进行保存,然后使用TX将数据每次增加一个数值后再发送出去。这个测试与上位机测试代码将能够一起对PCIe的带宽进行测试。
接着在之前的MIZ7035_IO.xdc中添加约束:
#PCIe
set_property IOSTANDARD LVCMOS33 [get_ports PCIE_RESET_N]
set_property PACKAGE_PIN V19 [get_ports PCIE_RESET_N]
set_property PULLUP true [get_ports PCIE_RESET_N]
set_false_path -from [get_ports PCIE_RESET_N]
create_clock -period 10.000 -name sys_clk [get_ports PCIE_REFCLK_P]
set_property PACKAGE_PIN W6 [get_ports PCIE_REFCLK_P]
set_property PACKAGE_PIN W5 [get_ports PCIE_REFCLK_N]
set_property PACKAGE_PIN AC2 [get_ports {PCI_EXP_TXP[0]}]
set_property PACKAGE_PIN AE2 [get_ports {PCI_EXP_TXP[1]}]
set_property PACKAGE_PIN AF4 [get_ports {PCI_EXP_TXP[2]}]
set_property PACKAGE_PIN AF8 [get_ports {PCI_EXP_TXP[3]}]
set_property PACKAGE_PIN AC1 [get_ports {PCI_EXP_TXN[0]}]
set_property PACKAGE_PIN AE1 [get_ports {PCI_EXP_TXN[1]}]
set_property PACKAGE_PIN AF3 [get_ports {PCI_EXP_TXN[2]}]
set_property PACKAGE_PIN AF7 [get_ports {PCI_EXP_TXN[3]}]
set_property PACKAGE_PIN AD4 [get_ports {PCI_EXP_RXP[0]}]
set_property PACKAGE_PIN AC6 [get_ports {PCI_EXP_RXP[1]}]
set_property PACKAGE_PIN AE6 [get_ports {PCI_EXP_RXP[2]}]
set_property PACKAGE_PIN AD8 [get_ports {PCI_EXP_RXP[3]}]
set_property PACKAGE_PIN AD3 [get_ports {PCI_EXP_RXN[0]}]
set_property PACKAGE_PIN AC5 [get_ports {PCI_EXP_RXN[1]}]
set_property PACKAGE_PIN AE5 [get_ports {PCI_EXP_RXN[2]}]
set_property PACKAGE_PIN AD7 [get_ports {PCI_EXP_RXN[3]}]
这样就可以了,进行综合、实现、生成bit。
看一下实现后的Schematic
红色框内是PCIe相关的,蓝色框内是之前HDMI和MIG相关的。可以看到两者直接没有任何连接,也确实是我们还没有添加它们之间的通信关系,等验证完接口后,我再来用它的CHNL通道来实现视频数据的连接。
输出到SDK,新建一个fsbl工程,更显原有工程,然后生成一个BOOT.bin。将文件复制到SD卡,插入到MIZ7035上,并将开发板启动方式改为SD卡启动,这样我们就不用JTAG进行下载了。
将开发板插到电脑上并上电,然后启动PC,用之前安装过的Windriver就可以看到我们的设备了,说明PCIe接口已经启动。下面我们来用RIFFA进行测试。
先看一下./install/windows/README.txt
里面说到只能支持Windows 7的32、64位系统。
再看./source/driver/windows/README.txt
里面说到Debugging on Windows is difficult because there exists no kernel log file.
我也试了一下,WDK真是不会用,编译的时候也出错了。等有空了再来试吧。
Windows 7的WDK下载地址:https://www.microsoft.com/en-us/download/details.aspx?id=11800
./install/linux/README.txt
里面说直接使用driver目录下的编译和安装驱动即可
./source/driver/linux/README.txt
里面说了驱动的安装指令
$ sudo make setup
$ make
$ sudo make install
很简单的就能够安装了,在应用程序编写时直接include riffa.h 并link -lriffa即可,看起来很方便。
问题又来了,我电脑上没有Linux实体机,而VMWare的虚拟机又不支持PCIe设备的连入,别人的电脑搞起来又不方便,之前zcu102 petalinux的pcie还没
编完。
眼前一亮,看到了一个TX1开发板,搞起?
将MIZ7035插到TX1的PCIe插槽上,打开MIZ7035的电源,等待SDK启动成功。
在TX1上连接网线、HDMI显示器,按下TX1的POWER BTN按键打开TX1开发板。
我直接用Xshell通过网络登录到了TX1的命令行。
看一下pci设备
ubuntu@tegra-ubuntu:~$ lspci
00:01.0 PCI bridge: NVIDIA Corporation Device 0fae (rev a1)
01:00.0 Memory controller: Xilinx Corporation Device 7024
可以看到MIZ7035的PCIe已经被识别到了。
用U盘或TFTP将./source/driver,./source/c_c++,./source/python复制到开发板上去,然后进入./driver/linux/目录执行
$ sudo make setup
结果提示安装linux-headers-3.10.96-tegra时找不到软件包。网上也没有一个回答清楚的答案。试了试执行make指令,通过警告信息,可以看到定位到了目录/usr/src/linux-headers-3.10.96-tegra
查看该目录下的README,
These headers are provided to enable external module builds. They must be prepared on the target system before being used for module compilation. To prepare the headers, go into the headers package top level directory, and issue the following command:
sudo make modules_prepare
After preparation completes, external modules can be built following the process described in Documentation/kbuild/modules.txt.
原来是这样TX1 tegra的linux-headers是从apt-get源上找不到的,我们需要直接用开发板自己提供的,编译一下就好了。
在该目录下执行命令编译linux-headers
$ sudo make modules_prepare
返回刚才的的./driver/linux/目录
$ sudo make setup
$ make
$ sudo make install
这样riffa的功能看起来就完成编译了,激动人心的测试要开始了。
进入目录./c_c++/linux/x64/sample_app,这个app就是用来对应FPGA中的chnl_tester模块的。编译:
$ make clean
$ make
测试前可以看一下源码,了解测试命令。测试:
ubuntu@tegra-ubuntu:~/pcie/c_c++/linux/x64/sample_app$ ./testutil 0
Number of devices: 1
0: id:0
0: num_chnls:1
0: name:0000:01:00.00
0: vendor id:10EE
0: device id:7024
ubuntu@tegra-ubuntu:~/pcie/c_c++/linux/x64/sample_app$ ./testutil 1 0
ubuntu@tegra-ubuntu:~/pcie/c_c++/linux/x64/sample_app$ ./testutil 2 0 0 5000000
words sent: 5000000
words recv: 5000000
recvBuffer[0]: 4999997
recvBuffer[1]: 4999998
recvBuffer[2]: 4999999
recvBuffer[3]: 5000000
recvBuffer[4]: 5
recvBuffer[5]: 6
recvBuffer[6]: 7
recvBuffer[7]: 8
recvBuffer[8]: 9
recvBuffer[9]: 10
recvBuffer[10]: 11
recvBuffer[11]: 12
recvBuffer[12]: 13
recvBuffer[13]: 14
recvBuffer[14]: 15
recvBuffer[15]: 16
recvBuffer[16]: 17
recvBuffer[17]: 18
recvBuffer[18]: 19
recvBuffer[19]: 20
recvBuffer[4463772]: 0, expected 4463773
send bw: 943.800814 MB/s 20.209229ms
recv bw: 588.343826 MB/s 32.418945ms
ubuntu@tegra-ubuntu:~/pcie/c_c++/linux/x64/sample_app$ ./testutil 2 0 0 93312000
words sent: 93312000
words recv: 93312000
recvBuffer[0]: 93311997
recvBuffer[1]: 93311998
recvBuffer[2]: 93311999
recvBuffer[3]: 93312000
recvBuffer[4]: 5
recvBuffer[5]: 6
recvBuffer[6]: 7
recvBuffer[7]: 8
recvBuffer[8]: 9
recvBuffer[9]: 10
recvBuffer[10]: 11
recvBuffer[11]: 12
recvBuffer[12]: 13
recvBuffer[13]: 14
recvBuffer[14]: 15
recvBuffer[15]: 16
recvBuffer[16]: 17
recvBuffer[17]: 18
recvBuffer[18]: 19
recvBuffer[19]: 20
recvBuffer[702060]: 0, expected 702061
send bw: 1028.932190 MB/s 345.947998ms
recv bw: 1577.181281 MB/s 225.691895ms
下面分析一下测试指令
PCIe Gen2 x 4的理论带宽为2000MB/s,看来这个RIFFA是会有一些损失的,不过还能接受。
python测试
ubuntu@tegra-ubuntu:~/pcie/python$ sudo python setup.py install
ubuntu@tegra-ubuntu:~/pcie/python$ cd sample_app/
ubuntu@tegra-ubuntu:~/pcie/python/sample_app$ vim sampleapp.py
将脚本中的amt改为20 :wq
ubuntu@tegra-ubuntu:~/pcie/python/sample_app$ python sampleapp.py
array('I', [1L, 2L, 3L, 4L, 5L, 6L, 7L, 8L, 9L, 10L, 11L, 12L, 13L, 14L, 15L, 16L, 17L, 18L, 19L, 20L])
array('I', [17L, 18L, 19L, 20L, 5L, 6L, 7L, 8L, 9L, 10L, 11L, 12L, 13L, 14L, 15L, 16L, 17L, 18L, 19L, 20L])
ubuntu@tegra-ubuntu:~/pcie/python/sample_app$
可以看到测试的前4个数据发生了错误,还不知道是什么原因。先不管了
这次使用了老师给我推荐的RIFFA功能来测试PCIe的通信。一开始想着自己来做所有的工作,现在看来真的没必要,毕竟相关的东西太多太多,光是linux内核的驱动就够我受的了。RIFFA这个框架从知道这个名称,经过MIZ7035开发板一直,到最后用测试用例测试带宽,整个过程只有不到5个小时,感觉还是非常非常效率的
接下来要去熟悉一下它的CHNL接口,然后再这个接口上添加我自己的通道,实现最先计划的PCIe视频传输。