4、诊断服务实现(上):0x10诊断会话控制服务实现、0x27安全访问服务实现、0x22读取数据标识符服务实现
各位同学,今天我们正式开始啃诊断栈的硬骨头——服务实现。这部分内容,说白了就是ECU和诊断仪之间的“对话协议”。你想想看,车上的ECU那么多,诊断仪怎么知道该跟谁说话?怎么确认对方身份?怎么读取数据?这三个服务就是干这个的。
我个人习惯把诊断服务分成两类:一类是“控制类”,比如会话切换、安全解锁;另一类是“数据类”,比如读数据、写数据。今天讲的三个服务,正好覆盖了这两类。咱们一个一个来拆解。
4.1 0x10诊断会话控制服务
先说说0x10服务。这个服务是诊断通信的“大门”。ECU上电后,默认处于默认会话(Default Session)。在这个会话里,你只能做点基本操作,比如读个VIN码。想干点“大事”?不行,得先切换会话。
UDS标准定义了三种会话:默认会话(0x01)、扩展会话(0x02)、编程会话(0x03)。扩展会话用于配置参数,编程会话用于刷写固件。我见过不少新手,上来就想刷写,结果发现ECU根本不响应——因为还在默认会话里呢。
实现上,我们需要维护一个会话状态机。看代码:
typedef enum {
SESSION_DEFAULT = 0x01,
SESSION_EXTENDED = 0x02,
SESSION_PROGRAMMING = 0x03
} SessionType;
static SessionType g_currentSession = SESSION_DEFAULT;
static uint32_t g_sessionTimer = 0;
// 会话切换处理
UdsResponse_t SessionControl_Handler(uint8_t targetSession) {
// 检查目标会话是否合法
if (targetSession != SESSION_DEFAULT &&
targetSession != SESSION_EXTENDED &&
targetSession != SESSION_PROGRAMMING) {
return UDS_NRC_INVALID_FORMAT; // 0x13
}
// 检查切换路径是否合法
if (g_currentSession == SESSION_EXTENDED &&
targetSession == SESSION_PROGRAMMING) {
return UDS_NRC_CONDITIONS_NOT_CORRECT; // 0x22
}
// 执行切换
g_currentSession = targetSession;
g_sessionTimer = 0; // 重置S3定时器
// 返回肯定响应
return UDS_POSITIVE_RESPONSE;
}
这里有个坑——S3定时器。每个会话都有超时时间,默认会话一般是5秒,扩展和编程会话可能更长。超时后ECU会自动切回默认会话。我曾经在一个项目里,S3定时器忘了复位,结果ECU总是莫名其妙地跳回默认会话,排查了两天才找到原因。
4.2 0x27安全访问服务
0x27服务,说白了就是“解锁”。你想写数据、刷固件?先证明你有权限。UDS用的是种子-密钥机制:ECU发一个随机种子,诊断仪用算法算出密钥,ECU验证通过后解锁。
实现分两步:
- 请求种子(0x27 0x01):ECU生成随机种子,返回给诊断仪
- 发送密钥(0x27 0x02):诊断仪计算密钥,ECU验证
看代码实现:
static uint32_t g_seed = 0;
static uint8_t g_securityLevel = 0;
static uint8_t g_attemptCount = 0;
UdsResponse_t SecurityAccess_SeedHandler(uint8_t level) {
// 检查安全等级是否支持
if (level != 1 && level != 3) { // 假设只支持等级1和3
return UDS_NRC_INVALID_FORMAT;
}
// 防暴力破解:连续失败3次后锁定
if (g_attemptCount >= 3) {
return UDS_NRC_EXCEEDED_NUMBER_OF_ATTEMPTS; // 0x36
}
// 生成随机种子
g_seed = rand() % 0xFFFFFFFF;
g_securityLevel = level;
// 返回种子
return UDS_POSITIVE_RESPONSE;
}
UdsResponse_t SecurityAccess_KeyHandler(uint32_t key) {
uint32_t expectedKey;
// 计算期望密钥
expectedKey = CalculateKey(g_seed, g_securityLevel);
if (key == expectedKey) {
g_attemptCount = 0; // 成功,重置计数
return UDS_POSITIVE_RESPONSE;
} else {
g_attemptCount++;
return UDS_NRC_INVALID_KEY; // 0x35
}
}
密钥算法怎么选?我个人建议用AES-128或者自定义的异或+移位算法。AES安全性高,但计算量大;自定义算法轻量,但容易被逆向。看你的项目需求——量产车用AES,工装设备用自定义就够了。
4.3 0x22读取数据标识符服务
0x22服务,就是读数据。ECU内部有很多数据,比如VIN码、软件版本、故障码数量等,每个数据都有一个DID(数据标识符),2个字节。
实现上,我们需要维护一个DID映射表:
typedef struct {
uint16_t did;
uint8_t* dataPtr;
uint8_t dataLength;
uint8_t (*readCallback)(uint8_t* buffer, uint16_t* length);
} DidEntry_t;
static DidEntry_t g_didTable[] = {
{0xF190, NULL, 17, ReadVIN}, // VIN码,17字节
{0xF195, NULL, 4, ReadSWVersion}, // 软件版本,4字节
{0xF197, NULL, 2, ReadDTCCount}, // 故障码数量,2字节
};
UdsResponse_t ReadDataByIdentifier_Handler(uint16_t did) {
// 查找DID
for (int i = 0; i < sizeof(g_didTable)/sizeof(DidEntry_t); i++) {
if (g_didTable[i].did == did) {
uint8_t buffer[256];
uint16_t length = 0;
// 调用回调函数读取数据
if (g_didTable[i].readCallback(buffer, &length) == 0) {
// 返回数据
return UDS_POSITIVE_RESPONSE;
} else {
return UDS_NRC_GENERAL_REJECT; // 0x10
}
}
}
return UDS_NRC_REQUEST_OUT_OF_RANGE; // 0x31
}
这里有个细节——数据长度。UDS规定,响应报文里要先返回DID,再返回数据。比如读VIN码,响应是:0x62 0xF1 0x90 + 17字节VIN。长度不对,诊断仪会报错。我踩过这个坑,有一次VIN码长度写成了16字节,结果诊断仪一直显示“数据格式错误”。
4.4 三个服务的协同工作
这三个服务不是孤立的。你想想看:
- 想读某些敏感数据(比如密钥),必须先进入扩展会话(0x10),再解锁安全访问(0x27),最后才能用0x22读取
- 会话切换后,安全解锁状态会丢失,需要重新解锁
- S3定时器超时后,会话和安全状态都会重置
我建议在实现时,把这三个服务的状态机统一管理。比如:
typedef struct {
SessionType session;
uint8_t securityLevel;
uint8_t isUnlocked;
uint32_t s3Timer;
} DiagState_t;
static DiagState_t g_diagState = {
.session = SESSION_DEFAULT,
.securityLevel = 0,
.isUnlocked = 0,
.s3Timer = 0
};
这样,每个服务在处理请求时,都可以检查当前状态是否允许操作。代码清晰,也不容易出bug。
好了,今天的内容就到这里。代码示例我都放在附件里了,建议大家动手跑一跑。下节课我们继续讲剩下的服务——0x2E写入、0x31例程控制、0x34请求下载。到时候见。
公众号:蓝海资料掘金营,微信deep3321