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)
// 初始化最大块大小,初始值为 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即完成刷写。

Logo

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

更多推荐