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

UDS 诊断服务精讲:$22 ReadDataByIdentifier、$2E WriteDataByIdentifier 与 $31 RoutineC...

0
分享至


UDS 诊断服务精讲: 、 2E WriteDataByIdentifier 与 $31 RoutineControl


在基于 ISO 14229 的 UDS 诊断协议中,$22、$2E和$31是最常用的三个服务,分别用于读取数据、写入数据和执行例程。它们构成了诊断交互的核心:获取车辆信息、标定配置、触发复杂动作。本文将详细讲解这三个服务的协议格式、正负响应机制,并提供可直接运行的 C++ 模拟代码,帮助开发者快速理解与实现。

一、$22 ReadDataByIdentifier(按标识符读数据) 1.1 功能说明

$22服务允许诊断仪(Tester)读取一个或多个数据标识符(DID,Data Identifier)对应的数据值。DID 通常为两个字节(如0xF190表示 VIN 码)。ECU 收到请求后,从内部存储或实时计算中获得数据,并以$62响应回传。

1.2 协议格式

  • 请求:22 DID [DID2 ...]

    • 22:服务 ID

    • DID:高字节 + 低字节(例如F1 90)

    • 可连续请求多个 DID(但多数实现一次只请求一个,避免长度过长)

  • 正响应:62 DID dataRecord

    • 62:响应服务 ID

    • DID:回显请求中的 DID

    • dataRecord:该 DID 对应的数据字节(长度由 DID 定义决定)

  • 负响应:7F 22 NRC

    • 7F:负响应固定值

    • 22:原始请求服务 ID

    • NRC:负响应码(见下文)

1.3 常见负响应码(NRC)

NRC

含义

0x13

报文长度或格式错误(例如 DID 数量不匹配)

0x22

条件不满足(例如车辆未上 ON 档)

0x31

DID 不支持或当前会话不可用

0x33

安全访问未解锁,DID 受保护


1.4 C++ 代码示例:模拟 ECU 处理 $22 请求

#include  

#include
#include
#include
using namespace std;

// 模拟 ECU 内部 DID 数据库
class EcuDataBase {
public:
EcuDataBase() {
// 预定义 DID 及其数据(VIN 码示例)
dataStore[0xF190] = { 'L', 'F', 'V', '3', 'A', '2', '1', 'K', '8', 'M', '5', '0', '0', '0', '0', '1' };
// 软件版本号 DID = 0xF188
dataStore[0xF188] = { 0x01, 0x02, 0x03 };
}

bool isDIDSupported(uint16_t did) const {
return dataStore.find(did) != dataStore.end();
}

const vector& getData(uint16_t did) const {
static vector empty;
auto it = dataStore.find(did);
if (it != dataStore.end())
return it->second;
return empty;
}

// 模拟条件检查(如安全等级、ON档状态等)
bool isReadable(uint16_t did) const {
// 示例:0xF190 任何时候可读;0xF188 需要解锁
if (did == 0xF188)
return securityUnlocked; // 需要安全解锁
return true;
}

void setSecurityUnlocked(bool locked) { securityUnlocked = locked; }

private:
map> dataStore;
bool securityUnlocked = false;
};

// 处理 $22 请求的函数
// 输入:完整请求报文(首字节为 0x22),输出:响应报文
vector handleReadDataByIdentifier(const vector& request, EcuDataBase& ecu) {
const uint8_t SID = 0x22;
const uint8_t POS_RESP_SID = 0x62;

// 负响应辅助
auto negResp = [&](uint8_t nrc) -> vector {
return { 0x7F, SID, nrc };
};

if (request.size() < 3) // 至少 SID + DID(2字节)
return negResp(0x13); // 格式错误

// 检查是否支持该 DID
uint16_t did = (request[1] << 8) | request[2];
if (!ecu.isDIDSupported(did))
return negResp(0x31); // 不支持

// 检查读取条件
if (!ecu.isReadable(did))
return negResp(0x33); // 安全未解锁

// 构造正响应
vector response;
response.push_back(POS_RESP_SID);
response.push_back(request[1]); // DID 高字节
response.push_back(request[2]); // DID 低字节
const auto& data = ecu.getData(did);
response.insert(response.end(), data.begin(), data.end());

return response;
}

// 演示
int main() {
EcuDataBase ecu;
// 请求读取 VIN (DID=0xF190)
vector request = { 0x22, 0xF1, 0x90 };
vector response = handleReadDataByIdentifier(request, ecu);

cout << "Request: ";
for (auto b : request) printf("%02X ", b);
cout << "\nResponse: ";
for (auto b : response) printf("%02X ", b);
cout << endl;

// 请求需要解锁的 DID (0xF188) 且未解锁
request = { 0x22, 0xF1, 0x88 };
response = handleReadDataByIdentifier(request, ecu);
cout << "\nRequest (locked): ";
for (auto b : request) printf("%02X ", b);
cout << "\nResponse: ";
for (auto b : response) printf("%02X ", b);
cout << endl;

// 解锁后再次请求
ecu.setSecurityUnlocked(true);
response = handleReadDataByIdentifier(request, ecu);
cout << "\nRequest (unlocked): ";
for (auto b : request) printf("%02X ", b);
cout << "\nResponse: ";
for (auto b : response) printf("%02X ", b);
cout << endl;

return 0;
}

输出示例(实际运行时):

Request: 22 F1 90 
Response: 62 F1 90 4C 46 56 33 41 32 31 4B 38 4D 35 30 30 30 30 31

Request (locked): 22 F1 88
Response: 7F 22 33

Request (unlocked): 22 F1 88
Response: 62 F1 88 01 02 03
二、$2E WriteDataByIdentifier(按标识符写数据) 2.1 功能说明

$2E服务用于诊断仪向 ECU 写入一个 DID 对应的数据值。典型应用包括写入 VIN 码、配置字、车架号、标定参数等。注意:可读 DID 未必可写,写操作通常受更严格的安全和条件约束。

2.2 协议格式

  • 请求:2E DID dataRecord

    • 2E:服务 ID

    • DID:两字节标识符

    • dataRecord:要写入的数据字节流(长度由 DID 定义决定)

  • 正响应:6E DID(仅回显 DID,不返回数据内容)

  • 负响应:7F 2E NRC

2.3 常见负响应码(NRC)

NRC

含义

0x13

数据长度或格式与 DID 定义不符

0x22

当前条件不满足写入(如车速不为0)

0x31

DID 不支持写入(只读 DID)

0x33

需要安全访问解锁

0x72

写入时内部错误(如校验失败、非易失存储故障)


重点坑:动态定义的 DID(如通过 $2C 服务动态创建)通常不能作为 $2E 的写入目标。
2.4 C++ 代码示例:模拟 ECU 处理 $2E 请求

#include  

#include
// 沿用前面的 EcuDataBase,扩展写入支持

class EcuDataBaseWithWrite : public EcuDataBase {
public:
// 检查是否允许写入
bool isWriteable(uint16_t did) const {
// 示例:0xF190 可写但需安全解锁;0xF188 只读
if (did == 0xF190) return securityUnlockedWrite;
if (did == 0xF188) return false; // 只读 DID
return false; // 其它 DID 不允许写
}

bool writeData(uint16_t did, const vector& data) {
if (!isWriteable(did))
return false;
// 长度校验(示例:VIN 必须为 17 字节)
if (did == 0xF190 && data.size() != 17)
return false;
dataStore[did] = data; // 覆盖写入
return true;
}

void setWriteUnlocked(bool unlocked) { securityUnlockedWrite = unlocked; }

private:
bool securityUnlockedWrite = false;
};

// 处理 $2E 请求
vector handleWriteDataByIdentifier(const vector& request, EcuDataBaseWithWrite& ecu) {
const uint8_t SID = 0x2E;
const uint8_t POS_RESP_SID = 0x6E;

auto negResp = [&](uint8_t nrc) -> vector {
return { 0x7F, SID, nrc };
};

if (request.size() < 4) // SID + DID(2) + 至少1字节数据
return negResp(0x13);

uint16_t did = (request[1] << 8) | request[2];
vector writeData(request.begin() + 3, request.end());

// 检查 DID 是否支持写入
if (!ecu.isWriteable(did))
return negResp(0x31);

// 执行写入
if (!ecu.writeData(did, writeData))
return negResp(0x72); // 写入失败(长度或条件)

// 正响应
return { POS_RESP_SID, request[1], request[2] };
}

int main() {
EcuDataBaseWithWrite ecu;
// 尝试写入 VIN (DID=0xF190) 但未解锁
vector newVin = { '1','G','N','P','K','D','K','E','X','R','1','2','3','4','5','6','7' };
vector request = { 0x2E, 0xF1, 0x90 };
request.insert(request.end(), newVin.begin(), newVin.end());

vector resp = handleWriteDataByIdentifier(request, ecu);
cout << "Write without unlock: ";
for (auto b : resp) printf("%02X ", b);
cout << endl;

// 解锁并写入
ecu.setWriteUnlocked(true);
resp = handleWriteDataByIdentifier(request, ecu);
cout << "Write with unlock: ";
for (auto b : resp) printf("%02X ", b);
cout << endl;

// 验证读取
vector readReq = { 0x22, 0xF1, 0x90 };
auto readResp = handleReadDataByIdentifier(readReq, ecu);
cout << "Verify read after write: ";
for (auto b : readResp) printf("%02X ", b);
cout << endl;

return 0;
}

输出示例:

Write without unlock: 7F 2E 31 
Write with unlock: 6E F1 90
Verify read after write: 62 F1 90 31 47 4E 50 4B 44 4B 45 58 52 31 32 33 34 35 36 37
三、$31 RoutineControl(例程控制) 3.1 功能说明

$31服务用于触发 ECU 内部一段预定义的例程(Routine),例如自检、内存擦除、钥匙学习、刷写前检查等。与$22/$2E操作静态数据不同,$31执行的是动作(函数回调)。例程通过两字节的例程标识符(RID)区分。

3.2 控制类型(sub-function)

含义

0x01

启动例程 (Start)

0x02

停止例程 (Stop)

0x03

请求例程结果 (RequestResults)

请求中可携带附加参数(例如擦除起始地址、长度),由 RID 定义决定。

3.3 协议格式

  • 请求:31 routineControlType RID [routineData...]

  • 正响应:71 routineControlType RID [routineStatusData...]

    • 对Start:可能返回预计执行时间或状态

    • 对RequestResults:返回例程执行的结果数据(如故障码个数)

  • 负响应:7F 31 NRC

3.4 常见负响应码(NRC)

NRC

含义

0x12

子功能(controlType)不支持

0x13

报文长度错误(缺少必需参数)

0x22

当前条件不满足(如发动机运转中不能擦除)

0x24

顺序错误(例如未 Start 就 RequestResults)

0x31

RID 不支持或参数超出范围

0x33

需要安全访问解锁

0x72

例程执行失败(例如擦除校验错误)


3.5 C++ 代码示例:模拟 ECU 处理例程控制

#include  

#include
#include
#include
#include

enum RoutineState { IDLE, RUNNING, FINISHED };
class EcuRoutineController {
public:
EcuRoutineController() : state(IDLE), resultReady(false) {}

// 注册例程回调
void registerRoutine(uint16_t rid, function&)> startFunc,
function stopFunc,
function()> resultFunc) {
routines[rid] = { startFunc, stopFunc, resultFunc };
}

// 处理 $31 请求
vector handleRoutineControl(const vector& request) {
const uint8_t SID = 0x31;
const uint8_t POS_RESP_SID = 0x71;
auto negResp = [&](uint8_t nrc) -> vector {
return { 0x7F, SID, nrc };
};

if (request.size() < 4) // SID + SubFunc + RID(2)
return negResp(0x13);

uint8_t ctrlType = request[1];
uint16_t rid = (request[2] << 8) | request[3];
vector param(request.begin() + 4, request.end());

// 检查子功能支持
if (ctrlType != 0x01 && ctrlType != 0x02 && ctrlType != 0x03)
return negResp(0x12);

// 检查 RID 是否存在
auto it = routines.find(rid);
if (it == routines.end())
return negResp(0x31);

auto& routine = it->second;
bool success = false;
vector resultData;

switch (ctrlType) {
case 0x01: // Start
if (state != IDLE)
return negResp(0x24); // 顺序错误:已运行或未正确停止
success = routine.startFunc(param);
if (success) {
state = RUNNING;
// 模拟异步执行,这里简单同步,实际可启动线程
// 为演示效果,立即标记完成(真实场景需延迟)
state = FINISHED;
resultReady = true;
resultCache = routine.resultFunc(); // 预存结果
}
break;
case 0x02: // Stop
if (state != RUNNING && state != FINISHED)
return negResp(0x24);
success = routine.stopFunc();
if (success) state = IDLE;
break;
case 0x03: // RequestResults
if (!resultReady)
return negResp(0x24); // 未 Start 或未完成
resultData = routine.resultFunc();
// 不改变状态
break;
}

if (!success)
return negResp(0x72); // 例程执行失败

// 构造正响应
vector response = { POS_RESP_SID, ctrlType, uint8_t(rid >> 8), uint8_t(rid & 0xFF) };
response.insert(response.end(), resultData.begin(), resultData.end());
return response;
}

private:
struct Routine {
function&)> startFunc;
function stopFunc;
function()> resultFunc;
};
map routines;
RoutineState state;
bool resultReady;
vector resultCache;
};

// 演示:内存擦除例程 RID=0xFF01
int main() {
EcuRoutineController ecu;
// 注册擦除例程
ecu.registerRoutine(0xFF01,
[](const vector& params) -> bool {
cout << "Routine Start: Erasing memory, params size=" << params.size() << endl;
// 模拟条件检查(例如参数必须包含有效地址)
if (params.size() < 4) return false;
return true;
},
[]() -> bool {
cout << "Routine Stop: Stop erasing" << endl;
return true;
},
[]() -> vector {
cout << "Routine Result: Erase success, 0 errors" << endl;
return { 0x00, 0x00 }; // 成功标志
}
);

// 请求启动例程,带参数(起始地址0x0000,长度0x1000)
vector request = { 0x31, 0x01, 0xFF, 0x01, 0x00, 0x00, 0x10, 0x00 };
auto resp = ecu.handleRoutineControl(request);
cout << "Start response: ";
for (auto b : resp) printf("%02X ", b);
cout << endl;

// 请求结果
request = { 0x31, 0x03, 0xFF, 0x01 };
resp = ecu.handleRoutineControl(request);
cout << "Result response: ";
for (auto b : resp) printf("%02X ", b);
cout << endl;

// 未 Start 就请求结果(状态机已变化,这里演示顺序错误,需重置状态?实际需真实模拟)
// 为了展示 NRC 0x24,再创建一个新控制器
EcuRoutineController ecu2;
ecu2.registerRoutine(0xFF01, [](auto){return true;}, []{return true;}, []{return vector{};});
request = { 0x31, 0x03, 0xFF, 0x01 };
resp = ecu2.handleRoutineControl(request);
cout << "Result without start: ";
for (auto b : resp) printf("%02X ", b);
cout << endl;

return 0;
}

输出示例:

Routine Start: Erasing memory, params size=4
Start response: 71 01 FF 01 00 00
Routine Result: Erase success, 0 errors
Result response: 71 03 FF 01 00 00
Result without start: 7F 31 24
四、总结:NRC 排查思路

无论遇到哪个服务的负响应,都可以按照以下金字塔顺序排查:

  1. 服务/子功能支持性(0x12,0x31)
    检查对方 ECU 是否实现了该 SID 或 RID/ DID。

  2. 安全访问状态(0x33)
    多数写操作和敏感读取需要先执行$27服务解锁。

  3. 会话模式(0x7F常伴随0x12或0x31)
    某些服务仅在扩展会话下可用。

  4. 报文长度/格式(0x13)
    核对 DID 定义的长度,是否漏传或多传了数据。

  5. 运行条件(0x22)
    是否需要车辆处于特定状态(ON档、发动机停止、车速为零)。

  6. 调用顺序(0x24)
    对于$31,是否严格遵循 Start → (Stop) → RequestResults 的顺序。

  7. 内部执行错误(0x72)
    例程内部逻辑失败,如擦除校验失败、写入 EEPROM 超时。

掌握这些服务的协议与实现细节,能让你在开发诊断工具或 ECU 固件时,快速定位问题,写出稳定可靠的代码。

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

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-24 22:58:08
出差喝多了在桑拿房睡了一夜,被炒后她向公司索赔7,600万英镑!

出差喝多了在桑拿房睡了一夜,被炒后她向公司索赔7,600万英镑!

英国那些事儿
2026-09-26 23:14:54
美联储10月加息的概率为64.8%

美联储10月加息的概率为64.8%

每日经济新闻
2026-09-28 06:19:41
新加坡网上吵翻天:才在节日庆祝多元团结,转头就对印度裔开骂

新加坡网上吵翻天:才在节日庆祝多元团结,转头就对印度裔开骂

记录生活日常阿蜴
2026-09-27 18:38:00
泰山水货外援终于再就业,离开泰山队开启流浪模式,失业长达近一年

泰山水货外援终于再就业,离开泰山队开启流浪模式,失业长达近一年

体坛风之子
2026-09-27 05:00:08
1963 年刘少奇出访风光回国,周总理一通电话怒斥犯忌!

1963 年刘少奇出访风光回国,周总理一通电话怒斥犯忌!

往史
2026-09-26 20:00:42
从退耕还林到退林还耕,从严禁超生到鼓励三胎,到底是谁错了?

从退耕还林到退林还耕,从严禁超生到鼓励三胎,到底是谁错了?

一些小事
2026-09-03 08:57:27
联大离奇一幕,以色列代表巡视现场拉黑名单,中国代表岿然不动

联大离奇一幕,以色列代表巡视现场拉黑名单,中国代表岿然不动

奇思妙想生活家
2026-09-27 18:37:01
兰香如故封神一幕!许兰香当众凶人,才是最真的深情

兰香如故封神一幕!许兰香当众凶人,才是最真的深情

陈意小可爱
2026-09-28 01:58:16
出生28天被送养到山东,58年后回浙江与亲人相认,男子身高比哥哥高了一个头!儿子:可能和山东饮食有关系

出生28天被送养到山东,58年后回浙江与亲人相认,男子身高比哥哥高了一个头!儿子:可能和山东饮食有关系

农视网
2026-09-26 19:47:25
嘴炮喊遍全网,上阵直接跑路!俄网红战神翻车

嘴炮喊遍全网,上阵直接跑路!俄网红战神翻车

老马拉车莫少装
2026-07-06 23:48:32
下雨天,你主动把伞借给别人,对方只会感谢你一次。但如果你说:“我这把伞有点小,咱俩一起撑吧。”他会记住你的好很久。

下雨天,你主动把伞借给别人,对方只会感谢你一次。但如果你说:“我这把伞有点小,咱俩一起撑吧。”他会记住你的好很久。

品读时刻
2026-07-22 09:04:16
夺冠后他却在担心合同:我爱纽约,但有点失望

夺冠后他却在担心合同:我爱纽约,但有点失望

星河漫山野
2026-09-28 02:53:45
“卖货给欧洲”的时代结束了:45%关税逼出大招!中国不卖货改建厂

“卖货给欧洲”的时代结束了:45%关税逼出大招!中国不卖货改建厂

大卫聊科技
2026-09-26 12:58:39
无底线了!内塔尼亚胡之子被伊朗特工暗杀,弃行李连夜逃回以色列

无底线了!内塔尼亚胡之子被伊朗特工暗杀,弃行李连夜逃回以色列

梦史
2026-08-29 06:21:20
彭伟国接手广西恒宸4个月闯入冲超区:靠什么运营思路逆袭?

彭伟国接手广西恒宸4个月闯入冲超区:靠什么运营思路逆袭?

林子说事
2026-09-27 14:44:48
升学宴5死再升级!亲历者终于说出实情,墙塌另有原因,本可避免

升学宴5死再升级!亲历者终于说出实情,墙塌另有原因,本可避免

番外行
2026-08-22 13:56:47
几乎全是假货!利润高达2400%,为何消费者还前赴后继争相购买?

几乎全是假货!利润高达2400%,为何消费者还前赴后继争相购买?

星星跌入梦里中
2026-09-26 05:47:11
3轮0球0助攻,昔日阿根廷天才神童惨遭维拉冷落沦为鸡肋,或转投西甲

3轮0球0助攻,昔日阿根廷天才神童惨遭维拉冷落沦为鸡肋,或转投西甲

零度眼看球
2026-09-27 08:45:24
“父爱永久消失”,高考女儿起诉父亲不买苹果手机:不如赶出家门

“父爱永久消失”,高考女儿起诉父亲不买苹果手机:不如赶出家门

泽泽先生
2026-09-25 15:05:14
2026-09-28 07:48:49
新能源自动驾驶 incentive-icons
新能源自动驾驶
专注于半导体行业资讯
1015文章数 348关注度
往期回顾 全部

科技要闻

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

头条要闻

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

头条要闻

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

体育要闻

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

娱乐要闻

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

财经要闻

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

汽车要闻

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

态度原创

游戏
教育
时尚
艺术
数码

《神鬼寓言4》伦敦首次开放试玩 战斗配音获玩家称赞

教育要闻

普通一本转工科要看笔试科目哦

兰香如故:谁为打工人的情绪工伤买单?

艺术要闻

致敬,刘欢硬笔书法欣赏

数码要闻

Beats 360上手体验

无障碍浏览 进入关怀版