ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

统一NI CAN驱动接口:NiCanDrv封装NICAN与NI-XNET实战

统一NI CAN驱动接口:NiCanDrv封装NICAN与NI-XNET实战 简介本资源是面向.NET与VBA开发者的NI-XNET CAN通信适配中间件专为解决高级语言无法直接调用NI官方C接口的痛点而设计适用于车辆网络仿真、工业CAN设备控制及自动化测试等场景。压缩包仅含2个核心文件1个C源文件实现底层封装1个H头文件定义接口函数总大小2KB结构精简、即插即用便于集成到Visual Studio或Office VBA工程中。已有791人学习下载反映出其在跨语言CAN开发中的实际需求热度。开发者可直接调用封装后的初始化、报文收发、过滤器配置等标准化方法无需处理指针内存管理与异常映射显著降低NI-XNET在.NET Framework和Excel/Word VBA环境中的使用门槛同时保留原生API的实时性与稳定性。 做汽车电子或者工业自动化测试的朋友应该都绕不开CAN总线。早些年我一直在用NI的CAN接口卡先是用NICAN老驱动后来因为项目要上64位系统和实时系统被NI-XNET的架构逼着改了两轮代码最后实在受不了两套API逻辑割裂干脆自己写了一个叫NiCanDrv的通信库函数封装层把NICAN和NI-XNET统一成同一套接口。这篇就聊聊这个库的设计思路、底层实现和踩坑记录希望对正在做NI平台CAN通信开发、或者准备从NICAN迁到NI-XNET的团队有点帮助。1. 为什么会有NiCanDrvNICAN与NI-XNET的路线之争1.1 经典NICAN驱动的功与过NICAN这代驱动在NI的CAN产品线上存活了很多年当年几乎所有的LabVIEW测控项目里只要能找到PCI-CAN板卡就一定能看到ncOpenComport、ncWriteNet、ncReadNet这种前缀的函数调用。它的好处是简单直接面向端口操作把CAN收发抽象成类似串口的方式一个端口号、一个波特率、一个收发函数就能跑起来对现场工程师来说非常友好。但真把它用到复杂项目里问题就来了。第一是64位系统兼容性。NICAN早期版本的驱动在Windows 7 64位和Windows 10 64位上经常出现句柄不释放、内存占用持续增长的问题尤其是程序反复开关端口的时候跑几小时就会把系统资源耗尽。第二是时间戳精度不够NICAN的时间戳一般是毫秒级或者以系统tick为单位做高精度报文记录和ECU诊断时根本不够用。第三它只支持CAN 2.0A/B不支持CAN FD也不支持LIN、FlexRay这些协议一旦项目要扩总线协议这套接口就废了。1.2 NI-XNET的架构优势与迁移痛点NI-XNET是NI后来的新一代驱动协议栈支持CAN、CAN FD、LIN、FlexRayAPI风格从面向端口变成了面向会话Session。它不再是一个全局端口号就能搞定的而是要创建会话、配置属性、启动会话、收发帧、停止会话、关闭会话一整套生命周期管理。好处很明显以帧为核心的数据结构更完整每个帧自带仲裁ID、数据长度码DLC、帧类型、64位纳秒时间戳支持多接口高精度同步支持加载DBC数据库然后通过信号名读写数据。但坏处也很明显——学习曲线陡峭。官方示例代码动辄上百行配置条目多错误码复杂初次上手的工程师很容易被NX_ERROR 0xBFF62008这种错误码搞到崩溃。而且如果你原来用NICAN写了几百个子VI迁移到XNET不是改一个函数名那么简单是整套调用逻辑都要重写。我当年接手的项目就是这样老代码全部基于NICAN新硬件只支持XNET驱动我就需要在不改动上层业务逻辑的前提下把底层通讯全部换掉于是就有了NiCanDrv这个库。1.3 封装统一库的目标和收益做NiCanDrv的目标很明确向上层提供一个统一的、和硬件驱动无关的CAN通信接口向下封装NICAN和NI-XNET两套后端让调用方不需要关心底层用的是老硬件还是新硬件。这个库要同时支持Windows和Linux实时系统要支持标准CAN和CAN FD要支持同步收发和异步轮询还要在极端情况下能自恢复。封装完成之后收益非常直接。第一同一个测试软件可以无缝切换不同的硬件和驱动不用改业务代码。第二新项目推广到别的团队时只需要告诉他们用NiCanDrv的四个函数不需要每个人都理解XNET的会话机制。第三后续升级驱动版本或者增加新硬件只需在底层适配层做增量开发对上层完全透明。2. NiCanDrv通信库的整体设计与架构思路2.1 统一API设计一套接口适配两套后端我设计NiCanDrv的第一原则就是接口小、语义清楚。整个通信库对外暴露的函数加起来不到15个核心的收发只有4个分别是初始化、发送一帧、接收一帧、关闭释放。这样设计的好处是上层逻辑非常简单类似于用串口一样操作CAN任何工程师看一眼头文件就能上手。为了做到两套后端能切换我在中间加了一层驱动适配层。每个后端实现相同的内部函数指针编译时通过宏选择启用哪个后端或者运行时通过配置文件决定。实现时我在结构体里放了一组函数指针初始化时根据驱动类型填充不同的函数地址上层调用的时候走统一入口再由入口分发给当前生效的适配器这样就不会在业务代码里出现一堆if判断是NICAN还是XNET。2.2 关键数据结构帧、配置与错误码的归一化设计统一数据结构是这个库里面最费心思的部分。NICAN和XNET的帧格式长得完全不一样NICAN的收发函数是按参数一个个传的XNET则是用nxFrame_t结构体一把梭。为了让两套接口在一个屋檐下工作我自己定义了一套内部帧结构体。配置结构体里面放了通道号、波特率、驱动类型、接口名称、CAN FD使能标志等字段。这里还有个小细节就是接口名称字段。XNET后端需要CAN1、CAN2这样的字符串来识别硬件资源而NICAN后端只需要端口号所以我保存了一份驱动类型枚举初始化时根据类型决定是只读端口号还是拼接口名字符串。错误码部分也做了统一映射。NICAN的返回值和XNET的错误码完全不是一套体系不统一的话上层根本没法做判断。我规定库里的返回值统一为整数0代表成功负数代表失败并且保留一个获取最近错误字符串的接口方便调试时快速定位是哪一层出了问题。2.3 驱动适配层的实现方式编译期还是运行期这里有个取舍问题编译期确定后端还是运行期动态选择后端。如果只在某个项目里用编译期宏是一个简单办法编译器只把需要的后端编译进去生成出来的库体积小。但我的使用场景比较复杂同一个库希望既能接老硬件又能接新硬件所以我实现了运行期选择。做法是将NICAN和XNET的调用代码分别封装成两个模块模块之间有统一的回调接口不直接互相依赖。在初始化函数里根据配置结构体里的驱动类型字段决定填充哪一组回调指针。这样做也有个缺点两套后端的代码都会被编译进库里库体积大一些但换来的是灵活性这个取舍我觉得值得。2.4 从源码到交付DLL/SO的动态库导出策略CAN通信库的最终形态我建议直接用动态库。Windows下编译成DLLLinux RT下编译成.so这样上层无论是C、C#还是LabVIEW都能通过标准的库引用方式来调用。导出函数时要注意不要导出C的类方法最好导出一组extern C的纯函数因为LabVIEW的Call Library Function Node只认这种形式。3. 核心功能模块的实现与实操细节3.1 初始化流程通道探测、波特率映射与采样点设置NiCanDrv的通信库初始化函数是整个库的地基。很多初学者上来就忽略初始化细节拿个默认波特率跑结果发现CAN通信时好时坏这个问题十有八九出在波特率和采样点配置上。NICAN后端初始化相对简单一个ncOpenComport函数把端口号、波特率、CAN类型传进去就行老驱动内部对采样点采用的是固定策略给到12MHz晶振下的典型值但并不能保证每种总线拓扑下都是最优。XNET后端就复杂得多不仅要创建会话、指定协议栈模式还要设置波特率属性和采样点属性下面给出一个CAN FD初始化会话的示例代码。static int32_t xnet_init(const NiCanConfig_t* cfg) { nxSessionRef_t sessionRef 0; nxCreateSession(cfg-dbName, cfg-clusterName, cfg-intfName, nxMode_CS_ReadWrite, sessionRef); if (cfg-canFdEnabled) { nxSetProperty(sessionRef, nxProp_Session_Protocol, nxProtocol_CAN_FD, 0); } nxSetProperty(sessionRef, nxProp_Can_BaudRate, cfg-baudRate, 0); if (cfg-fdBaudRate 0) { nxSetProperty(sessionRef, nxProp_Can_FDBaudRate, cfg-fdBaudRate, 0); } nxSetProperty(sessionRef, nxProp_Can_SamplingPoint, cfg-samplePointPercent, 0); nxStart(sessionRef, 0); g_sessionRef sessionRef; return 0; }手动配置采样点时我一般按75%到80%这个区间来设这是一个兼顾绝大多数总线拓扑的经验值。采样点太靠前容易采样到信号边沿太靠后远端节点发出的帧如果传播延迟较大就会在采样点处读到不稳定电平。3.2 标准帧/扩展帧收发与DLC规范化处理CAN帧看似简单但如果每帧数据都在业务代码里去判断是标准帧还是扩展帧、是数据帧还是远程帧代码就很容易写成一片混乱。因此在NiCanDrv里我直接在中层就把帧封装成一个结构体并在接收处理时把驱动原始帧统一转换成内部结构体。这里有一个容易出错的地方DLC和数据长度的关系。在CAN 2.0里DLC最大值是8但在CAN FD里DLC可以大于8用的是压缩编码例如DLC12表示16字节DLC15表示64字节。如果直接把DLC当成实际数据长度用上层使用数据时会越界或读空数据这个问题经常在传统CAN向CAN FD迁移时出现。我在转换函数里写了一个映射表把DLC安全地换算成真实数据长度。3.3 CAN FD与硬件时间戳的处理如果用的是带CAN FD功能的XNET硬件通信库在适配层里需要主动去识别帧的FD标志和BRS标志位并将其映射到统一结构体。XNET的nxFrame_t里面自带了一个flags字段其中会标出CAN_FD_DATA和CAN_FD_BRS之类的标志不处理这几个标志上层就无法区分这是一帧传统CAN还是一帧CAN FD。硬件时间戳的处理是我在这个项目里很看重的一环。NICAN传统接口的时间信息非常粗糙精度在毫秒级甚至更低而XNET可以提供64位纳秒级硬件时间戳。适配层在把原始帧转成统一帧结构体时会把XNET的纳秒时间戳直接存到uint64_t字段中这样应用层在做报文记录、间隔分析、总线负载计算时数据就过硬了。如果是NICAN后端我会用系统时钟补一个软时间戳并在字段里做个标记让上层知道这个时间不是硬件级的。3.4 错误处理机制总线状态诊断与超时重试CAN总线长期运行难免碰到错误帧、总线关闭、节点离线这些状况。设计通信库的时候我特意加了一个底层状态探测函数主动去查询总线状态和接口状态而不是等收发超时了再被动报错。XNET后端可以查询nxProp_Session_StateEx等属性NICAN后端则可以通过专用函数读端口状态。推荐的轮询周期一般是100到200毫秒太长会导致掉线之后很长时间才发现太短则会占用太多的CPU时间。发现总线状态异常后我采取的恢复策略是停止会话、重新初始化端口、再启动会话这个流程正常需要约200毫秒对大多数测试流程来说是可以接受的。需要注意的是恢复流程中要把设备上收到的残留消息清空避免恢复后立刻读出一堆脏数据。4. 常见问题与排查实战记录4.1 从NICAN迁移到XNET时最容易踩的三个坑第一个坑是两个驱动的接口命名不一样。NICAN用整型端口号XNET用字符串接口名。实际项目中经常有人写了一个1然后XNET把1当成接口名找CAN1结果连不上其实可能接口名是CAN2。我的建议是初始化配置里明确写清楚接口名字符串不要图省事。第二个坑是XNET会话被别的进程占用。如果你用NI-XNET的同时还开着NI的Bus Monitor或者另一套软件也打开了同一块接口卡XNET会直接报错。NICAN时代这种冲突不明显因为老驱动没有强制独占会话。解决的办法是设计通信库初始化时加一个检测占用的接口初始化前先尝试打开看看失败就给出明确的提示而不是让用户自己去翻日志。第三个坑是CAN FD的波特率配置。XNET的CAN FD节点要把仲裁段波特率和数据段波特率分开配置很多人只设了仲裁波特率结果FD帧一发送就报错。因为数据段的波特率如果还停留在默认值根本匹配不上对端这也是我在统一配置结构体里加fdBaudRate字段的原因。4.2 会话运行中掉线后的自愈方案实际使用过程中最烦人的就是硬件明明连着但跑着跑着XNET会话突然没响应。早期排查时我发现并不是板卡坏了而是总线上出现了严重的错误状态或者报文缓冲区溢出。针对这种情况我在通信库内部实现了一个自愈逻辑接收线程每次调用读取函数后都会检查返回值如果连续多次返回超时或错误就触发一次轻量级复位这种轻量级复位不会把整个会话都销毁重建只是停止收发再重新启动可以保留大部分已配置的属性。经过几十台设备的实际测试这套自愈流程可以在几百毫秒内把通信恢复到正常状态应用层几乎无感。4.3 结构体对齐、大小端与数据解析问题在Windows的Visual Studio和Linux GCC之间跨平台编译时结构体对齐问题经常把人折磨得够呛。XNET的nxFrame_t在Windows下默认对齐是4字节在Linux下如果不加处理可能变成8字节直接导致从DLL或者.so里取回的数据错位。这个问题的排查方案很简单在头文件里显式声明pack(1)或者更稳妥的方式是不要把驱动原始结构体暴露给上层在适配层内部就完成转换。我最终选的是后者所有上层代码只和NiCanDrv自己的统一结构体打交道这个结构体我也显式设置了1字节对齐这样任何平台、任何编译器下解析结果都是一致的。另外在大小端问题上CAN总线上传输的数据本身是大端模式而x86处理器是小端模式。有些工程师直接把uint8_t数组强转成uint16_t读结果得到的大小端完全反了。我在处理报文解析时专门写了一个字节序转换工具函数凡是涉及多字节数值的物理量都统一走这个函数避免数据出现高低字节互换。4.4 多后端共存时的调试技巧用NI-XNET工具链验证当通信库同时支持NICAN和XNET后端时调试问题的难度会成倍增加因为你很难判断是库写错了还是硬件出问题了。我的习惯是先用NI的官方工具验证硬件链路再回来测自己的库。如果是XNET后端我一般会把NI-XNET Bus Monitor打开直接看看总线上有没有我们发出的报文以及报文的ID、DLC、数据是否和预期一致。如果是NICAN后端用老的NI-CAN测试面板也能看到类似的收发信息。这一步可以快速把问题范围从库的适配代码缩小到上层业务逻辑或硬件接线。4.5 常见问题速查表现象可能原因解决建议XNET初始化失败接口名写错或会话被占用先关掉Bus Monitor用MAX查看接口名CAN FD报文发送失败数据段波特率未配置在配置中单独设置fdBaudRate接收数据出现错位结构体对齐不一致统一使用内部数据结构并设置pack(1)长时间运行后卡死句柄或会话资源未释放检查是否每次复位都调用了关闭接口总线掉线后无法自动恢复错误恢复流程不完整增加清空残留消息的步骤5. 通信库的二次开发与后续扩展方向5.1 在LabVIEW中高效调用NiCanDrv很多工程师虽然会用C语言写驱动但最终的应用层是在LabVIEW里搭的这会涉及到CLFNCall Library Function Node调用动态库的细节。在CLFN配置里最关键的是参数类型的声明结构体要选Struct类型并配好对应的数据布局字符串参数要指定为C字符串指针。踩过坑之后我总结出一个原则不要在LabVIEW里直接传递复杂的嵌套结构体最好是把配置参数拆成若干个独立的整型和字符串参数分别传给初始化函数。这样CLFN配置简单也不容易因为结构体对齐不匹配导致数据错乱。数据收发函数返回时可以返回一个整型错误码具体错误信息再通过单独的字符串查询接口获取这是最稳妥、最方便LabVIEW调用的方式。5.2 与第三方CAN工具链的对接思路在实际项目中我们经常需要和Vector的CANalyzer/CANoe等工具连用。对接方式一般有两种一种是对接网关或接口盒另一种是同一台电脑上运行两套软件。前一种比较稳定后一种则要特别注意总线资源的占用冲突两边同时打开同一个物理CAN通道必然有一方会初始化失败。我的建议是硬件条件允许的情况下尽量用多通道配置一个通道专门跑我们的测试软件另一个通道留给第三方工具做报文监控这样就从根本上避免了对同一通道的竞争。如果只有单通道硬件可以做一个简单的任务队列让测试软件把待发报文放到共享队列由第三方工具负责实际发送但这个方案对工具的二次开发能力要求比较高。5.3 从CAN扩展到LIN、FlexRay的升级路径NiCanDrv这套通信库的设计有一个好处就是内部框架和具体总线协议是解耦的。将来如果要支持LIN或者FlexRay不需要推翻重来只需要在适配层增加一套新的后端实现并且把统一帧结构体做相应的扩展即可。以LIN为例它的调度表、帧头响应机制和CAN完全不同但API层面依然可以抽象成初始化、发送一帧、接收一帧这种结构只是数据结构里要增加LIN特有的校验和相关字段。FlexRay就更接近XNET的原生优势了通过设置相应的集群属性和通信周期参数可以参考CAN的适配方式做一套FlexRay后端。如果一开始就把通信库里的数据和协议语义剥离开后面扩展总线类型就是模板化的工作。6. 一点个人经验与心路总结这个NiCanDrv通信库从立项到稳定运行我前后迭代了两版。第一版做得太急很多逻辑直接在适配层里写了硬编码结果换了一块不同型号的XNET网卡就出问题。第二版把所有硬件相关的差异都收敛到配置结构和适配函数里API保持稳定后面加新功能就不再需要大改了。在实际项目中最让我省心的其实是统一了错误码和调试手段这两件事。以前排查NICAN和XNET的问题要翻两套文档对着两套错误码表自言自语现在所有问题都回到NiCanDrv这一层一句话就能说出问题出在驱动、硬件还是上层代码。如果你也在维护一套长期运行的测试系统又正好被NICAN和NI-XNET这两套API折磨过我建议不妨也抽点时间做一层薄薄的封装。不用做得很庞大把收发初始化和错误处理这几个核心点统一起来长期收益真的很高。最后再分享一个小技巧不管驱动层怎么封装通信库的代码里一定要留调试打印的开关不要把它删掉。平时静默运行不输出出问题的时候打开环境变量就能看到每次调用底层驱动函数的返回值很多疑难杂症都会瞬间暴露出来。本文还有配套的精品资源点击获取
返回列表