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
| | (静默)
| | (静默)
| | ...循环下去...
这在以下几个场景中是灾难性的:
- 轮询开销:每获取一次数据就要走一个完整的请求—响应循环。100ms 的采样周期意味着每秒 10 次 CAN 帧交互,这在多 ECU 共享的 CAN 总线上不是小事。
- 采样抖动:实际采样间隔 = 测试仪软件周期 + CAN 总线仲裁延迟 + ECU 响应时间。在满载总线上,这个抖动可以达到几十毫秒——对于分析发动机瞬态工况来说,这个精度太粗糙了。
- 人机瓶颈:你只有一双手一双眼睛。要同时监控4个参数,要么写自动化脚本,要么放弃。
UDS 设计者的思路很清晰:既然测试仪已经明确表示“我接下来一段时间会持续关心这几个参数“,那不妨让 ECU 自己把这个任务接管过去。 ECU 内部有精确的定时器,对自身资源的访问也没有总线延迟,它来做周期上报比测试仪轮询高效得多。
这就引出了两种读取模式的本质区别:
| 服务 | 触发方式 | 数据流向 | 比喻 |
|---|---|---|---|
| 0x22 | 测试仪主动请求 | 请求→响应(一问一答) | 医生用听诊器听一下 |
| 0x2A | ECU 定时发送 | 一次注册→多次主动上报 | 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 定义了三种操作:
| 值 | 名称 | 含义 |
|---|---|---|
| 0x01 | startPeriodicTransmission | 启动周期上报 |
| 0x02 | stopPeriodicTransmission | 停止周期上报 |
| 0x03 | startAndRead | 启动周期上报,并立即读取一次 |
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小时 |
| 数据量 | 一页纸 | 数万次心跳的数据 |
| 适用场景 | 持续性问题 | 间歇性/偶发性问题 |
| 对应UDS | 0x22 一次性读取 | 0x2A 周期读取 |
更进一步:动态心电图上还可以有一个“事件按钮“——病人在感到不适时可以按一下,在数据上做一个标记。这不就是 0x86 (事件响应) 的雏形吗?(后续章节会读到它。)
核心洞察:
功能寻址提醒:0x2A应通过物理寻址启动。如果用功能寻址向多个ECU同时注册周期读取,每个ECU都会以相同频率向总线主动上报数据——例如10个ECU每100ms各发一帧,总线每秒暴增100帧。0x2A的设计预期是一对一使用,不要通过功能寻址批量注册。
0x2A 是 UDS 对“我不想要一直问,你定期告诉我就行“这个需求的正式回答。 它的设计将数据推送的职责从测试仪转移到了 ECU——不是因为你懒得发请求,而是因为 ECU 用自己的定时器做周期采样比测试仪通过 CAN 总线做远程轮询要精准得多、高效得多。这是一次协议设计哲学的转折:之前的服务(0x22、0x2E、0x19、0x14……)都是测试仪主导的,0x2A 第一次让 ECU 在通信节奏上获得了主动权——尽管这个主动权仍然需要测试仪的“注册“才能启动。
这个思路也打开了 UDS 走向真正事件驱动的大门——如果 ECU 可以定时报,那是不是也能在“条件满足时“报?0x86(事件响应)将在后续章节回答这个问题。
本篇小结
- 0x2A ReadDataByPeriodicIdentifier 是周期数据读取服务,用于持续监控一组 DID 的值变化
- 与
0x22(一次性快照)不同,0x2A 一次注册后 ECU 按固定周期主动上报数据,无需测试仪反复轮询 - 三种子功能:
0x01启动周期上报、0x02停止周期上报、0x03启动并立即读取一次 - 请求报文携带 transmissionRate 编码(0x01=最快,0x02=100ms,……,0x08=10s),ECU 将其转换为内部定时器周期
- 周期数据帧复用
0x62的 DID+Data 对格式,仅 SID 不同(0x6Avs0x62) - 医院隐喻:24小时动态心电图(Holter)—— 不是 “这一刻怎么样”,而是 “这一段时间都在发生什么”
- 核心设计价值:将周期性采样的职责从测试仪(不可靠、高开销)转移到 ECU(精确、低开销)
【下集预告】:0x2A 让你能周期读取一组参数,但问题是:你只能从预先定义好的 DID 列表里选。如果你想要一个组合——“发动机转速 + 油门踏板位置 + 变速箱油温 + 车速”——但找不到一个现成的 DID 恰好包含这四个参数怎么办?
在下一章,UDS 将向你展示一项“反直觉“的能力:动态创建 DID。测试仪可以在运行时定义一个新的虚拟 DID,拼凑多个源 DID 的指定字节——就像医生不点“血常规“+“肝功能”+“甲状腺”,而是创建一张自定义化验单。欢迎进入
0x2C的世界。