4、诊断服务实现(DSL):DSL子模块划分、服务调度机制、P2与P2*定时器管理
好,咱们今天聊聊诊断栈里最核心的干活部分——DSL(Diagnostic Session Layer)。说白了,它就是负责把诊断请求拆开、分发、再组装回去的那一层。我做了这么多年,发现很多新手工程师容易把DSL和DSP(诊断服务处理层)搞混。其实很简单:DSL管的是“怎么传”,DSP管的是“传什么”。
4.1 DSL子模块划分
我个人习惯把DSL拆成三个子模块。为什么是三个?你想想看,一个诊断请求从CAN总线上进来,总得有人接、有人查、有人回吧?
- 请求接收模块(DSL_Req):负责从PDU层拿数据,做最基础的格式检查。比如长度对不对、格式对不对。我在项目中遇到过,有个同事没做长度检查,结果一个超长报文直接把接收缓冲区撑爆了,整个ECU都挂了。
- 服务调度模块(DSL_Sched):这是大脑。它决定当前请求该由哪个DSP服务去处理。比如0x10是诊断会话控制,0x22是读取数据,调度器会根据SID(服务标识符)做路由。
- 响应发送模块(DSL_Resp):处理完的结果,由它打包发回去。包括正响应和负响应。嗯,这里要注意,负响应的优先级其实比正响应高,因为ECU得先告诉客户端“我出错了”。
核心原则:DSL的三个子模块之间,只能通过内部接口通信,不能直接调用DSP的服务函数。这是分层架构的基本要求,也是我踩过坑之后才深刻理解的。
4.2 服务调度机制
服务调度,说白了就是“谁来干活”的问题。DSL_Sched收到一个请求后,会做以下几件事:
- SID匹配:查表,看这个SID有没有注册过。没注册?直接回NRC 0x11(服务不支持)。
- 会话检查:当前会话模式是否允许这个服务?比如0x31(例程控制)在默认会话里是不允许的。我建议把会话权限表做成静态数组,查起来快,也容易维护。
- 安全访问检查:有些服务需要解锁才能用。比如0x2E(写入数据),没解锁就直接拒绝。
- 调用DSP处理函数:通过函数指针数组来调用。这样新增一个服务,只需要在数组里加一项,不用改调度逻辑。
/* 服务调度表示例 */
typedef struct {
uint8_t SID;
uint8_t (*handler)(uint8_t* data, uint16_t len);
} ServiceEntry_t;
static ServiceEntry_t serviceTable[] = {
{0x10, DiagSessCtrl_Handler},
{0x22, ReadDataByIdentifier_Handler},
{0x2E, WriteDataByIdentifier_Handler},
/* 新增服务只需在这里加 */
};
uint8_t DS L_ScheduleService(uint8_t sid, uint8_t* data, uint16_t len) {
for (int i = 0; i < sizeof(serviceTable)/sizeof(ServiceEntry_t); i++) {
if (serviceTable[i].SID == sid) {
return serviceTable[i].handler(data, len);
}
}
return NRC_SERVICE_NOT_SUPPORTED;
}
小技巧:函数指针数组的查找效率是O(n),如果服务很多(比如超过50个),可以考虑用哈希表或者二分查找。不过一般诊断服务也就二三十个,线性查找足够了。
4.3 P2与P2*定时器管理
P2和P2*,这两个定时器是诊断通信里最容易出问题的地方。我曾经在一个项目里,就因为P2定时器配置错了,导致ECU在产线上总是超时,差点被退货。
P2定时器:服务器处理请求的最大时间。客户端发出请求后,服务器必须在P2时间内给出响应。如果处理不完,就得先发一个NRC 0x78(请求正确接收,正在处理),然后进入P2*模式。
P2*定时器:扩展响应时间。当服务器发了0x78之后,客户端会等待P2*的时间。如果P2*也超时了,客户端就会认为通信失败。
| 参数 | 典型值 | 说明 |
|---|---|---|
| P2_Server_Max | 50 ms | 正常响应时间,超过则发0x78 |
| P2*_Server_Max | 5000 ms | 扩展响应时间,超过则超时 |
| P2_Client_Max | 50 ms + 抖动 | 客户端等待时间,一般比服务器略长 |
定时器的实现,我建议用硬件定时器+软件计数的方式。为什么?因为诊断服务可能同时有多个会话在跑,每个会话都需要独立的P2计时。硬件定时器数量有限,软件计数可以无限扩展。
/* P2定时器管理示例 */
typedef struct {
uint16_t p2Counter; /* P2倒计时 */
uint16_t p2StarCounter; /* P2*倒计时 */
uint8_t sessionState; /* 当前会话状态 */
} DiagSession_t;
void DS L_TimerTick(DiagSession_t* session) {
if (session->p2Counter > 0) {
session->p2Counter--;
if (session->p2Counter == 0) {
/* P2超时,进入P2*模式 */
session->p2StarCounter = P2STAR_MAX;
SendResponse(0x78); /* 发送等待响应 */
}
}
if (session->p2StarCounter > 0) {
session->p2StarCounter--;
if (session->p2StarCounter == 0) {
/* P2*超时,终止当前服务 */
TerminateCurrentService(session);
}
}
}
避坑指南:我曾经在P2定时器里直接调用了发送函数,结果造成了递归调用——发送函数又触发了定时器中断,系统直接死机。记住:定时器中断里只做计数,不要做任何I/O操作。把状态变化留给主循环去处理。
还有一个容易忽略的点:P2和P2*的时间单位。有些芯片的定时器是1ms一个tick,有些是10ms。我建议统一用1ms作为基本单位,这样配置起来更灵活。如果硬件定时器精度不够,可以用软件累加的方式补偿。
嗯,DSL这块其实还有很多细节,比如多会话管理、请求序列化、响应缓存等等。但核心就是这三个部分:子模块划分、服务调度、定时器管理。把这三点搞清楚了,DSL的骨架就搭起来了。剩下的,就是往里面填肉了。