资讯详情 LTE PUCCH符号生成实战:ZC序列、循环移位与OCC扩频详解
📅 2026/10/22 3:18:43
简介这份资源围绕LTE物理上行控制信道PUCCH符号的产生展开面向无线通信、LTE协议栈开发与物理层算法学习的中高级读者帮助理解PUCCH的结构、格式与发送方式解决控制信息在有限资源下高效传输的实现问题。压缩包共32个文件约18.13MB以17个m脚本和7个asv备份文件为主覆盖PUCCH调制解调、Zadoff-Chu序列生成、循环移位与信道编码等环节另含4个doc、2个docx和2个pdf参考文档涉及36.211、36.212等协议及LTE-A深入解析。已有339人学习下载。读者可借助脚本与文档对照梳理PUCCH Format 0/1/1a/1b/2/2a/2b/3等格式的生成流程理解BPSK/QPSK调制、发射分集与资源分配逻辑并参考协议原文与说明文档完成从理论到代码的验证适合作为物理层链路仿真与协议学习的实践素材。1. PUCCH 符号生成从一份被文件名出卖的工程笔记说起一份名为「物理上行控制信道符号的产生」的压缩包文件名里塞满了 LTE、上行、PUCCH、物理上行控制信道这些关键词还夹着一串数字和一个人名。这种命名方式一看就是某个工程师在项目现场随手打包的产物——没有 README没有目录结构说明只有一堆源码和可能的仿真脚本。但恰恰是这种「脏」文件往往藏着最真实的工程细节。PUCCH 是 LTE 上行链路里承载 HARQ 反馈、调度请求和 CSI 报告的控制信道。它不像 PDSCH 那样动辄几十兆的吞吐但少了它整个上行调度就会变成瞎子。符号产生是整个 PUCCH 处理链的第一环决定了后续加扰、调制、资源映射能不能跑通。这篇文章面向的是需要在本地复现 PUCCH 基带信号、做链路仿真或者排查上行接入问题的工程师。如果你正在对着 36.211 协议发愁或者想搞清楚 ZC 序列、循环移位、正交覆盖码到底怎么组合成那 12 个 RE 上的符号接下来的内容应该能帮你省掉几个晚上的翻协议时间。2. PUCCH 符号生成的协议框架与选型逻辑2.1 为什么 PUCCH 格式决定了符号生成的复杂度LTE PUCCH 有 Format 1/1a/1b、Format 2/2a/2b、Format 3 这几大类。格式不同符号生成的路径完全不一样。Format 1 系列走的是 Zadoff-Chu 序列加循环移位再乘正交覆盖码的路子一个时隙里 7 个 SC-FDMA 符号其中 3 个是解调参考信号4 个承载数据。Format 2 系列反过来参考信号占 2 个符号数据占 5 个而且数据部分用的是 QPSK 调制后的 ZC 序列。Format 3 则是 DFT-S-OFDM 结构支持更大的载荷。选型逻辑很直接如果你要传 1-2 比特的 HARQ ACKFormat 1a/1b 就够了如果要传 CSI 这种几十比特的报告得上 Format 2 或者 Format 3。符号生成的第一步就是根据格式确定参考信号和数据符号的时隙位置这个映射关系在 36.211 的 Table 5.4.1-1 和 5.4.2-1 里写得明明白白。2.2 ZC 序列的根参数与循环移位怎么定ZC 序列是 PUCCH 符号生成的基石。序列长度取小于 12 的最大质数也就是 11但实际映射到 12 个子载波上时最后一个 RE 是补零还是重复第一个元素取决于具体实现。根序列索引 u 和循环移位 α 决定了序列的自相关和互相关特性。import numpy as np def zc_sequence(u, N_zc12): 生成长度为 N_zc 的 Zadoff-Chu 序列 u: 根序列索引 N_zc: 序列长度PUCCH 中通常取 12 返回: 复数序列 n np.arange(N_zc) # 标准 ZC 序列公式注意 q 的取法影响序列的对称性 q u # 简化处理实际协议中 q 由组跳变和序列跳变决定 seq np.exp(-1j * np.pi * q * n * (n 1) / N_zc) return seq def cyclic_shift(seq, alpha): 对序列做循环移位 alpha: 循环移位量单位弧度 N len(seq) n np.arange(N) return seq * np.exp(1j * alpha * n)这段代码里u的取值不是随便定的。协议规定了 30 个序列组每组里有两个序列具体用哪个由小区 ID 和时隙号通过跳变图案决定。alpha的取值在 Format 1 里是 2π 乘以一个 0 到 11 的整数再除以 12对应 12 个正交的循环移位。实际工程里如果你不做多用户复用alpha可以固定为 0但一旦要区分同一小区里的多个 UE循环移位就是第一道正交隔离手段。2.3 正交覆盖码的时域扩频与参数映射Format 1 系列的数据符号要在时域上做扩频用的就是正交覆盖码。长度为 4 的 OCC 用于数据符号长度为 3 的 OCC 用于参考信号。这些码字是 Walsh-Hadamard 码的变体具体索引由 PUCCH 资源索引隐式确定。def occ_code(length, index): 生成长度为 length 的正交覆盖码 length: 4 或 3 index: 码字索引 if length 4: # 长度为 4 的 Walsh 码 codes np.array([ [1, 1, 1, 1], [1, -1, 1, -1], [1, 1, -1, -1], [1, -1, -1, 1] ]) elif length 3: # 长度为 3 的 DFT 码 codes np.array([ [1, 1, 1], [1, np.exp(1j*2*np.pi/3), np.exp(1j*4*np.pi/3)], [1, np.exp(1j*4*np.pi/3), np.exp(1j*2*np.pi/3)] ]) return codes[index % len(codes)]这里有个容易翻车的点OCC 的索引不是独立配置的而是和循环移位、资源块位置一起通过一个联合索引 n_PUCCH 算出来的。如果你在仿真里手动指定了 OCC 索引但忘了同步更新循环移位接收端解出来的星座图会直接散掉。我一般会在生成符号之前先把 n_PUCCH 到 (alpha, OCC_index, RB) 的映射表打印出来核对一遍。3. 从源码到波形PUCCH 符号生成的实操路径3.1 搭建最小可跑的符号生成环境不需要完整的 LTE 协议栈一个 Python 环境加 NumPy 和 SciPy 就够了。如果你手头有那份压缩包里的源码大概率是 MATLAB 或者 C 写的但核心逻辑用 Python 重写一遍反而更容易调试。先确认你的环境里 NumPy 版本不要太老复数运算和 FFT 的精度在 1.20 之后的版本里比较稳定。pip install numpy scipy matplotlib安装完之后建一个工作目录把协议里用到的常数先定义好。比如每个时隙的 SC-FDMA 符号数、PUCCH 占用的资源块数、采样率这些。采样率不用一上来就怼到 30.72 MHz先用 1.92 MHz 跑通逻辑后面再升采样。3.2 生成一个 Format 1a 的完整符号序列下面这段代码把 ZC 序列、循环移位、OCC 扩频串起来生成一个时隙内 7 个符号的频域数据。注意参考信号和数据符号的处理顺序不一样。def generate_pucch_format1a(ack_bit, n_pucch, cell_id): 生成 Format 1a 一个时隙的频域符号 ack_bit: 0 或 1对应 ACK/NACK n_pucch: PUCCH 资源索引 cell_id: 小区 ID N_sc 12 # 每个符号占用的子载波数 N_symb 7 # 一个时隙的 SC-FDMA 符号数 # 根据 n_pucch 计算循环移位和 OCC 索引 # 这里简化了协议里的映射公式实际要查表 alpha_idx n_pucch % 12 occ_idx_data (n_pucch // 12) % 4 occ_idx_ref (n_pucch // 12) % 3 alpha 2 * np.pi * alpha_idx / 12 base_seq zc_sequence(ucell_id % 30, N_zcN_sc) shifted_seq cyclic_shift(base_seq, alpha) # 数据符号的调制ACK 用 BPSKNACK 用另一个点 d 1 if ack_bit 0 else -1 # 参考信号符号位置符号 2, 3, 4从 0 开始计数 ref_positions [2, 3, 4] data_positions [0, 1, 5, 6] occ_data occ_code(4, occ_idx_data) occ_ref occ_code(3, occ_idx_ref) slot_symbols np.zeros((N_symb, N_sc), dtypecomplex) # 填充参考信号 for i, pos in enumerate(ref_positions): slot_symbols[pos, :] shifted_seq * occ_ref[i] # 填充数据符号 for i, pos in enumerate(data_positions): slot_symbols[pos, :] shifted_seq * d * occ_data[i] return slot_symbols这段代码跑完你会得到一个 7×12 的复数矩阵。每一行对应一个 SC-FDMA 符号上的 12 个子载波。接下来要做 IFFT 才能变成时域波形但那是下一步的事。先检查这个矩阵参考信号位置的三个符号应该只差一个 OCC 系数数据位置的四个符号应该只差一个 OCC 系数和调制符号。如果这个关系不对说明循环移位或者 OCC 索引算错了。3.3 参数怎么改从单用户到多用户复用单用户跑通之后下一步通常是模拟多用户。同一小区里最多可以有 36 个 PUCCH 用户通过循环移位和 OCC 正交复用。改参数的地方就两个n_pucch和cell_id。但这里有个坑n_pucch不是随便取的它和上行资源分配、CQI 请求这些高层信令绑在一起。如果你只是做算法验证可以手动遍历 0 到 35 的n_pucch观察生成的符号序列之间的相关性。# 验证两个不同 n_pucch 生成的序列是否正交 seq1 generate_pucch_format1a(0, n_pucch0, cell_id1) seq2 generate_pucch_format1a(0, n_pucch1, cell_id1) corr np.abs(np.sum(seq1 * np.conj(seq2))) print(f互相关值: {corr:.4f})理想情况下不同n_pucch对应的序列互相关应该接近 0。如果算出来很大检查你的循环移位步进是不是 2π/12OCC 码字有没有取错。我见过有人把 OCC 的长度搞反了数据符号用了长度 3 的码参考信号用了长度 4 的码结果整个时隙的正交性全乱套。4. 避坑与排查PUCCH 符号生成里的五个血泪教训4.1 现象星座图旋转解调全错原因循环移位量 α 的单位搞错了。协议里 α 是弧度但有些参考代码里用的是「循环移位索引」直接拿索引当弧度用相当于把 α 放大了 2π/12 倍。解决在代码里加一行断言确认alpha的最大值不超过 2π。如果用的是索引记得乘上2*np.pi/12。4.2 现象参考信号和数据符号的功率不一致原因OCC 扩频之后没有做功率归一化。长度为 4 的 OCC 码字元素是 ±1扩频后功率不变但长度为 3 的 DFT 码字元素是复数模长是 1功率也不变。问题出在 ZC 序列本身如果生成长度 12 的序列时最后一个元素补零实际有效功率只有 11 个 RE但接收端按 12 个 RE 算功率就会差 0.4 dB 左右。解决要么把补零改成重复第一个元素要么在功率计算时扣除补零的 RE。4.3 现象多用户仿真时误码率居高不下原因cell_id变了但u没跟着跳。协议里 ZC 序列的根索引和组跳变是跟小区 ID 和时隙号绑定的如果仿真里固定u不变不同小区的干扰就会直接叠加上来。解决实现组跳变和序列跳变至少要把u和cell_id、时隙号关联起来。偷懒的做法是每个时隙重新算一次u。4.4 现象IFFT 之后波形带外泄漏严重原因频域符号直接做了 IFFT没有做子载波映射和滤波。PUCCH 只占用系统带宽边缘的几个 RB如果不做频谱整形带外泄漏会干扰相邻信道的接收。解决在 IFFT 之前把 12 个 RE 映射到正确的频域位置然后做 IFFT最后加窗或者滤波。如果只是做算法验证可以先用过采样加低通滤波看效果。4.5 现象Format 2 的 CSI 报告解出来全是噪声原因Format 2 的数据符号用的是 QPSK 调制后的 ZC 序列但参考信号和数据符号的循环移位可能不同。如果接收端假设两者相同信道估计就会把数据的相位也估进去。解决确认参考信号和数据符号的循环移位配置Format 2 里参考信号的循环移位是固定的数据符号的循环移位随符号索引变化。5. 进阶验证用相关峰和 EVM 判断符号生成对不对符号生成完怎么知道它是对的最直接的办法是算自相关和互相关。把生成的频域符号做 IFFT 变成时域然后和本地序列做滑动相关看峰值位置和幅度。如果峰值出现在预期的循环移位位置而且旁瓣比主瓣低 13 dB 以上基本可以认为 ZC 序列和循环移位是对的。另一个手段是 EVM。把生成的符号经过一个理想的加性高斯白噪声信道再解调回来算误差向量幅度。Format 1a 的 EVM 在 10 dB SNR 下应该能跑到 5% 以内。如果 EVM 很大先查 OCC 扩频的顺序再查调制符号的映射。def verify_evm(tx_symbols, snr_db): 简单的 EVM 验证 tx_symbols: 发送的频域符号矩阵 snr_db: 信噪比 snr_linear 10 ** (snr_db / 10) noise_power np.mean(np.abs(tx_symbols)**2) / snr_linear noise np.sqrt(noise_power/2) * (np.random.randn(*tx_symbols.shape) 1j * np.random.randn(*tx_symbols.shape)) rx_symbols tx_symbols noise error rx_symbols - tx_symbols evm np.sqrt(np.mean(np.abs(error)**2) / np.mean(np.abs(tx_symbols)**2)) return evm这个函数跑出来的 EVM 值可以作为一个快速检查。如果 EVM 比理论值高很多说明符号生成里引入了额外的幅度或相位失真。我一般会在生成符号之后、加噪声之前先算一遍符号的峰均比。PUCCH Format 1 的峰均比通常在 3-5 dB 左右如果超过 8 dB大概率是 OCC 或者循环移位的组合出了问题。最后说一个习惯每次改完参数不要只看星座图把生成的 7×12 矩阵打印出来肉眼扫一遍。参考信号那三行应该长得像同一个序列乘了不同的系数数据那四行应该只差一个符号。这个肉眼检查比任何自动化脚本都快而且能抓到很多脚本覆盖不到的边界情况。希望帮到你。本文还有配套的精品资源点击获取