PCIE_DMA流程

你这个问题本质是在问三件事:

  1. lspci 是怎么“读到 BAR 信息”的
  2. CPU 访问 BAR 时底层用的是什么协议
  3. PCIe 整体通信流程到底怎么走

我给你从软件 → 协议 → FPGA接口一层层拆开。


一、lspci -vv 访问 BAR,用的是什么协议?

👉 答案:PCIe Configuration Space 访问(配置空间协议)


1️⃣ lspci 干了什么

当你执行:

1
lspci -vv -s 01:00.0

本质是在读:

1
PCIe Configuration Space(配置空间)

2️⃣ BAR 在哪里?

BAR寄存器在:

1
PCI配置空间 offset 0x10 ~ 0x24

3️⃣ 底层协议

👉 使用的是:

1
PCIe TLP:Configuration Read / Write

4️⃣ 流程

1
2
3
4
5
lspci
→ 内核 pci_read_config_dword()
→ 生成 Config Read TLP
→ 发到 PCIe 设备
FPGA PCIe IP 自动响应

5️⃣ 关键点

👉 这个阶段 FPGA 用户逻辑完全不参与

因为:

  • Config Space 是 PCIe IP 硬核处理的
  • BAR 只是“寄存器值”

二、访问 BAR 数据,用的是什么协议?

👉 答案:Memory Read / Memory Write TLP


1️⃣ 场景

例如:

1
*(volatile uint32_t *)bar0 = 0x1234;

2️⃣ 底层协议

👉 转换成:

1
PCIe Memory Write TLP

3️⃣ 完整流程

写(Host → FPGA)

1
2
3
4
5
6
7
8
CPU store
MMIO(内存访问)
PCIe Root Complex
Memory Write TLP
FPGA PCIe IP
s_axis_cq
AXI-Lite
→ 寄存器

读(Host → FPGA → Host)

1
2
3
4
5
6
CPU load
→ Memory Read TLP
→ FPGA
→ 你逻辑返回数据
→ Completion TLP
→ Host

4️⃣ FPGA接口对应

操作 PCIe接口
BAR写 s_axis_cq
BAR读响应 m_axis_cc

三、两种访问的本质区别(核心)

类型 用途 TLP类型 FPGA是否参与
lspci 枚举设备 Config TLP ❌ 不参与
访问BAR 数据交互 Memory TLP ✅ 参与

四、PCIe完整通信流程(重点)


1️⃣ 分层结构

1
2
3
4
5
6
7
应用层(你的FPGA逻辑)

Transaction Layer(TLP)

Data Link Layer(ACK/重传)

Physical Layer(SerDes)

2️⃣ 三种核心通信路径


🔵 场景1:枚举设备(lspci)

1
2
3
4
Host
Config Read TLP
→ FPGA PCIe IP
→ 返回 BAR 信息

👉 不经过你逻辑


🔵 场景2:CPU访问BAR(控制路径)

1
2
3
4
5
6
CPU
→ Memory Write
→ PCIe TLP
→ FPGA
→ AXI-Lite
→ 寄存器

👉 用于:

  • 控制
  • 配置
  • 状态读取

🔵 场景3:DMA(数据路径)


FPGA → Host(发数据)

1
2
3
4
FPGA
→ m_axis_rq
→ Memory Write TLP
→ Host DDR

FPGA ← Host(取数据)

1
2
3
4
FPGA
→ 发 Memory Read
→ Host返回 Completion
→ s_axis_rc

五、用一个统一模型帮你彻底理解

你可以这样记:

1
PCIe = 三条通道

1️⃣ 配置通道(Config)

👉 给系统用

  • lspci
  • 枚举设备
  • BAR大小

2️⃣ 控制通道(BAR / MMIO)

👉 给驱动用

  • 寄存器访问
  • 启停DMA
  • 配置队列

3️⃣ 数据通道(DMA)

👉 给数据用

  • 网络包
  • AI数据
  • 大流量

六、结合你 FPGA 设计(非常关键)


1️⃣ 你必须实现的只有两个

✅ BAR逻辑(AXI-Lite)

处理:

1
s_axis_cq → 寄存器

✅ DMA逻辑

处理:

1
m_axis_rq / s_axis_rc

2️⃣ 你不用管的

👉 Config Space

因为:

  • PCIe IP 已经实现

七、常见误区(你一定要避免)


❌ 误区1

“lspci 是访问 BAR”

👉 错!

✔ 它访问的是:

1
Config Space

❌ 误区2

“BAR 是一种协议”

👉 错!

✔ BAR 是:

1
地址映射机制

❌ 误区3

“PCIe 就是 AXI”

👉 错!

✔ 实际:

1
PCIe → TLP → AXI(IP内部转换)

八、一句话总结(核心认知)

👉 lspci 用的是 Config TLP(配置空间)
👉 访问 BAR 用的是 Memory TLP(内存读写)
👉 真正数据传输靠 DMA(也是 Memory TLP)


如果你下一步想更深入,我可以帮你画一个:

👉 “pcie4_uscale_plus + BAR + DMA + Linux驱动”完整时序图(精确到TLP级)

这个能直接让你把整个 PCIe 架构彻底打通。

这段代码本质就是一个典型 Linux 网卡驱动 TX 路径(Descriptor + DMA)实现。你问的核心是:

👉 “DMA 是在哪一步真正启动的?”
👉 “整个发送流程是怎么闭环的?”

我按代码逐行 → DMA触发点 → 完整时序(Host ↔ FPGA)给你拆。


一、这段代码在整个网络栈的位置

函数:

1
cndm_start_xmit(struct sk_buff *skb, struct net_device *ndev)

👉 是 Linux 网络栈的:

1
TCP/IP → dev_queue_xmit → ndo_start_xmit(你这个函数)

👉 作用:

把 skb 转成 DMA 描述符,交给网卡(FPGA)发送


二、逐段讲解(带关键点)


1️⃣ 获取队列 & ring

1
struct cndm_ring *sq = priv->txq;

👉 sq = Send Queue(发送队列)

通常结构:

1
Descriptor Ring(环形队列)

2️⃣ 找当前 descriptor 位置

1
index = sq->prod_ptr & sq->size_mask;

👉 经典 ring buffer:

1
index = prod_ptr % size

3️⃣ 定位 descriptor

1
tx_desc = (struct cndm_desc *)(sq->buf + index*16);

👉 每个 desc 16 字节:

典型结构:

1
| addr (64bit) | len (32bit) | flags |

4️⃣ 处理时间戳(可选)

1
if (shinfo->tx_flags & SKBTX_HW_TSTAMP)

👉 如果需要 PTP 硬件时间戳:

  • 标记 tx_info
  • 后续 FPGA 要回 timestamp

5️⃣ 关键步骤:DMA 映射

1
dma_addr = dma_map_single(dev, skb->data, len, DMA_TO_DEVICE);

🔴 重点:这一步到底做了什么?

👉 不是 DMA 传输!

👉 是:

1
CPU虚拟地址 → PCIe DMA地址(IOMMU/物理地址)

结果:

1
2
3
skb->dataCPU地址)

dma_addrPCIe可访问地址)

6️⃣ 写 descriptor(关键)

1
2
tx_desc->len = len;
tx_desc->addr = dma_addr;

👉 告诉 FPGA:

1
去这个地址取数据,长度是多少

7️⃣ 保存上下文(用于回收)

1
2
tx_info->skb = skb;
tx_info->dma_addr = dma_addr;

👉 用于:

  • DMA完成后 unmap
  • 释放 skb

8️⃣ 更新生产者指针

1
sq->prod_ptr++;

👉 表示:

1
新 descriptor 已准备好

9️⃣ 队列满控制

1
2
if (sq->prod_ptr - sq->cons_ptr >= 128)
netif_tx_stop_queue()

👉 防止 ring overflow


🔟 内存屏障(非常关键)

1
dma_wmb();

👉 保证:

1
2
descriptor 已经写到内存
再通知设备

🔥 11️⃣ 关键点:启动 DMA

1
cndm_sq_write_prod_ptr(sq);

三、DMA 是在哪里真正启动的?

👉 就在这一句:

1
cndm_sq_write_prod_ptr(sq);

它本质做了什么?

通常实现是:

1
writel(sq->prod_ptr, BAR0 + TX_PROD_REG);

👉 也就是:

1
写 FPGA BAR寄存器

🔴 转换成 PCIe 行为:

1
2
3
CPU写寄存器
→ Memory Write TLP
→ FPGA

FPGA 收到后:

1
2
3
发现 prod_ptr 增加
→ 读取 descriptor
→ 发起 DMA(PCIe Memory Read

四、完整发送流程(重点)


🔵 阶段1:CPU准备数据

1
2
3
应用层
skb
start_xmit()

🔵 阶段2:驱动准备 descriptor

1
2
3
dma_map_single()        (地址转换)
写 desc(addr + len)
更新 prod_ptr

🔵 阶段3:通知 FPGA(关键)

1
2
3
writel(prod_ptr)
→ PCIe Memory Write TLP
→ FPGA BAR寄存器

🔵 阶段4:FPGA 启动 DMA

1
2
3
4
FPGA:
读取 descriptor
→ 发 PCIe Memory Read TLP
→ Host DDR

🔵 阶段5:数据进入 FPGA

1
2
3
4
PCIe → s_axis_rc
→ DMA engine
→ FIFO / MAC
→ 发包

🔵 阶段6:发送完成

1
2
3
FPGA:
更新 cons_ptr
(可选)发中断 MSI

🔵 阶段7:驱动回收

1
2
3
中断 or NAPI poll:
dma_unmap_single()
dev_kfree_skb()

五、关键理解(你必须抓住)


❗ DMA 不是 CPU 启动的

👉 CPU 只是:

1
2
1. 准备 descriptor
2. 写 doorbell(prod_ptr)

✅ 真正 DMA 发起者

👉 是 FPGA:

1
FPGA 主动发 PCIe TLP

六、用一句话总结

👉 DMA 启动点 = 写 FPGA doorbell(prod_ptr寄存器)

1
2
3
4
5
6
7
8
CPU:
desc
→ 写 BAR寄存器(doorbell)

FPGA:
看到 doorbell
→ 发 PCIe Memory Read
→ 完成 DMA

七、结合你方向(非常关键)

你现在做 FPGA NIC / AI:

可以扩展:


1️⃣ Descriptor增强

1
| addr | len | AI tag | flow id |

2️⃣ FPGA行为

  • AI分类 → 决定发哪个队列
  • ACL判断 → 是否丢包
  • PTP → 打时间戳

3️⃣ 高阶优化

  • 多队列 TX(RSS / QoS)
  • 零拷贝(DPDK思路)
  • 批量 doorbell(减少 PCIe TLP)

八、如果你要更深入

我可以帮你下一步做:

👉 “FPGA端 DMA Engine(Verilog)如何解析 descriptor + 发 TLP”

包括:

  • m_axis_rq 如何构造 Memory Read
  • Completion 如何处理
  • 和 Linux 驱动完全对齐

这个就是你现在这个驱动的硬件对口实现

这个函数在你这段 TX 代码里是一个很关键但容易被误解的点。我给你从内核语义 → 调用路径 → PTP/硬件时间戳 → 和你FPGA的关系讲清楚。


一、skb_tx_timestamp() 的作用(核心一句话)

👉 为这个 skb 生成“发送时间戳事件”,通知上层(socket/应用)


二、它到底干了什么?

函数:

1
skb_tx_timestamp(skb);

本质是:

1
2
3
根据 skb 的标志位
→ 决定是否要做 TX timestamp
→ 触发 软件 or 硬件 时间戳流程

三、结合你代码看(关键)

你前面有:

1
2
3
4
if (shinfo->tx_flags & SKBTX_HW_TSTAMP) {
shinfo->tx_flags |= SKBTX_IN_PROGRESS;
tx_info->ts_requested = 1;
}

这些 flag 的含义

flag 含义
SKBTX_HW_TSTAMP 用户要求硬件时间戳
SKBTX_IN_PROGRESS 驱动正在处理时间戳

👉 所以这段逻辑是:

1
2
3
用户请求时间戳
→ 标记这个 skb
→ 后面 skb_tx_timestamp() 会处理

四、skb_tx_timestamp() 内部逻辑

它会判断两种情况:


🔵 情况1:软件时间戳

如果没有硬件支持:

1
skb->tstamp = ktime_get_real();

👉 直接打时间(CPU时间)


🔵 情况2:硬件时间戳(你现在的重点)

如果:

1
SKBTX_HW_TSTAMP

👉 那它不会立即生成时间戳

而是:

1
等待驱动 later 回填时间戳

五、完整 TX 时间戳流程(重点)


🔵 Step 1:应用层请求

用户态:

1
setsockopt(... SO_TIMESTAMPING ...)

发送数据时:

1
skb->tx_flags = SKBTX_HW_TSTAMP

🔵 Step 2:驱动 start_xmit

你这段代码:

1
2
tx_info->ts_requested = 1;
skb_tx_timestamp(skb);

👉 作用:

1
注册“这个包需要时间戳”

🔵 Step 3:FPGA发送数据

1
2
3
FPGA:
发包
在 MAC 层打时间戳(PTP)

🔵 Step 4:FPGA回传时间戳

方式:

  • descriptor写回
  • 专用FIFO
  • 寄存器 + 中断

🔵 Step 5:驱动回填时间戳(关键)

驱动在 TX completion 时:

1
skb_tstamp_tx(skb, &hwtstamp);

👉 把 FPGA 时间戳写回 skb


🔵 Step 6:内核通知用户态

通过:

1
socket error queue

用户用:

1
recvmsg(MSG_ERRQUEUE)

拿到时间戳


六、你代码里的关键点

1
skb_tx_timestamp(skb);

👉 它做的是:

1
把 skb 加入“时间戳跟踪体系”

但:

真正时间戳不是这里产生的


七、容易混淆的点(非常重要)


❌ 误区1

“skb_tx_timestamp 就是打时间戳”

👉 错!

✔ 它只是:

1
触发流程 / 注册事件

❌ 误区2

“调用完就有时间戳了”

👉 错!

✔ 硬件时间戳流程是:

1
异步的!

❌ 误区3

“驱动不用管”

👉 错!

✔ 驱动必须实现:

1
skb_tstamp_tx()

八、结合你 FPGA NIC(重点)

你现在这个驱动:

1
tx_info->ts_requested = 1;

👉 FPGA 端必须做:


1️⃣ 在发送时记录时间

1
2
MAC层 / PHY层
PTP timestamp

2️⃣ 把时间戳带回来

常见方式:

1
2
desc writeback
或 completion queue

3️⃣ 驱动处理

1
2
3
if (tx_info->ts_requested) {
skb_tstamp_tx(skb, &ts);
}

九、你这个系统的完整模型(强烈建议记住)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
应用层

SO_TIMESTAMPING

skb->tx_flags

start_xmit()

skb_tx_timestamp() ← 注册

FPGA发送

FPGA产生timestamp

驱动skb_tstamp_tx() ← 回填

用户态recvmsg()

十、一句话总结

👉 skb_tx_timestamp() = 告诉内核“这个包需要 TX 时间戳”,并启动时间戳流程

不是:

❌ 打时间
❌ 获取时间

而是:

注册 + 触发机制


十一、结合你当前项目的关键建议

你做 FPGA + PTP:

必须补齐三块:


1️⃣ FPGA

  • MAC层打时间戳
  • 回传机制

2️⃣ 驱动

  • tx_info->ts_requested
  • completion时调用 skb_tstamp_tx

3️⃣ 用户态

  • SO_TIMESTAMPING
  • recvmsg(MSG_ERRQUEUE)

如果你下一步要深入,我可以帮你:

👉 画一个 “LinuxPTP + FPGA + 驱动 + skb 时间戳完整时序图(纳秒级)”

这个对你做 TSN / PTP 网卡是核心能力。

这段代码很短,但它就是你 TX 路径里最关键的“DMA触发点(doorbell)”。我从代码语义 → PCIe协议 → FPGA侧行为 → 时序保证给你拆清楚。


一、代码逐行解释

1
2
3
4
void cndm_sq_write_prod_ptr(const struct cndm_ring *sq)
{
iowrite32(sq->prod_ptr & 0xffff, sq->db_addr);
}

1️⃣ sq->prod_ptr

👉 发送队列的生产者指针(Producer Pointer)

表示:

1
已经准备好的 descriptor 数量

例如:

1
prod_ptr = 100

说明:

👉 前 100 个 descriptor 已经写好,可以发送


2️⃣ & 0xffff

👉 只取低 16 bit:

1
sq->prod_ptr & 0xffff

原因通常是:

  • FPGA doorbell 寄存器只有 16bit
  • ring size ≤ 64K
  • 或者硬件只关心低位(wrap 设计)

3️⃣ sq->db_addr

👉 doorbell 寄存器地址(非常关键)

本质是:

1
BAR空间中的某个寄存器地址

例如:

1
db_addr = BAR0 + 0x100

4️⃣ iowrite32()

👉 向设备寄存器写 32bit

本质:

1
CPU → MMIO写 → PCIe Memory Write TLP

二、这一句到底做了什么(核心)

1
iowrite32(...)

👉 转换成 PCIe 行为:

1
2
3
4
5
CPU写寄存器
Root Complex
Memory Write TLP
FPGA PCIe IP
BAR寄存器

三、为什么叫 Doorbell(门铃)

👉 这就是标准 NIC / NVMe 模型:

1
2
3
4
5
6
CPU:
写 descriptor

按门铃(doorbell)

设备开始干活

四、完整 TX 启动流程(结合你代码)


🔵 Step 1:CPU准备 descriptor

1
2
3
tx_desc->addr = dma_addr;
tx_desc->len = len;
sq->prod_ptr++;

🔵 Step 2:内存屏障

1
dma_wmb();

👉 保证:

1
descriptor 已写入内存

🔵 Step 3:敲门(关键)

1
iowrite32(prod_ptr, db_addr);

🔵 Step 4:PCIe 传输

1
2
Memory Write TLP
→ FPGA

🔵 Step 5:FPGA 收到 doorbell

FPGA逻辑:

1
2
if (new_prod_ptr != old_prod_ptr)
→ 有新包

🔵 Step 6:FPGA 启动 DMA

1
2
3
读取 descriptor
→ 发 PCIe Memory Read TLP
→ Host DDR

🔵 Step 7:数据发送

1
DMA → FIFO → MAC → 发包

五、FPGA 侧对应逻辑(你必须实现)


1️⃣ doorbell寄存器

1
2
3
4
always @(posedge clk) begin
if (bar_wr && addr == DB_ADDR)
prod_ptr <= wdata;
end

2️⃣ 检测新任务

1
2
if (prod_ptr != cons_ptr)
start_dma <= 1;

3️⃣ 读取 descriptor

1
desc = desc_mem[cons_ptr];

4️⃣ 发起 PCIe DMA

1
m_axis_rq → Memory Read TLP

六、为什么必须用 doorbell?

👉 因为 FPGA 不会主动扫内存!

如果没有这一步:

1
2
3
CPU写完 descriptor
→ FPGA不知道
→ DMA不会启动

七、为什么要 dma_wmb()(关键配合)

顺序必须是:

1
2
3
1. 写 descriptor
2. dma_wmb()
3. 写 doorbell

否则:

❌ FPGA可能看到新 prod_ptr
但 descriptor 还没写完

👉 直接读到垃圾数据


八、常见问题(你很可能会遇到)


❗ 1. 写了 doorbell 但 FPGA没反应

检查:

  • BAR地址是否对
  • FPGA是否正确decode
  • PCIe写是否到达(ILA抓)

❗ 2. DMA读到错误数据

99%原因:

1
dma_wmb() 缺失

❗ 3. ring乱序

原因:

  • prod_ptr / cons_ptr 没同步好
  • FPGA没有做 wrap 处理

九、这一句的本质(必须记住)

1
iowrite32(prod_ptr, db_addr);

👉 本质是:

1
2
3
通过 PCIe Memory Write TLP
通知 FPGA:
“有新 descriptor,可以开始 DMA 了”

十、一句话总结

👉 这行代码不是写寄存器,而是“启动整个 DMA 发送链路”


十一、结合你当前方向(给你一个提升点)

你可以在这个 doorbell 机制上做优化:


1️⃣ 批量 doorbell

减少 PCIe TLP:

1
2
if (batch_ready)
iowrite32(...)

2️⃣ 多队列 doorbell

1
2
queue0 → db0
queue1 → db1

3️⃣ AI调度

1
AI分类 → 不同 queue → 不同 doorbell

如果你下一步想深入,我可以帮你做一个:

👉 “FPGA DMA Engine(m_axis_rq 发 TLP)完整 Verilog 模板”

直接对接你这个驱动,做到:

  • descriptor解析
  • PCIe读请求生成
  • completion处理

这一步就是从“会用PCIe”到“做高端NIC”的分水岭。


PCIE_DMA流程
http://witbit.cn/FPGA/taxi/PCIE_DMA流程.html
作者
朝彻
发布于
2026年3月23日
许可协议