ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

基于UDS的CAN本地OTA升级实现与Bootloader开发

基于UDS的CAN本地OTA升级实现与Bootloader开发 1. 为什么用UDS做CAN本地OTA而不是传统IAP如果你做过产线刷写或者售后升级大概率会遇到过这样一幕车间里同时摆着好几款控制器每一款都配了一套专用刷写工具有的是串口协议有的是私有CAN协议换一个芯片平台上位机脚本就要跟着推翻重来。我在刚接手CAN OTA相关项目时最先面对的就是这个烂摊子。当时市面上能买到的刷写工具和诊断仪各说各话我把时间花在适配各家私有bootloader上真正写业务逻辑的时间反而被压缩得厉害。转用UDS诊断协议做CAN本地OTA之后情况彻底变了。UDSUnified Diagnostic Services统一诊断服务最大的价值不是某一个功能点有多惊艳而是把设备升级这件事的操作流程标准化了。无论底层芯片是STM32、S32K还是国产GD系列上层工具永远只用同一套诊断服务去对话进入会话、安全解锁、请求下载、传输数据、退出传输、复位。工具侧我不需要再为每个ECU单独维护协议设备侧只要把UDS服务栈在Bootloader里实现一遍后面换芯片、换平台只要做适配层移植工作量一下子就降下来了。1.1 传统CAN Bootloader的困境传统IAPIn Application Programming通常由MCU厂商提供固件升级例程方案本身没有对错但在整车或工业控制器场景下有几个绕不过去的问题。第一是协议不统一。每家方案商都会自定义一套“擦除命令”“写入命令”“校验命令”字节序、帧格式、握手时序全部自己定。这导致每换一个供应商测试部门就得重新学习一套操作手册出了问题还要拉着原厂一起看协议文档沟通成本极高。第二是安全机制缺失。很多私有bootloader只做简单的地址跳转既不区分会话模式也没有安全访问限制。只要把CAN报文发到对应ID上谁都可以把固件区擦掉。在实验室里问题不大放到量产车上就是严重的安全隐患。第三是调试手段太差。私有协议通常没有统一的状态码烧写失败后只能靠示波器抓波形或者打log反复试。有时候明明只是应答超时却因为协议里没有“服务未被接受”这类反馈排查过程会变得非常痛苦。1.2 UDS标准化到底解决了什么问题UDS协议ISO 14229定义了统一的服务ID、子功能、正响应格式、负响应码NRC以及会话管理和安全访问机制。拿最基础的负响应码NRC来说当服务执行失败时ECU会返回0x7F 服务ID NRC码例如0x31表示请求超出范围、0x33表示安全访问未通过、0x72表示编程失败。这个机制让排查问题从“猜”变成“看”我能直接从上位机日志里定位到是哪一步没通过。UDS还定义了P2和P2*两个服务器响应时间参数。P2通常为50ms表示ECU对常规诊断请求应在该时间内给出响应如果该服务需要长时间擦写FlashECU可以回复NRC 0x78responsePending表示“我正在处理请继续等待”然后上位机进入P2*轮询周期通常为5000ms。这种明确的超时机制比我之前用私有协议时自己拍脑袋设定的固定延时科学得多。另一个实用点是UDS协议栈本身和传输层解耦。CAN上的UDS报文通过ISO-TPISO 15765-2做分帧传输单条UDS消息最长可以到4095字节而经典CAN数据帧只有8字节。只要底层ISO-TP实现稳定上层UDS服务写起来就和操作一块内存差不多不需要关心数据是怎么拆帧、合帧的。1.3 “本地OTA”和车联网OTA的关系项目标题里写的“CAN本地OTA”这里的“本地”是指升级工具和设备通过CAN总线直连在产线、售后维修站或台架环境下完成固件刷写而不是走云平台远程下发。有人会问既然要做OTA为什么不做完整的车联网远程升级我的实际体会是本地OTA是整个远程OTA链路里最核心、也最需要先跑通的一段。远程OTA最终也要走诊断协议把固件写到ECU里区别只在于文件来源变成云端、传输路径多了网关和无线网络。但真正容易出问题的恰恰是底层这一段Bootloader是否能在任意时刻被稳定唤醒Flash分区设计是否支持断电恢复ISO-TP在总线上是否扛得住连续大流量传输。这些基础能力如果不先在本地OTA场景里验证到位直接上远程OTA只会把问题放大。所以我始终认为先把基于UDS的CAN本地OTA做扎实就是给后续车联网升级铺路。2. 整体Flash分区设计和启动引导方案2.1 Boot/App/下载暂存区三段式划分我在最初设计这个项目的时候并没有一上来就写CAN驱动而是先把Flash分区表定下来。分区方案直接决定升级失败后能不能救回来这是整个项目里最不该省事的地方。以常用的STM32F407为例它自带1MB Flash起始地址是0x08000000。我采用三段式划分这种布局在量产ECU里非常常见分区起始地址大小作用Bootloader0x0800000032KBUDS诊断栈、ISO-TP、Flash驱动、跳转逻辑下载暂存区0x08008000448KB接收新固件并暂存等待校验APP运行区0x08078000512KB正式运行的程序区下载暂存区的存在允许我把升级过程拆成“先收完整包再搬移到运行区”两段。这样即使下载过程中途断电APP运行区还是上一次的完整固件控制器上电后依然能正常执行旧程序。只有当整个固件包都校验通过后Bootloader才会执行搬移操作搬移的耗时很短断电风险窗口被压缩到最小。当然这种方案要求Flash容量够用。如果芯片只有256KB Flash挪出128KB做暂存区可能不现实这时可以退化为“边收边写直接覆盖App区”但必须在Bootloader里保留完整的UDS刷写能力保证即使App区已经写坏Boot仍然能响应下载请求实现救砖。2.2 启动引导的判断顺序Bootloader启动后的执行顺序决定了控制器在各种异常状态下会走哪条路。我的设计是严格按照下面这个顺序来的初始化系统时钟、CAN控制器和必要的GPIO。检查是否有外部强制刷写请求。这个请求可以来自诊断仪发来的特定会话也可以来自一个硬件引脚电平。如果存在强制刷写请求不进入App直接驻留在Boot并等待UDS服务。如果没有强制刷写请求检查下载暂存区是否有“固件下载完成”标志和CRC校验值。如果校验通过把暂存区内容复制到APP运行区复制成功后清掉标志再次CRC校验。检查APP运行区起始位置是否为有效的栈顶地址。对Cortex-M来说0x08078000处应存放一个指向RAM区域的栈顶指针值通常落在0x20000000到0x20020000之间。若App区有效跳转执行若无效停留在Boot的UDS诊断循环里等待刷写。这套逻辑看着简单但它在“App损坏”和“升级中途断电”两种场景下都能兜住。尤其第5步检查栈顶地址虽然不能替代完整CRC但可以快速过滤掉“完全空白”或“写了一半”的Flash区域是一个低成本高效率的可用性判断。2.3 跳转前的关键准备从Boot跳转到App不是简单地把PC指过去就行。Cortex-M内核要求App代码必须能看到正确的中断向量表。STM32上通过修改SCB-VTOR寄存器来实现中断向量表重定向但要注意VTOR对齐要求偏移量必须是0x200的整数倍。如果App起始地址没有对齐中断向量表解析就会出错App一运行就进HardFault。另一个容易翻车的地方是外设状态。Bootloader在初始化时开启过CAN外设、定时器、看门狗跳转前如果不把这些外设复位干净App启动后执行自己的外设初始化时可能会因为外设还停留在Boot设定的状态而卡住。我在实际项目里吃过一次亏Boot初始化过CAN后跳转App再次执行CAN初始化时没有先做DeInit结果CAN模块停在BusOff状态收发都不正常。正确做法是在跳转前把用到的外设全部DeInit并关闭全局中断然后再跳转。跳转代码本身很简单核心是取出App起始地址处的栈顶指针和复位向量然后调用一个函数指针typedef void (*AppMain)(void); void jump_to_app(uint32_t app_addr) { uint32_t app_sp *(volatile uint32_t *)app_addr; AppMain app_main (AppMain)(*(volatile uint32_t *)(app_addr 4)); __disable_irq(); SysTick-CTRL 0; HAL_RCC_DeInit(); HAL_DeInit(); SCB-VTOR app_addr; __set_MSP(app_sp); app_main(); }这段代码对所有Cortex-M内核都适用网上很多Bootloader例程也是这么写的。但重点在跳转之前那些“清理动作”缺一个都可能在App里引发莫名其妙的问题。3. 与OTA强相关的UDS服务及其实现细节3.1 实际项目中用到的服务清单UDS协议一共定义了几十种服务但CAN OTA真正高频用到的就那十来个。我把项目里固定的服务整理成了下表方便参考SID服务名称子功能/参数在OTA中的作用0x10诊断会话控制0x02编程会话、0x03扩展会话切换工作模式0x27安全访问0x01请求种子、0x02发送密钥解锁刷写权限0x34请求下载地址、长度、压缩方式通知ECU准备接收固件0x36传输数据块序列计数数据实际传输固件内容0x37请求传输退出无子功能结束数据传输阶段0x22按标识符读取数据数据标识符读取版本号、刷写状态0x2E按标识符写入数据数据标识符数据写入版本号、升级标记0x19读取DTC信息0x02按状态掩码读取确认故障码状态0x14清除DTC信息无子功能刷写前清除历史故障0x85控制DTC设置0x02关闭刷写过程中抑制故障误报0x11ECU复位0x01硬复位、0x03软复位让新程序生效从这表里能看出来UDS刷写的本质就是“一群标准服务的组合”。工具侧只要按顺序发这些服务ECU就能按标准化流程完成升级。我之前写私有bootloader时光“擦除写入校验”这三个动作就要定义七八种命令相比之下UDS的清晰度完全不在一个量级。3.2 会话控制与安全访问进入刷写的第一步通常是发送0x10 02切换到编程会话。这里有个细节如果ECU当前处于默认会话0x01直接发0x10 03进入扩展会话再调用编程相关功能也是可行的但OTA场景下我一般直接切到编程会话。因为编程会话会关闭应用层部分正常通信逻辑降低升级过程中被干扰的概率。会话切换还牵扯到超时问题。UDS规定ECU在非默认会话下如果长时间没有收到诊断请求会自动跳回默认会话。这个超时时间由ECU实现决定我项目里配置的是5秒。也就是说上位机在两条请求之间的间隔必须小于这个值否则ECU回退到默认会话后续服务全部返回“服务不支持”之类的NRC。上位机开发者如果忽略了这个隐性约束刷写中途只要停顿几秒就会触发一连串失败。安全访问0x27是刷写前必须过的门。流程是上位机发0x27 01请求种子ECU返回种子数据工具通过种子计算密钥后发送0x27 02ECU比对正确才放行。实际开发中种子长度常见的有2字节、4字节算法可以是查表、线性反馈移位寄存器或者AES安全强度取决于产品定位。但不管算法多简单有三个工程点必须注意安全访问失败计数器连续错误达到一定次数后ECU应进入延时锁定状态防止暴力破解。种子不能重复每次请求种子都应生成不同值否则算法很容易被重放攻击。编程会话中安全访问状态清零切换会话或ECU复位后必须重新解锁才能刷写。我在项目里用的是一种带时间戳的伪随机种子算法配合简单查表做密钥计算。这套方案在工程上够用也方便逆向难度测试团队做验证。3.3 34/36/37三个下载服务的实现细节请求下载0x34这条指令携带的信息比较集中dataFormatIdentifier数据格式标识符、addressAndLengthFormatIdentifier地址长度格式、memoryAddress和memorySize。在CAN网络中这三个服务的真实交互流程是这样的工具发送0x34时会指定要写入的目标地址和长度。ECU收到后检查地址是否在自己的编程范围内、长度是否超限然后返回一个maxNumberOfBlockLength告诉工具“你每个0x36块最多可以传多少字节”。这个块长很关键如果工具忽略它、每帧塞了超过容量的数据ECU就会回NRC 0x13消息长度错误。传输数据0x36是刷写的主体服务。每帧0x36都带一个块序列计数BSCblockSequenceCounter从1开始每成功处理一帧就加1到0xFF后回绕到1。ECU内部需要记录上一次收到的BSC如果工具发来的BSC不是“上一次1”就会回NRC 0x73错误块序列计数。我在项目里曾遇到过CAN卡驱动重复发送最后一帧的情况导致ECU连续两次收到相同的BSC后来在工具侧做了去重才解决。请求传输退出0x37表示“整个固件包已经传完了”。ECU收到0x37后可以对整个数据区域做一次CRC校验或者简单检查下载区标志位。这里我建议工具在发0x37之前最好在固件末尾额外追加一个自定义的CRC32字段ECU在0x37阶段校验这个字段而不是扫一整块Flash。这样既快又准确。3.4 和刷写配套的辅助服务刷写不是从0x34到0x37就完了配套服务同样重要。比如0x85控制DTC设置刷写过程中如果ECU检测到某些信号缺失可能会记录一堆故障码干扰后续的刷写判断。刷写前先发0x85 02关闭DTC记录刷写完成后再发0x85 01打开能省掉很多不必要的麻烦。0x22/0x2E读写数据标识符也要在刷写流程里用起来。我习惯在刷写开始前通过0x22读取当前软件的版本号判断固件包和当前版本是否匹配刷写完成后通过0x2E把新版本号写入ECU这样后续诊断和追溯都有据可查。0x19读取DTC信息和0x14清除DTC信息同样是刷写前后必走的流程。刷写前记录当前故障码用于排查刷写后清除历史故障码保证车辆交付给用户时处于干净状态。4. 在STM32上的CAN协议栈与Flash驱动移植要点4.1 ISO-TP分帧层的落地UDS报文长度超过8字节后必须通过ISO-TP协议在CAN总线上分帧传输。ISO-TP定义了四种帧类型单帧SF、首帧FF、连续帧CF和流控帧FC。单帧用于最多7字节的数据超过7字节后工具先发首帧告知总长度接收方回一个流控帧表示“可以发了”然后发送方按序号发连续帧。ISO-TP的状态机是整个CAN OTA项目里最容易写乱的部分。我贴一个简化版的接收状态机基本能覆盖刷写场景typedef enum { TP_IDLE, TP_WAIT_FF, TP_WAIT_CF } TpRxState; typedef struct { TpRxState state; uint8_t buffer[4096]; uint16_t total_len; uint16_t received_len; uint8_t last_sn; } TpRxHandle; // 收到CAN帧时调用 void can_tp_on_frame(TpRxHandle *tp, uint32_t id, uint8_t *data, uint8_t len) { uint8_t frame_type data[0] 4; switch (tp-state) { case TP_IDLE: if (frame_type 0x0) { // 单帧 uint8_t payload_len data[0] 0x0F; memcpy(tp-buffer, data[1], payload_len); tp-total_len payload_len; tp-received_len payload_len; // 通知上层UDS处理 } else if (frame_type 0x1) { // 首帧 tp-total_len ((data[0] 0x0F) 8) | data[1]; tp-received_len 0; memcpy(tp-buffer, data[2], 6); tp-received_len 6; tp-last_sn 0; tp-state TP_WAIT_CF; // 发送流控帧BS0, STmin0 } break; case TP_WAIT_CF: if (frame_type 0x2) { // 连续帧 uint8_t sn data[0] 0x0F; uint8_t offset ((sn - tp-last_sn - 1) 0x0F) * 7; // 检查序列跳跃 memcpy(tp-buffer[tp-received_len], data[1], 7); tp-received_len 7; tp-last_sn sn; if (tp-received_len tp-total_len) { tp-state TP_IDLE; // 一条完整UDS消息交给上层处理 } } break; default: break; } }这段代码的重点是连续帧序号判断。CF的序号从1开始但是到达0xF后回绕到0所以比较序号顺序时不能用简单的“”而是要用增量差值判断。遗漏这个细节会在传输大固件包时偶发丢帧错序排查起来非常痛苦。4.2 Flash擦写时最容易忽视的约束Flash擦写远比RAM读写麻烦因为MCU的Flash控制器在编程时不允许对同一块Flash进行读操作。如果代码本身正在Flash里执行那么在写Flash时CPU取指会受影响甚至进入HardFault。这也是为什么有些工程要求把Flash写函数放到RAM里执行。但在我们的场景里Bootloader运行在Boot区而刷写目标是App区或下载暂存区两个区域是独立的。这意味着Boot区代码在执行Flash写操作时CPU取指不涉及正在被写的区域所以不需要把函数搬到RAM里。这一点在划分Flash分区时就已经定好了。另一个约束是写操作的位宽。STM32内部的写操作一般要求按16位半字进行且目标地址必须2字节对齐。如果固件包的大小是奇数或者工具按8位字节流发送ECU就必须在写Flash前做缓冲重组凑够半字再写入。我在项目里用一个32字节的RAM缓冲去攒数据攒够两个半字就调用一次Flash编程函数既满足对齐要求又减少了Flash编程次数。擦除方面STM32F407的Sector大小不统一前4个Sector是16KB后面是64KB或128KB。刷写时如果不知道目标固件占用了哪些Sector盲目执行全片擦除会把Bootloader一起擦掉这个错误在调试早期很容易犯。我的做法是先用0x34携带的地址和长度计算出Sector范围只擦除相关区域。4.3 刷写流程中的看门狗和中断策略刷写过程中如果开启了独立看门狗IWDG而ECU因为擦除大块Flash耗时太长导致没有及时喂狗MCU就会在刷写中途复位之前的努力全部白费。我建议在进入编程会话后直接关闭看门狗或者在看门狗中断服务里置一个“编程模式”标志在编程模式下跳过喂狗。中断策略也是容易出问题的地方。很多人图省事在写Flash前直接__disable_irq()关全局中断写完再打开。但如果刷写过程中CAN中断被关掉ISO-TP收到的连续帧就会被丢工具侧会等到超时。更合理的方案是CAN中断只负责把数据搬到RAM接收缓冲区并设置一个标志位主循环检测到完整消息后再执行Flash写入。这样写Flash期间即使发生CAN中断中断服务函数也不会访问Flash不会阻塞Flash控制器。5. 上位机刷写工具的开发与调试5.1 上位机技术选型设备端Bootloader只是升级链路的一半上位机刷写工具同样决定项目能不能顺利落地。我开发时选用的是Python python-can can-isotp这套组合原因很简单开发速度快调试方便还能直接对接PCAN或周立功的CAN卡。python-can负责把所有CAN设备抽象成统一接口can-isotp则帮我们完成了ISO-TP分帧和组帧的底层工作。有了这两个库我可以把注意力完全集中在UDS服务层的状态机上。对快速验证原型来说这套方案比用CANoe写CAPL脚本更灵活也比用C写Qt工具更省时间。一个常见问题是Python的性能。python-can在高负载下可能会有毫秒级的延迟波动但对本地OTA来说每包数据是8字节CAN帧波特率500kbps整个传输过程的上位机CPU开销很小Python完全扛得住。如果后续要做量产工具再把这个逻辑用C重写协议状态机本身可以无缝平移。5.2 刷写状态机与执行顺序刷写工具的核心不是一个函数而是一个状态机。状态后面的每一步都依赖上一步成功任何一个环节返回NRC工具都应该在日志里醒目地标出错误并停止后续动作。一个标准的UDS刷写流程是这样的发送0x10 02进入编程会话。发送0x27 01请求种子收到种子后计算密钥发送0x27 02解锁。发送0x85 02关闭DTC记录。发送0x19 02读取当前故障码记录现场信息。发送0x14清除DTC。发送0x34请求下载携带固件地址、长度。循环发送0x36传输数据直到整个固件包发送完毕。发送0x37请求传输退出。发送0x2E写入新版本号通过0x22读回验证。发送0x11 01执行ECU复位。等待ECU重新上线进入默认会话整个升级完成。这里我的经验是第2步和第1步之间不要省略安全访问。有些工具为了方便调试会在默认会话里直接发0x34这在某些ECU上会被NRC 0x33拒绝因为0x34本身属于编程会话的权限范围。严格遵守流程次序能少踩很多兼容性坑。5.3 超时、重传和0x78轮询超时控制是上位机最容易做错的地方。UDS标准规定的P2时间是50msP2*是5000ms。工具每发一帧诊断请求后如果在P2时间内没有收到响应可以认为ECU需要更长处理时间但不能立即判定超时而是要等待P2*期满。尤其是收到NRC 0x78之后ECU明确告诉工具“我在忙”工具应进入轮询状态每隔一段时间重复发送一个带有相同子功能的请求实际上正确的0x78处理方式是ECU在0x78响应中已经拒绝了本次请求的“即时应答”但把任务挂起来了。工具需要做的是等待然后重新发送原服务请求ECU会在处理完成后返回正响应。我项目里设置的轮询策略是收到0x78后每500ms重发一次原请求最多重发10次。也就是说工具最多给ECU 5秒的时间完成擦除或编程操作。如果ECU擦除一个大Sector需要3秒这个策略完全够用。需要特别注意的是ECU在0x78期间如果又收到一个新的会话请求当前编程任务会被打断因此工具千万不要在轮询期间发送任何不相关的服务。5.4 日志与可观测性刷写失败的时候日志就是唯一的破案线索。我自己的工具会记录每一帧CAN报文的收发方向、时间戳、CAN ID、数据内容以及UDS层解析出来的服务ID和NRC码。这样调试时可以直接对照UDS标准查NRC表判断是协议问题还是ECU内部状态问题。我在开发后期还加了一项简单的统计每一包0x36的平均传输耗时、最大耗时、重传次数。这些数据对评估刷写性能很有用。比如某一次升级整体耗时突然从10秒变成30秒日志里一对比就能发现是某个连续帧的间隔异常进而定位到CAN卡驱动或波特率配置问题。6. 真实现场中踩过的坑和排查方法6.1 Flash操作把CAN报文弄丢了我调试早期遇到过一个问题刷写进行到一半上位机发出去的0x36报文ECU完全没有响应也不回NRC就那么静默了。后来用CAN分析仪抓总线发现ECU确实没收到报文问题出在MCU自身。原因是这样的我最初在CAN接收中断里直接做了Flash编程的操作。当0x36数据到达时中断函数开始执行Flash擦写而Flash擦写期间Flash控制器忙同一时刻代码无法从Flash取指CPU被暂停。就在这个暂停窗口里CAN控制器又收到了新报文但CPU正卡在Flash擦写上没有及时去读接收邮箱导致报文覆盖丢失。解决思路很简单把Flash编程从中断上下文挪到主循环。CAN中断只负责把数据搬到RAM置一个“有新帧”标志主循环检测到标志后再去执行Flash操作。这样即使Flash编程耗时较长CAN中断依然能及时响应硬件事件。这个改动之后刷写过程的丢帧率直接降到了零。6.2 序号重复导致的0x73有一次在整车上联调刷写到200包左右时ECU稳定返回NRC 0x73错误块序列计数。一开始怀疑是我设备端BSC处理逻辑有误但本地用PCAN复现又总是好的。反复抓包后发现是车上网关把CAN报文做了路由转发个别报文在转发时被重复了一次导致ECU连续收到两个相同的BSC。这里的教训是工具和设备端都不能假设总线上绝对不丢帧、不重帧。上层UDS状态机处理时必须对“重复的BSC”做兼容。我的处理方式是如果收到的BSC和上一次完全相同说明是重复帧直接丢弃但回复正响应如果BSC和上一次差值不为1才视为序号错误。这个改动让刷写流程在面对网关路由或总线繁忙时稳定了很多。6.3 刷完复位后App起不来升级完成后我发0x11 01复位ECUCAN总线上能看到ECU重新上线但App就是没跑起来接调试器一看程序停在HardFault_Handler里。最终定位到两个原因。第一个是App中断向量表没有重定向。App编译时链接地址设置没问题但Cortex-M启动后默认从0x08000000读取中断向量表而那里是Bootloader的向量表。App里一旦发生中断就会跳到Boot的向量表去解析App的中断服务函数根本不会被调用最终引发HardFault。解决方式是在main函数最开头设置SCB-VTOR APP_START_ADDR。第二个原因是外设状态残留。Boot跳转前没有关闭CAN外设App执行CAN初始化时硬件还保持着Boot配置的工作模式。解决方式就是我前面提到的做法跳转前把用到的外设全部DeInit再跳转。6.4 现场刷写时的ID和波特率问题一个人调试的时候CAN ID和波特率是自己定的不会出问题。但到了现场诊断仪、网关、其他ECU都在同一个网络上ID冲突就会冒出来。我建议Bootloader的诊断请求ID和诊断响应ID做成可配置项让工具侧在刷写前先通过0x3E测试仪在线保持或0x10服务确认通信正常再进入正式流程。同时在工具配置界面上明确暴露CAN ID和波特率选项避免现场工程师拿着不匹配的配置去刷写。波特率方面大多数车用CAN网络是500kbps或250kbpsBootloader要固定支持某一个值。如果项目可能跨车型最好在Boot里做一次自动波特率检测工具先发送一串特定长度的唤醒报文Boot根据两次边沿时间差计算出波特率再完成同步。这个方法在量产工具中很常用能省掉不少现场配置的麻烦。6.5 升级断电后的恢复验证做OTA绕不开断电恢复测试。我的做法是模拟各种断电点在0x10后断电、在0x27后断电、在0x34后断电、在0x36传输中途断电、在0x37后断电分别验证控制器能否在恢复上电后依然可刷写。最终结论是只要Flash分区方案是“下载暂存区 App运行区”的设计并且Bootloader每次启动都检查暂存区完整性和App区有效性任何时间点断电都不会让控制器彻底变砖。最坏的情况下App区没被破坏继续运行旧程序稍坏一点App区写了一半Boot依然能响应UDS刷写请求重新刷一遍即可。这套恢复机制的价值在售后场景里会体现得特别明显。没有分区保护的车控ECU一旦升级断电只能返厂拆壳烧录有分区保护的车控ECU在维修店里用诊断仪重新刷一次就能恢复成本和体验完全是两个量级。回到我个人的项目体会基于UDS的CAN本地OTA最值得花时间的不是CAN驱动也不是Flash驱动而是先把状态机设计清楚、把Flash分区定明白。协议栈和驱动都是成熟技术网上资料一大把真正决定项目成败的往往是那些你在调试中才会撞上的边界情况。如果能在一开始就按“会话权限 安全访问 下载状态机 断电恢复”的框架去设计后面联调会顺畅很多希望这篇文章能帮你少走一段弯路。
返回列表