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
114 changes: 114 additions & 0 deletions REPORT.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
# 数字调制解调实验报告

## 1. 实验目的

本实验围绕无线通信中的数字基带调制展开,目标是用 Python 实现并验证 BPSK、QPSK 和 16-QAM 三种典型调制方式。通过本实验,我希望完成以下任务:

1. 理解二进制比特到复平面符号的映射关系。
2. 掌握格雷编码在 QPSK 与 16-QAM 星座映射中的作用。
3. 使用 NumPy 完成向量化调制实现,并保证符号能量归一化。
4. 使用 Matplotlib 生成星座图,观察不同调制方式的星座结构。
5. 结合自动化测试和 AI 编程工具,改进代码质量与实验报告表达。

## 2. 实验原理

### 2.1 BPSK

BPSK 是最简单的相移键控方式,每个符号只承载 1 bit 信息。实验中采用的映射为:

```text
0 -> +1
1 -> -1
```

因此 BPSK 的两个星座点位于实轴两端,虚部为 0。它的优点是抗噪声能力强、判决边界简单;缺点是频谱效率较低,因为一个符号只能传输一个比特。

### 2.2 QPSK

QPSK 每个符号承载 2 bit 信息,本实验采用格雷编码映射:

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

除以 `sqrt(2)` 后,每个 QPSK 符号的模长为 1,平均符号功率也为 1。格雷编码保证相邻星座点只相差 1 bit,因此当噪声导致符号落到相邻判决区域时,通常只产生单比特错误。

### 2.3 16-QAM

16-QAM 同时利用幅度和相位传输信息,每个符号承载 4 bit。实验中把 4 bit 分成 I、Q 两组,每组 2 bit,并在两个轴上采用相同的格雷编码电平:

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

最终符号为 `(I + jQ) / sqrt(10)`。其中 `sqrt(10)` 来自 16-QAM 星座的平均能量归一化:单个轴的平均能量为 `(9 + 1 + 1 + 9) / 4 = 5`,I/Q 两轴合计为 10,因此除以 `sqrt(10)` 后平均符号功率约为 1。16-QAM 的频谱效率高于 BPSK 和 QPSK,但星座点间距更小,对噪声更敏感。

## 3. 实验方法与步骤

1. 阅读作业给出的 `grading` 测试文件,确认评分关注的函数名、输入输出长度、星座映射、功率归一化和报告完整性。
2. 在 `src/modulation.py` 中实现三个调制函数:`bpsk_modulate`、`qpsk_modulate` 和 `qam16_modulate`。
3. 对输入比特进行数组化和合法性检查,确保只接受 0/1 比特;QPSK 要求长度为 2 的倍数,16-QAM 要求长度为 4 的倍数。
4. 使用字典明确表示格雷编码映射,避免分支逻辑过多导致映射顺序错误。
5. 调用绘图函数生成星座图;同时让绘图成为辅助行为,避免文件被占用时影响调制数值结果。
6. 运行自动化测试脚本,检查 BPSK、QPSK、16-QAM、环境测试、报告检查和 pylint 代码质量评分。

核心实现片段如下:

```python
_QAM16_LEVELS = {
(0, 0): 3,
(0, 1): 1,
(1, 1): -1,
(1, 0): -3,
}

groups = bit_array.reshape(-1, 4)
in_phase = np.array([_QAM16_LEVELS[tuple(group[:2])] for group in groups])
quadrature = np.array([_QAM16_LEVELS[tuple(group[2:])] for group in groups])
symbols = (in_phase + 1j * quadrature) / np.sqrt(10)
```

## 4. 实验结果

### 4.1 星座图

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

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

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

### 4.2 BER 曲线

![BER 对比曲线](results/ber_comparison.png)

从星座图可以看出,BPSK 只有两个点,判决最简单;QPSK 有四个等能量点,分布在四个象限;16-QAM 有 16 个点,并且不同点的瞬时功率不同,但整体平均功率经过归一化后约为 1。

## 5. 结果分析与讨论

BPSK 的星座点距离较远,在相同信噪比下通常具有最低误码率,但它的传输效率最低。QPSK 在不增加符号率的情况下把每个符号携带的信息量提高到 2 bit,是通信系统中常用的折中方案。16-QAM 每个符号携带 4 bit,频谱效率最高,但由于星座点更密集,噪声稍大时就更容易跨越判决边界。

本实验中的关键细节是能量归一化。如果不对 QPSK 除以 `sqrt(2)`,或者不对 16-QAM 除以 `sqrt(10)`,不同调制方式的平均功率就不一致,后续比较 BER 曲线时会引入不公平因素。格雷编码也是影响误码表现的重要设计:相邻点只差 1 bit,可以降低符号判决错误对比特错误数量的放大效应。

在实现层面,最容易出错的地方是比特分组顺序。例如 16-QAM 中前两位映射到 I 轴,后两位映射到 Q 轴;如果把顺序写反,星座图看起来仍然像 16-QAM,但测试中的具体映射会失败。因此我在实现中使用显式映射表,而不是依赖复杂公式推导。

## 6. 实验心得与 AI Coding 体会

这次作业中,AI 编程工具对我最大的帮助不是直接“替我写完”,而是帮助我快速发现自动评分真正关心的接口和边界条件。通过阅读测试脚本,可以把任务拆成更明确的工程目标:输出长度正确、映射点正确、平均功率正确、异常处理正确、报告格式完整。

我也体会到 AI 生成代码后仍然必须运行测试验证。调制函数看似简单,但文件输出、绘图、编码、pylint 等细节都会影响最终评分。比较有效的协作方式是:先让 AI 帮助梳理测试要求,再由我确认实验原理和报告表达,最后用自动化测试逐项闭环。

另一个收获是,报告不能只写“实现了什么”,还应该解释“为什么这样实现”。例如 16-QAM 为什么要除以 `sqrt(10)`、格雷编码为什么有利于降低比特错误扩散,这些内容比单纯贴代码更能体现对实验的理解。

## 7. 参考资料

1. John G. Proakis, Masoud Salehi, *Digital Communications*, 5th Edition.
2. NumPy 官方文档:https://numpy.org/doc/
3. Matplotlib 官方文档:https://matplotlib.org/stable/
4. GitHub Copilot 文档:https://docs.github.com/en/copilot
1 change: 0 additions & 1 deletion grading/calculate_grade.py
Original file line number Diff line number Diff line change
Expand Up @@ -280,4 +280,3 @@ def calculate_grade():

if __name__ == "__main__":
calculate_grade()

2 changes: 1 addition & 1 deletion grading/check_report.py
Original file line number Diff line number Diff line change
Expand Up @@ -112,4 +112,4 @@ def generate_report_score():

if __name__ == "__main__":
score = generate_report_score()
print(f"\n最终报告得分: {score}")
print(f"\n最终报告得分: {score}")
2 changes: 1 addition & 1 deletion grading/test_bpsk.py
Original file line number Diff line number Diff line change
Expand Up @@ -88,4 +88,4 @@ def test_constellation_file_exists():


if __name__ == "__main__":
pytest.main([__file__, "-v"])
pytest.main([__file__, "-v"])
2 changes: 1 addition & 1 deletion grading/test_qam16.py
Original file line number Diff line number Diff line change
Expand Up @@ -157,4 +157,4 @@ def test_constellation_file_exists():


if __name__ == "__main__":
pytest.main([__file__, "-v"])
pytest.main([__file__, "-v"])
2 changes: 1 addition & 1 deletion grading/test_qpsk.py
Original file line number Diff line number Diff line change
Expand Up @@ -118,4 +118,4 @@ def test_constellation_file_exists():


if __name__ == "__main__":
pytest.main([__file__, "-v"])
pytest.main([__file__, "-v"])
1 change: 1 addition & 0 deletions requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -4,4 +4,5 @@ scipy>=1.7.0
matplotlib>=3.4.0
pytest>=7.0.0
pytest-cov>=3.0.0
pytest-json-report>=1.5.0
pylint>=2.12.0
63 changes: 39 additions & 24 deletions src/demodulation.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,10 +35,9 @@ def bpsk_demodulate(symbols):
[0 1 0]
"""

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

raise NotImplementedError("请实现BPSK解调函数")
# BPSK解调:实部>0判为0,否则判为1
bits = (np.real(symbols) <= 0).astype(int)
return bits


def qpsk_demodulate(symbols):
Expand Down Expand Up @@ -72,21 +71,25 @@ def qpsk_demodulate(symbols):
>>> print(bits) # 应该是 [0, 0, 0, 1]
"""

# 定义QPSK参考星座点(格雷码)
constellation = {
0: (1 + 1j) / np.sqrt(2), # 00
1: (-1 + 1j) / np.sqrt(2), # 01
3: (-1 - 1j) / np.sqrt(2), # 11
2: (1 - 1j) / np.sqrt(2) # 10
# QPSK解调:最小欧氏距离判决
ref_points = [
(1 + 1j) / np.sqrt(2), # 00
(-1 + 1j) / np.sqrt(2), # 01
(1 - 1j) / np.sqrt(2), # 10
(-1 - 1j) / np.sqrt(2) # 11
]
bit_map = {
0: [0, 0],
1: [0, 1],
2: [1, 0],
3: [1, 1]
}

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

raise NotImplementedError("请实现QPSK解调函数")
bits_out = []
for s in symbols:
dists = [np.abs(s - pt) for pt in ref_points]
idx = int(np.argmin(dists))
bits_out.extend(bit_map[idx])
return np.array(bits_out, dtype=int)


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

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

raise NotImplementedError("请实现16-QAM解调函数")
# 16-QAM解调:分别判决I/Q分量
norm = np.sqrt(10)
I = np.real(symbols) * norm
Q = np.imag(symbols) * norm
def gray_decode(x):
if x > 2:
return [0, 0]
elif x > 0:
return [0, 1]
elif x > -2:
return [1, 1]
else:
return [1, 0]
bits_out = []
for i, q in zip(I, Q):
bits_out.extend(gray_decode(i))
bits_out.extend(gray_decode(q))
return np.array(bits_out, dtype=int)


def test_demodulation():
Expand Down
Loading
Loading