FPGA新手也能玩转DDS:用Vivado和Verilog手把手教你做个简易信号发生器
FPGA新手也能玩转DDS用Vivado和Verilog手把手教你做个简易信号发生器第一次接触FPGA时总觉得它像个神秘的黑盒子——直到我亲手用Verilog点亮了第一个LED。那种代码直接控制硬件的奇妙感觉至今难忘。今天我们要做的DDS信号发生器就是能让FPGA新手快速获得成就感的完美项目。不需要复杂的数学推导不用纠结DAC芯片选型只要一块常见的Artix或Zynq开发板就能看到实实在在的波形变化。1. 项目准备认识你的数字信号合成工具包1.1 什么是DDS想象你有一张记录着完整正弦波的密纹唱片DDS就像是用特定速度转动这张唱片——转得快频率就高从不同位置开始播放就能改变相位。只不过我们用数字方式实现相位累加器相当于唱片的转速控制波形查找表就是唱片上的沟槽纹路时钟信号决定我们读取纹路的精细程度在Vivado中这些组件都有现成的IP核可以直接调用。比如Xilinx的DDS Compiler IP连相位累加器都帮我们封装好了。但作为学习项目我们今天要用更基础的Block ROM来构建查找表这样能更清楚地理解底层机制。1.2 开发环境检查清单确保你的环境包含Vivado 2018.3或更新版本带至少2个按键的FPGA开发板如Basys3、PYNQ-Z2一根Micro USB数据线约1GB的可用磁盘空间用于IP核生成提示如果开发板没有模拟输出完全可以用ILA逻辑分析仪观察数字波形效果一样直观。2. 从零搭建十五分钟创建波形引擎2.1 创建Vivado工程打开Vivado选择Create Project器件型号根据你的开发板选择。比如Basys3使用的是xc7a35tcpg236-1。关键步骤create_project dds_tutorial ./dds_tutorial -part xc7a35tcpg236-1 set_property board_part digilentinc.com:basys3:part0:1.2 [current_project]2.2 生成正弦波数据不用MATLAB也能快速生成COE文件用这个Python脚本import math with open(sine.coe, w) as f: f.write(memory_initialization_radix16;\n) f.write(memory_initialization_vector\n) for i in range(256): val int(127 * math.sin(2*math.pi*i/256) 128) f.write(f{val:02x} (, if i255 else ;))保存后在Vivado中创建Block ROM IP核在Flow Navigator选择IP Catalog搜索Block Memory选择Single Port ROM载入刚生成的sine.coe文件设置端口宽度为8位深度2562.3 编写核心Verilog模块创建dds_core.v文件实现最简单的地址发生器module dds_core( input clk, input [7:0] freq_control, output [7:0] wave_data ); reg [15:0] phase_accumulator; wire [7:0] rom_address; always (posedge clk) phase_accumulator phase_accumulator {8d0, freq_control}; assign rom_address phase_accumulator[15:8]; blk_mem_gen_0 rom_inst ( .clka(clk), .addra(rom_address), .douta(wave_data) ); endmodule这个设计妙处在于通过freq_control输入控制输出频率相位累加器自动处理地址回绕256点的波形数据足够产生清晰的正弦波3. 交互增强给信号发生器装上旋钮3.1 按键消抖的极简实现原始方案中的状态机虽然严谨但对新手可能太复杂。试试这个更简单的消抖方案module debounce( input clk, input btn_in, output reg btn_out ); reg [19:0] counter; always (posedge clk) begin if (btn_in ! btn_out) counter counter 1; else counter 0; if (counter) btn_out btn_in; end endmodule工作原理当按键状态变化时启动计数器持续20ms50MHz时钟下计数到2^20只有稳定达到20ms才更新输出3.2 频率控制逻辑将消抖后的按键信号连接到频率调节器reg [7:0] freq_reg 8d1; always (posedge clk) begin if (btn_up !btn_up_prev freq_reg 8d100) freq_reg freq_reg 8d1; if (btn_down !btn_down_prev freq_reg 8d1) freq_reg freq_reg - 8d1; btn_up_prev btn_up; btn_down_prev btn_down; end这样就用两个按键实现了频率加减控制操作体验类似老式信号发生器的旋钮。4. 眼见为实三种方式验证你的设计4.1 仿真验证编写简单的testbench观察波形变化initial begin freq_control 8d1; #1000000 freq_control 8d2; #1000000 freq_control 8d4; #1000000 $finish; end在Vivado仿真器中可以看到波形频率随控制值翻倍。4.2 ILA实时抓取添加Integrated Logic Analyzer IP核在IP Catalog搜索ILA设置采样深度1024添加wave_data信号生成bitstream后下载到开发板在Hardware Manager中触发采集你会看到类似这样的正弦波数字表示值序列80 83 86 89 8c 8f 92 95 98 9b 9e a1 a4...4.3 进阶技巧PWM模拟输出即使没有DAC也能用PWM实现简易模拟输出reg [7:0] pwm_counter; always (posedge clk) pwm_counter pwm_counter 1; assign pwm_out (wave_data pwm_counter);用低通滤波器处理后就能在示波器上观察到真实的正弦波了。5. 项目扩展让你的信号发生器更专业5.1 多波形切换在ROM中存储多种波形数据通过地址偏移切换波形类型地址范围生成方法正弦波0x00-0xFF数学函数生成三角波0x100-0x1FF线性递增/递减方波0x200-0x2FF高低电平交替always (*) begin case(wave_select) 2b00: rom_addr phase_acc[15:8]; 2b01: rom_addr 16h100 phase_acc[15:8]; 2b10: rom_addr {phase_acc[15], 7b0}; endcase end5.2 幅度调节技巧数字幅度调节不需要乘法器用移位实现更高效wire [11:0] amp_out; assign amp_out {4d0, wave_data} amplitude;注意输出位宽要相应增加避免溢出。5.3 频率精度优化提高相位累加器位宽可获得更精细的频率控制reg [31:0] phase_acc; always (posedge clk) phase_acc phase_acc {24d0, freq_control};32位相位累加器在100MHz时钟下频率分辨率可达0.023Hz第一次成功看到自己生成的波形在屏幕上跳动时那种我做到了的兴奋感就是学习FPGA最大的乐趣。这个项目最棒的地方在于你可以随时添加新功能——比如用滑动开关控制波形选择或者增加LCD显示当前频率。我的Basys3开发板上就常驻这个程序用来测试其他电路时特别方便。