
简介基于AD9767高速双通道DAC芯片与Cyclone IV E FPGA的DDS信号发生器例程主要面向FPGA开发初学者、电子竞赛备赛者及数字信号处理实验者帮助解决DDS波形产生、DAC双通道驱动与Quartus工程搭建等常见问题。整个工程基于Quartus 13.0开发包含顶层模块、PLL锁相环时钟管理、按键参数控制、LED状态指示以及AD9767双路14位并行数据输出设计可直接学习DDS频率合成核心逻辑。压缩包共148个文件以Quartus编译生成的.cdb、.hdb等中间构建文件为主同时提供.v、.tdf源文件、.qsf/.qpf工程配置文件、.sof比特流下载文件与.rpt编译报告另含.mif初始化文件可直接打开工程完成综合布局布线并烧录至板卡验证。资源大小4.18MB体积轻量目前已有517人学习很适合快速上手。通过学习该工程读者可掌握频率控制字累加、相位累加器设计、DAC写时序控制、FPGA与高速DAC接口连接等方法还能借鉴作者对工程文件的分层管理方式将其迁移到任意波形发生器等自研项目中为课程设计、毕业设计或竞赛准备提供参考。1. 用Quartus 13.0打开这套AD9767高速双通道DAC与Cyclone IV E FPGA组成的DDS例程光按一次按键看示波器从1MHz跳到5MHz就能把相位累加器、频率控制字、DAC数据锁存三件事全部串起来。以前总倾向于用现成的NCO IP核但IP核会隐藏几个问题输出默认是有符号数据而AD9767的MODE引脚决定偏移二进制还是二进制补码格式不匹配波形中心就偏。这套例程把PLL、累加器、查找表、按键切换、DAC时钟都放在一个顶层文件里适合刚做完FPGA入门、想从头搭信号发生器的人也适合在调AD9767驱动时序的工程师参考。工程基于Quartus 13.0源码可编译复现。2. DDS频率合成原理与AD9767双通道接口时序2.1 频率控制字Fword与相位累加器的换算DDS的输出频率由相位累加器溢出速度决定。每个D_CLK上升沿把Fword加到自身累加器溢出意味着一个正弦周期完成。输出频率满足Fout Fword × Fclk / 2^NN是累加器位宽。代码里Fword声明为32位因此N32。假设PLL倍频到100MHz频率分辨率就是100MHz / 2^32 ≈ 0.023Hz。这个精度意味着只要Fword取整数实际输出频率与期望值的偏差不会超过半个分辨率频率计在这个尺度上也测不出误差。计算Fword最常用的方法是先确定目标频率、工作时钟和累加器位宽再代入公式。用Python一步算出来。def fword(fout, fclk, n32): return int(round(fout * 2**n / fclk)) fclk 100e6 for f in [1e6, 2e6, 5e6, 10e6]: fw fword(f, fclk) real fw * fclk / 2**32 print(f{f/1e6:3.1f}MHz - Fword{fw:10d} - {real/1e6:.6f}MHz)这段脚本输出每个档位的Fword和实际输出频率四个档位的结果是1MHz对应429496732MHz对应858993465MHz对应21474836510MHz对应429496730实际输出频率都落在目标值附近相对误差低于10的负七次方量级。把Fword换成十进制写进Verilog常量即可不需要在FPGA里做浮点运算。目标频率Fword实际输出频率1MHz429496731.000000MHz2MHz858993462.000000MHz5MHz2147483655.000000MHz10MHz42949673010.000000MHz2.2 32位相位截断成14位查找表地址32位累加器不会全部送进ROM否则要存储2的32次方个点毫无必要。AD9767只有14位数据总线工程里取累加器高14位phase_acc[31:18]作为ROM地址ROM深度16384点每个点存一个14位正弦采样。低18位相位被截断后理论上有相位噪声但噪声幅度落在14位DAC量化噪声之下实际观察不到。如果想进一步降低ROM占用可以改成12位地址取phase_acc[31:20]ROM深度4096点输出数据仍保持14位。相位截断误差会变大在窄带频谱上能看到杂散略高。做信号发生器实验时16384点是比较稳妥的选择Cyclone IV E的块RAM只占几个M9K容量压力可以忽略。2.3 AD9767的时钟与写信号同源顶层代码里DACA_CLK、DACB_CLK、DACA_WRT、DACB_WRT四个信号全部assign到D_CLK。AD9767在CLK上升沿锁存14位数据WRT复用同一个时钟是数据手册允许的简化接法。这个接法省了一路信号代价是时钟和数据从同一寄存器出来PCB走线必须尽量等长。D_CLK走线比数据线长时时钟先到DAC数据后到建立时间被压缩高频档位会先出现毛刺。PLL倍频到100MHz后还要注意数据线上的跳变沿不能离时钟上升沿太近。用示波器同时看DACA_CLK和DAC_DATA1数据翻转应该在时钟上升沿之前完成。看不到明显毛刺就不需要额外调整这是实验板的常态。2.4 MODE引脚与数据格式的匹配AD9767的MODE引脚决定输入格式。MODE接低时按偏移二进制解释接高时按二进制补码解释。两种格式的零电平点不同偏移二进制的0x2000对应模拟0V二进制补码的0x0000对应模拟0V。例程生成的正弦ROM是偏移二进制因此DAC板上MODE必须接低。如果MODE接高输出波形会整体平移一个直流偏置波形幅度不变但中心不在0V。如果板子设计时MODE已经固定接高不想动硬件就要在FPGA内部把ROM输出转换为二进制补码即每个样点减去8192再按有符号数送到DAC。这个转换只占一个减法器。难点在于只有知道数据格式不匹配时才会去查MODE引脚否则直流偏置很容易被误判成运放问题。3. Verilog源码解析PLL、按键、相位累加器与查找表3.1 顶层端口与数据通路分工顶层端口是固定的CLK50M作为系统时钟输入Key为按键输入led[3:0]做档位指示DACA_CLK/DACB_CLK和DACA_WRT/DACB_WRT是DAC侧的时钟与写信号DAC_DATA1和DAC_DATA2是两条14位数据总线。module AD9767_DDS( input CLK50M, input Key, // 按键默认高电平按下为低 output [3:0] led, // 指示当前频率档位 output DACA_CLK, // DAC A 时钟 output DACB_CLK, // DAC B 时钟 output DACA_WRT, // DAC A 写使能 output DACB_WRT, // DAC B 写使能 output [13:0] DAC_DATA1, // DAC A 数据总线 output [13:0] DAC_DATA2 // DAC B 数据总线 ); wire D_CLK; assign DACA_CLK D_CLK; assign DACB_CLK D_CLK; assign DACA_WRT D_CLK; assign DACB_WRT D_CLK; assign DAC_DATA2 DAC_DATA1; // 双通道输出同一波形 pll pll( .inclk0(CLK50M), .c0(D_CLK) ); wire [31:0] Fword; // 按键消抖、相位累加器、查找表和输出寄存器 // 在 3.2 ~ 3.5 节分别展开 endmoduleDAC_DATA2镜像DAC_DATA1是例程的缺省策略双通道输出完全一致的波形。如果需要两路独立波形这里就要改成第二套累加器和ROM的输出。注意DACA_WRT和DACB_WRT直接复用D_CLK而不是各自生成这保证了两个通道锁存沿完全同步。3.2 内部上电复位与按键消抖原始代码把Rst_n注释掉原因是不少开发板没有复位键。没有外部复位时必须在FPGA内部生成一个上电复位信号否则计数器、状态寄存器在上电后处于不确定状态。常见做法是用一个计数器在系统时钟下计数reg rst_n 1b0; reg [15:0] rst_cnt 16d0; always (posedge CLK50M) begin if (rst_cnt) begin rst_n 1b1; end else begin rst_cnt rst_cnt 1; rst_n 1b0; end endrst_cnt计数到全1需要65536个时钟周期在50MHz下约1.3ms。Cyclone IV E的PLL锁定时间通常在几毫秒到几十毫秒之间这里只是覆盖了PLL未稳定的窗口严格做法是把pll_locked接入复位释放逻辑PLL锁定后再放行整个DDS。如果板子有独立复位键直接用外部复位会更干净但大多数下载器开发板没有。按键消抖与边沿检测是另一组独立逻辑。按键默认高电平按下变低。先用两级寄存器同步避免亚稳态直接进入状态机再检测下降沿reg key_d1, key_d2; always (posedge D_CLK) begin key_d1 Key; key_d2 key_d1; end wire key_press key_d2 ~key_d1; // 检测按下瞬间两个时钟周期后key_d2变成低电平key_press变成单周期脉冲。这个信号只作为触发不会影响相位累加器的连续运行。如果按键抖动明显可以在key_d2之前加计数器等待稳定约10ms再更新电平。以下是一种补全的防抖写法reg [19:0] db_cnt; reg key_db; always (posedge D_CLK) begin if (Key ! key_db) begin db_cnt db_cnt 1; if (db_cnt 20d999_999) // 100MHz下约10ms key_db Key; end else db_cnt 20d0; enddb_cnt计数到接近100万时key_db才跟随输入也就是说只有电平持续稳定10ms才认为按键真正按下机械抖动产生的多个边沿都被过滤掉。3.3 频率档位切换与相位累加器按下按键一次freq_idx加一四档循环。Fword通过Fword_r组合逻辑选择后assign给顶层wire。注意Fword是wire型变量不能在多个always块里直接赋值用一个组合always给Fword_r赋值再assign是避免多驱动的常用做法。reg [1:0] freq_idx; always (posedge D_CLK) begin if (!rst_n) freq_idx 0; else if (key_press) freq_idx freq_idx 1; end reg [31:0] Fword_r; always (*) begin case (freq_idx) 2d0: Fword_r 32d42949673; // 1MHz 2d1: Fword_r 32d85899346; // 2MHz 2d2: Fword_r 32d214748365; // 5MHz default: Fword_r 32d429496730; // 10MHz endcase end assign Fword Fword_r;Fword_r只在freq_idx变化时更新正常工作时保持不变因此assign出来的Fword不会在任意时刻跳变避免了频率字在累加过程中被改一半的情况。四组频率字都按100MHz时钟计算如果PLL改成125MHz输出这四个常数必须重新算。相位累加器本身只是一个32位无符号加法器。溢出自然回绕不需要判断进位。每个D_CLK上升沿加一次Fword输出的相位序列就是频率字对应的相位离散点。reg [31:0] phase_acc; always (posedge D_CLK) begin if (!rst_n) phase_acc 32d0; else phase_acc phase_acc Fword; end这里复位置零可以让上板后从一个已知相位开始方便用示波器观察两个DAC通道的相位关系。单通道应用时复位与否不敏感关键是复位释放前D_CLK必须已经稳定。3.4 PLL锁相环参数配置PLL核在Quartus 13.0里的配置路径是Tools → MegaWizard Plug-In Manager选择ALTPLL。输入50MHzc0输出100MHz相移0度。关键参数如下参数值说明Reference clock50MHz板载晶振c0 multiply/divide2 / 1输出100MHzPhase shift0度不延迟Duty cycle50%默认选择100MHz而不是125MHz满速是因为工程要为建立保持时间留裕量且100MHz与2的32次方累加器配合计算频率字时数字更规整。若板载晶振是25MHz倍频比改成4比1。改完PLL后所有频率档位的Fword都要按新D_CLK重新算一遍否则按键切频后实际频率和档位名对不上。3.5 正弦查找表与输出寄存器级正弦表可以用MIF文件配合LPM_ROM也可以直接用$readmemh读入十六进制文件。工程目录下放一个sine14.hex内容用脚本生成import math DEPTH 16384 with open(sine14.hex, w) as f: for i in range(DEPTH): v int(round(8192 8191 * math.sin(2 * math.pi * i / DEPTH))) v max(0, min(16383, v)) f.write(f{v:04X}\n)脚本生成的是偏移二进制正弦值范围0到16383中心点在8192正好对应AD9767偏移二进制模式的零电平。ROM模块如下module sine_rom( input wire clk, input wire [13:0] addr, output reg [13:0] q ); reg [13:0] mem [0:16383]; initial $readmemh(sine14.hex, mem); always (posedge clk) q mem[addr]; endmoduleROM输出比地址晚一个时钟周期。地址来自phase_acc[31:18]所以读出的数据本身就和D_CLK有固定的一拍延迟。为了把数据对齐到D_CLK上升沿输出到DAC之前再加一级寄存器reg [13:0] dac_data_r; always (posedge D_CLK) begin if (!rst_n) dac_data_r 14d8192; else dac_data_r sin_q; end assign DAC_DATA1 dac_data_r;这样DAC_DATA1在D_CLK上升沿之后稳定跳变与DACA_CLK同源同相位。多一级寄存器对单频正弦没有可闻影响但在按键切换频率的瞬间能避免数据和时钟错位造成的输出毛刺。4. Quartus 13.0工程文件、引脚约束与上板排查4.1 编译产物文件的角色工程列表里的文件名很劝退但它们都不是源码而是Quartus 13.0在分析综合和布局布线过程中生成的中间文件。文件角色如下文件类来源阶段是否需要保留.vpr工程记录保留.map.cdb / .map.bpm分析与综合阶段可删除.cmp.cdb / .cmp.bpm适配阶段可删除.root_partition.*.cdb根分区数据可删除.ammdb数据库缓存可删除真正决定工程属性的是.qpf和.qsf前者是工程入口后者保存引脚约束、时序约束和器件型号。如果压缩包里有它们双击.qpf即可打开如果只有.vpr而缺.qpf/.qsf需要新建空工程并添加所有.v源文件再重新做引脚分配。拿到工程后先删除db文件夹、incremental_db文件夹、.qws、.qdf再编译能避免大量莫名其妙的版本报错。4.2 引脚分配与I/O电平匹配AD9767的数据总线是14位加上两组时钟和写信号共约20个FPGA引脚。引脚分配在.qsf里指定典型写法set_location_assignment PIN_E1 -to CLK50M set_location_assignment PIN_M1 -to Key set_location_assignment PIN_M2 -to DACA_CLK set_location_assignment PIN_M3 -to DACB_CLK set_location_assignment PIN_N1 -to DACA_WRT set_location_assignment PIN_N2 -to DACB_WRT set_location_assignment PIN_P1 -to DAC_DATA1[0] set_location_assignment PIN_P2 -to DAC_DATA1[1] # 其余数据引脚按板卡原理图逐脚补齐 set_instance_assignment -name IO_STANDARD 3.3-V LVTTL -to CLK50M set_instance_assignment -name IO_STANDARD 3.3-V LVTTL -to DAC_DATA1[0]CLK50M必须落在专用时钟引脚上Cyclone IV E上通常是PIN_E1这类全局时钟输入普通IO不能直接驱动PLL的输入。数据引脚和时钟引脚最好安排在同一个Bank并确保该Bank的VCCIO与AD9767的DVDD电压一致。大多数AD9767开发板用3.3V数字电源Quartus里选3.3-V LVTTL即可匹配。DAC侧的空闲引脚也要处理MODE接地选择偏移二进制SLEEP接地保持正常工作FSADJ接一个1kΩ到2kΩ的电阻设定满量程电流。这些引脚不在Verilog代码里却在电路板上直接影响输出幅度和波形中心。4.3 编译通过但上板无输出的排查顺序编译通过只是第一步上板没波形时要按照信号链从FPGA往DAC推不要一上来就怀疑代码。用示波器测CLK50M引脚确认晶振起振且频率是50MHz。烧录sof后测D_CLK若板上没有引出来用SignalTap观测pll的c0。测DAC板的MODE引脚电位确认和ROM数据格式一致。测IOUTA对AGND电压静态值应该接近满量程的一半。确认IOUTA引脚对地有负载电阻通常板载50Ω到100Ω没有的话要外接。常见报错与处理方式现象原因处理方法编译报Cant place all pins引脚分配冲突或时钟引脚被占按板卡原理图重新分配PLL不锁定inclk0频率与PLL设置不匹配核对输入时钟频率下载后LED全灭内部复位没有释放检查复位计数器是否依赖D_CLK而D_CLK未起振DAC输出是直流MODE与数据格式不匹配核对MODE引脚与ROM编码高频档波形毛刺明显数据与时钟偏斜过大数据线加33Ω串阻或缩短走线内部复位用系统时钟计数如果PLL没有锁定D_CLK不稳会让复位释放后的逻辑跑在错误状态。稳妥做法是把pll_locked与内部复位用与逻辑组合等PLL稳定后再放行整个DDS。5. 从单频正弦到正交输出的验证与扩展先把第一组验证做完示波器探头接IOUTA开交流耦合垂直档位放100mV/div触发源选D_CLK所在引脚。按下Key波形频率按1、2、5、10MHz循环变化LED指示与档位对应。10MHz档时每周期只有10个采样点正弦边缘会显得近似梯形这不是设计错误是采样点数不够。想要10MHz更平滑把D_CLK提到125MHz但要确认板级时序还能满足。如果只看LED不放心可以用SignalTap抓phase_acc高8位和Fword。添加探针后用Key上升沿触发一次抓够4个档位的跳变能看到频率字从42949673到85899346再到214748365的变化这和示波器上的频率切换完全同步。SignalTap抓内部信号比示波器测量引脚更直接尤其当板上没有引出数据线时。扩展成双通道正交信号源是最有价值的改动。正交意味着DAC_A输出sinDAC_B输出cos相位差固定为90度。实现方法是给DAC_B增加第二个相位累加器初值设为32h40000000也就是2的30次方正好四分之一个周期reg [31:0] phase_acc_b; always (posedge D_CLK) begin if (!rst_n) phase_acc_b 32h40000000; // 相位初值90度 else phase_acc_b phase_acc_b Fword; end wire [13:0] sin_q_b; sine_rom u_sin_b( .clk (D_CLK), .addr (phase_acc_b[31:18]), .q (sin_q_b) );再把DAC_DATA2从镜像assign改为sin_q_b对应寄存器双通道就从同相变为正交。两个通道的相位差只取决于累加器初值不会随频率切换累积漂移这是DDS相比模拟移相器的明显优势。频域验证时10MHz输出的杂散主要来自相位截断与ROM量化。仅考虑量化噪声14位DAC的信噪比极限约86dB实际SFDR通常落在60到70dBc主要限制来自DAC的微分非线性和时钟抖动。如果杂散峰离主频很近多半是数据线与时钟之间的串扰通过降低FPGA输出驱动强度或加串阻可以改善。本文还有配套的精品资源点击获取