ARTICLE DETAIL

资讯详情

深耕商务建站与企业官网运营的一线实战洞察。

FPGA DDS ADDA测试工程:位宽、SPI配置与时序联调

FPGA DDS ADDA测试工程:位宽、SPI配置与时序联调 简介面向FPGA数字信号处理与ADDA闭环实验的DDS测试工程适合具备Verilog/VHDL基础、需完成DAC波形合成与ADC采集频谱验证的学习者。工程以50MHz时钟驱动DDS IP输出正弦波将DAC模拟输出自环至ADC由MMCM分频提供25MHz采样时钟并使用ILA捕获不少于2048点数据配合Matlab完成频谱分析还可通过VIO修改频率字生成1MHz和3MHz信号核对频谱正确性。使用中需注意AN108为34针插头1脚与Zynq底板对齐其低通滤波器通带约0-20MHz。压缩包共613个文件、约46.11MB以Verilog/VHDL源码、约束与工程脚本、文本记录、Matlab及批处理文件为主并含bit与ILA数据便于复现、对照和排错。已有321人学习可作为课程实验、项目验证与自学参考。1. 一块收发板卡调不通问题往往出在 DDS 那笔位宽账上某开发者接手一块带 DAC 和 ADC 的板卡DAC 口接上示波器要么一条直线要么频谱上杂散一大片。他先怀疑焊接又怀疑芯片换了两片依旧。最后把工程翻出来逐行看才发现相位累加器的截断位选错了SPI 配置里还漏写了一个分频寄存器。这套 dds adda 测试工程文件就是一条从数字域生成波形、送进 DAC、再由 ADC 采回数字域的完整链路参考实现包含 DDS 内核、DAC 驱动、ADC 采集、SPI 配置、仿真 testbench 和约束文件。它适合三类人刚上手 FPGA 想练一个完整闭环的初学者、正在做信号源或采集板需要快速验证 ADDA 芯片的硬件工程师、以及想拿一套可改的骨架去派生产品的开发者。下面按代码结构一层层拆。2. DDS 内核拆解32 位相位累加器到 12 位波形输出的位宽账DDS 的核心只有两件事一个每个时钟加一次的相位累加器和一张把相位映射成幅度的查找表。听起来简单但输出频率准不准、频谱干不干净全压在这两个模块的位宽选择上。这一章把内核拆成三块讲累加器与频率控制字、查找表的生成与例化、以及用 testbench 把结果对一遍。2.1 相位累加器与频率控制字输出频率怎么算准相位累加器的本质是一个模 2^N 的计数器每个采样时钟加上一个固定的步进值这个步进就是频率控制字。N 选 32 位是这类工程的常见做法因为 32 位配 50 MHz 采样率能给出约 0.0116 Hz 的频率分辨率做扫频和频点微调都够用如果只做固定频点输出24 位也能凑合但一旦要改频就会明显感觉到台阶。// dds_core.v —— 相位累加器 正弦查找表 module dds_core #( parameter PHASE_W 32, // 相位累加器位宽决定频率分辨率 fs/2^PHASE_W parameter LUT_AW 10, // 查找表地址位宽即保留的高位数 parameter DATA_W 12 // 输出幅度位宽决定量化噪声底 )( input wire clk, input wire rst_n, input wire [PHASE_W-1:0] ftw, // 频率控制字 output reg [DATA_W-1:0] wave_out ); reg [PHASE_W-1:0] phase_acc; wire [LUT_AW-1:0] lut_addr; // 取高 LUT_AW 位做地址低 (PHASE_W-LUT_AW) 位被丢弃 assign lut_addr phase_acc[PHASE_W-1 -: LUT_AW]; // 1024 点正弦表12 bit 无符号中心码 2048 reg [DATA_W-1:0] sin_lut [0:(1LUT_AW)-1]; always (posedge clk or negedge rst_n) begin if (!rst_n) begin phase_acc {PHASE_W{1b0}}; wave_out {DATA_W{1b0}}; end else begin phase_acc phase_acc ftw; // 每拍累加一次 wave_out sin_lut[lut_addr]; // 查表输出固定一拍延迟 end end endmodule输出频率的公式是fout ftw × fs / 2^PHASE_W其中 fs 是驱动这个模块的采样时钟。举个具体数字fs 取 50 MHz想要 1 MHz 输出ftw 1e6 × 2^32 / 50e6 ≈ 85899346。这个数在写寄存器或上位机下发时要按整数处理所以实际输出会有极小的频偏做频率精度测试时把这个偏差算进去别把它当成芯片问题。lut_addr那行用了 Verilog 的位选语法[PHASE_W-1 -: LUT_AW]意思是从最高位往下取 LUT_AW 位。这一句就是相位截断发生的地方也是后面频谱杂散的主要来源之一位宽账算不清杂散就会来敲门。2.2 波形查找表生成与例化LUT 深度和幅度位宽怎么定查找表不用手写用脚本生成一个 hex 文件综合时用$readmemh读进去。之所以用 hex 而不是直接写initial块是因为 1024 个点手写容易错位而且不同综合工具对initial推断 ROM 的支持程度不一样hex 加显式 ROM 原语是最稳的组合。# gen_lut.py —— 生成 1024 点 12 bit 正弦表 import numpy as np LUT_AW, DATA_W 10, 12 n 1 LUT_AW t np.arange(n) / n # 正弦搬到 [0, 2^DATA_W-1] 的无符号区间中心码为 2048 wave np.round((np.sin(2 * np.pi * t) 1) / 2 * (2**DATA_W - 1)).astype(int) with open(sin_lut.hex, w) as f: for v in wave: f.write(f{v:03x}\n) print(points , n, max , wave.max(), min , wave.min())参数怎么定取决于你想要的指标。查找表深度 LUT_AW 决定相位量化台阶10 位就是 1/1024 个周期对应的相位截断杂散大约在 -60 dBc 量级如果做通信信号源要求 -80 dBc 以上就得把 LUT_AW 提到 12 到 14或者上相位抖动技术。幅度位宽 DATA_W 决定量化噪声12 bit 对应的理论 SNR 约 74 dB一般测试够用要做到 16 bit 就得确认 DAC 本身的有效位数撑不撑得住别出现前端很干净、后端芯片先饱和的情况。还要注意 ROM 的推断方式。在综合工具里显式例化块 RAM 或 ROM 原语把初始化文件绑定到原语上比让工具自己从case语句推断更可控。我一般会在顶层单独放一个rom_sin包装模块把原语和初始化路径都收进去改位宽时只动这一个文件。2.3 仿真验证用 Testbench 把频率、幅度和杂散对一遍上板之前先在仿真里把数对清楚能省掉大量示波器前的来回。Testbench 要做三件事给时钟和复位、下发一个已知的 ftw、把输出抓下来做频谱分析。// tb_dds.v —— 1 MHz 输出验证 timescale 1ns/1ps module tb_dds; reg clk, rst_n; reg [31:0] ftw; wire [11:0] wave; // 50 MHz 采样时钟 initial clk 1b0; always #10 clk ~clk; dds_core #(.PHASE_W(32), .LUT_AW(10), .DATA_W(12)) u_dds ( .clk(clk), .rst_n(rst_n), .ftw(ftw), .wave_out(wave) ); initial begin rst_n 1b0; ftw 32d0; #100 rst_n 1b1; // fout ftw * 50MHz / 2^32 1MHz ftw 32d85899346; #200000 $stop; end // 把输出写文件交给脚本做 FFT integer fd; initial fd $fopen(wave_out.txt, w); always (posedge clk) if (rst_n) $fdisplay(fd, %d, wave); endmodule这个 testbench 有两个细节值得说。第一ftw是在复位释放之后才赋值的这样累加器从零相位起步波形起点干净方便对齐分析。第二把每个采样点写进文本文件后面用脚本读出来做 FFT比在仿真波形窗口里目测周期靠谱得多。跑 200000 个时钟点1 MHz 信号大约有 4000 个周期做频谱分析的分辨率足够看清前几次谐波和杂散。仿真跑完看三个数基波频点是否落在预期的 1 MHz 附近、幅度是否接近满量程的 80%、以及二次三次谐波和相位截断杂散大概在什么电平。三项都符合预期再上板否则先回去改 LUT_AW 或 DATA_W。3. ADDA 链路搭建DAC 输出到 ADC 回采的时钟与接口对齐DDS 出来的是一串数字码要变成模拟波形得经过 DAC模拟波形再回到数字域得经过 ADC。这一段链路的坑比 DDS 内核还密集因为涉及外部器件、PCB 走线和时钟相位。这一章按信号流向来拆DAC 接口、ADC 回采、SPI 配置。3.1 DAC 并行接口时序建立保持时间不是玄学大多数中速 DAC 走并行 CMOS 或 LVDS 接口主机在每个时钟沿把数据推出去DAC 在下一个沿采样。问题出在数据和时钟的相位关系上时钟走线比数据线长一点或者负载电容不同采样沿就可能落在数据跳变的位置输出波形上表现为毛刺或码间干扰。// dac_drv.v —— 把 DDS 输出打一拍再送出去 module dac_drv ( input wire dac_clk, // 与数据同源的输出时钟 input wire rst_n, input wire [11:0] din, // 来自 dds_core output reg [11:0] dac_data, output wire dac_clk_o ); // 输出时钟反相让 DAC 在数据稳定的中间采样 assign dac_clk_o ~dac_clk; always (posedge dac_clk or negedge rst_n) begin if (!rst_n) dac_data 12d0; else dac_data din; // 寄存器输出缩短组合路径 end endmodule这里把时钟反相是常见做法目的是把采样沿挪到数据眼图中心。具体反不反相得看 DAC 手册里写的采样沿是上升沿还是下降沿以及 PCB 上时钟和数据谁长。血泪经验是别凭感觉先用示波器同时抓时钟和数据看沿和沿之间差多少再决定要不要在 PLL 里加相位偏移。现在很多 FPGA 的 IO 单元支持可编程输出延迟比改 PCB 现实得多。还有一点DAC 的时钟最好和 FPGA 内部逻辑时钟同源从同一个 PLL 出来别一个用外部晶振一个用内部 PLL那样相位关系每次上电都可能不一样。3.2 ADC 回采与数据对齐源同步接口怎么接ADC 回来的数据通常带一个随路时钟DCO数据在这个时钟的沿上有效。FPGA 端要做的第一件事是用 IDDR 或 IDELAY 把数据和时钟对齐第二件事才是把它拼成完整样本送进内部逻辑。// adc_capture.v —— 源同步双沿采样 module adc_capture ( input wire adc_clk_p, // 随路差分时钟 input wire adc_clk_n, input wire [11:0] adc_d_p, input wire [11:0] adc_d_n, output reg [11:0] sample, output reg sample_vld ); wire adc_clk; wire [11:0] adc_d; // 差分输入转单端实际工程用 IBUFDS 原语 IBUFDS u_clk (.I(adc_clk_p), .IB(adc_clk_n), .O(adc_clk)); genvar i; generate for (i0; i12; ii1) begin : g_ibuf IBUFDS u_d (.I(adc_d_p[i]), .IB(adc_d_n[i]), .O(adc_d[i])); end endgenerate // 用随路时钟直接采数据配合 IDELAY 调相位 always (posedge adc_clk) begin sample adc_d; // 一字节对齐 sample_vld 1b1; end endmodule这段代码是骨架实际工程里要根据 ADC 的数据格式决定是 SDR 还是 DDR。如果 ADC 是 DDR 输出一个时钟周期出两个样本那就得用 IDDR 原语把上升沿和下降沿的数据分别抓下来再拼成一个 24 位的字。很多初次接触的人在这里翻车直接把 DDR 数据当 SDR 处理采到的波形频率正好差一半。数据位对齐也是容易出错的地方。ADC 输出可能是 12 位左对齐或右对齐上位机解析时如果位序搞反波形就会变成阶梯状的乱码。判断方法很简单输入一个已知幅度的直流或低频正弦看码值范围对不对偏了就是位序问题。3.3 SPI 配置寄存器上电序列与配置帧ADDA 芯片上电后不会自己工作得靠 SPI 把内部寄存器一个个写进去时钟分频、输出使能、增益、省电模式。配置顺序在手册里通常有明确规定比如必须先写时钟分频再开输出否则输出端可能出瞬态冲击。// spi_cfg.v —— 按序写入 16 bit 配置帧 module spi_cfg #( parameter CLK_DIV 8 // 50MHz / (2*8) ≈ 3.1MHz SCLK )( input wire clk, input wire rst_n, output reg sclk, output reg sdin, output reg cs_n, output reg cfg_done ); // 配置表高位是寄存器地址低位是数据 reg [15:0] cfg_rom [0:7]; initial begin cfg_rom[0] 16h1C_03; // 时钟分频 cfg_rom[1] 16h20_01; // 数据格式 cfg_rom[2] 16h24_80; // 增益设置 cfg_rom[3] 16h00_01; // 输出使能放最后 // 其余按芯片手册补充 end reg [3:0] idx; reg [4:0] bit_cnt; reg [15:0] shift_reg; reg [7:0] div_cnt; reg busy; always (posedge clk or negedge rst_n) begin if (!rst_n) begin idx 4d0; bit_cnt 5d0; sclk 1b0; cs_n 1b1; cfg_done 1b0; busy 1b0; end else if (!busy) begin // 取下一帧并拉低片选 if (idx 4d4) begin shift_reg cfg_rom[idx]; cs_n 1b0; busy 1b1; bit_cnt 5d15; end else begin cfg_done 1b1; // 全部写完通知上层 end end else begin // 简单分频打 SCLK上升沿推数据 if (div_cnt CLK_DIV-1) begin div_cnt 8d0; sclk ~sclk; if (!sclk) begin sdin shift_reg[15]; shift_reg {shift_reg[14:0], 1b0}; if (bit_cnt 0) begin busy 1b0; cs_n 1b1; idx idx 1b1; end else begin bit_cnt bit_cnt - 1b1; end end end else begin div_cnt div_cnt 1b1; end end end endmodule这段状态机比较朴素但把三件关键事讲清楚了配置表顺序、SCLK 分频、以及cfg_done握手信号。CLK_DIV不能设太小很多 ADDA 芯片的 SCLK 上限只有几 MHz跑太快写进去的值会错位表现出来的现象是部分寄存器生效、部分不生效。稳妥做法是先按手册上限的一半跑调通之后再往上加。cfg_done这个信号一定要接到顶层用它去释放 DDS 和 DAC 驱动的复位保证配置写完之前数据通路是静默的避免上电瞬间输出一段不受控的波形冲击后级电路。4. 工程结构与板级联调从综合约束到 ILA 抓波形代码写对只是第一步工程结构乱、约束不到位、板上调试没手段一样跑不出结果。这一章讲目录怎么分、SDC 怎么写、以及用 ILA 或 SignalTap 抓关键信号的方法。4.1 目录结构RTL、仿真、约束、脚本怎么分层测试工程最容易越改越乱一个顶层文件里塞进 DDS、SPI、ADC 采集和状态机改一个参数要翻半天。我一般按功能分层源码、仿真、约束、构建脚本各占一个目录顶层只做例化和连线。dds_adda_test/ ├── rtl/ │ ├── dds_top.v # 顶层只例化和连接 │ ├── dds_core.v # 相位累加器 查找表 │ ├── rom_sin.v # 显式 ROM 原语包装 │ ├── dac_drv.v # DAC 并行接口 │ ├── adc_capture.v # ADC 源同步采集 │ ├── spi_cfg.v # 上电配置 │ └── clk_gen/ # PLL 与时钟分频 ├── sim/ │ ├── tb_dds.v │ ├── tb_spi_cfg.v │ └── sin_lut.hex ├── constr/ │ ├── pin_assign.tcl # 引脚约束 │ └── timing.sdc # 时序约束 ├── script/ │ ├── build.tcl # 综合实现脚本 │ └── gen_lut.py └── doc/ └── reg_map.md # 寄存器映射表这样分层的好处是二次开发时替换器件只动clk_gen和pin_assign.tcl改 DDS 位宽只动dds_core.v的参数模块之间靠端口连接不靠全局信号互相耦合。reg_map.md这份寄存器映射表看着不起眼实际调试时是最省事的文档SPI 配置改了哪一位不用回头翻代码。4.2 时序约束与时序收敛SDC 怎么写不写约束直接跑综合工具会按默认规则布线跑个几十兆可能没问题一旦 DAC 时钟上到百兆路径延迟超标就会出现间歇性数据错误这种问题最难查因为逻辑仿真完全正确。# timing.sdc —— DDS ADDA 关键约束 create_clock -name sys_clk -period 20.000 [get_ports clk_50m] create_clock -name dac_clk -period 8.000 [get_ports dac_clk_p] # DAC 输出接口数据和时钟同源约束输出延迟 set_output_delay -clock dac_clk -max 2.000 [get_ports dac_data[*]] set_output_delay -clock dac_clk -min -1.000 [get_ports dac_data[*]] # ADC 输入接口按芯片手册的 tCO 和板级走线延迟给值 set_input_delay -clock adc_clk -max 3.000 [get_ports adc_d_p[*]] set_input_delay -clock adc_clk -min 1.000 [get_ports adc_d_p[*]] # DDS 内部路径为单周期不用额外 multicycle set_false_path -from [get_ports rst_n]set_output_delay里的数字来自 DAC 的建立时间和板级走线延迟没有实测数据时先按手册典型值给跑完时序报告再看裕量。set_input_delay同理ADC 手册里会给一个 tCO时钟到数据输出延迟板级延迟用走线长度除以信号传播速度估算。这些值不是凑出来的每一项都能在手册或 PCB 文件里找到出处。约束跑完之后重点看两个报告建立时间 slack 是不是正、保持时间有没有违例。建立违例可以通过降频或插寄存器解决保持违例通常意味着时钟偏斜太大得回去看 PLL 和时钟树。4.3 板上调试用 ILA 或 SignalTap 抓相位累加器和 ADC 数据第一次上板最想知道的是 DDS 到底有没有在跑、SPI 有没有配置成功、ADC 回来的数据长什么样。这三个问题都可以用嵌入式逻辑分析仪回答不用外接示波器。// 在顶层插入 ILA 观察点Xilinx 风格Altera 对应 SignalTap ila_dds u_ila ( .clk (dac_clk), .probe0 (phase_acc), // 32 bit 相位累加器 .probe1 (wave_out), // 12 bit 波形输出 .probe2 (cfg_done), // 配置完成标志 .probe3 (adc_sample) // ADC 回采样本 );抓相位累加器是最直接的诊断手段如果它一直在累加说明 DDS 在跑如果它停在某个值不动检查时钟和复位。抓wave_out看波形是否连续、有没有跳变。抓cfg_done确认 SPI 配置走完。抓adc_sample和wave_out同时对比能看出回采链路有没有对齐。触发条件设成cfg_done的上升沿就能抓到配置完成那一刻前后的信号判断配置是否按预期生效。ILA 的采样深度有限抓长时间扫频时把触发设在扫频起点避免采到一堆重复数据。5. 避坑与常见问题排查频率翻倍、杂散和数据全零调试这套工程时遇到的多数问题都有固定套路现象、原因、解决办法能对得上号。下面五条是按出现频率排的每条都按现象、原因、解决三步写。5.1 现象DAC 输出频率正好是设定值的一半或两倍原因通常是接口的采样沿理解错了。DAC 如果是双沿采样DDR一个时钟周期输出两个点而代码按单沿给数据实际输出频率就差一倍。另一种情况是 DDS 的采样时钟取自 DAC 时钟的二分频ftw 却按原始时钟计算。解决办法是先确认 DAC 手册里写的是 SDR 还是 DDRDDR 情况下用 ODDR 原语把两个样本拼到一个时钟周期里然后核对fout ftw × fs / 2^N里的 fs 是不是真正驱动 DDS 内核的那个时钟频率别拿外部晶振频率直接代进去。5.2 现象频谱上出现等间隔杂散基波两侧对称这是相位截断和幅度量化的典型特征。相位累加器 32 位、查找表只取高 10 位低 22 位被丢掉相当于给相位加了一个周期性的误差反映到频谱上就是围绕基波的一串离散杂散。幅度量化 12 bit 贡献的是宽带噪声底两者叠加。解决办法分两步先加大 LUT_AW 到 12 或 13杂散电平会明显下降如果还不够加相位抖动在截断前叠加一个伪随机序列把离散杂散摊平成噪声。幅度侧可以在输出前做一级数字预失真或者干脆换 14 bit 的 DAC 扩大动态范围。5.3 现象ADC 回采数据全零或全 F先看 ADC 时钟有没有到芯片用示波器量 ADC 的输入时钟脚没波形就是 PLL 或时钟分配的问题。时钟正常但数据不动多半是 SPI 配置里输出使能位没写、或者省电模式没退出。解决办法是把 SPI 配置表逐条读回验证——很多 ADDA 芯片支持读寄存器用同一个 SPI 接口发读命令把读回值和写入值比对。读回不对就降低 SCLK 频率重试SCLK 太快是配置失败最常见的原因。5.4 现象上电偶尔不正常按一下复位就好这种偶发问题最消耗时间。常见原因是 SPI 配置和 DDS 复位释放的先后顺序没控制好配置还没写完 DDS 就开始出数DAC 输出一段失控波形或者复位信号是异步的释放时刻落在时钟沿附近触发亚稳态。解决办法是把cfg_done作为 DDS 和 DAC 驱动复位的释放条件串成一条链上电复位 → SPI 配置 → 配置完成 → 释放数据通路。复位信号进各模块前先做两级同步消除亚稳态。5.5 现象仿真全对板上 ILA 抓到的查找表输出不对综合工具把case语句推断成 ROM 时地址位宽或初始化方式可能和预期不一致尤其是换工具链或换器件系列之后。另一种情况是$readmemh的文件路径在综合时找不到工具静默生成全零 ROM仿真因为路径设置不同反而正常。解决办法是显式例化 ROM 原语把初始化文件绑定到原语的INIT_FILE属性上不依赖工具推断。综合日志里搜一下readmem相关的 warning路径问题一般会有提示别忽略。6. 二次开发把测试工程改成可扫频、可调幅的信号源这套工程跑通之后离一个能用的信号源还有段距离。测试工程通常只输出固定频点、固定幅度实际项目里往往需要扫频、调幅、甚至加简单调制。改造的入口在 DDS 的频率控制字和幅度通路上不用动 ADDA 接口层。扫频的常见做法是加一个步进计数器每隔固定时间把 ftw 加上一个增量增量大小决定扫频速度起始值和终止值决定扫频范围。下面是一个最小改动版本// sweep_ctrl.v —— 线性扫频控制 module sweep_ctrl #( parameter PHASE_W 32, parameter FS 50_000_000, // 采样率 parameter F_START 100_000, // 起始频率 100 kHz parameter F_STOP 10_000_000, // 终止频率 10 MHz parameter STEP_MS 10 // 每 10 ms 步进一次 )( input wire clk, input wire rst_n, input wire sweep_en, output reg [PHASE_W-1:0] ftw ); localparam integer FTW_START (F_START * (1PHASE_W)) / FS; localparam integer FTW_STOP (F_STOP * (1PHASE_W)) / FS; localparam integer FTW_STEP 1024; // 步进量越小扫得越细 localparam integer CNT_MAX (FS / 1000) * STEP_MS; reg [31:0] cnt; always (posedge clk or negedge rst_n) begin if (!rst_n) begin cnt 32d0; ftw FTW_START; end else if (sweep_en) begin if (cnt CNT_MAX) begin cnt 32d0; // 到顶回绕形成循环扫频 ftw (ftw FTW_STEP FTW_STOP) ? FTW_START : ftw FTW_STEP; end else begin cnt cnt 1b1; end end end endmodule调幅更简单在 DDS 输出后面串一个乘法器用另一个低频 DDS 产生调制包络两者相乘再送 DAC。注意位宽要扩展12 bit 乘 12 bit 得到 24 bit截位回 12 bit 时加饱和逻辑避免溢出翻转产生爆音。改造完怎么验证我的习惯是分三步先在仿真里把扫频起止频率和步进时间打印出来和预期值对再上板用 ILA 抓 ftw确认它按步进递增最后接示波器看实际输出频率变化。三步里任何一步对不上就回到对应模块改别跳步。还有一点值得提前考虑如果后面要接上位机控制把 ftw、幅度因子、扫频参数都做成寄存器用一个简单的命令解析模块统一管理比散落在各模块里的参数好维护得多。寄存器映射表在第一次写的时候就建好后面加功能不容易乱。从那以后我每次改 DDS 相关工程都强制先跑一遍仿真频谱分析再上板抓 ILA两条都对上才动示波器。这个习惯帮我省掉了很多次在板子前盲猜的时间。希望帮到你。本文还有配套的精品资源点击获取
返回列表
PREV
查看更多资讯
NEXT
返回资讯列表