基于FPGA的DDS信号发生器设计_设计示例保姆级教程及原理浅谈_NBU数字系统工程实践

张开发
2026/4/13 20:32:21 15 分钟阅读

分享文章

基于FPGA的DDS信号发生器设计_设计示例保姆级教程及原理浅谈_NBU数字系统工程实践
一、原理浅谈由于本人十分的笨蛋上课也没用心听所以设计过程中出现了很多麻烦。希望用笨蛋的方式分享一下对这个项目中DDS原理的理解帮助和我一样上课没听懂的童鞋们弄懂原理。二、设计示例一新建工程1.第1页目录在资源管理器中新建一个空白文件夹作为工程根目录 → 工程名和顶层实体名为dds_final2.第三页器件选择FamilyCyclone Ⅳ E → Available deviceEP4CE6E22C8二生成ROM IP核1.下载本文章附带的sin_rom.v文件到工程根目录里2.点击Tools → 点击MegaWizard Plug-In Manager→点击Create a new custom megafunction variation →点击Next→点击Memory Compiler → 选中ROM1-PORT→将sin_rom这七个字添加到输出文件名的最后→将q宽度设置为10点击Next→再点击Next进入到下图的Mem Init页面点击Browse...→Files of type选择MIF Files*.mif或All files→点击sin_rom.mif点击Open→点击Next→下一页也点Next最后来到Summary页面点击Finish→Finish之后会出现这个页面叉掉即可或者按键盘左上角的Esc键三Verilog HDL代码编写1.新建Vlog代码文件点击Files 标签 → 点击New → 点击Verilog HDL File → 粘贴以下代码// // DDS信号发生器 - 核心基本参数表 // // 1. 系统硬件参数 // 主控芯片EP4CE6E22C8 // 系统时钟12MHz (板载晶振 PIN_23) // DAC芯片MS9714 (12位 板载直驱) // 数码管8位共阴极 // 2. DDS核心算法参数理论真实频率 // 相位累加器位宽21bit (N21) 2^21 2097152 // 频率控制字M_word (20bit) // 理论真实频率公式f_out (M_word × 12000000) / 2097152 // 频率分辨率5.722045898 Hz // 3. 数码管显示规则 // 显示M值仅点亮最右侧5位数码管左侧3位熄灭 // 显示频率8位全亮 // - 左6位(dg08-dg03)频率整数部分 // - 第7位(dg02)仅显示小数点不显示数字 // - 第8位(dg01)1位小数100Hz显示真实值否则固定0 // 例97.2Hz → 000097.2 | 286102Hz → 286102.0 // 4. 输入控制参数 // K1~K5步长选择开关 (1/10/100/1000/10000) // K6M值清零开关 // PK1M值累加触发脉冲 // PK2显示模式切换脉冲(M值/频率) // // 5. 硬件接线 // K1接IO51K2接IO52K3接IO53K4接54K5接IO55K6接IO58 // P1接IO66P2接IO67 // // 6. 使用规则通用实例 // 通用规则 // 1. 自锁开关K1-K5选择调节步长仅允许一个开关闭合 // 2. 按下脉冲按键PK1M值按照选中的步长累加 // 3. 闭合自锁开关K6M值直接清零 // 4. 按下脉冲按键PK2切换显示模式M值 ↔ 输出频率 // 5. 显示M值时仅右5位亮显示频率时8位全亮固定小数点 // 操作实例 // 例1需要M值1 → 闭合K1按一下PK1 // 例2需要M值清零 → 闭合K6 // 例3查看输出频率 → 按一下PK2切换模式 // // 顶层模块DDS信号发生器总控模块整合所有子功能 module dds_final( // 系统时钟输入12MHz板载晶振全局工作时钟 input clk, // 自锁开关输入端口 // 步长选择开关K1~K5控制M值每次累加的数值 input sw1, // 硬件K1 ↔ IO51 → 功能步长1个位调节 input sw2, // 硬件K2 ↔ IO52 → 功能步长10十位调节 input sw3, // 硬件K3 ↔ IO53 → 功能步长100百位调节 input sw4, // 硬件K4 ↔ IO54 → 功能步长1000千位调节 input sw5, // 硬件K5 ↔ IO55 → 功能步长10000万位调节 input sw6, // 硬件K6 ↔ IO58 → 功能M值一键清零控制 // 脉冲按键输入端口 input pk1, // 硬件P1 ↔ IO66 → 功能M值累加触发按键 input pk2, // 硬件P2 ↔ IO67 → 功能显示模式切换按键 // DAC波形输出端口 output [11:0] da, // 12位DAC数据输出输出正弦波数字量 output dac_clk, // DAC工作时钟直接使用系统12MHz时钟 // 数码管段选输出端口 // 共阴极数码管a~g段驱动信号控制数字笔画点亮 output reg sa, sb, sc, sd, se, sf, sg, output reg dp, // 数码管小数点驱动信号 // 数码管位选输出端口 // 8位数码管片选信号控制当前哪个数码管点亮 output reg dg01, dg02, dg03, dg04, // 数码管右侧4位 output reg dg05, dg06, dg07, dg08 // 数码管左侧4位 ); // 1. 全局基础参数与寄存器定义 // DAC时钟赋值将系统时钟直接分配给DAC保证同步工作 assign dac_clk clk; // DDS核心参数相位累加器位宽固定为21位 parameter N 21; // 相位累加器寄存器21位DDS核心部件持续累加M值生成相位 reg [N-1:0] phase_acc 0; // 频率控制字寄存器20位控制输出频率的核心参数用户可调节 reg [19:0] M_word 0; // 正弦ROM表地址线取相位累加器高8位作为ROM地址 wire [7:0] rom_addr; // 2. 按键消抖模块解决机械按键抖动问题 // 20位计数器用于生成100ms消抖采样时钟计数范围0~1048575 reg [19:0] delay_cnt; // 消抖采样标志计数到100000时产生一个采样脉冲 wire sample_tick; // 时钟驱动系统时钟每来一个上升沿计数器自增1 always (posedge clk) delay_cnt delay_cnt 1d1; // 采样脉冲生成计数器等于100000时输出高电平脉冲100ms采样一次 assign sample_tick (delay_cnt 20d100_000); // 按键打拍寄存器3级寄存器对按键信号延时打拍用于检测按键边沿 reg [2:0] pk1_r, pk2_r; // 采样时刻每100ms将按键信号存入寄存器过滤抖动 always (posedge clk) begin if(sample_tick) begin pk1_r {pk1_r[1:0], pk1}; // PK1按键信号移位存储 pk2_r {pk2_r[1:0], pk2}; // PK2按键信号移位存储 end end // 有效脉冲生成检测按键下降沿按下瞬间输出稳定触发信号 wire pk1_pulse (pk1_r[2:1] 2b01) sample_tick; // M累加有效触发 wire pk2_pulse (pk2_r[2:1] 2b01) sample_tick; // 显示切换有效触发 // 3. 步长选择模块根据K1-K5状态选择累加步长 // 步长寄存器存储当前选中的M值累加数值 reg [19:0] step; // 组合逻辑根据5个开关的电平状态匹配对应的步长值 always (*) begin // 开关编码sw5(万位) sw4(千位) sw3(百位) sw2(十位) sw1(个位) case({sw5, sw4, sw3, sw2, sw1}) 5b00001: step 1; // 仅K1闭合 → 步长1 5b00010: step 10; // 仅K2闭合 → 步长10 5b00100: step 100; // 仅K3闭合 → 步长100 5b01000: step 1000; // 仅K4闭合 → 步长1000 5b10000: step 10000; // 仅K5闭合 → 步长10000 default: step 0; // 无开关闭合/多开关闭合 → 步长0不累加 endcase end // 4. M值更新模块用户控制核心清零累加 // 时序逻辑系统时钟上升沿更新M值 always (posedge clk) begin if(sw6) // 优先级1K6闭合 → M值强制清零 M_word 0; else if(pk1_pulse) // 优先级2PK1按下 → M值 当前选中的步长 M_word M_word step; end // 5. DDS相位累加器核心模块生成线性相位 // 时序逻辑每个时钟周期相位累加器 M值 // 原理M值越大相位增长越快输出频率越高 always (posedge clk) phase_acc phase_acc M_word; // 地址分配取相位累加器的最高8位作为正弦波ROM表的读取地址 assign rom_addr phase_acc[N-1:N-8]; // 6. 正弦波ROM例化存储正弦波波形数据 // 调用预制的ROM IP核根据输入的相位地址输出对应的正弦波数字量 sin_rom sin_rom_inst( .address(rom_addr), // 输入相位地址8位 .clock(clk), // 输入ROM工作时钟 .q(da[11:2]) // 输出12位正弦波数据高10位赋值给DAC ); // DAC低2位补0补齐12位数据宽度保证DAC正常输出 assign da[1:0] 2b00; // 7. 显示模式切换模块 // 显示模式寄存器0显示M值1显示输出频率 reg disp_sel 1b0; // 时序逻辑PK2按下时翻转显示模式 always (posedge clk) begin if(pk2_pulse) disp_sel ~disp_sel; end // 8. 精准频率计算模块理论公式无符号运算防溢出 // 固定除数2^212097152DDS频率公式固定参数 localparam DIV_PARAM 32d2097152; // 总频率计算无符号64位乘法 → M_word × 12MHz杜绝数据溢出 wire [63:0] freq_total $unsigned(M_word) * 64d12000000; // 频率整数部分无符号除法直接计算理论频率整数位 wire [31:0] freq_int $unsigned(freq_total / DIV_PARAM); // 除法余数用于计算小数部分 wire [31:0] freq_remain $unsigned(freq_total % DIV_PARAM); // 1位小数部分余数×10再÷除数得到小数点后1位 wire [3:0] freq_dec $unsigned((freq_remain * 10) / DIV_PARAM); // 低频判断标志频率整数部分 100Hz时为高电平 wire freq_less100 (freq_int 100); // 9. BCD码拆分模块将数字转为数码管显示数据 // 显示数据数组8个4位数据对应8位数码管的显示数字 reg [3:0] data [7:0]; // 组合逻辑根据显示模式拆分M值/频率为单个数字 always (*) begin // 初始化所有显示数字默认为0 data[0]0;data[1]0;data[2]0;data[3]0; data[4]0;data[5]0;data[6]0;data[7]0; if(!disp_sel) begin // 模式1显示M值 → 仅拆分右侧5位数字 data[4] (M_word/10000) % 10; // M值万位 data[3] (M_word/1000) % 10; // M值千位 data[2] (M_word/100) % 10; // M值百位 data[1] (M_word/10) % 10; // M值十位 data[0] M_word % 10; // M值个位 end else begin // 模式2显示频率 → 拆分6位整数1位小数 data[7] (freq_int/100000) % 10; // 频率十万位左1位 data[6] (freq_int/10000) % 10; // 频率万位左2位 data[5] (freq_int/1000) % 10; // 频率千位左3位 data[4] (freq_int/100) % 10; // 频率百位左4位 data[3] (freq_int/10) % 10; // 频率十位左5位 data[2] freq_int % 10; // 频率个位左6位 // 最右侧数码管100Hz显示真实小数否则固定显示0 data[0] freq_less100 ? freq_dec : 1d0; end end // 10. 数码管扫描分频模块动态扫描驱动 // 扫描分频计数器8位控制数码管切换速度 reg [7:0] clk_div; // 时序逻辑计数器自增满200清零扫描频率≈60Hz无闪烁 always (posedge clk) begin if(clk_div 8d199) clk_div 8d0; else clk_div clk_div 1d1; end // 数码管扫描选择信号0~7循环对应8位数码管 reg [2:0] scan_sel; // 时序逻辑计数器满值时扫描信号1切换下一个数码管 always (posedge clk) begin if(clk_div 8d199) begin scan_sel (scan_sel 3d7) ? 3d0 : scan_sel 1d1; end end // 11. 数码管位选控制模块控制哪个数码管点亮 // 组合逻辑根据扫描信号和显示模式点亮对应数码管 always (*) begin // 默认状态所有数码管熄灭 dg010;dg020;dg030;dg040;dg050;dg060;dg070;dg080; if(!disp_sel) begin // 模式1显示M值 → 仅点亮最右侧5个数码管 case(scan_sel) 3d0: dg011; 3d1: dg021; 3d2: dg031; 3d3: dg041; 3d4: dg051; default: ; endcase end else begin // 模式2显示频率 → 8个数码管全部点亮 case(scan_sel) 3d0: dg011; 3d1: dg021; 3d2: dg031; 3d3: dg041; 3d4: dg051; 3d5: dg061; 3d6: dg071; 3d7: dg081; endcase end end // 12. 段码表 小数点控制 reg [7:0] seg; always (*) begin seg 8h00; // 默认所有笔画熄灭 // 显示M值时左侧3位熄灭 if(!disp_sel (scan_sel 3d5)) begin seg 8b00000000; // 8h00段码全灭 end else begin // 数字段码二进制显式书写 十六进制注释共阴极数码管 case(data[scan_sel]) 4d0: seg8b00111111; // 8h3F → 显示数字0 4d1: seg8b00000110; // 8h06 → 显示数字1 4d2: seg8b01011011; // 8h5B → 显示数字2 4d3: seg8b01001111; // 8h4F → 显示数字3 4d4: seg8b01100110; // 8h66 → 显示数字4 4d5: seg8b01101101; // 8h6D → 显示数字5 4d6: seg8b01111101; // 8h7D → 显示数字6 4d7: seg8b00000111; // 8h07 → 显示数字7 4d8: seg8b01111111; // 8h7F → 显示数字8 4d9: seg8b01101111; // 8h6F → 显示数字9 // 扩展A-F字符如需显示可启用同样二进制十六进制注释 4d10: seg8b01110111; // 8h77 → 显示字母A 4d11: seg8b01111100; // 8h7C → 显示字母b 4d12: seg8b00111001; // 8h39 → 显示字母C 4d13: seg8b01011110; // 8h5E → 显示字母d 4d14: seg8b01111001; // 8h79 → 显示字母E 4d15: seg8b01110001; // 8h71 → 显示字母F default:seg8b00000000;// 8h00 → 无匹配时熄灭 endcase end // 核心规则仅第7位(dg02/scan_sel1)点亮小数点其余全灭 dp (disp_sel (scan_sel 3d1)) ? 1b1 : 1b0; // 第7位数码管只显示小数点数字完全熄灭 if(disp_sel (scan_sel 3d1)) seg 8b00000000; // 8h00 end // 13. 数码管硬件引脚映射 // 组合逻辑将段码信号分配到对应硬件引脚 always (*) begin sa seg[6]; // 硬件引脚PIN_34 → 数码管a段 sb seg[5]; // 硬件引脚PIN_33 → 数码管b段 sc seg[4]; // 硬件引脚PIN_31 → 数码管c段 sd seg[3]; // 硬件引脚PIN_32 → 数码管d段 se seg[2]; // 硬件引脚PIN_28 → 数码管e段 sf seg[1]; // 硬件引脚PIN_38 → 数码管f段 sg seg[0]; // 硬件引脚PIN_39 → 数码管g段 end endmodule2.修改引脚相关设置这一步别忽略不然会报错点击Assignment → 点击Device→点击Device and Pin Options...→将nCEO修改为Use as regular I/O3.引脚分配在工程根目录里找到dds_final.qsf文件选择打开方式为以记事本打开不用更改后缀为.txt将以下内容粘贴到dds_final.qsf文件最末尾这样就不用在PinPlanner里一个个分配引脚了一百分的方便# 时钟 set_location_assignment PIN_23 -to clk # 开关 set_location_assignment PIN_51 -to sw1 set_location_assignment PIN_52 -to sw2 set_location_assignment PIN_53 -to sw3 set_location_assignment PIN_54 -to sw4 set_location_assignment PIN_55 -to sw5 set_location_assignment PIN_58 -to sw6 # 脉冲按键 set_location_assignment PIN_66 -to pk1 set_location_assignment PIN_67 -to pk2 # DAC set_location_assignment PIN_100 -to dac_clk set_location_assignment PIN_103 -to da[11] set_location_assignment PIN_101 -to da[10] set_location_assignment PIN_104 -to da[9] set_location_assignment PIN_110 -to da[8] set_location_assignment PIN_106 -to da[7] set_location_assignment PIN_112 -to da[6] set_location_assignment PIN_111 -to da[5] set_location_assignment PIN_114 -to da[4] set_location_assignment PIN_113 -to da[3] set_location_assignment PIN_119 -to da[2] set_location_assignment PIN_115 -to da[1] set_location_assignment PIN_121 -to da[0] # 数码管段选 set_location_assignment PIN_34 -to sa set_location_assignment PIN_33 -to sb set_location_assignment PIN_31 -to sc set_location_assignment PIN_32 -to sd set_location_assignment PIN_28 -to se set_location_assignment PIN_38 -to sf set_location_assignment PIN_39 -to sg set_location_assignment PIN_30 -to dp # 数码管位选 set_location_assignment PIN_49 -to dg01 set_location_assignment PIN_50 -to dg02 set_location_assignment PIN_44 -to dg03 set_location_assignment PIN_46 -to dg04 set_location_assignment PIN_42 -to dg05 set_location_assignment PIN_43 -to dg06 set_location_assignment PIN_10 -to dg07 set_location_assignment PIN_11 -to dg084.点击Start Compilation开始编译四实验箱接线1.K1接IO51K2接IO52K3接IO53K4接54K5接IO55K6接IO582.P1接IO66P2接IO67五使用规则1. 自锁开关K1-K5选择调节步长仅允许一个开关闭合2. 按下脉冲按键PK1M值按照选中的步长累加详情见代码开头的注释3. 闭合自锁开关K6M值直接清零4. 按下脉冲按键PK2切换显示模式M值 ↔ 输出频率5. 显示M值时仅右5位亮显示频率时8位全亮固定小数点操作实例例1需要M值1 → 闭合K1按一下PK1例2需要M值清零 → 闭合K6例3查看输出频率 → 按一下PK2切换模式

更多文章