lucky88717

用FPGA实现DDS任意波形发生器

0
阅读(20296)

DDS直接数字式频率合成器(Direct Digital Synthesizer),相信所有人看到这个名字就觉得不会陌生。有些资料讲述的方式太高大上,不少人一时半会接受不了。本篇文章从双口RAM入手,由浅入深脱掉DDS高大上的外衣。


基本原理框图:



两个关键术语:
       a. 相位累加器:Phase = Phase + freq_ctrl,可以暂且理解为i = i + 1一样的东西。
       b. 频率控制字:freq_ctrl,这个东西的值直接影响输出信号的频率。

假设系统工作时钟(查表时钟)为150MHz,ROM表深度为4096,存储波形为1个周期(如正弦波每周期抽样量化为4096个点),也就是一个周期的波形由4096个采样点组成,意味着输出波形一个周期最多4096个采样点。比如Data输出10M的正弦波,输出的正弦波每周期只有15个采样点;而输出1M的正弦波,每周期将有150个采样点;我们也可以知道当输出频率小于等于36.621KHz时,输出波形每周期由4096个点构成。输出信号的每周期点越多,阶梯效过越不明显,经过低通滤波器后波形越好看。


如果freq_ctrl为1时,那么输出信号为150MHz/4096=36.621KHz,如果freq_ctrl为2时,那么输出信号为150MHz*2/4096=73.242KHz。因此当需要输出正弦波频率为fout MHz时,

Fout = 150MHz*freq_ctrl/4096,所以freq_ctrl = Fout*4096/150MHz。


如果上面的大家都理解了,那么恭喜你已经完全理解了DDS的核心部分。至于其他DDS相关的内如,比如频率分辨率(因为rom地址必须是整数,所以freq_ctrl必须是整数,所以上例的频率分辨率为),旁瓣抑制比(量化多1bit,多6db = 20lg2)……(╯-_-)╯╧╧

好了理解了上面,下面来一个FPGA工程实现一个DDS。


工程目标:通过SPI可以配置RAM的值,通过SPI配置频率控制字,输出数据给DAC,同时提供随路时钟。此工程可外接MCU通过任何你想得到的方式配置用户想输出的周期信号(不仅仅只是sin、cos、方波、锯齿、三角,可以是任何波形哦)。


软件环境vivado2014.2


1. 新建工程


2. 例化IP

a) 使用MMCM将时钟3倍频


b) 例化真双口RAM 16bitsx4096



3. 直接贴代码

module dds_top(
//global
input wire i_clk,//50M
input wire i_rst,
//spi
input wire i_spi_sclk,
input wire i_spi_miso,
input wire i_spi_nss,
//dac
output wire o_dac_clk,
output wire [15:0]o_dac_data
);

wire clk;//main clk,150M
wire mmcm_locked;
wire asy_rst_wire;
reg [7:0]asy_rst_reg;
wire asy_rst;

reg [7:0]spi_sclk_reg;
reg [7:0]spi_miso_reg;
reg [7:0]spi_nss_reg;
wire spi_sclk_pos;
wire spi_nss_neg;
wire spi_nss_pos;

reg [14:0]spi_addr;
reg [15:0]spi_data;
reg [7:0]spi_cnt;
reg [31:0]spi_miso_data;
reg [15:0]spi_cnt_reg;
reg spi_wen;

reg [11:0]phase;//phase: 0-4095    
///////////////////clk and reset///////////////////

clk_wiz u_clk_wiz 
 (
 // Clock in ports
  .clk_in1	(	i_clk	),//50M
  // Clock out ports
  .clk_out1	(	clk	),  //150M
  // Status and control signals
  .reset		(	i_rst	),
  .locked		(	mmcm_locked	)
 );

assign asy_rst_wire = i_rst | (~mmcm_locked);

always@(posedge clk or posedge i_rst)
	if(i_rst)
		asy_rst_reg <= 8'd0;
	else
		asy_rst_reg <= {asy_rst_reg[6:0],asy_rst_wire};

assign asy_rst = asy_rst_reg[7];

/////////////////// spi  input ///////////////////
//spi frame 32bit
// wen   addr    data
// [31] [30:16] [15:0]  

always@(posedge clk or posedge asy_rst)
	if(asy_rst)
		begin
			spi_sclk_reg <= 8'd0;
			spi_miso_reg <= 8'd0;
			spi_nss_reg <= 8'd0;
		end
	else
		begin
			spi_sclk_reg <= {spi_sclk_reg[6:0],i_spi_sclk};
			spi_miso_reg <= {spi_miso_reg[6:0],i_spi_miso};
			spi_nss_reg <= {spi_nss_reg[6:0],i_spi_nss};
		end

//get sclk posedge
assign spi_sclk_pos = ~spi_sclk_reg[7] & spi_sclk_reg[6] ;
//get nss negedge & posedge
assign spi_nss_neg = spi_nss_reg[6] & ~spi_nss_reg[5] ;
assign spi_nss_pos = ~spi_nss_reg[6] & spi_nss_reg[5] ;

always@(posedge clk or posedge asy_rst)
	if(asy_rst)
		spi_cnt <= 8'd0;
	else if(spi_nss_neg)
		spi_cnt <= 8'd0;
	else if(spi_sclk_pos)
		spi_cnt <= spi_cnt + 1'b1;
	else
		spi_cnt <= spi_cnt;

always@(posedge clk or posedge asy_rst)
	if(asy_rst)
		spi_miso_data <= 32'd0;
	else if(spi_nss_neg)
		spi_miso_data <= 32'd0;
	else if(spi_sclk_pos)
		spi_miso_data <= {spi_miso_data[30:0],spi_miso_reg[6]};
	else
		spi_miso_data <= spi_miso_data;

always@(posedge clk or posedge asy_rst)
	if(asy_rst)
		begin
			spi_addr <= 15'd0;
			spi_data <= 16'd0;
		end
	else
		if(spi_cnt == 8'd32)
			begin
				spi_addr <= spi_miso_data[30:16];
				spi_data <= spi_miso_data[15:0];
			end
		else
			begin
				spi_addr <= 15'd0;
				spi_data <= 16'd0;
			end

always@(posedge clk or posedge asy_rst)
	if(asy_rst)
		spi_cnt_reg <= 16'd0;
	else
		spi_cnt_reg <= {spi_cnt_reg[7:0],spi_cnt[7:0]};


//address 0-4095 for dds_ram
always@(posedge clk or posedge asy_rst)
	if(asy_rst)
		spi_wen <= 1'b0;
	else if(spi_cnt_reg == {8'd31,8'd32} && spi_miso_data[31:28] == 4'b1000)
		spi_wen <= 1'b1;
	else
		spi_wen <= 1'b0;

//address 4096-16383,16385-32767 reserved

//address 16384 for freq control,low 12bits
reg [15:0]freq_ctrl_reg;
always@(posedge clk or posedge asy_rst)
	if(asy_rst)
		freq_ctrl_reg <= 16'd0;
	else if(spi_cnt_reg == {8'd31,8'd32} && spi_miso_data[31:16] == {1'b1,15'd16384})
		freq_ctrl_reg <= spi_miso_data[15:0];
	else
		freq_ctrl_reg <= freq_ctrl_reg;
		
////////////////////////dpram////////////////////////

//16*4096
dds_ram u_dds_ram (
  .clka		(	clk	),
  .ena		(	1'b1	),
  .wea		(	spi_wen	),
  .addra	(	spi_addr[11:0]	),//4096-12bit
  .dina		(	spi_data	),
  .douta	(		),
  .clkb		(	clk	),
  .enb		(	1'b1	),
  .web		(	1'b0	),
  .addrb	(	phase	),//4096-12bit
  .dinb		(	16'd0	),
  .doutb	(	o_dac_data	)
);

always@(posedge clk or posedge asy_rst)
	if(asy_rst)
		phase <= 12'd0;
	else
		phase <= phase + freq_ctrl_reg[11:0];

ODDR #(
   .DDR_CLK_EDGE("SAME_EDGE"), // "OPPOSITE_EDGE" or "SAME_EDGE" 
   .INIT(1'b0),    // Initial value of Q: 1'b0 or 1'b1
   .SRTYPE("SYNC") // Set/Reset type: "SYNC" or "ASYNC" 
) ODDR_inst (
   .Q(o_dac_clk),   // 1-bit DDR output
   .C(clk),   // 1-bit clock input
   .CE(1'b1), // 1-bit clock enable input
   .D1(1'b1), // 1-bit data input (positive edge)
   .D2(1'b0), // 1-bit data input (negative edge)
   .R(asy_rst),   // 1-bit reset
   .S(1'b0)    // 1-bit set
);

endmodule


4. 时序和管脚约束

create_clock -period 6.667 -name clk150 -waveform {0.000 3.333} -add [get_nets clk]

set_property PACKAGE_PIN V4 [get_ports i_clk]
set_property PACKAGE_PIN V3 [get_ports i_rst]
set_property PACKAGE_PIN V2 [get_ports i_spi_miso]
set_property PACKAGE_PIN W2 [get_ports i_spi_nss]
set_property PACKAGE_PIN W1 [get_ports i_spi_sclk]
set_property PACKAGE_PIN W4 [get_ports o_dac_clk]
set_property PACKAGE_PIN AB1 [get_ports {o_dac_data[15]}]
set_property PACKAGE_PIN AB2 [get_ports {o_dac_data[14]}]
set_property PACKAGE_PIN AB3 [get_ports {o_dac_data[13]}]
set_property PACKAGE_PIN AA1 [get_ports {o_dac_data[12]}]
set_property PACKAGE_PIN AA3 [get_ports {o_dac_data[11]}]
set_property PACKAGE_PIN Y1 [get_ports {o_dac_data[10]}]
set_property PACKAGE_PIN Y2 [get_ports {o_dac_data[9]}]
set_property PACKAGE_PIN Y3 [get_ports {o_dac_data[8]}]
set_property PACKAGE_PIN AA4 [get_ports {o_dac_data[7]}]
set_property PACKAGE_PIN U1 [get_ports {o_dac_data[6]}]
set_property PACKAGE_PIN U2 [get_ports {o_dac_data[5]}]
set_property PACKAGE_PIN T1 [get_ports {o_dac_data[4]}]
set_property PACKAGE_PIN R1 [get_ports {o_dac_data[3]}]
set_property PACKAGE_PIN R2 [get_ports {o_dac_data[2]}]
set_property PACKAGE_PIN P1 [get_ports {o_dac_data[1]}]
set_property PACKAGE_PIN P2 [get_ports {o_dac_data[0]}]


5. 实现报告


一个可配波形的DDS就这么简单!我们可以将多个简单的程序放在一起,比如增加幅度控制、正交调制、载波相乘、触发控制等配合依托MCU的交互界面和功放,就变成了一个复杂的信号源了。


6. vivado仿真




参照SPI时序,编写testbench,本例中通过SPI接口配置了两次频率控制字,第一次配置为1(频率为36.621KHz),第二次配置为8(频率为4.687MHz)。通过仿真结果可以观察到SPI时序正确,频率控制字配置正确,输出波形频率满足期望。




此图中输入50MHz时钟对应一周期为10ns,倍频后150MHz时钟对应一周期为3.333ns,


图中(31974-18321)/3.333 = 4096,因此频率与预期一致。


PS:vivado simulator 的仿真效果不错,就是速度与专业仿真软件modelsim/questasim相比没那么给力。当然,如果是用第三方软件仿真需要编译库才可使用。


编译库使用compile_simlib命令: 

例 compile_simlib -simulator modelsim -family virtex7 -library unisim -library simprim -language vhdl

我喜欢 compile_simlib -simulator modelsim -family virtex7 -library all -language all


编译好的库保存在当前工程目录内,可以拷出来通过修改modelsim.ini 文件,使modelsim直接支持对应的库,免得以后再编译。