FPGA入门学习Day1——设计一个DDS信号发生器

news2025/4/24 22:11:07

目录

一、DDS简介

(一)基本原理

(二)主要优势

(三)与传统技术的对比

二、FPGA存储器

(一)ROM波形存储器

(二)RAM随机存取存储器

(三)FIFO先进先出存储器

三、自主设计过程

(一)相位累加器的设计

(二)波形存储器ROM的设计

(三)设计实现

四、总结


一、DDS简介

        直接数字频率合成(Direct Digital Frequency Synthesis,简称DDS)是一种通过数字技术生成高精度、高分辨率频率信号的电子方法。它广泛应用于通信、雷达、仪器测量和信号处理等领域。以下是其核心要点:

(一)基本原理

DDS的核心思想是利用数字控制的方式直接合成模拟信号,主要模块包括:

  1. 相位累加器:根据输入的频率控制字(FTW)逐步累加相位值,生成连续的相位序列。
  2. 波形存储器(ROM):存储目标波形(如正弦波、方波)的幅度-相位对应表,将项为值转换为数字幅度值。
  3. 数模转换器(DAC):将数字幅度转换为模拟信号。
  4. 低通滤波器(LPF):滤除DAC输出的高频杂散成分,输出平滑的模拟信号。

(二)主要优势

  1. 高频率分辨率:输出频率的最小步进由相位累加器位数决定,可达毫赫兹(mHz)级精度。

  2. 快速频率切换:通过改变频率控制字,可在纳秒级时间内切换频率,无传统锁相环(PLL)的锁定延迟。

  3. 相位连续性:频率切换时相位连续,避免信号突变。

  4. 灵活波形生成:支持正弦波、三角波、方波等多种波形,通过修改ROM数据可自定义波形。

  5. 全数字控制:易于与微处理器或FPGA集成,实现自动化频率调制(如FSK、PSK)。

(三)与传统技术的对比

  • DDS vs. PLL:DDS频率切换更快、分辨率更高,但输出频率范围较窄;PLL适合高频但分辨率较低。

  • DDS vs. 模拟振荡器:DDS频率和相位可控性更优,但噪声性能可能略逊。

二、FPGA存储器

        常见的FPGA存储器有3种,RAM( 随机访问内存)ROM(只读存储器)FIFO(先入先出)这三种存储器的区别如下:

  • 其中RAM通常都是在掉电之后就丢失数据,ROM在系统停止供电的时候仍然可以保持数据
  • 可以向RAM和ROM中的任意位置写入数据,也可以读取任意的位置的数据
  • 而FIFO的数据先入先出,先进去的数据先出来,只能顺序写入数据,顺序的读出数据,其数据地址由内部读写指针自动加1完成,不能像普通存储器那样可以由地址线决定读取或写入某个指定的地址。

这三种存储器的应用场合:

  • RAM和ROM常用于存储指令或者中间的数据
  • FIFO常用于数据传输通道中用于缓存数据,避免数据丢失,如不同速率时钟模块间的数据传输就需要用到异步FIFO

(一)ROM波形存储器

        ROM(Read-Only Memory,只读存储器)是一种仅支持数据读取操作的半导体存储器,其存储内容在写入后不可修改。由于FPGA芯片本身不具备非易失性存储单元,其ROM功能需通过特殊方式实现:利用FPGA内部的RAM资源构建ROM模块,并通过加载数据文件在运行时对模块进行初始化,从而模拟非易失存储特性。

        Altera(现Intel PSG)提供的ROM IP核主要分为两类:单端口ROM仅配置单个地址读取通道和对应的数据输出端口,支持单向读取操作;双端口ROM则扩展为两组独立的地址/数据端口,可同时进行两路并行读取操作。两种类型均保持ROM的只读特性,核心差异体现在接口数量和并行读取能力上。

 1、调用IP

(1)创建文件

我们单独创建一个ROM文件来熟悉ROM调用IP,我们在Quartus中创建一个新项目命名为ROM。

(2)调用IP

在左侧的IP Catalog搜索栏搜索“ROM”,然后选择“ROM:1-PORT”。

接下来在最开始我们创建的文件夹中添加一个名为IP的文件夹,随后我们将把IP文件保存在其中。 

 

(3)配置IP

  • 第一部分是IP核的输出数据位宽以及IP核的存储容量
  • 第二部分是存储单元类型,默认即可
  • 第三部分是选择时钟模式,单时钟或者双时钟,我们这里选择的是单时钟。

随后点击next进入下一步配置。

这一步包括以下三个部分

  • 第一部分是选择输出端口Q是否寄存。
  • 第二部分是时钟使能信号,通常默认不勾选。
  • 第三部分是选择是否创建已补复位信号“acir”和读使能信号“rden”。这里两项都不勾选。

这一步是将生成的MIF文件添加进去。本次采用matlab生成一个FPGA所需要的正弦波MIF文件,sin_wave_8x256.mif会生成在你的资源管理器中,把他添加到你的FPGA工程文件下

 MIF文件代码:

clc, clear, close all
F1=1; %信号频率
Fs=10^2; %采样频率
P1=0; %信号初始相位
N=10^2; %采样点数
t=[0:1/Fs:(N-1)/Fs]; %采样时刻
ADC=2^7 - 1; %直流分量
A=2^7; %信号幅度
%生成正弦信号
s=A*sin(2*pi*F1*t + pi*P1/180) + ADC;
plot(s); %绘制图形
%创建 coe 文件
fild = fopen('sin_wave_100x8.coe','wt');
%写入 coe 文件头
%固定写法,表示写入的数据是 10 进制表示
fprintf(fild, '%s\n','memory_initialization_radix=10;');
%固定写法,下面开始写入数据
fprintf(fild, '%s\n\n','memory_initialization_vector ='); 
for i = 1:N
    s2(i) = round(s(i)); %对小数四舍五入以取整
    if s2(i) <0 %负 1 强制置零
        s2(i) = 0
    end
    fprintf(fild, '%d',s2(i)); %数据写入
    
    if i==N
        fprintf(fild, '%s\n',';'); %最后一个数据用;
    else
        fprintf(fild,',\n'); % 其他数据用,
    end
end
fclose(fild); % 写完了,关闭文件

复卷机: 04-09 16:48:55
% 方波信号波形采集参考代码(square_wave.m):
 
F1 = 1;    %信号频率
Fs = 10^2; %采样频率
P1 = 0;    %信号初始相位
N = 10^2;  %采样点数
t = [0:1/Fs:(N-1)/Fs]; %采样时刻
ADC = 2^7 - 1; %直流分量
A = 2^7;       %信号幅度
 
%生成方波信号
s = A*square(2*pi*F1*t + pi*P1/180) + ADC;
plot(s); %绘制图形
 
%创建 coe 文件
fild = fopen('squ_wave_100x8.coe','wt');
%写入 coe 文件头
%固定写法,表示写入的数据是 10 进制表示
fprintf(fild, '%s\n','memory_initialization_radix=10;');
%固定写法,下面开始写入数据 
fprintf(fild, '%s\n\n','memory_initialization_vector =');
for i = 1:N
    s2(i) = round(s(i)); %对小数四舍五入以取整
    if s2(i) <0 %负 1 强制置零
        s2(i) = 0
    end
    fprintf(fild, '%d',s2(i)); %数据写入
    if i==N
        fprintf(fild, '%s\n',';'); %最后一个数据用分号
    else
        fprintf(fild,',\n'); % 其他数据用 ,
    end
end
fclose(fild); % 写完了,关闭文件

MATLAB生成的正弦信号波形图:

 最后一步勾选如图所示,生成例化文档。

2、代码

(1)单端口ROM

  • 顶层模块rom
module rom
(
	input wire			sys_clk	 ,
	input wire			sys_rst_n,
	
	output	wire [7:0]	rom_data	
);

wire [7:0]	rom_addr;

rom_ctrl rom_ctrl_inst
(
	.sys_clk		(sys_clk),
	.sys_rst_n		(sys_rst_n),
	.rom_addr	    (rom_addr)
);

rom_sin	rom_sin_inst (
	.address ( rom_addr ),
	.clock ( sys_clk ),
	.q ( rom_data )
	);


endmodule
  •  rom_Ctrl模块代码:
module	rom_ctrl
(
	input	wire			sys_clk		,
	input	wire			sys_rst_n	,
	
	output	reg		[7:0]	rom_addr	
);

always@(posedge sys_clk or negedge sys_rst_n)
	if(sys_rst_n == 1'b0)
		rom_addr <= 8'd0;
	else
		rom_addr <= rom_addr + 1'b1;

endmodule
  • 测试代码 
`timescale 1ns/1ns 
module	tb_rom();

reg		sys_clk;
reg		sys_rst_n;


wire	rom_data;

initial
	begin
		sys_clk = 1'b0;
		sys_rst_n <= 1'b0;
		#30
		sys_rst_n <= 1'b1;
	end
	
always	#10 sys_clk = ~sys_clk;


rom		rom_inst
(
	.sys_clk		(sys_clk),
	.sys_rst_n		(sys_rst_n),

	.rom_data	    (rom_data)
);

endmodule

  • 效果及其总结

        由仿真波形数据得正弦波的周期是5120ns,而ROM存储的一个完整正弦波周期也是5120ns。仿真结果正确。

(2)双端口ROM

在原来的基础,采用双端口ROM输出两路信号,一路正弦波一路方波。步骤与上诉类似 

  • 通过MATLAB生成两路信号如下为MIF文件代码:
clc;                    %清除命令行命令
clear all;              %清除工作区变量,释放内存空间
F1=1;                   %信号频率
Fs=2^8;                %采样频率
P1=0;                   %信号初始相位
N=2^8;                 %采样点数
t=[0:1/Fs:(N-1)/Fs];    %采样时刻
ADC=2^7 - 1;            %直流分量
A=2^7;                  %信号幅度
s1=A*sin(2*pi*F1*t + pi*P1/180) + ADC;          %正弦波信号
s2=A*square(2*pi*F1*t + pi*P1/180) + ADC;       %方波信号
%创建mif文件
fild = fopen('wave_512x8.mif','wt');
%写入mif文件头
fprintf(fild, '%s\n','WIDTH=8;');           %位宽
fprintf(fild, '%s\n\n','DEPTH=512;');     %深度
fprintf(fild, '%s\n','ADDRESS_RADIX=UNS;'); %地址格式
fprintf(fild, '%s\n\n','DATA_RADIX=UNS;');  %数据格式
fprintf(fild, '%s\t','CONTENT');            %地址
fprintf(fild, '%s\n','BEGIN');              %开始
for j = 1:2
    for i = 1:N
        if j == 1       %打印正弦信号数据
            s0(i) = round(s1(i));    %对小数四舍五入以取整
            fprintf(fild, '\t%g\t',i-1);  %地址编码
        end

        if j == 2       %打印方波信号数据
            s0(i) = round(s2(i));    %对小数四舍五入以取整
            fprintf(fild, '\t%g\t',i-1+N);  %地址编码
        end

        if s0(i) <0             %负1强制置零
            s0(i) = 0
        end
        
        fprintf(fild, '%s\t',':');      %冒号
        fprintf(fild, '%d',s0(i));      %数据写入
        fprintf(fild, '%s\n',';');      %分号,换行
    end
end
fprintf(fild, '%s\n','END;');       %结束
fclose(fild);
  • 顶层模块rom
module	rom
(
	input	wire			sys_clk		,
	input	wire			sys_rst_n	,
	
	output	wire	[7:0]	rom_data	,
	output	wire	[7:0]	sin_d		,
	output	wire	[7:0]	squ_d	
);

wire	[7:0]		rom_addr;
wire	[8:0]		rom_sin_d;
wire	[8:0]		rom_squ_d;


rom_ctrl  rom_ctrl_inst
(
	.sys_clk		(sys_clk),
	.sys_rst_n		(sys_rst_n),

	.rom_addr	    (rom_addr),
	.rom_sin_d		(rom_sin_d),
	.rom_squ_d		(rom_squ_d)
);



rom_double	rom_double_inst (	//双端口ROM
	.address_a ( rom_sin_d ),
	.address_b ( rom_squ_d ),
	.clock ( sys_clk ),
	.q_a ( sin_d ),
	.q_b ( squ_d )
	);


rom_sin	rom_sin_inst (
	.address ( rom_addr ),
	.clock ( sys_clk ),
	.q ( rom_data )
	);


endmodule
  • rom_ctrl模块
module	rom_ctrl
(
	input	wire			sys_clk		,
	input	wire			sys_rst_n	,
	
	output	reg		[7:0]	rom_addr	,
	output	reg		[8:0]	rom_sin_d		,
	output	reg		[8:0]	rom_squ_d
);

localparam		SQU_Z = 9'd256;

always@(posedge sys_clk or negedge sys_rst_n)
	if(sys_rst_n == 1'b0)
		rom_addr <= 8'd0;
	else	
		rom_addr <= rom_addr + 1'b1;


always@(posedge sys_clk or negedge sys_rst_n)
	if(sys_rst_n == 1'b0)
		rom_sin_d <= 9'd0;
	else	if(rom_sin_d == 9'd255)
		rom_sin_d <= 9'd0;
	else
		rom_sin_d <= rom_addr + 1'b1;
		
always@(posedge sys_clk or negedge sys_rst_n)
	if(sys_rst_n == 1'b0)
		rom_squ_d <= SQU_Z;
	else
		rom_squ_d <= rom_addr + SQU_Z;


endmodule
  • 测试代码
`timescale 1ns/1ns 
module	tb_rom();

reg		sys_clk;
reg		sys_rst_n;


wire	rom_data;
wire	rom_sin_d;
wire	rom_squ_d;

initial
	begin
		sys_clk = 1'b0;
		sys_rst_n <= 1'b0;
		#30
		sys_rst_n <= 1'b1;
	end
	
always	#10 sys_clk = ~sys_clk;


rom		rom_inst
(
	.sys_clk		(sys_clk),
	.sys_rst_n		(sys_rst_n),

	.rom_data	    (rom_data),
	.sin_d			(rom_sin_d),
	.squ_d			(rom_squ_d)
);

endmodule

  • 效果及其总结

(二)RAM随机存取存储器

        随机存取存储器(Random Access Memory,RAM)是一种能够随时从指定地址读取数据或向指定地址写入数据的存储器件,其读写速度由时钟频率直接决定。RAM通常用于临时存储运行中的程序代码、执行时产生的中间数据及运算结果,是计算机系统中实现高速数据存取的核心组件之一。在Altera(现为Intel FPGA)的硬件架构中,RAM的实现主要分为两种类型: 

(1)单端口RAM:仅通过单一组地址线控制数据的写入和读取操作,读写操作无法同时进行;

端口名端口描述
dataRAM读写数据
addressRAM读写地址,读地址写地址共用同一个地址
wren写使能信号,高电平有效
rden读使能信号,高电平有效
clken时钟使能信号
aclr复位信号,高电平有效
inclock/outclock单端口RAM支持双时钟模式和单时钟模式

(2)双端口RAM:配备两组独立的地址线,可分别控制写入端口和读取端口,允许同时进行读写操作,从而提升数据吞吐效率,适用于需要并行数据处理的场景(如缓存、实时信号处理等)。

        两种结构的核心差异在于端口资源的分配,双端口RAM通过硬件级并行设计突破了单端口RAM的时序限制,但会占用更多的逻辑资源。

1、调用IP

(1)创建文件

我们单独创建一个RAM文件来熟悉RAM调用IP,我们在Quartus中创建一个新项目命名为RAM。

(2)调用IP

在左侧的IP Catalog搜索栏搜索“RAM”,然后选择“RAM:1-PORT”。

接下来在最开始我们创建的文件夹中添加一个名为IP的文件夹,随后我们将把IP文件保存在其中。

(3)配置IP

  • 第一部分是IP核的输出数据位宽
  • 第二部分是IP核的存储容量
  • 第三部分是存储单元类型,默认即可
  • 第四部分是选择时钟模式,单时钟或者双时钟,我们这里选择的是单时钟。

随后点击next进入下一步配置。

这一步包括以下三个部分

  • 第一部分是选择输出端口Q是否寄存。
  • 第二部分是时钟使能信号,通常默认不勾选。
  • 第三部分是选择是否创建已补复位信号“acir”和读使能信号“rden”。这里两项都勾选。

随后点击next进入下一步配置。

这一步是配置某个地址写入数据的同时读取数据,通常默认“New data”即可,随后点击next进入下一步配置。

 这一步是配置RAM存储器初始化参数包括以下两个部分:

  • 第一部分是确定是否配置初始化文件,根据需求可以自主选择,此处默认不改。
  • 第二部分是确定是否选择允许系统存储器内容编辑器采集和更新内容在与系统时钟无关的情况下,默认不勾选。

最后一步是将inst文件勾选上点击finish完成配置,随后点击Yes。

2、代码

(1).v代码

module ram_ip( 
    input          clk      ,
    input          rst_n    ,
    input  [5:0]   address  ,
    input  [7:0]   data     ,
    input          redn     ,
    input          wren     ,
    output [7:0]   q
);

ram	ram_inst (
	.aclr    ( ~rst_n  ),
	.address ( address ),
	.clock   ( clk     ),
	.data    ( data    ),
	.rden    ( rden    ),
	.wren    ( wren    ),
	.q       ( q       )
	);

endmodule

(2)测试代码 

这个测试代码的意思就是将地址依次增加,并且每加一次写进去一个数据。之后再从头的地址读数据。

`timescale  1ns/1ns

module tb_ram();
reg         tb_clk   ;
reg         tb_rst_n ;
reg  [5:0]  address  ;
reg  [7:0]  data     ;
reg         rden     ;
reg         wren     ;
wire [7:0]  q        ;


parameter CYCLE = 20;

ram_ip ram_ip_inst( 
    /*input          */.clk     ( tb_clk   ) ,
    /*input          */.rst_n   ( tb_rst_n ) ,
    /*input  [5:0]   */.address ( address  ) ,
    /*input  [7:0]   */.data    ( data     ) ,
    /*input          */.redn    ( rden     ) ,
    /*input          */.wren    ( wren     ) ,
    /*output [7:0]   */.q       ( q        )   
);

always #(CYCLE/2) tb_clk = ~tb_clk;

integer i;
initial begin
    tb_clk = 1'b1;
    tb_rst_n = 1'b1;
    #(CYCLE*2);
    tb_rst_n = 1'b0;
    address = 0;//复位赋初值
    data = 0;
    rden = 0;
    wren = 0;
    #(CYCLE*10);
    tb_rst_n = 1'b1;
    #(100*CYCLE);
    for (i=0;i<64 ;i=i+1 ) begin //写入数据
        address = i;
        data = i+1;
        wren = 1'b1;
        #CYCLE;
    end
    wren = 0;
    #(CYCLE*10);
    for (i=10;i<32 ;i=i+1 ) begin //读取数据
        address = i;
        rden = 1'b1;
        #CYCLE;
    end
    rden = 0;
    $stop;
end

endmodule

不会仿真的朋友可以点击如下链接跳转去学习:FPGA入门学习Day0——状态机相关内容解析HDLbits练习_boost峰值电流模控制的二进制码状态机用什么表征-CSDN博客

3、效果及其总结

        在仿真过程中,当写使能信号“wren”置为高电平时,系统会向指定地址的存储单元中写入对应的数据;而读使能信号“rden”(或“redn”)置为高电平时,则会从该地址的存储单元中读取已存储的数据。这两个信号通过高低电平状态分别独立控制数据的写入和读取操作,确保存储访问的时序逻辑清晰且功能明确。

(三)FIFO先进先出存储器

        FIFO(先进先出存储器)是一种基于顺序存取原则的存储器件,无外部地址线,数据按写入顺序依次读出,主要用于数据缓冲、速率匹配及跨时钟域异步数据交互。与RAM(支持随机读写,用于程序及运行时数据存储)和ROM(仅读不可改)不同,FIFO专注于流式数据处理,无法通过地址线指定读写位置。

        FIFO分为单时钟(SCFIFO)双时钟(DCFIFO)两类。单时钟FIFO所有操作由单一时钟同步控制,适用于同时钟域的数据缓存;双时钟FIFO的读写端口分别由独立时钟(wrclk和rdclk)驱动,进一步分为普通双时钟FIFO(读写位宽一致)和混合位宽双时钟FIFO(支持读写位宽转换,如32位转16位)。

1、调用IP

(1)创建文件

创建一个文件夹其内容分布如下:

  • doc:存放结果、图片等等
  • ip :存放IP核
  • prj :存放工程文件
  • rtl :存放主要代码
  • tb :存放testbench代码

(2)调用IP

在左侧的IP Catalog搜索栏搜索“FIFO”,然后选择“FIFO”。

将IP存入文件夹中IP内,点击OK。

(3)配置IP

  • 第一部分是FIFO的位宽
  • 第二部分是FIFO的深度
  • 第三部分是确定是否设置成双时钟FIFO。

随后点击next进入下一步配置。

 第二页不用修改,保持默认即可。

 这一步将方框中框住的全打勾即可

这一步是配置FIFO的工作模式,具体配置分为如下两个部分:

  • 第一部分:FIFO工作模式分为正常模式和前显模式。正常模式下读使能后下一时钟输出数据;前显模式下数据在读请求前已有效,但空信号(empty)滞后写操作两周期,适用于需低延迟的实时处理场景,这里选此模式优化效率。
  • 第二部分的配置选择默认Auto即可

这一步把例化文件勾选上,可以看左边的简化图,点击finish完成配置。

2、代码

(1).v代码

module fifo_test( 
    input   wire        wr_clk      ,//写时钟
    input   wire        rd_clk      ,//读时钟
    input   wire        rst_n       ,
    input   wire [7:0]  wr_din      ,//写入fifo数据
    input   wire        wr_en       ,//写使能
    input   wire        rd_en       ,//读使能
    output  reg  [7:0]  rd_dout     ,//读出的数据
    output  reg         rd_out_vld   //读有效信号       
);

//------------<参数说明>--------------------------------------------

wire   [7:0]    wr_data  ;
wire   [7:0]    q        ;
wire            wr_req   ;//写请求
wire            rd_req   ;//读请求
wire            wr_empty ;//写空
wire            wr_full  ;//写满
wire            rd_empty ;//读空
wire            rd_full  ;//读满
wire   [7:0]    wr_usedw ;//在写时钟域下,FIFO 中剩余的数据量;
wire   [7:0]    rd_usedw ;//在读时钟域下,FIFO 中剩余的数据量。

fifo	fifo_inst (
	.data     ( wr_data      ),
	.rdclk    ( rd_clk       ),
	.rdreq    ( rd_req       ),
	.wrclk    ( wr_clk       ),
	.wrreq    ( wr_req       ),
	.q        ( q            ),
	.rdempty  ( rd_empty     ),
	.rdfull   ( rd_full      ),
	.rdusedw  ( rd_usedw     ),
	.wrempty  ( wr_empty     ),
	.wrfull   ( wr_full      ),
	.wrusedw  ( wr_usedw     )
	);
assign wr_data = wr_din;
assign wr_req = (wr_full == 1'b0)?wr_en:1'b0;
assign rd_req = (rd_empty == 1'b0)?rd_en:1'b0;

always @(posedge rd_clk or negedge rst_n)begin
    if(!rst_n)begin
        rd_dout <= 0;
    end
    else begin
        rd_dout <= q;
    end
end
always @(posedge rd_clk or negedge rst_n)begin
    if(!rst_n)begin
        rd_out_vld <= 1'b0;
    end
    else begin
        rd_out_vld <= rd_req;
    end
end

endmodule

(2)测试代码:

`timescale 1 ns/1 ns
module tb_fifo();
//时钟和复位
   reg       wr_clk;
   reg       rd_clk;
   reg       rst_n ;
//输入信号
   reg [7:0] wr_din;
   reg       wr_en ;
   reg       rd_en ;
//输出信号
   wire       rd_out_vld;
   wire [7:0] rd_dout;

parameter WR_CYCLE = 20;//写时钟周期,单位为 ns,
parameter RD_CYCLE = 30;//读时钟周期,单位为 ns,
parameter RST_TIME = 3 ;//复位时间,此时表示复位 3 个时钟周期的时间。
//待测试的模块例化


fifo_test  fifo_test_inst( 
    /*input   wire        */.wr_clk     ( wr_clk     ) ,//写时钟
    /*input   wire        */.rd_clk     ( rd_clk     ) ,//读时钟
    /*input   wire        */.rst_n      ( rst_n      ) ,
    /*input   wire [7:0]  */.wr_din     ( wr_din     ) ,//写入fifo数据
    /*input   wire        */.wr_en      ( wr_en      ) ,//写使能
    /*input   wire        */.rd_en      ( rd_en      ) ,//读使能
    /*output  reg  [7:0]  */.rd_dout    ( rd_dout    ) ,//读出的数据
    /*output  reg         */.rd_out_vld ( rd_out_vld )  //读有效信号       
);

    integer i = 0;
//生成本地时钟 50M
    initial wr_clk = 0;
    always #(WR_CYCLE/2) wr_clk=~wr_clk;
    initial rd_clk = 0;
    always #(RD_CYCLE/2) rd_clk=~rd_clk;
//产生复位信号
    initial begin
        rst_n = 1;
        #2;
        rst_n = 0;
        #(WR_CYCLE*RST_TIME);
        rst_n = 1;
    end
//输入信号赋值
    initial begin
        #1;
        wr_din = 0;//赋初值
        wr_en = 0;
        #(10*WR_CYCLE);
        for(i=0;i<500;i=i+1)begin//开始赋值
            wr_din = {$random};
            wr_en = {$random};
            #(1*WR_CYCLE);
        end
        #(100*WR_CYCLE);
    end
    initial begin
        #1;
        rd_en = 0;//赋初值
        #(12*RD_CYCLE); 
        for(i=0;i<500;i=i+1)begin//开始赋值
            rd_en = {$random};
            #(1*RD_CYCLE);
        end
        #(100*RD_CYCLE);
        $stop;
    end
endmodule

3、结果和总结

        在数据流模式下,FIFO的操作表现为:当写使能信号(wren)有效时,数据立即写入队列;读使能信号(rden)有效时,数据同步从队列读出。由于数据在填满前即被持续读出,空和满状态信号未被触发,表明队列始终未达到存储容量上限或下限。这种模式适用于持续流式数据传输场景。

三、自主设计过程

(一)相位累加器的设计

        相位累加器在时钟的驱动下,将输入的频率控制字转换为地址并输出,它决定着频率的范围和分辨率。本设计不使用相位调制器,相位累加器采用m=17位的二进制累加器和寄存器构成,其结果直接送到后面的存储器。在Quartus中新建工程,新建.v文件取名为“addr_cnt”,代码如下:

module addr_cnt(CPi,K,ROMaddr,Address);
    input CPi;
    input [12:0] K;
    output reg [9:0] ROMaddr;
    output reg [16:0] Address;
    always @(posedge CPi) begin
        Address=Address+K;
        ROMaddr=Address[16:7];
    end
endmodule

保存后,右击文件 →  Set as Top Level Entity,编译运行后再次右键点击 → Create Symbol Files for Current File

模块符号如下所示: 

(二)波形存储器ROM的设计

1、方波模块

        由于方波的实现算法相对简单,可以不用ROM表,直接用寄存器来保存方波的输出值。方波只有高、低电平两种状态,因此只需要在一个周期的中间位置翻转电平即可。其实现原理如下:由于相位累加器的值是线性累加的,因此地址值(Address)也是线性累加的,对地址值Address进行判断,当地址值的最高位为0时,便将存储波形幅值的存储器的每一位赋值为1,否则赋值为0。具体源程序如下:

module squwave(CPi,RSTn,Address,Qsquare);
    input CPi;
    input RSTn;
    input [16:0] Address;
    output reg [11:0] Qsquare;
    always @(posedge CPi)
    if (!RSTn)Qsquare=12'h000; 
    else begin
        if(Address<=17'h0FFFF)
            Qsquare=12'hFFF;
        else Qsquare=12'h000;
    end
endmodule

模块符号如下所示:

2、正弦波

        这个比较复杂,需要用IP核调用ROM存储。但是直接IP核生成ROM并更改存储数据太麻烦了,我们可以编写一个C语言程序,生成存储器的初始化文件Sine1024.mif。

C语言程序如下所示:

/*myMIF.c*/
#include <stdio.h>
#include <math.h>
#define PI 3.141592
#define DEPTH 1024
#define WIDTH 12
int main(void)
{
	int n,temp;
	float v;
	FILE *fp;
	fp=fopen("Sine1024.mif","w+");
	if(NULL==fp)printf("Can not creat file!\r\n");
	else
	{
		printf("File created successfully!\n");
		fprintf(fp,"DEPTH=%d;\n",DEPTH);
		fprintf(fp,"WIDTH=%d;\n",WIDTH);
		fprintf(fp,"ADDRESS_RADIX=HEX;\n");
		fprintf(fp,"DATA_RADIX=HEX;\n");
		fprintf(fp,"CONTENT\n");
		fprintf(fp,"BEGIN\n");
		for(n=0;n<DEPTH;n++)
		{
			v=sin(2*PI*n/DEPTH);
			temp=(int)((v+1)*4095/2);
			fprintf(fp,"%04x : %03x;\n",n,temp);
		}
		fprintf(fp,"END;\n");
		fclose(fp);
	}
 } 

配置ROM:

        按照前面第二章的步骤进行配置单端口ROM,将IP核的输出数据位宽以及IP核的存储容量设置为12、1024。再将上述c语言生成的mif文件添加进去。

3、锁相环倍频电路

现在需要调用宏模块定制一个100MHz的锁相环模块。其过程如下所示

  • 在IP Catalog搜索栏输入APTPLL并双击。

 按照下图进行设置即可

4、 顶层电路设计

将其设为顶层文件再编译

module DDS_top (CLOCK_50,RSTn,WaveSel,K,
WaveValue,LEDG,CLOCK_100);
    input CLOCK_50;
    input RSTn;
    input [1:0] WaveSel;

    input [12:0] K;
    output reg [11:0] WaveValue;
    wire [9:0] ROMaddr/* synthesis keep */;
    wire [16:0] Address;
    wire [11:0] Qsine,Qsquare;
    output [0:0] LEDG;
    output CLOCK_100;
    wire CPi=CLOCK_100;
    PLL100M_CP PLL100M_CP_inst(
        .inclk0(CLOCK_50),
        .c0(CLOCK_100),
        .locked(LEDG[0])
    );

    addr_cnt U0_instance(CPi,K,ROMaddr,Address);

    SineROM ROM_inst(
        .address(ROMaddr),
        .clock(CPi),
        .q(Qsine)
    );
    squwave U1(CPi,RSTn,Address,Qsquare);
    always @(posedge CPi)
    begin
        case(WaveSel)
            2'b01:WaveValue=Qsine;
            2'b10:WaveValue=Qsquare;
            default:WaveValue=Qsine;
        endcase
    end
endmodule

 (三)设计实现

设计好DDS之后我们使用DE2-115开发板来实现,如下我们将着手在DE2-115开发板上进行操作

1、代码介绍:

        首先我们创建dds_top的顶层代码,将下述的代码写入其中,此代码:

module dds_top(
    input wire clk_50m,       // 50MHz系统时钟
    input wire rst_n,         // 复位信号
    input wire [31:0] fcw,    // 频率控制字
    input wire wave_sel,      // 波形选择(0:正弦波, 1:方波)
    output wire [7:0] dac_out // 8位DAC输出
);
 
wire [7:0] phase;            // 相位地址
wire [7:0] sine_data;        // 正弦波数据
wire [7:0] square_data;      // 方波数据
 
// 相位累加器模块
phase_accumulator u_phase_accum(
    .clk(clk_50m),
    .rst_n(rst_n),
    .fcw(fcw),
    .phase(phase)
);
 
// 正弦波ROM表
sine_rom u_sine_rom(
    .address(phase),
    .clock(clk_50m),
    .q(sine_data)
);
 
// 方波生成模块
square_wave_gen u_square_gen(
    .phase(phase),
    .square_data(square_data)
);
 
// 波形选择输出
assign dac_out = wave_sel ? square_data : sine_data;
 
endmodule

2、模块解读:

模块结构

  • 输入:50MHz时钟(clk_50m)、低有效复位(rst_n)、32位频率控制字(fcw)、波形选择(wave_sel)

  • 输出:8位DAC数据(dac_out)

  • 内部信号:8位相位地址(phase)、正弦波数据(sine_data)、方波数据(square_data

相位累加器

  • 采用32位累加器,每个时钟周期累加fcw值

  • 取累加器高8位[31:24]作为相位地址(phase)

  • 频率分辨率:f_out = (fcw × 50MHz)/2³² ≈ 0.0116Hz/FCW步进

正弦波生成

  • 使用256深度ROM(sine_rom)

  • 相位地址直接作为ROM查表地址

  • 同步读取设计,输出延迟1个时钟周期

方波生成

  • 组合逻辑实现,无时钟延迟

  • 相位值≥128时输出0xFF(高电平),否则0x00(低电平)

  • 生成50%占空比方波,频率与正弦波一致

输出选择

  • 多路选择器根据wave_sel选择波形

  • 正弦波存在1周期延迟,方波实时输出

引脚配置如下所示:

 随后打开波形仿真得到以下结果(不会波形仿真的同志可以阅读如下博客进行学习:FPGA学习(一)数字逻辑与FPGA实现基基础-CSDN博客):

四、总结

        通过本次DDS信号发生器的设计与实现,我深入理解了直接数字频率合成的核心原理,掌握了FPGA中ROM、RAM等存储器的配置与调用技巧。在构建相位累加器、波形ROM表及方波生成模块时,深刻体会到时序同步与模块协同的重要性。实验过程中,通过Matlab生成波形数据、Quartus调用IP核以及仿真验证,进一步巩固了理论与实践的结合能力。尽管在波形切换的相位连续性优化上仍有提升空间,但成功实现了可调频率的正弦波与方波输出,为后续复杂信号合成奠定了扎实基础。

参考博客:

FPGA基础--【Altera】IP核(2)---RAM随机存取存储器_fpga板上ram-CSDN博客MATLAB生成FPGA ROM与双端口ROM实现正弦波与方波存储与读取-CSDN博客

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/2341977.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

微信小程序拖拽排序有效果图

效果图 .wxml <view class"container" style"--w:{{w}}px;" wx:if"{{location.length}}"><view class"container-item" wx:for"{{list}}" wx:key"index" data-index"{{index}}"style"--…

WT2000T专业录音芯片:破解普通录音设备信息留存、合规安全与远程协作三大难题

在快节奏的现代商业环境中&#xff0c;会议是企业决策、创意碰撞和战略部署的核心场景。然而&#xff0c;传统会议记录方式常面临效率低、信息遗漏、回溯困难等痛点。如何确保会议内容被精准记录并高效利用&#xff1f;会议室专用录音芯片应运而生&#xff0c;以智能化、高保真…

【Python 学习笔记】 pip指令使用

系列文章目录 pip指令使用 文章目录 系列文章目录前言安装配置使用pip 管理Python包修改pip下载源 前言 提示&#xff1a;这里可以添加本文要记录的大概内容&#xff1a; 当前文章记录的是我在学习过程的一些笔记和思考&#xff0c;可能存在有误解的地方&#xff0c;仅供大家…

C# 文件读取

文件读取是指使用 C# 程序从计算机文件系统中获取文件内容的过程。将存储在磁盘上的文件内容加载到内存中&#xff0c;供程序处理。主要类型有&#xff1a;文本文件读取&#xff08;如 .txt, .csv, .json, .xml&#xff09;&#xff1b;二进制文件读取&#xff08;如 .jpg, .pn…

leetcode125.验证回文串

class Solution {public boolean isPalindrome(String s) {s s.replaceAll("[^a-zA-Z0-9]", "").toLowerCase();for(int i0,js.length()-1;i<j;i,j--){if(s.charAt(i)!s.charAt(j))return false;}return true;} }

【Android面试八股文】Android系统架构【一】

Android系统架构图 1.1 安卓系统启动 1.设备加电后执行第一段代码&#xff1a;Bootloader 系统引导分三种模式&#xff1a;fastboot&#xff0c;recovery&#xff0c;normal&#xff1a; fastboot模式&#xff1a;用于工厂模式的刷机。在关机状态下&#xff0c;按返回开机 键进…

【数据可视化-21】水质安全数据可视化:探索化学物质与水质安全的关联

&#x1f9d1; 博主简介&#xff1a;曾任某智慧城市类企业算法总监&#xff0c;目前在美国市场的物流公司从事高级算法工程师一职&#xff0c;深耕人工智能领域&#xff0c;精通python数据挖掘、可视化、机器学习等&#xff0c;发表过AI相关的专利并多次在AI类比赛中获奖。CSDN…

【prometheus+Grafana篇】从零开始:Linux 7.6 上二进制安装 Prometheus、Grafana 和 Node Exporter

&#x1f4ab;《博主主页》&#xff1a;奈斯DB-CSDN博客 &#x1f525;《擅长领域》&#xff1a;擅长阿里云AnalyticDB for MySQL(分布式数据仓库)、Oracle、MySQL、Linux、prometheus监控&#xff1b;并对SQLserver、NoSQL(MongoDB)有了解 &#x1f496;如果觉得文章对你有所帮…

STM32(M4)入门:GPIO与位带操作(价值 3w + 的嵌入式开发指南)

一&#xff1a;GPIO 1.1 了解时钟树&#xff08;必懂的硬件基础&#xff09; 在 STM32 开发中&#xff0c;时钟系统是一切外设工作的 “心脏”。理解时钟树的工作原理&#xff0c;是正确配置 GPIO、UART 等外设的核心前提。 1.1.1 为什么必须开启外设时钟&#xff1f; 1. 计…

Linux419 三次握手四次挥手抓包 wireshark

还是Notfound 没连接 可能我在/home 准备配置静态IP vim ctrlr 撤销 u撤销 配置成功 准备关闭防火墙 准备配置 YUM源 df -h 未看到sr0文件 准备排查 准备挂载 还是没连接 计划重启 有了 不重启了 挂载准备 修改配置文件准备 准备清理缓存 ok 重新修改配…

CSS-跟随图片变化的背景色

CSS-跟随图片变化的背景色 获取图片的主要颜色并用于背景渐变需要安装依赖 colorthief获取图片的主要颜色. 并丢给背景注意 getPalette并不是个异步方法 import styles from ./styles.less; import React, { useState } from react; import Colortheif from colorthief;cons…

解决Docker 配置 daemon.json文件后无法生效

vim /etc/docker/daemon.json 在daemon中配置一下dns {"registry-mirrors": ["https://docker.m.daocloud.io","https://hub-mirror.c.163.com","https://dockerproxy.com","https://docker.mirrors.ustc.edu.cn","ht…

虚幻基础:ue碰撞

文章目录 碰撞&#xff1a;碰撞体 运动后 产生碰撞的行为——碰撞响应由引擎负责&#xff0c;并向各自发送事件忽略重叠阻挡 碰撞响应关系有忽略必是忽略有重叠必是重叠有阻挡不一定阻挡&#xff08;双方都为阻挡&#xff09; 碰撞启用&#xff1a;纯查询&#xff1a;开启移动检…

数据治理体系的“三驾马车”:质量、安全与价值挖掘

1. 执行摘要 数据治理已从合规驱动的后台职能&#xff0c;演变为驱动业务成果的战略核心。本文将深入探讨现代数据治理体系的三大核心驱动力——数据质量、数据安全与价值挖掘——它们共同构成了企业在数字时代取得成功的基石。数据质量是信任的基石&#xff0c;确保决策所依据…

leetcode 二分查找应用

34. Find First and Last Position of Element in Sorted Array 代码&#xff1a; class Solution { public:vector<int> searchRange(vector<int>& nums, int target) {int low lowwer_bound(nums,target);int high upper_bound(nums,target);if(low high…

Ngrok 内网穿透实现Django+Vue部署

目录 Ngrok 配置 注册/登录 Ngrok账号 官网ngrok | API Gateway, Kubernetes Networking Secure Tunnels 直接cmd运行 使用随机生成网址&#xff1a;ngrok http 端口号 使用固定域名生成网址&#xff1a;ngrok http --domain你的固定域名 端口号 Django 配置 1.Youre a…

利用OLED打印调试信息: 控制PC13指示灯点灯的实验

Do口暗的时候才是高电平,因为光敏电阻传感器的高电平是依靠LM393电压比较器上引脚进入高阻态再加上上拉电阻上拉产生的高电平DO口什么时候会输出高阻态?电压比较器的正极输入电压大于负极输入电压,而正极输入电压是光敏电阻分得的电压,光敏电阻的阻值越大,已分得的电压就越大,…

Appium安装 -- app笔记

调试环境&#xff1a;JDK&#xff08;java&#xff09; SDK&#xff08;android&#xff09; Node.js 雷神模拟器&#xff08;或 真机&#xff09; Appium&#xff08;Appium Server【内外件&#xff08;dos内件、界面化工具&#xff09;】、Appium Inspector&#xff09; p…

【OpenGL】OpenGL学习笔记-1:VS2019配置OpenGL开发环境

在Visual Studio 2019中可以通过手动配置库文件或NuGet包管理器快速安装的方法配置OpenGL环境&#xff0c;详细步骤如下&#xff1a; 一、打开VS2019&#xff0c;创建新的控制台项目 二、方法一&#xff1a;手动配置GLEW/GLFW/GLAD库 GLFW是窗口管理和输入事件的基础设施&…

集结号海螺捕鱼游戏源码解析(第二篇):水浒传捕鱼模块逻辑与服务器帧同步详解

本篇将全面解构“水浒传”子游戏的服务端核心逻辑、帧同步机制、鱼群刷新规则、客户端命中表现与服务器计算之间的协同方式&#xff0c;聚焦于 C 与 Unity3D 跨端同步的真实实现过程。 一、水浒传捕鱼模块资源结构 该模块包含三部分核心目录&#xff1a; 子游戏/game_shuihuz…