基于FPGA的正弦信号发生器、滤波器的设计(DAC输出点数受限条件下的完整正弦波产生器)

前言

本设计只使用到了Zynq的PL端,未使用PS端,设计包含ROM(存储波形数据点)、DA输出模块、串口(TX输出)模块。

本设计不是一个单纯的DDS生成器,不是通过简单的存储波形信息输出,而是考察对于数字信号处理的理解。当波形输出点数不足时(如32点),波形会呈现明显的阶梯状,这是为什么呢?

其实是点数不足,正弦波形不平滑,导致产生梯形波,而梯形波含有非常高阶的谐波分量,因此在示波器中,我们可以看到波形呈现明显的锯齿状结构,要还原出不失真的平滑波形,滤波器的设计存在必不可少。设计滤波器是一大难点,要保证波形不失真的被滤出来的同时,也要避免滤波器对于原始信号的峰峰值削减。

此外,更需要注意的时候,本设计没有要求全频段滤波,这样的话,设计滤波器比较麻烦,老师要求我们对于1-100Hz进行滤波,以及对DDS所能达到的极限频率进行滤波处理即可,测评的时候只观察这两段波形的失真情况。但是,这里要求滤波器能够自动切换,需要引入分路开关、电子开关等原件,考虑到输出信号的频率达到兆赫兹,一般普通的电子开关直接传导信号可能会导致波形的变形、失真、削波等情况,因此器件选型非常之重要。


本次设计的源代码工程已经上传到github,需要的小伙伴自取,别忘了点个Star哦,谢谢!

https://github.com/lgddyza/FPGA-Based-DDS-filter-Designhttps://github.com/lgddyza/FPGA-Based-DDS-filter-Design

一、设计目的和要求

目的

结合电子系统设计一的设计结果,搭建正弦波发生器,理解正弦波发生器、DA转换模块工作原理,。通过学习和应用DDS技术,深入理解数字信号生成的工作原理,掌握相关硬件设计、程序设计及调试过程,并培养团队合作与自主学习的能力。

设计要求

利用上年度设计的DAC或外购DAC芯片,也可以使用STM32自带的DAC,单片机采用DDS的方法设计制作一台正弦信号发生器。输出信号的每个周期至少由8个点构成。(系统也可用FPGA实现)

基本要求:

输出信号频率至少在1Hz-100Hz可调,要求可调整到最高允许频率输出大小(不失真)。(用按键调整)

发挥部分:

输出信号峰峰值:3-5V可调(用按键调整)。根据单片机及DA的性能,估算并测量系统在每周期32点输出时正弦波的最高频率。

说明:

输出只要正弦波,不要方波、锯齿波等。输出正弦波不含直流成分。1-10Hz最多用上128点,10-100Hz最多用上64点,作品的最高频率只能用32点。

特殊要求!

不同于常规的电子系统设计实践,这一次的设计要求是,在输出点数较少的情况下,通过滤波的方式,得到完整的、不失真的信号波形。这一设计要求考察的是我们的数字信号处理、信号与系统的知识,也考察我们对滤波器电路的设计。

二、设计原理

2.1 方案选择

基于我们目前可选用的硬件,我们有三种方案可以选择,并且对三种方案都进行了代码设计与功能测试、实现,这三种方案不分优劣,其最终目的都是通过相位累加法和低通滤波器,实现对正弦波形的产生和还原。

这三种方案的核心原理都是相同的,通过MATLAB对正弦波形进行采样,产生32点一周期的正弦波形,再通过相位累加法,调整频率控制字以输出不同频率的正弦波,这些正弦波需要通过低通滤波器,消除高次谐波的干扰,以还原最原始的波形。

(1)STC8H8K64U(51单片机)方案

(2)STM32F405RGT6方案

(3)FPGA(Zynq-7020)方案

在本次设计报告中,我们选择以FPGA开发板为典型案例介绍相位累加法DDS的实现,其他两种方案我们不过多赘述,通过FPGA的Verilog HDL语言,我们可以更贴近底层原理地来了解整个DDS的工作流程,而51、STM32单片机的实现代码我们也放在工程文件夹中供参考。

我们的硬件设备包括FPGA Zynq-7020开发板和外部挂载的高速AD/DA板卡,以及低通滤波器,设计并实现一个频率可调的、波形不失真的DDS正弦信号发生器。

2.2 工作原理

(1)DDS设计原理

DDS 是直接数字式频率合成器(Direct Digital Synthesizer)的英文缩写,是一项关键的数字化技术。与传统的频率合成器相比,DDS具有低成本、低功耗、高分辨率和快速转换时间等优点,广泛使用在电信与电子仪器领域,是实现设备全数字化的一个关键技术。

DDS 的基本结构主要由相位累加器、相位调制器、波形数据表 ROM、D/A 转换器等四大结构组成,通常在数模转换器之后增加一个低通滤波器以提高输出效果。通过在ROM中存放波形数据,通过相位累加器、相位调制器来实现波形输出、频率调整,通过DA转换器,把完整的波形(如正弦波)输出出来。

相位累加器是整个DDS的核心,在这里完成相位累加,生成相位码。相位累加器的输入为频率字输入 K,表示相位增量,设其位宽为N,满足等式:K= 2^N * fout / fCLK。

其在输入相位累加器之前,在系统时钟同步下做数据寄存,数据改变时不会干扰相位累加器的正常工作。波形数据表ROM中存有一个完整周期的正弦波信号,波形数据表ROM以相位调制器传入的相位码为ROM读地址,将地址对应存储单元中的电压幅值数字量输出。

D/A 转换器将输入的电压幅值数字量转换为模拟量输出,就得到输出信号CLK_OUT。输出信号CLK_OUT的信号频率fOUT = K * fCLK / 2^N。当K = 1时,可得DDS最小分辨率为:fOUT = fCLK / 2^N,此时输出信号频率最低。根据采样定理,K的最大值应小于2^N / 2。

(2)系统设计原理

系统由FPGA核心开发板、DA模块、两路信号选通模块、两个低通滤波器、继电器以及按键输入模块构成。先由MATLAB按照设计要求生成不同点数的正弦波形,再由FPGA控制产生对应点数的正弦波(DDS原理),产生波形,并加入按键控制逻辑(幅度、频率的调整)。

本设计主要对两个频率段进行滤波:

(1)1-100Hz频率段(低频段)

(2)1MHz - 4MHz(高频段)

并依此设计了两个低通滤波器,以实现对正弦波形的不失真还原。

两路选通模块、继电器模块是为了保证信号的路径正确传输,两路信号选通模块针对FPGA传来的频率信号进行判断,选择合适的滤波通路,再由继电器实现两路并为一路的设计,能够通过电子开关选通合适的滤波通路,无需手动开关实现电路滤波参数切换。

这里选用两路选通模块+继电器的设计原因:并非冗余而是确有必要。

两路选通模块将信号源分为了高低两段频率波形输出,并通过来自FPGA的选通信号进行滤波器通路选择,实现两路信号的分离,避免串扰;而继电器则是对两路滤波器的输出进行选通:按照原理此处本应该设计为电压跟随器,以实现输出缓冲的效果,隔离前级两个滤波器的干扰。但此处由于有高频信号的存在,运放的跟随效果受到硬件电路的影响比较大,并且有较高的性能和双电源(此时滤波输出已经为交流信号)的要求,因此设计电压跟随器是比较麻烦且易引入额外干扰的。因此这里采用继电器,而非模拟电子开关,也有避免硬件电路设计影响波形效果的考虑存在。继电器的通路性能接近理想导线,隔离度非常好,很适合作为高频信号的选通开关使用;而模拟开关IC,如CD4053等,对1MHz以上的信号通过时存在各种性能缺陷,会导致波形效果变差。

(3)滤波器设计原理

本次设计需要设计滤波器的原因:

本次设计的要求是通过较少的点数(32点)构成正弦波的一周期,通过单片机或FPGA等设备实现相位累加法输出正弦波形,由于波形的点数较少,在实际的DAC输出波形中,会产生梯形(多边形)的不规则形状,这样的梯形波存在着非常丰富的高次谐波分量,会导致波形存在锯齿的边缘形状,偏离正弦波。而我们可以通过设计滤波器的方式,滤除掉这些高频分量,从波形中提取、还原出原始的波形。

巴特沃思低通滤波器(有源)原理:

巴特沃思低通滤波器是一种基于运算放大器的有源滤波器,其核心原理是通过运放和RC网络构成反馈系统,实现频率选择性衰减。在通带内具有最大平坦的幅频响应(无纹波),截止频率处衰减为-3dB,阻带以每倍频程-20n dB/oct(n为阶数)的速率单调下降。二阶有源巴特沃思滤波器通常采用Sallen-Key拓扑结构,通过两个RC网络和单运放构成,传递函数为二阶系统,Q值为0.707,确保通带最平坦且无过冲。运放提供电压增益和隔离作用,使滤波器特性不受负载影响,且可实现增益大于1的放大功能。具体的巴特沃斯滤波器实现方法可以通过multisim等仿真软件的滤波器设计向导来辅助实现,见后文详细说明。

二阶RC低通滤波器原理:

二阶RC低通滤波器由两个RC级联构成,属于无源滤波器。其基本原理是利用电容的容抗随频率升高而减小的特性,对高频信号产生分流衰减。两个RC节通过阻抗匹配或缓冲隔离级联,传递函数为二阶系统,截止频率:

通带内信号基本无衰减通过,阻带以-40dB/dec(或-12dB/oct)的斜率衰减。由于无源结构,存在负载效应,实际特性受前后级阻抗影响,且通带内存在插入损耗。若直接级联,两节RC之间会相互影响,需通过缓冲器或阻抗匹配设计来逼近理想二阶特性。

2.3 关键器件

外载AD/DA板卡的DA部分使用高速DA芯片AD9708。AD9708是一款高性能、低功耗CMOS数模转换器(DAC)的8位分辨率产品。最大特点是八位并行的AD、DA模块,并行结构决定其速度可以达到惊人的50MHz输出速率。


TS12A12511模块,高速单刀双掷模拟开关,是一款双向单通道单极双投(SPDT)模拟开关,最大可传递摆幅为-6V至6V的信号。模块供电范围为2.7V至6V,支持轨对轨双向输入输出,信号带宽接近100MHz,通道间隔离度可达70dB。


DR011单路数字电位器模块,数字可调电阻,可通过串口实现39 Ω - 10k Ω范围内的电阻调节。


六脚继电器,这里起到单刀双掷开关的作用,当频率改变需要切换滤波器时,输出信号改变继电器的接入端,从而切换滤波器通路。

2.4 程序设计

Sys_clk为系统时钟信号线,同时也是DA转换器的控制时钟线;sys_rst_n是低电平复位信号线,通过输入低电平实现整机的复位;key是按键输入信号,一共有四个按键,两个分别控制SIgnal_AMP信号幅度的增加、减少,共分为20档步进调整;两个分别控制频率的加、减,步进按照当前频率的最大单位来调整。

Dac_data作为信号输出线,负责把DAC数据传递给下一级的DA转换器,用于产生正弦波信号。同时还有两路滤波器选通信号,负责控制滤波器链路的通断。Signal_Select信号有两条,分别控制两个分别由双通信号开关、六脚继电器构成的两个电子开关的选通。TX串口输出信号负责输出电阻控制信息,用于调整滤波器的截止频率参数,以实现跟随式的不失真、不削波滤波效果。

当系统输出的频率控制字范围属于波形输出频率的1-10Hz区间时,输出波形由128个点构成,即从对应ROM中提取波形数据,在频率控制字下输出对应的波形。当频率控制字输出的波形频率范围大于100Hz时,波形均由32个采样点构成。此时,系统通过动态切换ROM中的波形采样点数量,在保证输出精度的同时兼顾实时性与资源利用率;在频率调节过程中,FPGA内部逻辑自动识别当前频段并选择对应的波形查找表,确保各频段输出的正弦信号连续、平滑且无跳变。当按键调整幅度或频率时,控制模块实时更新DAC控制字与滤波器参数。

同时,Signal_Select信号会根据当前的频率范围,选中特定的滤波器通路,以匹配不同频段的滤波需求,确保信号在低频段能够不失真、不削波的被还原为正弦波;在高频段则有效抑制谐波干扰,提升输出信号的纯净度。滤波器链路由二阶RC网络与巴特沃斯滤波器构成,结合继电器切换不同截止频率的滤波通路,实现宽频带内的自适应滤波响应。

整个程序以 50 MHz 系统时钟为基础,实现一个可调频、可调幅的 DDS 正弦波发生器:系统上电后,按键首先经过同步与消抖处理,形成单个有效脉冲;当按下频率键时,根据当前频率大小自动选择合适的步进值,对 32 位频率控制字 freq_word 进行加减,从而改变 DDS 相位累加器的增长速度;相位累加器的高位作为地址去查正弦波 ROM,在低频段自动切换为 128 点表以提高波形平滑度,其余频段使用 32 点表;查表得到的波形数据再与由幅度按键控制的 Q15 格式增益系数相乘,实现幅度调节,最终输出到 DAC;同时根据频率范围生成指示信号;当检测到频率按键事件时,系统会将当前频率映射成参数字节,并通过串口按固定帧格式(5A 00 XX A5)发送出去,串口采用标准 8N1 时序;整个系统通过"按键控制 → 频率/幅度计算 → DDS 波形合成 → 幅度缩放 → DAC 输出 → 串口反馈"的流程完成信号发生与状态上报.

2.5 流程介绍

关键代码解释:

1) DDS 核心:相位累加器 + ROM 地址

功能:用 freq_word 控制相位增长速度,从而决定输出频率。

bash 复制代码
always @(posedge sys_clk or negedge sys_rst_n) begin
  if(!sys_rst_n)
      fre_add <= 32'd0;
  else
      fre_add <= fre_add + freq_word;
end

fre_add 是 32 位相位累加器,每个时钟累加freq_word。

DDS 输出频率公式(理论): fOUT = freq_word / 232 * fCLK


ROM 地址提取:

bash 复制代码
rom_addr_128 fre_add[31:25];// 128> 7it
rom_addr_32  <= fre_add[31:27];  // 32点  -> 5bit

取相位高位作为查表地址:高位变化快、代表相位进度。

2)幅度档位、增益映射(20档线性)

bash 复制代码
always @(posedge sys_clk or negedge sys_rst_n) begin

  if(!sys_rst_n) amp_level <= AMP_LEVEL_INIT[4:0];

  else begin

      if (key_amp_up_pulse) begin

          if (amp_level < AMP_LEVEL_MAX[4:0]) amp_level <= amp_level + 1'b1;

      end else if (key_amp_down_pulse) begin

          if (amp_level > 5'd0) amp_level <= amp_level - 1'b1;

      end

  end

end

增益计算(线性插值 + 四舍五入):

bash 复制代码
assign gain_q15 = GAIN_MIN_Q15

              + ( (amp_level * GAIN_DELTA_Q15 + (AMP_LEVEL_MAX/2)) / AMP_LEVEL_MAX );

amp_level=0 → 增益约 0.66

amp_level=19 → 增益约 1.00

3) 频率控制字更新:动态步进

步进函数:

bash 复制代码
function automatic [31:0] get_step;

  input [31:0] fw_now;

  begin

      if      (fw_now < 32'd859)       get_step = 32'd86;        // 1Hz

      else if (fw_now < 32'd8590)      get_step = 32'd859;       // 10Hz

      ...

  end

endfunction

assign freq_step_dyn = get_step(freq_word);

加减频

bash 复制代码
always @(posedge sys_clk or negedge sys_rst_n) begin

  if(!sys_rst_n) freq_word <= FREQ_INIT;

  else begin

      if (key_up_pulse) begin

          if (freq_word <= (FREQ_MAX - freq_step_dyn))

              freq_word <= freq_word + freq_step_dyn;

          else

              freq_word <= FREQ_MAX;

      end else if (key_dn_pulse) begin

          if (freq_word >= (FREQ_MIN + freq_step_dyn))

              freq_word <= freq_word - freq_step_dyn;

          else

              freq_word <= FREQ_MIN;

      end

  end

end
  1. 频率范围指示输出(切换滤波器)
bash 复制代码
if (freq_word >= FW_1HZ && freq_word < FW_10HZ) flag=0;

else if (freq_word >= FW_10HZ && freq_word <= FW_100HZ) flag=1;

else flag=0;

在不同频率范围下切换滤波器通路

  1. 串口发送:触发→组帧→状态机发送

触发事件(检测频率按键脉冲出现)

bash 复制代码
if (key_press_event) begin

    tx_buffer[0] <= 8'h5A;

    tx_buffer[1] <= 8'h00;

    tx_buffer[2] <= freq_param; // 由freq_word映射出来

    tx_buffer[3] <= 8'hA5;

end

发送给可调电阻器,根据频率及时调整滤波器参数,实现更好地滤波效果。

三、电路调试

3.1 滤波器设计

当频率输出较低时,波形阶梯状较为明显,频率较高时波形失真较严重,于是决定在输出端填加低通滤波器。通过控制单极双投(SPDT)模拟开关和继电器来选择滤波器通路。

低频段(1-100HZ)我们采用二阶RC低通滤波器进行滤波,数字电位器根据输出频率及时跟随改变阻值,调整滤波器参数,获得最好的滤波效果。

在高频段,我们采用巴特沃斯滤波器,对此可以采用Multism的滤波器设计工具进行基础设计,在此基础上再进行修改。

3.2 程序设计调整

在最初的设计中,按要求不断按下增幅键,超过一定幅度时,波形顶反而会从底下冒出来,这是因为 DDS 中幅度调节是在"数字域乘法 + 有限位宽 + 溢出/截断"条件下完成的,幅度超过数值表示范围后发生了回绕,于是正弦波的峰值从负半轴出现。本质原因是数值溢出 + 二进制补码表示。

对此,我们将正弦波 ROM 采用无符号、带直流偏置的数据格式,幅度调节使用 Q15 定点增益且最大值限制为 1.0(只衰减不放大),并在乘法后按比例右移取位,因此乘法结果永不溢出、不回绕,从设计源头消除了波形翻转问题。

bash 复制代码
localparam [15:0] GAIN_MIN_Q15 = 16'd21627; // 0.66

localparam [15:0] GAIN_MAX_Q15 = 16'd32768; // 1.00

3.3 实际测试输出波形

|-----------|----------------------------------------------------------------------------|
| 输出频率 | 波形图(蓝色为滤波前原始波形) |
| 5HZ | |
| 10HZ | |
| 20HZ | |
| 30HZ | |
| 40HZ | |
| 50HZ | |
| 60HZ | |
| 70HZ | |
| 80HZ | |
| 90HZ | |
| 100HZ | |
| 1MHZ | |
| 2MHZ | |

3.4 电路中仍存在的缺陷及优化路径

在验收过程中,赖老师给我们测试了低频情况下的波形输出,发现在5Hz的时候,波形的峰峰值存在轻微衰减,从5.0V的峰峰值衰减到了4.78V,表明低频段信号传输路径中存在微弱的增益压缩现象。经查找资料了解到,该问题源于电容在低频段的容抗增大,导致信号耦合效率下降,尤其在多级滤波与放大环节级联时累积形成微小衰减。此外,运算放大器在接近直流工作点时的开环增益降低,亦会引入非线性误差。优化路径包括改用更高容值的耦合电容以降低低频阻抗,选用低频响应更优的运放型号,并在反馈网络中加入直流偏置补偿机制。同时可对DAC输出端实施预加重处理,在数字域适度提升低频增益,以抵消模拟链路中的衰减,从而实现全频段内幅频特性的一致性。预加重策略可通过在FPGA内设置可编程的数字滤波器实现,其系数随输出频率动态调整,在低频段提供适量增益补偿。结合实测数据建立幅频响应误差模型,将补偿算法嵌入信号生成流程,可在不增加硬件成本的前提下显著改善平坦度。此外,布局上应缩短模拟信号走线长度,减少寄生参数对低频信号的影响。电源去耦亦需优化,确保运放供电稳定性。

此外,本次设计由于时间原因,并未设计PCB,而是采用洞洞板焊接的方式设计的滤波电路和放大电路,导致布线较长且难以做到阻抗匹配,引入了额外的分布电容与串扰,尤其在高频段影响更为显著。手工焊接的节点也存在接触电阻不均问题,进一步加剧信号失真。这种非理想因素叠加,使得实际幅频响应偏离理论设计,相位延迟亦出现波动。从工程实践看,临时搭建的测试环境虽能满足功能验证,但可能对系统的性能极限测试造成一定影响。

四、总结

4.1 设计效果

本次课程设计完成了一套基于 DDS 原理的正弦信号发生电路,实现了正弦波信号的稳定输出。系统通过 FPGA 实现相位累加与正弦查表运算,经 DAC 转换后输出模拟信号,并在输出端引入低通滤波器和放大电路,有效改善了波形质量。本设计最高输出频率可以达到2MHZ基本不变形、不削波(保持输出的波形峰峰值时钟保持在5V±0.25V,允许电路5%左右的浮动)。

在设计结果方面,系统能够在设定的频率范围内实现连续、可调的正弦信号输出,频率转换速率快,输出频率与理论计算值基本一致,频率精度主要由频率控制字位宽和系统时钟决定,满足课程设计要求。通过查找表方式生成正弦波,相较于实时运算方法有效减小了低幅值区域的失真,提高了波形平滑度。输出端低通滤波器能够有效抑制 DAC 输出中的高频阶梯分量,滤波后波形更加接近理想正弦信号。整体电路工作稳定,功能实现完整。

4.2 极限性能测试

在设计过程中,对于极限频率的测试,理论与实际结果的讨论:

(1)极限频率的理论计算:极限频率主要受限于板卡性能、DA转换器的速度两个因素,同时也考虑到滤波器、放大器的带宽限制等的影响。

(2)系统采用FPGA设计,逻辑电路并行的架构保证了信号处理的实时性与低延迟,尤其在生成高频复杂波形时展现出显著优势。所以此处我们不考虑硬件上的处理延迟对系统性能的影响。

(3)高速DAC的最大输出频率典型值为50MHz,极限值为130MHz。因此,在生成一周期32点的正弦波的条件下,理论极限频率可达130MHz / 32 ≈ 4.06MHz,实际测试中,不添加滤波器和电路设计时,系统在4MHz时波形仍基本完整,且电压峰峰值能保持良好状态。当加入模拟滤波与放大电路后,受限于运放增益带宽积及电路寄生参数,实际可用频率上限降至约2MHz,且在6 MHz以上时出现明显的失真、削波情况。

高速DAC的建立时间与保持时间在接近奈奎斯特频率时成为限制因素,实际输出波形在4MHz的时候发生了轻微的削波情况,5V的峰峰值仅存4.5V,表明驱动能力受限。经示波器观测,边沿陡峭度下降,过渡时间延长,反映出输出级带宽不足。

4.3 重难点的解决

在设计过程中,重点解决了以下关键问题:DDS 数字正弦信号的高效生成与频率精确控制;DAC 输出高频噪声对波形质量的影响;

DDS数字正弦信号的生成、频率控制通过相位累加器与查找表的协同设计,深入学习了相位累加法的正弦波实现过程,通过设置合理的步进,完成设计要求目标。掌握了通过单片机、FPGA等控制DDS系统频率字与相位字的调节方法,实现高分辨率频率输出;按键消抖、幅度、频率控制,以及对FPGA内部逻辑与外围电路时序匹配等环节的优化,提高对嵌入式设计的综合把控能力。

DAC输出高频噪声的来源主要来自32点时波形自身的阶梯状特征,这也是我们本次设计中主要讨论并解决的重点部分,了解噪声的来源、去除干扰的方法,通过设计低通滤波器有效抑制了高次谐波分量,同事加深对低通滤波器设计原理的理解,结合电路仿真优化截止频率与衰减特性,使输出波形光滑度显著提升。结合数字信号处理课程的教学内容,再次对巴特沃思滤波器、二阶RC滤波器的设计加深理解。同时,在实际调试中进一步验证了滤波器阶数与截止频率对输出波形的影响,发现二阶巴特沃思滤波器在兼顾过渡带陡峭性与相位线性方面表现最优。

五、心得体会

这是一次软硬件综合的电子系统设计,既需要掌握编程设计技巧,又需要有硬件滤波器设计的知识体系。对于电子类专业的同学考察的方面比较广。通过本次实践,我对 DDS 的工作原理,尤其是相位累加算法和查表法生成正弦波的过程有了更加深入的理解。

在设计初期,我通过查阅相关资料并结合课堂所学,完成了基于 FPGA 的相位累加器、正弦波 ROM 查表以及频率控制字的整体架构设计。在实际调试过程中,我逐渐体会到理论公式与工程实现之间的差异,例如频率控制字位宽选择、ROM 点数对波形质量的影响等问题。通过反复仿真和板级验证,我进一步掌握了如何利用相位累加器高位作为查表地址,并根据频率范围动态切换查找表点数,从而在保证波形质量的同时兼顾系统性能。

此外,在系统调试过程中,我还参与了幅度调节逻辑和频率步进算法的优化设计,对定点数运算、溢出控制以及数值稳定性有了更直观的认识。这让我意识到,在 FPGA 设计中,合理的数据格式与边界控制对于系统可靠性至关重要。

通过本次课程设计,我不仅加深了对 DDS 数字信号合成原理的理解,也提升了 Verilog HDL 编程能力和系统级调试能力,为今后进一步学习 FPGA 和数字系统设计奠定了良好基础。

六、附录

bash 复制代码
程序代码:
`timescale 1ns/1ns

module dds
(
  input   wire        sys_clk     ,   // 系统时钟 50MHz
  input   wire        sys_rst_n   ,   // 复位信号 低有效

  // key_wave -> 幅度增加(往大调整,低有效)
  input   wire        key_wave   ,

  input   wire        key_freq_up,   // 频率增加按键(低有效)
  input   wire        key_freq_dn,   // 频率减小按键(低有效)

  // key_amp -> 幅度减小(往小调整,低有效)
  input   wire        key_amp    ,

  output  wire [7:0]  data_out   ,   // 波形输出
  output  wire        dac_clk    ,
  
  // 新增:频率范围指示信号
  output  wire        freq_range_sig1,  // 频率范围指示信号1
  output  wire        freq_range_sig2,  // 频率范围指示信号2
  
  // 新增:串口TX输出
  output  wire        uart_tx         // 串口TX信号
);

assign dac_clk = ~sys_clk;

//====================================================================
// 参数配置
//====================================================================
parameter integer  DEBOUNCE_CNT_MAX = 1_000_000;

parameter [31:0]   FREQ_INIT = 32'd85899;
parameter [31:0]   FREQ_MIN  = 32'd0;
parameter [31:0]   FREQ_MAX  = 32'hFFFF_FFFF;

parameter integer  AMP_LEVEL_MAX  = 19;
parameter integer  AMP_LEVEL_INIT = 0;

localparam [15:0] GAIN_MIN_Q15   = 16'd21627;  // 0.66
localparam [15:0] GAIN_MAX_Q15   = 16'd32768;  // 1.00
localparam [15:0] GAIN_DELTA_Q15 = GAIN_MAX_Q15 - GAIN_MIN_Q15;

// 频率阈值定义(freq_word域)
localparam [31:0] FW_1HZ     = 32'd86;      // 1Hz
localparam [31:0] FW_10HZ    = 32'd820;     // 10Hz
localparam [31:0] FW_100HZ   = 32'd8600;    // 100Hz

// 串口参数
localparam CLK_FREQ   = 50_000_000;  // 系统时钟频率 50MHz
localparam BAUD_RATE  = 19200;       // 波特率
localparam BAUD_CNT_MAX = CLK_FREQ / BAUD_RATE;  // 每个位周期的时钟数

//====================================================================
// 内部信号
//====================================================================
wire [7:0] data_256;
wire [7:0] data_32;

reg  [31:0] fre_add;
reg  [7:0]  rom_addr_256;
reg  [4:0]  rom_addr_32;

reg  [31:0] freq_word;
wire [31:0] freq_step_dyn;

wire [7:0]  data_sel;

reg  [4:0]  amp_level;
wire [15:0] gain_q15;

wire [23:0] mult_full;
wire [7:0]  data_scaled;

// 串口发送相关信号
reg  [3:0]  tx_state;         // 串口发送状态
reg  [15:0] baud_cnt;         // 波特率计数器
reg  [3:0]  bit_cnt;          // 位计数器
reg  [7:0]  tx_data;          // 待发送数据
reg  [7:0]  uart_tx_reg;      // 串口TX寄存器
reg  [3:0]  byte_cnt;         // 字节计数器
wire        tx_start;         // 发送启动信号
wire        tx_busy;          // 发送忙信号
reg  [7:0]  tx_buffer [0:3];  // 发送缓冲区
reg         tx_enable;        // 发送使能标志

//====================================================================
// 新增:频率范围判断信号
//====================================================================
reg freq_range_flag;  // 频率范围标志
// 0: 1-10Hz (低电平)
// 1: 10-100Hz (高电平)
// 0: >100Hz (低电平)

always @(posedge sys_clk or negedge sys_rst_n) begin
  if(!sys_rst_n) begin
      freq_range_flag <= 1'b0;
  end else begin
      if (freq_word >= FW_1HZ && freq_word < FW_10HZ) begin
          // 1-10Hz范围(不含10Hz)
          freq_range_flag <= 1'b0;
      end else if (freq_word >= FW_10HZ && freq_word <= FW_100HZ) begin
          // 10-100Hz范围(含10Hz,不含100Hz)
          freq_range_flag <= 1'b1;
      end else begin
          // 其他范围(<1Hz, >=100Hz)
          freq_range_flag <= 1'b0;
      end
  end
end

// 分配输出信号
assign freq_range_sig1 = freq_range_flag;
assign freq_range_sig2 = freq_range_flag;

//====================================================================
// 频率控制字:按键加减(动态步进)
//====================================================================
always @(posedge sys_clk or negedge sys_rst_n) begin
  if(!sys_rst_n) begin
      freq_word <= FREQ_INIT;
  end else begin
      if (key_up_pulse) begin
          if (freq_word <= (FREQ_MAX - freq_step_dyn))
              freq_word <= freq_word + freq_step_dyn;
          else
              freq_word <= FREQ_MAX;
      end else if (key_dn_pulse) begin
          if (freq_word >= (FREQ_MIN + freq_step_dyn))
              freq_word <= freq_word - freq_step_dyn;
          else
              freq_word <= FREQ_MIN;
      end
  end
end

// 根据当前频率获取变化参数
function automatic [7:0] get_freq_param;
  input [31:0] freq_word;
  begin
      if (freq_word >= FW_10HZ && freq_word < 2*FW_10HZ) begin
          // 10-20Hz
          get_freq_param = 8'h4F;  // 10Hz
      end else if (freq_word >= 2*FW_10HZ && freq_word < 4*FW_10HZ) begin
          // 20-40Hz
          get_freq_param = 8'h3E;  // 20、30Hz
      end else if (freq_word >= 4*FW_10HZ && freq_word < 5*FW_10HZ) begin
          // 40-50Hz
          get_freq_param = 8'h2B;  // 40Hz
      end else if (freq_word >= 5*FW_10HZ && freq_word < 6*FW_10HZ) begin
          // 50-60Hz
          get_freq_param = 8'h20;  // 50Hz
      end else if (freq_word >= 6*FW_10HZ && freq_word < 7*FW_10HZ) begin
          // 60-70Hz
          get_freq_param = 8'h19;  // 60Hz
      end else if (freq_word >= 7*FW_10HZ && freq_word < 8*FW_10HZ) begin
          // 70-80Hz
          get_freq_param = 8'h15;  // 70Hz
      end else if (freq_word >= 8*FW_10HZ && freq_word < 11*FW_10HZ) begin
          // 80-110Hz
          get_freq_param = 8'h10;  // 80、90、100Hz
      end else begin
          // 其他频率
          get_freq_param = 8'h00;
      end
  end
endfunction

// 获取频率对应的参数
wire [7:0] freq_param = get_freq_param(freq_word);

// 检测频率按键按下事件
reg key_up_pulse_d1, key_dn_pulse_d1;
always @(posedge sys_clk or negedge sys_rst_n) begin
  if(!sys_rst_n) begin
      key_up_pulse_d1 <= 1'b0;
      key_dn_pulse_d1 <= 1'b0;
  end else begin
      key_up_pulse_d1 <= key_up_pulse;
      key_dn_pulse_d1 <= key_dn_pulse;
  end
end

// 检测按键上升沿,用于触发一次串口发送
wire key_up_rising = (~key_up_pulse_d1) & key_up_pulse;
wire key_dn_rising = (~key_dn_pulse_d1) & key_dn_pulse;
wire key_press_event = key_up_rising | key_dn_rising;

// 填充发送缓冲区
always @(posedge sys_clk or negedge sys_rst_n) begin
  if(!sys_rst_n) begin
      tx_buffer[0] <= 8'h5A;
      tx_buffer[1] <= 8'h00;
      tx_buffer[2] <= 8'h4F;
      tx_buffer[3] <= 8'hA5;
  end else begin
      // 当有按键事件时,更新缓冲区
      if (key_press_event) begin
          tx_buffer[0] <= 8'h5A;      // 固定帧头
          tx_buffer[1] <= 8'h00;      // 固定为00
          tx_buffer[2] <= freq_param; // 变化参数
          tx_buffer[3] <= 8'hA5;      // 固定帧尾
      end
  end
end

// 发送使能信号
always @(posedge sys_clk or negedge sys_rst_n) begin
  if(!sys_rst_n) begin
      tx_enable <= 1'b0;
  end else begin
      if (key_press_event && !tx_busy) begin
          tx_enable <= 1'b1;  // 按键释放时触发发送
      end else if (tx_state == 4'd0 && tx_enable) begin
          tx_enable <= 1'b0;  // 发送完成后清除
      end
  end
end

assign tx_start = tx_enable && (tx_state == 4'd0);
assign tx_busy = (tx_state != 4'd0);

//====================================================================
// 串口发送控制逻辑
//====================================================================
always @(posedge sys_clk or negedge sys_rst_n) begin
  if(!sys_rst_n) begin
      tx_state <= 4'd0;
      baud_cnt <= 16'd0;
      bit_cnt <= 4'd0;
      tx_data <= 8'd0;
      uart_tx_reg <= 1'b1;  // 空闲状态为高电平
      byte_cnt <= 4'd0;
  end else begin
      case(tx_state)
          4'd0: begin  // 空闲状态
              uart_tx_reg <= 1'b1;  // 保持高电平
              baud_cnt <= 16'd0;
              bit_cnt <= 4'd0;
              
              if (tx_start) begin
                  tx_state <= 4'd1;  // 进入发送状态
                  tx_data <= tx_buffer[0];  // 发送第一个字节
                  byte_cnt <= 4'd1;  // 从第一个字节开始
              end
          end
          
          4'd1: begin  // 发送起始位
              uart_tx_reg <= 1'b0;  // 起始位为低电平
              if (baud_cnt == BAUD_CNT_MAX-1) begin
                  baud_cnt <= 16'd0;
                  tx_state <= 4'd2;  // 进入数据位发送
              end else begin
                  baud_cnt <= baud_cnt + 1'b1;
              end
          end
          
          4'd2: begin  // 发送数据位
              uart_tx_reg <= tx_data[bit_cnt];
              if (baud_cnt == BAUD_CNT_MAX-1) begin
                  baud_cnt <= 16'd0;
                  if (bit_cnt == 4'd7) begin
                      bit_cnt <= 4'd0;
                      tx_state <= 4'd3;  // 进入停止位发送
                  end else begin
                      bit_cnt <= bit_cnt + 1'b1;
                  end
              end else begin
                  baud_cnt <= baud_cnt + 1'b1;
              end
          end
          
          4'd3: begin  // 发送停止位
              uart_tx_reg <= 1'b1;  // 停止位为高电平
              if (baud_cnt == BAUD_CNT_MAX-1) begin
                  baud_cnt <= 16'd0;
                  if (byte_cnt == 4'd4) begin
                      // 4个字节发送完成
                      tx_state <= 4'd0;  // 返回空闲
                  end else begin
                      // 发送下一个字节
                      tx_data <= tx_buffer[byte_cnt];
                      byte_cnt <= byte_cnt + 4'd1;
                      tx_state <= 4'd1;  // 发送起始位
                  end
              end else begin
                  baud_cnt <= baud_cnt + 1'b1;
              end
          end
          
          default: begin
              tx_state <= 4'd0;
          end
      endcase
  end
end

assign uart_tx = uart_tx_reg;

//====================================================================
// freq_word 动态步进选择(50MHz 下的阈值/步进)
//====================================================================
function automatic [31:0] get_step;
  input [31:0] fw_now;
  begin
      if      (fw_now < 32'd859)       get_step = 32'd86;        // <10Hz  -> 1Hz步进
      else if (fw_now < 32'd8590)      get_step = 32'd859;       // <100Hz -> 10Hz步进
      else if (fw_now < 32'd85899)     get_step = 32'd8590;      // <1kHz  -> 100Hz步进
      else if (fw_now < 32'd858993)    get_step = 32'd85899;     // <10kHz -> 1kHz步进
      else if (fw_now < 32'd8589935)   get_step = 32'd858993;    // <100k  -> 10kHz步进
      else if (fw_now < 32'd85899346)  get_step = 32'd8589935;   // <1MHz  -> 100k步进
      else                              get_step = 32'd85899346; // >=1MHz -> 1MHz步进
  end
endfunction

assign freq_step_dyn = get_step(freq_word);

//====================================================================
// 按键:同步 + 消抖 + 单次按下脉冲(低有效,检测下降沿)
//====================================================================

// 1) 两级同步
reg key_wave_s0, key_wave_s1;
reg key_up_s0,   key_up_s1;
reg key_dn_s0,   key_dn_s1;
reg key_amp_s0,  key_amp_s1;

always @(posedge sys_clk or negedge sys_rst_n) begin
  if(!sys_rst_n) begin
      key_wave_s0 <= 1'b1; key_wave_s1 <= 1'b1;
      key_up_s0   <= 1'b1; key_up_s1   <= 1'b1;
      key_dn_s0   <= 1'b1; key_dn_s1   <= 1'b1;
      key_amp_s0  <= 1'b1; key_amp_s1  <= 1'b1;
  end else begin
      key_wave_s0 <= key_wave;     key_wave_s1 <= key_wave_s0;
      key_up_s0   <= key_freq_up;  key_up_s1   <= key_up_s0;
      key_dn_s0   <= key_freq_dn;  key_dn_s1   <= key_dn_s0;
      key_amp_s0  <= key_amp;      key_amp_s1  <= key_amp_s0;
  end
end

// 2) 消抖
reg [19:0] wave_cnt, up_cnt, dn_cnt, amp_cnt;

reg wave_db, up_db, dn_db, amp_db;
reg wave_db_d, up_db_d, dn_db_d, amp_db_d;

wire wave_stable_change = (key_wave_s1 != wave_db);
wire up_stable_change   = (key_up_s1   != up_db);
wire dn_stable_change   = (key_dn_s1   != dn_db);
wire amp_stable_change  = (key_amp_s1  != amp_db);

always @(posedge sys_clk or negedge sys_rst_n) begin
  if(!sys_rst_n) begin
      wave_cnt <= 20'd0; up_cnt <= 20'd0; dn_cnt <= 20'd0; amp_cnt <= 20'd0;

      wave_db <= 1'b1; up_db <= 1'b1; dn_db <= 1'b1; amp_db <= 1'b1;
      wave_db_d <= 1'b1; up_db_d <= 1'b1; dn_db_d <= 1'b1; amp_db_d <= 1'b1;
  end else begin
      wave_db_d <= wave_db;
      up_db_d   <= up_db;
      dn_db_d   <= dn_db;
      amp_db_d  <= amp_db;

      if (wave_stable_change) begin
          if (wave_cnt == DEBOUNCE_CNT_MAX-1) begin
              wave_cnt <= 20'd0; wave_db <= key_wave_s1;
          end else wave_cnt <= wave_cnt + 1'b1;
      end else wave_cnt <= 20'd0;

      if (up_stable_change) begin
          if (up_cnt == DEBOUNCE_CNT_MAX-1) begin
              up_cnt <= 20'd0; up_db <= key_up_s1;
          end else up_cnt <= up_cnt + 1'b1;
      end else up_cnt <= 20'd0;

      if (dn_stable_change) begin
          if (dn_cnt == DEBOUNCE_CNT_MAX-1) begin
              dn_cnt <= 20'd0; dn_db <= key_dn_s1;
          end else dn_cnt <= dn_cnt + 1'b1;
      end else dn_cnt <= 20'd0;

      if (amp_stable_change) begin
          if (amp_cnt == DEBOUNCE_CNT_MAX-1) begin
              amp_cnt <= 20'd0; amp_db <= key_amp_s1;
          end else amp_cnt <= amp_cnt + 1'b1;
      end else amp_cnt <= 20'd0;
  end
end

// 3) 单次按下脉冲(下降沿)
wire key_amp_up_pulse   = (wave_db_d == 1'b1) && (wave_db == 1'b0);
wire key_up_pulse       = (up_db_d   == 1'b1) && (up_db   == 1'b0);
wire key_dn_pulse       = (dn_db_d   == 1'b1) && (dn_db   == 1'b0);
wire key_amp_down_pulse = (amp_db_d  == 1'b1) && (amp_db  == 1'b0);

//====================================================================
// 幅值控制:20阶(0.66~1.00)
//====================================================================
always @(posedge sys_clk or negedge sys_rst_n) begin
  if(!sys_rst_n) begin
      amp_level <= AMP_LEVEL_INIT[4:0];
  end else begin
      if (key_amp_up_pulse) begin
          if (amp_level < AMP_LEVEL_MAX[4:0])
              amp_level <= amp_level + 1'b1;
      end else if (key_amp_down_pulse) begin
          if (amp_level > 5'd0)
              amp_level <= amp_level - 1'b1;
      end
  end
end

assign gain_q15 = GAIN_MIN_Q15
              + ( (amp_level * GAIN_DELTA_Q15 + (AMP_LEVEL_MAX/2)) / AMP_LEVEL_MAX );

//====================================================================
// DDS 核心:相位累加器
//====================================================================
always @(posedge sys_clk or negedge sys_rst_n) begin
  if(!sys_rst_n)
      fre_add <= 32'd0;
  else
      fre_add <= fre_add + freq_word;
end

always @(posedge sys_clk or negedge sys_rst_n) begin
  if(!sys_rst_n)
      rom_addr_128<= 7'd0;
  else
      rom_addr_128 <= fre_add[31:25];
end

always @(posedge sys_clk or negedge sys_rst_n) begin
  if(!sys_rst_n)
      rom_addr_32 <= 5'd0;
  else
      rom_addr_32 <= fre_add[31:27];
end

//====================================================================
// 波形输出规则:1~10Hz 为128点,其它均为32点
// 说明:freq_word域下
//  - 1Hz 约等于 86
//  - 10Hz 约等于 859
// 这里采用:>=1Hz 且 <=10Hz 用128点;其余(含0Hz、>10Hz)用32点
//====================================================================
wire use_128pt;
assign use_128pt = (freq_word >= FW_1HZ) && (freq_word <= FW_10HZ);

assign data_sel = use_128pt ? data_128 : data_32;

//====================================================================
// 幅值缩放
//====================================================================
assign mult_full   = data_sel * gain_q15;
assign data_scaled = mult_full[23:15];
assign data_out    = data_scaled;

//====================================================================
// ROM 实例化
//====================================================================
rom_128_sin rom_128_sin_inst
(
  .clka  (sys_clk),
  .addra (rom_addr_128),
  .douta (data_128)
);

rom_32_sin rom_32_sin_inst
(
  .clka  (sys_clk),
  .addra (rom_addr_32),
  .douta (data_32)
);

endmodule
相关推荐
仲南音19 分钟前
Xilinx FPGA——时钟IP核MMCM、PLL
fpga开发
I'm a winner14 小时前
基于Xilinx FPGA的LVDS高速串行通信系统(三)--数据训练及握手机制【文末源码】
fpga开发
upper202016 小时前
coremark移植坑之-nostartfiles -nostdlib参数
学习·fpga开发·coremark移植·riscv设计
Eloudy1 天前
Vivado 纯命令行构建 FPGA 项目教程
fpga开发
Rambo.xia1 天前
AXI跨时钟域与数据一致性——CDC FIFO深度不够、写后读冒险、多Master仲裁、乱序返回,出了Bug根本查不出来
fpga开发·bug
upper20201 天前
FPGA部署卷积神经网络识别MINST图片
fpga开发·卷积神经网络·minst·minst识别·部署神经网络·神经网络识别图片
你是我的解忧王子2 天前
嵌入式 ADC采样 原始数据分析上层(平均值 波动率 正态分布 线性补偿 系统误差)
fpga开发·数据挖掘·数据分析
9527华安2 天前
FPGA纯verilog代码ISP图像处理培训教程,基于IMX214,提供工程源码+视频教程+FPGA开发板
图像处理·fpga开发·isp·imx214·mipi
千寻xun3 天前
二、实战篇-NVME SSD控制之ZYNQ实现(六)-读写NVME SSD硬盘数据
fpga开发·nvme·nvme ssd
千寻xun3 天前
二、实战篇-NVME SSD控制之ZYNQ实现(三)
fpga开发·nvme ssd·nvme协议