ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

PMC-5565反射内存卡RTX64驱动与延迟测试实践

PMC-5565反射内存卡RTX64驱动与延迟测试实践

简介:这份资料围绕VMIC GE公司PMC-5565板卡在RTX64 3.x环境下的驱动开发与测试,面向从事嵌入式实时系统、工业控制及航空航天设备开发的工程师与进阶学习者。压缩包共54个文件、约713KB,以C/C++源码、头文件、工程与解决方案文件为主,并包含RTdll动态库、库文件、INF安装文件及可执行测试程序,覆盖驱动封装、RTSS实时程序与Windows互测等环节。已有1231人学习,说明其在rfm2g接口适配与RTX64驱动调试场景中具有一定参考价值。读者可从中梳理PMC-5565在实时扩展环境下的驱动组织方式,理解rfm2g的RTdll封装思路,并借助RTX64测试程序与Windows互测程序对照验证兼容性与稳定性,为类似板卡的驱动移植和排错提供可复用的工程参考。

1. PMC-5565 反射内存卡在 RTX64 下的驱动与测试:一块卡、两个系统、三种延迟

某开发者接手的半实物仿真项目里,控制周期被压到 500 微秒,两台工控机之间用普通网口交换状态量,网络协议栈处理稍一卡顿,模型就发散。后来换成 PMC-5565 反射内存卡,配上 RTX64 3.x 实时子系统的驱动,端到端延迟压到了十几微秒,抖动进了微秒量级。这套源码做的就是这件事:把一块 PMC-5565 反射内存卡在 RTX64 实时环境下驱动起来,并附带可直接运行的读写、中断、DMA 三类测试程序。适合正在做半实物仿真、实时数据采集、多机同步采样的开发者——尤其是那些已经被“软件收包路径”折腾过的团队。跑通它不难,难的是把驱动装对、把中断配顺、把 DMA 缓冲区分配到物理连续内存上,后面几章就围绕这几个点展开。

2. 项目定位与技术栈:RTX64 实时子系统 + RFM2g 接口层

2.1 反射内存卡解决的是确定性,不是带宽

先把这个前提说清楚,否则后面很多设计看不懂。反射内存卡的做法是:每台机器上插一块卡,卡上有独立的板载内存(PMC-5565 常见是 128MB 或 256MB),所有卡用光纤或同轴串成一个环或者接在 hub 上。你往本机卡的内存里写一个字节,卡的硬件逻辑会把这个写操作打包成环网报文发出去,其他所有节点的硬件收到后直接写进各自的板载内存,全程 CPU 不参与接收,不进协议栈,不做中断处理之外的任何软件动作。

带来的结果是:读对端数据,本质上是“读本机内存”,几个指令周期就回来了;写数据,末端节点看到更新的延迟主要由环网节点数和硬件转发决定,通常是微秒量级以内。代价也很明显:带宽只有百兆字节每秒这个量级,节点数一般不超过 256,而且它是强一致广播模型,你没法像 TCP 那样做重传、做拥塞控制。

所以这块卡的正确用法是:周期性的、定长的、小块的、高频的状态交换,比如控制指令、传感器快照、仿真步长同步。它不适合传文件、不适合传图像流。我见过有团队拿它当大文件通道用,一帧 8MB 推过去,带宽直接打满,反而把实时线程饿死了。

2.2 RTX64 3.x 里 RTSS 侧和 Win32 侧的活儿怎么分

RTX64 是在 Windows 上叠一层实时子系统(RTSS),它有自己的调度器、自己的地址空间、自己的驱动加载机制,和 Win32 侧是两套独立的东西。这带来的第一个坑就是:RFM2g 的库有两份。

  • Win32 侧的rfm2g.dll+ 导入库,给普通 Windows 进程用,适合做配置工具、日志记录、人机界面;
  • RTSS 侧的rfm2g.rtdll,给实时进程链接,只有走这一侧才能拿到微秒级的调用延迟。

同一个RFM2gOpen调用,在两侧的返回值、超时行为、甚至设备名解析顺序都可能有差别。项目里的测试程序通常也是两份:一份.exe跑在 Win32 做功能验证和参数读写,一份.rtss跑在 RTSS 做真实的延迟和吞吐打点。如果你只在 Win32 侧测,看到的数字里混着 Windows 的调度抖动,参考价值不大。

另一个分工点是内存。实时侧要用RtAllocateContiguousMemory之类的接口申请物理连续内存,Win32 侧申请的缓冲区在物理页上可能是散的,硬件 DMA 拿到的地址对不上,跑几分钟就崩。这个后面第 4 章会展开。

2.3 技术栈清单与编译环境

把项目需要的环境列一下,缺一个就编不过:

组成说明备注
实时子系统RTX64 3.x(含 SDK 与运行时)版本要与驱动包匹配,3.0 与 3.4 的 Rtapi 有差异
宿主系统Windows 10 / 11 64 位(或对应的嵌入式版本)子系统只支持 64 位
编译工具Visual Studio + RTX64 SDK 集成平台要选 RTX64 64-bit,不是 x64
硬件PMC-5565 卡 + PMC 转 PCI/PCIe 载板载板型号决定了设备枚举顺序
接口库rfm2g_api.h/rfm2g_types.h+ 两侧导入库头文件版本必须和驱动同批
辅助环网线缆、hub(可选)、示波器(测硬件同步时用)双机对传至少两台机器或两个节点

编译配置上有两个细节容易漏。第一,RTSS 工程的运行库要选静态链接或者确保 RTX64 的运行库在目标机 PATH 里,否则RtssRun会报找不到 DLL。第二,头文件包含顺序上rtapi.h要放在rfm2g_api.h之前,因为后者内部用了RtPrintf的宏重定向,反过来会有一堆重定义告警。

3. 目录结构与构建加载:从源码到跑起来的 RTSS 进程

3.1 源码目录分层

包解开之后目录大致是这么分的(不同批次打包可能略有出入,以实际为准):

pmc5565_rtx64/ ├─ driver/ # 驱动本体、INF、数字签名 │ ├─ win32/ # Windows 侧驱动与安装信息 │ └─ rtss/ # 实时子系统侧驱动 ├─ include/ # rfm2g_api.h、rfm2g_types.h、错误码定义 ├─ lib/ # x64 与 RTX64 两套导入库 ├─ tests/ │ ├─ rfm2g_basic/ # 打开、读写、关闭的最小闭环 │ ├─ rfm2g_intr/ # 中断/事件收发 │ ├─ rfm2g_dma/ # 大块搬运吞吐测试 │ └─ rfm2g_latency/ # 延迟打点,RTSS 侧运行 ├─ tools/ # 设备枚举、寄存器 dump、节点号设置 ├─ scripts/ # 安装、启动、停止脚本 └─ docs/ # 板卡手册摘要、偏移分配约定

分层逻辑很直白:include和lib是接口层,driver是内核与实时侧的实现,tests是调用方的四类典型用法,tools是排障用的。二次开发时你基本只动tests和docs里的偏移分配表,前三个目录当黑盒用。

docs里那份偏移分配表是整个项目里最值得先看的东西。反射内存是一块裸内存,没有文件系统也没有表结构,谁写到哪个偏移全靠人肉约定。项目里通常约定前 4KB 保留给控制和握手,比如节点在位标志、心跳计数、全局时间戳,之后按 64KB 或者 1MB 一段切给不同业务。

3.2 编译产物:.rtss、.rtdll、.sys 分别是什么

三种产物别搞混:

  • .sys是内核态驱动,由 Windows 的服务管理器加载,负责枚举 PCI 设备、映射 BAR 空间、挂中断;
  • .rtdll是实时侧的动态库,由 RTSS 进程加载,把RFM2gRead/Write这些调用翻译成对已映射内存的访问;
  • .rtss是实时侧的可执行文件,本质上是 PE 格式但由 RTX64 的加载器接管,用RtssRun启动。

编译时最容易翻车的地方是平台选错。VS 的配置管理器里,“x64”和“RTX64 64-bit”是两个不同平台,前者编出来的是普通 Windows 程序,链接的是rfm2g.lib;后者才会去找rfm2g.rtdll并且生成.rtss后缀。我见过有人把 RTSS 的测试程序编成了 exe,跑起来“也能出数”,但那是 Win32 侧的路径,测出来的延迟完全没有参考意义。

3.3 安装与加载顺序:INF、控制面板、脚本

安装是有严格顺序的,顺序错了就是设备管理器里一个黄色感叹号:

:: 1. 装 RTX64 子系统(如果还没装),装完必须重启 :: 2. 装 PM5565 的 INF:右键 inf 选安装,或用 pnputil pnputil /add-driver driver\win32\pm5565.inf /install :: 3. 确认设备已被识别 devcon status *PMC5565* :: 4. 启动实时子系统(控制面板里 Start,或用服务方式) net start "RTX64 Subsystem" :: 5. 把 RTSS 测试进程拉起来 RtssRun tests\rfm2g_basic\rtx64\Release\rfm2g_basic.rtss

几个命令的含义:pnputil /add-driver是把驱动加入驱动仓库并尝试安装,如果设备已经插着,/install会触发一次匹配;devcon status只是看状态,不修改任何东西,排障时先跑它;RtssRun启动的进程如果不带-w之类的参数,默认不阻塞当前命令行,日志要靠RtPrintf输出到调试器或者重定向到文件。

提示:子系统没起来的时候,RFM2gOpen返回的错误码和“设备不存在”是同一个,很容易误判成驱动没装。排障第一步永远是先确认子系统在跑。

还有一点:PMC 载板的枚举顺序决定了设备名编号。单卡机器上就是rfm2g0,多卡或者多载板的情况下编号可能和你插的槽位顺序不一致。用tools里的枚举工具先把rfm2g0、rfm2g1对应到具体的物理卡,再写死在配置里,别靠猜。

4. 核心模块拆解:从 RFM2gOpen 到 DMA 传输的调用链

4.1 打开设备与配置读取

所有调用都从RFM2gOpen开始,拿到句柄之后才能做后面的事。写法上没太多花样,但错误处理必须做实:

#include <rtapi.h> #include "rfm2g_api.h" #define NODE_NAME "rfm2g0" #define OFF_STATE 0x1000 /* 状态区起始偏移,见 docs 里的分配表 */ static RFM2G_HANDLE g_h = NULL; static RFM2G_CONFIG g_cfg; int rfm_init(void) { RFM2G_STATUS st; st = RFM2gOpen(NODE_NAME, &g_h); if (st != RFM2G_SUCCESS) { RtPrintf("[rfm] open %s failed, status=0x%x\n", NODE_NAME, st); return -1; } st = RFM2gGetConfig(g_h, &g_cfg); if (st != RFM2G_SUCCESS) { RtPrintf("[rfm] get config failed, status=0x%x\n", st); RFM2gClose(g_h); g_h = NULL; return -1; } RtPrintf("[rfm] mem=%u MB node=%u speed=%u\n", (unsigned)(g_cfg.MemorySize >> 20), (unsigned)g_cfg.NodeId, (unsigned)g_cfg.Speed); return 0; }

RFM2gOpen的第一个参数是设备实例名,驱动按枚举顺序分配,编号从 0 起;第二个参数是输出句柄,后续所有调用都要带上它。RFM2gGetConfig把板卡的基本信息填进RFM2G_CONFIG结构,常用字段有三个:MemorySize是板载内存字节数,用来做偏移越界检查;NodeId是环网节点号,全网唯一,重复了链路会出奇怪的问题;Speed反映链路速率档位。

注意DeviceName这个参数在 RTSS 侧和 Win32 侧的解析规则可能不同,有的版本要求带上前缀(例如\\\\.\\rfm2g0这种设备路径形式),有的直接给短名就行。判断方法很简单:如果RFM2gOpen一直失败但设备管理器里卡是正常的,换一种写法试一次,八成能过。

4.2 内存映射读写:偏移、字节序、对齐

RFM2gRead/RFM2gWrite是这套接口里用得最多的两个函数,语义非常朴素:从板卡内存的某个字节偏移读一段到本地缓冲区,或者反过来写。真正容易出事的是结构体布局。

#pragma pack(push, 1) typedef struct { unsigned int seq; /* 递增序号,接收端用来判丢帧 */ unsigned int status; /* 位域状态字 */ double pos[3]; /* 位置,米 */ double vel[3]; /* 速度,米每秒 */ unsigned int crc; /* 简单异或校验,可关 */ } RT_STATE; #pragma pack(pop) int rfm_publish(const RT_STATE *s) { return (RFM2gWrite(g_h, OFF_STATE, (void *)s, sizeof(RT_STATE)) == RFM2G_SUCCESS) ? 0 : -1; } int rfm_fetch(RT_STATE *s) { return (RFM2gRead(g_h, OFF_STATE, (void *)s, sizeof(RT_STATE)) == RFM2G_SUCCESS) ? 0 : -1; }

第一个参数是句柄,第二个是板卡内存内的字节偏移,第三个是本地缓冲区指针,第四个是长度。返回RFM2G_SUCCESS表示这次访问完成,不代表对端已经看到——这点必须清楚,反射内存是硬件广播,写返回的时候数据已经在本地内存里,环上的传播是另一回事。

#pragma pack(1)这行千万别省。默认对齐下编译器会在status后面和vel前后插填充,发送端按 48 字节布局写,接收端如果编译选项不同按 56 字节解释,读到的东西全是错的,而且错得非常“有规律”,容易误判成硬件问题。我一般的做法是在头文件里把所有跨节点结构体集中定义,统一加pack(1),再加一行编译期断言把sizeof卡死,谁改了字段没同步,编不过。

字节序的问题在纯 x86 环境里不存在,但环上只要挂一台大端设备就会暴露。板卡通常有字节交换的配置位,要么在初始化时统一配置,要么在协议层约定好网络序、应用层自己转。项目里如果全是 x86,建议索性在文档里写死“全部小端,不交换”,省得后面有人误开。

4.3 中断与事件:从硬中断到用户回调

轮询能跑,但会浪费 CPU,而且延迟不可控。反射内存卡支持写触发中断:发送端往某个特定偏移写一个约定值,接收端的卡检测到就产生中断,驱动再把这个中断翻译成用户态的“事件”。

static volatile int g_stop = 0; int rfm_event_loop(void) { RFM2G_STATUS st; RT_STATE s; st = RFM2gEnableEvent(g_h, RFM2G_EVENT_INTR); if (st != RFM2G_SUCCESS) { RtPrintf("[rfm] enable event failed: 0x%x\n", st); return -1; } while (!g_stop) { st = RFM2gWaitForEvent(g_h, RFM2G_EVENT_INTR, 100 /* ms */); if (st == RFM2G_SUCCESS) { rfm_fetch(&s); /* 只搬数据,别在这做业务 */ g_last_seq = s.seq; RtSetEvent(g_evtDataReady); /* 唤醒处理线程 */ } else if (st == RFM2G_TIMEOUT) { continue; /* 超时是常态,不是错误 */ } else { RtPrintf("[rfm] wait event err: 0x%x\n", st); break; } } RFM2gDisableEvent(g_h, RFM2G_EVENT_INTR); return 0; }

RFM2gEnableEvent的第二个参数是事件类型,硬件中断用RFM2G_EVENT_INTR,还有一种软件事件RFM2G_EVENT_SOFT,靠本节点自己触发,通常用来做测试和握手,不跨节点传播。RFM2gWaitForEvent的第三个参数是毫秒级超时,返回RFM2G_TIMEOUT是完全正常的状态,别当错误处理。

这段代码里最关键的是那条注释:事件线程里只搬数据、只置标志,不执行业务逻辑。事件线程的优先级被驱动提到了很高,你在里面做一次浮点运算密集的计算或者调一次可能阻塞的接口,整个实时链路的抖动就上去了。业务处理交给另一个线程,用事件或者信号量唤醒。

4.4 DMA 大块搬运与缓冲区分配

小数据块用RFM2gRead/Write就够,走的是 CPU 逐字拷贝。数据量上到几十 KB、上百 KB 一帧的时候,CPU 拷贝会成为瓶颈,这时候要换 DMA。DMA 的接口本身不难,难在缓冲区。

#define DMA_LEN (1024 * 1024) /* 一次搬 1MB */ int rfm_dma_roundtrip(void) { void *buf; RFM2G_UINT32 xfer = 0; RFM2G_STATUS st; /* 必须是物理连续内存,普通分配在 RTSS 侧可能是分页的 */ buf = RtAllocateContiguousMemory(DMA_LEN, 0x1000); if (buf == NULL) { RtPrintf("[rfm] contiguous alloc %u failed\n", DMA_LEN); return -1; } st = RFM2gDmaRead(g_h, OFF_STATE, buf, DMA_LEN, &xfer, 1000); RtPrintf("[rfm] dma read st=0x%x xfer=%u\n", st, (unsigned)xfer); st = RFM2gDmaWrite(g_h, OFF_STATE, buf, DMA_LEN, &xfer, 1000); RtPrintf("[rfm] dma write st=0x%x xfer=%u\n", st, (unsigned)xfer); RtFreeContiguousMemory(buf); return 0; }

参数按顺序是:句柄、板卡偏移、本地缓冲区、长度、实际传输字节数(输出)、超时毫秒。具体签名以rfm2g_api.h为准,不同小版本可能多一个方向标志或者少一个超时参数,编译报参数不匹配的时候改这一处就行。

核心约束在分配方式上。DMA 引擎拿到的必须是物理地址连续的缓冲区,RtAllocateContiguousMemory就是干这个的,第二个参数是对齐粒度,给 4KB 页大小最省事。用普通堆分配(包括 Win32 侧malloc或 RTSS 侧的常规池)在这条链路上会出各种问题:轻则传输出错,重则子系统挂死。这不是玄学,是物理页不连续导致 DMA 地址翻译失败,日志里往往什么线索都没有。

另外,DMA 和中断别混在同一个线程里同步跑。一次 1MB 的 DMA 在百兆字节每秒量级的链路上要几毫秒,这期间如果还在等事件,很容易错过下一帧。

5. 测试程序与避坑排查:跑通双机对传的完整流程

5.1 单机自环和双机对传的拓扑搭建

先跑单机。单机自环的意思是:一台机器上只装一块卡,把卡的发送口和接收口用一根光纤短接(或者接在同一个 hub 的两个口上),然后本机写、本机读。这种拓扑下环网报文发出去绕一圈又回来,能验证驱动装载、内存映射、基本读写是不是通的。PMC-5565 的环网 LED 指示灯通常有“链路建立”和“数据活动”两个状态,单机自环时如果写数据,活动灯应该闪。

单机自环通过之后,再上双机。两台机器,各一块卡,A 的发接 B 的收,B 的发接 A 的收,构成一个环。这个接线方向如果搞反了,链路建不起来,表现是两块卡都报链路错。有的载板丝印上 RX/TX 标得很小,看不清就拿手机拍一张放大看。

节点号必须全网唯一。项目里我一般约定:节点 0 给主控,节点 1 到 N 给从设备,节点号写在板卡的 EEPROM 里,用tools里的工具改,改完要重新上电才生效。曾经有一次两台机器都默认节点 0,链路指示灯一切正常,但数据就是时好时坏——这种问题不看节点号能查一整天。

第三种拓扑是接 hub。hub 的好处是某个节点掉电不会把整个环断掉,代价是引入了额外的转发延迟。做延迟测试的时候要明确当前用的是哪种拓扑,把拓扑写进测试报告,否则数字没法复现。

5.2 延迟与带宽测试程序怎么写

延迟测试的核心是打时间戳。RTSS 侧用 RTX64 提供的 100ns 分辨率时钟取值:

static unsigned __int64 now100ns(void) { RT_CLOCK_TIME t; RtGetClockTime(&t); /* 取 100ns 单位的系统时钟 */ return (unsigned __int64)t.QuadPart; } void latency_loop(int iters) { RT_STATE s = {0}; unsigned __int64 t0, t1, worst = 0, total = 0; for (int i = 0; i < iters; i++) { s.seq = (unsigned int)i; t0 = now100ns(); rfm_publish(&s); /* 写到环上 */ while (g_last_seq != s.seq) { /* 等对端回写,g_last_seq 由事件线程更新 */ RtSleep(0); } t1 = now100ns(); total += (t1 - t0); if ((t1 - t0) > worst) worst = (t1 - t0); } RtPrintf("[lat] avg=%llu.%llu us, worst=%llu.%llu us\n", total / iters / 10, total / iters % 10, worst / 10, worst % 10); }

这段代码测的是往返延迟:本机写出去,对端收到后回写一个序号,本机看到序号变化为止。分母是迭代次数,单位换算上 10 个 100ns 是 1 微秒,所以除以 10 得到微秒。真正的单向延迟需要两端时钟同步,实践中很难做到严格同步,往返延迟除以二是个常见的近似,报告里要写清楚用的是哪种口径。

带宽测试反过来,不关心单次延迟,只看单位时间搬了多少字节。做法是固定发 N 帧 1MB 的 DMA,记总耗时,总字节 / 总时间。注意带宽测试要把发送和接收放在不同节点,并且接收端只做丢弃不落盘,否则瓶颈会变成磁盘。

测试程序的日志要带三样东西:拓扑(自环/双机/hub)、节点数、以及编译时的优化等级。少一样,后面数据对不上就说不清。

5.3 踩坑记录:五类典型故障的现象、原因与解法

现象一:RFM2gOpen一直返回失败,设备管理器里卡正常。原因通常是实时子系统没起来,或者驱动只装了 Win32 侧、RTSS 侧没装。RTX64 的子系统服务停了之后,Win32 侧的设备照样在,但实时侧的库打不开设备。解决:先在控制面板确认子系统状态是 Running,再确认driver/rtss下的驱动装过;两者都正常还失败,就换设备名写法试一次。

现象二:跑几分钟蓝屏,或者子系统直接挂死,日志里没线索。原因是 DMA 缓冲区不是物理连续内存,或者中断处理路径里调了阻塞接口。DMA 地址翻译失败在内核态是致命的,用户态看不到任何东西。解决:所有给 DMA 用的缓冲区一律走RtAllocateContiguousMemory,用完立即释放;中断/事件线程里只允许RFM2gRead和RtSetEvent这类不阻塞的调用。

现象三:能写进去,对端读出来是错位的乱码,但字段值“看着眼熟”。两种可能:一是偏移量不匹配,一端按字节偏移 0x1000 写,另一端按 0x2000 读;二是结构体没做 1 字节对齐,编译器插了填充。解决:把偏移定义集中到一个头文件里,用宏而不是魔数;结构体统一#pragma pack(1),并在头文件里加sizeof断言把长度锁死。

现象四:延迟测试的均值很漂亮,但每隔几百次出现一次几毫秒的尖峰。测试线程跑在 Win32 侧,被系统调度打断;或者跑在 RTSS 侧但优先级不够,被别的实时线程抢了;还有一种隐蔽情况是结构体跨了 cache line,两个核反复抢同一行。解决:测试进程做成.rtss,线程优先级提到 90 以上,热数据结构按 64 字节对齐并独占 cache line。

现象五:双机带宽只有标称的一半,怎么调都上不去。环网上有一台机器掉电或者网口没插好,环没闭合,报文要绕行;或者中间接了 hub 而某个端口速率档位和其他不一致,整条链路降速跑;还有一种是自己代码里多拷贝了一次,接收端先 DMA 到缓冲区再 memcpy 到业务结构,白吃一次内存带宽。解决:先看各节点的链路指示灯,再看 hub 端口状态,最后用性能计数器确认拷贝次数。

6. 二次开发:把 demo 改成你自己的实时数据通道

测试程序跑通之后,真正的工作才开始。我的习惯是先稳住协议层,再动业务层。协议层的做法是定义一套“偏移分配表 + 帧头”,把板卡内存当作一块共享的寄存器区来用,而不是当作一块可以随便写的裸内存:

#pragma pack(push, 1) typedef struct { unsigned int magic; /* 固定值,用来检测初始化状态 */ unsigned int seq; /* 帧序号,接收端判丢帧 */ unsigned int len; /* 有效负载长度 */ unsigned int crc; /* 负载校验 */ unsigned char payload[4088];/* 一帧刚好 4KB,对齐到页 */ } FRAME; #pragma pack(pop)

每类数据分配固定页,控制命令一页、状态回传一页、日志一页,页与页之间留空页做隔离。这么做的好处是二次开发时新增一类数据不用改别人的偏移,也不会因为结构体变大把后面的数据挤掉。代价是内存利用率低,但 128MB 的板卡上这点浪费不值一提。

双缓冲是我强烈建议加的一层。反射内存的写操作不是原子的,对端可能读到一个“改了一半”的结构体:pos已经更新了,vel还是上一帧的。做法是写两页交叉使用,帧头里带seq和valid标志,接收端读到seq变了再校验crc,校验通过才切换。这层逻辑只有十几行,但没有它,偶发的数据撕裂能让你怀疑人生。

验证方法我是这么排的:第一步单机自环跑 10 万次读写,确认没有越界和句柄泄漏;第二步双机对传跑 30 分钟,看seq有没有断号;第三步注入抖动,人为在对端延迟 100 微秒再回写,看业务层会不会崩;第四步才是上真实模型。前三步任何一步不过,都不要往下走。

有一个具体技巧值得说:把RFM2gGetConfig拿到的MemorySize在初始化时打印出来,并且用它对所有偏移做一次越界断言。看着很傻,但我遇到过两次板卡实际内存比手册写的小的情况,写越界之后表现是链路随机丢包,查了整整两天。从那以后,每次新板卡上机,我都强制先跑一遍配置读取,把内存大小、节点号、链路速率三个值抄进日志再开始测。希望帮到你。

本文还有配套的精品资源,点击获取

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进