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:服務 IDDID:高字節 + 低字節(例如F1 90)可連續請求多個 DID(但多數實現一次只請求一個,避免長度過長)
正響應:
62 DID dataRecord62:響應服務 IDDID:回顯請求中的 DIDdataRecord:該 DID 對應的數據字節(長度由 DID 定義決定)
負響應:
7F 22 NRC7F:負響應固定值22:原始請求服務 IDNRC:負響應碼(見下文)
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;
}
輸出示例(實際運行時):
二、$2E WriteDataByIdentifier(按標識符寫數據) 2.1 功能說明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 33Request (unlocked): 22 F1 88
Response: 62 F1 88 01 02 03
$2E服務用于診斷儀向 ECU 寫入一個 DID 對應的數據值。典型應用包括寫入 VIN 碼、配置字、車架號、標定參數等。注意:可讀 DID 未必可寫,寫操作通常受更嚴格的安全和條件約束。
2.2 協議格式
請求:
2E DID dataRecord2E:服務 IDDID:兩字節標識符dataRecord:要寫入的數據字節流(長度由 DID 定義決定)
正響應:
6E DID(僅回顯 DID,不返回數據內容)負響應:
7F 2E 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
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 排查思路無論遇到哪個服務的負響應,都可以按照以下金字塔順序排查:
服務/子功能支持性(
0x12,0x31)
檢查對方 ECU 是否實現了該 SID 或 RID/ DID。安全訪問狀態(
0x33)
多數寫操作和敏感讀取需要先執行$27服務解鎖。會話模式(
0x7F常伴隨0x12或0x31)
某些服務僅在擴展會話下可用。報文長度/格式(
0x13)
核對 DID 定義的長度,是否漏傳或多傳了數據。運行條件(
0x22)
是否需要車輛處于特定狀態(ON檔、發動機停止、車速為零)。調用順序(
0x24)
對于$31,是否嚴格遵循 Start → (Stop) → RequestResults 的順序。內部執行錯誤(
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.