TDMS文件将数据划分为三个不同层次的对象。最顶层由单个对象组成,其中包含了文件本身特有的信息,如作者或标题。每个文件可包含任意数量的组,每组又可包含任意数量的通道。在下图中,文件example events.tdms包含两个组,每组包含两个通道。
每个TDMS对象都有唯一的路径来进行标识。每个路径都是一个字符串,其中包含在TDMS层次结构中的对象名称及其所有者名称,由正斜杠分隔开来。每个名称都加引号。对象名称中的所有单引号都替换为双引号。下表展示了每种TDMS对象类型的路径格式示例:
对象名称 | 对象 | 路径 |
-- | 文件 | / |
Measured Data | 组 | /'Measured Data' |
Amplitude Sweep | 通道 | /'Measured Data'/'Amplitude Sweep' |
Dr.T's Events | 组 | /'Dr.T''s Events' |
Time | 通道 | /'Dr.T''s Events'/'Time' |
为了使所有TDMS客户端应用程序正常工作,每个TDMS文件都必须包含一个file对象。每个file对象都必须包含一个group对象,对应通道路径中使用的每个组名。此外,一个file对象可以包含任意数量没有通道的组对象。
每个TDMS对象可具有任意数量的属性。每个TDMS属性由名称(始终为字符串)、类型标识符和值组成。属性的典型数据类型包括数值类型,如整型或浮点数、时间标识或字符串。TDMS属性不支持数组。如果TDMS文件位于NI DataFinder的搜索区域内,所有属性都将自动可用于搜索。
只有TDMS文件中的通道对象可以包含原始数据数组。当前TDMS版本仅支持一维数组。
每个TDMS文件都包含两种类型的数据:元数据和原始数据。元数据是存储在对象或属性中的描述性数据。原始数据则是附加到通道对象的数据数组。TDMS文件包含一个连续数据块中多个通道的原始数据。为了能够从该数据块提取原始数据,TDMS文件使用原始数据索引,其中包括有关数据块组成的信息,包括与该数据对应的通道,数据块针对该通道包含的值的数量,以及数据存储的顺序。
数据以段为单位写入TDMS文件。每次将数据附加到TDMS文件时,都会创建一个新的数据段。有关此规则的例外情况,请参考本文的元数据和原始数据部分。数据段由以下三部分组成:
TDMS文件中的所有字符串(如对象路径、属性名称、属性值和原始数据值)均以UTF-8 Unicode编码。除原始数据值外,所有这些值都以32位无符号整型开头,包含以字节为单位的字符串长度,不包含长度值本身。TDMS文件中的字符串可能以NULL终止,但是由于存储了长度信息,当读取文件时,NULL终止符将被忽略。
TDMS文件中的时间标识以两个部分组成的结构形式存储:
布尔值每个存储为1个字节,其中1表示TRUE,0表示FALSE。
前端不仅包含用于验证数据段的信息,还包含用于随机访问TDMS文件的信息。以下示例显示了TDMS文件前端部分的二进制占用空间情况:
二进制布局(十六进制) | 说明 |
54 44 53 6D | "TDSm"标签 |
0E 00 00 00 | ToC掩码0x1110(数据段包含对象列表、元数据、原始数据) |
69 12 00 00 | 版本号(4713) |
E6 00 00 00 00 00 00 00 | 下一个数据段偏移(值:230) |
DE 00 00 00 00 00 00 00 | 原始数据偏移(值:222) |
上表中的前端部分包含以下信息:
标志 | 说明 |
#define kTocMetaData (1L<<1) | 数据段包含元数据 |
#define kTocRawData (1L<<3) | 数据段包含原始数据 |
#define kTocDAQmxRawData (1L<<7) | 数据段包含DAQmx原始数据 |
#define kTocInterleavedData (1L<<5) | 数据段中的原始数据是交错的(如果未设置标志,数据则是连续的) |
#define kTocBigEndian (1L<<6) | 数据段中的所有数值(包括前端、原始数据和元数据)均采用大端格式(如果未设置标志,则数据为小端格式)。ToC不受字节顺序影响,始终为小端格式。 |
#define kTocNewObjList (1L<<2) | 数据段包含新的对象列表(例如,该数据段中的通道与上一个数据段所包含的通道不同) |
TDMS元数据由三个不同层次的数据对象组成,包括文件、组和通道。这些对象类型均可包含任意数量的属性。元数据部分在磁盘上具有以下二进制布局:
磁盘上单个TDMS对象的二进制布局由多个部分按以下顺序组成。对象可能仅包含这些组成部分的子集,具体取决于存储在特定数据段中的信息。
下表列举了一个组和一个通道的元信息示例。其中,组包含两个属性、一个字符串和一个整型。通道包含原始数据索引,无属性。
二进制占用空间(十六进制) | 说明 |
02 00 00 00 | 对象数 |
08 00 00 00 | 第一个对象路径的长度 |
2F 27 47 72 6F 75 70 27 | 对象路径(/'Group') |
FF FF FF FF | 原始数据索引(“FF FF FF FF”表示未将原始数据分配给对象) |
02 00 00 00 | /'Group'的属性数量 |
04 00 00 00 | 第一个属性名称的长度 |
70 72 6F 70 | 属性名称(prop) |
20 00 00 00 | 属性值的数据类型(tdsTypeString) |
05 00 00 00 | 属性值的长度(仅适用于字符串) |
76 61 6C 75 65 | prop属性值(value) |
03 00 00 00 | 第二个属性名称的长度 |
6E 75 6D | 属性名称(num) |
03 00 00 00 | 属性值的数据类型(tdsTypeI32) |
0A 00 00 00 | num属性值(10) |
13 00 00 00 | 第二个对象路径的长度 |
2F 27 47 72 6F 75 70 27 2F 27 43 68 61 6E 6E 65 6C 31 27 | 第二个对象的路径(/'Group'/'Channel1') |
14 00 00 00 | 索引信息的长度 |
03 00 00 00 | 分配给该对象的原始数据的数据类型 |
01 00 00 00 | 原始数据数组的维数(必须为1) |
02 00 00 00 00 00 00 00 | 原始数据值的数量 |
00 00 00 00 | /'Group'/'Channel1'的属性数(无属性) |
下表列举了DAQmx原始数据索引的示例。
二进制占用空间(十六进制) | 说明 |
03 00 00 00 | 对象数 |
23 00 00 00 | 组对象路径的长度 |
2F 27 4D 65 61 73 75 72 65 64 20 54 68 72 6F 75 67 68 70 75 74 20 44 61 74 61 20 28 56 6F 6C 74 73 29 27 | 对象路径(/'Measured Throughput Data (Volts)') |
FF FF FF FF | 原始数据索引(“FF FF FF FF”表示未将原始数据分配给对象) |
00 00 00 00 | /'Measured Throughput Data (Volts)'的属性数 |
34 00 00 00 | 通道对象路径的长度 |
2F 27 4D 65 61 73 75 72 65 64 20 54 68 72 6F 75 67 68 70 75 74 20 44 61 74 61 20 28 56 6F 6C 74 73 29 27 2F 27 50 58 49 31 53 6C 6F 74 30 33 2d 61 69 30 27 69 12 00 00 | /'Measured Throughput Data (Volts)'/'PXI1Slot03-ai0' |
69 12 00 00 | DAQmx原始数据索引,包含格式更改换算值 |
FF FF FF FF | 数据类型,DAQmx原始数据 |
01 00 00 00 | 数据维数 |
00 00 00 00 00 00 00 00 | 值的数量,此数据段中没有值 |
01 00 00 00 | 格式更改换算值的向量大小 |
05 00 00 00 | 第一个格式更改换算值的DAQmx数据类型 |
00 00 00 00 | 第一个格式更改换算值的原始缓冲区索引 |
00 00 00 00 | 跨度内的原始字节偏移 |
00 00 00 00 | 格式位图示例 |
00 00 00 00 | 换算ID |
01 00 00 00 | 原始数据宽度的向量大小 |
08 00 00 00 | 原始数据宽度向量中的第一个元素 |
06 00 00 00 | /'Measured Throughput Data (Volts)'/'PXI1Slot03-ai0'的属性数 |
11 00 00 00 | 第一个属性名称的长度 |
4E 49 5F 53 63 61 6C 69 6E 67 5F 53 74 61 74 75 73 | 属性名称("NI_Scaling_Status") |
20 00 00 00 | 属性值的数据类型(tdsTypeString) |
08 00 00 00 | 属性值的长度(仅适用于字符串) |
75 6E 73 63 61 6C 65 64 | prop属性值("unscaled") |
13 00 00 00 | 第二个属性名称的长度 |
4E 49 5F 4E 75 6D 62 65 72 5F 4F 66 5F 53 63 61 6C 65 73 | 属性名称("NI_Number_Of_Scales") |
07 00 00 00 | 属性值的数据类型(tdsTypeU32) |
02 00 00 00 | 属性值(2) |
16 00 00 00 | 第三个属性名称的长度 |
4E 49 5F 53 63 61 6C 65 5B 31 5D 5F 53 63 61 6C 65 5F 54 79 70 65 | 属性名称("NI_Scale[1]_Scale_Type") |
20 00 00 00 | 属性的数据类型(tdsTypeString) |
06 00 00 00 | 属性值的长度 |
4C 69 6E 65 61 72/span> | 属性值("Linear") |
18 00 00 00 | 第四个属性名称的长度 |
4E 49 5F 53 63 61 6C 65 5B 31 5D 5F 4C 69 6E 65 61 72 5F 53 6C 6F 70 65 | 属性名称("NI_Scale[1]_Linear_Slope") |
0A 00 00 00 | 属性的数据类型(tdsTypeDoubleFloat) |
04 E9 47 DD CB 17 1D 3E | 属性值(1.693433E-9) |
1E 00 00 00 | 第五个属性名称的长度 |
4E 49 5F 53 63 61 6C 65 5B 31 5D 5F 4C 69 6E 65 61 72 5F 59 5F 49 6E 74 65 72 63 65 70 74 | 属性名称("NI_Scale[1]_Linear_Y_Intercept") |
0A 00 00 00 | 属性的数据类型(tdsTypeDoubleFloat) |
00 00 00 00 00 00 00 00 | 属性值(0) |
1F 00 00 00 | 第六个属性名称的长度 |
4E 49 5F 53 63 61 6C 65 5B 31 5D 5F 4C 69 6E 65 61 72 5F 59 6E 70 75 74 5F 53 6F 75 72 63 65 | 属性名称("NI_Scale[1]_Linear_Input_Source") |
07 00 00 00 | 属性的数据类型(tdsTypeU32) |
00 00 00 00 | 属性值(0) |
如上表所示,通道"/'Measured Throughput Data (Volts)'/'PXI1Slot03-ai0"包含两种换算值。一种换算值是格式更改,其信息存储在DAQmx原始数据索引中。另一种换算值是线性换算值,其信息存储为TDMS属性。格式更改换算值是可识别的,其中线性缩放器的斜率是1.693433E-9,截距是0,输入源ID是0。
与之前数据段相匹配的元信息可以在后续数据段中省略。这属于可选操作,但省略冗余的元信息能够显著提高文件的读取速度。如选择写入冗余信息,后续可使用LabVIEW、LabWindows/CVI、MeasurementStudio等提供的TDMS碎片整理功能移除这些信息。
以下示例显示了紧跟上述数据段的数据段元数据部分的二进制占用空间。写入新数据段的唯一元信息是新属性值。
二进制布局(十六进制) | 说明 |
01 00 00 00 | 新对象/更改对象数 |
08 00 00 00 | 对象路径的长度 |
2F 27 47 72 6F 75 70 27 | 对象路径(/'Group') |
FF FF FF FF | 原始数据索引(未将原始数据分配给对象) |
01 00 00 00 | 新对象/更改属性数 |
03 00 00 00 | 属性名称的长度 |
6E 75 6D | 属性名称(num) |
03 00 00 00 | 属性值的数据类型(tdsTypeI32) |
07 00 00 00 | num新的属性值(7) |
数据段最终包含与每个通道关联的原始数据。所有通道的数据数组都按照通道在数据段的元信息部分中显示的确切顺序连接在一起。数值数据需要根据前端中的小端/大端标志进行格式化。请注意,通道在首次写入后便无法更改其大小端格式或数据类型。
字符串类型通道经过预处理,可实现快速随机访问。所有字符串都以一个连续的内存段相连。该连续内存段中每个字符串第一个字符偏移都存储到32位无符号整型数组中。该偏移值数组首先存储,然后再存储连接的字符串值。得益于这种布局,客户端应用程序可以通过重新定位文件指针(最多三次)从文件中的任意位置访问任意字符串值,而无需读取客户端不需要的任何数据。
如果数据段之间的元信息不变,则前端和元信息部分可以完全省略,而原始数据可以附加到文件末尾。接下来的每个原始数据块都具有相同的二进制布局,可以根据前端和元信息来计算数据块数量,具体步骤如下所示:
原始数据可分为两种布局类型:交错和非交错。数据段前端的ToC位掩码表明数据段中的数据是否交错。例如:将32位整型值存储到通道1(1、2、3)和通道2(4、5、6)将产生以下布局:
数据布局 | 二进制占用空间(十六进制) |
非交错 | 01 00 00 00 02 00 00 00 03 00 00 00 04 00 00 00 05 00 00 00 06 00 00 00 |
交错 | 01 00 00 00 04 00 00 00 02 00 00 00 05 00 00 00 03 00 00 00 06 00 00 00 |
以下枚举类型描述了TDMS文件中属性或通道的数据类型。对于属性,数据类型值将存储在名称和二进制值之间。对于通道,数据类型将成为原始数据索引的一部分。
typedef enum {
tdsTypeVoid,
tdsTypeI8,
tdsTypeI16,
tdsTypeI32,
tdsTypeI64,
tdsTypeU8,
tdsTypeU16,
tdsTypeU32,
tdsTypeU64,
tdsTypeSingleFloat,
tdsTypeDoubleFloat,
tdsTypeExtendedFloat,
tdsTypeSingleFloatWithUnit=0x19,
tdsTypeDoubleFloatWithUnit,
tdsTypeExtendedFloatWithUnit,
tdsTypeString=0x20,
tdsTypeBoolean=0x21,
tdsTypeTimeStamp=0x44,
tdsTypeFixedPoint=0x4F,
tdsTypeComplexSingleFloat=0x08000c,
tdsTypeComplexDoubleFloat=0x10000d,
tdsTypeDAQmxRawData=0xFFFFFFFF
} tdsDataType;
注意:
有关TDMS写入功能的更多信息,请参阅《借助基于VI的API编写TDMS文件》。
LabVIEW波形在TDMS文件中表示为数值通道,其中波形属性作为属性添加到通道中。
如前几节所述,应用格式定义将创建完全有效的TDMS文件。但是,TDMS支持NI软件(如LabVIEW、LabWindows/CVI、MeasurementStudio等)常用的各种优化。如果应用程序尝试读取NI软件编写的数据,则需要支持本段所述的优化机制。
只有当对象路径、属性和原始索引等元信息发生变化时,才会添加到数据段中。以下示例说明了什么是增量元信息。
在第一次写入迭代中,将写入通道1和通道2。每个通道具有三个32位整型值(1、2、3和4、5、6)和几个描述性属性。第一个数据段的元信息部分包含通道1和通道2的路径、属性和原始数据索引。设置ToC位字段的kTocMetaData、kTocNewObjList和kTocRawData标志。第一次写入迭代会创建一个数据段。下表描述了第一个数据段的二进制占用空间。
部分 | 二进制占用空间(十六进制) |
前端 | 54 44 53 6D 0E 00 00 00 69 12 00 00 8F 00 00 00 00 00 00 00 77 00 00 00 00 00 00 00 |
对象数 | 02 00 00 00 |
元信息对象1 | 13 00 00 00 2F 27 67 72 6F 75 70 27 2F 27 63 68 61 6E 6E 65 6C 31 27 14 00 00 00 03 00 00 00 01 00 00 00 03 00 00 00 00 00 00 00 01 00 00 00 04 00 00 00 70 72 6F 70 20 00 00 00 05 00 00 00 76 61 6C 69 64 |
元信息对象2 | 13 00 00 00 2F 27 67 72 6F 75 70 27 2F 27 63 68 61 6E 6E 65 6C 32 27 14 00 00 00 03 00 00 00 01 00 00 00 03 00 00 00 00 00 00 00 00 00 00 00 |
原始数据通道1 | 01 00 00 00 02 00 00 00 03 00 00 00 |
原始数据通道2 | 04 00 00 00 05 00 00 00 06 00 00 00 |
在第二次写入迭代中,所有属性均未更改,通道1和通道2仍分别具有三个值,并且未写入其他通道。因此,该迭代将不会写入任何元数据。上一个数据段的元数据仍然有效。该迭代不会创建新的数据段;相反,它仅会将原始数据附加到现有数据段中,然后更新前端部分的下一个数据段偏移。下表描述了更新数据段的二进制占用空间。
部分 | 二进制占用空间(十六进制) |
前端 | 54 44 53 6D 0E 00 00 00 69 12 00 00 A7 00 00 00 00 00 00 00 77 00 00 00 00 00 00 00 |
对象数 | 02 00 00 00 |
元信息对象1 | 13 00 00 00 2F 27 67 72 6F 75 70 27 2F 27 63 68 61 6E 6E 65 6C 31 27 14 00 00 00 03 00 00 00 01 00 00 00 03 00 00 00 00 00 00 00 01 00 00 00 04 00 00 00 70 72 6F 70 20 00 00 00 05 00 00 00 76 61 6C 69 64 |
元信息对象2 | 13 00 00 00 2F 27 67 72 6F 75 70 27 2F 27 63 68 61 6E 6E 65 6C 32 27 14 00 00 00 03 00 00 00 01 00 00 00 03 00 00 00 00 00 00 00 00 00 00 00 |
原始数据通道1 | 01 00 00 00 02 00 00 00 03 00 00 00 |
原始数据通道2 | 04 00 00 00 05 00 00 00 06 00 00 00 |
原始数据通道1 | 01 00 00 00 02 00 00 00 03 00 00 00 |
原始数据通道2 | 04 00 00 00 05 00 00 00 06 00 00 00 |
在上表中,最后两行包含在第二次写入迭代期间附加到第一个数据段的数据。
第三次写入迭代将向每个通道再添加三个值。在通道1中,status属性曾在第一个数据段中设置为valid,但现在需要设置为error。这次迭代将创建一个新的数据段,该数据段的元数据部分现包含属性通道的对象路径、名称、类型和值。之后读取文件时,error值将覆盖先前写入的valid值。但是,之前的有效值将保留在文件中,除非对其进行了碎片整理。下表描述了第二个数据段的二进制占用空间。
部分 | 二进制占用空间(十六进制) |
前端 | 54 44 53 6D 0A 00 00 00 69 12 00 00 50 00 00 00 00 00 00 00 38 00 00 00 00 00 00 00 |
对象数 | 01 00 00 00 |
元信息对象1 | 13 00 00 00 2F 27 67 72 6F 75 70 27 2F 27 63 68 61 6E 6E 65 6C 31 27 00 00 00 00 01 00 00 00 04 00 00 00 70 72 6F 70 20 00 00 00 05 00 00 00 65 72 72 6F 72 |
原始数据通道1 | 01 00 00 00 02 00 00 00 03 00 00 00 |
原始数据通道2 | 04 00 00 00 05 00 00 00 06 00 00 00 |
第四次写入迭代添加了一个额外通道voltage,其中包含五个值(7、8、9、10、11)。该迭代将在TDMS文件中创建一个新的数据段,即第三个数据段。由于前一个数据段的所有其他元数据仍然有效,第四个数据段的元数据部分仅包括对象路径、属性和通道电压的索引信息。原始数据部分包含通道1的三个值,通道2的三个值和通道电压的五个值。下表描述了第三个数据段的二进制占用空间。
部分 | 二进制占用空间(十六进制) |
前端 | 54 44 53 6D 0A 00 00 00 69 12 00 00 5E 00 00 00 00 00 00 00 32 00 00 00 00 00 00 00 |
对象数 | 01 00 00 00 |
元信息对象3 | 12 00 00 00 2F 27 67 72 6F 75 70 27 2F 27 76 6F 6C 74 61 67 65 27 14 00 00 00 03 00 00 00 01 00 00 00 05 00 00 00 00 00 00 00 00 00 00 00 |
原始数据通道1 | 01 00 00 00 02 00 00 00 03 00 00 00 |
原始数据通道2 | 04 00 00 00 05 00 00 00 06 00 00 00 |
原始数据通道3 | 07 00 00 00 08 00 00 00 09 00 00 00 0A 00 00 00 0B 00 00 00 |
在第四个数据段中,通道2现有27个值。所有其他通道保持不变。现在,元数据部分包含通道2的对象路径和新原始数据索引,不包含通道2的属性。下表描述了第四个数据段的二进制占用空间。
部分 | 二进制占用空间(十六进制) |
前端 | 54 44 53 6D 0A 00 00 00 69 12 00 00 BF 00 00 00 00 00 00 00 33 00 00 00 00 00 00 00 |
对象数 | 01 00 00 00 |
元信息对象2 | 13 00 00 00 2F 27 67 72 6F 75 70 27 2F 27 63 68 61 6E 6E 65 6C 32 27 14 00 00 00 03 00 00 00 01 00 00 00 1B 00 00 00 00 00 00 00 00 00 00 00 |
原始数据通道1 | 01 00 00 00 02 00 00 00 03 00 00 00 |
原始数据通道2 | 01 00 00 00 02 00 00 00 03 00 00 00 04 00 00 00 05 00 00 00 06 00 00 00 07 00 00 00 08 00 00 00 09 00 00 00 0A 00 00 00 0B 00 00 00 0C 00 00 00 0D 00 00 00 0E 00 00 00 0F 00 00 00 10 00 00 00 11 00 00 00 12 00 00 00 13 00 00 00 14 00 00 00 15 00 00 00 16 00 00 00 17 00 00 00 18 00 00 00 19 00 00 00 1A 00 00 00 1B 00 00 00 |
原始数据通道3 | 07 00 00 00 08 00 00 00 09 00 00 00 0A 00 00 00 0B 00 00 00 |
在第五个数据段中,应用程序将停止编写通道2。该应用程序仅继续编写通道1和通道电压。由于通道顺序的变化,需要编写新的通道路径列表。必须设置ToC位kTocNewObjList。新数据段的元数据部分必须包含所有对象路径的完整列表,但不包含属性和原始数据索引,除非它们也发生变化。下表描述了第五个数据段的二进制占用空间。
部分 | 二进制占用空间(十六进制) |
前端 | 54 44 53 6D 0E 00 00 00 69 12 00 00 61 00 00 00 00 00 00 00 41 00 00 00 00 00 00 00 |
对象数 | 02 00 00 00 |
元信息对象1 | 13 00 00 00 2F 27 67 72 6F 75 70 27 2F 27 63 68 61 6E 6E 65 6C 31 27 00 00 00 00 00 00 00 00 |
元信息对象2 | 12 00 00 00 2F 27 67 72 6F 75 70 27 2F 27 76 6F 6C 74 61 67 65 27 00 00 00 00 00 00 00 00 |
原始数据通道1 | 01 00 00 00 02 00 00 00 03 00 00 00 |
原始数据通道3 | 07 00 00 00 08 00 00 00 09 00 00 00 0A 00 00 00 0B 00 00 00 |
写入TDMS文件的所有数据都存储在扩展名为*.tdms的文件中。TDMS文件可以随附*.tdms_index可选索引文件。索引文件用于加快*.tdms文件的读取速度。如果NI应用程序打开没有索引文件的TDMS文件,则将自动创建索引文件。如果LabVIEW或LabWindows/CVI等NI应用程序编写TDMS文件,则将同时创建索引文件和主文件。
索引文件是*.tdms文件的精确副本,不同之处在于它不包含任何原始数据,并且每个数据段都以TDSh标签而不是TDSm标签开头。索引文件包含各种信息,可精确定位*.tdms文件中任意通道的任意值。
简而言之,TDMS文件格式旨在以极高的速度写入和读取测量数据,同时保持描述性信息的层次结构。尽管二进制布局本身非常简单,但是通过增量写入元数据实现的优化可能会导致文件配置异常复杂。
LabWindows标志经Microsoft公司授权使用。Windows是Microsoft Corporation在美国和其他国家和地区的注册商标。