网易首页 > 网易号 > 正文 申请入驻

OTA升级中的UDS诊断测试:从条件确认到刷写验证的完整方案

0
分享至


一、UDS诊断与OTA升级概述

UDS(Unified Diagnostic Services,统一诊断服务)是ISO 14229标准定义的汽车电子诊断协议,采用客户端-服务器模型,外部诊断仪(Tester)通过请求报文与ECU通信。UDS定义了26种服务,覆盖诊断管理、数据传输、故障诊断、例程控制、上传下载六大类。

OTA(Over-The-Air)远程升级本质上是UDS诊断协议在固件更新场景下的典型应用。OTA升级依赖两大核心技术:Bootloader(启动引导程序)与UDS协议。OTA升级过程中,诊断测试是必做项,贯穿升级前、升级中、升级后三个阶段的完整流程。

二、OTA升级全流程UDS服务详解 2.1 升级前:条件确认

升级前需要通过诊断服务确认ECU满足升级条件。

(1)0x22 ReadDataByIdentifier——读版本号

22服务用于按数据标识符(DID)读取ECU数据。DID是2字节编号,相当于每个数据的“门牌号”。常用DID包括:

DID

0xF195

Boot软件版本

0xF196

应用软件版本

0xF190

零件编号

0xF187

序列号

0xF188

ECU软件号码

0xF189

ECU软件版本号

(2)0x22 读ECU状态

通过读取特定DID获取ECU运行状态(如电压、温度、内存使用率等),判断是否满足升级条件。

2.2 升级中:Bootloader刷写流程

刷写流程涉及7个核心UDS服务,按顺序执行:

Step 1:0x10 DiagnosticSessionControl——会话切换

ECU上电后处于默认会话(Default Session, 0x01)。刷写前需切换到编程会话(Programming Session, 0x02)。不同会话支持不同的诊断服务权限。扩展会话(Extended Session, 0x03)通常用于解锁高权限诊断服务。

Step 2:0x27 SecurityAccess——安全访问

刷写操作涉及敏感内存区域,必须通过安全访问认证。流程为:请求种子(27 01)→ ECU返回随机种子 → 发送密钥(27 02)→ ECU验证。密钥由种子通过特定算法(如AES-128)计算得出。

Step 3:0x31 RoutineControl——擦除例程

31服务用于执行ECU中预定义的诊断例程。擦除例程用于在下载前清除目标存储区域。子功能0x01表示启动例程。

Step 4:0x34 RequestDownload——请求下载

34服务用于向ECU请求下载数据,告知ECU即将开始数据传输。请求中包含数据格式标识符、内存地址和大小等参数。

Step 5:0x36 TransferData——数据传输

36服务用于实际传输固件数据。数据分多次传输,每次包含块序列号(blockSequenceCounter)。所有固件数据通过36服务逐块发送。

Step 6:0x37 RequestTransferExit——退出传输

所有数据传输完成后,调用37服务通知ECU传输结束。如果34和36服务都顺利执行,37将收到肯定响应。

Step 7:0x31 RoutineControl——校验例程

传输完成后再次使用31服务执行校验例程(如CRC校验),验证固件数据的完整性和正确性。最后通过0x11服务实现ECU复位并跳转至新程序。

2.3 升级后:验证确认

(1)0x22 读版本号确认升级成功

再次使用22服务读取软件版本号DID,确认版本已更新为目标版本。

(2)验证所有诊断功能正常

确认22、10、27、31、34/36/37等核心诊断服务均能正常响应。

三、C++代码示例

以下代码演示OTA升级过程中各UDS服务的请求构建与发送(基于CAN通信)。

#include  

#include
#include
#include

// ==================== 基础类型定义 ====================

using Byte = uint8_t;
using ByteArray = std::vector ;

// UDS服务ID (SID)
enum class UdsSid : Byte {
DiagnosticSessionControl = 0x10,
ReadDataByIdentifier = 0x22,
SecurityAccess = 0x27,
RoutineControl = 0x31,
RequestDownload = 0x34,
TransferData = 0x36,
RequestTransferExit = 0x37,
EcuReset = 0x11
};

// 否定响应码 (NRC)
enum class Nrc : Byte {
GeneralReject = 0x10,
ServiceNotSupported = 0x11,
SubFunctionNotSupported = 0x12,
IncorrectMessageLength = 0x13,
ConditionsNotCorrect = 0x22,
RequestSequenceError = 0x24,
SecurityAccessDenied = 0x33,
InvalidKey = 0x35,
ExceedNumberOfAttempts = 0x36,
RequiredTimeDelay = 0x37
};

// ==================== CAN通信接口抽象 ====================

class CanInterface {
public:
virtual ~CanInterface() = default;
virtual bool send(const ByteArray& data) = 0;
virtual bool receive(ByteArray& data, int timeoutMs) = 0;
};

// ==================== UDS客户端类 ====================

class UdsClient {
public:
explicit UdsClient(CanInterface* can) : m_can(can) {}

// ---------- 0x10 诊断会话控制 ----------
// subFunction: 0x01=默认会话, 0x02=编程会话, 0x03=扩展会话
bool diagnosticSessionControl(Byte subFunction) {
ByteArray request = {static_cast (UdsSid::DiagnosticSessionControl), subFunction};
return sendRequest(request);
}

// ---------- 0x22 读DID ----------
// did: 2字节数据标识符,如0xF196表示应用软件版本
ByteArray readDataByIdentifier(uint16_t did) {
ByteArray request;
request.push_back(static_cast (UdsSid::ReadDataByIdentifier));
request.push_back(static_cast ((did >> 8) & 0xFF));
request.push_back(static_cast (did & 0xFF));
if (!sendRequest(request)) {
return {};
}
ByteArray response;
if (!m_can->receive(response, 1000)) {
return {};
}
// 检查肯定响应: SID+0x40 = 0x62
if (response.size() >= 2 && response[0] == 0x62) {
// 返回数据部分(跳过SID和DID)
return ByteArray(response.begin() + 3, response.end());
}
return {};
}

// ---------- 0x27 安全访问 ----------
// step: 0x01=请求种子, 0x02=发送密钥
bool securityAccess(Byte step, const ByteArray& data = {}) {
ByteArray request;
request.push_back(static_cast (UdsSid::SecurityAccess));
request.push_back(step);
request.insert(request.end(), data.begin(), data.end());
return sendRequest(request);
}

// 完整的安全访问流程:请求种子 → 计算密钥 → 发送密钥
bool securityAccessFull() {
// Step 1: 请求种子 (27 01)
ByteArray requestSeed = {static_cast (UdsSid::SecurityAccess), 0x01};
if (!sendRequest(requestSeed)) return false;
ByteArray response;
if (!m_can->receive(response, 1000)) return false;
// 检查肯定响应: 0x67
if (response.size() < 3 || response[0] != 0x67) return false;
// 提取种子(跳过SID和子功能)
ByteArray seed(response.begin() + 2, response.end());
// Step 2: 根据种子计算密钥(使用AES-128或自定义算法)
ByteArray key = calculateKey(seed);
// Step 3: 发送密钥 (27 02)
ByteArray sendKey;
sendKey.push_back(static_cast (UdsSid::SecurityAccess));
sendKey.push_back(0x02);
sendKey.insert(sendKey.end(), key.begin(), key.end());
if (!sendRequest(sendKey)) return false;
if (!m_can->receive(response, 1000)) return false;
// 肯定响应为 0x67
return (response.size() >= 2 && response[0] == 0x67);
}

// ---------- 0x31 例程控制 ----------
// routineType: 0x01=启动, 0x02=停止, 0x03=获取结果
// routineId: 2字节例程标识符
bool routineControl(Byte routineType, uint16_t routineId,
const ByteArray& optionRecord = {}) {
ByteArray request;
request.push_back(static_cast (UdsSid::RoutineControl));
request.push_back(routineType);
request.push_back(static_cast ((routineId >> 8) & 0xFF));
request.push_back(static_cast (routineId & 0xFF));
request.insert(request.end(), optionRecord.begin(), optionRecord.end());
return sendRequest(request);
}

// ---------- 0x34 请求下载 ----------
// dataFormat: 数据格式标识符(压缩/加密方式)
// address: 起始内存地址
// size: 数据大小
bool requestDownload(Byte dataFormat, uint32_t address, uint32_t size) {
ByteArray request;
request.push_back(static_cast (UdsSid::RequestDownload));
request.push_back(dataFormat);
// 内存地址参数长度(通常4字节)
request.push_back(0x44); // 4字节地址 + 4字节大小
// 地址(大端序)
request.push_back(static_cast ((address >> 24) & 0xFF));
request.push_back(static_cast ((address >> 16) & 0xFF));
request.push_back(static_cast ((address >> 8) & 0xFF));
request.push_back(static_cast (address & 0xFF));
// 大小(大端序)
request.push_back(static_cast ((size >> 24) & 0xFF));
request.push_back(static_cast ((size >> 16) & 0xFF));
request.push_back(static_cast ((size >> 8) & 0xFF));
request.push_back(static_cast (size & 0xFF));
return sendRequest(request);
}

// ---------- 0x36 数据传输 ----------
bool transferData(Byte blockSeqNum, const ByteArray& data) {
ByteArray request;
request.push_back(static_cast (UdsSid::TransferData));
request.push_back(blockSeqNum);
request.insert(request.end(), data.begin(), data.end());
return sendRequest(request);
}

// ---------- 0x37 请求退出传输 ----------
bool requestTransferExit() {
ByteArray request = {static_cast (UdsSid::RequestTransferExit)};
return sendRequest(request);
}

// ---------- 完整OTA刷写流程 ----------
bool performOtaUpgrade(const ByteArray& firmware, uint32_t flashAddress) {
std::cout << "=== OTA升级开始 ===" << std::endl;
// ----- 升级前:条件确认 -----
std::cout << "[1] 读取当前版本号..." << std::endl;
ByteArray version = readDataByIdentifier(0xF196); // 应用软件版本
if (!version.empty()) {
std::cout << " 当前版本: ";
for (Byte b : version) std::cout << std::hex << (int)b;
std::cout << std::dec << std::endl;
}
// ----- 升级中:刷写流程 -----
// Step 1: 切换到编程会话 (10 02)
std::cout << "[2] 切换到编程会话..." << std::endl;
if (!diagnosticSessionControl(0x02)) {
std::cerr << " 切换会话失败" << std::endl;
return false;
}
// Step 2: 安全访问 (27)
std::cout << "[3] 安全访问认证..." << std::endl;
if (!securityAccessFull()) {
std::cerr << " 安全访问失败" << std::endl;
return false;
}
// Step 3: 擦除例程 (31 01)
std::cout << "[4] 擦除目标存储区域..." << std::endl;
if (!routineControl(0x01, 0xFF01)) { // 0xFF01 = 擦除例程ID
std::cerr << " 擦除失败" << std::endl;
return false;
}
// Step 4: 请求下载 (34)
std::cout << "[5] 请求下载 (固件大小: " << firmware.size() << " 字节)..." << std::endl;
if (!requestDownload(0x00, flashAddress, firmware.size())) {
std::cerr << " 请求下载失败" << std::endl;
return false;
}
// Step 5: 分块传输数据 (36)
std::cout << "[6] 传输固件数据..." << std::endl;
const size_t BLOCK_SIZE = 4096;
Byte blockNum = 1;
for (size_t offset = 0; offset < firmware.size(); offset += BLOCK_SIZE) {
size_t len = std::min(BLOCK_SIZE, firmware.size() - offset);
ByteArray block(firmware.begin() + offset, firmware.begin() + offset + len);
if (!transferData(blockNum, block)) {
std::cerr << " 数据块 " << (int)blockNum << " 传输失败" << std::endl;
return false;
}
blockNum++;
// 进度显示
if (offset % (BLOCK_SIZE * 10) == 0) {
std::cout << " 进度: " << (offset * 100 / firmware.size()) << "%" << std::endl;
}
}
std::cout << " 进度: 100%" << std::endl;
// Step 6: 退出传输 (37)
std::cout << "[7] 退出传输..." << std::endl;
if (!requestTransferExit()) {
std::cerr << " 退出传输失败" << std::endl;
return false;
}
// Step 7: 校验例程 (31 01)
std::cout << "[8] 校验固件完整性..." << std::endl;
if (!routineControl(0x01, 0xFF02)) { // 0xFF02 = 校验例程ID
std::cerr << " 校验失败" << std::endl;
return false;
}
// ----- 升级后:验证 -----
std::cout << "[9] 读取新版本号验证升级..." << std::endl;
ByteArray newVersion = readDataByIdentifier(0xF196);
if (!newVersion.empty()) {
std::cout << " 新版本: ";
for (Byte b : newVersion) std::cout << std::hex << (int)b;
std::cout << std::dec << std::endl;
}
std::cout << "=== OTA升级完成 ===" << std::endl;
return true;
}

private:
bool sendRequest(const ByteArray& request) {
// 实际实现需添加:ISO-TP分段、流控、超时重传等
return m_can->send(request);
}
ByteArray calculateKey(const ByteArray& seed) {
// 实际实现需使用与ECU一致的算法(如AES-128)
// 此处为简化示例
ByteArray key = seed;
for (auto& b : key) b ^= 0xAA;
return key;
}

private:
CanInterface* m_can;
};

// ==================== 使用示例 ====================

int main() {
// 假设已实现CAN通信类
// CanInterface* can = new MyCanDriver();
// UdsClient client(can);
// 模拟固件数据
ByteArray firmware(1024 * 1024); // 1MB固件
for (size_t i = 0; i < firmware.size(); ++i) {
firmware[i] = static_cast (i & 0xFF);
}
// 执行OTA升级
// client.performOtaUpgrade(firmware, 0x08000000);
return 0;
}
四、总结

OTA越普及,诊断测试的需求越大。OTA测试本身就是诊断测试的延伸方向。UDS通过标准化的服务ID(0x10会话控制、0x22读取数据、0x27安全访问、0x31例程控制、0x34~0x37刷写服务)实现了“一次开发、多ECU兼容”的工程目标。

完整的OTA诊断测试覆盖:

阶段

核心服务

测试目的

升级前

0x22读版本号、读ECU状态

确认升级条件满足

升级中

0x10→0x27→0x31→0x34→0x36→0x37→0x31

验证刷写流程各步骤

升级后

0x22读版本号、全服务功能验证

确认升级成功、功能正常

特别声明:以上内容(如有图片或视频亦包括在内)为自媒体平台“网易号”用户上传并发布,本平台仅提供信息存储服务。

Notice: The content above (including the pictures and videos if any) is uploaded and posted by a user of NetEase Hao, which is a social media platform and only provides information storage services.

相关推荐
热点推荐
银行直供房激增,都在打折出售

银行直供房激增,都在打折出售

谭谈财经
2026-09-27 23:44:02
人大代表建议机关事业单位双休调整为“大周休3天,小周休2天”

人大代表建议机关事业单位双休调整为“大周休3天,小周休2天”

职场资深秘书
2026-09-27 19:40:23
中方为何此时发起联合演训?专家:有两大背景原因,中方必将奉陪到底,菲方只会搬起石头砸自己的脚

中方为何此时发起联合演训?专家:有两大背景原因,中方必将奉陪到底,菲方只会搬起石头砸自己的脚

政知新媒体
2026-09-27 18:30:22
用了十年文件传输助手才发现,微信单人建群才是隐藏王者!

用了十年文件传输助手才发现,微信单人建群才是隐藏王者!

另子维爱读史
2026-09-22 19:06:20
中国为什么总说要扩大消费,却迟迟没有进行大规模收入分配改革?

中国为什么总说要扩大消费,却迟迟没有进行大规模收入分配改革?

素衣读史
2026-06-25 22:12:10
穆帅的皇马爱将贝林厄姆荣升英格兰队一哥,拜仁球星凯恩发送祝贺

穆帅的皇马爱将贝林厄姆荣升英格兰队一哥,拜仁球星凯恩发送祝贺

福酱的小时光
2026-09-28 07:50:22
TA:热苏斯的葡萄牙已经成形,球队不会再一切围绕C罗来构建

TA:热苏斯的葡萄牙已经成形,球队不会再一切围绕C罗来构建

懂球帝
2026-09-28 08:07:03
万万没想到,消失11年的李兆会,竟给44岁未婚的车晓留下一手好牌

万万没想到,消失11年的李兆会,竟给44岁未婚的车晓留下一手好牌

世界尽在眼前
2026-09-25 11:56:45
马斯克暴走了?突然在,X,上放话:只要,6,个月,我的,AI,就能世界领先!

马斯克暴走了?突然在,X,上放话:只要,6,个月,我的,AI,就能世界领先!

一个小豹子
2026-09-25 23:29:17
53张图片被传上网,OpenAI最强模型训练紧急叫停

53张图片被传上网,OpenAI最强模型训练紧急叫停

固件更新中
2026-09-27 07:07:46
变天了?韩国瑜、卢秀燕、侯友宜或首次合体,蒋万安最大对手出现

变天了?韩国瑜、卢秀燕、侯友宜或首次合体,蒋万安最大对手出现

精彩一网打尽
2026-09-27 19:59:41
恭喜!中国女排又一名将生娃升级当妈妈,曾联手朱婷夺世锦赛亚军

恭喜!中国女排又一名将生娃升级当妈妈,曾联手朱婷夺世锦赛亚军

体坛小二哥
2026-09-27 20:57:17
开国上将黄永胜被捕入狱后,为何他的妻子官职却反而一路不断升高?

开国上将黄永胜被捕入狱后,为何他的妻子官职却反而一路不断升高?

微野谈写作
2026-09-27 14:25:10
蒙古女网红闯中企营地施暴打砸的后续来了,结局真的大快人心!

蒙古女网红闯中企营地施暴打砸的后续来了,结局真的大快人心!

怪味历史连连看
2026-08-25 14:50:14
暴跌41.3%!砸4.36亿广告也救不活,网红小家电彻底凉了

暴跌41.3%!砸4.36亿广告也救不活,网红小家电彻底凉了

青眼财经
2026-09-25 19:37:21
这个社会怎么了?国家连年给退休人员涨养老金,还有人吐槽没必要

这个社会怎么了?国家连年给退休人员涨养老金,还有人吐槽没必要

老媹古装影视解说
2026-08-14 13:33:07
亚运会9月28日赛程:国乒冲2金,或再战张本美和,王楚钦警惕翻车!

亚运会9月28日赛程:国乒冲2金,或再战张本美和,王楚钦警惕翻车!

刘姚尧的文字城堡
2026-09-28 08:08:17
开拓者官宣18人训练营名单:杨瀚森开启第2季 利拉德莫兰特领衔

开拓者官宣18人训练营名单:杨瀚森开启第2季 利拉德莫兰特领衔

罗说NBA
2026-09-28 08:09:09
发现没,一个家庭最大的悲哀,就是全家没一个能上得了台面的人

发现没,一个家庭最大的悲哀,就是全家没一个能上得了台面的人

富书
2026-09-03 11:53:44
周末消息大反转,周一开盘,行情要变天

周末消息大反转,周一开盘,行情要变天

魏家东
2026-09-27 09:08:55
2026-09-28 08:44:49
新能源自动驾驶 incentive-icons
新能源自动驾驶
专注于半导体行业资讯
1015文章数 348关注度
往期回顾 全部

科技要闻

特斯拉人形机器人被曝周产量已达数百台

头条要闻

13岁少年和老爸"手搓"涡喷发动机 建家庭实验室花20万

头条要闻

13岁少年和老爸"手搓"涡喷发动机 建家庭实验室花20万

体育要闻

114项指控成立,曼城已经完蛋了吗?

娱乐要闻

刘欢去世后谣言四起,韩磊被翻旧账

财经要闻

标价298元核桃油, 实为大豆油冒充

汽车要闻

MAZDA EZ-60激光版上市 焕新全系11.39万元起

态度原创

时尚
健康
游戏
本地
公开课

女人过了三四十岁,一定要准备一件小西装,大方得体又显瘦

鼻子三角区的痘,千万别乱挤!

《最终幻想7:启示》总监解释为何这么早确认推出DLC

本地新闻

中秋逛白塔寺,体验国医妙荟雅集

公开课

李玫瑾:为什么性格比能力更重要?

无障碍浏览 进入关怀版