FPGA实现AM调制解调:从DDS载波生成到包络检波实践
在开始之前我先交代一下背景。我这次要写的是 FPGA 上实现 AM 包络检调制解调的项目。AM 调幅广播已经有一百多年历史看起来像是一个“老掉牙”的方案但实际上它背后的信号链路包含了很多非常适合 FPGA 入门到进阶的经典模块DDS 载波生成、数字混频、包络检波、FIR 滤波、AD/DA 接口时序。如果你能把这个项目完整跑通那么之后做数字上变频、数字下变频、软件无线电甚至更复杂的数字通信算法时都会有非常扎实的底子。这篇文章既不是简单的 Demo 复现教程我也不打算把 Vivado 的每一个点击步骤都写下来而是想把这条链路从“为什么这样可以工作”到“实际调板子会碰到什么问题”从头到尾捋一遍。整个过程我会尽量还原真实开发中的思维方式包括那些失败尝试和最终选型的原因。1. 选题思路——为什么 AM 这种“老古董”还值得在 FPGA 上做1.1 从模拟包络检波到数字包络检波传统 AM 接收机的经典结构是天线接收到射频信号后经过高放、混频到中频再经过二极管包络检波、RC 低通滤波最后输出音频信号。这套电路在教科书里画出来只有几个元件但放到实际环境里问题很多。二极管的非线性特性随温度漂移、RC 低通的时间常数需要根据调制信号的最高频率精确设计、而且模拟电路元件之间存在阻抗匹配问题稍有不慎就会造成检波失真。而在 FPGA 中包络检波是一个纯粹的数学运算。ADC 以固定采样率采集到包含载波和调制信息的数字信号之后我可以选择整流取绝对值、平方开方、或者 Hilbert 变换来提取包络。这些运算在逻辑上不会有温度漂移也不会出现元件老化的问题精度完全取决于位宽和滤波器的系数设计。更重要的是算法替换的成本极低——我在硬件上只改一个系数文件和几行逻辑就相当于模拟工程师更换了一整套滤波电路。这其实也引出了我选题的一个核心动机AM 调制解调虽然简单但它能把“模拟信号数字化处理”的完整流程串起来非常直观。你不需要掌握高深的同步算法也不需要理解复杂的信道均衡就能看着示波器上的波形验证整条链路。而一旦这条链路理解了后面做 ASK、OOK、甚至 QAM 的接收解调都是在这个骨架上演变。1.2 FPGA 做 AM 调制解调的三个现实场景可能会有朋友问AM 调制解调用一个小芯片甚至纯模拟电路就能实现了为什么要大动干戈用 FPGA我梳理一下目前实际项目里常见的几个应用场景大家可以根据自己的情况参考。第一类是教学和科研验证平台。很多实验室需要让学生直观看到从调制到解调的完整信号变化过程同时又不希望学生在模拟电路上花太多时间。FPGA 配合 AD/DA 板卡可以在逻辑分析仪上同时看到调制信号、载波、已调波和解调输出这几个关键节点这是任何模拟实验箱都做不到的。第二类是软件无线电和认知无线电的基带处理原型验证。现代 SDR 系统里经常会同时存在多种调制体制AM 作为其中一种兼容模式存在。比如在应急通信频段、航空通信AM 还是 VHF 语音的主力体制中需要接收端支持 AM 解调。而接收机的中频、基带处理如果放在 FPGA 里完成全数字解调整个接收链路就可以做到全可配置。第三类是一些特殊的低频通信场景。比如说在某些工业现场传感器和上位机之间通过电力线或专用的两线制线路传输信号采用 AM 调制甚至比复杂的数字调制更可靠接收端直接做包络检波不需要额外提取位同步时钟。这时用 FPGA 实现并集成到现有采集控制板卡里比外接一个模拟解调板卡要紧凑得多。对我个人而言做这个项目的最大收益倒不是搞定了多少应用场景而是把整个调制解调链路上的每个“环节”为什么是这样设计、每个参数是怎么算出来的彻底弄明白了一遍。这些内容纯看书很容易跳过真的在逻辑上实现一遍后印象会深得多。2. 发射端实现——DDS 载波生成与数字 AM 调制2.1 DDS 基本原理与可用方案要实现 AM 调制首先要有一个可调频率、可调幅度的载波源。在 FPGA 里最直接的选择是 DDSDirect Digital Synthesis直接数字频率合成器。DDS 的核心思想可以用一句话概括用相位累加器的值做索引去一个正弦查找表里读取当前相位对应的正弦幅度值。具体来说假设参考时钟频率是 f_clk相位累加器位宽是 N频率控制字是 FTWFrequency Tuning Word那么输出频率为f_out FTW * f_clk / 2^N比如我的系统参考时钟是 50MHz累加器位宽 32 位如果要产生 1MHz 载波那么 FTW 大约是FTW 1MHz * 2^32 / 50MHz ≈ 85,899,346这里有几个比较关键的细节。相位累加器位数做大了频率分辨率极高但查表的 ROM 不可能做 2^32 这么深所以实际工程会做“相位截断”——只取高 12 位或 16 位去寻址正弦表。相位截断会引入杂散体现在频谱上就是离主频很近的旁瓣。一般来说如果相位截断位数达到 12 位以上SFDR 就能做到 70dBc 左右足以满足 AM 通信的需求。实现 DDS 有三种常见路径直接调用 Xilinx 的 DDS Compiler IP 核自己用 Verilog 写相位累加器加 ROM 查找表或者使用 Vivado 里现成的 Block Design 集成。我的建议是如果是做系统原型直接用 IP 核最省事但如果想彻底理解 DDS 内部机制建议先自己手动实现一遍然后用 IP 核替换验证结果是否一致。我自己实际调板时用的是 Xilinx DDS Compiler IP因为它的 AXI 接口、参数配置界面、以及生成的波形文件都更适合系统集成。2.2 数字 AM 调制的数学形式和防过调设计AM 调制在时域上的表达式是s(t) [A0 m(t)] * cos(2π * f_c * t)其中 A0 是载波幅度m(t) 是基带调制信号f_c 是载波频率。为了不产生过调失真必须保证 A0 m(t) 始终为正。对于单音正弦调制如果调制信号幅度是 Am那么调制指数 μ Am / A0μ 必须小于 1否则在信号负半周期会出现包络截断解调出来波形就失真了。在 FPGA 里实现这个运算并不复杂。我的做法是把 DDS 输出的正弦波幅度固定为 12 位范围从 -2048 到 2047有符号数。调制信号也是 12 位有符号数。先计算 A0 μ * m(t)再和载波做乘法。注意这里的“加法”和“乘法”在实际处理时涉及到位宽对齐——调制信号需要先经过一定逻辑以确保直流偏置 A0 存在然后与载波相乘得到 AM 已调信号。实际调试时最容易犯错的是忘记处理有符号数扩展。调制的中间结果很可能是正负交变的如果乘法器的输入没有按有符号数处理载波负半周期会被截断包络检波的结果就会完全不对。这一点在用 Verilog 写乘法的时候要特别小心。我习惯在写 RTL 前先在 MATLAB 里做定点化仿真确认每个节点的位宽和量化误差可接受后再落到代码里。另外还有一个经常踩坑的地方是 DDS 输出的直流分量。理想 DDS 输出是纯正弦没有直流分量但实际查找表里存的可能是有符号数进行后续处理时我们通常希望它是有符号表示。AM 调制需要在载波上叠加一个直流偏置 A0。这个 A0 在数字域里就是给调制信号加上一个常数偏置。比如调制信号原始幅度是 [A_m_min, A_m_max]我给它加一个 BIAS 2048让它在 [0, 4095] 范围内无符号变化然后再与载波相乘。这样的好处是让 DAC 的输出始终是正电压范围避免 DAC 本身出现半波失真。2.3 DAC 输出与频谱验证我的实验平台用的是 125MSPS 的 DAC 芯片12 位并行输入接口数据格式是二进制补码。在 Vivado 里通过一个简单的双端口 RAM 跨时钟域把逻辑侧数据送到 DAC 时钟域。这里有个经验要分享DAC 的接口时序比很多人想象的严格尤其要注意建立保持时间和 clk 到 data 的偏移约束。如果不做约束偶尔会出现一个瓜亮glitch毛刺噪声会直接体现在频谱上。上板之后一定要用频谱仪或者用高采样率示波器做 FFT确认输出频谱的形态。正常的单音 AM 已调信号的频谱应该是一个载波频率分量加两个边带分量调制指数对应的边带幅度应该比载波低 20lg(μ/2) dB 左右。如果看到频谱上多了奇怪的尖峰多半是 DDS 相位截断产生的杂散如果在载波附近有鼓包那就是调制信号里混入了直流偏置或低频噪声。我调试时碰到过一次很隐蔽的问题DDS 输出的数据速率和 DAC 的数据速率设置不一致导致输出信号在频谱上出现了很多镜像image后来查阅芯片手册发现 DAC 的输入数据必须按一致的采样率灌入否则它会周期性的“丢字”。解决方法是把 DDS 的 output clock 直接连给 DAC 的数据时钟并且保证所有处理链路都同步在这个时钟树上不要图省事在局部用高频时钟去采样低速数据。3. 接收端解调——数字包络检波的三种实现方案既然调制端只是“乘法 直流偏置”接收端的核心任务就是把包络 m(t) 恢复出来。数字域里实现包络检波有三个梯队复杂度逐渐递增恢复效果也逐级改善。我建议先把最简单的跑通再根据实际工程指标决定要不要升级。3.1 整流加 FIR 低通最简单的路这是最开始我也是这么做的。AM 信号经过 ADC 采样后得到一个数字序列 x[n]x[n] [A0 m(nT_s)] * cos(2π * f_c * nT_s)取绝对值后信号变成|x[n]| |[A0 m(nT_s)]| * |cos(...)|由于在 AM 调制设计时我们保证 A0 m(t) 始终大于 0所以 |x[n]| ≈ [A0 m(nT_s)] * |cos(...)|。注意 |cos(...)| 是一个频率为 2f_c 的周期信号因为余弦取绝对值后周期减半如果此时用一个截止频率远低于 f_c、但又高于基带信号最高频率的低通滤波器就能把 2f_c 分量滤除留下的就是 A0 m(t)即原始基带加一个直流偏置。这里有一个容易忽略的约束低通滤波器的截止频率必须同时满足两个条件——要小于两倍载频 f_c 才能有效衰减载波分量要大于基带调制信号的最高频率 f_m否则 m(t) 本身会被滤掉。比如我的系统载波是 455kHz调制信号最高频率 5kHz那么低通截止频率选在 10kHz 左右是比较合适的既能保留 5kHz 的音频又能在 455kHz 上有足够的衰减。用 Vivado FIR Compiler 生成一个 10kHz 截止频率的 FIR 滤波器非常简单关键是系数的量化位宽要够。我用 16 位系数做了一版实测在音频端的谐波失真大约在 -55dB 以下已经满足大部分需求。整条链路的 FPGA 资源消耗很小逻辑资源大概只占 ZYNQ-7020 的百分之几几乎不占 DSP Slice滤波器系数小的时候可以用分布式逻辑实现。3.2 平方律检波与开方补偿整流法虽然简单但有一个数学上的缺陷|cos(x)| 中不仅包含 2f_c 分量还包含 4f_c、6f_c 等高频谐波。如果低通滤波器的阻带衰减不够尖锐这些高频谐波会折叠回基带形成失真。更好的方案是平方律检波。平方律检波的第一步是把 x[n] 平方x²[n] [A0 m(nT_s)]² * cos²(2π * f_c * nT_s)利用三角恒等式 cos²θ (1 cos2θ)/2得到x²[n] [A0 m(nT_s)]² * (1 cos(4π * f_c * nT_s)) / 2低通滤波后2f_c 分量被去掉剩余y[n] ≈ [A0 m(nT_s)]² / 2此时再对 y[n] 做一次开方运算就恢复出了 [A0 m(nT_s)]。从数学上讲平方律检波把载波频率抬到了 2f_c而不是像整流法那样基波是 2f_c、谐波是 4f_c、6f_c因此低通滤波器的阻带要求更宽松同样的阶数能达到更好的抑制效果。FPGA 实现开方有两种思路调用 CORDIC IP 核的 Square Root 模式或者直接查表。CORDIC 的精度高但延迟较大查表速度快但位宽大了消耗 BRAM 很厉害。我实测下来如果用 24 位输入、16 位输出的 CORDIC 开方资源增加得并不多性能完全可以接受。平方律检波在 SNR 较高时比整流法的失真指标要好 3~5dB 左右。3.3 Hilbert 变换最标准的数学包络整流和平方律检波其实都是间接求包络而包络在数学上最严格的定义是调制信号取有效值——也就是解析信号取模。对一个实信号 x(t)它的解析信号定义是z(t) x(t) j * H{x(t)}其中 H{x(t)} 是 x(t) 的 Hilbert 变换。解析信号的模就是瞬时包络Envelope(t) sqrt(x²(t) H{x(t)}²)用 FPGA 实现 Hilbert 变换的标准方法是用一个全通滤波器网络一路用 FIR 实现近似 90 度相移另一路进行相应的时延补偿然后两路信号分别平方相加。这样得到的包络在数学上最准确不会因为低通滤波造成包络细节损失理论上能够完整恢复任意频率的基带信号。但是 Hilbert 变换在 FPGA 里实现相对复杂。常见的方法是用希尔伯特滤波器一个设计为 90 度相移的 FIR 滤波器对实信号进行滤波得到虚部。由于 FIR 的群延迟必须和时延补偿路径精确对齐所以在实现时通常要把“滤波器长度减一的一半”作为补偿延迟。这个对齐一旦出错包络会出现明显的频率相关失真。我第一版实现时为了省事没有补偿结果解调出来的音频就像一个浑浊的“电话声”后来加上群延迟补偿后一下子清亮了。资源成本上Hilbert 加取模需要两个实时支路、若干乘法器和一个 CORDIC。在 ZYNQ-7020 上大概占用约 8~10 个 DSP Slice对中型开发板来说完全不是问题。如果是在入门级的 Artix-7 35T 上做只要基带带宽不太高也够用。3.4 三种方案对比检波方式数学基础FPGA 资源消耗延迟适用场景整流 低通x的谐波分离最低平方律 开方cos²θ 恒等式中乘法器 CORDIC中对通带外噪声抑制要求高的场景Hilbert 变换解析信号取模高两个 FIR 支路 CORDIC高宽带信号、需要完整包络信息从我个人的工程取舍来说如果是做一个完整的通信接收机我会直接把包络检波做到 Hilbert 这一级。虽然开发时间会多出两天但换来的优势是巨大的接收机前端不需要依赖“A0 m(t) 0”这个约束即便信号出现过调比如外界干扰叠加到负半周Hilbert 方法也能正确恢复包络而整流法和平方律法在这种场景下已经完全崩溃了。4. 滤波器参数怎么定——最容易“能用但不好用”的环节4.1 设计指标的计算示例包络检波的效果好坏很大程度上取决于低通滤波器的参数。很多人直接在 Vivado FIR Compiler 里随便填一个截止频率出来的效果时好时坏就是因为没有把滤波器设计指标和系统指标对应起来。假设我们的目标系统指标是音频带宽 5kHz调制载波 455kHzADC 采样率 fs 2.5MSPS。包络检波后信号中有用的基带分量在 0~5kHz需要滤除的分量在 2*f_c - 5kHz 处也就是 905~915kHz。因此低通滤波器的技术指标应该是通带截止频率5kHz阻带起始频率900kHz考虑过渡带余量通带纹波 0.1dB阻带衰减 60dB用 MATLAB fdatool 计算要达到 60dB 阻带衰减、过渡带宽度 895kHzFIR 滤波器阶数只需要十几阶就够了因为过渡带相对于采样率来说非常窄滤波器效率比较高。但这里有个反直觉的地方如果 ADC 采样率很低比如只有 1MSPS那么 2f_c 频率已经高于奈奎斯特频率会出现频谱混叠此时低通滤波器设计得再好也无济于事。所以包络检波前一定要保证 ADC 采样率远高于 2f_c。反过来如果调制信号带宽变宽了比如要传输 20kHz 的音频滤波器过渡带就只有 900kHz - 20kHz 880kHz滤波器阶数同样不高。真正让滤波器阶数暴涨的是采样率非常高而通带极窄的情况比如采样率 100MSPS、只恢复 5kHz 以下的包络这时 FIR 的阶数可能要几百甚至上千阶消耗的 DSP Slice 会非常多。遇到这种情况就得考虑多速率处理先 CIC 滤波抽取再 FIR 整形而不是一上来就扛一个超长 FIR。4.2 Vivado FIR IP 核的配置要点如果用 Xilinx FIR Compiler我建议注意这几个配置项。输入数据位宽和系数位宽要保持良好的匹配。以我的项目为例ADC 输出 12 位有符号数FIR IP 输入选 12 位系数选 16 位输出位宽在 1216ceil(log2(N)) 的范围内自动扩展但实际使用时我习惯设成 24 位再手动截取需要的位宽。这样中间的累加不会溢出最后截位时保留有效位。滤波结构选择“转置形式”Transposed Form因为它天然支持流水线时钟频率可以拉得比较高而且每个抽头之间的关键路径短。如果是低阶滤波器用“脉动结构”也可以但语言复述时记不清配置的具体名称总之在 Select Architecture 里优先推荐能满足时序要求的那种。系数文件的格式一般是 COE 文件。用 MATLAB 生成的系数是浮点小数需要按固定点数转换为整数格式。Vivado 里要填好 Coefficient Width 和 Coefficient Fractional Bits比如系数最大 0.5量化到 16 位有符号小数位宽就是 15。这里千万不能把小数位宽填错否则滤波器幅频响应会完全乱掉。还有一点很多人会忽略FIR Compiler IP 的输出需要经过一定的“滤波延迟”。这个延迟会体现在整条信号链上。如果后面还有其他需要对齐的模块比如和接收端的符号时钟配合就一定要提前在总时序里预留延迟补偿。不要等系统联调发现问题再匆忙在前面加延迟 FIFO那会非常痛苦。5. 联调中我踩过的几个坑5.1 ADC 采样时钟与 DAC 时钟不同源这是第一个把我坑到的点。发射端 DDS 用的是 125MHz 的 DAC 时钟接收端 ADC 用的是一个独立的 50MHz 晶振。在单机调试时各自都正常但一旦把发射端和接收端放在同一个系统里就开始出现一种“间歇性”解调失败——包络时好时坏。排查过程是这样的先用逻辑分析仪观察解调前的 ADC 数据发现在某几个采样点会出现明显的相位跳变。后来用示波器对比 DAC 的输出和 ADC 的时钟信号发现两者的相位不是固定关系而是缓慢漂移。这会导致 ADC 每次以不同的相对相位去采样载波包络自然就起伏不定。解决方法是把 DAC 时钟和 ADC 采样时钟统一到一个时钟源。我最后用了一个 PLL 同时生成 125MHz 和 50MHz确保它们是同源锁定关系。如果系统里没有 PLL也可以把 DAC 的 125MHz 分频出 50MHz 给 ADC 用注意分频后的时钟抖动对 ADC 采样精度的影响。这件事让我养成了一个习惯任何调制解调系统只要模拟前端存在多个时钟第一件事检查时钟树是不是同源。用 Locked 信号做复位仲裁保证在 PLL 稳定之后再启动数据链路。5.2 直流偏置带来的解调失真另一个让我调了很久的问题是解调输出中始终有一个接近 1.2V 的直流分量。按理论分析包络检波输出的直流偏置应该等于 A0也就是载波幅度。但我的 A0 明明是数字域里的 4096 对应的模拟电压 2V解调输出却总差了几百毫伏。深入排查发现ADC 的输入端有一个交流耦合电容把信号的直流分量滤掉了。我不小心把 ADC 的输入配置成了 AC-coupled结果包络检波恢复出来的信号丢失了直流失真程度非常高。无论是直接用整流法还是 Hilbert 法只要前端信号链路没有把直流偏置保留住后面的运算结果都会和理论值不符。后来改为 DC-coupled 模式同时在 FPGA 内部重新叠加一个固定偏置来补偿 ADC 前端偏置电压的偏移最终解调出来的直流分量才和理论值吻合。这个问题的教训是当电路板上存在电容、变压器、甚至有源滤波器时一定要确认它们对直流和低频分量的频率响应是否符合系统需求。AM 信号的包络是低频分量不像纯载波那样能随便隔直。5.3 群延迟对不同频率分量包络的影响这个问题是在做调制信号频率扫描测试时发现的。我用一个扫频源从 100Hz 扫到 4kHz发现解调输出在低频段幅度正常但到 3kHz 以上明显衰减并在 3.5kHz 左右出现一个小小的谐振峰。原因是低通滤波器虽然设计成 10kHz 截止但在 3~5kHz 范围内群延迟并不是恒定的。对于包络检波来讲不同频率分量的群延迟不一致会导致包络波形失真。FIR 滤波器实际上是线性相位对称系数理论上群延迟恒定但我在 Vivado FIR 配置时选择了“Best Resource”优化模式它自动把滤波器改造成了最小相位形式群延迟不再恒定。后来在配置里强制使用对称系数结构保证了线性相位特性这个谐振峰就消失了。所以在配置滤波器时如果会对解调后的基带信号质量有较高要求尽量保持线性相位而不要为了省几个乘法器去选择最小相位结构。5.4 内部节点观察与 ILA 调试技巧FPGA 调试最怕的是“信号链太长出了问题不知道在哪一级”。我做这个项目时在 RTL 里预先定义了几个关键的内部探测点DDS 载波输出、AM 已调波、ADC 后数据、整流输出、滤波输出、开方或 Hilbert 的输出。然后把这些节点全部引到一个 ILA 核上。ILA 的触发条件我设成了解调输出信号的上升沿。这样每次上电后只需要在发射端给一个单音调制信号就能在 ILA 里观察到一整段完整的信号链路波形。对照理想仿真波形很快能定位是哪一级出了问题。有个细节是 ILA 的采样深度要足够如果采样深度太浅可能只看到一段包络的瞬态看不出周期性的问题。我一般设成 4096 或者 8192 深度采样周期选 50MHz这样可以看到约 80~160 微秒时长的信号足够覆盖几个完整调制周期。6. 从包络检波走向更现代的信号处理6.1 为什么 AM 发射机可以直接复用 I/Q 调制器的骨架做完 AM 解调之后我稍微扩展了一下思路。很多现代调制方式比如 QPSK、QAM在 FPGA 里都是用 I/Q 两路正交调制实现的。AM 调制其实可以看作 Q 路为 0、I 路直接承载调制信号的特殊情况。也就是说调制的数学表示是I(t) A0 m(t), Q(t) 0然后经过正交调制器输出s(t) I(t) * cos(ω_c t) - Q(t) * sin(ω_c t) [A0 m(t)] * cos(ω_c t)这就和 AM 调制的表达式完全一致。所以从 RTL 架构上看AM 调制器根本不需要专门设计只要在你的 I/Q 调制链路里把 Q 路置零把基带信号送到 I 路就能输出标准 AM 信号。这个发现让我重新审视了很多“专用模块”——本质上它们往往是更通用结构的一个特例。这也意味着如果后面想从 AM 升级为其他调制方式发射端的改动成本极低只需要换一个基带映射表格和成型滤波器DAC 链路和载波生成的 DDS 部分完全复用。6.2 数字包络检波在低信噪比环境的边界最后我必须提醒一点包络检波在低信噪比下的性能是有天然极限的。当接收信号的信噪比很低时包络检波器的输出会产生一个所谓的“倒拖”现象——噪声的包络和信号的包络混在一起造成输出 SNR 严重恶化。在很多通信理论教材里包络检波的门限效应大概在 SNR 低于 0dB 时开始明显。而上限性能更好的方案是同步检波相干解调它需要一个本地载波恢复环路。如果只做 AM 广播频段的语音接收信噪比通常都很高包络检波完全够用。但如果你的应用场景是弱信号接收比如远距离数据遥测建议认真考虑把接收链路改成 I/Q 下变频 载波恢复的架构而不是在包络检波上投入更多滤波资源。我在项目末尾已经预留了接收链路的 I/Q 接口扩展为的就是以后可以平滑升级到相干解调方案。各位如果要在自己的板子上复现这套东西建议从最基础的“整流 FIR 低通”开始跑通整条链路再逐级切换到平方律检波或 Hilbert 变换。这样既有阶段性成就感也能在每一个环节感受到算法升级带来的变化。DDS、FIR、CORDIC、ILA 这些 IP 核的配置细节熟悉一遍之后后续做更复杂的调制解调项目会顺很多。