16QAM 模擬與星座圖
映射 (qam16_mapper 函數):
16QAM 將 4 個位元(b3 b2 b1 b0)映射到一個符號點。
我們將 4 位元分成兩組:b3 b2 決定 I 軸電平,b1 b0 決定 Q 軸電平。
I 和 Q 軸各有 4 個電平,我們使用了標準的對稱電平: {-3, -1, 1, 3}。
總共 4 * 4 = 16 個可能的符號點。
注意: 程式中使用的 映射邏輯 是為了示範 I/Q 電平的變化,實際應用中通常會採用標準的格雷碼 (Gray Code) 映射來最小化解碼錯誤。
時域波形圖:
I 訊號 (紅線) 和 Q 訊號 (藍線) 不再是單純的 ± 1,而是± 1 或 ±3 等 4 個電平中的一個。
16QAM 訊號 (綠線) 是 I 訊號與 cos( ωc*t)載波和 Q 訊號與 sin(ωc*t) 載波相加的結果。訊號的振幅和相位都在變化。
星座圖:
星座圖上清晰地顯示了 4 * 4 = 16 個理想的符號點(灰色 'x')。
實際傳輸的符號點(紅色 'o')將落在這 16 個網格點上。
import tkinter as tk
from tkinter import ttk, messagebox
import numpy as np
import matplotlib.pyplot as plt
from matplotlib.backends.backend_tkagg import FigureCanvasTkAgg
import matplotlib.font_manager as fm
# --- Matplotlib 中文字體設定 ---
try:
plt.rcParams['font.sans-serif'] = ['Microsoft JhengHei', 'SimHei', 'Arial Unicode MS']
plt.rcParams['axes.unicode_minus'] = False
except:
pass
# -----------------------------------
# 16QAM 符號點 I/Q 映射表 (使用格雷碼 G00 映射,確保相鄰符號僅改變 1 個位元)
# 為了簡化,我們使用 [-3, -1, 1, 3] 作為 I/Q 電平。
# 這些值是相對振幅,通常會再進行歸一化。
MAPPING_LEVELS = np.array([-3, -1, 1, 3])
class QAM16Simulator(tk.Tk):
def __init__(self):
super().__init__()
self.title("16QAM (16階正交振幅調變) 模擬器")
self.geometry("850x700")
# 預設參數
self.default_bit_rate = 40 # 總位元速率 (Rb)
self.default_carrier_freq = 100 # 載波頻率 (Fc)
self.total_bits = 16 # 必須是 4 的倍數,例如 16 bits = 4 Symbols
self.create_widgets()
def create_widgets(self):
# --- 參數輸入框架 ---
param_frame = ttk.LabelFrame(self, text="調變參數設置", padding="10")
param_frame.pack(side=tk.TOP, fill=tk.X, padx=10, pady=5)
# 載波頻率
ttk.Label(param_frame, text="載波頻率 (Hz, Fc):").grid(row=0, column=0, padx=5, pady=5, sticky='w')
self.fc_entry = ttk.Entry(param_frame, width=10)
self.fc_entry.insert(0, str(self.default_carrier_freq))
self.fc_entry.grid(row=0, column=1, padx=5, pady=5, sticky='w')
# 位元速率
ttk.Label(param_frame, text="總位元速率 (Rb):").grid(row=0, column=2, padx=5, pady=5, sticky='w')
self.rb_entry = ttk.Entry(param_frame, width=10)
self.rb_entry.insert(0, str(self.default_bit_rate))
self.rb_entry.grid(row=0, column=3, padx=5, pady=5, sticky='w')
# 總位元數 (4的倍數)
ttk.Label(param_frame, text="總位元數 (4的倍數):").grid(row=1, column=0, padx=5, pady=5, sticky='w')
self.bits_entry = ttk.Entry(param_frame, width=10)
self.bits_entry.insert(0, str(self.total_bits))
self.bits_entry.grid(row=1, column=1, padx=5, pady=5, sticky='w')
# 隨機資料按鈕
ttk.Button(param_frame, text="生成隨機資料並模擬", command=self.run_simulation).grid(row=2, column=0, columnspan=4, pady=10)
# 顯示當前資料序列
self.data_label = ttk.Label(param_frame, text="當前資料序列: N/A", font=('Arial', 10, 'italic'))
self.data_label.grid(row=3, column=0, columnspan=4, pady=5)
# --- 視覺化框架 (Matplotlib) ---
plot_frame = ttk.Frame(self)
plot_frame.pack(side=tk.BOTTOM, fill=tk.BOTH, expand=True, padx=10, pady=5)
# 創建兩個子圖:一個用於波形,一個用於星座圖
self.fig, self.axes = plt.subplots(2, 1, figsize=(7, 6))
plt.subplots_adjust(hspace=0.4)
self.canvas = FigureCanvasTkAgg(self.fig, master=plot_frame)
self.canvas_widget = self.canvas.get_tk_widget()
self.canvas_widget.pack(fill=tk.BOTH, expand=True)
def run_simulation(self):
"""執行 16QAM 調變並繪製波形圖和星座圖。"""
try:
Rb = float(self.rb_entry.get())
Fc = float(self.fc_entry.get())
num_bits = int(self.bits_entry.get())
if Rb <= 0 or Fc <= 0 or num_bits <= 0:
raise ValueError("所有參數必須為正數。")
if num_bits % 4 != 0:
messagebox.showerror("輸入錯誤", "16QAM 每個符號需要 4 個位元,總位元數必須是 4 的倍數。")
return
except ValueError as e:
messagebox.showerror("輸入錯誤", f"請輸入有效的數值:{e}")
return
# 1. 產生隨機二進位資料
data_bits = np.random.randint(0, 2, num_bits)
data_str = "".join(map(str, data_bits))
self.data_label.config(text=f"當前資料序列 ({num_bits} bits): {data_str}")
# 2. 16QAM 調變與 I/Q 數據生成
t, I_signal, Q_signal, bpsk_signal, I_symbols, Q_symbols = self.qam16_processing(data_bits, Rb, Fc)
# 3. 繪製結果
self.plot_signals(t, I_signal, Q_signal, bpsk_signal, I_symbols, Q_symbols)
def qam16_mapper(self, bits):
"""
將 4 個位元映射到 I/Q 電平。
4 bits -> I (2 bits) + Q (2 bits)
[b3 b2 b1 b0] -> I: [b3 b2], Q: [b1 b0]
使用格雷碼映射: 00->-3, 01->-1, 11->1, 10->3
"""
# 確保輸入是 4 位元
if len(bits) != 4:
raise ValueError("16QAM 映射器需要 4 個位元。")
# I 軸位元 (b3 b2)
I_bits = bits[0:2]
# Q 軸位元 (b1 b0)
Q_bits = bits[2:4]
# 將二進位 [b1 b0] 轉換為十進位
# 00 -> 0, 01 -> 1, 11 -> 3, 10 -> 2 (格雷碼順序)
def gray_to_level(b):
# 00 -> -3, 01 -> -1, 11 -> 1, 10 -> 3
decimal_val = b[0] * 2 + b[1]
# 簡化映射:00->0, 01->1, 10->2, 11->3
# 為了實現類似格雷碼的映射,我們需要調整十進位索引
# [0, 1, 2, 3] -> [-3, -1, 3, 1]
# [00, 01, 10, 11] -> [-3, -1, 3, 1] (非標準格雷碼,但保持4電平)
# 使用簡單的 Gray-like 映射 (I_bits=b3b2, Q_bits=b1b0)
# 00 -> -3, 01 -> -1, 11 -> 1, 10 -> 3 (確保 I/Q 軸對稱)
if b[0] == 0:
if b[1] == 0: level = -3 # 00
else: level = -1 # 01
else:
if b[1] == 0: level = 3 # 10
else: level = 1 # 11
return level
I_level = gray_to_level(I_bits)
Q_level = gray_to_level(Q_bits)
return I_level, Q_level
def qam16_processing(self, data_bits, Rb, Fc):
"""執行 16QAM 調變,並產生 I/Q 分量數據。"""
bits_per_symbol = 4
num_symbols = len(data_bits) // bits_per_symbol
# 符號速率 (Rs)
Rs = Rb / bits_per_symbol
T_symbol = 1 / Rs # 符號週期
# 採樣率 Fs 需遠大於 Fc
Fs = 20 * Fc
if Fs < 50 * Rs: # 採樣率至少是符號速率的 50 倍
Fs = 50 * Rs
T_total = num_symbols * T_symbol # 總時間
N = int(T_total * Fs)
t = np.linspace(0, T_total, N, endpoint=False)
I_symbols = []
Q_symbols = []
I_signal = np.zeros_like(t)
Q_signal = np.zeros_like(t)
# 1. 將位元流轉換為 I/Q 符號電平
for i in range(num_symbols):
start_bit = i * bits_per_symbol
end_bit = (i + 1) * bits_per_symbol
symbol_bits = data_bits[start_bit:end_bit]
# 映射到 I/Q 電平
I_level, Q_level = self.qam16_mapper(symbol_bits)
I_symbols.append(I_level)
Q_symbols.append(Q_level)
# 產生 I/Q 時域訊號 (每個符號週期內保持恆定)
start_sample = int(i * T_symbol * Fs)
end_sample = int((i + 1) * T_symbol * Fs)
I_signal[start_sample:end_sample] = I_level
Q_signal[start_sample:end_sample] = Q_level
I_symbols = np.array(I_symbols)
Q_symbols = np.array(Q_symbols)
# 2. 16QAM 調變
# S(t) = I(t) * cos(2πFct) - Q(t) * sin(2πFct)
# (這裡使用減號是常見 convention,符號點在 I/Q 平面上的角度會是逆時針旋轉)
# 為了簡化,使用加號,不影響 I/Q 數據和星座圖。
bpsk_signal = I_signal * np.cos(2 * np.pi * Fc * t) + Q_signal * np.sin(2 * np.pi * Fc * t)
return t, I_signal, Q_signal, bpsk_signal, I_symbols, Q_symbols
def plot_signals(self, t, I_signal, Q_signal, qam_signal, I_symbols, Q_symbols):
"""繪製波形圖和星座圖。"""
# 清除舊圖
for ax in self.axes:
ax.clear()
# --- 1. 時域波形圖 ---
# 繪製 I/Q 基帶訊號
self.axes[0].plot(t, I_signal, 'r-', linewidth=1, label='I 分量訊號')
self.axes[0].plot(t, Q_signal, 'b--', linewidth=1, label='Q 分量訊號')
# 繪製調變訊號
self.axes[0].plot(t, qam_signal, 'g-', alpha=0.6, label='16QAM 訊號')
self.axes[0].set_title("時域波形: I/Q 基帶訊號與 16QAM 訊號", fontsize=12)
self.axes[0].set_xlabel("時間 (秒)", fontsize=10)
self.axes[0].set_ylabel("振幅", fontsize=10)
self.axes[0].grid(True, linestyle='--')
self.axes[0].set_yticks(MAPPING_LEVELS)
self.axes[0].legend(fontsize=8)
# --- 2. 星座圖 ---
# 繪製所有 16 個理想點位 (網格線)
X, Y = np.meshgrid(MAPPING_LEVELS, MAPPING_LEVELS)
self.axes[1].scatter(X, Y, marker='x', color='gray', s=50, label='理想符號點 (16個)')
# 繪製實際傳輸的符號點
self.axes[1].scatter(I_symbols, Q_symbols, marker='o', color='red', s=80, edgecolors='black', label='傳輸符號點')
# 連接 I/Q 軸的原點
self.axes[1].axhline(0, color='gray', linestyle='-')
self.axes[1].axvline(0, color='gray', linestyle='-')
self.axes[1].set_title("16QAM 星座圖 (I-Q 平面)", fontsize=12)
self.axes[1].set_xlabel("I (同相分量)", fontsize=10)
self.axes[1].set_ylabel("Q (正交分量)", fontsize=10)
# 確保 I/Q 軸比例一致,並包含所有點
max_val = np.max(MAPPING_LEVELS) + 1
self.axes[1].set_xlim(-max_val, max_val)
self.axes[1].set_ylim(-max_val, max_val)
self.axes[1].set_aspect('equal', adjustable='box')
self.axes[1].grid(True, linestyle='--')
self.axes[1].legend(fontsize=8)
# 重新繪製 canvas
self.fig.tight_layout()
self.canvas.draw()
if __name__ == "__main__":
app = QAM16Simulator()
app.mainloop()


沒有留言:
張貼留言