近期在网上淘来个源码,看了之后觉得还不错。完全刷新我对ZYNQ的认知啊,原来ZYNQ也可以这么玩的。PS-PL交互很方便。话不多说,分享一下他们的说明书(源码呢?源码呢?源码呢?大哥源码要几大洋咱不敢放上来啊)
本系统在ZYNQ平台上实现PCIe的DMA收发(EP端),兼容ZYNQ系列所有带PCIe资源的芯片,如7015、7030、7045、ZCU102(MP9EG)等等。实现包括BAR0&BAR1空间访问,PS/PL/RC三方可同时访问BAR0空间,PL/RC双方同时访问BAR1空间(也可扩展到PS三方)。实现包括RC DDR数据DMA传输到EP DDR中,以及EP DDR数据DMA传输到RC DDR中。传输速度在实际项目中测试结果为理论值的80%。其中lane x1 x2略高于80%,lane x4略低于80%(DDR速率为1066时)
此文源自于一个项目的PCIE模块的总结,暂且不对基础知识作过多的补充说明。
推荐阅读(感谢以下的作者们):
本系统是ZYNQ PCIE DMA,FPGA作为EP端。框架和文件结构如下图所示:
其中RC端可以是PC机,也可以是DSP/ARM等设备。
Pcie_support模块包含了IP Core的例化,clock时钟模块为IP Core提供时钟,以及IP Core的默认配置。一般PCIe support模块是不需要变动的涉及到的东西也是很底层的。
而PCIe app模块里则是用户对TLP事务的处理以及对IP Core一些信号的处理,比如复位、中断等信号。PCIe app模块包含EP模块和CTRL模块,CTRL模块与电源和复位有关。
EP模块则包含TX模块、RX模块、MEM模块,是TLP事务处理的三个主要模块。TX模块实现TLP的打包与发送,RX模块实现TLP的接收与解析,MEM模块实现BAR空间的存储操作。
REG_Table模块实现PS-PL交互,实现PS控制PL以及PL信号反馈到PS上。
DDR read&write模块实现DDR的读写操作,是标准模块且官方有相应的IP,可以是MIG操作DDR,也可以是AXI操作DDR,数据量小的话也可以直接采用片上BRAM。某T某B上买回来的皮卡丘之狂电码农的源码默认是PCIE核心功能,并没有提供DDR read&write模块。其他模块有提供,需要自己加上DDR两模块。
A. BARx空间读写
对于上层的linux软件编程人员来说,EP的BAR空间与RC的BAR空间是一一对应的。EP BAR0对应RC BAR0,EP BAR0对应RC BAR1.这种对应关系表现在读写同步上。RC/EP任意一方读写操作BAR空间,另一方都会同步改变。
底层原理上来说RC一但操作BAR空间,或读或写或刷新都会进行一次TLP事务交易。而发生的TLP事务交易正是同步了EP的BAR空间。细心的读者会发现,这不是会出现竞争关系?是的,所以用户需要约定互锁关系,以避免竞争出现。
源码分析:RX_ENGINE模块的wr_addr/wr_data/wr_xx,以及TX_ENGINE模块的rd_addr/rd_data/rd_xx连接到EP_MEM_ACCESS模块。在EP_MEM_ACCESS模块里分为BRA0与BAR1;而BAR0连接到外部的双口RAM上,通过AXI-GP与PS共享;BAR1则直接在MEM模块里例化一些寄存器,与RC进行交互,方便RC控制EP。RTL见下图:
B. EP DDR to RC DDR的数据传输
对于RC端应用开发人员来说,只需要操作BAR1将目标地址填写到wr32_RC_address上,将源地址填写到wr32_EP_address上,将数据长度填写到wr32_length上,再发起一个wr32_start脉冲信号。则可以发起一次EP DDR to RC DDR的DMA数据传输。直到收到wr32_finish信号,此次DMA传输完成。
原理上实际为将DDR读上来的数据,进行TLP格式打包,从AXI接口传入到IP Core,再从PCIE总线上发送到RC设备。
细心的人会发现,其实也可以将这些寄存器放在EP端操作。相当于EP写RC DDR,但这样会产生写DDR覆盖问题,需要严格的RC-EP DDR地址分配约定。否则EP操作RC DDR会另RC死机(相当于RC野指针)。此处RC主动控制传输,相当于RC读数据不会搞死EP,安全性有很大提高。
源码分析:较为复杂,详看源码注释。主要步骤集中在TX模块的几个状态机状态里。
C.RC DDR to EP DDR的数据传输
对于EP端开发人员来说,相似于以上情况。通过AXI-GP操作reg table即可。reg table是在PL端建立的一个寄存表,用于PS-PL交互便于PS控制PL。
原理上就是EP端主动去读RC的数据,宏观结果就是RC数据传输到EP上。EP将需要读取的RC地址和长度,进行TLP格式打包,发送到RC上。RC则会回应CPLD TLP包。EP再接收这些CPLD TLP包,解析得到数据,再将数据写入的指定的DDR地址上。
源码分析:较为复杂,详看源码注释。在TX及RX模块的多个状态机状态里。看的时候按照原理流程一步步看会清晰很多。
关键点:1.在start之前,length/address(RC&EP)必需准备完成。2.只有ready和valid两信号同时有效,data才算是进行了有效的传输。3.只有ready和valid两信号同时有效的上升沿计数足够length个数(表明data有效传输了length个),finish才会有效。否则会一直等待状态,没有超时设计。
简化理解:寄存器配置完成后,start传输,输入或输出length个data后,finish置高结束传输。
对于lane x1 x2 x4来说,资源变化不大。以下是7015的资源使用情况。
RegisterMap_PL.h ,如下:
代码片
.
/*
* ZYNQ-PL register map
* register table
* */
#pragma once
#include
//base address
#define PCIE_AXI_CTRL_BASEADDR 0x40000000
#define PCIE_BAR0_BASEADDR 0x80000000
//PCIE CONTROL table
#define reg_link_up 0
#define reg_rd32_ready 1
#define reg_rd32_start 2
#define reg_rd32_length 3 // in 64bit
#define reg_rd32_rcaddr 4 // 32bit allign
#define reg_rd32_epaddr 5 // 32bit allign
#define reg_rd32_finish 6
//operation
#define PL_REG(Base,reg) ( *((volatile uint32_t *)(Base) +(reg)) )
main.c ,如下:
代码片
.
/*
* main.cpp
*/
#include
#include
#include
#include
#include
#include "RegisterMap_PL.h"
using namespace std;
//虚拟地址映射
int Phy2Vir_MMap(uint32_t physAddr, uint32_t memSize , uint32_t *pMemVirtAddr);
int Phy2Vir_unmapMem(uint32_t physAddr, uint32_t memSize , uint32_t *pMemVirtAddr);
//全局变量
unsigned int PCIE_AXI_CTRL_VirtAddr = 0 ;
unsigned int PCIE_BAR0_VirtAddr = 0 ;
//主程序
int main()
{
printf("hello world \r\n");
//虚拟地址映射
uint32_t PCIE_AXI_CTRL_Vritaddr = 0 ;
Phy2Vir_MMap(PCIE_AXI_CTRL_BASEADDR, 16*4, &PCIE_AXI_CTRL_Vritaddr);
//等待链路训练成功
while(1){
int link_up =PL_REG(PCIE_AXI_CTRL_Vritaddr,reg_link_up) ;
if( link_up ){
printf("pcie link up ... \r\n");
break;
}
printf("pcie linking ... \r\n");
usleep(100*10000);
}
//等待ZYNQ DDR/DDR控制模块准备完成
while(1){
int rd32_ready =PL_REG(PCIE_AXI_CTRL_Vritaddr,reg_rd32_ready) ;
if( rd32_ready ){
printf("zynq ddr control ready done ... \r\n");
break;
}
printf("waitting for zynq ddr control ready singal ... \r\n");
usleep(100*10000);
}
//填写读取长度、目标地址、源地址信息
//从PCIE RC端DDR物理地址为0x20000000处,读取长度为1920*1080 byte的数据,保存到EP DDR物理地址0x3a800000处。
PL_REG(PCIE_AXI_CTRL_Vritaddr,reg_rd32_start ) = 0 ;
PL_REG(PCIE_AXI_CTRL_Vritaddr,reg_rd32_length) = 1920*1080/8 ; //in 64bit(8byte)
PL_REG(PCIE_AXI_CTRL_Vritaddr,reg_rd32_rcaddr) = 0x20000000 ; //DDR PHY address
PL_REG(PCIE_AXI_CTRL_Vritaddr,reg_rd32_epaddr) = 0x3a800000 ; //DDR PHY address
//开始传输
PL_REG(PCIE_AXI_CTRL_Vritaddr,reg_rd32_start ) = 1 ;
//等待DMA传输完成
while(1){
int rd32_finish =PL_REG(PCIE_AXI_CTRL_Vritaddr,reg_rd32_finish) ;
if( rd32_finish ){
printf("pcie get data from RC done ... \r\n");
break;
}
printf("pcie getting data ... \r\n");
usleep(100*10000);
}
///////////////////////////////////////////////////////////
// BAR 空间读写demo
///////////////////////////////////////////////////////////
//虚拟地址映射
uint32_t PCIE_BAR0_Vritaddr = 0 ;
Phy2Vir_MMap(PCIE_BAR0_BASEADDR, 2*1024, &PCIE_BAR0_Vritaddr);
//写入顺序数,RC的BAR空间同时会得到此处写入的数据
//同理,在RC的BAR空间写入数据,此处也可以读到
for(int i=0; i<128; i++){
*((volatile uint32_t *)PCIE_BAR0_Vritaddr +i) = i ;
}
//整块操作,RC的BAR空间同时会得到此处写入的数据
//同理,在RC的BAR空间写入数据,此处也可以读到
uint32_t *a = (uint32_t *)PCIE_BAR0_Vritaddr ;
uint32_t *b = (uint32_t *)PCIE_BAR0_Vritaddr +256 ;
memcpy(b,a,128*sizeof(uint32_t));
printf("exit main \r\n");
return 0 ;
}
本文完结