最近 hyperframes 这个词又频繁出现在通信圈的话题里。我第一次被它折腾到深夜,是在调GSM加密流程的时候:同一个手机、同一把密钥,两段业务流的密文竟然在某个时刻完全重样,排查到最后才发现是“帧号回绕”这个基础概念没处理干净。所谓 hyperframe(超帧),本质是无线帧嵌套结构最上面那一层——一套用来给时间刻度编号的“大数”,从GSM时代一直延续到LTE和5G的PDCP层。这篇文章我打算把它一次讲透:超帧为什么存在、数值是怎么算出来的、它在加密和同步里扮演什么角色,以及现代4G/5G协议里那个叫 Hyper Frame Number 的亲戚。适合正在啃协议规范的通信专业学生,也适合用基站日志和抓包工具排查无线指标的工程师。
1. 先从一次计数器回绕说起:无线帧为什么需要“超帧”这把大尺子
1.1 一帧只有4.6毫秒:小刻度下的调度难题
GSM空口的时间基础是TDMA帧,一个TDMA帧8个时隙,持续4.615毫秒左右。一路话音业务只占其中一个时隙,每个时隙发射一个突发脉冲。单独看这个刻度,事情其实挺简单:到点了就发,发完等下一帧。但真实系统里绝不止“每个时隙发一段语音”这么干净。语音编解码器每20毫秒吐出一个语音块,经过信道编码后变成456比特,要打散到相邻的多个突发里去抵抗衰落;同时,手机还要不断监听下行公共控制信道、接收系统消息、上发测量报告,这些开销全都挤在同一套帧结构里。
如果只有“4.6毫秒”这一把尺子,系统根本说不清楚“下次测量报告该在哪个瞬间发”。所以协议规定:在TDMA帧之上再往上套多帧,多帧之上再套超帧,超帧之上再套超帧周期——也就是 hyperframe。每一层都是给下面一层提供更粗粒度的时间坐标,让协议栈里各个模块都能用一套统一的编号去描述“现在到哪了”。
1.2 为什么偏偏是26和51:两把互质的尺子
GSM里话音业务用的是26帧业务多帧,时长正好120毫秒;公共控制信道用的是51帧控制多帧,大约235毫秒。为什么选26和51?因为26和51互质。两个不同周期的结构只要周期互质,它们的相对位置就要经过26×51=1326个帧才会回到原点。换句话说,一个业务多帧和一个控制多帧的关系,在一个超帧长度内会遍历所有组合,不会提前撞车。
这个性质对系统设计太重要了:不管业务信道落在哪一个时隙,它都能在一个可控的时间窗口内和公共信道、测量周期重新对齐。这种“用互质周期避免重复”的设计思路,在后面的跳频和加密规划里还会反复出现。1326个TDMA帧组成了一个超帧(superframe),时长6.12秒。为什么叫“超”帧?因为站在这个尺度上往下看,业务的排列、控制的排列已经不再是零散的单帧,而是有明确相位关系的整体。
1.3 一个里程表式的类比
把TDMA帧想成车轮转一圈,4.6毫秒一闪而过;多帧就是路边的百米牌;超帧是公里大牌;hyperframe 则是里程表上的累计总里程。没有总里程表,两辆车同时报“我现在在第7公里”,你根本不知道它们相隔多远、是否前后重复。无线同理:加密、跳频、测量、调度都依赖一个全局唯一的、长时间不重复的编号,而这个编号的最大周期,就是 hyperframe。
GSM的 hyperframe 由2048个超帧组成,总共2,715,648个TDMA帧,大约3小时28分53.76秒才会走完一整圈回绕一次。这套嵌套结构刚看会觉得绕,但它解决的是一个非常务实的工程问题:如何用有限位数的计数器,给空中每一个瞬间一个不产生歧义的“身份证号”。
2. 把26、51、1326、2048逐层拆开:超帧的算术与帧号分解
2.1 每一层到底多长:一张表算出所有数字
先给出完整参数,这些数字在3GPP TS 05.02系列里有明确定义,实际工作中最常用的是这几个:
| 层级 | 构成 | 时长 |
|---|---|---|
| TDMA帧 | 8个时隙 | 4.6153846毫秒 |
| 业务多帧 | 26个TDMA帧 | 120毫秒 |
| 控制多帧 | 51个TDMA帧 | 235.3846毫秒 |
| 超帧 | 51个业务多帧(或26个控制多帧) | 6.12秒 |
| hyperframe | 2048个超帧 | 3小时28分53.76秒 |
拿计算器按一遍就能看出规律:120除以26得到4.6153846毫秒;51个TDMA帧就是51乘以4.6153846,约235.3846毫秒。超帧用“51个业务多帧”来算更直观:51乘以120毫秒等于6.12秒,再乘以2048等于12533.76秒,换算成时分秒就是3小时28分53.76秒。这个精确值在协议测试里偶尔会踩到:有的自动化脚本把帧号周期写成了2715647,有的直接写死了一个“一整天”的循环,结果跑到 hyperframe 边界时状态机就乱了。
需要复现这些数字的时候,我习惯用下面这段Python直接算,避免手写浮点误差:
frame_ms = 120 / 26 # 4.6153846... ms hyperframe_ms = 2048 * 1326 * frame_ms seconds = hyperframe_ms / 1000 hours = int(seconds // 3600) minutes = int((seconds % 3600) // 60) secs = seconds % 60 print(f"{hours}h {minutes}m {secs:.2f}s")2.2 帧号分解:T1、T2、T3三个小表盘
GSM帧号FN并不直接用一个22位大整数传遍空口,而是拆成三个分量(T1、T2、T3),再由接收端按公式重新组合。T1取值范围0到2047,正好对应2048个超帧;T2取值范围0到25,对应一帧在这个业务多帧里的位置;T3取值范围0到50,对应一帧在控制多帧里的位置。这样只要在同步信道SCH里携带这些分量,手机就能算出自己处在超帧周期的哪一格。
这个分解关系是可以直接写成代码的,我在做协议栈时把它做成了最基础的自测函数:
def fn_to_t(fn): t1 = fn // (51 * 26) t2 = fn % 26 t3 = fn % 51 return t1, t2, t3 def t_to_fn(t1, t2, t3): return 51 * ((t3 - t2) % 26) + t3 + 51 * 26 * t1注意 t_to_fn 里那个模26的差值运算不是装饰品。因为FN在26和51两套周期里同时计数,直接用“t1乘个大数加t2乘个小数”是还原不出唯一FN的,必须先把两套尺子之间的相位差补回来。这个公式我见过不下三次被“聪明”地精简掉,结果就是解码出来的帧号时而对、时而差26个帧。
2.3 2048这个数字其实是被位宽逼出来的
为什么超帧周期是2048而不是2049、3000?因为T1要用11个比特表示,T2用5个比特,T3用6个比特,加起来22个比特刚好装进同步信道的规定字段。2048正好是2的11次方,作为T1的计数范围,既让超帧编号有足够区分度,又不浪费空口比特。
这是老协议里非常典型的“用位宽反推周期”设计:先定好信令字段能装多少比特,再回头决定时间尺子的量程。后面讲LTE/5G的HFN时你会看到,这个思路一点没变——上层协议的序列号多少位,直接决定了高层计数器怎么划分、回绕周期有多长。
3. 加密序列的“启爆点”:超帧号在空口安全里的真正角色
3.1 帧号是A5算法的第二把钥匙
GSM的话音和信令加密依赖A5系列算法,A5/1、A5/2、A5/3的输入都由两部分组成:会话密钥Kc,以及当前TDMA帧的帧号FN。算法对每一个突发脉冲重新算一遍密钥流,用这个密钥流逐比特去异或明文。为什么非要掺一个帧号进去?因为同一个会话里,每个突发必须拿到一股和其他突发绝不重复的密钥流;如果两个突发用了相同的“密钥加帧号”组合,那两个突发各自的密文异或在一起,正好等于它们明文的异或——攻击者借助明文冗余就能把内容剥出来。
hyperframe 在这里决定了密钥流多久会“重样”。在同一个连接内,只要通话时长小于3小时28分53.76秒且期间不变更密钥,FN就不会绕回原位,理论上不会出现同密钥同FN的情况。这个量程对单次通话绰绰有余,但对一个连续运转的基站而言,问题在另一方面:基站的绝对帧计数器是在跨呼叫持续累加的,并不会因为一次通话结束就复位。手机能保持和它对表,靠的正是第2节里那套T1/T2/T3的同步机制。
3.2 BTS和手机怎么对表:SCH上的“广播帧号”
手机开机后第一步是扫频,找到FCCH后确定频率基准,再解SCH拿到省略得只剩关键分量的帧号信息。SCH里携带的是简化的帧号(RFN),通常包含T1、T2、T3以及T3'这样的派生态,手机拿到这些字段后,用第2节的组合公式反算出本地应该把FN初始化成多少。此后手机在每帧末尾把自己的FN加1,通过连续监听SCH和自己的时隙分配来不断核对。
真正细致的工程点在于加密启动的瞬间。网络侧下发加密模式命令(Ciphering Mode Command)后,双方并不是从“收到命令的这一帧”就立刻加解密,而是选定一个统一的启动点:网络按自己维护的绝对帧号计算好,手机在收到命令后也按相同规则推算出同一个启动点。任何一个环节对帧号的推算差一帧,都会导致启动后立刻出问题。我在排障时见过最简单也最容易犯的错:实现者直接用了“当前帧号”作为启动点,而协议要求的其实是启动点与信令接收时刻之间的固定偏移关系,最终两边总是差4帧——刚好是交织深度,听起来就像通话里有持续杂音,但用编辑器逐帧比对SACCH块就露馅了。
3.3 计数器位数决定攻击面:一个历史视角
GSM时代把FN限制在22比特、大约3.5小时回绕一次,在当年看来够用;但后来A5/1被爆出多种攻击路径,反复使用相同“Kc+FN”组合的风险上升,3GPP才在UMTS、LTE里把COUNT这类加密输入扩展到32比特,并配合更频繁的密钥刷新策略。回头看,hyperframe 的周期设计其实一直在回答同一个问题:在密钥不换的情况下,计数器能不能保证数学上不重样,或者至少重样周期长到失去利用价值。
做协议实现时,ETSI给A5/3(KASUMI)准备了成体系的测试向量,3GPP TS 33.401的附录C也给LTE的NEA/NIA算法准备了含具体COUNT值的向量。我见过有人只拿全零参数跑通一次就宣布加密搞定,结果一换非零COUNT的高比特就全错位。关于计数器的向量测试,永远不要嫌多。
4. 从GSM走进LTE/5G:Hyper Frame Number换了个名字继续干活
4.1 PDCP的COUNT:超帧号的当代翻版
到了LTE和NR,物理层不再用“TDMA帧套多帧”的老办法,但那套“小计数器套大计数器”的思想原封不动地搬到了PDCP层。PDCP给每个协议数据单元分配一个序列号(SN),而加密和完整性操作真正使用的是32位的COUNT,COUNT的低位由SN充当,高位就是 Hyper Frame Number(超帧号,HFN)。
举个例子:一个LTE数据无线承载配置了12比特的PDCP SN,则COUNT低12位是SN,高20位是HFN。发射侧每发一个PDCP PDU就把SN加1,等SN从全1翻回全0的那一刻,HFN加1。这不就是用冒泡的方式复刻了“多帧到头、超帧进一”的逻辑吗?在NR里SN可以配置到18比特,HFN相应缩到14比特,但套路完全一样。
| 参数 | GSM TDMA FN | LTE/NR PDCP COUNT |
|---|---|---|
| 基础刻度 | 时隙/突发 | PDCP PDU |
| 嵌套结构 | T1/T2/T3 | HFN + SN |
| 回绕周期 | 2,715,648个TDMA帧 | 2的32次方个COUNT |
| 主要用途 | A5加密、跳频、同步 | NEA/NIA加密与完整性 |
4.2 HFN不同步:传输侧和接收侧的“表”各走各的
HFN的维护是PDCP里最容易出错的地方,因为它不是简单加一。接收侧要解密一个PDCP PDU,必须先估计出它对应的COUNT:SN能从包头里直接读,HFN则需要根据自己当前维护的下一个期望COUNT,再结合接收窗口推算。RLC层在上行或下行出现大量乱序、丢包、重复时,接收侧的SN窗口会跳来跳去,HFN估算就可能比发射侧多走一轮或者少走一轮。
一旦HFN差一格,之后的PDCP PDU明明SN看起来正常,解密出来的却全是乱码,因为解密、完整性校验用的COUNT整体偏了2的SN位宽那么大。表现到业务上就是突然的数据中断,重传也没用,直到高层触发PDCP重建或RRC重配才恢复。现场排查这类问题,不要一上来怀疑信道质量,先看日志里的PDCP丢弃计数和重排序窗口统计,再比对收发两侧记录的COUNT差值是不是恰好等于2的SN次方——是的话,几乎可以锁定HFN估算逻辑。
4.3 切换、双连接与超帧号的交接
HFN还有一个隐蔽的坑在移动性流程里。LTE切换时,源基站必须把当前PDCP状态(包括下一个待发送的SN和HFN)通过上下文交给目标基站;如果交接过程把这个字段弄丢,目标侧就得重新初始化HFN,结果就是切换后用户面立刻解密失败。5G非独立组网的分流承载更复杂,主节点和辅节点各自管理一部分PDCP,COUNT如何协调、HFN如何同步,是协议栈实现中出了名的难点。做设备联调时,专门翻上下文信令里 next_HFN 字段,比对切换前后的COUNT曲线,几乎是我排查用户面失败的固定动作。
从安全角度看,无论是NEA还是NIA,输入参数里固定有COUNT,这意味着COUNT的重复或者两端的错位,不仅影响业务,还会直接削弱机密性和完整性保护的有效性。现行规范里用户面完整性是否强制开启,不同网络有不同策略;但不管策略如何,管理好HFN是加密和完整性的共同前提。
5. 工程实战:和超帧/超帧号纠缠的三个经典翻车现场
5.1 翻车一:加密启动后前几秒全是杂音
先说一个真实项目里的例子。一个正在做GSM协议栈验证的项目,功能一切正常,唯独每次 Ciphering Mode 之后的前几百毫秒内语音全是“沙沙声”,然后自己恢复。抓日志对比发现,手机启动加密的帧号比网络期待的启动点提前了一个传输块。原因出在实现里把“当前帧号”当成了加密激活帧号,而没有计算信令到启动时刻之间的调度偏移。
对策是把启动帧号作为显式参数在状态机里传递,并写一条单测:给任意FN,推算激活帧号,再用第2节的 t_to_fn 反过来校验。从那以后,我在任何涉及“帧号边界”的代码里都强制做组合往返测试,这个习惯救了我很多次。
5.2 翻车二:模拟器加速跑过 hyperframe 边界
另一个真实场景发生在长时间浸泡测试。为了压缩回归时间,团队把协议栈挂在虚拟时间加速器里,一天当一个月跑。跑到某个节点,所有呼叫一夜之间全部加密失败。排查到最后,发现是虚拟时间的帧号加速逻辑没有处理 hyperframe 回绕:当FN走到2,715,647再回到0时,会话状态还能继续,但小区的绝对帧参考没有同步重置,导致新建立的呼叫把旧FN当成了新FN。
修复很简单:在帧号加一的地方挂一个回绕钩子,把回绕事件广播给所有依赖FN的模块。这类问题在纸上简单,到了真实系统里却极难发现,因为它要连续几小时不出错才踩到边界。所以,凡是在做加速仿真、clock scaling、或者长时间压力测试的团队,建议一开始就在代码里明确“帧号回绕”这个事件,而不是等系统自己触发问题。
5.3 翻车三:下行吞吐率骤降,PDCP窗口错位
LTE场景:某次外场测试,下行灌大流量二十分钟后吞吐率突然掉到零,空口指标正常,RRC连接也没有释放,但PDCP层持续报解码失败。翻基站日志发现,接收侧期望的COUNT和实际收到的COUNT始终差2的12次方——典型的HFN比发射侧多走了一轮。根源是传输侧在某个时刻因为丢弃定时器超时,一口气丢了大量PDU,而接收侧又处于重排序状态,SN窗口出现了错误的“回跳”,于是HFN被多推进了一次。
最终通过调整丢弃定时器、重启重排序流程解决。这个case让我彻底明白:HFN不是“高层不用管”的细节,它和丢包策略、重排序窗口、丢弃定时器这几个参数绑在一起,属于真正的工程设计问题。排查时先区分的不是“信号好不好”,而是“两端的计数器表走了有没有分岔”。
5.4 复现与验证的实用工具
遇到这类问题,我一般按三个步骤走。第一步,从抓包里提取PDCP SN,画出随时间的曲线,看有没有超过半量程的大跳变;第二步,翻上下文信令里的 next_HFN 字段,确认切换或重建后的HFN基线;第三步,如果条件允许,用仿真平台把HFN强制偏离一位,观察业务是否出现同样的解码失败特征,反过来验证结论。
这里不需要覆盖所有场景,只要能把“计数器不同步”和“无线信道差”区分开,问题就解决了一半。至于GSM侧,用软件无线电设备扫一个小区、解出SCH里的RFN分量,再用第2节的函数还原绝对帧号,也是很好的练手和验证手段。把抓包、日志、仿真三样东西拼起来看,超帧号相关的坑基本都能在半天内定位。
说句实在话,我第一次翻GSM的TS 05.02时也被这套套娃结构绕晕过,后来把公式抄到脚本上,对着SCH解码结果一行行验证,才算真正建立感觉。以后无论是读LTE/5G的PDCP规范、写协议栈,还是排空口故障,我都会先问一句:这一段里,谁的计数器在回绕、谁在维护超帧号、两边的表有没有可能差一格。带着这三个问题去看协议,很多文档就顺了。希望这篇围绕 hyperframes 的长文,能帮你少走一点我当时走过的弯路。