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

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-28 10:17:29
广东一租客因欠租搬走后,13岁男孩被独留停水停电出租屋不上学:母亲拒绝帮助,父亲电话停机;房东急了

广东一租客因欠租搬走后,13岁男孩被独留停水停电出租屋不上学:母亲拒绝帮助,父亲电话停机;房东急了

扬子晚报
2026-09-28 10:28:50
耿同学又出手!揭《Nature》一论文造假,24个原始值完全一致,通讯作者已承认并撤稿

耿同学又出手!揭《Nature》一论文造假,24个原始值完全一致,通讯作者已承认并撤稿

材料科学与工程
2026-09-24 11:48:39
女子千里赴约让情人白睡,2016年情夫和她缠绵完很满足,将她杀死并抛尸

女子千里赴约让情人白睡,2016年情夫和她缠绵完很满足,将她杀死并抛尸

汉史趣闻
2026-09-26 09:25:49
亚运会:世界第1爆冷出局!冯彦哲/黄东萍0-2印尼混双,5连胜终结

亚运会:世界第1爆冷出局!冯彦哲/黄东萍0-2印尼混双,5连胜终结

钉钉陌上花开
2026-09-28 09:13:11
主张联俄抗中的专家叛变了!去俄罗斯待了7天,回来就换了说法

主张联俄抗中的专家叛变了!去俄罗斯待了7天,回来就换了说法

游文刀
2026-09-24 21:18:36
这跟不穿有啥区别?三角区明显,41岁女星穿透视3D打印裙惹争议

这跟不穿有啥区别?三角区明显,41岁女星穿透视3D打印裙惹争议

铁锤妹妹是只猫
2026-09-26 19:04:43
西贝又上热搜了,不是罗永浩,是俩探店博主线下吃出来的热度。

西贝又上热搜了,不是罗永浩,是俩探店博主线下吃出来的热度。

荆楚寰宇文枢
2026-09-27 20:53:44
再看刘欢的从头再来:为什么是普通工人而不是干部和领导?

再看刘欢的从头再来:为什么是普通工人而不是干部和领导?

阿废冷眼观察所
2026-09-27 16:01:39
塞尔维亚总统武契奇宣布辞职

塞尔维亚总统武契奇宣布辞职

澎湃新闻
2026-09-28 02:47:02
知名投资人曝刘欢真实死因是肝癌!8月底他曾在餐厅吃饭状态不错

知名投资人曝刘欢真实死因是肝癌!8月底他曾在餐厅吃饭状态不错

一个小豹子
2026-09-27 09:40:19
绝了!唐僧取经先被办证窗口问崩溃,教师资格证采矿证捕捞证一张不能少

绝了!唐僧取经先被办证窗口问崩溃,教师资格证采矿证捕捞证一张不能少

三言四拍
2026-09-27 19:24:04
王祉怡0-2不敌山口茜无缘亚运会女单决赛,收获铜牌

王祉怡0-2不敌山口茜无缘亚运会女单决赛,收获铜牌

懂球帝
2026-09-28 12:12:06
创业板指跌逾2%,全市场下跌个股超4200只

创业板指跌逾2%,全市场下跌个股超4200只

界面新闻
2026-09-28 09:39:39
七连斩世界第5!刘圣书/谭宁2-1逆转大马王牌,剑指亚运女双首冠

七连斩世界第5!刘圣书/谭宁2-1逆转大马王牌,剑指亚运女双首冠

钉钉陌上花开
2026-09-28 10:06:57
伊朗总统当着美媒面诉苦:石油卖给中国,钱却卡在账户里

伊朗总统当着美媒面诉苦:石油卖给中国,钱却卡在账户里

共工之锚
2026-09-28 00:20:41
C罗暴怒原因揭晓!约好出战半小时却变卦 72岁新帅:他不该拒绝谢场

C罗暴怒原因揭晓!约好出战半小时却变卦 72岁新帅:他不该拒绝谢场

我爱英超
2026-09-28 07:14:39
陈圆将造世界名画:夺金后霸气庆祝 赛前无视他的日本选手绝望跪地

陈圆将造世界名画:夺金后霸气庆祝 赛前无视他的日本选手绝望跪地

风过乡
2026-09-28 07:37:22
穆里尼奥狂喜!皇马天降超级喜讯!巴萨这下要慌了

穆里尼奥狂喜!皇马天降超级喜讯!巴萨这下要慌了

澜归序
2026-09-28 08:25:24
李克勤演唱会迟到,救场歌手连唱近20首,本人表示:“3万人的演出这还是第一次,之前最多是五六千人”

李克勤演唱会迟到,救场歌手连唱近20首,本人表示:“3万人的演出这还是第一次,之前最多是五六千人”

韩小娱
2026-09-27 16:58:06
2026-09-28 12:36:49
新能源自动驾驶 incentive-icons
新能源自动驾驶
专注于半导体行业资讯
1015文章数 348关注度
往期回顾 全部

科技要闻

智能体惹了多少祸,OpenAI自己没查清!

头条要闻

媒体:俄杜马选举普京获压倒性胜利 俄乌冲突有2大走向

头条要闻

媒体:俄杜马选举普京获压倒性胜利 俄乌冲突有2大走向

体育要闻

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

娱乐要闻

好汉刘欢:一副好嗓子,一颗赤子心

财经要闻

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

汽车要闻

神龙科技与Momenta达成全球战略合作 2027年起推新车

态度原创

健康
亲子
房产
手机
公开课

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

亲子要闻

世界上最危险的十大玩具,为何成为了可怕的伤人利器?

房产要闻

全年霸榜TOP1!南海・叁號院周年官宣:新作即将登场

手机要闻

手机芯片追平桌面显卡!A20 Pro跑3A持平GTX 1650:能效比1180%碾压

公开课

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

无障碍浏览 进入关怀版