Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
90 changes: 90 additions & 0 deletions REPORT.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
# 无线通信技术实验报告:信道编码与信道均衡

- 姓名:徐雷
- 学号:2022090123
- 实验名称:信道编码与信道均衡综合实验

## 1. 实验目的

本实验通过补全 Python 仿真代码,理解无线通信系统中信道编码和信道均衡的基本作用。Part 1 使用 Hamming(7,4) 线性分组码完成编码、伴随式检测和单比特纠错,观察冗余校验位对误比特率 BER 的改善。Part 2 面向多径信道造成的符号间干扰,实现 ZF 迫零均衡器、FIR 滤波和 LMS 自适应均衡器,观察均衡前后波形、误差曲线和 BER 变化。同时,本实验还练习了 pytest 本地测试、结果图生成和 GitHub 自动评分流程。

## 2. 实验原理

### 2.1 信道编码

Hamming(7,4) 码每 4 个信息比特生成 7 个码字比特,其中前 4 位为信息位,后 3 位为校验位。本实验使用系统码生成矩阵 `HAMMING_G`,编码过程是在 GF(2) 上计算 `c = uG mod 2`。接收端使用校验矩阵 `HAMMING_H` 计算伴随式 `s = rH^T mod 2`。

如果伴随式为全零,说明没有检测到单比特错误;如果伴随式非零,则将它与 `HAMMING_H` 的各列比较。由于 Hamming(7,4) 的每一列对应一个码字位置,匹配到的列号就是错误比特位置,翻转该位置即可纠正单比特错误。Hamming(7,4) 通过增加 3 个冗余位提升可靠性,但码率从 1 降为 4/7,因此会牺牲一部分传输效率。

### 2.2 信道均衡

多径信道可表示为 `r[n] = sum h[k]s[n-k] + w[n]`。当多个延迟路径叠加时,当前接收符号会包含相邻符号的影响,形成符号间干扰 ISI。均衡器的目标是设计 FIR 滤波器,使信道和均衡器的整体卷积响应尽量接近单位冲激。

ZF 均衡器通过求解 `A @ taps ≈ d` 得到抽头,其中 `A` 是由信道冲激响应构造的卷积矩阵,`d` 是中心位置为 1 的目标冲激响应。ZF 能压低 ISI,但如果信道频谱存在深衰落,可能把噪声一并放大。

LMS 均衡器是一种自适应方法。每次迭代取当前接收样本和过去样本组成输入向量 `x[n]`,计算输出 `y[n] = w^T x[n]`,再用训练符号得到误差 `e[n] = d[n] - y[n]`,最后按 `w = w + μ e[n] x[n]` 更新抽头。步长 `μ` 过小会导致收敛慢,过大则可能振荡甚至发散。

## 3. 实验环境

- Python 版本:3.13.5
- 主要依赖:NumPy 2.1.3、SciPy 1.15.3、Matplotlib 3.10.0、pytest 8.3.4
- 操作系统环境:Windows PowerShell
- AI 助手使用情况:解释 Hamming(7,4)、ZF 和 LMS 的实现思路,并在本地运行 pytest 与评分脚本验证结果。最终提交内容经过本地测试和结果检查。

## 4. 实验方法与步骤

### 4.1 Part 1:信道编码

首先将输入比特序列转换为一维整数数组,并检查长度是否为 4 的倍数。然后把比特按每 4 位分组,计算 `(blocks @ HAMMING_G) % 2` 得到编码码字。接收端先将长度为 7 的倍数的接收序列 reshape 成码字矩阵,再计算 `(codewords @ HAMMING_H.T) % 2` 得到伴随式。译码时,对于每个非零伴随式,逐列匹配 `HAMMING_H`,找到错误位置并翻转,最后取每个码字的前 4 位作为译码结果。

实验脚本中对原始比特和 Hamming 编码比特分别通过二元对称信道,比较未编码 BER 和编码后译码 BER,并生成 BER 曲线。

### 4.2 Part 2:信道均衡

ZF 均衡部分先根据信道响应构造卷积矩阵,使矩阵乘法结果等价于 `np.convolve(channel, taps)`,再以中心冲激为目标用最小二乘法求解均衡器抽头。FIR 滤波部分使用 `np.convolve(signal, taps, mode='full')`,并截取前 `len(signal)` 个样本,使输出长度和输入一致。

LMS 均衡部分将抽头初始化为中心抽头为 1,其余为 0。从第 `num_taps - 1` 个样本开始,每次取当前及过去接收样本组成输入窗口,计算输出、误差并更新抽头。训练完成后,将学到的抽头应用到完整接收序列,比较均衡前后的 BER,并绘制波形对比和误差曲线。

## 5. 实验结果

![编码BER曲线](results/coding_ber_curve.png)

![均衡眼图对比](results/equalization_eye_comparison.png)

![LMS误差曲线](results/equalization_mse_curve.png)

Part 1 的 BER 统计结果如下:

| 信道翻转概率 | 未编码 BER | Hamming(7,4) BER |
| --- | ---: | ---: |
| 0.001 | 0.00075 | 0.00000 |
| 0.003 | 0.00300 | 0.00000 |
| 0.010 | 0.01125 | 0.00000 |
| 0.030 | 0.02925 | 0.00700 |
| 0.060 | 0.05775 | 0.01950 |
| 0.100 | 0.10100 | 0.06800 |

Part 2 中,均衡前 BER 为 0.00100,LMS 均衡后 BER 为 0.00000。LMS 训练误差前 100 次迭代均方误差约为 1.05550,最后 100 次约为 0.03828。ZF 等效响应主峰约为 0.94440,旁瓣能量约为 0.05251。

## 6. 结果分析与讨论

Hamming(7,4) 能纠正单比特错误,是因为任意一个码字位置翻转都会产生唯一的非零伴随式,该伴随式正好等于校验矩阵的对应列。因此译码器可以根据 syndrome 直接定位错误位置。在较低信道翻转概率下,一个 7 位码字中出现两位及以上错误的概率较低,单比特纠错能力能够显著降低 BER。随着翻转概率升高,多比特错误增多,Hamming(7,4) 不能全部纠正,因此编码后 BER 仍会上升。

信道编码的代价是引入冗余。本实验中每 4 个信息比特需要发送 7 个码字比特,码率为 4/7。冗余位本身不携带新的用户信息,但提供了检错和纠错能力,这体现了可靠性和传输效率之间的折中。

均衡实验中,多径信道使接收波形相对发送符号出现拖尾和叠加。ZF 均衡通过最小二乘方式让等效冲激响应接近中心单峰,测试中主峰明显高于旁瓣,说明 ISI 被压制。但 ZF 只从消除 ISI 的角度设计,如果某些频率分量被信道严重衰减,逆滤波会放大这些频率上的噪声。

LMS 的误差曲线整体下降,说明抽头通过训练序列逐步逼近可用的均衡器。步长太小时,更新幅度小,训练需要更多符号才能收敛;步长太大时,抽头可能在最优值附近振荡,甚至使误差发散。本实验步长 0.01 在当前信道和训练长度下能稳定收敛,均衡后 BER 降到 0。

## 7. 实验心得

本实验把课件中的矩阵编码、伴随式译码和自适应滤波公式落实到可运行代码中。Hamming(7,4) 的实现重点在于所有矩阵运算都必须在 GF(2) 中进行,译码时 syndrome 与校验矩阵列的对应关系非常关键。均衡部分的重点在于时序对齐:FIR 输出、ZF 卷积矩阵和 LMS 输入窗口必须采用一致的因果卷积约定,否则即使公式正确,误差也可能无法下降。

通过 pytest 和本地评分脚本,可以及时发现函数签名、返回形状和边界输入问题。AI 工具适合用于解释算法、检查实现思路和辅助调试,但实验结果必须通过本地运行、图像生成和评分脚本验证,不能只依赖生成代码本身。

## 8. 参考资料

- 课程课件:第 6 章 信道编码
- 课程课件:第 7 章 均衡
- 仓库文档:`docs/theory_channel_coding.md`
- 仓库文档:`docs/theory_equalization.md`
Binary file added results/coding_ber_curve.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added results/equalization_eye_comparison.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added results/equalization_mse_curve.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
37 changes: 28 additions & 9 deletions src/part1_channel_coding.py
Original file line number Diff line number Diff line change
Expand Up @@ -48,8 +48,9 @@ def hamming74_encode(bits):
if not np.all((bits == 0) | (bits == 1)):
raise ValueError('bits 只能包含 0 或 1')

# TODO: 将 bits reshape 为 (-1, 4),再与 HAMMING_G 相乘并对 2 取模。
raise NotImplementedError('请实现 Hamming(7,4) 编码')
blocks = bits.reshape(-1, 4)
encoded = (blocks @ HAMMING_G) % 2
return encoded.reshape(-1)


def hamming74_syndrome(codewords):
Expand All @@ -70,8 +71,12 @@ def hamming74_syndrome(codewords):
if codewords.shape[1] != 7:
raise ValueError('每个 Hamming(7,4) 码字长度必须为 7')

# TODO: 计算 s = r H^T mod 2。
raise NotImplementedError('请实现伴随式计算')
if codewords.ndim != 2:
raise ValueError('codewords 必须是一维或二维数组')
if not np.all((codewords == 0) | (codewords == 1)):
raise ValueError('codewords 只能包含 0 或 1')

return (codewords @ HAMMING_H.T) % 2


def hamming74_decode(received):
Expand All @@ -94,8 +99,22 @@ def hamming74_decode(received):
if received.ndim != 1 or len(received) % 7 != 0:
raise ValueError('received 必须是一维数组,长度为 7 的倍数')

# TODO: 使用 hamming74_syndrome 完成单比特纠错,并返回前 4 个信息位。
raise NotImplementedError('请实现 Hamming(7,4) 译码')
if not np.all((received == 0) | (received == 1)):
raise ValueError('received 只能包含 0 或 1')

codewords = received.reshape(-1, 7).copy()
syndromes = hamming74_syndrome(codewords)
syndrome_table = HAMMING_H.T

for row_index, syndrome in enumerate(syndromes):
if np.all(syndrome == 0):
continue
matches = np.where(np.all(syndrome_table == syndrome, axis=1))[0]
if len(matches) == 1:
error_position = matches[0]
codewords[row_index, error_position] ^= 1

return codewords[:, :4].reshape(-1)


def convolutional_encode(bits):
Expand Down Expand Up @@ -152,11 +171,11 @@ def run_coding_demo():
'Hamming(7,4) 编码前后 BER 对比',
'coding_ber_curve.png',
)
print(' 已生成 results/coding_ber_curve.png')
print('[OK] 已生成 results/coding_ber_curve.png')
except NotImplementedError as error:
print(f'⏸️ 尚未完成核心函数:{error}')
print(f'[TODO] 尚未完成核心函数:{error}')
except Exception as error:
print(f' Part 1 运行失败:{error}')
print(f'[ERROR] Part 1 运行失败:{error}')


if __name__ == '__main__':
Expand Down
42 changes: 32 additions & 10 deletions src/part2_equalization.py
Original file line number Diff line number Diff line change
Expand Up @@ -38,8 +38,16 @@ def estimate_zf_equalizer(channel, num_taps):
if num_taps < 1:
raise ValueError('num_taps 必须为正整数')

# TODO: 构造卷积矩阵并求解 ZF 均衡器抽头。
raise NotImplementedError('请实现 ZF 均衡器估计')
output_length = len(channel) + num_taps - 1
matrix = np.zeros((output_length, num_taps), dtype=float)
for tap_index in range(num_taps):
matrix[tap_index:tap_index + len(channel), tap_index] = channel

desired = np.zeros(output_length, dtype=float)
desired[output_length // 2] = 1.0

taps, *_ = np.linalg.lstsq(matrix, desired, rcond=None)
return taps


def apply_fir_filter(signal, taps):
Expand All @@ -58,8 +66,7 @@ def apply_fir_filter(signal, taps):
if signal.ndim != 1 or taps.ndim != 1:
raise ValueError('signal 和 taps 必须是一维数组')

# TODO: 使用 np.convolve,并截取与 signal 等长的输出。
raise NotImplementedError('请实现 FIR 滤波')
return np.convolve(signal, taps, mode='full')[: len(signal)]


def lms_equalizer(rx_train, tx_train, num_taps, step_size=0.01):
Expand Down Expand Up @@ -89,8 +96,23 @@ def lms_equalizer(rx_train, tx_train, num_taps, step_size=0.01):
if num_taps < 1:
raise ValueError('num_taps 必须为正整数')

# TODO: 实现 LMS 自适应均衡训练。
raise NotImplementedError('请实现 LMS 均衡器')
if rx_train.ndim != 1 or tx_train.ndim != 1:
raise ValueError('rx_train 和 tx_train 必须是一维数组')
if len(rx_train) < num_taps:
raise ValueError('训练序列长度必须不小于 num_taps')

taps = np.zeros(num_taps, dtype=float)
taps[num_taps // 2] = 1.0
errors = []

for sample_index in range(num_taps - 1, len(rx_train)):
window = rx_train[sample_index - num_taps + 1:sample_index + 1][::-1]
output = float(taps @ window)
error = tx_train[sample_index] - output
taps = taps + step_size * error * window
errors.append(error)

return taps, np.asarray(errors, dtype=float)


def run_equalization_demo():
Expand All @@ -106,7 +128,7 @@ def run_equalization_demo():
rx = multipath_channel(symbols, channel, noise_std=0.12, seed=7)

zf_taps = estimate_zf_equalizer(channel, num_taps=7)
zf_output = apply_fir_filter(rx, zf_taps)
apply_fir_filter(rx, zf_taps)

lms_taps, errors = lms_equalizer(rx[:800], symbols[:800], num_taps=7, step_size=0.01)
lms_output = apply_fir_filter(rx, lms_taps)
Expand All @@ -118,11 +140,11 @@ def run_equalization_demo():

plot_equalization_results(symbols, rx, lms_output, 'equalization_eye_comparison.png')
plot_mse_curve(errors, 'equalization_mse_curve.png')
print(' 已生成均衡结果图')
print('[OK] 已生成均衡结果图')
except NotImplementedError as error:
print(f'⏸️ 尚未完成核心函数:{error}')
print(f'[TODO] 尚未完成核心函数:{error}')
except Exception as error:
print(f' Part 2 运行失败:{error}')
print(f'[ERROR] Part 2 运行失败:{error}')


if __name__ == '__main__':
Expand Down
Loading