ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

汽车ECU BootLoader升级实战:PBL/SBL分层与UDS刷写服务链详解

汽车ECU BootLoader升级实战:PBL/SBL分层与UDS刷写服务链详解

1. 从一次刷写失败说起:为什么BootLoader值得单独拎出来讲

前阵子帮一个朋友排查问题,他手里有个ECU样件,用诊断仪走UDS刷写,进度条卡在31%死活不动,换了两台设备、三根线束、四个版本的刷写脚本,结果一模一样。他一度怀疑是硬件坏了,差点把板子拆了重焊。最后定位下来,问题出在SBL(Secondary Boot Loader)的下载地址和Flash驱动里的擦除区间对不上——SBL被写进了Application的保留区,一擦就把自己擦没了。

这个坑其实非常典型。很多人做ECU升级,注意力全放在"UDS服务怎么发、34/36/37怎么拼"上,却忽略了BootLoader本身的分层逻辑和内存布局。PBL(Primary Boot Loader)和SBL(Secondary Boot Loader)的分工,才是整个刷写流程能不能跑通的地基。地基没打牢,上层服务调得再花哨也是白搭。

这篇内容我打算把汽车ECU BootLoader升级这件事从头到尾捋一遍。从PBL/SBL的分层设计动机,到UDS 0x34/0x36/0x37/0x31/0x11这套刷写服务链的完整时序,再到实际工程里那些文档不会写、但踩一次就记一辈子的细节。适合刚接触ECU诊断刷写的嵌入式工程师,也适合做了几年应用层、想往底层刷写方向补课的同行。读完你至少能搞清楚三件事:为什么要有两级BootLoader、UDS刷写的每个服务到底在干什么、以及刷写失败时该从哪几个维度去排查。

我尽量不堆术语,能用生活类比的地方就用类比。但该严谨的地方不会含糊,毕竟刷写这事儿,一个字节写错位置,轻则刷不进去,重则把ECU刷成砖。

2. PBL与SBL的分工:不是多此一举,是保命设计

2.1 为什么不能只有一个BootLoader

先想一个问题:如果ECU里只有一个BootLoader,它负责接收新固件、擦写Flash、跳转到Application,看起来挺简洁,为什么要拆成PBL和SBL两级?

核心原因有两个:一是BootLoader自身不能被自己擦除,二是不同项目/不同车型的刷写协议和Flash驱动需要灵活替换。

打个比方。PBL就像小区大门的门禁系统,它只认一把最基础的钥匙,功能极简,常年不变,谁也别想动它。SBL像是单元楼门口的临时管家,今天这个项目用A管家(支持CAN刷写),明天那个项目换B管家(支持CAN FD或者以太网刷写),管家可以换,但门禁系统不动。Application就是你家,管家把你领进门之后就没他什么事了。

具体到技术层面,PBL通常固化在芯片的受保护扇区里,通过选项字节或者读保护机制锁死,正常刷写流程根本碰不到它。它的职责非常窄:上电后判断是进刷写模式还是跳转Application,如果进刷写模式,就负责把SBL下载到RAM或者指定的Flash区域,然后跳过去执行。SBL才是真正干活的——它包含完整的Flash驱动、UDS服务处理、校验算法,负责把Application固件写进目标地址。

这样设计的好处很直接:SBL可以随项目迭代,刷写协议升级、Flash型号更换、校验算法调整,都只改SBL,PBL纹丝不动。而PBL因为足够简单,出bug的概率极低,保证了"即使SBL刷坏了,PBL还在,还能重新把SBL刷回来"这条底线。

2.2 PBL的启动逻辑与跳转条件

PBL上电后的行为,不同芯片厂商的实现差异挺大,但大逻辑是相通的。我把它拆成几个关键判断点。

第一,检查刷写请求标志。这个标志可能来自Flash里的一个特定区域(比如某个保留扇区的一个magic number),也可能来自RAM里的一个变量(但RAM掉电就没了,所以通常配合Flash标志一起用)。诊断仪在请求刷写时,会先发一个"进入编程会话"的请求,PBL或者当前运行的Application收到后,往指定地址写一个标志,然后复位。复位后PBL读到这个标志,就知道该进刷写模式了。

第二,检查Application有效性。如果没收到刷写请求,PBL会去校验Application区域。校验方式通常是CRC或者哈希,校验范围包括Application的代码区和它自己的校验字段。校验通过就跳转,不通过就停在BootLoader里等待刷写。这一步很关键,它保证了Application被刷坏或者刷了一半断电的情况下,ECU不会跳到一个残缺的程序里跑飞。

第三,跳转前的环境准备。跳转前要关中断、复位外设、设置堆栈指针、把向量表偏移寄存器指向Application的向量表地址。这几步任何一步漏了,跳过去大概率HardFault。我见过一个案例,跳转后Application跑不起来,查了半天发现是中断没关干净,一个定时器中断在跳转瞬间触发了,直接跳到Application还没初始化的中断向量里去了。

提示:PBL的代码量要尽量小,能不用库就不用库,能不用中断就不用中断。它的存在意义是"稳",不是"功能多"。

2.3 SBL的下载与执行:RAM跑还是Flash跑

SBL下载到哪里执行,这是设计时要做的一个关键取舍。常见两种方案:下载到RAM执行,或者下载到Flash的临时区域执行。

下载到RAM执行的好处是速度快,RAM读写没有Flash擦写那么耗时,而且SBL本身不占用Flash空间。但缺点是RAM容量有限,SBL如果功能复杂(带完整UDS协议栈、多种Flash驱动、加密校验),可能塞不下。另外RAM执行对链接脚本和启动代码有额外要求,SBL得编译成位置无关或者指定RAM地址的镜像。

下载到Flash临时区域执行的好处是空间充裕,SBL可以做得功能完整。缺点是下载SBL本身就要擦写Flash,耗时更长,而且这块临时区域的管理要小心,别和Application区域重叠。

实际项目里,如果SBL比较轻量(只做UDS解析和Flash驱动),RAM方案很常见;如果SBL要支持多种刷写协议、带安全访问和签名校验,Flash方案更稳妥。选哪个没有绝对优劣,看芯片资源和项目需求。

这里有个容易忽略的点:SBL下载完成后,PBL跳转到SBL之前,要确保SBL的入口地址、堆栈指针、向量表都正确设置。如果SBL是下载到RAM的,还要确保RAM区域没有被其他代码占用。我踩过一次坑,SBL下载到RAM后跳转过去直接跑飞,后来发现是PBL自己用的堆栈和SBL的堆栈区域重叠了,SBL一压栈就把PBL的数据覆盖了。

3. UDS刷写服务链:34/36/37/31/11到底在干什么

3.1 刷写前的会话切换与安全访问

UDS刷写不是上来就发0x34的,前面有一串准备工作。整个流程走下来,服务调用顺序大致是这样的:

步骤服务作用
10x10 0x03进入扩展会话
20x85 0x02关闭DTC故障码记录
30x28 0x03 0x01关闭非诊断报文的收发
40x10 0x02进入编程会话
50x27 0x01/0x02安全访问(请求种子/发送密钥)
60x34请求下载
70x36传输数据
80x37请求退出传输
90x31 0x01例程控制(校验)
100x11 0x01ECU复位

这个顺序不是随便定的。0x85关DTC是因为刷写过程中Flash被擦写,各种故障码会疯狂上报,不关掉的话诊断仪会被淹没。0x28关非诊断报文是因为刷写时总线负载要尽量低,保证刷写数据不被干扰。这两步在很多刷写脚本里被省略,短时间可能没事,但一旦总线负载高或者刷写时间长,就容易出问题。

安全访问0x27是绕不过去的。ECU会返回一个随机种子,诊断仪用预置的算法算出密钥发回去,算法对了才解锁刷写权限。这个算法的具体实现每个项目不同,有的用AES,有的用自定义的异或移位组合。做刷写工具的话,这个算法通常由ECU供应商提供,或者从已有的诊断数据库里提取。

注意:安全访问的种子和密钥是有时效的,解锁后如果长时间不发刷写请求,会话可能超时,需要重新解锁。刷写脚本里最好在解锁后尽快发0x34。

3.2 0x34请求下载:地址和长度的博弈

0x34服务是刷写的起点,它的请求格式是:34 + dataFormatIdentifier + addressAndLengthFormatIdentifier + memoryAddress + memorySize。

dataFormatIdentifier通常填0x00,表示不压缩不加密。如果固件是压缩的,这里要填对应的压缩算法标识,ECU收到后会先解压再写入。

addressAndLengthFormatIdentifier这个字节决定了后面地址和长度各占几个字节。比如填0x44,表示地址4字节、长度4字节;填0x24,表示地址2字节、长度4字节。这个字段填错,后面解析全乱。

memoryAddress是要写入的目标地址,memorySize是要写入的字节数。这两个参数必须和SBL里的Flash驱动配置完全一致。我开头提到的那个刷写卡31%的案例,就是memoryAddress指向了SBL自己的区域,SBL收到0x34后开始擦除,把自己擦没了,后续0x36自然传不进去。

ECU收到0x34后,会检查地址是否合法、长度是否超出范围、当前会话和安全等级是否满足。都通过的话返回0x74,正响应里会带上maxNumberOfBlockLength,告诉诊断仪每个0x36最多能带多少字节数据。这个值很关键,诊断仪要按它来分包,超了ECU会拒绝。

3.3 0x36传输数据:分包与流控

0x36是真正传固件数据的服务。请求格式是:36 + blockSequenceCounter + data。

blockSequenceCounter从0x01开始,每发一包加1,加到0xFF后回绕到0x00。ECU会检查这个计数器,如果发现跳号或者重复,会返回0x73(wrongBlockSequenceCounter),诊断仪需要重发上一包。

数据长度不能超过0x34响应里的maxNumberOfBlockLength。实际工程里,这个值受限于诊断报文的最大长度。经典CAN一帧8字节,ISO-TP多帧传输后单次诊断消息最多4095字节;CAN FD一帧最多64字节,单次诊断消息可以更长。但ECU的接收缓冲区有限,maxNumberOfBlockLength通常不会给到理论最大值,常见的是512字节或者1024字节。

传输过程中最怕的是丢包和超时。ISO-TP有流控帧机制,接收方通过流控帧告诉发送方"继续发""等等""缓冲区满了"。如果流控帧丢了,发送方会等N_Bs超时,然后重发或者报错。刷写时如果总线干扰大,建议降低传输速率或者增加重试次数。

我个人的经验是,0x36的每包数据之间最好留一点间隔,不要背靠背猛发。有些ECU的Flash写入速度跟不上接收速度,缓冲区满了之后如果流控没处理好,就会丢数据。间隔多少合适?看ECU的Flash写入时间,一般每包之间留1到5毫秒比较稳妥。

3.4 0x37退出传输与0x31校验

所有数据传完后,发0x37告诉ECU传输结束。ECU收到后会做几件事:检查收到的总字节数和0x34里声明的memorySize是否一致、检查blockSequenceCounter是否连续、把Flash里写入的数据做一次完整性校验。

0x37的正响应是0x77,后面可能带一个参数表示还需要多少字节或者校验结果。如果字节数对不上,ECU会返回0x71(transferDataSuspended)或者直接负响应。

0x37之后是0x31例程控制,常用的例程有:

  • 0x31 0x01 0xFF 0x00:检查编程完整性,ECU会计算Flash里固件的CRC或者哈希,和预期值比对。
  • 0x31 0x01 0xFF 0x01:检查编程依赖性,比如Application和标定数据的版本是否匹配。
  • 0x31 0x01 0x02 0x02:擦除内存,有些项目把擦除单独做成一个例程。

校验通过后,发0x11 0x01复位ECU。复位后PBL重新跑,这次因为刷写标志已经被清除(或者Application校验通过),PBL会跳转到新的Application。

提示:0x11复位后不要立刻发诊断请求,ECU需要时间做Flash初始化和Application启动。一般等500毫秒到2秒再发,具体看ECU的启动时间。

4. 刷写失败的排查链路:从现象反推根因

4.1 卡在0x34之前:会话和安全访问的问题

刷写卡在0x34之前,现象通常是诊断仪发了0x34但ECU回负响应,或者干脆没响应。这时候按下面的顺序排查。

先看会话有没有进对。0x10 0x02进编程会话,ECU可能要求先满足某些条件才允许进入,比如车速为零、发动机未运行。有些ECU在编程会话下会关闭CAN收发,如果诊断仪没处理好,后续请求就发不进去了。

再看安全访问有没有过。0x27的种子密钥算法如果不对,ECU会返回0x35(invalidKey)。这时候要确认算法实现和ECU里的是否一致。有个容易忽略的点:种子是有时效的,如果诊断仪算密钥花的时间太长,种子过期了,也会返回0x35。

然后看0x34的参数。addressAndLengthFormatIdentifier填错、memoryAddress超出允许范围、memorySize为0或者过大,都会导致负响应。ECU返回的负响应码里,0x31(requestOutOfRange)通常表示地址或长度有问题,0x33(securityAccessDenied)表示安全等级不够,0x22(conditionsNotCorrect)表示当前条件不满足。

4.2 卡在0x36传输中:分包、流控与Flash写入

0x36传输中卡住,是最常见的刷写故障。现象是进度条走到某个百分比不动了,诊断仪等超时,或者ECU返回0x73。

第一个要查的是blockSequenceCounter。如果诊断仪的分包逻辑有bug,计数器跳号了,ECU会拒绝后续所有包。这时候看诊断仪的日志,确认每包的计数器是不是从0x01开始连续递增。

第二个要查的是maxNumberOfBlockLength。如果诊断仪每包发的数据超过了ECU声明的最大值,ECU会返回0x13(incorrectMessageLengthOrInvalidFormat)。有些诊断仪默认按自己的最大值发,不读0x34响应里的值,就会踩这个坑。

第三个要查的是流控和超时。ISO-TP的流控帧如果丢了,发送方会等N_Bs超时。如果ECU的接收缓冲区满了,会发流控帧让发送方等(FC.WAIT),如果发送方不支持等待或者等待超时,就会中断。刷写时建议打开ISO-TP的日志,看流控帧的交互是否正常。

第四个要查的是Flash写入。如果ECU的Flash驱动有bug,比如擦除没擦干净、写入地址算错、写入时序不对,数据写进去读出来不一致,0x37之后的校验就会失败。这种问题在0x36阶段可能不报错,但0x31校验时暴露出来。

我遇到过一次,0x36传到80%左右必卡。查了半天发现是ECU的Flash驱动在擦除某个扇区时,擦除时间超过了ISO-TP的超时时间,ECU还在擦,诊断仪已经等超时了。解决办法是把那个扇区的擦除拆成多次,或者把ISO-TP的超时时间调大。

4.3 卡在0x37之后:校验失败与复位异常

0x37之后卡住,通常是0x31校验失败或者0x11复位后ECU没起来。

校验失败的原因:数据在传输过程中被篡改或丢失、Flash写入有误、校验算法实现不一致、校验范围不对。排查时先把Flash里的数据读回来,和原始固件逐字节比对,看是从哪个地址开始不一致的。如果是从某个扇区边界开始不一致,大概率是那个扇区的擦写有问题。

复位后ECU没起来的原因:Application的向量表地址不对、堆栈指针没设置、时钟配置没初始化、看门狗没处理好。这时候ECU可能反复复位,或者停在PBL里不跳转。用调试器连上去看PC指针停在哪里,基本就能定位。

还有一种情况是复位后ECU起来了,但诊断仪连不上。这可能是Application里的诊断配置和BootLoader里的不一致,比如CAN ID变了、波特率变了。刷写前要确认新固件的诊断配置和刷写工具匹配。

4.4 那些文档不会写的实操细节

第一,刷写前先读一遍当前固件的版本和校验值。这样刷写失败后可以对比,确认是刷写过程出了问题还是固件本身有问题。

第二,刷写时给ECU供稳定的电。刷写过程中断电,ECU大概率变砖。如果PBL没被破坏,还能重新刷SBL救回来;如果PBL也被破坏了,就只能拆芯片用编程器写了。所以刷写电源要稳,最好用稳压电源,不要用USB口供电。

第三,刷写脚本里加日志。每一包0x36的发送时间、ECU的响应、流控帧的交互,都记下来。出问题的时候,日志是唯一的线索。

第四,刷写完成后不要立刻断电。等ECU复位完成、Application正常启动、诊断能正常响应之后,再断电。有些ECU复位后需要写一些非易失数据,断电太早会丢。

第五,不同批次的ECU可能Flash型号不同。同一款ECU,不同批次用的Flash芯片可能不一样,擦除时间、扇区大小、写入时序都不同。SBL里的Flash驱动要能兼容这些差异,或者刷写前先读Flash ID做适配。

5. 从零搭建一个可用的刷写环境:工具链与配置要点

5.1 硬件选型:诊断接口与电源

刷写环境的硬件核心是诊断接口。经典CAN用CAN卡,比如常见的USB-CAN分析仪;CAN FD需要支持CAN FD的接口卡;以太网刷写(DoIP)需要支持DoIP的接口。选型时注意接口卡的驱动是否稳定、是否支持ISO-TP、是否支持CAN FD的数据段波特率切换。

电源方面,ECU刷写时电流可能比正常运行时大,尤其是Flash擦写瞬间。稳压电源的电流余量要留够,一般建议至少2A以上。电压要稳定在ECU的工作电压范围内,不要用那种电压漂移大的电源。

线束也很关键。CAN_H和CAN_L要双绞,终端电阻要匹配。刷写时线束太长或者没有终端电阻,信号反射会导致通信错误。我见过一个案例,刷写台架上线束拉了3米多,没加终端电阻,刷写成功率只有一半,加了120欧终端电阻后一次过。

5.2 软件工具:诊断仪与刷写脚本

诊断仪可以用商业的,也可以用开源的。商业诊断仪功能全但贵,开源方案灵活但需要自己搭。不管用哪种,核心能力是:支持UDS服务、支持ISO-TP、支持CAN FD(如果需要)、支持脚本化刷写流程。

刷写脚本通常用Python或者CAPL写。Python的话,用python-can做CAN通信,用udsoncan做UDS协议栈,自己实现ISO-TP或者用can-isotp库。CAPL的话,Vector的工具链比较成熟,但需要License。

脚本里要实现的逻辑包括:会话切换、安全访问、0x34/0x36/0x37的收发、0x31校验、0x11复位、错误处理和重试。每个服务的请求和响应都要做超时处理,超时后根据情况重试或者报错。

5.3 固件文件的准备:格式与校验

固件文件通常是S19或者HEX格式,里面包含地址和数据。刷写前要把固件解析成地址-数据对的列表,然后按0x34的memoryAddress和memorySize分段发送。

解析S19/HEX的时候要注意:地址可能不连续,中间有空洞;数据长度可能不是0x36最大长度的整数倍,最后一包要截断;校验和要验证,确保固件文件本身没损坏。

如果固件是加密或者压缩的,要先解密解压,或者把加密压缩标识填到0x34的dataFormatIdentifier里让ECU处理。加密固件的刷写,安全访问的等级可能更高,需要额外的密钥。

5.4 刷写流程的自动化与回归测试

刷写脚本写好后,要做回归测试。测试内容包括:正常刷写流程、中途断电恢复、错误固件刷写、重复刷写、不同批次的ECU刷写。

自动化测试可以用CI工具跑,每次固件更新后自动刷写并校验。测试时要记录每次刷写的耗时、成功率、失败原因。如果某个环节的成功率低于预期,就要重点排查。

回归测试里有个容易忽略的点:刷写后的ECU要能正常通信和运行。有些刷写脚本只验证0x31校验通过,不验证Application是否正常启动。结果刷进去的固件虽然校验通过,但Application有bug跑不起来。所以刷写后要发几个常规诊断请求,确认ECU能正常响应。

6. 几个容易翻车的边界情况

6.1 刷写过程中断电了怎么办

断电是刷写最怕的情况。如果断电发生在0x36传输过程中,Flash里是半擦半写的状态,Application肯定不完整。这时候复位,PBL会校验Application失败,停在BootLoader里等待重新刷写。只要PBL没被破坏,重新走一遍刷写流程就能救回来。

如果断电发生在SBL下载过程中,SBL可能只下载了一半。这时候PBL跳转到SBL会跑飞,但因为PBL还在,重新上电后PBL会检测到SBL不完整(如果PBL有SBL校验逻辑),重新进入SBL下载模式。如果PBL没有SBL校验逻辑,那就需要特殊手段让PBL强制进入下载模式,比如拉高某个引脚或者发特定的CAN帧。

如果断电发生在PBL擦写过程中(正常刷写流程不会擦PBL,但如果是用编程器刷或者特殊模式刷),PBL被破坏了,ECU就真砖了,只能拆芯片用编程器写。

注意:刷写前确认PBL的保护区设置是否正确。PBL所在的Flash扇区应该通过选项字节或者读保护锁死,防止被意外擦除。

6.2 不同Flash型号的兼容性

同一款ECU,不同批次可能用不同厂商的Flash。不同Flash的扇区大小、擦除时间、写入时序、状态寄存器定义都可能不同。SBL里的Flash驱动要能识别Flash型号并适配。

识别Flash型号的方法通常是读Flash的ID寄存器。SBL启动后先读ID,然后查表找到对应的驱动配置。如果读不到ID或者ID不在支持列表里,SBL应该报错并停在BootLoader里,而不是盲目擦写。

我见过一个项目,SBL只支持一种Flash,结果换批次后Flash型号变了,SBL擦除时用了错误的扇区大小,把相邻扇区的数据也擦了,导致刷写后Application跑飞。后来SBL加了Flash ID识别和驱动适配才解决。

6.3 安全访问算法的版本管理

安全访问算法不是一成不变的。不同项目、不同车型、不同年份的ECU,算法可能不同。刷写工具要能根据ECU返回的种子或者ECU的版本信息,选择对应的算法。

算法版本管理混乱是刷写工具开发的一个大坑。如果工具用错了算法版本,安全访问过不了,刷写根本进行不下去。建议把算法做成插件式,每个项目一个算法模块,工具根据配置加载对应的模块。

6.4 刷写对总线负载的影响

刷写时0x36数据包会占用大量总线带宽。如果总线上还有其他节点在通信,刷写数据可能被干扰或者延迟。刷写前最好把其他节点断开,或者让它们进入静默模式。

如果总线上有多个ECU需要刷写,不要同时刷,一个一个来。同时刷的话总线负载太高,流控和超时问题会成倍增加。

7. 我个人在实际刷写中攒下的几条经验

刷写这事儿,理论归理论,实际做的时候总会有各种意想不到的情况。我把自己踩过的坑和攒下的经验列几条,都是文档里不会写的。

第一条,永远先读一遍再刷。刷写前把当前固件的版本、校验值、Flash ID读出来存好。刷写失败的时候,这些信息能帮你快速判断是刷写过程的问题还是固件本身的问题。

第二条,刷写脚本的错误处理要比正常流程还细致。正常流程谁都能写,但刷写失败时的错误处理才是体现功力的地方。每个服务的负响应码都要有对应的处理逻辑,超时要重试,重试次数要有限制,重试失败要报错并保留现场。

第三条,刷写台架要固定。线束、电源、接口卡、电脑,都固定下来,不要每次刷写都换。变量越多,出问题越难排查。台架搭好后先跑几十次正常刷写,确认稳定性。

第四条,固件文件要版本管理。哪个版本的固件刷到哪个ECU上,要有记录。刷写工具要能根据ECU的版本信息自动选择对应的固件,避免刷错版本。

第五条,刷写时间要有预期。一个1MB的固件,经典CAN 500kbps,理论传输时间大概20秒,加上擦除、校验、复位,实际可能要1到2分钟。如果刷写时间远超预期,说明某个环节有问题,要查。

第六条,不要迷信商业诊断仪。商业诊断仪功能全,但刷写逻辑是固定的,遇到特殊项目可能不适用。自己写脚本虽然麻烦,但灵活,能针对项目做定制。我现在的习惯是,商业诊断仪用来做参考和验证,实际刷写用自己写的脚本。

第七条,刷写后的ECU要跑一段时间再交付。刷写后立刻能通信不代表没问题,有些问题要跑一段时间才暴露,比如Flash写入不牢导致的数据缓慢丢失、看门狗配置不当导致的偶发复位。刷写后至少让ECU跑几个小时,确认稳定再交付。

这些经验说起来简单,但每一条背后都是真金白银的教训。刷写这事儿,稳比快重要,一次刷成功比刷十次快但有一次失败要划算得多。

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进