简介:IRIG-106-19 遥测标准资料包面向从事飞行试验、导弹遥测、数据采集与车载记录仪研发的工程师及高校相关专业学习者,系统汇编 2019 版标准正文与配套附录,可用于理解发射机与接收机系统、频分复用、脉冲编码调制、数字音频、包遥测下行、数字数据总线采集、遥测属性传输及数字车载记录仪等传统遥测链路,也覆盖遥测网络(TmNS)、网络协议套件、元数据配置、管理资源、TmNSDataMessage 传输、射频网络接入与射频网络管理等扩展内容,适用于白沙导弹试验场一类靶场遥测场景下的协议选型与设备开发。包内共 46 个文件,以 31 份 PDF 标准章节为主,辅以 5 个 ZIP 附件、4 个 XLSX 计算表、3 个 HTML 管理资源页面及 MIB、JSON、CSV 等配置与数据文件,压缩包约 51.45MB,章节与附录分列便于按主题检索。已有 1067 人学习下载。读者可据此核对标准条款、查阅附录细节,并利用管理资源矩阵、数据率与链路预算计算表辅助系统设计与联调,适合作为遥测协议选型、设备开发和试验实施的案头参考。
1. 20 字节包头读错,几百 GB 遥测数据就是噪声
IRIG 106 是试验遥测领域那份「谁都得认」的标准文档集。它约束的范围是从天线接收数据,到最终生成可回放文件的过程中,每一层的字段边界;它不规定某台设备内部如何工作。
多数人第一次接触它,是在解析一段采回来的数据时:同步字已经对上,帧也能切出来,结果通道值全是乱跳的乱码。问题通常不在算法,而在某个字段的位宽、字节序或时间基准被想当然地处理了。
这份标准值得下功夫研究的原因很实在:它把「约定」写成了可验证的数字。20 字节包头里每个 bit 的位置、10 ns 的时间刻度、帧长与码率的整除关系,目的都是让两个互不相识的团队在几年之后,依然能读懂对方的数据。搞清楚这些边界,解析器才不会在换一批数据时突然失败。
2. 分册结构:一份标准为何拆成十几本,而非合成一本规范
把压缩包解开,第一反应往往是「怎么这么多本」。射频一本、复用一本、PCM 编码一本、总线采集一本、元数据一本、记录器与文件格式一本……看起来是把一本厚书切碎了。实际上,这种拆分是整套标准最重要的设计决策之一,它决定了你后面写解析器时的所有取舍。
2.1 一条数据链在接口处被切开
遥测数据从产生到保存,会依次经过几个性质完全不同的环节。第一层是射频与调制:载波落在哪个频段、用什么调制方式、占用多少带宽,这一层关心的是频谱效率和发射机线性度。第二层是复用:多路信号怎么共享一条链路,频分把带宽切成亚载波,时分把时间切成槽位。第三层是编码:采样值怎么量化、怎么排进 PCM 帧、同步字放在哪里。第四层是采集与打包:总线上的消息、视频流、离散量,怎么变成统一的数据单元。第五层是元数据:每个槽位对应哪个物理量、量纲是什么、标度因子是多少。第六层是存储与传输:数据怎么写成文件、怎么加时间戳、怎么切分。
这套标准只在层与层的接口处规定格式。射频分册不规定你的发射机用什么功放,PCM 分册不规定你的采集器是几个通道,记录器分册也不规定你用哪种存储介质;它只规定,当数据要交给下一层时,必须长成什么样。
2.2 拆分的真正理由是「变更频率不一样」
如果只是模块化,一个附录也能做到。真正的动机是:每一层的稳定程度相差一个数量级。
射频与调制这一层受器件工艺和频谱使用惯例约束,一套调制方案定型后能用十几年;PCM 帧结构更是几十年没动过核心,同步字加主副帧那套逻辑,和早期的实现几乎一模一样;记录器与文件格式这一层最活跃,存储介质从磁带变成固态盘,包格式也跟着加字段、加类型;元数据那一层几乎每一版都在增补字段,因为新的传感器、新的总线类型不断出现。
把变更频率不同的内容装订在一起,代价是任何一处小改都要整体换版,实现方也没法「部分符合」:一个只做地面解析的团队,凭什么要为射频那一章的修订重新做一遍符合性验证?拆开之后,符合性变成了可以按分册声明的东西——做记录器的只声明记录器分册,做解调的只声明射频与编码分册。分册边界同时是责任边界,出了问题能精确定位到「谁不符合哪一章」,而不是笼统地说「不兼容」。
这种结构带来一个副作用,值得提前说清楚:标准大量使用保留位、保留字段和长度前缀。新版要加东西,优先塞进保留位,或者加一个新的类型编号,让老解析器不认识但能按长度跳过。这是典型的前向兼容手段,代价是标准文本里到处是「保留供将来使用」,第一遍读会觉得冗余。
真正的代价在互通性上。两台设备都声明「符合标准」,接起来仍然可能不通,因为标准把大量选择权交给了配置:帧长多少、同步字用哪个码型、槽位怎么排,全在配置文件里。
提示:标准的角色是一份「契约模板」,它不替代具体的接口控制文件。决定两份数据能不能对上的,往往是那份配置,而非标准原文。
这也解释了后面几章的结构:编码层和记录层的格式是硬性约定,必须逐字节对齐;元数据层的设计逻辑则完全不同,它要解决的是「怎么把配置本身也变成可交换的东西」。
3. PCM 帧:同步字、主副帧,和那条「帧长必须整除」的硬约束
PCM 是整套标准里最老、也最经得起时间考验的一层。它的核心思路相当简洁:把采样值按固定节奏排成一条比特流,每帧开头放一个特征码,接收端靠这个码找到帧边界,然后按预先约定的槽位表往回取值。简洁不等于简单,几乎所有解析器的重大问题都发生在这一层。
3.1 同步字不能随便挑:误锁概率是可以算出来的
同步字的本质是一个「在随机比特流里不容易撞上」的标记。如果同步字长 N 位,随机数据在每个比特位置撞上它的概率是 2⁻ᴺ。位速率越高,撞得越频繁。
按 10 Mbit/s 估算:16 位同步字平均每 65536 bit 就会误撞一次,也就是 6.5 ms 一次;24 位大约 1.7 s 一次;32 位则是 4.29×10⁹ bit,约 429 s,七分钟一次。这就是为什么主流方案都选 32 位同步字,16 位只在低码率、短帧的场合才勉强可用。
但七分钟一次误撞依然不能接受,所以标准并不把「找到同步字」当作锁定条件。常见的实现思路是一个三态状态机:搜索态先按位滑动找候选同步字;校验态要求接下来连续若干帧都在预期的位置出现同步字,并且帧长字段自洽;只有连续命中达到阈值,才进入锁定态。锁定之后同样要设丢锁阈值,连续若干帧失配才退回搜索态。这套机制把误锁概率从 2⁻³² 压到了 2⁻³²ᵏ 的量级,k 是连续校验帧数。
码型的选择也有讲究。常用的是 0x1ACFFC1D、0xFE6B2840 这类自相关旁瓣很低的序列——把同步字和它自身循环移位后的版本做相关,峰值要足够尖锐,旁瓣要足够平。绝不能选 0x00000000 或 0xFFFFFFFF,因为数据区里全 0 填充和全 1 填充太常见了,选这种码型等于主动增加误锁风险。
还有一个容易忽略的细节:如果链路用了加扰(randomizer)来均衡频谱,同步字必须排除在加扰之外。否则接收端要先解扰才能找到同步,而解扰算法本身又需要一个同步起点,会形成循环依赖。血泪经验往往就来自这种地方。
3.2 主帧、副帧:用一个计数器把慢变量塞进快帧
并不是所有参数都需要高速采样。温度、压力这类慢变量,一秒采一次就够了;振动、电压瞬变可能每秒要几万次。PCM 用主帧和副帧的机制来解决这个矛盾。
主帧是固定的时间单位,帧率决定主帧多久来一次。一个主帧内部被切成若干副帧,副帧之间的切换靠一个副帧计数器字来标识。某个慢变量只在自己的副帧里占一个槽位,其余副帧的同一位置放别的参数或者填充。这样一来,帧率可以很高,但单个慢变量的实际采样率是帧率除以副帧数。
举个具体数字:帧率 1000 fps,副帧数 16,某个温度参数每主帧出现一次,它的采样率就是 1000 ÷ 16 = 62.5 Hz。如果某个参数在副帧里还做了进一步的分频,比如每 4 个主帧才出现一次,那采样率再除 4,变成 15.625 Hz。算错这个除数,是数据「看起来对但物理量不对」最常见的原因。
采样率 = 帧率 ÷ 副帧数 ÷ 超帧倍数另一个机制是异步嵌入数据。有些数据(比如一条变长的总线消息、一段文本)长度不固定,塞不进固定槽位。常见的做法是在帧尾划一段「异步区」,区内第一个字是长度,接收端按长度取本次有效字节,剩下的忽略。这是把不定长数据装进定长帧的标准手法,代价是每帧都要多带一个长度字,并且异步区的大小必须按最坏情况预留,平时大部分是浪费的。
3.3 帧长为什么优先选 2 的幂
帧长和码率的关系是硬约束:
码率 (bit/s) = 帧长 (bit) × 帧率 (frame/s)帧率通常由位时钟整数分频得到。很多遥测系统的位时钟是 2 的幂(1.024 MHz、2.048 MHz 这种),因为它要从一个高稳定晶振分频下来。如果你的帧长是 1000 bit,位时钟 1.024 MHz,那帧率就是 1024 fps,多出来的 24 帧去哪了?答案是帧率会变成 1.024 kHz 这种非整数值,所有基于帧率推算出来的采样率都跟着带上小数,长期累积下来,不同架次的数据在时间轴上就对不齐了。
反过来,帧长取 256、512、1024、2048 bit,帧率就是位时钟的整数分之一,采样率、时间对齐、跨架次比对全部变整齐。这条经验听起来很朴素,但它是把「玄学对不齐」变成「算得清」的关键。
字长同样要固定:8、16、32 bit 是常见选项,比特序通常 MSB first,校验位可选。这些参数一旦定了,所有解析器都必须一致,没有中途调整的余地。
下面这段代码把「按位搜索同步字、切帧、算槽位对应的采样率」串起来:
SYNC = 0x1ACFFC1D # 32 位同步字,自相关旁瓣低 SYNC_BITS = 32 FRAME_BITS = 1024 # 主帧长度,取 2 的幂便于整数分频 SUBMODS = 16 # 副帧数 def bit_iter(buf): for byte in buf: for shift in range(7, -1, -1): yield (byte >> shift) & 1 # MSB first,和标准约定一致 def find_sync(buf, sync=SYNC, sync_bits=SYNC_BITS, limit=1 << 24): """按位滑动搜索,不能按字节对齐找,PCM 帧往往不对齐字节边界""" acc = 0 mask = (1 << sync_bits) - 1 for i, bit in enumerate(bit_iter(buf)): acc = ((acc << 1) | bit) & mask if acc == sync: return i - sync_bits + 1 # 同步字首比特的绝对位偏移 if i > limit: break return -1 def frame_offsets(total_bits, first_bit, frame_bits, max_frames=8): """从首个同步字起切出连续帧,供同步校验态使用""" count = (total_bits - first_bit) // frame_bits return [(k, first_bit + k * frame_bits) for k in range(min(count, max_frames))] def sample_rate(frame_rate, submods=SUBMODS, superframe=1): """某参数每主帧出现一次时的实际采样率""" return frame_rate / submods / superframe三个函数的职责分别是:find_sync返回比特级偏移,因为帧起点未必落在字节边界上,按字节find会漏掉七种对齐位置;frame_offsets只是把候选帧切出来,判断是否真的锁定还要看后续帧的同步字和副帧计数器是否连续;sample_rate把副帧数和超帧倍数这两级分频都算进去。实践中,frame_rate不要从字面帧长反推,而要从位时钟和帧长的整数关系推,否则小数误差会在长时间记录里放大。
4. 记录器包:0xEB90 之后的 20 个字节到底规定了什么
如果 PCM 解决的是「怎么在一条链路上排数据」,记录器分册解决的就是另一件事:把多种来源、多种速率、多种类型的数据混在一起存成文件,还能被任何人按时间顺序还原。这里的设计思路和 PCM 完全不同,它从「固定槽位」转向了「自描述包」。
4.1 定长二进制头的意义:不认识也能跳过
包头的字段布局可以归结成一张表:
| 字段 | 字节数 | 说明 |
|---|---|---|
| 同步字 0xEB90 | 2 | 包起始标记,部分实现前面还有 1 字节位同步 |
| 通道号 | 2 | 高 4 位数据类型,低 12 位通道编号 |
| 包长 | 4 | 整包字节数,含包头,可能含包尾校验 |
| 数据长 | 4 | 本包有效数据字节数 |
| 绝对时间 | 8 | 10 ns 为最小刻度,纪元为 2000-01-01 UTC |
| 数据体 | 变长 | 由数据类型决定内部格式 |
这套结构里最关键的性质,其实是包长字段的位置和含义。解析器拿到任意一个包,先校验同步字,再读包长,然后直接跳到下一包的起点,完全不需要理解数据体里的内容。遇到一个从没见过的数据类型,解析器不会崩溃,它会跳过这个包,继续往下读。
这就是前向兼容的落地方式:不是靠解析器预知所有类型,而是靠包长把「理解」变成可选项。相比之下,如果包头是变长的、或者长度信息在包尾,跳转就必须先完整解析整个包,未知类型立刻变成硬性故障。
数据长的存在则是为了处理填充。包长减去包头,可能大于数据长,多出来的部分是末尾填充,用于对齐或预留扩展。这两个字段不一致是正常的,不该报错,但需要在解析时按数据长截断。
4.2 通道号:一个文件里装下全部的异构数据
高 4 位类型、低 12 位编号,这个划分直接决定了文件能装什么。16 个类型足够覆盖 PCM 流、总线消息、模拟量、离散量、视频、时间信息、文本标注这些类别;12 位通道号意味着每类最多 4096 个通道,对绝大多数试验任务都够用。
关键在于,同一份文件里的包是按时间顺序交错写入的,哪个通道的数据先产生,哪个包就先落盘。所以文件本身是一个按绝对时间排序的事件流,回放时要做的是按通道分流,把每个通道的包抽出来组成各自的时间序列。这个过程不能假设同一通道的包在文件里连续,也不能假设不同通道包的数量成比例——高速通道的包数量可能是低速通道的上百倍。
注意:全 0 的通道号一般不承载业务数据,常被记录器用于自身的状态或索引记录。解析时遇到它,不要当成未知类型丢弃,先看一眼是不是设备自己写的信息。
4.3 绝对时间:10 ns 刻度与 2000 年纪元带来的换算
时间戳是 64 位无符号数,最小刻度 10 ns,纪元定在 2000-01-01 00:00:00 UTC。选这个纪元有现实考量:整个试验数据生态是在那之后建立起来的,用较近的纪元能让 64 位计数在高分辨率下撑得足够久,同时避免和 Unix 时间戳混淆。
换算时最容易错的一步是纪元偏差。2000-01-01 与 Unix 纪元相差 30 年,其中包含 7 个闰日,总共 946,684,800 秒。把 10 ns 计数直接当成 Unix 时间处理,时间戳会稳定地偏移三十年,而且因为偏移量是常数,很多人第一眼看不出来,只会在做跨系统对时的时候才发现所有数据都对不上。
时间源本身也有层级。链路上的绝对时间通常来自被外部时钟驯服的本地时钟,外部时钟可能是 IRIG-B 码或者卫星授时。IRIG-B 是每秒 100 个脉冲的串行码,内容包含年内天数、时、分、秒的 BCD 值,秒的小数部分用 10 位二进制表示,分辨率大约 1 ms;年份放在控制字段里,用 BCD 编码,所以跨年不会错。这里有个认知差异要注意:B 码只给年内天数和 UTC 时刻,没有时区概念,解析出来必须显式标成 UTC,否则和本地时间混用会多出几个小时的偏移。
精度链要清醒看待。时钟本身的抖动、打包延迟、时间戳打点位置(是采样时刻、打包时刻还是写入时刻),每一环都会引入误差。指望这套格式给你优于微秒的对齐,是不现实的;跨设备比对的误差预算里,这几项都要算进去。
import struct from datetime import datetime, timedelta, timezone HDR = struct.Struct(">HHIIQ") # 同步字、通道号、包长、数据长、绝对时间 SYNC = 0xEB90 EPOCH = datetime(2000, 1, 1, tzinfo=timezone.utc) UNIX_GAP = 946_684_800 # 2000 纪元相对 Unix 纪元的秒数 def parse_packet(buf, off=0): sync, ch_id, pkt_len, data_len, ticks = HDR.unpack_from(buf, off) if sync != SYNC: raise ValueError(f"sync 0x{sync:04X} at {off}") if pkt_len < HDR.size + data_len: raise ValueError(f"包长自相矛盾: pkt={pkt_len} data={data_len}") sec, rem = divmod(ticks, 100_000_000) # 1 秒 = 1e8 个 10 ns ts = EPOCH + timedelta(seconds=sec, microseconds=rem // 100) body_end = off + HDR.size + data_len tail = pkt_len - HDR.size - data_len # 末尾剩余字节 return { "type": (ch_id >> 12) & 0x0F, # 高 4 位:数据类型 "chan": ch_id & 0x0FFF, # 低 12 位:通道号 "ts": ts, "body": buf[off + HDR.size: body_end], "checksum": buf[body_end: off + pkt_len] if tail == 2 else None, "next": off + pkt_len, # 不依赖数据体即可跳转 }parse_packet里有两处刻意的写法。时间换算用divmod做整数分解,而不是ticks * 10e-9直接乘浮点,因为 64 位计数转成双精度会丢掉低位,几十秒之后误差就能看出来;微秒部分用rem // 100截断,等于把精度定在 100 ns,这已经比大多数时钟源的抖动要好,没必要为了凑满 10 ns 去用浮点。tail == 2的判断是「包尾是否带 16 位校验」的常见识别方式:如果包长减掉头和有效数据正好剩 2 字节,那基本就是校验字,否则就是纯填充。这个判断不是标准强制的,但比写死配置更省事。
5. 排查:解析器上线后最容易翻车的五个地方
写完第一版解析器,跑通样例数据,很容易产生「搞定了」的错觉。真正的考验在第二批、第三批数据上。下面这几条都是反复出现过的翻车记录,按现象、原因、解决的顺序写。
5.1 五条踩坑记录
第一条:同步能找到,切出来的帧全是乱的。现象是同步字位置正确、帧边界也能推出来,但按槽位表取出的数值毫无物理意义,跳变得像随机数。原因通常是位序或字序搞反了——MSB first 写成 LSB first,或者帧内 16 位字的高低字节颠倒。这类错误不会报异常,只会静默出错。解决办法是先打印一帧的原始十六进制,
本文还有配套的精品资源,点击获取