2.21 事件响应(0x86)——护士按铃
医生查房是按时间表来的——早上八点、下午四点。但如果病人在凌晨两点心绞痛发作,他不能等到早上八点才说话。病房里有一个按钮,按下它,护士站就会响——不是医生来找病人,而是病人呼叫医生。UDS 的 0x86 就是这个按钮。
场景:一万辆车的车队,你不可能每秒都在问“你出故障了吗“
晚上九点半,车队监控中心。
你面前的屏幕上滚动着 10,000 辆物流卡车的实时状态。每辆车上都有一个 T-Box 通过 4G 连接到云端,T-Box 内部通过 CAN 与各 ECU 通信。你们的系统协议很直接:T-Box 每 30 秒向发动机 ECU 发一次 0x19 0x01(ReadDTCInformation by status mask),如果返回的 DTC 列表有变化,就把新故障码上报云端。
10,000 辆车 × 30 秒间隔 = 每秒约 333 次诊断请求。正常运行时,99% 的 0x19 0x01 返回的是“没有新故障“——花这么多通信资源,绝大多数时间是空转。更糟的是,30 秒意味着一次偶发故障最多可能被延迟 30 秒才发现。
你盯着屏幕,脑子里在算一笔账:如果轮询间隔缩短到 5 秒来降低延迟,每秒就是 2000 次请求——服务器的 DDoS、T-Box 的电量消耗、蜂窝流量费用……这些开销换来的只有偶尔在某个返回包里的一个非零 DTC。
你想:能不能反过来?ECU 在发现新故障时,自己发消息给 T-Box,由 T-Box 转发到云端? 不轮询,不空转——只在有事情发生时才说话。
这不是你的异想天开。ISO 14229-1 早就做好了这件事,它就是:事件响应 (ResponseOnEvent),SID 0x86。
为什么需要0x86?——从周期性查房到事件驱动呼叫
我们不妨回顾一下 UDS 通信模式的演进:
【模式1: 同步请求-响应】(0x22, 0x19, 0x2E, ……)
测试仪 ──[请求]──→ ECU
测试仪 ←──[响应]── ECU
测试仪 (等待) ECU (静默)
测试仪 ──[请求]──→ ECU
测试仪 ←──[响应]── ECU
特征:测试仪完全主导节奏。ECU 只在被问到时才回答。
【模式2: 请求-周期上报】(0x2A)
测试仪 ──[注册]──→ ECU
测试仪 ←──[确认]── ECU
测试仪 ←──[数据]── ECU [t=0ms]
测试仪 ←──[数据]── ECU [t=100ms]
测试仪 ←──[数据]── ECU [t=200ms]
特征:ECU 获得了通信节奏的主动权,但仍需测试仪先注册。
上报时机由时钟驱动,而非业务逻辑驱动。
【模式3: 请求-事件上报】(0x86)
测试仪 ──[注册事件]──→ ECU "当出现新DTC时通知我
测试仪 ←──[确认]───── ECU
... (可能过了5分钟,也可能过了5小时) ...
测试仪 ←──[事件通知]── ECU "DTC P0301 出现了!
测试仪 ←──[事件通知]── ECU "DTC P0420 也出现了!
特征:ECU 获得了完全的发送主动权。并非按时钟,而是按业务事件。
测试仪的注册是"授权"而非"调度"。
0x86 是 UDS 中唯一一个ECU 主动发起数据传输的服务。它从根本上改变了通信双方的关系——从“主从“变成了“订阅发布“。
但有一个关键限制:ECU 的“主动“是有限授权的。测试仪必须先通过 0x86 注册一个事件类型,ECU 不能在自己没被注册的情况下随便说话。这就像病人可以按铃呼叫护士,但前提是护士已经把呼叫铃放到了病人床头——没有铃,你不能指望病人用意念通知你。
请求报文:事件类型决定一切
基本结构
Byte 0: SID = 0x86
Byte 1: SubFunction
· 0x01 = startResponseOnEvent(开始监听事件)
· 0x02 = stopResponseOnEvent(停止监听事件)
Byte 2: eventType(事件类型标识)
· 0x01 = onDTCStatusChange(DTC状态变化时)
· 0x02 = onTimerInterrupt(定时器触发时)
· 0x03 = onChangeOfDataIdentifier(数据变化时)
· 0x04 = onComparisonOfValues(比较条件满足时)
· 0x05 = reportMostRecentDTC(上报最近DTC)
· 0x06 = onStartOfDiagnosticSession(会话启动时)
Byte 3+: eventTypeSpecificParameters(与事件类型相关的额外参数)
事件类型详解
下图展示了每个事件类型的触发条件和典型应用场景:
┌──────────────────────────────────────────────────────────┐
│ 事件类型全景 │
├──────────────┬───────────────┬────────────────────────────┤
│ 事件类型 │ 触发条件 │ 典型应用 │
├──────────────┼───────────────┼────────────────────────────┤
│ DTC状态变化 │ 任何 DTC 的 │ 车队远程故障告警 │
│ (0x03) │ statusByte 变 │ "P0301 出现了,请立即处理" │
│ │ 化(出现/消失) │ │
├──────────────┼───────────────┼────────────────────────────┤
│ 定时器中断 │ 预设定时器到期 │ 周期性采集+存储到本地 │
│ (0x04) │ │ "每10秒记录一次关键参数" │
├──────────────┼───────────────┼────────────────────────────┤
│ 数据标识符变化│ 指定 DID 的值 │ 监控关键阈值变化 │
│ (0x05) │ 发生变化 │ "当电池电压<11.5V时通知我" │
├──────────────┼───────────────┼────────────────────────────┤
│ 比较条件满足 │ 指定比较操作 │ 灵活的条件触发 │
│ (0x06) │ 为真 │ "当转速>5000且油温>120°C" │
├──────────────┼───────────────┼────────────────────────────┤
│ 上报最近DTC │ 事件触发时 │ 与其他事件组合 │
│ (0x07) │ 顺带上报DTC │ "定时器触发时,顺便报DTC" │
├──────────────┼───────────────┼────────────────────────────┤
│ 会话启动时 │ 新诊断会话 │ 会话级初始化 │
│ (0x08) │ 被激活时 │ "进入扩展会话时自动开周期读取" │
└──────────────┴───────────────┴────────────────────────────┘
实战:注册一个 DTC 状态变化事件
回到车队监控的场景。你想让发动机 ECU 在任何一个 DTC 的 statusByte 发生变化时主动上报。请求报文如下:
请求: 86 01 03 00 7F FF FF 00 00 00 00 01
│ │ │ │ └──────────┘ │ │ │
│ │ │ │ DTC mask │ │ └─ eventWindowTime: 01=永久窗口
│ │ │ │ │ └──── serviceToRespondWith: 00=仅上报事件
│ │ │ └─ eventType: └─────── eventWindowTime: 01
│ │ └──── (0x00 = 不使用, DTC事件类型自己定义在SubFunction中)
│ └─────── SubFunction: 0x03 = onDTCStatusChange
└────────── SID: 0x86
逐字段解析:
| 字节 | 值 | 含义 |
|---|---|---|
| 0 | 0x86 | SID |
| 1 | 0x01 | startResponseOnEvent(启动监听) |
| 2 | 0x03 | onDTCStatusChange(事件类型:DTC状态变化) |
| 3 | 0x00 | eventType 参数(DTC事件不使用此字段) |
| 4-7 | 0x7F 0xFF 0xFF 0x00 | DTCStatusMask —— 0x7F=监控所有状态位,0xFFFF00=匹配所有DTC高3字节 |
| 8 | 0x00 | NumberOfStoredDTCRecords(上报多少条DTC,0=全部) |
| 9 | 0x00 | serviceToRespondWith(0=仅上报事件本身,非0=事件触发后执行指定SID) |
| 10 | 0x01 | eventWindowTime(0x01=永久激活窗口) |
ECU 正响应:
响应: C6 01
│ └─ 子功能回显
└─── 正响应 SID = 0x86 | 0x40
此后,每当 ECU 内部检测到一个 DTC 状态变化(例如 P0301 从“未激活“变为“已激活且待确认“),ECU 会主动发送一条 UUDT(Unacknowledged Unsegmented Data Transfer,无应答不分段数据传输):
UUDT: 86 03 03 01 2F 01 2F
│ │ │ └───────── DTC P0301 (0x0301), statusByte=0x2F
│ │ └─ DTC 数量 = 1
│ └─ eventType = 0x03 (DTC状态变化)
└─── SID = 0x86
这里有一个容易混淆的点:UUDT 帧的 SID 编码规则——它是 0x86 | 0x40 = 0xC6,和正响应位规则一致。 但重要的是:它不是对某个请求的直接回复——它是自发消息。
事件窗口:one-shot 还是 permanent?
0x86 有一个独特而优雅的设计:事件窗口(eventWindow)。
eventWindowTime 编码:
· 0x00 = 无限期等待(事件永不过期)
· 0x01 = 永久窗口(permanent,即使事件触发后仍保持激活)
· 0x02 ~ 0xFF = 事件窗口时间(秒),超时后自动停止监听
这里有三个层次的时间语义:
| eventWindowTime | 行为 |
|---|---|
| 0x01 (permanent) | 注册后持续监听,触发一次上报一次,触发N次上报N次,直到测试仪发 stop |
| 0x00 (infinite) | 和 permanent 类似,但在某些实现中“infinite“意味着“直到会话结束“ |
| 0x02~0xFF (N秒) | 注册后最多等待N秒。如果超时没有事件发生,ECU 发一条超时通知并自动停止。如果事件在窗口内发生,某些实现会在触发一次后自动停止 |
最典型的使用模式是 0x01(permanent)——这正是车队监控场景需要的:注册一次,永久激活,每来一个故障就报一次。
用一张状态迁移图来理解:
┌─────────┐
注册事件 │ IDLE │ (事件未注册)
───────────────→│ │
└────┬────┘
│
0x86 0x01 (start)
│
┌────▼────┐
timer触发 │ ACTIVE │ 事件条件满足
(time window) │ │──────────────┐
└────┬────┘ │
│ ▼
│ ┌──────────────┐
│ │ 发送 UUDT │
│ │ 0xC6 + event │
│ └──────┬───────┘
│ │
│ permanent? │
│ ┌── Yes ───────┘
│ │ (回到 ACTIVE,继续等待)
│ │
│ └── No → 自动回到 IDLE
│
0x86 0x02 (stop) / 会话结束 / 窗口超时
│
┌────▼────┐
│ IDLE │
└─────────┘
实现侧:事件调度器的核心逻辑
在 ECU 的 DSP 层,0x86 需要一个事件管理器来处理事件的注册、监控、触发和清理。以下是一个简化的事件管理器骨架:
#define MAX_EVENT_ENTRIES 4
typedef enum {
EVENT_IDLE = 0,
EVENT_DTC_CHANGE,
EVENT_TIMER,
EVENT_DATA_CHANGE,
EVENT_COMPARISON,
EVENT_DIAG_SESSION_START
} EventType;
typedef struct {
uint8 active; /* 此条目是否激活 */
EventType eventType; /* 事件类型 */
uint32 dtcStatusMask; /* DTC事件的status mask */
uint32 dtcMaskHigh; /* DTC事件的高位掩码 */
uint8 serviceToRespond; /* 触发后要执行的SID */
uint8 eventWindow; /* 事件窗口类型 */
uint32 windowTimer; /* 窗口计时器(毫秒) */
uint8 triggered; /* 是否已被触发(one-shot判断) */
} EventEntry;
static EventEntry gEventTable[MAX_EVENT_ENTRIES];
static void Dsp_Event_CheckAndFire(void)
{
uint8 i;
for (i = 0; i < MAX_EVENT_ENTRIES; i++)
{
if (gEventTable[i].active == 0x00)
continue;
switch (gEventTable[i].eventType)
{
case EVENT_DTC_CHANGE:
if (Dsp_Event_CheckDtcChange(&gEventTable[i]))
{
Dsp_Event_SendUUDT(&gEventTable[i]);
if (gEventTable[i].eventWindow != 0x01)
{
/* 非 permanent:触发一次后自动停止 */
gEventTable[i].active = 0x00;
}
}
break;
case EVENT_TIMER:
if (Dsp_Event_CheckTimer(&gEventTable[i]))
{
Dsp_Event_SendUUDT(&gEventTable[i]);
}
break;
case EVENT_DATA_CHANGE:
if (Dsp_Event_CheckDataChange(&gEventTable[i]))
{
Dsp_Event_SendUUDT(&gEventTable[i]);
if (gEventTable[i].eventWindow != 0x01)
{
gEventTable[i].active = 0x00;
}
}
break;
default:
break;
}
}
}
这个函数被放在 ECU 主循环里周期调用(比如每 10ms 一次)。它遍历所有激活的事件条目,检查触发条件,条件满足时构造 UUDT 帧并发送。
注意这里的关键性能约束:事件检查必须在 ECU 的主循环中执行,这意味着每个事件类型的检查函数必须轻量级。一个高效的实现会预计算比较值、用中断标记代替轮询扫描等方式优化。
与 0x2A 的微妙关系
0x86 中的 onTimerInterrupt(0x04)从行为上看很像 0x2A(周期读取)。但它们有一个本质区别:
| 0x2A 周期读取 | 0x86 onTimerInterrupt | |
|---|---|---|
| 触发 | 时钟固定周期 | 时钟固定周期 |
| 数据内容 | 固定 DID 列表的值 | 由 eventType + 附加参数决定 |
| 发送格式 | 0x6A + DID/Data 对 | UUDT (0xC6) + 事件结果 |
| 注册方式 | 0x2A 0x01 | 0x86 0x01 + eventType=0x04 |
| 是否可和其他事件组合 | 否 | 是(可以和 onDTCStatusChange 等并存) |
简而言之:0x2A 是为“持续看一组参数“设计的专用通道,0x86 onTimerInterrupt 是为“在事件驱动框架下做定时触发“设计的通用机制。后者更灵活(可以指定 serviceToRespondWith,让定时器触发其他 UDS 服务),但前者的交互更简洁。两者各有适用场景。
医院隐喻:护士呼叫铃
住院病房的床头都有一个按钮——护士呼叫铃。它的工作方式和 0x86 如出一辙:
- 注册(授权):护士把呼叫铃放在病人床头,告诉病人“有事按这个“(
0x86 0x01 start) - 事件窗口:呼叫铃在整个住院期间都有效(
eventWindowTime = 0x01 permanent) - 事件触发:病人感到不适,按下按钮(ECU 检测到 DTC 状态变化)
- 主动上报:护士站的铃声响起,显示屏上出现病房号(ECU 发送 UUDT 报文)
- 解除注册:病人出院,护士收回呼叫铃(
0x86 0x02 stop)
医生查房(轮询 0x19)是定时制的——早上八点来一次,下午四点来一次。如果病人在凌晨两点出状况,他必须等到早上八点医生来才发现。这是轮询的天然局限性。而呼叫铃(0x86)解决的就是这个时间差——第一时间通知,第一时间响应。
但这个比喻还有一个更深层的含义:呼叫铃是“病人主动“,但主动权是护士授予的。 如果没有护士把铃放在床头,病人无法呼叫护士——正如 ECU 不能在没有注册事件的情况下自行发送 UUDT。UDS 的设计者始终没有放弃“测试仪作为通信主导方“这一根本约束,0x86 只是在“主导方的授权窗口内“给了 ECU 一个有限的发言权。
| 医院元素 | UDS 对应 |
|---|---|
| 护士放铃 = 授权 | 0x86 0x01 startResponseOnEvent |
| 病人按铃 = 事件触发 | DTC statusByte 变化 / 数据超阈值 / 定时器到期 |
| 护士站响铃 = 上报 | ECU 发送 UUDT (0xC6) |
| 铃的类型(疼痛/不适/需要帮助) | 事件类型(DTC/DataChange/Timer/Comparison) |
| 收回铃 = 停止监听 | 0x86 0x02 stopResponseOnEvent |
| 铃只在住院期间有效 = 事件窗口 | eventWindowTime = 会话生命周期 |
核心洞察:
0x86 是 UDS 中唯一一个将通信不对称性翻转的服务。 在 25 个 UDS 服务中,24 个都是 “测试仪请求 → ECU 响应” 的传统客户端-服务器模式。唯有 0x86,让 ECU 在事先注册的条件下获得了“主动发言权“。这个设计不是对主从架构的背叛,而是对它的补充——它承认在某些诊断场景中,由被监控者(ECU)主动报告事件,比由监控者(测试仪)频繁轮询要高效得多。
0x86 的 eventWindow 机制也是一个值得注意的设计:它用极少的字节(1 个)同时表达了“触发后是否自动停止“(one-shot vs permanent)、“超时怎么处理”(有限窗口 vs 无限窗口)以及“是否需要服务响应“(serviceToRespondWith)多层语义。这是在资源极度受限的 CAN 协议中做语义压缩的典型案例——不追求字段的可读性,而是追求信息的密度。
本篇小结
- 0x86 ResponseOnEvent 是 UDS 中唯一让 ECU 主动发起数据传输的服务,工作模式为“注册-监听-上报“
- 与传统同步请求-响应模式不同,0x86 实现了事件驱动通信:ECU 在满足条件时推送消息,无需测试仪轮询
- 两大控制操作:
0x01startResponseOnEvent(启动监听)、0x02stopResponseOnEvent(停止监听) - 六大事件类型:DTC 状态变化、定时器中断、数据变化、比较条件满足、上报最近 DTC、会话启动
- 事件窗口(eventWindowTime)控制监听的生命周期:permanent(永久)、one-shot(触发一次即停)、有限秒数(超时自动停)
- ECU 上报使用 UUDT(Unacknowledged Unsegmented Data Transfer)格式,SID 为
0x86 | 0x40 = 0xC6 - serviceToRespondWith 参数允许事件触发后自动执行指定 UDS 服务(如触发时顺便读取数据)
- 医院隐喻:护士呼叫铃——病人(ECU)在护士授权下获得主动呼叫权,出事时立即通知而非等待查房
- 0x86 和 0x2A 的
onTimerInterrupt有功能交集但设计哲学不同:前者是通用事件框架中的定时器能力,后者是专用的周期数据推送通道
【下集预告】:事件响应让ECU获得了主动发言权——但ECU内部的数据结构远不止DID和DTC。现代ECU运行着文件系统,存储着标定文件、日志记录、固件镜像……UDS有办法直接操作这些文件吗?0x38 RequestFileTransfer——把诊断仪变成ECU的文件管理器。