RAM Player 实验:hololink-write 开流 + RoCEv2 接收分析

日期:2026-09-16

硬件:pynq RFSoC 开发板(HSB IP 0x2606,IP 192.168.0.2,MAC CA:FE:C0:FF:EE:00)

主机:mlx5_1(enp33s0f1np1,192.168.0.101/24,RoCEv2)

1. 概述

复制代码
┌─ FPGA (pynq/rfsoc-pynq) ─────────────────────────────┐
│  ram_player (APB psel[4] → 0x5000_0000)              │
│    RAM(512×512bit) ──AXI-Stream──▶ HOLOLINK_top      │
│    寄存器: 0x5000_0004 enable / 0x5000_0008 timer     │
│           0x5000_000C window_size / 0x5000_0010 num  │
│           0x5010_0000+ RAM 内容                       │
│         每 window_size 字节 = 1 帧 ─▶ RoCEv2 (UC QP)  │
└──────────────────────┬───────────────────────────────┘
                       │ RDMA WRITE(帧末 write-with-immediate)
                       ▼
┌─ 主机 ────────────────────────────────────────────────┐
│  roce_ramplayer_analyzer:                             │
│   1) bootp 枚举 → hif=0x2000300, vp=0x1000, vp_mask=1  │
│   2) ibverbs UC QP + 页式接收环(frame+128B metadata)  │
│   3) 写 FPGA VP/HIF 数据面寄存器(QPN/rkey/VA/MAC/IP)  │
│   4) 轮询完成队列,解析 FrameMetadata,统计              │
└───────────────────────────────────────────────────────┘

整个实验由两个部件组成:ramplayer_on/off.sh (通过 hololink-write 拨动 FPGA

寄存器)与 roce_ramplayer_analyzer (原生 C++,链接 hololink_core + ibverbs,

不依赖 holoscan-sdk)。

2. 设计原理

2.1 ram_player 模块(FPGA 侧)

源码:fpga/nv_hsb_ip/data_gen/ram_player.sv;在 pynq 顶层

fpga/pynq/rfsoc-pynq/rtl/top/FPGA_top.sv 中以 W_DATA=512 实例化,输出

AXI-Stream 直接驱动 sif_rx_axis_*[0](sensor RX 口 0,也是该口唯一驱动源),

HOLOLINK_top 打包后经 100G 以太网发出。

寄存器 (APB 32 位,paddr[21:20]==00 选择):

偏移 名称 说明
0x00 scratch 读写测试用
0x04 enable bit0 ram_ena;bit1 ptp_ena;bit2 loop_dis;bit3 ptp_bram_ena
0x08 timer 相邻两个 window 之间的等待拍数(usr_clk,本设计 ≈322.27 MHz)
0x0C window_size 每 window 字节数 = 主机看到的 1 帧
0x10 window_num 每次突发包含的 window 数

RAMpaddr[21:20]==01 选择,基址 +0x10_0000):512 字 × 512 bit,

s_apb_ram_dyn 拆成 16 个 32 位 bank,按 paddr[31:11] 选 bank、paddr[10:2]

选字 ------ 因此帧 dword j ↔ RAM word(j/16+起始偏移) 的 lane(j%16)

APB 地址 = 0x5010_0000 + lane*0x800 + word*4(交错映射,非线性)。

FSM (IDLE→SEND_DATA→WAIT_TIME/DONE):ram_ena 置位后按 window_size 流出一个

window,window_num 个 window 为一轮;timer==0 时一轮结束进 DONE(loop_dis=0

则进 WAIT_TIME 循环),非零 timer 时每轮之间等待 timer 拍。窗口首 word 依进入

路径为 word 0 或 1(IDLE/WAIT_TIME 分支对 rd_addr 预加 1 的差异)。

2.2 控制面地址推导(0x5000_0000 的来历)

HSB 的 APB 互连是两级译码(fpga/nv_hsb_ip/lib_apb/apb_intc_top.sv +

reg_map/regmap_pkg.sv):

  • Level 1(ADDR_SW_USER=28,按 paddr[31:28]):0x0=Hololink IP 寄存器;
    0x1...0x8 = 8 个用户 APB 口,psel[i]0x(i+1)000_0000
  • ram_player 挂在 psel[4](FPGA_top.sv),故寄存器基址 = 0x5000_0000
    RAM 基址 = 0x5010_0000

主机侧每条 hololink-write <addr> <value> 命令的生命周期:

bootp 枚举找到板子 → 建立控制会话(start()/configure_hsb,含 PTP/EVT 等

约 15 次寄存器写)→ 发出 1 次目标写 → 退出。这也是脚本每次调用约 1-2s 的

原因(进程级开销),以及为什么它的 start() 突发在 pynq 控制面丢事务时会

偶发超时(见 §6)。

枚举元数据公式src/hololink/core/enumerator.cpp,BasicEnumerationStrategy):

板子 bootp 报文携带 data_plane=0,主机据此推导 ------

hif_address = 0x0200_0300 + 0x10000*data_plane

sif_address = 0x0100_0000 + 0x10000*sensor

vp_address = 0x1000 + 0x40*sensorvp_mask = 1 << sensor

本板(data_plane=0, sensor=0)→ hif=0x2000300, vp=0x1000, vp_mask=1。

2.3 数据面寄存器模型(VP/HIF)

DataChannel::configure_rocesrc/hololink/core/data_channel.cpp)完全

一致的两组寄存器(src/hololink/core/data_channel.hpp):

地址 寄存器 本实验值 作用
hif+0x04 DP_PACKET_SIZE 11 每网络包载荷(128B 页)=1408B(MTU1500−RoCE 开销74,向下取 128B 整倍)
hif+0x08 DP_PACKET_UDP_PORT 12288 FPGA 源 UDP 端口
hif+0x0C DP_VP_MASK bit0 每 VP 一位发送使能;先清零再配置,最后置位开流
vp+0x00 DP_QP 分析器 QPN 目的 QP 号
vp+0x04 DP_RKEY 分析器 MR rkey RDMA 写权限
vp+0x08/0x0C DP_PAGE_LSB/MSB 收环 VA 页 0 的 64 位虚地址
vp+0x10 DP_PAGE_INC 33 页步长(128B 页)=4224B
vp+0x14 DP_MAX_BUFF 63 最后页索引(页数−1)
vp+0x18 DP_BUFFER_LENGTH 4096 帧长:收满这么多字节即产生帧尾(=ram_player window_size)
vp+0x20/0x24 DP_HOST_MAC 00:00:00:00:00:01 目的 MAC
vp+0x28 DP_HOST_IP 192.168.0.101 目的 IP(inet_network 序)
vp+0x2C DP_HOST_UDP_PORT 4791 目的 UDP 端口(RoCEv2 标准端口)

帧/包/页关系:FPGA 把 VP 数据按 DP_PACKET_SIZE(1408B)切成网络包,每个包是

一条 RDMA WRITE,写入 DP_PAGE + 页内偏移;收满 DP_BUFFER_LENGTH 字节后,最后

一个包用 write-with-immediate 发出,imm 低 12 位携带落盘页号。页写满

DP_MAX_BUFF 后回卷到页 0(环形)。

2.4 RoCEv2 接收设计(主机分析器)

复刻 src/hololink/operators/roce_receiver/roce_receiver.cpp 的最小实现:

  • QP 类型 UC (Unreliable Connected):HSB 单向流不需要可靠连接;
    dest_qp_num=0rq_psn=0path_mtu=4096
  • GID :RoCEv2 + IPv4 映射格式 ::FFFF:a.b.c.d(subnet_prefix=0,
    interface_id=(ip_nbo<<32)|0xFFFF0000);本地按路由出口网卡的 IPv4 扫 GID
    表自动选定 ibv 设备与 gid_index(-d 可覆盖)。
  • 接收缓冲 :64 页环形(默认),页 = round_up(frame,128) + 128B metadata;
    posix_memalign(4096)ibv_reg_mr 注册后把 VA/rkey 交给 FPGA。
  • 接收 WR :UC 下每个 write-with-imm 消耗一个 recv WR(无 sge);
    启动投 2048 个,每完成一帧补投一个,避免耗尽。
  • 完成解析wc.opcode==IBV_WC_RECV_RDMA_WITH_IMMimm = ntohl(wc.imm_data)
    page = imm & 0xFFFpsn = (imm >> 12) & 0xFFFFF(同
    RoceReceiver::page_from_imm/psn_from_imm)。
  • FrameMetadata :页内 frame 之后的 128B,大端:
    flags(4) psn(4) crc(4) timestamp_s(8) timestamp_ns(4) bytes_written(8) pad(2) frame_number(2) metadata_s(8) metadata_ns(4)
    (同 hololink.cpp::deserialize_metadata)。

2.5 统计口径

统计项 计算方式
frames / fps 每个 write-with-imm 完成计 1 帧;fps = 帧数/壁钟时间
Mb/s Σmetadata.bytes_written × 8 / 壁钟时间
dropped frame_number(16 位)相邻帧差 −1(<0x8000 计入)
psn_mismatch metadata.psn 低 20 位 ≠ imm 中 PSN(same_psn 判据)
size_err bytes_written ≠ 期望帧长(window_size)
fpga-interval 相邻帧 metadata.timestamp 差:均值/最小/最大/抖动(标准差)
host-interval 相邻帧主机收到时刻(CLOCK_MONOTONIC)差:同上四项
fpga-latency metadata_ts − timestamp(同一 FPGA 时钟域):均值/最小/最大
rtt 启动时 N 次读 0x80 的 UDP 往返:min/avg/max

2.6 可靠性设计(针对 pynq 控制面偶发丢事务)

三层互补的重试/规避:

  1. 脚本层hl_write() 对每次 hololink-write 重试至 15 次(其 start()
    突发任一步失败都会整体崩溃,重试即可)。
  2. 分析器层 :枚举/start/数据面配置整体重试 5 次;每个寄存器写使用
    Timeout(10, 0.2)(10s 内 0.2s 间隔重试,替代默认 0.5s/0.1s)。
  3. 规避层metadata["ptp_enable"]=0 跳过 start() 中 PTP 寄存器写
    (最易超时;时间戳为 FPGA 自由运行时钟,帧间隔仍有效);
    metadata["sequence_number_checking"]=0 关闭控制面序号校验,避免与
    并发的 hololink-write 会话互相顶号(SEQUENCE_CHECK_FAIL)。

3. 构建命令

bash 复制代码
cd /home/ruler/ex_holoscan/tmp08_buils_hsb_test_no_hsdk/holoscan-sensor-bridge

# 配置(首次已完成则跳过;fmt/GTest 由 CMake 自动 FetchContent)
cmake -S . -B build -G Ninja \
    -D HOLOLINK_BUILD_ONLY_NATIVE=ON \
    -D CMAKE_BUILD_TYPE=Release

# 编译(分析器在 tools/roce_ramplayer_analyzer/,链接原生 hololink_core + ibverbs,无需 holoscan-sdk)
cmake --build build -j 64

# 安装(可选;得到 bin/roce_ramplayer_analyzer、bin/ramplayer_on.sh、bin/ramplayer_off.sh)
cmake --install build --prefix /home/ruler/ex_holoscan/tmp08_buils_hsb_test_no_hsdk/hsb-local

产物位置:

  • build 树内:build/tools/roce_ramplayer_analyzer/roce_ramplayer_analyzerbuild/tools/write/hololink-write
  • 安装后:hsb-local/bin/{roce_ramplayer_analyzer, ramplayer_on.sh, ramplayer_off.sh, hololink-write}

4. 运行方式

bash 复制代码
cd /home/ruler/ex_holoscan/tmp08_buils_hsb_test_no_hsdk/holoscan-sensor-bridge
export PATH=$PWD/build/tools/write:$PATH
S=$PWD/tools/roce_ramplayer_analyzer

# ① 后台启动 RoCEv2 分析器(自动枚举、配 QP、写 FPGA 数据面寄存器)
#    timeout 须覆盖 配置(含重试)+on/off脚本往返+采集时间,给 120s 兜底
stdbuf -o0 -e0 timeout 120 ./build/tools/roce_ramplayer_analyzer/roce_ramplayer_analyzer \
    -f 4096 -i 3 --dump-first > /tmp/final.log 2>&1 &
APID=$!

# ② 等待其打印 "receiving..."(控制面就绪、数据面已配置);夭折则报错
ok=0
for i in $(seq 1 40); do
    sleep 1
    grep -q "receiving" /tmp/final.log 2>/dev/null && { ok=1; break; }
    kill -0 "$APID" 2>/dev/null || break
done
[ "$ok" = 1 ] || { echo "analyzer failed:" >&2; cat /tmp/final.log >&2; exit 1; }

# ③ 用 hololink-write 打开 ramplayer(4KiB/帧,每 10M usr_clk ≈ 31ms 一帧)
$S/ramplayer_on.sh -s 4096 -n 1 -t 10000000 -f 0 2>/dev/null | tail -1

sleep 11          # 采集 ~11 秒

# ④ 关闭 ramplayer,通知分析器收尾并等 TOTAL 打印完成
$S/ramplayer_off.sh 2>/dev/null | tail -1
kill -INT "$APID" 2>/dev/null
wait "$APID" 2>/dev/null
cat /tmp/final.log

注意(初版脚本教训):timeout 30 过短 ------ 分析器配置(枚举+start+写寄存器,

控制面丢事务时还会重试)和 on/off 脚本(每次 hololink-write 一次进程级

start()+写往返,5 次写约 5-15s)加起来常超过 30s,会导致分析器在 ramplayer

真正开流前就被 timeout 杀掉,统计全为 0。仓库根目录的 hello.sh 是上述

修正后的可直接运行版本。

脚本参数:ramplayer_on.sh [-H 板IP] [-s window_size] [-n window_num] [-t timer周期] [-f 填充dword数]

-f N>0 会先向 RAM 写入递增 dword 图样(帧 dword j → RAM word(j/16+1) lane(j%16),

地址 0x5010_0000 + lane*0x800 + word*4);每个 dword 一次 hololink-write 往返,较慢,默认 1024。

分析器常用参数:-f 帧长(=window_size)、--pages N(收页环大小)、-n 帧数上限

-i 统计周期秒--dump-first(首帧 hex+metadata)、-v(逐帧日志)、

--verify-pattern(校验递增图样)、--no-config(跳过 FPGA 数据面配置)、

--rtt=N(控制面 RTT 探测采样数,默认 20,0 关闭)、

-d ibdev(指定 RDMA 设备,默认按路由自动选)。

延迟测量快捷脚本 run_latency.sh(仓库根目录):bash ./run_latency.sh [帧长=4096] [timer=1000000] [采集秒=15],默认 ~322 fps 采集 15s 并自动汇总 rtt / fpga-latency /

到达抖动三类指标。实测输出:

复制代码
rtt: control-plane round-trip min 0.035 ms avg 148.399 ms max 599.995 ms (31/50 samples)
[interval] frames=1612 ... | 322.4 fps, 10.6 Mb/s | fpga-interval mean 3.104 ms jitter 0.000 ms
         | host-interval mean 3.103 ms jitter 0.296 ms | fpga-latency mean 0.64 us
[TOTAL] frames=9537 dropped=0 psn_mismatch=0 ... | nic rx_write_requests=38148

5. 实测结果与解释

一次完整运行(4KiB/帧、timer=10M)实测输出:

复制代码
board: peer=192.168.0.2 data_plane=0 sensor=0 hif=0x2000300 vp=0x1000 vp_mask=0x1
host: ip=192.168.0.101 dev=enp33s0f1np1 mac=00:00:00:00:00:01
buffer: 270336 bytes = 64 pages x 4224 (frame 0x1000 + metadata 0x80)
roce: device=mlx5_1 port=1 gid_index=3
qp: qpn=434 rkey=0x1bf5f3
fpga: data plane configured (payload=1408B/packet, vp_mask=0x1 enabled)
receiving... (Ctrl-C to stop)
first frame head:
  00000100010001000200010003000100
  04000100050001000600010007000100
  ...
metadata: flags=0x1 psn=0xc17 crc=0xebf134dc ts=32467.806152830 bytes_written=512 frame_number=1171 meta_ts=32838.626686330
[interval] frames=97 dropped=0 psn_mismatch=0 size_err=0 pattern_err=0 wc_err=0 other_op=0 | 32.3 fps, 1.1 Mb/s | fpga-interval mean 31.036 ms jitter 0.000 ms | host-interval mean 31.030 ms jitter 0.386 ms | fpga-latency mean 0.64 us
[TOTAL] frames=548 dropped=0 psn_mismatch=0 size_err=0 pattern_err=0 wc_err=0 other_op=0 | ... | nic rx_write_requests=2192

字段解释:

字段 含义 实测解读
frames 收到的完整帧数(write-with-imm 完成) 开流期间稳定 ~97 帧/3s
fps / Mb/s 帧率 / 有效载荷吞吐 32.3 fps ≈ 1/(31.036ms),与 timer=10M×(1/322.27MHz) 一致(usr_clk=100G CMAC 时钟)
fpga-interval mean/min/max/jitter 由 FPGA 时间戳计算的帧间隔统计 全部 31.036 ms、jitter=0 ------ FPGA 侧定时完全精确
host-interval mean/min/max/jitter 主机收到时刻的帧间隔统计(CLOCK_MONOTONIC) mean 31.03 ms、jitter ~0.39 ms ------ 网络+NIC+驱动引入的到达抖动
fpga-latency mean/min/max FPGA 内部延迟:metadata 发出 − 帧首数据到达(同一 FPGA 时钟域) ~0.64 µs ------ 纯流水线转发,几乎无缓冲
rtt(启动时探测) 控制面 UDP 读寄存器往返 min/avg/max min ≈ 37 µs 是链路往返真实值;avg/max 被丢事务重试污染,看 min 即可
dropped 由 metadata frame_number 跳号推断的丢帧 0
psn_mismatch metadata 包序号与 immediate 中 PSN 不一致次数 0(无丢包)
size_err bytes_written ≠ window_size 的帧 首 2 帧是配置切换前的 512B 残留帧(flags=0x1),属预期
flags FPGA 帧标志位分布 稳态全 0
nic rx_write_requests mlx5 硬件计数器增量 ≈帧数×4(每帧 3 个数据包 + 1 个帧尾 imm),量级一致
首帧 hex dump 0000 0100 0100 0200 ... 小端 dword = 0x00010000+L 即 RAM word0 的 lane0...15 图样,证明 RAM 内容确实随帧到达主机

延迟测量说明(三种"延迟")

  1. FPGA 内部延迟fpga-latency):同一 FPGA 时钟域内 metadata_ts − timestamp
    无需对时,精确。实测 0.64 µs。
  2. 到达抖动host-interval 对比 fpga-interval):FPGA 侧零抖动而主机侧
    jitter≈0.39 ms,反映网络/NIC/主机调度的排队变化,不是绝对延迟
  3. 端到端单向延迟 :需要 FPGA 与主机 PTP 对时才有意义。本程序设置
    ptp_enable=0(FPGA 时钟自由运行),故不算绝对延迟。若需测量:
    主机对 enp33s0f1np1 跑 ptp4l + phc2sys,去掉分析器中
    metadata["ptp_enable"]=0 一行,即可用
    主机收到时刻(CLOCK_REALTIME) − metadata_ts 得到单向延迟。

rtt 到底测的是哪段链路

rtt控制面一次 UDP 寄存器读事务(read_uint32(0x80))的完整主机→FPGA→主机
往返
,分段为:

复制代码
主机用户态 ─①UDP请求→ 内核UDP/IP栈+驱动 ─②→ mlx5 NIC MAC/PCS ─③→ 100G网线传播
─④→ FPGA: 以太网rx解析 → 控制指令译码(ECB) → APB读0x80 ─⑤→ FPGA组响应包+MAC发送
─⑥→ 原路返回主机用户态      = 一个 rtt 样本

各段延迟与占比分析(以实测 min ≈ 35 µs 为基准估算):

环节 延迟估算 占比 性质
③④ 100G 网线传播 + 两端 NIC MAC/PCS(双向) ~1-2 µs ~3-6% 纯硬件链路,固定小
①②⑥ 主机 UDP/IP 协议栈 + NIC 驱动 + 系统调用(双向) ~5-15 µs ~15-40% 主机软件路径
FPGA 控制面处理:rx 解析 → ECB 指令译码 → APB 读 0x80 → 组响应包 → MAC 发送 ~15-25 µs ~45-70% 主要成分:控制 FSM 非线速,逐周期访问寄存器

(三段中值 1.5+10+20 ≈ 31.5 µs ≈ 实测 35 µs,自洽。)

因此 rtt 是"主机用户态 ↔ FPGA 控制寄存器"这条控制链路 的延迟,是纯链路

传播延迟的上界 ;它不代表 RoCE 数据面路径。两者对照:

控制面(rtt 所测) 数据面(ramplayer 帧)
FPGA 内路径 控制 FSM + APB 寄存器访问(慢,~15-25 µs) dp_pkt 纯硬件流水线(实测 fpga-latency 0.64 µs
主机内路径 UDP socket + 协议栈 + 用户态读写(~5-15 µs) NIC RDMA 直写内存 + WC 完成事件(无 socket)
帧数据是否经过 ------ 不经过 rtt 路径的这些慢速环节

数据面真实单向延迟 = FPGA 流水线(0.64 µs)+ 链路传播(~1 µs)+ NIC RDMA

处理(µs 级),远小于 rtt;精确值需 PTP 对时(见上一条)。

6. 已知注意事项

  1. pynq 控制面偶发丢事务 :ramplayer 开流时更明显(每次 hololink-write 的
    start() 配置突发中可能超时)。两个脚本已对每次写自动重试(最多 15 次);
    分析器对 枚举/start/数据面配置 整体重试(5 次)并对单写使用 10s 超时
    Timeout(10, 0.2))。若仍失败,重跑即可。
  2. hololink-write 值域限制 :工具用 std::stoi 解析,值/地址的 bit31 置位会
    std::out_of_range(如 0xAAAA...)。脚本填充图样请使用 ≤ 0x7FFFFFFF 的值。
  3. payload 字节/字序 :实测帧内 dword j 对应窗口首 word 的 lane j(小端字节序);
    窗口起始 word 依 FSM 进入状态为 word 0 或 1(ram_player.sv IDLE/WAIT_TIME
    路径对 rd_addr 预加 1 的差异)。--verify-pattern 默认按 word 起始 1 校验,
    仅作调试用。
  4. PTP/序列号 :分析器设置了 ptp_enable=0(跳过 start() 里易超时的 PTP
    寄存器写;时间戳为 FPGA 自由运行时钟,帧间隔仍有效)与
    sequence_number_checking=0(避免与脚本并发写产生 SEQUENCE_CHECK_FAIL)。
  5. QP 类型为 UC (Unreliable Connected),与 RoceReceiver 一致;
    dest_qp_num=0rq_psn=0、GID 取 RoCEv2/IPv4 映射格式(::FFFF:a.b.c.d)。
相关推荐
FPGA小迷弟12 小时前
FPGA 应用技术能力的完整闭环(5-8)
fpga开发·dsp开发·fpga·sdr·无线电
Eloudy18 小时前
vitis 工程 ping FPGA 丢包(~50% 随机)问题分析
fpga
学习FPGA的电气小兴兴21 小时前
基于FPGA的CORDIC向量模式实现arctan运算,Verilog HDL实现
嵌入式硬件·fpga开发·数字信号处理·verilog·fpga·cordic·arctan
FPGA小迷弟2 天前
FPGA 应用技术能力的完整闭环(1-4)
fpga开发·dsp开发·fpga·sdr·无线电
NamoAmitabha1282 天前
vivado时序报红/插入流水线寄存器/降低扇出
fpga·vivado
明德扬2 天前
AD9653单通道波形怎么看?幅度、频率与基础测量入门
学习·fpga开发·fpga
学习FPGA的电气小兴兴7 天前
基于FPGA的CORDIC旋转模式实现sin和cos运算
fpga开发·信号处理·数字信号处理·verilog·fpga·verilog hdl·cordic
明德扬7 天前
AD9653采集模块怎样连接FPGA底板?从ADC采样到数据处理
学习·fpga开发·fpga
明德扬8 天前
AD9653采集模块、PCIe采集卡和完整采集系统怎么选?
fpga开发·fpga