一、简介

        本设计基于ALINX ZYNQ7010核心板,DAC904和AD9226模块,采用verilog语言实现AM、ASK、FM、FSK、PSK调制信号的解调和对应参数的估计,同时含有部分框图和步骤解答,相信对电赛辅导和指导毕业设计有一定的帮助。本文只粘贴必要代码,不提供源文件,目的为培养大家的自我探知能力,需要源文件需私信我,同时IP核的配置我也是从各大平台自学的,就不一一赘述,本文只贴出配置参数,不含具体配置步骤,不会配置的请借鉴其他帖子经验。 

        本设计的设计目标:

        输入信号分别为AM/FM/ASK/FSK/PSK,所有信号的载频均为1MHz,幅度为10Vpp。设计的具体要求如下:

(1)AM 信号,其正弦调制信号频率 F =1-5kHz。Ma为0.3-0.8,测量并显示该 AM 信号的调幅系数Ma,误差不大于0.1 ;输出解调信号Uo 无明显失真,幅度大于200mV。

(2)FM 信号,其正弦调制信号频率 F =1-5kHz,Mf为2-5,估计并显示该 FM波的调频系数Mf ,误差不大于0.3 ;输出解调信号Uo无明显失真,幅度大于200mV。

(3)ASK/FSK/PSK信号,三者均传输二进制不归零(NRZ)码,码速率 Rc 可为 6kbps、8kbps、10kbps 中的一种;要求装置能够自主识别并显示Um的键控方式,并输出解调后的波形。

        总模块框图如下:

二、FPGA的数据类型简介

       在 FPGA 设计中,所有数据均以二进制形式流转。数据位宽的定义需综合考量算法动态范围及接口器件的物理限制,比如本设计用到的DAC904为14bit(16384深度),就需要开辟[13:0]的位宽,对应模块的14个数据引脚的输出,而AD9226为12bit(4096深度)则需开辟[11:0]的位宽,对应模块的12个数据引脚的输出。此外,在多模块级联传输过程中,由于不同模块对数据精度的要求不同,必须严格进行位宽匹配管理。这通常涉及截位处理以防止数据溢出,或符号位扩展以保留运算精度。合理的位宽规划与截位策略,是保障系统信噪比(SNR)与运算效率的关键。

三、时钟(Clocking Wizard IP核)

        时钟是FPGA系统的核心。为避免亚稳态并确保数据可靠传输,设计上优先保证各模块时钟同源;对于异步时钟域,则采用FIFO进行缓存处理。针对DAC904与AD9226器件间的频率差异,本设计采取降频策略,将DAC904的工作频率调整至与AD9226一致的65MHz,从而实现全局时钟同步。由于开发板仅提供125MHz板载时钟,因此需利用分频器从中提取所需的65MHz工作时钟。

        本设计的Clocking Wizard IP核具体配置参数如下:

四、AM/ASK解调模块

1.总模块设计

本设计的AM/ASK解调采用包络解调的方法,具体为:

        AM/ASK解调模块的源码为:

module AM_Demodulation(
    input  wire                clk_65m,
    input  wire                rst_n,
    input  wire signed [11:0]  data,        
    output wire [13:0]  AM_ASK_data
);
    // ================= 包络提取部分 =================
    wire signed [11:0] abs12 = data[11] ? (~data + 1'b1) : data;
    wire signed [15:0] abs16 = {abs12, 4'b0000};
    wire signed [15:0] lpf_out16;

    AM_LPF envelope_filter (
        .clk(clk_65m),
        .clk_enable(1'b1),
        .reset(rst_n),
        .filter_in(abs16),     
        .filter_out(lpf_out16)    
    );

//    assign env_out_16 = lpf_out16;
    //================= 输出波形逻辑 =================
    reg  signed [13:0] AM_ASK_data_reg;
    wire signed [13:0] env_for_dac = lpf_out16[15:2];  

    always @(posedge clk_65m or negedge rst_n) begin
        if (!rst_n) begin
            AM_ASK_data_reg <= 14'sd0;
        end else begin
            AM_ASK_data_reg <= env_for_dac;
        end
    end
    wire signed [13:0] AM_gain = 14'd20;

    wire signed [13:0] AM_ASK_data_reg_out = AM_ASK_data_reg << 4;

    assign AM_ASK_data = {~AM_ASK_data_reg_out[13], AM_ASK_data_reg_out[12:0]}-14'd3300;

//ila_0 AM (
//	.clk(clk_65m), // input wire clk


//	.probe0(data), // input wire [11:0]  probe0  
//	.probe1(env_out_16) // input wire [15:0]  probe1
//);
endmodule

2.整流处理

整流处理其实非常简单,即取平均值然后补高位到滤波器的输入位宽即可。

    wire signed [11:0] abs12 = data[11] ? (~data + 1'b1) : data;
    wire signed [15:0] abs16 = {abs12, 4'b0000};

3.低通滤波器设计  

      其中,AM_LPF选用1阶低通IIR巴特沃斯滤波器,采样频率为65000kHz,截止频率为100kHz,具体参数由matlab的fdatool工具生成,具体配置已贴出:

        注意:要设置为直接Ⅱ型转置,二阶节,点击设计滤波器,得到滤波器后点击上方编辑->转换结构->Direct-Form Ⅱ Transposed,SOS,直到滤波器结构转换完成,点击左下角设置量化参数,滤波器算法设为定点,最后点击应用,直到量化完成后再点击上方目标->生成HDL...,打开滤波器生成工具,language选verilog,在Global Settings里的Reset asserted level设为Active-low(因为本系统所有复位都是低电平有效),然后选择滤波器生成的位置,找到对应的.v文件即可

 五、VIVADO ROM IP核

1.ROM IP核

        ROM 是只读存储器(Read-Only Memory)的简称,是一种只能读出事先所存数据的固态半导体存储器。其特性是一旦储存资料就无法再将之改变或删除,且资料不会因为电源关闭而消失。而事实上在 FPGA 中通过 IP 核生成的 ROM 或 RAM 调用的都是 FPGA 内部的 RAM 资源,掉电内容都会丢失,用 IP 核生成的 ROM 模块只是提前添加了数据文件(.coe 格式),在 FPGA 运行时通过数据文件给 ROM 模块初始化。https://blog.csdn.net/yishuihanq/article/details/130767125

2.波形数据

        在基于 DAC904 的正弦波发生架构中,数字域的离散特性决定了模拟输出的台阶效应。由于 DAC904 为 14bit 分辨率,其数字输入码可表示2^{14}=16384个量化等级。结合其 ±5V 的基准量程(峰峰值 10V),系统的最小量化步长(LSB)计算如下:

LSB=\frac{V_{max}-V_{min}}{2^{14}}=\frac{10V}{16384}\approx 0.61035 mV

        这意味着,FPGA 每输出一个递增的数字码,模拟电压仅改变约 0.6mV。一个标准的满幅正弦波,其波峰对应 +5V(数字码 16383),波谷对应 -5V(数字码 0 或补码形式),整个周期内将包含 16384 个离散的电平采样点。

        波形数据的产生我用的忘记是谁开发的软件了,如有侵权请联系,用matlab直接生成coe文件也可,需注意数据集是有符号类型还是无符号类型。
链接: https://pan.baidu.com/s/1XYoQWVRFX-wA8fMdy-slvA?pwd=1111 提取码: 1111 

3.为什么要用ROM IP核

        我们一般用Matlab提前生成需要的波形数据(一般为满幅波形,即与深度相等),存储到coe文件中,在ROM核中进行对应的配置,就可以将波形数据存放到程序之中,之后就可调用了。

4.ROM IP核的配置

六、FM/FSK解调模块

1.总模块设计

 该模块采用数字锁相鉴频环路结构,主要包括数控振荡器、鉴相器、低通滤波器以及环路滤波器等部分。

        数控振荡器模块:由ROM IP核和相位累加器构成,通过输入的频率控制字和相位控制字控制ROM IP核输出不同频率和相位的余弦参考信号。

        鉴相器模块:由乘法器实现,输入信号与本地余弦信号进行相乘运算,产生鉴相器输出,该输出包括低频误差以及两倍频分量,在程序中先对乘法的结果取高位,然后送入滤波器滤去高频部分,只留下相位误差。

        低通滤波器模块:依旧1阶低通IIR巴特沃斯滤波器,采样频率为65MHz,截止频率为100kHz。

        环路滤波器模块:经过低通滤波后相位误差进入环路滤波器模块,产生频率控制量,此频率控制量同时用于NCO频偏调整以及作为FM/FSK解调结果输出,具体参数(C1、C2)需要根据实际情况进行调节。

module FM_demodulation(
    input clk,
    input rst_n,
    input signed [11:0] data,    
    output [13:0] FM_FSK_data 
    );

// 频率控制字定义 
// 65MHz采样率下, 1MHz = (1e6/65e6) * 2^32 ≈ 66076419
localparam [31:0] PHASE_STEP_1MHZ = 32'd66076419;  // 中心频率 1MHz
localparam [31:0] INITIAL_FREQ = PHASE_STEP_1MHZ;  // 初始频率1MHz

// PLL相关信号
wire [11:0] phase_addra;
wire signed [11:0] cos_out;
wire signed [23:0] pd_mult_out;         // 鉴相器(乘法器)输出
wire signed [31:0] freq_control;        // 环路滤波器输出(频率控制字)
wire signed [15:0] lpf_out_full;        // 低通滤波器输出
wire signed [11:0] phase_error;         // 截位后的相位误差
// ================= 环路滤波器模块 =================
// 接收鉴相误差,输出频率控制字控制NCO
loop_filter_FM loop_filter_inst (
    .clk(clk),
    .rst_n(rst_n),
    .theta(phase_error),      // 输入相位误差
    .delta(freq_control)      // 输出频率调整量 (此信号即为FM解调结果)
);

// ================= NCO (数控振荡器) =================
PhaseAccumulator nco (
    .CLK(clk), 
    .RSTn(rst_n),
    .PhaseOffset(12'd0),
    .PhaseStep(INITIAL_FREQ + $unsigned(freq_control)), // 跟踪输入信号频率
    .MemAddr(phase_addra)
);

// 产生本地载波 (FM PLL只需要一路余弦或正弦即可完成鉴相)
rom_sin_12bit cos_rom (
    .clka(clk),
    .addra(phase_addra + 12'd1024),  // cos相位偏移
    .douta(cos_out)
);

// 将 ROM 输出的无符号偏移二进制码转为有符号二进制补码
wire signed [11:0] cos_out_signed = {~cos_out[11], cos_out[10:0]};

// ================= 鉴相器 (Phase Detector) =================
// 输入FM信号与本地载波相乘,产生包含相位差的高频与低频分量
mult_gen_0 pd_multiplier (
    .CLK(clk),
    .A(data),  
    .B(cos_out_signed),      // 本地载波
    .SCLR(~rst_n),  // input wire SCLR
    .P(pd_mult_out)          // 鉴相输出
);

// ================= 低通滤波器 (LPF) =================
// 滤除乘法器产生的高频分量 (2倍频),保留低频的相位误差分量
wire signed [15:0] lpf_in = pd_mult_out[22:7];

LPF_100k loop_lpf (
    .clk(clk),
    .clk_enable(1'b1),  
    .reset(rst_n),     
    .filter_in(lpf_in),      
    .filter_out(lpf_out_full)      
);

// ================= 相位误差截位 =================
// 将LPF输出截位匹配 loop_filter 的输入位宽 (12bit)
assign phase_error = lpf_out_full[15:4]; 

// ================= 解调输出 =================
wire signed [11:0] fm_demod = freq_control[22:11];
//assign fm_demod_out = fm_demod; 
/**********************************输出波形逻辑*************************************************/
reg signed [13:0] FM_FSK_data_reg;

    always @(posedge clk or negedge rst_n) begin
        if (!rst_n) begin
            FM_FSK_data_reg <= 14'd0;
        end else begin
            // 注意:这里输入已经是 14-bit (FM_FSK_in),你之前的注释写的是 12->14
            // 这里保持你原来的赋值:直接透传 14 位
            FM_FSK_data_reg <= fm_demod; 
        end
    end
    wire signed [13:0] gain = 14'd6;
    wire signed [13:0] FM_FSK_data_reg_out = FM_FSK_data_reg << 2;
    assign FM_FSK_data = {~FM_FSK_data_reg_out[13], FM_FSK_data_reg_out[12:0]};
//ila_0 FM (
//	.clk(clk), // input wire clk


//	.probe0(data), // input wire [11:0]  probe0  
//	.probe1(fm_demod_out) // input wire [11:0]  probe1
//);
endmodule

2.分模块说明       

相位累加器(数控振荡器)

         这里用到了上一章节提到的ROM IP核,与相位累加器相配合可以输出需要的任意频率和相位的波形,由于FM/FSK解调需要先产生本地的参考载波进行相乘,故引入相位累加器PhaseAccumulator模块。

/*
输入信号:
CLK,
RSTn,
PhaseStep: 相位步进字
PhaseOffset: 相位偏移量

输出信号:
MemAddr:存储器地址,
*/

module PhaseAccumulator#(
    parameter PhaseAcc_BITS_NUM = 32,
    parameter Mem_BITS_NUM  = 12,
    parameter PhaseOffset_BITS_NUM = 12
)(
     CLK, 
	 RSTn,
	 PhaseStep,
	 PhaseOffset,           // 新增:相位偏移输入
	 MemAddr
);
 input CLK;
 input RSTn;
 input [PhaseAcc_BITS_NUM-1:0] PhaseStep;
 input [PhaseOffset_BITS_NUM-1:0] PhaseOffset;  // 新增相位偏移输入
 output [Mem_BITS_NUM-1:0] MemAddr;
 
reg [PhaseAcc_BITS_NUM-1:0] PhaseSum;
wire [Mem_BITS_NUM-1:0] PhaseHighBits;  // 相位累加器的高位
wire [Mem_BITS_NUM:0] AddResult;         // 带进位的加法结果

always @ ( posedge CLK or negedge RSTn)
if( !RSTn ) begin
	PhaseSum <= 32'd0;
end
else
begin
	PhaseSum <= PhaseSum + PhaseStep;
end

// 取相位累加器高Mem_BITS_NUM位
assign PhaseHighBits = PhaseSum[PhaseAcc_BITS_NUM-1:PhaseAcc_BITS_NUM-Mem_BITS_NUM];

// 将相位偏移加到相位高位上
assign AddResult = PhaseHighBits + PhaseOffset;

// 取低Mem_BITS_NUM位,允许自动回绕
assign MemAddr = AddResult[Mem_BITS_NUM-1:0];

endmodule

低通滤波器

        依旧1阶低通IIR巴特沃斯滤波器,采样频率为65MHz,截止频率为100kHz,见AM/ASK低通滤波器配置部分。

环路滤波器

        这个模块具体算法逻辑跟PID类似,公式太多就不放推导了,需要推导的各位可以复制粘贴给AI

module loop_filter_FM #(
    // 滤波器参数配置 (根据实际系统需求调整)
    parameter signed [31:0] C1 = 32'd80000, // 比例增益 (Proportional Gain)
    parameter signed [31:0] C2 = 32'd100    // 积分增益 (Integral Gain)
)(
    input  wire                   clk,
    input  wire                   rst_n,
    input  wire signed [11:0]     theta,  // 鉴相器输出的相位误差
    output wire signed [31:0]     delta   // 环路滤波器输出的控制字 (NCO频率控制字偏移)
);

    // 内部寄存器与连线定义
    reg signed [31:0] integral_acc; // 积分累加器
    reg signed [31:0] delta_reg;    // 输出寄存器

    // 1. 符号位扩展:将 12-bit 有符号数安全扩展为 32-bit,防止后续运算溢出
    wire signed [31:0] theta_ext;
    assign theta_ext = {{20{theta[11]}}, theta};

    // 2. 乘法运算 (通常综合工具会自动映射为 DSP block)
    wire signed [31:0] prop_term;
    wire signed [31:0] integ_term;
    assign prop_term  = theta_ext * C1;
    assign integ_term = theta_ext * C2;

    // 3. 时序逻辑:积分累加与输出计算
    always @(posedge clk or negedge rst_n) begin
        if (!rst_n) begin
            integral_acc <= 32'd0;
            delta_reg    <= 32'd0;
        end else begin
            // 积分路径累加:I[n] = I[n-1] + C2 * theta[n]
            integral_acc <= integral_acc + integ_term;
            
            // 比例路径 + 积分路径:为了优化时序,这里直接使用上一拍的积分值
            // delta[n] = P[n] + I[n-1]
            delta_reg    <= prop_term + integral_acc; 
        end
    end

    // 4. 驱动输出端口
    assign delta = delta_reg;

endmodule

鉴相器(乘法器)

       由乘法器实现,输入信号与本地余弦信号进行相乘运算,产生鉴相器输出,该输出包括低频误差以及两倍频分量,在程序中先对乘法的结果取高位,然后送入滤波器滤去高频部分,只留下相位误差。

        具体配置如下:

七、PSK解调模块

        该模块其实非常简单,我做程序的时候是先实现的PSK的解调又在该解调模块的基础上实现的FM/FSK的解调,大家看这个框图就可以发现与FM/FSK的解调模块存在很多类似的部分。

        但是在调试的过程中,我发现当信号源在FSK、FM、PSK几个信号之间轮换的时候会出现锁相环锁不到的情况,故又给加入状态机,使压控振荡器不会跑飞。

        具体代码如下:

module PSK_demodulation(
    input clk_65m,
    input rst_n,
    input signed [11:0] data,
    output wire PSK_valid,
    output signed [13:0] PSK_data_wire
    );

// ===== 频率控制字 =====
localparam [31:0] PHASE_STEP_1MHZ = 32'd66076419;
localparam [31:0] INITIAL_FREQ    = PHASE_STEP_1MHZ;

// ===== 锁定迟滞阈值 =====
// 上门限:超过此值判定为PSK已锁定
// 下门限:低于此值判定为失锁,触发复位重捕
localparam signed [31:0] LOCK_HIGH = 32'sd10_000;
localparam signed [31:0] LOCK_LOW  = 32'sd2_000;

// 复位保持周期(512周期 ≈ 7.9us @65MHz)
localparam [9:0] RESET_HOLD = 10'd512;

// ==============================================================
// ================= 自动状态机 =================================
// ==============================================================
// 状态定义
localparam S_RESET    = 2'd0;  // 环路复位中(刚上电 / 失锁后)
localparam S_SEARCH   = 2'd1;  // 搜索中(环路正常运行,等待锁定)
localparam S_LOCKED   = 2'd2;  // 已锁定

reg [1:0]  state;
reg [9:0]  reset_cnt;
reg        loop_rst_n_r;  // 受控的环路复位信号

// psk_lock_acc 前向声明(需要在状态机中读取)
reg signed [31:0] psk_lock_acc;

always @(posedge clk_65m or negedge rst_n) begin
    if (!rst_n) begin
        state        <= S_RESET;
        reset_cnt    <= RESET_HOLD;
        loop_rst_n_r <= 1'b0;
    end else begin
        case (state)

            // ---- 状态0:复位保持 ----
            S_RESET: begin
                loop_rst_n_r <= 1'b0;
                if (reset_cnt > 10'd0) begin
                    reset_cnt <= reset_cnt - 1'b1;
                end else begin
                    // 复位倒计时结束,进入搜索态
                    state     <= S_SEARCH;
                    reset_cnt <= RESET_HOLD;
                end
            end

            // ---- 状态1:搜索(自由捕获)----
            S_SEARCH: begin
                loop_rst_n_r <= 1'b1;  // 释放环路
                if (psk_lock_acc > LOCK_HIGH) begin
                    state <= S_LOCKED;  // 锁定成功
                end
            end

            // ---- 状态2:已锁定 ----
            S_LOCKED: begin
                loop_rst_n_r <= 1'b1;
                if (psk_lock_acc < LOCK_LOW) begin
                    // 失锁(切到FSK或信号消失),触发复位重捕
                    state        <= S_RESET;
                    reset_cnt    <= RESET_HOLD;
                    loop_rst_n_r <= 1'b0;
                end
            end

            default: state <= S_RESET;
        endcase
    end
end

// 环路滤波器实际复位 = 系统复位 OR 状态机控制复位
wire loop_rst_n_sig = rst_n & loop_rst_n_r;

// 鉴相误差门控:仅在SEARCH/LOCKED状态下向环路滤波器输出有效误差
// RESET状态下输入0,冻结积分器防止乱积
wire signed [11:0] phase_error;
wire signed [11:0] phase_error_gated = loop_rst_n_r ? phase_error : 12'sd0;

// ==============================================================
// ================= Costas 环主体 ==============================
// ==============================================================

wire [11:0]        phase_addra;
wire signed [11:0] sin_out, cos_out;
wire signed [23:0] i_mult_out, q_mult_out;
wire signed [31:0] freq_control;

// ===== 环路滤波器 =====
loop_filter_PSK loop_filter_inst (
    .clk   (clk_65m),
    .rst_n (loop_rst_n_sig),    // 受状态机控制的复位
    .theta (phase_error_gated), // 门控误差输入
    .delta (freq_control)
);

// ===== NCO =====
PhaseAccumulator nco (
    .CLK        (clk_65m),
    .RSTn       (rst_n),
    .PhaseOffset(12'd0),
    .PhaseStep  (INITIAL_FREQ + $unsigned(freq_control)),
    .MemAddr    (phase_addra)
);

// ===== 正交载波 =====
rom_sin_12bit sin_rom (
    .clka (clk_65m),
    .addra(phase_addra),
    .douta(sin_out)
);

rom_sin_12bit cos_rom (
    .clka (clk_65m),
    .addra(phase_addra + 12'd1024),
    .douta(cos_out)
);

wire signed [11:0] cos_out_signed = {~cos_out[11], cos_out[10:0]};
wire signed [11:0] sin_out_signed = {~sin_out[11], sin_out[10:0]};

// ===== 混频器 =====
mult_gen_0 i_multiplier (
    .CLK (clk_65m),
    .A   (data),
    .B   (cos_out_signed),
    .SCLR(~rst_n),
    .P   (i_mult_out)
);

mult_gen_0 q_multiplier (
    .CLK (clk_65m),
    .A   (data),
    .B   (sin_out_signed),
    .SCLR(~rst_n),
    .P   (q_mult_out)
);

// ===== 低通滤波器 =====
wire signed [15:0] i_filter_in = i_mult_out[23:8];
wire signed [15:0] q_filter_in = q_mult_out[23:8];

wire signed [15:0] i_filter_out_full;
wire signed [15:0] q_filter_out_full;

PSK_LPF i_filter (
    .clk       (clk_65m),
    .clk_enable(1'b1),
    .reset     (rst_n),
    .filter_in (i_filter_in),
    .filter_out(i_filter_out_full)
);

PSK_LPF q_filter (
    .clk       (clk_65m),
    .clk_enable(1'b1),
    .reset     (rst_n),
    .filter_in (q_filter_in),
    .filter_out(q_filter_out_full)
);

// ===== 鉴相器 =====
wire signed [11:0] i_base = i_filter_out_full[15:4];
wire signed [11:0] q_base = q_filter_out_full[15:4];

assign phase_error = (i_base[11] == 1'b0) ? q_base : -$signed(q_base);

// ===== 解调输出 =====
//assign jietiao = q_base;

// ==============================================================
// ================= 锁定检测器 =================================
// ==============================================================
wire [11:0] abs_i = i_base[11] ? -$signed(i_base) : i_base;
wire [11:0] abs_q = q_base[11] ? -$signed(q_base) : q_base;

wire signed [12:0] lock_diff = $signed({1'b0, abs_q}) - $signed({1'b0, abs_i});

always @(posedge clk_65m or negedge rst_n) begin
    if (!rst_n) begin
        psk_lock_acc <= 32'sd0;
    end else if (!loop_rst_n_r) begin
        // 状态机处于RESET状态时,清零累积器
        psk_lock_acc <= 32'sd0;
    end else begin
        // 正常积分(SEARCH和LOCKED状态均有效)
        psk_lock_acc <= psk_lock_acc - (psk_lock_acc >>> 14) + lock_diff;
    end
end

assign PSK_valid = (state == S_LOCKED);

  /************************************输出波形逻辑*****************************************/
   reg signed [13:0] PSK_data_reg;

    always @(posedge clk_65m or negedge rst_n) begin
        if (!rst_n) begin
            PSK_data_reg <= 14'd0;
        end else begin
            // 符号位扩展:把 12 位的最高位复制给 14 位的高两位
            PSK_data_reg <= { {2{q_base[11]}}, q_base };
        end
    end
assign PSK_data_wire = PSK_data_reg;


//// ===== ILA 调试 =====
//ila_0 FM (
//	.clk(clk_65m), // input wire clk


//	.probe0(data), // input wire [11:0]  probe0  
//	.probe1(jietiao) // input wire [11:0]  probe1
//);

endmodule

        该模块包括一个自定义的状态机、数控振荡器产生正交载波、I/Q两个混频器、一个低通滤波器、一个相位误差计算电路以及一个锁相检测电路。

        与FM/FSK不一样的是鉴相器模块,本模块采用的鉴相方法比较简洁,为下表中的第二种方法,也忘记是从哪篇论文里借鉴的了,如有侵权请联系。

        注意:1、该模块的环路滤波器不能沿用FM/FSK中的参数C1、C2,需另调节。

                   2、低通滤波器同前两模块一样,可以沿用。

                   3、混频器即为乘法器,可沿用前一模块。

                   4、正交载波即在正交载波的ROM IP地址上加1/4周期,即4096/4=12‘d1024。

八、调制模式判断模块AMC

        调制模式判断说白了就是看解调波形是否为方波或正弦波,也就是看跳变的时间是否陡峭。

        本模块在一个滑动窗口计数器window_cnt内计数,抓取特征出现次数进而判断波形类型,具体参数要根据具体ILA抓取波形的数据来确定。

        本模块的框图如下:

        本模块的代码如下:

module AMC_Classifier(
    input  wire        clk_65m,
    input  wire        rst_n,
    input  wire [13:0] env_in,
    input  wire [13:0] freq_in,
    input  wire        psk_locked_in,

    // [4]:AM, [3]:ASK, [2]:FM, [1]:FSK, [0]:PSK
    output reg  [4:0]  o_mod_type
);

    // ================== 1. 参数 ==================
    localparam [16:0] WINDOW_SIZE = 17'd65000;

    // 14bit 输入范围:0 ~ 16383
    // 这些阈值后面可以按 ILA 实测再微调
    localparam [15:0] TH_ENV_ACTIVE   = 16'd1500;   // 包络变化明显
    localparam [15:0] TH_FREQ_ACTIVE  = 16'd16300;   // 频率变化明显

    localparam [15:0] TH_ENV_EDGE_MIN = 16'd100;    // ASK 最小绝对跳变
    localparam [15:0] TH_FREQ_JUMP_MIN = 16'd1100;  // FSK 最小绝对跳变

    // Q8 比例阈值:
    // env_diff_max / env_range_now > 32 / 256 = 12.5% 判为 ASK
    // freq_jump_max / freq_range_now > 32 / 256 = 12.5% 判为 FSK
    localparam [7:0] TH_ENV_STEP_RATIO_Q8  = 8'd32;
    localparam [7:0] TH_FREQ_STEP_RATIO_Q8 = 8'd16;

    // PSK lock 在一个窗口内至少保持 1/8 窗口,才认为有效
    localparam [16:0] PSK_LOCK_MIN = WINDOW_SIZE >> 3;

    // ================== 2. 寄存器 ==================
    reg [16:0] window_cnt;

    reg [13:0] env_d1;
    reg [13:0] freq_d1;

    reg [13:0] freq_delay_line [0:63];
    integer i;

    reg [13:0] env_max_val;
    reg [13:0] env_min_val;
    reg [13:0] freq_max_val;
    reg [13:0] freq_min_val;

    reg [15:0] env_diff_max;
    reg [15:0] freq_jump_max;

    reg [16:0] psk_lock_cnt;

    // ================== 3. 实时特征 ==================
    wire [15:0] env_diff =
        (env_in >= env_d1) ? ({2'd0, env_in} - {2'd0, env_d1}) :
                             ({2'd0, env_d1} - {2'd0, env_in});

    wire [13:0] delayed_freq = freq_delay_line[63];

    wire [15:0] freq_jump =
        (freq_in >= delayed_freq) ? ({2'd0, freq_in} - {2'd0, delayed_freq}) :
                                    ({2'd0, delayed_freq} - {2'd0, freq_in});

    wire [15:0] env_range_now =
        (env_max_val >= env_min_val) ?
        ({2'd0, env_max_val} - {2'd0, env_min_val}) :
        ({2'd0, env_min_val} - {2'd0, env_max_val});

    wire [15:0] freq_range_now =
        (freq_max_val >= freq_min_val) ?
        ({2'd0, freq_max_val} - {2'd0, freq_min_val}) :
        ({2'd0, freq_min_val} - {2'd0, freq_max_val});

    // ================== 4. 归一化判决 ==================
    wire cond_env_active  = (env_range_now  > TH_ENV_ACTIVE);
    wire cond_freq_active = (freq_range_now < TH_FREQ_ACTIVE);

//    wire [31:0] env_step_lhs  = ({16'd0, env_diff_max}  << 8);
//    wire [31:0] env_step_rhs  = env_range_now  * TH_ENV_STEP_RATIO_Q8;

//    wire [31:0] freq_step_lhs = ({16'd0, freq_jump_max} << 8);
//    wire [31:0] freq_step_rhs = freq_range_now * TH_FREQ_STEP_RATIO_Q8;

    wire cond_env_step =
        cond_env_active &&
        (env_diff_max > TH_ENV_EDGE_MIN);

    wire cond_freq_step =
        cond_freq_active &&
        (freq_jump_max > TH_FREQ_JUMP_MIN) ;
//        &&
//        (freq_step_lhs > freq_step_rhs);

    wire cond_psk_like = (psk_lock_cnt >= PSK_LOCK_MIN);

    // debug_flags[7:0]
    // [7] cond_psk_like
    // [6] cond_env_active
    // [5] cond_env_step
    // [4] cond_freq_active
    // [3] cond_freq_step
    // [2] psk_locked_in
    // [1] reserved
    // [0] reserved
    wire [7:0] debug_flags = {
        cond_psk_like,
        cond_env_active,
        cond_env_step,
        cond_freq_active,
        cond_freq_step,
        psk_locked_in,
        1'b0,
        1'b0
    };

    // ================== 5. 主逻辑 ==================
    always @(posedge clk_65m or negedge rst_n) begin
        if (!rst_n) begin
            window_cnt    <= 17'd0;
            env_d1        <= 14'd0;
            freq_d1       <= 14'd0;

            env_max_val   <= 14'd0;
            env_min_val   <= 14'h3FFF;
            freq_max_val  <= 14'd0;
            freq_min_val  <= 14'h3FFF;

            env_diff_max  <= 16'd0;
            freq_jump_max <= 16'd0;
            psk_lock_cnt  <= 17'd0;

            o_mod_type    <= 5'b00000;

            for (i = 0; i < 64; i = i + 1) begin
                freq_delay_line[i] <= 14'd0;
            end
        end
        else begin
            env_d1  <= env_in;
            freq_d1 <= freq_in;

            freq_delay_line[0] <= freq_in;
            for (i = 1; i < 64; i = i + 1) begin
                freq_delay_line[i] <= freq_delay_line[i-1];
            end

            if (window_cnt < WINDOW_SIZE) begin
                window_cnt <= window_cnt + 1'b1;

                if (env_in > env_max_val)
                    env_max_val <= env_in;
                if (env_in < env_min_val)
                    env_min_val <= env_in;

                if (freq_in > freq_max_val)
                    freq_max_val <= freq_in;
                if (freq_in < freq_min_val)
                    freq_min_val <= freq_in;

                if (env_diff > env_diff_max)
                    env_diff_max <= env_diff;

                if (freq_jump > freq_jump_max)
                    freq_jump_max <= freq_jump;

                if (psk_locked_in && psk_lock_cnt < WINDOW_SIZE)
                    psk_lock_cnt <= psk_lock_cnt + 1'b1;
            end
            else begin
                window_cnt <= 17'd0;

                // ================== 6. 判决优先级 ==================
                // PSK:由外部锁定信号优先判断
                // ASK/AM:看包络是否明显变化
                // FSK/FM:看频率是否明显变化
                if (cond_env_active) begin
                    if (cond_env_step)
                        o_mod_type <= 5'b01000; // ASK
                    else
                        o_mod_type <= 5'b10000; // AM
                end
                else if (cond_freq_active) begin
                    if (cond_freq_step)
                        o_mod_type <= 5'b00010; // FSK
                    else
                        o_mod_type <= 5'b00100; // FM
                end else if (cond_psk_like) begin
                    o_mod_type <= 5'b00001; // PSK
                end
                else begin
                    o_mod_type <= 5'b00000;
                end

                // ================== 7. 新窗口初始化 ==================
                env_max_val   <= env_in;
                env_min_val   <= env_in;
                freq_max_val  <= freq_in;
                freq_min_val  <= freq_in;

                env_diff_max  <= 16'd0;
                freq_jump_max <= 16'd0;
                psk_lock_cnt  <= 17'd0;
            end
        end
    end

    // ================== 8. ILA 调试 ==================
//    ila_0 your_instance_name (
//        .clk    (clk_65m),
//        .probe0 (env_in),          // [13:0]
//        .probe1 (freq_in),         // [13:0]
//        .probe2 (env_range_now),   // [15:0]
//        .probe3 (env_diff_max),    // [15:0]
//        .probe4 (freq_range_now),  // [15:0]
//        .probe5 (freq_jump_max),   // [15:0]
//        .probe6 (debug_flags),     // [7:0]
//        .probe7 (o_mod_type)       // [4:0]
//    );

endmodule

九、调制参数处理模块

        该模块即根据抓取窗口内实际的数值估算出需要测量的参数,都是规定好的公式,得出的参数不同就慢慢调。接收来自AMC的模式识别输入后,输出对应的参数。

`timescale 1ns / 1ps

module data_proc #(
    parameter integer CLK_FRE       = 65_000_000,
    parameter integer WINDOW_SIZE   = 65_000,
    parameter integer N_WIN         = 4,
    parameter integer PSK_DEBOUNCE  = 32,

    // 14bit 解调频率码值满量程对应的频偏 Hz,需要按你的硬件实测校准
    parameter integer FREQ_CODE_HZ  = 92_000,

    // 防止毛刺边沿,最大允许测量频率
    parameter integer MAX_MEAS_HZ   = 20_000,

    // 模式稳定多少拍后才切换
    parameter integer MODE_STABLE_N = 200,

    // 你前面实测 FM/FSK 是函数发生器的 2 倍,所以默认除 2
    parameter integer FM_DIV2       = 1,
    parameter integer FSK_DIV2      = 1
)(
    input  wire        clk,
    input  wire        rst_n,
    input  wire [4:0]  i_mode_type,
    input  wire [13:0] sig_in,

    output reg  [15:0] ma,
    output reg  [15:0] mf,
    output reg  [31:0] bps,
    output reg  [31:0] fre
);

localparam MODE_AM  = 5'b10000;
localparam MODE_ASK = 5'b01000;
localparam MODE_FM  = 5'b00100;
localparam MODE_FSK = 5'b00010;
localparam MODE_PSK = 5'b00001;

localparam integer W_CNT = $clog2(WINDOW_SIZE);
localparam integer N_CNT = (N_WIN <= 1) ? 1 : $clog2(N_WIN);

localparam [31:0] ADC_SCALE  = 32'd16384;
localparam [13:0] HYST_MIN   = 14'd16;
localparam [15:0] MA_GAIN_Q8 = 16'd680;
localparam [31:0] TIMEOUT    = CLK_FRE;
localparam [63:0] CLK_FRE_64 = CLK_FRE;

localparam [31:0] MIN_EDGE_GAP_RAW =
    (MAX_MEAS_HZ > 0) ? (CLK_FRE / MAX_MEAS_HZ) : 32'd1;

localparam [31:0] MIN_EDGE_GAP =
    (MIN_EDGE_GAP_RAW < PSK_DEBOUNCE) ? PSK_DEBOUNCE : MIN_EDGE_GAP_RAW;

// ==================================================
// 模式稳定锁存
// ==================================================
reg [4:0]  mode_sample;
reg [4:0]  mode_r;
reg [4:0]  mode_d;
reg [15:0] mode_stable_cnt;

always @(posedge clk or negedge rst_n) begin
    if (!rst_n) begin
        mode_sample     <= 5'd0;
        mode_r          <= 5'd0;
        mode_d          <= 5'd0;
        mode_stable_cnt <= 16'd0;
    end else begin
        mode_d <= mode_r;

        if (i_mode_type != mode_sample) begin
            mode_sample     <= i_mode_type;
            mode_stable_cnt <= 16'd0;
        end else if (mode_stable_cnt < MODE_STABLE_N[15:0]) begin
            mode_stable_cnt <= mode_stable_cnt + 1'b1;
        end else begin
            mode_r <= mode_sample;
        end
    end
end

wire mode_changed = (mode_r != mode_d);

// ==================================================
// 窗口计数
// ==================================================
reg [W_CNT-1:0] win_cnt;
wire win_end = (win_cnt == WINDOW_SIZE - 1);

always @(posedge clk or negedge rst_n) begin
    if (!rst_n)
        win_cnt <= 0;
    else if (mode_changed)
        win_cnt <= 0;
    else if (win_end)
        win_cnt <= 0;
    else
        win_cnt <= win_cnt + 1'b1;
end

reg [N_CNT-1:0] win_acc_cnt;
wire group_end = win_end && (win_acc_cnt == N_WIN - 1);

// ==================================================
// 最大值 / 最小值 / 动态中点
// ==================================================
reg [13:0] x_max_acc;
reg [13:0] x_min_acc;
reg [13:0] x_max_latch;
reg [13:0] x_min_latch;

reg [13:0] zero_mid_r;
reg [13:0] hyst_r;
reg        mid_valid;
reg        latch_valid;

wire [14:0] x_range_now =
    (x_max_acc >= x_min_acc) ?
    ({1'b0, x_max_acc} - {1'b0, x_min_acc}) : 15'd0;

wire [14:0] x_mid_now =
    ({1'b0, x_max_acc} + {1'b0, x_min_acc}) >> 1;

wire [13:0] hyst_from_range = {3'd0, x_range_now[14:4]};

wire [13:0] hyst_next =
    (hyst_from_range < HYST_MIN) ? HYST_MIN : hyst_from_range;

always @(posedge clk or negedge rst_n) begin
    if (!rst_n) begin
        win_acc_cnt <= 0;
        x_max_acc   <= 14'd0;
        x_min_acc   <= 14'h3FFF;
        x_max_latch <= 14'd0;
        x_min_latch <= 14'd0;
        zero_mid_r  <= 14'd8192;
        hyst_r      <= HYST_MIN;
        mid_valid   <= 1'b0;
        latch_valid <= 1'b0;
    end else if (mode_changed) begin
        win_acc_cnt <= 0;
        x_max_acc   <= sig_in;
        x_min_acc   <= sig_in;
        x_max_latch <= 14'd0;
        x_min_latch <= 14'd0;
        zero_mid_r  <= 14'd8192;
        hyst_r      <= HYST_MIN;
        mid_valid   <= 1'b0;
        latch_valid <= 1'b0;
    end else begin
        latch_valid <= 1'b0;

        if (win_end) begin
            if (group_end) begin
                x_max_latch <= x_max_acc;
                x_min_latch <= x_min_acc;
                zero_mid_r  <= x_mid_now[13:0];
                hyst_r      <= hyst_next;
                mid_valid   <= 1'b1;
                latch_valid <= 1'b1;

                x_max_acc   <= sig_in;
                x_min_acc   <= sig_in;
                win_acc_cnt <= 0;
            end else begin
                win_acc_cnt <= win_acc_cnt + 1'b1;
            end
        end else begin
            if (sig_in > x_max_acc)
                x_max_acc <= sig_in;
            if (sig_in < x_min_acc)
                x_min_acc <= sig_in;
        end
    end
end

// ==================================================
// 动态中点滞回比较
// ==================================================
wire [15:0] data_u16 = {2'd0, sig_in};
wire [15:0] mid_u16  = {2'd0, zero_mid_r};
wire [15:0] hyst_u16 = {2'd0, hyst_r};

wire [15:0] cmp_hi = mid_u16 + hyst_u16;
wire [15:0] cmp_lo = (mid_u16 > hyst_u16) ? (mid_u16 - hyst_u16) : 16'd0;

reg level;
reg level_d1;

wire level_rise = level && !level_d1;
wire level_edge = level ^ level_d1;

always @(posedge clk or negedge rst_n) begin
    if (!rst_n) begin
        level    <= 1'b0;
        level_d1 <= 1'b0;
    end else if (mode_changed) begin
        level    <= (sig_in >= zero_mid_r);
        level_d1 <= (sig_in >= zero_mid_r);
    end else begin
        level_d1 <= level;

        if (mid_valid) begin
            if (data_u16 > cmp_hi)
                level <= 1'b1;
            else if (data_u16 < cmp_lo)
                level <= 1'b0;
        end
    end
end

// ==================================================
// AM / FM 频率:上升沿到上升沿平均周期
// ==================================================
reg [31:0] period_timer;
reg        period_first;
reg [47:0] period_sum;
reg [23:0] period_cnt;
reg [31:0] period_rate_r;

wire period_mode =
    mid_valid &&
    ((mode_r == MODE_AM) || (mode_r == MODE_FM));

wire [63:0] period_rate_calc =
    (period_cnt != 0 && period_sum != 0) ?
    ((CLK_FRE_64 * period_cnt) / period_sum) : 64'd0;

wire [31:0] period_rate_calc32 =
    (period_rate_calc > 64'h0000_0000_FFFF_FFFF) ?
    32'hFFFF_FFFF : period_rate_calc[31:0];

always @(posedge clk or negedge rst_n) begin
    if (!rst_n) begin
        period_timer  <= 32'd0;
        period_first  <= 1'b0;
        period_sum    <= 48'd0;
        period_cnt    <= 24'd0;
        period_rate_r <= 32'd0;
    end else if (mode_changed || !period_mode) begin
        period_timer  <= 32'd0;
        period_first  <= 1'b0;
        period_sum    <= 48'd0;
        period_cnt    <= 24'd0;
        period_rate_r <= 32'd0;
    end else if (group_end) begin
        period_rate_r <= period_rate_calc32;

        period_timer <= 32'd0;
        period_first <= 1'b0;
        period_sum   <= 48'd0;
        period_cnt   <= 24'd0;
    end else begin
        if (period_timer >= TIMEOUT) begin
            period_timer <= 32'd0;
            period_first <= 1'b0;
        end else begin
            period_timer <= period_timer + 1'b1;
        end

        if (level_rise && period_timer >= MIN_EDGE_GAP) begin
            if (period_first) begin
                period_sum <= period_sum + period_timer;
                period_cnt <= period_cnt + 1'b1;
            end

            period_first <= 1'b1;
            period_timer <= 32'd0;
        end
    end
end

// ==================================================
// ASK / FSK / PSK 波特率:按 ASK 同样逻辑,上升沿到上升沿
// ==================================================
reg [31:0] bit_timer;
reg        bit_first;
reg [47:0] bit_period_sum;
reg [23:0] bit_period_cnt;
reg [31:0] bit_rate_r;

wire bit_mode =
    mid_valid &&
    ((mode_r == MODE_ASK) ||
     (mode_r == MODE_FSK) ||
     (mode_r == MODE_PSK));

wire [63:0] bit_rate_calc =
    (bit_period_cnt != 0 && bit_period_sum != 0) ?
    ((CLK_FRE_64 * bit_period_cnt) / bit_period_sum) : 64'd0;

wire [31:0] bit_rate_calc32 =
    (bit_rate_calc > 64'h0000_0000_FFFF_FFFF) ?
    32'hFFFF_FFFF : bit_rate_calc[31:0];

always @(posedge clk or negedge rst_n) begin
    if (!rst_n) begin
        bit_timer      <= 32'd0;
        bit_first      <= 1'b0;
        bit_period_sum <= 48'd0;
        bit_period_cnt <= 24'd0;
        bit_rate_r     <= 32'd0;
    end else if (mode_changed || !bit_mode) begin
        bit_timer      <= 32'd0;
        bit_first      <= 1'b0;
        bit_period_sum <= 48'd0;
        bit_period_cnt <= 24'd0;
        bit_rate_r     <= 32'd0;
    end else if (group_end) begin
        bit_rate_r <= bit_rate_calc32;

        bit_timer      <= 32'd0;
        bit_first      <= 1'b0;
        bit_period_sum <= 48'd0;
        bit_period_cnt <= 24'd0;
    end else begin
        if (bit_timer >= TIMEOUT) begin
            bit_timer <= 32'd0;
            bit_first <= 1'b0;
        end else begin
            bit_timer <= bit_timer + 1'b1;
        end

        if (level_rise && bit_timer >= MIN_EDGE_GAP) begin
            if (bit_first) begin
                bit_period_sum <= bit_period_sum + bit_timer;
                bit_period_cnt <= bit_period_cnt + 1'b1;
            end

            bit_first <= 1'b1;
            bit_timer <= 32'd0;
        end
    end
end

wire [31:0] fm_fre_out =
    (FM_DIV2 != 0) ? (period_rate_r >> 1) : period_rate_r;

wire [31:0] fsk_bps_out =
    (FSK_DIV2 != 0) ? (bit_rate_r >> 1) : bit_rate_r;

// ==================================================
// Ma = (max - min) / (max + min),Q8.8,再乘校准系数
// ==================================================
wire [14:0] ma_sum_now =
    {1'b0, x_max_acc} + {1'b0, x_min_acc};

wire [14:0] ma_diff_now =
    (x_max_acc >= x_min_acc) ?
    ({1'b0, x_max_acc} - {1'b0, x_min_acc}) : 15'd0;

wire [31:0] ma_q8_raw =
    (ma_sum_now != 0) ?
    (({17'd0, ma_diff_now} << 8) / ma_sum_now) : 32'd0;

wire [47:0] ma_q8_gain = ma_q8_raw * MA_GAIN_Q8;
wire [31:0] ma_q8_cal  = ma_q8_gain[39:8];

wire [15:0] ma_now_sat =
    (ma_q8_cal > 32'd256) ? 16'h0100 : ma_q8_cal[15:0];

reg [15:0] ma_r;

always @(posedge clk or negedge rst_n) begin
    if (!rst_n)
        ma_r <= 16'd0;
    else if (mode_r != MODE_AM)
        ma_r <= 16'd0;
    else if (group_end)
        ma_r <= ma_now_sat;
end

// ==================================================
// Mf = delta_f / fm,Q8.8
// 注意:FREQ_CODE_HZ 必须按你的 FM 解调比例校准
// ==================================================
wire [14:0] freq_pp_code_now =
    (x_max_acc >= x_min_acc) ?
    ({1'b0, x_max_acc} - {1'b0, x_min_acc}) : 15'd0;

wire [14:0] freq_peak_code = freq_pp_code_now >> 1;

wire [31:0] fm_rate_for_mf =
    (FM_DIV2 != 0) ? (period_rate_calc32 >> 1) : period_rate_calc32;

wire [47:0] delta_f_peak_hz =
    (({33'd0, freq_peak_code} * FREQ_CODE_HZ) / ADC_SCALE);

wire mf_rate_valid = (fm_rate_for_mf >= 32'd10);

wire [55:0] mf_q8_calc =
    (mf_rate_valid) ?
    (({8'd0, delta_f_peak_hz} << 8) / fm_rate_for_mf) : 56'd0;

wire [15:0] mf_sat =
    (mf_q8_calc > 56'd65535) ? 16'hFFFF : mf_q8_calc[15:0];

reg [15:0] mf_r;

always @(posedge clk or negedge rst_n) begin
    if (!rst_n)
        mf_r <= 16'd0;
    else if (mode_r != MODE_FM)
        mf_r <= 16'd0;
    else if (group_end)
        mf_r <= mf_sat;
end

// ==================================================
// 输出
// ==================================================
always @(posedge clk or negedge rst_n) begin
    if (!rst_n) begin
        ma  <= 16'd0;
        mf  <= 16'd0;
        bps <= 32'd0;
        fre <= 32'd0;
    end else begin
        case (mode_r)
            MODE_AM: begin
                ma  <= ma_r;
                mf  <= 16'd0;
                bps <= 32'd0;
                fre <= period_rate_r;
            end

            MODE_ASK: begin
                ma  <= 16'd0;
                mf  <= 16'd0;
                bps <= bit_rate_r;
                fre <= 32'd0;
            end

            MODE_FM: begin
                ma  <= 16'd0;
                mf  <= mf_r;
                bps <= 32'd0;
                fre <= fm_fre_out;
            end

            MODE_FSK: begin
                ma  <= 16'd0;
                mf  <= 16'd0;
                bps <= fsk_bps_out;
                fre <= 32'd0;
            end

            MODE_PSK: begin
                ma  <= 16'd0;
                mf  <= 16'd0;
                bps <= bit_rate_r;
                fre <= 32'd0;
            end

            default: begin
                ma  <= 16'd0;
                mf  <= 16'd0;
                bps <= 32'd0;
                fre <= 32'd0;
            end
        endcase
    end
end

// ==================================================
// ILA:8 个探针
// ==================================================
wire [7:0] debug_flags = {
    mid_valid,
    latch_valid,
    group_end,
    mode_changed,
    level,
    level_d1,
    level_rise,
    level_edge
};

wire [15:0] mf_calc_dbg =
    (mf_q8_calc > 56'd65535) ? 16'hFFFF : mf_q8_calc[15:0];

ila_0 your_instance_name (
    .clk    (clk),
    .probe0 (sig_in),           // [13:0]
    .probe1 (mode_r),           // [4:0]
    .probe2 (zero_mid_r),       // [13:0]
    .probe3 (x_max_latch),      // [13:0]
    .probe4 (x_min_latch),      // [13:0]
    .probe5 (period_rate_r),    // [31:0] FM 原始测频,输出 fre 默认除 2
    .probe6 (mf_calc_dbg),      // [15:0] Mf 计算值
    .probe7 (debug_flags)       // [7:0]
);

endmodule

十、数据发送模块

        该模块的作用是将计算出的调制参数打包用uart协议发出,具体的数据包定义如下:

包头 调制类型 调制信号频率 Ma Mf bps 包尾
start AM/ASK/FM/FSK/PSK Q8.8 Q8.8 end
module uart_packet_sender
#(
    parameter CLK_FRE   = 65,
    parameter BAUD_RATE = 9600
)
(
    input             i_clk_sys,
    input             i_rst_n,
    input             i_send_en,
    input  [4:0]      i_mod_type,
    input  [31:0]     i_freq,
    input  [15:0]     i_Ma,            // Q8.8
    input  [15:0]     i_Mf,            // Q8.8
    input  [31:0]     i_symbol_rate,
    output            o_uart_tx,
    output reg        o_busy
);

localparam BUF_LEN = 96;

// 主状态
localparam S_IDLE           = 6'd0;
localparam S_LATCH          = 6'd1;
localparam S_HEAD           = 6'd2;
localparam S_TYPE           = 6'd3;
localparam S_COMMA          = 6'd4;
localparam S_DABBLE_INIT    = 6'd5;
localparam S_DABBLE_SHIFT   = 6'd6;
localparam S_DABBLE_DONE    = 6'd7;
localparam S_FIND_FIRST     = 6'd8;
localparam S_WRITE_DIGIT    = 6'd9;
localparam S_WRITE_Q8_INT   = 6'd10;
localparam S_WRITE_Q8_DOT   = 6'd11;
localparam S_WRITE_Q8_FRACH = 6'd12;
localparam S_WRITE_Q8_FRACL = 6'd13;
localparam S_TAIL           = 6'd14;
localparam S_SEND_LOAD      = 6'd15;
localparam S_SEND_WAIT      = 6'd16;
localparam S_WAIT_GAP       = 6'd17;

// 字段
localparam FIELD_FREQ = 3'd0;
localparam FIELD_MA   = 3'd1;
localparam FIELD_MF   = 3'd2;
localparam FIELD_BPS  = 3'd3;
localparam FIELD_DONE = 3'd4;

reg [5:0] r_state;

// 锁存输入
reg [4:0]  l_mod_type;
reg [31:0] l_freq, l_symbol_rate;
reg [15:0] l_Ma, l_Mf;

// type 字符串
reg [7:0] r_type_buf [0:4];
reg [2:0] r_type_len;

// 填充 buffer
reg [7:0] r_buf [0:BUF_LEN-1];
reg [6:0] r_fill_idx;
reg [6:0] r_buf_len;
reg [6:0] r_byte_cnt;

reg [2:0] r_cur_field;
reg [2:0] r_wr_digit_idx;
reg [7:0] r_tx_data;
reg       r_tx_valid;
wire      w_tx_done;

reg [19:0] r_wait_gap_cnt;

// ---------- Double Dabble(只给 freq / bps 用) ----------
localparam BCD_W = 40;  // 支持到 10 位十进制以内
reg [BCD_W-1:0] r_bcd;
reg [31:0]      r_bin_shift;
reg [5:0]       r_dabble_cnt;
reg [5:0]       r_dabble_total;
reg [7:0]       r_digit_buf [0:9];

function [3:0] add3;
    input [3:0] x;
    begin
        add3 = (x >= 4'd5) ? (x + 4'd3) : x;
    end
endfunction

wire [BCD_W-1:0] w_bcd_next;
assign w_bcd_next = {
    add3(r_bcd[39:36]),
    add3(r_bcd[35:32]),
    add3(r_bcd[31:28]),
    add3(r_bcd[27:24]),
    add3(r_bcd[23:20]),
    add3(r_bcd[19:16]),
    add3(r_bcd[15:12]),
    add3(r_bcd[11:8]),
    add3(r_bcd[7:4]),
    add3(r_bcd[3:0])
};

// ---------- Q8.8 直接拆分 ----------
wire [7:0] w_q8_int = (r_cur_field == FIELD_MA) ? l_Ma[15:8] : l_Mf[15:8];

// 先扩位再乘100,避免位宽截断
wire [15:0] w_ma_frac_mul = {8'd0, l_Ma[7:0]} * 16'd100;
wire [15:0] w_mf_frac_mul = {8'd0, l_Mf[7:0]} * 16'd100;

wire [7:0] w_ma_frac = w_ma_frac_mul[15:8];
wire [7:0] w_mf_frac = w_mf_frac_mul[15:8];

wire [7:0] w_q8_frac = (r_cur_field == FIELD_MA) ? w_ma_frac : w_mf_frac;

wire [7:0] w_q8_hund = w_q8_int / 8'd100;
wire [7:0] w_q8_tens = (w_q8_int % 8'd100) / 8'd10;
wire [7:0] w_q8_ones =  w_q8_int % 8'd10;

wire [7:0] w_q8_frac_tens = w_q8_frac / 8'd10;
wire [7:0] w_q8_frac_ones = w_q8_frac % 8'd10;

wire [1:0] w_q8_digits = (w_q8_int >= 8'd100) ? 2'd3 :
                         (w_q8_int >= 8'd10 ) ? 2'd2 : 2'd1;
// ============================================================
// 主状态机
// ============================================================
always @(posedge i_clk_sys or negedge i_rst_n) begin
    if (!i_rst_n) begin
        r_state        <= S_IDLE;
        o_busy         <= 1'b0;
        r_fill_idx     <= 7'd0;
        r_buf_len      <= 7'd0;
        r_byte_cnt     <= 7'd0;
        r_wr_digit_idx <= 3'd0;
        r_cur_field    <= FIELD_FREQ;
        r_tx_data      <= 8'd0;
        r_tx_valid     <= 1'b0;
        r_wait_gap_cnt <= 20'd0;

        l_mod_type     <= 5'd0;
        l_freq         <= 32'd0;
        l_symbol_rate  <= 32'd0;
        l_Ma           <= 16'd0;
        l_Mf           <= 16'd0;

        r_type_len     <= 3'd0;

        r_bcd          <= {BCD_W{1'b0}};
        r_bin_shift    <= 32'd0;
        r_dabble_cnt   <= 6'd0;
        r_dabble_total <= 6'd0;
    end else begin
        r_tx_valid <= 1'b0;

        case (r_state)
            // ------------------------------------------------
            S_IDLE: begin
                o_busy         <= 1'b0;
                r_fill_idx     <= 7'd0;
                r_byte_cnt     <= 7'd0;
                r_wr_digit_idx <= 3'd0;
                r_wait_gap_cnt <= 20'd0;
                if (i_send_en) begin
                    o_busy  <= 1'b1;
                    r_state <= S_LATCH;
                end
            end

            // ------------------------------------------------
            S_LATCH: begin
                l_mod_type    <= i_mod_type;
                l_freq        <= i_freq;
                l_Ma          <= i_Ma;
                l_Mf          <= i_Mf;
                l_symbol_rate <= i_symbol_rate;

                case (i_mod_type)
                    5'b10000: begin
                        r_type_buf[0] <= "A";
                        r_type_buf[1] <= "M";
                        r_type_len    <= 3'd2;
                    end
                    5'b01000: begin
                        r_type_buf[0] <= "A";
                        r_type_buf[1] <= "S";
                        r_type_buf[2] <= "K";
                        r_type_len    <= 3'd3;
                    end
                    5'b00100: begin
                        r_type_buf[0] <= "F";
                        r_type_buf[1] <= "M";
                        r_type_len    <= 3'd2;
                    end
                    5'b00010: begin
                        r_type_buf[0] <= "F";
                        r_type_buf[1] <= "S";
                        r_type_buf[2] <= "K";
                        r_type_len    <= 3'd3;
                    end
                    5'b00001: begin
                        r_type_buf[0] <= "P";
                        r_type_buf[1] <= "S";
                        r_type_buf[2] <= "K";
                        r_type_len    <= 3'd3;
                    end
                    default: begin
                        r_type_buf[0] <= "U";
                        r_type_buf[1] <= "N";
                        r_type_buf[2] <= "K";
                        r_type_len    <= 3'd3;
                    end
                endcase

                r_wr_digit_idx <= 3'd0;
                r_state        <= S_HEAD;
            end

            // ------------------------------------------------
            S_HEAD: begin
                case (r_wr_digit_idx)
                    3'd0: r_buf[r_fill_idx] <= "s";
                    3'd1: r_buf[r_fill_idx] <= "t";
                    3'd2: r_buf[r_fill_idx] <= "a";
                    3'd3: r_buf[r_fill_idx] <= "r";
                    3'd4: r_buf[r_fill_idx] <= "t";
                    3'd5: r_buf[r_fill_idx] <= ",";
                    default: r_buf[r_fill_idx] <= ",";
                endcase

                r_fill_idx <= r_fill_idx + 1'b1;

                if (r_wr_digit_idx == 3'd5) begin
                    r_wr_digit_idx <= 3'd0;
                    r_state        <= S_TYPE;
                end else begin
                    r_wr_digit_idx <= r_wr_digit_idx + 1'b1;
                end
            end

            // ------------------------------------------------
            S_TYPE: begin
                r_buf[r_fill_idx] <= r_type_buf[r_wr_digit_idx];
                r_fill_idx        <= r_fill_idx + 1'b1;

                if (r_wr_digit_idx == r_type_len - 1'b1) begin
                    r_wr_digit_idx <= 3'd0;
                    r_cur_field    <= FIELD_FREQ;
                    r_state        <= S_COMMA;
                end else begin
                    r_wr_digit_idx <= r_wr_digit_idx + 1'b1;
                end
            end

            // ------------------------------------------------
            S_COMMA: begin
                r_buf[r_fill_idx] <= ",";
                r_fill_idx        <= r_fill_idx + 1'b1;

                case (r_cur_field)
                    FIELD_FREQ: begin
                        r_bin_shift    <= l_freq;
                        r_dabble_total <= 6'd32;
                        r_state        <= S_DABBLE_INIT;
                    end

                    FIELD_MA: begin
                        r_wr_digit_idx <= 3'd0;
                        r_state        <= S_WRITE_Q8_INT;
                    end

                    FIELD_MF: begin
                        r_wr_digit_idx <= 3'd0;
                        r_state        <= S_WRITE_Q8_INT;
                    end

                    FIELD_BPS: begin
                        r_bin_shift    <= l_symbol_rate;
                        r_dabble_total <= 6'd32;
                        r_state        <= S_DABBLE_INIT;
                    end

                    default: begin
                        r_wr_digit_idx <= 3'd0;
                        r_state        <= S_TAIL;
                    end
                endcase
            end

            // ------------------------------------------------
            // 只给 freq / bps 用
            S_DABBLE_INIT: begin
                r_bcd        <= {BCD_W{1'b0}};
                r_dabble_cnt <= 6'd0;
                r_state      <= S_DABBLE_SHIFT;
            end

            S_DABBLE_SHIFT: begin
                r_bcd        <= {w_bcd_next[BCD_W-2:0], r_bin_shift[31]};
                r_bin_shift  <= r_bin_shift << 1;
                r_dabble_cnt <= r_dabble_cnt + 1'b1;

                if (r_dabble_cnt == r_dabble_total - 1'b1)
                    r_state <= S_DABBLE_DONE;
            end

            S_DABBLE_DONE: begin
                r_digit_buf[0] <= r_bcd[3:0]   + 8'h30;
                r_digit_buf[1] <= r_bcd[7:4]   + 8'h30;
                r_digit_buf[2] <= r_bcd[11:8]  + 8'h30;
                r_digit_buf[3] <= r_bcd[15:12] + 8'h30;
                r_digit_buf[4] <= r_bcd[19:16] + 8'h30;
                r_digit_buf[5] <= r_bcd[23:20] + 8'h30;
                r_digit_buf[6] <= r_bcd[27:24] + 8'h30;
                r_digit_buf[7] <= r_bcd[31:28] + 8'h30;
                r_digit_buf[8] <= r_bcd[35:32] + 8'h30;
                r_digit_buf[9] <= r_bcd[39:36] + 8'h30;
                r_state        <= S_FIND_FIRST;
            end

            S_FIND_FIRST: begin
                if      (r_digit_buf[9] != "0") r_wr_digit_idx <= 3'd9;
                else if (r_digit_buf[8] != "0") r_wr_digit_idx <= 3'd8;
                else if (r_digit_buf[7] != "0") r_wr_digit_idx <= 3'd7;
                else if (r_digit_buf[6] != "0") r_wr_digit_idx <= 3'd6;
                else if (r_digit_buf[5] != "0") r_wr_digit_idx <= 3'd5;
                else if (r_digit_buf[4] != "0") r_wr_digit_idx <= 3'd4;
                else if (r_digit_buf[3] != "0") r_wr_digit_idx <= 3'd3;
                else if (r_digit_buf[2] != "0") r_wr_digit_idx <= 3'd2;
                else if (r_digit_buf[1] != "0") r_wr_digit_idx <= 3'd1;
                else                            r_wr_digit_idx <= 3'd0;
                r_state <= S_WRITE_DIGIT;
            end

            S_WRITE_DIGIT: begin
                r_buf[r_fill_idx] <= r_digit_buf[r_wr_digit_idx];
                r_fill_idx        <= r_fill_idx + 1'b1;

                if (r_wr_digit_idx == 3'd0) begin
                    if (r_cur_field == FIELD_FREQ) begin
                        r_cur_field <= FIELD_MA;
                        r_state     <= S_COMMA;
                    end else begin
                        r_cur_field <= FIELD_DONE;
                        r_state     <= S_TAIL;
                    end
                end else begin
                    r_wr_digit_idx <= r_wr_digit_idx - 1'b1;
                end
            end

            // ------------------------------------------------
            // Ma / Mf 直接按 Q8.8 打印
            S_WRITE_Q8_INT: begin
                case (w_q8_digits)
                    2'd3: begin
                        case (r_wr_digit_idx)
                            3'd0: r_buf[r_fill_idx] <= "0" + w_q8_hund;
                            3'd1: r_buf[r_fill_idx] <= "0" + w_q8_tens;
                            default: r_buf[r_fill_idx] <= "0" + w_q8_ones;
                        endcase
                    end

                    2'd2: begin
                        case (r_wr_digit_idx)
                            3'd0: r_buf[r_fill_idx] <= "0" + w_q8_tens;
                            default: r_buf[r_fill_idx] <= "0" + w_q8_ones;
                        endcase
                    end

                    default: begin
                        r_buf[r_fill_idx] <= "0" + w_q8_ones;
                    end
                endcase

                r_fill_idx <= r_fill_idx + 1'b1;

                if (r_wr_digit_idx == w_q8_digits - 1'b1) begin
                    r_wr_digit_idx <= 3'd0;
                    r_state        <= S_WRITE_Q8_DOT;
                end else begin
                    r_wr_digit_idx <= r_wr_digit_idx + 1'b1;
                end
            end

            S_WRITE_Q8_DOT: begin
                r_buf[r_fill_idx] <= ".";
                r_fill_idx        <= r_fill_idx + 1'b1;
                r_state           <= S_WRITE_Q8_FRACH;
            end

            S_WRITE_Q8_FRACH: begin
                r_buf[r_fill_idx] <= "0" + w_q8_frac_tens;
                r_fill_idx        <= r_fill_idx + 1'b1;
                r_state           <= S_WRITE_Q8_FRACL;
            end

            S_WRITE_Q8_FRACL: begin
                r_buf[r_fill_idx] <= "0" + w_q8_frac_ones;
                r_fill_idx        <= r_fill_idx + 1'b1;

                if (r_cur_field == FIELD_MA) begin
                    r_cur_field <= FIELD_MF;
                    r_state     <= S_COMMA;
                end else begin
                    r_cur_field <= FIELD_BPS;
                    r_state     <= S_COMMA;
                end
            end

            // ------------------------------------------------
            S_TAIL: begin
                case (r_wr_digit_idx)
                    3'd0: r_buf[r_fill_idx] <= ",";
                    3'd1: r_buf[r_fill_idx] <= "e";
                    3'd2: r_buf[r_fill_idx] <= "n";
                    3'd3: r_buf[r_fill_idx] <= "d";
                    3'd4: r_buf[r_fill_idx] <= 8'h0D; // \r
                    3'd5: r_buf[r_fill_idx] <= 8'h0A; // \n
                    default: r_buf[r_fill_idx] <= 8'h0A;
                endcase

                r_fill_idx <= r_fill_idx + 1'b1;

                if (r_wr_digit_idx == 3'd5) begin
                    r_buf_len  <= r_fill_idx + 1'b1;
                    r_byte_cnt <= 7'd0;
                    r_state    <= S_SEND_LOAD;
                end else begin
                    r_wr_digit_idx <= r_wr_digit_idx + 1'b1;
                end
            end

            // ------------------------------------------------
            S_SEND_LOAD: begin
                r_tx_data  <= r_buf[r_byte_cnt];
                r_tx_valid <= 1'b1;
                r_state    <= S_SEND_WAIT;
            end

            S_SEND_WAIT: begin
                if (w_tx_done) begin
                    r_wait_gap_cnt <= 20'd0;
                    r_state        <= S_WAIT_GAP;
                end
            end

            S_WAIT_GAP: begin
                if (r_wait_gap_cnt < 20'd62500) begin
                    r_wait_gap_cnt <= r_wait_gap_cnt + 1'b1;
                end else begin
                    if (r_byte_cnt == r_buf_len - 1'b1) begin
                        o_busy  <= 1'b0;
                        r_state <= S_IDLE;
                    end else begin
                        r_byte_cnt <= r_byte_cnt + 1'b1;
                        r_state    <= S_SEND_LOAD;
                    end
                end
            end

            default: r_state <= S_IDLE;
        endcase
    end
end

uart_tx #(
    .CLK_FRE   (CLK_FRE),
    .BAUD_RATE (BAUD_RATE)
) u_uart_tx (
    .i_clk_sys    (i_clk_sys),
    .i_rst_n      (i_rst_n),
    .i_data_tx    (r_tx_data),
    .i_data_valid (r_tx_valid),
    .o_uart_tx    (o_uart_tx),
    .o_tx_done    (w_tx_done)
);

endmodule

        STM32的接收数据处理逻辑:

void uart_proc(void)
{
    uint32_t len = 0;

    __disable_irq();

    if (usart_rb.itemCount == 0)
    {
        __enable_irq();
        return;
    }

    len = usart_rb.itemCount;
    if (len >= BUUFER_SIZE)
        len = BUUFER_SIZE - 1;

    if (ringbuffer_read(&usart_rb, usart_read_buffer, len) != 0)
    {
        __enable_irq();
        return;
    }

    __enable_irq();

    usart_read_buffer[len] = '\0';
    printf("ringbuffer data: %s\r\n", usart_read_buffer);

	// 3. 初始化 strtok,并寻找包头 "start"
    char *token = strtok((char *)usart_read_buffer, ",");
    
    if (token != NULL && strcmp(token, "start") == 0) 
    {
        // --- 提取第 2 段:调制类型 ---
        token = strtok(NULL, ",");
        if(token != NULL) {
            if(strcmp(token, "AM") == 0)       g_mod_params.mod_type = 0;
            else if(strcmp(token, "ASK") == 0)  g_mod_params.mod_type = 1;
            else if(strcmp(token, "FM") == 0)   g_mod_params.mod_type = 2;
            else if(strcmp(token, "FSK") == 0)  g_mod_params.mod_type = 3;
            else if(strcmp(token, "PSK") == 0)  g_mod_params.mod_type = 4;
            else                                g_mod_params.mod_type = 5; // 其他
        }

        // --- 提取第 3 段:调制信号频率 ---
        token = strtok(NULL, ",");
        if(token != NULL) g_mod_params.mod_freq = atoi(token);

        // --- 提取第 4 段:Ma ---
        token = strtok(NULL, ",");
        if(token != NULL) g_mod_params.Ma = atof(token);

        // --- 提取第 5 段:Mf ---
        token = strtok(NULL, ",");
        if(token != NULL) g_mod_params.Mf = atof(token);

        // --- 提取第 6 段:码元速率 ---
        token = strtok(NULL, ",");
        if(token != NULL) g_mod_params.symbol_rate = atoi(token);

        // --- 提取第 7 段:包尾校验 ---
        token = strtok(NULL, ",");
        if (token != NULL && (strcmp(token, "end") == 0)) 
        {
            printf("Parse Success! Type:%d, Freq:%d\n", g_mod_params.mod_type, g_mod_params.mod_freq);
            // 解析成功后可以触发 OLED 更新
        }
    }

    // 4. 清空缓冲区,准备下次接收
    memset(usart_read_buffer, 0, BUUFER_SIZE);
}
void OLED_Proc(void)
{
    // 1. 静态变量记录上次数据
    static ModParams_t last_params; 
    
    // 2. 数据变化检查
    if (g_mod_params.mod_type == last_params.mod_type &&
        g_mod_params.mod_freq == last_params.mod_freq &&
        g_mod_params.Ma == last_params.Ma &&
        g_mod_params.Mf == last_params.Mf &&
        g_mod_params.symbol_rate == last_params.symbol_rate)
    {
        return; 
    }

    // --- 开始刷新流程 ---
    char *xinhao[] = {"AM ", "ASK", "FM ", "FSK", "PSK", "..."};
    
    // 第一行:始终显示模式
    OLED_ShowString(0, 0, "Mode:", 16, 0);
    uint8_t type = (g_mod_params.mod_type > 5) ? 5 : g_mod_params.mod_type;
    OLED_ShowString(48, 0, xinhao[type], 16, 0);

    // --- 第二行:频率逻辑处理 ---
    if (g_mod_params.mod_type == 1 || g_mod_params.mod_type == 3 || g_mod_params.mod_type == 4)
    {
        // 如果是 ASK(1), FSK(3), PSK(4),清空第二行,不显示 Freq
        OLED_ShowString(0, 2, "                ", 16, 0); 
    }
    else 
    {
        // AM 和 FM 模式显示频率
        OLED_ShowString(0, 2, "Freq:", 16, 0);
        OLED_ShowNum(48, 2, g_mod_params.mod_freq, 5, 16, 0);
        OLED_ShowString(48 + 40, 2, "Hz", 16, 0); 
    }

    // --- 第三行:参数逻辑处理 ---
    // 先清空第三行
    OLED_ShowString(0, 4, "                ", 16, 0); 

    if (g_mod_params.mod_type == 0) 
    {
        // AM 显示 Ma
        OLED_ShowString(0, 4, "Ma:", 16, 0);
        OLED_Showdecimal(32, 4, g_mod_params.Ma, 1, 2, 16, 0);
    } 
    else if (g_mod_params.mod_type == 2) 
    {
        // FM 显示 Mf
        OLED_ShowString(0, 4, "Mf:", 16, 0);
        OLED_Showdecimal(32, 4, g_mod_params.Mf, 1, 2, 16, 0);
    } 
    else if (g_mod_params.mod_type == 1 || g_mod_params.mod_type == 3 || g_mod_params.mod_type == 4)
    {
        // ASK, FSK, PSK 显示 Rate
        OLED_ShowString(0, 4, "Rate:", 16, 0);
        OLED_ShowNum(48, 4, g_mod_params.symbol_rate, 5, 16, 0);
        OLED_ShowString(48 + 40, 4, "Baud", 16, 0); // 码元速率单位通常也是Hz或Baud
    }
    else
    {
        OLED_ShowString(0, 4, "Wait Data...", 16, 0);
    }

    // 更新备份
    last_params = g_mod_params;
}

十一、总结

        本设计在1-3kHz的FM波形的Mf估计中,由于解调出的FM波形幅度非常小,故抓不到频率,后续还可改善,基本可以实现五路信号的模式自动识别,AM/ASK/PSK参数较为准确,如果PSK还是出现锁不住的情况,此时可改变一下PSK的波特率,调大或调小后更容易锁住。

        当用函数发生器输入信号的时候,要加一个测试端口,拿示波器打一下波形是否为10Vpp满幅信号,看一下输入阻抗是否为50Ω,如果输入阻抗为1MΩ则示波器会显示峰峰值为设定的一半。

        还有最重要的ILA IP核配置没有放,大家可以去网上公开资料里学习配置方法。程序的编写工作占一成,仿真工作占两成,调试工作占七成。FPGA虽数据处理能力非常迅速,但是跟shi一样的编译速度非常让人头疼,特别是到后面程序特别多的时候,编译一次要五分钟还多,一天编译不了几次,在编译的空闲时间里我甚至将权力的游戏给刷完了。

Logo

免费领 150 小时云算力,进群参与显卡、AI PC 幸运抽奖

更多推荐