2025年11月5日 星期三

16QAM 模擬與星座圖

 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()

    沒有留言:

    張貼留言

    ESP32 (ESP-IDF in VS Code) MFRC522 + MQTT + PYTHON TKinter +SQLite

     ESP32 (ESP-IDF in VS Code) MFRC522 + MQTT + PYTHON TKinter +SQLite  ESP32 VS Code 程式 ; PlatformIO Project Configuration File ; ;   Build op...