简介
学习PCIe有一段时间了,这里将这段时间的学习做一个总结。由于手里没有包含PCIe的板子,因此所做的也就是尽力将XILINX提供的实例工程中的关键模块进行分析,包括 PIO_RX_ENGINE.v,PIO_TX_ENGINE.v,PIO_EP_MEM_ACCESS.v ,希望对和我一样的初学者有所帮助。
软件:VIVADO2017.4
第一步:PCIe基础知识
PCIe协议比较复杂,XILINX官方提供了相关文档(pg054),此外也有不少好的中文学习资料(PCIe入门,PCIe体系结构导读)。文章结尾会上传部分学习资料,有需要的同学可以下载。
PCIe 规范对于设备的设计采用分层的结构,有事务层、数据链路层和物理层组成,各层有都分为发送和接收两功能块。总线的物理链路如下图所示。
PCIe总线层次关系如下。
在设备的发送部分,首先根据来自设备核和应用程序的信息,在事务层形成事务层包(TLP),储存在发送缓冲器里,等待推向下层;在数据链路层,在 TLP 包上再串接一些附加信息,这些信息是对方接收 TLP 包时进行错误检查要用到的;在物理层,对 TLP 包进行编码,占用链路中的可用通道,从查封发送器发送出去。
层次结构及数据传输(摘自PCIe入门 )。
事务层包(TLP),数据链路层包(DLLP),物理层(PLP)产生于各自所在层,最后通过电或光等介质和另一方通讯。其中数据链路层包(DLLP),物理层(PLP)的包平常不需要关心,在 IP 核中封装好了。在 FPGA 上做 PCIe 的功能,变成完成事务层包(TLP)的处理。
TLP 有三部分组成,帧头、数据、摘要(或者称ECRC)。TLP 头标长 3 或者 4 个 DW,格式和内容随事物类型变化;数据端为 TLP 帧头定义下的数据段,如果该 TLP 不携带数据,那该段为空。 Digest段(Optional)是基于头标、数据字段计算出来的 CRC,成为 ECRC,一般 Digest 段有 IP 核填充。所以,PCIe 的处理在用户层表现为处理 TLP 中头标和数据段。
如下图所示,Byte0~Byte11是头标(3DW,1DW=4Byte),Byte12~Byte23数据(多个DW),Byte24~Byte27是摘要。
1.Fmt 和 Type 字段确认当前 TLP 使用的总线事务,TLP 头的大小是由 3 个双字还是 4 个双字组成,当前 TLP 是否包含有效负载。Fmt+Type配置信息如下
2.TC 字段表示当前 TLP 的传送类型,PCIe 总线规定了 8 种传输类型,分别为TC0~TC7,缺省值为 TC0,该字段与 PCIe 的 QoS 相关。
3.Attr 字段由 3 位组成,其中第 2 位表示该 TLP 是否支持 PCIe 总线的 ID-basedOrdering;第 1 位表示是否支持 Relaxed Ordering;而第 0 位表示该 TLP 在经过 RC 到达存储器时,是否需要进行 Cache 共享一致性处理。
4.TH 位为 1 表示当前 TLP 中含有 TPH(TLP Processing Hint)信息,TPH 是 PCIe V2.1 总线规范引入的一个重要功能。TLP 的发送端可以使用 TPH 信息,通知接收端即将访问数据的特性,以便接收端合理地预读和管理数据。
5.TD 位表示 TLP 中的 TLP Digest 是否有效,为 1 表示有效,为 0 表示无效。
6.EP 位表示当前 TLP 中的数据是否有效,为 1 表示无效,为 0 表示有效。
7.AT 与 PCIe 总线的地址转换相关,可暂时不考虑。
8.Length字段用来描述 TLP 的有效负载(Data Payload)大小。PCIe 总线规范规定一个 TLP的 Data Payload 的大小。PCIe 总线设置 Length 字段的目的是提高总线的传送效率。Length 字段以 DW 为单位,其最小单位为 1 个 DW。
9.Request ID字段包含“生成返个 TLP 报文”的 PCIe 设备的总线号(Bus Number)、设备号(Device Number)和功能号(Function Number)
10.Last DW BE和First DW BE,在PCIe 总线以字节为基本单位迕行数据传递,但是 Length 字段以 DW 为最小单位。为此TLP 使用 Last DW BE 和 First DW BE 返两个字段迕行字节使能,使得在一个 TLP 中,有效数据以字节为单位。
一个简单的TLP
这是一个TLP报文:01a0090f40000001+0403020100000010,由于一次发送64位,因此这个报文的前64位是标头,后64位是数据和地址。
按照上图的数据格式可知
Request ID:16’b0000_0001_1010_0000
Tag:8’b0000_1001
Last DW BE:4’b0000
First DW BE:4’b1111
Fmt:2’b10
Type:5’b0_0000
Length:10’b00_0000_0001
Data:32’h04030201
Address:32’h00000010(低两位无效)
完成报文(摘自PCIe入门)
当发送端发送一个TLP给接收端后,接收端有时需要发送一个完成报文返回给发送端,如发送端发送一个Memory Read(存储器读请求)到接收端,那么接收端就会读取相应地址下的数据,拼接到完成报文中反馈给发送端,这样就完成了一次存储器读请求。
不是所有的请求都会反馈一个完成报文,下图中Non-Posted 表示需要反馈完成报文,而Posted 表示不需要反馈完成报文。
完成报文的格式
其中Byte0~Byte3的字段功能和普通TLP的一致,不一样的字段分析如下:
Requester ID 和 Tag 字段完成报文使用 ID 路由方式。完成报文头的长度为 3DW,完成报文头包含 Transaction ID 信息,由 Requester ID 和 Tag 字段组成,返个 ID 必须与源设备发送的数据请求报文的 Transaction ID 对应,完成报文使用 Transaction ID 运行 ID 路由,并将数据发送给源设备。当 PCIe 设备收到存储器读、I/O 读写或者配置读写请求 TLP 时,需要首先保存返些报文的 Transaction ID,然后当该设备准备好完成报文后,将完成报文的 Requester ID 和 Tag 字段赋值为当前保存的 Transaction ID 字段。
Completer ID 字段存放发送完成报文的PCIe设备的 ID号,PCIe设备运行数据请求时需要在TLP字段中包含Requester ID字段;而使用完成报文结束数据请求时,需要提供 Completer ID 字段。
Lower Address 字段,如果当前完成报文为存储器读完成 TLP,该字段存放在存储器读完成 TLP 中第一个数据所对应地址的最低位。值得注意的是,在完成报文中,并不存在 First DW BE 和 Last DW BE字段,因此接收端必须使用存储器读完成 TLP 的 Low Address 字段,识别一个TLP中包含数据的起始地址。
Status字段保存当前完成报文的完成状态,表示当前 TLP 是正确地将数据传递给数据请求端;还是在数据传递过程中出现错误;或者要求数据请求方运行重试。
BAR空间
PCIe板卡访问PC内存时,板卡向 PC 发送 TLP 包,例如 MWr 包,地址信息就是PC 的物理地址;如果是 MRd 包,那 PC 收到后回复一个完成包,板卡从完成包分析出数据即得到 MRd 读取地址的数据。这是PCIe板卡访问PC。
PC访问PCIe板卡,简单的解释,PC 启动是,BIOS 探测所有的外设。对 PCIe (PCI)设备来说,BIOS 检测到板卡有多少个 BAR 空间,每个空间有多大,然后对应为这些 BAR 空间分配地址。对 PC 设备来说,它能“看”到 PCIe 板卡的空间只有 BAR 空间,也就只能访问这些 BAR 空间。也就是说,板卡可以发送合法的 PCIe TLP 包,并得到 PC 端的相应;但是 PC 端访问板卡被局限在 BAR 空间。
以上是PCIe TLP的部分基础知识,当然不止这些,详细内容请参考PCIe相关中英文资料。
第二步:工程生成及代码分析
1.使用VIVADO新建一个工程,都是很常规的操作,但是注意在选择芯片或开发板时,FPGA芯片一定带有PCIe(可选xc7z035ffg676-2),然后点击Next直到Finish。
2.在IP Catalog中找到PCIe IP核,然后双击IP核进行配置。
3.配置x1,5GT/s
4.配置ID
5.配置BARs
6.配置Core Capabilities
7.配置Interrupts
8.点击Generate
9.生成Example Design
10.选择文件夹存放Example Design
11.然后就可以在指定的文件夹中找到这个工程了。
第三步:关键代码分析
1.整体结构
打开工程后可以看见这个工程的结构如上图所示,xilinx_pcie_2_1_ep_7x是顶层模块,其下有pcie_7x_0_support_i 和app 两个子模块;其中pcie_7x_0_support_i属于PCIe IP核,是已经封装好了的,不需要修改;而app 是属于应用模块,我们能修改的就是这个模块。
app 中又包含PIO_EP_MEM_ACCESS,PIO_RX_ENGINE 和PIO_TX_ENGINE 三个子模块,这三个模块就是我们重点关注的。
PIO_EP_MEM_ACCESS 用于控制FPGA的存储器的读写;
PIO_RX_ENGINE 是接收引擎,用于接收、解析TLP;
PIO_TX_ENGINE 是发送引擎,用于组装、发送TLP;
PIO 工作流程大概描述:
首先差分接收接口(rxn、rxp)接收到信号,信号经过物理层、数据链路层、事务层之后变成TLP进入接收引擎(PIO_RX_ENGINE )进行解析;接着根据标头判断这个TLP是读存储器还是写存储器,若是写存储器,就将下一个TLP中的地址和数据解析出来(因为这里一次发送64bit,所以第一个TLP中不包含地址和数据,第二个TLP中包含地址和数据),然后通过PIO_EP_MEM_ACCESS 模块将数据写入指定的地址中;若是读存储器,就将下一个TLP中包含的地址解析出来,再通过PIO_EP_MEM_ACCESS 模块将数据从指定的地址中读取出来,然后经过发送引擎(PIO_TX_ENGINE )进行完成包拼接,最后通过事务层、数据链路层、物理层封装之后,通过差分发送接口(txn、txp)将数据发送出去。
2.主要模块代码分析
由上可知,PIO_RX_ENGINE 、PIO_TX_ENGINE 和PIO_EP_MEM_ACCESS 是主要的分析对象。
PIO_RX_ENGINE.v
接收引擎中包含6种头标
支持64位和128位数据位宽,这里分析是64位数据位宽。
此外,从代码中可以得知,接收引擎是通过AXI接口与其他模块进行信号交换。其中m_axis_rx_tdata 就是传入接收引擎的TLP。
分析来看,接收引擎的关键在于状态机跳转。
状态机分析
首先进入PIO_RX_RST_START 状态,这是一个复位状态,在复位状态中,首先判断sop 信号是否有效,sop 信号是TLP开始的信号,若这个信号无效,则程序会一直在复位状态中直到sop 信号有效;若sop 信号有效,根据上文一个简单的TLP 中分析可知,TLP的24~30位代表着这个TLP的类型,所以接着分析m_axis_rx_tdata[30:24] 判定包的类型。
若分析后是PIO_RX_MEM_RD32_FMT_TYPE 状态
m_axis_rx_tready 信号标志着接受设备是否准备好接收TLP,所以当接收到一个TLP后m_axis_rx_tready <= 1’b0 ,表示刚接收到一个TLP,还没有准备好接收下一个,等到当前TLP处理完之后才能准备好接收下一个TLP。
m_axis_rx_tdata[9:0] 是代表当前TLP的Length,这个Length表示这个TLP中数据的数量,单位是DW(64bit),m_axis_rx_tdata[9:0]==10’b1这是因为PIO通常一次传输一个DW数据。
所以判断m_axis_rx_tdata[9:0]==10’b1,确定当前是PIO模式,接着解析出TLP中的其他字段信息,然后跳转到PIO_RX_MEM_RD32_DW1DW2 状态。
由上文对完成报文的介绍可知,存储器如请求是需要反馈完成报文的,而且这个完成报文反馈的是从存储器指定地址下的数据,req_addr 表示需要读取的存储器的地址,req_compl 表示需要发送完成报文,req_compl_wd 表示完成报文中包含数据,然后跳转到PIO_RX_WAIT_STATE 状态。
PIO_RX_WAIT_STATE 状态是一个等待完成状态,在该状态下,程序先通过tlp_type 和compl_done 两个信号为条件进行判定。
tlp_type 表示当前TLP的类型,compl_done 表示完成包已经发送完成,由发送引擎反馈给接收引擎。在PIO_RX_MEM_RD32_DW1DW2状态中有的req_compl 、req_compl_wd 和req_addr 三个信号,其中req_addr 信号输入到存储器模块中读取该地址下数据,然后又将数据发送到发送引擎中。req_compl和req_compl_wd这两个信号输入到发送引擎中用以合成完成报文,当合成完成报文并发送之后,发送引擎产生compl_done 信号,表明完成包已经发送完成,该信号输入到接收引擎中,这时表明一个存储器读请求已经完成,所以在PIO_RX_WAIT_STATE 状态中将m_axis_rx_tready信号置1,表明接收引擎准备好可以接收下一个TLP了,状态又跳转到PIO_RX_RST_STATE状态。
以上完成对PIO_RX_MEM_RD32_FMT_TYPE 这个存储器读请求的分析,总的来说就是在这个TLP中包含req_compl 、req_compl_wd 和req_addr 三个重要信号,req_compl 和req_compl_wd 告诉发送引擎发送一个带数据的完成包;req_addr 信号先传入发送引擎中截取出存储器地址,在传入存储器模块读取这个地址下的数据,返回到发送引擎中作为完成包的数据,这样就完成了存储器读请求TLP的操作。
接下来将分析PIO_RX_MEM_WR32_FMT_TYPE TLP。这个TLP的目是向存储器指定的地址中写入数据,而这些数据包含在这个TLP中。
首先是从TLP中解析出TLP类型和这个TLP中数据的长度(单位是DW);然后同样将ready信号置0,表明还没有准备好接收下一个TLP;接着判定这个TLP中的数据长度是否为1DW(因为这是PIO模式,所以数据长度为1DW);最后解析出FIRST DW BE和LAST DW BE,判断传输过来的数据是否有效;状态跳转到PIO_RX_MEM_WR32_DW1DW2。
在PIO_RX_MEM_WR32_DW1DW2 状态中,首先通过m_axis_rx_tdata[63:32]解析出这个TLP中的数据;然后设置wr_en有效,这个信号是使能存储器写功能;接着设置m_axis_rx_tready无效,表明还没准备好接收下一个包;最后解析出需要写入存储器的地址wr_addr,状态跳转到PIO_RX_WAIT_STATE。
同样,PIO_RX_WAIT_STATE是一个等待状态,判定TLP的类型和wr_busy 信号。wr_busy 信号是一个写繁忙信号,因为这个TLP是想往存储器中写数据,当要写数据送入存储器模块之后,存储器模块产生一个写繁忙有效信号表明此时在写存储器,当写接收之后,写繁忙信号失效,这时在接收引擎中接收到wr_busy 信号失效,表明存储器写请求处理完成,所以设置ready信号有效,表明可以接收下一个TLP了,状态也跳转回复位状态。
以上完成了在接收引擎(RX_ENGINE)中PIO_RX_MEM_RD32_FMT_TYPE和PIO_RX_MEM_WR32_FMT_TYPE两种头标的分析,其余类型的头标的分析与此类似。接着分析发送引擎(TX_ENGINE)。
PIO_TX_ENGINE.v
发送引擎中包含两种头标
然而这两种头标的完成包只有带不带数据的区别,包内其他信息几乎一样。
实现发送完成包的关键还是状态机分析。
首先设置compl_done <= 1’b0表示完成包发送未完成,而这个信号就是在接收引擎等待状态中的compl_done 信号。接着判定req_compl_q 信号(这个信号就是从接收引擎传来的发送完成包信号),如果需要发送完成包,则设置compl_busy_i 信号有效,表示正在发送完成包,接下来就是状态机跳转发送完成包了。
首先进入复位状态,在compl_busy_i有效的情况下进行初始化,然后判定从机是否准备好接收信号,接着跳转到PIO_TX_CPLD_QW1_FIRST 状态。
这个状态中传输第一帧(64bit)数据,这一帧数据是头标的前2DW数据,其中通过req_compl_wd_q 信号判定这个完成包是否带有数据,同时因为这一帧信号都是有效的信号,所以s_axis_tx_tkeep 为FF。接着状态机跳转。
这个状态比较简单,只设置了valid信号,说明主机准备好发送第二帧。接着跳转到PIO_TX_CPLD_QW1 状态。
在这个状态中,首先通过s_axis_tx_tready 信号判断从设备是否准备好接受信号;由于完成包由3DW标头和1DW的数据构成,总共2帧,所以这一帧是最后一帧,因此此时设置s_axis_tx_tlast为1表明这是最后一帧;然后设置s_axis_tx_tvalid为1表明此时主设备准备好发送数据;接着就根据完成包格式拼接发送数据。拼接完成后通过req_compl_wd_q设置s_axis_tx_tkeep信号,由于一次传输64bit,所以第二帧刚好1DW标头+1DW数据,这一帧都有效,所以s_axis_tx_tkeep为FF,如果这个完成包不带数据,即req_compl_wd_q无效,则最后一帧数据中只有1DW标头,那么s_axis_tx_tkeep就为0F。
至此这个带数据的完成包就发送完成了,所以设置compl_done有效,这个信号返回到接收引擎中,使得接收引擎准备接收下一个TLP,设置compl_busy_i无效,说明又可以发送完成包了,同时状态机跳转至PIO_TX_RST_STATE状态。
PIO_EP_MEM_ACCESS.v
存储器的读写控制相对了发送和接收引擎来说简单许多,读操作就是将指定地址下的数据读取出来;存储器写操作就是不太一样,是先将该地址的数据读取出来,然后将数据改变成需要写入的数据,最后将数据写入。写操作的状态机跳转如下:
首先进去PIO_MEM_ACCESS_WR_RST状态
在该状态下,判断wr_en信号,这是从接收引擎中引入的信号,表示写存储器写控制状态机开始,然后进去写等待状态PIO_MEM_ACCESS_WR_WAIT。
在这个状态下,程序先让write_en无效,表示这时不能对存储器进行写入,而准备将这个地址下的数据读出,进入下一状态。
在这个状态下,将w_pre_wr_data的值赋给pre_wr_data,而w_pre_wr_data的值就是从存储器中读出的值,可通过如下代码看出。
由上可知,pre_wr_data 中的值就是存储器中读出的值。之后又进入存储器写状态PIO_MEM_ACCESS_WR_WRITE。
由于这个数据是接收引擎中的携带的数据,而且是最后一帧的最后一个DW,所以需要通过LAST DW BE信号判定这个DW是否都有效,而wr_be信号就是LAST DW BE信号,由程序可知,如果这个DW中的某一个字节有效,则就将这个字节写入存储器,否则这个字节的数据还是原来的数据。这就是存储器的写操作原则。
在分析信号时,建议在VIVADO中将Schematic打开,以辅助程序分析。
结束
以上就是PCIe的部分基础知识,以及本人对PIO中的重要模块的简单分析,理解有误之处还望斧正;此外上传了一些基础文档以及上述三个重要模块的代码(不是工程,就是三个.v文件),代码有较为详细的注释,有需要的同学可以下载看看(https://download.csdn.net/download/cllovexyh/10330415)。