![]()
引言
隨著汽車電子電氣架構日益復雜,電子控制單元(ECU)的數量和功能不斷增加。ECU軟件的在線升級(OTA)或售后診斷刷寫成為不可或缺的技術。統一診斷服務(UDS)協議作為國際標準(ISO 14229),為診斷通信和刷寫功能提供了統一框架。本文將深入講解基于UDS的刷寫流程,并給出C++代碼示例,幫助開發人員快速掌握診斷刷寫實現方法。
一、UDS診斷協議基礎
UDS(Unified Diagnostic Services)位于OSI模型的第五層(會話層)和第七層(應用層),本質上是服務集合。診斷儀(Tester)向ECU發送請求(Request),ECU返回肯定響應(Positive Response)或否定響應(Negative Response)。
1.1 常用術語
術語
SID
服務標識符,例如0x10會話控制、0x27安全訪問
DID
數據標識符,用于讀取或寫入特定數據
RID
例程控制標識符,用于執行耗時操作(如擦除、校驗)
NRC
否定響應碼,如0x12子功能不支持、0x35非法密鑰
DTC
診斷故障代碼
1.2 請求與響應格式
診斷請求格式:
格式1:
[SID] + [Sub-function]格式2:
[SID] + [DID]格式3:
[SID] + [Sub-function] + [DID]
肯定響應:對應SID最高位置1,即SID + 0x40,后跟參數。否定響應:0x7F + SID + NRC(例如7F 27 35表示安全訪問服務非法密鑰)。
1.3 尋址模式
物理尋址:點對點,訪問單個ECU(標準幀CAN ID通常為ECU物理地址,如
0x701)。功能尋址:廣播,一對多(標準幀常用
0x7DF)。
單幀(SF)和多個幀(首幀FF、流控幀FC、連續幀CF)管理數據長度超過8字節的通信。
單幀:首字節高4位為
0,低4位為數據長度(如0x03表示3字節數據)。首幀:首字節高4位為
1,低4位及第二字節組合表示總數據長度。流控幀:首字節高4位為
3,低4位為流狀態(0=繼續發送),后續字節為塊大小和最小間隔。連續幀:首字節高4位為
2,低4位為序列號(從1開始遞增)。
刷寫過程分為預編程、主編程、后編程三個階段。
2.1 預編程步驟(功能尋址,廣播所有ECU)
服務及其參數
10 03
進入擴展會話模式,禁止ECU間正常通信并關閉DTC存儲
3E 80
周期性發送在線請求,維持非默認會話
31 01 02 03
檢查編程前置條件
85 02
關閉DTC設置
28 03 03
禁止非診斷報文發送與接收
22 xx yy
讀取被刷寫ECU的狀態(如當前軟件版本)
2.2 主編程步驟(物理尋址,點對點)
服務及其參數
10 02
進入編程會話,ECU進入Bootloader
27 09/0A
請求種子/發送密鑰(安全訪問)
31 01 FF 00
擦除內存(例程控制)
2E F1 5A
寫入指紋信息(診斷儀標識)
34 xx yy zz
請求下載(指定地址、長度)
36 00 + data
傳輸數據(多次調用)
37
請求退出傳輸
31 01 02 02
檢查編程完整性(如校驗和)
31 01 FF 01
檢查依賴性和一致性
11 01
ECU硬復位
2.3 后編程步驟(功能尋址)
服務及其參數
10 03
再次進入擴展會話(復位后的ECU需同步)
28 00 03
開啟非診斷報文收發
85 01
開啟DTC設置
10 01
回到默認會話,停止發送3E 80
14 FF FF FF
清除所有DTC(物理尋址)
三、C++代碼舉例
以下代碼實現一個簡單的UDS診斷通信類,支持單幀/多幀處理,并實現刷寫核心步驟。
3.1 診斷通信類定義(UDSClient.h)
3.2 關鍵方法實現(UDSClient.cpp) 3.2.1 單幀請求#pragma once
#include
#include
// 模擬CAN數據幀(實際項目需替換為硬件接口)
struct CanFrame {
uint32_t id; // CAN ID
uint8_t data[8]; // CAN數據
uint8_t len; // 有效數據長度(實際CAN幀長度通常為8)
};
class UDSClient {
public:
UDSClient(uint32_t physAddr, uint32_t funcAddr);
virtual ~UDSClient() = default;
// 底層CAN發送(需用戶實現)
virtual bool sendCanFrame(const CanFrame& frame);
// 底層CAN接收,帶超時(ms)
virtual bool receiveCanFrame(CanFrame& frame, int timeoutMs);
// ----- UDS服務 -----
// 單幀請求(不超過8字節)并等待響應
std::vector requestRaw(const std::vector& req, int timeoutMs = 100);
// 多幀發送(自動分包,ISO 15765-2)
bool requestMultiFrame(const std::vector& fullData, int timeoutMs = 1000);
// 高等級服務封裝
bool diagnosticSessionControl(uint8_t sessionType, bool physAddr = true);
bool securityAccess(uint8_t level, const std::vector& key = {});
bool routineControl(uint16_t rid, uint8_t subfunc, const std::vector& data = {});
bool requestDownload(uint32_t address, uint32_t size);
bool transferData(uint8_t blockSeq, const std::vector& data);
bool requestTransferExit();
bool ecuReset(uint8_t resetType);
// 刷寫流程示例
bool performFlash(const std::vector& firmware, uint32_t startAddr);
private:
// 解析響應,如果是否定響應則拋出NRC異常
void checkResponse(const std::vector& resp, uint8_t expectedSid);
// 組裝多幀發送
void buildMultiFrame(const std::vector& data, std::vector & frames) ;
// 接收流控幀并發送連續幀
bool sendMultiFrameData(const std::vector & cfFrames, int timeoutMs);uint32_t physId; // 物理尋址CAN ID
uint32_t funcId; // 功能尋址CAN ID
};
3.2.2 多幀發送(ISO 15765-2實現)std::vector UDSClient::requestRaw(const std::vector& req, int timeoutMs) {
if (req.size() > 8) return {}; // 多幀請調用requestMultiFrame
CanFrame frame;
frame.id = funcId; // 根據實際場合可動態切換為physId
frame.len = 8;
frame.data[0] = (req.size() & 0x0F); // 單幀標識+長度
memcpy(&frame.data[1], req.data(), req.size());
if (req.size() < 7) {
// 未使用字節填充為0xAA或0x55,依規范
memset(&frame.data[1 + req.size()], 0xAA, 7 - req.size());
}if (!sendCanFrame(frame)) return {};
CanFrame resp;
if (!receiveCanFrame(resp, timeoutMs)) return {};
return std::vector(resp.data, resp.data + resp.len);
}
3.2.3 安全訪問(27服務)bool UDSClient::requestMultiFrame(const std::vector& fullData, int timeoutMs) {
std::vector frames;
buildMultiFrame(fullData, frames);
if (frames.empty()) return false;
// 發送首幀
if (!sendCanFrame(frames[0])) return false;
// 等待流控幀
CanFrame fc;
if (!receiveCanFrame(fc, timeoutMs)) return false;
if ((fc.data[0] >> 4) != 3) return false; // 不是流控幀
uint8_t flowStatus = fc.data[0] & 0x0F;
if (flowStatus != 0) return false; // 非繼續發送,錯誤處理
// 發送剩余連續幀
for (size_t i = 1; i < frames.size(); ++i) {
if (!sendCanFrame(frames[i])) return false;
}
return true;
}
void UDSClient::buildMultiFrame(const std::vector& data, std::vector & frames) {
size_t totalLen = data.size();
frames.clear();
// 首幀:1 + 0A(高4位1,低4位高位字節),第二字節為長度低8位
CanFrame first;
first.len = 8;
first.id = physId; // 多幀通常用物理尋址
first.data[0] = 0x10 | ((totalLen >> 8) & 0x0F);
first.data[1] = totalLen & 0xFF;
size_t offset = 0;
size_t copyLen = std::min((size_t)6, totalLen);
memcpy(&first.data[2], data.data(), copyLen);
offset += copyLen;
frames.push_back(first);uint8_t seq = 1;
while (offset < totalLen) {
CanFrame cf;
cf.len = 8;
cf.id = physId;
cf.data[0] = 0x20 | (seq++ & 0x0F);
size_t toCopy = std::min((size_t)7, totalLen - offset);
memcpy(&cf.data[1], data.data() + offset, toCopy);
// 剩余字節填充
if (toCopy < 7) memset(&cf.data[1 + toCopy], 0xAA, 7 - toCopy);
frames.push_back(cf);
offset += toCopy;
}
}
3.2.4 請求下載(34服務)bool UDSClient::securityAccess(uint8_t level, const std::vector& key) {
// 請求種子
std::vector req = {0x27, level}; // level如0x09
auto resp = requestRaw(req);
if (resp.empty()) return false;
checkResponse(resp, 0x27); // 內部檢查是否否定響應
// 肯定響應格式:67 level seed[0..n]
if ((resp[0] != 0x67) || (resp.size() < 3)) return false;
std::vector seed(resp.begin() + 2, resp.end());
// 此處應調用外部算法計算密鑰(例:seed解碼)
std::vector computedKey = seed; // 實際需實現安全算法
if (!key.empty()) computedKey = key;// 發送密鑰
std::vector sendKey = {0x27, static_cast(level + 1)};
sendKey.insert(sendKey.end(), computedKey.begin(), computedKey.end());
auto keyResp = requestRaw(sendKey);
if (keyResp.empty()) return false;
checkResponse(keyResp, 0x27);
return (keyResp[0] == 0x67);
}
3.2.5 數據傳輸(36服務)及退出(37)bool UDSClient::requestDownload(uint32_t address, uint32_t size) {
// 格式:34 + 地址長度(4字節) + 地址 + 大小長度(4字節) + 大小
std::vector req = {0x34};
req.push_back(0x44); // 地址長度=4字節,內存大小長度=4字節(常用組合)
// 地址高位在前
req.push_back((address >> 24) & 0xFF);
req.push_back((address >> 16) & 0xFF);
req.push_back((address >> 8) & 0xFF);
req.push_back(address & 0xFF);
// 大小
req.push_back((size >> 24) & 0xFF);
req.push_back((size >> 16) & 0xFF);
req.push_back((size >> 8) & 0xFF);
req.push_back(size & 0xFF);auto resp = requestRaw(req);
if (resp.empty()) return false;
checkResponse(resp, 0x34);
// 響應:74 + 最大長度(可選)
return (resp[0] == 0x74);
}
3.3 刷寫主流程示例bool UDSClient::transferData(uint8_t blockSeq, const std::vector& data) {
std::vector req = {0x36, blockSeq};
req.insert(req.end(), data.begin(), data.end());
// 如果總長度小于8字節,使用單幀;否則需多幀。為簡化,假定data不超過7字節
auto resp = requestRaw(req);
if (resp.empty()) return false;
checkResponse(resp, 0x36);
return (resp[0] == 0x76);
}bool UDSClient::requestTransferExit() {
std::vector req = {0x37};
auto resp = requestRaw(req);
if (resp.empty()) return false;
checkResponse(resp, 0x37);
return (resp[0] == 0x77);
}
bool UDSClient::performFlash(const std::vector& firmware, uint32_t startAddr) {
// 1. 預編程(假設已通過功能尋址完成,此處僅作示例)
// 切換到物理尋址,進入編程會話
if (!diagnosticSessionControl(0x02, true)) return false;
// 2. 安全訪問(種子密鑰算法假設已實現)
if (!securityAccess(0x09)) return false;
// 3. 寫入指紋(可選)
std::vector fingerprint = {0x54, 0x65, 0x73, 0x74}; // "Test"
std::vector writeFinger = {0x2E, 0xF1, 0x5A};
writeFinger.insert(writeFinger.end(), fingerprint.begin(), fingerprint.end());
auto fpResp = requestRaw(writeFinger);
if (fpResp.empty() || (fpResp[0] != 0x6E)) return false;
// 4. 擦除內存(例程控制)
if (!routineControl(0xFF00, 0x01)) return false; // 啟動擦除
// 5. 請求下載
if (!requestDownload(startAddr, firmware.size())) return false;
// 6. 分塊傳輸數據(每個塊最多7字節,實際會使用多幀或循環36服務)
const size_t blockSize = 7;
uint8_t seq = 0;
for (size_t off = 0; off < firmware.size(); off += blockSize) {
size_t len = std::min(blockSize, firmware.size() - off);
std::vector block(firmware.begin() + off, firmware.begin() + off + len);
if (!transferData(seq++, block)) return false;
}
// 7. 傳輸退出
if (!requestTransferExit()) return false;
// 8. 完整性校驗
if (!routineControl(0x0202, 0x01)) return false;
// 9. ECU復位
if (!ecuReset(0x01)) return false;
return true;
}void UDSClient::checkResponse(const std::vector& resp, uint8_t expectedSid) {
if (resp.empty()) throw std::runtime_error("No response");
if (resp[0] == 0x7F) {
if (resp.size() >= 3)
throw std::runtime_error("NRC: 0x" + std::to_string(resp[2]));
throw std::runtime_error("Negative response");
}
if ((resp[0] != (expectedSid + 0x40))) {
throw std::runtime_error("Unexpected response SID");
}
}
注意:以上代碼為教學示例,實際產品需考慮超時重傳、流控幀的塊大小限制、序列號正確管理、多幀響應接收等完整ISO 15765-2實現。四、總結
汽車診斷刷寫是一項嚴謹的系統工程,要求開發人員深入理解UDS協議棧、NRC錯誤處理、尋址模式以及ISO 15765-2傳輸層。本文從原理出發,結合C++核心類設計,展示了從單幀/多幀通信到安全訪問、下載等關鍵步驟的實現。實際應用中還需適配不同ECU的個性化要求(如安全算法、DID/RID定義等),希望本示例能為汽車電子軟件工程師提供實用的參考起點。
特別聲明:以上內容(如有圖片或視頻亦包括在內)為自媒體平臺“網易號”用戶上傳并發布,本平臺僅提供信息存儲服務。
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.