简介:这份资源是一套面向汽车电子与CAN总线数据分析人员的Matlab脚本工具,用于解析asc格式的CAN报文并筛选指定信号,最终输出为csv文件,适合需要批量处理路测数据、做信号级分析的工程师或学习者使用。资源包共10个文件,约9KB,以7个m脚本为主体,配合2个pl脚本与1个txt配置文件,其中m文件承担信号查找、周期计算与主流程调度,pl脚本用于报文预过滤以提升解析效率,txt则集中管理可调参数。脚本支持同时解析多个asc文件(按0.asc、1.asc依次命名)与多个CAN信号,只需在配置中填写与dbc定义一致的信号名,并将asc、dbc文件置于同一路径,运行AnalysisTask.m即可完成分析。目前已有2546人学习下载,源码附带部分注释,便于理解解析逻辑与二次修改,也可作为CAN信号周期、节点归属等分析场景的参考实现。
1. 从一堆 CAN 报文里捞出有用信号:这个 MATLAB 脚本到底能干什么
如果你手头有一份.asc格式的 CAN 总线日志,想快速把里面某几个 CAN ID 的信号值提取出来画个曲线,又不想装一堆商业分析软件,那这个基于 MATLAB 2020a 的解析脚本就是给你准备的。它干的事情很纯粹:读入 Vector 格式的 ASC 日志文件,按行解析出时间戳、CAN ID、数据长度和 8 字节数据,再根据你给的信号定义把原始字节换算成物理值,最后输出成矩阵或直接画图。适合汽车电子工程师、标定测试人员,以及做车辆网络数据分析的学生。整个脚本不依赖额外工具箱,纯 MATLAB 基础语法就能跑,源码结构清晰,改起来也方便。
2. 环境准备与文件结构:跑通前先把这些理清楚
2.1 MATLAB 版本与依赖确认
脚本标注的是 MATLAB 2020a,但实际测试下来,2016b 之后的版本基本都能跑,因为用到的函数都是基础文件 I/O 和字符串处理。不需要 Simulink、Vehicle Network Toolbox 或任何附加产品。你只需要确认 MATLAB 安装时勾选了基础组件即可。如果打开脚本发现readlines或extractAfter报错,那说明版本太老,换成fgetl循环读取和strsplit手动切分就行,后面改造章节会讲怎么替换。
2.2 目录结构说明
拿到源码后,建议按下面这样组织文件夹,避免路径混乱:
can_asc_parser/ ├── main_parse_asc.m % 主入口脚本 ├── parse_asc_file.m % ASC 文件解析函数 ├── decode_can_signal.m % 信号解码函数 ├── dbc_lite.mat % 简化的信号定义表(可选) ├── sample_data/ │ └── test_log.asc % 示例 ASC 日志 └── output/ └── parsed_result.mat % 解析结果保存目录主脚本负责调度,解析函数只做文本到结构体的转换,解码函数负责物理值换算。这种拆分的好处是:你换一个 ASC 文件只需要改主脚本里的文件名,信号定义变了只动解码部分,互不影响。
2.3 示例 ASC 文件格式速览
ASC 文件本质是文本,每行一条报文,典型长这样:
0.000000 1 123 Rx d 8 11 22 33 44 55 66 77 88 0.010000 1 456 Rx d 8 AA BB CC DD EE FF 00 11字段依次是:时间戳、通道号、CAN ID(十六进制)、方向、数据长度、8 个字节的十六进制数据。脚本要做的就是把这些字段拆出来,转成数值。注意有些 ASC 文件带扩展帧标记(x或X),有些带错误帧,解析时要能跳过或标记。
3. 核心解析逻辑:从文本行到信号矩阵
3.1 主入口脚本的调度流程
主脚本不长,但决定了整个解析的走向。它做四件事:指定文件路径、调用解析函数、调用解码函数、保存或绘图。
% main_parse_asc.m % 主入口:读取 ASC 文件并解析 CAN 信号 clear; clc; asc_file = 'sample_data/test_log.asc'; % ASC 日志路径 target_ids = [0x123, 0x456]; % 只关心这两个 CAN ID signal_def = load('dbc_lite.mat'); % 加载信号定义 % 第一步:解析原始报文 raw_msgs = parse_asc_file(asc_file); % 第二步:按 ID 过滤并解码 decoded = decode_can_signal(raw_msgs, target_ids, signal_def); % 第三步:保存结果 save('output/parsed_result.mat', 'decoded'); fprintf('解析完成,共 %d 条目标报文\n', numel(decoded.time));这里target_ids用十六进制写,方便对照 DBC 文件。signal_def是一个结构体,里面存了每个 ID 对应的信号起始位、长度、缩放因子和偏移量。如果你只想快速看数据,可以把target_ids设成空数组,脚本会解析所有报文。
3.2 ASC 文件逐行解析函数
这是整个脚本最核心的部分,负责把文本变成结构体数组。关键点在于处理不定长空格和十六进制转换。
function msgs = parse_asc_file(filename) % 解析 ASC 文件,返回结构体数组 fid = fopen(filename, 'r'); if fid == -1 error('无法打开文件: %s', filename); end msgs = struct('time', {}, 'id', {}, 'data', {}); line_count = 0; while ~feof(fid) line = fgetl(fid); line_count = line_count + 1; % 跳过空行和注释行 if isempty(line) || startsWith(line, '//') || startsWith(line, 'date') continue; end % 按空白切分,但保留数据字节 parts = strsplit(strtrim(line)); if numel(parts) < 6 continue; % 格式不对,跳过 end % 时间戳 t = str2double(parts{1}); if isnan(t) continue; % 不是报文行 end % CAN ID(十六进制字符串转数值) can_id = hex2dec(parts{3}); % 找数据长度字段 'd' 后面的数字 d_idx = find(strcmp(parts, 'd'), 1); if isempty(d_idx) continue; end dlc = str2double(parts{d_idx + 1}); % 提取数据字节 data_bytes = zeros(1, dlc); for k = 1:dlc data_bytes(k) = hex2dec(parts{d_idx + 1 + k}); end % 存入结构体 msgs(end+1).time = t; msgs(end).id = can_id; msgs(end).data = data_bytes; end fclose(fid); fprintf('共解析 %d 行,有效报文 %d 条\n', line_count, numel(msgs)); end逻辑说明:fgetl逐行读取,strsplit按空格切分。hex2dec把十六进制字符串转成十进制数值。数据字节从d字段后面开始取,长度由 DLC 决定。注意这里没有处理扩展帧和远程帧,如果你的日志里有,需要在parts{3}里判断是否带x后缀。
参数说明:filename是 ASC 文件的完整路径或相对路径。返回的msgs是结构体数组,每个元素包含time(秒)、id(十进制 CAN ID)、data(1×DLC 的字节数组)。
3.3 信号解码与物理值换算
拿到原始字节后,下一步是按信号定义提取位并换算。这里用一个简化版 DBC 结构来演示。
function out = decode_can_signal(msgs, target_ids, def) % 按信号定义解码 CAN 报文 out.time = []; out.signals = struct(); for i = 1:numel(msgs) msg = msgs(i); if ~ismember(msg.id, target_ids) continue; end % 找到该 ID 的信号定义 def_idx = find([def.id] == msg.id, 1); if isempty(def_idx) continue; end sig_list = def(def_idx).signals; out.time(end+1) = msg.time; for s = 1:numel(sig_list) sig = sig_list(s); % 提取起始位和长度对应的原始值 raw_val = extract_bits(msg.data, sig.start_bit, sig.length); % 物理值 = 原始值 × 缩放 + 偏移 phys_val = raw_val * sig.scale + sig.offset; fname = sig.name; if ~isfield(out.signals, fname) out.signals.(fname) = []; end out.signals.(fname)(end+1) = phys_val; end end end function val = extract_bits(data, start_bit, len) % 从字节数组中提取指定起始位和长度的值(小端 Motorola 格式简化版) byte_idx = floor(start_bit / 8) + 1; bit_idx = mod(start_bit, 8); val = 0; for k = 0:len-1 b = bitget(data(byte_idx), bit_idx + 1); val = val + b * 2^k; bit_idx = bit_idx + 1; if bit_idx == 8 bit_idx = 0; byte_idx = byte_idx + 1; end end end逻辑说明:extract_bits按位提取,支持跨字节。scale和offset来自 DBC 定义,比如温度信号可能是raw * 0.1 - 40。解码结果存到out.signals结构体里,每个信号一个字段,值是时间序列数组。
参数说明:msgs是上一步的输出,target_ids是关心的 CAN ID 列表,def是信号定义结构体数组,每个元素有id和signals字段。signals里每个信号包含name、start_bit、length、scale、offset。
4. 避坑与常见问题:这些地方最容易翻车
4.1 时间戳精度丢失
现象:解析出来的时间戳只有小数点后两位,和原始文件对不上。原因:ASC 文件里时间戳是 6 位小数,但某些 MATLAB 版本str2double默认精度不够。解决:用sscanf指定格式,或者把时间戳当字符串存下来,需要时再转。
4.2 十六进制字符串带空格
现象:hex2dec报错,提示输入不是有效十六进制。原因:ASC 文件里数据字节之间可能有多个空格,strsplit后得到空字符串。解决:切分后过滤掉空字符串,或者用regexp按\s+分割。
4.3 扩展帧 ID 解析错误
现象:某些 CAN ID 解析出来是负数或超大数。原因:扩展帧 ID 是 29 位,ASC 文件里可能带x后缀,直接hex2dec会出错。解决:先判断字符串末尾是否带x,去掉后再转,同时记录帧类型。
4.4 信号起始位定义混淆
现象:解码出来的物理值明显不对,比如温度变成几千度。原因:DBC 里起始位有 Motorola 和 Intel 两种格式,脚本默认按一种处理。解决:在信号定义里加一个byte_order字段,解码时分支处理。常见做法是先用已知信号验证,再批量跑。
4.5 大文件内存溢出
现象:解析几百 MB 的 ASC 文件时 MATLAB 卡死或报内存不足。原因:一次性把所有报文读进结构体数组,内存占用翻倍。解决:分批读取,每读 10 万行就解码并保存,清空临时变量。或者改用datastore做流式处理。
5. 二次开发与改造:让脚本适配你自己的项目
5.1 替换信号定义来源
现在信号定义是手动存的.mat文件,实际项目里更常见的是直接读 DBC。你可以写一个parse_dbc.m,把 DBC 里的BO_和SG_行解析成同样的结构体。关键代码片段:
% 解析 DBC 中的信号定义(简化版) function def = parse_dbc(dbc_file) fid = fopen(dbc_file, 'r'); def = struct('id', {}, 'signals', {}); while ~feof(fid) line = fgetl(fid); if startsWith(line, 'BO_ ') % BO_ 291 ECU1: 8 ECU1 parts = strsplit(line); can_id = str2double(parts{2}); def(end+1).id = can_id; def(end).signals = struct('name', {}, 'start_bit', {}, ... 'length', {}, 'scale', {}, 'offset', {}); elseif startsWith(line, ' SG_ ') % SG_ Temp : 0|8@1+ (0.1,-40) [0|100] "C" ECU1 parts = strsplit(strtrim(line)); sig.name = parts{2}; bit_info = strsplit(parts{4}, {'|', '@'}); sig.start_bit = str2double(bit_info{1}); sig.length = str2double(bit_info{2}); factor_info = strsplit(parts{5}, {'(', ',', ')'}); sig.scale = str2double(factor_info{2}); sig.offset = str2double(factor_info{3}); def(end).signals(end+1) = sig; end end fclose(fid); end这样你就不用手动维护.mat文件了,DBC 更新后直接重新解析即可。注意 DBC 里起始位和字节序的细节,不同工具导出的格式略有差异,建议先用一小段日志验证。
5.2 增加图形化输出
解析完数据后,最直观的验证方式就是画图。在main_parse_asc.m末尾加几行:
% 绘制信号曲线 figure; signal_names = fieldnames(decoded.signals); for i = 1:numel(signal_names) subplot(numel(signal_names), 1, i); plot(decoded.time, decoded.signals.(signal_names{i}), 'LineWidth', 1.2); ylabel(signal_names{i}); grid on; end xlabel('Time (s)');如果信号数量多,建议只画关心的那几个,或者用tiledlayout分页。画图前记得检查时间轴是否单调递增,有些日志里时间戳会回绕。
5.3 性能优化与批量处理
如果你要处理几十个 ASC 文件,逐个跑太慢。可以把主脚本改成函数,用parfor并行:
files = dir('logs/*.asc'); parfor i = 1:numel(files) raw = parse_asc_file(fullfile(files(i).folder, files(i).name)); dec = decode_can_signal(raw, target_ids, signal_def); save(fullfile('output', [files(i).name '.mat']), 'dec'); end注意parfor里不能直接改共享变量,每个文件独立保存。另外并行池启动有开销,文件少的时候反而更慢,建议超过 10 个文件再用。
5.4 验证解析正确性的小技巧
改完代码后怎么确认没跑偏?我一般会挑一条已知报文,手动算一遍物理值,和脚本输出对比。比如 ID 0x123 的第一个信号,原始字节是11 22,起始位 0,长度 8,缩放 0.5,偏移 0,那结果应该是0x11 * 0.5 = 8.5。如果对不上,就回去查字节序和起始位定义。这个习惯帮我省了很多调试时间。
从那以后我每次改完解析逻辑,都强制走一遍这个手动验证,确认无误再批量跑。希望帮到你。
本文还有配套的精品资源,点击获取