在国产的FT-M6678 DSP上实现KCF算法是我研究生期间的主要工作,KCF算法的原理与实现已经在之前的文章以及我的Gitee仓库中有部分介绍。这里主要介绍DSP与上位机通信的方式,以及XDMA Linux驱动的使用。具体的设计细节可以看我的毕业设计补充材料。
SRIO与PCIe桥接器
如果只考虑实现DSP与上位机通信,设计硬件的时候就完全可以将DSP的PCIe接口与上位机连接。但我所用的硬件平台实际上是DSP与FPGA通过SRIO连接,FPGA与上位机通过PCIe连接,所以我就在FPGA里做了一个接口转换的功能。实现数据在SRIO接口与PCIe接口之间的传输,这一设计我称之为“SRIO与PCIe桥接器”。
图中的SRIO IP对外与DSP采用SRIO链路互连,XDMA对外提供PCIe接口与上位机互连。桥接器提供了AXI4-Lite slave接口,可以通过配置寄存器来控制它工作。XDMA在实例化的时候勾选上AXI4-Lite接口,上位机就可以直接通过Lite接口控制桥接器;DSP想要控制桥接器工作可以通过EMIF总线接口实现。
我另外设计了EMIF与AXI4-Lite接口转换器,使DSP可以通过简单的存储器访问操作实现对桥接器的控制。使用的时候需要按照特定的步骤来实现读写操作。地址线额外采用一个bit来区分读操作和写操作。比如存在地址空间[0,A-1], 则往地址[0,A-1]写就是一般的写,往地址[A,2A-1]写则是发起读请求。根据不同的地址范围解析从地址FIFO输出的地址,来确定将地址发往AW通道或是AR通道。通过EMIF接口读写AXI4-Lite寄存器的操作如下所示:
- 读寄存器
uint32_t readReg(uint32_t *addr){
while(地址FIFO满); // 等待地址FIFO非满
*((uint32_t *)((uint32_t)(addr) + offset)) = 0; // 写地址
while(读数据FIFO空); // 等待读数据FIFO非空
return *(addr); // 读数据
}
- 写寄存器
void writeReg(uint32_t *addr, uint32_t val){
while(地址FIFO满); // 等待地址FIFO非满
*addr = val; // 写数据
}
桥接器的具体功能
- 向DSP发送门铃事务包或SW事务包;
- 接收DSP发送过来的门铃事务包或NW事务包;
- 收到的门铃是事务包可以直接转发至PC,向上位机发起MSI中断;
- 发送的SW事务包的数据源可以源于FPGA的片内外存储器或者XDMA的H2C通道;
- 接收的NW事务包数据可以存入FPGA的片内外存储器或者发往XDMA的C2H通道。
数据传输过程
有了FPGA上的硬件支持,DSP与上位机之间的数据传输通路才能得以建立。目前数据的交互都是由DSP来主动控制的,也可以采用上位机控制,但我没有做。
DSP向上位机发送数据
DSP首先通过EMIF接口配置桥接器的工作模式,令它将收到的NW事务包转发至XDMA的C2H通道。DSP接着发送门铃事务包,桥接器转发至上位机,上位机解析门铃信息,启动C2H通道传输。从而实现数据从DSP到上位机的流式传输。
DSP从上位机接收数据
DSP首先发送门铃事务包,桥接器转发至上位机。上位机解析门铃信息后发起H2C的DMA通道传输,与此同时DSP通过EMIF接口启动桥接器发送SW事务包,同时SW事务包的数据源于H2C通道。从而实现数据从上位机到DSP的流式传输。
数据传输速率测试
对多种不同的数据量做了传输速率测试,每种数据量多次实验。得到下图结果:
数据的传输时间与数据量的关系可以建模为线性关系,即 y = k x + b y=kx+b y=kx+b。其中斜率 k k k表示数据传输速率 v v v的倒数,即 v = 1 / k v=1/k v=1/k。 b b b表示数据传输固有的时间开销。图中横纵坐标都采用对数坐标系,描点,绘制趋势线后可以对测试点线性拟合,得到的结果表明实际的数据传输速率都在1 GB/s左右。
XDMA Linux驱动使用
Xilinx在Github上提供了XDMA配套的Linux驱动源码,而且也给出了具体的使用方式,但是实际使用过程中也有一些需要注意的细节在这里记录一下。
驱动编译
驱动编译需要先切换到xdma目录下,再编译。readme.txt里提示是用make install,但install驱动的过程是需要管理员权限的,因此需要加上sudo。编译过程如下:
sudo make install
设备识别
在加载驱动前可以先用lspci查看一下PCI设备。
图里的Xilinx Corporation Device 7028就是我们在FPGA里实例化XDMA IP后,由上位机识别到的PCI设备。这里的“7028”是在Vivado里设置的。
如果lspci看不到对应的Xilinx设备,那么大概率就是硬件问题了,得检查一下硬件电路是否有问题或者FPGA烧写是否到位。
加载驱动
加载驱动用的就是tests目录下的load_driver.sh。记得把这个脚本的第21行device_id的设置与前面的设备ID一致再运行这个脚本。
# device_id=903f
device_id=7028
加载驱动过程中也有可能会出错,遇到出错了也不要紧,可以用dmesg查看内核打印输出的信息,大概率都能找到问题的原因。
dmesg
对驱动源码的修改
DMA通道的配置寄存器所在的BAR位置是会变化的,驱动识别PCIe外设的过程是依次读取不同BAR空间特定地址的数据来判断该BAR空间是不是DMA配置空间(config BAR)
libxdma.c里有一个identify_bars的函数就是识别每个BAR具体的功能。我在Vivado里勾选了XDMA的AXI4-Lite功能,就是如上图中红框中的配置。但是这个驱动里的is_config_bar函数没有识别到BAR1就是config BAR。
虽然这让人很疑惑,但是我后来发现可以提前指定config BAR的位置,就是在libxdma.h里放开XDMA_CONFIG_BAR_NUM的注释,并且指定config BAR的number为1。然后驱动就可以识别到BAR1是config BAR,而BAR0可以用identify_bars函数来识别。
软件调用
加载驱动成功后会显示这样的结果。
驱动加载完成后会在/dev/目录下生成几个xdma开头的设备文件,对这些文件的读写就能够转换为数据传输操作。而这些加载驱动时生成的文件都是需要管理员权限才能访问的,因此需要用chmod改变它们的读写权限,让所有人都能读写。
#define H2C_DEV_NAME "/dev/xdma0_h2c_0"
#define C2H_DEV_NAME "/dev/xdma0_c2h_0"
#define CTRL_DEV_NAME "/dev/xdma0_control"
#define USER_DEV_NAME "/dev/xdma0_user"
#define EVENT_DEV_NAME "/dev/xdma0_events_0"
我用到的几个设备文件主要是上面的。h2c和c2h分别是H2C的DMA传输和C2H的DMA传输,control对应到电路上就是AXI4-Lite接口,user对应到电路上是DMA Bypass的接口。events文件是用来响应MSI中断的。具体使用细节可以看代码。