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

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-06-25 22:12:10
昔日国乒名将陈龙灿发声!张本宇留日另有原因,当年选择不简单

昔日国乒名将陈龙灿发声!张本宇留日另有原因,当年选择不简单

潋滟晴方DAY
2026-08-13 13:06:45
反转!中国男篮被爆出内幕,郭士强这次可能被冤枉,真错怪他了

反转!中国男篮被爆出内幕,郭士强这次可能被冤枉,真错怪他了

宗介说体育
2026-09-27 07:59:37
贷款中介,这几天集体变安静了

贷款中介,这几天集体变安静了

21世纪经济报道
2026-09-27 22:07:57
王曼昱头号贵人,不是刘国梁也不是王励勤,竟是已离开国家队的他

王曼昱头号贵人,不是刘国梁也不是王励勤,竟是已离开国家队的他

完善法
2026-09-09 04:59:50
国产显卡撞上性能墙!砺算LX 7G100号称对标RTX 4060:实测勉强赢过AMD九年前的RX 580

国产显卡撞上性能墙!砺算LX 7G100号称对标RTX 4060:实测勉强赢过AMD九年前的RX 580

快科技
2026-09-26 14:11:07
太瘆人了!日本男子潜入单身女子家100多次,打扫卫生、补充饮料,甚至偷走内衣后再买新的换回去。

太瘆人了!日本男子潜入单身女子家100多次,打扫卫生、补充饮料,甚至偷走内衣后再买新的换回去。

日本物语
2026-09-27 20:34:09
一座1亿瓦的核反应堆,只有洗衣机大小,运行63年却没发一度电,专门生产一些造出来就快速消失的东西,已帮助了数亿生命

一座1亿瓦的核反应堆,只有洗衣机大小,运行63年却没发一度电,专门生产一些造出来就快速消失的东西,已帮助了数亿生命

徐德文科学频道
2026-09-27 14:20:48
刘欢离世爆出真实家底,不拍戏不代言,不靠综艺为何坐拥上亿身家

刘欢离世爆出真实家底,不拍戏不代言,不靠综艺为何坐拥上亿身家

东方不败然多多
2026-09-27 16:17:04
刘欢临终细节:妻子卢璐和女儿刘一丝守在床边,母女俩哭到站不住

刘欢临终细节:妻子卢璐和女儿刘一丝守在床边,母女俩哭到站不住

情感大头说说
2026-09-28 05:34:17
刚拿冠军就退役,当晚直接打包行李走人,这姑娘的操作把全网看傻

刚拿冠军就退役,当晚直接打包行李走人,这姑娘的操作把全网看傻

静若梨花
2026-08-29 10:42:55
嫁错人太可怕!被前夫公开骂“性欲太强”的她,活成张柏芝对照组

嫁错人太可怕!被前夫公开骂“性欲太强”的她,活成张柏芝对照组

阿讯说天下
2026-08-02 04:31:47
19岁松岛辉空,正式公布退役计划

19岁松岛辉空,正式公布退役计划

乒乓乐园
2026-08-13 12:18:37
索尼低价区试水?《战神劳菲》能成为PS首个试金石吗

索尼低价区试水?《战神劳菲》能成为PS首个试金石吗

游民星空
2026-09-27 19:13:07
港媒:大陆高铁已被“粪便淹没”!再不解决后果严重,真的假的?

港媒:大陆高铁已被“粪便淹没”!再不解决后果严重,真的假的?

铭记历史呀
2026-09-02 02:21:33
电影院的至暗时刻:174亿票房、场均不到6人、五大公司全亏

电影院的至暗时刻:174亿票房、场均不到6人、五大公司全亏

草莓解说体育
2026-09-27 17:07:15
人活多久,看起夜就知道?寿命短的人,起夜一般有这3个特征

人活多久,看起夜就知道?寿命短的人,起夜一般有这3个特征

芹姐说生活
2026-09-10 23:50:29
20岁德国天才后卫闪耀德甲,开价3500万欧,良心价,皇马情有独钟

20岁德国天才后卫闪耀德甲,开价3500万欧,良心价,皇马情有独钟

零度眼看球
2026-09-28 07:36:21
日本人怒斥高市,非要硬刚中国,这下全世界都知道敌国条款了

日本人怒斥高市,非要硬刚中国,这下全世界都知道敌国条款了

爱看剧的阿峰
2026-09-27 13:15:55
人大代表建议机关事业单位双休调整为“大周休3天,小周休2天”

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

职场资深秘书
2026-09-27 19:40:23
2026-09-28 07:52:49
新能源自动驾驶 incentive-icons
新能源自动驾驶
专注于半导体行业资讯
1015文章数 348关注度
往期回顾 全部

科技要闻

星舰第14飞,为什么准备绕地球六圈?

头条要闻

小伙汽车电瓶被偷刚报完案车又被砸 强忍蚊虫蹲守一宿

头条要闻

小伙汽车电瓶被偷刚报完案车又被砸 强忍蚊虫蹲守一宿

体育要闻

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

娱乐要闻

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

财经要闻

“为了看短剧,我妈两年花了22万 ”

汽车要闻

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

态度原创

艺术
亲子
健康
手机
公开课

艺术要闻

致敬,刘欢硬笔书法欣赏

亲子要闻

深度长文:2.5 亿精子闯关,奔赴 10 厘米外的卵子,最终赢家只有 1 个

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

手机要闻

苹果A20 Pro运行《GTA 5》性能表现达到GTX 1650水平 能效高出近12倍

公开课

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

无障碍浏览 进入关怀版