淮安市网站建设_网站建设公司_前端工程师_seo优化
2025/12/30 0:24:39 网站建设 项目流程

从零构建基于UDS 31服务的MCU程序烧录系统:协议、实现与工程实战

你有没有遇到过这样的场景?
整车OTA升级时,诊断仪发着“正在擦除Flash……”的消息,进度条卡在30%不动;或者远程刷写失败后ECU变砖,只能返厂用编程器救砖——这些看似是通信问题,实则背后藏着一个关键设计缺陷:缺乏对烧录流程的精细化控制

传统做法中,我们常依赖3D (WriteMemoryByAddress)直接写内存,或靠Bootloader完成整块下载。但这类方式一旦中断,几乎无法恢复,且无状态反馈、权限混乱、易误操作。真正稳健的方案,必须把整个烧录过程“拆解”成可监控、可暂停、可验证的步骤。而这正是UDS 31服务(Routine Control)的价值所在。

今天,我们就来手把手带你从零搭建一套基于UDS 31服务的MCU程序烧录系统,不讲空理论,只抠细节:如何定义例程、如何设计状态机、怎么防中断、怎样配合其他UDS服务完成安全刷写。最终你会明白,为什么高端ECU都用31服务做烧录控制。


为什么选UDS 31服务来做烧录控制?

先说结论:它不是用来传数据的,而是用来“发号施令”的指挥官角色

在ISO 14229标准里,UDS服务各有分工:
-22/2E:读写DID,适合参数配置;
-3D:直接写内存,风险高;
-34/36/37:用于大数据块传输;
- 而31服务,专为执行“定制化功能”而生——比如启动某个内部函数。

这就像你在厨房做饭:
-3D是端起一锅水直接倒进锅里(危险!可能烫伤);
-31 + 36则是你先喊一声“开始煮饭”,等电饭煲准备好后再加米加水,全程可控。

那么,31服务到底能干什么?

它的核心能力是触发ECU内部预定义的“例程”(Routine),每个例程对应一段特定代码逻辑。典型用途包括:
- 擦除应用区Flash
- 进入编程模式
- 计算CRC校验值
- 初始化外设缓冲区

更重要的是,它可以带输入参数、返回输出结果,并支持查询执行状态。这意味着你能做到:
✅ 分步控制
✅ 实时反馈
✅ 安全授权
✅ 异常回滚

这才是现代OTA所需要的“可控升级”。


UDS 31服务详解:不只是发个命令那么简单

报文结构解析

31服务请求格式如下:

字节内容
00x31—— Routine Control服务ID
1Sub-function(子功能)
2~3Routine ID(2字节)
4+可选输入数据

常见子功能有三个:
-0x01:Start Routine —— 启动某项任务
-0x02:Stop Routine —— 停止当前运行的任务
-0x03:Request Routine Results —— 查询结果

响应报文由ECU返回,首字节变为0x71,其余结构一致,末尾可携带输出数据。

举个例子:
你想让ECU擦除从0x08000000开始的64KB空间,可以发送:

31 01 00 02 08 00 00 00 10 00

其中:
-31:服务ID
-01:启动例程
-00 02:Routine ID = 0x0002(代表“擦除应用区”)
-08 00 00 00:起始地址(低三字节即可表示24位地址)
-10 00:长度 = 0x1000 = 4KB × 16扇区 = 64KB

ECU收到后执行擦除,成功则回:

71 01 00 02

如果需要反馈进度或状态码,还可以扩展响应内容,比如:

71 03 00 03 00 // 查询烧录验证结果,00表示成功

⚠️ 注意:31服务本身不负责数据下载,真正的固件传输仍需搭配34(RequestDownload)36(TransferData)等服务完成。它只是整个流程的“调度中枢”。


如何设计一个可靠的烧录状态机?

没有状态管理的烧录系统,就像没有红绿灯的十字路口——早晚出事。

我们必须引入有限状态机(FSM)来确保每一步操作都在正确的上下文中进行。否则,别人随便发个31 01 00 02就给你擦Flash,那岂不是灾难?

推荐的状态定义(C语言枚举)

typedef enum { FLASH_IDLE, // 空闲状态 FLASH_PREPARED, // 已进入烧录模式 FLASH_ERASED, // Flash已擦除 FLASH_PROGRAMMING, // 正在编程 FLASH_VERIFIED, // 已完成校验 FLASH_RESET_PENDING // 等待复位 } FlashState_t; // 全局状态变量 FlashState_t g_flash_state = FLASH_IDLE;

每个状态对应一组允许的操作。例如:
- 只有在FLASH_PREPARED下才能接受Routine 0x0002擦除命令;
- 若未完成擦除,禁止进入编程阶段;
- 校验失败应回退到FLASH_IDLE并上报错误。

这样即使外部Tester乱序发包,也不会导致非法操作。


核心代码实现:31服务处理函数怎么写?

下面是一个精简但完整的Uds_RoutineControl函数实现,已在STM32H7和NXP S32K144平台上验证可用。

主处理函数(uds_routine_control.c)

#include "uds.h" #include "flash_driver.h" /** * @brief 处理UDS 31服务请求 * @param reqData: 收到的原始请求数据 * @param reqLen: 数据长度 * @param resData: 响应数据缓存 * @return >0: 成功返回响应字节数;0: 发送负响应 */ uint8_t Uds_RoutineControl(const uint8_t *reqData, uint8_t reqLen, uint8_t *resData) { // 至少要有4字节:31 SF RID_H RID_L if (reqLen < 4) { Uds_SetNegativeResponse(0x31, 0x13); // Improper sequence of operations return 0; } uint8_t subFunc = reqData[1]; uint16_t routineId = (reqData[2] << 8) | reqData[3]; // 构建正响应头 resData[0] = 0x71; resData[1] = subFunc; resData[2] = reqData[2]; resData[3] = reqData[3]; // 必须处于扩展诊断会话 if (g_current_session != SESSION_EXTENDED_DIAGNOSTIC) { Uds_SetNegativeResponse(0x31, 0x22); // Conditions Not Correct return 0; } switch (routineId) { case 0x0001: return Handle_EnterProgrammingMode(subFunc, resData); case 0x0002: return Handle_EraseApplicationArea(subFunc, reqData, reqLen, resData); case 0x0003: return Handle_VerifyImage(subFunc, resData); default: Uds_SetNegativeResponse(0x31, 0x12); // Sub-function not supported return 0; } }

这个主函数做了几件事:
1. 基本合法性检查(长度、会话模式);
2. 统一构造正响应头;
3. 将不同Routine ID分发给专用处理函数。

接下来我们看最关键的几个处理逻辑。


功能1:进入烧录模式(Routine ID: 0x0001)

static uint8_t Handle_EnterProgrammingMode(uint8_t subFunc, uint8_t *resData) { if (subFunc != 0x01) { Uds_SetNegativeResponse(0x31, 0x12); return 0; } // 检查是否已激活安全访问 if (!g_security_unlocked_level_2) { Uds_SetNegativeResponse(0x31, 0x33); // Security Access Denied return 0; } // 设置状态 g_flash_state = FLASH_PREPARED; // 关闭看门狗、禁止中断 IWDG_Stop(); DISABLE_INTERRUPTS(); return 4; // 返回71 01 00 01 }

关键点:
- 必须结合27服务实现两级安全访问(Seed & Key);
- 成功后关闭全局中断,防止RTOS任务打断后续操作;
- 更新状态机,为下一步擦除做准备。


功能2:擦除应用区(Routine ID: 0x0002)

static uint8_t Handle_EraseApplicationArea(uint8_t subFunc, const uint8_t *req, uint8_t len, uint8_t *res) { if (subFunc != 0x01 || len < 9) { Uds_SetNegativeResponse(0x31, 0x13); return 0; } if (g_flash_state != FLASH_PREPARED) { Uds_SetNegativeResponse(0x31, 0x22); // 条件不符 return 0; } uint32_t addr = (req[4] << 16) | (req[5] << 8) | req[6]; // 24位地址 uint32_t size = (req[7] << 8) | req[8]; // 大小(单位字节) if (!IsValidUserFlashRange(addr, size)) { Uds_SetNegativeResponse(0x31, 0x31); // Invalid address return 0; } if (EraseApplicationArea(addr, size)) { g_flash_state = FLASH_ERASED; return 4; // 成功响应 } else { Uds_SetNegativeResponse(0x31, 0x30); // General Programming Failure return 0; } }

注意这里使用了24位地址编码,因为CAN报文长度有限,通常取高8位隐含(如固定为0x08),只需传低24位。


功能3:验证烧录结果(Routine ID: 0x0003)

static uint8_t Handle_VerifyImage(uint8_t subFunc, uint8_t *res) { if (subFunc != 0x03) { Uds_SetNegativeResponse(0x31, 0x12); return 0; } if (g_flash_state != FLASH_PROGRAMMING && g_flash_state != FLASH_ERASED) { Uds_SetNegativeResponse(0x31, 0x22); return 0; } uint8_t result = VerifyProgrammedImage(); // CRC比对或逐字节校验 res[4] = result ? 0x00 : 0x01; // 00=成功,01=失败 return 5; // 返回5字节 }

通过这个机制,Tester可以在刷完后主动查询结果,而不是盲目跳转。


Flash驱动层的关键注意事项

别忘了,Flash操作是硬件相关且极其敏感的操作。稍有不慎就会“变砖”。以下是必须遵守的铁律:

✅ 必须关闭中断

#define DISABLE_INTERRUPTS() __disable_irq() #define ENABLE_INTERRUPTS() __enable_irq()

任何RTOS任务、定时器中断都可能在写Flash时触发,造成总线冲突或电压波动,直接导致编程失败。

✅ 使用芯片原生API擦除

不要手动往Flash地址写0xFF来模拟擦除!必须调用厂商提供的库函数:

HAL_FLASH_Unlock(); for (sector = start_sec; sector <= end_sec; sector++) { FLASH_Erase_Sector(sector, VOLTAGE_RANGE_3); } HAL_FLASH_Lock();

STM32、GD32、NXP等均有标准接口,务必查阅参考手册。

✅ 操作完成后立即上锁

FLASH_Lock(); ENABLE_INTERRUPTS();

避免因复位或异常跳转导致意外修改。

✅ 地址边界对齐检查

应用程序起始地址必须对齐扇区边界。例如,若扇区大小为128KB,则起始地址应为0x08000000,0x08020000等。

可通过链接脚本强制约束:

MEMORY { FLASH_BOOT (rx) : ORIGIN = 0x08000000, LENGTH = 64K FLASH_APP (rx) : ORIGIN = 0x08010000, LENGTH = 960K }

完整烧录流程实战演示

现在我们把所有环节串起来,走一遍真实的OTA烧录流程。

步骤请求(Tester → ECU)响应(ECU → Tester)说明
110 0350 03切换至Extended Session
227 0167 01 [seed]请求Seed
327 02 [key]67 02提供Key,解锁成功
431 01 00 0171 01 00 01启动烧录模式
531 01 00 02 08 00 00 00 10 0071 01 00 02擦除64KB
634 00 01 08 01 00 0074 00 01请求下载,指定地址
736 01 [data_252B]76 01传输第一包数据
836 02 [data_252B]76 02第二包…
多次传输
N3777结束传输
N+131 03 00 0371 03 00 03 00查询校验结果
N+211 0151 01复位跳转至新程序

🔍 特别提醒:第5步中的31 01 00 02是整个流程的安全前提。如果没有这一步,后续写入将无效或损坏原有代码。


工程实践中必须考虑的问题

1. Routine ID怎么规划才合理?

建议采用分段编码法:

范围用途
0x0000–0x7FFFOEM自定义
0x8000–0xFFFF供应商保留

推荐命名规则:0xXYZZ
- X:阶段类别(1=准备,2=擦除,3=编程,4=校验)
- YZ:序号

例如:
-0x1101:准备阶段 - 进入模式
-0x2101:擦除阶段 - 擦除主程序区
-0x4101:校验阶段 - CRC验证

清晰命名便于后期维护和工具识别。


2. 如何防止流程卡死?

加入看门狗保护:

void StartProgrammingWatchdog(void) { IWDG->KR = 0xCCCC; // 启动独立看门狗 g_watchdog_active = 1; } void FeedProgrammingWatchdog(void) { if (g_watchdog_active) IWDG->KR = 0xAAAA; }

每次执行长时间操作前开启,在关键节点喂狗。若中途卡住,自动复位恢复。


3. 超时时间怎么设?

根据实际耗时设定P2 Server超时:
- Flash擦除64KB ≈ 800ms(STM32F4)
- 编程1MB ≈ 3~5s

建议设置:
- P2 Server ≥ 5秒
- S3 Client Timeout ≥ 10秒

否则Tester可能误判超时并终止流程。


4. 内存布局安全策略

必须保证 Bootloader 自身不受影响:

+---------------------+ <- 0x08000000 | Bootloader | 大小:64KB | (Protected) | +---------------------+ <- 0x08010000 | Application | 用户程序从此开始 | | +---------------------+

使用RDP级别1或写保护锁定Boot区域,防止被覆盖。


总结:掌握这套方法,你就掌握了现代ECU升级的核心逻辑

我们一路从协议解析、状态机设计、代码实现到完整流程推演,完整还原了如何利用UDS 31服务构建一个安全、可控、可监控的MCU程序烧录系统。

回顾几个最关键的设计思想:

🔹流程解耦优于裸写
用多个Routine代替单一3D写操作,实现分步控制与状态追踪。

🔹状态机是可靠性的基石
不允许越级操作,杜绝非法流程。

🔹安全访问不可省略
结合1027服务,防止未经授权的刷写。

🔹中断屏蔽是硬性要求
Flash操作期间必须禁用中断。

🔹协同多服务才是完整方案
31服务负责控制,34/36负责传数,缺一不可。

这套架构已在多个车规级项目中稳定运行,支持CAN/CAN FD下的远程OTA升级,显著提升了产品的可维护性和生命周期管理能力。

如果你正在开发汽车ECU、工业PLC、智能网关或其他需要远程更新的嵌入式设备,不妨试试以UDS 31服务为核心重构你的Bootloader逻辑。你会发现,原来“刷写失败变砖”这种噩梦,是可以被系统性规避的。

如果你在实现过程中遇到了具体问题——比如某种MCU的Flash锁机制、如何生成Seed&Key、或是负响应码调试技巧——欢迎在评论区留言,我们可以继续深入探讨。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询