Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

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

逐字段解析:

字节含义
00x86SID
10x01startResponseOnEvent(启动监听)
20x03onDTCStatusChange(事件类型:DTC状态变化)
30x00eventType 参数(DTC事件不使用此字段)
4-70x7F 0xFF 0xFF 0x00DTCStatusMask —— 0x7F=监控所有状态位,0xFFFF00=匹配所有DTC高3字节
80x00NumberOfStoredDTCRecords(上报多少条DTC,0=全部)
90x00serviceToRespondWith(0=仅上报事件本身,非0=事件触发后执行指定SID)
100x01eventWindowTime(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 0x010x86 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 协议中做语义压缩的典型案例——不追求字段的可读性,而是追求信息的密度。


本篇小结

  1. 0x86 ResponseOnEvent 是 UDS 中唯一让 ECU 主动发起数据传输的服务,工作模式为“注册-监听-上报“
  2. 与传统同步请求-响应模式不同,0x86 实现了事件驱动通信:ECU 在满足条件时推送消息,无需测试仪轮询
  3. 两大控制操作:0x01 startResponseOnEvent(启动监听)、0x02 stopResponseOnEvent(停止监听)
  4. 六大事件类型:DTC 状态变化、定时器中断、数据变化、比较条件满足、上报最近 DTC、会话启动
  5. 事件窗口(eventWindowTime)控制监听的生命周期:permanent(永久)、one-shot(触发一次即停)、有限秒数(超时自动停)
  6. ECU 上报使用 UUDT(Unacknowledged Unsegmented Data Transfer)格式,SID 为 0x86 | 0x40 = 0xC6
  7. serviceToRespondWith 参数允许事件触发后自动执行指定 UDS 服务(如触发时顺便读取数据)
  8. 医院隐喻:护士呼叫铃——病人(ECU)在护士授权下获得主动呼叫权,出事时立即通知而非等待查房
  9. 0x86 和 0x2A 的 onTimerInterrupt 有功能交集但设计哲学不同:前者是通用事件框架中的定时器能力,后者是专用的周期数据推送通道

【下集预告】:事件响应让ECU获得了主动发言权——但ECU内部的数据结构远不止DID和DTC。现代ECU运行着文件系统,存储着标定文件、日志记录、固件镜像……UDS有办法直接操作这些文件吗?0x38 RequestFileTransfer——把诊断仪变成ECU的文件管理器。