> 技术文档 > 适用于Kvaser CAN硬件的开源UDS诊断软件——EcuBus-Pro

适用于Kvaser CAN硬件的开源UDS诊断软件——EcuBus-Pro

EcuBus-Pro是一款免费的开源诊断软件,可以解决汽车ECU固件更新和诊断通信问题。本教程旨在演示如何使用EcuBus-Pro软件实现完整的UDS(统一诊断服务)Bootloader。适用于以下应用场景:

  • 汽车电子控制单元(ECU)的固件在线升级
  • 汽车诊断系统的开发和测试
  • 基于CAN总线的车载网络诊断
  • 基于UDS,CAN-TP的bootloader升级流程
  • 符合ISO 14229标准的诊断服务开发

  1. 测试环境准备工作
  • 下载并安装软件:Install | EcuBus-Pro
  • CAN/CAN FD设备:软件兼容所有Kvaser CAN设备,本示例使用Leaf V3

EcuBus-Pro对Kvaser硬件具有很好的兼容性,其中Kvaser Leaf v3是Kvaser主推的高性价比单通道CAN FD通讯仪,是Kvaser leaf light v2的升级版。支持每秒20000条报文,50μs时间戳。

  • 开发板:具备UDS功能的开发板即可,本示例使用的DUT为S32K144EVB-Q100

  1. 工程配置

2.1 基础配置

在Hardware>Device界面配置如下,选择Leaf V3,其他CAN卡配置类似,只要保证波特率在500K。

可以根据实际情况来选择对应的采样点。

设定寻址方式,S32K144官方的CAN UDS Bootloader例程采用Normal fixed addressing,配置如下。

2.2诊断Services配置

诊断服务配置是UDS Bootloader实现的核心环节,每个诊断服务都有其特定的功能和作用,用户需要先把bootloader升级所用到的所有服务配置出来。

2.2.1 DiagnosticSessionControl(诊断会话控制)($10)服务

进入扩展会话的配置如下:

进入编程会话的配置如下:

2.2.2 ECUReset(ECU 复位)($11)服务

硬件复位的配置如下,这里使用硬件复位。

null

2.2.3 SecurityAccess(安全访问)($27)服务

请求种子的配置如下,需要进入Response界面,将securitySeed的长度改为128bit,不然无法收全MCU回复的种子数据。

  • 回复秘钥的配置如下,data段会在脚本中将大小改为128bit,并填充计算得到的密钥。

2.2.4 CommunicationControl(通信控制)($28)服务

  • 关闭网络管理报文和正常报文的收发器,配置如下。

2.2.5 WriteDataByIdentifier(通过标识符写数据)($2E)服务

  • 写入指定的DID,S32K144官方的CAN UDS Bootloader例程使用是的0xF15A,配置如下。

2.2.6 RoutineControl(例程控制)($31)服务

routineID为0x0202的例程用于通知MCU进行CRC校验,配置如下。

routineID为0xFF00的例程用于通知MCU进行Flash擦除,配置如下。

routineID为0xFF01的例程用于通知MCU进行固件检查,配置如下。

2.2.7 RequestDownload(请求下载)($34)服务

请求下载的配置如下,存储地址和存储空间在实际使用时,会在脚本中进行赋值。

2.2.8 TransferData(传输数据)($36)服务

传输数据的配置如下,实际使用时会在脚本中重复调用并赋值。

2.2.9 RequestTransferExit(请求传输终止)($37)服务

停止传输的配置如下。

2.2.10 JobFunction

什么是Job,Job是EcuBus-Pro一种抽象的服务,当使用Job的时候,必须有对应的脚本,通过脚本,来实现Job的返回,Job的返回要求是一个数组,可以是0-N个正常的UDS服务或者Job。Job通常用于固件的下载,上传等需要多个未知数量的0x36服务的时候,当然也可以用于其他任何你想用的情况。

增加空的JobFunction0和JobFunction1,方便通过脚本将下载相关的服务进行组合。

2.3 Sequence配置

序列配置定义了整个UDS固件更新过程的执行顺序和逻辑流程,确保各个诊断服务按照正确的时序执行,会用到之前配置过的UDS服务。

整个UDS升级的流程配置如下图:

主要分三个阶段:

预编程阶段:

  1. 通过$10服务进入扩展会话 (DiagnosticSessionControl160);
  2. 通过$28服务关闭网络管理报文和正常报文的收发 (CommunicationControl400);

编程阶段:

  1. 通过$10服务进入编程会话(DiagnosticSessionControl161);
  2. 通过$27服务请求种子并返回计算出的秘钥,成功通过验证 (SecurityAccess390,SecurityAccess391);
  3. 通过$2E服务写入指定的DID(0xF15A) (WriteDataByIdentifier460);
  4. 通过$34服务发起flash driver的下载请求 (JobFunction0);
  5. 通过$36服务传输flash driver (JobFunction1);
  6. 通过$37服务结束传输 (JobFunction1);
  7. 通过$31服务通知MCU进行CRC校验,和Flash擦除 (RoutineControl490)
  8. 通过$34服务发起APP的下载请求;(JobFunction0)
  9. 通过$36服务传输APP;(JobFunction1)
  10. 通过$37服务结束传输;(JobFunction1)
  11. 通过$31服务通知进行CRC校验,和固件检查 (RoutineControl491)

后编程阶段:

  1. 通过$11服务进行硬件复位 (ECUReset170)

3.实现JobFunction

下面是具体操作的详细步骤

3.1 准备工作

在工程所在目录新建一个ts文件

将升级需要用到的文件放到工程所在目录;

在UDS Tester加载该脚本文件,并打开vs code进行脚本编写;

3.2 脚本编写

3.2.1 导入必要的模块

// 导入必要的模块import crypto from \'crypto\'import { CRC, DiagRequest, DiagResponse } from \'ECB\'import path from \'path\'import fs from \'fs/promises\'

3.2.2 准备需要的CRC算法,以及相关变量

// 创建 CRC 实例,配置参数:类型为 \'self\',位数 16,多项式 0x3d65,初始值 0,异或值 0xffff,输入输出反转const crc = new CRC(\'self\', 16, 0x3d65, 0, 0xffff, true, true)// 初始化最大块大小,初始值为 undefinedlet maxChunkSize: number | undefined = undefined// 初始化文件内容缓冲区,初始值为 undefinedlet content: undefined | Buffer = undefined

3.2.3 固件文件的配置

// 定义文件列表,包含每个文件的起始地址和文件路径const fileList: { addr: number file: string}[] = [ { // 第一个文件的起始地址 addr:0x1FFF8010, // 第一个文件的路径,拼接自项目根目录、bin 文件夹和文件名 file: path.join(process.env.PROJECT_ROOT, \'bin\', \'flash_api.bin\') }, { // 第二个文件的起始地址 addr:0x00014200, // 第二个文件的路径,拼接自项目根目录、bin 文件夹和文件名 file: path.join(process.env.PROJECT_ROOT, \'bin\', \'S32k144_UDS_Bootloader_App_Test.bin\') }]

3.2.4 安全访问的相关处理

/** * 监听安全访问响应事件,对收到的安全种子进行加密并发送响应请求 * @param v - 安全访问响应对象 */Util.On(\'S32K144_CAN_UDS_Bootloader.SecurityAccess390.recv\', async (v) => { // 从响应中获取安全种子数据 const data = v.diagGetParameterRaw(\'securitySeed\') // 创建 AES-128-CBC 加密器,使用固定密钥和全零初始化向量 const cipher = crypto.createCipheriv( \'aes-128-cbc\', Buffer.from([0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15]), Buffer.alloc(16, 0) ) // 对安全种子数据进行加密 const encrypted = cipher.update(data) // 完成加密操作 cipher.final() // 创建安全访问响应请求对象 const req = DiagRequest.from(\'S32K144_CAN_UDS_Bootloader.SecurityAccess391\') // 设置请求参数 \'data\' 的原始值为加密后的数据 req.diagSetParameterRaw(\'data\', encrypted) // 发起服务变更请求 await req.changeService()})

3.2.5 实现JobFunction0,包含获取固件、请求下载、CRC校验的功能

/** * 注册作业函数 0,处理文件下载请求和 CRC 校验请求 */Util.Register(\'S32K144_CAN_UDS_Bootloader.JobFunction0\', async () => { // 从文件列表中取出第一个文件项 const item = fileList.shift() if (item) { // 创建请求下载的诊断请求对象 const r34 = DiagRequest.from(\'S32K144_CAN_UDS_Bootloader.RequestDownload520\') // 创建 4 字节的缓冲区用于存储内存地址 const memoryAddress = Buffer.alloc(4) // 将文件的起始地址以大端字节序写入缓冲区 memoryAddress.writeUInt32BE(item.addr) // 设置请求参数 \'memoryAddress\' 的原始值为内存地址缓冲区 r34.diagSetParameterRaw(\'memoryAddress\', memoryAddress) // 异步读取文件内容到缓冲区 content = await fs.readFile(item.file) // 计算文件内容的 CRC 值 const crcResult = crc.compute(content) // 创建例行控制的诊断请求对象,用于 CRC 校验 const crcReq = DiagRequest.from(\'S32K144_CAN_UDS_Bootloader.RoutineControl490\') // 创建 4 字节的缓冲区用于存储 CRC 结果 const crcBuffer = Buffer.alloc(4) // 将 CRC 结果以大端字节序写入缓冲区的后 2 字节 crcBuffer.writeUInt16BE(crcResult, 2) // 设置例行控制选项记录参数的大小为 4 字节(32 位) crcReq.diagSetParameterSize(\'routineControlOptionRecord\', 4 * 8) // 设置例行控制选项记录参数的原始值为存储 CRC 结果的缓冲区 crcReq.diagSetParameterRaw(\'routineControlOptionRecord\', crcBuffer) // 发起例行控制服务请求 await crcReq.changeService() // 设置请求下载诊断请求对象的 \'memorySize\' 参数为文件内容的长度 r34.diagSetParameter(\'memorySize\', content.length) // 监听请求下载诊断请求的响应事件 r34.On(\'recv\', (resp) => { // 从响应中获取最大块长度参数,并读取其第一个字节作为最大块大小 maxChunkSize = resp.diagGetParameterRaw(\'maxNumberOfBlockLength\').readUint8(0) }) // 返回包含请求下载诊断请求对象的数组 return [r34] } else { // 若文件列表为空,返回空数组 return [] }})

3.2.6 实现JobFunction1,包含传输固件、退出传输、固件验证的功能

/** * 注册作业函数 1,处理文件分块传输请求和传输退出请求 */Util.Register(\'S32K144_CAN_UDS_Bootloader.JobFunction1\', () => { // 检查最大块大小是否未定义或过小 if (maxChunkSize == undefined || maxChunkSize <= 2) { // 若不满足条件,抛出错误 throw new Error(\'maxNumberOfBlockLength is undefined or too small\') } if (content) { // 最大块大小减去 2 maxChunkSize -= 2 // 确保最大块大小是 8 的倍数 if (maxChunkSize & 0x07) { maxChunkSize -= maxChunkSize & 0x07 } // 计算文件内容需要分块的数量 const numChunks = Math.ceil(content.length / maxChunkSize) // 初始化存储传输请求对象的数组 const list = [] // 循环生成每个分块的传输请求 for (let i = 0; i < numChunks; i++) { // 计算当前分块的起始位置 const start = i * maxChunkSize // 计算当前分块的结束位置,不超过文件内容的长度 const end = Math.min(start + maxChunkSize, content.length) // 从文件内容中截取当前分块 const chunk = content.subarray(start, end) // 创建传输数据的诊断请求对象 const transferRequest = DiagRequest.from(\'S32K144_CAN_UDS_Bootloader.TransferData540\') // 设置传输请求参数记录的大小为当前分块的字节数 transferRequest.diagSetParameterSize(\'transferRequestParameterRecord\', chunk.length * 8) // 设置传输请求参数记录的原始值为当前分块 transferRequest.diagSetParameterRaw(\'transferRequestParameterRecord\', chunk) // 计算块序号 (从 1 开始) const blockSequenceCounter = Buffer.alloc(1) // 使用循环计数 1 - 255,将块序号写入缓冲区 blockSequenceCounter.writeUInt8((i + 1) & 0xff) // 设置块序号参数的原始值为存储块序号的缓冲区 transferRequest.diagSetParameterRaw(\'blockSequenceCounter\', blockSequenceCounter) // 将传输请求对象添加到数组中 list.push(transferRequest) } // 创建请求传输退出的诊断请求对象 const r37 = DiagRequest.from(\'S32K144_CAN_UDS_Bootloader.RequestTransferExit550\') // 设置传输请求参数记录的大小为 0 r37.diagSetParameterSize(\'transferRequestParameterRecord\', 0) // 将请求传输退出的诊断请求对象添加到数组中 list.push(r37) // 清空文件内容缓冲区 content = undefined // 重置最大块大小为 undefined maxChunkSize = undefined // 返回包含所有传输请求和传输退出请求的数组 return list } else { // 若文件内容缓冲区为空,返回空数组 return [] }})

4 启动Bootloader及完成刷写

4.1 启动Bootloader

点击左上角绿色按钮启动刷写。

4.2 完成结果

显示Success即完成刷写。