ARTICLE DETAIL

资讯详情

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

Veristand自定义CAN报文Checksum与Counter工程实践

Veristand自定义CAN报文Checksum与Counter工程实践 还记得第一次做带Checksum和Counter的自定义CAN报文时我盯着示波器看了整整一个下午——波形没有问题ID没有冲突波特率也对得上可被测的ECU就是死活不给响应。后来一台CANoe挂在总线上抓包才看到真相每一帧的Counter都在乱跳Checksum算出来跟规范差了十万八千里对方早就把这些报文当成攻击流量悄悄丢掉了。这个场景在自动驾驶仿真测试里太常见了。摄像头、毫米波雷达、域控制器之间大量消息都走CAN总线而这些消息里几乎都带了端到端保护E2E机制。所谓端到端保护拆开来看常常就两个最基础的元素用于检查数据有没有被篡改的Checksum和用于检测报文有没有丢失、被重复或被重放的Counter。如果你的仿真平台——不管是用NI Veristand搭的HIL还是纯软件环境——不能正确生成这两个字段被测的ECU就会把仿真数据当作非法报文忽略掉整个测试根本白做。这篇文章我就以NI Veristand为例把自定义CAN报文Checksum和Counter这条路的完整思路、具体实现和那些坑一次聊透。内容会比较偏实操但背后的“为什么”我也会讲清楚因为做仿真最怕的不是不会操作而是不知道为什么这么操作。1. 仿真环境里的安全校验为什么ECU会“扔掉”你的报文1.1 Checksum和Counter不是“保险丝”而是整车的信任基础传统CAN协议在物理层和数据链路层上是没有加密和认证概念的。总线上任何一个节点只要把CAN收发器挂上去理论上就能发出仲裁ID合法的报文。这对实车来说是个巨大的安全隐患——假如某个节点因为干扰或者故障发出错误数据接收方很难在第一时间判断“这个数据到底可不可信”。于是OEM们在应用层设计了两道最基础的防线Checksum校验和对报文中若干个字节做一道数学运算生成一个校验值放在报文的特定字节位。接收方按同样算法重新算一遍如果对不上就说明发送过程中数据被干扰或者被篡改了。Counter滚动计数器一个循环递增的数值每发一帧就加一。接收方通过观察Counter的连续性可以判断有没有丢帧、有没有重复帧、有没有被重放进来的旧帧。我经常用一个生活化的类比来解释这两者的区别Checksum就像快递包裹上的重量记录收到货先称一下重量对不上说明运输途中被拆包或调包了Counter就像快递单号的连续序号如果你收到的包裹序号是1、2、5、6那中间3、4两件大概率是丢了。在自动驾驶的域控制器测试中这些机制直接与功能安全等级挂钩。ISO 26262和AUTOSAR E2E规范里对这类端到端通信保护有明确要求很多ECU在通信层发现Checksum或Counter异常时会直接进入FAILSAFE安全降级状态而不是继续使用可疑数据。1.2 仿真时忽略它们会引发什么后果做自动驾驶仿真目标往往是验证域控制器的策略逻辑或故障诊断功能。很多人一开始觉得“反正我仿真里数据都是自己生成的加不加Checksum无所谓的”结果就掉进下面几个大坑第一种坑ECU完全不响应仿真指令。比如你想通过总线给转向控制器发一个转角指令报文ID和信号值填得都没错但Checksum不对控制器把帧一收校验失败直接丢弃。你从仿真端看只能看到“发送成功”根本看不到数据在接收端被扔掉。第二种坑故障注入测试彻底失真。故障注入是自动驾驶仿真的核心需求之一要模拟报文丢失、信号跳变、通信超时。但是如果你的基础报文本身的Counter就是乱跳的ECU会把“正常工况”也当成“通信故障”你注入一个丢包故障后根本无法分辨ECU的反应到底是针对哪个故障的。第三种坑跨团队联调时互相甩锅。仿真团队说“报了错”感知团队说“没有收到”软件团队拿到日志一查发送端和接收端的Counter根本对不上整个下午就在那里定位是哪个环节把数据改坏了。其实大概率只是发送侧没有实现E2E保护。所以在仿真环境里把Checksum和Counter做对不是“锦上添花”而是“入场券”。它要让被测ECU相信你仿真出来的报文和真实传感器发出的报文在通信层面没有区别。2. Veristand与CAN报文的交互通路动手前先搞清楚这几件事2.1 Veristand中一条CAN报文从UUT走向总线的路径NI Veristand是HIL测试中很常用的实时测试环境它最大的优势是“一套环境里同时管理实时模型、硬件I/O、故障注入和测试序列”。但这也带来了一个副作用很多人对它的CAN数据处理链路不太清楚遇到问题不知道去哪个环节查。一个典型的Veristand发送CAN报文链路是这样的信号在系统定义中赋值 ↓ 通过System Mapping映射到CAN Frame中的Signal ↓ VeriStand引擎把信号数据交给NI-XNET驱动 ↓ NI-XNET按Frame配置的周期和仲裁ID发送 ↓ CAN收发器把电平信号送上物理总线注意Veristand本身不是一个“报文编辑器”你不能直接在界面上手写一帧数据然后发送出去。它强调的是“信号级”的操作——你先定义好一个报文里有哪些信号然后把信号值映射给某个Frame至于帧头、DLC、填充、发送周期这些都在Frame配置里管理。理解这条链路后你就明白自定义Checksum和Counter的核心思路了这两个东西本质上是报文里的特殊“信号”它们的数据源不是来自测试人员手动赋值而是由其他信号实时计算得到的。后面所有实现方案都是围绕“怎么把实时计算出来的值映射回报文指定的字节位置”来展开的。2.2 DBC、Frame与Signal三个层级不能混淆在Veristand里操作CAN报文最常见的入口是导入DBC文件。很多人以为“DBC导入进来报文就配置好了”其实不是。DBC是Vector公司定义的一种CAN数据库描述文件它描述的是一种“理想报文布局”某个CAN ID下有哪些信号每个信号起始位、长度、字节序、缩放因子是多少。Veristand导入DBC后会把它转成自己的三层对象模型Interface接口对应物理上的一个CAN通道比如“CAN1”或“CAN2”。Frame报文帧对应一个CAN ID比如0x123。配置里有DLC、发送类型周期/事件/触发、波特率等。Signal信号对应报文数据区里的某个字段比如“Counter”“Checksum”“SteeringAngle”。这里最关键的认知是在Veristand中Foo被抽象成一个FrameBar被抽象成它下面的Signal。你想修改报文的一个字节实质上是修改该Frame下某个Signal的映射关系。所以当你说“我要自定义一个报文的Checksum和Counter”第一步不是去写代码而是先确认自己在Veristand的对象模型里能不能看到这个报文Frame以及Frame里有没有预留Counter和Checksum这两个Signal的位置。如果没有预留要么改DBC要么在Veristand手动新建Signal再关联到报文对应的Byte位置。2.3 三种自定义实现方式怎么选在Veristand中实现Checksum和Counter业界通常有三条路线我按推荐程度排个序实现方式灵活度改动量实时性适用场景Calculation Function计算通道中小中受执行率限制无状态、逻辑简单的快速验证Simulink模型E2E保护模块高中高与模型任务同步带状态的滚动计数器、复杂校验算法Custom Device自定义设备极高大极高产品级复用、需要直接访问寄存器级数据很多人一看表格就会选第一条路因为Calculation Function在Veristand里创建非常快写个公式就能用。但我在实际项目中吃过亏——Calculation Function本质上是一个周期性刷新的无状态公式计算器它默认不保存上一次的计算结果。而Counter恰恰是需要记忆状态的逻辑它需要记住“上一帧是多少然后加一”。纯Calculation实现这个会很别扭你需要额外引入脉冲边沿检测、状态寄存器之类的辅助手段复杂度反而上去了。所以我的实际建议是如果只是做简单的演示、临时验证用Calculation Function就够如果是要长期稳定跑HIL测试尤其是被测对象有严格的E2E状态机推荐把Counter和Checksum的逻辑放进Simulink模型里做成一个“E2E保护模块”再作为Veristand的实时模型运行。这样整个算法的状态管理由模型负责时序同步、仿真验证都更容易控制。至于Custom Device那是给真正追求极致性能和复用性的场景准备的。比如你在Simulink中做一个自定义的NI-XNET驱动替代品或者需要在FPGA层面逐帧处理CAN报文才值得投入人力去做。对大多数测试团队来说Simulink模型方案已经是性价比最高的选择了。3. 在Veristand中自定义Checksum和Counter的落地步骤3.1 报文数据区布局设计示例假设我们现在要发送一个周期为10ms的CAN报文ID是0x123DLC8。为了讲清楚原理我设计一个尽可能接近真实工程场景的报文布局字节位置信号名称说明Byte0Counter滚动计数器0x00~0xFF循环递增Byte1SteeringAngle_H方向盘转角高字节Byte2SteeringAngle_L方向盘转角低字节Byte3BrakePosition制动踏板开度Byte4GearPos挡位信号Byte5Reserved_1预留Byte6Reserved_2预留Byte7Checksum对Byte0~Byte6的校验和这里有两个设计惯例值得记住一是Counter通常放在Byte0因为这个字段被接收方用来做帧连续性检查放在前面解析起来最顺手二是Checksum通常放在报文的最后一个字节这样计算覆盖范围是“除自己之外的所有数据字节”符合大多数OEM的E2E规范习惯。Checksum算法我选择常见的“累加和取反加一”也叫二进制补码校验和很多乘用车控制器都用它。具体计算方式是sum Byte0 Byte1 Byte2 Byte3 Byte4 Byte5 Byte6 checksum (256 - (sum 0xFF)) 0xFF为什么用取反加一而不是直接取低字节因为取反加一的校验和有一个好处接收端把包括Checksum在内的所有字节全部相加如果结果是0x00就说明数据没有错误。这样接收端做校验时只需要一个加法操作非常高效。3.2 在Veristand中建立信号并接入Frame3.2.1 通过DBC导入还是手动新建Frame如果你手里有正式的DBC文件直接导入最省事。导入后检查三件事Frame的仲裁ID是否正确0x123DLC是否等于8有没有名为Counter和Checksum的Signal或者至少有没有信号落在Byte0和Byte7的位置。如果DBC里没有Counter和Checksum的定义你需要在Veristand的CAN Frame编辑界面里手动添加两个Signal并分别设置起始位为0和7长度为8bit类型为Unsigned。这个小步骤很容易被忽略但少了它后面无论怎么算这两个值都不会送进报文字节里。3.2.2 在System Definition中配置发送参数进入Veristand System Definition后找到目标CAN Interface下的Frame把Frame配置成周期发送模式周期设为10ms。同时要确认波特率匹配比如500kbps。这些基础参数一旦和实车不一致后面所有验证都会失真。在System Mapping中把应用变量比如方向盘转角、制动踏板、挡位映射到Frame下的对应Signal。这一步新手最常犯的错误是只映射了“自己想控制的那几个信号”忽略了Counter和Checksum需要指定数据源结果报文里这两个字段一直是默认值0x00。3.3 Calculation Function与Simulink模型两种实现路径对比3.3.1 用Calculation Function实现无状态Checksum如果你只是想快速看一个“能跑通”的版本可以用Veristand的Calculation Function。在System Definition中新建一个Calculation比如命名为CHKSUM_Calc_0x123然后在表达式里写类似这样的伪公式CHKSUM mod(256 - mod(Counter SteeringAngle_H SteeringAngle_L BrakePosition GearPos Reserved_1 Reserved_2, 256), 256)注意Veristand不同版本的Calculation语法在函数命名上略有差异实际编写时以你安装版本内置的函数名为准。关键点是参与计算的所有操作数都必须是已经映射到Veristand的通道或变量最终结果要再映射回Frame的Checksum Signal数据类型建议统一用UInt8避免double转uint带来的截断问题。这种方法能解决Checksum但解决不了Counter的状态问题。我见过很多人在这个路口卡住Checksum算出来了Counter却不递增。原因就是上面说的——Calculation Function是无状态的你没法在公式里表达“上一帧的值再加一”。3.3.2 用Simulink模型实现带状态的E2E模块如果要长期做HIL测试我强烈建议把E2E保护逻辑放到Simulink模型里。这里给出一个经过验证的模块设计思路Counter生成子模块使用Unit Delay保存当前计数值每收到一个“发送使能脉冲”或者每执行N次模型任务就加1通过Modulo模块模256。Checksum计算子模块把Counter和Byte1~Byte6的数据用Byte Packing拼成一个8字节数组然后经过Sum、Bitwise NOT等模块计算校验值。这比在Veristand里写一行公式更直观也更容易在仿真阶段提前验证。输出端子把Counter和Checksum作为模型的输出端口输出。这个模块在Simulink里跑一遍仿真你就可以画出Counter随时间变化的阶梯曲线确认它是0、1、2……255、0循环递增也可以手动改一帧数据确认Checksum输出符合预期。验证通过后把模型编译部署到Veristand实时环境。很多人问为什么非要把这块逻辑搬进模型模型跑在实时任务里执行周期和Veristand的实时循环是严格同步的。你的CAN报文周期是10ms模型的定时任务也是10ms或者一个10ms的发送使能计数器那么Counter的递增和报文的发送就天然绑定在一起不会出现“计算通道更新率跟不上发送速率”这种同步问题。3.4 信号回映射与发送配置这一步漏了就前功尽弃无论你选择用Calculation还是Simulink模型生成的值都必须“送回”Frame的Signal报文才能真正带上Checksum和Counter。这一环节的典型错误是计算模块里数值已经对了但System Mapping的连线没拉或者拉到了别的Frame上。以模型方案为例需要在System Mapping中做两个映射E2E_Model.Counter_OUT → CAN1_Interface.CAN_Frame_0x123.Counter E2E_Model.Checksum_OUT → CAN1_Interface.CAN_Frame_0x123.Checksum映射完成后仿真运行前还要检查一次Frame的发送方式。如果Frame配置成了“事件触发发送”那么只有你显式触发时才发如果配置成了“周期发送”只要系统运行就会按10ms周期持续往外发。对于模拟传感器上游数据的场景通常选周期发送。另外提醒一个细节如果你的模型里还引用了其他传感器应用信号比如方向盘转角、制动踏板这些信号同样需要映射到Frame。不要只顾着映射E2E相关值结果报文里其他字节全是0一旦接收端把这些0当成真实信号去算Checksum一样会校验失败。4. 验证自定义机制一帧一帧地证明它在工作4.1 搭建最小验证环境在把系统接到真实ECU之前强烈建议先搭一个最小验证环境一台运行Veristand的实时机或PXI系统一个CAN接口卡NI-XNET系列、Vector或者PCAN都可以一台运行CAN分析工具的电脑CANoe、PCAN-View、周立功CANTest都行两端各接一个120Ω终端电阻有的接口卡内置了这个要先查手册确认。物理连接不复杂Veristand实时机的CAN口连到分析仪工具的CAN口两台设备共享一个物理总线。波特率两端必须严格一致这属于基础配置但真有人因为一端设了500kbps、另一端设了250kbps查了一整天才发现是波特率不匹配。4.2 用独立脚本逐帧核对Counter与Checksum总线分析工具能告诉你“这一帧发了什么数据”但通常不会帮你逐帧校验Counter的连续性和Checksum的正确性。所以我习惯写一个小脚本把CAN分析工具导出的日志读进来脱离Veristand和ECU独立验证一次。假设CANoe或PCAN导出的日志文件每一行格式如下0x123 00 64 32 28 01 00 00 A5第一列是ID后面8列是报文数据十六进制。下面这个Python脚本可以逐帧校验import re def decode_line(line): parts line.strip().split() frame_id parts[0] data [int(x, 16) for x in parts[1:]] return frame_id, data def calc_checksum(data): # 对 Byte0 ~ Byte6 做累加和取反加一 s sum(data[0:7]) 0xFF return (256 - s) 0xFF expected_counter None frame_count 0 error_count 0 with open(can_log.txt, r) as f: for line in f: line line.strip() if not line or line.startswith(#): continue frame_id, data decode_line(line) if frame_id.lower() ! 0x123: continue frame_count 1 actual_counter data[0] actual_checksum data[7] calc_checksum_val calc_checksum(data) # 校验 Counter 连续性允许 0xFF - 0x00 if expected_counter is None: expected_counter actual_counter else: if actual_counter ! (expected_counter 1) % 256: print(f[Counter Error] frame {frame_count}: fexpected {expected_counter:02X}, got {actual_counter:02X}) error_count 1 expected_counter actual_counter # 校验 Checksum if actual_checksum ! calc_checksum_val: print(f[Checksum Error] frame {frame_count}: fexpected {calc_checksum_val:02X}, got {actual_checksum:02X}) error_count 1 print(ftotal 0x123 frames: {frame_count}, errors: {error_count}) if expected_counter is not None: print(ffinal counter: {expected_counter:02X})这个脚本有个好处即使你手头没有CANoe这种昂贵工具只有一份导出的纯文本日志也能完成自动化校验。脚本逻辑很清晰——Counter连续性单独报错Checksum错误单独报错两类问题不会被混在一起。4.3 把总线分析仪和信号级验证结合起来脚本验证的是“应用层数据是否正确”但CAN总线上的物理层问题电平异常、位错误从脚本里看不出来。所以你仍然需要看一眼总线分析仪上的错误帧统计功能如果错误帧数量持续上涨说明物理层有问题优先检查波特率、终端电阻、地电位。Checksum和Counter做得再好也救不了物理层错误。如果错误帧为零但脚本报Counter或Checksum错误那就是应用层逻辑问题回Veristand配置或Simulink模型里查。这两种问题必须分开排查。我曾经在一个项目里花了大半天调E2E算法最后发现是总线终端电阻没接好导致偶发位错误接收端采到损坏数据Checksum自然对不上。这不是算法问题是物理层问题。所以验证流程一定要分层次。5. 调试笔记那些坑以及对应的排查思路5.1 Counter不递增或连续重复多半是同步和映射的问题Counter不递增是仿真调试里出现频率最高的问题。按我的排查顺序基本三步走第一步看映射表。在System Mapping里检查Counter的输出源是否映射到了Frame的Counter Signal上。这个原因听着低级但实际操作中很容易因为名字相似把Counter_OUT映射到了别的Frame上或者被之后的映射覆盖掉了。第二步看计算任务周期。如果用的是Calculation Function确认它的执行率是否高于CAN报文的发送率。比如报文周期是10ms但Calculation执行周期是100ms那么这100ms内的10帧报文里Counter可能只在第一帧更新过一次后面的9帧全部是同一个值。接收方一旦发现连续多帧Counter不变基本都会判定为重复帧并丢弃。第三步看是否有多个数据源同时写入Counter。有时候你在Calculation里算了一个Counter又在Simulink模型里算了一个Counter两个返回结果都映射到了同一个Signal上。Veristand的映射优先级不会报错但实际生效的是后映射的那个很容易覆盖掉你之前调好的值。我建议一个Frame里的一个Signal只允许一个数据源避免交叉映射。5.2 Checksum计算范围与字节序算法对了也可能白算Checksum算出来不对除了算法本身写错还有几个隐性原因计算范围搞错。这是最常见的。有的OEM规范要求Checksum的覆盖范围包括报文ID有的要求排除Counter有的要求从Byte1开始而非Byte0。不要想当然一定要看你们项目里那套正式的通信矩阵文档。曾经有个项目里Checksum差一个字节一直不对最后查文献发现对方把CAN ID低8位也纳入了求和范围。字节序问题。如果你的Checksum是多字节字段DBC里的Intel格式和Motorola格式在起始位编号规则上完全不同同样一个Checksum0xA5B6Intel格式下低字节在前Motorola格式下高字节在前。对于单字节Checksum这种8位字段这个问题不明显但一旦有OEM采用16位CRC就必须认真核对DBC的字节序。数值类型被截断。在Veristand Calculation和Simulink模型里double转uint8时如果没做取整和mod运算结果可能被四舍五入到一个完全不同的值。我的习惯是每一步运算都明确定义数据类型在Simulink里用Data Type Conversion模块显式转换在Veristand Calculation里把中间变量都声明为UInt8。5.3 发送周期与模型任务周期概念上不能划等号这是我在项目里跟人讨论最多的问题。很多人说“我模型是10ms的任务报文也是10ms周期那Counter每帧加1不是理所当然吗”——不一定。关键在于模型每10ms执行一次不等于每10ms就会发出一帧报文。报文什么时候发由Veristand的CAN调度决定模型只是给报文提供数据。如果你的模型执行和报文发送在同一个实时循环里还好但如果你在模型里做的是“每执行一次就Counter1”而报文实际发送频率是模型执行频率的一半那就会变成每两帧Counter才加1接收方看到的就是连续的奇偶跳变一样会判E2E错误。正确的做法是让“Counter1”的触发条件与“报文实际发送”的条件严格一致。在Simulink里我会用一个发送使能计数器如果报文周期是模型周期的10倍就等模型执行满10次后才允许Counter加1并更新输出否则保持上一帧的输出不变。这样才能保证“发一帧Counter加一”。5.4 与真实ECU联调时别忘了E2E状态机的启动和容忍窗口当你把仿真系统接到真实ECU之后又会发现一些单纯对总线工具联调时看不出来的问题。首先是ECU的E2E初始化窗口。很多ECU在启动后会有一段“宽限期”在前几帧内不会严格检查Counter的连续性或者允许连续丢N帧后才报故障。如果你一上电就要求Counter从0x00开始严格递增而对方规范其实是0xFF开始递减那对接初期必然报错。这种情况下要以对方ECU的E2E配置文档为准而不是你以为的“标准做法”。其次是Counter跳变的容忍阈值。不同ECU对“跳变”的定义不同有的允许Counter从0x0F直接跳到0x11即允许跳过一个值有的必须严格等于1。仿真系统如果因为任务调度偶发导致一帧没发出去Counter就会出现缺口。这种问题很难从报文日志里发现因为单看数据似乎正常但ECU已经把这个缺口记为一次通信故障。所以联调时不仅要看分析仪抓到的帧还要看被测ECU的诊断故障码里有没有E2E相关的记录。最后是CAN FD的差异。如果项目里已经切到CAN FD那DLC可变、数据场更大、校验算法也可能换成了CRC而不是简单的累加和。传统CAN上验证通过的Checksum和Counter实现不能直接复制到CAN FD上。至少要把CRC多项式、覆盖范围、数据填充规则重新按规范核对一遍。再分享一个习惯做法每次修改完E2E相关配置我都会先抓至少三分钟的总线日志跑一遍校验脚本再连真实ECU做联调。三分钟看起来不长但对一个10ms周期的报文来说足够覆盖18000帧数据Counter至少循环70轮脚本跑完基本能覆盖所有边角情况。这个习惯帮我挡掉过不少低级错误也希望对你有效。
返回列表