尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

基于LabVIEW和CAN总线的UDS ECU刷写上位机开发实践

基于LabVIEW和CAN总线的UDS ECU刷写上位机开发实践 做ECU开发或者售后升级的朋友大概率都干过同一件事拿着供应商给的刷写工具插上诊断仪等着进度条一点点跑完。可一旦涉及产线批量、研发阶段高频刷写或者想夹带一些自动化验证逻辑商业工具的授权费用和封闭性就成了绕不过去的坎。我当时解决得很朴素用LabVIEW写一个基于CAN总线的UDS升级上位机硬件用手头这块图莫斯CAN卡软件从无到有把完整的ECU刷写流程跑通。这篇文章就是这次折腾的完整复盘核心围绕LabVIEW如何调用CAN卡、UDS协议层怎么拆、刷写状态机怎么写以及实测中那些一踩一个准的坑。适合刚接触UDS刷写的ECU测试工程师、BMS/VCU软件工程师以及想在成本可控前提下自己搭建刷写工具的朋友。1. 为什么自己动手做ECU刷写上位机1.1 商业工具到底贵在哪很多团队的第一反应是买现成方案例如CANoe配合ODX-Flash、原厂诊断仪或者专门的售后刷写工具。这些工具本身能力没问题但账算下来并不轻松CANoe的基础授权不便宜UDS刷写模块通常还要单独买授权ODX脚本和诊断数据库也得按车型定制。如果只是研发阶段自用或者产线上只需要一个“点按钮、等结果”的简单工位这笔投入其实很难回本。更重要的是商业工具往往把协议细节封得很死。出了问题只能看到“刷写失败”四个字到底是CAN物理层没通ISO-TP分包没对还是ECU回了NRC你完全看不到。这对做ECU开发的人来说几乎是不可接受的。自己做上位机最大的收益不是省钱而是整个协议链路都在你手里任何一层出问题都能定位。1.2 这套方案适合谁图莫斯CAN卡算是国产CAN卡里性价比很高的一类典型的USB-CAN分析仪形态驱动以DLL方式提供LabVIEW通过调用库函数节点就能对接。硬件的坑很少真正的工程量在软件侧。我的使用场景主要有三个研发联调每天要刷几十次固件用上位机一键完成“进入编程会话、解锁、擦除、写入、校验、复位”省掉手工一条条发诊断指令的重复劳动。产线工位操作员不需要懂协议上位机只暴露一个“开始刷写”按钮和红绿状态灯刷完自动记录ECU序列号和固件版本。售后返修当ECU的App损坏、无法正常通信时通过Bootloader刷写恢复。这时候上位机反而是最可靠的救命工具。如果你也在做类似的事情我的建议很明确不要一上来就追求功能大而全先跑通一个ECU的完整刷写再把架构抽成可配置的通用框架。2. 图莫斯CAN卡接入LabVIEWDLL调用模型与踩坑点2.1 驱动接口的通用风格绝大多数国产USB-CAN卡都走同一类DLL接口风格图莫斯也不例外。典型的函数名字和结构体长这样具体以官方头文件为准但逻辑万变不离其宗typedef struct _CAN_OBJ { DWORD ID; // CAN ID DWORD TimeStamp; // 时间戳 BYTE TimeFlag; BYTE SendType; // 0正常发送 BYTE RemoteFlag; // 0数据帧 BYTE ExternFlag; // 1扩展帧(29位ID) BYTE DataLen; // 数据长度 BYTE Data[8]; // 数据 BYTE Reserved[3]; } CAN_OBJ; DWORD VCI_OpenDevice(DWORD DeviceType, DWORD DeviceInd, DWORD Reserved); DWORD VCI_InitCAN(DWORD DeviceType, DWORD DeviceInd, DWORD CANInd, PVCI_INIT_CONFIG pInitConfig); DWORD VCI_StartCAN(DWORD DeviceType, DWORD DeviceInd, DWORD CANInd); DWORD VCI_Transmit(DWORD DeviceType, DWORD DeviceInd, DWORD CANInd, PVCI_CAN_OBJ pSend, DWORD Len); DWORD VCI_Receive(DWORD DeviceType, DWORD DeviceInd, DWORD CANInd, PVCI_CAN_OBJ pReceive, DWORD Len, DWORD WaitTime); DWORD VCI_CloseDevice(DWORD DeviceType, DWORD DeviceInd);这套接口的核心流程就是打开设备 → 初始化通道 → 启动CAN → 循环收发 → 关闭设备。LabVIEW侧只需要把每个C函数翻译成“调用库函数节点”再把结构体翻译成LabVIEW簇。2.2 在LabVIEW里用CLFN正确调用DLLLabVIEW调用DLL最常用的节点是“调用库函数节点”配置时最容易出问题的是参数类型和结构体布局。我的做法是打开设备、启动CAN、关闭设备这类只有数值参数的函数直接在CLFN里配置成数值传递注意DWORD是U32不是U8。CAN_OBJ结构体必须当作簇处理。LabVIEW中定义簇时成员顺序要和C结构体完全一致。C的DWORD对应U32BYTE对应U8。尤其要注意Data[8]在LabVIEW里不是字符串而是U8数组否则内存布局错位发送的报文数据全是乱的。结构体必须通过“沿数据值引用”传入DLL否则DLL侧拿不到返回的数据。很多人在这一步卡了很久函数返回值显示1说明调用本身成功了但接收缓冲区读不到报文多半就是簇没有按引用传递或者Data数组大小和C侧不匹配。初始化通道时的波特率配置需要特别小心。经典VCI风格驱动用Timing0和Timing1查表设置波特率比如500k、250k各有固定数值新一点的驱动可能直接支持填写500000这种十进制值。图莫斯的驱动具体用哪种拿到手先看头文件和例程。无论如何波特率务必和ECU侧一致否则总线上全是错误帧。2.3 收发模型不要在主界面循环里读CANDLL的VCI_Receive本身支持超时等待但我们写LabVIEW上位机最忌讳的事情就是在一个大循环里又发又收。界面一卡刷写进度都不知道跑到哪了。我最终的架构是生产者/消费者一个独立的CAN接收循环循环里调用VCI_ReceiveWaitTime给20ms左右。有报文就打包成簇写入队列。主界面的事件结构负责消费队列解析UDS响应刷新状态。发送操作在状态机里通过另一个子VI调用VCI_Transmit完成。这套模型的好处是接收不阻塞UI、不丢帧后续做超时控制也方便。要注意VCI_Receive的WaitTime别设太大否则软件退出时会多等好几秒才能关掉循环。3. UDS刷写核心流程拆解一次升级的完整剧本3.1 会话与安全解锁先拿到“许可证”UDSISO 14229是请求/响应式协议。正常情况下ECU收到0x10 03请求后回复0x50 03表示已进入扩展会话。刷写前最好先通过扩展会话过渡不要直接从默认会话跳到编程相关服务很多ECU会回NRC 0x22条件不满足。会话切换之后是安全解锁。典型过程是发送0x27 01请求种子ECU回复0x67 01 种子数据。上位机用OEM约定的算法计算密钥发送0x27 02 密钥。ECU回复0x67 02表示解锁成功如果密钥错误回0x7F 27 33安全访问拒绝。密钥算法五花八门常见的有种子加固定常数、种子查表、字节按位取反或者叠加CRC。LabVIEW里处理时特别注意字节序。很多算法文档里写的是DWORD运算但CAN报文里种子是逐字节发的LabVIEW的数值转字节数组默认大端序直接拿来算经常满拧。我建议把种子字节提取出来按文档要求的字节顺序拼成一个数值再做运算最后再按同样顺序拆分发送。3.2 下载主流程0x34、0x36、0x37三连解锁之后正式开始下载固件。三个核心服务分别是服务作用关键参数0x34 RequestDownload请求下载告诉ECU我要往哪个地址写多长数据数据格式标识符、地址长度格式标识符、内存地址、内存大小0x36 TransferData实际传输固件数据块序列计数器、固件数据0x37 RequestTransferExit请求结束当前下载过程无0x34请求里的addressAndLengthFormatIdentifier很关键高4位表示地址所占字节数低4位表示长度所占字节数。例如0x44就是4字节地址加4字节长度。这个字节写错ECU直接回NRC 0x13。响应0x74里会带回maxNumberOfBlockLength表示ECU单次能接受的数据量上限。注意这个值通常包含了0x36服务本身的SID和块序号字节实际能放的固件数据往往是它减2具体以ECU规范为准。0x36的块序号从1开始每成功发一包加1到255后回绕为0。ECU每成功接收一包会回复0x76 块序号。如果序号不连续ECU会判定传输错误。0x36循环发送时必须等上一包的0x76响应再发下一包这是和ISO-TP流控完全不同的另一个层面的控制逻辑两者容易搞混。0x37通常在最后一包0x36得到0x76确认后发送表示“我传完了”。ECU回复0x77后整个传输阶段结束。如果跳过0x34直接0x36ECU会回NRC 0x24请求序列错误这就是状态机的重要性所在。3.3 收尾阶段例程与复位数据传完并不代表刷写结束。常见的收尾服务有服务典型用途0x31 01 RID 0xFF00擦除Flash0x31 01 RID 0xFF01检查编程依赖、软件完整性预检0x31 01 RID 0xFF02校验写入后的内存0x2E WriteDataByIdentifier写指纹、写零件号、写日期0x11 ECUReset复位ECU让新固件运行起来0x31例程的RID不是标准固定的每个OEM定义可能完全不同必须查ECU的刷写规范。擦除Flash这个例程比较特殊ECU执行时耗时可能长达几十秒甚至更久期间不会回复正常响应。上位机在这里一定要把超时时间拉长不能用默认的P2*五秒否则包挂。最后的0x11复位也需要注意ECU复位后应用层可能需要几秒钟才上线。刷写工具不要马上发读取软件版本的请求最好做个“轮询等待ECU上线”的状态也就是周期发送0x22 读版本号直到收到有效响应才算真正完成。4. LabVIEW状态机落地从协议到可运行的上位机4.1 为什么必须用状态机UDS刷写不是一个线性函数它是一串有严格先后顺序、有失败回退、有超时重试的流程。如果你用一长串平铺的while循环和顺序结构来写后续加一个ECU型号、改一个超时时间都会非常痛苦。状态机的核心价值在于每个状态只做一件事状态之间的跳转条件清清楚楚出了问题知道卡在哪个环节。我设计的刷写状态列表是IDLEENTER_EXTENDED_SESSIONSECURITY_ACCESS_SEEDSECURITY_ACCESS_KEYCHECK_PRECONDITIONERASE_MEMORYREQUEST_DOWNLOADTRANSFER_DATAREQUEST_TRANSFER_EXITCHECK_MEMORYWRITE_FINGERPRINTECU_RESETVERIFY_APPFINISH / ERRORLabVIEW里我用一个枚举控件定义状态状态机主循环通过移位寄存器传递“当前状态”和“当前重试次数”。每个状态分支里调用对应的UDS子VI比如UDS_RequestDownload.vi、UDS_TransferData.vi返回正常就跳到下一状态返回NRC就记录日志并进入错误处理分支。4.2 ISO-TP分包把大块固件塞进CAN帧UDS跑在CAN上并不是每个UDS请求都能塞进一帧CAN报文。普通CAN一帧数据区只有8字节减去CAN-TP的协议控制信息单帧最多传7字节。像0x36传几KB固件时必须走ISO 15765-2定义的ISO-TP分包机制。ISO-TP的帧类型主要有四种帧类型用途特点单帧SF总长度≤7时使用PCI低4位表示长度首帧FF开始一个长消息PCI共12位表示总长度最多4095字节连续帧CF后续数据PCI低4位是序号1~15循环流控帧FC接收方控制发送节奏包含FlowStatus、BlockSize、STminLabVIEW端的发送器逻辑是这样的判断整个消息长度是否≤7是则发单帧。否则发首帧PCI字节 0x10 | ((总长度 8) 0x0F)第二字节 总长度 0xFF再跟上前6字节数据。等待接收方的流控帧。流控帧PCI 0x30 | FlowStatusFlowStatus为0表示允许发送为1表示等待为2表示溢出。收到流控帧后按BlockSize和STmin参数发送连续帧。每帧PCI 0x20 | 序号序号从1开始到15后回绕到0。接收方向同理如果收到的是首帧先解析总长度回一个流控帧再收连续帧并拼包。流控帧里的BS和STmin建议在开发初期就做成可配置项因为不同ECU对发送间隔的敏感度完全不一样。我实测过有些ECU对连续帧间隔要求很高STmin给小了会丢帧给大了刷写速度又上不去。4.3 超时控制与进度显示UDS有一个容易忽略的机制P2和P2等待时间。正常情况下ECU收到请求应该在P2默认50ms内回复但如果ECU处理比较慢会先回一个0x7F SID 0x78响应待定告诉测试仪“我还在忙”之后再回真正的响应。上位机收到0x78后应该把超时计时器从P2切换到P2默认5000ms。很多自制工具在这一步只处理了普通超时导致一遇到ECU忙就误判失败。我实现接收状态机时每个请求都同时维护两个计时器一个是P2超时另一个是P2*超时。逻辑不复杂但必须每个状态都做不能只加在0x36传输上。进度条也别直接按“已写入字节/文件总大小”算。擦除、校验、复位这些阶段根本没有数据传输但耗时占比很高。我建议把刷写流程按权重分段处理比如连接会话10%、擦除25%、数据传输50%、校验10%、复位5%。这样操作员看到的进度才接近真实状态不会盯着一个40%的进度条干等几十秒。5. 实测中的典型故障与排查链路5.1 DLL调用成功但总线无报文这是最让人抓狂的问题VCI_Transmit返回1LabVIEW没报错但CAN总线分析工具就是抓不到报文。排查链路如下检查ExternFlag。UDS刷写绝大多数使用29位扩展ID例如0x18DA10F1。如果ExternFlag写成0驱动会把ID当成11位标准帧截断总线上的ID完全对不上。检查CAN ID换了个说法扩展ID在LabVIEW里要用U32保存不能放进I32。I32显示为正数没问题但一旦超过0x7FFFFFFF就会被当成负数传递到DLL就可能出错。检查波特率。用CAN卡自带的工具先单独收发包确认波特率正确、能看到其它ECU的报文再对接LabVIEW。检查CAN_H和CAN_L是否接反终端电阻是否匹配。这个问题在实验台上最容易出现因为短距离有时接反也不明显但通信就是不稳定。5.2 ISO-TP流控异常导致刷写中断刷写到一半卡住日志显示上一包0x36已经回复了0x76下一包发送却迟迟没有后续大概率是ISO-TP层的问题。我遇到过的几种情况和对应处理发送器发出首帧后没有等流控帧就继续发连续帧。ECU侧会直接丢弃刷写超时。解决方法是严格把ISO-TP发送器做成“发首帧-等FC-发CF”的状态机。收到了FC WaitFlowStatus1就直接判失败。FC Wait表示ECU暂时忙这时候应该把超时时间拉长继续等待或重新发送FC请求而不是立即终止刷写。CF序号错乱。ISO-TP的序号只有4位加到15后回绕到0。很多人在这个回绕判断上写错导致ECU回NRC 0x13。0x34返回的maxNumberOfBlockLength解析错误导致每包0x36携带数据过大ECU直接拒绝。我的建议是开发阶段把每一帧CAN报文都打印出来包括方向、ID、PCI字节、序号回看日志时能很直观地定位卡在哪一步。5.3 NRC错误码与对策UDS刷写失败时ECU通常回负响应格式是0x7F 请求SID NRC。下面是我整理的高频NRC对照表NRC含义常见原因与对策0x13报文长度或格式错误addressAndLengthFormatIdentifier写错、ISO-TP分包序号错乱0x22条件不满足没进扩展/编程会话就发服务先切会话0x24请求序列错误跳过0x34直接0x36或0x37后重复0x360x31请求超出范围擦除地址、固件地址超出ECU地址映射范围0x33安全访问拒绝种子/密钥算法错、字节序反了、重复解锁请求太频繁0x72一般编程失败Flash擦写失败常见于供电不稳、地址越界0x78响应待定不是错误是ECU忙需要切到P2*继续等待排查NRC时我的习惯是先查地址段。因为0x31、0x72有一半以上是固件地址和ECU内存映射对不上。尤其是Intel HEX文件里可能有多个数据段LabVIEW解析时要按段地址分别发起0x34请求不能把整个文件当成一段连续数据往一个地址里灌。6. 从“能用”到“好用”功能扩展与安全底线6.1 扩展成诊断刷写一体平台刷写跑通之后思路一下子就打开了。同样是UDS协议栈往上加0x22读取ECU软件版本号、0x2E写配置参数、0x31例程控制做自检诊断工具和刷写工具就可以合并成一套。树状图或者选项卡界面里放几个不同ECU的刷写配置产线切换车型时不用改代码只改配置文件。配置文件的组织方式我推荐用INI或JSON把每个ECU的诊断ID、会话子功能、安全算法类型、固件文件路径、NRC映射表都放进去。LabVIEW解析JSON稍微麻烦一点但值得做尤其是要管理多个ECU或多种车型时。另一个很实用的扩展是CAN FD支持。现在越来越多的ECU刷写走CAN FD单帧数据长度从8字节变成64字节ISO-TP单帧上限从7变成63刷写速度能快一个数量级。图莫斯CAN卡如果支持CAN FDDLL接口会多出对应的接口LabVIEW侧的改动主要集中在ISO-TP发送器的长度字段处理上。6.2 刷写安全的几条铁律做刷写工具功能可以不强但安全底线必须守住。我有几条长期实践总结出的硬性规矩刷写过程中无论什么原因都不要切断电源。上位机软件层面要做到启动刷写前检查供电状态如果ECU由外部适配器供电要提示用户确认稳压电源已开启。不要在整车正常行驶的CAN网络上测试刷写。刷写前把ECU隔离到台架或专用诊断口上避免总线冲突把其它ECU也搞挂。能回读固件的ECU刷写前先读出来备份。虽然有些ECU的安全等级不允许回读但只要允许多读一次就是给自己留一条退路。安全解锁的种子密钥算法必须通过合法渠道获得通常OEM会在技术协议或供应商文档里提供。不要尝试逆向破解一方面是合规问题另一方面也没有必要工具本身的价值在流程和控制不在绕过别人的安全机制。固件文件管理要严格。上位机在选择固件文件时计算并显示CRC或哈希值刷写前和ECU规范里的预期值比对防止拿错版本刷进去。我在实际使用中还有一个体会这类工具最怕的不是代码写得丑而是协议时序没吃透。很多问题其实不是LabVIEW编程问题是ISO-TP层谁先谁后的逻辑没理顺。建议你做的时候先拿官方刷写工具抓一份完整的报文日志对照着把状态机画出来再动手写代码后面的效率会高很多。
返回列表