目录
一、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: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:仅通过单一组地址线控制数据的写入和读取操作,读写操作无法同时进行;
端口名 | 端口描述 |
---|---|
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/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博客