
1. 从一次刷写失败说起为什么BootLoader值得单独拎出来讲前阵子帮一个朋友排查问题他手里有个ECU样件用诊断仪走UDS刷写进度条卡在31%死活不动换了两台设备、三根线束、四个版本的刷写脚本结果一模一样。他一度怀疑是硬件坏了差点把板子拆了重焊。最后定位下来问题出在SBLSecondary Boot Loader的下载地址和Flash驱动里的擦除区间对不上——SBL被写进了Application的保留区一擦就把自己擦没了。这个坑其实非常典型。很多人做ECU升级注意力全放在UDS服务怎么发、34/36/37怎么拼上却忽略了BootLoader本身的分层逻辑和内存布局。PBLPrimary Boot Loader和SBLSecondary 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型号更换、校验算法调整都只改SBLPBL纹丝不动。而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会检查这个计数器如果发现跳号或者重复会返回0x73wrongBlockSequenceCounter诊断仪需要重发上一包。数据长度不能超过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会返回0x71transferDataSuspended或者直接负响应。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会返回0x35invalidKey。这时候要确认算法实现和ECU里的是否一致。有个容易忽略的点种子是有时效的如果诊断仪算密钥花的时间太长种子过期了也会返回0x35。然后看0x34的参数。addressAndLengthFormatIdentifier填错、memoryAddress超出允许范围、memorySize为0或者过大都会导致负响应。ECU返回的负响应码里0x31requestOutOfRange通常表示地址或长度有问题0x33securityAccessDenied表示安全等级不够0x22conditionsNotCorrect表示当前条件不满足。4.2 卡在0x36传输中分包、流控与Flash写入0x36传输中卡住是最常见的刷写故障。现象是进度条走到某个百分比不动了诊断仪等超时或者ECU返回0x73。第一个要查的是blockSequenceCounter。如果诊断仪的分包逻辑有bug计数器跳号了ECU会拒绝后续所有包。这时候看诊断仪的日志确认每包的计数器是不是从0x01开始连续递增。第二个要查的是maxNumberOfBlockLength。如果诊断仪每包发的数据超过了ECU声明的最大值ECU会返回0x13incorrectMessageLengthOrInvalidFormat。有些诊断仪默认按自己的最大值发不读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跑几个小时确认稳定再交付。这些经验说起来简单但每一条背后都是真金白银的教训。刷写这事儿稳比快重要一次刷成功比刷十次快但有一次失败要划算得多。