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
183 changes: 183 additions & 0 deletions REPORT.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,183 @@
# 信道编码与信道均衡综合实验报告

## 一、实验目的

本实验围绕信道编码与信道均衡两个主题展开,目的是通过代码实现和结果分析,加深对数字通信基础方法的理解。具体包括以下内容:

- 理解 Hamming(7,4) 信道编码的基本思想,掌握通过增加冗余提升传输可靠性的方法。
- 掌握 syndrome 伴随式检测方法,理解接收端如何利用校验矩阵进行错误检测。
- 掌握 Hamming(7,4) 单比特纠错流程,理解错误定位与纠正的实现方式。
- 理解多径信道的物理含义,认识符号间干扰 ISI 对接收判决的影响。
- 掌握 ZF 均衡器的基本设计思路,理解其抑制 ISI 的方法与局限。
- 掌握 LMS 自适应均衡算法的更新过程,理解训练序列在均衡器收敛中的作用。

## 二、实验原理

### 2.1 Hamming(7,4) 信道编码

Hamming(7,4) 是一种典型的线性分组码,将 4 位信息比特编码为 7 位码字。与直接发送原始比特相比,它额外加入了 3 位冗余校验比特,因此能够在接收端具备一定的检错和纠错能力。

本实验采用生成矩阵进行编码,编码公式为:

$$
c = uG \bmod 2
$$

其中,`u` 表示长度为 4 的信息比特向量,`G` 为生成矩阵,`c` 为长度为 7 的编码码字。通过这种方式,可以把信息位和校验位统一表示为矩阵运算,便于程序实现。

### 2.2 Syndrome 伴随式与单比特纠错

接收端对收到的码字使用校验矩阵 `H` 计算 syndrome,公式为:

$$
s = rH^T \bmod 2
$$

其中,`r` 为接收码字,`s` 为伴随式。如果 `s=0`,则认为没有检测到单比特错误;如果 `s` 非零,则说明接收码字很可能出现了错误。

在 Hamming(7,4) 码中,非零 syndrome 可以与校验矩阵 `H` 的各列进行比较。若某一列与 syndrome 完全一致,就可以判定对应位置发生了单比特错误,再将该比特翻转完成纠错。由于本实验使用的是系统码结构,纠错后取码字前 4 位即可作为译码输出。

### 2.3 多径信道与 ISI

无线信道中,信号通常不会只沿一条路径到达接收端,而是会经过反射、绕射、散射等过程沿多条路径传播。不同路径具有不同的时延和衰减,因此接收端实际接收到的是多个延迟分量的叠加。

当一个符号的尾部延伸到后续符号时,就会对相邻符号产生干扰,这种现象称为符号间干扰 ISI。ISI 会使接收波形失真,判决边界变得模糊,从而降低检测准确率并增大误码率。

### 2.4 ZF 均衡

ZF(Zero-Forcing)均衡器的目标是让“信道 + 均衡器”的整体响应尽量接近一个理想冲激响应,从而压制 ISI。实验中通过构造卷积矩阵,将均衡器设计问题写成线性方程组,并使用最小二乘方法估计均衡器抽头系数。

ZF 的优点是思路直接、实现清晰,对抑制码间干扰比较有效;但它也存在明显局限。当信道某些频率分量较弱时,ZF 为了强行补偿失真,可能会把噪声一同放大,因此在低信噪比条件下效果不一定稳定。

### 2.5 LMS 自适应均衡

LMS(Least Mean Squares)是一种基于训练序列的自适应均衡方法。它不直接求解析最优解,而是在每个时刻根据当前误差逐步更新抽头,使均衡器输出逐渐逼近期望信号。

其核心关系为:

- 输出:`y = taps @ x`
- 误差:`e = d - y`
- 更新:`taps = taps + step_size * e * x`

其中,`x` 为当前输入向量,`d` 为期望发送符号,`step_size` 为步长。步长过大可能导致震荡,过小则会使收敛速度变慢。LMS 的优点是实现简单、适应性较强,适合处理实际信道变化。

## 三、实验环境

- 操作系统:Windows
- Python 版本:Python 3.13.5
- 主要依赖:NumPy、SciPy、Matplotlib、pytest
- 开发工具:VS Code / PowerShell / GitHub

如需与本次实验完全一致,建议根据本地实际环境再次核对依赖版本。

## 四、实验方法

### 4.1 环境配置

首先根据仓库说明安装依赖,并检查实验环境是否正常。使用的命令如下:

```bash
pip install -r requirements.txt
python src/test_environment.py
```

完成环境配置后,再分别进入 Part 1 与 Part 2 的函数补全和结果验证。

### 4.2 Part 1:Hamming(7,4) 编码与译码

本部分在 `src/part1_channel_coding.py` 中补全了以下函数:

- `hamming74_encode(bits)`
- `hamming74_syndrome(codewords)`
- `hamming74_decode(received)`

实现过程中,先利用生成矩阵完成 Hamming(7,4) 编码,再通过校验矩阵计算 syndrome;若 syndrome 非零,则与 `H` 的列向量比较以定位错误位,并进行单比特纠错。

完成代码后,使用以下命令进行测试与实验运行:

```bash
python -m pytest grading/test_part1_coding.py -v
python src/part1_channel_coding.py
```

### 4.3 Part 2:ZF 与 LMS 信道均衡

本部分在 `src/part2_equalization.py` 中补全了以下函数:

- `estimate_zf_equalizer(channel, num_taps)`
- `apply_fir_filter(signal, taps)`
- `lms_equalizer(rx_train, tx_train, num_taps, step_size)`

其中,ZF 均衡器通过最小二乘法估计 FIR 抽头;`apply_fir_filter` 用于完成等长滤波输出;LMS 均衡器则利用训练序列不断更新权值,观察误差随迭代的变化。

完成代码后,使用以下命令进行测试与实验运行:

```bash
python -m pytest grading/test_part2_equalization.py -v
python src/part2_equalization.py
```

## 五、实验结果

### 5.1 Hamming(7,4) 编码 BER 曲线

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

从结果图可以看出,未编码传输和 Hamming(7,4) 编码传输的 BER 随信道误码概率变化而变化。一般来说,在误码概率较低到中等的范围内,Hamming(7,4) 编码后的 BER 会低于未编码情况,说明适当增加冗余后,系统具备了更强的抗错能力。

出现这种改善的原因是 Hamming(7,4) 可以对单比特错误进行定位和纠正。当每个码字中只出现一个错误时,接收端能够利用 syndrome 找到错误位置并恢复正确比特,因此整体误码率会下降。

不过也应注意,Hamming(7,4) 的主要能力是单比特纠错。如果同一码字中出现两个或更多错误,译码结果可能无法纠正,甚至可能产生误判。因此当信道误码率进一步升高时,其性能提升会受到限制。

### 5.2 均衡前后眼图对比

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

从眼图对比可以看到,均衡前的接收信号受到多径和 ISI 影响,眼图张开程度相对较差,判决区域不够清晰。符号之间的波形相互叠加后,接收端更容易出现误判。

经过均衡后,眼图开口有所改善,说明均衡器在一定程度上抑制了符号间干扰。眼图越开阔,通常表示采样时刻的判决裕量越大,系统对噪声和失真的容忍能力也更强。

### 5.3 LMS 均衡误差曲线

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

根据实验运行结果:

- 均衡前 BER: 0.0010
- LMS 均衡后 BER: 0.0000

从这一结果可以看出,LMS 均衡后接收信号质量得到了明显改善。在本次实验参数下,均衡器输出已经足以使后续 BPSK 判决不再出现误比特,因此 BER 降低到了 0。

同时,从误差曲线来看,如果 MSE 随训练过程整体呈下降趋势,就说明 LMS 更新过程是逐步收敛的,均衡器抽头正在向更合适的方向调整。若你后续重新运行代码得到的曲线细节略有差异,可结合实际图像进行补充说明。

## 六、结果分析

1. Hamming(7,4) 能提升通信可靠性,原因在于它通过增加冗余校验位,让接收端不仅能传输信息,还能利用结构化冗余来检测并纠正部分错误。对单比特错误而言,这种编码方式能明显降低最终误码率。

2. syndrome 的作用在于把“是否出错”和“错误在哪里”转化为矩阵运算结果。若 syndrome 为零,说明未检测到单比特错误;若 syndrome 非零,则可通过与 `H` 的列向量比较完成错误定位。

3. 多径信道会产生 ISI,是因为一个符号经过不同路径传播后,会以不同延迟叠加到接收端。这样前一个符号的残留成分会干扰后一个符号,导致接收波形失真。

4. ZF 和 LMS 的区别在于:ZF 更像一次性根据已知信道求一个近似逆系统,思路直接,但容易放大噪声;LMS 则依靠训练序列逐步更新抽头,适应性更强,更适合在未知或变化信道中使用。

5. 本实验中 LMS 均衡后 BER 从 0.0010 降到 0.0000,说明在当前信道响应、噪声水平和训练长度条件下,LMS 均衡器已经有效抑制了 ISI,使判决性能得到明显改善。这也说明所选步长和抽头数在本次实验中是有效的。

6. 本实验结果还会受到多个因素影响,例如信噪比越低,误码率通常越高;信道冲激响应越复杂,均衡难度越大;均衡器抽头数过少可能补偿不足,过多则会增加复杂度;LMS 步长设置不当会影响收敛速度和稳定性。这些因素都会影响最终 BER 和图像表现。

## 七、AI 助手使用说明

本实验中使用 AI 助手辅助理解 Hamming(7,4) 编码、syndrome 纠错、ZF 均衡和 LMS 更新公式,并辅助检查代码实现思路和分析实验结果。代码最终经过本地 `pytest` 测试和实验脚本运行验证,报告内容根据实际实验结果整理。

## 八、实验心得

通过本次实验,我对信道编码和信道均衡的基本思路有了更具体的认识。Hamming(7,4) 部分让我理解了通信系统如何通过增加冗余提升可靠性,也掌握了编码、syndrome 检测和单比特纠错的完整流程。

在均衡部分,我进一步理解了多径信道产生 ISI 的原因,并通过实现 ZF 与 LMS 两类均衡方法,体会了固定均衡与自适应均衡在思路上的差异。结合实验图像和 BER 结果,可以更直观地看到均衡对接收信号质量的改善作用。

此外,本实验还让我熟悉了使用 GitHub、`pytest` 和自动评分流程完成代码实验的基本方法,对后续课程实验和工程实践都有帮助。

## 九、参考资料

- 课程课件:第 6 章 信道编码
- 课程课件:第 7 章 均衡
- 本实验 GitHub 仓库中的 `README.md`、`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.
97 changes: 87 additions & 10 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)
codewords = (blocks @ HAMMING_G) % 2
return codewords.astype(int).reshape(-1)


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

# TODO: 计算 s = r H^T mod 2。
raise NotImplementedError('请实现伴随式计算')
syndrome = (codewords @ HAMMING_H.T) % 2
return syndrome.astype(int)


def hamming74_decode(received):
Expand All @@ -94,8 +95,18 @@ 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) 译码')
codewords = received.reshape(-1, 7).copy()
syndromes = hamming74_syndrome(codewords)

for index, syndrome in enumerate(syndromes):
if np.all(syndrome == 0):
continue
matches = np.all(HAMMING_H.T == syndrome, axis=1)
if np.any(matches):
error_position = int(np.argmax(matches))
codewords[index, error_position] ^= 1

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


def convolutional_encode(bits):
Expand All @@ -108,8 +119,20 @@ def convolutional_encode(bits):
if not np.all((bits == 0) | (bits == 1)):
raise ValueError('bits 只能包含 0 或 1')

# TODO: 选做任务,可参考课件第6章卷积码部分。
raise NotImplementedError('选做:请实现卷积码编码')
if bits.ndim != 1:
raise ValueError('bits 必须是一维数组')

state = np.zeros(2, dtype=int)
padded_bits = np.concatenate([bits, np.zeros(2, dtype=int)])
encoded = []

for bit in padded_bits:
register = np.array([bit, state[0], state[1]], dtype=int)
encoded.append((register[0] + register[1] + register[2]) % 2)
encoded.append((register[0] + register[2]) % 2)
state = register[:2]

return np.asarray(encoded, dtype=int)


def viterbi_decode_hard(received_bits):
Expand All @@ -120,8 +143,62 @@ def viterbi_decode_hard(received_bits):
if len(received_bits) % 2 != 0:
raise ValueError('卷积码接收序列长度必须是 2 的倍数')

# TODO: 选做任务,可使用汉明距离作为路径度量。
raise NotImplementedError('选做:请实现 Viterbi 硬判决译码')
if received_bits.ndim != 1:
raise ValueError('received_bits 必须是一维数组')
if not np.all((received_bits == 0) | (received_bits == 1)):
raise ValueError('received_bits 只能包含 0 或 1')

symbols = received_bits.reshape(-1, 2)
num_steps = symbols.shape[0]
num_states = 4
inf = np.inf

next_states = np.zeros((num_states, 2), dtype=int)
outputs = np.zeros((num_states, 2, 2), dtype=int)

for state in range(num_states):
memory = np.array([(state >> 1) & 1, state & 1], dtype=int)
for bit in (0, 1):
register = np.array([bit, memory[0], memory[1]], dtype=int)
next_states[state, bit] = bit * 2 + memory[0]
outputs[state, bit] = np.array([
(register[0] + register[1] + register[2]) % 2,
(register[0] + register[2]) % 2,
], dtype=int)

path_metrics = np.full(num_states, inf)
path_metrics[0] = 0
predecessors = np.full((num_steps, num_states), -1, dtype=int)
decisions = np.zeros((num_steps, num_states), dtype=int)

for step in range(num_steps):
new_metrics = np.full(num_states, inf)
for state in range(num_states):
if np.isinf(path_metrics[state]):
continue
for bit in (0, 1):
next_state = next_states[state, bit]
branch_metric = np.sum(outputs[state, bit] != symbols[step])
metric = path_metrics[state] + branch_metric
if metric < new_metrics[next_state]:
new_metrics[next_state] = metric
predecessors[step, next_state] = state
decisions[step, next_state] = bit
path_metrics = new_metrics

final_state = 0 if not np.isinf(path_metrics[0]) else int(np.argmin(path_metrics))
decoded = np.zeros(num_steps, dtype=int)
state = final_state

for step in range(num_steps - 1, -1, -1):
decoded[step] = decisions[step, state]
state = predecessors[step, state]
if state < 0 and step > 0:
raise ValueError('Viterbi 回溯失败,请检查输入序列')

if num_steps < 2:
return np.array([], dtype=int)
return decoded[:-2].astype(int)


def run_coding_demo():
Expand Down
30 changes: 24 additions & 6 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 均衡器估计')
total_length = len(channel) + num_taps - 1
a_matrix = np.zeros((total_length, num_taps), dtype=float)

for tap_index in range(num_taps):
a_matrix[tap_index : tap_index + len(channel), tap_index] = channel

desired = np.zeros(total_length, dtype=float)
desired[total_length // 2] = 1.0
taps, _, _, _ = np.linalg.lstsq(a_matrix, desired, rcond=None)
return taps


def apply_fir_filter(signal, taps):
Expand All @@ -58,8 +66,8 @@ 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 滤波')
filtered = np.convolve(signal, taps, mode='full')
return filtered[: len(signal)]


def lms_equalizer(rx_train, tx_train, num_taps, step_size=0.01):
Expand Down Expand Up @@ -89,8 +97,18 @@ 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 均衡器')
taps = np.zeros(num_taps, dtype=float)
taps[num_taps // 2] = 1.0
errors = []

for index in range(num_taps - 1, len(rx_train)):
x = rx_train[index - num_taps + 1 : index + 1][::-1]
y = taps @ x
error = tx_train[index] - y
taps = taps + step_size * error * x
errors.append(error)

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


def run_equalization_demo():
Expand Down
Loading