日期: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 数 |
RAM (paddr[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*sensor、vp_mask = 1 << sensor。
本板(data_plane=0, sensor=0)→ hif=0x2000300, vp=0x1000, vp_mask=1。
2.3 数据面寄存器模型(VP/HIF)
与 DataChannel::configure_roce(src/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=0、rq_psn=0、path_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_IMM;imm = ntohl(wc.imm_data),
page = imm & 0xFFF,psn = (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 控制面偶发丢事务)
三层互补的重试/规避:
- 脚本层 :
hl_write()对每次 hololink-write 重试至 15 次(其start()
突发任一步失败都会整体崩溃,重试即可)。 - 分析器层 :枚举/start/数据面配置整体重试 5 次;每个寄存器写使用
Timeout(10, 0.2)(10s 内 0.2s 间隔重试,替代默认 0.5s/0.1s)。 - 规避层 :
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_analyzer、build/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 内容确实随帧到达主机 |
延迟测量说明(三种"延迟")
- FPGA 内部延迟 (
fpga-latency):同一 FPGA 时钟域内metadata_ts − timestamp,
无需对时,精确。实测 0.64 µs。 - 到达抖动 (
host-interval对比fpga-interval):FPGA 侧零抖动而主机侧
jitter≈0.39 ms,反映网络/NIC/主机调度的排队变化,不是绝对延迟。 - 端到端单向延迟 :需要 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. 已知注意事项
- pynq 控制面偶发丢事务 :ramplayer 开流时更明显(每次 hololink-write 的
start()配置突发中可能超时)。两个脚本已对每次写自动重试(最多 15 次);
分析器对 枚举/start/数据面配置 整体重试(5 次)并对单写使用 10s 超时
(Timeout(10, 0.2))。若仍失败,重跑即可。 - hololink-write 值域限制 :工具用
std::stoi解析,值/地址的 bit31 置位会
抛std::out_of_range(如 0xAAAA...)。脚本填充图样请使用 ≤ 0x7FFFFFFF 的值。 - payload 字节/字序 :实测帧内 dword j 对应窗口首 word 的 lane j(小端字节序);
窗口起始 word 依 FSM 进入状态为 word 0 或 1(ram_player.svIDLE/WAIT_TIME
路径对 rd_addr 预加 1 的差异)。--verify-pattern默认按 word 起始 1 校验,
仅作调试用。 - PTP/序列号 :分析器设置了
ptp_enable=0(跳过 start() 里易超时的 PTP
寄存器写;时间戳为 FPGA 自由运行时钟,帧间隔仍有效)与
sequence_number_checking=0(避免与脚本并发写产生 SEQUENCE_CHECK_FAIL)。 - QP 类型为 UC (Unreliable Connected),与
RoceReceiver一致;
dest_qp_num=0、rq_psn=0、GID 取 RoCEv2/IPv4 映射格式(::FFFF:a.b.c.d)。