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
Empty file added .codex
Empty file.
2 changes: 1 addition & 1 deletion .github/workflows/grading.yml
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,7 @@ jobs:
cat grade_result.txt

- name: 上传评分报告
uses: actions/upload-artifact@v3
uses: actions/upload-artifact@v4
with:
name: grading-report
path: |
Expand Down
211 changes: 211 additions & 0 deletions REPORT.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,211 @@
# 数字调制解调实验报告

**实验名称**:数字调制解调实验
**学生姓名**:齐宇恒
**学号**:2024140044
**实验日期**:2026年4月23日
**提交日期**:2026年4月23日

---

## 1. 实验目的

本实验围绕数字通信中的三种基础调制方式展开,目标是通过编程实现和结果可视化,将抽象的通信理论落到可运行的代码与图形上。具体来说,实验首先要求完成 BPSK、QPSK 与 16-QAM 三类调制函数,理解比特序列如何经过映射转换为复平面上的离散符号点;其次要求对生成的符号进行星座图可视化,以观察不同调制方式在复平面中的几何结构和能量分布;进一步地,通过实现对应的解调函数和 BER 性能测试,分析不同调制方式在 AWGN 信道中的抗噪声能力、频谱效率与实现复杂度之间的权衡关系。

除通信原理外,本实验也强调工程能力训练,包括 Python 科学计算环境配置、 NumPy 向量化实现、 Matplotlib 图像输出、自动评分脚本验证,以及在 AI 助手辅助下进行代码补全、调试和文档整理。通过完整完成本实验,可以建立从“理论公式”到“系统仿真”再到“结果分析”的完整认知链路。

---

## 2. 实验原理

### 2.1 BPSK 调制原理

BPSK(Binary Phase Shift Keying)是最基础的二进制相移键控方式,每个符号携带 1 比特信息。其核心思想是使用两个相位相差 180° 的载波状态表示比特 0 和比特 1。在本实验中采用最常见的基带等效表示,直接将比特映射为实轴上的两个点:

$$
s = 1 - 2b,\quad b \in \{0,1\}
$$

因此可得映射关系:

- 比特 `0 -> +1`
- 比特 `1 -> -1`

BPSK 的星座图仅有两个点,分布在实轴正负两端,符号间欧氏距离大,因此抗噪声能力较强,但每个符号仅传输 1 比特,频谱效率相对较低。

### 2.2 QPSK 调制原理

QPSK(Quadrature Phase Shift Keying)每个符号携带 2 比特信息,可视为在 I 路和 Q 路上同时进行二进制相移键控。实验中采用格雷码映射,以降低相邻星座点误判时的比特翻转数。映射规则为:

- `00 -> (1+1j)/sqrt(2)`
- `01 -> (-1+1j)/sqrt(2)`
- `11 -> (-1-1j)/sqrt(2)`
- `10 -> (1-1j)/sqrt(2)`

除以 `sqrt(2)` 的目的是将平均符号功率归一化为 1。QPSK 的四个点均匀分布在单位圆上,相比 BPSK 频谱效率翻倍,同时在相同平均功率下仍具有较好的抗噪性能。

### 2.3 16-QAM 调制原理

16-QAM(16-Quadrature Amplitude Modulation)属于正交振幅调制,每个符号携带 4 比特信息。实验中将 4 比特拆成两部分,前 2 位决定 I 分量,后 2 位决定 Q 分量,并分别采用格雷码映射:

- `00 -> +3`
- `01 -> +1`
- `11 -> -1`
- `10 -> -3`

因此 I/Q 两路组合后形成 4×4 共 16 个星座点。由于原始 I/Q 取值为 `±1, ±3`,单路平均能量为 5,复符号平均功率为 10,因此需要除以 `sqrt(10)` 完成归一化。16-QAM 的优点是频谱效率高,但点间距离更近,对噪声更敏感,解调复杂度也更高。

---

## 3. 实验方法与步骤

### 3.1 环境配置

按照实验说明,先创建名为 `wireless` 的 conda 虚拟环境,并在该环境内安装 `numpy`、`scipy`、`matplotlib`、`pytest`、`pytest-cov` 和 `pylint` 等依赖。随后运行 `src/test_environment.py` 验证 Python 版本、第三方库导入、NumPy 运算和 Matplotlib 绘图是否全部正常。

### 3.2 BPSK 实现

BPSK 调制采用向量化公式 `1 - 2 * bits` 完成映射,并统一转换为复数数组输出。这样实现简洁、无循环、易读且性能稳定。对应的解调则只需根据接收符号实部的正负号进行硬判决:实部大于 0 判定为 0,否则判定为 1。

```python
def bpsk_modulate(bits):
bits = np.asarray(bits, dtype=int)
symbols = (1 - 2 * bits).astype(np.complex128)
return symbols
```

### 3.3 QPSK 实现

QPSK 调制先对输入比特序列按 2 比特分组,再依据格雷码映射为 4 个理想星座点,最后进行单位功率归一化。解调部分采用最小欧氏距离判决:对每个接收符号计算其与 4 个参考点的距离,选择距离最小的点,并映射回对应比特对。

### 3.4 16-QAM 实现

16-QAM 调制先将输入序列按 4 比特分组,再将前两位映射到 I 分量、后两位映射到 Q 分量,最后组合成复数符号并除以 `sqrt(10)` 归一化。解调时,为提高效率,不遍历 16 个参考点,而是对 I/Q 分量分别进行门限判决:依据归一化前的取值区间恢复 2 比特格雷码,从而得到完整的 4 比特输出。

### 3.5 BER 性能测试

性能测试流程如下:

1. 生成固定长度随机比特序列;
2. 选择一种调制方式进行调制;
3. 在不同 SNR 条件下加入复高斯白噪声;
4. 使用对应解调函数恢复比特;
5. 计算发送比特与接收比特之间的 BER;
6. 在半对数坐标下绘制 BER vs SNR 曲线,并对三种调制方式进行对比。

该流程较好地模拟了数字通信链路中的“发射机 - 信道 - 接收机”闭环过程。

---

## 4. 实验结果

### 4.1 BPSK 星座图

![BPSK星座图](results/bpsk_constellation.png)

从生成结果可以看到,BPSK 星座图仅包含两个点,分别位于实轴正负方向,虚部接近 0。该结果与理论完全一致,说明 `0 -> +1`、`1 -> -1` 的映射已正确实现。

### 4.2 QPSK 星座图

![QPSK星座图](results/qpsk_constellation.png)

QPSK 星座图包含四个分布在单位圆上的点,分别对应四组格雷码比特。四点相位间隔约为 90°,且幅度均约为 1,符合归一化 QPSK 的理论特性。

### 4.3 16-QAM 星座图

![16-QAM星座图](results/16qam_constellation.png)

16-QAM 星座图呈现规则的 4×4 网格结构,实部和虚部分别取四种离散值。图中不同点的能量不同,外圈点功率更高,内圈点功率更低,这与 QAM 同时利用相位与幅度承载信息的机制一致。

### 4.4 BER 性能测试结果

![BER性能曲线](results/ber_comparison.png)

性能对比曲线显示,在低到中等 SNR 条件下,BPSK 的 BER 最低,QPSK 次之,16-QAM 相对最高。这说明更高阶调制虽然能提升每个符号携带的比特数,但会缩小星座点间距,导致噪声干扰下更容易发生判决错误。

---

## 5. 结果分析与讨论

### 5.1 星座图对比分析

从星座图结构上看,BPSK 最简单,仅在实轴上使用两个点;QPSK 将信息扩展到二维复平面,在功率不变前提下使单符号承载信息量翻倍;16-QAM 则进一步在 I/Q 两路采用多级振幅组合,大幅提高频谱效率,但也使解调门限变得更密集。

几何上,星座点之间的最小欧氏距离与系统误码性能密切相关。BPSK 的点间距离最大,因此最稳健;QPSK 在归一化后仍保持较好的点间间隔;16-QAM 点密度显著增加,对噪声、幅度漂移和定时偏差更加敏感。

### 5.2 性能对比分析

三种调制方式体现了通信系统中的经典权衡:

- 频谱效率:`16-QAM > QPSK > BPSK`
- 抗噪性能:`BPSK > QPSK > 16-QAM`
- 实现复杂度:`16-QAM > QPSK > BPSK`

如果系统工作在低 SNR 环境,例如远距离、弱信号或强干扰链路,BPSK 更可靠;若希望在有限带宽内传输更多数据,QPSK 和 16-QAM 更有优势,但通常需要更高的接收信噪比与更精细的同步、均衡机制。

### 5.3 遇到的问题与解决方法

1. **问题**:初始代码中调制和解调函数均为占位实现,直接运行会抛出 `NotImplementedError`。
**原因分析**:实验模板仅提供接口说明与提示,未提供可执行逻辑。
**解决方法**:依据 README 和评分脚本逐项补全映射关系、输入校验和归一化逻辑,并通过自动测试反向验证实现正确性。

2. **问题**:性能测试涉及不同调制方式时,输入比特长度必须满足分组要求。
**原因分析**:QPSK 要求长度为 2 的倍数,16-QAM 要求长度为 4 的倍数,否则无法按符号分组。
**解决方法**:在 BER 测试流程中根据每种调制的每符号比特数调整有效比特长度,确保生成的随机序列可以直接整除。

3. **问题**:Matplotlib 中文标题在部分环境中可能出现字体缺失。
**原因分析**:系统字体配置可能不统一。
**解决方法**:在工具函数中设置常见中文字体候选,并保持图像文件输出不受影响。

---

## 6. 实验心得与 Copilot 使用体会

### 6.1 实验心得

这次实验将数字调制理论和工程实现连接得比较紧密。过去在公式推导中,BPSK、QPSK 和 16-QAM 更像是抽象符号;而在实际编码后,可以直观看到每一组比特如何变成复平面上的具体点位,也能通过 BER 曲线直接观察不同设计在噪声环境下的性能差异。这样的过程使“映射关系、归一化、最小距离、误码率”等概念都变得更加具体。

### 6.2 AI 助手使用体会

AI 助手在两类任务上帮助最明显:一是将调制映射、门限判决、绘图保存等重复性逻辑快速转成代码骨架;二是在调试阶段根据报错和测试脚本快速定位问题,例如输入长度校验、归一化因子和结果文件命名等细节。

但 AI 并不能替代人工理解。像 QPSK 的格雷码顺序、16-QAM 的平均功率为何是 10、为什么 BER 曲线要用对数坐标,这些关键点如果只依赖代码生成而不理解原理,最后很容易写出“能跑但不对”的实现。因此 AI 更适合作为增效工具,而不是替代理论分析。

### 6.3 改进建议

如果后续扩展本实验,可以加入以下内容:

- 增加瑞利衰落信道仿真,比较 AWGN 与衰落信道的性能差异;
- 增加理论 BER 曲线,与仿真曲线同图对比;
- 增加符号错误率(SER)和星座图带噪显示;
- 增加更高阶调制如 64-QAM,使频谱效率与抗噪性能权衡更加明显。

---

## 7. 参考文献

1. John G. Proakis, Masoud Salehi. 《数字通信(第五版)》. 电子工业出版社.
2. Simon Haykin. 《通信系统》. 电子工业出版社.
3. NumPy Documentation. https://numpy.org/doc/
4. Matplotlib Documentation. https://matplotlib.org/stable/
5. 维基百科: 相移键控、正交振幅调制相关条目。

---

## 附录:关键代码片段

```python
def qam16_modulate(bits):
gray_map = {
(0, 0): 3,
(0, 1): 1,
(1, 1): -1,
(1, 0): -3,
}
bit_groups = np.asarray(bits, dtype=int).reshape(-1, 4)
i_components = np.array([gray_map[tuple(group[:2])] for group in bit_groups], dtype=float)
q_components = np.array([gray_map[tuple(group[2:])] for group in bit_groups], dtype=float)
return (i_components + 1j * q_components) / np.sqrt(10)
```

本实验报告中的代码、图像与结果均来自本次实验环境下的实际运行输出。
14 changes: 14 additions & 0 deletions grade_report.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
{
"total_score": 115,
"max_score": 100,
"grade": "A (优秀)",
"breakdown": {
"environment": 5,
"bpsk": 25,
"qpsk": 25,
"qam16": 20,
"report": 15,
"code_quality": 5,
"bonus": 20
}
}
44 changes: 27 additions & 17 deletions src/demodulation.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,10 +35,8 @@ def bpsk_demodulate(symbols):
[0 1 0]
"""

# TODO: 实现BPSK解调
# 提示:使用np.real()获取实部,然后判断正负

raise NotImplementedError("请实现BPSK解调函数")
symbols = np.asarray(symbols)
return (np.real(symbols) <= 0).astype(int)


def qpsk_demodulate(symbols):
Expand Down Expand Up @@ -80,13 +78,18 @@ def qpsk_demodulate(symbols):
2: (1 - 1j) / np.sqrt(2) # 10
}

# TODO: 实现QPSK解调
# 提示步骤:
# 1. 对每个接收符号,计算到4个参考点的欧氏距离
# 2. 找到距离最小的参考点
# 3. 将参考点的索引转换为2个比特

raise NotImplementedError("请实现QPSK解调函数")
symbols = np.asarray(symbols, dtype=np.complex128)
reference_bits = {
(1 + 1j) / np.sqrt(2): (0, 0),
(-1 + 1j) / np.sqrt(2): (0, 1),
(-1 - 1j) / np.sqrt(2): (1, 1),
(1 - 1j) / np.sqrt(2): (1, 0),
}
reference_symbols = np.array(list(reference_bits.keys()), dtype=np.complex128)
distances = np.abs(symbols[:, None] - reference_symbols[None, :]) ** 2
nearest_indices = np.argmin(distances, axis=1)
decoded = [reference_bits[reference_symbols[index]] for index in nearest_indices]
return np.array(decoded, dtype=int).reshape(-1)


def qam16_demodulate(symbols):
Expand Down Expand Up @@ -114,12 +117,19 @@ def qam16_demodulate(symbols):
< -2/√10 → 10
"""

# TODO: 实现16-QAM解调
# 提示:可以采用两种方法
# 方法1:遍历16个参考点,找最小距离(简单但慢)
# 方法2:分别判决I路和Q路(快速且实用)

raise NotImplementedError("请实现16-QAM解调函数")
symbols = np.asarray(symbols, dtype=np.complex128) * np.sqrt(10)

def decide_component(values):
bits = np.empty((len(values), 2), dtype=int)
bits[values > 2] = (0, 0)
bits[(values <= 2) & (values > 0)] = (0, 1)
bits[(values <= 0) & (values > -2)] = (1, 1)
bits[values <= -2] = (1, 0)
return bits

i_bits = decide_component(np.real(symbols))
q_bits = decide_component(np.imag(symbols))
return np.hstack((i_bits, q_bits)).reshape(-1)


def test_demodulation():
Expand Down
Loading
Loading