ZYNQ PCIe EP实现DMA+Linux交互,非常简洁的程序

ZYNQ PCIe-DMA源码 例程 PS-PL交互 linux/裸机 verilog C/C++

  • ZYNQ PCIe-DMA的实现过程
    • 一、概述
    • 二、基础知识
    • 三、系统总框架
    • 四、工作原理与工作模式
    • 五、接口时序
    • 六、资源使用情况
    • 七、PS-PL交互以及测试程序

ZYNQ PCIe-DMA的实现过程

近期在网上淘来个源码,看了之后觉得还不错。完全刷新我对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模块的总结,暂且不对基础知识作过多的补充说明。
推荐阅读(感谢以下的作者们):

  1. Xilinx FPGA高速串行传输技术与应用 陈博,张建辉 等 (作者)
  2. https://blog.csdn.net/wenjia7803/article/details/80086284
  3. http://xilinx.wikidot.com/zynq-pcie-trd
  4. https://zhuanlan.zhihu.com/p/34096340
  5. https://blog.csdn.net/long_fly/article/details/79150820
  6. https://blog.csdn.net/Qiuzhongweiwei/article/details/78652951
    如果是零基础,想学得深一点的话,建议搞到第1本书阅读一下PCIE的章节。估计两三天可以读完。仅看上面的链接,再配合某T某B上买回来的源码以及注释,也可以理清整个过程。略微修改一下便能成为自己的代码,也算方便。
    对于基础知识,个人总结如下
    +1. PCIE配置空间以及配置寄存器之类的,暂时是不用管。
    +2. 最底层当然是调用xilinx的PCIe IP Core,IP core的配置也可以使用demo默认的。
    +3. 对于FPGA人来说,编程面向的是TLP层协议。无非就是完成不同的交易事务的处理。如发送数据就是从数据源读出数据,按照TLP格式打包成相应的报文,写入IP Core(AXI操作);而接收数据就是从IP Core得到TLP报文(AXI接口),按照相应TLP格式进行解析,然后将解析到的数据写入DDR。

三、系统总框架

本系统是ZYNQ PCIE DMA,FPGA作为EP端。框架和文件结构如下图所示:
ZYNQ PCIe EP实现DMA+Linux交互,非常简洁的程序_第1张图片
其中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见下图:
ZYNQ PCIe EP实现DMA+Linux交互,非常简洁的程序_第2张图片
B. EP DDR to RC DDR的数据传输
ZYNQ PCIe EP实现DMA+Linux交互,非常简洁的程序_第3张图片
对于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的数据传输
ZYNQ PCIe EP实现DMA+Linux交互,非常简洁的程序_第4张图片
对于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模块的多个状态机状态里。看的时候按照原理流程一步步看会清晰很多。

五、接口时序

ZYNQ PCIe EP实现DMA+Linux交互,非常简洁的程序_第5张图片
ZYNQ PCIe EP实现DMA+Linux交互,非常简洁的程序_第6张图片
关键点: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的资源使用情况。
ZYNQ PCIe EP实现DMA+Linux交互,非常简洁的程序_第7张图片
ZYNQ PCIe EP实现DMA+Linux交互,非常简洁的程序_第8张图片

七、PS-PL交互以及测试程序

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 ;
}


本文完结

你可能感兴趣的:(PCIE)