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

## 1. 实验目的

本实验旨在通过 Python 编程实践,掌握以下三项核心技能:

1. **信道编码**:理解 Hamming(7,4) 线性分组码的编码原理、伴随式计算方法以及单比特纠错译码流程;通过 BER 仿真对比编码前后的性能增益,理解编码增益的物理意义。
2. **信道均衡**:理解多径信道引起的符号间干扰(ISI)的产生机理;掌握迫零(ZF)均衡器的设计方法及 LMS 自适应均衡算法的原理与实现;通过均衡前后波形和误码率对比,评估均衡效果。
3. **GitHub 自动评分**:熟悉基于 GitHub Classroom + GitHub Actions 的自动评分工作流,体验工业界持续集成(CI)与自动化测试的实践流程。

## 2. 实验原理

### 2.1 信道编码

**Hamming(7,4) 线性分组码**

Hamming(7,4) 是一种典型的线性分组码,将每 4 个信息比特编码为 7 个码字比特,记为 (7,4) 码。其码率为:

$$R = \frac{k}{n} = \frac{4}{7} \approx 0.571$$

编码过程使用系统生成矩阵 G(4×7),其中前 4 列构成单位阵,后 3 列构成奇偶校验部分。编码公式为 GF(2) 域上的矩阵乘法:

$$c = u \cdot G \pmod{2}$$

其中 u 为 4 比特信息序列,c 为 7 比特码字。系统码的优点在于码字的前 4 位即为原始信息比特,译码后可直接提取。

本实验使用的生成矩阵为:

```
G = [[1, 0, 0, 0, 1, 1, 0],
[0, 1, 0, 0, 1, 0, 1],
[0, 0, 1, 0, 0, 1, 1],
[0, 0, 0, 1, 1, 1, 1]]
```

**伴随式与单比特纠错**

伴随式(syndrome)是译码的关键。接收码字 r 的伴随式定义为:

$$s = r \cdot H^T \pmod{2}$$

其中 H(3×7)为校验矩阵。若接收码字无错误,伴随式 s = [0, 0, 0];若第 i 个比特发生错误,则 s 等于 H 的第 i 列。通过查表即可定位并翻转错误比特,实现单比特纠错。

本实验使用的校验矩阵为:

```
H = [[1, 1, 0, 1, 1, 0, 0],
[1, 0, 1, 1, 0, 1, 0],
[0, 1, 1, 1, 0, 0, 1]]
```

**编码增益**

编码增益指在给定目标 BER 下,编码系统所需的信噪比(SNR)相对于未编码系统降低的分贝数。Hamming(7,4) 能纠正每个 7 比特块中的单比特错误,因此在低误码率信道下可获得显著的编码增益。但编码引入了冗余,码率从 1 降为 4/7,信息传输效率下降。

**选做——卷积码与 Viterbi 译码**

本实验还实现了 (2,1,3) 卷积码编码(生成多项式 g1 = 111, g2 = 101)和硬判决 Viterbi 译码。与分组码不同,卷积码以连续流方式编码,编码器包含记忆(两个移位寄存器),Viterbi 译码通过"加-比-选"操作在格状图上寻找最短路径(最小汉明距离),实现最大似然序列估计。

### 2.2 信道均衡

**符号间干扰(ISI)与多径信道**

在宽带无线通信中,信号经过多条路径到达接收端,不同路径具有不同的衰减系数和传播时延。离散基带多径信道模型为:

$$r[n] = \sum_{k} h[k] \cdot s[n-k] + w[n]$$

其中 s[n] 为发送符号,h[k] 为信道冲激响应,w[n] 为加性噪声,r[n] 为接收符号。由于多径引起的时延扩展,前一个符号的延迟分量会混叠到当前符号上,造成符号间干扰(ISI),严重恶化误码性能。

本实验使用三径信道 h = [0.9, 0.35, -0.25] 模拟典型多径环境。

**迫零(ZF)均衡**

ZF 均衡器通过设计 FIR 滤波器系数,使信道与均衡器的卷积逼近单位冲激响应:

$$h[n] * e[n] \approx \delta[n]$$

具体实现中,构造卷积线性方程 A·taps ≈ d,其中 A 为信道卷积矩阵,d 为中心位置为 1 的冲激向量,通过最小二乘法(np.linalg.lstsq)求解均衡器抽头系数。

ZF 均衡的缺点在于:当信道频率响应存在深衰落时,ZF 会在对应频点施加极高增益,从而显著放大噪声。本实验中 ZF 均衡器用于快速获得初始均衡系数。

**LMS 自适应均衡**

最小均方(LMS)算法是一种自适应滤波算法,使用训练序列逐步调整抽头系数,使输出与期望符号之间的均方误差最小化。LMS 迭代公式为:

$$y[n] = w^T x[n]$$

$$e[n] = d[n] - y[n]$$

$$w[n+1] = w[n] + \mu \cdot e[n] \cdot x[n]$$

其中 w 为抽头系数向量,x[n] 为接收信号向量,d[n] 为期望符号,μ 为步长。LMS 算法计算量小,适合实时应用。

步长 μ 的选择至关重要:
- μ 过大:收敛速度快,但稳态误差大,甚至可能导致算法发散
- μ 过小:稳态误差小,但收敛速度慢,需要更长的训练序列

## 3. 实验环境

- **Python 版本**:3.13.7
- **主要依赖**:NumPy 1.23+, SciPy 1.9+, Matplotlib 3.6+, pytest 7.0+
- **操作系统**:Windows 11
- **AI 助手使用情况**:本实验过程中使用 Claude Code 作为 AI 编程辅助工具,协助代码编写、调试、单元测试验证及实验报告撰写。AI 辅助显著提升了开发效率,但核心算法理解仍需结合课程理论知识。

## 4. 实验方法与步骤

### 4.1 Part 1:信道编码

**步骤 1:Hamming(7,4) 编码**

实现 `hamming74_encode()` 函数。将输入比特序列按每 4 比特分块,每块与生成矩阵 G(4×7)在 GF(2) 上做矩阵乘法,输出长度为输入长度的 7/4 倍。核心代码:

```python
reshaped = bits.reshape(-1, 4)
encoded = (reshaped @ HAMMING_G) % 2
return encoded.flatten()
```

**步骤 2:伴随式计算**

实现 `hamming74_syndrome()` 函数。将接收码字按每 7 比特分块,与校验矩阵 H 的转置在 GF(2) 上相乘,得到每个码字的 3 比特伴随式:

```python
return (codewords @ HAMMING_H.T) % 2
```

**步骤 3:单比特纠错译码**

实现 `hamming74_decode()` 函数。对每个接收码字:计算伴随式 → 若非零则与 H 的各列比对定位错误位 → 翻转该比特 → 提取前 4 位信息比特。

**步骤 4:BER 对比仿真**

调用 `run_coding_demo()`,在 BSC 信道误码概率 [0.001, 0.003, 0.01, 0.03, 0.06, 0.1] 下分别测量未编码和 Hamming(7,4) 编码的 BER,绘制对比曲线保存至 `results/coding_ber_curve.png`。

**步骤 5(选做):卷积码与 Viterbi 译码**

实现了 `convolutional_encode()` 和 `viterbi_decode_hard()` 函数,分别完成 (2,1,3) 卷积码编码和硬判决 Viterbi 译码。

### 4.2 Part 2:信道均衡

**步骤 1:ZF 均衡器估计**

实现 `estimate_zf_equalizer()` 函数。输入信道冲激响应 h 和均衡器抽头数,构造卷积矩阵 A,目标向量 d 为中心冲激,用最小二乘法求解均衡器系数。

**步骤 2:FIR 滤波**

实现 `apply_fir_filter()` 函数,对接收信号应用 FIR 均衡器,使用 `np.convolve` 实现线性卷积,截取与输入等长部分。

**步骤 3:LMS 自适应均衡**

实现 `lms_equalizer()` 函数。初始化抽头系数(中心抽头为 1,其余为 0),对训练序列逐符号迭代:计算滤波输出 → 计算误差 → 更新抽头系数。返回训练后的抽头系数和误差序列。

**步骤 4:均衡效果评估**

调用 `run_equalization_demo()`,使用三径信道 h = [0.9, 0.35, -0.25] 和噪声标准差 0.12,分别计算 ZF 和 LMS 均衡输出,对比均衡前后 BER,绘制波形对比图和 LMS 误差收敛曲线,保存至 `results/` 目录。

## 5. 实验结果

### Part 1:信道编码 BER 曲线

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

图中横轴为信道误码概率(即 BSC 信道的翻转概率),纵轴为译码后的 BER(对数坐标)。两条曲线分别对应"未编码"直接传输和"Hamming(7,4)"编码传输。可观察到:在低信道误码率下(如 p ≤ 0.01),Hamming(7,4) 码的 BER 显著低于未编码系统,体现了编码增益;但当信道误码率较高时(如 p = 0.1),单个码字内可能出现多个错误,超出 Hamming 码的单比特纠错能力,编码优势减弱。

### Part 2:均衡效果对比

**均衡前后波形对比**

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

图中展示了发送符号(原始 BPSK 信号 ±1)、多径接收信号(受 ISI 和噪声污染)以及 LMS 均衡输出的前 120 个符号对比。可以观察到:多径接收信号的幅度已明显偏离 ±1 电平,眼图张开度下降;经过 LMS 均衡后,输出信号更接近原始发送符号,幅度恢复至 ±1 附近,ISI 得到有效抑制。

**LMS 误差收敛曲线**

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

图中横轴为 LMS 迭代次数,纵轴为瞬时平方误差(对数坐标)。误差曲线从较大值迅速下降,随后稳定在较低水平,表明 LMS 算法成功收敛。训练均衡后的抽头系数对全部 2000 个符号进行滤波,均衡后 BER 较均衡前显著降低。

## 6. 结果分析与讨论

### 1. Hamming(7,4) 为什么能纠正单比特错误?

Hamming(7,4) 码利用 3 个奇偶校验位(7 - 4 = 3)产生 2^3 = 8 种可能的伴随式模式。8 种模式恰好对应"无误"(全零伴随式)和 7 个可能的单比特错误位置。伴随式 s 直接等于校验矩阵 H 中对应错误位置的列向量,因此可以通过查表唯一确定并翻转错误比特。这是 Hamming 码最小距离为 3 的直接体现——最小距离 d_min = 3 保证了单比特纠错能力 t = ⌊(d_min - 1)/2⌋ = 1。

### 2. 为什么信道编码会引入冗余并降低码率?

信道编码在信息比特之外附加了额外的校验比特,这些校验比特并不携带新的信息,而是提供错误检测和纠正能力。以 Hamming(7,4) 为例,每 4 个信息比特附加了 3 个校验比特,码率从 1(无编码)降为 4/7 ≈ 0.571。这是可靠性(纠错能力)与有效性(频谱效率)之间的基本权衡——部分带宽或功率被用于传输校验比特,换取更低的目标误码率。

### 3. ZF 均衡为什么可能放大噪声?

ZF 均衡器的设计目标是使信道与均衡器的级联响应逼近理想冲激,即:

$$H(z) \cdot E(z) \approx 1$$

等效于在频域对信道频率响应取倒数:E(z) ≈ 1/H(z)。当信道在某些频点存在深衰落(|H(f)| 很小)时,均衡器在这些频点的增益 ∝ 1/|H(f)| 将非常大。噪声在这些频段被大幅放大,导致输出信噪比严重恶化。因此 ZF 均衡在低 SNR 或多径严重的情况下性能较差,实际中常使用 MMSE 均衡在 ISI 消除和噪声放大之间取得平衡。

### 4. LMS 的步长过大或过小会出现什么问题?

LMS 步长 μ 直接影响算法的收敛性能:

- **步长过大**:抽头系数更新幅度大,初期收敛快,但稳态时在最优解附近大幅振荡,导致稳态均方误差(MSE)较大。极端情况下,若 μ 超过上限(通常为 2/λ_max,其中 λ_max 为输入自相关矩阵的最大特征值),算法将发散,误差趋于无穷。
- **步长过小**:抽头系数更新缓慢,需要大量迭代才能收敛到较优解。在训练序列长度有限时,若步长过小,抽头系数可能尚未收敛,均衡后残余 ISI 仍然较大。

实际应用中常采用变步长策略:初始阶段使用大步长加快收敛,后期减小步长降低稳态误差。

### 5. 均衡前后 ISI 有什么变化?

均衡前,由于多径信道 h = [0.9, 0.35, -0.25] 的存在,当前符号不仅受到自身的衰减,还叠加了前一符号的延时和反转分量,BPSK 信号在 ±1 电平处的眼图闭合,判决边界模糊。均衡后,LMS 自适应滤波器使等效信道逼近理想冲激响应,经过均衡的信号幅度恢复至 ±1 附近,残余 ISI 大幅减小,眼图重新张开,BER 显著下降。

## 7. 实验心得

**信道编码**

通过亲手实现 Hamming(7,4) 的编码、伴随式计算和单比特纠错全过程,我对线性分组码的代数结构有了更直观的理解。伴随式"零即无误,非零则定位"的机制简洁而优美。BER 曲线的仿真结果直观展示了编码增益的意义——用冗余换取可靠性。选做部分实现的卷积码与 Viterbi 译码让我体会到格状图上"加-比-选"的动态规划思想,以及硬判决译码的局限性。

**信道均衡**

均衡实验让我深刻理解了多径传播带来的 ISI 问题。ZF 均衡器简单直观,但其"逆滤波"思想也暴露了对噪声的敏感性。LMS 自适应均衡的实践展示了实际系统中"边接收边调整"的自适应能力:不需要信道先验信息,仅凭训练序列即可收敛到较优的均衡器系数。步长选取的 trade-off 体现了实际工程中"快速收敛 vs 低稳态误差"的经典矛盾。

**自动评分**

GitHub Actions 自动评分系统实现了"代码提交即测试"的 CI 理念。每次 push 都会触发 pytest 运行,涵盖单元测试、结果文件验证、报告完整性检查和代码质量检查。这种机制让人提前适应工业界开发流程,也强化了"写代码必须写测试"的工程意识。

**AI 编程辅助**

本次实验全程使用 Claude Code 作为编程辅助。AI 在代码框架填充、单元测试调试、错误定位等方面提供了高效帮助,显著缩短了开发周期。但核心算法选择、参数调节(如 LMS 步长、均衡器抽头数)以及结果分析仍需结合对通信原理的理解,AI 工具是放大器而非替代品。

## 8. 参考资料

- 课程课件:第6章 信道编码([06章-信道编码-v2.pdf](course_materials/06章-信道编码-v2.pdf))
- 课程课件:第7章 均衡([07章-均衡.pdf](course_materials/07章-均衡.pdf))
- Hamming, R. W. (1950). Error detecting and error correcting codes. *Bell System Technical Journal*, 29(2), 147-160.
- Widrow, B., & Hoff, M. E. (1960). Adaptive switching circuits. *IRE WESCON Convention Record*, 4, 96-104.
- Proakis, J. G., & Salehi, M. (2008). *Digital Communications* (5th ed.). McGraw-Hill.
83 changes: 73 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) 编码')
reshaped = bits.reshape(-1, 4)
encoded = (reshaped @ HAMMING_G) % 2
return encoded.flatten()


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

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


def hamming74_decode(received):
Expand All @@ -94,8 +94,16 @@ 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)
syndromes = hamming74_syndrome(codewords)
corrected = codewords.copy()
for i, syndrome in enumerate(syndromes):
if np.any(syndrome):
for col in range(7):
if np.array_equal(syndrome, HAMMING_H[:, col]):
corrected[i, col] ^= 1
break
return corrected[:, :4].flatten()


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

# TODO: 选做任务,可参考课件第6章卷积码部分。
raise NotImplementedError('选做:请实现卷积码编码')
bits_padded = np.append(bits, [0, 0])
s1, s2 = 0, 0
encoded = []
for u in bits_padded:
u = int(u)
out1 = (u ^ s1 ^ s2) & 1
out2 = (u ^ s2) & 1
encoded.extend([out1, out2])
s1, s2 = u, s1
return np.array(encoded, dtype=int)


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

# TODO: 选做任务,可使用汉明距离作为路径度量。
raise NotImplementedError('选做:请实现 Viterbi 硬判决译码')
num_pairs = len(received_bits) // 2
n_states = 4

trellis_next = np.zeros((n_states, 2), dtype=int)
trellis_out = np.zeros((n_states, 2, 2), dtype=int)
for state in range(n_states):
s1 = (state >> 1) & 1
s2 = state & 1
for u in (0, 1):
trellis_next[state, u] = (u << 1) | s1
trellis_out[state, u, 0] = u ^ s1 ^ s2
trellis_out[state, u, 1] = u ^ s2

path_metrics = np.full(n_states, np.inf)
path_metrics[0] = 0.0
survivors = []

for step in range(num_pairs):
r = received_bits[2 * step : 2 * step + 2]
new_metrics = np.full(n_states, np.inf)
new_survivor = -np.ones((n_states, 2), dtype=int)

for prev_state in range(n_states):
if np.isinf(path_metrics[prev_state]):
continue
for u in (0, 1):
next_state = trellis_next[prev_state, u]
expected = trellis_out[prev_state, u]
branch = int(expected[0] != r[0]) + int(expected[1] != r[1])
metric = path_metrics[prev_state] + branch
if metric < new_metrics[next_state]:
new_metrics[next_state] = metric
new_survivor[next_state, 0] = prev_state
new_survivor[next_state, 1] = u

path_metrics = new_metrics
survivors.append(new_survivor)

best_state = int(np.argmin(path_metrics))
decoded = []
state = best_state
for step in range(num_pairs - 1, -1, -1):
prev_state = survivors[step][state, 0]
u = survivors[step][state, 1]
decoded.append(u)
state = prev_state

decoded.reverse()
return np.array(decoded[:-2], dtype=int)


def run_coding_demo():
Expand Down
Loading
Loading