Xilinx FPGA PCIe | XDMA IP 核 / 应用 / 测试 / 实践

news2025/5/13 13:30:16

注:本文为 “Xilinx FPGA 中 PCIe 技术与 XDMA IP 核的应用” 相关文章合辑。

图片清晰度受引文原图所限。
略作重排,未整理去重。
如有内容异常,请看原文。


FPGA(基于 Xilinx)中 PCIe 介绍以及 IP 核 XDMA 的使用

Njustxiaobai 已于 2023-11-22 16:10:41 修改

一、PCIe 总线概述

1. PCIe 总线架构

PCIe 总线架构与以太网的 OSI 模型类似,是一种分层协议架构,分为事务层(Transaction Layer)、数据链路层(Data Link Layer)和物理层(Physical Layer)。每一层又分为两部分:一部分处理出站(发送)信息,另一部分处理入站(接收)信息。

PCIe 总线架构

事务层

事务层的主要职责是事务层包(TLP)的组装和拆卸。它接收来自 PCIe 设备核心层的数据,并将其封装为 TLP,用于传达事务(如读取和写入),并确定事件类型。事务层还负责管理 TLP 的基于信用的流控制。每个需要响应的数据包都作为拆分事务实现,每个数据包都有唯一标识符,使响应数据包能够定向到正确的始发者。数据包格式支持不同形式的寻址,具体取决于事务类型(内存、I/O、配置和消息)。数据包还可能具有诸如 No Snoop、Relaxed Ordering 和基于 ID 的排序(IDO)等属性。事务层支持四个地址空间:包括三个 PCI 地址空间(内存、I/O 和配置)以及消息空间。消息空间用于支持所有先前 PCI 的边带信号(如中断、电源管理请求等),作为带内消息事务。

数据链路层

数据链路层是事务层和物理层之间的中间阶段,主要职责包括链路管理和数据完整性,涵盖错误检测与纠正。数据链路层的发送方接收事务层组装的 TLP,计算并应用数据保护代码和 TLP 序列号,然后将其提交给物理层以在链路上传输。接收方数据链路层负责检查接收到的 TLP 的完整性,并将其提交给事务层进行进一步处理。在检测到 TLP 错误时,该层负责请求重发 TLP,直到正确接收信息或确定链路失败为止。数据链路层还生成并使用用于链路管理功能的数据包。为了区分这些数据包与事务层(TLP)使用的数据包,将数据链路层生成和使用的数据包称为“数据链路层数据包(DLLP)”。

物理层

PCIe 总线的物理层为 PCIe 设备间的数据通信提供传输介质,为数据传输提供可靠的物理环境。它包括用于接口操作的所有电路,如驱动器、输入缓冲器、并行至串行和串行至并行转换器、PLL 和阻抗匹配电路。物理层还包含与接口初始化和维护相关的逻辑功能。物理层以特定格式与数据链路层交换信息,负责将从数据链路层接收的信息转换为适当的序列化格式,并以与连接到链路另一端的设备兼容的频率和通道宽度在 PCIe 链路上传输。

在 PCIe 总线的物理链路中,一个数据通路(Lane)包含两组差分信号,共 4 根信号线。发送端的 TX 与接收端的 RX 使用一组差分信号连接,该链路也被称为发送端的发送链路和接收端的接收链路;而发送端的 RX 与接收端的 TX 使用另一组差分信号连接,该链路也被称为发送端的接收链路和接收端的发送链路。一个 PCIe 链路可以由多个 Lane 组成,目前支持的 Lane 数量包括 1、2、4、8、12、16 和 32,即 ×1、×2、×4、×8、×12、×16 和 ×32 宽度的 PCIe 链路。每个 Lane 上使用的总线频率与 PCIe 总线版本相关。

2. PCIe 不同版本的性能指标及带宽计算

PCIe indicators

PCIe 版本编码方式总线频率传输速率单 Lane 的峰值带宽 (x1)双 Lane 的峰值带宽 (x2)
1.x8/10b 编码2.5GHz2.5GT/s250MB/s500MB/s
2.x8/10b 编码5GHz5GT/s500MB/s1GB/s
3.0128/130b 编码8GHz8GT/s984.6MB/s1.969GB/s
4.0128/130b 编码16GHz16GT/s1.969GB/s3.938GB/s
5.0128/130b 编码32GHz32GT/s or 25GT/s3.9GB/s or 3.08GT/s7.8GB/s or 6.16GB/s
  • GT/s(Giga Transitions per second):表示每秒传输的次数,描述物理层通信协议的速率属性,与链路宽度无关。
  • Gbps(Giga bits per second):表示每秒传输的比特数。GT/s 与 Gbps 之间没有固定比例关系,需根据线路编码方式计算。

吞吐量 = 传输速率 × 线路编码方案

例如,PCIe 2.0 协议的传输速率为 5.0 GT/s,每条 Lane 每秒传输 5G 个比特。其物理层协议采用 8b/10b 编码方案,因此每条 Lane 的带宽为 5 GT/s × 8/10 = 4 Gbps = 500 MB/s。对于一个 PCIe 2.0 x2 通道,其可用带宽为 4 Gbps × 2 = 8 Gbps = 1 GB/s。

同理,PCIe 3.0 协议的传输速率为 8.0 GT/s,每条 Lane 每秒传输 8G 个比特。其物理层协议采用 128b/130b 编码方案,因此每条 Lane 的带宽为 8 GT/s × 128/130 = 7.877 Gbps = 984.6 MB/s。

3. PCIe 接口信号

电源与地信号

PCIe 设备使用两种电源信号供电:Vcc 和 Vaux,其额定电压均为 3.3V。Vcc 是主电源,为 PCIe 设备的主要逻辑模块供电;Vaux 用于供电与电源管理相关的逻辑。在 PCIe 设备中,一些特殊寄存器(如 Sticky Register)使用 Vaux 供电,即使 Vcc 被移除,这些寄存器的内容和相关逻辑状态也不会改变。使用 Vaux 的主要目的是降低功耗并缩短系统恢复时间,因为 Vaux 在多数情况下不会被移除,设备在 Vcc 恢复后可以快速恢复到正常工作状态。

链路信号

PCIe 链路的最大宽度为 ×32,但在实际应用中,×32 链路宽度极少使用。在一个处理器系统中,通常提供 ×16 的 PCIe 插槽,使用 PETp015、PETn015 和 PERp015、PERn015 共 64 根信号线组成 32 对差分信号,其中 16 对 PETxx 信号用于发送链路,16 对 PERxx 信号用于接收链路。此外,PCIe 总线还使用以下辅助信号:

PERST# 信号(低电平有效)

该信号为全局复位信号,由处理器系统提供,用于复位 PCIe 插槽和 PCIe 设备的内部逻辑。当该信号有效时,PCIe 设备将进行复位操作。PCIe 总线定义了多种复位方式,其中 Cold Reset 和 Warm Reset 的实现与该信号有关。

REFCLK+ 和 REFCLK- 信号

PCIe 总线物理链路间的数据传输采用基于时钟的同步传输机制。PCIe 总线的接收端包含时钟恢复模块(CDR),用于从接收报文中提取接收时钟,从而实现同步数据传输。值得注意的是,PCIe 设备除了从报文中提取时钟外,还使用 REFCLK+ 和 REFCLK- 信号作为本地参考时钟。

在 PCIe 设备配置空间的 Link Control Register 中,包含一个“CommonClockConfiguration”位。当该位为 1 时,表示设备与 PCIe 链路对端设备使用“同相位”的参考时钟;如果为 0,则表示使用的参考时钟是异步的。在使用 PCIe 进行机箱间互联时,异步时钟无需连接时钟线,降低了连接难度。

WAKE# 信号(休眠唤醒信号)

当 PCIe 设备进入休眠状态且主电源已停止供电时,设备使用该信号向处理器系统提交唤醒请求,使处理器系统重新为该设备提供主电源 Vcc。在 PCIe 总线中,WAKE# 信号是可选的,因此使用该信号唤醒 PCIe 设备的机制也是可选的。需要注意的是,产生该信号的硬件逻辑必须使用辅助电源 Vaux 供电。

SMCLK 和 SMDAT 信号

SMCLK 和 SMDAT 信号与 x86 处理器的 SMBus(System Management Bus)相关。SMBus 由 Intel 于 1995 年提出,由 SMCLK 和 SMDAT 信号组成。SMBus 源于 I2C 总线,但与 I2C 总线存在一些差异。SMBus 的最高总线频率为 100 kHz,而 I2C 总线可以支持 400 kHz 和 2 MHz 的总线频率。此外,SMBus 上的从设备具有超时功能,当从设备发现主设备发出的时钟信号保持低电平超过 35 ms 时,将引发从设备的超时复位。在正常情况下,SMBus 的主设备使用的总线频率最低为 10 kHz,以避免从设备在正常使用过程中出现超时。在 SMBus 中,如果主设备需要复位从设备,可以使用这种超时机制。而 I2C 总线只能通过硬件信号实现复位操作,在 I2C 总线中,如果从设备出现错误,单纯通过主设备无法复位从设备。

SMBus 还支持 Alert Response 机制。当从设备产生一个中断时,并不会立即清除该中断,直到主设备向 0b0001100 地址发出命令。

SMBus 和 I2C 总线的区别主要体现在物理层和链路层上,但 SMBus 还包含网络层。SMBus 在网络层定义了 11 种总线协议,用于实现报文传递。SMBus 在 x86 处理器系统中得到了广泛应用,主要用于管理处理器系统的外部设备,并收集外设的运行信息,特别是与智能电源管理相关的信息。PCI 和 PCIe 插槽也为 SMBus 预留了接口,以便于 PCI/PCIe 设备与处理器系统进行交互。

JTAG 信号

JTAG(Joint Test Action Group)是一种国际标准测试协议,与 IEEE 1149.1 兼容,主要用于芯片内部测试。目前,绝大多数器件都支持 JTAG 测试标准。JTAG 信号由 TRST#、TCK、TDI、TDO 和 TMS 信号组成,其中 TRST# 为复位信号,TCK 为时钟信号,TDI 和 TDO 分别对应数据输入和数据输出,TMS 信号用于模式选择。

JTAG 允许多个器件通过 JTAG 接口串联在一起,形成一个 JTAG 链。目前,FPGA 和 CPLD 可以通过 JTAG 接口实现在线编程(ISP,In-System Programming)功能。处理器也可以使用 JTAG 接口进行系统级调试工作,例如设置断点、读取内部寄存器和存储器等操作。此外,JTAG 接口还可用于“逆向工程”,分析产品的实现细节。因此,在正式产品中,一般不保留 JTAG 接口。

二、XDMA

1. XDMA 与其他 PCIe IP 的区别

XDMA 与其他 PCIe IP 的区别

7 Series Integrated Block for PCI Express

7 Series Integrated Block for PCI Express 是最基础的 PCIe IP,实现了 PCIe 的物理层、链路层和事务层,为用户提供以 AXI4-stream 接口定义的 TLP 包。这是三种 IP 中资源占用最少且最灵活的,但开发难度最大。该 IP 将大部分开发工作留给用户。如果用户需要向主机发送数据,必须在逻辑端组好 MEM_WR 事务包并发送到 AXI4-stream 接口;同样,如果需要从主机获取数据,则必须发送 MEM_RD 事务包,并从 COMPLETE 事务包中提取数据。使用该 IP 核时,需要对 PCIe 协议有清晰的理解,尤其是对事务包 TLP 报文格式。

AXI Memory Mapped to PCI Express IP

该 IP 封装了 7 Series Integrated Block for PCI Express IP,并提供了 AXI MM/S 桥。该桥不仅实现了 AXI4 to stream 的功能,还提供了事务层包 TLP 的组装和拆卸、地址转换、错误处理等功能。使用该 IP 时,用户只需通过 AXI4 接口接收和发送 PCIe 数据,无需自行组装和拆卸事务包。从逻辑资源消耗的角度来看,该 IP 居于三种 IP 之间,开发难度也居中。

DMA/Bridge Subsystem for PCI Express (PCIe)(XDMA)

该 IP 不仅完成了事务层的组包解包,还添加了完整的 DMA 引擎。XDMA 虽然简单易用,但也存在局限性,主要表现在以下两个方面:

  1. XDMA 适用于大批量数据传输场景,不适用于小数据场景。
  2. XDMA 仅用于 PCIe 的终端(endpoint)设备,不能用于 Root Port。其他两种 IP 既可以用于终端设备,也可以用于 Root Port。

PCIe 的终端设备(如 PCIe 视频采集卡、显卡等)通常只有 PCIe 金手指,没有 PCIe 插槽;而 Root Port 带有 PCIe 插槽,可以插入 PCIe 终端设备。由于大多数 PCIe 应用开发针对的是 PCIe 终端设备,因此第二点局限性实际上影响较小。

2. XDMA 简介

XDMA 通常使用 AXI4 接口,该接口可以连接到系统总线互联,适用于大数据量异步传输,通常会使用到 BRAM 或 DDR 内存。AXI4-Stream 接口则适用于低延迟数据流传输。在配置页面可以选择 AXI4 或 AXI4-Stream 接口。

XDMA 不同接口的区别

XDMA 接口区别

XDMA 接口区别

XDMA 接口区别

  • AXI-MM 接口:用于高性能、大带宽的数据传输。
  • AXI Lite Master 接口:是 AXI 接口的简化版本,用于少量数据的通信,通常用于配置外设寄存器等轻量级数据传输场景。

DMA Bypass 是普通的 PCIe 传输。如果 DMA 的传输长度较短,其效率与 PCIe to AXI Lite 差不多;但如果传输长度较大,DMA 的性能通常更好。PCIe DMA Bypass 占用的是另一个 BAR,通常由主机直接发起操作,与 DMA 相比会消耗更多主机资源。如果这不是问题,则可以使用。

主机可以通过以下两个接口直接访问用户逻辑:

  1. AXI4-Lite Master 配置接口:该端口是固定的 32 位端口,用于对用户配置和状态寄存器进行非关键性能访问。
  2. AXI Memory Mapped Master CQ 旁路(Bypass)端口:该端口的宽度与 DMA 通道数据路径相同,旨在用于点对点传输等应用程序中可能需要的对用户内存的高带宽访问。

用户逻辑可以通过 AXI4-Lite Slave 配置接口访问 XDMA 内部配置和状态寄存器。在此接口上发起的请求不会转发到 PCI Express。

三、IP 核例化

参考文档:XDMA 文档 ~

BASIC 标签页

BASIC 标签页

  • Functional Mode:功能模式。对于 A7 系列 FPGA 芯片,仅支持 DMA 模式。
  • Mode:配置模式,选择 Basic 配置模式。
  • Device/Port Type:设备/端口类型,仅支持 PCI Express Endpoint device。
  • PCIe Block Location:FPGA 芯片中可选的 PCIe 块位置,默认为 X0Y0。

PCIe 接口选项:

  • Lane Width:链路宽度。对于 PCIe x2,选择 x2。
  • Max Link Speed:最大链路速度,对应 PCIe 版本的传输速率。对于 PCIe 2.*,为 5.0 GT/s。
  • Reference Clock:参考时钟频率,选择 100 MHz。

AXI 接口选项:

  • AXI Address Width:AXI 地址宽度。目前,XDMA 仅支持 64 位宽度。
  • AXI Data Width:AXI 数据宽度。
  • AXI Clock Frequency:AXI 时钟频率。
  • DMA Interface Option:DMA 接口选项,可选择 AXI4 和 AXIS。
  • AXI4-Lite Slave Interface:选择是否启用 AXI4-Lite Slave 接口以访问 DMA 状态寄存器。

PCIe ID 标签页

ID Initial Values:

  • Vendor ID(供应商 ID):用于识别器件或应用的制造商。有效标识由 PCI Special Interest Group 指定,以确保每个标识唯一。默认值 10EEh 为 Xilinx 的供应商标识。

  • Device ID(器件 ID):对应于应用的唯一标识。默认值为 70h,该值取决于所选配置。该字段可采用任何值,需根据应用进行更改。默认器件 ID 参数取决于以下因素:

    • 器件系列:9 表示 UltraScale+,8 表示 UltraScale,7 表示 7 系列器件。
    • EP 或 RP 模式。
    • 链路宽度:1 表示 x1,2 表示 x2,4 表示 x4,8 表示 x8,F 表示 x16。
    • 链路速度:1 表示 Gen1,2 表示 Gen2,3 表示 Gen3,4 表示 Gen4。
  • 如果上述任意值发生更改,则将重新计算“器件 ID”值,以替换先前设置的值。

  • Revision ID(版本 ID):表示器件或应用的版本,作为器件 ID 的扩展。默认值为 00h,需根据应用输入相应值。

  • Subsystem Vendor ID(子系统供应商 ID):进一步限定器件或应用的制造商。在此处输入子系统供应商 ID,默认值为 10EEh。通常,该值与供应商 ID 相同。将该值设为 0000h 可能导致合规性测试出现问题。

  • Subsystem ID(子系统 ID):进一步限定器件或应用的制造商。该值通常与器件 ID 相同,默认值取决于所选通道宽度和链路速度。将该值设为 0000h 可能导致合规性测试出现问题。

  • Enable PCIe-ID Interface:启用 PCIe-ID 接口。如果选中该参数,则根据选中的 PFx 数量,在 IP 顶层边界处会显示 PCIe ID 端口:cfg_vend_idcfg_subsys_vend_idcfg_dev_id_pf*cfg_rev_id_pf*cfg_subsys_id_pf*,并可供用户逻辑驱动。如果未选中该参数,则不会在顶层显示这些端口,并根据自定义时设置的值来驱动这些端口。

Class Code Lookup Assistant:

  • Class Code Look-up Assistant(类代码查找助手):类代码查找助手可针对选定的器件常规功能提供对应的基本类、子类和接口值。
  • Class Code(类代码):类代码用于识别器件的常规功能,分为以下 3 个字节大小的字段:
    • Base Class(基本类):用于广泛识别器件执行的功能类型。
    • Sub-Class(子类):进一步具体识别器件功能。
    • Interface(接口):用于定义特定寄存器级别编程接口(如果有),允许不从属于器件的软件与器件进行连接。

PCIe:BARs 标签页

该标签页主要用于配置 BAR。BAR 是 Base Address Register 的缩写,即 基址寄存器。通过将读取或写入请求映射到基址寄存器(BAR),可以从主机访问 XDMA 内部的配置和状态寄存器以及用户逻辑中的配置和状态寄存器。XDMA 根据 BAR 命中,将请求路由到适当的位置。例如,对于 PCIe to AXI-Lite Master (BAR0) 地址映射,命中 PCIe 到 AXI4-Lite Master 的事务将路由到 AXI4-Lite 内存映射用户接口。该接口支持 32 位地址空间和 32 位读取和写入请求。PCIe to AXI-Lite Master (BAR0) 地址映射可由用户逻辑定义。

BAR 分为两种大小:

  • 32 位 BAR:地址空间最小可达 128 字节,最大可达 2 GB(千兆字节)。用于内存或 I/O。
  • 64 位 BAR:地址空间最小可达 128 字节,最大可达 8 EB(艾字节)。仅用于内存。

BAR 也可分为两种类型(Type)——I/O 和内存:

  • I/O:I/O BAR 只能采用 32 位;“可预取(Prefetchable)”选项不适用于 I/O BAR。仅限针对传统 PCI Express 端点才能启用 I/O BAR。
  • 内存(Memory):内存 BAR 可采用 64 位或 32 位,并且可预取。

与 BAR 寄存器相关的选项含义:

  • Size(大小):可用大小范围取决于所选 PCIe 器件/端口类型和 BAR 类型。
  • Value(值):基于当前选择分配给 BAR 的值。
  • 64bit Enable:是否使用 64 位 BAR。
  • Prefetchable(可预取):识别内存空间预取功能。

每个 BAR 空间可以单独选择 64bit Enable 选项。每个 64 位 BAR 空间都可以选择是否预取。

PCIe:BARs 配置

  • PCIe to AXI Lite Master Interface:选择是否启用 AXI-Lite Master Interface 接口。该接口相当于显卡的用户接口,主机侧可以通过该接口控制显卡的风扇转速、LED 开关和显示效果等功能。因此,如果需要使用 PCIe 接口控制 FPGA 侧的用户逻辑(如控制 LED 灯等),则需要启用该接口。
  • Size 和 Value:定义用户侧空间的大小,与 AXI-Lite Master Interface 对接的 AXI4-Lite 总线设备的空间大小有关,可以根据实际需要自定义大小。Size 大小决定了主机能够访问的地址空间的大小。
  • PCIe to AXI Translation:PCIe 到 AXI 的转换。主机一侧 BAR 地址为 0,用户逻辑侧 AXI Lite 的地址为 0x40000000,则主机访问 AXI Lite 用户逻辑时,XDMA 将根据该设置将主机侧 BAR 地址 0 转换到 AXI Lite 总线地址 0x40000000。对该值的设置有两种方式:一种是手动指定,然后修改后面 AXI Lite 总线的偏移地址;另一种是先确定 AXI Lite 总线的偏移地址,然后根据偏移地址设置该值。例如,修改地址映射 AXI Lite 总线的偏移地址为 0x40000000,因此设置此值为 0x40000000。
  • 此外,PCIe to AXI Translation 也是 AXI Lite 总线的基地址。当 AXI Lite 总线连接多个 AXI IP 核时,会有多个偏移地址。上位机访问其他 IP 核时的偏移地址是以 PCIe to AXI Translation 的值为基址 0 进行参考的。例如,AXI Lite 总线连接的另一个 AXI IP 的偏移地址为 0x40010000,上位机访问该 IP 核的偏移地址就是该 IP 核的偏移地址 0x40010000 - PCIe to AXI Translation 的值 0x40000000 = 0x10000。因此,当 AXI Lite 总线连接多个 AXI IP 核时,需确保 PCIe to AXI Translation 的值小于等于这些 AXI IP 核偏移地址的最小值。
  • PCIe to DMA Interface:PCIe 至 DMA 接口,默认一直开启,支持 prefetchable 和 nonprefetchable(可预取和不可预取)。
  • PCIe to DMA Bypass Interface:选择是否启用 PCIe 至 DMA 旁路接口。DMA Bypass 是普通的 PCIe 传输,不使用 DMA 逻辑,而是直接通过 PCIe 进行通信,其传输效率通常高于 PCIe to AXI Lite。

关于 Prefetchable:传统上,可预取性意味着预先将内存取出到一个小缓冲区中,以便读取操作变得更快。例如,如果有两个 PCIe 总线通过网桥连接,当主总线中的主机必须访问次级总线中的内存时,网桥将从内存中获取数据并将其存储在网桥缓冲区中。然后主机可以定期访问该缓冲区,从而提高读取速度。然而,内存必须是可预取的。如果内存是不可预取的,一旦数据被加载到桥接器的缓冲区,数据将从内存中丢失。如果主机不能从网桥收集数据,那么数据就永远丢失了。如果内存是可预取的,则不存在数据丢失的风险。

PCIe:MISC 标签页

PCIe:MISC 标签页

  • Number of User Interrupt Requests:用户中断请求数,最多可以选择 16 个用户中断请求。
  • Legacy Interrupt Settings:可以选择传统中断之一:INTA、INTB、INTC 或 INTD。
  • MSI Capabilities:默认情况下,启用 MSI 功能,并且启用 1 个向量,最多可以选择 32 个向量。通常,Linux 仅将 1 个向量用于 MSI,可以禁用此选项。
  • MSI-X Capabilities:使能 MSI-X 功能。
  • Finite Completion Credits(高级配置模式下有):在支持有限完成信用的系统上,可以启用此选项以获得更好的性能。
  • Extended Tag Field:扩展标签字段,默认情况下,使用 6 位完成标签。对于 UltraScale 和 Virtex-7 器件,扩展标签选项提供 64 个标签。对于 UltraScale+ 器件,扩展标签选项提供 256 个标签。如果未选择扩展标签选项,则 DMA 将 32 个标签用于所有设备。
  • Configuration Management Interface:是否使用 PCIe 配置管理接口。

关于 MSI-X 中断:用户可以尝试使用 MSI-X 中断,而不是 MSI 或传统中断。使用 MSI-X 中断时,数据速率优于使用 MSI 或基于传统中断的设计。

PCIe:DMA 标签页

  • Number of Read Channels:主机到 PCIe 卡(H2C)的 DMA 读通道数。对于 7 系列 Gen2 IP,最多两个通道(通道数越多,同等情况下传输速率越快)。
  • Number of Write Channels:PCIe 卡到主机(C2H)的 DMA 写通道数。对于 7 系列 Gen2 IP,最多两个通道(通道数越多,同等情况下传输速率越快)。
  • Number of Request IDs for Read channel:读通道的请求 ID 数,即每个通道的最大未完成请求数,可选范围为 2 到 64。
  • Number of Request IDs for Write channel:写通道的请求 ID 数,即每个通道的最大未完成请求数,可选范围为 2 到 32。
  • Descriptor Bypass for Read (H2C):读描述符旁路,适用于所有选定的读通道。每个二进制数字对应一个通道,LSB 对应于通道 0。值为 1 的位表示相应的通道启用了描述符旁路。
  • Descriptor Bypass for Write (C2H):写描述符旁路,适用于所有选定的写通道。每个二进制数字对应一个通道,LSB 对应于通道 0。值为 1 的位表示相应的通道启用了描述符旁路。
  • AXI ID Width:默认值为 4 位宽,也可以选择 2 位宽。
  • DMA Status port:DMA 状态端口可用于所有通道。

基于 XDMA 的 PCIe 子系统

用户逻辑通过 AXI4 接口与 XDMA 进行交互。AXI4 Master 接口用于连接显存,AXI Lite Master 接口用于控制 LED 灯和按键 KEY,按键用于中断。

基于 XDMA 的 PCIe 子系统

基于 XDMA 的 PCIe 子系统

GPIO 的寄存器地址

GPIO 寄存器地址

GPIO 寄存器地址

  • GPIO_DATA:配置各个接口的值。
  • GPIO_TRI:用于动态配置 GPIO 的状态是输入还是输出。“0” 表示配置对应的 GPIO 为输出。

参考手册:AXI GPIO IP 核手册。

中断 usr_irq_req

当使能 AXI GPIO IP 的中断寄存器后(在上位机中设置),按下按键 KEY 时,AXI GPIO IP 核的中断输出引脚 ip2intc_irpt 会输出高电平,从而产生中断。

中断示意图

usr_irq_ack 信号的高电平时间很短,仅为纳秒级,因此无法通过蜂鸣器判断是否响应。usr_irq_ack 信号的典型用途是:当用户设计的中断源产生中断时,可以通过 usr_irq_ack 信号的电平状态来决定何时拉低 usr_irq_req 信号。

PCIe 支持三种中断类型:

  • Legacy 中断:主要用于兼容传统 PCI,现在很少使用,不推荐使用。Legacy 中断设置可以选择 INTA、INTB、INTC 或 INTD;通常选择 None,即不使用 Legacy 中断。
  • MSI(Message Signaled Interrupt)中断:基于消息的中断机制,最多支持 32 个中断请求,且要求中断向量连续。
  • MSI-X 中断:是 MSI 的扩展,可以支持更多的中断请求,且不要求中断向量连续。对于 XDMA,MSI 和 MSI-X 最多支持 16 个可用的用户中断向量。

推荐使用 MSI-X 中断,其数据速率优于 MSI 或传统中断的设计。MSI-X 中断优先级高于 MSI 中断,MSI 中断优先级高于 Legacy 中断。如果同时启用了 Legacy 中断、MSI 和 MSI-X 中断,XDMA 只会产生 MSI-X 中断。

关于 AXI GPIO IP 核的寄存器空间,可参考手册 AXI GPIO IP 核手册。

AXI GPIO IP 核寄存器空间

  • GIER:全局中断使能寄存器,控制 AXI GPIO IP 的总中断开关,偏移地址为 0x11C。
  • IPIER(IP Interrupt Enable):IP 中断使能寄存器,用于控制每个通道的中断使能与否,偏移地址为 0x128。

上位机可以通过读取 IPISR 的值,判断中断来源于哪个通道。


Xilinx DMA 的几种方式与架构

posted @ 2022-06-17 16:09 Hello-FPGA

DMA 是 Direct Memory Access 的缩写。在 FPGA 系统中,常见的几种 DMA 需求如下:

  1. 在 PL 内部无 PS(CPU 这里统一称为 PS)持续干预搬移数据,常见的接口形态为 AXI Stream 与 AXI,以及 AXI 与 AXI。
  2. 从 PL 与 PS 之间搬移数据。对于 ZYNQ,这属于单个芯片内部接口;对于 PCIe 等其他接口,则稍微复杂一些,属于多个芯片之间的接口。

探索 DMA 方式的目的如下:

  1. 了解芯片内部数据搬移的方法,包括 DMA 的常用接口及实现方式。
  2. 了解芯片之间的数据搬移方法,包括 DMA 的常用接口及实现方式。

通过这些了解,可以建立一个系统数据搬移的框架结构。当出现类似需求时,实际上只需要调用已有的模块去实现。

本文从 Xilinx 的各个 DMA IP 着手介绍,主要从接口的角度进行分析。

1 AXI4 TO AXI4

1.1 AXI Central DMA Controller

The AXI CDMA provides high - bandwidth Direct Memory Access (DMA) between a memory - mapped source address and a memory - mapped destination address using the AXI4 protocol. An optional Scatter Gather (SG) feature can be used to offload control and sequencing tasks from the system CPU. Initialization, status, and control registers are accessed through an AXI4 - Lite slave interface, suitable for the Xilinx MicroBlaze™ processor.

“CDMA”这一名称的由来是:它主要用于处理 CPU 挂载的 AXI 接口内存内部的数据传输,典型的场景是 MicroBlaze。为什么不提 ZYNQ 呢?ZYNQ 的 AXI 内部是直接挂载在 CPU 内部的,不需要一个 DMA 去控制。如果需要,直接通过软件进行拷贝,拷贝的动作就会内部发起一次 AXI DMA 操作。但如果 ZYNQ 想要使用 PL 侧的 DDR,那么就需要用 CDMA 来操作,因为 ZYNQ 没有 AXI Master 接口,只有 SLAVE(这里说的是高性能 HP 接口,不是 GP 低速接口)。

img

图 1‑1 AXI CDMA 接口与参数选项,其中 S_AXI_LITE 连接到主控 CPU,M_AXI 连接到存储器,M_AXI_SG 连接到存储器(用于存储 SG DMA 模式下的 dma descriptor)

img

图 1‑2 CMDA 内部结构框图

img

图 1‑3 ZYNQ 框图,只有高性能的 AXI SLAVE,没有 MASTER

我们来看一下内部寄存器,这实际上更加直观,它告诉用户使用该模块的配置方法。从配置方法可以简单估计使用该模块的复杂程度。从寄存器定义来看,控制、状态检测、SG DMA 的 descriptor 指针、简单 DMA 的起始地址、目标地址、长度等,使用起来并不复杂。那么对应的软件代码建议直接拷贝 Xilinx 的 ZYNQ 侧 driver 代码,然后稍作修改即可使用。

img

最后再来看一下资源占用情况。资源占用与位宽成正比。典型的在 64 bit 数据位宽下,占用大约 1500 个 LUT、2500 个 FF。从我的角度来看,这个资源相当不错。

img

图 1‑4 资源占用

img

图 1‑5 传输效率、带宽

2 AXI Stream to AXI4

2.1 AXI DataMover

The AXI Datamover is a key Interconnect Infrastructure IP which enables high - throughput transfer of data between AXI4 memory - mapped domain to AXI4 - Stream domain. The AXI Datamover provides MM2S and S2MM AXI4 - Stream channels which operate independently in a full - duplex - like manner. The AXI Datamover is a key building block for the AXI DMA core and enables 4 kbyte address boundary protection, automatic burst partitioning, as well as providing the ability to queue multiple transfer requests using nearly the full bandwidth capabilities of the AXI4 - Stream protocol. Furthermore, the AXI Datamover provides byte - level data realignment allowing memory reads and writes to any byte offset location.

AXI Datamover 是一个重要的基础 IP,Xilinx 所有的 DMA IP 基本都包含这个模块。该模块可以将 AXI Stream 与 AXI 格式的数据进行转换。类似 XDMA、VDMA、AXI DMA、AXI MCDMA 等几乎所有 DMA IP 均包含该模块。如果 Xilinx 的这些已有 DMA 不能满足需求,那么用户就可以自行设计一个 DMA 控制器完成 DMA 操作。

img

图 2‑1 AXI Datamover 接口与配置项

img

图 2‑2 MM2S 读数据通道,读取 AXI4 接口数据,转换成 AXI Stream 数据输出

img

图 2‑3 S2MM 写数据通道,AXI Stream 数据输入,转换成 AXI4 数据写到 AXI4 接口存储器

img

图 2‑4 资源占用

2.2 AXI DMA Controller

The AXI Direct Memory Access (AXI DMA) IP provides high - bandwidth direct memory access between memory and AXI4 - Stream - type target peripherals. Its optional scatter gather capabilities also offload data movement tasks from the Central Processing Unit (CPU) in processor - based systems. Initialization, status, and management registers are accessed through an AXI4 - Lite slave interface.

简而言之,AXI DMA Controller 为 AXI Stream 和 AXI4 接口的转换(数据存储)提供了一个可由软件控制(通过 AXI Lite 接口实现)的简单方式。

img

图 2‑5 AXI DMA 内部框图

这里需要说明一下:如果选择不使能 SG DMA 模式,而是单纯的寄存器控制模式,对于 AXI DMA 这个 IP 来说,资源占用会减少,性能会降低(Xilinx 其他的 DMA IP 也是类似的)。为什么性能会降低呢?这是因为寄存器模式不支持预先设定传输指令,只能等一次传输结束后开启下一次传输,这就降低了带宽,增加了 CPU 的干预。不过这种模式也最为简单,还是要看设计中的传输要求。

我们来看一下寄存器表格。表格列出了 SG DMA 和寄存器 DMA 两种方式下的寄存器。从表中可以看出,要实际使用 AXI DMA 并不复杂。不过我还是建议直接参考 Xilinx SDK 的驱动代码,裸机驱动、example 即可,简单直接、易用。

img

图 2‑6 表中给出了 SG 模式和寄存器模式下的相关寄存器及其含义

img

图 2‑7 资源占用上来看,还是不少的

img

图 2‑8 延迟、性能、带宽数据,带宽数据还不错,一般来说能做到 80% 是很好的,读比写快,因此 MM2S 的带宽接近 100%,S2MM 只有 75%

2.3 AXI Multichannel DMA

简单来说,AXI MCDMA 是 AXI DMA 的多通道版本,是为了应对多通道、低速的数据传输。AXI MCDMA 最多支持双向各 16 通道,且各个通道间相互独立,允许单独配置,这给很多低速、多功能的应用提供了一个小面积 FPGA 的解决方案。

由于 AXI MCDMA 是 AXI DMA 的多通道版本,因此不做过多介绍。

The AXI MCDMA facilitates large data migration, offloading the task from the embedded processor. It sits as an intermediary between an AXI Memory - Mapped embedded subsystem and an AXI Streaming subsystem. The MCDMA IP is full - duplex, scatter - gather, and supports up to 16 channels. It may be configured as weighted round - robin or strict priority.

img

图 2‑9 AXI MCDMA 结构框图

2.4 AXI Video DMA

The AXI Video Direct Memory Access (AXI VDMA) core is a soft Xilinx IP core that provides high - bandwidth direct memory access between memory and AXI4 - Stream - type video target peripherals. The core provides efficient two - dimensional DMA operations with independent asynchronous read and write channel operation. Initialization, status, interrupt, and management registers are accessed through an AXI4 - Lite slave interface.

为什么有了 AXI DMA 还要有 AXI VDMA 呢?从下面这段话可以看出原因:Xilinx 的视频处理多用 AXI Stream 格式,而实际应用中很多需要改变帧速率、缓存帧的需求。直接使用 AXI DMA 也不是不可以,只是不能很好地与其他 AXI Stream 接口的 Video IP 匹配,因此专门开发了 AXI VDMA。主要是为了缓存图像帧。有朋友可能会问,不能用 BRAM 缓存吗?不能,因为图像对应的每一帧可能很大,用 BRAM 资源不够。

AXI VDMA 的使用方式与其他 DMA IP 大同小异,这里不做更多介绍,需要使用的可以直接查看官方手册。

img

3 PCIe DMA

Xilinx 为 PCIe 接口推出了 AXI DMA 接口,对应为 Xilinx DMA for PCIe。同理,类似 USB、SRIO 等其他接口,用户也可以设计出类似的 DMA 解决方案,构建高可靠、灵活的系统内部架构。

The Xilinx® LogiCORE™ DMA for PCI Express® (PCIe) implements a high - performance, configurable Scatter Gather DMA for use with the PCI Express Integrated Block. The IP provides an optional AXI4 - MM or AXI4 - Stream user interface.

img

图 3‑1 XDMA 接口与参数配置项

img

图 3‑2 XDMA 内部框图

那么 XDMA 可以做什么事情呢?它有很多用途。有了这个模块,你的 PCIe Endpoint 设备就可以构建在 AXI 总线的基础之上,从而拥有一个灵活、可靠、高性能的片上系统架构。

4 启发

用户可以根据 Xilinx DMA 的框图架构,在 AXI、AXI Stream 接口的互联下,构建灵活可靠的 FPGA 系统。如果这些 IP 无法满足要求,还可以模仿 PCIe XDMA 等 IP 的架构方式,搭建属于自己的片上系统架构。


关于 DMA 环通实验的 SDK 部分代码理解

懒羊羊的奶油蛋糕于 2024-04-11 10:22:29 发布

一、定义部分

#include "xaxidma.h"
#include "xparameters.h"
#include "xil_printf.h"
#include "xscugic.h"

#define DMA_DEV_ID XPAR_AXIDMA_0_DEVICE_ID
#define INT_DEVICE_ID XPAR_SCUGIC_SINGLE_DEVICE_ID
#define INTR_ID XPAR_FABRIC_AXI_DMA_0_S2MM_INTROUT_INTR // 中断定义

#define FIFO_DATABYTE 4 // 字节数
#define TEST_COUNT 80
#define MAX_PKT_LEN TEST_COUNT * FIFO_DATABYTE // 发送包长度

#define TEST_START_VALUE 0xC // 开始值

#define NUMBER_OF_TRANSFERS 2 // 传输次数

/*
 * Function declaration // 数据检测函数
 */
int XAxiDma_Setup(u16 DeviceId);
static int CheckData(void);
int SetInterruptInit(XScuGic *InstancePtr, u16 IntrID, XAxiDma *XAxiDmaPtr);

XScuGic INST;
XAxiDma AxiDma;

u8 TxBufferPtr[MAX_PKT_LEN]; // 发送
u8 RxBufferPtr[MAX_PKT_LEN]; // 接收

二、建立中断代码部分

思路:初始化,配置 ID 和地址 → 中断处理函数:将 ID 和中断处理函数连接 → 启动中断 → 中断异常:初始化异常 → 处理异常函数 → 返回成功。

代码解释:

int SetInterruptInit(XScuGic *InstancePtr, u16 IntrID, XAxiDma *XAxiDmaPtr)
{
    XScuGic_Config *Config; // XAxiDma_Config 是一个 AXI_DMA 配置的信息结构体,它里面包含需要配置的各种信息,传递给 API 函数
    int Status;

    Config = XScuGic_LookupConfig(INT_DEVICE_ID); // 初始化,XScuGic_Config 其中的中断函数
    Status = XScuGic_CfgInitialize(&INST, Config, Config->CpuBaseAddress); // 初始化,配置(cfg)地址,scugic:系统级中断控制器
    if (Status != XST_SUCCESS)
        return XST_FAILURE;

    Status = XScuGic_Connect(InstancePtr, IntrID,
                             (Xil_ExceptionHandler)CheckData,
                             XAxiDmaPtr); // 中断设置中断处理函数:将中断 ID 和中断处理函数连接起来,当中断被识别后执行相应的处理函数
    if (Status != XST_SUCCESS) {
        return Status;
    }

    XScuGic_Enable(InstancePtr, IntrID); // 启动中断

    Xil_ExceptionInit(); // 通用 API,用于 ARM 处理器中初始化异常处理程序
    Xil_ExceptionRegisterHandler(XIL_EXCEPTION_ID_INT,
                                 (Xil_ExceptionHandler)XScuGic_InterruptHandler,
                                 InstancePtr); // 为异常情况注册一个处理器,当处理器遇到指定异常时,调用此处理程序。XIL_EXCEPTION_ID_INT 是 Xilinx 处理器定义的
    Xil_ExceptionEnable(); // 使能中断(用于控制处理器是否响应中断)异常

    return XST_SUCCESS;
}

三、DMA 部分(重要)

思路:初始化 DMA → 确认 SG/Simple 模式 → 建立中断系统 → 启动 S2MM 中断 → 赋值 → Cache 刷新 → 开始传输接收。

代码:

int XAxiDma_Setup(u16 DeviceId)
{
    XAxiDma_Config *CfgPtr;
    int Status;
    int Tries = NUMBER_OF_TRANSFERS;
    int Index;
    u8 Value;

    /* Initialize the XAxiDma device. */
    // 初始化 DMA 设备 cfg 配置文件 ptr 指针记录

    CfgPtr = XAxiDma_LookupConfig(DeviceId); // 将 DMA 设备 ID 赋值给 XAxiDma_Config 结构体
    if (!CfgPtr) {
        xil_printf("No config found for %d\r\n", DeviceId);
        return XST_FAILURE;
    }

    /* 初始化 DMA 引擎,将 DMA 设备 ID 赋值给 XAxiDma_Config 结构体
     * 根据 PL 端对 DMA core 的配置参数,PS 对 DMA 进行真正的配置初始化过程,
     * axidma 还存储在 PS 端的 AXI — DMA 配置表,根据对 PL 参数的读取,
     * PS 运行对 PL 侧的 DMA 配置,这个配置过程是通过 GP0 接口对 AXI_Lite4 总线的控制完成的
     */

    Status = XAxiDma_CfgInitialize(&AxiDma, CfgPtr);
    if (Status != XST_SUCCESS) {
        xil_printf("Initialization failed %d\r\n", Status);
        return XST_FAILURE;
    }

    /* SG/Simple mode?,如果是 SG,则配置失败。
     * 配置的是使用 PL 侧 DMA 的直接寄存器访问模式,所以数据传递也是通过该方式运行的,
     * 为了以防万一,在这里运行一下 SG 查询函数看看是不是配置成了 SG 模式
     */
    if (XAxiDma_HasSg(&AxiDma)) {
        xil_printf("Device configured as SG mode \r\n");
        return XST_FAILURE;
    }

    // 建立中断系统
    Status = SetInterruptInit(&INST, INTR_ID, &AxiDma);
    if (Status != XST_SUCCESS)
        return XST_FAILURE;

    /* 使能 DMA 中断,启动 S2MM 中断 */
    XAxiDma_IntrEnable(&AxiDma, XAXIDMA_IRQ_IOC_MASK, XAXIDMA_DEVICE_TO_DMA);
    XAxiDma_IntrDisable(&AxiDma, XAXIDMA_IRQ_ALL_MASK, XAXIDMA_DMA_TO_DEVICE);

    // 对写入数据进行赋值
    Value = TEST_START_VALUE;

    for (Index = 0; Index < MAX_PKT_LEN; Index++) {
        TxBufferPtr[Index] = Value;

        Value = (Value + 1) & 0xFF;
    }

    /* 在 DMA 传输前刷新 SrcBuffer,以防数据缓存。
     * 将要写入 fifo 的数据刷入 Cache
     */
    Xil_DCacheFlushRange((UINTPTR)TxBufferPtr, MAX_PKT_LEN); // 刷新 Data Cache
    Xil_DCacheFlushRange((UINTPTR)RxBufferPtr, MAX_PKT_LEN); // 刷新到 ddr,Cache 关联地址的数据写入到 DDR 中,并把 Cache 里的数据清空,将 CACHE 数据更新到 MEMORY

    for (Index = 0; Index < Tries; Index++) {
        // 开始传输
        Status = XAxiDma_SimpleTransfer(&AxiDma, (UINTPTR)TxBufferPtr,
                                        MAX_PKT_LEN, XAXIDMA_DMA_TO_DEVICE);

        if (Status != XST_SUCCESS) {
            return XST_FAILURE;
        }

        // 开始接收
        Status = XAxiDma_SimpleTransfer(&AxiDma, (UINTPTR)RxBufferPtr,
                                        MAX_PKT_LEN, XAXIDMA_DEVICE_TO_DMA);

        if (Status != XST_SUCCESS) {
            return XST_FAILURE;
        }

        while ((XAxiDma_Busy(&AxiDma, XAXIDMA_DEVICE_TO_DMA)) ||
               (XAxiDma_Busy(&AxiDma, XAXIDMA_DMA_TO_DEVICE))) {
            /* Wait */
        }
    }

    /* Test finishes successfully */
    return XST_SUCCESS;
}

四、数据检测

思路:请求待处理的中断 → 刷新 Cache → 检查数据缓冲区 → 数据验证


XDMA 传输模式

浩瀚之水_csdn 于 2025-03-17 14:45:18 发布

XDMA 传输模式 是 Xilinx(AMD)FPGA 通过 PCIe 与主机(CPU)之间进行数据传输的核心机制,其性能和应用场景因模式不同而有所差异。

以下是 XDMA 支持的 主要传输模式、配置方法及适用场景的详细解析:

1. XDMA 传输模式分类

XDMA 支持以下三种主要传输模式:

传输模式特点适用场景
块传输模式(Block DMA)基于 AXI4 Memory-Mapped(AXI-MM) 接口,传输连续大块数据。大文件传输、图像帧处理、批量计算
流传输模式(Streaming DMA)基于 AXI4-Stream(AXI-S) 接口,实时流式传输,无固定地址。传感器数据流、视频流、网络包处理
Scatter-Gather(SG)模式支持 非连续内存传输,通过描述符链表管理多块数据。分散数据聚合、数据库操作

2. 块传输模式(Block DMA)

2.1 工作原理

  • AXI-MM 接口:通过 FPGA 的 DDR 或 BRAM 缓存数据,主机与 FPGA 通过物理地址直接读写。
  • 单次传输:每个 DMA 请求传输一块连续的物理内存数据。
  • 方向控制
    • 主机到 FPGA(H2C):主机写入 FPGA 内存。
    • FPGA 到主机(C2H):FPGA 读取主机内存。

2.2 配置步骤(Vivado)

  1. 在 XDMA IP 核配置中启用 AXI-MM 接口
  2. 设置 AXI-MM 数据位宽(通常 512-bit 以匹配 PCIe 带宽)。
  3. 定义 BAR(Base Address Register)空间,供主机映射 FPGA 内存。

2.3 示例代码(主机端)

// Linux 用户层代码(H2C 传输)
int fd = open("/dev/xdma0_h2c_0", O_RDWR);
void *host_buf = malloc(4096);
void *fpga_buf = mmap(NULL, 4096, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);

// 主机填充数据
memcpy(host_buf, data, 4096);
// 启动 DMA 传输(主机到 FPGA)
write(fd, host_buf, 4096);

2.4 性能优化

  • 对齐内存:DMA 缓冲区按 4KB 对齐(避免分页开销)。
  • 批量传输:单次传输尽量接近 PCIe 最大负载(如 256 Bytes/TLP)。

3. 流传输模式(Streaming DMA)

3.1 工作原理

  • AXI-S 接口:数据按流式传输,无固定地址,适用于实时性要求高的场景。
  • 双工传输
    • H2C 流:主机发送数据到 FPGA 的 AXI-S 接口。
    • C2H 流:FPGA 发送数据到主机的内存。

3.2 配置步骤(Vivado)

  1. 在 XDMA IP 核中启用 AXI4-Stream 接口
  2. 配置流接口位宽(如 64-bit)和 FIFO 深度(防止数据溢出)。

3.3 FPGA 逻辑设计

// 示例:FPGA 接收 AXI-S 流数据
axis_slave #(.DATA_WIDTH(64)) stream_in ();
xdma_core xdma_inst (
    .s_axis_c2h_tdata (stream_in.tdata),
    .s_axis_c2h_tkeep (stream_in.tkeep),
    .s_axis_c2h_tlast (stream_in.tlast),
    .s_axis_c2h_tvalid(stream_in.tvalid),
    .s_axis_c2h_tready(stream_in.tready)
);

3.4 适用场景

  • 实时视频处理:将摄像头数据流直接传输到 FPGA 进行编解码。
  • 高频交易:FPGA 处理网络包并实时回传结果。

4. Scatter-Gather(SG)模式

4.1 工作原理

  • 描述符链表:主机或 FPGA 构建链表,定义多个非连续数据块的传输任务。
  • 自动执行:XDMA 引擎按链表顺序自动完成传输,减少 CPU 干预。

4.2 配置流程

  1. 硬件启用 SG 模式:在 Vivado 中勾选 XDMA IP 的 Enable Scatter-Gather

  2. 驱动支持:Linux 需使用 dmaengine 框架,Windows 需配置 SG 描述符寄存器。

  3. 构建描述符链表

    struct sg_desc {
        uint64_t src_addr;
        uint64_t dst_addr;
        uint32_t length;
        uint32_t flags; // 方向(H2C/C2H)、中断使能等
        uint64_t next_desc; // 下一个描述符物理地址
    };
    

4.3 性能优势

  • 减少拷贝次数:直接传输分散数据,无需合并到连续缓冲区。
  • 高吞吐量:通过链表预取和并行传输最大化 PCIe 带宽利用率。

5. 传输模式选择建议

场景推荐模式理由
大数据块传输(>1MB)块传输模式连续地址传输效率高,适合 DDR 缓存。
实时流数据(如音频)流传输模式低延迟,无地址管理开销。
非连续数据(如网络包)SG 模式自动聚合分散数据,减少 CPU 负担。
混合负载块 + 流混合模式同时处理批量计算和实时流(需 FPGA 逻辑支持多接口)。

6. 常见问题与调试

6.1 传输失败

  • 原因:PCIe 链路不稳定、地址越界、驱动未正确加载。
  • 调试
    • 使用 lspci -vvv 检查 PCIe 设备状态。
    • 通过 XDMA 的状态寄存器查看错误码。

6.2 性能瓶颈

  • PCIe 带宽:计算理论带宽(如 PCIe Gen3 x8 = 8 GB/s),对比实际传输速率。
  • FPGA 逻辑瓶颈:检查 AXI 接口是否满带宽运行(使用 Vivado 逻辑分析仪)。

6.3 内存对齐错误

  • 症状:传输速度远低于预期。
  • 解决:使用 posix_memaligncudaMallocHost(GPU 缓冲区)确保内存对齐。

7. 进阶应用

7.1 多通道并行传输

  • 配置多个 DMA 通道:在 Vivado 中实例化多个 XDMA 通道,独立处理不同任务。
  • 负载均衡:将数据按优先级分配到不同通道(如视频流和控制信号分离)。

7.2 与 RDMA 结合

  • RoCE over FPGA:在 FPGA 中实现 RDMA 协议,绕过主机 CPU,直接与其他节点通信。

7.3 动态重配置

  • Partial Reconfiguration:在不重启 FPGA 的情况下切换传输模式,适应动态负载。

8. 总结

XDMA 的传输模式选择直接影响系统性能与实时性:

  • 块传输适合大数据块,流传输适合低延迟实时数据,SG 模式解决非连续内存问题。
  • 合理配置 PCIe 参数、内存对齐和多通道并发是优化关键。
  • 结合 FPGA 逻辑设计和主机端驱动调优,可充分发挥 PCIe 带宽潜力,满足 AI、金融、通信等领域的高性能需求。

Xilinx XDMA 说明和测试 - MM

ཌ斌赋ད 已于 2023-10-08 17:36:04 修改

1. 测试工程

使用 Vivado 创建的 XDMA 测试工程如下图所示。XDMA IP 的设置如下,其他保持默认。XDMA 的 AXI、AXI Lite 和 AXI Bypass 都连接到 BRAM,每个 BRAM 的地址设置如下图。该工程可以从 Github 下载,使用的 FPGA 板卡为浪潮的 F37X 加速器。运行工程目录下的 run.sh 执行 run.tcl 即可完成工程的创建和编译。注意:AXI Lite 接口需要连接,如果不连接,系统重启时 XDMA 的驱动可能无法通过 AXI Lite 接口读取配置信息,从而导致重启失败。

  • PCIe Block Location:PCIE4C X1Y0
  • Lane Width:X16
  • Maximum Link Speed:8.0 GT/s
  • Reference Clock Frequency:100 MHz
  • AXI Data Width:512 bit
  • AXI Clock Frequency:250 MHz
  • Vendor ID:10EE
  • Device ID:903F
  • 勾选
    • PCIe to AXI Lite Master Interface 和 64bit Enable
    • PCIe to DMA Interface 和 64bit Enable
    • PCIe to DMA Bypass Interface 和 64bit Enable

测试工程配置

测试工程配置

2. 驱动安装

本节主要介绍 XDMA 驱动的源文件、编译和安装过程。

2.1 源文件说明

  1. 下载地址:DMA 驱动下载
  2. 文件说明
文件说明
include编译依赖文件
tests测试文件
load_driver:加载驱动
run_test.sh:执行 DMA 测试
dma_memory_mapped_test:MM 测试调用的脚本
dma_streaming_test.sh:Stream 测试调用的脚本
data:测试数据
tools工具文件
reg_rw.c:AXI Lite 和 AXI Bypass 通道读写寄存器工具
dma_to_device.c:AXI 通道写寄存器工具
dma_from_device.c:AXI 通道读寄存器工具
xdma驱动源文件
xdma_mod.c:添加新设备的 Vendor ID 和 Device ID
readme.txt说明文件
RELEASE版本说明
COPYING权限
LICENSE证书

2.2 驱动编译安装

  1. 驱动编译:在 xdma 目录中执行 sudo make install 命令编译驱动,生成 xdma.ko 文件。
  2. 编译工具:在 tools 目录中执行 make 命令编译工程,生成 reg_rwdma_to_devicedma_from_device 可执行文件。
  3. 驱动加载:在 tests 目录中执行 sudo ./load_driver.sh 加载驱动。
  4. 增加设备 ID:如果需要增加设备 ID,需要修改 xdma/xdma_mod.c 文件,在“pci_device_id”结构体中增加 PCI_DEVICE 的 Vendor ID 和 Device ID。

3. 测试说明

本节介绍 XDMA 的测试结果。

3.1 设备管理

  1. 下载 FPGA 程序:重新启动系统后,执行 sudo ./load_driver.sh 加载驱动。显示如下信息表示驱动加载和设备识别成功。执行 rmmod xdma 可卸载驱动。

设备管理

  1. 查看驱动加载状态:执行 lsmod | grep xdma 查看 xdma 驱动是否成功加载。

查看驱动加载状态

  1. 查看 xdma 设备:执行 ls /dev/xdma* 查看 xdma 设备是否存在。xdma0_h2c_0 是 AXI 主机到卡的通道,xdma0_c2h_0 是 AXI 卡到主机的通道。由于 XDMA IP 中只启用了一个 H2C 和 C2H 通道,因此这里只有一个通道。xdma0_user 是 AXI Lite 通道,xdma0_bypass 是 AXI Bypass 通道,xdma0_control 是 PCIe 配置空间的读写通道,xdma0_event_* 是 16 个用户中断(在 IP 核配置时启用才有用)。

查看 xdma 设备

  1. 多设备冲突:当主机系统中有两块加速卡,都下载了 XDMA 工程时,执行 ls /dev/xdma* 会列出多个设备。直接使用脚本 run_test.sh 进行测试会导致失败。

多设备冲突

  1. 查看设备信息:使用命令 lspci -vd 1bd4: 可查看设备的信息,其中 1bd4 是 XDMA IP 设置时设置的 Vendor ID。“Memory at e0000000…” 表示 BAR0 上 AXI Lite 通道的存储空间,可以在 IP 设置时进行修改。“Memory at f0000000…” 表示 BAR2 上 AXI 通道的存储空间。不同设置下各通道占据的 BAR 空间位置如下图所示。

查看设备信息

查看设备信息

3.2 数据读写

  1. reg_rw 工具:执行 reg_rw -h 可查看使用说明
reg_rw <device> <address> <type> <data>
--<device>: ls /dev/xdma* 中的设备
--<address>: 寄存器地址
--<type>: 数据类型:b (byte)-8 字节;h (halfword)-16 字节;w (word)-32 字节
--<data>: 写入数据,如果没有这一项,则表示读取数据

1.1 写入数据

写入数据

写入数据

1.2 读出数据

读出数据

读出数据

  1. dma_to_device:执行 ./dma_to_device -h 可查看使用说明
dma-to-device [OPTIONS]
-d:ls /dev/xdma* 中的设备
-f:发送给卡的数据文件
-w:从卡中读数据写入的文件
-s:发送数据的字节数
-a:寄存器地址
-c:传输次数
-o:offset
-k:aperture

dma_to_device

dma_to_device

  1. dma_from_device:执行 ./dma_from_device -h 可查看使用说明
dma-from-device [OPTIONS]
-d:ls /dev/xdma* 中的设备
-f:发送给卡的数据文件
-w:从卡中读数据写入的文件
-s:发送数据的字节数
-a:寄存器地址
-c:传输次数
-o:offset
-k:aperture

dma_from_device

dma_from_device

3.3 测试结果

执行 tests 文件夹下的脚本 run_test.sh 完成对 XDMA 的测试。整个测试流程包括查询使能通道、确定接口方式(MM 或 ST),根据接口方式调用脚本 dma_memory_mapped_test.shdma_streaming_testdma_memory_mapped_test.shdma_streaming_test 脚本首先使用 dma_to_device 命令发送测试数据到 BRAM,然后使用 dma_from_device 命令从 BRAM 中取出数据,最后对比两个文件的数据是否一致。测试结果如下图所示。

测试结果


XDMA 使用小结

mu_guang_ 于 2020-10-07 17:28:23 发布

1. XDMA IP 核的功能

完成 PC 和 FPGA 通过 PCIe 接口的通信,主要进行数据传输。数据读写分为两种:一种是数据的读写,另一种是配置数据的读写。在数据读写部分,DMA 通过 MIG 控制 DDR 完成数据读写。配置数据读写通过与 BRAM 通过 AXI-Lite 总线连接完成。XDMA 将 PCIe 配置信息存储在 BRAM 中,在进行配置信息读写时,将主机地址映射到用户逻辑的地址,然后与偏移地址处理(物理地址 = 段地址 << 4 + 偏移地址)。因此,在 BRAM 设置时,需要将其偏移地址设置为与主机地址映射的偏移地址相同。

2. AXI 总线传输模式

  • AXI4.0-lite:AXI4.0-full 的简化版,用于简单、低吞吐量的内存映射通信。由于没有突发传输相关的信号线,因此不能进行突发传输,每次传输只能传输一个数据(数据宽度取决于带宽)。例如,对于 32 位宽度的总线,一次可以传输 4 个字节。
  • AXI4.0-full:用于高性能内存映射需求。包含突发控制信号,可以进行突发传输。在指定一次地址后,可以一次传输多达 256 个数据(数据宽度取决于带宽)。
  • AXI_stream:用于高速流数据传输。由于没有地址总线,因此用于数据流传送,允许无限制的数据突发传输规模。

应用场景

  • AXI4.0-lite:主要用于内核和外设寄存器之间的通信。
  • AXI4.0-full:主要用于向 DDR 或 OCM 中写入大量数据。
  • AXI_stream:主要用于向 FIFO 等没有地址的数据缓冲区传送大量数据。

3. IP 核配置

3.1 Basic

  • Functional Mode:功能模式,即 DMA 模式。
  • Mode:模式,选择 Basic。Basic 与 Advanced 的区别在于 Advanced 模式开放更多的可选选项与功能,而 Basic 是默认模式。
  • Device/Port Type:选择设备与端口类型,为端点设备。
  • PCIe Block Location:从可用的集成块中选择,以启用生成特定位置的约束文件和输出。通常使用 X0Y0。

Basic 配置

  • Lane Width:通道宽度,根据接口进行选择。
  • AXI Address Width:AXI 地址宽度选择,仅支持 64 bit(参考用户手册第 73 页)。
  • AXI Data Width:AXI 总线上传输的数据宽度,可以是 64 bit、128 bit、256 bit、512 bit(此选项仅适用于 UltraScale+,具体配置需参考 datasheet 第 249 页)。

AXI 数据宽度配置

  • DMA Interface Option:DMA 的接口类型用于数据传输,有两种选择:AXI MM(memory mapped)与 AXI ST(stream)。一般情况下,AXI MM 用于与 DDR 之间的通信,AXI stream 用于 FIFO。

DMA 接口类型配置

3.2 PCIe BARs

  • PCIe to AXI Lite Master Interface:使能后,可以在主机一侧通过 PCIe 访问用户逻辑侧寄存器或其他 AXI-Lite 总线设备。配置信息存储在 BRAM 中,通过 AXI-Lite 总线读写 BRAM。
    • BAR 类型:32 bit,不使能 64 bit,不使能 prefetchable。
    • 映射空间:选择 1M,大小可根据实际需求自定义。
    • PCIe to AXI Translation:主机侧 BAR 地址与用户逻辑侧地址不同,通过设置转换地址实现 BAR 地址到 AXI 地址的转换。例如,主机侧 BAR 地址为 0,则主机访问 BAR 地址 0 转换到 AXI-Lite 总线地址为 0x80000000。
  • PCIe to DMA Interface:数据传输宽度为 64 bit,DMA 控制器通常只支持数据 8 字节对齐的情况。当数据从上位机通过 PCIe 接口发送到端点设备时,XDMA 内部自行解包并对数据与指令进行分析,得到读写操作的指令地址,并对 DDR 进行读写操作。操作结果通过 AXI 接口返回 XDMA,XDMA 对数据进行组包后通过物理层发出,实现数据的 DMA 控制。

PCIe BARs 配置

4. XDMA 连接

主要连接部分为 M_AXI 和 M_AXI_LITE,通常连接到 AXI Interconnect。M_AXI 一般与 DDR 或 FIFO 相连,用于数据通信;M_AXI_LITE 用于配置信息传输,访问用户逻辑侧寄存器。

XDMA 连接

5. 使用原理解析

5.1 PC 写数据
  1. PC 通过调用函数写数据,更新 dsTail,并写入对应的寄存器中。
  2. writeReg 相当于一条指令 down_tail+1,XDMA 会进行地址映射。在 PC 上对寄存器进行操作时,会通过 AXI-Lite 总线传输,从而对目标地址上的数据进行加 1 操作。
  3. 控制模块 queue 对 AXI-Lite 数据进行解析,对 down_tail 进行 +1 操作。
  4. 计算 down_cnt,从而控制 AXI 从 DDR 上对数据进行读取。
  5. AXI 从 DDR 上读取数据后,down_head+1,然后反馈给控制模块 queue,计算 down_cnt
void updateDsTail (int pkgCnt)
{
    for (; pkgCnt > 0; pkgCnt--)
    {
        dsTail += DS_PSIZE;
        if (dsTail >= DS_ADDR_LIMIT)
            dsTail = DS_BASE_ADDR;
    }
    writeReg (DS_TAIL_REG, dsTail);
}

PC 写数据流程

5.2 PC 读数据
  1. PC 读取数据,对 up_head 寄存器进行 +1 操作。
  2. 通过 XDMA 对相关寄存器地址进行映射,并通过 AXI-Lite 总线对相应的地址写入数据。
  3. 控制模块 queue 通过 up_cnt = tail - head 计算 up_cnt
  4. up_cnt 用于控制 FPGA 将数据写入 DDR,up_cnt 也是一个满标志。

20 基于 XDMA 实现 PCIe 通信方案

posted @ 2023-12-27 20:13 米联客(milianke)

软件版本

  • Vivado 2021.1
  • 操作系统:WIN10 64bit

硬件平台

  • 适用 XILINX A7/K7/Z7/ZU/KU 系列 FPGA

1. 概述

本方案作为通用教程,适用于 XILINX 各类支持 PCIe 通信的板卡。米联客在 XDMA 中使用了自行编写的 FDMA 控制 IP,可以简单方便地完成数据交换。

2. 系统构架

本系统的关键在于编写了一个 uixdmairq IP,用于配合驱动处理中断。uixdmairq 提供了 AXI-Lite 接口,上位机通过访问用户空间地址读写 uixdmairq 的寄存器。该 IP 在 user_irq_req_i 输入的中断位上寄存中断位号,并输出给 XDMA IP。当上位机的驱动响应中断时,在中断中写入 uixdmairq 的寄存器,清除已处理的中断。

此外,本方案通过 AXI-BRAM 演示用户空间的读写访问测试。

系统构架

3. XDMA 概述

Xilinx 提供的 DMA Subsystem for PCI Express IP 是一个高性能、可配置的适用于 PCIe 2.0 和 PCIe 3.0 的 SG 模式 DMA,提供用户可选择的 AXI4 接口或 AXI4-Stream 接口。一般情况下,配置为 AXI4 接口可以加入系统总线互联,适用于大数据量异步传输,通常会使用到 DDR。AXI4-Stream 接口适用于低延迟数据流传输。

XDMA 是 SGDMA(Scatter-Gather DMA),并非 Block DMA。在 SG 模式下,主机会将要传输的数据组成链表的形式,然后将链表首地址通过 BAR 传送给 XDMA。XDMA 会根据链表结构首地址依次完成链表所指定的传输任务。

XDMA 工作原理

  • AXI4、AXI4-Stream:必须选择一个,用于数据传输。
  • AXI4-Lite Master:可选,用于实现 PCIe BAR 地址到 AXI4-lite 寄存器地址的映射,可以用于读写用户逻辑寄存器。
  • AXI4-Lite Slave:可选,用于将 XDMA 内部寄存器开放给用户逻辑,用户逻辑可以通过此接口访问 XDMA 内部寄存器,不会映射到 BAR。
  • AXI4 Bypass:可选,用于实现 PCIe 直通用户逻辑访问,适用于低延迟数据传输。

4. 基于 XDMA 的 PCIe FPGA 工程搭建

4.1 XDMA IP 配置

1. 添加 XDMA IP 核

添加 XDMA IP 核

添加 XDMA IP 核

2. 配置 XDMA IP

双击 XDMA IP 进行配置:

  • Mode:配置模式,选择 BASE 配置。
  • Lane Width:选择 PCIe 的通道数量。对于 MA703FA 为 2 个通道。每个开发板支持的通道数量不同,通道数量越多,通信速度越快。用户需要根据硬件的实际通道数量选择正确的通道数。
  • Max Link Speed:选择 5.0 GT/s,即 PCIe 2.0。
  • Reference Clock:100 MHz,参考时钟为 100 MHz。
  • DMA Interface Option:接口选择 AXI4 接口。
  • AXI Data Width:128 bit,即 AXI4 数据总线宽度为 128 bit。
  • AXI Clock:125 MHz,即 AXI4 接口时钟为 125 MHz。
  • DMA Interface Option:设置为 AXI Memory Mapped 方式。

XDMA IP 配置

  • PCIE ID 配置:选择默认配置即可。默认设备类型为 Simple communication controllers。

PCIE ID 配置

  • PCIE BAR 配置:此部分配置较为重要。首先使能 PCIe to AXI Lite Master Interface,这样可以在主机一侧通过 PCIe 访问用户逻辑侧寄存器或其他 AXI4-Lite 总线设备。映射空间选择 1M,用户也可以根据实际需求自定义大小。
    • PCIe to AXI Translation:此设置较为重要。通常情况下,主机侧 PCIe BAR 地址与用户逻辑侧地址不同,此设置用于进行 BAR 地址到 AXI 地址的转换。例如,主机侧 BAR 地址为 0,IP 内部转换设置为 0x44A00000,则主机访问 BAR 地址 0 转换到 AXI Lite 总线地址为 0x44A00000。
    • PCIe to DMA Interface:选择 64 bit 使能。
    • DMA Bypass:暂时不使用。

PCIE BAR 配置

  • PCIE 中断设置
    • User Interrupts:用户中断。XDMA 提供 16 条中断线给用户逻辑,此处可以配置使用几条中断线。
    • Legacy Interrupt:XDMA 支持 Legacy 中断,此处不选。
    • MSI Capabilities:选择支持 MSI 中断,支持 4 个中断消息向量。

注意:MSI 中断和 MSI-X 中断只能选择一个,否则会报错。如果选择了 MSI 中断,则可以选择 Legacy 中断;如果选择了 MSI-X 中断,则 MSI 必须取消选择,同时 Legacy 也必须选择 None。此 IP 对于 7 系列设置有此问题,如果使用 Ultrascale 系列,则可以全部选择。

  • MSI-X Capabilities:不选。
  • Miscellaneous:选 Extended Tag Field。
  • Link Status Register:选 Enable Slot Clock Configuration。

PCIE 中断设置

  • 配置 DMA 相关内容
    • Number of DMA Read Channel(H2C)和 Number of DMA Write Channel(C2H)通道数:对于 PCIe 2.0,最大只能选择 2。XDMA 可以提供最多两个独立的写通道和两个独立的读通道。独立通道在实际应用中具有重要意义,在带宽允许的前提下,一个 PCIe 可以实现多种不同的传输功能,并且互不影响。此处选择 1。
    • Number of Request IDs for Read(Write)channel:每个通道设置允许的最大 outstanding 数量,按照默认值即可。

DMA 相关配置

4.2 完成自动连线

配置完成后,点击 Run Block Automation,可以看到之前的配置信息。如果发现与目标配置不一致,需要手动修改。点击 OK,完成配置。

自动连线

自动连线

配置完成后,Vivado 会自动进行必要的连线。

自动连线完成

到此为止,XDMA IP 配置完成。为了让 XDMA 和上位机密切配合工作,还需要继续搭建其他部分的功能模块。

4.3 基于图形设计的 XDMA 工程

基于图形设计的 XDMA 工程

4.4 添加中断测试代码

`timescale 1ns / 1ps

/*******************************MILIANKE*******************************
* Company : MiLianKe Electronic Technology Co., Ltd.
* WebSite: https://www.milianke.com
* TechWeb: https://www.uisrc.com
* tml-shop: https://milianke.tmall.com
* jd-shop: https://milianke.jd.com
* taobao-shop1: https://milianke.taobao.com
* Create Date: 2022/05/01
* File Name: pcie_top.v
* Description:
* Declaration:
* The reference demo provided by Milianke is only used for learning.
* We cannot ensure that the demo itself is free of bugs, so users
* should be responsible for the technical problems and consequences
* caused by the use of their own products.
* Copyright: Copyright (c) MiLianKe
* All rights reserved.
* Revision: 1.0
* Signal description
* 1) _i input
* 2) _o output
* 3) _n active low
* 4) _dg debug signal
* 5) _r delay or register
* 6) _s state machine
*********************************************************************/
module pcie_top
(
    // PCIe 串行数据端口
    input [1:0] pcie_mgt_rxn,
    input [1:0] pcie_mgt_rxp,
    output [1:0] pcie_mgt_txn,
    output [1:0] pcie_mgt_txp,
    // PCIe 参考时钟
    input [0:0] pcie_ref_clk_n,
    input [0:0] pcie_ref_clk_p,
    input pcie_rst_n
);

wire axi_aclk;
wire user_irq_en_o;

// 内部计数器产生一个延迟复位
reg [21:0] timer_cnt;
reg timer_r1, timer_r2;
reg [1:0] int_p;
reg [3:0] user_irq_req_i;
wire inter = !timer_r2 && timer_r1;

always @(posedge axi_aclk) begin
    if (!axi_aresetn || !user_irq_en_o) begin
        timer_cnt <= 22'd0;
    end else begin
        timer_cnt <= timer_cnt + 1'b1;
    end
end

always @(posedge axi_aclk) begin
    if (!axi_aresetn || !user_irq_en_o) begin
        timer_r1 <= 1'd0;
        timer_r2 <= 1'd0;
    end else begin
        timer_r1 <= timer_cnt [20];
        timer_r2 <= timer_r1;
    end
end

// 产生用户中断
always @(posedge axi_aclk) begin
    if (!axi_aresetn || !user_irq_en_o) begin
        int_p [1:0] <= 4'd0;
        user_irq_req_i <= 4'd0;
    end else begin
        if (inter) int_p <= int_p + 1'b1;
        user_irq_req_i <= 4'd0;
        user_irq_req_i [int_p] <= 1'b1;
    end
end

// 接口例化
pcie_system pcie_system_i
(
    .pcie_mgt_rxn (pcie_mgt_rxn),
    .pcie_mgt_rxp (pcie_mgt_rxp),
    .pcie_mgt_txn (pcie_mgt_txn),
    .pcie_mgt_txp (pcie_mgt_txp),
    .pcie_ref_clk_n (pcie_ref_clk_n),
    .pcie_ref_clk_p (pcie_ref_clk_p),
    .pcie_rst_n (pcie_rst_n),
    .axi_aclk (axi_aclk),
    .user_irq_en_o (user_irq_en_o),
    .user_irq_req_i (user_irq_req_i)
);
endmodule

4.5 地址分配

进行地址分配:

  • 将挂在 M_AXI 上的 DDR(对于 MA703-35T 挂 BRAM)地址分配从 0 开始(对于 Windows 系统必须为 0)。M_AXI 用于进行 DMA 操作。
  • M_AXI_LITE 挂载的 BRAM 和 uixdmairq 中断控制单元映射到用户 BAR 地址空间,该地址是前面 XDMA IP 中设置的地址。默认情况下,需要设置 uixdmairq 中断控制单元的地址与 XDMA 中设置的用户 BAR 地址空间一致,例如 0x44A00000。BRAM 的地址空间为 0x44A010000。

关于地址空间的具体含义,结合软件的使用会更加清晰。初学者暂且根据教程设置。

地址分配

5. 硬件安装

先下载程序,调试阶段下载 bit 文件,然后再开电脑。这样才能正确识别并确保后续测试工作正常开展。

硬件安装

6. 硬件识别

硬件识别

7. 应用程序测试

7.1 xdma_rw.exe 功能介绍

打开一个终端(如果双击运行会很快退出),进入到上一节编译生成的应用程序目录,找到 xdma_rw.exe。该应用程序用于操作所有 PCIe 设备。在终端中仅输入 xdma_rw.exe,可以查看程序的使用说明。

xdma_rw.exe 使用说明

参数说明
  • DEVNODE

    • control:控制通道,用于控制 XDMA 的寄存器。由于精力原因,米联客对控制通道对 XDMA 寄存器的设置没有深入研究。
    • event_*:中断事件,其中 * 表示中断号。
    • user:用户空间,数据通过 AXI4-LITE 接口传输。
    • h2c_*:主机到卡(Host to Card),PC 发送 DMA 数据到板卡,其中 * 表示通道号,通常使用通道 0。数据通过 AXI4-FULL 通道传输。
    • c2h_*:卡到主机(Card to Host),板卡发送数据到 PC,其中 * 表示通道号,通常使用通道 0。数据通过 AXI4-FULL 通道传输。
  • ADDR:读写地址偏移。

    • 对于 DMA 通道,地址从 0 开始。
    • 对于 PS DDR 内存,必须偏移至少 20 MB 开始读写 PS DDR。
    • 对于 user 的读写,偏移地址是 AXI-LITE 接口的 IP 地址,减去在 XDMA IP 中配置的 PCIe to AXI Translation 地址。对于米联客的 XDMA 方案,由于修改了驱动中对于中断的响应,因此 PCIe to AXI Translation 必须和默认的 uixdmairq 地址一致。之后再分区其他 AXI-LITE 接口外设。

ADDR 参数说明

  • OPTION

    • a:设置内存对齐。
    • b:打开一个二进制文件。
    • f:读取或写入文件。
    • l:数据长度。
    • v:更详细的输出。
  • DATA:十进制或十六进制数,必须用空格间隔。例如:

    • 17 34 51 68
    • 0x11 0x22 0x33 0x44
DMA 批量数据测试

DMA 传输是使用最频繁的一种方式,需要用到 h2c 或 c2h 通道。

当前目录下有一个 datafile4k.bin 文件,可以测试将该文件传输到 FPGA 的 DDR(或 MA703FA-35T 的 BRAM),然后读取出来。

在终端输入以下指令:

xdma_rw.exe h2c_0 write 0x0000000 -b -f datafile4K.bin -l 4096

该指令的含义是:使用 h2c_0 设备,以二进制形式读取文件 datafile4k.bin,将其写入到 BRAM 内存地址 0x0000000,长度为 4096 字节。

DMA 写入

使用以下命令读取数据:

xdma_rw.exe c2h_0 read 0x0000000 -b -f datafile4K_recv.bin -l 4096

DMA 读取

接下来可以使用 WinHex 等软件检查两个文件的数据是否一致。如果一致,则说明传输功能正常。

数据一致性检查

7.2 user 通道测试

通过 AXI-LITE 接口写入 2 个数据到挂在 AXI-LITE 接口的 BRAM 中:

xdma_rw.exe user write 0x10000 0x11 0x22

通过 AXI-LITE 接口读取 2 个数据从挂在 AXI-LITE 接口的 BRAM 中:

xdma_rw.exe user read 0x10000 -l 2

7.3 event 中断测试

1. XDMA 中断 FPGA 部分代码

首先需要理解 XDMA 的中断类型及控制时序:

  1. Legacy Interrupts

    • 对于 Legacy Interrupts,当中断请求 user_irq_ack 第一次为 1 时,usr_irq_req 可以清 0。当中断请求 user_irq_ack 第二次为 1 时,可以重新设置 usr_irq_req 发起中断。
    • 在 PCI 总线中,INTx 中断由四条可选的中断线决定。这种中断方式是共享式的,所有 PCI 设备将中断信号在一条中断线上相与,再上报给 CPU。CPU 收到中断后,需要查询具体是哪个设备产生了中断。
    • 在 PCIe 总线中,已经没有实体的 INTx 物理中断线了。PCIe 标准使用专门的 Message 事务包来实现 INTx 中断,这是为了兼容以前的 PCI 软件。INTx 是共享式的,CPU 响应中断后还需要查询具体中断源,效率较低。

    Legacy Interrupts

  2. MSI Interrupts

    • MSI 发出 usr_irq_req 中断请求后,user_irq_ack 为 1 只是说明中断已经被主机接收,但不代表已经处理。软件或驱动层可以清零 usr_irq_req
    • MSI 和 MSI-X 都是通过向配置的 CPU 中断寄存器写入内存操作来产生中断,效率比共享式的 INTx 高。MSI 最多支持 32 个中断向量,而 MSI-X 最多支持 2048 个中断向量。

    MSI Interrupts

  3. MSI-X Interrupts

    • usr_irq_req 中断请求后,user_irq_ack 为 1 时可以清零 usr_irq_req,但没有说明何时可以置 1,重启下次中断。

    MSI-X Interrupts

经过以上所有中断方式测试,发现使用 Legacy 和 MSI 已经足够,且相对稳定。上位机驱动通过访问用户 BAR 地址空间和米联客编写的 Uixdmairq IP-core 一起管理接收的中断。

中断测试结果

Uixdmairq.v 源码:

/*******************************MILIANKE*******************************
* Company : MiLianKe Electronic Technology Co., Ltd.
* WebSite: https://www.milianke.com
* TechWeb: https://www.uisrc.com
* tml-shop: https://milianke.tmall.com
* jd-shop: https://milianke.jd.com
* taobao-shop1: https://milianke.taobao.com
* Create Date: 2022/05/01
* Module Name: uixdmairq
* File Name: uixdmairq.v
* Description:
* The reference demo provided by Milianke is only used for learning.
* We cannot ensure that the demo itself is free of bugs, so users
* should be responsible for the technical problems and consequences
* caused by the use of their own products.
* Copyright: Copyright (c) MiLianKe
* All rights reserved.
* Revision: 1.0
* Signal description
* 1) _i input
* 2) _o output
* 3) _n active low
* 4) _dg debug signal
* 5) _r delay or register
* 6) _s state machine
*********************************************************************/
`timescale 1ns / 1ps

module uixdmairq #(
    parameter integer XMDA_REQ_NUM = 8
) (
    // Users to add ports here
    input wire [XMDA_REQ_NUM-1:0] user_irq_req_i,
    output wire [XMDA_REQ_NUM-1:0] xdma_irq_req_o,
    output wire user_irq_en_o,
    input wire S_AXI_ACLK,
    input wire S_AXI_ARESETN,
    input wire [3:0] S_AXI_AWADDR,
    input wire [2:0] S_AXI_AWPROT,
    input wire S_AXI_AWVALID,
    output wire S_AXI_AWREADY,
    input wire [31:0] S_AXI_WDATA,
    input wire [3:0] S_AXI_WSTRB,
    input wire S_AXI_WVALID,
    output wire S_AXI_WREADY,
    output wire [1:0] S_AXI_BRESP,
    output wire S_AXI_BVALID,
    input wire S_AXI_BREADY,
    input wire [3:0] S_AXI_ARADDR,
    input wire [2:0] S_AXI_ARPROT,
    input wire S_AXI_ARVALID,
    output wire S_AXI_ARREADY,
    output wire [31:0] S_AXI_RDATA,
    output wire [1:0] S_AXI_RRESP,
    output wire S_AXI_RVALID,
    input wire S_AXI_RREADY
);

reg [XMDA_REQ_NUM-1:0] user_irq_req;
reg [XMDA_REQ_NUM-1:0] user_irq_req_r1;
reg [XMDA_REQ_NUM-1:0] user_irq_req_r2;
reg [XMDA_REQ_NUM-1:0] user_irq_req_r3;
reg [XMDA_REQ_NUM-1:0] xdma_irq_ack_r1;
reg [XMDA_REQ_NUM-1:0] xdma_irq_ack_r2;
reg [XMDA_REQ_NUM-1:0] xdma_irq_ack_r3;
// reg [XMDA_REQ_NUM-1:0] xdma_irq_ack;

reg [XMDA_REQ_NUM-1:0] xdma_irq_req;

// AXI4LITE signals
reg [3:0] axi_awaddr;
reg axi_awready;
reg axi_wready;
reg [1:0] axi_bresp;
reg axi_bvalid;
reg [3:0] axi_araddr;
reg axi_arready;
reg [31:0] axi_rdata;
reg [1:0] axi_rresp;
reg axi_rvalid;

// Example-specific design signals
// local parameter for addressing 32 bit / 64 bit C_S_AXI_DATA_WIDTH
// ADDR_LSB is used for addressing 32/64 bit registers/memories
// ADDR_LSB = 2 for 32 bits (n downto 2)
// ADDR_LSB = 3 for 64 bits (n downto 3)
localparam integer ADDR_LSB = 2;
localparam integer OPT_MEM_ADDR_BITS = 1;

//----------------------------------------------
//-- Signals for user logic register space example
//------------------------------------------------
//-- Number of Slave Registers 4
reg [31:0] slv_reg0;
reg [31:0] slv_reg1;
wire slv_reg_rden;
wire slv_reg_wren;
reg [31:0] reg_data_out;
integer byte_index;
reg aw_en;

// I/O Connections assignments
assign S_AXI_AWREADY = axi_awready;
assign S_AXI_WREADY = axi_wready;
assign S_AXI_BRESP = axi_bresp;
assign S_AXI_BVALID = axi_bvalid;
assign S_AXI_ARREADY = axi_arready;
assign S_AXI_RDATA = axi_rdata;
assign S_AXI_RRESP = axi_rresp;
assign S_AXI_RVALID = axi_rvalid;

// Implement axi_awready generation
// axi_awready is asserted for one S_AXI_ACLK clock cycle when both
// S_AXI_AWVALID and S_AXI_WVALID are asserted. axi_awready is
// de-asserted when reset is low.
always @(posedge S_AXI_ACLK) begin
    if (S_AXI_ARESETN == 1'b0) begin
        axi_awready <= 1'b0;
        aw_en <= 1'b1;
    end else begin
        if (!axi_awready && S_AXI_AWVALID && S_AXI_WVALID && aw_en) begin
            // slave is ready to accept write address when
            // there is a valid write address and write data
            // on the write address and data bus. This design
            // expects no outstanding transactions.
            axi_awready <= 1'b1;
            aw_en <= 1'b0;
        end else if (S_AXI_BREADY && axi_bvalid) begin
            aw_en <= 1'b1;
            axi_awready <= 1'b0;
        end else begin
            axi_awready <= 1'b0;
        end
    end
end

// Implement axi_awaddr latching
// This process is used to latch the address when both
// S_AXI_AWVALID and S_AXI_WVALID are valid.
always @(posedge S_AXI_ACLK) begin
    if (S_AXI_ARESETN == 1'b0) begin
        axi_awaddr <= 0;
    end else begin
        if (!axi_awready && S_AXI_AWVALID && S_AXI_WVALID && aw_en) begin
            // Write Address latching
            axi_awaddr <= S_AXI_AWADDR;
        end
    end
end

// Implement axi_wready generation
// axi_wready is asserted for one S_AXI_ACLK clock cycle when both
// S_AXI_AWVALID and S_AXI_WVALID are asserted. axi_wready is
// de-asserted when reset is low.
always @(posedge S_AXI_ACLK) begin
    if (S_AXI_ARESETN == 1'b0) begin
        axi_wready <= 1'b0;
    end else begin
        if (!axi_wready && S_AXI_WVALID && S_AXI_AWVALID && aw_en) begin
            // slave is ready to accept write data when
            // there is a valid write address and write data
            // on the write address and data bus. This design
            // expects no outstanding transactions.
            axi_wready <= 1'b1;
        end else begin
            axi_wready <= 1'b0;
        end
    end
end

assign slv_reg_wren = axi_wready && S_AXI_WVALID && axi_awready && S_AXI_AWVALID;

always @(posedge S_AXI_ACLK) begin
    if (S_AXI_ARESETN == 1'b0) begin
        slv_reg0 <= 0;
    end else if (slv_reg_wren) begin
        if (axi_awaddr [3:2] == 2'd0) slv_reg0 [31:0] <= S_AXI_WDATA [31:0];
        else if (axi_awaddr [3:2] == 2'd1) slv_reg1 [31:0] <= S_AXI_WDATA [31:0];
    end else begin
        slv_reg0 <= 0;
        slv_reg1 <= slv_reg1;
    end
end

// Implement write response logic generation
// The write response and response valid signals are asserted by the slave
// when axi_wready, S_AXI_WVALID, axi_wready and S_AXI_WVALID are asserted.
// This indicates the acceptance of the write transaction and the status of
// the write transaction.
always @(posedge S_AXI_ACLK) begin
    if (S_AXI_ARESETN == 1'b0) begin
        axi_bvalid <= 0;
        axi_bresp <= 2'b0;
    end else begin
        if (axi_awready && S_AXI_AWVALID && !axi_bvalid && axi_wready && S_AXI_WVALID) begin
            // indicates a valid write response is available
            axi_bvalid <= 1'b1;
            axi_bresp <= 2'b0; // 'OKAY' response
        end else if (S_AXI_BREADY && axi_bvalid) begin
            axi_bvalid <= 0;
        end
    end
end

// Implement axi_arready generation
// axi_arready is asserted for one S_AXI_ACLK clock cycle when
// S_AXI_ARVALID is asserted. axi_arready is de-asserted when reset is low.
// The read address is also latched when S_AXI_ARVALID is asserted.
always @(posedge S_AXI_ACLK) begin
    if (S_AXI_ARESETN == 1'b0) begin
        axi_arready <= 0;
        axi_araddr <= 32'b0;
    end else begin
        if (!axi_arready && S_AXI_ARVALID) begin
            // indicates that the slave has accepted the valid read address
            axi_arready <= 1'b1;
            // Read address latching
            axi_araddr <= S_AXI_ARADDR;
        end else begin
            axi_arready <= 0;
        end
    end
end

// Implement axi_rvalid generation
// axi_rvalid is asserted for one S_AXI_ACLK clock cycle when both
// S_AXI_ARVALID and axi_arready are asserted. The slave registers
// data are available on the axi_rdata bus at this instance. The
// assertion of axi_rvalid marks the validity of the read data on the
// bus and axi_rresp indicates the status of the read transaction.
always @(posedge S_AXI_ACLK) begin
    if (S_AXI_ARESETN == 1'b0) begin
        axi_rvalid <= 0;
        axi_rresp <= 0;
    end else begin
        if (axi_arready && S_AXI_ARVALID && !axi_rvalid) begin
            // Valid read data is available on the read data bus
            axi_rvalid <= 1'b1;
            axi_rresp <= 2'b0; // 'OKAY' response
        end else if (axi_rvalid && S_AXI_RREADY) begin
            // Read data is accepted by the master
            axi_rvalid <= 0;
        end
    end
end

// Implement memory mapped register select and read logic generation
// Slave register read enable is asserted when valid address is available
// and the slave is ready to accept the read address.
assign slv_reg_rden = axi_arready & S_AXI_ARVALID & !axi_rvalid;

always @(posedge S_AXI_ACLK) begin
    if (S_AXI_ARESETN == 1'b0) begin
        axi_rdata <= 0;
    end else if (slv_reg_rden) begin
        if (axi_araddr [3:2] == 2'd0) axi_rdata [31:0] <= slv_reg0 [31:0];
        else if (axi_araddr [3:2] == 2'd1) axi_rdata [31:0] <= slv_reg1 [31:0];
    end
end

// Add user logic here
reg [4:0] i;
reg [4:0] j;
reg [4:0] k;

assign xdma_irq_req_o = xdma_irq_req;
assign user_irq_en_o = slv_reg1 [31];
assign xdma_irq_ack = slv_reg0 [XMDA_REQ_NUM-1:0];

always @(posedge S_AXI_ACLK) begin
    if (S_AXI_ARESETN == 1'b0 || user_irq_en_o == 1'b0) begin
        user_irq_req_r1 <= 0;
        user_irq_req_r2 <= 0;
        user_irq_req_r3 <= 0;
    end else begin
        user_irq_req_r1 <= user_irq_req_i;
        user_irq_req_r2 <= user_irq_req_r1;
        user_irq_req_r3 <= user_irq_req_r2;
    end
end

always @(posedge S_AXI_ACLK) begin
    if (S_AXI_ARESETN == 1'b0 || user_irq_en_o == 1'b0) begin
        j <= 5'd0;
        user_irq_req <= 0;
    end else begin
        for (j = 0; j <= XMDA_REQ_NUM-1; j = j + 1) begin
            user_irq_req [j] <= !user_irq_req_r3 [j] & user_irq_req_r2 [j];
        end
    end
end

always @(posedge S_AXI_ACLK) begin
    if (S_AXI_ARESETN == 1'b0 || user_irq_en_o == 1'b0) begin
        i <= 5'd0;
        xdma_irq_req <= 0;
    end else begin
        for (i = 0; i <= XMDA_REQ_NUM-1; i = i + 1) begin
            if (xdma_irq_ack [i]) begin
                xdma_irq_req [i] <= 1'b0;
            end else if (user_irq_req [i]) begin
                xdma_irq_req [i] <= 1'b1;
            end
        end
    end
end

// User logic ends
endmodule

为了方便测试中断,在 pcie_top.v 中增加了定时产生用户中断的程序。

wire axi_aclk;
wire axi_aresetn;
wire user_irq_en_o;

// 内部计数器产生一个延迟复位
reg [21:0] timer_cnt;
always @(posedge axi_aclk) begin
    if (!axi_aresetn || !user_irq_en_o) begin
        timer_cnt <= 22'd0;
    end else begin
        timer_cnt <= timer_cnt + 1'b1;
    end
end

reg timer_r1, timer_r2;
wire inter = !timer_r2 && timer_r1;

always @(posedge axi_aclk) begin
    if (!axi_aresetn || !user_irq_en_o) begin
        timer_r1 <= 1'd0;
        timer_r2 <= 1'd0;
    end else begin
        timer_r1 <= timer_cnt [20];
        timer_r2 <= timer_r1;
    end
end

reg [1:0] int_p;
reg [3:0] user_irq_req_i;

always @(posedge axi_aclk) begin
    if (!axi_aresetn || !user_irq_en_o) begin
        int_p <= 4'd0;
        user_irq_req_i <= 4'd0;
    end else begin
        if (inter) begin
            int_p <= int_p + 1'b1;
            user_irq_req_i <= 4'd0;
            user_irq_req_i [int_p] <= 1'b1;
        end
    end
end

7.4 上位机中断测试代码

实现中断程序的源码 intr_event.c

#include <Windows.h>
#include <assert.h>
#include <stdlib.h>
#include <stdio.h>
#include <strsafe.h>
#include <stdint.h>
#include <SetupAPI.h>
#include <INITGUID.H>
#include <WinIoCtl.h>
#include <io.h>
#include "xdma_public.h"

#pragma comment (lib, "setupapi.lib")
#pragma warning (disable:4996)

BYTE start_en;
HANDLE h_user;
HANDLE h_event0;
HANDLE h_event1;
HANDLE h_event2;
HANDLE h_event3;

BYTE user_irq_ack[1];

char base_path[MAX_PATH + 1] = "";

static int verbose_msg(const char* const fmt, ...) {
    int ret = 0;
    va_list args;
    if (1) {
        va_start(args, fmt);
        ret = vprintf(fmt, args);
        va_end(args);
    }
    return ret;
}

static BYTE* allocate_buffer(size_t size, size_t alignment) {
    if (size == 0) {
        size = 4;
    }
    if (alignment == 0) {
        SYSTEM_INFO sys_info;
        GetSystemInfo(&sys_info);
        alignment = sys_info.dwPageSize;
    }
    verbose_msg("Allocating host-side buffer of size %d, aligned to %d bytes\n", size, alignment);
    return (BYTE*)_aligned_malloc(size, alignment);
}

static int get_devices(GUID guid, char* devpath, size_t len_devpath) {
    SP_DEVICE_INTERFACE_DATA device_interface;
    PSP_DEVICE_INTERFACE_DETAIL_DATA dev_detail;
    DWORD index;
    HDEVINFO device_info;
    wchar_t tmp[256];
    device_info = SetupDiGetClassDevs((LPGUID)&guid, NULL, NULL, DIGCF_PRESENT | DIGCF_DEVICEINTERFACE);
    if (device_info == INVALID_HANDLE_VALUE) {
        fprintf(stderr, "GetDevices INVALID_HANDLE_VALUE\n");
        exit(-1);
    }
    device_interface.cbSize = sizeof(SP_DEVICE_INTERFACE_DATA);
    for (index = 0; SetupDiEnumDeviceInterfaces(device_info, NULL, &guid, index, &device_interface); ++index) {
        ULONG detailLength = 0;
        if (!SetupDiGetDeviceInterfaceDetail(device_info, &device_interface, NULL, 0, &detailLength, NULL) &&
            GetLastError() != ERROR_INSUFFICIENT_BUFFER) {
            fprintf(stderr, "SetupDiGetDeviceInterfaceDetail - get length failed\n");
            break;
        }
        dev_detail = (PSP_DEVICE_INTERFACE_DETAIL_DATA)HeapAlloc(GetProcessHeap(), HEAP_ZERO_MEMORY, detailLength);
        if (!dev_detail) {
            fprintf(stderr, "HeapAlloc failed\n");
            break;
        }
        dev_detail->cbSize = sizeof(SP_DEVICE_INTERFACE_DETAIL_DATA);
        if (!SetupDiGetDeviceInterfaceDetail(device_info, &device_interface, dev_detail, detailLength, NULL, NULL)) {
            fprintf(stderr, "SetupDiGetDeviceInterfaceDetail - get detail failed\n");
            HeapFree(GetProcessHeap(), 0, dev_detail);
            break;
        }
        StringCchCopy(tmp, len_devpath, dev_detail->DevicePath);
        wcstombs(devpath, tmp, 256);
        HeapFree(GetProcessHeap(), 0, dev_detail);
    }
    SetupDiDestroyDeviceInfoList(device_info);
    return index;
}

HANDLE open_devices(char* device_base_path, char* device_name, DWORD accessFlags) {
    char device_path[MAX_PATH + 1] = "";
    wchar_t device_path_w[MAX_PATH + 1];
    HANDLE h;
    verbose_msg("Device base path: %s\n", device_base_path);
    strcpy_s(device_path, sizeof(device_path), device_base_path);
    strcat_s(device_path, sizeof(device_path), device_name);
    verbose_msg("Device node: %s\n", device_name);
    mbstowcs(device_path_w, device_path, sizeof(device_path));
    h = CreateFile(device_path_w, accessFlags, 0, NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL);
    if (h == INVALID_HANDLE_VALUE) {
        fprintf(stderr, "Error opening device, win32 error code: %ld\n", GetLastError());
    }
    return h;
}

static int read_device(HANDLE device, long address, DWORD size, BYTE* buffer) {
    DWORD rd_size = 0;
    unsigned int transfers;
    unsigned int i;
    if (INVALID_SET_FILE_POINTER == SetFilePointer(device, address, NULL, FILE_BEGIN)) {
        fprintf(stderr, "Error setting file pointer, win32 error code: %ld\n", GetLastError());
        return -3;
    }
    transfers = (unsigned int)(size / MAX_BYTES_PER_TRANSFER);
    for (i = 0; i < transfers; i++) {
        if (!ReadFile(device, (void*)(buffer + i * MAX_BYTES_PER_TRANSFER), (DWORD)MAX_BYTES_PER_TRANSFER, &rd_size, NULL)) {
            return -1;
        }
        if (rd_size != MAX_BYTES_PER_TRANSFER) {
            return -2;
        }
    }
    if (!ReadFile(device, (void*)(buffer + i * MAX_BYTES_PER_TRANSFER), (DWORD)(size - i * MAX_BYTES_PER_TRANSFER), &rd_size, NULL)) {
        return -1;
    }
    if (rd_size != (size - i * MAX_BYTES_PER_TRANSFER)) {
        return -2;
    }
    return size;
}

static int write_device(HANDLE device, long address, DWORD size, BYTE* buffer) {
    DWORD wr_size = 0;
    unsigned int transfers;
    unsigned int i;
    transfers = (unsigned int)(size / MAX_BYTES_PER_TRANSFER);
    if (INVALID_SET_FILE_POINTER == SetFilePointer(device, address, NULL, FILE_BEGIN)) {
        fprintf(stderr, "Error setting file pointer, win32 error code: %ld\n", GetLastError());
        return -3;
    }
    for (i = 0; i < transfers; i++) {
        if (!WriteFile(device, (void*)(buffer + i * MAX_BYTES_PER_TRANSFER), MAX_BYTES_PER_TRANSFER, &wr_size, NULL)) {
            return -1;
        }
        if (wr_size != MAX_BYTES_PER_TRANSFER) {
            return -2;
        }
    }
    if (!WriteFile(device, (void*)(buffer + i * MAX_BYTES_PER_TRANSFER), (DWORD)(size - i * MAX_BYTES_PER_TRANSFER), &wr_size, NULL)) {
        return -1;
    }
    if (wr_size != (size - i * MAX_BYTES_PER_TRANSFER)) {
        return -2;
    }
    return size;
}

DWORD WINAPI thread_event0(LPVOID lpParam) {
    BYTE val0[1] = "";
    DWORD i = 0;
    char* device_name1 = "\\event_0";
    HANDLE h_event0 = open_devices(base_path, device_name1, GENERIC_READ);
    while (1) {
        if (start_en) {
            read_device(h_event0, 0, 1, val0); // wait for irq
            Sleep(1);
            if (val0[0] == 1)
                printf("event_0 done!\n");
            else
                printf("event_0 timeout!\n");
            i++;
        }
    }
    CloseHandle(h_event0);
    return 0;
}

DWORD WINAPI thread_event1(LPVOID lpParam) {
    BYTE val0[1] = "";
    DWORD i = 0;
    char* device_name1 = "\\event_1";
    HANDLE h_event1 = open_devices(base_path, device_name1, GENERIC_READ);
    while (1) {
        if (start_en) {
            read_device(h_event1, 0, 1, val0); // wait for irq
            Sleep(1);
            if (val0[0] == 1)
                printf("event_1 done!\n");
            else
                printf("event_1 timeout!\n");
            i++;
        }
    }
    CloseHandle(h_event1);
    return 0;
}

DWORD WINAPI thread_event2(LPVOID lpParam) {
    BYTE val0[1] = "";
    DWORD i = 0;
    char* device_name1 = "\\event_2";
    HANDLE h_event2 = open_devices(base_path, device_name1, GENERIC_READ);
    while (1) {
        if (start_en) {
            read_device(h_event2, 0, 1, val0); // wait for irq
            Sleep(1);
            if (val0[0] == 1)
                printf("event_2 done!\n");
            else
                printf("event_2 timeout!\n");
            i++;
        }
    }
    CloseHandle(h_event2);
    return 0;
}

DWORD WINAPI thread_event3(LPVOID lpParam) {
    BYTE val0[1] = "";
    DWORD i = 0;
    char* device_name1 = "\\event_3";
    HANDLE h_event3 = open_devices(base_path, device_name1, GENERIC_READ);
    while (1) {
        if (start_en) {
            read_device(h_event3, 0, 1, val0); // wait for irq
            Sleep(1);
            if (val0[0] == 1)
                printf("event_3 done!\n");
            else
                printf("event_3 timeout!\n");
            i++;
        }
    }
    CloseHandle(h_event3);
    return 0;
}

int __cdecl main(int argc, char* argv[]) {
    HANDLE h_event0;
    HANDLE h_event1;
    HANDLE h_event2;
    HANDLE h_event3;
    char* device_name = "\\user";
    DWORD num_devices = get_devices(GUID_DEVINTERFACE_XDMA, base_path, sizeof(base_path));
    verbose_msg("Devices found: %d\n", num_devices);
    if (num_devices < 1) {
        printf("No devices found\n");
        return -1;
    }
    h_user = open_devices(base_path, device_name, GENERIC_READ | GENERIC_WRITE);
    if (h_user == INVALID_HANDLE_VALUE) {
        fprintf(stderr, "Error opening device, win32 error code: %ld\n", GetLastError());
        return -1;
    }
    h_event0 = CreateThread(NULL, 0, thread_event0, NULL, 0, NULL);
    h_event1 = CreateThread(NULL, 0, thread_event1, NULL, 0, NULL);
    h_event2 = CreateThread(NULL, 0, thread_event2, NULL, 0, NULL);
    h_event3 = CreateThread(NULL, 0, thread_event3, NULL, 0, NULL);
    user_irq_ack[0] = 0xffff0000;
    write_device(h_user, 0x00004, 4, (BYTE*)user_irq_ack); // start irq
    start_en = 1;
    printf("Start interrupts\n");
    WaitForSingleObject(h_event0, INFINITE);
    WaitForSingleObject(h_event1, INFINITE);
    WaitForSingleObject(h_event2, INFINITE);
    WaitForSingleObject(h_event3, INFINITE);
    user_irq_ack[0] = 0x00000000;
    write_device(h_user, 0x00004, 4, (BYTE*)user_irq_ack); // stop irq
    CloseHandle(h_user);
    CloseHandle(h_event0);
    CloseHandle(h_event1);
    CloseHandle(h_event2);
    CloseHandle(h_event3);
    return 0;
}

以下指令启动中断:

user_irq_ack[0] = 0xffff0000;
write_device(h_user, 0x00004, 4, (BYTE*)user_irq_ack);

以下指令关闭中断:

user_irq_ack[0] = 0x00000000;
write_device(h_user, 0x00004, 4, (BYTE*)user_irq_ack);

以上程序设置了 4 个中断事件,每个事件开启了一个线程。当中断等待时,线程处于挂起状态;当中断产生后,线程继续执行。XDMA 最大支持 16 个中断。

7.5 XDMA 中断测试

执行 xdma_event.exe 程序。

XDMA 中断测试

XDMA 中断测试

可以看到运行结果是 4 个中断事件。实际上,XDMA 最大支持 16 个中断事件。更多的中断事件可以更好地发挥 CPU 多核多线程的性能。

查看 FPGA 抓取的波形信号:

FPGA 波形信号

到此,XDMA 的 PCIe 方案核心内容已经介绍完毕。Xilinx 官方提供的资料往往没有细化,米联客对驱动进行了修改,以更好地支持中断。

8. PCIe 速度测试

8.1 测试码表上位机程序设计

感谢网友贡献的测试码表控件源码。笔者对其进行了修改,使其能够适用于本次的例子。设计思路很简单:在 Qt 中开启了 2 个定时器,分别用于 h2c 和 c2h 通道,每 100 ms 定时器进行一次读操作或写操作。在 pcie_fun.c 文件中,有测试函数,完成传输测试后,将结果输出到 myspeed 测速码表控件。

测试码表上位机程序

以上代码中,显示速度的控件代码是 myspeed.cmyspeed.h。关于 PCIe 通信的核心代码是 pcie_fun.cpcie_fun.h。详细的实现过程可以阅读程序源码。

8.2 实验结果

不同的板卡,PCIe 的最大带宽不同,以实际为准。以下是 PCIEX2 2.0 的测速指标。

PCIEX2 2.0 测速指标

9. 内存读写测试

9.1 上位机程序设计

以下代码中,on_TestDDR_clicked 是对 AXI4 接口的 DDR 或 BRAM 的测试,on_TestBAR_clicked 是对用户 BAR 空间的测试。

void MainWindow::on_TestDDR_clicked() {
    unsigned int buf1[1024];
    unsigned int buf2[1024];
    unsigned int i = 0;
    unsigned int error_cnt = 0;
    for (i = 0; i < 1024; i++) {
        buf1[i] = i;
    }
    h2c_transfer(0, 1024 * 4, (unsigned char*)buf1);
    c2h_transfer(0, 1024 * 4, (unsigned char*)buf2);
    for (i = 0; i < 1024; i++) {
        if (buf1[i] != buf2[i]) error_cnt++;
    }
    if (error_cnt) {
        QString str = QString("%1 %2").arg("DDR bad data =").arg(error_cnt);
        ui->labelDDRPASS->setText(str);
    } else {
        m_pass1++;
        QString str = QString("%1 %2").arg("DDR PASS Times =").arg(m_pass1);
        ui->labelDDRPASS->setText(str);
    }
}

void MainWindow::on_TestBAR_clicked() {
    unsigned int val;
    unsigned int i = 0;
    unsigned int error_cnt = 0;
    for (i = 0; i < 1024; i++) {
        user_write(0x10000 + i * 4, 4, (unsigned char*)&i);
    }
    for (i = 0; i < 1024; i++) {
        user_read(0x10000 + i * 4, 4, (unsigned char*)&val);
        if (i != val) error_cnt++;
    }
    if (error_cnt) {
        QString str = QString("%1 %2").arg("BAR bad data =").arg(error_cnt);
        ui->labelBARPASS->setText(str);
    } else {
        m_pass2++;
        QString str = QString("%1 %2").arg("BAR PASS Times =").arg(m_pass2);
        ui->labelBARPASS->setText(str);
    }
}

9.2 实验结果

实验结果

本文来自米联客 (milianke),作者:米联客 (milianke)。

登录米联客 (MiLianKe) FPGA 社区 - www.uisrc.com 观看免费视频课程、在线答疑解惑!

posted @ 2023-12-27 20:13 米联客 (milianke)


linux 驱动框架与驱动开发实战

开发者认证为什么要改昵称呢 已于 2025-04-05 21:12:35 修改

一、Linux 驱动框架概述

Linux 驱动是操作系统内核与硬件设备之间的桥梁,它使得硬件设备能够被操作系统识别和管理。Linux 内核提供了一套完善的驱动框架,开发者可以基于这些框架开发各种硬件设备的驱动程序。

1.1 Linux 驱动的分类

Linux 驱动主要分为以下几类:

  1. 字符设备驱动:以字节流形式进行数据读写,如键盘、鼠标等
  2. 块设备驱动:以数据块为单位进行读写,如硬盘、SSD 等
  3. 网络设备驱动:用于网络通信的设备,如网卡
  4. 其他特殊类型:如 USB 驱动、PCI 驱动等框架驱动

Linux 驱动模型分层:
在这里插入图片描述

1.2 Linux 驱动的基本框架

无论哪种类型的驱动,Linux 都提供了相应的框架和接口。一个典型的 Linux 驱动包含以下组成部分:

  1. 模块加载和卸载函数module_init ()module_exit ()
  2. 文件操作接口file_operations 结构体
  3. 设备注册与注销register_chrdev () 等函数
  4. 中断处理request_irq () 和中断处理函数
  5. 内存管理kmalloc (), ioremap () 等函数
  6. 同步机制:自旋锁、信号量、互斥锁等

二、Linux 驱动关键 API 详解

2.1 模块相关 API

module_init (init_function);  // 指定模块加载时执行的函数
module_exit (exit_function);  // 指定模块卸载时执行的函数
MODULE_LICENSE ("GPL");       // 声明模块许可证
MODULE_AUTHOR ("Author");     // 声明模块作者
MODULE_DESCRIPTION ("Desc"); // 声明模块描述

2.2 字符设备驱动 API

// 注册字符设备
int register_chrdev (unsigned int major, const char *name, const struct file_operations *fops);

// 注销字符设备
void unregister_chrdev (unsigned int major, const char *name);

// 文件操作结构体
struct file_operations {
    struct module *owner;
    loff_t (*llseek) (struct file *, loff_t, int);
    ssize_t (*read) (struct file *, char __user *, size_t, loff_t *);
    ssize_t (*write) (struct file *, const char __user *, size_t, loff_t *);
    long (*unlocked_ioctl) (struct file *, unsigned int, unsigned long);
    int (*open) (struct inode *, struct file *);
    int (*release) (struct inode *, struct file *);
    // 其他操作...
};

2.3 内存管理 API

// 内核内存分配
void *kmalloc (size_t size, gfp_t flags);
void kfree (const void *objp);

// 物理地址映射
void *ioremap (phys_addr_t offset, unsigned long size);
void iounmap (void *addr);

// 用户空间与内核空间数据拷贝
unsigned long copy_to_user (void __user *to, const void *from, unsigned long n);
unsigned long copy_from_user (void *to, const void __user *from, unsigned long n);

2.4 中断处理 API

// 申请中断
int request_irq (unsigned int irq, irq_handler_t handler, unsigned long flags,
                const char *name, void *dev);

// 释放中断
void free_irq (unsigned int irq, void *dev_id);

// 中断处理函数原型
irqreturn_t irq_handler (int irq, void *dev_id);

2.5 PCI 设备驱动 API

// PCI 设备 ID 表
static const struct pci_device_id ids [] = {
    { PCI_DEVICE (VENDOR_ID, DEVICE_ID) },
    { 0, }
};
MODULE_DEVICE_TABLE (pci, ids);

// PCI 驱动结构体
static struct pci_driver pci_driver = {
    .name = "xdma_driver",
    .id_table = ids,
    .probe = xdma_probe,
    .remove = xdma_remove,
    // 其他回调...
};

// 注册 PCI 驱动
pci_register_driver (&pci_driver);

// 注销 PCI 驱动
pci_unregister_driver (&pci_driver);

三、Xilinx XDMA 驱动开发详解

3.1 XDMA 概述

Xilinx DMA (XDMA) 是一种高性能的 DMA 控制器,用于在 FPGA 和主机内存之间传输数据。XDMA 驱动通常作为 PCIe 设备驱动实现,支持 DMA 传输、中断处理等功能。

其实现 DMA 传输流程如下:

在这里插入图片描述

User Kernel DMA 引擎 write () 系统调用 配置源地址 / 目标地址 传输完成中断 唤醒等待进程 User Kernel DMA 引擎

3.2 XDMA 驱动开发步骤

步骤 1:定义 PCI 设备 ID
#define PCI_VENDOR_ID_XILINX 0x10ee
#define PCI_DEVICE_ID_XDMA 0x7028

static const struct pci_device_id xdma_pci_ids [] = {
    { PCI_DEVICE (PCI_VENDOR_ID_XILINX, PCI_DEVICE_ID_XDMA) },
    { 0, }
};
MODULE_DEVICE_TABLE (pci, xdma_pci_ids);
步骤 2:定义驱动主结构体
struct xdma_dev {
    struct pci_dev *pdev;
    void __iomem *bar [MAX_BARS];  // PCI BAR 空间映射
    int irq;                     // 中断号
    struct cdev cdev;            // 字符设备
    dev_t devno;                 // 设备号
    struct dma_chan *dma_chan;   // DMA 通道
    // 其他设备特定数据...
};
步骤 3:实现 PCI probe 函数

PCI 设备探测流程:

在这里插入图片描述

具体探测函数(probe)实现:

static int xdma_probe (struct pci_dev *pdev, const struct pci_device_id *id)
{
    struct xdma_dev *xdev;
    int err, i;
    
    // 1. 分配设备结构体
    xdev = devm_kzalloc (&pdev->dev, sizeof (*xdev), GFP_KERNEL);
    if (!xdev)
        return -ENOMEM;
    
    xdev->pdev = pdev;
    pci_set_drvdata (pdev, xdev);
    
    // 2. 使能 PCI 设备
    err = pci_enable_device (pdev);
    if (err) {
        dev_err (&pdev->dev, "Failed to enable PCI device\n");
        goto fail;
    }
    
    // 3. 请求 PCI 资源
    err = pci_request_regions (pdev, "xdma");
    if (err) {
        dev_err (&pdev->dev, "Failed to request PCI regions\n");
        goto disable_device;
    }
    
    // 4. 映射 BAR 空间
    for (i = 0; i < MAX_BARS; i++) {
        if (!pci_resource_len (pdev, i))
            continue;
            
        xdev->bar [i] = pci_iomap (pdev, i, pci_resource_len (pdev, i));
        if (!xdev->bar [i]) {
            dev_err (&pdev->dev, "Failed to map BAR% d\n", i);
            err = -ENOMEM;
            goto release_regions;
        }
    }
    
    // 5. 设置 DMA 掩码
    err = pci_set_dma_mask (pdev, DMA_BIT_MASK (64));
    if (err) {
        err = pci_set_dma_mask (pdev, DMA_BIT_MASK (32));
        if (err) {
            dev_err (&pdev->dev, "No suitable DMA available\n");
            goto unmap_bars;
        }
    }
    
    // 6. 申请中断
    xdev->irq = pdev->irq;
    err = request_irq (xdev->irq, xdma_irq_handler, IRQF_SHARED, "xdma", xdev);
    if (err) {
        dev_err (&pdev->dev, "Failed to request IRQ\n");
        goto unmap_bars;
    }
    
    // 7. 初始化 DMA 引擎
    err = xdma_init_dma (xdev);
    if (err)
        goto free_irq;
    
    // 8. 注册字符设备
    err = xdma_setup_cdev (xdev);
    if (err)
        goto deinit_dma;
    
    dev_info (&pdev->dev, "XDMA driver loaded successfully\n");
    return 0;
    
    // 错误处理...
}

// 初始化 DMA 引擎
static int xdma_init_dma (struct xdma_dev *xdev)
{
    dma_cap_mask_t mask;
    
    dma_cap_zero (mask);
    dma_cap_set (DMA_MEMCPY, mask);
    
    xdev->dma_chan = dma_request_channel (mask, NULL, NULL);
    if (!xdev->dma_chan) {
        dev_err (&xdev->pdev->dev, "Failed to get DMA channel\n");
        return -ENODEV;
    }
    
    return 0;
}

// 设置字符设备
static int xdma_setup_cdev (struct xdma_dev *xdev)
{
    int err;
    dev_t devno;
    
    err = alloc_chrdev_region (&devno, 0, 1, "xdma");
    if (err < 0) {
        dev_err (&xdev->pdev->dev, "Failed to allocate device number\n");
        return err;
    }
    
    xdev->devno = devno;
    cdev_init (&xdev->cdev, &xdma_fops);
    xdev->cdev.owner = THIS_MODULE;
    
    err = cdev_add (&xdev->cdev, devno, 1);
    if (err) {
        dev_err (&xdev->pdev->dev, "Failed to add cdev\n");
        unregister_chrdev_region (devno, 1);
        return err;
    }
    
    return 0;
}
步骤 4:实现文件操作接口
static const struct file_operations xdma_fops = {
    .owner = THIS_MODULE,
    .open = xdma_open,
    .release = xdma_release,
    .read = xdma_read,
    .write = xdma_write,
    .unlocked_ioctl = xdma_ioctl,
    .llseek = no_llseek,
};

static int xdma_open (struct inode *inode, struct file *filp)
{
    struct xdma_dev *xdev = container_of (inode->i_cdev, struct xdma_dev, cdev);
    filp->private_data = xdev;
    return 0;
}

static int xdma_release (struct inode *inode, struct file *filp)
{
    filp->private_data = NULL;
    return 0;
}

static ssize_t xdma_read (struct file *filp, char __user *buf, size_t count, loff_t *f_pos)
{
    struct xdma_dev *xdev = filp->private_data;
    // 实现 DMA 读取操作...
    return count;
}

static ssize_t xdma_write (struct file *filp, const char __user *buf, size_t count, loff_t *f_pos)
{
    struct xdma_dev *xdev = filp->private_data;
    // 实现 DMA 写入操作...
    return count;
}

static long xdma_ioctl (struct file *filp, unsigned int cmd, unsigned long arg)
{
    struct xdma_dev *xdev = filp->private_data;
    
    switch (cmd) {
    case XDMA_IOCTL_START_DMA:
        // 启动 DMA 传输
        break;
    case XDMA_IOCTL_STOP_DMA:
        // 停止 DMA 传输
        break;
    case XDMA_IOCTL_GET_STATUS:
        // 获取 DMA 状态
        break;
    default:
        return -ENOTTY;
    }
    
    return 0;
}
步骤 5:实现中断处理
static irqreturn_t xdma_irq_handler (int irq, void *dev_id)
{
    struct xdma_dev *xdev = dev_id;
    u32 status;
    
    // 读取中断状态寄存器
    status = ioread32 (xdev->bar [0] + XDMA_IRQ_STATUS_REG);
    
    if (status & XDMA_IRQ_DONE) {
        // DMA 传输完成中断
        complete (&xdev->dma_complete);
    }
    
    if (status & XDMA_IRQ_ERROR) {
        // DMA 错误中断
        dev_err (&xdev->pdev->dev, "DMA error occurred\n");
    }
    
    // 清除中断状态
    iowrite32 (status, xdev->bar [0] + XDMA_IRQ_STATUS_REG);
    
    return IRQ_HANDLED;
}
步骤 6:实现 DMA 传输
static int xdma_do_transfer (struct xdma_dev *xdev, dma_addr_t src, 
                           dma_addr_t dst, size_t len)
{
    struct dma_async_tx_descriptor *tx;
    struct dma_device *dma_dev = xdev->dma_chan->device;
    enum dma_ctrl_flags flags = DMA_CTRL_ACK | DMA_PREP_INTERRUPT;
    dma_cookie_t cookie;
    int err;
    
    // 准备 DMA 描述符
    tx = dma_dev->device_prep_dma_memcpy (xdev->dma_chan, dst, src, len, flags);
    if (!tx) {
        dev_err (&xdev->pdev->dev, "Failed to prepare DMA descriptor\n");
        return -EIO;
    }
    
    tx->callback = xdma_dma_callback;
    tx->callback_param = xdev;
    
    // 提交 DMA 传输
    cookie = dmaengine_submit (tx);
    err = dma_submit_error (cookie);
    if (err) {
        dev_err (&xdev->pdev->dev, "Failed to submit DMA transfer\n");
        return err;
    }
    
    // 触发 DMA 传输
    dma_async_issue_pending (xdev->dma_chan);
    
    // 等待传输完成
    if (!wait_for_completion_timeout (&xdev->dma_complete, msecs_to_jiffies (1000))) {
        dev_err (&xdev->pdev->dev, "DMA transfer timeout\n");
        dmaengine_terminate_all (xdev->dma_chan);
        return -ETIMEDOUT;
    }
    
    return 0;
}

static void xdma_dma_callback (void *data)
{
    struct xdma_dev *xdev = data;
    complete (&xdev->dma_complete);
}
步骤 7:实现 remove 函数
static void xdma_remove (struct pci_dev *pdev)
{
    struct xdma_dev *xdev = pci_get_drvdata (pdev);
    int i;
    
    // 1. 移除字符设备
    cdev_del (&xdev->cdev);
    unregister_chrdev_region (xdev->devno, 1);
    
    // 2. 释放 DMA 资源
    if (xdev->dma_chan)
        dma_release_channel (xdev->dma_chan);
    
    // 3. 释放中断
    free_irq (xdev->irq, xdev);
    
    // 4. 取消 BAR 空间映射
    for (i = 0; i < MAX_BARS; i++) {
        if (xdev->bar [i])
            pci_iounmap (pdev, xdev->bar [i]);
    }
    
    // 5. 释放 PCI 资源
    pci_release_regions (pdev);
    
    // 6. 禁用 PCI 设备
    pci_disable_device (pdev);
    
    // 7. 释放设备结构体
    devm_kfree (&pdev->dev, xdev);
    
    dev_info (&pdev->dev, "XDMA driver unloaded\n");
}
步骤 8:定义 PCI 驱动结构体并注册
static struct pci_driver xdma_driver = {
    .name = "xdma",
    .id_table = xdma_pci_ids,
    .probe = xdma_probe,
    .remove = xdma_remove,
};

static int __init xdma_init (void)
{
    return pci_register_driver (&xdma_driver);
}

static void __exit xdma_exit (void)
{
    pci_unregister_driver (&xdma_driver);
}

module_init (xdma_init);
module_exit (xdma_exit);

MODULE_LICENSE ("GPL");
MODULE_AUTHOR ("Your Name");
MODULE_DESCRIPTION ("Xilinx XDMA Driver");

3.3 步骤总结

上文以 xilinx XDMA 为例介绍了 Linux PCI 设备驱动开发步骤,总结成流程图如下:

在这里插入图片描述

四、XDMA 驱动测试与调试

4.1 加载驱动模块

# 加载驱动
sudo insmod xdma.ko

# 查看加载的模块
lsmod | grep xdma

# 查看内核日志
dmesg | tail

4.2 测试 DMA 传输

可以使用简单的用户空间程序测试 DMA 功能:

//test_xdma.c
#include <stdio.h>
#include <stdlib.h>
#include <fcntl.h>
#include <unistd.h>
#include <sys/ioctl.h>

#define XDMA_DEV "/dev/xdma"
#define BUF_SIZE (1024 * 1024)  // 1MB

int main ()
{
    int fd = open (XDMA_DEV, O_RDWR);
    if (fd < 0) {
        perror ("Failed to open device");
        return -1;
    }
    
    // 分配测试缓冲区
    char *src = malloc (BUF_SIZE);
    char *dst = malloc (BUF_SIZE);
    
    if (!src || !dst) {
        perror ("Failed to allocate buffers");
        close (fd);
        return -1;
    }
    
    // 填充源缓冲区
    memset (src, 0xAA, BUF_SIZE);
    memset (dst, 0, BUF_SIZE);
    
    // 写入数据到设备
    ssize_t written = write (fd, src, BUF_SIZE);
    printf ("Written % zd bytes to device\n", written);
    
    // 从设备读取数据
    ssize_t readed = read (fd, dst, BUF_SIZE);
    printf ("Read % zd bytes from device\n", readed);
    
    // 验证数据
    if (memcmp (src, dst, BUF_SIZE) {
        printf ("Data verification failed!\n");
    } else {
        printf ("Data verification passed!\n");
    }
    
    free (src);
    free (dst);
    close (fd);
    return 0;
}

4.3 常见问题调试

  1. PCI 设备未识别

    • 检查 lspci -nn 确认设备 ID 是否正确
    • 确认内核配置中启用了 PCI 支持
  2. DMA 传输失败

    • 检查 DMA 掩码设置
    • 确认物理地址是否正确
    • 检查 DMA 引擎是否支持所需操作
  3. 中断不触发

    • 确认中断号是否正确
    • 检查中断状态寄存器
    • 确认中断处理函数已正确注册

五、性能优化技巧

5.1 使用分散 / 聚集 DMA

static int xdma_sg_transfer (struct xdma_dev *xdev, 
                           struct scatterlist *sg_src,
                           struct scatterlist *sg_dst,
                           int sg_count)
{
    struct dma_async_tx_descriptor *tx;
    struct dma_device *dma_dev = xdev->dma_chan->device;
    enum dma_ctrl_flags flags = DMA_CTRL_ACK | DMA_PREP_INTERRUPT;
    dma_cookie_t cookie;
    int err;
    
    tx = dma_dev->device_prep_dma_sg (xdev->dma_chan, 
                                    sg_dst, sg_count,
                                    sg_src, sg_count,
                                    flags);
    if (!tx) {
        dev_err (&xdev->pdev->dev, "Failed to prepare SG DMA descriptor\n");
        return -EIO;
    }
    
    tx->callback = xdma_dma_callback;
    tx->callback_param = xdev;
    
    cookie = dmaengine_submit (tx);
    err = dma_submit_error (cookie);
    if (err) {
        dev_err (&xdev->pdev->dev, "Failed to submit SG DMA transfer\n");
        return err;
    }
    
    dma_async_issue_pending (xdev->dma_chan);
    
    if (!wait_for_completion_timeout (&xdev->dma_complete, msecs_to_jiffies (1000))) {
        dev_err (&xdev->pdev->dev, "SG DMA transfer timeout\n");
        dmaengine_terminate_all (xdev->dma_chan);
        return -ETIMEDOUT;
    }
    
    return 0;
}

5.2 实现零拷贝

static int xdma_mmap (struct file *filp, struct vm_area_struct *vma)
{
    struct xdma_dev *xdev = filp->private_data;
    unsigned long offset = vma->vm_pgoff << PAGE_SHIFT;
    unsigned long size = vma->vm_end - vma->vm_start;
    int ret;
    
    // 将 BAR 空间映射到用户空间
    if (offset >= pci_resource_len (xdev->pdev, 0) || 
        size > pci_resource_len (xdev->pdev, 0) - offset) {
        return -EINVAL;
    }
    
    ret = remap_pfn_range (vma, vma->vm_start,
                         (pci_resource_start (xdev->pdev, 0) + offset) >> PAGE_SHIFT,
                         size, vma->vm_page_prot);
    if (ret)
        return -EAGAIN;
    
    return 0;
}

5.3 使用 DMA 池

// 初始化 DMA 池
xdev->dma_pool = dma_pool_create ("xdma_pool", &xdev->pdev->dev,
                                POOL_SIZE, POOL_ALIGN, 0);
if (!xdev->dma_pool) {
    dev_err (&xdev->pdev->dev, "Failed to create DMA pool\n");
    return -ENOMEM;
}

// 从 DMA 池分配内存
void *buf = dma_pool_alloc (xdev->dma_pool, GFP_KERNEL, &dma_handle);
if (!buf) {
    dev_err (&xdev->pdev->dev, "Failed to allocate from DMA pool\n");
    return -ENOMEM;
}

// 释放 DMA 池内存
dma_pool_free (xdev->dma_pool, buf, dma_handle);

// 销毁 DMA 池
dma_pool_destroy (xdev->dma_pool);

六、总结

本文详细介绍了 Linux 驱动框架和关键 API,并以 Xilinx XDMA 驱动为例,展示了 Linux 驱动开发的完整流程。关键点包括:

  1. 理解 Linux 驱动框架:掌握字符设备、块设备和网络设备驱动的基本结构
  2. 熟悉关键 API:模块加载、文件操作、内存管理、中断处理等核心 API
  3. PCI 驱动开发:从设备发现到资源管理的完整流程
  4. DMA 传输实现:包括标准 DMA 和分散 / 聚集 DMA
  5. 驱动调试技巧:日志分析、用户空间测试程序等

通过 XDMA 驱动的实例,我们可以看到 Linux 驱动开发需要综合考虑硬件特性、内核 API 和性能优化等多个方面。希望本文能为 Linux 驱动开发者提供有价值的参考。


via:

  • FPGA(基于 xilinx)中 PCIe 介绍以及 IP 核 XDMA 的使用_xilinx pcie-CSDN 博客
    https://blog.csdn.net/Njustxiaobai/article/details/132874083

  • Xilinx DMA 的几种方式与架构 - Hello-FPGA - 博客园
    https://www.cnblogs.com/xingce/p/16386108.html

  • 关于 DMA 环通实验的 SDK 部分代码理解_sdk dma-CSDN 博客
    https://blog.csdn.net/2301_80250829/article/details/137629092

  • XDMA 传输模式_xdma 数据传输原理 - CSDN 博客
    https://blog.csdn.net/a8039974/article/details/146314970

  • Xilinx XDMA 说明和测试 - MM-CSDN 博客
    https://blog.csdn.net/weixin_43956013/article/details/128608551

  • xdma 使用小结 - PCIe 与 FPGA 通信:XDMA IP 核配置与 AXI 总线解析 - CSDN 博客
    https://blog.csdn.net/mu_guang_/article/details/108951919

  • 20 基于 XDMA 实现 PCIE 通信方案 - 米联客 (milianke) - 博客园
    https://www.cnblogs.com/milianke/p/17931352.html

  • [实战] linux 驱动框架与驱动开发实战 - CSDN 博客
    https://blog.csdn.net/jz_ddk/article/details/147015183

  • Xilinx 中 PCIe 简介以及 IP 核 XDMA 的使用 - CSDN 博客
    https://blog.csdn.net/bingcheby/article/details/136186955

  • 【DMA 使用指南】设计说明 | 立创开发板技术文档中心
    https://wiki.lckfb.com/zh-hans/hspi-d133ebs/rtos-sdk/system/dma-operating-guide/design-description.html

  • GitHub - Xilinx/dma_ip_drivers: Xilinx QDMA IP Drivers
    https://github.com/Xilinx/dma_ip_drivers

  • PCIe 基础篇——PCIe 传输速率计算-CSDN博客
    https://blog.csdn.net/u013253075/article/details/108926633

  • PCIe speed table (from gen 1 to gen 6) – NAS Compares
    https://nascompares.com/answer/what-is-pcie-speed-from-gen1-to-gen6/

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/2374689.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

winreg查询Windows注册表的一些基本用法

注册表是Windows操作系统中用于存储配置信息的数据库。它包含了关于系统硬件、已安装的应用程序、用户账户设置以及系统设置的信息。 特别地&#xff0c;当我们需要某些软件的配置配息时&#xff0c;主要在HKEY_CURRENT_USER和HKEY_LOCAL_MACHINE下的SoftWare内进行查询操作。 …

计算机网络|| 路由器和交换机的配置

一、实验目的 1. 了解路由器和交换机的工作模式和使用方法&#xff1b; 2. 熟悉 Cisco 网络设备的基本配置命令&#xff1b; 3. 掌握 Cisco 路由器的基本配置方式及配置命令&#xff1b; 4. 掌握路由器和交换机的基本配置与管理方法。 二、实验环境 1. 运行 Windows 操作…

推理加速新范式:火山引擎高性能分布式 KVCache (EIC)核心技术解读

资料来源&#xff1a;火山引擎-开发者社区 分布式 KVCache 的兴起 背景 在大模型领域&#xff0c;随着模型参数规模的扩大和上下文长度增加&#xff0c;算力消耗显著增长。在 LLM 推理过程中&#xff0c;如何减少算力消耗并提升推理吞吐已经成为关键性优化方向。以多轮对话场…

中央处理器(CPU)(概述、指令周期)

一、概述 主要功能&#xff1a;&#xff08;1&#xff09;程序控制&#xff08;2&#xff09;操作控制&#xff08;3&#xff09;时序控制&#xff08;4&#xff09;数据加工&#xff08;5&#xff09;中断处理 组成&#xff1a;早期冯诺依曼计算机的 CPU 主要由运算器和控制…

MiniCPM-V

一、引言 在多模态大语言模型(MLLMs)快速发展的背景下,现有模型因高参数量(如 72B、175B)和算力需求,仅能部署于云端,难以适配手机、车载终端等内存和算力受限的端侧设备。MiniCPM-V聚焦 “轻量高效” 与 “端侧落地”,通过架构创新、训练优化和部署适配,打造高知识密…

Screeps Arena基础入门

本文主要内容 JavaSsript语法使用VScode编译环境Screeps Arena游戏规则 JavaSsript语法使用 基本数据类型 // String, Numker,Boolean,null, undefined const username "John"; const age 30; const rate 4.5; const iscool true; const x null; #表示值为…

开疆智能Profinet转Canopen网关连接sick RFID读写器配置案例

打开CANopen总线配置软件设置CANopen参数&#xff1a; 1. 使用Profinet转CANopen网关的配置软件修改CANopen主站参数&#xff1a; 首先新建项目&#xff0c;选择对应网关模块 2. 设置波特率&#xff1a;250 kbps&#xff08;需与SICK RFID读写器一致&#xff09;。 设置同步…

17.three官方示例+编辑器+AI快速学习webgl_buffergeometry_lines

本实例主要讲解内容 这个Three.js示例展示了如何使用BufferGeometry创建大量线段&#xff0c;并通过**变形目标(Morph Targets)**实现动态变形效果。通过随机生成的点云数据&#xff0c;结合顶点颜色和变形动画&#xff0c;创建出一个视觉效果丰富的3D线条场景。 核心技术包括…

深入掌握CSS定位:构建精密布局的核心技术

一、定位的定义 定位&#xff08;Positioning&#xff09;是CSS中用于控制元素在网页中的具体位置的一种机制。通过定位&#xff0c;可以将元素放置在页面的任意位置&#xff0c;并控制其与其他元素的层叠关系。 二、定位的特点与作用 自由摆放位置&#xff1a; 允许元素摆放…

Go语言多线程爬虫与代理IP反爬

有个朋友想用Go语言编写一个多线程爬虫&#xff0c;并且使用代理IP来应对反爬措施。多线程在Go中通常是通过goroutine实现的&#xff0c;所以应该使用goroutine来并发处理多个网页的抓取。然后&#xff0c;代理IP的话&#xff0c;可能需要一个代理池&#xff0c;从中随机选择代…

node.js 实战——express图片保存到本地或服务器(七牛云、腾讯云、阿里云)

本地 ✅ 使用formidable 读取表单内容 npm i formidable ✅ 使用mime-types 获取图片后缀 npm install mime-types✅ js 中提交form表单 document.getElementById(uploadForm).addEventListener(submit, function(e){e.preventDefault();const blob preview._blob;if(!blob)…

Shadertoy着色器移植到Three.js经验总结

Shadertoy是一个流行的在线平台&#xff0c;用于创建和分享WebGL片段着色器。里面有很多令人惊叹的画面&#xff0c;甚至3D场景。本人也移植了几个ShaderToy上的着色器。本文将详细介绍移植过程中需要注意的关键点。 1. 基本结构差异 想要移植ShaderToy的shader到three.js&am…

电脑端音乐播放器推荐:提升你的听歌体验!

在快节奏的职场环境中&#xff0c;许多上班族都喜欢用音乐为工作时光增添色彩。今天要分享的这款音乐工具&#xff0c;或许能为你的办公时光带来意想不到的惊喜。 一、软件介绍-澎湃 澎湃音乐看似是个普通的播放器&#xff0c;实则藏着强大的资源整合能力。左侧功能栏清晰陈列着…

VIC-2D 7.0 为平面样件机械试验提供全视野位移及应变数据软件

The VIC-2D系统是一个完全集成的解决方案&#xff0c;它基于优化的相关算法为平面试样的力学测试提供非接触、全场的二维位移和应变数据&#xff0c;可测量关注区域内的每个像素子集的面内位移&#xff0c;并通过多种张量选项计算全场应变。The VIC-2D 系统可测量超过 2000%变形…

一周学完计算机网络之三:1、数据链路层概述

简单的概述 数据链路层是计算机网络体系结构中的第二层&#xff0c;它在物理层提供的基本服务基础上&#xff0c;负责将数据从一个节点可靠地传输到相邻节点。可以将其想象成一个负责在两个相邻的网络设备之间进行数据 “搬运” 和 “整理” 的 “快递中转站”。 几个重要概念…

单片机-STM32部分:13-1、蜂鸣器

飞书文档https://x509p6c8to.feishu.cn/wiki/V8rpwIlYIiEuXLkUljTcXWiKnSc 一、应用场景 大部分的电子产品、家电&#xff08;风扇、空调、电水壶&#xff09;都会有蜂鸣器&#xff0c;用于提示设备的工作状态 二、原理 蜂鸣器是一种将电信号转换为声音信号的器件&#xff0…

动态IP技术赋能业务创新:解锁企业数字化转型新维度

在数字经济高速发展的今天&#xff0c;IP地址已不再是简单的网络标识符&#xff0c;而是演变为支撑企业数字化转型的核心基础设施之一。动态IP技术凭借其灵活、高效、安全的特性&#xff0c;正在重塑传统业务模式&#xff0c;催生出诸多创新应用场景。本文将深入剖析动态IP的技…

TDengine 在金融领域的应用

简介 金融行业正处于数据处理能力革新的关键时期。随着市场数据量的爆炸式增长和复杂性的日益加深&#xff0c;金融机构面临着寻找能够高效处理大规模、高频次以及多样化时序数据的大数据处理系统的迫切需求。这一选择将成为金融机构提高数据处理效率、优化交易响应时间、提高…

OSCP - Hack The Box - Sau

主要知识点 CVE-2023-27163漏洞利用systemd提权 具体步骤 执行nmap扫描&#xff0c;可以先看一下55555端口 Nmap scan report for 10.10.11.224 Host is up (0.58s latency). Not shown: 65531 closed tcp ports (reset) PORT STATE SERVICE VERSION 22/tcp o…

QT6 源(93)篇三:阅读与注释共用体类 QVariant 及其源代码,本类支持比较运算符 ==、!=。

&#xff08;9&#xff09; 本类支持比较运算符 、! &#xff1a; 可见&#xff0c; QString 类型里可存储多个 unicode 字符&#xff0c;即使只存储一个 unicode 字符也不等于 QChar。 &#xff08;10&#xff09;本源代码来自于头文件 qvariant . h &#xff1a; #ifndef Q…