
简介面向无线通信学习者的一份扩频序列专题文档围绕m序列、Walsh码和Gold序列的原理展开内容包含最长线性移位寄存器序列的结构、反馈系数与起始状态的影响Walsh码的Hadamard矩阵生成思路和64阶前向链路码分配以及Gold序列由优选对模2加得到的构造方法。配套MATLAB函数代码演示了从Hadamard矩阵经Paley排序到Walsh排序的完整变换便于理解正交扩频码的编程实现与矩阵转换过程。压缩包内为单个Word文档体积355KB内容紧凑适合通信课程复习、CDMA系统原理自学或扩频码生成实验对照查阅。文档还特别分析了多径传播和小区间异步导致的非正交问题说明多址干扰为何不能完全消除有助于理解实际系统的局限。该份资料目前已有636人学习适合需要快速梳理扩频码概念并希望结合代码验证的初学者也可作为相关课程报告的参考素材。1. 扩频系统里的 m 序列和 Walsh 码选错一次就全盘翻车第一次搭 CDMA 物理层仿真时很多人卡在同一个地方手上明明有 m 序列又有 Walsh 码都是“扩频序列”到底该用哪个做用户区分我见过不少新手拍板选了 m 序列结果两路用户数据解出来互相打架误码率怎么调都压不下去也有反过来的拿 Walsh 码去干同步捕获相关峰糊成一片帧头永远找不准。问题的根源不是哪个序列“更好”而是这两类码在扩频系统里负责的是完全不同的活m 序列是伪随机码靠尖锐的自相关峰做定时同步、扰码和测距Walsh 码是正交码靠严格正交性把各路信号在码域上分开。这篇笔记就把它们的生成方式、参数配置、配合使用和最常见的翻车点一次讲透适合做通信物理层仿真、软件无线电和课设的从业者照着复现。2. 用本原多项式生成 m 序列抽头配置和自相关特性才是真正的门槛2.1 从 LFSR 到最长序列为什么周期偏偏是 2 的 n 次方减一m 序列的全称是“最长线性反馈移位寄存器序列”工程里最常见的生成载体就是 LFSR。一个 n 级移位寄存器每个时钟周期把若干级的异或结果反馈回输入端得到的输出序列就是线性递归序列。线性反馈的结构决定了它最多能遍历 2 的 n 次方减 1 个非零状态——因为全 0 状态会永远锁死在 0 上所以必须把它排除掉。能刚好走满这 2 的 n 次方减 1 个状态的反馈配置对应的特征多项式就是本原多项式。这里有一个新手最容易搞错的点不是随便接几根反馈线就能得到 m 序列。抽头取自本原多项式才算数取错了序列周期会缩短甚至产生多条短周期子序列拼起来的退化序列。比如 4 阶 LFSR本原多项式有 x^4x1 和 x^4x^31 两个等价形式周期都是 15但如果你拿 x^4x^21 这种非本原多项式去搭周期就只有 6频谱特性完全不对。生成 m 序列时的工程约定有两个一是初始化寄存器不能是全 0否则异或反馈永远输出 0二是通常把“1”映射为负电平、“0”映射为正电平来做双极性扩频这样序列的直流分量最小。至于抽头选哪几级常见的做法是查本原多项式表而不是自己试探后面给出可以直接用的配置。2.2 生成 m 序列的最小 Python 实现抽头、初始状态和周期验证下面这段代码用整数位移的方式模拟 LFSR比按位列表操作更快也更好理解反馈过程def mseq(n, taps, init1, lengthNone): 生成 m 序列双极性1 - -1, 0 - 1 :param n: LFSR 阶数移位寄存器级数 :param taps: 反馈抽头列表如 [4, 3] 表示第 4 级和第 3 级参与异或 :param init: 初始状态不能为 0默认取 1 :param length: 输出序列长度默认 2^n - 1刚好一个完整周期 :return: numpy 数组取值 1 和 -1 import numpy as np if init 0: raise ValueError(初始状态不能为 0否则 LFSR 锁死在全零) if length is None: length (1 n) - 1 if length (1 n) - 1: raise ValueError(长度不能超过 m 序列最大周期) state init ((1 n) - 1) # 只保留 n 位有效状态 mask (1 n) - 1 taps_mask 0 for t in taps: taps_mask | (1 (t - 1)) seq np.zeros(length, dtypeint) for i in range(length): seq[i] state 1 # 取最低位作为输出 fb bin(state taps_mask).count(1) 1 # 抽头异或结果 state ((state 1) | (fb (n - 1))) mask return np.where(seq 1, -1.0, 1.0) # 1 - -1, 0 - 1逻辑说明函数在每一个时钟周期先输出当前寄存器的最低位再计算抽头位置的异或结果把反馈值移入最高位。bin(state taps_mask).count(1) 1这行是求抽头位的奇偶校验等价于多个抽头的异或。参数说明n通常取 5 到 15阶数越高周期越长但捕获时滑动搜索的计算量也越大taps必须配本原多项式的抽头下面是几张常用表。这里给出一个常用抽头配置表直接照着用不会出问题阶数 n周期 2^n - 1本原多项式一种形式抽头 taps说明37x^3x1[3, 1]适合做短帧同步序列415x^4x1[4, 1]仿真验证最常用周期短、算得快531x^5x^21[5, 2]兼顾周期与计算量7127x^7x1[7, 1]扩频增益已比较可观101023x^10x^31[10, 3]常用于卫星测距、帧同步码2.3 自相关和互相关m 序列能干什么、不能干什么全看这两个指标m 序列最值钱的特性是周期自相关函数主峰等于序列长度 N旁瓣恒定为 -1。归一化之后主峰是 1旁瓣只有负的 N 分之一。这意味着接收端拿本地 m 序列和接收信号做滑动相关只要对齐了就会出现一个非常尖锐的峰错开哪怕 1 个码片就会掉到旁瓣电平。这个特性让它成为同步捕获和测距的首选。互相关就没这么好看了。不同抽头配置生成的两条 m 序列互相关峰值大约在 2 的 n/2 次方分之一到 1 之间波动并不是零。换句话说m 序列不能用来做多用户正交区分两个用户如果各拿一条 m 序列同时发数据解扩之后会残留约一半功率的互干扰。有人拿 m 序列做 CDMA 用户码然后发现用户数一多信噪比急剧恶化就是这个原因——它本来就不是干这个的。验证这两个特性的代码很简单直接复用上面的mseq函数对两条序列分别做循环相关再归一化即可。实测 4 阶 m 序列周期间自相关旁瓣就是 -1互相关则可能出现 3、5 这类非零峰值和理论值对得上。3. 从 Hadamard 矩阵造出 Walsh 码说要正交代价是每个码片都要对齐3.1 为什么 Walsh 码“正交”内积为零才是硬指标Walsh 码是一组取值 1 和 -1 的正交序列工程上通过 Hadamard 矩阵的行或列来取。Hadamard 矩阵的递归构造规则是2N 阶矩阵由四个 N 阶子块拼成左上、右上、左下都是原矩阵右下是原矩阵取反。这样构造出来的任意两行做逐位相乘再求和结果恒为零用数学语言说就是行与行之间两两正交。正交性有什么工程意义假设两路用户数据分别乘上两条不同的 Walsh 码叠加在一起发送接收端只要拿其中一条 Walsh 码做内积另一路信号的贡献会被积分成零等效于那路用户“不存在”。这就是码分多址最基本的原理。但注意这个结论有一个隐含前提所有用户的码片在接收端必须精确对齐错半个码片正交性就开始劣化错一个码片基本等于伪正交。所以 Walsh 码对同步的要求比 m 序列严苛得多这也是后面避坑章节重点展开的内容。3.2 递归生成 Walsh 码的最小实现行号、码长怎么选生成 Walsh 码最直接的方式就是递归构造 Hadamard 矩阵然后按行取序列。下面这段代码在 Python 里可以一次生成任意 2 的幂次长度的 Walsh 码集def hadamard_matrix(order): 递归构造 Hadamard 矩阵 :param order: 矩阵阶数必须为 2 的幂次如 4/8/16/64 :return: numpy 2D 数组每行是一条 Walsh 码 import numpy as np if order 1: return np.array([[1.0]]) half hadamard_matrix(order // 2) top np.hstack([half, half]) bottom np.hstack([half, -half]) return np.vstack([top, bottom])逻辑说明代码递归地把小阶 Hadamard 矩阵拼成大阶矩阵右下角取负是保证正交性的关键少了这一步拼出来的矩阵行与行之间就不正交了。参数说明order取 16 时得到 16 条长度为 16 的 Walsh 码能支持 16 个正交码道IS-95 前向链路用的是 64 阶 Walsh 码对应 64 个码道其中一部分留给导频和控制信道。仿真阶段一般从 16 开始跑通了再放大到 64因为 64 阶时滑动相关和矩阵运算的规模会翻十几倍。选取 Walsh 码时还有一个约定俗成的坑直接从 Hadamard 矩阵取行得到的是“Hadamard 序”即按递归生成顺序排列而很多教材和协议文档里用的是“Walsh 序”也叫 sequency 序按行内符号翻转次数从少到多排列。两种排序本身都正交但工程对接时如果发送端按 Hadamard 序、接收端按 sequency 序配置用户号解扩出来就是另一路用户的数据。对接前先确认排序约定这是血泪经验。3.3 别忘了双极性映射0/1 表示下“正交”会失效一半Walsh 码的教科书定义往往写成 0/1 序列但做扩频调制时必须映射成 1/-1 双极性信号。原因很直接用 0 和 1 做相关运算时两条不同 Walsh 码的内积不是零而是出现在 {0, 1} 之间的某个值并且同一路信号的自相关也不等于码长而是等于码长的一半因为直流分量混在里面。双极性映射之后直流分量消失任意两行内积严格为零自相关等于码长 N判决阈值也是干净的零。这个坑在仿真里经常表现为数据解出来了但幅度总是偏小一半或者叠加两路用户后解扩结果带有固定偏置。检查步骤很简单——看发端映射函数里是不是用了2*x-1或np.where(x0, -1, 1)做了双极性转换。4. 集中避坑同步误差、正交性假象和误用错配的四个常见翻车点4.1 解扩后残留大量干扰信号错了一个码片正交性就垮了现象两路用户都按正交 Walsh 码发送接收端解扩某一路后另一路用户的语音/数据串扰清晰可闻信噪比只比没扩频时好一点点。原因Walsh 码的正交性建立在所有码片在接收端精确时间对齐的基础上。收发两端时钟不同步、或者多径信道引入时延都会让码片边界错位。错位超过 0.5 个码片后两行 Walsh 码的相关值从零变成接近码长一半的伪峰正交性名存实亡。解决在 Walsh 解扩之前必须先做码片级同步常见做法是用 m 序列做帧头/导频捕获到相关峰后以此为时间基准对齐码片再做 Walsh 内积。同步精度要控制在正负 0.2 个码片以内否则正交性余量会被消耗殆尽。4.2 用 m 序列区分多用户用户数一多就集体翻车现象给每个用户分配一条不同抽头的 m 序列仿真空旷信道下单独跑一路没问题加到三路以上误码率急剧恶化和信噪比无关。原因m 序列是伪随机码不是正交码。不同 m 序列之间互相关峰值最大可以达到接近 1 的水平远谈不上正交。多用户叠加后每路用户收到的都是其他所有用户的互相关干扰之和信噪比增长完全跟不上用户数增长。解决区分用户用 Walsh 码或 OVSF 码这类正交码m 序列退回去做帧同步和扰码。这也是实际 CDMA 系统的通用架构Walsh 码做信道化m 序列做小区扰码和同步。4.3 拿 Walsh 码做同步捕获相关峰永远找不准现象把本地 Walsh 码和接收信号做滑动相关主峰不够尖锐旁边连着好几个大小差不多的旁瓣定时估计抖动很大帧头位置判断错误频繁出现。原因Walsh 码是正交码设计目标是行间互相关为零不是自相关尖锐。它的周期自相关旁瓣不是 -1 而是可能达到相当高的值码长越长、旁瓣越多做滑动相关时根本没有“唯一峰”的概念。解决定时同步交给 m 序列这类 PN 码Walsh 码只做解扩和用户区分。若链路预算实在紧张也可以用 m 序列做粗同步后再用 Walsh 做码片级精同步微调但主同步手段不能换。4.4 扩频信号频谱出现线谱离散峰PN 码看起来不够“随机”现象把 m 序列扩频后的信号送到频谱仪看到在载频附近冒出一根根离散的线谱而不是平滑的扩频包络能量集中度比理论预期高。原因m 序列是周期的周期短到一定程度频谱自然出现间隔为码速率除以周期长度的离散谱线。尤其是 4 阶、5 阶这类短周期 m 序列周期只有 15 或 31谱线间隔非常宽既不利于隐蔽性也容易形成干扰尖峰。解决长周期化处理。常见做法是拿多个不同相位的 m 序列加长合成、或者用 m 序列和 Walsh 码相乘混合成复合扩频序列也可以把短 m 序列作为扰码再叠加在 Walsh 扩频输出上破坏周期性。工程上如果协议允许直接把 m 序列阶数提到 10 以上频谱离散度肉眼可见改善。5. 搭建一条完整的扩频链路m 序列定帧头、Walsh 做码分用户区分5.1 最小仿真链路参数与整体流程把两条正交 Walsh 码分配给两个用户数据用 BPSK 调制每符号用一个 16 位的 Walsh 码做扩频帧头用一条 15 位的 4 阶 m 序列插在每帧数据前面。接收端先拿本地 m 序列做滑动相关找帧起点再按对齐后的码片边界做 Walsh 解扩恢复两个用户的数据。完整参数如下。参数取值说明扩频码Walsh 码长度 16支持 16 个正交码道仿真取 2 路用户同步码m 序列4 阶周期 15帧头捕获相关峰旁瓣为 -1数据调制BPSK双极性 1/-1与扩频码直接相乘每帧结构m 序列帧头 8 个数据符号帧头长度短于 Walsh 码长方便观测捕获峰信道AWGN无多径先排除信道因素单独验证码分正交性用户数据每用户 8 bit独立随机两路数据完全独立5.2 发送端帧头插入与 Walsh 扩频叠加发送端的核心操作是每个用户的数据符号与一条固定分配的 Walsh 码逐码片相乘两路用户的结果直接相加得到一帧基带信号再在帧头位置填入 m 序列。下面是完整的发送端实现。import numpy as np def tx_frame(m_seq, walsh_mat, user1_bits, user2_bits): 发送端m 序列作帧头 两路用户 Walsh 扩频叠加 :param m_seq: 4 阶 m 序列长度 15双极性 :param walsh_mat: 16x16 Walsh 矩阵按行取码 :param user1_bits: 0/1 数组长度 8 :param user2_bits: 0/1 数组长度 8 :return: 基带发送序列 w1 walsh_mat[0] # 用户 1 分配第 0 行 w2 walsh_mat[1] # 用户 2 分配第 1 行 # BPSK 调制并扩频每个符号乘以整条 Walsh 码 user1_sym np.where(user1_bits 1, 1.0, -1.0) user2_sym np.where(user2_bits 1, 1.0, -1.0) spread1 np.kron(user1_sym, w1) # 符号序列与 Walsh 码做 Kronecker 积 spread2 np.kron(user2_sym, w2) data_part spread1 spread2 # 两路用户码域叠加 return np.concatenate([m_seq, data_part])逻辑说明np.kron(user_sym, w1)把每个数据符号扩展成 16 个码片等效于符号与 Walsh 码逐位相乘两路用户的扩频结果直接相加就是码分叠加后的信号。帧头 m 序列拼在数据前面共用一个发射功率。参数说明walsh_mat[0]和walsh_mat[1]必须取自同一个 Hadamard 矩阵构造结果且行序要按双方约定好的排序这里的两个 Walsh 码在 16 阶矩阵下互相关为零。5.3 接收端滑动相关捕获帧头再做 Walsh 解扩判决接收端拿到一帧完整信号后第一步是先找到帧头起点。具体做法是拿本地 m 序列与接收信号从第一个码片开始逐点滑动做相关找到相关峰的位置这个位置就是数据部分的起始边界。注意滑动步长是 1 个码片所以搜索范围越长计算量越大。def rx_frame(rx, m_seq, walsh_mat, frame_len): 接收端m 序列滑动相关捕获帧头 Walsh 解扩 :param rx: 接收序列已加噪声 :param m_seq: 本地 m 序列长度 15 :param walsh_mat: 16x16 Walsh 矩阵 :param frame_len: 发送帧总长度 :return: 恢复的两路用户符号 (0/1 数组) m_len len(m_seq) # 1. 滑动相关捕获帧头 corr np.array([ np.dot(rx[i:i m_len], m_seq) for i in range(len(rx) - m_len 1) ]) head_pos int(np.argmax(np.abs(corr))) # 相关峰位置 帧头起点 # 2. 从帧头后开始截取数据区 data_start head_pos m_len data_part rx[data_start:data_start frame_len - m_len] # 3. 按符号逐个解扩与两条 Walsh 码分别内积符号判决 w1, w2 walsh_mat[0], walsh_mat[1] n_sym len(data_part) // 16 sym1, sym2 [], [] for s in range(n_sym): chip data_part[s * 16:(s 1) * 16] r1 np.dot(chip, w1) # 用户 1 的相关值 r2 np.dot(chip, w2) sym1.append(1 if r1 0 else 0) sym2.append(1 if r2 0 else 0) return np.array(sym1), np.array(sym2)逻辑说明滑动相关用np.dot(rx[i:im_len], m_seq)计算每个候选位置的相关系数捕获后取最大值位置作为帧起点。Walsh 解扩则是对每个符号片段的 16 个码片分别与两条本地 Walsh 码做内积内积结果大于零判为 1、小于零判为 0。参数说明滑动相关窗口长度等于 m 序列长度这里帧头是 15 位所以窗口步长 1 遍历整个接收帧Walsh 码长 16所以数据区长度必须是 16 的整数倍否则最后一个符号会截断。把帧头捕获和 Walsh 解扩分开写是为了排查问题时能独立验证同步模块和解扩模块的好坏。6. 验证链路做没做对测三个指标再用同步容差当体检链路搭完后不要急着看误码率先做三个验证任何一个不过关都说明前面的实现有偏差。第一个验证是 m 序列自相关峰。把本地 m 序列和自身做循环相关主峰应为 15旁瓣全部为 -1如果旁瓣有超过 -1 的值说明生成的不是本原序列。第二个验证是 Walsh 正交性把 Walsh 矩阵任意两行做内积全部为零并把第 0 行和第 1 行分别与自身做内积确认自相关为 16这步能直接暴露 0/1 映射没转双极性的问题。第三个验证是无噪声环境下解扩结果发端 8 bit 随机数据进tx_frame接收端rx_frame恢复后逐一对比误码率必须为 0此时再逐步把信噪比降到 10 dB、5 dB观察误码率曲线是否符合 BPSK 理论趋势。我个人的习惯是再补一个同步容差测试把接收序列人为向右偏移 1 到 8 个码片观察 Walsh 解扩误码率从哪个偏移量开始明显恶化。实测下来偏移 1 个码片时正交性已损失明显偏移超过 4 个码片基本等于没有扩频。这个测试能直接告诉你在真实系统里码片级同步的精度红线在哪里值得每次改完链路都跑一遍。等这套流程跑顺了再把 Walsh 码长升到 64、给每路用户叠加独立的 m 序列扰码就是一套可以往实际工程迁移的 CDMA 前端原型了。希望这些踩过的弯能帮你少走一段。本文还有配套的精品资源点击获取