简介:DLT(Diagnostic Log and Trace)是汽车电子软件开发中通用的诊断日志格式,这份资源即围绕其数据查看与分析的可视化工具包,面向车内通信调试、日志排错的一线开发者与测试人员。压缩包共572个文件,约24.15MB,以 cmake 与 make 构建脚本、C/C++ 源码(.c、.h、.cpp)和 txt 说明文档为主体,同时包含编译产物(.o、.dll、.so)与 qm 多语言资源、png 界面素材,另附一份 docx 操作说明,可直接对照使用。资源中既有 dlt-viewer 可视化程序,也有 dlt-master 工程源码,涵盖 dlt-daemon、dlt-receive、dlt-convert、dlt-control 及多组测试与示例程序,便于理解日志采集、转换与展示的完整链路。已有161人学习。读者可借助它快速搭建 DLT 数据监视环境,掌握过滤、检索与导出分析的方法,并依据源码与文档定位问题、优化系统性能。
1. DLT 日志不是文本文件:先搞清 dlt-master 到底在解什么
路试车回来,域控里拖出来一个 400MB 的.dlt文件,用文本编辑器打开全是\x00和乱码,这是很多人第一次接触 DLT 日志的场景。DLT(Diagnostic Log and Trace)是车载与嵌入式系统里常见的二进制日志协议,dlt-master 则是这套协议的开源实现包,里面包含 daemon、接收端、转换工具和库。它解决的核心问题不是“把日志变好看”,而是把带 ECU、APID、CTID、时间戳、参数类型信息的二进制帧,稳定还原成可检索、可关联的结构化记录。QNX 平台上的 DLT 采集经常被单独拿出来说,因为它的启动方式、时钟源和 socket 转发都跟桌面 Linux 不一样。适合做诊断分析、域控日志排查、以及需要自己接一套可视化链路的人。
2. 拆开 DLT 包体:从标准头到 verbose 模式的字段偏移
2.1 标准头 12 字节里藏着三个会变长的开关
DLT 帧不是固定长度,最先要看的不是 payload,而是HTYP这个字节。它决定了后面有没有 Session ID、有没有 Timestamp、是不是大端数值、扩展头里有没有NOAR。标准头的骨架是这样:
| 字段 | 长度 | 说明 |
|---|---|---|
| HTYP | 1 字节 | 标志位,决定头部长度和字节序 |
| MCNT | 1 字节 | 报文计数器,丢包判断用 |
| LEN | 2 字节 | 从 ECU ID 到帧尾的字节数,不含前 4 字节 |
| ECU ID | 4 字节 | 发送方标识,通常是 ASCII |
| SID | 4 字节 | 服务 ID,日志帧里常为 0 |
| Session ID | 可选 4 字节 | HTYP 的 bit3 置位时存在 |
| Timestamp | 可选 4 字节 | HTYP 的 bit4 置位时存在,单位 0.1ms |
HTYP的常见位含义:bit0 表示带扩展头,bit1 表示数值参数按大端解释,bit2 表示 ECU ID 有效,bit3 表示带 Session ID,bit4 表示带 Timestamp。很多解析器翻车就翻在LEN上:它不包含HTYP、MCNT、LEN自身,所以算整帧长度时要用4 + LEN,不是直接拿LEN当总长度。
扩展头分两种。verbose 模式下,扩展头先是 1 字节NOAR(参数个数),再是 4 字节APID和 4 字节CTID;非 verbose 模式没有NOAR,直接是APID和CTID。如果这里少读或多读一个字节,后面所有参数类型信息都会错位,表现出来就是“日志能出来,但参数值全是天文数字”。
2.2 先用 dlt-convert 做一次裸解析,确认帧边界
拿到文件先别急着写代码,用 dlt-master 自带的转换工具跑一遍,能快速确认文件是不是完整、帧长度有没有对不上。我一般会在一个干净目录里做三件事:看文件大小、转文本、抽查前 20 行。
# 查看文件基本信息,确认不是 0 字节或明显截断 ls -lh trace.dlt # 把二进制 DLT 转成可读文本,-a 表示输出所有帧 dlt-convert -a trace.dlt > trace.txt # 只看前 20 行,确认 ECU、APID、CTID 是否按预期出现 head -n 20 trace.txtdlt-convert的-a会把每个帧按文本形式展开,输出里通常能看到时间戳、ECU 名、APID、CTID 和 payload。如果trace.txt里出现大量“未知”或者 payload 为空,先回去看LEN和HTYP,不要直接怀疑文件损坏。另一个常用工具是dlt-receive,它从网络端口收流并落盘:
# 监听 3490 端口,把收到的 DLT 流写成文件 dlt-receive -o trace.dlt 0.0.0.0参数说明:-o指定输出文件,后面的地址是绑定地址。宿主机上跑这个命令,QNX 侧把日志发过来,就能在不拔盘的情况下持续采集。这里要注意,dlt-receive收到的是流,不是帧对齐的文件,中途断开可能留下半帧,后面解析时要允许最后一帧不完整。
2.3 QNX 侧采集:别把 Linux 的启动脚本直接搬过去
QNX 上没有 systemd,也没有和桌面 Linux 完全一致的进程管理方式。常见做法是交叉编译 dlt-daemon 和 dlt 库,然后在启动脚本里拉起 daemon,并把输出指向宿主机的 IP。QNX 的时钟源和 Linux 也有差异,如果帧里带 Timestamp,最好同时记录接收端的本地时间,后面做对齐。
# QNX 侧启动脚本示例,放到 /etc/rc.d 或项目自定义启动目录 export DLT_ECU_ID="qnx_domain" export DLT_LOG_LEVEL=4 # 启动 daemon,-c 指定配置文件,-d 前台运行便于串口观察 dlt-daemon -c /etc/dlt.conf -d & # 确认进程和端口 pidin | grep dlt netstat -an | grep 3490参数上,DLT_ECU_ID建议固定成短横线或下划线风格,长度控制在 4 字节以内,避免被截断后和别的 ECU 撞名。dlt.conf里重点是接收端地址、端口、以及是否开启本地缓存。QNX 侧如果直接写本地文件,断电时容易留下不完整帧,所以更稳的做法是 daemon 转发加宿主机落盘。QNX 自带的 slogger2 可以保留系统级日志,但 DLT 的应用日志还是走 DLT 链路更利于后续按 APID/CTID 过滤。
3. 把 dlt-master 跑起来:编译参数、依赖与三种启动姿势
3.1 依赖清单和 cmake 选项,先做减法
dlt-master 的构建系统是 CMake,桌面端编译主要用于拿到dlt-convert、dlt-receive和dlt-viewer所需的库。依赖通常包括 CMake、C 编译器、zlib,如果要跑 GUI 还要 Qt 开发包。第一次编译不建议把所有可选特性都打开,先保证核心工具能出来,再按需加。
# 在源码目录外建构建目录,避免污染源码树 mkdir -p build && cd build # 核心选项:Release 构建、生成动态库、打开测试工具 cmake .. \ -DCMAKE_BUILD_TYPE=Release \ -DBUILD_SHARED_LIBS=ON \ -DWITH_DLTTEST=ON # 并行编译,核数按机器来 cmake --build . -j"$(nproc)"参数说明:CMAKE_BUILD_TYPE=Release会打开优化,解析大文件时差别明显;BUILD_SHARED_LIBS=ON生成动态库,方便后面自己写的小工具链接;WITH_DLTTEST这类测试开关按项目实际选项名调整,拿不准就先跑cmake -L ..看可用选项。如果 Qt 没装全,GUI 目标会失败,但命令行工具通常还能出来,先确认dlt-convert是否生成。
编译报错里最常见的是找不到zlib.h或 Qt 的Qt5CoreConfig.cmake。前者装开发包,后者检查CMAKE_PREFIX_PATH是否指向 Qt 安装路径。不要一上来就改源码,先看 CMake 的输出末尾,它会把缺的包名写出来。
3.2 daemon 模式和离线文件模式,用途不一样
dlt-master 有两种典型用法。一种是 daemon 模式:目标板上跑dlt-daemon,宿主机跑dlt-receive或dlt-viewer,日志实时过来。另一种是离线文件模式:已经拿到.dlt文件,直接用dlt-convert或dlt-viewer打开。实时排查用前者,事后复盘用后者。
# 宿主机启动 dlt-viewer 并监听网络 dlt-viewer -p 3490 # 或者只收流落盘,之后再分析 dlt-receive -o session_01.dlt 0.0.0.0-p指定监听端口,和 daemon 配置里的端口保持一致。实时模式下,dlt-viewer 的过滤条件可以按 ECU、APID、CTID 组合,但过滤是在接收端做的,目标板仍然会把所有日志发过来,带宽紧张时要在 daemon 侧先做裁剪。
离线模式下,dlt-convert还有一个实用参数是只输出指定 ECU 的帧。具体参数名以dlt-convert -h为准,不同分支可能有差异,但思路是先按 ECU 粗筛,再按 APID 细筛,避免一次性把几百兆文本倒进编辑器。
3.3 构造一条报文做注入自测
在改解析器之前,最好自己造一条最小 DLT 帧,确认工具链能正确识别。下面这段 Python 生成一个带扩展头的 verbose 帧,ECU 为TEST,APID 为APP1,CTID 为CTX1,payload 是一个字符串参数。
import struct def make_dlt_string_frame(text: str) -> bytes: ecu = b"TEST" sid = b"\x00\x00\x00\x00" apid = b"APP1" ctid = b"CTX1" # 字符串参数类型信息:0x21 表示 STRG,后面跟长度 payload = b"\x21" + struct.pack(">H", len(text)) + text.encode("utf-8") # 扩展头:NOAR=1,APID,CTID ext = b"\x01" + apid + ctid body = ecu + sid + ext + payload # HTYP:UEH=1,MSBF=1,WEID=1 -> 0x07 htyp = 0x07 mcnt = 0x01 length = len(body) return bytes([htyp, mcnt]) + struct.pack(">H", length) + body frame = make_dlt_string_frame("hello dlt") with open("mini.dlt", "wb") as f: f.write(frame) print(frame.hex())逻辑说明:LEN只算body,也就是从 ECU 到帧尾;HTYP的 bit0 打开扩展头,bit1 表示大端,bit2 表示 ECU 有效。字符串参数的类型字节0x21是 DLT 里常见的 STRG 类型,后面两个字节是字符串长度。生成后丢给dlt-convert -a mini.dlt,如果输出里能看到TEST、APP1、CTX1和hello dlt,说明基础链路是通的。这一步能提前暴露字节序和扩展头长度问题,比拿真实日志盲调快得多。
4. 可视化层怎么接:从 dlt-master 输出到时间轴视图
4.1 dlt-viewer 的过滤、标记和导出
dlt-viewer 是 Qt 写的桌面端工具,打开.dlt文件后,左侧是 ECU/APID/CTID 树,右侧是报文列表。真正提高效率的是过滤表达式和标记功能,不是单纯滚动看日志。
常见操作顺序:先按 ECU 过滤掉无关域控,再按 APID 缩到某个模块,最后用 CTID 定位具体上下文。过滤条件支持组合,比如同时限定 ECU 和 APID。找到异常区间后打 marker,导出这一段给同事,比发整个文件靠谱得多。
导出 CSV 时要注意,payload 里的参数值是否展开取决于帧里的类型信息是否完整。如果类型信息缺失,导出结果里可能只有十六进制原始字节。这时候不要怪工具,回去看扩展头的NOAR和参数类型字节。
4.2 自己写解析器:Python 读二进制流的最小闭环
工具链覆盖不了所有场景,比如要把 DLT 接到自有平台,或者做批量统计。下面这个解析器只做三件事:切帧、解标准头、解 verbose 扩展头和字符串参数。
import struct import csv def iter_frames(path): with open(path, "rb") as f: data = f.read() off = 0 while off + 4 <= len(data): htyp = data[off] mcnt = data[off + 1] length = struct.unpack(">H", data[off + 2:off + 4])[0] total = 4 + length if off + total > len(data): break # 最后一帧不完整,直接丢弃 body = data[off + 4:off + total] yield htyp, mcnt, body off += total def parse_body(htyp, body): pos = 0 ecu = body[pos:pos + 4].decode("ascii", "replace"); pos += 4 sid = body[pos:pos + 4].hex(); pos += 4 if htyp & 0x08: pos += 4 # Session ID if htyp & 0x10: pos += 4 # Timestamp apid = ctid = "" noar = 0 if htyp & 0x01: noar = body[pos]; pos += 1 apid = body[pos:pos + 4].decode("ascii", "replace"); pos += 4 ctid = body[pos:pos + 4].decode("ascii", "replace"); pos += 4 payload = body[pos:] text = "" if payload and payload[0] == 0x21 and len(payload) >= 3: n = struct.unpack(">H", payload[1:3])[0] text = payload[3:3 + n].decode("utf-8", "replace") return ecu, sid, apid, ctid, noar, text rows = [] for htyp, mcnt, body in iter_frames("trace.dlt"): ecu, sid, apid, ctid, noar, text = parse_body(htyp, body) rows.append([ecu, apid, ctid, noar, text]) with open("trace.csv", "w", newline="", encoding="utf-8") as f: w = csv.writer(f) w.writerow(["ecu", "apid", "ctid", "noar", "text"]) w.writerows(rows)参数说明:iter_frames里的total = 4 + length是关键,length不包含前 4 字节;parse_body里先读 ECU 和 SID,再根据HTYP的 bit3、bit4 跳过可选字段;扩展头里NOAR只在 verbose 模式存在。字符串参数这里只处理了0x21,真实日志里还有整型、浮点、数组和结构体,类型字节的高位表示数组或结构,低位表示基础类型,扩展时要按类型表逐个补。
4.3 多 ECU 时间戳对齐:别拿接收时间当发送时间
多个 ECU 的日志混在一个文件里时,时间轴很容易看起来“因果颠倒”。原因是每个 ECU 的 Timestamp 来自各自时钟,dlt-receive 落盘时又可能加上接收时间。常见做法是同时保留帧内时间戳和接收时间戳,用同一事件在两个 ECU 上的出现顺序做线性拟合。
具体操作:先找出两个 ECU 都记录的同一个状态跳变,拿到两边的帧内时间戳,算出差值;再在不同时间点取几组,看差值是否稳定。如果差值漂移,说明晶振有偏差,简单减一个固定偏移不够,要做线性映射。QNX 域控休眠唤醒后时钟可能跳变,分段拟合比全局拟合稳。没有把握时,宁可只用接收时间排序,也不要把两个时钟源混着用。
5. 避坑排查:五个让日志「看起来是空的」的真实原因
5.1 解析层面的三个坑
现象:dlt-convert 能出文本,但自己写的解析器读出来全是空字符串。原因:扩展头长度判断错了。非 verbose 模式没有NOAR,如果按 verbose 去读,会多吞一个字节,后面APID整体错位。 解决:先看HTYP的 bit0,再决定要不要读NOAR;用 3.3 节构造的最小帧做回归,确认两种模式都能解。
现象:帧长度看起来对,但每隔几帧就错位一次。原因:把LEN当成了整帧长度。LEN从 ECU ID 开始算,不含HTYP、MCNT和LEN自身,少加 4 字节就会越切越歪。 解决:统一用total = 4 + struct.unpack(">H", buf[2:4])[0],并在循环里判断剩余长度,最后一帧不完整就停,不要硬解。
现象:字符串参数显示成乱码,或者长度明显不对。原因:字符串长度字段的字节序跟HTYP的 MSBF 位不一致,或者把 UTF-8 当 ASCII 解。 解决:数值字段按HTYP的 bit1 决定大小端,字符串按 UTF-8 解码并保留errors="replace";长度字段先确认是 2 字节还是 4 字节,别凭感觉读。
5.2 采集与展示层面的两个坑
现象:QNX 侧 daemon 活着,宿主机 dlt-viewer 却一直接不到日志。原因:QNX 启动脚本里的接收地址写成了127.0.0.1,或者端口被别的进程占用;也可能是 daemon 前台运行但启动脚本没有放到正确的启动阶段。 解决:先在 QNX 上用netstat确认监听状态,再从宿主机用dlt-receive单独测端口;启动脚本里把DLT_ECU_ID和接收地址写成显式变量,别依赖登录 shell 的环境。
现象:日志文件能打开,但按 ECU 过滤后一条都不剩。原因:ECU ID 在帧里是定长 4 字节,写入时带了空格或大小写不一致,过滤表达式按精确匹配就找不到。 解决:先用不过滤的视图看原始 ECU 字段,确认实际字节;命名规范里统一用大写短名,长度不超过 4 字节,采集前在 daemon 配置里固定死。
6. 验证解析正确性:用一条构造报文反推字段偏移
解析器写完,最怕的是“看起来能跑”。我习惯用一条自己构造的帧做反向验证:先定好每个字段的值,再让解析器输出,最后逐字节对偏移。下面这条帧带 Session ID 和 Timestamp,用来覆盖可选字段的分支。
| 字段 | 设定值 | 预期偏移 |
|---|---|---|
| HTYP | 0x1F | 0 |
| MCNT | 0x02 | 1 |
| LEN | 待算 | 2 |
| ECU ID | TEST | 4 |
| SID | 00000001 | 8 |
| Session ID | 00000002 | 12 |
| Timestamp | 00000003 | 16 |
| NOAR | 0x01 | 20 |
| APID | APP1 | 21 |
| CTID | CTX1 | 25 |
| 参数类型 | 0x21 | 29 |
| 字符串长度 | 0x0005 | 30 |
| 字符串内容 | hello | 32 |
import struct body = ( b"TEST" + b"\x00\x00\x00\x01" + b"\x00\x00\x00\x02" + b"\x00\x00\x00\x03" + b"\x01" + b"APP1" + b"CTX1" + b"\x21" + struct.pack(">H", 5) + b"hello" ) frame = bytes([0x1F, 0x02]) + struct.pack(">H", len(body)) + body assert len(frame) == 4 + len(body) print("total bytes:", len(frame))HTYP=0x1F同时打开扩展头、大端、ECU 有效、Session ID 和 Timestamp,解析器如果漏掉任何一个可选字段,后面的APID就不会落在 21。跑完这条断言,再拿真实日志抽查前 100 帧,确认LEN累计和文件大小对得上,基本就能排除切帧错误。
从那以后我每次改 DLT 解析代码,都强制先跑这条构造帧和一条非 verbose 帧,再碰真实日志。血泪经验是:真实日志的错位往往不是协议复杂,而是某个可选字段被想当然地跳过了。希望帮到你。
本文还有配套的精品资源,点击获取