DTMB帧结构详解:超帧、PN帧头与系统信息扩频解调

发布时间:2026/11/2 3:13:42
DTMB帧结构详解:超帧、PN帧头与系统信息扩频解调

简介:《中国数字地面播DTMB帧结构介绍》面向数字电视传输、通信工程学习者及从事地面广播调试的技术人员,围绕国标DTMB分级帧结构与快速稳定同步机制进行系统梳理,可用于标准查阅、课程复习和工程参数核对。资源包共1个PDF文件,约612KB,内容集中、便于检索。资料依次说明信号帧、超帧、复帧、分帧和日帧的层级关系,并拆解帧头与帧体组成:帧体含36个系统信息符号和3744个数据符号,共3780个符号,帧头则覆盖PN420、PN595、PN945三种模式,涉及前同步、后同步、LFSR生成及不同相位序列识别等内容。文中还交代超帧定时校准、系统信息扩频保护与各模式参数差异;日帧以公历自然日为周期,由1440个分帧构成,分帧时长为1分钟,含480个超帧。目前已有739人学习,适合作为DTMB帧结构入门与速查参考。

1. 一开始就卡在帧头上:DTMB 帧结构到底解决什么问题

手上有一块解调板,频谱上看得到 8MHz 的包络,但同步模块一直报帧头相关峰不稳——这类问题九成要回到 DTMB 帧结构上找答案。它不是一张示意图,而是一套带绝对时间基准的分级结构:信号帧 4200 / 4375 / 4725 个符号、超帧 125ms、分帧 60s、日帧 24h,每一级都能用 7.56Msps 的符号率反算出来,算不平就说明某一级理解错了。

这份资料把复帧、超帧、分帧、日帧的包含关系,三种帧头模式(PN420 / PN595 / PN945)的 LFSR 构造与相位复用,以及 6 比特系统信息扩频成 36 个复符号、再和 3744 个数据符号复接成帧体的完整链路拆开讲了一遍。适合正在写同步、解帧体、做接收机联调的从业者,也适合第一次读标准文本、被"前同步 / 后同步""相位序号"绕晕的新手。

2. 五级帧结构的时间闭链:225 / 216 / 200 与 125ms 怎么对上

帧结构最容易翻车的地方不是看不懂图,而是把某一级的单位记混。比如把"一个超帧 225 个信号帧"记成"一个分帧 225 个信号帧",代码跑起来帧号会一直对不上,相关峰却是好的,排查起来非常费时间。这一章先把每一级的包含关系和时间长度钉死,再用一段脚本把三种帧头模式的超帧长度全部验算一遍。

2.1 从信号帧到日帧:谁是谁的整数倍

信号帧是基本单元,由帧头和帧体两部分时域信号组成,两者的基带符号率相同,都是 7.56Msps。帧体是固定的:36 个符号的系统信息加 3744 个符号的数据,共 3780 个符号,对应 500μs(3780 × 1/7.56μs)。帧头是可变的三选一:420、595、945 个符号,所以信号帧总长分别是 4200、4375、4725 个符号。

超帧定义为一组信号帧,时间长度固定为 125ms,和帧头模式无关。这里有个设计上的巧劲:帧头变长了,超帧里的信号帧数就相应变少,三种模式分别是 225、216、200 帧,乘出来都是 945000 个符号。超帧中的第一个信号帧叫首帧,接收机靠系统信息里的指示来确认它,从而把超帧计数器复位。

分帧是一组超帧,时长 60 秒,包含 480 个超帧。日帧是顶层,以一个公历自然日为周期重复,由 1440 个分帧构成,正好 24 小时。在北京时间 00:00:00 或其它选定的参考时间,日帧被复位,开始一个新的日帧。这条"绝对时间"约束是后面做单频网校准和端到端自检的基础。

层级组成时长换算
信号帧帧头 + 帧体555.6 / 578.7 / 625μs4200 / 4375 / 4725 符号
超帧225 / 216 / 200 个信号帧125ms945000 符号,每秒 8 个
分帧480 个超帧60s每秒 8 超帧 × 60
日帧1440 个分帧24h00:00:00 复位

这张表看着简单,但每一项都要能自己算出来才算真懂。7.56Msps 下每秒 8 个超帧、1800 个信号帧(模式 1)、1728 个(模式 2)、1600 个(模式 3),这三个数字在写帧计数器时是硬编码的常量,写错一个后面全错。

2.2 三种帧头模式下的超帧换算表与脚本校验

手工验算容易出错,尤其是 595 这种不规整的数。我一般会写十来行脚本,把三种模式的帧长、帧体长度、超帧时长一次性打出来,和文档里的数字对一遍,对不上就说明参数抄错了。

SYMBOL_RATE = 7.56e6 # 基带符号率,帧头与帧体相同 # 模式名: (帧头符号数, 信号帧总符号数, 每超帧信号帧数) MODES = { "PN420": (420, 4200, 225), "PN595": (595, 4375, 216), "PN945": (945, 4725, 200), } for name, (head_len, frame_len, frames_per_sf) in MODES.items(): body_len = frame_len - head_len # 帧体应恒为 3780 frame_us = frame_len / SYMBOL_RATE * 1e6 # 单帧时长 sf_us = frames_per_sf * frame_len / SYMBOL_RATE * 1e6 print(f"{name}: 帧头{head_len} 帧体{body_len} 帧长{frame_len} " f"= {frame_us:.2f}us, 超帧 {sf_us/1000:.3f}ms, " f"每秒 {SYMBOL_RATE/frame_len:.0f} 帧")

跑出来的关键一行是:三种模式的帧体都必须是 3780,超帧都必须是 125.000ms。body_len如果算出 3780 以外的值,说明帧头长度和总长不匹配,这是最常见的抄写错误;sf_us不是 125000 就说明每超帧帧数错了。参数上只有三列需要改:帧头符号数、总符号数、超帧帧数,其余全部由符号率推出来,所以这段代码同时也是一个自检工具。

落地时还有一个细节:SYMBOL_RATE用 7.56e6 而不是 7.56,浮点除法在帧数上百的时候误差会累积到微秒级,做时间戳对齐时会慢慢漂。真要长期跑,建议把单位统一成"符号数"整数运算,只在最后打印时转成时间,避免浮点累计误差。这个习惯在做日帧对齐时特别值钱,因为一天是 86400 秒、约 6.5×10⁸ 个符号,浮点早就飘了。

3. 帧头三模式选型:PN420、PN595、PN945 的阶数与相位复用

帧头是整个接收机链条上第一个被处理的信号,它的长度直接决定两件事:同步能扛多长的多径,以及每秒要浪费多少符号在非数据上。很多人选模式的时候只看"哪个余量大",其实反过来——帧头越长,保护间隔越长,但开销也越大,这是一笔很明确的账。

3.1 为什么帧头要用 m 序列而不是固定前导

帧头由 PN 序列构成,经"0"到 +1、"1"到 -1 的映射变成非归零二进制符号。用 m 序列而不是固定的全 1 前导,核心原因是自相关特性:m 序列的周期自相关在零延迟处是一个尖峰,其它位置接近零,接收机做滑动相关就能拿到一个非常干净的峰位,峰位就是帧起点。

第二个原因是复用。模式 1 和模式 3 在一个超帧内给每个信号帧分配不同相位的 PN 序列,相位序号本身就编码了帧号,接收机相关出峰之后,用本地哪个相位的序列匹配上的,就知道当前是超帧里的第几帧,不需要额外的信令。模式 2 是个例外:一个超帧里 216 个信号帧用相同的 PN 序列,帧号得靠计数器另算,实现简单但少了一条自校验路径。

第三个原因常被忽略:帧头在 C=3780 模式下同时充当 OFDM 的保护间隔。PN420 对应 55.6μs、PN595 对应 78.7μs、PN945 对应 125μs,按光速折算,分别对应约 16.7km、23.6km、37.5km 的等效路径差。单频网组网时站间距一旦超过这个量级,回波就会落到保护间隔外面,产生符号间干扰。所以模式不是随便挑的,得按网络规划反推。

3.2 LFSR 阶数、循环扩展与相位序号

三种模式对应三个不同阶数的 m 序列,构造方式各不相同:

模式阶数原始序列长度帧头长度构成方式超帧内帧数相位使用
模式 18 阶255420前同步 82 + PN255 + 后同步 83225序号 0~224
模式 210 阶1023595截取前 595 个码片216每帧相同
模式 39 阶511945前同步 217 + PN511 + 后同步 217200序号 0~199

模式 1 和模式 3 的前同步、后同步都定义为较短 PN 序列的循环扩展:PN420 是把 PN255 循环延拓,前面取 82 个符号、后面取 83 个符号;PN945 是把 PN511 循环延拓,前后各取 217 个符号。循环扩展的作用是让帧头和帧体之间的过渡在相关器看来仍然连续,边缘不会突然截断。

相位复用方面,8 阶 LFSR 能产生 255 个不同相位,标准选其中 225 个(序号 0~224);9 阶能产生 511 个相位,选其中 200 个(序号 0~199)。在每个超帧开始时 LFSR 复位到序号 0 的初始相位,也就是说相位序号和超帧内的帧号是一一对应的,这一点在写接收端帧计数时可以直接用。

注意:LFSR 的移位方向和抽头位置必须和发射端严格一致。方向反了,相关峰照样出,但相位序号会整体镜像,帧号全乱,而且因为峰是好的,很难第一眼看出来。

3.3 用通用 LFSR 脚本拼出 PN420 并验证长度

生成多项式在各家文档里都是公式图,抄进代码最容易出错。我的做法是写一个通用的 m 序列发生器,把抽头掩码作为参数传进去,抽头按原始文档的公式逐位对照填写,这样序列长度和前后同步的拼接可以独立验证。

def m_sequence(n_stages, taps, init_state, length): """通用 m 序列发生器 n_stages : 寄存器级数(8 / 9 / 10) taps : 反馈抽头下标集合,从 0 开始,按生成多项式逐项填写 init_state: 初始相位,长度必须等于 n_stages,每帧/每超帧复位时传入 length : 需要的输出码片数,不足一个周期时截断 """ reg = list(init_state) out = [] for _ in range(length): out.append(reg[-1]) # 末级输出 fb = 0 for t in taps: # 抽头异或 fb ^= reg[t] reg = [fb] + reg[:-1] # 右移,反馈进首级 return out def pn_map(bits): """0 -> +1, 1 -> -1 的映射""" return [1 if b == 0 else -1 for b in bits] # 模式 1:8 阶,先产生 255 长序列,再循环扩展成 420 pn255 = pn_map(m_sequence(8, taps={0, 2, 3, 4}, init_state=[0]*7 + [1], length=255)) pn420 = pn255[-82:] + pn255 + pn255[:83] assert len(pn420) == 420, "帧头长度不对,检查前同步/后同步取值" # 模式 3:9 阶,511 循环扩展成 945 pn511 = pn_map(m_sequence(9, taps={0, 2, 3, 5}, init_state=[0]*8 + [1], length=511)) pn945 = pn511[-217:] + pn511 + pn511[:217] assert len(pn945) == 945 # 模式 2:10 阶,1023 截断到 595 pn595 = pn_map(m_sequence(10, taps={0, 3}, init_state=[0]*9 + [1], length=595))

taps和init_state两个参数是这段代码的唯一变量,抽头必须对着原始文档的生成多项式逐项抄,初始相位则决定当前是第几个相位的序列——超帧开始时用序号 0 的初相,之后每帧换一次。pn420拼接那一行里,-82:取的是序列尾部做前同步、[:83]取头部做后同步,如果你的接收端实现里移位方向相反,这两段要对调,否则相关峰位置会整体偏移,帧起点判断差几十个符号,解出来的系统信息就是噪声。

验证方法很直接:把pn420和它自己滑动相关,零延迟处应该是 420 的峰,其它位置接近 0。这个自检跑通,再去接实际采集数据,排查范围能缩小一大半。

4. 从 6 比特系统信息到 3780 符号帧体:扩频链路与两种子载波模式

帧头解决"从哪里开始",系统信息解决"这一段是怎么调的"。36 个复符号挤在帧体最前面,位置固定、长度固定,但承载的信息决定了后面 3744 个数据符号怎么解——星座映射、码率、交织方式全在里面。这条链路如果解错,后面的信道解码一定是白噪声。

4.1 6 个比特里到底装了什么

系统信息在扩频前是 6 个比特,一共预设 64 种模式。位定义大致是这样:最高位保留,次高位是交织信息,低 4 位是编码调制模式。4 个比特的编码调制模式意味着 16 种组合,覆盖星座映射方式与 LDPC 码率的搭配,接收端拿到这 4 个比特,就知道该用哪个解调星座、哪个码率的 LDPC 译码器。

这 6 个比特不是直接发出去的,要经过扩频变成 32 比特,再映射成 32 个复符号。为什么要费这个劲?因为系统信息是整个帧里最不能错的部分:它错了,后面全错,而且没有重传机会。32 位扩频带来约 15dB 的扩频增益,在低信噪比下也能把它捞出来,代价只是每帧多花 32 个符号——相对 3780 的帧体,开销不到 1%。

保留位不要当成常量写死。虽然当前标准里它是保留的,但接收端如果做严格校验,把它当固定值去比对,遇到后续扩展就会误判整帧无效。稳妥做法是解出来但不参与判决。

4.2 Walsh 加扰四步:64 个 32 位矢量怎么来

从 6 比特到 32 比特的扩频过程分四步,每一步都能用脚本复现:

第一步,产生 32 个 32 位长的 Walsh 矢量,就是 32×32 Walsh 块的各行。第二步,把这 32 个矢量取反,连同原来的 32 个,一共 64 个矢量,正好对应 6 比特的 64 种取值;每个矢量按"+1 → 1、-1 → 0"映射成二进制矢量。第三步,这 64 个矢量与一个长度为 32 的随机序列按位异或。这个随机序列由 5 比特 LFSR 产生 31 位最大长度序列后补一个 0 得到,初始相位 00001,在每个信号帧开始时复位。第四步,32 比特按 I、Q 相同的 4QAM 映射成 32 个复符号。

第三步的加扰不能省。Walsh 码之间有正交性,但在频域上可能出现窄带干扰恰好打在某个码字上,加了一层随机序列之后,干扰被摊平到所有码字上,误判概率明显下降。

import numpy as np def walsh32(): H = np.array([[1]], dtype=int) while H.shape[0] < 32: H = np.block([[H, H], [H, -H]]) # 递归构造 Hadamard 矩阵 return H H = walsh32() vecs = np.vstack([H, -H]) # 64 个 32 位矢量,行优先 bits = (vecs < 0).astype(int) # +1 -> 1, -1 -> 0 # 5 阶 LFSR 产生 31 位 m 序列,补一个 0 得到 32 位随机序列 pn31 = m_sequence(5, taps={0, 2}, init_state=[0, 0, 0, 0, 1], length=31) rand32 = np.array(pn31 + [0], dtype=int) sysinfo_vecs = bits ^ rand32 # 按位异或,得到 64 个系统信息矢量 print(sysinfo_vecs.shape) # (64, 32)

walsh32()用递归块构造,得到的 32 行互相正交,这是接收端能一次解出 6 比特的前提。vecs竖直堆叠正、反两组,行号 0~63 直接对应系统信息模式编号。pn31的抽头同样按文档公式填,rand32补的那个 0 位置固定在最末,不能随手补在前面,否则异或后的码字全部错位。

接收端的解扩正好是逆过程,而且因为 Walsh 矩阵正交,可以用一次矩阵乘法把所有 64 个假设都算完:

def decode_sysinfo(rx32, rand32, H): """rx32: 收到的 32 个实数值(I 路即可,I、Q 相同) 返回 0~63 的系统信息模式编号""" y = np.array(rx32) * (1 - 2 * rand32) # 用 ±1 序列解扰 corr = H @ y # 与 32 个 Walsh 码做相关 return int(np.argmax(np.abs(corr)))

np.abs是必须的:取反的那 32 个矢量相关出来是负峰,取绝对值之后 64 个假设的峰统一比较,argmax的下标就是模式编号。这一步在 FPGA 上就是一次 32 点的快速变换,资源占用很小。

4.3 36 个复符号的复用位置与 C 模式指示

32 个系统信息复符号前面还要加 4 个复符号,用来指示帧体模式:C=1 模式时这 4 个比特是"0000",C=3780 模式时是"1111",同样按 I、Q 相同的 4QAM 映射。加上这 4 个,正好 36 个复符号,连续排列在帧体数据的前 36 个符号位置,后面接 3744 个数据符号。

这个位置是硬约束,不能挪。接收机一旦通过帧头相关确定了帧起点,就按固定偏移去取第 1 到第 36 个符号做解扩,偏移差一个符号,36 个符号全部错位,解出来就是随机数。所以前面章节反复强调帧长必须算准,误差会直接体现在这里。

C 指示的意义在于接收端不需要额外信令就知道该走哪条解调路径。4 个符号看着少,但它是硬判决的:解出来 4 个都是 +1 判 C=1,都是 -1 判 C=3780,出现混合就说明同步有问题或者信噪比太低,可以直接丢帧,不用浪费后面的解码算力。

4.4 C=1 双导频与 C=3780 频域交织的差别

两条路径的差别是单载波和 OFDM 的差别,实现上几乎是两套代码。

C=1 时,帧体的 3780 个符号就是时域样点,直接成形输出,占用的射频带宽 7.56MHz,时域信号块长度 500μs。作为可选项,可以在 ±0.5 符号速率位置插入双导频,两个导频的总功率相对数据总功率是 -16dB。插入方式是从日帧的第一个符号(编号 0)开始数,在奇数符号上实部加 1、虚部加 0,在偶数符号上实部加 -1、虚部加 0。注意起点是日帧的第一个符号,不是信号帧的第一个符号,这个细节后面避坑章节会专门说。

C=3780 时,帧体信息符号 X(k) 先做频域交织得到 X(n),再做反变换生成时域信号。相邻子载波间隔 2kHz,3780 个子载波正好铺满 7.56MHz(3780 × 2kHz = 7.56MHz),时域块长度 500μs(1/2kHz)。这里的 3780 点变换不是 2 的幂,常规做法是按 3780 = 4 × 27 × 5 × 7 做混合基分解,或者直接上通用长度的 DFT 算法,硬套 4096 点 FFT 会引入额外的频谱搬移和误差,解出来星座是散的。

两条路径共用同一个帧结构、同一个帧头,所以同步模块可以完全复用,只在帧体处理处分叉。做验证的时候,我一般先确认 C 指示解对,再分别用单载波和 OFDM 的测试向量各跑一遍,两边都过才算这条链路通了。

5. 落地避坑:五条帧计数与解扩翻车记录

帧结构文档看起来是纯理论,但真正卡住进度的往往是几个很小的约定问题。下面五条是我在实际联调里踩过的,每条都按"现象 → 原因 → 解决"记下来,遇到了可以直接对照。

5.1 帧计数与时间基准上的三条坑

坑一:把"复帧"当成了计数单位。现象是帧计数器跑到 225(或 216、200)就复位,逻辑看着没问题,但时间戳和外部时钟对不上,每隔几秒就偏一点。原因是文档里"复帧定义为一组超帧"这句话容易被读成"复帧就是那个 225 帧的组",实际上 225 帧组成的是超帧,125ms,这才是有明确时间长度的单位。解决办法是把计数单位统一到"超帧(125ms)→ 分帧(60s)→ 日帧(24h)"这条链上,用时间反推帧号,而不是用帧数去凑时间。

坑二:帧长按 3780 算,忘了加帧头。现象很典型:帧头相关峰又尖又稳,但解出来的系统信息全是随机值,C 指示一会儿全正一会儿全负。原因是取帧体的偏移写成了 3780,实际应该是 4200、4375 或 4725,具体看帧头模式。这个错误隐蔽在于同步环节完全正常,只有往后取数据才出问题。解决是把偏移量做成由模式查表得到的常量,并且在初始化时断言帧长 - 帧头长度 == 3780。

坑三:双导频的起点从信号帧 0 开始数。C=1 模式插双导频时,符号编号的零点定义在日帧的第一个符号上,不是每个信号帧重新从 0 开始。现象是导频位置每隔一段时间整体偏移一个符号,均衡器系数缓慢发散,误码率随运行时间上升。原因是每个信号帧的帧体符号数 3780 是偶数,如果从帧起点重新计数,奇偶性会逐帧翻转。解决是把符号计数器做成全局的,从日帧复位点开始累加,只在模 2 判断奇偶时使用。

5.2 解扩与变换上的两条坑

坑四:把系统信息当普通 QPSK,I、Q 分开解。现象是两个支路单独看都有相关峰,但合起来判决总是错一半。原因是系统信息的 4QAM 是 I、Q 相同的映射,同一个比特同时决定实部和虚部,分开解相当于把信噪比浪费了一半。解决办法是只用 I 路(或 I、Q 相加)做 32 位相关,argmax取绝对值,实现简单且增益最高。

坑五:3780 点变换直接套 4096 点 FFT。现象是 C=3780 模式下星座图旋转且发散,换个符号率又好像好一点,非常像"玄学"。原因是 3780 不是 2 的幂,用 4096 点 FFT 再做截断,等价于在频域加了一个矩形窗,子载波之间产生泄漏。解决办法是按 4 × 27 × 5 × 7 做混合基分解,或者用支持任意长度的变换库,先在仿真里把 3780 点正反变换的误差跑到 1e-6 以下再上板子。

提示:这五条里有四条都能用一段"帧长自检 + 全局符号计数"的前置检查提前发现,建议把它做成上电自检的一部分,比事后抓波形省时间。

6. 收尾技巧:用符号计数器把日帧复位点验一遍

前面所有的坑,归根到底都是"某个计数器从哪个零点开始数"的问题。所以最后一件事我不看星座图,先做一个符号计数器,从日帧复位点开始累加,看能不能把任意时刻精确定位到"第几个分帧、第几个超帧、第几个信号帧",再和接收端上报的帧号比对。这一步过了,帧结构这块基本就没什么可翻车的了。

SYMBOL_RATE = 7.56e6 SYMS_PER_SF = 945_000 # 三种帧头模式的超帧符号数都是这个值 SF_PER_SUBFRAME = 480 # 一个分帧 480 个超帧 def locate(t_sec, frames_per_sf): """t_sec: 相对日帧复位点(00:00:00)的秒数 frames_per_sf: 225 / 216 / 200 返回 (分帧号, 超帧号, 信号帧号)""" total = int(round(t_sec * SYMBOL_RATE)) # 全程整数运算,避免浮点漂移 sf_idx, off = divmod(total, SYMS_PER_SF) frame_len = SYMS_PER_SF // frames_per_sf # 4200 / 4375 / 4725 subframe, sf_in_sub = divmod(sf_idx, SF_PER_SUBFRAME) frame_idx, _ = divmod(off, frame_len) return subframe, sf_in_sub, frame_idx # 例:模式 1,日帧复位后 3723.4 秒 print(locate(3723.4, 225))

total用int而不是浮点,是因为一天下来符号数是 6.5×10⁸ 量级,双精度浮点的有效位已经不够逐符号精确了。divmod连用三次,正好对应超帧、分帧、信号帧三级,frame_len由超帧符号数除以帧数得到,改模式只需要改一个入参。跑出来的三元组如果和接收端上报的帧号只差最后一位,基本可以判定是帧起点对齐差了几个符号;如果分帧号差 1,那八成是日帧复位点没对齐到 00:00:00,而不是代码算错了。

从那以后我每次接一个新的解调链路,都会先把这个计数器跑通,再去调星座和译码,因为同步问题和解调问题长得太像了,不先把时间基准钉死,后面全是在猜。希望帮到你。

本文还有配套的精品资源,点击获取