diff --git a/REPORT.md b/REPORT.md new file mode 100644 index 0000000..c782957 --- /dev/null +++ b/REPORT.md @@ -0,0 +1,92 @@ +# 无线通信技术实验报告:信道编码与信道均衡 + +## 1. 实验目的 + +1. 掌握信道编码技术:通过编写代码实现 Hamming(7,4) 编码与译码过程,深刻理解信道编码通过引入冗余信息来提升通信系统可靠性的核心思想。 +2. 掌握信道均衡技术:通过实现迫零(ZF)和最小均方(LMS)均衡器,理解多径信道带来的符号间干扰(ISI)问题及其消除方法。 +3. 熟悉现代开发流程:熟练掌握基于 Git/GitHub 的代码版本控制、Pull Request(PR)提交流程,并学会在 GitHub Actions + 自动化测试与自动评分环境下进行工程开发。 + +## 2. 实验原理 + +### 2.1 信道编码 + + - Hamming(7,4):一种线性分组码,将 4 个信息比特和 3 个校验比特组合成 7 个比特的码字(码率 R = 4/7)。通过生成矩阵 G + 实现编码:c = uG \bmod 2。 + - 伴随式(Syndrome):接收端利用校验矩阵 H 对接收码字 r 进行校验,计算伴随式 s = rH^T \bmod 2。若 s=0,通常表示无错误。 + - 单比特纠错:若传输中发生 1 个比特的翻转,伴随式 s 将非零,且其值必定与校验矩阵 H + 的某一列完全相同。通过比对列的索引,即可精确定位并翻转纠正该错误比特。 + - 编码增益:在达到相同误码率(BER)的前提下,采用信道编码的系统相比未编码系统所节省的信噪比(SNR),体现了编码带来的性能提升。 + +### 2.2 信道均衡 + + - 多径信道与 + ISI:无线电波经过建筑物等反射产生多条路径,导致接收信号中不仅包含当前符号,还叠加了前后相邻符号的延迟衰减分量,造成符号间干扰(ISI),使眼图闭合。 + - ZF(迫零)均衡:强行将“信道 + 均衡器”的整体冲激响应强制设为冲激函数(仅在中心时刻为1,其余时刻为0),通过矩阵最小二乘法求得 FIR + 滤波器抽头,从而完全消除 ISI。 + - LMS(最小均方)自适应均衡:利用梯度下降算法,在已知训练序列的情况下,根据接收信号计算误差,并动态更新抽头系数(w \leftarrow w + \mu \cdot e \cdot x)。它能在消除 + ISI 和抑制噪声之间取得良好的折中。 + +## 3. 实验环境 + + - Python 版本:Python 3.9+ + - 主要依赖:NumPy(矩阵与数组运算)、Matplotlib(数据可视化绘图)、SciPy(科学计算)、pytest(单元测试框架)。 + - AI 助手使用情况:在本次实验中,使用了 AI 助手(如 ChatGPT / Copilot)辅助理解了 Numpy 数组在 LMS + 算法中的步长切片逻辑(例如时间反转 rx_train[i : i - num_taps : + -1]),并帮助排查了矩阵乘法时的维度对齐报错问题。所有代码均已亲自运行并验证。 + +## 4. 实验方法与步骤 + +### 4.1 Part 1:信道编码 + +1. 编码:利用 numpy.reshape 将输入比特流划分为 4 位一组的块,与生成矩阵 HAMMING_G 进行矩阵乘法并对 2 取模,生成 7 + 位码字。 +2. 加噪/翻转:模拟二进制对称信道(BSC),根据给定的误码概率 p_e 随机翻转传输比特,模拟信道噪声。 +3. 译码:接收端计算每个 7 位码字的伴随式。遍历检测非零伴随式,与 HAMMING_H 各列比对定位错误并翻转,最后提取前 4 位信息位。 +4. BER 对比:统计纠错前后的错误比特数,计算误码率(BER),并利用 Matplotlib 绘制不同误码概率下的 BER 对比曲线。 + +### 4.2 Part 2:信道均衡 + +1. 模拟多径信道:利用已知的信道冲激响应(包含主径和多条延迟径)与发送信号进行卷积,模拟 ISI 的产生。 +2. ZF 均衡器设计:构造信道卷积矩阵 A 和期望的冲激响应向量 d,使用 np.linalg.lstsq 求解最小二乘解,得到迫零均衡器的 FIR 抽头。 +3. LMS 均衡器设计:初始化滤波器抽头,利用滑动窗口截取接收序列,计算输出值与期望训练符号之间的误差,按照 LMS + 更新规则迭代修正抽头系数,记录每次的瞬时平方误差。 +4. 效果评估:将接收信号通过 np.convolve 分别送入 ZF 和 LMS 滤波器,截断后观察均衡效果,绘制误差收敛曲线与均衡前后的眼图。 + +## 5. 实验结果 + +![编码BER曲线](results/coding_ber_curve.png) +![均衡眼图对比](results/equalization_eye_comparison.png) +![LMS误差曲线](results/equalization_mse_curve.png) + +## 6. 结果分析与讨论 +回答: +1. Hamming(7,4) 为什么能纠正单比特错误? 答:因为 Hamming(7,4) 包含 3 个校验位,可以产生 2^3 - 1 = 7 + 种非零的伴随式状态。这刚好与一帧 7 个比特的每一个错误位置一一对应(即 H 矩阵的 7 + 个各不相同的列向量)。当发生单比特错误时,算出的伴随式必然指向唯一的出错位置。 +2. 为什么信道编码会引入冗余并降低码率? 答:为了在接收端发现甚至纠正错误,必须发送额外的“校验信息”(冗余)。Hamming(7,4) 中每 4 + 个有用信息需要搭售 3 个校验位,导致传输效率(码率)降低到了 4/7。这是用带宽(传输速率)换取可靠性(低误码率)的必然代价。 +3. ZF 均衡为什么可能放大噪声? 答:ZF + 均衡器在频域上相当于信道频率响应的倒数(1/H(f))。当多径信道在某些频率处发生深度衰落(H(f) \rightarrow 0)时,ZF + 均衡器会在这些频点施加极大的增益来强行补偿,这会把该频段的背景高斯噪声急剧放大,严重时甚至淹没有效信号。 +4. LMS 的步长过大或过小会出现什么问题? 答:若步长 \mu 过大,权重更新会非常剧烈,导致算法越过最优解来回震荡,误差无法收敛甚至直接发散;若步长 + \mu 过小,虽然最终误差较小且稳定,但收敛速度会极其缓慢,需要极长的训练序列才能达到稳态,且难以追踪时变的无线信道。 +5. 均衡前后 ISI 有什么变化? 答:均衡前,由于多径传播导致前后符号严重混叠,ISI 极大,表现在眼图上就是“眼睛完全闭合”,接收端无法正确判决 0 + 和 1;经过 ZF 或 LMS 均衡后,信道的延迟扩散被滤波器抽头补偿抵消,ISI + 被显著抑制,眼图重新清晰“张开”,散点收敛,恢复了良好的判决余量。 + +## 7. 实验心得 + +通过本次实验,我将《无线通信技术》课堂上的抽象数学公式(如矩阵乘法、卷积计算、梯度下降)真正转化为了可运行的 Python 代码。我不仅直观地看到了 +Hamming 编码在恶劣信道下对 BER 曲线的显著拉低作用,也深刻体会到了“带宽与可靠性”之间的折中。同时,自己动手编写 LMS +自适应迭代过程,让我彻底明白了步长参数对收敛性能的影响。 + +此外,本次实验引入了 GitHub 自动评分(CI/CD)流程,这种每次 Push +代码后自动给出测试反馈的模式非常有工业界软件开发的真实感,让我养成了提交前做本地测试的好习惯。在遇到数组维度报错等困难时,合理运用 +AI 辅助也极大地提升了我的 Debug 效率。 + +## 8. 参考资料 + + - 课程课件:第6章 信道编码 + - 课程课件:第7章 均衡 + - 实验指导手册:无线通信技术实验指导手册(学生版) diff --git a/src/part1_channel_coding.py b/src/part1_channel_coding.py index 1ecf55e..065b462 100644 --- a/src/part1_channel_coding.py +++ b/src/part1_channel_coding.py @@ -49,7 +49,16 @@ def hamming74_encode(bits): raise ValueError('bits 只能包含 0 或 1') # TODO: 将 bits reshape 为 (-1, 4),再与 HAMMING_G 相乘并对 2 取模。 - raise NotImplementedError('请实现 Hamming(7,4) 编码') + # 建议实现步骤: + # 1. 将 `bits` reshape 成形状 `(-1, 4)` + blocks = bits.reshape(-1, 4) + # 2. 计算 `blocks @ HAMMING_G` + encoded_blocks = np.dot(blocks, HAMMING_G) + # 3. 对结果取模 2 + encoded_blocks = encoded_blocks % 2 + # 4. 将二维数组 flatten 成一维数组返回 + return encoded_blocks.flatten() + #raise NotImplementedError('请实现 Hamming(7,4) 编码') def hamming74_syndrome(codewords): @@ -71,7 +80,17 @@ def hamming74_syndrome(codewords): raise ValueError('每个 Hamming(7,4) 码字长度必须为 7') # TODO: 计算 s = r H^T mod 2。 - raise NotImplementedError('请实现伴随式计算') + # 建议实现步骤: + # 1. 如果输入是一维数组,reshape 为 `(-1, 7)` + if codewords.ndim == 1: + codewords = codewords.reshape(-1, 7) + # 2. 计算 `codewords @ HAMMING_H.T` + syndrome = np.dot(codewords, HAMMING_H.T) + # 3. 对结果取模 2 + syndrome = syndrome % 2 + # 4. 返回伴随式矩阵 + return syndrome + #raise NotImplementedError('请实现伴随式计算') def hamming74_decode(received): @@ -95,7 +114,26 @@ def hamming74_decode(received): raise ValueError('received 必须是一维数组,长度为 7 的倍数') # TODO: 使用 hamming74_syndrome 完成单比特纠错,并返回前 4 个信息位。 - raise NotImplementedError('请实现 Hamming(7,4) 译码') + # 建议实现步骤: + # 1. 将 `received` reshape 为 `(-1, 7)`,复制一份避免修改原数据 + recv_blocks = received.copy().reshape(-1, 7) + # 2. 调用上面的函数计算伴随式 + syndromes = hamming74_syndrome(recv_blocks) + + # 3 & 4. 寻找非零伴随式并翻转对应比特 + for i in range(len(recv_blocks)): + s = syndromes[i] + if np.any(s != 0): # 检测到错误 + # 遍历校验矩阵H的每一列,寻找与伴随式匹配的列 + for col_idx in range(7): + if np.array_equal(s, HAMMING_H[:, col_idx]): + # 找到错误位置,翻转该比特 (0变1,1变0) + recv_blocks[i, col_idx] ^= 1 + break + + # 5. 取每个码字前 4 位 (信息位) 并 flatten 返回 + return recv_blocks[:, :4].flatten() + #raise NotImplementedError('请实现 Hamming(7,4) 译码') def convolutional_encode(bits): @@ -109,6 +147,7 @@ def convolutional_encode(bits): raise ValueError('bits 只能包含 0 或 1') # TODO: 选做任务,可参考课件第6章卷积码部分。 + raise NotImplementedError('选做:请实现卷积码编码') diff --git a/src/part2_equalization.py b/src/part2_equalization.py index 8cbf1d8..a757235 100644 --- a/src/part2_equalization.py +++ b/src/part2_equalization.py @@ -39,7 +39,25 @@ def estimate_zf_equalizer(channel, num_taps): raise ValueError('num_taps 必须为正整数') # TODO: 构造卷积矩阵并求解 ZF 均衡器抽头。 - raise NotImplementedError('请实现 ZF 均衡器估计') + from scipy.linalg import convolution_matrix # 可能需要导入此工具或自己构造 + # 1. 构造卷积矩阵 A + # channel 长度为 L,taps 长度为 N,卷积结果长度为 L + N - 1 + L = len(channel) + A = np.zeros((L + num_taps - 1, num_taps)) + for i in range(num_taps): + A[i:i+L, i] = channel + + # 2. 构造目标冲激响应 d + d = np.zeros(L + num_taps - 1) + delay = num_taps // 2 # 通常将 1 放在中心位置 + d[delay] = 1.0 + + # 3. 使用最小二乘解求 taps + taps, _, _, _ = np.linalg.lstsq(A, d, rcond=None) + + # 4. 返回 taps + return taps + #raise NotImplementedError('请实现 ZF 均衡器估计') def apply_fir_filter(signal, taps): @@ -59,7 +77,10 @@ def apply_fir_filter(signal, taps): raise ValueError('signal 和 taps 必须是一维数组') # TODO: 使用 np.convolve,并截取与 signal 等长的输出。 - raise NotImplementedError('请实现 FIR 滤波') + # 建议实现步骤极其简单: + filtered_signal = np.convolve(signal, taps, mode='full') + return filtered_signal[:len(signal)] + #raise NotImplementedError('请实现 FIR 滤波') def lms_equalizer(rx_train, tx_train, num_taps, step_size=0.01): @@ -90,7 +111,32 @@ def lms_equalizer(rx_train, tx_train, num_taps, step_size=0.01): raise ValueError('num_taps 必须为正整数') # TODO: 实现 LMS 自适应均衡训练。 - raise NotImplementedError('请实现 LMS 均衡器') + # 1. 初始化 taps,中心抽头为1,其余为0 + taps = np.zeros(num_taps) + taps[num_taps // 2] = 1.0 + + errors = [] + # 2. 从第 num_taps - 1 个样本开始迭代 + for i in range(num_taps - 1, len(rx_train)): + # 3. 构造当前输入向量 x (注意时间反转,越新的样本在越前面) + x = rx_train[i : i - num_taps : -1] if i - num_taps >= -1 else rx_train[i::-1] + # 如果切片不够长(边界情况),通常补零,但从 num_taps-1 开始切片长度刚好是 num_taps + if len(x) < num_taps: + x = np.pad(x, (0, num_taps - len(x))) + + # 4. 计算输出 y + y = np.dot(taps, x) + + # 5. 计算误差 e + d = tx_train[i - num_taps // 2] # 对齐中心延迟 + e = d - y + errors.append(e) + + # 6. 更新 taps + taps = taps + step_size * e * x + + return taps, np.array(errors) + #raise NotImplementedError('请实现 LMS 均衡器') def run_equalization_demo():