简介:这份资源面向需要用Matlab处理LabVIEW采集数据的工程师与科研人员,解决TDMS二进制文件难以直接在Matlab中读取与二次分析的问题。TDMS格式占用磁盘空间小、支持高速写盘,常见于高速采集系统,但Matlab原生并不支持,因此需要借助转换脚本将其转为mat文件。压缩包共10个文件,约90KB,包含4个m脚本、2个tdms样本、2个txt说明、1个tdms_index索引及1个mat结果文件,其中转换函数、结构体转mat函数与主程序分工明确,并附带样本数据与英文使用说明,便于直接运行验证。目前已有4170人学习下载。读者可据此掌握TDMS到mat的完整转换流程,理解结构体与mat格式的映射关系,并借助样本文件快速复现与排错,为后续信号处理与数据分析打好基础。
1. 别再用Excel硬扛了:Matlab读取TDMS文件并存储为mat格式到底省了多少事
如果你在测试台架、数据采集卡或某类便携记录仪旁边待过,大概率见过一种后缀为.tdms的文件。它不像 CSV 那样双击就能看,也不像 MAT 那样天生就是 Matlab 的“亲儿子”。很多工程师第一次拿到 TDMS 文件时,习惯性地用 Excel 打开,结果要么乱码,要么只看到一堆二进制符号,最后只能截图求助。这个标题要解决的就是这个具体问题:用 Matlab 把 TDMS 文件里的通道数据、时间戳和属性读出来,再存成.mat格式,方便后续做滤波、频谱分析或批量画图。适合的人群很明确——手里有 TDMS 数据、日常用 Matlab 做算法验证、又不想每次都在格式转换上重复造轮子的从业者。TDMS 本身是一种分层结构的二进制格式,内部按“文件—通道组—通道”三级组织,还带属性元数据,所以读取时不能像读文本那样一行行扫,得按它的层级去取。存成 MAT 之后,最大的好处是加载快、变量结构清晰,而且 Matlab 的save和load对 MAT 的支持最稳,后续跑循环批处理时不用再挂第三方依赖。
2. TDMS 的层级结构决定了你该怎么读:先搞懂通道组再动手
2.1 为什么直接load会翻车:TDMS 不是纯文本也不是标准 MAT
TDMS 的全称是 Technical Data Management Streaming,它的设计目标是把高速采集的数据按流式写入磁盘,同时保留元数据。文件内部并不是一张平铺的表,而是先有根对象,下面挂若干通道组,通道组下面再挂若干通道。每个通道有自己的数据数组,每个层级都可以带属性,比如采样率、单位、量程、传感器编号。很多人第一次用 Matlab 读 TDMS 时,会下意识地找load('xxx.tdms')或者readmatrix,结果要么报错,要么读出一堆无意义的数字。原因就是 TDMS 的二进制布局和文本格式完全不同,必须用能解析其头部和索引的接口。
常见做法是使用 Matlab 社区里成熟的 TDMS 读取函数,比如TDMS_readTDMSFile这类工具,或者调用厂商提供的 DLL 再包一层。我一般会先确认手头有没有现成的读取函数,如果没有,就自己按 TDMS 的规范写一个轻量解析器,只取我需要的通道,不追求全量解析。这里要提醒一句:TDMS 有 32 位和 64 位两种内部指针格式,读之前最好先看文件头里的版本标识,否则偏移量算错,后面全乱。
2.2 用TDMS_readTDMSFile读一个文件:最小可跑通的命令
假设你已经拿到了一个可用的 TDMS 读取函数,放在当前工作目录下。下面这段代码是最小可跑通的版本,只读一个文件,把通道数据取出来看看结构。
% 指定待读取的 TDMS 文件路径 tdmsFile = 'test_data.tdms'; % 调用读取函数,返回结构体 % 常见返回字段:channelData、channelNames、groupNames、properties dataStruct = TDMS_readTDMSFile(tdmsFile); % 查看有哪些通道组 disp('通道组列表:'); disp(dataStruct.groupNames); % 查看第一个通道组下的通道名 firstGroup = dataStruct.groupNames{1}; disp(['第一个通道组:', firstGroup]); disp('该组下的通道:'); disp(dataStruct.channelNames.(firstGroup)); % 取出第一个通道的数据看看长度 firstChan = dataStruct.channelNames.(firstGroup){1}; rawData = dataStruct.channelData.(firstGroup).(firstChan); fprintf('通道 %s 的数据长度:%d\n', firstChan, length(rawData));这段代码的逻辑很直白:先定位文件,再调用读取函数,然后把返回结构体里的通道组和通道名打印出来,最后取一个通道的数据看长度。参数方面,tdmsFile可以是相对路径或绝对路径,建议用绝对路径,避免因为当前目录切换导致找不到文件。TDMS_readTDMSFile这类函数通常还有可选参数,比如是否读取属性、是否只读某个通道组,读大文件时把不需要的组过滤掉能省不少内存。如果你发现读出来的数据长度和预期对不上,先检查是不是把属性数组也当成数据读了,或者通道索引从 0 开始还是从 1 开始没对齐。
2.3 把读到的数据存成 MAT:变量命名和结构设计
读出来只是第一步,存成 MAT 才是这个标题的落脚点。直接save('out.mat')会把当前工作区所有变量都存进去,文件容易臃肿,而且变量名一多就乱。我一般会重新组织一个结构体,把元数据和数据分开存。
% 假设 dataStruct 是上一步读出来的结果 % 构造一个干净的输出结构 out = struct(); % 存采样率等关键属性(如果读取函数返回了 properties) if isfield(dataStruct, 'properties') out.properties = dataStruct.properties; end % 存通道组和通道名,方便以后追溯 out.groupNames = dataStruct.groupNames; out.channelNames = dataStruct.channelNames; % 存实际数据,按通道组嵌套 out.data = struct(); for gi = 1:length(dataStruct.groupNames) gName = dataStruct.groupNames{gi}; out.data.(gName) = struct(); chanList = dataStruct.channelNames.(gName); for ci = 1:length(chanList) cName = chanList{ci}; out.data.(gName).(cName) = dataStruct.channelData.(gName).(cName); end end % 存成 MAT 文件,使用 v7.3 格式以支持大文件 save('converted_data.mat', '-struct', 'out', '-v7.3');这里的关键点是-struct选项,它让save直接把结构体的字段展开成 MAT 文件里的变量,而不是把整个结构体塞进一个变量。-v7.3是为了支持大于 2GB 的文件,TDMS 经常用来存高速采集数据,文件很容易超过这个阈值。变量命名上,我习惯把原始通道名保留,因为后面做分析时经常要按通道名去索引,改名反而增加记忆负担。如果通道名里有空格或特殊字符,Matlab 的字段名会报错,这时候要么用matlab.lang.makeValidName转一下,要么把名字存在单独的元数据数组里,数据用编号索引。
3. 批量转换和参数调优:让脚本替你值夜班
3.1 批量遍历文件夹:把dir和循环写稳
单个文件转换跑通之后,下一步就是批量处理。测试台架跑一晚上,往往生成几十个 TDMS 文件,手动一个个读不现实。下面这个脚本会遍历指定文件夹下所有.tdms文件,逐个转换并保存到同名 MAT 文件。
% 设置输入和输出文件夹 inputDir = 'D:\data\tdms_batch'; outputDir = 'D:\data\mat_batch'; % 如果输出目录不存在就创建 if ~exist(outputDir, 'dir') mkdir(outputDir); end % 获取所有 tdms 文件 fileList = dir(fullfile(inputDir, '*.tdms')); fprintf('共找到 %d 个 TDMS 文件\n', length(fileList)); % 逐个处理 for k = 1:length(fileList) thisFile = fullfile(inputDir, fileList(k).name); fprintf('正在处理:%s\n', fileList(k).name); try dataStruct = TDMS_readTDMSFile(thisFile); out = struct(); out.groupNames = dataStruct.groupNames; out.channelNames = dataStruct.channelNames; out.data = dataStruct.channelData; % 构造输出文件名,把 .tdms 换成 .mat [~, baseName, ~] = fileparts(fileList(k).name); outFile = fullfile(outputDir, [baseName, '.mat']); save(outFile, '-struct', 'out', '-v7.3'); fprintf('已保存:%s\n', outFile); catch ME % 单个文件失败不影响后续文件 fprintf('处理失败:%s,原因:%s\n', fileList(k).name, ME.message); end end这段代码里有两个细节值得说。第一是try-catch,批量处理时最怕一个文件损坏导致整个循环中断,把异常捕获住,记录失败原因,继续跑后面的文件。第二是输出文件名用fileparts取基名再拼.mat,避免手动截字符串时把路径里的点号也截掉。参数上,fullfile比手动拼路径安全,能自动处理不同操作系统的分隔符。如果文件数量特别多,可以在循环里加一个进度打印,或者用parfor改成并行,但要注意 TDMS 读取函数本身是不是线程安全的,不确定的话先用for跑稳再说。
3.2 内存不够怎么办:分块读取和只存必要通道
TDMS 文件动辄几个 GB,如果一次性把所有通道读进内存再存 MAT,机器内存不够就会直接崩。我遇到过好几次半夜跑批处理,早上起来发现 Matlab 报Out of memory,前面跑的全白费。后来改成两种策略:一是只读我关心的通道,二是分块读取后追加写入 MAT。
% 策略一:只读指定通道组和通道 targetGroup = 'Analog'; targetChannels = {'Voltage', 'Current'}; % 假设读取函数支持指定通道,具体参数名看函数文档 dataStruct = TDMS_readTDMSFile(tdmsFile, ... 'groupNames', {targetGroup}, ... 'channelNames', targetChannels); % 策略二:如果函数不支持过滤,就读完后立即裁剪再存 % 读完后只保留目标通道,其余字段清掉 if isfield(dataStruct.channelData, targetGroup) allChans = fieldnames(dataStruct.channelData.(targetGroup)); for ci = 1:length(allChans) if ~ismember(allChans{ci}, targetChannels) dataStruct.channelData.(targetGroup) = ... rmfield(dataStruct.channelData.(targetGroup), allChans{ci}); end end end如果连单个通道都大到内存放不下,那就得分块。常见做法是按时间片读,比如每次读 10 秒的数据,存到 MAT 里再读下一段。Matlab 的matfile函数支持对 MAT 文件进行部分写入,不用一次性把整个变量加载到内存。
% 用 matfile 分块写入大通道数据 m = matfile('large_data.mat', 'Writable', true); blockSize = 100000; % 每块 10 万个采样点 totalLen = 500000; % 假设总长度 for startIdx = 1:blockSize:totalLen endIdx = min(startIdx + blockSize - 1, totalLen); % 这里假设 readTDMSBlock 是你自己封装的按块读取函数 blockData = readTDMSBlock(tdmsFile, 'Voltage', startIdx, endIdx); m.voltage(startIdx:endIdx, 1) = blockData; endmatfile的好处是写入时只操作指定区间,不会把整个变量载入内存。参数上,blockSize要根据机器内存和单个采样点字节数来算,一般留出至少一半内存给系统和其他变量。注意matfile对结构体嵌套的支持有限,复杂结构最好先展平再存。
3.3 时间戳和采样率:别让存下来的数据变成“黑匣子”
TDMS 文件里通常带时间戳和采样率属性,如果转换时只存了数据数组,过两个月再回头看,根本不知道这段数据是什么时候采的、采样率是多少。我一般会把关键属性单独提出来,存成 MAT 里的独立变量。
% 假设 dataStruct.properties 里有采样率和起始时间 out.meta = struct(); if isfield(dataStruct, 'properties') props = dataStruct.properties; % 不同读取函数返回的属性字段名可能不同,按实际情况取 if isfield(props, 'samplingRate') out.meta.fs = props.samplingRate; end if isfield(props, 'startTime') out.meta.t0 = props.startTime; end end % 如果没有属性,就手动补一个默认值并记录 if ~isfield(out.meta, 'fs') warning('未找到采样率属性,设为默认值 1000 Hz'); out.meta.fs = 1000; end这里用warning而不是直接报错,是因为批量处理时有些老文件可能确实没写属性,直接中断不值得。但警告信息要打出来,方便事后排查。存的时候把meta和data分开,后续画图时先读meta.fs再算时间轴,不会因为忘了采样率而把频谱画错。
4. 避坑与排查:TDMS 转 MAT 时最容易翻车的 5 个地方
4.1 现象:读出来的数据全是 0 或 NaN
原因通常有两种:一是通道索引对错了,把属性数组当成了数据数组;二是 TDMS 文件里该通道本身就没有有效数据,可能是采集时通道没接传感器。解决方法是先用读取函数把通道名和属性打印出来,确认哪个是数据通道,再检查原始文件在采集软件里能不能正常显示波形。如果采集软件里也是平的,那就是数据源问题,跟转换脚本无关。
4.2 现象:MAT 文件比 TDMS 大好几倍
TDMS 是二进制紧凑存储,MAT 的 v7 格式默认会对数据进行压缩,但如果你存的是double类型,而原始数据是int16,体积就会翻倍。解决方法是读出来之后先看数据类型,如果原始是整型,存 MAT 时保持整型,或者用single代替double。另外-v7.3格式基于 HDF5,对某些数据模式的压缩率不如 v7,如果文件没超过 2GB,可以试试不加-v7.3看体积变化。
4.3 现象:批量转换到一半报“文件正在被占用”
这种多半是采集软件还在往 TDMS 文件里写数据,或者上一个读取函数的文件句柄没释放。解决方法是转换前确认采集已停止,或者在脚本里加一个重试机制,等几秒再读。如果用的是第三方 DLL,要确保每次读完都调用了关闭文件的接口,否则句柄泄漏,跑几十个文件后就会报错。
4.4 现象:通道名里有特殊字符导致save报错
Matlab 的结构体字段名不能包含空格、横杠、点号等字符。如果 TDMS 里的通道名是Temp-1或Voltage Phase A,直接用作字段名会报Invalid field name。解决办法是用matlab.lang.makeValidName转换,或者把原始名存在一个单独的元数据数组里,数据用ch1、ch2这样的编号索引。我一般选后者,因为转换后的名字可能重复,反而不好追溯。
4.5 现象:时间轴对不上,数据整体偏移
TDMS 文件里可能同时存在绝对时间戳和相对时间,如果读取时把相对时间当成了绝对时间,画出来的图就会整体偏移。解决方法是先确认属性里t0的基准是什么,是 Unix 时间戳还是从 0 开始的秒数。存 MAT 时把t0和fs一起存,后续重建时间轴用t0 + (0:length(data)-1)/fs,不要依赖读取函数返回的时间数组,因为不同版本返回的基准可能不一样。
5. 进阶技巧:用matfile做增量写入和快速验证
批量转换跑通之后,我习惯再加一步验证:随机抽几个 MAT 文件,把数据画出来和原始 TDMS 在采集软件里的波形对比。这一步能抓出很多隐蔽问题,比如通道顺序错了、采样率填错了、数据被截断了。验证脚本不用太复杂,读一个 MAT,取前 1000 个点画个图,肉眼扫一眼有没有明显异常。
% 快速验证转换结果 testMat = 'converted_data.mat'; m = matfile(testMat); whos(m); % 查看文件里有哪些变量 % 假设数据存在 data 结构里,取一个通道画图 if isfield(m, 'data') % 注意 matfile 对嵌套结构的读取方式 % 这里只是示意,实际字段名按你的结构来 disp('请根据实际结构手动取通道数据'); end % 更稳妥的方式:用 load 加载小文件验证 S = load(testMat); if isfield(S, 'meta') && isfield(S.meta, 'fs') fs = S.meta.fs; fprintf('采样率:%f Hz\n', fs); endmatfile适合在文件很大时只读部分变量,但嵌套结构支持有限,验证阶段用load更省心。如果 MAT 文件超过内存,就还是用matfile按变量名读。我一般会在转换脚本最后加一个fprintf输出每个文件的通道数和采样点数,和原始 TDMS 的属性对一遍,数字对上了才放心。
另一个技巧是给 MAT 文件加一个版本标记,比如在结构里存一个out.version = 'v1',以后读取脚本升级了,可以按版本号做兼容。TDMS 读取函数本身也可能更新,不同版本返回的字段名不一样,存的时候把函数版本也记下来,过半年再回头看就知道当时用的是哪套逻辑。
说到底,TDMS 转 MAT 这件事本身不复杂,复杂的是数据量大、通道多、属性杂,加上批量处理时各种边界情况。我自己的习惯是:先拿一个文件跑通,把结构定死,再写批量脚本,最后加验证和日志。这样即使半夜跑批处理,第二天也能从日志里看出哪个文件出了问题,不用重新跑一遍。希望帮到你。
本文还有配套的精品资源,点击获取