﻿# Linux 与 FPGA 通信的方式 —— 从 MMIO 到 DMA

> FPGA 加速卡插在服务器的 PCIe 插槽上，主机 CPU 怎么跟它通信？这个问题本质上是"两个设备挂在同一条 PCIe 总线上，怎么交换数据"——跟 CPU 和网卡/NVMe/GPU 的通信是一回事，但 FPGA 更灵活：你可以在 FPGA 里实现任意接口。本篇系统化梳理 Linux 下与 FPGA 通信的所有方式，从最原始到最高级，讲清每种方式的原理、适用场景和性能对比。


> 相关：FPGA 为什么必须用 DMA 见 [dma.md](/concepts/io/dma.md) §五，PCIe 总线的物理传输通道（拓扑/BAR/MMIO）见 [pcie.md](/concepts/io/pcie/pcie.md)，中断处理见 [../process/interrupts.md](/concepts/process/interrupts.md)。

## 零、先看全景：一张图讲清所有方式

```plantuml
@startuml
skinparam shadowing false
skinparam rectangle {
  BackgroundColor<<app>> #E3F2FD
  BorderColor<<app>> #1976D2
  BackgroundColor<<kern>> #C8E6C9
  BorderColor<<kern>> #388E3C
  BackgroundColor<<hw>> #FFF9C4
  BorderColor<<hw>> #F9A825
  BackgroundColor<<bar>> #FFE0B2
  BorderColor<<bar>> #EF6C00
}
rectangle "用户态程序" <<app>> as APP
rectangle "内核\n(UIO驱动/VFIO驱动/\nFPGA专用驱动)" <<kern>> as KERN
rectangle "FPGA 加速卡\n(PCIe Endpoint)" <<hw>> as FPGA
rectangle "BAR0: 控制寄存器" <<bar>> as BAR_CTL
rectangle "BAR1: 数据缓冲区\n(映射成一块\"内存\")" <<bar>> as BAR_BUF
rectangle "BAR2: DMA 描述符环" <<bar>> as BAR_DMA
FPGA -down- BAR_CTL
FPGA -down- BAR_BUF
FPGA -down- BAR_DMA
' 通信路径
APP -down-> KERN : ① 用户态直接访问\n(UIO mmap / VFIO mmap)
KERN -down-> BAR_CTL : ② 内核态 MMIO 访问\n(ioremap → readl/writel)
KERN -down-> BAR_BUF : ③ BAR 空间映射\n(大数据直接读写的备选)
KERN -down-> BAR_DMA : ④ DMA 方式\n(主机内存↔FPGA 大批量传输)
note bottom of BAR_CTL : 控制路径(慢,小数据):\nMMIO/iowrite32
note bottom of BAR_DMA : 数据路径(快,大数据):\nFPGA DMA 引擎主动搬数据
@enduml
```

> **核心认知**：FPGA 通信分两条路径——**控制路径**（MMIO，CPU 主动读写 FPGA 寄存器，慢但简单）和**数据路径**（DMA，FPGA 主动搬大批量数据，快但复杂）。选哪种方式取决于数据量和实时性要求。

## 一、方式 1：MMIO（Memory-Mapped I/O）—— CPU 主动读写 FPGA 寄存器

### 原理

FPGA 在 PCIe 枚举时通过 BAR 向系统声明一段地址空间。CPU 用普通访存指令就能读写这段空间——硬件上，CPU 的 load/store 落到这个地址范围时，Root Complex 把它翻译成 PCIe Memory Read/Write TLP，发到 FPGA。

```plantuml
@startuml
skinparam shadowing false
skinparam sequence {
  ParticipantBackgroundColor #E3F2FD
  ParticipantBorderColor     #1976D2
  ArrowColor                 #37474F
}
participant "CPU/驱动" as CPU
participant "Root Complex" as RC
participant "FPGA BAR\n(寄存器/缓冲区)" as FPGA
CPU -> RC : ① 驱动调用 ioread32()/iowrite32()\n→ CPU 发物理地址 load/store 指令
RC -> FPGA : ② 地址落在 FPGA BAR 范围\n→ 翻译成 PCIe Memory Read/Write TLP
FPGA -> RC : ③ FPGA 返回数据(读)或接收数据(写)
RC -> CPU : ④ 数据返回 CPU 寄存器
note over CPU, FPGA : MMIO 本质是 CPU 主动发起的\n每次读写都要等 PCIe 往返延迟\n大量数据时 CPU 被 stall 住
@enduml
```

### 代码层面

```c
// 内核驱动中
void __iomem *bar0 = ioremap(pci_resource_start(pdev, 0), pci_resource_len(pdev, 0));
u32 status = ioread32(bar0 + STATUS_REG_OFFSET);   // 读状态寄存器
iowrite32(CMD_START, bar0 + CMD_REG_OFFSET);        // 写命令寄存器
```

### 特点

| 维度 | 评价 |
|------|------|
| **带宽** | 低，每次 4/8 字节，受 PCIe 往返延迟限制（~几百 ns 一次） |
| **CPU 占用** | 高，CPU 全程参与每次读写 |
| **延迟** | 低（单次访问），适合控制命令 |
| **适用数据量** | 少量控制数据（KB 级），如写命令寄存器、读状态、配置参数 |
| **实现复杂度** | 低，内核标准 API |

> **一句话**：MMIO 是控制路径——用 `ioread32`/`iowrite32` 读写 FPGA 寄存器，简单但只能传小量控制信息。想传 MB/GB 级数据？必须上 DMA。

## 二、方式 2：DMA —— FPGA 主动读写主机内存

### 原理

FPGA 内部实现一个 DMA 引擎 IP 核，让它成为 PCIe 总线主设备（Bus Master），能主动发起对主机内存的读写。CPU 只需告诉 FPGA"数据在主机内存的物理地址 0x...、长度 N"——FPGA 自己把活干完，完事发中断通知 CPU。

完整流程见 [dma.md](/concepts/io/dma.md) §五 的时序图，这里不再重复。重点讲 FPGA DMA 的三种实现方式：

### 2.1 三种 DMA 模式

```plantuml
@startuml
skinparam shadowing false
skinparam rectangle {
  BackgroundColor<<mode>> #E3F2FD
  BorderColor<<mode>> #1976D2
}
rectangle "简单 DMA\n(Simple / Block DMA)" <<mode>> as SIMPLE
rectangle "SG-DMA\n(Scatter-Gather DMA)" <<mode>> as SG
rectangle "描述符环 DMA\n(Ring / Queue DMA)" <<mode>> as RING
note right of SIMPLE : 一次传输一块连续物理内存\n最简单,适合固定大小缓冲区
note right of SG : 用描述符链表传不连续物理页\n灵活性高,零拷贝基础
note right of RING : 生产者-消费者环形队列\n持续流式传输,类似网卡\n最常用
@enduml
```

**描述符环 DMA（最常用）**：

```plantuml
@startuml
skinparam shadowing false
skinparam rectangle {
  BackgroundColor<<mem>> #E3F2FD
  BorderColor<<mem>> #1976D2
  BackgroundColor<<desc>> #FFF9C4
  BorderColor<<desc>> #F9A825
}
rectangle "主机内存" <<mem>> as MEM {
  rectangle "描述符环\n(环形队列)" <<desc>> as RING
  rectangle "数据缓冲区 0" <<mem>> as BUF0
  rectangle "数据缓冲区 1" <<mem>> as BUF1
  rectangle "数据缓冲区 2" <<mem>> as BUF2
  rectangle "..." <<mem>> as BUFX
}
rectangle "FPGA" <<mem>> as FPGA {
  rectangle "DMA 引擎\n维护读/写指针" <<desc>> as DMAENG
}
RING -right-> DMAENG : FPGA 消费描述符\n读取/填充缓冲区
note bottom of MEM : 驱动往环里填描述符(写指针++)\nFPGA 消费描述符(读指针++)\n中断通知 CPU 哪些已完成
@enduml
```

### 2.2 FPGA DMA 的内核驱动实现要点

```c
// 1. 分配 DMA 缓冲区
dma_addr_t dma_handle;
void *cpu_addr = dma_alloc_coherent(&pdev->dev, BUF_SIZE, &dma_handle, GFP_KERNEL);
// 2. 把物理地址告诉 FPGA（通过 MMIO 写 FPGA 寄存器）
iowrite32(lower_32_bits(dma_handle), bar0 + DMA_SRC_ADDR_LO);
iowrite32(upper_32_bits(dma_handle), bar0 + DMA_SRC_ADDR_HI);
iowrite32(BUF_SIZE, bar0 + DMA_LEN);
iowrite32(DMA_START, bar0 + DMA_CTL);  // 启动
// 3. 等 FPGA 中断通知完成
wait_event_interruptible(fpga_dev->dma_wq, fpga_dev->dma_done);
// 4. 处理数据、回收缓冲区
process_data(cpu_addr);
```

### 特点

| 维度 | 评价 |
|------|------|
| **带宽** | 高，可达 PCIe 线速（Gen4 x16 ≈ 32GB/s） |
| **CPU 占用** | 极低（只在启动和完成中断时参与） |
| **延迟** | 单次传输有启动开销（μs 级），持续流式传输延迟低 |
| **适用数据量** | MB~GB 级大数据 |
| **实现复杂度** | 高，需要 FPGA 端实现 DMA 引擎 + 驱动端管理描述符队列和中断 |

## 三、方式 3：UIO（Userspace I/O）—— 把 FPGA 寄存器暴露给用户态

### 原理

**UIO（Userspace I/O）** 是一个极简的内核驱动框架：内核只负责处理中断，把设备寄存器的物理地址通过 `mmap` 映射给用户态程序——用户态程序直接 `mmap` 后读写 FPGA 寄存器，**完全绕过内核**。

```plantuml
@startuml
skinparam shadowing false
skinparam rectangle {
  BackgroundColor<<app>> #E3F2FD
  BorderColor<<app>> #1976D2
  BackgroundColor<<kern>> #C8E6C9
  BorderColor<<kern>> #388E3C
  BackgroundColor<<hw>> #FFF9C4
  BorderColor<<hw>> #F9A825
}
rectangle "用户态程序" <<app>> as APP
rectangle "UIO 内核驱动\n(极简:只处理中断)" <<kern>> as UIO
rectangle "FPGA\n(PCIe EP)" <<hw>> as FPGA
APP -> UIO : ① open(/dev/uio0)
UIO -> FPGA : ② 内核映射 FPGA BAR 物理地址
APP -> FPGA : ③ mmap → 用户态直接读写 FPGA BAR!\n(不经过内核,没有 syscall 开销)
FPGA -> UIO : ④ FPGA 中断 → UIO 驱动
UIO -> APP : ⑤ read(/dev/uio0) 阻塞等待中断\n(或 poll/epoll)
@enduml
```

### 代码示例

```c
// 用户态程序
int fd = open("/dev/uio0", O_RDWR);
// mmap FPGA 的 BAR 空间到用户态
void *bar0 = mmap(NULL, BAR0_SIZE, PROT_READ | PROT_WRITE,
                  MAP_SHARED, fd, 0);
// 直接读写 FPGA 寄存器！（没有系统调用）
volatile uint32_t *reg = (uint32_t *)(bar0 + REG_OFFSET);
*reg = CMD_START;                    // 写命令
uint32_t status = *reg;              // 读状态
// 等中断
uint32_t irq_count;
read(fd, &irq_count, sizeof(irq_count));  // 阻塞直到 FPGA 发中断
```

### 特点

| 维度 | 评价 |
|------|------|
| **延迟** | 极低（无系统调用、无内核往返） |
| **吞吐** | 同 MMIO（受限于 CPU 搬数据的速度） |
| **开发难度** | 低（用户态编程，不用写内核驱动） |
| **安全性** | 差（用户态直接访问硬件，没有权限控制） |
| **DMA 支持** | UIO 本身不支持 DMA；需要配合 `uio_dmem_genirq` 等变体，或自己写混合驱动 |
| **适用场景** | 快速原型、实验室环境、对安全要求不高的场景 |

> **关键限制**：UIO 只映射 BAR 空间（MMIO），不做 DMA 管理。如果 FPGA 需要 DMA，必须在用户态管理 DMA 缓冲区——这涉及到物理地址获取、cache 一致性等问题，用户态很难做对。

## 四、方式 4：VFIO —— 把整个 FPGA 设备直通给用户态

### 原理

**VFIO（Virtual Function I/O）** 比 UIO 更进一步：它利用 IOMMU（Intel VT-d / AMD-Vi），把**整个 PCIe 设备的 BAR 空间、DMA 能力、MSI-X 中断**全部安全地暴露给用户态程序。这是 DPDK/SPDK 等高性能用户态驱动的基础。

```plantuml
@startuml
skinparam shadowing false
skinparam rectangle {
  BackgroundColor<<app>> #E3F2FD
  BorderColor<<app>> #1976D2
  BackgroundColor<<kern>> #C8E6C9
  BorderColor<<kern>> #388E3C
  BackgroundColor<<hw>> #FFF9C4
  BorderColor<<hw>> #F9A825
  BackgroundColor<<iommu>> #FFCDD2
  BorderColor<<iommu>> #C62828
}

rectangle "用户态程序\n(DPDK/SPDK/自研驱动)" <<app>> as APP
rectangle "VFIO 内核框架" <<kern>> as VFIO
rectangle "IOMMU\n(地址翻译+隔离)" <<iommu>> as IOMMU
rectangle "FPGA\n(PCIe Endpoint)" <<hw>> as FPGA
rectangle "主机内存" <<hw>> as MEM

' == 控制面：垂直分层 (用户态 → 内核 → 硬件) ==
APP -down-> VFIO : ① open /dev/vfio/XX
VFIO -down-> IOMMU : ② 创建 IOMMU domain\n配置 IOVA → PA 页表
APP -down-> FPGA : ③ mmap BAR\n用户态直接读写 FPGA 寄存器
APP -down-> MEM : ④ 分配 DMA 缓冲区\nVFIO_IOMMU_MAP_DMA ioctl

' 硬件层三个盒子水平排列
IOMMU -[hidden]right-> FPGA
FPGA -[hidden]right-> MEM

' == 数据面：DMA 写入 + 中断通知 (水平流) ==
FPGA -right-> IOMMU : ⑤ DMA 写: IOVA
IOMMU -left-> MEM : ⑥ 翻译为 PA → 写物理内存
FPGA -up-> VFIO : ⑦ MSI-X 中断
VFIO -up-> APP : ⑧ eventfd/epoll 通知
@enduml
```

### 代码示例

```c
// 用户态 VFIO 驱动的关键步骤
int container = open("/dev/vfio/vfio", O_RDWR);
int group = open("/dev/vfio/XX", O_RDWR);  // FPGA 所在的 IOMMU group
// 绑定 IOMMU
ioctl(group, VFIO_GROUP_SET_CONTAINER, &container);
ioctl(container, VFIO_SET_IOMMU, VFIO_TYPE1_IOMMU);
// 获取设备 fd
int device = ioctl(group, VFIO_GROUP_GET_DEVICE_FD, "0000:01:00.0");
// mmap BAR 到用户态
void *bar0 = mmap(NULL, bar_size, PROT_READ | PROT_WRITE,
                  MAP_SHARED, device, bar0_offset);
// 用户态分配 DMA 内存并注册到 IOMMU
void *dma_buf = mmap(NULL, DMA_BUF_SIZE, PROT_READ | PROT_WRITE,
                     MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);
struct vfio_iommu_type1_dma_map dma_map = {
    .vaddr = (uint64_t)dma_buf,
    .iova  = 0x100000000,  // FPGA DMA 看到的地址
    .size  = DMA_BUF_SIZE,
    .flags = VFIO_DMA_MAP_FLAG_READ | VFIO_DMA_MAP_FLAG_WRITE,
};
ioctl(container, VFIO_IOMMU_MAP_DMA, &dma_map);
// 把 IOVA 告诉 FPGA（通过 MMIO 写寄存器）
iowrite32(0x100000000, bar0 + FPGA_DMA_ADDR_REG);
// 等中断（通过 eventfd）
struct vfio_irq_info irq = { .index = VFIO_PCI_MSIX_IRQ_INDEX };
int eventfd = eventfd(0, 0);
ioctl(device, VFIO_DEVICE_SET_IRQS, &irq_set);  // 绑定 eventfd
poll(&pfd, 1, -1);  // 阻塞等中断
```

### VFIO vs UIO 对比

| 维度 | UIO | VFIO |
|------|-----|------|
| DMA 支持 | 不支持（需自己 hack） | 原生支持（通过 IOMMU） |
| IOMMU 隔离 | 无 | 有（DMA 地址翻译 + 访问控制） |
| MSI-X 中断 | 有限支持 | 完整支持（多向量 + eventfd） |
| 安全 | 差（用户态随意访问物理地址） | 好（IOMMU 限制 DMA 只能访问映射过的内存） |
| 适用场景 | 快速原型、非生产环境 | 生产环境、需要 DMA 的高性能场景 |
| 复杂度 | 低 | 中等 |

## 五、方式 5：`/dev/mem` —— 最原始的方式（不推荐）

```c
// 直接把 FPGA BAR 的物理地址通过 /dev/mem 映射到用户态
int fd = open("/dev/mem", O_RDWR | O_SYNC);
void *bar0 = mmap(NULL, BAR_SIZE, PROT_READ | PROT_WRITE,
                  MAP_SHARED, fd, FPGA_BAR_PHYS_ADDR);
```

| 优点 | 缺点 |
|------|------|
| 零代码，不需要任何驱动 | 完全绕过内核，无任何保护 |
| 调试/验证阶段快速验证 | 无中断支持（只能轮询） |
| | 无 DMA 管理、无 cache 一致性 |
| | **生产环境绝对不能用** |

## 六、方式 6：专用内核驱动 + 字符设备接口

这是最传统也是最灵活的方式——为 FPGA 写一个完整的内核驱动，通过 `/dev/fpga0` 等字符设备向上层暴露 `read/write/mmap/ioctl` 接口：

```bash
用户态程序
    │
    ├── open("/dev/fpga0")
    ├── ioctl(FPGA_IOC_SEND_CMD, &cmd)    // 控制命令
    ├── ioctl(FPGA_IOC_DMA_START, &dma)   // 启动 DMA 传输
    ├── mmap(...)                          // 映射 DMA 缓冲区到用户态
    └── read(...) / poll(...)              // 等待完成通知
```

**优点**：最灵活，可以根据 FPGA 的具体功能定制接口（加密/压缩/网络处理各有各的接口语义）。

**缺点**：开发工作量大，需要写完整的内核驱动。

## 七、全方式对比总结

```plantuml
@startuml
skinparam shadowing false
skinparam rectangle {
  BackgroundColor<<low>> #FFCDD2
  BorderColor<<low>> #C62828
  BackgroundColor<<mid>> #FFF9C4
  BorderColor<<mid>> #F9A825
  BackgroundColor<<high>> #C8E6C9
  BorderColor<<high>> #388E3C
}
rectangle "/dev/mem\n最原始\n无中断无DMA" <<low>> as M1
rectangle "MMIO\n(ioremap)\n控制路径" <<mid>> as M2
rectangle "UIO\n用户态 MMIO\n低延迟原型" <<mid>> as M3
rectangle "专用内核驱动\n最灵活\n最复杂" <<high>> as M4
rectangle "VFIO\n用户态 DMA\nIOMMU 隔离" <<high>> as M5
M1 -right-> M2 : 加内核驱动
M2 -down-> M3 : 移到用户态
M2 -down-> M4 : 加 DMA + 设备接口
M4 -right-> M5 : 用户态 + IOMMU
@enduml
```

| 方式 | 数据路径 | 控制路径 | 中断 | DMA | IOMMU | 用户态编程 | 适用数据量 | 推荐度 |
|------|---------|---------|------|-----|-------|-----------|-----------|--------|
| `/dev/mem` | MMIO | MMIO | ❌ | ❌ | ❌ | ✅ | KB | ⛔ 仅调试 |
| MMIO (内核) | MMIO | MMIO | ✅ | ❌ | ❌ | ❌ | KB | ⭐⭐ |
| UIO | MMIO | MMIO | ✅ | ❌ | ❌ | ✅ | KB | ⭐⭐⭐ 原型 |
| 专用内核驱动 | DMA | MMIO | ✅ | ✅ | 可选 | ❌ | MB~GB | ⭐⭐⭐⭐ |
| VFIO | DMA | MMIO | ✅ | ✅ | ✅ | ✅ | MB~GB | ⭐⭐⭐⭐⭐ |

## 八、实际场景选型建议

| 场景 | 推荐方式 | 原因 |
|------|---------|------|
| **调试/验证 FPGA 寄存器读写** | `/dev/mem` 或 UIO | 零代码启动，快速验证硬件 |
| **实验室原型、快速迭代** | UIO | 用户态编程，开发效率高 |
| **控制类应用（少量配置数据）** | MMIO（内核驱动）或 UIO | 数据量小，DMA 的复杂度不划算 |
| **数据流处理（加密/压缩/网络处理）** | VFIO 或专用内核驱动 + DMA | 数据量大（MB~GB/s），必须 DMA |
| **生产环境高性能 FPGA 应用** | VFIO | IOMMU 隔离 + 用户态 DMA + MSI-X，性能和安全兼得 |
| **需要集成进现有内核子系统** | 专用内核驱动 | 如把 FPGA 实现为网卡（netdev）、块设备（blkdev）、媒体设备（V4L2） |

## 九、观测与排查

```bash
# 查看 FPGA 的 PCIe 设备信息
lspci -vvv -s 01:00.0 | grep -E "Region|BAR|Bus Master"
# 查看 IOMMU 分组（VFIO 需要）
ls -la /sys/kernel/iommu_groups/*/devices/
# 查看 DMA 映射情况
cat /sys/kernel/debug/dma-api/dump
# 绑定设备到 vfio-pci 驱动
echo "vfio-pci" > /sys/bus/pci/devices/0000:01:00.0/driver_override
echo "0000:01:00.0" > /sys/bus/pci/drivers/xxxx/unbind
echo "0000:01:00.0" > /sys/bus/pci/drivers/vfio-pci/bind
# 查看中断情况
cat /proc/interrupts | grep -E "vfio|fpga|uio"
# DMA 性能测试
perf stat -e dma-engine,bus-cycles dd if=/dev/fpga0 of=/dev/null bs=1M count=1024
```

## 十、和本仓库其他文档的关系

- **DMA 原理**：[dma.md](/concepts/io/dma.md)——FPGA DMA 引擎的工作原理、为什么必须用 DMA
- **PCIe 总线**：[pcie.md](/concepts/io/pcie/pcie.md)——FPGA 挂载的物理总线、BAR 空间分配、MMIO 原理
- **中断处理**：[../process/interrupts.md](/concepts/process/interrupts.md)——FPGA DMA 完成后通过 MSI-X 中断通知 CPU
- **NUMA**：[../numa/numa.md](/concepts/numa/numa.md)——FPGA 插在哪个 NUMA 节点的 PCIe 槽上，DMA 缓冲区和 CPU 应在同一节点

## 十一、一句话总结

> **Linux 与 FPGA 通信分两条路——控制走 MMIO（CPU 主动读写 FPGA BAR，`ioread32`/`iowrite32`，内核态或 UIO 用户态），数据走 DMA（FPGA 主动搬大批量数据进出主机内存，VFIO 或专用内核驱动管理描述符队列和 IOMMU 映射）。选型原则很简单：KB 级控制数据用 MMIO/UIO，MB~GB 级流式数据必须上 DMA+VFIO。**

