适用于Kvaser CAN硬件的开源UDS诊断软件——EcuBus-Pro
EcuBus-Pro是一款免费的开源诊断软件,可以解决汽车ECU固件更新和诊断通信问题。
EcuBus-Pro是一款免费的开源诊断软件,可以解决汽车ECU固件更新和诊断通信问题。本教程旨在演示如何使用EcuBus-Pro软件实现完整的UDS(统一诊断服务)Bootloader。适用于以下应用场景:
- 汽车电子控制单元(ECU)的固件在线升级
- 汽车诊断系统的开发和测试
- 基于CAN总线的车载网络诊断
- 基于UDS,CAN-TP的bootloader升级流程
- 符合ISO 14229标准的诊断服务开发
- 测试环境准备工作
- 下载并安装软件: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

- 工程配置
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升级的流程配置如下图:

主要分三个阶段:
预编程阶段:
- 通过$10服务进入扩展会话 (DiagnosticSessionControl160);
- 通过$28服务关闭网络管理报文和正常报文的收发 (CommunicationControl400);
编程阶段:
- 通过$10服务进入编程会话(DiagnosticSessionControl161);
- 通过$27服务请求种子并返回计算出的秘钥,成功通过验证 (SecurityAccess390,SecurityAccess391);
- 通过$2E服务写入指定的DID(0xF15A) (WriteDataByIdentifier460);
- 通过$34服务发起flash driver的下载请求 (JobFunction0);
- 通过$36服务传输flash driver (JobFunction1);
- 通过$37服务结束传输 (JobFunction1);
- 通过$31服务通知MCU进行CRC校验,和Flash擦除 (RoutineControl490)
- 通过$34服务发起APP的下载请求;(JobFunction0)
- 通过$36服务传输APP;(JobFunction1)
- 通过$37服务结束传输;(JobFunction1)
- 通过$31服务通知进行CRC校验,和固件检查 (RoutineControl491)
后编程阶段:
- 通过$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)
// 初始化最大块大小,初始值为 undefined
let maxChunkSize: number | undefined = undefined
// 初始化文件内容缓冲区,初始值为 undefined
let 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即完成刷写。

DAMO开发者矩阵,由阿里巴巴达摩院和中国互联网协会联合发起,致力于探讨最前沿的技术趋势与应用成果,搭建高质量的交流与分享平台,推动技术创新与产业应用链接,围绕“人工智能与新型计算”构建开放共享的开发者生态。
更多推荐



所有评论(0)