ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

PUCCH MATLAB仿真源码包:从Zadoff-Chu序列到解调全流程解析

PUCCH MATLAB仿真源码包:从Zadoff-Chu序列到解调全流程解析

简介:面向LTE上行物理信道研究的MATLAB实现包,围绕PUCCH符号生成这一核心环节,提供从CAZAC序列生成、循环移位配置到Format 1/2/2a/2b调制解调的完整代码流程,适合通信专业学生与基站物理层开发人员对照3GPP协议进行仿真验证。压缩包共32个文件、18.13MB,包括17个m源程序、7个asv自动备份,以及多份doc/docx与pdf文档;其中m脚本可直接运行,asv文件利于版本回溯,协议原文与中文说明便于查阅编码和调制细节。已有339人浏览学习。通过这批程序,读者可以掌握PUCCH格式选择、信道编码、调制映射及解调判决的实现思路,结合36.211、36.212协议文档快速搭建上行控制信道仿真环境,为进一步研究LTE-A和5G上行物理层打下基础。

1. PUCCH物理上行控制信道:这套MATLAB仿真源码包到底能帮你解决什么

做LTE物理层上行仿真的人,多半在PUCCH符号产生这件事上栽过跟头:3GPP规范里的公式一环扣一环,从Zadoff-Chu基序列到循环移位再到扩频调制,中间任何一个细节算错,星座图就彻底散掉,而且很难定位是哪一步出的问题。这个资源包把PUCCH符号产生的全过程拆成了二十多个独立的.m脚本,覆盖Format 1/1a/1b、2/2a/2b的调制与解调,还附带36.211和36.212规范文档,正好解决"知道原理但写不出能跑的代码"这个痛点。适合正在搭LTE上行链路级仿真、需要验证PUCCH算法、或者被资源映射和循环移位公式绕晕的从业者。

2. PUCCH符号产生流程:Zadoff-Chu序列、循环移位与资源映射

2.1 Zadoff-Chu基序列:PUCCH符号的数学根基

PUCCH不能像PUSCH那样直接拿数据比特映射到子载波,它必须先构造一个恒包络零自相关的基序列,再在这个基序列上做调制和用户区分。LTE里这个基序列就来自CAZAC序列族中的Zadoff-Chu序列,PUCCH每个资源块对应12个子载波,所以基序列长度通常取12。

资源包里Zseq_generation.m干的就是这件事。核心逻辑是根据物理根序列索引u和实际根序列序号q,生成一段长度为N的ZC序列:

function base_seq = Zseq_generation(u, q, N) % 生成PUCCH基序列(ZC序列循环扩展) % 输入: % u: 物理根序列索引,取值0~29,由小区ID映射 % q: 实际根序列序号,由u和N根据36.211 5.2.2节计算 % N: 基序列长度,PUCCH一个RB取12 % 输出: % base_seq: 长度为N的复序列 zc = zeros(1, N); for m = 0:N-1 % ZC序列标准公式,注意q取整数,m从0开始 zc(m+1) = exp(-1j * pi * q * m * (m+1) / N); end % PUCCH场景下N=12,正好一个PRB的子载波数,无需循环扩展 % 如果N>12才需要做循环截断/扩展 base_seq = zc; end

这里最容易搞混的是u和q的关系。u是物理根序列索引,它由小区ID通过36.211里的查表逻辑确定,而q是实际生成ZC序列时用的序号,需要根据u和序列长度N做一次取模映射。很多早期仿真代码为了省事直接让q=u,在单小区单用户场景下碰巧能跑通,一旦切到多用户或者换小区ID就对不上了。我一般会在主程序里把u和q都打印出来核对一遍,确认和规范一致再继续。

2.2 循环移位:同一个基序列怎么区分多个用户

有了基序列r_u_v之后,PUCCH通过循环移位来区分同一时频资源上的不同用户。循环移位的本质是对基序列做圆周平移,移位量α=2π·n_cs/N,n_cs在0到11之间取整数值。这样12个用户可以用同一个基序列但互不干扰。

资源包里的PUCCHGenAlpha2.m和TxPUCCHCaln_cell_cs.m就是算这个n_cs的。n_cs不是随便填的,它由高层配置的cyclicShift和时隙号共同决定,同时还要叠加一个与小区ID相关的伪随机序列项。简化后的核心计算长这样:

function alpha = PUCCHGenAlpha2(n_cs, N) % 计算循环移位值alpha % 输入: % n_cs: 循环移位序号,范围0~11 % N: 基序列长度,通常12 % 输出: % alpha: 每个符号的循环移位相位 % 标准公式:alpha = 2*pi*n_cs/N % 注意n_cs在子帧内每个OFDM符号上可能不同 alpha = 2 * pi * n_cs / N; end

这段代码看起来简单,实际工程中的坑都在n_cs怎么来。完整流程是:先由高层信令拿到cyclicShift,再根据时隙号和符号号查找36.211 Table 5.5.2.2.1-1得到n_cs的初值,最后叠加伪随机序列c(i)生成的偏移量。TxPUCCHCaln_cell_cs.m就是把这个查表和伪随机过程封装了起来。如果直接跳过它、自己写死一个n_cs,单用户仿真可能看不出问题,但同一小区里多个用户的循环移位一旦撞了,解调端相关峰就会互相干扰,误码率下不去。

2.3 PUCCH时频资源映射:符号位置记错一切白搭

基序列和循环移位都准备好之后,关键是把这些符号放到正确的时频位置上。PUCCH在频域上占据频带边缘的两个RB,每个时隙内只在部分OFDM符号上发送数据,其余符号留给参考信号(DMRS)。这个位置关系是有严格规定的,不同格式还不一样。

正常循环前缀下,Format 1/1a/1b的数据符号是时隙内的第0、1、5、6个OFDM符号,DMRS占第2、3、4个;而Format 2/2a/2b的数据符号是第0、2、3、4、6个,DMRS在第1、5个。资源包里的PUCCHformat1.asv和PUCCHformat2.asv就是分别处理这两套映射的。我见过不止一个人把Format 1的DMRS位置套到Format 2上,结果解调端提取符号时全部踩空,星座图一片混乱。这种错误定位起来非常耗时,因为链路每个环节看起来都对,就是整体对不上。所以拿到这个包之后,建议先把映射关系对着36.211的表核一遍,再跑代码。文档包里的"36.211-890_TDD_0912(第2部分:物理信道和调制)"正是用来做这件事的。

3. PUCCH各格式调制链路:Format 1/1a/1b与2/2a/2b的编码差异

3.1 Format 1/1a/1b:调度请求与ACK/NACK的调制路径

Format 1用于传输调度请求(SR),它本身不携带调制比特,纯粹靠循环移位区分用户;Format 1a传1比特ACK/NACK,用BPSK调制;Format 1b传2比特ACK/NACK,用QPSK调制。调制符号生成后,乘基序列再做正交扩频。

正交扩频用的是Walsh码,正常CP下扩频因子是4,对应4个数据符号。资源包PUCCH_modulation.m里,这部分逻辑大致是:

% PUCCH Format 1a/1b调制主流程(简化示意) % w是正交扩频码,正常CP下长度4 w = [1 1 1 1; ... % 扩频因子4,正交码表 1 -1 1 -1; 1 1 -1 -1; 1 -1 -1 1]; % 1比特用BPSK,2比特用QPSK if num_bits == 1 d = (bits(1) == 1) * 2 - 1; % BPSK: 0->-1, 1->+1 else d = QPSK(bits); % QPSK符号,能量归一化 end % 每个数据符号上的发射信号 = 调制符号 * 扩频码 * 基序列 for m = 0:3 tx_symbol(m+1, :) = d * w(m+1, :) .* base_seq_with_cs; end

注意扩频码w是乘在调制符号上的,不是乘在基序列上的。这个顺序一旦反了,解扩频时相关峰会消失。另外Format 1a用BPSK时,d的取值是+1和-1,幅度千万别乘错。

3.2 Format 2/2a/2b:CQI与MIMO反馈的编码链路

Format 2承载CQI/PMI/RI这类信道质量信息,信息比特最多20比特,需要先做信道编码,再QPSK调制,最后映射到每时隙10个调制符号上。编码方式在36.212里定义为卷积编码,约束长度7、码率1/2,生成多项式为133和171(八进制)。

资源包CH_encode.m实现的就是这个编码过程:

function coded_bits = CH_encode(bits) % PUCCH Format 2信道编码:卷积编码(40, 20) % 输入: % bits: 最多20比特的CQI/PMI/RI信息 % 输出: % coded_bits: 40比特编码输出 % 36.212 5.2.2.1节定义的卷积编码器 % 约束长度7, 码率1/2, 生成多项式[133 171](八进制) trellis = poly2trellis(7, [133 171]); % 编码前末尾追加6个0比特收尾,保证编码器回到零状态 coded_bits = convenc(bits, trellis); end

为什么用卷积编码而不用Turbo码?因为PUCCH的CQI信息块很小,最多20比特,卷积编码的收尾开销低,译码延迟也小;Turbo码在这个比特长度下增益不明显,反而引入交织器和迭代译码的复杂度。Format 2a和2b的区别在于,它们除了CQI外还要携带1或2比特的ACK/NACK。这多出来的比特不增加CQI的编码长度,而是直接调制后在第二个DMRS符号上叠加——Format 2a用BPSK叠加1比特,2b用QPSK叠加2比特。资源包里的PUCCH_2a_modulation.m和PUCCH_2b_modulation.m就是专门处理这种叠加的,叠加完记得重新做能量归一化,否则符号功率不一致会影响解调。

3.3 解调侧:PUCCH_deModulation.m的逆过程

解调端做的事情可以理解为调制端的逆过程:接收信号先做相关解扩,把基序列的共轭乘回去以消除循环移位,再解扩频码得到调制符号,最后QPSK/BPSK解调并译码。资源包里的De_Zseq_generation.m、DeZ_seq_generation1b.asv就是去基序列的过程,PUCCH_2_deModulation.m和PUCCH_2a/2b_deModulation.m分别处理Format 2系列和2a/2b的叠加信息提取。

一个值得注意的细节是:解调端必须知道发送端用的循环移位量和扩频码序号,否则解扩解不出来。这些参数在真实系统里由基站通过下行控制信息指定,在仿真里则是直接把发送端的alpha和w传过去。我见过有人把解调端的alpha写死成0,而发送端用了循环移位6,结果相关峰能量被均摊到所有移位上,误码率直接崩掉。跑这个包的时候,建议先单用户、alpha=0跑通一遍,再加循环移位和用户数。

4. 源码包运行指南:脚本调用关系与关键参数设置

4.1 文件结构梳理:哪些是主程序、哪些是函数、哪些是备份

这个资源包里的文件名后缀混合了.m和.asv。.asv是MATLAB的自动保存文件,本质上是保存快照,不能直接作为函数调用,但把后缀改成.m后通常可以正常执行。所以第一步是把需要的.asv文件复制一份改名成.m再运行,不要直接对原始.asv动手,以免破坏备份。

按功能可以把文件分几组:PUCCH_modulation.m是调制端主流程,PUCCH_deModulation.m是解调端主流程;Zseq_generation.m和rsgenforPUCCH.m负责序列生成;PUCCHGenAlpha1.m、PUCCHGenAlpha2.m、TxPUCCHCaln_cell_cs.m负责循环移位和小区级参数计算;CH_encode.m做信道编码;QPSK.m、PUCCH_2a_modulation.m、PUCCH_2b_modulation.m做调制;PUCCH_2_deModulation.m、PUCCH_2a_deModulation.m、PUCCH_2b_deModulation.m做对应解调;GenRandomSeq.m生成随机比特序列当作测试输入。

4.2 一条典型调用路径:从小区参数到调制符号

实际跑通一条链路,推荐按下面的顺序执行。先从TxPUCCHCaln_cell_cs.m开始,它会根据小区ID和时隙配置算出基序列索引和循环移位需要的基础参数:

% Step1: 生成小区级配置参数 cell_cs = TxPUCCHCaln_cell_cs(cell_id, cyclicShift); % 返回结构体,包含n_cs基础值和伪随机序列需要的初始化种子 % Step2: 生成基序列 base_seq = Zseq_generation(cell_cs.u, cell_cs.q, 12); % Step3: 计算循环移位alpha(每个符号独立) alpha = PUCCHGenAlpha2(cell_cs.n_cs, 12); % Step4: 生成发送随机比特,走调制流程 tx_bits = GenRandomSeq(20); % Format 2场景,20比特CQI coded = CH_encode(tx_bits); mod_symbols = QPSK(coded); pucch_tx = PUCCH_modulation(mod_symbols, alpha, base_seq, format);

参数说明:cell_id是0到503之间的整数,决定根序列索引;cyclicShift由高层配置,单用户仿真先设0;12是基序列长度,对应一个PRB的12个子载波。format变量控制走Format 1系列还是2系列的分支。这一步是验证代码能不能跑通的最小闭环,全部走完应该得到一段长度为时隙内有效符号数的复信号序列。

4.3 关键参数怎么改:小区ID、循环移位、用户数

参数所在脚本典型值修改后影响
小区IDTxPUCCHCaln_cell_cs.m0~503决定根序列索引u和伪随机序列初始化,影响所有用户的基序列和移位
循环移位序号n_csPUCCHGenAlpha2.m0~11用户区分的关键,两个用户设为不同值才正交
基序列长度NZseq_generation.m12正常CP为12,扩展CP下Format 1系列为6
调制格式PUCCH_modulation.m1a/1b/2/2a/2b决定编码方式、调制阶数、符号映射位置
CQI信息比特数CH_encode.m最多20超过20需截断,PUCCH最多支持20比特质量信息

单用户场景验证通过后,试着把两个用户的n_cs分别设成0和6,叠加发送后解调端分别用两个alpha去解。如果星座图和单用户时基本一致,说明循环移位的正交复用逻辑通了。这里有个经验值:n_cs相差6是最大正交间隔,相差3以下在低信噪比下会出现互相泄漏,不建议工程上这样配置。

5. 仿真避坑:PUCCH符号产生中五个常见问题

5.1 .asv文件混进运行路径导致调用失败

  • 现象:运行PUCCHformat2时报错"未定义函数或变量",或者调用的代码内容明显是旧版本。
  • 原因:.asv是MATLAB自动保存的备份快照,不是正常的.m源文件。MATLAB不会把.asv当作函数文件加载,直接调用必然失败。如果曾经把.asv复制改成.m之后又编辑过原.m,改名的备份和当前文件内容不一致,还会造成隐性版本混乱。
  • 解决:先把所有.asv复制一份改成.m,放到单独的运行目录里,确认每个函数的接口和变量名和主程序对得上。跑通之后建议把.asv从工作路径移走,只保留.m文件。

5.2 基序列长度N取错导致相关峰多峰

  • 现象:解调端对接收序列做相关运算时,主峰下方出现多个副峰,幅度差不够大,误判概率升高。
  • 原因:正常循环前缀下PUCCH基序列长度是12,但扩展循环前缀下Format 1/1a/1b的基序列长度是6,扩频因子也变成2。如果只按12来写,扩展CP场景直接对不上。另外,ZC公式里N和q的取值之间有模运算关系,N变了q也要跟着重新计算。
  • 解决:先确认目标场景的CP配置,再看Zseq_generation.m里的N传的是12还是6。跑扩展CP验证时把N改成6,同时更新TxPUCCHCaln_cell_cs.m里的q映射逻辑。

5.3 循环移位n_cs只填了高层配置、漏了伪随机项

  • 现象:发送端和接收端用的是同一个cyclicShift,但解调出来的星座图整体旋转了一个固定角度,符号错判率偏高。
  • 原因:循环移位的n_cs由两部分组成——高层配置的cyclicShift和伪随机序列c(i)产生的动态偏移。伪随机序列的初始化依赖小区ID和时隙号,如果只把cyclicShift直接当n_cs用,动态偏移就丢了,收发两端相位对不上。
  • 解决:核对接收端和发送端是否共用同一套TxPUCCHCaln_cell_cs.m输出。重点检查伪随机序列c(i)的初始化函数有没有正确传参,小区ID、时隙号、符号号三项缺一不可。

5.4 QPSK调制符号功率没有归一化

  • 现象:星座图四个点落在半径约为√2的圆上,解调端软信息幅度整体偏大,误码率曲线和理论值对不上。
  • 原因:QPSK标准映射里符号是(1+j)/√2等四个点,很多代码偷懒写成(1+j)就发出去,功率变成2倍。Format 2a/2b在第二个DMRS上叠加ACK/NACK后,符号功率从1变成2,如果叠加后不重新归一化,后续解调端的参考幅度就全乱了。
  • 解决:QPSK.m里确认每个输出符号都除以了√2。Format 2a/2b叠加完成后,检查PUCCH_2a_modulation.m里有没有对叠加后的符号做整体归一化处理。这个错误在单用户高信噪比下不明显,一旦加噪声或者跑误码率曲线立刻显形。

5.5 参考信号和数据符号位置按格式搞混

  • 现象:解调出的数据符号星座图完全打散,但同样的代码切到Format 1就正常。
  • 原因:Format 1系列和Format 2系列的DMRS位置不同。Format 1的数据符号是第0、1、5、6个OFDM符号,DMRS是第2、3、4个;Format 2的数据符号是第0、2、3、4、6个,DMRS是第1、5个。写死了其中一个格式的索引,切换格式时没有同步更新映射表就会踩空。
  • 解决:对着36.211第5.4.1节的表核对PUCCHformat1.asv和PUCCHformat2.asv里的符号索引。建议把数据符号序号和DMRS符号序号分别定义成常量数组,切换格式时只替换数组,不要散落在各处硬编码。

6. 进阶验证:把仿真结果和36.211规范公式逐项对照

拿到源码包跑通链路只是第一步,真正确定这个包能用在工程验证里,需要把仿真输出和3GPP规范逐项对照。我习惯的做法是从发射端开始,每算出一个中间量就停下来和36.211里的公式核对一遍,而不是等到最后看星座图。

首先是基序列。用Zseq_generation.m生成的序列,挑前几个点手算一遍,对照公式x_q(m)=exp(-jπqm(m+1)/N)。这里q的值、N的值、m从0开始这三个要素缺一不可,任何一个对不上,后面全部白做。接着是循环移位α,把2π·n_cs/12手算出来,和PUCCHGenAlpha2.m的输出对比。注意n_cs要算上伪随机动态项,不是单纯的高层配置值。

然后是调制符号。Format 1a的BPSK输出只能是±1,Format 1b的QPSK输出幅度必须是1/√2的整数倍。这个用MATLAB的abs函数一眼就能看出来。最后是资源映射,把发射符号按符号序号画出来,对照36.211 Table 5.4.1-1确认数据符号和DMRS各在哪个位置。

验证完成后,有一个技巧值得养成习惯:把每个中间变量用save缓存下来,解调端单独加载调试。这样调制端改了参数,解调端不受影响,定位问题快得多。

% 调制端跑完后保存中间变量 save('pucch_tx_cache.mat', 'tx_signal', 'alpha', 'base_seq', 'mod_symbols'); % 解调端单独跑,加载缓存 load('pucch_tx_cache.mat'); % 解调时直接复用发送端的alpha和base_seq,不重新计算

这个习惯帮我避免过很多次"改了调制端、解调端跟着变、错误被掩盖"的尴尬情况。从那以后每次做PUCCH仿真,我都强制走一遍基序列、循环移位、调制符号、资源映射四层核对,配合缓存文件把收发两端解耦,整个过程清爽很多。希望帮到你。

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

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进