尧图建网站 尧图建网站 YAOTU WEB BUILD 免费咨询
ARTICLE DETAIL

资讯详情

深耕网站建设与建站编程的一线实战洞察。

纯C语言实现BLF文件解析:CAN日志处理与工程实践

纯C语言实现BLF文件解析:CAN日志处理与工程实践 简介本资源是一个面向C语言中高级开发者与嵌入式/日志分析工程师的BLFBinary Log File二进制日志解析实战工程聚焦系统级日志读取、结构化解析与跨平台数据处理能力培养。项目基于Visual Studio构建完整包含C源码、头文件、编译依赖库lib/dll、典型测试BLF样本、DBC/CAN配置文件及PDF技术手册覆盖从文件I/O、字节序处理、结构体内存映射到VS工程配置的全流程实践。压缩包共21个文件含2个核心C/H源码、4个lib与4个dll运行依赖、3个说明文本、2个cfg配置、1个PDF手册、1个DBC协议定义及1个实测BLF样本等总大小859KB结构清晰、即开即用。已有2485人学习下载读者可直接复用工程框架解析车载CAN总线日志、对接CANalyzer/CANoe工具链掌握二进制日志解析的关键排错逻辑与VS调试技巧为智能汽车、工业控制等领域的日志分析与系统集成打下坚实基础。 做过车载总线测试的朋友应该都有过这种经历CANoe或者CANalyzer录了一堆BLF日志文件回头要批量分析报文、统计错误帧或者把特定ID的数据抽出来做后处理。BLFVector Binary Logging Format是Vector工具链默认的二进制日志格式相比ASC、CSV这些文本格式它写入快、体积小但代价就是没法用文本编辑器直接打开必须写代码去解析。今天分享的这套工程就是用纯C语言实现BLF文件解析包含了设计好的.c/.h模块和完整的VS工程源代码可以拿来做数据提取、报文统计、自动化测试也能嵌入到自己的上位机工具里。我自己最初做这个工程是被一个需求逼的有一批超过2GB的CAN日志要批量解析用CANoe的导出功能一个个操作太慢而且测试部门同事只想在命令行里一键出CSV。后来工期紧了干脆自己写了个C语言的解析器跑起来发现这套思路比脚本方案稳得多速度也快。这篇就跟大家把工程细节完整拆一遍从文件格式、源码设计到VS工程配置和踩坑点一次讲透。1. 从哪里入手BLF文件格式本质与解析思路1.1 为什么要自己写BLF解析BLF是二进制日志格式设计目标是在车辆行驶、台架测试时高速连续写入总线数据。它比文本日志更紧凑写入性能好所以Vector工具链默认用它记录CAN、CAN FD、LIN、FlexRay、Ethernet等总线数据。不过BLF是专有格式外部工具解析麻烦如果只想提取几百帧报文打开CANoe手工导出还能忍但遇到批量测试、自动化回归、现场故障复现后的日志分析手动方案就完全不现实了。自己写解析还有一个好处可以把解析逻辑直接集成到公司的测试框架里。比如测试脚本跑完后自动收集BLF日志自动统计总线负载率、错误帧数量、特定报文超时情况这些在自动化测试里都是刚需。另外一个很重要的场景是嵌入式设备端解析——有些台架设备没有安装CANoe只有Linux工控机跑着采集程序这时候一个纯C的解析库就是最方便的选择。1.2 解析方案选型为什么不用Python偏要写C有人可能觉得Python有python-can库直接pip install就可以解析BLF没必要用C。这话在追求快捷的时候有道理但放在工程环境里会碰到几个现实问题首先是平台的限制很多工控机是离线环境装Python依赖本身就很麻烦其次是性能大日志文件用Python解析会明显偏慢而C语言处理二进制文件就是天然的优势最后是集成C解析器可以编成静态库或DLL给C、C#、Python都能调用灵活性最高。所以我的建议是临时分析用Python没问题但如果你要做一套长期维护的日志处理工具C语言写的解析引擎更靠谱。这个工程的定位就是这样——核心解析逻辑全在.c和.h文件里不依赖任何第三方库放到哪个平台都能编译甚至能直接移植到嵌入式环境。1.3 模块划分与文件组织工程结构很直接把“接口”和“实现”分开、把“格式解析”和“业务处理”分开。我习惯把文件组织成下面这样BLFParserDemo/ ├── blf_parser.h ├── blf_parser.c ├── main.c ├── BLFParserDemo.vcxproj └── README.mdblf_parser.h对外暴露的接口定义了数据结构、错误码和函数声明调用方只需包含这个头文件。blf_parser.c所有BLF解析逻辑的实现包括文件头解析、对象头读取、CAN/CAN FD消息解析。main.c演示程序循环读取BLF文件并把CAN消息导出为CSV格式。这种拆法的好处是接口稳定、内部细节随便改。以后想增加LIN总线解析只要在blf_parser内部加一个对象类型分支对外接口不用动。头文件里我坚持“只暴露必要内容”内部用的结构体全部放在.c文件中防止外部误用。2. BLF格式核心文件头、对象头与CAN消息对象写解析器之前必须把文件格式彻底搞清楚。BLF格式从6.x版本开始比较稳定下面以常见的BLF 6.x版本为例说明核心结构。强烈建议你先拿一个实际录制的BLF文件用十六进制编辑器打开对着看比看文档直观得多。2.1 文件头怎么读BLF文件的起始4字节固定为ASCII字符“LGGM”十六进制是4C 47 47 4C。这个其实不是魔数“MAGIC”那种复杂的校验而是Vector内部定义的文件签名。在文件头里签名后面紧跟文件版本号再往后是文件头大小等信息。我整理了一个常见的文件头布局注意不同BLF版本字段略有差异但前12字节基本一致偏移长度字段说明04字节signature固定为4C 47 47 4CLGGM44字节fileVersion高16位为主版本低16位为次版本84字节headerSize文件头长度一般20~32个字节124字节compression0表示无压缩1表示压缩168字节uncompressedSize未压缩的整个文件大小解析文件头其实不用太纠结所有字段我们只关心三件事签名是否正确、版本号是多少、文件头多长。因为文件头长度在不同版本下不一样定位到第一个对象头时不能写死偏移量必须用headerSize字段跳过去。这个细节决定了程序兼容性一定要处理。2.2 对象头版本与时间戳BF文件由很多“对象”组成每个对象都有一个对象头。对象头有版本1和版本2两种布局对应不同的时间戳精度。版本1的对象头是16字节相对古老版本2的对象头是24字节纳秒级时间戳。现在新录制的基本都是版本2但老文件依然存在所以解析器不能只支持一种。版本2对象头的24字节布局如下偏移长度字段说明02字节headerSize2422字节headerVersion244字节objectSize整个对象总大小含对象头84字节objectType对象类型如1CAN消息124字节objectFlags标志位压缩标志、时间戳标志等168字节objectTimeStamp对象时间戳纳秒计数注意objectSize是整个对象块的长度解析完对象头之后要跳过消息体剩余部分才能到达下一个对象头。很多初学者写完解析发现读出来的数据错乱基本都是没按objectSize跳转而导致的。2.3 CAN消息对象体与DLC转换对象类型为1时消息体就是CAN报文。CAN报文对象体在对象头之后占用24字节结构为通道号4字节、标志位4字节、DLC 4字节、CAN ID 4字节、数据8字节。如果是CAN FD对象类型通常为11数据域最大可以到64字节所以消息体后面还会续接数据区。这里有一个非常容易踩坑的点CAN FD的DLC字段不是直接代表字节数。传统CAN的DLC是0~8直接对应字节数但CAN FD的DLC只有4位取值9~15时不等于实际字节数而是映射到12、16、20、24、32、48、64。解析CAN FD报文时如果直接按DLC去读取数据后面的所有字节就全部错位。正确做法是先转换再读取static const uint8_t canfd_dlc_to_len[16] { 0, 1, 2, 3, 4, 5, 6, 7, 8, 12, 16, 20, 24, 32, 48, 64 };拿到DLC后用canfd_dlc_to_len[dlc 0x0F]得到真实字节数再决定读多少个字节。这个表我在多个项目里都用到过建议直接复制进你的代码里。3. 工程源代码解析.c/.h 核心实现3.1 头文件接口设计blf_parser.h是整套工程的对外窗口。设计接口时我尽量避免把内部结构体暴露出去只提供一个不透明的解析器句柄外部代码拿到的只是一个指针。这样做的好处是以后内部结构再怎么改外部都不受影响。#ifndef BLF_PARSER_H #define BLF_PARSER_H #include stdint.h #include stdio.h #ifdef __cplusplus extern C { #endif #define BLF_OK 0 #define BLF_END_OF_FILE 1 #define BLF_ERR_FILE (-1) #define BLF_ERR_FORMAT (-2) #define BLF_ERR_IO (-3) #define BLF_ERR_MEMORY (-4) typedef struct blf_parser blf_parser; typedef struct { int64_t timestamp_ms; uint32_t channel; uint32_t dir; uint32_t can_id; uint32_t dlc; uint32_t is_fd; uint8_t data[64]; } blf_can_message; blf_parser* blf_open(const char* path); int blf_read_can_message(blf_parser* parser, blf_can_message* msg); void blf_close(blf_parser* parser); const char* blf_error_string(int code); #ifdef __cplusplus } #endif #endif这里面有几个细节值得说。blf_can_message的data数组我直接分配64字节既能存下CAN FD的64字节数据又不需要在结构体里放指针或者做动态内存管理省去内存释放的麻烦。is_fd字段用来区分是普通CAN还是CAN FD这样业务层不需要关心底层对象类型编号。3.2 文件头与对象头解析实现blf_parser.c中核心逻辑分为两大块打开文件时解析文件头读取时循环解析对象头。打开文件的流程很直接#include stdlib.h #include string.h #include blf_parser.h #define BLF_SIGNATURE LGGM #define OBJ_CAN_MESSAGE 1 #define OBJ_CAN_FD_MESSAGE 11 #define OBJ_HEADER_V2_SIZE 24 #pragma pack(push, 1) typedef struct { uint16_t headerSize; uint16_t headerVersion; uint32_t objectSize; uint32_t objectType; uint32_t objectFlags; int64_t objectTimeStamp; } blf_obj_header_v2; #pragma pack(pop) struct blf_parser { FILE* fp; uint32_t fileVersion; uint32_t fileHeaderSize; int64_t baseTimeMs; }; blf_parser* blf_open(const char* path) { blf_parser* ctx (blf_parser*)calloc(1, sizeof(blf_parser)); if (!ctx) return NULL; ctx-fp fopen(path, rb); if (!ctx-fp) { free(ctx); return NULL; } char magic[4]; if (fread(magic, 1, 4, ctx-fp) ! 4 || memcmp(magic, BLF_SIGNATURE, 4) ! 0) { fclose(ctx-fp); free(ctx); return NULL; } uint32_t version 0; uint32_t headerSize 0; fread(version, 4, 1, ctx-fp); fread(headerSize, 4, 1, ctx-fp); ctx-fileVersion version; ctx-fileHeaderSize headerSize; fseek(ctx-fp, headerSize, SEEK_SET); return ctx; }注意文件打开模式用的是rb这在Windows下特别关键。如果不加b文件会被当作文本模式打开遇到0x0A会变成0x0D 0x0A二进制数据全部错乱。我调试早期版本时就被这个坑过解析结果时好时坏检查半天才发现问题是fopen模式不对。对象头解析是兼容性的关键。每个对象头前2字节是headerSize所以可以先读出这2个字节判断是16还是24再决定用哪种结构体解析static int read_object_header(blf_parser* ctx, blf_obj_header_v2* hdr) { uint16_t headerSize 0; if (fread(headerSize, 2, 1, ctx-fp) ! 1) { return BLF_END_OF_FILE; } if (headerSize OBJ_HEADER_V2_SIZE) { /* 版本1的对象头读取16字节初始化v2结构体字段 */ uint8_t shortHdr[16] {0}; shortHdr[0] (uint8_t)(headerSize 0xFF); shortHdr[1] (uint8_t)((headerSize 8) 0xFF); if (fread(shortHdr 2, 1, 14, ctx-fp) ! 14) { return BLF_ERR_FORMAT; } memcpy(hdr, shortHdr, 16); hdr-objectTimeStamp 0; return BLF_OK; } else { if (fread(hdr-headerVersion, 2, 1, ctx-fp) ! 1 || fread(hdr-objectSize, 4, 1, ctx-fp) ! 1 || fread(hdr-objectType, 4, 1, ctx-fp) ! 1 || fread(hdr-objectFlags, 4, 1, ctx-fp) ! 1 || fread(hdr-objectTimeStamp, 8, 1, ctx-fp) ! 1) { return BLF_ERR_FORMAT; } hdr-headerSize headerSize; return BLF_OK; } }这段逻辑的好处是不会因为文件版本不同而崩溃。读到版本2的对象头就用完整24字节读到版本1的也能兼容——虽然时间戳为0但至少CAN消息的ID、DLC、数据这些关键信息能提出来。实际项目中老版本BLF文件占比不多有这种兜底处理就够用了。3.3 消息读取与CSV导出读取CAN消息的函数是blf_parser的核心。它用一个循环反复读对象头遇到CAN相关类型就解析消息体遇到其他类型就跳过直到文件末尾int blf_read_can_message(blf_parser* ctx, blf_can_message* msg) { blf_obj_header_v2 hdr; while (1) { int ret read_object_header(ctx, hdr); if (ret BLF_END_OF_FILE) { return BLF_END_OF_FILE; } else if (ret ! BLF_OK) { return ret; } if (hdr.objectSize hdr.headerSize) { return BLF_ERR_FORMAT; } uint32_t payloadSize hdr.objectSize - hdr.headerSize; if (hdr.objectType OBJ_CAN_MESSAGE || hdr.objectType OBJ_CAN_FD_MESSAGE) { uint8_t buf[64 16]; if (payloadSize sizeof(buf)) { fseek(ctx-fp, payloadSize, p a hrefhttps://download.csdn.net/download/baobingji/79657840 stylecolor:#ec7500;font-size:14px; 本文还有配套的精品资源点击获取 /a img altmenu-r.4af5f7ec.gif srchttps://csdnimg.cn/release/wenkucmsfe/public/img/menu-r.4af5f7ec.gif stylewidth:16px;margin-left:4px;vertical-align:text-bottom;cursor:text; /p
返回列表