FPGA入门学习Day1——设计一个DDS信号发生器
目录
一、DDS简介
(一)基本原理
(二)主要优势
(三)与传统技术的对比
二、FPGA存储器
(一)ROM波形存储器
(二)RAM随机存取存储器
(三)FIFO先进先出存储器
三、自主设计过程
(一)相位累加器的设计
(二)波形存储器ROM的设计
(三)设计实现
四、总结
一、DDS简介
直接数字频率合成(Direct Digital Frequency Synthesis,简称DDS)是一种通过数字技术生成高精度、高分辨率频率信号的电子方法。它广泛应用于通信、雷达、仪器测量和信号处理等领域。以下是其核心要点:
(一)基本原理
DDS的核心思想是利用数字控制的方式直接合成模拟信号,主要模块包括:
- 相位累加器:根据输入的频率控制字(FTW)逐步累加相位值,生成连续的相位序列。
- 波形存储器(ROM):存储目标波形(如正弦波、方波)的幅度-相位对应表,将项为值转换为数字幅度值。
- 数模转换器(DAC):将数字幅度转换为模拟信号。
- 低通滤波器(LPF):滤除DAC输出的高频杂散成分,输出平滑的模拟信号。
(二)主要优势
-
高频率分辨率:输出频率的最小步进由相位累加器位数决定,可达毫赫兹(mHz)级精度。
-
快速频率切换:通过改变频率控制字,可在纳秒级时间内切换频率,无传统锁相环(PLL)的锁定延迟。
-
相位连续性:频率切换时相位连续,避免信号突变。
-
灵活波形生成:支持正弦波、三角波、方波等多种波形,通过修改ROM数据可自定义波形。
-
全数字控制:易于与微处理器或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:Ns2(i) = round(s(i)); %对小数四舍五入以取整if s2(i) <0 %负 1 强制置零s2(i) = 0endfprintf(fild, '%d',s2(i)); %数据写入if i==Nfprintf(fild, '%s\n',';'); %最后一个数据用;elsefprintf(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:Ns2(i) = round(s(i)); %对小数四舍五入以取整if s2(i) <0 %负 1 强制置零s2(i) = 0endfprintf(fild, '%d',s2(i)); %数据写入if i==Nfprintf(fild, '%s\n',';'); %最后一个数据用分号elsefprintf(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;elserom_addr <= rom_addr + 1'b1;endmodule
- 测试代码
`timescale 1ns/1ns
module tb_rom();reg sys_clk;
reg sys_rst_n;wire rom_data;initialbeginsys_clk = 1'b0;sys_rst_n <= 1'b0;#30sys_rst_n <= 1'b1;endalways #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:2for i = 1:Nif j == 1 %打印正弦信号数据s0(i) = round(s1(i)); %对小数四舍五入以取整fprintf(fild, '\t%g\t',i-1); %地址编码endif j == 2 %打印方波信号数据s0(i) = round(s2(i)); %对小数四舍五入以取整fprintf(fild, '\t%g\t',i-1+N); %地址编码endif s0(i) <0 %负1强制置零s0(i) = 0endfprintf(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;elserom_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;elserom_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;initialbeginsys_clk = 1'b0;sys_rst_n <= 1'b0;#30sys_rst_n <= 1'b1;endalways #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:仅通过单一组地址线控制数据的写入和读取操作,读写操作无法同时进行;
端口名 | 端口描述 |
---|---|
data | RAM读写数据 |
address | RAM读写地址,读地址写地址共用同一个地址 |
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/1nsmodule 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 begintb_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;endwren = 0;#(CYCLE*10);for (i=10;i<32 ;i=i+1 ) begin //读取数据address = i;rden = 1'b1;#CYCLE;endrden = 0;$stop;
endendmodule
不会仿真的朋友可以点击如下链接跳转去学习: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)beginif(!rst_n)beginrd_dout <= 0;endelse beginrd_dout <= q;end
end
always @(posedge rd_clk or negedge rst_n)beginif(!rst_n)beginrd_out_vld <= 1'b0;endelse beginrd_out_vld <= rd_req;end
endendmodule
(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;
//生成本地时钟 50Minitial wr_clk = 0;always #(WR_CYCLE/2) wr_clk=~wr_clk;initial rd_clk = 0;always #(RD_CYCLE/2) rd_clk=~rd_clk;
//产生复位信号initial beginrst_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);endinitial 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) beginAddress=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 beginif(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)begincase(WaveSel)2'b01:WaveValue=Qsine;2'b10:WaveValue=Qsquare;default:WaveValue=Qsine;endcaseend
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博客
相关文章:
FPGA入门学习Day1——设计一个DDS信号发生器
目录 一、DDS简介 (一)基本原理 (二)主要优势 (三)与传统技术的对比 二、FPGA存储器 (一)ROM波形存储器 (二)RAM随机存取存储器 (三&…...
JavaScript-立即执行函数(Immediately Invoked Function Expression,IIFE)
立即执行函数(Immediately Invoked Function Expression,IIFE)是 JavaScript 里一种很独特的函数,它在定义后会马上执行。下面会详细介绍它的语法、用途、优点以及注意事项。 一、语法 立即执行函数一般有两种常见的语法形式&am…...
【Leetcode 每日一题 - 补卡】2537. 统计好子数组的数目
问题背景 给你一个整数数组 n u m s nums nums 和一个整数 k k k,请你返回 n u m s nums nums 中 好 子数组的数目。 一个子数组 a r r arr arr 如果有 至少 k k k 对下标 ( i , j ) (i, j) (i,j) 满足 i < j i < j i<j 且 a r r [ i ] a r r [ …...
【工具-Krillin AI】视频翻译、配音、语音克隆于一体的一站式视频多语言转换工具~
Krillin AI 是全能型音视频本地化与增强解决工具。这款简约而强大的工具,集音视频翻译、配音、语音克隆于一身,支持横竖屏格式输出,确保在所有主流平台(哔哩哔哩,小红书,抖音,视频号,…...
常用绑定事件方式有哪几种
绑定事件分为3种: 1、内联模式:将函数名直接作为标签属性的属性值(注意:这里是带括号的,不带括号不生效,但是在vue中可以加括号也可以不加括号,如果需要穿参数就加括号,不需要传参数可以不加&am…...
数据结构之BFS广度优先算法(腐烂的苹果)
队列这个数据结构在很多场景下都有使用,比如在实现二叉树的层序遍历,floodfill问题(等等未完成)中,都需要借助队列的先进先出特性,下面给出这几个问题的解法 经典的二叉树的层序遍历 算法图示,以下图所示的二叉树为例…...
linux 学习 1.开始学习
准备学习linux记录一下学习内容,只会包含必要的知识,和部分演示 我采用的系统是Ubuntu24.04 初始掌握 学习首先需要掌握如何查看帮助手册 man man # man 加任何命令可以看具体命令的帮助手册 man mkdir进入手册按 d(down):往下翻半页u(u…...
Flink-01学习 介绍Flink及上手小项目之词频统计
flink简介 官网 概述: 学习Flink具体包括四个关键概念:流数据的持续处理,事件时间,有状态流处理和状态快照。 Apache Flink 是一个开源的流处理框架,旨在处理批处理和实时数据处理,具有高吞吐量和低延迟的…...
【Linux我做主】探秘gcc/g++和动静态库
TOC Linux编译器gcc/g的使用 github地址 有梦想的电信狗 前言 在软件开发的世界中,编译器如同匠人的工具,将人类可读的代码转化为机器执行的指令。 对于Linux开发者而言,gcc和g是构建C/C程序的核心工具链,掌握它们的原理和使…...
工控系统前端设计(pyqt)
题目源自:白月黑羽的项目实战四-[工控系统前端] 代码已上传至gitcode https://gitcode.com/m0_37662818/Industrial_Control_System_Front_End 心得体会:直接用组态软件或者js吧 项目亮点 tablemodel的使用,绑定了表格和数据风机自定义ite…...
一台 Master 多节点玩转 Kubernetes:sealos 一键部署实践
文章目录 一台 Master 多节点玩转 Kubernetes:sealos 一键部署实践🔗 参考链接🌐 部署环境📦 安装包说明🔧 前期准备🚀 使用 sealos 安装 Kubernetes✅ 验证集群状态📌 后续可做的优化和拓展&am…...
写书的三驾马车
2019年8月19日23:52:28 先亮出我们的兵器组合: GitBook Git Markdown,享受行云流水一般的写作 个人秀 GitBook : 一个基于 Node.js 的文档格式转换工具,支持 Markdown 和 AsciiDoc 两种语法格式,可以输出 HTML、PDF等格式的…...
科学护理进行性核上性麻痹,缓解病痛提升生活质量
进行性核上性麻痹是一种罕见的神经系统变性疾病,患者常出现姿势平衡障碍、吞咽困难、眼球运动异常等症状。通过科学的健康护理,能在一定程度上减轻患者痛苦,提升生活质量。 日常护理,保障安全舒适 患者日常活动时,需确…...
第七章:7.2求方程a*x*x+b*x+c=0的根,用3个函数,分别求当:b*b-4*a*c大于0、等于0和小于0时的根并输出结果。从主函数输入a、b、c的值
//求方程a*x*xb*xc0的根,用3个函数,分别求当:b*b-4*a*c大于0、等于0和小于0时的根并输出结果。 //从主函数输入a、b、c的值 #define _CRT_SECURE_NO_WARNINGS #include<stdio.h> #include<math.h> void s1(float a, float b, fl…...
优选算法系列(7.BFS 解决最短路问题)
简介: 先走到A,之后弹出A再把A能走到的地方加进去向外扩展把队列里面的元素(B,C)弹出来,再把B,C能到的地方入队列 一直这样那么最短路程就是扩展的层数。 迷宫中离入口最近的出口(me…...
实现定时发送邮件,以及时间同步
定时发送邮件 部署邮件服务 查看有没有安装mailx,安装了 [root192 ~]# rpm -q mailx mailx-12.5-43.fc38.x86_64去网易拿一下授权码,写到配置文件里 vim /etc/mail.rcset fromxxxxxxx163.com set smtpsmtp.163.com set smtp-auth-userxxxxxxx163.com set smtp-auth-passwor…...
Java反射知识点学习笔记
目录 一、定义 二、获取class对象的三种方式 1、Class.forName("全类名") 2、类名.class 3、对象.getClass() 三、案例 1、获取 class 反射对象三种方式 2、利用反射获取构造方法 3、利用反射获取成员变量 4、利用反射获取成员方法 Java反射是一种强大的编…...
Unity ShaderLab引用HLSL文件找不到其中函数
在写Unity Shader的过程中,常常需要将方法封装到HLSL文件中,今天遇到一个这样的报错, 明明hlsl文件路径引用没问题,却引用不到方法 并且将分散文件中的函数复制过来一切正常,最终定位到HLSL的预编译指令中 这指令的…...
【文献笔记】LLM-based control code generation using image recognition
LLM-based control code generation using image recognition 原文代码 标题翻译:基于图像识别的LLM控制代码生成 1. 内容介绍 1.1. 简介 论文提出了一种基于LLM的新方法,通过图像识别从管道仪表图(Piping and Instrumentation Diagrams,…...
算法之贪心算法
贪心算法 贪心算法核心思想常见应用场景典型案例案例一:找零问题案例二:活动选择问题案例三:货仓选址问题 贪心算法的应用详解霍夫曼编码最小生成树Dijkstra最短路径算法 总结 贪心算法 核心思想 贪心算法(Greedy Algorithm&…...
从“链主”到“全链”:供应链数字化转型的底层逻辑
1. 制造业与供应链数字化转型的必然性 1.1. 核心概念与战略重要性 制造业的数字化转型,是利用新一代数字技术(如工业互联网、人工智能、大数据、云计算、边缘计算等)对制造业的整体价值链进行根本性重塑的过程。这不仅涉及技术的应用&#…...
【Windows本地部署n8n工作流自动平台结合内网穿透远程在线访问】
💝💝💝欢迎来到我的博客,很高兴能够在这里和您见面!希望您在这里可以感受到一份轻松愉快的氛围,不仅可以获得有趣的内容和知识,也可以畅所欲言、分享您的想法和见解。 推荐:kwan 的首页,持续学…...
Python中如何加密/解密敏感信息(如用户密码、token)
敏感信息,如用户密码、API密钥、访问令牌(token)、信用卡号以及其他个人身份信息(PII),构成了现代应用程序和系统中最为关键的部分。这些信息一旦被未经授权的第三方获取,可能引发灾难性的后果,从个人隐私泄露到企业经济损失,甚至是大规模的社会安全问题。保护这些敏感…...
Win10如何一键切换IP地址教程
切换IP地址可能对于许多用户来说是一个相对陌生但又可能经常需要进行的操作。无论是出于网络安全、突破网络限制还是仅仅为了测试目的,一键切换IP地址都能带来极大的便利。以下是在 Windows 10 中通过批处理脚本实现一键切换 IP 地址的详细教程: 方法一&…...
2021-11-09 C++三位数平方含有该数
缘由求解,运算函数,哪位大神教一下-编程语言-CSDN问答 void 三位数平方含有该数() {//缘由https://ask.csdn.net/questions/7560152?spm1005.2025.3001.5141int a 100, aa 1000, f 0;while (a < aa){f a*a;while (f > a)if ((f - a) % aa)f …...
高效检测书签网址,告别无效链接烦恼
软件介绍 你是否有过面对浏览器中满满的书签,却不知道哪些网址还“健在”,哪些已经“跑路”的烦恼?别担心,今天就给大家介绍一款神奇的小工具——“网址小卫士”。 检测轻松搞定 还在一个个手动检查书签网址的有效性吗…...
SpringBoot高校学生评教系统设计实现
概述 基于SpringBoot的高校学生评教系统项目,该系统包含了学生评教、教师管理等功能,适合作为JavaWeb学习项目。 主要内容 1. 学生功能模块 查看评教信息:可以查看学期、院系、任课教师、课程名称等信息评价打分功能:可以对课…...
代码随想录算法训练营第二十天
LeetCode题目: 39. 组合总和40. 组合总和 II131. 分割回文串2176. 统计数组中相等且可以被整除的数对(每日一题) 其他: 今日总结 往期打卡 39. 组合总和 跳转: 39. 组合总和 学习: 代码随想录公开讲解 问题: 给你一个 无重复元素 的整数数组 candidates 和一个目标整数 targ…...
C++入门基础:命名空间,缺省参数,函数重载,输入输出
命名空间: C语言是基于C语言的,融入了面向对象编程思想,有了很多有用的库,所以接下来我们将学习C如何优化C语言的不足的。 在C/C语言实践中,在全局作用域中变量,函数,类会有很多,这…...
GPU怎么绑定到服务器上
确认服务器与 GPU 兼容性1:不同的服务器和 GPU 型号连接方式有所不同,要确保所选的 GPU 卡与服务器兼容。可通过服务器和 GPU 的产品文档,或使用服务器厂商提供的兼容性查询工具进行确认。安装前准备:关闭服务器电源,并…...
opencv函数展示2
一、像素操作与算术运算 1.cv2.split() 2. cv2.merge() 3.cv2.add() 4.cv2.bitwise_and() 5.cv2.bitwise_or() 6.cv2.inRange() 二、仿射变换 1.cv2.getRotationMatrix2D() 2.cv2.warpAffine() 3.cv2.flip() 4.cv2.resize() 三、透视变换 1.cv2.getPerspectiveTransform() 2…...
零基础上手Python数据分析 (16):DataFrame 常用统计分析方法
写在前面 —— 超越简单排序,探索数据内在规律,掌握Pandas统计分析基础 上一篇博客,我们学习了如何使用 Pandas 对 DataFrame 进行排序和排名,这使得我们能够更好地组织数据并快速定位关键信息。 然而,仅仅对数据进行排序和排名,还不足以完全理解数据。 要想更深入地解…...
文件系统 软硬连接
🌻个人主页:路飞雪吖~ 🌠专栏:Linux 目录 一、理解文件系统 🌠磁盘结构 二、软硬连接 🌟软硬链接 🌠软链接: 🌠硬链接: 🌟理解软硬链接的应…...
Linux环境基础开发工具使用
本节目标: 1. 学习yum工具,进行软件安装 2. 掌握vim编辑器使用,学会vim的简单配置 3. 掌握gcc/g编译器的使用,并了解其过程,原理 4. 掌握简单gdb使用于调试 5. 掌握简单的Makefile编写,了解其运行思想…...
秘密任务 2.0:如何利用 WebSockets + DTOs 设计实时操作
在之前的文章中,我们探讨了为什么 DTO 是提升 API 效率和安全性的秘密武器。现在,我们进入了一个全新的场景——我们将深入探讨如何通过 WebSockets DTOs 实现实时操作! Agent X 正在进行一项高风险的卧底任务。突然,总部更新了…...
LeetCode hot 100—括号生成
题目 数字 n 代表生成括号的对数,请你设计一个函数,用于能够生成所有可能的并且 有效的 括号组合。 示例 示例 1: 输入:n 3 输出:["((()))","(()())","(())()","()(())",&…...
2025.04.17【Dendrogram】生信数据可视化:Dendrogram图表详解
Dendrogram customization Go further with ggraph: edge style, general layout, node features, adding labels, and more. Customized circular dendrogram Learn how to build a circular dendrogram with proper labels. 文章目录 Dendrogram customizationCustomized c…...
SDL基础
SDL SDL(Simple DirectMedia Layer)是一个开源的跨平台多媒体开发库,主要用于开发需要图形、音频和输入设备支持的应用程序。它使用C语言编写,提供了简单易用的API,**能够帮助开发者快速实现跨平台的多媒体功能。**SD…...
硬件工程师面试常见问题(2)
第六问:你知道那些常用逻辑电平?TTL与COMS电平可以直接互连吗? 逻辑电平:是数字电路中用于表示二进制逻辑状态(0 和 1)的电压或电流信号范围,是数字系统中器件间信号传输的统一标准。 注:逻辑电…...
Python自学第2天:条件语句,循环语句
条件语句 1.条件判断 score 60 if score > 90:print("优秀") elif score > 60:print("及格") else:print("不及格") 注意: 1、每个条件后面要使用冒号 :,表示接下来是满足条件后要执行的语句块。2、使用缩进来划…...
2025年4月16日华为笔试第一题100分
📌 点击直达笔试专栏 👉《大厂笔试突围》 💻 春秋招笔试突围在线OJ 👉 笔试突围OJ 01. 博物馆展览规划 问题描述 卢小姐是一家著名博物馆的策展人,她需要从众多展品中选择一些组成新的展览。每件展品可以展示不同的历史文化主题,而博物馆希望通过最少的展品数量覆…...
智能体开发的范式革命:Cangjie Magic全景解读与实践思考
引言:当智能体开发遇见仓颉魔法 在人工智能技术日新月异的今天,智能体(Agent)开发正从实验室走向产业应用的核心舞台。2025年3月,仓颉社区推出的Cangjie Magic开源平台,以其创新的设计理念和技术架构,为这一领域带来了…...
LeetCode hot 100—单词搜索
题目 给定一个 m x n 二维字符网格 board 和一个字符串单词 word 。如果 word 存在于网格中,返回 true ;否则,返回 false 。 单词必须按照字母顺序,通过相邻的单元格内的字母构成,其中“相邻”单元格是那些水平相邻或…...
基于flask+vue框架的灯饰安装维修系统u49cf(程序+源码+数据库+调试部署+开发环境)带论文文档1万字以上,文末可获取,系统界面在最后面。
系统程序文件列表 项目功能:用户,工单人员,服务项目,订单记录,服务记录,评价记录 开题报告内容 基于 FlaskVue 框架的灯饰安装维修系统开题报告 一、选题背景与意义 (一)选题背景 随着城市化进程的加速与居民生活品质的显著提升…...
C/C++指针
为什么要使用指针 函数的值传递,无法通过调用函数,来修改函数的实参;被调用函数需要提供更多的“返回值”给调用函数;减少值传递时带来的额外开销,提高代码执行效率 指针定义:指针是什么 int age18; /* …...
Unity编辑器扩展之项目资源查找工具
一、需要实现的效果如下: 二、在项目的Asset目录下新增Editor目录,新增AssetSearchWindow和EditorDefine和EditorTools这三个C#脚本,并复制以下的代码保存好之后,就可以实现上述功能啦。 -------------------------------------------EditorTools脚本Begin----------------…...
什么是分布式锁?
分布式锁是一种在分布式系统中控制资源共享的机制。 一、背景和作用 在单机环境下,当多个线程同时访问共享资源时,可以通过线程锁(如 Java 中的 synchronized 关键字、ReentrantLock 等)来保证操作的原子性、可见性和有序性&#…...
ESP32- 开发笔记- 硬件设计-ESP32-C3 天线设计-利用嘉立创EDA来设计
这个硬件设计,只是一个随手记录文档。如果中间有什么问题,欢迎大家提出来。 1 板载天线 1.1 背景介绍 PCB(Printed Circuit Board)板载天线是现代电子设备中用于无线通信的一种关键组件,它直接集成在电路板上&#…...
setTimeoutsetIntervalrequestAnimationFrame
requestAnimationFrame 详解及与 setTimeout/setInterval 的比较 requestAnimationFrame(简称 rAF)是浏览器提供的专门用于 动画渲染 的 API,相比 setTimeout 和 setInterval,它在性能和流畅度上有显著优势。以下是详细解析和对比…...
Python内置函数---anext()
用于异步迭代器的核心工具,专为处理异步数据流设计。 1. 基本语法 await anext(async_iterator, default) 参数: async_iterator :实现了异步迭代协议的对象(如异步生成器、异步迭代器类)。 default (可选…...