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_radix=16;\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 i<255 else ";"))保存后,在Vivado中创建Block ROM IP核:
- 在Flow Navigator选择IP Catalog
- 搜索"Block Memory"
- 选择Single Port ROM
- 载入刚生成的sine.coe文件
- 设置端口宽度为8位,深度256
2.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 + {8'd0, 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工作原理:
- 当按键状态变化时启动计数器
- 持续20ms(50MHz时钟下计数到2^20)
- 只有稳定达到20ms才更新输出
3.2 频率控制逻辑
将消抖后的按键信号连接到频率调节器:
reg [7:0] freq_reg = 8'd1; always @(posedge clk) begin if (btn_up && !btn_up_prev && freq_reg < 8'd100) freq_reg <= freq_reg + 8'd1; if (btn_down && !btn_down_prev && freq_reg > 8'd1) freq_reg <= freq_reg - 8'd1; btn_up_prev <= btn_up; btn_down_prev <= btn_down; end这样就用两个按键实现了频率加减控制,操作体验类似老式信号发生器的旋钮。
4. 眼见为实:三种方式验证你的设计
4.1 仿真验证
编写简单的testbench观察波形变化:
initial begin freq_control = 8'd1; #1000000 freq_control = 8'd2; #1000000 freq_control = 8'd4; #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) 2'b00: rom_addr = phase_acc[15:8]; 2'b01: rom_addr = 16'h100 + phase_acc[15:8]; 2'b10: rom_addr = {phase_acc[15], 7'b0}; endcase end5.2 幅度调节技巧
数字幅度调节不需要乘法器,用移位实现更高效:
wire [11:0] amp_out; assign amp_out = {4'd0, wave_data} << amplitude;注意输出位宽要相应增加,避免溢出。
5.3 频率精度优化
提高相位累加器位宽可获得更精细的频率控制:
reg [31:0] phase_acc; always @(posedge clk) phase_acc <= phase_acc + {24'd0, freq_control};32位相位累加器在100MHz时钟下,频率分辨率可达0.023Hz!
第一次成功看到自己生成的波形在屏幕上跳动时,那种"我做到了"的兴奋感,就是学习FPGA最大的乐趣。这个项目最棒的地方在于,你可以随时添加新功能——比如用滑动开关控制波形选择,或者增加LCD显示当前频率。我的Basys3开发板上就常驻这个程序,用来测试其他电路时特别方便。
