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.19 周期读取(0x2A)——24小时心电图

如果把ECU比作一个病人,测试仪就是医生的听诊器。但有些时候,一张静态的化验单不够,医生需要一份24小时动态心电图——不是“你现在怎么样“,而是“你这一天都在发生什么“。


场景:那条路有一截坡,发动机在2800转的时候会抖

凌晨六点半,试车跑道才刚刚苏醒。你把诊断仪连上一台工程样车,准备一趟30分钟的路试。

“师傅开慢点,我要盯着数据。“你对试车员说。

试车员点头。你打开诊断仪,熟练地发出一条 0x22 读取发动机转速的请求:

02 22 01 0C 00 00 00 00

ECU回应:

03 62 01 0C 0A F0 00 00

DID 0x010C 是发动机转速,返回值为 0x0AF0 = 2800 rpm。好,当前怠速正常。

车子驶出停车场,上了测试道路。你知道第三公里处有一段缓坡,发动机在那个工况下会偶发性抖动。问题是:抖动不是持续发生的——它只在某个转速区间的某个负荷点出现。你现在的做法是:每隔两秒手动发一次 0x22,然后盯着屏幕上的十六进制数字在脑海里做计算:

02 22 01 0C ... → 0x0B40 = 2880 rpm
02 22 01 0C ... → 0x0B60 = 2912 rpm
02 22 01 0C ... → 0x0B80 = 2944 rpm
02 22 01 0C ... → 0x09C0 = 2496 rpm ← ???

你手慢了,没看清那一下掉转速的瞬间。而且你发现:在30分钟的测试里,你发了将近 900 条 0x22 请求,CAN 总线的负载被你一个人的诊断流量抬高了将近 8%。更糟糕的是,你还要同时看节气门开度、进气歧管压力、点火提前角——你根本不可能同时手动轮询四个 DIDs,还要保证足够的采样密度来捕捉那个转瞬即逝的异常。

你关掉诊断仪,靠在座位上叹了口气。

如果 ECU 不是等你来问,而是自己每隔一段时间就主动汇报一下数据——就像医生给病人挂上一个24小时动态心电图仪,病人回家正常生活,第二天医生读报告就行——那该多好。

ISO 14229-1 的设计者也是这么想的。这个念头最终结晶为 SID 0x2A:周期数据读取 (ReadDataByPeriodicIdentifier)。


为什么需要0x2A?——从“我问你答”到“你定时报”

让我们把问题抽象一下。

0x22 (ReadDataByIdentifier) 解决的是静态快照问题:测试仪想知道某一个时刻某一个参数的值,发送请求,收到响应。这是一次性的、同步的、“看一眼就走“的操作。

它的工作模式用一张图来表示:

    测试仪                        ECU
      |                           |
      |------- 0x22 + DID ------->|  "现在RPM是多少?
      |<------ 0x62 + Data -------|  "2800
      |                           |  (静默)
      |                           |  (静默)
      |                           |  (静默)
      |------- 0x22 + DID ------->|  "现在RPM是多少?
      |<------ 0x62 + Data -------|  "2912
      |                           |  (静默)
      |                           |  (静默)
      |                           |  ...循环下去...

这在以下几个场景中是灾难性的:

  1. 轮询开销:每获取一次数据就要走一个完整的请求—响应循环。100ms 的采样周期意味着每秒 10 次 CAN 帧交互,这在多 ECU 共享的 CAN 总线上不是小事。
  2. 采样抖动:实际采样间隔 = 测试仪软件周期 + CAN 总线仲裁延迟 + ECU 响应时间。在满载总线上,这个抖动可以达到几十毫秒——对于分析发动机瞬态工况来说,这个精度太粗糙了。
  3. 人机瓶颈:你只有一双手一双眼睛。要同时监控4个参数,要么写自动化脚本,要么放弃。

UDS 设计者的思路很清晰:既然测试仪已经明确表示“我接下来一段时间会持续关心这几个参数“,那不妨让 ECU 自己把这个任务接管过去。 ECU 内部有精确的定时器,对自身资源的访问也没有总线延迟,它来做周期上报比测试仪轮询高效得多。

这就引出了两种读取模式的本质区别:

服务触发方式数据流向比喻
0x22测试仪主动请求请求→响应(一问一答)医生用听诊器听一下
0x2AECU 定时发送一次注册→多次主动上报24小时动态心电图仪

用一个更形象的方式理解0x2A的工作流程:

    测试仪                        ECU
      |                           |
      |-- 0x2A 0x01 + rate + DIDs ->|  "开始周期上报,每100ms一次
      |<-- 0x6A 0x01 --------------|  "收到,已启动
      |                           |  
      |<-- 0x6A DID1=... DID2=... |  [t=0ms]  主动上报
      |<-- 0x6A DID1=... DID2=... |  [t=100ms] 主动上报
      |<-- 0x6A DID1=... DID2=... |  [t=200ms] 主动上报
      |<-- 0x6A DID1=... DID2=... |  [t=300ms] 主动上报
      |                           |       ...持续...
      |                           |  
      |-- 0x2A 0x02 + DIDs ------->|  "停止周期上报
      |<-- 0x6A 0x02 --------------|  "收到,已停止
      |                           |  (恢复静默)

这就是0x2A的本质:一次注册,持续订阅。


请求报文:不只是“传个DID“那么简单

0x2A的请求格式比0x22复杂不少,因为它需要同时传递三件事:操作类型上报频率关心的参数列表

完整的请求格式如下:

Byte 0: SID = 0x2A
Byte 1: SubFunction (操作子功能)
Byte 2: TransmissionRate (上报频率编码)
Byte 3+: dataIdentifier[] (DID列表,每个2字节,数量由报文长度隐含)

SubFunction 子功能

0x2A 定义了三种操作:

名称含义
0x01startPeriodicTransmission启动周期上报
0x02stopPeriodicTransmission停止周期上报
0x03startAndRead启动周期上报,并立即读取一次

0x03 是一个很贴心的设计:你发起周期读取后,第一个数据包要等到下一个定时器周期才会发出。如果周期设的是1秒,那你就要干等1秒才能看到第一组数据。0x03 告诉 ECU:“先立刻给我读一次,然后再按周期执行”——省去了等待第一帧的时间。

在 C 代码中,这三个子功能的处理入口大致是这样的:

static Std_ReturnType Dsp_ReadDataByPeriodicIdentifier(
    uint16 did,
    uint8 subFunction,
    uint8 transmissionRate,
    P2VAR(uint16, AUTOMATIC, AUTOMATIC) pDidList,
    uint8 didCount,
    P2VAR(uint8, AUTOMATIC, AUTOMATIC) pResponse
)
{
    Std_ReturnType result = E_OK;

    switch (subFunction)
    {
        case 0x01: /* startPeriodicTransmission */
            result = Dsp_Periodic_Start(did, transmissionRate,
                                        pDidList, didCount);
            break;

        case 0x02: /* stopPeriodicTransmission */
            result = Dsp_Periodic_Stop(did, pDidList, didCount);
            break;

        case 0x03: /* startAndRead */
            result = Dsp_Periodic_Start(did, transmissionRate,
                                        pDidList, didCount);
            if (result == E_OK)
            {
                /* 立即读取一次,不等定时器 */
                result = Dsp_Periodic_ReadOnce(did, pResponse);
            }
            break;

        default:
            result = Dsp_SendNegativeResponse(DID, 0x2A, NRC_SUBFUNCTION_NOT_SUPPORTED);
            break;
    }

    return result;
}

TransmissionRate 频率编码

这是一个非常精巧的编码表。它把常用采样率映射到一个字节:

编码值      含义              典型场景
0x01      最快速率           需要最高时间分辨率时
0x02      100 ms             发动机瞬态分析
0x03      200 ms             中等速度监控
0x04      500 ms             缓慢变化参数
0x05      1 秒               一般诊断监控
0x06      2 秒               温度等慢变量
0x07      5 秒               趋势观察
0x08      10 秒              长周期数据记录
0x09 ~ 0xFE  自定义         OEM 可自行定义
0xFF      停止               等效于 stop

不是所有的频率在所有 ECU 上都能实现。ECU 内部的主循环周期(例如 10ms)决定了它能支持的最小上报间隔。如果测试仪请求了 0x01(最快速率)但 ECU 只能做到 10ms,ECU 会接受请求并自动按自己的最快速率执行——它尽力而为,不会因为做不到而报 NRC。

在实现侧,频率编码会被转换成 ECU 内部的定时器计数值:

static uint16 Dsp_Periodic_RateToMs(uint8 rateCode)
{
    switch (rateCode)
    {
        case 0x01: return 10;    /* 最快:一个主循环周期 */
        case 0x02: return 100;
        case 0x03: return 200;
        case 0x04: return 500;
        case 0x05: return 1000;
        case 0x06: return 2000;
        case 0x07: return 5000;
        case 0x08: return 10000;
        default:   return 1000;  /* 未知编码默认1秒 */
    }
}

正响应格式

ECU 收到启动请求后,立即回复正响应确认:

Byte 0: SID_PositiveResponse = 0x6A (即 0x2A | 0x40)
Byte 1: SubFunction echo (回显 0x01/0x02/0x03)
Byte 2: activeDiagSessionIndex (当前诊断会话索引)

之后,每隔一个 transmissionRate 周期,ECU 主动发送如下格式的数据帧:

Byte 0: SID = 0x6A
Byte 1+: dataIdentifier + dataRecord 对(格式完全同 0x62 正响应)
         即:DID(2B) + Data(NB) + DID(2B) + Data(NB) + ...

注意这里的巧妙之处:周期上报的数据格式复用了 0x22 的正响应格式(即 0x62 的那一套 DID+Data 对结构),只是在 SID 字节上用了 0x6A 而不是 0x62。这样测试仪的解析代码可以最大限度地复用。

让你感受一下真实的报文序列:

# 启动周期读取:DID 0x010C(转速) + 0x0105(水温),100ms一次
请求: 2A 01 02 02 01 0C 01 05
解析: SID=0x2A,Sub=start,Rate=100ms,DID数=2,DIDs=[0x010C,0x0105]

# 正响应确认
响应: 6A 01 01
解析: SID=0x6A(正响应),Sub=0x01(start echo),会话=1

# 100ms后,第一组周期数据
上报: 6A 01 0C 0B 00 01 05 5A
解析: SID=0x6A,DID1=0x010C,转速=0x0B00=2816rpm,DID2=0x0105,水温=0x5A=90°C

# 200ms后,第二组周期数据
上报: 6A 01 0C 0B 20 01 05 5A
解析: 转速=0x0B20=2848rpm,水温=90°C

医院隐喻:24小时动态心电图(Holter Monitor)

如果你去过心内科,你可能见过一种设备:病人胸前贴几个电极,连着一个比手机稍大的记录仪,回家正常生活24小时,第二天回到医院,医生把记录仪里的数据导出来分析。这就是24小时动态心电图,医学上叫 Holter 监测。

为什么心内科医生不只做一张普通心电图?因为普通心电图(对应UDS的 0x22)只记录几十秒——它能抓住持续性的心律失常,但抓不住偶发的早搏、阵发性的房颤、或运动中才出现的ST段改变。这些问题只在特定条件下才出现,就像那台工程样车只在2800转爬坡时才会抖。

Holter的思路和 0x2A 完全一致:

  • 普通心电图 (0x22):医生请你躺好 → 连接电极 → 打印几十秒波形 → 取下电极。这一刻你的心脏怎么样?

  • 动态心电图 (0x2A):医生给你贴上电极 → 设置记录仪参数(采样率、导联)→ 你回家 → 24小时后取下。这一整天你的心脏都在发生什么?

普通心电图24小时动态心电图
触发方式医生主动操作医生“注册“后,设备自行记录
持续时间几十秒24小时
数据量一页纸数万次心跳的数据
适用场景持续性问题间歇性/偶发性问题
对应UDS0x22 一次性读取0x2A 周期读取

更进一步:动态心电图上还可以有一个“事件按钮“——病人在感到不适时可以按一下,在数据上做一个标记。这不就是 0x86 (事件响应) 的雏形吗?(后续章节会读到它。)


核心洞察

功能寻址提醒:0x2A应通过物理寻址启动。如果用功能寻址向多个ECU同时注册周期读取,每个ECU都会以相同频率向总线主动上报数据——例如10个ECU每100ms各发一帧,总线每秒暴增100帧。0x2A的设计预期是一对一使用,不要通过功能寻址批量注册。

0x2A 是 UDS 对“我不想要一直问,你定期告诉我就行“这个需求的正式回答。 它的设计将数据推送的职责从测试仪转移到了 ECU——不是因为你懒得发请求,而是因为 ECU 用自己的定时器做周期采样比测试仪通过 CAN 总线做远程轮询要精准得多、高效得多。这是一次协议设计哲学的转折:之前的服务(0x22、0x2E、0x19、0x14……)都是测试仪主导的,0x2A 第一次让 ECU 在通信节奏上获得了主动权——尽管这个主动权仍然需要测试仪的“注册“才能启动。

这个思路也打开了 UDS 走向真正事件驱动的大门——如果 ECU 可以定时报,那是不是也能在“条件满足时“报?0x86(事件响应)将在后续章节回答这个问题。


本篇小结

  1. 0x2A ReadDataByPeriodicIdentifier 是周期数据读取服务,用于持续监控一组 DID 的值变化
  2. 0x22(一次性快照)不同,0x2A 一次注册后 ECU 按固定周期主动上报数据,无需测试仪反复轮询
  3. 三种子功能:0x01 启动周期上报、0x02 停止周期上报、0x03 启动并立即读取一次
  4. 请求报文携带 transmissionRate 编码(0x01=最快,0x02=100ms,……,0x08=10s),ECU 将其转换为内部定时器周期
  5. 周期数据帧复用 0x62 的 DID+Data 对格式,仅 SID 不同(0x6A vs 0x62
  6. 医院隐喻:24小时动态心电图(Holter)—— 不是 “这一刻怎么样”,而是 “这一段时间都在发生什么”
  7. 核心设计价值:将周期性采样的职责从测试仪(不可靠、高开销)转移到 ECU(精确、低开销)

【下集预告】:0x2A 让你能周期读取一组参数,但问题是:你只能从预先定义好的 DID 列表里选。如果你想要一个组合——“发动机转速 + 油门踏板位置 + 变速箱油温 + 车速”——但找不到一个现成的 DID 恰好包含这四个参数怎么办?

在下一章,UDS 将向你展示一项“反直觉“的能力:动态创建 DID。测试仪可以在运行时定义一个新的虚拟 DID,拼凑多个源 DID 的指定字节——就像医生不点“血常规“+“肝功能”+“甲状腺”,而是创建一张自定义化验单。欢迎进入 0x2C 的世界。