万字长文——基于CANoe/CAPL的UDS Bootloader上位机实现(附完整可运行代码及工程文件)

关于UDS Bootloader的理论细节和具体流程,网上已有许多博主撰写了非常优秀的文章进行深入解析,本文就不再赘述。

这里我们直接长枪直入,跳过理论铺垫,专注于CANoe环境下的CAPL代码实战,手把手带你实现ECU升级的核心逻辑。

目录

一、UDS升级的基本流程:

二、刷写前的工程配置(演示版本,CANoe12SP6)

[2.1 实现方法对比](#2.1 实现方法对比)

方案一:使用CANoe诊断函数

方案二:使用TP层函数

[方案三:基于定时器/Output/On Message手写TP](#方案三:基于定时器/Output/On Message手写TP)

[2.2 使用诊断函数前的配置](#2.2 使用诊断函数前的配置)

[2.3 刷写可视化面板绘制](#2.3 刷写可视化面板绘制)

[2.4 刷写可视化相关系统变量创建及绑定](#2.4 刷写可视化相关系统变量创建及绑定)

三、刷写代码的CAPL实现

[3.1 使用到的主要诊断函数及变量类型简介](#3.1 使用到的主要诊断函数及变量类型简介)

[3.2 Step1:进入扩展会话](#3.2 Step1:进入扩展会话)

[3.3 Step2:关闭DTC检测](#3.3 Step2:关闭DTC检测)

[3.4 Step3:关闭非诊断通信](#3.4 Step3:关闭非诊断通信)

[3.5 Step4:检查预编程条件](#3.5 Step4:检查预编程条件)

[3.6 Step5:进入编程会话](#3.6 Step5:进入编程会话)

[3.7 Step6:安全等级解锁](#3.7 Step6:安全等级解锁)

[3.7.1 基于CANoe官方提供的SeedKey DLL算法生成的VS工程,自己编译一个DLL使用](#3.7.1 基于CANoe官方提供的SeedKey DLL算法生成的VS工程,自己编译一个DLL使用)

[3.7.2 CAPL代码实现安全解锁](#3.7.2 CAPL代码实现安全解锁)

[3.8 Step7:写入刷写指纹](#3.8 Step7:写入刷写指纹)

[3.9 Step8:请求下载数据(Driver)](#3.9 Step8:请求下载数据(Driver))

[3.9.1 CAPL解析Hex文件](#3.9.1 CAPL解析Hex文件)

[3.9.2 CAPL解析S19文件](#3.9.2 CAPL解析S19文件)

[3.9.3 CAPL解析Bin文件](#3.9.3 CAPL解析Bin文件)

[3.9.4 实现通用的解析器](#3.9.4 实现通用的解析器)

[3.9.5 CAPL实现34请求下载](#3.9.5 CAPL实现34请求下载)

[3.10 Step9:传输Driver数据](#3.10 Step9:传输Driver数据)

[3.11 Step10 退出Driver数据传输](#3.11 Step10 退出Driver数据传输)

[3.12 Step11 Driver数据完整性检查(CRC32)](#3.12 Step11 Driver数据完整性检查(CRC32))

[3.13 Step12 擦除APP块数据](#3.13 Step12 擦除APP块数据)

[3.14 Step13 请求下载数据(APP)](#3.14 Step13 请求下载数据(APP))

[3.15 Step14 传输APP数据](#3.15 Step14 传输APP数据)

[3.16 Step15 退出App数据传输](#3.16 Step15 退出App数据传输)

[3.17 Step16 App数据完整性检查(CRC32)](#3.17 Step16 App数据完整性检查(CRC32))

[3.18 Step17 编程依赖性检查](#3.18 Step17 编程依赖性检查)

[3.19 Step18 ECU复位](#3.19 Step18 ECU复位)

[3.20 Step19 进入扩展会话](#3.20 Step19 进入扩展会话)

[3.21 Step20 开启DTC检测](#3.21 Step20 开启DTC检测)

[3.22 Step21 开启非诊断通信](#3.22 Step21 开启非诊断通信)

[3.23 Step22 进入默认会话](#3.23 Step22 进入默认会话)

[3.24 整合执行所有刷写步骤](#3.24 整合执行所有刷写步骤)

[3.25 执行刷写,进行测试;](#3.25 执行刷写,进行测试;)

四、总结与展望


一、UDS升级的基本流程:

正式写代码前,我们先把要实现的通用流程列一下:

  1. 进入扩展会话 (10 03)
  2. 关闭DTC检测 (85 02)
  3. 关闭非诊断通信 (28 03 03)
  4. 检查预编程条件 (31 01 02 03)
  5. 进入编程会话 (10 02)
  6. 安全等级解锁 (27 xx)
  7. 写入刷写指纹 (2E F1 90 XX XX...)
  8. 请求下载数据(Driver) (34 00 44 + 4字节起始地址 + 4字节长度)
  9. 传输数据(Driver) (36 + Sequence + Data)
  10. 退出数据传输 (37)
  11. 完整性检查 (31 01 F0 01 + CRC)
  12. APP地址块擦除 (31 01 FF 00 + 4字节起始地址 + 4字节长度)
  13. 请求下载数据(App) (34 00 04 + 4字节起始地址 + 4字节长度)
  14. 传输数据(App) (36 + Sequence + Data)
  15. 退出数据传输 (37)
  16. 完整性检查 (31 01 F0 01 + CRC)
  17. 编程依赖性检查 (31 01 FF 01)
  18. ECU复位 (11 01)
  19. 进入扩展会话(10 03)
  20. 开启DTC检测 (85 02)
  21. 开启非诊断通信(28 00 03)
  22. 进入默认会话 (10 01)

常见的升级流程大概是这样,不同的主机厂,不同的ECU可能在步骤上略有偏差,例如会增加一些读版本号机制,写零件号等...但大体的流程是一致的,学会了一类流程的代码实现之后,增删改流程是非常容易的,话不多说,下面我们直接开始实现。

二、刷写前的工程配置(演示版本,CANoe12SP6)

要想实现UDS升级,首要任务是搞定诊断数据的收发。在CANoe环境下,通常有三种主流实现路径,它们各有优劣。为了帮你理清思路,我将这三种方法的优缺点整理如下,方便对比选择:

2.1 实现方法对比

方案一:使用CANoe诊断函数

这是最直接的方式,直接调用CANoe内置的诊断API(如DiagSendRequestDiagGetLastResponse等)。

  • 优点
    • 开发效率极高:封装完善,几行代码即可完成复杂服务的发送与接收。
    • 容错性好:底层协议处理由CANoe接管,无需手动处理流控、超时等细节。
  • 缺点
    • 依赖诊断数据库:必须加载CDD/PDX/ODX等数据库文件或配置基础诊断模块,脱离了这些文件,代码寸步难行。
    • 灵活性受限:难以在代码运行时动态修改诊断ID(例如切换目标ECU地址),通常需要在配置阶段写死。

方案二:使用TP层函数

利用CANoe提供的传输层接口(如osek_tp.dll),在传输层进行数据交互。

  • 优点
    • 动态性强:可以在CAPL代码中动态修改诊断对象的ID,非常适合需要动态切换刷写目标或地址的场景。
    • 控制力适中:比纯手写TP简单,又比诊断函数灵活。
  • 缺点
    • 环境依赖:需要在CANoe节点的配置中显式加载VModule或DLL文件,配置过程相对繁琐。

方案三:基于定时器/Output/On Message手写TP

完全脱离CANoe的高级封装,利用底层定时器(Timer)、报文发送(Output)和接收回调(On Message)从零构建传输逻辑。

  • 优点
    • 移植性无敌:这是最接近硬件裸机开发的方式。代码逻辑与工具解耦,稍作修改即可移植到其他平台(如TsMaster/ZxDoc/Python),仅需替换底层的收发函数。
    • 无外部依赖:不需要加载任何DLL或数据库,纯代码实现。
  • 缺点
    • 开发难度大:需要开发者对ISO 15765-2(TP层协议)有深刻理解,必须手动处理首帧、流控帧、连续帧的分包与重组,以及超时重传逻辑,对编码能力要求较高。

考虑到读者的水平可能高低不齐,为了通用性和简便性,本篇文章暂时选择使用第一种方案,借助诊断函数实现。后续计划会陆续发表基于osek_tp和手写tp的方法进行实现。

2.2 使用诊断函数前的配置

习惯性地,我通常会先创建一个文件夹,用于存放本工程的所有文件和数据;这里,我先创建一个一个文件夹,并出于我的习惯,会在文件夹内创建如下文件夹;

CaplIncludesFile:我通常用来存放一些可供其他节点调用的库文件,如公共的.can/.cin/.dll文件;

Databases:我通常用来存放总线数据库文件,如dbc/arxml/ldf文件等;

Diagnostic:我通常用来存放诊断数据库,如cdd/pdx/odx文件以及安全访问的dll文件等;

Nodes:我通常用来存放db中的各节点的仿真代码.can文件;

Panels:我通常用来存放工程中所用到的panel面板;

SysVariables:我通常用来存放工程所使用的系统变量文件;

TestModules:我通常用来存放测试模块的一些主体代码和测试报告/Log文件;

Logging:我通常用来存放总线日志Log文件;

随后打开CANoe,创建一个CAN 500波特率1通道的工程,并将配置文件也存储到这个文件夹内,如上图;

注意:现在创建完毕之后,开始创建诊断控制台,必须创建诊断控制台之后,才能使用CAPL的诊断函数

点击上图,打开诊断配置界面,随后在CAN处右击鼠标,有cdd/pdx/odx等诊断数据库文件的朋友,可以添加自己的数据库文件,没有的朋友,则可以像我一样,选择第二个选项,添加一个UDS基础诊断文件;

添加完毕诊断数据库或者添加基础诊断文件完毕后,就会得到上图一样的界面,注意,红框中的"ECU qualifier"这里后面显示框的名字,这个名字很关键,如果是添加的cdd等文件,这个名字会是一个其他的名字,如果是添加的基础诊断文件,就会和我的一样。

随后点击Transport Layer 在中间配置诊断的请求ID和响应ID,以及功能寻址ID,还有一些时间参数,时间参数我就不改了,这里我简单修改一下三个ID。

我这里设置请求ID为0x601,响应ID为0x701,功能寻址ID为0x7DF;大家可以根据自己的ECU实际情况设置;

要实现刷写,几乎所有的ECU都必须通过安全访问,而要通过CANoe的诊断来解锁ECU,必须添加SeedKey的dll文件,接下来在切换到Diagnostic Layer界面,在下方的Seed & Key DLL处添加自己的 安全算法dll文件(如果有的话,就添加在这里面,如果没有,后面我会教大家如何制作这个dll文件。这里我也先空着,稍后的章节讲解到如何制作dll的时候,会制作完毕之后一起添加。)

到此,我们的诊断配置就暂时结束了,接着就是按照第一节的流程,来逐步实现代码。

2.3 刷写可视化面板绘制

为了方便修改被刷写的文件,以及方便观察CAPL脚本中输出的刷写进度等信息,我们最好是创建一个Panel面板来可视化显示和修改这些数据。

在Panel处,选择New Panel;

关于Panel中各个控件的具体用法,不了解的朋友可以参考我的这篇文章:

CANoe上位机常见的Panel控件使用

这里我就不再讲解了;

直接添加panel面板以及控件并保存到Panels文件夹;

这里添加如下控件:

2个文件路径选择框,控件名:"Path Dialog",并设置它的过滤类型为"Driver File|*.hex;*.s19;*.bin|All Files|*.*" 这样子在点击旁边的文件夹图标时,可以过滤文件类型,只显示Hex/S19/bin文件/

另一个设置为"App File|*.hex;*.s19;*.bin|All Files|*.*" 即可让APP选择的时候,也只显示hex/s19/bin文件;

1个按钮,设置显示的文字为"开始刷写",用以点击这个按钮时,执行刷写流程;

2个进度条和2个输入输出框,对齐放在一起,用于显示刷写进度和APP传输进度(Driver文件通常传输非常快,在几百ms内,这里就不显示Driver文件的传输进度了)

1个输入输出框,用于显示刷写的结果;

2.4 刷写可视化相关系统变量创建及绑定

在第三步中,我们创建了一个Panel面板用于可视化刷写进度,刷写结果,以及设置刷写文件的路径,但这些控件并没有与任何系统变量相关联,没有绑定系统变量/信号的Panel控件是没有功能的,为了使得这些控件能够正常工作,我们还需要创建几个系统变量;关于系统变量的创建,如果朋友们还不是很会操作,可以参考我的这篇文章:这边文章主要是讲解函数的,但在其的第6部分,提到了系统变量的创建方法;

系统变量创建方法

点击如图图标,打开系统变量界面,并右击创建一个系统变量;

首先,设置一个NameSpace,名为"EcuBootload",并设置这个系统变量的名称为"DriverFilePath",并设置其类型为String类型,点击OK;

紧接着,继续创建其他的变量;

这里创建了:

一个"AppFilePath",设置为String类型,用于存储APP文件的路径;

一个"DriverFilePath",设置为String类型,用于存储Driver文件的路径;

一个"ApptransmitProgress",设置为Int32类型,用于显示APP文件的传输进度;

一个"EcuFlashProgress",设置为Int32类型,用于显示当前的刷写流程进度;

一个"EcuFlashResult",设置为String类型,用于显示本次刷写的结果;

一个"StartFlashButton",设置为Int32类型,用于控制刷写流程启动;

为了保持系统变量的持久性与一致性,这里再选择一下将系统变量导出到文件中进行存储,

点击如图的图标,选择 Export All 进行系统变量的导出,我将其导出到SysVariables文件夹中,命名为SysVariable

做这一步的目的是为了方便移植,目前暂无其他用处;

创建完系统变量后,我们把这些系统变量和2.3中的Panel面板里的控件进行关联,

依次将

AppFilePath关联到APP 文件的路径选择器控件;

DriverFilePath关联到Driver 文件的路径选择器控件;

ApptransmitProgress关联到数据传输进度的进度条以及后面的输入框控件;

EcuFlashProgress关联到刷写进度的进度条以及后面的输入框控件;

EcuFlashResult关联到刷写结果的显示框控件;

StartFlashButton关联到开始刷写的按钮控件;

关联完毕后,就可以关闭Panel编辑界面了;

三、刷写代码的CAPL实现

在CANoe中编写CAPL代码,通常有三种节点类型,分别是,Simulation Step/Test Step以及Measurement Step,分别是仿真节点,测试节点和测量节点。

测量节点在Measurement Step中可以找一个方块的地方右击鼠标选择Insert Program Node插入,通常用于纯粹的测量和计算。这里不做演示,我在日常上位机开发中使用的也较少,个人认为使用频率不高。

仿真节点和测试节点,则可在Simulation Step中右击总线处进行插入,第一个Network Node就是仿真节点,而下方的三个Test Module就是测试节点,仿真节点和测试节点各有优劣,仿真节点可以触发CAPL中的任意事件,而测试节点则无法触发例如on start/on stop等事件,但仿真节点可以实现同步延时(一般可以认为是死等,可以一直等待...),这里为了简单,我选择使用测试节点,因为测试节点的延时等待机制,非常契合UDS中发生请求之后,等待到ECU响应的过程,实现刷写上位机比较容易,而仿真节点的话,要想实现刷写,需要对状态机的切换非常熟练,否则很难做到。

接下来,我们可以选择插入一个CAPL Test Module(使用起来最方便最简单)。

插入完毕之后,鼠标右击该节点,可以在Module name处给节点改个利于辨识的名字。

并且设置该节点的启动方法为系统变量启动,选择2.4中创建的StartFlashButton系统变量。

随后,正式开始我们的编程之旅;

首次点击如图框选的铅笔图标,会弹出弹窗让我们选择一个地方来存放由此节点创建的CAPL代码.can文件,这里我在先前的文件中打开TestMoudle文件夹,并且再在里面新建一个名为Bootload 的文件夹,将.can文件存储在我的Bootload文件夹内,命名为我的BootloadMain.can随后点击打开,即可打开CAPL编辑器开始代码的编写。

3.1 使用到的主要诊断函数及变量类型简介

|---|---|---|---|
| diagRequest;//诊断请求类型,本质上是一个C++的类,或者可以简单理解为是一个结构体,里面包含了要请求的uds数据数组,要请求的数据长度等信息。要通过CAPL的诊断函数发送诊断请求,则此类型是必须了解的 diagResponse;//诊断响应类型,本质上也是一个C++的类,里面同样地,包含了来自目标ECU返回的uds数据,以及返回的数据长度等信息,要通过CAPL的诊断函数获取诊断响应里的信息,这个类型也是必须要了解的。 ||||

|---|---|---|---|
| long diagSetTarget (char ecuName[]);//用于设置目标诊断ECU,也就是你的诊断请求要发到那个ECU里面去,这个函数的参数,表面上写的是ecuName,看上去是ECU的名称,实际上,应该填写第二章节中提到的诊断配置界面上的"ECU qualifier"这里后面显示框的名字,因为这各qualifier里面就包含了对应的目标ECU的Tx,Rx还有功能寻址ID。 ||||

|---|---|---|---|
| long diagSendRequest (diagRequest obj);//用于向目标ECU发送指定的诊断请求 ||||

|---|---|---|---|
| long diagGetLastResponse (diagRequest req, diagResponse respOut); long diagGetLastResponse (diagResponse respOut);//该函数有两种重载形式,所谓重载,就是给一个函数提供了可以传入不同种类和不同参数数量的方法,这里的第一种形式是2个参数的,可以获取某个指定的诊断请求的最近一次响应,而第二种形式,只能获取到最近的一次诊断响应数据,无法指定某个请求。 ||||

|---|---|---|---|
| long diagGetLastResponseCode (diagRequest req); long diagGetLastResponseCode ();//这个函数可以获取上一次的诊断响应的响应码,如果是否定响应,返回值为-1,简而言之,可以使用这个函数来判断上一次的诊断响应是不是肯定响应,同样具备两种重载,具备指定请求的能力。 ||||

|---|---|---|---|
| long TestWaitForDiagResponse (diagRequest request, dword timeout); // form 1 long TestWaitForDiagResponse (dword timeout); // form 2 这个函数可以用来判断在一段时间内诊断是否响应,如果响应了,则会返回1,否则返回0.同样地,也有重载形式,可以指定等待某个诊断请求的响应。 ||||

|---|---|---|---|
| long diagGetPrimitiveData (diagResponse obj, byte* buffer, DWORD buffersize); long diagGetPrimitiveData (diagRequest obj, byte* buffer, DWORD buffersize); long diagSetPrimitiveData (diagResponse obj, byte* buffer, DWORD buffersize); long diagSetPrimitiveData (diagRequest obj, byte* buffer, DWORD buffersize);这个函数可以用来设置一个诊断请求/响应的tp层数据,例如要把某个诊断请求的数据设置为10 03,则只要定义一个数组,数组数据填写为10 03,并调用这个函数设置给对应的诊断请求或者响应即可。 ||||

|---|---|---|---|
| long diagSetPrimitiveByte( diagRequest request, DWORD bytePos, DWORD newValue); long diagSetPrimitiveByte( diagResponse response, DWORD bytePos, DWORD newValue);//这个函数和上一个的功能一致,都是用来给指定的诊断请求/响应设置数据的,区别就在于,上一个函数是可以一次性设置所有的字节,也就是把数据全部设置完,而这个函数,一次只能设置一个指定的字节。 ||||

|---|---|---|---|
| long diagGetPrimitiveData (diagResponse obj, byte* buffer, DWORD buffersize); long diagGetPrimitiveData (diagRequest obj, byte* buffer, DWORD buffersize);//这个函数可以用来获取一个指定的诊断请求/响应的tp层数据,会把所有的数据都填充到传入的数组buffer中。 ||||

|---|---|---|---|
| long diagGetPrimitiveByte( diagRequest request, DWORD bytePos); long diagGetPrimitiveByte( diagResponse response, DWORD bytePos);//这个函数和上一个基本一致,也是获取一个指定的诊断请求/响应的tp层数据,区别在于,此函数仅能获取一个指定的字节。 ||||

|---|---|---|---|
| long DiagGetPrimitiveSize( diagRequest request); long DiagGetPrimitiveSize( diagResponse response);//这个函数用于获取一个指定的诊断请求/响应的tp层数据长度。 ||||

|---|---|---|---|
| long diagGenerateKeyFromSeed ( byte seedArray[], dword seedArraySize, dword securityLevel, char variant[], char ipOption[], byte keyArray[], dword maxKeyArraySize, dword& keyActualSizeOut); // form 1 long DiagGenerateKeyFromSeed(char ecuQualifier[], byte seedArray[] , dword seedArraySize, dword securityLevel, char variant[], char option[] , byte keyArray[], dword maxKeyArraySize, dword& keyActualSizeOut); // form 2 这个函数用于27服务计算密钥,通过传入seed数组和key数组以及对应的安全等级,会将计算出的key填充在key数组中,并以输出参数(指针(引用))的方式,将计算的密钥的字节数填充给到keyActualSizeOut这个参数; ||||

注意,以上所有的参数为DiagRequest或者DiagResponse类型的函数,除了上述这种用法之外,还可以使用诊断类型的成员函数的方法实现(也就是原本的long diagSendRequest (diagRequest obj);这种写法,当我们定义了一个DiagRequest * obj,定义了一个obj之后,也可以通过 obj.SendRequest();这样子的写法来实现,简单总结一下就是,所有的参数是诊断请求/诊断响应的函数,均可以省略掉前面的Diag,然后直接使用obj.xxxx实现)

以上就是在UDS刷写中主要使用到的诊断函数了,CAPL诊断函数库中有着非常多的函数,功能也各式各样,大家可以自行查看CAPL的帮助文档,包括上述我介绍的这些函数,我也只是做了最基本的简介,具体的用法和功能,也可以通过CAPL帮助文档了解。

3.2 Step1:进入扩展会话

这里我们创建一个函数,函数名就叫FlashStepA吧,用来表示进入第一步,后面的其他步骤就暂时以BCDEFG...等命名;

首先创建一个全局的诊断请求对象,我定义为gDiagRequest;

再定义一个全局的诊断响应对象,我定义为gDiagResponse;

然后就可以开始编写函数了,这里编写一个返回值为int的函数,函数名"FlashStepA"

用于进入扩展会话,如果进入成功(收到ECU回复的肯定响应),就返回1给调用者,否则返回0给调用者;

cpp 复制代码
variables
{
  diagRequest * gDiagRequest;//定义全局诊断请求
  diagResponse * gDiagResponse;//定义全局诊断响应
}

/*进入扩展会话,成功返回1,失败返回0*/
int FlashStepA()
{
  byte reqData[2] = {0x10,0x03};//定义诊断请求数据
  const dword waitRespMaxTimeout = 1000;//定义最大的响应等待时间(ms)
  long respRet;
  byte respData[256];//定义存储响应数据的缓冲区(数组)
  long respByteNum;//定义存储响应数据总字节长度的变量
  
  sysSetVariableString(sysvar::EcuBootload::EcuFlashResult,"正在进入扩展会话");
  gDiagRequest.Resize(elcount(reqData));//此函数用于重置(设置)指定的诊断对象的字节数
  gDiagRequest.SetPrimitiveData(reqData,elcount(reqData));//设置诊断请求的数据为10 03
  gDiagRequest.SendRequest();//发送10 03诊断请求
  
  respRet = testWaitForDiagResponse(gDiagRequest,waitRespMaxTimeout);//
  if(respRet != 1)//未响应
  {
    return 0;
  }
  diagGetLastResponse(gDiagRequest,gDiagResponse);//将10 03的响应数据获取了存到gDiagResponse里面。
  respByteNum = gDiagResponse.GetPrimitiveSize();//获取响应的数据长度
  gDiagResponse.GetPrimitiveData(respData,elcount(respData));//获取响应的数据
  
  if(diagGetLastResponseCode(gDiagRequest) == -1)//如果是肯定响应  返回1,表示成功
    return 1;
  else
    return 0;//返回0,表示失败;
}

ok呀,代码编写完毕,接着我们来测试一下功能是否实现了。

再CAPL编辑器的左侧,选择Test Control,右击鼠标,新建一个MainTest;

由于我们的代码中调用了诊断函数,要使得诊断函数能够知道目标的ECU是谁(能够以正确的诊断请求ID和诊断响应进行数据的收发),我们需要先设置一下诊断的目标ECU,使用DiagSetTarget函数;设置一下我们先前提到的"ECU qualifier"的名字;

并且调用一下FlashStepA函数,通过返回值来看看,函数的功能是否实现了;

如果大家手里有真实的ECU,这一步就可以接入真实的ECU来测试一下看看了;

这里由于是教学,我就不使用真实的ECU了,防止有的朋友没有。我将增加一个节点来仿真ECU做出对应的响应,这一步友友们可以不需要关注,我的评论区下方会附带这个刷写工具的完整工程,里面就有这个仿真的ECU节点,对于没有真实节点的友友,可以使用我这个工程来进行学习和仿真,我的仿真ECU会自动回复响应。

cpp 复制代码
void MainTest()
{
  int ret;
  diagSetTarget("BasicDiagnosticsEcu");
  
  ret = FlashStepA();
  write("FlashStepA:result:%d",ret);
}

随后,我们点击一下开始刷写/或者点击这个测试节点处的启动图标;(这里由于我是仿真,我就把总线模式由real bus调整为simulated bus了)

观察trace的报文和write窗口的打印;

这里看到,当我们的ECU接收到10 03的请求时,如果回复了肯定响应(图上是我的虚拟ECU回复的,所以报文方向是Tx,这个没关系)

在检测到肯定响应时,我们的代码打印了结果为1,说明我们的代码正确的判断到了肯定响应的出现。

紧接着,我把我的虚拟节点修改为回复负响应看看;大家有真实节点的可以试着发一个不支持的会话看ECU的回复;

这里看到,当ECU回复负响应时,代码成功检测到了非正响应,打印了0;

紧接着再测试一下不响应的情况,有真实ECU的朋友可以试着把ECU拔掉看看;

这里看到,当ECU不响应时,我们也正常的检测到了非正响应(未响应嘛,肯定就不是成功了);

到此,经过验证,我们的StepA这个写法,是可以成功的检测到步骤是否通过了的,对吧。

那么接下来,除了几个特殊的步骤(27服务需要计算种子和密码,31服务除了需要判断正响应,往往还需要判断例程结果字节(byte4),34服务需要计算单次36的数据长度,36服务需要填充传输的数据)外,其他的步骤我们的代码是不是可以直接复制了呢,复制之后改一下请求的数组内容不久ok了?答案,是的。所以接下来的几个非特殊的步骤,就可以直接复制代码了。

3.3 Step2:关闭DTC检测

cpp 复制代码
/*关闭DTC检测,成功返回1,失败返回0*/
int FlashStepB()
{
  byte reqData[2] = {0x85,0x02};//定义诊断请求数据
  const dword waitRespMaxTimeout = 1000;//定义最大的响应等待时间(ms)
  long respRet;
  byte respData[256];//定义存储响应数据的缓冲区(数组)
  long respByteNum;//定义存储响应数据总字节长度的变量
  
  sysSetVariableString(sysvar::EcuBootload::EcuFlashResult,"正在关闭DTC检测");
  gDiagRequest.Resize(elcount(reqData));//此函数用于重置(设置)指定的诊断对象的字节数
  gDiagRequest.SetPrimitiveData(reqData,elcount(reqData));//
  gDiagRequest.SendRequest();
  
  respRet = testWaitForDiagResponse(gDiagRequest,waitRespMaxTimeout);//
  if(respRet != 1)//未响应
  {
    return 0;
  }
  diagGetLastResponse(gDiagRequest,gDiagResponse);//响应数据获取了存到gDiagResponse里面。
  respByteNum = gDiagResponse.GetPrimitiveSize();//获取响应的数据长度
  gDiagResponse.GetPrimitiveData(respData,elcount(respData));//获取响应的数据
  
  if(diagGetLastResponseCode(gDiagRequest) == -1)//如果是肯定响应  返回1,表示成功
    return 1;
  else
    return 0;//返回0,表示失败;
}

3.4 Step3:关闭非诊断通信

cpp 复制代码
/*关闭非诊断通信,成功返回1,失败返回0*/
int FlashStepC()
{
  byte reqData[3] = {0x28,0x03,0x03};//定义诊断请求数据
  const dword waitRespMaxTimeout = 1000;//定义最大的响应等待时间(ms)
  long respRet;
  byte respData[256];//定义存储响应数据的缓冲区(数组)
  long respByteNum;//定义存储响应数据总字节长度的变量
  
  sysSetVariableString(sysvar::EcuBootload::EcuFlashResult,"正在关闭非诊断通信");
  gDiagRequest.Resize(elcount(reqData));//此函数用于重置(设置)指定的诊断对象的字节数
  gDiagRequest.SetPrimitiveData(reqData,elcount(reqData));//
  gDiagRequest.SendRequest();
  
  respRet = testWaitForDiagResponse(gDiagRequest,waitRespMaxTimeout);//
  if(respRet != 1)//未响应
  {
    return 0;
  }
  diagGetLastResponse(gDiagRequest,gDiagResponse);//响应数据获取了存到gDiagResponse里面。
  respByteNum = gDiagResponse.GetPrimitiveSize();//获取响应的数据长度
  gDiagResponse.GetPrimitiveData(respData,elcount(respData));//获取响应的数据
  
  if(diagGetLastResponseCode(gDiagRequest) == -1)//如果是肯定响应  返回1,表示成功
    return 1;
  else
    return 0;//返回0,表示失败;
}

3.5 Step4:检查预编程条件

这条step会稍微特殊一点,因为使用的是31服务,31服务除了需要判断是不是肯定响应以外,通常还需要判断例程的结果,也就是在byte4的位置,例如请求 31 01 02 03,回复71 01 02 03 XX,通常XX这个字节,会指示例程执行的结果,对于31服务,大部分的OEM和ECU厂商定义的都是,result == 0x00表示通过,result != 0x00表示不通过,那么,我们这里就需要对这个byte4再加以判断一下,需要是肯定响应且byte4为0x00时才返回1给调用者;

cpp 复制代码
/*预编程检查,成功返回1,失败返回0*/
int FlashStepD()
{
  byte reqData[4] = {0x31,0x01,0x02,0x03};//定义诊断请求数据
  const dword waitRespMaxTimeout = 1000;//定义最大的响应等待时间(ms)
  long respRet;
  byte respData[256];//定义存储响应数据的缓冲区(数组)
  long respByteNum;//定义存储响应数据总字节长度的变量
  
  sysSetVariableString(sysvar::EcuBootload::EcuFlashResult,"正在进行预编程检查");
  gDiagRequest.Resize(elcount(reqData));//此函数用于重置(设置)指定的诊断对象的字节数
  gDiagRequest.SetPrimitiveData(reqData,elcount(reqData));//
  gDiagRequest.SendRequest();
  
  respRet = testWaitForDiagResponse(gDiagRequest,waitRespMaxTimeout);//
  if(respRet != 1)//未响应
  {
    return 0;
  }
  diagGetLastResponse(gDiagRequest,gDiagResponse);//响应数据获取了存到gDiagResponse里面。
  respByteNum = gDiagResponse.GetPrimitiveSize();//获取响应的数据长度
  gDiagResponse.GetPrimitiveData(respData,elcount(respData));//获取响应的数据
  
  if(diagGetLastResponseCode(gDiagRequest) == -1 && respData[4] == 0x00)//如果是肯定响应并且例程结果为00  返回1,表示成功
    return 1;
  else
    return 0;//返回0,表示失败;
}

3.6 Step5:进入编程会话

对于编程会话,进入编程会话之后,ECU会复位,可能时间会略长,所以我们等待响应的时间可以适当改长一些,当然了,每一步的等待时间,朋友们其实都应该根据自己的ECU和主机厂的实际情况决定,我这里只是作为演示。

cpp 复制代码
/*进入编程话,成功返回1,失败返回0*/
int FlashStepE()
{
  byte reqData[2] = {0x10,0x02};//定义诊断请求数据
  const dword waitRespMaxTimeout = 2000;//定义最大的响应等待时间(ms)
  long respRet;
  byte respData[256];//定义存储响应数据的缓冲区(数组)
  long respByteNum;//定义存储响应数据总字节长度的变量
  
  sysSetVariableString(sysvar::EcuBootload::EcuFlashResult,"正在进入编程会话");
  gDiagRequest.Resize(elcount(reqData));//此函数用于重置(设置)指定的诊断对象的字节数
  gDiagRequest.SetPrimitiveData(reqData,elcount(reqData));//设置诊断请求的数据为10 02
  gDiagRequest.SendRequest();//发送10 02诊断请求
  
  respRet = testWaitForDiagResponse(gDiagRequest,waitRespMaxTimeout);//
  if(respRet != 1)//未响应
  {
    return 0;
  }
  diagGetLastResponse(gDiagRequest,gDiagResponse);//将10 02的响应数据获取了存到gDiagResponse里面。
  respByteNum = gDiagResponse.GetPrimitiveSize();//获取响应的数据长度
  gDiagResponse.GetPrimitiveData(respData,elcount(respData));//获取响应的数据
  
  if(diagGetLastResponseCode(gDiagRequest) == -1)//如果是肯定响应  返回1,表示成功
    return 1;
  else
    return 0;//返回0,表示失败;
}

3.7 Step6:安全等级解锁

安全解锁这步就稍微麻烦一些了,通过安全解锁需要先请求种子,收到种子之后,再拿接收到的种子去使用ECU端的校验算法进行计算,将计算的密码再发送给ECU,然后判断ECU的响应才知道是否解锁成功了。

那么该怎么调用解锁算法呢?这里有3种方法:

方法1:如果你有该ECU的seedKey.dll文件,那么可以根据你的CANoe程序的位数(据我所知,好像CANoe15及以上版本,运行的程序是64位的,CANoe15以下的版本,都是32位的),把对应位数的解锁算法dll加载进我在第2.2步提到的,加载安全算法dll的地方。此时,就可以在CAPL中通过计算密钥的函数 diagGenerateKeyFromSeed 直接使用了;

方法2:如果你拥有该ECU的底层算法代码,可以把这个C代码转换成CAPL的语法,写一个CAPL的函数,自己调用去计算密钥,这个也可以,但我相信大部分读者,如果不是该项目的底软开发者,是不可能拥有这种代码源码的。

方法3:如果你的dll,是基于CAPL DLL的语法进行封装过的,那么则无需加载进诊断控制台中,直接在CAPL代码中使用 #pragma library("xxx.dll")动态加载这个capl dll,调用其中的函数进行计算;但这个CAPL DLL兼容的dll编程语法,可能没几个人熟悉,不会生成。

最简单的方法呢,还是使用方法1,把dll加载进诊断控制台中

鉴于有的读者,可能有ECU的C算法源文件,想封装一个dll给同事/朋友在CANoe/TsMaster这些地方的诊断控制台调用,但不知道怎么封装这个dll文件;也可能有的朋友单纯就是为了学习ECU的刷写流程,根本没有这些文件;

现在,我将在本篇文章中,附带教大家,怎么生成这个可以在CANoe/TsMaster/ZXDoc等软件中可以在诊断控制台中调用的SeedKey.dll文件;(前提,需要安装一个Visual Studio 2013之后的任意版本都可以,并且安装C++桌面开发扩展)

3.7.1 基于CANoe官方提供的SeedKey DLL算法生成的VS工程,自己编译一个DLL使用

大家可以在自己电脑的这个路径,找到CANoe提供的官方demo

"C:\Users\Public\Documents\Vector\CANoe\Sample Configurations 12.0.221\CAN\Diagnostics\UDSSystem\SecurityAccess\Sources"

其中,Sample Configurations 12.0.221这里,不一定和我一样,这里显示的是大家安装的CANoe的版本。

这两个文件夹里面就是CANoe官方提供的生成SeedKey.dll的demo文件,哪个文件夹都可以。

这里我选择上面这个文件夹,先把它复制一份,放到我们的工程文件夹里面,然后我们在复制的上面修改,免得不小心改错了,使得demo都没法用了;

拷贝完成之后,使用VS打开这个文件,就可以开始编辑它的Demo代码了。

由于这个Demo编写时的IDE比较老了,大家使用现在的新的VS打开,会提示让更新解决方案等,大家直接更新就可以了。

打开之后我们可以看到,工程生成的文件默认是32位的(x86)Debug模式的dll,

大家如果要编译64位的dll,需要修改一下目标位数;

点击这里的下拉箭头,选择配置管理器;

选择新建,并选择平台为x64,并且选择从x86处复制设置,确定后,关闭这个界面即可。

现在我们的工程就可以选择编译32位还是64位的dll程序了,然后左边的debug大家可以下拉,改成release模式,因为debug编译的dll,依赖于一些VS的debug库,别人的电脑上可能没有,为了通用性,也是为了标准,建议选择release模式编译。

由于我的CANoe是12版本,所以我就编译x86的了(32位 release模式)

大家可以修改这个函数里面的实现,然后直接编译,就可以生成对应的dll文件了。

这里因为是演示,我就不改他的demo代码了,也就是收到种子之后,直接把种子取反然后输出出去,如果大家想修改这个算法和逻辑,可以自行修改后编译。

点击编译后,可能会提示这个报错信息,不要担心

这是因为这个工程实际生成的dll叫 SeednKey.dll,并不叫GenerateKeyExlmpl.dll导致的,这应该是官方demo配置的时候,名字忘记改了。

实际上只要你点击编译之后,就会把dll生成到这两个文件夹中,在哪里取决于你是以什么模式编译的,另外,如果按照我刚刚的配置方法,编译了64位的dll,dll也在这两个文件夹里面,注意,也在这两个文件夹里面,并不是在x64里面。(就是说,不管你是x86编译的还是x64编译的,生成的文件,都在这个文件夹里面。而且名字也是一样的)

如果大家不确定自己的dll是32位还是64位的,我教大家一个方法可以区分。

首先,需要一台安装了VS的电脑,然后再电脑的搜索界面,搜索,developer这个软件,这是VS自带的。

打开之后,输入

dumpbin /headers "C:\Users\58248\Desktop\CAPL_UdsBootload\KeyGenDll_GenerateKeyEx - 副本\Debug\SeednKey.dll"

这里的路径是你自己的dll路径,输入完毕后回车,即可看到这个dll的位数

这个地方会显示x86/x64.

至此,SeedKey.dll的生成教程就结束了,大家可以把生成的dll拷贝到我们的工程文件夹里面 Diagnostic这个文件夹中,然后在诊断控制台中加载这个dll即可。

3.7.2 CAPL代码实现安全解锁

首先,假设我们的安全等级是levelX,

那么我们需要先发送 27 levelX给ECU,当ECU回复 67 levelX YY YY YY YY...之后,YY YY YY YY...就是我们的种子,需要把这个种子存储起来,并拿去调用 diagGenerateKeyFromSeed这个函数,计算处对应的密码,然后再把密码 填充到 27 LevelX+1之后,发送 27 LevelX+1 KK KK KK KK...,并等待ECU响应,判断是否为 67 LevelX+1,来知道是否通过了安全访问。

这边我假设我的levelX为0x05;

cpp 复制代码
/*安全解锁,成功返回1,失败返回0*/
int FlashStepF()
{
  byte reqData[64];//定义诊断请求数据
  const dword waitRespMaxTimeout = 1000;//定义最大的响应等待时间(ms)
  long respRet;
  byte respData[256];//定义存储响应数据的缓冲区(数组)
  long respByteNum;//定义存储响应数据总字节长度的变量
  const safeLevel = 0x05;//假设我的安全等级为0x05
  byte seedArr[4],keyArr[4];
  dword keySize;
  
  sysSetVariableString(sysvar::EcuBootload::EcuFlashResult,"正在进行安全等级解锁");
  gDiagRequest.Resize(2);//设置请求的字节数为2  27 05
  reqData[0] = 0x27;reqData[1] = safeLevel;
  gDiagRequest.SetPrimitiveData(reqData,2);//设置诊断请求的数据为27 05
  gDiagRequest.SendRequest();//发送 诊断请求
  
  respRet = testWaitForDiagResponse(gDiagRequest,waitRespMaxTimeout);//
  if(respRet != 1)//未响应
  {
    return 0;
  }
  diagGetLastResponse(gDiagRequest,gDiagResponse);//将27 05的响应数据获取了存到gDiagResponse里面。
  respByteNum = gDiagResponse.GetPrimitiveSize();//获取响应的数据长度
  gDiagResponse.GetPrimitiveData(respData,elcount(respData));//获取响应的数据
  
  if(diagGetLastResponseCode(gDiagRequest) != -1) return 0;//种子没回复肯定响应  失败
  
  //没有返回出去,说明种子肯定回复了肯定响应 那么我们把种子拷贝出来
  memcpy_off(seedArr,0,respData,2,respByteNum - 2);//从响应数据的byte2开始拷贝,拷贝respByteNum-2 字节,放到seedArr的byte0的地方开始
  //拷贝完毕,开始算密码了
  DiagGenerateKeyFromSeed(seedArr,respByteNum - 2,safeLevel,"","",keyArr,elcount(keyArr),keySize);//
  
  //把密钥拷贝到请求数据里面
  memcpy_off(reqData,2,keyArr,0,keySize);
  
  gDiagRequest.Resize(keySize + 2);//设置请求的字节数为密钥的长度+2 
  reqData[1] = safeLevel + 1;
  gDiagRequest.SetPrimitiveData(reqData,keySize + 2);//设置诊断请求的数据为27 06 + 四字节密钥
  gDiagRequest.SendRequest();//发送 诊断请求
  
  respRet = testWaitForDiagResponse(gDiagRequest,waitRespMaxTimeout);//
  if(respRet != 1)//未响应
  {
    return 0;
  }
  diagGetLastResponse(gDiagRequest,gDiagResponse);//将27 06的响应数据获取了存到gDiagResponse里面。
  respByteNum = gDiagResponse.GetPrimitiveSize();//获取响应的数据长度
  gDiagResponse.GetPrimitiveData(respData,elcount(respData));//获取响应的数据
  
  if(diagGetLastResponseCode(gDiagRequest) == -1)//如果是肯定响应  返回1,表示成功
    return 1;
  else
    return 0;//返回0,表示失败;
}

我的代码经过测试,可以看到,确实将种子取反后发送了出去。安全访问ok(算法符合dll)

3.8 Step7:写入刷写指纹

写入刷写指纹这一条,通常的ECU和主机厂,写入的都是刷写时的北京时间,方便日后查找证据的时候,知道是什么时候被刷写的,那么这里,我也实时获取当前的北京时间并且进行写入了。

cpp 复制代码
/*写刷写指纹,成功返回1,失败返回0*/
int FlashStepG()
{
  byte reqData[9];//定义诊断请求数据
  const dword waitRespMaxTimeout = 1000;//定义最大的响应等待时间(ms)
  long respRet;
  byte respData[256];//定义存储响应数据的缓冲区(数组)
  long respByteNum;//定义存储响应数据总字节长度的变量
  long lt[9];
  
  sysSetVariableString(sysvar::EcuBootload::EcuFlashResult,"正在写入指纹");
  reqData[0] = 0x2E;reqData[1] = 0xF1;reqData[2] = 0x90;//设置did
  getLocalTime(lt);//获取当前北京时间
  reqData[3] = lt[5] - 100;//年  lt[5]是从1900年开始计数的,所以2026他返回的就是126,那么我们就需要减去100
  reqData[4] = lt[4] + 1;//月  lt[4]是从0开始计数 0表示1月 11表示12月,所以要+1
  reqData[5] = lt[3];//日
  reqData[6] = lt[2];//小时
  reqData[7] = lt[1];//分钟
  reqData[8] = lt[0];//秒
  gDiagRequest.Resize(elcount(reqData));//此函数用于重置(设置)指定的诊断对象的字节数
  gDiagRequest.SetPrimitiveData(reqData,elcount(reqData));//设置诊断请求的数据为
  gDiagRequest.SendRequest();//发送诊断请求
  
  respRet = testWaitForDiagResponse(gDiagRequest,waitRespMaxTimeout);//
  if(respRet != 1)//未响应
  {
    return 0;
  }
  diagGetLastResponse(gDiagRequest,gDiagResponse);//将的响应数据获取了存到gDiagResponse里面。
  respByteNum = gDiagResponse.GetPrimitiveSize();//获取响应的数据长度
  gDiagResponse.GetPrimitiveData(respData,elcount(respData));//获取响应的数据
  
  if(diagGetLastResponseCode(gDiagRequest) == -1)//如果是肯定响应  返回1,表示成功
    return 1;
  else
    return 0;//返回0,表示失败;
}

3.9 Step8:请求下载数据(Driver)

本步骤需要告诉ECU,我们要传输的数据位于哪个地址,数据总共有多长,那么这些数据从哪里来呢?通常,我们会把这些数据记录在Hex/S19/Bin文件中(bin只有数据,无地址,需要手动指定地址),那么我们怎么获取这些数据呢,答案很明显,那就是去读取Hex/S19/Bin文件,并解析其中的数据,怎么解析?具体的原理我也不再说太多,网上很多博主都有对Hex/S19/Bin文件的格式详解,我们是实用派,直接上手写代码,需要了解原理的,可以阅读其他博主的讲解文章,这里我只做简单的讲解和描述。

为了使得我们这个解析文件的代码,具备解耦性(能够在任何一个文件都能编译过,减少依赖),我这里创建一个新的文件,专门用来解析文件;

在CAPL浏览器的左上角,右击Includes 选择Add Include,增加一个头文件,这里我就把这个文件放在先前创建的CaplIncludesFile文件夹内了(通用性文件,建议放在一起)

创建完毕后,CAPL文件的includes处就会出现对这个文件的包含。

这边为了保持解析函数接口通用性,我定义了一些通用的变量;

所有block的数据顺序拼接存放在一个 2MB 的一维 byte 数组中,通过索引数组记录每个block的元信息:

这样做的好处是:避免了二维大数组带来的 CAPL 内存限制问题,一个一维数组加几个小索引数组即可管理所有block。

cpp 复制代码
variables
{
    /* ============ 全局block存储 ============ */
    byte   gAllBlockData[0x200000];   // 2MB, 所有block数据的平坦存储
    dword  gBlockOffset[20];          // 每个block在gAllBlockData中的起始偏移
    dword  gBlockStartAddr[20];       // 每个block的Flash起始地址
    dword  gBlockDataLen[20];         // 每个block的数据长度(字节)
    dword  gBlockCount = 0;           // 解析出的block总数
    dword  gTotalDataLen = 0;         // gAllBlockData中已使用的总字节数
}

而且解析完成后,可以通过设计两个函数来返回任意一个block中的数据(hex/s19中可能存在不知一个block)

由于Hex文件和S19文件中,一个16进制数据是以两个字符存储的,需要把字符转换到16进制数据,所以,需要实现两个函数;

函数:hexCharToVal 用于将单个十六进制 ASCII 字符转换为 0~15 的数值。同时兼容大写 A-F 和小写 a-f。无效字符返回 0xFF

函数:hexPairToByte 从 char 数组的指定位置取相邻两个十六进制字符,调用 hexCharToVal 分别得到高4位和低4位,拼合为一个 byte。这是 HEX 和 S19 解析中最核心的基础操作------将文本形式的十六进制字符串还原为二进制数据。

cpp 复制代码
/*
 * 将单个十六进制字符转换为数值(0~15)
 * 参数: c - 十六进制字符 ('0'-'9', 'A'-'F', 'a'-'f')
 * 返回: 0~15 成功, 0xFF 无效字符
 */
byte hexCharToVal(char c)
{
    if (c >= '0' && c <= '9')
    {
        return (byte)(c - '0');
    }
    if (c >= 'A' && c <= 'F')
    {
        return (byte)(c - 'A' + 10);
    }
    if (c >= 'a' && c <= 'f')
    {
        return (byte)(c - 'a' + 10);
    }
    return 0xFF;
}

/*
 * 从行缓冲区的指定位置解析两个十六进制字符为一个字节
 * 参数: lineBuf - 行缓冲区, pos - 起始位置(两个hex字符中第一个的下标)
 * 返回: 解析出的字节值
 */
byte hexPairToByte(char lineBuf[], int pos)
{
    return (byte)((hexCharToVal(lineBuf[pos]) << 4) | hexCharToVal(lineBuf[pos + 1]));
}

第一个block的数据通过函数参数 data[]startAddrdataLen 直接返回给调用者

所有block(包括第一个)的数据都存储在全局变量中

调用者后续可通过 getBlockData(blockIndex, ...) 按索引提取任意block

调用者可通过 getBlockCount() 获取block总数

cpp 复制代码
/*
 * 获取指定block的数据
 *
 * 参数:
 *   blockIndex - block索引 (0 ~ gBlockCount-1)
 *   data       - 足够大的byte数组, 用于接收block数据
 *   startAddr  - [输出] 该block的起始地址
 *   dataLen    - [输出] 该block的数据长度(字节)
 *
 * 返回值:
 *   1  成功
 *   0  失败(索引越界)
 */
long getBlockData(dword blockIndex, byte data[], dword &startAddr, dword &dataLen)
{
    dword i;
    dword offset;

    if (blockIndex >= gBlockCount)
    {
        write("getBlockData: 索引%d越界, 当前共%d个block", blockIndex, gBlockCount);
        startAddr = 0;
        dataLen = 0;
        return 0;
    }

    startAddr = gBlockStartAddr[blockIndex];
    dataLen = gBlockDataLen[blockIndex];
    offset = gBlockOffset[blockIndex];

    for (i = 0; i < dataLen; i++)
    {
        data[i] = gAllBlockData[offset + i];
    }

    return 1;
}
cpp 复制代码
/*
 * 获取解析出的block总数
 */
dword getBlockCount()
{
    return gBlockCount;
}

3.9.1 CAPL解析Hex文件

需要解析文件,首先我们肯定要知道文件的路径,所以,这个函数需要接受一个文件路径作为参数,然后我们再把解析出来的数据数组,以及起始地址,数据长度告知调用者,那么我准备为这个函数设计四个参数;

cpp 复制代码
/*
 * 解析Intel HEX文件
 *
 * 参数:
 *   filePath  - hex文件路径
 *   data      - 足够大的byte数组, 用于接收第一个block的数据
 *   startAddr - [输出] 第一个block的起始地址
 *   dataLen   - [输出] 第一个block的数据长度(字节)
 *
 * 返回值:
 *   > 0   解析成功, 返回值为解析的总行数
 *   -1    文件格式错误
 *   -2    行数据格式错误(缺少':')
 *   -3    打开文件失败
 *   -4    block数量超限
 *   -5    校验和错误
 *
 * 说明:
 *   所有block的数据均存储在全局变量中,
 *   可通过 getBlockData() 获取任意block的数据,
 *   通过 getBlockCount() 获取block总数.
 */
long parseHexFile(char filePath[], byte data[], dword &startAddr, dword &dataLen)
{
    long   fileHandle;
    char   lineBuf[600];
    dword  lineNum;
    dword  baseAddr;          // 基地址, 由type 02/04记录设置
    dword  fullAddr;          // 当前数据记录的完整32位地址
    byte   byteCount;         // 当前行的数据字节数
    byte   recordType;        // 记录类型
    word   lineAddr;          // 行地址字段(16位)
    byte   calcSum;           // 计算得到的校验和
    byte   fileSum;           // 文件中的校验和
    dword  i;
    int    blockStarted;      // 标记是否已开始第一个block
    dword  currBlockNextAddr; // 当前block期望的下一个连续地址

    // 初始化全局存储
    gBlockCount = 0;
    gTotalDataLen = 0;
    baseAddr = 0;
    blockStarted = 0;
    startAddr = 0;
    dataLen = 0;

    fileHandle = openFileRead(filePath, 0);
    if (fileHandle == 0)
    {
        write("打开文件失败: %s", filePath);
        return -3;
    }

    lineNum = 0;

    while (fileGetString(lineBuf, elcount(lineBuf), fileHandle) != 0)
    {
        lineNum++;

        // 验证起始标记
        if (lineBuf[0] != ':')
        {
            write("第%d行格式错误: 缺少起始标记':'", lineNum);
            fileClose(fileHandle);
            return -2;
        }

        // 解析固定字段
        byteCount  = hexPairToByte(lineBuf, 1);
        lineAddr   = ((word)hexPairToByte(lineBuf, 3) << 8) | (word)hexPairToByte(lineBuf, 5);
        recordType = hexPairToByte(lineBuf, 7);

        // 校验和验证: 从byteCount到最后一个数据字节, 再加上checksum本身, 总和低8位应为0
        calcSum = byteCount + (byte)(lineAddr >> 8) + (byte)(lineAddr & 0xFF) + recordType;
        for (i = 0; i < byteCount; i++)
        {
            calcSum += hexPairToByte(lineBuf, (int)(9 + i * 2));
        }
        calcSum = (byte)((~calcSum) + 1);
        fileSum = hexPairToByte(lineBuf, (int)(9 + byteCount * 2));

        if (calcSum != fileSum)
        {
            write("第%d行校验和错误: 计算值=0x%02X, 文件值=0x%02X", lineNum, calcSum, fileSum);
            fileClose(fileHandle);
            return -5;
        }

        // ---- 根据记录类型分别处理 ----

        if (recordType == 0x00) // 数据记录
        {
            fullAddr = baseAddr + (dword)lineAddr;

            if (!blockStarted)
            {
                // 第一条数据记录 → 开始第一个block
                blockStarted = 1;
                currBlockNextAddr = fullAddr;
                gBlockOffset[0] = 0;
                gBlockStartAddr[0] = fullAddr;
                gBlockDataLen[0] = 0;
                gBlockCount = 1;
            }
            else if (fullAddr != currBlockNextAddr)
            {
                // 地址不连续 → 新block
                if (gBlockCount >= 20)
                {
                    write("block数量超过最大限制(20), 停止解析");
                    fileClose(fileHandle);
                    return -4;
                }
                gBlockOffset[gBlockCount] = gTotalDataLen;
                gBlockStartAddr[gBlockCount] = fullAddr;
                gBlockDataLen[gBlockCount] = 0;
                gBlockCount++;
            }

            // 将本行数据存入全局缓冲区
            for (i = 0; i < byteCount; i++)
            {
                gAllBlockData[gTotalDataLen] = hexPairToByte(lineBuf, (int)(9 + i * 2));
                gTotalDataLen++;
                gBlockDataLen[gBlockCount - 1]++;
            }

            currBlockNextAddr = fullAddr + (dword)byteCount;
        }
        else if (recordType == 0x01) // 文件结束记录
        {
            break;
        }
        else if (recordType == 0x02) // 扩展段地址记录
        {
            baseAddr = (((dword)hexPairToByte(lineBuf, 9) << 8) | (dword)hexPairToByte(lineBuf, 11)) << 4;
        }
        else if (recordType == 0x04) // 扩展线性地址记录
        {
            baseAddr = (((dword)hexPairToByte(lineBuf, 9) << 8) | (dword)hexPairToByte(lineBuf, 11)) << 16;
        }
        // type 03 (起始段地址) 和 type 05 (起始线性地址) 不影响数据, 直接跳过
    }

    fileClose(fileHandle);

    // 将第一个block的数据拷贝给调用者
    if (gBlockCount > 0)
    {
        startAddr = gBlockStartAddr[0];
        dataLen = gBlockDataLen[0];
        for (i = 0; i < gBlockDataLen[0]; i++)
        {
            data[i] = gAllBlockData[i]; // block 0 的 offset 始终为 0
        }
    }

    write("HEX文件解析完成: 共%d行, %d个block", lineNum, gBlockCount);
    for (i = 0; i < gBlockCount; i++)
    {
        write("  Block %d: 起始地址=0x%08X, 长度=%d字节", i, gBlockStartAddr[i], gBlockDataLen[i]);
    }

    return (long)lineNum;
}

3.9.2 CAPL解析S19文件

S19文件同样的,保持和Hex解析一样的参数形式

cpp 复制代码
/*
 * 解析Motorola S-record (S19/S28/S37) 文件
 *
 * 参数:
 *   filePath  - S19文件路径
 *   data      - 足够大的byte数组, 用于接收第一个block的数据
 *   startAddr - [输出] 第一个block的起始地址
 *   dataLen   - [输出] 第一个block的数据长度(字节)
 *
 * 返回值:
 *   > 0   解析成功, 返回值为解析的总行数
 *   -2    行数据格式错误(缺少'S'或类型非法)
 *   -3    打开文件失败
 *   -4    block数量超限
 *   -5    校验和错误
 *
 * 说明:
 *   支持 S0/S1/S2/S3/S5/S7/S8/S9 全部常见记录类型.
 *   数据记录 S1(16位地址) / S2(24位地址) / S3(32位地址) 均可正确解析.
 *   所有block的数据均存储在全局变量中,
 *   可通过 getBlockData() 获取任意block的数据,
 *   通过 getBlockCount() 获取block总数.
 */
long parseS19File(char filePath[], byte data[], dword &startAddr, dword &dataLen)
{
    long   fileHandle;
    char   lineBuf[600];
    dword  lineNum;
    dword  fullAddr;           // 当前数据记录的完整地址
    byte   byteCount;          // S-record中的字节计数(地址+数据+校验和)
    byte   recType;            // 记录类型数字 (0~9)
    byte   addrLen;            // 地址字段的字节数: 2/3/4
    byte   dataByteCnt;        // 当前行的纯数据字节数
    int    dataStartPos;       // 数据字段在lineBuf中的起始下标
    byte   calcSum;            // 校验和累加
    dword  i;
    int    blockStarted;       // 标记是否已开始第一个block
    dword  currBlockNextAddr;  // 当前block期望的下一个连续地址

    // 初始化全局存储
    gBlockCount = 0;
    gTotalDataLen = 0;
    blockStarted = 0;
    startAddr = 0;
    dataLen = 0;

    fileHandle = openFileRead(filePath, 0);
    if (fileHandle == 0)
    {
        write("打开文件失败: %s", filePath);
        return -3;
    }

    lineNum = 0;

    while (fileGetString(lineBuf, elcount(lineBuf), fileHandle) != 0)
    {
        lineNum++;

        // 验证起始标记
        if (lineBuf[0] != 'S')
        {
            write("第%d行格式错误: 缺少起始标记'S'", lineNum);
            fileClose(fileHandle);
            return -2;
        }

        // 解析记录类型
        if (lineBuf[1] < '0' || lineBuf[1] > '9')
        {
            write("第%d行格式错误: 非法记录类型'%c'", lineNum, lineBuf[1]);
            fileClose(fileHandle);
            return -2;
        }
        recType = (byte)(lineBuf[1] - '0');

        // 解析字节计数 (位于lineBuf[2..3])
        byteCount = hexPairToByte(lineBuf, 2);

        // 根据记录类型确定地址字段长度
        if (recType == 0 || recType == 1 || recType == 5 || recType == 9)
        {
            addrLen = 2;
        }
        else if (recType == 2 || recType == 8)
        {
            addrLen = 3;
        }
        else if (recType == 3 || recType == 7)
        {
            addrLen = 4;
        }
        else
        {
            // 未知类型, 跳过
            continue;
        }

        // 校验和验证
        // S-record校验和 = byteCount字节 + 地址字节 + 数据字节 + 校验和字节, 低8位之和应等于0xFF
        calcSum = byteCount;
        for (i = 0; i < (dword)byteCount; i++)
        {
            calcSum += hexPairToByte(lineBuf, (int)(4 + i * 2));
        }
        if ((byte)(calcSum & 0xFF) != 0xFF)
        {
            write("第%d行校验和错误: 累加结果=0x%02X, 期望0xFF", lineNum, calcSum & 0xFF);
            fileClose(fileHandle);
            return -5;
        }

        // ---- 仅处理数据记录 S1/S2/S3 ----
        if (recType >= 1 && recType <= 3)
        {
            // 解析完整地址 (大端序, 逐字节拼接)
            fullAddr = 0;
            for (i = 0; i < (dword)addrLen; i++)
            {
                fullAddr = (fullAddr << 8) | (dword)hexPairToByte(lineBuf, (int)(4 + i * 2));
            }

            // 纯数据字节数 = byteCount - 地址字节数 - 1(校验和)
            dataByteCnt = byteCount - addrLen - 1;
            // 数据在lineBuf中的起始位置
            dataStartPos = 4 + (int)addrLen * 2;

            if (!blockStarted)
            {
                // 第一条数据记录 → 开始第一个block
                blockStarted = 1;
                currBlockNextAddr = fullAddr;
                gBlockOffset[0] = 0;
                gBlockStartAddr[0] = fullAddr;
                gBlockDataLen[0] = 0;
                gBlockCount = 1;
            }
            else if (fullAddr != currBlockNextAddr)
            {
                // 地址不连续 → 新block
                if (gBlockCount >= 20)
                {
                    write("block数量超过最大限制(20), 停止解析");
                    fileClose(fileHandle);
                    return -4;
                }
                gBlockOffset[gBlockCount] = gTotalDataLen;
                gBlockStartAddr[gBlockCount] = fullAddr;
                gBlockDataLen[gBlockCount] = 0;
                gBlockCount++;
            }

            // 将本行数据存入全局缓冲区
            for (i = 0; i < (dword)dataByteCnt; i++)
            {
                gAllBlockData[gTotalDataLen] = hexPairToByte(lineBuf, (int)(dataStartPos + i * 2));
                gTotalDataLen++;
                gBlockDataLen[gBlockCount - 1]++;
            }

            currBlockNextAddr = fullAddr + (dword)dataByteCnt;
        }
        else if (recType == 7 || recType == 8 || recType == 9)
        {
            // 结束记录 S7/S8/S9, 停止解析
            break;
        }
        // S0(头记录) 和 S5(记录计数) 不含有效数据, 直接跳过
    }

    fileClose(fileHandle);

    // 将第一个block的数据拷贝给调用者
    if (gBlockCount > 0)
    {
        startAddr = gBlockStartAddr[0];
        dataLen = gBlockDataLen[0];
        for (i = 0; i < gBlockDataLen[0]; i++)
        {
            data[i] = gAllBlockData[i]; // block 0 的 offset 始终为 0
        }
    }

    write("S19文件解析完成: 共%d行, %d个block", lineNum, gBlockCount);
    for (i = 0; i < gBlockCount; i++)
    {
        write("  Block %d: 起始地址=0x%08X, 长度=%d字节", i, gBlockStartAddr[i], gBlockDataLen[i]);
    }

    return (long)lineNum;
}

3.9.3 CAPL解析Bin文件

为了保持接口一致性,即便我们的Bin文件不含地址信息,我们也为它设计四个参数吧,认为把地址传入,脚本内可以不处理或者默认处理为0,使用者解析完毕后再自行填充地址到34服务中。

cpp 复制代码
/*
 * 解析纯二进制(BIN)文件
 *
 * 参数:
 *   filePath  - bin文件路径
 *   data      - 足够大的byte数组, 用于接收第一个block的数据
 *   startAddr - [输出] 第一个block的起始地址 (bin文件固定为0)
 *   dataLen   - [输出] 第一个block的数据长度(字节)
 *
 * 返回值:
 *   1   解析成功
 *   -1  文件为空或读取失败
 *   -3  打开文件失败
 *
 * 说明:
 *   bin文件为纯数据流, 不含地址信息, 因此:
 *   - 起始地址固定输出为 0
 *   - 始终只有 1 个block
 *   解析后同样可通过 getBlockData() / getBlockCount() 访问.
 */
long parseBinFile(char filePath[], byte data[], dword &startAddr, dword &dataLen)
{
    long   fileHandle;
    dword  readSize;
    dword  i;

    // 初始化全局存储
    gBlockCount = 0;
    gTotalDataLen = 0;
    startAddr = 0;
    dataLen = 0;

    // 以二进制模式打开文件 (第2参数=1)
    fileHandle = openFileRead(filePath, 1);
    if (fileHandle == 0)
    {
        write("打开文件失败: %s", filePath);
        return -3;
    }

    // 一次性读取整个文件到全局缓冲区
    readSize = fileGetBinaryBlock(gAllBlockData, elcount(gAllBlockData), fileHandle);
    fileClose(fileHandle);

    if (readSize == 0)
    {
        write("文件为空或读取失败: %s", filePath);
        return -1;
    }

    // bin文件始终为1个block, 起始地址为0
    gBlockCount = 1;
    gTotalDataLen = readSize;
    gBlockOffset[0] = 0;
    gBlockStartAddr[0] = 0;
    gBlockDataLen[0] = readSize;

    // 拷贝到调用者的数组
    startAddr = 0;
    dataLen = readSize;
    for (i = 0; i < readSize; i++)
    {
        data[i] = gAllBlockData[i];
    }

    write("BIN文件解析完成: 数据长度=%d字节", readSize);
    write("  Block 0: 起始地址=0x%08X, 长度=%d字节", gBlockStartAddr[0], gBlockDataLen[0]);

    return 1;
}

3.9.4 实现通用的解析器

由于有三种文件格式,为了可以兼容用户任意传入类型文件,这里我再做一个自适应解析器;

cpp 复制代码
/*
 * 统一解析接口, 根据文件后缀自动选择解析器
 *
 * 参数:
 *   filePath  - 文件路径 (支持 .hex / .s19 / .s28 / .s37 / .bin)
 *   data      - 足够大的byte数组, 用于接收第一个block的数据
 *   startAddr - [输出] 第一个block的起始地址
 *   dataLen   - [输出] 第一个block的数据长度(字节)
 *
 * 返回值:
 *   同各子解析函数的返回值
 *   -10  无法识别的文件后缀
 */
long parseFile(char filePath[], byte data[], dword &startAddr, dword &dataLen)
{
    dword len;
    char  c1, c2, c3, c4;

    len = strlen(filePath);
    if (len < 5)
    {
        write("文件路径过短, 无法识别后缀: %s", filePath);
        return -10;
    }

    // 取最后4个字符用于后缀判断
    c1 = filePath[len - 4];
    c2 = filePath[len - 3];
    c3 = filePath[len - 2];
    c4 = filePath[len - 1];

    // 判断 .hex (.HEX)
    if (c1 == '.'
        && (c2 == 'h' || c2 == 'H')
        && (c3 == 'e' || c3 == 'E')
        && (c4 == 'x' || c4 == 'X'))
    {
        write("识别为Intel HEX文件, 开始解析...");
        return parseHexFile(filePath, data, startAddr, dataLen);
    }

    // 判断 .s19 (.S19)
    if (c1 == '.'
        && (c2 == 's' || c2 == 'S')
        && c3 == '1'
        && c4 == '9')
    {
        write("识别为Motorola S19文件, 开始解析...");
        return parseS19File(filePath, data, startAddr, dataLen);
    }

    // 判断 .s28 (.S28)
    if (c1 == '.'
        && (c2 == 's' || c2 == 'S')
        && c3 == '2'
        && c4 == '8')
    {
        write("识别为Motorola S28文件, 开始解析...");
        return parseS19File(filePath, data, startAddr, dataLen);
    }

    // 判断 .s37 (.S37)
    if (c1 == '.'
        && (c2 == 's' || c2 == 'S')
        && c3 == '3'
        && c4 == '7')
    {
        write("识别为Motorola S37文件, 开始解析...");
        return parseS19File(filePath, data, startAddr, dataLen);
    }

    // 判断 .bin (.BIN)
    if (c1 == '.'
        && (c2 == 'b' || c2 == 'B')
        && (c3 == 'i' || c3 == 'I')
        && (c4 == 'n' || c4 == 'N'))
    {
        write("识别为BIN文件, 开始解析...");
        return parseBinFile(filePath, data, startAddr, dataLen);
    }

    write("无法识别的文件后缀: %s", filePath);
    return -10;
}

3.9.5 CAPL实现34请求下载

基于上述三个步骤,我们已经完成了文件的解析,可以正常的提取出hex/s19/bin文件中的数据了,出于演示,我这边就以hex文件为例了(默认一个hex文件中只有一个block,如果有多个block,则可以使用我上面的getBlockData函数,获取每一个block的数据)。

首先,我们需要明白,34请求的常用格式;34 YY MN ....

这里34是服务ID,然后YY表示的是压缩算法,这个我只见过0x00(不压缩),没见过其他的值,我就认为是00吧,MN的话,这里的M表示,我要下载的这个地址,它用几个字节来表示,N的话,表示,我要下载的这个数据的长度,用几个字节来表示。

例如 34 00 44 则表示,我要下载的这个数据的起始地址,用了四个字节来表示(不论它的地址有多小,哪怕起始地址是0x01,它也要用4字节来表示这个地址 0x00 00 00 01);要下载的数据的长度,也用了四个字节来表示(不论它到底有多长,哪怕只有10个字节的长度,它也要用4字节来表示这个长度 0x00 00 00 0A);

并且,34请求时,ECU会回复 74 0M ...

这里的M,表示ECU一次允许下载多少个字节的数据这个数量所占的长度,这个可能比较绕啊,这里简单的解释一下,例如 回复 74 02 XX XX 表示后面有2个字节的数据,这2字节用来表示一次36 传输允许传输多长的数据给到ECU,如果时74 04 XX XX XX XX的话,就表示后面有4个字节的数据,这4字节表示的是可以一次传输多长的数据给ECU,哪怕这个数字很小,也是用4个字节来表示。

ok,理论简单的介绍了一下,接下来开始实现代码。

这里我们暂时不在这个步骤里面解析文件(为了后续封装更为通用的函数),假设文件已经解析完毕了,且driver数据在block0里面。我们直接使用。

先更新之前的全局定义,增加几个全局变量

cpp 复制代码
variables
{
  diagRequest * gDiagRequest;//定义全局诊断请求
  diagResponse * gDiagResponse;//定义全局诊断响应
  
  byte gDriverData[0x100000];//driver数据 给个1M的缓冲区,一般的文件都不可能有这么大,完全足够了
  byte gAppData[0x100000];//App数据 给个1M的缓冲区,一般的文件都不可能有这么大,完全足够了
  
  dword gDriverStartAddress;//驱动起始地址
  dword gAppStartAddress;//App起始地址
  dword gDriverLength;//驱动数据长度
  dword gAppLength;//App数据长度

  dword gOnceTransmitBlockSize;//36服务一次可以传输的字节数,由34服务回复时获得
  
}
cpp 复制代码
/*请求下载Driver文件,成功返回1,失败返回0*/
int FlashStepH()
{
  byte reqData[11];//定义诊断请求数据
  const dword waitRespMaxTimeout = 2000;//定义最大的响应等待时间(ms)
  long respRet;
  byte respData[256];//定义存储响应数据的缓冲区(数组)
  long respByteNum;//定义存储响应数据总字节长度的变量
  int i;
  
  sysSetVariableString(sysvar::EcuBootload::EcuFlashResult,"正在请求下载Driver数据");
  reqData[0] = 0x34;reqData[1] = 0x00;reqData[2] = 0x44;//设置地址和长度的表示格式
  for(i = 0;i < 4;i++)
  {
    reqData[i + 3] = (gDriverStartAddress >> ((3 - i) * 8)) & 0xFF;//设置起始地址
    reqData[i + 7] = (gDriverLength >> ((3 - i) * 8)) & 0xFF;//设置长度
  }
  
  gDiagRequest.Resize(elcount(reqData));//此函数用于重置(设置)指定的诊断对象的字节数
  gDiagRequest.SetPrimitiveData(reqData,elcount(reqData));//设置诊断请求的数据为
  gDiagRequest.SendRequest();//发送诊断请求
  
  respRet = testWaitForDiagResponse(gDiagRequest,waitRespMaxTimeout);//
  if(respRet != 1)//未响应
  {
    return 0;
  }
  diagGetLastResponse(gDiagRequest,gDiagResponse);//将的响应数据获取了存到gDiagResponse里面。
  respByteNum = gDiagResponse.GetPrimitiveSize();//获取响应的数据长度
  gDiagResponse.GetPrimitiveData(respData,elcount(respData));//获取响应的数据
  
  if(diagGetLastResponseCode(gDiagRequest) == -1)//如果是肯定响应  返回1,表示成功
  {
    byte blockSizeByteLen;
    byte len;
    gOnceTransmitBlockSize = 0;
    blockSizeByteLen = respByteNum - 2;//长度减去2个字节,即减去74 和 xx  //假设为4
    for(len = 0;len < blockSizeByteLen;len++)
    {
      gOnceTransmitBlockSize |= respData[2 + len] << (8 * (blockSizeByteLen - len - 1));//拼凑一包可以传输的长度
    }
    return 1;
  }
  else
    return 0;//返回0,表示失败;
}

3.10 Step9:传输Driver数据

36传输时,我们一个36包能够传输多少个字节的数据,是由34请求时回复的长度决定的。

而回复的这个长度,代码中记为 gOnceTransmitBlockSize ,这个size,是包含了 36 Seq这两个字节的,Seq是什么,这个有的朋友可能不知道,36传输时,第一次被传输的36包,序号为36 01,

第二次被传输的36包,序号则为36 02...如果还有数据需要传输,那么这个数字就会一直增加,这个字节就被称为Seq(Sequence序列),当这个数字到达0xFF之后,就要反转到0x00重新开始。

那么假设34请求时回复的单词可以传输的长度为4002字节,那么我们一次36可以传输的真正的hex文件的数据是多少字节呢?是4000字节,因为36 Seq这两个字节要占用掉2个,所以实际能传输的数据只有4000字节。那么,怎么知道我们需要传输多少个36才能结束呢?我们先再定义一个变量,假设就叫 onceTxHexLen(一次传输的hex数据长度),根据上面的讲解,我们已经知道了,这个数字实际上就是gOnceTransmitBlockSize - 2; 这里我们可以使用整个Hex的block块的数据长度,使用这个长度来对 onceTxHexLen 这个数据来先取整,再取余。为什么要取余和取整呢,我们上学时应该学过,如果A除以B,得到的余数为0,说明A是B的整数倍,那么这个时候,就不会有剩下的数,也就是说,如果Hex的数据长度来除以onceTxHexLen的余数是0,那么我们只要使用Hex的数据长度来对onceTxHexLen取一次整(商),得到的商是多少,就说明我们需要传输多少次36了,如果余数不是0,那么只需要在这个商的基础上,再加上1,就是我们需要传输的36的次数了。

当然,如果最后的余数不是0,说明我们剩下的数据已经不足一整个36包了,这时候,我们要发送的最后这一包36的数据长度,就要随之减小,减小到多少?减小到剩余的Hex数据+2。

明白了以上的逻辑,我们就可以实现这个36传输的代码了;

每次36传输时,根据当前正准备传输的包的序号,

由于每次36传输的数据都不一样,这里我先增加一个辅助函数,用于清空一个byte数组,届时我们每次传输前先把数组清空,然后再去hex文件里面拷贝数据,确保数据不传错。

cpp 复制代码
/*清空数组*/
void initArr(byte data[])
{
  dword i;
  for(i = 0;i < elcount(data);i++)data[i] = 0x00;
}
cpp 复制代码
/*传输Driver文件,成功返回1,失败返回0*/
int FlashStepI()
{
  dword hexDataTotalLen;//文件数据总长度
  int transmitTotalPackageCnt;//要传输的包数
  int remainder;//余数
  int quotient;//商
  byte packageIndex;//包的索引;
  word index;//总索引
  dword onceTxHexLen;
  byte respData[4096];
  const dword waitRespMaxTimeout = 10000;//定义最大的响应等待时间(ms)
  long respRet;
  long respByteNum;//定义存储响应数据总字节长度的变量
  sysSetVariableString(sysvar::EcuBootload::EcuFlashResult,"正在传输Driver数据");
  onceTxHexLen = gOnceTransmitBlockSize - 2;//一包能传输的hex数据长度
  
  packageIndex = 1;//初始化索引为1
  hexDataTotalLen = gDriverLength;
  remainder = hexDataTotalLen % onceTxHexLen;//取余
  quotient = hexDataTotalLen / onceTxHexLen;//取整
  
  transmitTotalPackageCnt = remainder == 0?quotient:quotient+1;//如果整除,就取商,无法整除则取商+1
  for(index = 0;index < transmitTotalPackageCnt;index++)
  {
    byte data[0xFFF];
    dword len;
    initArr(data);//清空数组
    data[0] = 0x36;
    data[1] = packageIndex;//
    if(index < transmitTotalPackageCnt - 1)//不是最后一包
    {
      memcpy_off(data,2,gDriverData,index * onceTxHexLen,onceTxHexLen);//将hex文件的数据拷贝过去
      gDiagRequest.Resize(gOnceTransmitBlockSize);//设置要传输的数据长度
      gDiagRequest.SetPrimitiveData(data,gOnceTransmitBlockSize);//设置诊断数据
      gDiagRequest.SendRequest();//传输1包数据
    }
    else if(index == transmitTotalPackageCnt - 1)//最后一包
    {
      word remainingHexDataLen;//剩余的hex长度
      word remainingDataLen;//剩余所有数据长度,包含数据头36 xx
      remainingHexDataLen = hexDataTotalLen - index * onceTxHexLen;
      remainingDataLen = remainingHexDataLen + 2;
      memcpy_off(data,2,gDriverData,index * onceTxHexLen,remainingHexDataLen);//将剩余的hex文件的数据拷贝过去
      gDiagRequest.Resize(remainingDataLen);//设置要传输的数据长度
      gDiagRequest.SetPrimitiveData(data,remainingDataLen);//设置诊断数据
      gDiagRequest.SendRequest();//传输1包数据
    }
    
    respRet = testWaitForDiagResponse(gDiagRequest,waitRespMaxTimeout);//
    if(respRet != 1)//未响应
    {
      return 0;
    }
    diagGetLastResponse(gDiagRequest,gDiagResponse);//将的响应数据获取了存到gDiagResponse里面。
    respByteNum = gDiagResponse.GetPrimitiveSize();//获取响应的数据长度
    gDiagResponse.GetPrimitiveData(respData,elcount(respData));//获取响应的数据
    
    if(diagGetLastResponseCode(gDiagRequest) != -1)//如果不是肯定响应  返回0,表示失败
      return 0;
    
    if(respData[1] != data[1]) return 0;//这里说明回复的Seq和我请求的Seq不是同一个  也是错误的
    
    packageIndex < 0xFF?packageIndex++:packageIndex = 0;//翻转Seq
  }
  return 1;
}

3.11 Step10 退出Driver数据传输

大多数ECU退出传输这里都比较简单,只需要判断是否是肯定响应即可;

cpp 复制代码
/*退出驱动数据传输,成功返回1,失败返回0*/
int FlashStepJ()
{
  byte reqData[1] = {0x37};//定义诊断请求数据
  const dword waitRespMaxTimeout = 2000;//定义最大的响应等待时间(ms)
  long respRet;
  byte respData[256];//定义存储响应数据的缓冲区(数组)
  long respByteNum;//定义存储响应数据总字节长度的变量
  
  sysSetVariableString(sysvar::EcuBootload::EcuFlashResult,"正在退出Driver数据传输");
  gDiagRequest.Resize(elcount(reqData));//此函数用于重置(设置)指定的诊断对象的字节数
  gDiagRequest.SetPrimitiveData(reqData,elcount(reqData));//
  gDiagRequest.SendRequest();
  
  respRet = testWaitForDiagResponse(gDiagRequest,waitRespMaxTimeout);//
  if(respRet != 1)//未响应
  {
    return 0;
  }
  diagGetLastResponse(gDiagRequest,gDiagResponse);//响应数据获取了存到gDiagResponse里面。
  respByteNum = gDiagResponse.GetPrimitiveSize();//获取响应的数据长度
  gDiagResponse.GetPrimitiveData(respData,elcount(respData));//获取响应的数据
  
  if(diagGetLastResponseCode(gDiagRequest) == -1)//如果是肯定响应  返回1,表示成功
    return 1;
  else
    return 0;//返回0,表示失败;
}

3.12 Step11 Driver数据完整性检查(CRC32)

通过上面的步骤,我们已经成功的把Driver文件的数据传输给了ECU,那么我们在传输的过程中,总线上可能由于电气干扰,电平不稳等出现一些问题,例如我原本要传输的字节是00,经过干扰之后,变成了01,或者就是不小心传输了错误的数据,怎么保证我的ECU对错误的数据不会进行操作呢?这里就需要使用到我们的CRC(循环冗余校验算法),这里使用的CRC通常都是CRC32,将整个传输的Block的数据通过CRC32算法,得到一个32位的整数,再把整数按照高位在前,低位在后的顺序,拆分成四个字节发给ECU,ECU收到我们发送过去的CRC32之后,就把当前已经接收到了的Driver数据也做一遍相同的CRC32算法,然后拿我们发过去的CRC32值与ECU内部自行计算出来的值进行比较,如果值一致,说明我们传输给到ECU的数据没有出错,那么ECU将会回复肯定响应表示数据完整性检查成功;

既然要计算CRC32,那么我们就需要先实现一个计算CRC32的函数,这里可能需要根据各个ECU的具体要求算法进行实现,不过大多数的ECU使用的都是标准的CRC32算法,也就是生成多项式为 0xEDB88320,CRC初值为 0xFFFFFFFF的算法;计算CRC通常有查表法和直接计算法两种方法,在资源紧张,算力有限的嵌入式平台中通常使用查表法,查表法需要预先定义出CRC表,占用一部分全局变量空间,由于我们的上位机是在PC上运行,算力强大,这里就以直接计算法进行实现了;

cpp 复制代码
/*计算CRC32*/
dword Calculate_CRC32(byte buf[], dword len) 
{
  dword i, j;
  dword crc;
  const dword POLY = 0xEDB88320; // 标准 CRC32 多项式(反转形式)

  crc = 0xFFFFFFFF; //初始值

  for (i = 0; i < len; i++) 
  {
    // 将当前字节与 CRC 的低8位进行异或
    crc ^= buf[i];

    // 逐位处理:循环8次,处理当前字节的8个位
    for (j = 0; j < 8; j++) 
    {
      if (crc & 1) 
      {
        // 如果最低位是1,则右移并异或多项式
        crc = (crc >> 1) ^ POLY;
      } 
      else 
      {
        // 如果最低位是0,则直接右移
        crc = (crc >> 1);
      }
    }
  }

  return crc ^ 0xFFFFFFFF; // 结果取反
}

有了计算CRC32的函数之后,我们就可以实现完整性检查这个步骤了;

cpp 复制代码
/*驱动完整性检查,成功返回1,失败返回0*/
int FlashStepK()
{
  byte reqData[8];//定义诊断请求数据
  const dword waitRespMaxTimeout = 2000;//定义最大的响应等待时间(ms)
  long respRet;
  byte respData[256];//定义存储响应数据的缓冲区(数组)
  long respByteNum;//定义存储响应数据总字节长度的变量
  dword crc32;
  
  sysSetVariableString(sysvar::EcuBootload::EcuFlashResult,"正在进行Driver数据完整性检查");
  reqData[0] = 0x31;reqData[1] = 0x01;reqData[2] = 0xF0;reqData[3] = 0x01;//完整性检查的RID
  
  //计算crc32
  crc32 = Calculate_CRC32(gDriverData,gDriverLength);//使用driver数据求crc32
  reqData[4] = crc32 >> 24;
  reqData[5] = crc32 >> 16;
  reqData[6] = crc32 >> 8;
  reqData[7] = crc32 & 0xFF;//依次取出crc32的每一字节
  
  gDiagRequest.Resize(elcount(reqData));//此函数用于重置(设置)指定的诊断对象的字节数
  gDiagRequest.SetPrimitiveData(reqData,elcount(reqData));//
  gDiagRequest.SendRequest();//发送完整性检查请求
  
  respRet = testWaitForDiagResponse(gDiagRequest,waitRespMaxTimeout);//
  if(respRet != 1)//未响应
  {
    return 0;
  }
  diagGetLastResponse(gDiagRequest,gDiagResponse);//响应数据获取了存到gDiagResponse里面。
  respByteNum = gDiagResponse.GetPrimitiveSize();//获取响应的数据长度
  gDiagResponse.GetPrimitiveData(respData,elcount(respData));//获取响应的数据
  
  if(diagGetLastResponseCode(gDiagRequest) == -1 && respData[4] == 0x00)//如果是肯定响应并且例程结果为00  返回1,表示成功
    return 1;
  else
    return 0;//返回0,表示失败;
}

3.13 Step12 擦除APP块数据

Driver数据传输完毕之后,通常就要开始向ECU传输APP数据了,对于ECU来说,在传输之前,APP的内存块是有数据的,有数据时我们没法直接往APP内存区下载数据,所以需要先通过诊断命令来擦除APP内存区的数据。在擦除时,需要指定擦除的地址和要擦除的数据长度。而我们的地址和长度,又可以在解析hex/s19/bin文件这些文件时获得(还是和传输Driver时一样,我们先假定APP数据已经解析完毕了,因为我准备在最后面搭建整体的流程框架的时候,在刷写最开始之前解析文件,虽然现在还没写,但我们先按照已经解析完毕了开始实现),也就是说,我们直接发送31服务擦除对应的APP地址和长度就可以了。

cpp 复制代码
/*擦除APP内存,成功返回1,失败返回0*/
int FlashStepL()
{
  byte reqData[12];//定义诊断请求数据
  const dword waitRespMaxTimeout = 2000;//定义最大的响应等待时间(ms)
  long respRet;
  byte respData[256];//定义存储响应数据的缓冲区(数组)
  long respByteNum;//定义存储响应数据总字节长度的变量
  int i;
  
  sysSetVariableString(sysvar::EcuBootload::EcuFlashResult,"正在擦除APP内存");
  reqData[0] = 0x31;reqData[1] = 0x01;reqData[2] = 0xFF;reqData[3] = 0x00;//擦内存的RID
  
  for(i = 0;i < 4;i++)
  {
    reqData[4 + i] = (gAppStartAddress >> ((3 - i) * 8)) & 0xFF;//把app地址填充到请求数据
    reqData[8 + i] = (gAppLength >> ((3 - i) * 8)) & 0xFF;//把app长度填充
  }
  
  gDiagRequest.Resize(elcount(reqData));//此函数用于重置(设置)指定的诊断对象的字节数
  gDiagRequest.SetPrimitiveData(reqData,elcount(reqData));//
  gDiagRequest.SendRequest();//发送完整性检查请求
  
  respRet = testWaitForDiagResponse(gDiagRequest,waitRespMaxTimeout);//
  if(respRet != 1)//未响应
  {
    return 0;
  }
  diagGetLastResponse(gDiagRequest,gDiagResponse);//响应数据获取了存到gDiagResponse里面。
  respByteNum = gDiagResponse.GetPrimitiveSize();//获取响应的数据长度
  gDiagResponse.GetPrimitiveData(respData,elcount(respData));//获取响应的数据
  
  if(diagGetLastResponseCode(gDiagRequest) == -1 && respData[4] == 0x00)//如果是肯定响应并且例程结果为00  返回1,表示成功
    return 1;
  else
    return 0;//返回0,表示失败;
}

3.14 Step13 请求下载数据(APP)

由于我们上面已经实现了Driver文件的下载,APP文件的下载方法和Driver就完全一致了,区别仅在于这里我们把传输的数据,数据地址,数据长度替换成APP的就可以了。

cpp 复制代码
/*请求下载App文件,成功返回1,失败返回0*/
int FlashStepM()
{
  byte reqData[11];//定义诊断请求数据
  const dword waitRespMaxTimeout = 2000;//定义最大的响应等待时间(ms)
  long respRet;
  byte respData[256];//定义存储响应数据的缓冲区(数组)
  long respByteNum;//定义存储响应数据总字节长度的变量
  int i;
  
  sysSetVariableString(sysvar::EcuBootload::EcuFlashResult,"正在请求下载App数据");
  reqData[0] = 0x34;reqData[1] = 0x00;reqData[2] = 0x44;//设置地址和长度的表示格式
  for(i = 0;i < 4;i++)
  {
    reqData[i + 3] = (gAppStartAddress >> ((3 - i) * 8)) & 0xFF;//设置起始地址
    reqData[i + 7] = (gAppLength >> ((3 - i) * 8)) & 0xFF;//设置长度
  }
  
  gDiagRequest.Resize(elcount(reqData));//此函数用于重置(设置)指定的诊断对象的字节数
  gDiagRequest.SetPrimitiveData(reqData,elcount(reqData));//设置诊断请求的数据为
  gDiagRequest.SendRequest();//发送诊断请求
  
  respRet = testWaitForDiagResponse(gDiagRequest,waitRespMaxTimeout);//
  if(respRet != 1)//未响应
  {
    return 0;
  }
  diagGetLastResponse(gDiagRequest,gDiagResponse);//将的响应数据获取了存到gDiagResponse里面。
  respByteNum = gDiagResponse.GetPrimitiveSize();//获取响应的数据长度
  gDiagResponse.GetPrimitiveData(respData,elcount(respData));//获取响应的数据
  
  if(diagGetLastResponseCode(gDiagRequest) == -1)//如果是肯定响应  返回1,表示成功
  {
    byte blockSizeByteLen;
    byte len;
    gOnceTransmitBlockSize = 0;
    blockSizeByteLen = respByteNum - 2;//长度减去2个字节,即减去74 和 xx  //假设为4
    for(len = 0;len < blockSizeByteLen;len++)
    {
      gOnceTransmitBlockSize |= respData[2 + len] << (8 * (blockSizeByteLen - len - 1));//拼凑一包可以传输的长度
    }
    return 1;
  }
  else
    return 0;//返回0,表示失败;
}

3.15 Step14 传输APP数据

这里也是一样的,由于在传输driver数据时,我们已经讲解并且完成了数据传输的逻辑,那么这里就也一样,只需要把传输的数据替换成APP数据就可以了。

cpp 复制代码
/*传输App文件,成功返回1,失败返回0*/
int FlashStepN()
{
  dword hexDataTotalLen;//文件数据总长度
  int transmitTotalPackageCnt;//要传输的包数
  int remainder;//余数
  int quotient;//商
  byte packageIndex;//包的索引;
  word index;//总索引
  dword onceTxHexLen;
  byte respData[4096];
  const dword waitRespMaxTimeout = 10000;//定义最大的响应等待时间(ms)
  long respRet;
  long respByteNum;//定义存储响应数据总字节长度的变量
  sysSetVariableString(sysvar::EcuBootload::EcuFlashResult,"正在传输App数据");
  onceTxHexLen = gOnceTransmitBlockSize - 2;//一包能传输的hex数据长度
  
  packageIndex = 1;//初始化索引为1
  hexDataTotalLen = gAppLength;
  remainder = hexDataTotalLen % onceTxHexLen;//取余
  quotient = hexDataTotalLen / onceTxHexLen;//取整
  
  transmitTotalPackageCnt = remainder == 0?quotient:quotient+1;//如果整除,就取商,无法整除则取商+1
  for(index = 0;index < transmitTotalPackageCnt;index++)
  {
    byte data[0xFFF];
    dword len;
    initArr(data);//清空数组
    data[0] = 0x36;
    data[1] = packageIndex;//
    if(index < transmitTotalPackageCnt - 1)//不是最后一包
    {
      memcpy_off(data,2,gAppData,index * onceTxHexLen,onceTxHexLen);//将hex文件的数据拷贝过去
      gDiagRequest.Resize(gOnceTransmitBlockSize);//设置要传输的数据长度
      gDiagRequest.SetPrimitiveData(data,gOnceTransmitBlockSize);//设置诊断数据
      gDiagRequest.SendRequest();//传输1包数据
    }
    else if(index == transmitTotalPackageCnt - 1)//最后一包
    {
      word remainingHexDataLen;//剩余的hex长度
      word remainingDataLen;//剩余所有数据长度,包含数据头36 xx
      remainingHexDataLen = hexDataTotalLen - index * onceTxHexLen;
      remainingDataLen = remainingHexDataLen + 2;
      memcpy_off(data,2,gAppData,index * onceTxHexLen,remainingHexDataLen);//将剩余的hex文件的数据拷贝过去
      gDiagRequest.Resize(remainingDataLen);//设置要传输的数据长度
      gDiagRequest.SetPrimitiveData(data,remainingDataLen);//设置诊断数据
      gDiagRequest.SendRequest();//传输1包数据
    }
    
    respRet = testWaitForDiagResponse(gDiagRequest,waitRespMaxTimeout);//
    if(respRet != 1)//未响应
    {
      return 0;
    }
    diagGetLastResponse(gDiagRequest,gDiagResponse);//将的响应数据获取了存到gDiagResponse里面。
    respByteNum = gDiagResponse.GetPrimitiveSize();//获取响应的数据长度
    gDiagResponse.GetPrimitiveData(respData,elcount(respData));//获取响应的数据
    
    if(diagGetLastResponseCode(gDiagRequest) != -1)//如果不是肯定响应  返回0,表示失败
      return 0;
    
    if(respData[1] != data[1]) return 0;//这里说明回复的Seq和我请求的Seq不是同一个  也是错误的
    
    packageIndex < 0xFF?packageIndex++:packageIndex = 0;//翻转Seq
  }
  return 1;
}

3.16 Step15 退出App数据传输

一样的,这里的逻辑和Driver退出时就完全一致了;

cpp 复制代码
/*退出App数据传输,成功返回1,失败返回0*/
int FlashStepO()
{
  byte reqData[1] = {0x37};//定义诊断请求数据
  const dword waitRespMaxTimeout = 2000;//定义最大的响应等待时间(ms)
  long respRet;
  byte respData[256];//定义存储响应数据的缓冲区(数组)
  long respByteNum;//定义存储响应数据总字节长度的变量
  
  sysSetVariableString(sysvar::EcuBootload::EcuFlashResult,"正在退出App数据传输");
  gDiagRequest.Resize(elcount(reqData));//此函数用于重置(设置)指定的诊断对象的字节数
  gDiagRequest.SetPrimitiveData(reqData,elcount(reqData));//
  gDiagRequest.SendRequest();
  
  respRet = testWaitForDiagResponse(gDiagRequest,waitRespMaxTimeout);//
  if(respRet != 1)//未响应
  {
    return 0;
  }
  diagGetLastResponse(gDiagRequest,gDiagResponse);//响应数据获取了存到gDiagResponse里面。
  respByteNum = gDiagResponse.GetPrimitiveSize();//获取响应的数据长度
  gDiagResponse.GetPrimitiveData(respData,elcount(respData));//获取响应的数据
  
  if(diagGetLastResponseCode(gDiagRequest) == -1)//如果是肯定响应  返回1,表示成功
    return 1;
  else
    return 0;//返回0,表示失败;
}

3.17 Step16 App数据完整性检查(CRC32)

这里的逻辑也和Driver时的完整性检查完全一致,区别仅在于这里发送的CRC32是通过APP文件中的数据计算出来的。

cpp 复制代码
/*APP完整性检查,成功返回1,失败返回0*/
int FlashStepP()
{
  byte reqData[8];//定义诊断请求数据
  const dword waitRespMaxTimeout = 2000;//定义最大的响应等待时间(ms)
  long respRet;
  byte respData[256];//定义存储响应数据的缓冲区(数组)
  long respByteNum;//定义存储响应数据总字节长度的变量
  dword crc32;
  
  sysSetVariableString(sysvar::EcuBootload::EcuFlashResult,"正在进行App数据完整性检查");
  reqData[0] = 0x31;reqData[1] = 0x01;reqData[2] = 0xF0;reqData[3] = 0x01;//完整性检查的RID
  
  //计算crc32
  crc32 = Calculate_CRC32(gAppData,gAppLength);//使用App数据求crc32
  reqData[4] = crc32 >> 24;
  reqData[5] = crc32 >> 16;
  reqData[6] = crc32 >> 8;
  reqData[7] = crc32 & 0xFF;//依次取出crc32的每一字节
  
  gDiagRequest.Resize(elcount(reqData));//此函数用于重置(设置)指定的诊断对象的字节数
  gDiagRequest.SetPrimitiveData(reqData,elcount(reqData));//
  gDiagRequest.SendRequest();//发送完整性检查请求
  
  respRet = testWaitForDiagResponse(gDiagRequest,waitRespMaxTimeout);//
  if(respRet != 1)//未响应
  {
    return 0;
  }
  diagGetLastResponse(gDiagRequest,gDiagResponse);//响应数据获取了存到gDiagResponse里面。
  respByteNum = gDiagResponse.GetPrimitiveSize();//获取响应的数据长度
  gDiagResponse.GetPrimitiveData(respData,elcount(respData));//获取响应的数据
  
  if(diagGetLastResponseCode(gDiagRequest) == -1 && respData[4] == 0x00)//如果是肯定响应并且例程结果为00  返回1,表示成功
    return 1;
  else
    return 0;//返回0,表示失败;
}

3.18 Step17 编程依赖性检查

依赖性检查是干什么的呢?我们上面进行的完整性检查,只能证明我们传给ECU的数据,和ECU实际接收到的数据是一致的,但是并无法保证我们传输的程序的正确性,比如我们的ECU是A ECU,这时候我们把B ECU的Hex文件传输给A(把起始地址改成一样的,只是数据用B的数据),这时候如果我们的A ECU加载了B的软件,那么后果是很严重的,A可能彻底失灵,没有任何功能。依赖性检查的目的和作用就是在这里,用于检查我们发给ECU的数据是否合理。通常开发者会在Hex文件中设计几个字节的特殊编码和校验码,当我们传输给ECU的数据这些特殊编码和校验码错误时,ECU就可以知道,我们给它发送了无效的数据,它就会回复NRC,拒绝编程,防止误刷软件变砖;

cpp 复制代码
/*编程依赖性检查,成功返回1,失败返回0*/
int FlashStepQ()
{
  byte reqData[4] = {0x31,0x01,0xFF,0x01};//定义诊断请求数据
  const dword waitRespMaxTimeout = 2000;//定义最大的响应等待时间(ms)
  long respRet;
  byte respData[256];//定义存储响应数据的缓冲区(数组)
  long respByteNum;//定义存储响应数据总字节长度的变量
  
  sysSetVariableString(sysvar::EcuBootload::EcuFlashResult,"正在进行依赖性检查");
  gDiagRequest.Resize(elcount(reqData));//此函数用于重置(设置)指定的诊断对象的字节数
  gDiagRequest.SetPrimitiveData(reqData,elcount(reqData));//
  gDiagRequest.SendRequest();
  
  respRet = testWaitForDiagResponse(gDiagRequest,waitRespMaxTimeout);//
  if(respRet != 1)//未响应
  {
    return 0;
  }
  diagGetLastResponse(gDiagRequest,gDiagResponse);//响应数据获取了存到gDiagResponse里面。
  respByteNum = gDiagResponse.GetPrimitiveSize();//获取响应的数据长度
  gDiagResponse.GetPrimitiveData(respData,elcount(respData));//获取响应的数据
  
  if(diagGetLastResponseCode(gDiagRequest) == -1 && respData[4] == 0x00)//如果是肯定响应并且例程结果为00  返回1,表示成功
    return 1;
  else
    return 0;//返回0,表示失败;
}

3.19 Step18 ECU复位

只要上一步的依赖性检查通过了,其实我们的ECU升级就已经完成了,后面这里只不过是完成一些收尾性的工作,复位ECU是为了使得ECU能够启动我们刚刚新刷写进去的软件。

这里和最开始一样,只需要判断是否回复肯定响应就可以了,唯一需要额外关注的就是,ECU重启的时间可能会略长,这个等待响应的时间可以稍微给久一些,我这边就给2S了;

cpp 复制代码
/*ECU复位,成功返回1,失败返回0*/
int FlashStepR()
{
  byte reqData[2] = {0x11,0x01};//定义诊断请求数据
  const dword waitRespMaxTimeout = 2000;//定义最大的响应等待时间(ms)
  long respRet;
  byte respData[256];//定义存储响应数据的缓冲区(数组)
  long respByteNum;//定义存储响应数据总字节长度的变量
  
  sysSetVariableString(sysvar::EcuBootload::EcuFlashResult,"正在进行ECU复位重启");
  gDiagRequest.Resize(elcount(reqData));//此函数用于重置(设置)指定的诊断对象的字节数
  gDiagRequest.SetPrimitiveData(reqData,elcount(reqData));//
  gDiagRequest.SendRequest();
  
  respRet = testWaitForDiagResponse(gDiagRequest,waitRespMaxTimeout);//
  if(respRet != 1)//未响应
  {
    return 0;
  }
  diagGetLastResponse(gDiagRequest,gDiagResponse);//响应数据获取了存到gDiagResponse里面。
  respByteNum = gDiagResponse.GetPrimitiveSize();//获取响应的数据长度
  gDiagResponse.GetPrimitiveData(respData,elcount(respData));//获取响应的数据
  
  if(diagGetLastResponseCode(gDiagRequest) == -1)//如果是肯定响应  返回1,表示成功
    return 1;
  else
    return 0;//返回0,表示失败;
}

3.20 Step19 进入扩展会话

这里继续进入扩展会话的目的,是因为我们在最开始的时候,禁止了ECU的DTC检测和非诊断通信功能,重新进入扩展会话就是为了能够发送恢复这些功能的诊断请求;

需要注意的是,这里我们最好是再增加一个函数,而不是直接使用之前最开始的StepA,因为它毕竟是两个步骤,使用两个函数更方便我们后续统计到底执行了多少步骤,计算进度等;

cpp 复制代码
/*进入扩展会话,成功返回1,失败返回0*/
int FlashStepS()
{
  byte reqData[2] = {0x10,0x03};//定义诊断请求数据
  const dword waitRespMaxTimeout = 1000;//定义最大的响应等待时间(ms)
  long respRet;
  byte respData[256];//定义存储响应数据的缓冲区(数组)
  long respByteNum;//定义存储响应数据总字节长度的变量
  
  sysSetVariableString(sysvar::EcuBootload::EcuFlashResult,"正在进入扩展会话");
  gDiagRequest.Resize(elcount(reqData));//此函数用于重置(设置)指定的诊断对象的字节数
  gDiagRequest.SetPrimitiveData(reqData,elcount(reqData));//设置诊断请求的数据为10 03
  gDiagRequest.SendRequest();//发送10 03诊断请求
  
  respRet = testWaitForDiagResponse(gDiagRequest,waitRespMaxTimeout);//
  if(respRet != 1)//未响应
  {
    return 0;
  }
  diagGetLastResponse(gDiagRequest,gDiagResponse);//将10 03的响应数据获取了存到gDiagResponse里面。
  respByteNum = gDiagResponse.GetPrimitiveSize();//获取响应的数据长度
  gDiagResponse.GetPrimitiveData(respData,elcount(respData));//获取响应的数据
  
  if(diagGetLastResponseCode(gDiagRequest) == -1)//如果是肯定响应  返回1,表示成功
    return 1;
  else
    return 0;//返回0,表示失败;
}

3.21 Step20 开启DTC检测

cpp 复制代码
/*开启DTC检测,成功返回1,失败返回0*/
int FlashStepT()
{
  byte reqData[2] = {0x85,0x01};//定义诊断请求数据
  const dword waitRespMaxTimeout = 1000;//定义最大的响应等待时间(ms)
  long respRet;
  byte respData[256];//定义存储响应数据的缓冲区(数组)
  long respByteNum;//定义存储响应数据总字节长度的变量
  
  sysSetVariableString(sysvar::EcuBootload::EcuFlashResult,"正在开启DTC检测");
  gDiagRequest.Resize(elcount(reqData));//此函数用于重置(设置)指定的诊断对象的字节数
  gDiagRequest.SetPrimitiveData(reqData,elcount(reqData));//
  gDiagRequest.SendRequest();
  
  respRet = testWaitForDiagResponse(gDiagRequest,waitRespMaxTimeout);//
  if(respRet != 1)//未响应
  {
    return 0;
  }
  diagGetLastResponse(gDiagRequest,gDiagResponse);//响应数据获取了存到gDiagResponse里面。
  respByteNum = gDiagResponse.GetPrimitiveSize();//获取响应的数据长度
  gDiagResponse.GetPrimitiveData(respData,elcount(respData));//获取响应的数据
  
  if(diagGetLastResponseCode(gDiagRequest) == -1)//如果是肯定响应  返回1,表示成功
    return 1;
  else
    return 0;//返回0,表示失败;
}

3.22 Step21 开启非诊断通信

cpp 复制代码
/*开启非诊断通信,成功返回1,失败返回0*/
int FlashStepU()
{
  byte reqData[3] = {0x28,0x00,0x03};//定义诊断请求数据
  const dword waitRespMaxTimeout = 1000;//定义最大的响应等待时间(ms)
  long respRet;
  byte respData[256];//定义存储响应数据的缓冲区(数组)
  long respByteNum;//定义存储响应数据总字节长度的变量
  
  sysSetVariableString(sysvar::EcuBootload::EcuFlashResult,"正在开启非诊断通信");
  gDiagRequest.Resize(elcount(reqData));//此函数用于重置(设置)指定的诊断对象的字节数
  gDiagRequest.SetPrimitiveData(reqData,elcount(reqData));//
  gDiagRequest.SendRequest();
  
  respRet = testWaitForDiagResponse(gDiagRequest,waitRespMaxTimeout);//
  if(respRet != 1)//未响应
  {
    return 0;
  }
  diagGetLastResponse(gDiagRequest,gDiagResponse);//响应数据获取了存到gDiagResponse里面。
  respByteNum = gDiagResponse.GetPrimitiveSize();//获取响应的数据长度
  gDiagResponse.GetPrimitiveData(respData,elcount(respData));//获取响应的数据
  
  if(diagGetLastResponseCode(gDiagRequest) == -1)//如果是肯定响应  返回1,表示成功
    return 1;
  else
    return 0;//返回0,表示失败;
}

3.23 Step22 进入默认会话

到这里就已经结束本次的刷写了,这里的目的是为了回到默认会话,使ECU恢复默认诊断状态;

cpp 复制代码
/*进入默认会话,成功返回1,失败返回0*/
int FlashStepV()
{
  byte reqData[2] = {0x10,0x01};//定义诊断请求数据
  const dword waitRespMaxTimeout = 1000;//定义最大的响应等待时间(ms)
  long respRet;
  byte respData[256];//定义存储响应数据的缓冲区(数组)
  long respByteNum;//定义存储响应数据总字节长度的变量
  
  sysSetVariableString(sysvar::EcuBootload::EcuFlashResult,"正在进入默认会话");
  gDiagRequest.Resize(elcount(reqData));//此函数用于重置(设置)指定的诊断对象的字节数
  gDiagRequest.SetPrimitiveData(reqData,elcount(reqData));//设置诊断请求的数据为10 01
  gDiagRequest.SendRequest();//发送10 01诊断请求
  
  respRet = testWaitForDiagResponse(gDiagRequest,waitRespMaxTimeout);//
  if(respRet != 1)//未响应
  {
    return 0;
  }
  diagGetLastResponse(gDiagRequest,gDiagResponse);//将10 01的响应数据获取了存到gDiagResponse里面。
  respByteNum = gDiagResponse.GetPrimitiveSize();//获取响应的数据长度
  gDiagResponse.GetPrimitiveData(respData,elcount(respData));//获取响应的数据
  
  if(diagGetLastResponseCode(gDiagRequest) == -1)//如果是肯定响应  返回1,表示成功
    return 1;
  else
    return 0;//返回0,表示失败;
}

3.24 整合执行所有刷写步骤

在上面的篇幅中,我们已经完成了一般刷写的22个步骤的分步case,但是,一个完整的刷写流程应该是要执行这些所有的步骤,所以,接下来,我们再实现一下完整的刷写步骤。

首先,如果让大家来完成这个综合刷写步骤,大家会怎么进行编码呢?我看到过一些网友/同事的代码,会写一个函数,在函数内部来依次执行各个分支函数,如果上一步通过了,就走下一步,否则就不执行后面的流程。这样子写出来的代码,会使得这个综合函数里面if else漫天飞,且当需要增加一个步骤/插入一个步骤、删除一个步骤时,面对漫天的连贯的if else,改动起来会非常的痛苦。虽然我这里是使用的最简单的方法来实现刷写代码,但我依旧不想使用这种综合写的方法,在日常的编码中,我们最重要的是程序架构要合理,方便修改,简单易懂等。所以,在这里,我将使用一种非常优雅的方法来实现这个功能(表驱动法),如果有做嵌入式开发的朋友,应该对这种方法非常的熟悉了,由于CAPL中无法使用指针,所以这里没法使用函数指针来实现了。我准备把当前的所有流程步骤,写一个枚举。然后再创建一个函数,在这个函数里面,对所有的枚举值和我们的具体的步骤函数增加一个一一对应关系,模拟函数指针的操作。随后再写一个函数,用来执行我们的步骤数组里的枚举值,完成我们的表驱动方法。

首先,定义一个全局的枚举,用来和每一步进行一一对应;

所以这里会对我们之前的全局变量进行一些修改;

cpp 复制代码
variables
{
  diagRequest * gDiagRequest;//定义全局诊断请求
  diagResponse * gDiagResponse;//定义全局诊断响应
  
  byte gDriverData[0x100000];//driver数据 给个1M的缓冲区,一般的文件都不可能有这么大,完全足够了
  byte gAppData[0x100000];//App数据 给个1M的缓冲区,一般的文件都不可能有这么大,完全足够了
  
  dword gDriverStartAddress;//驱动起始地址
  dword gAppStartAddress;//App起始地址
  dword gDriverLength;//驱动数据长度
  dword gAppLength;//App数据长度
  
  dword gOnceTransmitBlockSize;//36服务一次可以传输的字节数,由34服务回复时获得
  
  enum FlashSteps{
    STEP_A = 0,//进入扩展会话
    STEP_B = 1,//关闭DTC检测
    STEP_C = 2,//关闭非诊断通信
    STEP_D = 3,//预编程检查
    STEP_E = 4,//进入编程会话
    STEP_F = 5,//安全等级解锁
    STEP_G = 6,//写入刷写指纹
    STEP_H = 7,//请求下载数据(Driver)
    STEP_I = 8,//传输Driver数据
    STEP_J = 9,//退出Driver传输
    STEP_K = 10,//Driver完整性校验
    STEP_L = 11,//擦除APP内存
    STEP_M = 12,//请求下载数据(APP)
    STEP_N = 13,//传输APP数据
    STEP_O = 14,//退出APP数据传输
    STEP_P = 15,//APP完整性校验
    STEP_Q = 16,//检查编程依赖性
    STEP_R = 17,//ECU复位
    STEP_S = 18,//复位后重新进入扩展会话
    STEP_T = 19,//开启DTC检测
    STEP_U = 20,//开启非诊断通信
    STEP_V = 21//进入默认会话
  };//刷写步骤枚举值定义
  
}

然后,写一个函数来使得个个枚举值与具体的步骤函数实现一一绑定;

cpp 复制代码
/* 统一的步骤处理器 */
int HandleStep(int stepEnum)
{
  switch(stepEnum)
  {
    //将每一个枚举值绑定到对应的step函数上
    case STEP_A: return FlashStepA();
    case STEP_B: return FlashStepB();
    case STEP_C: return FlashStepC();
    case STEP_D: return FlashStepD();
    case STEP_E: return FlashStepE();
    case STEP_F: return FlashStepF();
    case STEP_G: return FlashStepG();
    case STEP_H: return FlashStepH();
    case STEP_I: return FlashStepI();
    case STEP_J: return FlashStepJ();
    case STEP_K: return FlashStepK();
    case STEP_L: return FlashStepL();
    case STEP_M: return FlashStepM();
    case STEP_N: return FlashStepN();
    case STEP_O: return FlashStepO();
    case STEP_P: return FlashStepP();
    case STEP_Q: return FlashStepQ();
    case STEP_R: return FlashStepR();
    case STEP_S: return FlashStepS();
    case STEP_T: return FlashStepT();
    case STEP_U: return FlashStepU();
    case STEP_V: return FlashStepV();
    default:
      // 假设 write 是你项目中用于日志输出的函数
      write("Error: Unknown step %d", stepEnum);
      return -1;
  }
}

现在我们的步骤和上面的枚举值就有了一一对应关系了。

现在假设我们想执行全部的刷写步骤,那么我们可以定义一个int数组,然后把里面的元素设置为所有的Step枚举值,这时候,再写一个遍历数组的函数,遍历数组里面的所有元素,并且调用对应的Step函数即可。

所以,现在我们又要增加一下全局变量了;

cpp 复制代码
const FlashStepTotalCnt = STEP_V + 1;//总步骤就是最后一个枚举值+1
  
int FlashStep_NomalStep[FlashStepTotalCnt] = {
    STEP_A,STEP_B,STEP_C,STEP_D,STEP_E,
    STEP_F,STEP_G,STEP_H,STEP_I,STEP_J,
    STEP_K,STEP_L,STEP_M,STEP_N,STEP_O,
    STEP_P,STEP_Q,STEP_R,STEP_S,STEP_T,
    STEP_U,STEP_V
  };//定义默认情况下的刷写步骤表

int gCurrFlashStep = 0;//记录当前的刷写步骤

同时,我们再定义一个变量来记录当前执行到了哪一个步骤,那么当每一个步骤执行结束完后,都可以根据这个变量来对总共的步骤进行除法运算,从而获得当前的刷写进度。

同时,如果刷写到任意一个步骤失败了,我们最好是在panle上或者什么地方再提示一下。所以,这里我再增加一个失败时可以打印失败步骤到panel上的函数,如果失败了的了话,我们的gCurrFlashStep 这个变量最后的值是几,就代表是在哪一步失败了。

最后,再增加一个函数,用来在每次开始刷写之前,先把当前panel面板上显示的一些进度啊,刷写结果啊之类的,给它清空到默认值;免得连续刷写时,,上一次的结果对当前的刷写结果产生视觉干扰;

cpp 复制代码
/*打印刷写失败的步骤到panel面板*/
void PrintfFlashFailStepToPanel()
{
  switch(gCurrFlashStep)
  {
    case STEP_A:
      sysSetVariableString(sysvar::EcuBootload::EcuFlashResult,"进入扩展会话失败");
      break;
    case STEP_B:
      sysSetVariableString(sysvar::EcuBootload::EcuFlashResult,"关闭DTC检测失败");
      break;
    case STEP_C:
      sysSetVariableString(sysvar::EcuBootload::EcuFlashResult,"关闭非诊断通信失败");
      break;
    case STEP_D:
      sysSetVariableString(sysvar::EcuBootload::EcuFlashResult,"预编程检查失败");
      break;
    case STEP_E:
      sysSetVariableString(sysvar::EcuBootload::EcuFlashResult,"进入编程会话失败");
      break;
    case STEP_F:
      sysSetVariableString(sysvar::EcuBootload::EcuFlashResult,"安全等级解锁失败");
      break;
    case STEP_G:
      sysSetVariableString(sysvar::EcuBootload::EcuFlashResult,"写入刷写指纹失败");
      break;
    case STEP_H:
      sysSetVariableString(sysvar::EcuBootload::EcuFlashResult,"请求下载Driver数据失败");
      break;
    case STEP_I:
      sysSetVariableString(sysvar::EcuBootload::EcuFlashResult,"传输Driver数据失败");
      break;
    case STEP_J:
      sysSetVariableString(sysvar::EcuBootload::EcuFlashResult,"退出Driver传输失败");
      break;
    case STEP_K:
      sysSetVariableString(sysvar::EcuBootload::EcuFlashResult,"Driver完整性校验失败");
      break;
    case STEP_L:
      sysSetVariableString(sysvar::EcuBootload::EcuFlashResult,"擦除APP内存失败");
      break;
    case STEP_M:
      sysSetVariableString(sysvar::EcuBootload::EcuFlashResult,"请求下载APP数据失败");
      break;
    case STEP_N:
      sysSetVariableString(sysvar::EcuBootload::EcuFlashResult,"传输APP数据失败");
      break;
    case STEP_O:
      sysSetVariableString(sysvar::EcuBootload::EcuFlashResult,"退出APP数据传输失败");
      break;
    case STEP_P:
      sysSetVariableString(sysvar::EcuBootload::EcuFlashResult,"APP完整性校验失败");
      break;
    case STEP_Q:
      sysSetVariableString(sysvar::EcuBootload::EcuFlashResult,"检查编程依赖性失败");
      break;
    case STEP_R:
      sysSetVariableString(sysvar::EcuBootload::EcuFlashResult,"ECU复位失败");
      break;
    case STEP_S:
      sysSetVariableString(sysvar::EcuBootload::EcuFlashResult,"复位后重新进入扩展会话失败");
      break;
    case STEP_T:
      sysSetVariableString(sysvar::EcuBootload::EcuFlashResult,"开启DTC检测失败");
      break;
    case STEP_U:
      sysSetVariableString(sysvar::EcuBootload::EcuFlashResult,"开启非诊断通信失败");
      break;
    case STEP_V:
      sysSetVariableString(sysvar::EcuBootload::EcuFlashResult,"进入默认会话失败");
      break;
    default:
      sysSetVariableString(sysvar::EcuBootload::EcuFlashResult,"未知刷写步骤失败");
      break;
  }
}


/* 通用流程执行引擎 */
long ExecuteFlashProcess(int steps[], int stepCount)
{
  int result;
  int i;
  double flashProgress;
  char currentStepDesc[32];
  flashProgress = 0;
  gCurrFlashStep = 0; // 重置步骤计数器
  
  sysSetVariableString(sysvar::EcuBootload::EcuFlashResult,"正在刷写");//更新panel步骤信息为正在刷写
  for(i = 0; i < stepCount; i++)
  {
    result = HandleStep(steps[i]);//0 1 -1三种结果
    flashProgress = ((double)gCurrFlashStep / (double)stepCount) * 100;//计算当前进度
    sysSetVariableInt(sysvar::EcuBootload::EcuFlashProgress,flashProgress);//更新panel上显示的当前进度
    if(result != 1)
    {
      PrintfFlashFailStepToPanel();//打印失败的步骤到panel
      return result;
    }
    gCurrFlashStep ++; // 更新全局进度
  }
  flashProgress = 100;//能走到这里来,说明需要执行的所有步骤都成功了,更新进度到100%
  sysSetVariableInt(sysvar::EcuBootload::EcuFlashProgress,flashProgress);//更新panel上显示的当前进度  
  sysSetVariableString(sysvar::EcuBootload::EcuFlashResult,"刷写成功");
  return 1;
}

/*清空panle上关于刷写结果的变量*/
void InitPanelFlashResult()
{
  sysSetVariableString(sysvar::EcuBootload::EcuFlashResult,"");
  sysSetVariableInt(sysvar::EcuBootload::ApptransmitProgress,0);
  sysSetVariableInt(sysvar::EcuBootload::EcuFlashProgress,0);
}

ok呀,到此,所有的准备工作都就绪了;

现在我们可以编写一个默认执行所有刷写步骤的函数了。

这个函数体将会非常的简单,只需要先清空panel上的结果,然后再获取加载的文件,解析文件,并执行所有的步骤就可以了;

这里我选择把获取panel上加载的刷写文件,并且解析文件的功能也做成一个函数,这样更加的简洁美观;

为了防止之前的路径有残留,最好是在每次获取文件前,先清空一下之前的路径。

同时加载文件时,最好判断一下文件路径是不是空的,如果是空的,就不让刷写了,返回错误代码。

cpp 复制代码
/*
获取panel加载的文件路径,并且负责解析文件
返回值:
1  解析成功
-1 文件未加载
-2 解析Driver失败
-3 解析App失败
*/
int GetPanelLoadFileAndParse()
{
  char driverPath[1024];//如果大家的文件路径套的非常深,数组可以再给大一点。
  char appPath[1024];
  long ret;
  
  initString(driverPath);
  initString(appPath);
  
  sysGetVariableString(sysvar::EcuBootload::DriverFilePath,driverPath,elcount(driverPath));
  sysGetVariableString(sysvar::EcuBootload::AppFilePath,appPath,elcount(appPath));
  
  if(strlen(driverPath) == 0 || strlen(appPath) == 0)
  {
    write("刷写文件未加载完毕,请加载完毕后再开始刷写");
    return -1;//文件未加载完毕;
  }
  
  ret = parseFile(driverPath,gDriverData,gDriverStartAddress,gDriverLength);
  if(ret < 0)
  {
    write("解析Driver文件失败,无法刷写,请检查文件是否合法");
    return -2;//解析driver失败
  }
  
  ret = parseFile(appPath,gAppData,gAppStartAddress,gAppLength);
  if(ret < 0)
  {
    write("解析App文件失败,无法刷写,请检查文件是否合法");
    return -3;//解析App失败
  }
  
  return 1;
}
cpp 复制代码
/*执行默认的所有刷写步骤*/
int ExecuteFlashNomal()
{
  InitPanelFlashResult();
  if(GetPanelLoadFileAndParse() != 1)
    return 0;
  return ExecuteFlashProcess(FlashStep_NomalStep,elcount(FlashStep_NomalStep));
}

这样子的写法好处是什么呢?相信细心的朋友或者爱思考的朋友很快就能够发现了;

这样子的写法,要执行的步骤全部写在一个数组里。如果哪天,需要执行故障性测试,故意减少某个步骤或者跳过某个步骤,只需要修改或者新建一个新的数组,并在数组中修改里面的元素个数和对应的元素值就可以了,然后再写一个对应的故障性刷写函数,里面只需要修改 return ExecuteFlashProcess(FlashStep_NomalStep,elcount(FlashStep_NomalStep)); 这一行代码,几乎不需要改代码。大家发现了吗?

至此,本次所有的刷写步骤编程结束了;

剩下的,就是调用这个执行所有步骤的函数进行刷写了。

哦对了,我们最好是在之前的36传输APP数据时,增加一下传输数据的进度计算逻辑;这个很简单,只要拿已经传了36块数,来除以总的需要传输的36块数,再乘百分百就可以了,非常的简单。这里我已经增加完毕了。为了防止有的朋友不会增加,我直接把代码给出。大家可以替换掉地Step14那个步骤的代码;

cpp 复制代码
/*传输App文件,成功返回1,失败返回0*/
int FlashStepN()
{
  dword hexDataTotalLen;//文件数据总长度
  int transmitTotalPackageCnt;//要传输的包数
  int remainder;//余数
  int quotient;//商
  byte packageIndex;//包的索引;
  word index;//总索引
  dword onceTxHexLen;
  byte respData[4096];
  const dword waitRespMaxTimeout = 10000;//定义最大的响应等待时间(ms)
  long respRet;
  long respByteNum;//定义存储响应数据总字节长度的变量
  int appTransmitProgress;//数据传输进度
  sysSetVariableString(sysvar::EcuBootload::EcuFlashResult,"正在传输App数据");
  onceTxHexLen = gOnceTransmitBlockSize - 2;//一包能传输的hex数据长度
  
  packageIndex = 1;//初始化索引为1
  hexDataTotalLen = gAppLength;
  remainder = hexDataTotalLen % onceTxHexLen;//取余
  quotient = hexDataTotalLen / onceTxHexLen;//取整
  
  transmitTotalPackageCnt = remainder == 0?quotient:quotient+1;//如果整除,就取商,无法整除则取商+1
  for(index = 0;index < transmitTotalPackageCnt;index++)
  {
    byte data[0xFFF];
    dword len;
    initArr(data);//清空数组
    data[0] = 0x36;
    data[1] = packageIndex;//
    if(index < transmitTotalPackageCnt - 1)//不是最后一包
    {
      memcpy_off(data,2,gAppData,index * onceTxHexLen,onceTxHexLen);//将hex文件的数据拷贝过去
      gDiagRequest.Resize(gOnceTransmitBlockSize);//设置要传输的数据长度
      gDiagRequest.SetPrimitiveData(data,gOnceTransmitBlockSize);//设置诊断数据
      gDiagRequest.SendRequest();//传输1包数据
    }
    else if(index == transmitTotalPackageCnt - 1)//最后一包
    {
      word remainingHexDataLen;//剩余的hex长度
      word remainingDataLen;//剩余所有数据长度,包含数据头36 xx
      remainingHexDataLen = hexDataTotalLen - index * onceTxHexLen;
      remainingDataLen = remainingHexDataLen + 2;
      memcpy_off(data,2,gAppData,index * onceTxHexLen,remainingHexDataLen);//将剩余的hex文件的数据拷贝过去
      gDiagRequest.Resize(remainingDataLen);//设置要传输的数据长度
      gDiagRequest.SetPrimitiveData(data,remainingDataLen);//设置诊断数据
      gDiagRequest.SendRequest();//传输1包数据
    }
    
    respRet = testWaitForDiagResponse(gDiagRequest,waitRespMaxTimeout);//
    if(respRet != 1)//未响应
    {
      return 0;
    }
    diagGetLastResponse(gDiagRequest,gDiagResponse);//将的响应数据获取了存到gDiagResponse里面。
    respByteNum = gDiagResponse.GetPrimitiveSize();//获取响应的数据长度
    gDiagResponse.GetPrimitiveData(respData,elcount(respData));//获取响应的数据
    
    if(diagGetLastResponseCode(gDiagRequest) != -1)//如果不是肯定响应  返回0,表示失败
      return 0;
    
    if(respData[1] != data[1]) return 0;//这里说明回复的Seq和我请求的Seq不是同一个  也是错误的
    appTransmitProgress = ((double)index / (double)transmitTotalPackageCnt) * 100;
    sysSetVariableInt(sysvar::EcuBootload::ApptransmitProgress,appTransmitProgress);
    packageIndex < 0xFF?packageIndex++:packageIndex = 0;//翻转Seq
    
  }
  appTransmitProgress = 100;
  sysSetVariableInt(sysvar::EcuBootload::ApptransmitProgress,appTransmitProgress);
  return 1;
}

3.25 执行刷写,进行测试;

一步一步看到这里的同学,那么恭喜你,同时也佩服你,竟然有毅力看完我写的这个文章,我自己都写的又痛苦又累了(QAQ),恭喜你,到这里,相信你已经能够基本掌握怎么使用CAPL对CAN总线的ECU进行刷写了(LIN其实也一样的流程,相似度99%,唯一的区别就是需要配置一下NAD,感兴趣的朋友可以找我交流哦,至于以太网和FlexRay总线,我目前还不太熟悉)

现在我们正式开始测试吧!

首先,我们使用的节点是CAPL Test节点,这里有个Main函数,大家还记得吗?在我们写完第一个步骤(进入编程会话)时,曾经简单的使用过它来进行测试我们的函数功能是否ok。现在,我们又要使用到他了。

cpp 复制代码
/*
CAPL测试模块的主函数,所有的函数入口,任意一个函数(事件)要执行,都要从这里开始
同时,这个只有CAPL Test Module类型的测试节点能使用哦。
*/
void MainTest()
{
  diagSetTarget("BasicDiagnosticsEcu");//设置目标诊断ECU
  ExecuteFlashNomal();//执行默认的全部刷写步骤
}

好了,现在我们把工程运行起来,看看效果吧。

ps:由于使用的诊断控制台实现,多帧传输中连续帧的传输间隔(STmin)默认使用的是诊断数据库/基础诊断文件中的配置值,如果这个值不改,多帧会传输的很慢,可能没法在我们代码设置的10S传输超时内传输完毕1包36数据哦。

这边测试的话,建议大家可以重新打开诊断配置界面,手动把STmin改小一些(主要是博主还没研究明白怎么用代码改小这个值,哈哈,如果有知道的,麻烦告诉我一下呢);

我这里把这个值改到2ms了,可以稍微发的快一点,方便我测试。

刷写运行效果

运行效果如图和视频所示呀;

正常运行时,所有的进度会随着刷写进程的进度而增加,最终显示成功;

当出现异常时,会显示失败的步骤和信息;

以上就是全部的代码和运行效果了。(注意,我是使用仿真ECU的方式运行的,如果大家使用自己的ECU,需要在某些细节指令的地方,对指令稍作修改。)

关于我的仿真ECU,我也会一同配置在这个工程里面,上传到评论区的链接处;(时间有限,仿真ECU仅会在27服务安全访问秘钥错误、31服务完整性检查时CRC错误时抛出NRC负响应),其余时候都是默认回复正响应的,如果大家想要在什么步骤回复负响应进行代码的测试,可以自行修改仿真ECU的代码逻辑。

工程默认配置为仿真模式和虚拟ECU使能模式;

如需使用真实节点,请把总线模式改为 RealBus,并选中我的虚拟节点,随后按下空格按键,将节点变为灰色的(屏蔽仿真节点)

我在这个文件夹里面放了我的demo 驱动文件和app文件,hex/s19/bin格式各一份,供大家自行研究解析代码,也供大家仿真运行我的工程时使用;

然后 KeyGenDll_GenerateKeyEx 里面放的是VS生成SeedKey.dll的工程源文件;

四、总结与展望

本篇文章,洋洋洒洒,文字讲解和代码编写,累计写了约6万4千字,十分的庞大。本人也是耗时三四天才编写完毕,说实话有点累,由于太辛苦了,所以很多地方没有优化到最好(例如所有的步骤里面诊断的逻辑其实都基本相同,完全可以封装成函数减少代码量,例如刷写的UI界面全部是黑色的,不够美观,例如代码的讲解不足等)。

另外,由于本文章和代码,均由本人独自完成,由于作者水平有限,可能在某些地方存在描述不符,讲解出错,代码逻辑错误的地方,希望大家发现后能够即时指出,谢谢!

最后,希望大家看完我的这篇文章,能够对你们有所帮助,希望以此,让我们的汽车电子行业的爱好者,学习者,测试工程师们,软件开发工程师们都能有所收获,愿我国的汽车工业日益进步。如有想法,欢迎评论区或者私信交流,但我可能无法即时回复或者查看哦。

相关推荐
ulias2122 小时前
进程初识(1)
linux·运维·服务器·网络·c++
Shingmc32 小时前
【Linux】Socket编程UDP
网络·udp
Shingmc33 小时前
【Linux】网络基础概念
linux·服务器·网络
思麟呀3 小时前
数据链路层和物理层
网络·网络协议·http·智能路由器
春蕾夏荷_7282977253 小时前
libhv vs2019 udp简单的实例
网络·udp·libhv·结构体
阳光普照世界和平4 小时前
2026软件安全趋势解析:攻防迭代下,企业该如何破局?
网络·安全
被摘下的星星4 小时前
计算机网络的拓扑结构
网络·计算机网络
positive_zpc4 小时前
计算机网络——数据链路层(一)
网络·计算机网络
Chengbei115 小时前
2026护网HVV面试看这篇就够了!真题+技巧+培训福利一站式get
网络·安全·web安全·网络安全·面试·职场和发展·安全架构