24. 数据上传下载服务:0x34(请求下载)、0x36(数据传输)、0x37(请求上传)的实现
这三个服务:0x34请求下载、0x36数据传输、0x37请求上传,说白了就是ECU和诊断仪之间怎么“传文件”。
我记得刚入行那会儿,有个项目要做OTA升级,当时我对这组服务的理解还停留在“发数据、收数据”的层面。结果第一次测试就翻车了——数据传了一半,ECU死机了。嗯,从那以后我再也不敢小看这三个服务了。
24.1 服务概述与场景
这三个服务是UDS诊断协议里最“重”的一组。它们不像读取故障码那样轻量,而是涉及大量数据的传输。常见的应用场景有:
ECU固件升级:把新的二进制文件刷进ECU
标定数据下载:更新发动机的MAP图、参数表
日志数据上传:把ECU记录的运行数据导出来分析
配置信息读写:比如VIN码、序列号的写入
你想想看,这些场景都有一个共同点——数据量大。如果每次都用单个诊断请求去发,效率太低了。所以UDS设计了这种“三段式”的传输机制。
24.2 0x34 请求下载(Request Download)
0x34是下载的“敲门砖”。诊断仪告诉ECU:“我要给你发数据了,你准备一下。”
24.2.1 请求报文格式
|
|
|
|
|
|
0x34 |
服务ID |
|
|
dataFormatIdentifier |
数据格式标识(高4位压缩,低4位加密) |
|
|
addressAndLengthFormatIdentifier |
地址和长度格式标识 |
|
|
memoryAddress |
内存起始地址 |
|
|
memorySize |
数据长度 |
这里有个关键点——addressAndLengthFormatIdentifier。这个字节决定了地址和长度各占几个字节。我个人习惯把它拆成两个半字节来看:
高4位:地址字节数(0x00~0x0F,实际常用0x01~0x04)
低4位:长度字节数(0x00~0x0F)
举个例子:如果这个字节是0x24,表示地址占2个字节,长度占4个字节。
24.2.2 实现要点
在DCM模块里处理0x34时,我建议你注意以下几点:
验证地址范围:检查请求的地址是否在ECU允许的范围内。我在项目中遇到过,有人试图往Flash的保护区写数据,结果ECU直接拒绝。
检查安全状态:通常下载操作需要ECU处于特定的安全会话模式,并且已经通过了安全访问认证。
分配缓冲区:ECU需要为即将到来的数据分配临时存储空间。这里要小心内存溢出。
警告:千万不要在0x34响应里直接开始写Flash!这个服务只是“请求下载”,真正的数据还在后面。我曾经见过一个同事在0x34里就把数据写进去了,结果0x36传了一半断开了,ECU直接变砖。
24.2.3 正响应格式
响应格式:#1: 0x74 (服务ID + 0x40)#2: maxNumberOfBlockLength (每个数据块的最大字节数)
这个maxNumberOfBlockLength很重要。它告诉诊断仪:“你每次最多发这么多字节给我。” 这个值取决于ECU的接收缓冲区大小。我一般建议设成256或512字节,太大容易出问题。
24.3 0x36 数据传输(Transfer Data)
0x36是真正的“搬砖”服务。诊断仪把数据一块一块地发给ECU。
24.3.1 请求报文格式
|
|
|
|
|
|
0x36 |
服务ID |
|
|
blockSequenceCounter |
块序列计数器(从0x01开始) |
|
|
transferRequestParameterRecord |
实际数据 |
这里有个容易踩坑的地方——blockSequenceCounter。它从0x01开始递增,每发一包加1。当它达到0xFF时,下一包要回到0x00,然后再从0x01开始。为什么?
因为0xFF + 1 = 0x00,这是8位计数器的自然溢出。但注意,0x00是保留值,所以溢出后要跳到0x01。
小技巧:我在实现时,会用一个uint8的计数器,每次加1。如果等于0x00,就手动设成0x01。这样既简单又安全。
24.3.2 实现逻辑
DCM收到0x36后,要做的事情:
检查序列号:确认是期望的下一包。如果序列号不对,返回NRC 0x13(不正确消息长度或格式)。
拷贝数据:把数据从请求报文里提取出来,存到之前分配的缓冲区。
检查是否完成:如果收到的数据量达到了0x34里声明的memorySize,就触发写入操作。
正响应很简单:
#1: 0x76#2: blockSequenceCounter (回显请求中的序列号)
24.4 0x37 请求上传(Request Upload)
0x37和0x34是对称的。诊断仪说:“ECU,我要从你这里读数据,你准备一下。”
24.4.1 请求报文格式
格式和0x34几乎一样:
|
|
|
|
|
|
0x37 |
服务ID |
|
|
dataFormatIdentifier |
数据格式标识 |
|
|
addressAndLengthFormatIdentifier |
地址和长度格式标识 |
|
|
memoryAddress |
要读取的内存起始地址 |
|
|
memorySize |
要读取的数据长度 |
24.4.2 实现要点
0x37的实现比0x34简单一些,因为ECU只需要读数据,不需要写。但有几个坑要注意:
地址可读性检查:确保请求的地址是可读的。有些内存区域(比如某些配置寄存器)可能不支持读取。
数据准备:ECU需要把数据准备好,等诊断仪用0x36来“拉”数据。注意,这里0x36的角色变了——在下载场景里是诊断仪推数据,在上传场景里是诊断仪拉数据
关键区别:下载时,0x36是诊断仪发给ECU;上传时,0x36是诊断仪发给ECU来请求下一块数据,ECU在正响应里把数据带回去。
24.4.3 正响应格式
#1: 0x77#2: maxNumberOfBlockLength (ECU每次能返回的最大字节数)
24.5 状态机与流程控制
这三个服务是紧密关联的。我习惯用一个简单的状态机来管理:
IDLE:空闲状态,等待0x34或0x37
DOWNLOAD:下载进行中,只接受0x36(且序列号正确)
UPLOAD:上传进行中,只接受0x36(用于拉数据)
如果在DOWNLOAD或UPLOAD状态下收到新的0x34或0x37,应该返回NRC 0x21(忙重复)。
注意:如果在传输过程中收到其他诊断请求(比如0x10诊断会话控制),ECU应该终止当前传输。我见过有些实现会继续传完再响应,这其实不符合规范。
24.6 实际项目中的经验
最后分享几个我在项目里踩过的坑:
超时处理:诊断仪发完一包0x36后,ECU必须在P2时间内响应。如果ECU写Flash太慢(比如擦除扇区要几百毫秒),就会超时。我的做法是:先响应0x76,再异步执行写入。
数据校验:0x34和0x37本身不带校验。我建议在应用层加一个CRC校验,比如在数据传完后用0x31(例程控制)来触发校验。
中断保护:在写Flash时,要关中断。但关中断时间不能太长,否则会影响CAN通信。我一般每写一个扇区就开一次中断,处理一下接收到的报文。
这三个服务的内容就这些。说白了,它们就是UDS里的“文件传输协议”。理解了状态机的流转和缓冲区的管理,实现起来并不难。但细节决定成败——序列号的处理、地址的校验、超时的控制,每一个都可能让你的ECU在测试台上“翻车”。
夜雨聆风