大型 ASC 文件导入 MATLAB 内存不够怎么办
canMessageImport 读取大型 Vector ASC 日志时容易内存不足。四种把解码后的 CAN 信号导入 MATLAB 的方法:先截取、分块读取、转成 BLF,或在外部解码。
手上有一个带 CAN 报文和时间戳的 Vector .asc 日志、一个描述信号的 DBC,还有 MATLAB。最直接的做法是先 canMessageImport 再 canSignalTimetable,可一遇到长时间的记录,就会卡住或者报内存不足。本文先讲清楚原因,再介绍四种解决办法,从最快的到最灵活的。
为什么 canMessageImport 会内存不够
每一帧都变成一行
canMessageImport(file, "Vector", db) 会读取整个 ASC 文件,返回一个每帧一行的 timetable:时间戳、ID、通道、数据字节、标志位,附上数据库时还有这条报文解码出的信号。读入时不做任何筛选,你从来不看的总线上的帧,和你需要的帧占同样多的内存。
解码又多出一份
接着 canSignalTimetable(msgs, "VehicleStatus") 会根据上面的行,为每个报文再建一个 timetable。有一段时间里,原始报文和解码后的信号同时在内存里,MATLAB 通常就是在这一步撑不住的。
多大算大型 ASC 文件
ASC 是纯文本。一帧经典 CAN 报文写成一行大约 50 字节,繁忙的整车总线每秒有几千帧。三路通道记录一小时,轻松就是 1–2 GB 的文本、几千万行。下面每种方法的思路都一样:不要让 MATLAB 看到你不会解码的帧。
方法一:先把 ASC 文件变小
如果你知道哪几分钟、哪些报文是关键,这是最快的办法。
截取时间段和需要的 ID
ASC 截取与拆分工具在浏览器里流式读取文件,不限大小。它会按通道列出每个 CAN ID 及其帧数,并画出整段记录的报文密度。选好时间段,取消勾选不需要的 ID,就能下载一个更小的 .asc。日志行原样复制,文件头也会保留,canMessageImport 读取它和读原文件一样。
拆成几份,逐个导入
如果整段记录都要用,就按时长拆分,在循环里逐个解码。用 matfile 把解码结果追加到 MAT 文件里,内存里始终只有一份:
db = canDatabase("vehicle.dbc");
parts = dir("drive_part*.asc");
out = matfile("drive_speed.mat", "Writable", true);
out.time = zeros(0, 1);
out.speed = zeros(0, 1);
n = 0;
for k = 1:numel(parts)
msgs = canMessageImport(parts(k).name, "Vector", db);
sig = canSignalTimetable(msgs, "VehicleStatus");
t = seconds(sig.Time);
out.time(n+1:n+numel(t), 1) = t;
out.speed(n+1:n+numel(t), 1) = sig.VehicleSpeed;
n = n + numel(t);
clear msgs sig
end
拆分时保留原始时间戳(默认就是这样),追加的数据才会落在同一条时间轴上。
方法二:在 MATLAB 里分块读取
如果文件只能留在一台只装了 MATLAB 的电脑上,那就边读边筛。
只保留需要的帧
textscan 可以每次读固定行数。用正则表达式从每一行取出帧 ID,把要保留的行写进新文件,再导入这个新文件:
keep = ["1A0" "2B4"]; % ID 写法与日志中一致
fin = fopen("drive.asc", "r");
fout = fopen("drive_small.asc", "w");
first = true;
while ~feof(fin)
c = textscan(fin, "%s", 200000, "Delimiter", "\n", "Whitespace", "");
lines = string(c{1});
tok = regexp(lines, "^\s*\d+\.\d+\s+\d+\s+(\w+)\s+(Rx|Tx)", "tokens", "once");
isFrame = ~cellfun(@isempty, tok);
if first % 复制第一帧之前的文件头
fprintf(fout, "%s\n", lines(1:find(isFrame, 1) - 1));
first = false;
end
hit = false(size(lines));
hit(isFrame) = ismember(cellfun(@(t) string(t{1}), tok(isFrame)), keep);
fprintf(fout, "%s\n", lines(hit));
end
fclose(fin); fclose(fout);
msgs = canMessageImport("drive_small.asc", "Vector", canDatabase("vehicle.dbc"));
内存里只有一块 20 万行的数据,每 GB 大约需要几分钟。上面的正则只匹配经典 CAN 行,日志里有 CANFD 行的话需要扩展一下。
边处理边写出结果
日志特别长时,可以和方法一里的 matfile 循环结合:筛选一块、解码、追加信号,再处理下一块。matfile 创建的 MAT 文件是 7.3 版本,可以超过 2 GB。
方法三:转成 BLF,只读需要的部分
BLF 是 Vector 的压缩二进制日志,体积比 ASC 小好几倍,而且 blfread 可以在读取时筛选。
转换日志
在 CANoe 或 CANalyzer 里用 Logging File Conversion。没有 Vector 许可证的话,python-can 几行就能完成:
import can
reader = can.ASCReader("drive.asc")
with can.Logger("drive.blf") as writer:
for msg in reader:
writer.on_message_received(msg)
带筛选读取
blfread 可以指定通道、数据库、ID 过滤和时间范围,只有匹配的帧才会进入内存:
db = canDatabase("vehicle.dbc");
msgs = blfread("drive.blf", 1, "Database", db, ...
"CANStandardFilter", hex2dec(["1A0" "2B4"]), ...
"TimeRange", [600 900]);
sig = canSignalTimetable(msgs, "VehicleStatus");
如果记录仪本来就输出 MF4,那就继续用 MF4。它按通道组存储信号,MATLAB 可以只读取你指定的信号和时间范围。
方法四:在 MATLAB 外解码,只导入信号
能交给 MATLAB 的最小数据,就是解码后的信号本身:每个信号一个时间向量、一个数值向量,没有原始帧。
用 Python
python-can 逐行读取 ASC 文件,cantools 用你的 DBC 解码每一帧,内存里只保留你要的信号:
import can, cantools, numpy as np
from scipy.io import savemat
db = cantools.database.load_file("vehicle.dbc")
wanted = {db.get_message_by_name(n).frame_id for n in ["VehicleStatus", "BrakeStatus"]}
cols = {}
for msg in can.ASCReader("drive.asc"):
if msg.is_error_frame or msg.arbitration_id not in wanted:
continue
for name, value in db.decode_message(msg.arbitration_id, msg.data, decode_choices=False).items():
t, v = cols.setdefault(name, ([], []))
t.append(msg.timestamp)
v.append(value)
savemat("drive_signals.mat", {n: {"time": np.array(t), "value": np.array(v)} for n, (t, v) in cols.items()})
在 MATLAB 里 s = load("drive_signals.mat"),每个信号就是一个结构体,可以直接画图。
不写代码
ASC 转 MAT 工具在浏览器里做同样的事。给每个通道关联一个 DBC,它会为每个信号写出一个名为 报文名_信号名 的结构体,带 time、value、unit、message 和 channel 字段。不需要 Vehicle Network Toolbox,load 就能打开。单个文件最大 150 MiB,一次最多五个;更大的日志可以先用 ASC 截取与拆分按大小拆分(比如每份 140 MB),再一次性转换。
怎么选
| 你的情况 | 推荐方法 |
|---|---|
| 知道需要的时间段和报文 | 方法一:先截取再导入 |
| 整段日志都要导入 MATLAB | 方法一:拆分后循环导入,或方法四 |
| 电脑上只能用 MATLAB | 方法二:分块读取 |
| 有 CANoe,或有大量日志要归档 | 方法三:转成 BLF |
| 只需要解码后的信号 | 方法四:Python 或 ASC 转 MAT 工具 |
总结
canMessageImport 处理短日志没问题,但它要先读入所有帧才能解码。面对大型 ASC 文件,关键是减少 MATLAB 要承载的数据:先截取或拆分文件、读取时筛选、换成支持筛选读取的二进制格式,或者在 MATLAB 外解码、只导入信号。大多数情况下,第一种方法最快:先用 ASC 截取与拆分把日志变小,再照常导入。