简介:这份文档面向无线通信、CDMA系统与扩频技术的学习者,系统梳理m序列与Walsh码两类核心扩频序列的原理与实现。内容涵盖m序列的线性移位寄存器生成机制、周期特性与反馈系数选择,Walsh码基于Hadamard矩阵的正交构造及64阶序列在前向链路中的信道分配,并延伸至Gold序列的模2加生成思路,同时给出Walsh序列生成的Matlab函数代码,便于读者理解矩阵排列与转换过程。资源为单个doc文档,压缩包约355KB,结构紧凑,适合作为课堂笔记或自学参考。目前已有636人学习下载,可帮助读者掌握扩频码的生成逻辑、正交性与多址干扰之间的关系,为CDMA系统仿真与多用户检测研究打下基础。
1. 扩频序列 m 序列与 Walsh 码:从生成到误码率验证的完整链路
做直接序列扩频(DSSS)仿真时,很多人第一次跑出来的误码率曲线是平的——加性高斯白噪声功率怎么调,误码率都不动。翻车的原因往往不在信道模型,而在扩频序列本身:m 序列的优选对没选对,或者 Walsh 码的序号映射和扩频因子对不上。扩频序列 m 序列和 Walsh 码是两套来源完全不同的码:m 序列由线性反馈移位寄存器生成,靠本原多项式决定周期和自相关特性;Walsh 码来自 Hadamard 矩阵的行(或列),靠序号索引保证正交性。前者管抗干扰和同步,后者管多用户之间的正交隔离。这篇文章面向做通信链路仿真、需要把扩频环节跑通并验证误码率的工程师,从生成代码写到参数选择,再到误码率曲线对不上的排查路径,每一步都给出可复现的命令和参数含义。
2. m 序列生成:本原多项式、抽头与周期验证
2.1 为什么 m 序列的抽头位置不能随便选
m 序列的本质是 n 级线性反馈移位寄存器的最大长度输出。寄存器有 n 个,状态总数是 2^n,去掉全零态,最大周期就是 2^n - 1。要让输出达到这个周期,反馈抽头必须对应一个 n 次本原多项式。抽头选错,周期会缩短,自相关旁瓣会抬高,扩频增益直接打折扣。
常见做法是查本原多项式表。n=3 时本原多项式是 x^3 + x + 1,抽头在寄存器第 3 和第 1 级;n=4 时是 x^4 + x + 1,抽头在第 4 和第 1 级;n=5 时是 x^5 + x^2 + 1,抽头在第 5 和第 2 级。注意这里的抽头编号从 1 开始,对应寄存器从输入侧数的位置。不同教材的编号方向可能相反,写代码前先确认自己的移位方向。
我一般会先用一个短周期验证生成逻辑,再换长周期。下面这段 Python 用 n=4、抽头 [4,1] 生成周期 15 的 m 序列,并打印一个周期内的输出。
# m序列生成:n级LFSR,taps为本原多项式对应的抽头位置(从1开始编号) def m_sequence(n, taps, seed=1): state = seed & ((1 << n) - 1) # 初始状态,不能为0 if state == 0: raise ValueError("初始状态不能为全零") period = (1 << n) - 1 seq = [] for _ in range(period): # 输出当前最高位(对应寄存器第n级) out = (state >> (n - 1)) & 1 seq.append(out) # 计算反馈位:所有抽头位置异或 fb = 0 for t in taps: fb ^= (state >> (t - 1)) & 1 # 左移并填入反馈位 state = ((state << 1) | fb) & ((1 << n) - 1) return seq seq = m_sequence(4, [4, 1]) print("周期:", len(seq)) print("序列:", seq) print("自相关(零延迟):", sum(1 if b == 0 else -1 for b in seq) ** 2)这段代码里taps是抽头位置列表,seed是初始状态。输出位取的是最高位,反馈位是所有抽头位的异或。周期由(1 << n) - 1决定,n=4 时是 15。自相关零延迟值应该是周期平方,即 225。如果打印出来不是 15 或自相关不对,先检查抽头是否对应本原多项式,再检查初始状态是否落在全零。
2.2 平衡性、游程与自相关:三个必须打印的验证指标
生成完序列不能直接用,先验证三个性质。平衡性要求一个周期内 1 的个数比 0 多一个,n=4 时 1 有 8 个、0 有 7 个。游程特性要求长度为 k 的游程中,0 和 1 各占一半(k < n-1),最长游程是 n 个 1 和 n-1 个 0。自相关要求零延迟为周期,其他延迟为 -1。
下面这段代码把三个指标一次算完。
def check_m_properties(seq, n): period = len(seq) # 平衡性 ones = sum(seq) print(f"1的个数: {ones}, 0的个数: {period - ones}, 差值: {ones - (period - ones)}") # 游程统计 runs = [] cur = seq[0] cnt = 1 for b in seq[1:]: if b == cur: cnt += 1 else: runs.append((cur, cnt)) cur = b cnt = 1 runs.append((cur, cnt)) print("游程:", runs) # 自相关(双极性) bipolar = [1 if b == 1 else -1 for b in seq] for delay in range(period): corr = sum(bipolar[i] * bipolar[(i + delay) % period] for i in range(period)) if delay < 5 or delay == period - 1: print(f"延迟 {delay}: 自相关 = {corr}") check_m_properties(seq, 4)平衡性差值应该是 1。游程里长度为 1 的游程应该有 2^(n-2) 个 0 和 2^(n-2) 个 1,n=4 时各 4 个。自相关在延迟 0 处是 15,其他延迟是 -1。如果自相关旁瓣不是 -1,说明抽头不是本原多项式,或者序列生成有误。
2.3 用 m 序列做扩频:每个符号重复多少次
扩频因子决定了每个信息符号被扩展成多少个码片。假设信息符号速率是 1 kbps,扩频因子是 15,码片速率就是 15 kbps。实现时把每个信息比特和 m 序列的一个周期逐位相乘(异或),得到扩频后的码片流。
def spread_with_m(info_bits, m_seq): # 信息比特0映射为+1,1映射为-1;m序列同样映射 m_bipolar = [1 if b == 0 else -1 for b in m_seq] chips = [] for bit in info_bits: s = 1 if bit == 0 else -1 for m in m_bipolar: chips.append(s * m) return chips info = [1, 0, 1, 1, 0] chips = spread_with_m(info, seq) print("扩频后码片数:", len(chips)) print("前30个码片:", chips[:30])这里信息比特 0 映射为 +1、1 映射为 -1,m 序列同样做双极性映射。每个信息比特对应一个完整 m 序列周期,所以码片数是信息比特数乘以周期。解扩时用同一个 m 序列做相关,相关峰出现在符号边界上。
3. Walsh 码:Hadamard 矩阵生成与正交性验证
3.1 从 Hadamard 矩阵取行还是取列
Walsh 码的标准生成方式是递归构造 Hadamard 矩阵。H1 = [1],H2 = [[1,1],[1,-1]],H4 = [[H2,H2],[H2,-H2]],以此类推。矩阵的每一行(或每一列)就是一个 Walsh 码,行与行之间正交,列与列之间也正交。取行还是取列,取决于你的索引约定:如果用户 k 分配第 k 行,那所有用户用同一列方向做扩频;如果分配第 k 列,就统一用行方向。混用会导致正交性失效。
常见做法是统一取行,索引从 0 开始。下面代码生成 8 阶 Walsh 码并验证正交性。
import numpy as np def hadamard(n): # n必须是2的幂 if n == 1: return np.array([[1]]) h = hadamard(n // 2) return np.block([[h, h], [h, -h]]) N = 8 H = hadamard(N) print("Hadamard矩阵形状:", H.shape) # 验证行正交性 for i in range(N): for j in range(i + 1, N): dot = np.dot(H[i], H[j]) if dot != 0: print(f"行{i}和行{j}不正交: {dot}") print("行正交性验证完成") # 验证列正交性 for i in range(N): for j in range(i + 1, N): dot = np.dot(H[:, i], H[:, j]) if dot != 0: print(f"列{i}和列{j}不正交: {dot}") print("列正交性验证完成")hadamard函数用递归分块构造,np.block把四个子块拼成新矩阵。行正交和列正交都应该全部为 0。如果打印出不正交,检查 n 是否是 2 的幂,以及递归边界是否正确。
3.2 Walsh 码的序号映射:为什么用户 1 和用户 2 会撞车
Walsh 码的序号映射有两种常见约定:自然序和哈达玛序。自然序按矩阵行号直接分配,哈达玛序按二进制位反转后的顺序分配。两种序下,同一个用户拿到的码字不同。如果发送端用自然序、接收端用哈达玛序,解扩时相关峰位置会错位,误码率直接飙到 0.5。
我一般会在系统初始化时把码表打印出来,确认收发两端用的是同一张表。下面代码展示两种序的差异。
def bit_reverse(x, bits): result = 0 for _ in range(bits): result = (result << 1) | (x & 1) x >>= 1 return result N = 8 bits = 3 H = hadamard(N) print("自然序码表:") for i in range(N): print(f"用户{i}: {H[i]}") print("哈达玛序码表:") for i in range(N): idx = bit_reverse(i, bits) print(f"用户{i} -> 行{idx}: {H[idx]}")bit_reverse把索引的二进制位反转,bits是 log2(N)。自然序下用户 i 拿第 i 行,哈达玛序下用户 i 拿第 bit_reverse(i) 行。两种序的码字集合相同,但分配关系不同。多用户仿真时,如果每个用户用不同的 Walsh 码扩频,接收端用对应的码解扩,正交性保证其他用户的信号相关后为零。
3.3 用 Walsh 码做多用户扩频:叠加与解扩的最小验证
多用户场景下,每个用户的信息比特先用各自的 Walsh 码扩频,然后所有用户的码片流叠加在一起发送。接收端用目标用户的 Walsh 码做相关,其他用户的贡献因为正交性被抵消。
def spread_walsh(bits, code): # bits: 信息比特列表,code: Walsh码(+1/-1) chips = [] for b in bits: s = 1 if b == 0 else -1 chips.extend(s * code) return np.array(chips) N = 8 H = hadamard(N) users = 3 info = [[1, 0, 1], [0, 1, 1], [1, 1, 0]] tx = np.zeros(3 * N) for u in range(users): tx += spread_walsh(info[u], H[u]) print("叠加后码片流长度:", len(tx)) # 解扩用户0 rx_user0 = [] for k in range(3): segment = tx[k * N:(k + 1) * N] corr = np.dot(segment, H[0]) / N rx_user0.append(0 if corr > 0 else 1) print("用户0解扩结果:", rx_user0)spread_walsh把每个信息比特扩展成 N 个码片,tx是三个用户叠加后的码片流。解扩时按符号边界取 N 个码片,和目标用户的 Walsh 码做点积再除以 N。如果正交性成立,其他用户的贡献在点积后为零,corr只反映目标用户的符号。打印出的rx_user0应该和info[0]一致。
4. 扩频链路避坑:误码率曲线对不上的五个排查点
4.1 现象:误码率曲线在高信噪比下不下降
原因通常是扩频序列的周期和信息符号边界没有对齐。m 序列扩频时,如果每个信息符号对应的码片数不是完整周期,解扩时的相关峰会被截断,信噪比增益达不到理论值。解决方法是确保扩频因子等于 m 序列周期,或者在每个符号边界重置序列相位。
4.2 现象:多用户场景下误码率随用户数增加而恶化
Walsh 码的正交性只在码片级同步时成立。如果各用户的到达时间有偏移,正交性被破坏,用户间干扰直接进入解扩输出。解决方法是加入保护间隔,或者在接收端做码片级同步后再解扩。仿真时可以先假设理想同步,验证正交性无误后再加入时偏。
4.3 现象:m 序列自相关旁瓣不是 -1
抽头位置对应的不是本原多项式。n=4 时本原多项式是 x^4 + x + 1,抽头是 [4,1];如果误写成 [4,3],周期会变成 7 而不是 15。解决方法是查本原多项式表,或者用穷举法验证:对每个抽头组合生成长度 2^n - 1 的序列,检查自相关旁瓣是否全为 -1。
4.4 现象:Walsh 码解扩后符号判决全错
收发两端的 Walsh 码序号映射不一致。发送端用自然序,接收端用哈达玛序,解扩时用的码字和目标用户的码字不同,相关峰出现在错误的位置。解决方法是在系统初始化时把码表打印出来,确认收发两端用的是同一张表。
4.5 现象:扩频后信号功率异常增大
信息比特映射和扩频码映射的极性不一致。如果信息比特 0 映射为 +1、1 映射为 -1,而扩频码 0 映射为 -1、1 映射为 +1,乘积的符号会反转,解扩时相关峰极性相反。解决方法是统一映射规则:信息比特和扩频码都用 0 -> +1、1 -> -1,或者都用 0 -> -1、1 -> +1。
5. 从误码率曲线反推扩频增益:一个可复现的验证脚本
扩频增益的理论值是 10 * log10(扩频因子)。m 序列周期 15 对应约 11.8 dB,Walsh 码长度 8 对应约 9 dB。验证方法是:在相同信噪比下,比较扩频前后误码率曲线的水平偏移。如果偏移量接近理论值,说明扩频环节正确;如果偏移量偏小,说明有额外损耗。
下面这段脚本用 BPSK 调制、m 序列扩频、AWGN 信道,跑一条误码率曲线并和理论值对比。
import numpy as np def awgn(signal, snr_db): snr_linear = 10 ** (snr_db / 10) noise_power = 1 / snr_linear noise = np.sqrt(noise_power / 2) * np.random.randn(len(signal)) return signal + noise def ber_sim(snr_db, n_bits=10000, spread_factor=15): # 生成m序列 m_seq = m_sequence(4, [4, 1]) m_bipolar = np.array([1 if b == 0 else -1 for b in m_seq]) # 生成信息比特 bits = np.random.randint(0, 2, n_bits) symbols = np.array([1 if b == 0 else -1 for b in bits]) # 扩频 tx = np.array([s * m_bipolar for s in symbols]).flatten() # 加噪声 rx = awgn(tx, snr_db) # 解扩 rx_symbols = [] for k in range(n_bits): segment = rx[k * spread_factor:(k + 1) * spread_factor] corr = np.dot(segment, m_bipolar) / spread_factor rx_symbols.append(0 if corr > 0 else 1) # 统计误码 errors = np.sum(np.array(rx_symbols) != bits) return errors / n_bits snr_range = [0, 2, 4, 6, 8, 10] for snr in snr_range: ber = ber_sim(snr) print(f"SNR = {snr} dB, BER = {ber:.4f}")awgn函数按信噪比生成高斯噪声,ber_sim完成扩频、加噪、解扩、判决的完整链路。spread_factor必须等于 m 序列周期,否则解扩时相关峰会被截断。跑出来的误码率曲线在 6 dB 左右应该降到 1e-3 量级,和理论 BPSK 曲线相比有约 11.8 dB 的偏移。如果偏移量明显偏小,检查扩频因子是否匹配、解扩时是否做了归一化。
我自己的习惯是先把扩频因子设为 1 跑一遍,确认基础 BPSK 链路无误,再逐步增大扩频因子,观察误码率曲线的水平偏移是否按 10 * log10(扩频因子) 增加。这样能把扩频环节的问题和调制、信道环节的问题分开定位。希望帮到你。
本文还有配套的精品资源,点击获取