DDS的FPGA实现
说明:本文主要对DDS如何进行在FPGA上的实现进行分析,参考了文章https://www.fpga4fun.com/DDS2.html
第一部分:对lookup_table模块的分析
参考小脚丫的代码如下:
// --------------------------------------------------------------------
// >>>>>>>>>>>>>>>>>>>>>>>>> COPYRIGHT NOTICE <<<<<<<<<<<<<<<<<<<<<<<<<
// --------------------------------------------------------------------
// Module: lookup_table
//
// Description: 查找表,根据波表地址获取波表数据,并做偏置处理
// --------------------------------------------------------------------
module lookup_table
(
input [7:0] phase,
output reg [9:0] dac_data_out
);
//正弦波为圆旋转时的投影,具有一定的对称性,只需1/4的数据就可以将正弦波数据复现
//将完整的波表数据分为区间地址和1/4波形数据地址,可以节约fpga存储资源的占用
wire [5:0] address = phase[5:0]; //1/4波形数据地址
wire [1:0] sel = phase[7:6]; //区间地址
wire [9:0] sine;
//为输出的数据增加偏置,使对应输出都为正直
always @(*)
case (sel)
2'b00 : dac_data_out = {1'b1, sine[9:1]};
2'b01 : dac_data_out = {1'b1, sine[9:1]};
2'b10 : dac_data_out = {1'b0, 9'h1ff-sine[9:1]};
2'b11 : dac_data_out = {1'b0, 9'h1ff-sine[9:1]};
endcase
//例化
sine_table sine_table_uut
(
.sel(sel),
.address(address),
.sine(sine)
);
endmodule
// --------------------------------------------------------------------
// >>>>>>>>>>>>>>>>>>>>>>>>> COPYRIGHT NOTICE <<<<<<<<<<<<<<<<<<<<<<<<<
// --------------------------------------------------------------------
// Module: sine_table
//
// Description: 正弦波1/4波形查表及对称性处理
// --------------------------------------------------------------------
module sine_table
(
input [1:0] sel,
input [5:0] address,
output reg [9:0] sine
);
reg [5:0] table_addr;
//为4个区间做对称性处理
always @(sel or address)
case (sel)
2'b00: table_addr = address;
2'b01: table_addr = 6'h3f - address;
2'b10: table_addr = address;
2'b11: table_addr = 6'h3f - address;
endcase
//正弦波1/4波表
always @(table_addr)
case(table_addr)
6'h0: sine=10'h000;
6'h1: sine=10'h019;
6'h2: sine=10'h032;
6'h3: sine=10'h04B;
6'h4: sine=10'h064;
6'h5: sine=10'h07D;
6'h6: sine=10'h096;
6'h7: sine=10'h0AF;
6'h8: sine=10'h0C4;
6'h9: sine=10'h0E0;
6'ha: sine=10'h0F9;
6'hb: sine=10'h111;
6'hc: sine=10'h128;
6'hd: sine=10'h141;
6'he: sine=10'h159;
6'hf: sine=10'h170;
6'h10: sine=10'h187;
6'h11: sine=10'h19F;
6'h12: sine=10'h1B5;
6'h13: sine=10'h1CC;
6'h14: sine=10'h1E2;
6'h15: sine=10'h1F8;
6'h16: sine=10'h20E;
6'h17: sine=10'h223;
6'h18: sine=10'h238;
6'h19: sine=10'h24D;
6'h1a: sine=10'h261;
6'h1b: sine=10'h275;
6'h1c: sine=10'h289;
6'h1d: sine=10'h29C;
6'h1e: sine=10'h2AF;
6'h1f: sine=10'h2C1;
6'h20: sine=10'h2D3;
6'h21: sine=10'h2E5;
6'h22: sine=10'h2F6;
6'h23: sine=10'h307;
6'h24: sine=10'h317;
6'h25: sine=10'h326;
6'h26: sine=10'h336;
6'h27: sine=10'h344;
6'h28: sine=10'h353;
6'h29: sine=10'h360;
6'h2a: sine=10'h36D;
6'h2b: sine=10'h37A;
6'h2c: sine=10'h386;
6'h2d: sine=10'h392;
6'h2e: sine=10'h39C;
6'h2f: sine=10'h3A7;
6'h30: sine=10'h3B1;
6'h31: sine=10'h3BA;
6'h32: sine=10'h3C3;
6'h33: sine=10'h3CB;
6'h34: sine=10'h3D3;
6'h35: sine=10'h3DA;
6'h36: sine=10'h3E0;
6'h37: sine=10'h3E6;
6'h38: sine=10'h3EB;
6'h39: sine=10'h3F0;
6'h3a: sine=10'h3F3;
6'h3b: sine=10'h3F7;
6'h3c: sine=10'h3FA;
6'h3d: sine=10'h3FC;
6'h3e: sine=10'h3FE;
6'h3f: sine=10'h3FF;
endcase
endmodule
Look up table 简称 LUT,LUT是一个表,它保存着我们想要生成的模拟信号的形状。
这个表的作用其实很简单,就是把很多sin值存储在FPGA里面,然后让另一模块根据地址直接进行查找。DDS技术在普通的MCU上也能实现,但是根据角度值计算对应的sin值,这是需要CPU花费一些时间。这样的话,转换波形的过程就很慢了。
sin值储存的代码如下
//正弦波1/4波表
always @(table_addr)
case(table_addr)
6'h0: sine=10'h000;
6'h1: sine=10'h019;
6'h2: sine=10'h032;
6'h3: sine=10'h04B;
6'h4: sine=10'h064;
6'h5: sine=10'h07D;
6'h6: sine=10'h096;
6'h7: sine=10'h0AF;
6'h8: sine=10'h0C4;
6'h9: sine=10'h0E0;
6'ha: sine=10'h0F9;
6'hb: sine=10'h111;
6'hc: sine=10'h128;
6'hd: sine=10'h141;
6'he: sine=10'h159;
6'hf: sine=10'h170;
6'h10: sine=10'h187;
6'h11: sine=10'h19F;
6'h12: sine=10'h1B5;
6'h13: sine=10'h1CC;
6'h14: sine=10'h1E2;
6'h15: sine=10'h1F8;
6'h16: sine=10'h20E;
6'h17: sine=10'h223;
6'h18: sine=10'h238;
6'h19: sine=10'h24D;
6'h1a: sine=10'h261;
6'h1b: sine=10'h275;
6'h1c: sine=10'h289;
6'h1d: sine=10'h29C;
6'h1e: sine=10'h2AF;
6'h1f: sine=10'h2C1;
6'h20: sine=10'h2D3;
6'h21: sine=10'h2E5;
6'h22: sine=10'h2F6;
6'h23: sine=10'h307;
6'h24: sine=10'h317;
6'h25: sine=10'h326;
6'h26: sine=10'h336;
6'h27: sine=10'h344;
6'h28: sine=10'h353;
6'h29: sine=10'h360;
6'h2a: sine=10'h36D;
6'h2b: sine=10'h37A;
6'h2c: sine=10'h386;
6'h2d: sine=10'h392;
6'h2e: sine=10'h39C;
6'h2f: sine=10'h3A7;
6'h30: sine=10'h3B1;
6'h31: sine=10'h3BA;
6'h32: sine=10'h3C3;
6'h33: sine=10'h3CB;
6'h34: sine=10'h3D3;
6'h35: sine=10'h3DA;
6'h36: sine=10'h3E0;
6'h37: sine=10'h3E6;
6'h38: sine=10'h3EB;
6'h39: sine=10'h3F0;
6'h3a: sine=10'h3F3;
6'h3b: sine=10'h3F7;
6'h3c: sine=10'h3FA;
6'h3d: sine=10'h3FC;
6'h3e: sine=10'h3FE;
6'h3f: sine=10'h3FF;
endcase
在实际的使用中,可以增加更多的点数来提高分辨率,这段代码中,存储了64个点,对应了1/4正弦波从0度到90度的位置。
在 lookup_table中,例化了另外一个模块
module sine_table
(
input [1:0] sel,
input [5:0] address,
output reg [9:0] sine
);
其中,sel 代表区间,address1/4波形数据地址,sine则是代表了输出信号。 为啥会有区间呢?根据正弦函数的对称性,我们只要知道了1/4周期波形的数据,便可以根据这些数据得出整个周期的数据。我们来看这部分的代码
//为4个区间做对称性处理
always @(sel or address)
case (sel)
2'b00: table_addr = address;//0-1/4区间
2'b01: table_addr = 6'h3f - address;//1/4-2/4区间
2'b10: table_addr = address;///2/4-3/4区间
2'b11: table_addr = 6'h3f - address;//3/4-4/4区间
endcase
本段代码用了verliog语法中的case语句块,至于sel的取值,我们在下一模块中再进行分析。对于table_address的取值请看下图
显然,在区间为0-1/4这段正弦波时,储存的波形就是对应区间的,故直接赋值即可。在1/4-2/4这段正弦波时,显然由于对称的关系,根据0-1/4和1/4-2/4这两段正弦波之间的关系,容易知道两者的地址之和对应90度的那个地址,故有 由前面的分析可知table_address=6’h3f - address。读者可能会对
2'b10: table_addr = address;///2/4-3/4区间 2'b11: table_addr = 6'h3f - address;//3/4-4/4区间
有所困惑,实际上这里也运用了对称性,不用担心,且看这段代码
//为输出的数据增加偏置,使对应输出都为正直
always @(*)
case (sel)
2’b00 : dac_data_out = {1’b1, sine[9:1]};
2’b01 : dac_data_out = {1’b1, sine[9:1]};
2’b10 : dac_data_out = {1’b0, 9’h1ff-sine[9:1]};
2’b11 : dac_data_out = {1’b0, 9’h1ff-sine[9:1]};
endcase
dac_data_out是最后的输出,而sine[9:1]是来自波表查找的结果,大家可以发现在第三,第四区间,sine[9:1]前面都有了负号。
这里还有一个偏置的问题。DAC是不能够输出负电压的,所以,要得到完整的波形,还需将最低电平抬升到0以上的,故在输出时还得做相应的处理。
OK,大家看懂了上面的代码了吧,下面介绍主模块!!!
module DDS
(
input clk_in, //系统时钟
input rst_n_in, //系统复位,低有效
input [23:0] f_increment, //频率控制字
input [23:0] p_increment, //相位控制字
output dac_clk_out, //DAC时钟输出
output [9:0] dac_data_out //DAC数据输出
);
reg [23:0] phase_accumulator; //定义相位寄存器
//将DAC时钟连接系统时钟
assign dac_clk_out = clk_in;
//next_phase = phase_accumulator + f_increment;
//相位寄存器按照DAC时钟的节拍自加频率控制字
always @(posedge dac_clk_out)
begin
if(!rst_n_in) phase_accumulator <= 1'b0; //复位时清零
else phase_accumulator <= phase_accumulator + f_increment;
end
//定义相位波表地址寄存器,等于 相位寄存器 加上 相位控制字
wire [23:0] phase = phase_accumulator + p_increment;
//有了相位波表地址,可以按地址查找对应的波表数据,例化查找表模块
lookup_table u1
(
.phase (phase[23:16] ),
.dac_data_out (dac_data_out )
);
endmodule
emmmmm,这个应该是最主要的部分了,大家先看这张图
这个就是整个DDS的系统框图了,其中波形查找表这部分已经在前面进行了分析。首先,分析一下模块的端口
module DDS
(
input clk_in, //系统时钟
input rst_n_in, //系统复位,低有效
input [23:0] f_increment, //频率控制字
input [23:0] p_increment, //相位控制字
output dac_clk_out, //DAC时钟输出
output [9:0] dac_data_out //DAC数据输出
);
首先是系统时钟输入,然后是复位信号,还有频率控制字,相位控制字等输入端口,DAC时钟输出端口,DAC数据输出端口等。
这个频率控制字到底是啥呢,我刚看的时候也是有点迷糊的!且看它的定义:
input [23:0] f_increment, //频率控制字
它是一个24位的二进制数,最大也就是10进制的2的24次方。看它的名称,我们不难猜出,它是用来控制频率的,那么它和输出频率到底有和关联呢?请看到图中的相位累加器,我觉得,与其叫它为相位累加器,不如叫做地址累加器,这就和LUP那个模块有了关联。
我们来看看这段代码
/相位寄存器按照DAC时钟的节拍自加频率控制字
always @(posedge dac_clk_out)
begin
if(!rst_n_in) phase_accumulator <= 1'b0; //复位时清零
else phase_accumulator <= phase_accumulator + f_increment;
end
在DAC时钟的节拍下,地址寄存器加了频率控制字,改变了地址,在LUT模块中则通过该地址来查找波表。
如何计算输出的频率呢?
关于频率的控制详见第二篇文章。