☰
基于FPGA的高速ASK信号调制与解调:从原理到工程实践
2026/10/15 13:08:01 网站建设 项目流程

简介:本资源面向FPGA数字通信方向的学习者与工程实践者,提供一套基于Altera FPGA的高速ASK调制解调完整工程。核心设计采用10MHz正弦载波,基带信号由16阶伪随机序列产生,码元速率达5Mbps,并利用数字锁相环实现载波同步与位同步,最终完成ASK信号的调制与解调,已通过ModelSim仿真和SignalTap板级调试验证。压缩包共1248个文件,约31.24MB,以Verilog源码(300个v)、VHDL与TDF文件、Quartus工程配置(qsf、qpf、sof等)、仿真与综合中间文件(cdb、hdb、vo)及板级调试截图为主,另含readme说明与Tcl脚本,完整保留了从源码到比特流的工程链路。目前已有1375人学习下载。读者可借此掌握数字锁相环载波同步、伪随机序列生成、ASK调制解调链路搭建与板级验证的完整流程,并参考调试截图与工程配置快速复现实验,适合作为课程设计、竞赛或自学的实战参考。

1. 从一次误码率压不下去的调试说起

做 FPGA 数字信号处理的人,迟早会撞上 ASK 调制解调这个经典题目。我最早接触它是在一个短距离数据链项目里,基带侧用 FPGA 产生高速 ASK 调制波形,接收侧再做非相干解调。当时想得很简单:ASK 不就是有载波代表 1、无载波代表 0 吗,写个计数器控制载波通断就完事了。结果上板一测,误码率在 10⁻³ 附近怎么都压不下去,眼图糊成一团,这才意识到高速 ASK 的坑全在细节里——载波相位连续性、包络检波的时间常数、判决门限的自动跟踪,每一个都能让链路翻车。

这篇笔记就围绕「基于 FPGA 的高速 ASK 信号调制和解调」这条链路,把我在实际工程里踩过的坑、调过的参数、验证过的方法完整讲一遍。适合两类人看:一类是刚拿到类似课题、需要从零搭一套可上板验证的 ASK 收发链路的工程师;另一类是把 ASK 当成教学案例、但想搞清楚工程实现和仿真差距在哪的开发者。核心结论先放这里:高速 ASK 的难点不在调制,而在解调的包络恢复和判决门限自适应,FPGA 的优势是能把这两件事做成全流水线、确定性延迟的硬件逻辑。

2. 高速 ASK 调制在 FPGA 里到底怎么落地

2.1 调制方式的选型:为什么是 OOK 而不是多电平 ASK

ASK 家族里,最常用的是二进制 OOK(On-Off Keying),也就是用载波的有无表示 1 和 0。多电平 ASK(比如 4ASK)虽然频谱效率高,但在高速场景下对 DAC 线性度、信道信噪比、解调判决电平的要求都成倍上升。我一般建议:如果数据率在几十 Mbps 到几百 Mbps 之间、信道是短距离有线或简单无线,直接上 OOK,把复杂度留给解调侧的包络检测和门限跟踪。

OOK 的数学表达很干净:s(t) = A·m(t)·cos(2πf_c·t),其中 m(t) 是单极性基带码流,取值 0 或 1。FPGA 实现时,本质就是用一个高速载波计数器和一个基带码流做门控。但这里有个关键选择:门控方式决定了频谱特性。

常见做法有两种。第一种是直接门控,载波在码元边界硬切换,频谱旁瓣衰减慢,带外泄漏大。第二种是加成形滤波,在码元跳变处做升余弦过渡,频谱更干净,但需要额外的成形逻辑和更高的时钟余量。我在短距离有线场景下一般用直接门控,因为信道带宽足够;如果是无线或对频谱模板有要求的场合,会加一个简单的余弦成形。

2.2 载波生成:DDS 还是计数器分频

载波生成有两种主流方案。一是用 DDS(直接数字频率合成)IP 核,频率分辨率高、相位可连续调;二是用计数器对系统时钟分频,逻辑简单、资源占用少。高速 ASK 里我倾向用计数器分频,原因是 OOK 的载波频率通常是系统时钟的整数分频,用计数器最直接,而且相位连续性问题更容易控制。

下面是一个参数化的载波生成与 OOK 门控模块,系统时钟 200 MHz,载波频率 25 MHz,即 8 分频:

module ook_modulator #( parameter SYS_CLK_FREQ = 200_000_000, // 系统时钟频率 parameter CARRIER_FREQ = 25_000_000, // 载波频率 parameter DATA_WIDTH = 1 // 基带位宽,OOK 为 1 )( input wire clk, // 系统时钟 input wire rst_n, // 低有效复位 input wire data_in, // 基带码流输入 input wire data_valid, // 码流有效标志 output reg ask_out // ASK 调制输出 ); // 计算分频比,确保为整数 localparam integer DIV_RATIO = SYS_CLK_FREQ / CARRIER_FREQ; localparam integer CNT_WIDTH = $clog2(DIV_RATIO); reg [CNT_WIDTH-1:0] carrier_cnt; reg carrier_pulse; // 载波计数器:0 到 DIV_RATIO-1 循环 always @(posedge clk or negedge rst_n) begin if (!rst_n) begin carrier_cnt <= 0; carrier_pulse <= 0; end else begin if (carrier_cnt == DIV_RATIO - 1) begin carrier_cnt <= 0; carrier_pulse <= 1'b1; end else begin carrier_cnt <= carrier_cnt + 1'b1; carrier_pulse <= 1'b0; end end end // 门控输出:载波脉冲与基带码流相与 always @(posedge clk or negedge rst_n) begin if (!rst_n) ask_out <= 1'b0; else if (data_valid) ask_out <= carrier_pulse & data_in; else ask_out <= 1'b0; end endmodule

这段代码的逻辑很直白:carrier_cnt每个时钟周期加一,计满DIV_RATIO-1后归零并产生一个周期的carrier_pulse。ask_out就是载波脉冲和基带码流的与运算。参数说明:SYS_CLK_FREQ和CARRIER_FREQ必须满足整除关系,否则分频比取整会带来载波频率偏差;DATA_WIDTH在 OOK 下固定为 1,如果以后要扩展到多电平 ASK,这里需要改成多位并调整门控逻辑。

注意:data_valid为低时强制输出 0,这是为了防止基带码流无效期间载波泄漏,实际工程里这个细节能省掉很多莫名其妙的底噪问题。

2.3 码元速率与载波频率的约束关系

高速 ASK 里,码元速率 R_b 和载波频率 f_c 的关系直接决定了解调侧能不能干净地恢复包络。经验规则是 f_c ≥ 4·R_b,最好能到 8 倍以上。原因在于包络检波后的低通滤波需要至少几个载波周期来平滑,如果每个码元里只有一两个载波周期,包络会严重失真,判决门限根本稳不住。

举个例子:数据率 50 Mbps,载波至少 200 MHz,最好 400 MHz。如果系统时钟只有 200 MHz,那载波最多 100 MHz(2 分频),此时 f_c/R_b = 2,解调会非常吃力。这种情况下要么提高系统时钟,要么降低数据率,没有第三条路。我见过有人试图用数字滤波强行恢复,结果资源翻倍、延迟爆炸,误码率还是下不去。

2.4 上板前的仿真验证:看什么波形

调制侧仿真主要看三件事:载波频率是否准确、码元边界处是否有毛刺、输出频谱是否可接受。Testbench 里给一段伪随机码流,跑 10 μs,看ask_out的波形。如果码元边界出现窄脉冲毛刺,通常是data_in和carrier_pulse的时序对齐问题,可以在data_in路径上加一级寄存器打拍。

// 简化 testbench 片段 initial begin clk = 0; forever #2.5 clk = ~clk; // 200 MHz end initial begin rst_n = 0; data_in = 0; data_valid = 0; #100 rst_n = 1; #20 data_valid = 1; // 发送 10110010 码流,每比特 20 ns(50 Mbps) send_bit(1); send_bit(0); send_bit(1); send_bit(1); send_bit(0); send_bit(0); send_bit(1); send_bit(0); #100 $stop; end task send_bit(input bit_val); data_in = bit_val; #20; endtask

仿真里重点观察ask_out在码元切换时是否干净。如果发现载波相位在码元边界不连续,那是门控逻辑用了组合逻辑导致的,改成时序门控即可。

3. 解调侧:包络检波与判决门限的硬件实现

3.1 非相干解调的整体架构

高速 ASK 解调首选非相干方案,因为相干解调需要载波恢复环路,在高速下锁相环的捕获时间和稳定性都是麻烦。非相干解调的核心是包络检波加判决,链路是:ADC 采样 → 数字下变频(可选)→ 包络提取 → 低通滤波 → 判决门限比较 → 码元同步 → 输出基带。

如果 ADC 采样率足够高(比如 4 倍载波频率以上),可以直接对中频采样做包络提取。包络提取最简单的方法是取绝对值,然后低通滤波。更精细的做法是 I/Q 下变频后取模,但资源消耗大。我在 200 Mbps 以下的场景里一般用绝对值加滑动平均,效果够用。

3.2 包络检波的 FPGA 实现:绝对值加滑动平均

下面是一个包络检波模块,输入是 12 位 ADC 采样,输出是 16 位包络值。滑动平均窗口长度设为 8,对应载波周期的四分之一左右:

module envelope_detector #( parameter ADC_WIDTH = 12, parameter ENV_WIDTH = 16, parameter WIN_LEN = 8 // 滑动平均窗口长度 )( input wire clk, input wire rst_n, input wire signed [ADC_WIDTH-1:0] adc_data, // 有符号 ADC 采样 input wire adc_valid, output reg [ENV_WIDTH-1:0] envelope // 包络输出 ); // 绝对值 wire [ADC_WIDTH-1:0] abs_val; assign abs_val = adc_data[ADC_WIDTH-1] ? (~adc_data + 1'b1) : adc_data; // 滑动平均:用移位寄存器实现 reg [ADC_WIDTH-1:0] shift_reg [0:WIN_LEN-1]; reg [ENV_WIDTH-1:0] sum; integer i; always @(posedge clk or negedge rst_n) begin if (!rst_n) begin for (i = 0; i < WIN_LEN; i = i + 1) shift_reg[i] <= 0; sum <= 0; envelope <= 0; end else if (adc_valid) begin // 移位 for (i = WIN_LEN-1; i > 0; i = i - 1) shift_reg[i] <= shift_reg[i-1]; shift_reg[0] <= abs_val; // 求和(简化写法,实际综合时会优化成滑动求和) sum <= sum - shift_reg[WIN_LEN-1] + abs_val; // 除以窗口长度,右移 log2(WIN_LEN) 位 envelope <= sum >> 3; // WIN_LEN=8 时右移 3 位 end end endmodule

逻辑说明:先对 ADC 采样取绝对值,得到单极性包络的粗值;然后用长度为WIN_LEN的滑动平均滤掉载波纹波。sum的更新用了滑动求和技巧,避免每个周期重新累加所有寄存器,节省资源。参数说明:WIN_LEN的选择很关键,太小则载波纹波滤不干净,太大则码元边沿被平滑掉。经验值是窗口时间约等于载波周期的 1/4 到 1/2。如果载波 100 MHz、系统时钟 200 MHz,载波周期 2 ns 对应 4 个时钟周期,窗口取 1 到 2 即可;如果载波 25 MHz、系统时钟 200 MHz,载波周期 8 ns 对应 16 个时钟周期,窗口取 4 到 8。

注意:右移位数必须和WIN_LEN匹配,WIN_LEN=8时右移 3 位,WIN_LEN=16时右移 4 位。如果窗口长度不是 2 的幂,需要做除法,资源消耗会明显上升,所以尽量选 2 的幂。

3.3 判决门限的自适应跟踪:峰值检测加中点判决

固定门限在高速 ASK 里基本不可用,因为信号幅度会随距离、信道衰减、温度漂移。我一般用峰值检测加中点判决:跟踪包络的最大值和最小值,取中点作为判决门限。这样即使信号幅度整体漂移,门限也能跟着走。

module adaptive_slicer #( parameter ENV_WIDTH = 16, parameter ALPHA_SHIFT = 6 // 跟踪系数,越小跟踪越慢 )( input wire clk, input wire rst_n, input wire [ENV_WIDTH-1:0] envelope, input wire env_valid, output reg bit_out ); reg [ENV_WIDTH-1:0] peak_max; reg [ENV_WIDTH-1:0] peak_min; reg [ENV_WIDTH-1:0] threshold; always @(posedge clk or negedge rst_n) begin if (!rst_n) begin peak_max <= 0; peak_min <= {ENV_WIDTH{1'b1}}; threshold <= 0; bit_out <= 0; end else if (env_valid) begin // 峰值跟踪:缓慢逼近当前包络 if (envelope > peak_max) peak_max <= envelope; else peak_max <= peak_max - (peak_max >> ALPHA_SHIFT); if (envelope < peak_min) peak_min <= envelope; else peak_min <= peak_min + (({ENV_WIDTH{1'b1}} - peak_min) >> ALPHA_SHIFT); // 中点判决 threshold <= (peak_max + peak_min) >> 1; bit_out <= (envelope > threshold) ? 1'b1 : 1'b0; end end endmodule

逻辑说明:peak_max和peak_min分别跟踪包络的上下界。当包络超过当前最大值时立即更新,否则缓慢衰减;最小值同理。ALPHA_SHIFT控制跟踪速度,值越大跟踪越慢、越稳,但适应信道变化的能力越弱。参数说明:ALPHA_SHIFT=6意味着每次衰减约 1/64,在 200 MHz 时钟下时间常数约 320 ns,适合码元速率几十 Mbps 的场景。如果码元速率更高,需要减小ALPHA_SHIFT让跟踪更快。

3.4 码元同步:过采样与边沿检测

判决输出的是包络比较结果,还需要码元同步才能恢复出正确的比特流。常用方法是过采样加边沿检测:用 4 倍或 8 倍码元速率的时钟采样判决输出,检测跳变边沿,然后在码元中点采样。FPGA 里可以用一个简单的状态机实现。

module symbol_sync #( parameter OVERSAMPLE = 8 // 过采样倍数 )( input wire clk, input wire rst_n, input wire bit_in, // 判决输出 output reg bit_out, // 同步后比特 output reg bit_valid // 比特有效 ); reg [2:0] sample_cnt; reg last_bit; reg sync_found; always @(posedge clk or negedge rst_n) begin if (!rst_n) begin sample_cnt <= 0; last_bit <= 0; sync_found <= 0; bit_out <= 0; bit_valid <= 0; end else begin bit_valid <= 0; if (!sync_found) begin // 等待边沿 if (bit_in != last_bit) begin sample_cnt <= 0; sync_found <= 1; end last_bit <= bit_in; end else begin sample_cnt <= sample_cnt + 1; if (sample_cnt == OVERSAMPLE/2 - 1) begin // 码元中点采样 bit_out <= bit_in; bit_valid <= 1; end else if (sample_cnt == OVERSAMPLE - 1) begin sample_cnt <= 0; end end end end endmodule

逻辑说明:sync_found为 0 时等待bit_in的跳变边沿,检测到后从 0 开始计数。计数到OVERSAMPLE/2 - 1时正好是码元中点,此时采样输出。参数说明:OVERSAMPLE越大同步精度越高,但需要更高的采样时钟。一般取 8 就够,对应 200 MHz 时钟可以支持 25 Mbps 的码元速率。如果码元速率更高,需要提高时钟或降低过采样倍数。

4. 避坑与排查:高速 ASK 链路的五个血泪教训

4.1 误码率居高不下,眼图闭合

现象:上板后误码率在 10⁻³ 量级,示波器看解调眼图几乎闭合。

原因:最常见的是包络检波的滑动平均窗口太长,把码元边沿平滑掉了。另一个可能是判决门限跟踪太慢,信号幅度变化时门限没跟上。

解决:先把WIN_LEN减半试,观察眼图是否张开。如果改善明显,说明窗口过长。然后调小ALPHA_SHIFT让门限跟踪更快。我一般会留一个调试寄存器,可以在线改这两个参数,不用重新综合。

4.2 码元边界出现随机误判

现象:误码集中在码元跳变附近,连续相同码元时反而正确。

原因:码元同步的采样点没对准中点,或者过采样倍数不够导致采样相位抖动。

解决:检查symbol_sync的OVERSAMPLE参数,确保采样时钟是码元速率的整数倍且相位稳定。如果时钟抖动大,可以在判决输出后加一级寄存器打拍,减少亚稳态影响。

4.3 载波泄漏导致底噪抬高

现象:发送 0 时输出仍有残留载波,接收侧底噪明显。

原因:门控逻辑用了组合逻辑,data_in变化时ask_out有毛刺;或者data_valid无效时没有强制输出 0。

解决:把门控改成时序逻辑,ask_out用寄存器输出;确保data_valid为低时ask_out强制为 0。这个坑我在两个项目里都踩过,后来养成习惯:所有输出端口都过一级寄存器。

4.4 高速下时序不收敛

现象:综合后时序报告显示建立时间违例,上板行为不稳定。

原因:载波计数器或滑动平均的加法链太长,关键路径延迟大。

解决:把滑动平均的求和拆成流水线,或者用移位寄存器加并行加法树代替串行累加。载波计数器如果位宽大,可以拆成两级计数。另外,envelope_detector里的sum更新路径是典型的组合逻辑长路径,加一级寄存器打拍能明显改善时序。

4.5 ADC 采样率与载波频率不匹配

现象:包络检波输出纹波大,判决结果抖动。

原因:ADC 采样率低于载波频率的 4 倍,欠采样导致包络信息丢失。

解决:提高 ADC 采样率,或者在 ADC 前加模拟包络检波器,把中频信号先检波成基带包络再采样。后者适合载波频率很高的场景,但会增加模拟电路复杂度。我一般优先提高采样率,因为数字侧的处理更可控。

5. 进阶技巧:用在线误码统计反推链路余量

链路调通之后,怎么判断它离极限还有多远?我的习惯是在 FPGA 里加一个在线误码统计模块,用伪随机码流做测试图案,统计误码数和总比特数,实时输出误码率。这个模块不占太多资源,但能让你在调试时心里有数。

具体做法是:发送侧用一个 LFSR 产生伪随机码流,接收侧用相同的 LFSR 同步后比对。LFSR 多项式选 x¹⁵ + x¹⁴ + 1,周期 32767,足够覆盖测试需求。统计模块每 10⁶ 个比特输出一次误码率,通过 UART 或 SPI 读到上位机。

module ber_monitor #( parameter CNT_WIDTH = 20 // 统计窗口 2^20 ≈ 1M 比特 )( input wire clk, input wire rst_n, input wire rx_bit, // 接收比特 input wire rx_valid, // 比特有效 input wire tx_bit, // 本地参考比特(LFSR 同步后) output reg [CNT_WIDTH-1:0] bit_cnt, output reg [CNT_WIDTH-1:0] err_cnt, output reg ber_valid ); always @(posedge clk or negedge rst_n) begin if (!rst_n) begin bit_cnt <= 0; err_cnt <= 0; ber_valid <= 0; end else if (rx_valid) begin bit_cnt <= bit_cnt + 1'b1; if (rx_bit != tx_bit) err_cnt <= err_cnt + 1'b1; if (bit_cnt == {CNT_WIDTH{1'b1}}) begin ber_valid <= 1'b1; bit_cnt <= 0; err_cnt <= 0; end else begin ber_valid <= 1'b0; end end end endmodule

逻辑说明:bit_cnt和err_cnt在每个有效比特上更新,计满一个窗口后ber_valid拉高一个周期,外部逻辑可以在这时读取误码率。参数说明:CNT_WIDTH=20对应约 100 万比特的统计窗口,在 100 Mbps 下约 10 ms 更新一次,足够实时观察。如果误码率很低(比如 10⁻⁶ 以下),需要加大窗口才能看到统计意义。

有了这个模块,你可以做一件很有价值的事:逐步降低发送幅度或增加信道衰减,观察误码率从 0 到 10⁻⁶ 再到 10⁻³ 的拐点,这个拐点就是链路余量。我一般会留 3 dB 以上的余量,因为温度变化和器件老化都会吃掉余量。

最后一个习惯:每次改完参数,先跑仿真确认功能,再上板看误码率,最后用在线统计模块记录不同条件下的数据。这套流程看起来慢,但比反复上板盲调快得多。希望帮到你。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询