基于UDS的CAN本地OTA升级:Bootloader固件刷写实战解析

发布时间:2026/9/16 8:19:08
基于UDS的CAN本地OTA升级:Bootloader固件刷写实战解析
做汽车电子或者嵌入式开发的朋友应该都有过这种经历产品功能调通了代码也编译过了结果要升级固件的时候要么拿着烧录器一台台开壳要么让产线工人用J-Link排队怼效率低不说还容易把排针搞坏。我刚入行那会儿也觉得这事就该这样直到后来把基于UDS诊断协议的CAN本地OTA升级这套流程完整跑通才意识到之前那些折腾都是没必要的弯路。这个项目说白了就是用ISO 14229定义的UDS统一诊断服务协议通过车上的CAN总线直接给ECU刷写新固件不需要拆设备、不需要专用烧录器一台CAN卡加一个上位机软件就能搞定。它适用于芯片Bootloader开发、应用层固件更新、产线刷写、售后维修等场景也是远程OTA落地之前必须打通的地基。如果你正在做STM32、S32K、瑞萨这类MCU的Bootloader或者想把手动烧录升级成规范的诊断刷写流程这篇文章值得你花几分钟看完。1. 整体设计思路与协议栈拆解1.1 为什么选UDS而不是直接写Flash很多人刚接触OTA时会有个疑问既然就是要往Flash里写数据那我直接在Bootloader里搞一个自定义协议不就行了比如定义0xAA开头代表擦除、0xBB代表写数据。这么做在项目初期看起来省事但后面扩展到产线、售后、不同供应商ECU联调时就会很痛苦。车载行业有大量现成的诊断工具和测试规范都基于UDS协议你自研的协议工具链支持不了测试团队也没法下手最后还得回过头来兼容标准。UDS的好处在于它是一套完整的状态机和管理框架不只是“写Flash”这么简单。它定义了会话管理先切到编程会话才能刷写、安全访问先解锁才能写入、例程控制负责擦除和CRC校验、数据传输服务负责分段下发固件。每一步都有明确的请求/响应格式出错时还有标准的NRC否定响应码告诉你是哪个环节出了问题。这套机制不是我拍脑袋发明的而是ISO 14229和ISO 15765-2这两份规范沉淀了几十年的产物。所以我的建议很直接除非你的项目极其简单且完全没有外部工具交互需求否则走UDS标准协议是当下最稳、最不容易返工的选择。1.2 本地OTA的协议栈构成CAN本地OTA的核心分为三层每个层次都有自己的职责。最上面是应用层的UDS服务负责协商“要做什么”比如切换会话、解锁、请求下载、传数据、复位中间是传输层也就是ISO 15765-2通常简称CAN TP负责把大块数据拆分到CAN帧里或者把多个CAN帧拼成一个完整的UDS消息最底层就是CAN控制器和收发器负责在总线上实际发送接收报文。我用一个生活化的例子来解释UDS服务就像快递公司客服你告诉它“我要寄一个100MB的文件”它不会直接拿着100MB的文件去邮局而是把文件交给分包员分包员按邮局的纸箱规格把文件切成一个个小包裹贴上编号再交给邮递员骑电动车送出去。这里的“分包员”就是CAN TP而“邮递员”就是CAN控制器。经典CAN一帧数据场最多8字节CAN FD可以到64字节。而一条UDS消息动不动就是几十上百字节比如34服务的请求下载光地址和长度就要占掉好几个字节更别提36服务传输数据时要同时带块序号和数据。所以CAN TP是这个方案里绝对不能省的一层。如果跳过它直接操作CAN帧你会被各种分帧、重组、流控、超时重传问题折磨死。1.3 本地升级与远程OTA的关系有些做应用层的朋友可能会问现在不是流行远程OTA吗通过4G或者以太网把固件包下载到车机再通过网关刷到各个ECU。本地OTA还有没有价值我的看法是本地OTA是远程OTA的“子集”和“演习”。远程OTA的最后一公里绝大多数还是通过CAN把固件从网关或T-Box转发到目标ECU走的依然是UDS诊断服务。只不过在本地模式下“上位机CAN卡”替代了“云平台T-Box”数据来源是本地文件而不是网络下载。换句话说你先把本地刷写流程调稳后续做远程OTA时Bootloader侧代码几乎不用大改只需要把数据源换成网络通道就行。这也是为什么很多OEM和Tier1在SOP之前首先强制要求的就是把本地UDS刷写打通因为它是所有升级方案的共同底座绕不开也省不掉。2. Bootloader侧程序设计与分区规划2.1 Flash分区策略不能拍脑袋定在做Bootloader之前先花时间把芯片的Flash地址空间规划好。这一步如果偷懒后面调跳转、调擦除时会非常痛苦。以常见的STM32为例我一般建议把Flash分成至少四大块Boot区、APP区、备份区可选、标志位区。Boot区存放Bootloader代码它永远是上电后第一个执行的APP区存放用户应用程序备份区用来存放升级前的旧固件或者升级过程中的临时固件标志位区则记录当前APP是否有效、是否需要进入刷写模式、固件版本号等信息。地址分配不是随意定的要考虑Bootloader本身的大小、APP的起始地址对齐、每个扇区/页的大小。比如某颗芯片的Flash扇区是16KB对齐那你APP起始地址就不能选在0x08007C00这种非对齐位置否则擦除和写入时会把Bootloader或标志位一起抹掉。下面是我在一个项目中用过的分区方案供参考分区名地址范围大小用途Bootloader区0x08000000 - 0x08007FFF32KB启动代码、UDS刷写逻辑参数/标志区0x08008000 - 0x08008FFF4KB有效标志、版本号、刷写状态APP区0x08009000 - 0x0803FFFF220KB应用程序代码备份区0x08040000 - 0x0807FFFF256KB升级前备份或临时固件注意这个方案里APP区地址不是从0x08008000开始的因为我留了一小块独立区域给参数和标志位。更新标志位时单独擦写这个小块即可不至于为了写一个字节把整片APP擦掉。2.2 Flash驱动在RAM中运行的原因写Bootloader时有个容易踩的坑你在调用Flash擦除或写入函数时如果这段代码本身就在Flash里而你要擦除的扇区恰好包含这段代码所在的扇区就会导致“自己擦自己”的经典故障。轻则擦除函数执行到一半飞掉重则芯片直接进HardFault。解决思路是把Flash底层驱动复制到RAM中执行从RAM里调用擦写函数。具体做法是在链接脚本里定义一个可执行RAM段把flash_erase、flash_write这类函数放到这个段中并在启动阶段或者每次刷写前用memcpy把它从Flash搬运到RAM。注意RAM里的代码执行时地址要正确重定位函数指针也要指向RAM地址。STM32的IAP例程里通常有类似__RAM_FUNC的宏定义S32K的IDE里也可以通过section attribute来实现。这个细节看起来不起眼但实际调试时如果发现“擦一次就死机”九成是这个问题。2.3 跳转APP前的关键动作刷写完成后Bootloader要跳转到APP并让APP正常跑起来这一步看似简单实际很容易翻车。首先是中断向量表的位置在APP启动代码里必须把SCB-VTOR寄存器设置到APP区的起始地址否则APP里任何一个中断触发都会跳到Bootloader的向量表程序直接跑飞。其次是关闭全局中断和外设时钟。跳转前要确保所有中断都关掉至少要把SysTick、外设中断、CAN接收中断等全部禁用并把用到的外设时钟复位到默认状态避免APP初始化时读到残留配置。最后把主栈指针MSP设置为APP起始地址处存放的初始栈顶值再通过函数指针跳转到复位向量执行APP的Reset_Handler。我习惯在跳转前把R0、R1、R2清零并开启看门狗前先评估好跳转后APP最早喂狗的位置防止跳转瞬间看门狗复位。2.4 看门狗与升级包的校验策略刷写过程中如果开了看门狗而擦写Flash又比较耗时极易导致看门狗超时复位。我的做法是在进入刷写模式后把看门狗配置成“可暂停”状态或者在每次擦写循环里及时喂狗。S32K的WDOG窗口模式还要求喂狗时间不能太早也不能太晚所以刷写循环的节奏需要仔细计算。固件包校验方面强烈建议在APP有效标志区放一个CRC值每次上电Bootloader读取并验证APP区的CRC。如果CRC不对就自动进入刷写模式等待升级否则正常跳转APP。端到端的固件完整性校验通常用CRC32或CRC64也可以为每个数据块额外计算一个CRC与块序号一起发送让ECU实时校验收到的数据是否有误。CRC多项式、初值、输入输出反转这些参数上位机和ECU必须完全一致否则烧完就变砖。3. 刷写流程逐条拆解从会话切换到设备复位3.1 进入编程会话10服务整个刷写流程第一步是让ECU进入编程会话。UDS的10服务DiagnosticSessionControl带子功能0x02表示reprogramming会话0x01是默认会话0x03是扩展会话。进入编程会话后ECU会禁用部分功能禁止应用层某些操作为接下来的解锁和Flash操作做准备。这里有一个容易忽略的点10 02的肯定响应里会带上P2ServerMax和P2*ServerMax两个时间参数分别表示服务器处理请求的最长时间和增强超时时间单位是毫秒。上位机要根据这两个值合理设置响应超时时间不能一概用默认的500ms。如果把P2超时设得太短ECU擦除Flash耗时较长还没回复上位机就已经判定超时了。另外很多ECU在上电后自动进入Bootloader等待刷写而有些ECU则需要先运行APP再通过诊断指令请求进入Bootloader。这两种方式对应不同的应用场景前者适合产线刷写后者适合售后OTA。无论哪种方式10 02服务都是必由之路。3.2 安全访问与密钥算法27服务进入编程会话后下一步通常是安全访问。27服务SecurityAccess需要先发送Seed请求ECU收到后返回一串随机种子上位机通过预置的密钥算法算出Key再发回ECU对比Key是否正确正确就解锁允许执行敏感操作。我强烈建议不要在代码里明文存储密钥明文至少做个多字节异或或者查表混淆产线上和研发手上用的Key算法可以由同一套上位机工具下发不同的Key参数。安全访问有不少细节会影响联调效率。首先是失败计数绝大多数ECU都会限制安全访问尝试次数比如连续3次Key错误就锁定一段时间通常10秒。其次是种子有效时间种子发出后30秒内不使用就失效。种子和Key都是多字节通常2~4字节具体长度由厂商定义没有强制的统一标准所以上位机和ECU的算法、长度、字节序大端还是小端必须保持一致这个变量也要做成配置文件而不是写死在代码里。3.3 请求下载与擦除34、31服务安全解锁后接着就该告诉ECU“我要写多长的数据、写到哪个地址了”。这里用到的服务是34RequestDownload请求里包含dataFormatIdentifier、addressAndLengthFormatIdentifier、memoryAddress和memorySize四个字段。其中addressAndLengthFormatIdentifier的高四位表示地址长度低四位表示长度长度单位是字节比如0x24表示地址4字节、长度4字节。memoryAddress是要写入的起始Flash地址memorySize是固件数据的总字节数。ECU收到34请求后如果地址和大小合法就会返回一个肯定响应里面包含maxNumberOfBlockLength告诉上位机每个块最多可以传输多少字节。上位机的分块大小应该以这个值为准不要随意加大。很多ECU要求在正式写数据之前先擦除对应区域常见的做法是通过31服务RoutineControl的例程擦除也有的芯片在34请求到来时自动擦除。你需要在设计固件包时明确“先擦后写”的时序上位机的状态机要跟ECU端严格对应。3.4 数据传输36服务的块序列号坑点真正传数据时使用36服务TransferData块序列号blockSequenceCounter从1开始每发一帧加1到0xFF后回绕到0。ECU端会校验序列号是否连续如果收到重复帧或乱序帧很多ECU会直接回NRC 0x73或者忽略该帧等待上位机重新发送当前块。36服务的最大数据长度受34响应中maxNumberOfBlockLength的限制经典CAN基于TP层的数据场最多也就几个字节到几十字节。我建议把36服务的有效数据长度设置成4的倍数因为Flash编程通常按字或双字写入长度非对齐会带来额外处理麻烦。另一个经验如果上位机发送速度太快ECU擦写Flash的耗时跟不上就要靠CAN TP的流控帧来限速或者在上位机里对每个36请求等待响应后再发下一个。3.5 传输结束与ECU复位37、11服务数据全部发送完成后发送37服务RequestTransferExit告知ECU传输结束。ECU收到后通常会做一次内部完整性校验比如校验整个接收缓冲的CRC值校验通过就返回肯定响应。此时部分Bootloader还会通过31服务触发一次全片CRC校验或者校验APP的启动头确保不是一份残缺的固件。最后发11服务ECUReset让ECU复位执行新APP复位类型一般是0x01硬复位或0x03软复位。发完复位指令后上位机不能立刻断开连接要等ECU重启后重新进入默认会话甚至再次尝试读取版本号验证升级结果。整个流程顺序如果打乱比如没做安全访问就发34ECU会回NRC 0x33拒绝执行这些都是UDS状态机约束的结果。4. CAN TP分包粘包与超时管理实战4.1 单帧、首帧、连续帧、流控帧怎么配合ISO 15765-2定义了四种帧类型用于一个UDS消息的分包传输。数据小于等于7字节经典CAN通常是7字节因为占用1字节PCI时用单帧SF一帧发完。数据超过单帧容量时第一帧用首帧FF包含总长度信息和前2字节数据后续用连续帧CF携带剩余数据接收方收到首帧后必须先回一个流控帧FC告诉发送方可以发几个连续帧、帧间最小间隔是多少。流控帧里的参数需要认真理解。BlockSizeBS表示允许发送的连续帧数量如果BS0表示不再限制发送方可以一直发STmin是连续帧之间的最小间隔单位通常是毫秒也可以0~127us。很多CAN TP协议栈实现里默认BS0、STmin0但在高速传输时如果ECU处理不过来可以通过流控帧动态降低发送速率。实际项目中我遇到过TP层“丢帧”的情况排查后发现是STmin设置太小ECU接收FIFO溢出导致丢帧。4.2 大文件传输时常见的分块策略一个固件通常几十KB甚至几百KB而一帧CAN数据最多才几十字节这意味着要发送几千帧。如果每帧都要等待ECU应答后再发下一帧总耗时非常长。所以在设计固件打包和上位机状态机时我通常把固件分成若干个块Block每块包含一定数量的CAN TP消息块与块之间可以适当等待响应块内部按TP层流控连续发送。比如某款芯片的Flash页大小是4KB我可以把每个块的大小设为4KB足够数据通过TP分成若干帧再配合块序号和API接口上层的CRC校验。举例若固件为128KB每个Block为4KB则共32个Block上位机依次调用32次“请求下载-传输-退出”过程循环或按芯片规格一次性请求下载全部长度再逐块传输。这个块大小的选取还要考虑RAM缓冲ECU端如果接收缓冲只有2KB那上位机一个Block设4KB就会导致数据溢出。4.3 P2、S3超时机制与上位机状态机UDS协议里有几个重要定时器P2Server是服务器处理一个请求的最大时间P2Server是增强响应时间S3Server是会话保持超时时间。如果上位机在P2时间内没收到响应可以再等P2时间仍然没有则判定超时。S3超时是指如果ECU在S3时间内没有收到任何诊断请求就会自动从非默认会话回到默认会话甚至退出刷写状态所以刷写大文件时上位机的发送节奏不能太慢否则S3超时会打断整个流程。上位机的状态机设计相当重要。我建议把刷写流程拆成若干个阶段比如init、session_switch、security_access、erase、download、transfer_exit、reset、verify每个阶段严格做超时和响应码检查。不要把所有逻辑写在一个大函数里硬等响应而是要基于状态迁移来做这样即使某个阶段失败上位机也能精准报出“卡在安全访问NRC0x33”排查起来非常高效。5. 常见问题与排查技巧实录不管方案设计得多完美联调阶段总会遇到各种问题。我整理了一份高频问题清单每个都是实际调试中碰到过的你可以直接拿来对照。现象可能原因处理思路ECU完全无响应CANH/CANL接反、波特率不一致、终端电阻缺失用示波器或CAN卡自检回环确认物理链路能收到响应但频繁超时P2/P2*超时设置太短、TP流控参数不合适按10 02响应里的时间参数动态调整返回NRC 0x3134请求的地址不在有效范围、地址未对齐检查分区表和对齐规则返回NRC 0x33未做安全访问或Key错误确认27服务流程检查Seed/Key算法返回NRC 0x70块序列号不正确、传输长度不匹配核对36服务的连续性数据总长度是否等于34声明传数据过程中断或变砖看门狗复位、Flash擦写函数自身被擦、掉电在RAM运行Flash驱动升级期间维持喂狗加备份区跳转APP后死机VTOR未设置、外设状态未清理、APP地址错误单步调试看PC指针是否跳到了APP的Reset_HandlerCAN卡链路提示无法打开串口驱动程序未装、端口被占用、CAN卡供电不足重置USB后重新插拔换一个USB口重新安装驱动5.1 上位机模拟发现NRC 0x31的排查思路NRC 0x31表示请求超出范围这个错误码在刷写流程中出现频率最高。除了地址越界还有一个常见原因是ECU端对memorySize做了严格检查比如它要求长度必须是4的倍数你传了12345字节就会拒绝。还有的ECU要求一次只允许请求固定大小的区域如果超过某个阈值也会返回0x31。遇到这种情况先查ECU端的分区表限制和对齐要求再对照34请求的字节内容逐字段比对。5.2 CAN波特率与采样点设置CAN总线的物理层设置直接影响通信稳定性。常见波特率有500kbps、250kbps、125kbps整车和零部件之间要匹配才能通信。波特率不准会导致报错帧率非常高。另外采样点位置也很关键一般建议在70%到80%之间。对于500kbps如果总线时钟是8MHz预分频16同步跳转宽度设为1BS1设为5BS2设为2采样点就是(15)/(152)75%这是一个比较通用的起点。5.3 工具的选型与测试建议本地OTA调试需要一套趁手的工具。硬件方面我常用的是PCAN、CANoe、周立功USBCAN这类USB转CAN适配器都支持标准帧和扩展帧。软件方面CANoe做全流程验证最方便脚本语言CAPL可以用来模拟UDS刷写也可以用Python的开源库如python-can配合cantools解析DBC和ODX做一个轻量级上位机。先把上位机和ECU之间的流程打通再逐步增加异常注入测试比如故意丢帧、延迟响应、断电重启这样固化的代码质量会高很多。6. 实测参数与经验总结以我最近一次基于STM32F407 TJA1050的方案为例CAN波特率设置为500kbps数据场为经典CAN 8字节。Bootloader从接收到固件包到完成Flash编程并复位全程耗时大约1分20秒固件大小248KB。期间36服务共传输约32000个CAN TP帧没有出现一帧丢失NRC错误码只有一次是人为注入的地址越界。这个速度和稳定性已经足够项目量产使用。另外一个值得提的经验是每次刷写前先通过22服务读取一下当前APP的版本号把版本号显示在上位机界面上刷写完再读一次确认版本已更新。这一步虽然简单但在产线上能省下大量“到底刷没刷进去”的扯皮时间。我给固件包定义了一个固定文件头包含魔数、固件版本、目标MCU型号、固件长度、CRC32Bootloader先验证魔数和CRC再开始刷写从根上杜绝了拿错包刷错料的问题。有人可能觉得本地OTA比不过远程OTA“高级”但我的体会恰恰相反。把本地刷写流程做扎实你实际收获的不只是一套代码而是对整个诊断协议栈、Flash管理、错误处理体系的理解。后面再做远程OTA、多ECU刷写调度、产线自动化测试都会顺很多。如果接下来你想继续深入我建议重点研究UDS的34/36/37在CAN FD下的表现以及Bootloader持A/B分区后的回滚策略这两块是行业里真正值钱的方向。