ARTICLE DETAIL

资讯详情

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

HIL仿真中CAN报文Checksum与Counter的实现方法:基于NI Veristand的实战指南

HIL仿真中CAN报文Checksum与Counter的实现方法:基于NI Veristand的实战指南 做HIL测试的朋友应该都被一个问题折磨过仿真环境里明明把CAN报文发出来了被测ECU就是不认轻则信号无效重则直接报故障码。最后排查半天十有八九是Checksum或者Counter没做对。这事在自动驾驶仿真里尤其常见因为现在的主流ECU基本都带报文防篡改和防重放机制如果没有把Checksum和Counter按协议要求算好整个仿真闭环根本跑不起来。我在用NI Veristand搭自动驾驶仿真测试台架时就经常要处理这类问题。Veristand本身对CAN通信的支持已经比较完善但自定义Checksum算法和滚动Counter这一块官方文档讲得比较分散网上能搜到的案例也不多。这篇文章就把我实际用过的方法包括DBC信号定义、Veristand实时循环里的计算逻辑、几种常见校验算法的实现以及踩过的坑一次性整理出来。1. 项目整体设计与思路拆解1.1 为什么HIL仿真里要自己造Checksum和Counter先说清楚应用场景。做自动驾驶域控制器或者整车控制器的HIL测试时被测对象DUT会通过CAN/CAN FD跟台架上的仿真节点互发信息。台架要模拟的是真实车上那一堆ECU比如电池管理系统、车身控制器、智能传感器——它们发出的每一帧报文中可能都带着校验和与滚动计数器。Counter通常叫Rolling Counter或者Alive Counter的用途比较直接接收方靠它判断报文的连续性。如果连续几帧的Counter值不递增接收方就会认为链路出问题了可能走超时降级逻辑甚至直接置故障。Checksum的用途则五花八门有的是做数据完整性校验防止总线上的偶然干扰导致数据错乱有的是做简单的通信认证防止其他节点恶意注入报文。无论哪种只要DUT在接收端做了校验台架发出去的报文就必须能通过校验否则测试用例没法往下走。所以结论很明确在自动驾驶仿真中自定义Checksum和Counter不是可选项而是让HIL测试闭环跑起来的前置条件。这也是我把它当作一块基础能力看待的原因后面所有台架仿真工程只要涉及CAN通信几乎都会用到同一套思路。1.2 方案选型DBC配置加脚本计算还是Custom Device在Veristand里实现Checksum和Counter常见做法大致分两种。第一种是用Veristand的系统映射能力先把DBC中定义的Checksum和Counter信号映射成系统全局变量然后在Veristand的实时执行循环里动态计算并更新这些变量。NI-XNET会自动按照DBC定义把变量值打包进CAN报文发送出去。这个方案的优点是开发量小、上手快、可维护性好适合大多数常规测试场景。第二种是开发一个自定义设备Custom Device在RT Target上以插件形式接管完整的CAN报文生成逻辑。这种做法的灵活性最高能处理非常复杂的Checksum算法、跨帧计算逻辑或者非标加解密但代价是开发周期明显变长还得维护一套代码工程对版本兼容性要求也高。我个人的建议是先按方案一跑通整个链路。如果遇到算法太复杂、或者有特殊触发条件再考虑方案二。这里有个很重要的原因HIL测试工程一旦跑起来排查问题会非常依赖信号值可观测这个能力。用系统变量实现时Counter和Checksum在Veristand的变量浏览器里能直接看到实时变化定位问题非常方便。Custom Device虽然灵活但变量被封装在RT代码里观测起来反而多一层麻烦。1.3 设计前先搞清楚真实ECU到底怎么校这里想多说一句很多人一上来就急着写Checksum代码却忽略了一个前提不同的ECUChecksum算法和Counter更新规则完全不一样。有的用CRC8有的只是把所有数据字节求和后取低字节或取反有的Counter是4位0到15循环有的8位0到255循环还有的会在中间跳过某些值甚至有些ECU会把Checksum算完以后跟一个固定Key异或。所以在设计阶段最该做的是先拿到被测ECU的通信规范或者通过总线逆向分析确认Checksum从哪一字节到哪一字节、结果放在哪一字节、用什么算法、Counter从多少开始、递增间隔是多少。这一步搞清楚后面所有工作都是顺水推舟。我在实际项目中经常看到有人拿着没有Checksum协议的旧DBC文件硬上结果总线上一跑DUT直接进故障处理流程这种错误排查起来最费时间。2. 核心细节解析与实操要点2.1 Checksum和Counter在CAN报文里的常见位置关于字段位置实际工程中没有统一标准我总结了几类常见分布数据区前部有些协议把Counter放在Byte0的高四位Checksum放在Byte7两者分居首尾。数据区尾部很多基于CCP/XCP扩展的标定协议习惯把Checksum放到最后一字节。Counter和Length共用部分车厂协议会把Counter和报文长度编码到同一个字节的高/低四位里这种情况下Counter最大值往往被限制在15以内。在DBC文件中这类信号通常被定义为unsigned类型长度可能是4位或8位。DBC里Signal的起始位定义直接决定这些位在总线上的物理位置。如果定义错了轻则Checksum对不上重则整个报文其他信号都会被解析错。所以拿到协议文档后第一件事就是对照文档把每个信号的起始位、长度、字节顺序在DBC里核对一遍。2.2 几种常见Checksum算法与实现要点我整理一下HIL测试里最常见的四种Checksum算法大家对号入座。第一种是累加和校验。通常把Checksum字段之外的所有数据字节相加超过一字节的进位处理方式分两种直接截取低8位或者带循环进位。带循环进位的写法在很多老式ECU里很常见就是每次相加产生进位以后把进位加回到最低位再取低字节。写代码时要注意这个细节否则算出来的结果经常只差1或2排查起来非常隐蔽。第二种是异或和校验。把所有字节逐次异或得到一字节校验值。异或算法最大的优点是实现简单、速度快但安全性一般。不少国产BMS的私有协议就喜欢用这种。第三种是CRC8。CRC8虽然只有一字节长度但因为有多项式选择、初始值、输入反转、结果异或等一堆参数实际工程中变种非常多。常用多项式有0x1DSAE J1850、0x2FAUTOSAR相关、0x07一般CRC8。实现CRC8时最容易出错的地方是输入数据是否需要反转和计算范围是否包含Checksum字段以外的固定位。同样的多项式初始值不一样算出来的结果完全不同所以拿到协议文档时一定要把参数确认完整。第四种是查表法。很多ECU固件为了追求实时性会用查表方式计算CRC8。对做仿真的人来说只要最终计算结果一致就行不必纠结内部是查表还是逐位算。关于算法逆向我再提供一个小经验如果手头只有抓到的报文没有协议文档那就尽量多抓几组数据把每帧报文的数据段和Checksum字段单独存下来再写个Python脚本暴力尝试上述几种常见算法组合。多数情况下累加和、XOR、查表CRC8都能在半小时内匹配出来。2.3 Rolling Counter的循环逻辑Rolling Counter在DBC里往往被定义成一个普通信号但它的行为本质上是状态累积器。最常见的规则是每一帧新报文发送时加1到达上限后回绕到0。上限可能是0x0F4位、0xFF8位甚至更特殊。在Veristand里模拟这个逻辑最关键的一点是保证递增动作跟报文的发送周期严格同步。假如报文每10ms发一帧Counter也应该是每10ms加1而不是在某个异步任务里随便加。如果用了一个100ms的循环去更新Counter那总线上的报文就会表现为好几帧都不变然后突然跳一下接收端一看就知道不对。另外Counter的初值不是随便定的。有的ECU要求从0开始有的从1开始还有的复位以后从固定偏移开始。DBC文件里可以给信号配置默认值但真正的递增逻辑还是要靠Veristand的变量或脚本去驱动。2.4 Intel和Motorola字节顺序的坑说句实话我在现场排查过太多Checksum对不上的问题最后查下来都是字节序搞错了。CAN数据里的多字节信号有两种排布方式Intel格式小端和Motorola格式大端。对Checksum这种单字节信号本身不存在字节内顺序问题但它在整个报文里的位置定义会受DBC中起始位设定方式的影响。Intel格式和Motorola格式的起始位编号规则不一样在CANdb里信号起始位的写法也不同。一个更隐蔽的坑是如果Checksum算法要求按字节序从左到右累加Byte0到Byte6但你在DBC里定义报文时信号跨字节的方式没有理顺最终Veristand按DBC打包出来的字节顺序可能和你脚本里算的字节顺序不一致。我用一个笨办法避免这个问题在CANoe的报文细节窗口里直接看发送出去的数据字节序列跟Veristand系统变量里看到的信号值做对比。看到两者一致再放心往后做。3. 实操过程与核心环节实现3.1 环境准备我的项目环境大致如下供大家参考软件NI Veristand 2020 R5及以上NI-XNET驱动CANdb写DBC文件也可以用Vector CANdb或第三方DBC编辑工具。硬件NI PXI-8512/8513双端口CAN接口板卡一个PCIe转PXI的机箱用来跑RT Target。辅助工具Vector CANoe监控CAN总线上的原始报文和数据字节一台PCAN无源监听设备备用。如果手头没有CANoe用NI的CAN测试工具或者ZLG的CANPro也能达到同样验证目的。关键是能看到总线上的原始字节而不是只看DBC翻译后的信号值。很多时候Checksum算错了信号值看起来可能还正常但原始字节已经不对了所以必须看原始数据。3.2 第一步在CANdb里定义带Checksum和Counter的报文这里给出一个实际用过的例子。假设要模拟一个VCU状态报文ID是0x123标准帧DLC8布局如下字节内容Byte0高四位Counter低四位车速高四位Byte1车速低八位Byte2电机扭矩高八位Byte3电机扭矩低八位Byte4预留置0Byte5电池电压高八位Byte6电池电压低八位Byte7Checksum在CANdb里需要分别建这些信号其中Counter信号定义4位起始位要根据字节布局计算Checksum定义为8位无符号放在Byte7。Message属性里把ID设为0x123DLC设为8。给一个简化的DBC片段BO_ 291 VCU_Status: 8 VCU SG_ RollingCounter : 7|40 (1,0) [0|15] VCU SG_ Checksum : 55|80 (1,0) [0|255] VCU这里需要提醒DBC里起始位的表示方式跟CANdb图形界面里的起始位不完全一致。比如Byte0的高四位在DBC文件里写作7|40起始位7长度4小端无符号。如果对不上最好先用CANdb图形界面配置再导出DBC文件避免手写出错。3.3 第二步把DBC导入Veristand并建立信号映射在Veristand的项目树里添加一个NI-XNET CAN设备然后右键配置数据库导入上面写好的DBC文件。导入完成后Veristand会为报文中每个Signal分配一个系统变量路径类似XNET1::CAN1::VCU_Status::RollingCounter。为了后面方便做计算我会额外在System Definition里再建几个可写变量比如SystemVariables::VCU_Counter保存当前计数器值SystemVariables::VCU_Checksum保存计算出来的校验值车速、电机扭矩、电池电压等用于计算Checksum的输入量这些变量最后要和XNET信号建立Simulation Mapping关系也就是把VCU_Counter映射到XNET1::CAN1::VCU_Status::RollingCounter把VCU_Checksum映射到对应的Checksum信号。Veristand的一个特点是信号值在每周期发送前从映射的变量中读取所以我们只要在实时循环里更新这些变量就能控制最终报文内容。3.4 第三步让Counter真正滚起来Counter递增逻辑我推荐放在Veristand的实时脚本里做原因是可控性最强调试也方便。下面给一段在Veristand脚本里实现Counter自增的示例逻辑def update_counter(): # 读取当前计数值 cur nivs.read_variable(SystemVariables::VCU_Counter) # 加1到达上限后回绕 nxt (cur 1) 0x0F # 4位counter上限15 nivs.write_variable(SystemVariables::VCU_Counter, nxt)需要注意的是脚本的执行周期必须和报文发送周期对齐。比如报文是10ms一帧那这个脚本最好也按10ms周期循环执行。Veristand里可以通过定时循环实现或者挂在某个10ms的硬件定时器任务上。另一个细节是Counter的启动时机。很多ECU在系统上电以后要等待一段时间或者收到网络管理报文之后才开始往外发报文Counter初值也往往从某个非零值开始递增。这个可以通过脚本里的状态机逻辑控制。没特殊要求时直接用循环上限以内的任意初值即可只要能保证连续帧单调递增且周期内步进为1就行。3.5 第四步实现Checksum计算并写入信号以最常用的累加和取低字节为例在Veristand脚本里可以这样写def calc_checksum(counter, speed, torque, voltage): # 按Byte0到Byte6的顺序组织字节 # 注意Byte0高四位是Counter低四位是车速高四位 byte0 ((counter 0x0F) 4) | ((speed 8) 0x0F) byte1 speed 0xFF byte2 (torque 8) 0xFF byte3 torque 0xFF byte4 0 byte5 (voltage 8) 0xFF byte6 voltage 0xFF checksum (byte0 byte1 byte2 byte3 byte4 byte5 byte6) 0xFF return checksum写完以后把计算结果写到SystemVariables::VCU_Checksum由映射关系自动发送到总线上。如果校验算法是CRC8可以在Veristand里调用一个预先写好的C#函数库或者直接在脚本里实现查表法。C#的典型实现长这样public byte Crc8(byte[] data, int start, int len) { byte crc 0xFF; // 初始值按协议配置 for (int i start; i start len; i) { crc ^ data[i]; for (int j 0; j 8; j) { if ((crc 0x80) ! 0) crc (byte)((crc 1) ^ 0x1D); // 多项式 else crc (byte)(crc 1); } } return crc; }使用查表法也是一样把256字节的查找表灌进脚本里直接查表计算实时性能更好。这里有一个很重要的原则Checksum算法的计算范围通常是除了Checksum本身所在字节之外的所有数据字节但也有协议会把整个DLC范围内的数据都算进去甚至有些会把Counter排除在外。在写函数时务必看清协议文档里描述的覆盖范围而不是想当然地把所有字节加一遍。3.6 第五步用总线工具验证仿真工程跑起来以后我会先用CANoe的Trace窗口查看0x123报文。第一个要确认的是Counter连续性连续几帧的Counter值应当从某个初值开始逐步递增到达上限后回绕到0。第二个要确认的是Checksum正确性在CANoe里可以手动添加Checksum计算插件或者在Trace窗口里对照原始数据用计算器算一下。我之前多次遇到一种情况数据内容看起来没问题但用在线Checksum计算工具验证时结果对不上。后来发现原因是CANoe显示的数据字节顺序跟我脚本里组织字节的顺序差了整整一个字节。比如DBC里Byte0的起始位定义可能并不是你以为的那一行导致实际总线上的Byte0位移到了Byte1的位置。所以验证阶段一定要结合DBC信号的起始位定义仔细核对看到字节顺序完全一致再下结论。4. 常见问题与排查技巧实录4.1 字节序不一致Checksum对不上这种问题在项目初期最容易碰到。表现是Veristand系统变量里看Checksum信号值和脚本算出来的一样但CANoe里收到的原始字节却匹配不上。排查思路是先抓一帧报文把DBC定义的信号值翻译出来再和Veristand变量值逐一对比找到字节顺序在哪个位置开始错位。如果发现DBC里Signal的起始位配置错误直接回CANdb修正即可。这里补充一个实用技巧验证DBC定义对不对最快的办法是先在Veristand里把Checksum信号手动设成一个固定值比如0x5A然后看CANoe总线上的对应字节是否为0x5A。如果是说明这个信号的映射和打包没问题之后再去纠结算法本身的正确性。4.2 Counter不递增或者跳变有一次我在台架上发现总线上连续三帧Counter值相同第四帧直接跳到3DUT最终进入了超时降级模式。排查后发现原因有二一是脚本里的Counter变量虽然写了但写回的系统变量并没有和XNET发送信号建立映射导致报文里的Counter一直用的是DBC默认值二是脚本循环周期是50ms而报文是10ms一帧导致Counter每5帧才更新一次。经验是更新Counter的循环周期必须不大于报文发送周期。最稳妥的做法是把Counter更新放在和报文发送完全同源的一个实时任务里比如都用XNET的周期发送通道或者用同一路定时器中断触发。如果实在无法做到同源至少要保证Counter更新频率是发送频率的整数倍否则就会出现两帧不变一帧跳变的现象。4.3 总线负载高了之后报文周期性抖动Veristand的实时目标如果在同一周期内既要跑控制模型又要算Checksum还要处理大量XNET收发CPU占用率一高报文的发送周期就会变得不稳定。表现在CANoe里就是相邻帧间隔波动大严重时DUT也会报时序错误。我的处理方式把Checksum计算和Counter更新的脚本放到独立的实时任务中并且调高该任务的优先级同时把XNET的发送缓冲区设置加大。这种方式在大多数项目中都能缓解抖动问题。如果CPU负载实在太高就考虑把Checksum算法搬到一个独立的Custom Device中用C代码实现性能会好很多。4.4 不同ECU对Checksum算法的隐藏要求这一点我最想强调。很多ECU在文档里写的Checksum算法只是一个基础算法实际用的时候还会叠加额外规则比如计算前先给数据做一次字节级反转或者把某个固定Key加到结果里再取低字节。有一次我们按CRC8/SAE J1850算出来的值在仿真器里总是被ECU拒绝后来从供应商那边拿到内部协议说明才发现ECU还要在CRC8结果上异或一个0x22的固定值。所以收到新报文协议时不能只看算法名称就去写代码最好先用一两帧已抓取的原始数据做离线验证。手头没有现成工具的话我会把抓到的原始字节和Checksum列成一张Excel表再用Python批量匹配所有常见算法变种。验证通过以后再固化到Veristand工程里。4.5 仿真工程迁移后信号路径失效Veristand工程在不同机器或不同版本之间迁移时XNET设备的名称、DBC导入路径、系统变量路径都可能变化导致原有映射失效。我曾经把一个工程从Veristand 2018升到2021结果所有XNET信号映射全部断掉光是重新绑定就花了大半天。建议所有DBC文件都放在工程相对路径下并且尽量避免把系统变量命名为带版本号的绝对路径。另外Veristand的数据库缓存有时会保留旧的DBC内容修改DBC后需要手动刷新XNET设备配置。遇到明明改了DBC但总线上信号没变的问题时先重启XNET设备或者重新加载工程再做深入排查。4.6 Checksum和Counter一起失灵时的通用排查步骤如果出现两个信号一起异常我的排查顺序基本是固定的先看DBC文件本身确认Checksum和Counter的起始位、长度、字节序定义是否正确。在Veristand变量浏览器里强制给这两个信号写固定值观察CANoe总线上原始字节是否对应变化。确认映射关系无误后再检查脚本执行周期和发送周期是否匹配。最后才考虑算法层面的问题用离线字节数据单独验证算法正确性。这四步看起来简单但实践中非常有效。很多时候是前两步的问题算法本身反而是最不容易出错的。最后再分享一个我在实际使用中的体会Checksum和Counter这类功能在自动驾驶仿真里很容易被当成边角料但恰恰是它们决定了DUT能否真正进入闭环测试。宁可前期多花半小时把协议里的算法参数、字节序、Counter边界条件都确认清楚也不要等到台架跑起来以后再去抓总线数据、反向推导校验规则。把步骤固化成一套流程后面每次接新协议都能少走不少弯路。
返回列表