FPGA之DDS信号发生器(个人学习参考)

DDS是直接数字式频率合成器(Direct Digital Synthesizer)的英文缩写,是一项关键的数字化技术
 
DDS的基本结构主要由相位累加器、相位调制器、波形数据表ROM、D/A转换器等四大结构组成,其中较多设计还会在数模转换器之后增加一个低通滤波器。DDS结构示意图,具体见图 33-1
 
图 33-1中是不含低通滤波器的DDS结构示意图,图中主要包括相位累加器、相位调制器、波形存储器、数模(D/A)转换器等四大结构
系统时钟CLK为整个系统的工作时钟,频率为fCLK;频率字输入F_WORD,一般为整数,数值大小控制输出信号的频率大小,数值越大输出信号频率越高,反之,输出信号频率越低,后文中用K表示;相位字输入P_WORD,为整数,数值大小控制输出信号的相位偏移,主要用于相位的信号调制,后文用P表示;设输出信号为CLK_OUT,频率为fOUT
图中所展示的四大结构中,相位累加器是整个DDS的核心,在这里完成相位累加,生成相位码。相位累加器的输入为频率字输入K,表示相位增量,设其位宽为N,满足等式K = 2N * fOUT / fCLK 。其在输入相位累加器之前,在系统时钟同步下做数据寄存,数据改变时不会干扰相位累加器的正常工作。
相位调制器接收相位累加器输出的相位码, 在这里加上一个相位偏移值P,主要用于信号的相位调制,如应用于通信方面的相移键控等,不使用此部分时可以去掉,或者将其设为一个常数输入,同样相位字输入也要做寄存。
波形数据表ROM中存有一个完整周期的正弦波信号。假设波形数据ROM的地址位宽为12位,存储数据位宽为8位,即ROM有212 = 4096个存储空间,每个存储空间可存储1字节数据。将一个周期的正弦波信号,沿横轴等间隔采样212 = 4096次,每次采集的信号幅度用1字节数据表示,最大值为255,最小值为0。将4096次采样结果按顺序写入ROM的4096个存储单元,一个完整周期正弦波的数字幅度信号写入了波形数据表ROM中。波形数据表ROM以相位调制器传入的相位码为ROM读地址,将地址对应存储单元中的电压幅值数字量输出。

输出信号CLK_OUT的信号频率fOUT = K * fCLK / 2N。当K = 1时,可得DDS最小分辨率为:fOUT = fCLK / 2N,此时输出信号频率最低。根据采样定理,K的最大值应小于2N/ 2
讲到这里,读者会心存疑虑,相位累加器得到的相位码是如何实现ROM寻址的呢?
对于N位的相位累加器,它对应的相位累加值为2N,如果正弦ROM中存储单元的个数也是2N的话,这个问题就很好解决,但是这对ROM的对存储容量的要求较高。在实际操作中,我们使用相位累加值的高几位对ROM进行寻址,也就是说并不是每个系统时钟都对ROM进行数据读取,而是多个时钟读取一次,因为这样能保证相位累加器溢出时,从正弦ROM表中取出正好一个正弦周期的样点。
因此,相位累加器每计数2N次,对应一个正弦周期。而相位累加器1秒钟计数fCLK次,在k=1时,DDS输出的时钟频率就是频率分辨率。频率控制字K增加时,相位累加器溢出的频率增加,对应DDS输出信号CLK_OUT频率变为K倍的DDS频率分辨率。
设:ROM存储单元个数为4096,每个存储数据用8位二进制表示。即,ROM地址线宽度为12,数据线宽度为8;相位累加器位宽N = 32。
根据上述条件可以知道,相位调制器位宽M = 12,那么根据DDS原理。那么在相位调制器中与相位控制字进行累加时,应用相位累加器的高12位累加。而相位累加器的低20位只与频率控制字累加。
我们以频率控制字K = 1为例,相位累加器的低20位一直会加1,直到低20位溢出向高12位进位,此时ROM为0,也就是说,ROM的0地址中的数据被读了220次,继续下去,ROM中的4096个点,每个点都将会被读220次,最终输出的波形频率应该是参考时钟频率的1 / 220,周期被扩大了220 倍。同样当频率控制字为100时,相位累加器的低20位一直会加100,那么,相位累加器的低20位溢出的时间比上面会快100倍,则ROM中的每个点相比于上面会少读100次,所以最终输出频率是上述的10倍。
  • 如图,一个量化的32点的正弦波,也就是说一个ROM里存了32个这样的数据,每次读出一个数据要1ms,分别读出1,2,3...30,31,32,共32个点,读取完整的正弦波需要1ms * 32 = 32ms的时间

该正弦波参数为

  • 周期T = 1ms * 32 = 32ms,
  • 频率为 f = 1/T = 1/(1ms * (32/1))

  • 在读出一个数据时间不变(1ms)的情况下,想要让读出的正弦波频率增加一倍,那就要间隔读取,分别读出2,4,6,8,10...28,30,32,此时只需要读16个点

那么读出完整正弦波的参数为

  • 周期T = 1ms * 16 = 16ms
  • 频率f = 1/T = 1/(1ms * 16) = 1/(1ms * (32/2))

  • 想要读出的正弦波频率减少一倍,那就要插值读取,分别读出0.5,1,1.5,2,2.5,3...30.5,31,31.5,32,此时要读64个点

读出正弦波的参数为

  • 周期T = 1ms * 64 = 64ms
  • 频率f = 1/T = 1/(1ms * 64) = 1/(1ms * (32/0.5))

这里,1ms即为Tclk,fclk = 1/Tclk = 1/1ms;32 = 2^5即为N=5,而32除以的数(1,2,0.5)即为频率控制字fword,那么fo = (fclk * fword)/(2^N)

通常,FPGA并不擅长浮点运算,第三种情况,上式的(32/0.5)是很难实现的,因此在正弦波周期一样的情况下,将精度N调高一位,N=6,(2^5 * 2)/(0.5 * 2),此时fword就不用为0.5,而是1

相位控制字pword的参数解释,如果从x轴为8的数据开始取,那么相当于正弦波相移了90°,pword = 8,这就是相位控制字


 

为什么地址是由相位控制字加频率控制字高12位得到的?
1、本次实验使用的rom是宽度为14,深度为2^12 = 4096的数据,所以相位控制字根据rom的深度选择了12位宽
2、为什么ROM宽度是14,深度不取2^14?FPGA资源不够,没有这么多的寄存器存取这么多的数据
3、地址 = 相位 + 频率更迭,而相位宽度为12位,频率的宽度比相位多,所以频率控制字取高几位是由相位控制字的宽度决定的
4、取频率控制字高12位是如何完成频率变换的?
举例:
2^1 = 2'b10
2^2 = 3'b100
2^3 = 4'b1000
......
2^19 = 20'b1000_0000_0000_0000_0000
2^20 = 21'b1_0000_0000_0000_0000_0000
2^21 = 22'b10_0000_0000_0000_0000_0000

f = 1/T,N = 32
频率控制字为:2^20
fword_acc[31:0] + 2^20 相当于 (fword_acc[31:20] + 1)此时就是按照地址+1的速度寻址,假如Fclk = 50MHz(系统时钟),Tclk = 20ns,输出波形的周期就为:To = 20ns * 4096

频率控制字为:2^19
fword_acc[31:0] + (2^19 + 2^19) 相当于 (fword_acc[31:20] + 1),也就是要加两次频率控制字,才能实现一次地址+1,Tclk = 20ns,输出完整波形就要输出2次*4096个数据,输出的波形周期为:To = 20ns * (2 * 4096)

频率控制字为:2^21
fword_acc[31:0] + (2^21) 相当于 (fword_add[31:20] + 2'b10),加一次频率控制字,实现一次地址+2,Tclk = 20ns,因为是跳过一位地址取的数据,所以数据量减半,输出完整波形只需要输出4096/2个数据,输出的波形周期为:To = 20ns * (4096/2)

 
 
设计文件.v
`timescale 1ns / 1ps
//////////////////////////////////////////////////////////////////////////////////
// Company:
// Engineer:
//
// Create Date: 2022/01/10 15:31:44
// Design Name:
// Module Name: DDS
// Project Name:
// Target Devices:
// Tool Versions:
// Description:
//
// Dependencies:
//
// Revision:
// Revision 0.01 - File Created
// Additional Comments:
//
//////////////////////////////////////////////////////////////////////////////////


module DDS(
clk ,
ce ,
we,
data,
reset,
sine,
cose
);

//参数定义
parameter DATA_W = 32;
parameter DATA_W1 = 16;

//输入信号定义
input clk ;
input reset ;
input [DATA_W-1:0] data;
input we;
input ce;

//输出信号定义
output[DATA_W1-1:0] sine ;
output[DATA_W1-1:0] cose ;


//中间信号定义
reg [DATA_W-1:0] ADD_A ;
reg [DATA_W-1:0] ADD_B ;
reg [DATA_W1-1:0] cose_DR ;
reg [DATA_W1-1:0] sine_DR ;

wire [DATA_W-1:0] data;
wire [DATA_W1-1:0] cose_D;
wire [DATA_W1-1:0] sine_D;
wire [9:0] ROM_A;


assign cose=cose_DR;
assign sine=sine_DR;
assign ROM_A=ADD_B[31:22]; //这里比较关键,需要明白为什么是高位开始想加

//时序逻辑写法
always@(posedge clk or negedge reset)begin
if(reset)begin
ADD_A <=0;
end
else if(we) begin
ADD_A <=data;
end
end

always@(posedge clk or negedge reset)begin
if(reset)begin
ADD_B <=0;
end
else if(ce) begin
ADD_B <=ADD_B + ADD_A;
end
end

always@(posedge clk or negedge reset)begin
if(reset)begin
cose_DR <=0;
end
else if(ce) begin
cose_DR <=cose_D;
end
end

always@(posedge clk or negedge reset)begin
if(reset)begin
sine_DR <=0;
end
else if(ce) begin
sine_DR <=sine_D;
end
end

cos u1 (
.clka(clk), // input wire clka
.addra(ROM_A), // input wire [9 : 0] addra
.douta(cose_D) // output wire [15 : 0] douta
);

sine u2 (
.clka(clk), // input wire clka
.addra(ROM_A), // input wire [9 : 0] addra
.douta(sine_D) // output wire [15 : 0] douta
);
endmodule

  

 

 
 
 
仿真tb文件
 
 
 
 
 
 
 
 
 
`timescale 1ns / 1ps
//////////////////////////////////////////////////////////////////////////////////
// Company:
// Engineer:
//
// Create Date: 2022/01/10 15:57:17
// Design Name:
// Module Name: dds_tb
// Project Name:
// Target Devices:
// Tool Versions:
// Description:
//
// Dependencies:
//
// Revision:
// Revision 0.01 - File Created
// Additional Comments:
//
//////////////////////////////////////////////////////////////////////////////////
module dds_tb();

//时钟和复位
reg clk ;
reg rst_n;
reg we;
reg ce;
//uut的输入信号
reg[31:0] data ;

 

//uut的输出信号
wire[15:0] cos;
wire[15:0] sin;


//时钟周期,单位为ns,可在此修改时钟周期。
parameter CYCLE = 20;

//复位时间,此时表示复位3个时钟周期的时间。
parameter RST_TIME = 3 ;

//待测试的模块例化
DDS uut(
.clk (clk ),
.reset (rst_n ),
.we (we ),
.ce (ce ),
.data (data ),
.sine (sin ),
.cose (cos )

);


//生成本地时钟50M
initial begin
clk = 0;
forever
#(CYCLE/2)
clk=~clk;
end

//产生复位信号
initial begin
rst_n = 0;
#2;
rst_n = 1;
#(CYCLE*RST_TIME);
rst_n = 0;
end

//输入信号din0赋值方式
initial begin
#1;
//赋初值
we = 0;
#(5*CYCLE);
//开始赋值
we = 1;

end

//输入信号din1赋值方式
initial begin
#1;
//赋初值
ce = 0;
#(10*CYCLE);
//开始赋值
ce=1;


end

initial begin
#1;
//赋初值
data = 4194304;
end

endmodule

  

 

 
 
 
 
仿真结果
 
 
posted @ 2022-01-10 22:50  快乐气氛组阿宇  阅读(2752)  评论(0编辑  收藏  举报