FPGA--简易DDS信号发生器 (内涵DDS与CORDIC IP核详解)附源码

学习内容

简易DDS信号发生器的设计与验证,产生所需的正弦波、方波锯齿波,并进行verilog验证。

实现功能

可以通过按键控制来输出不同的波形。

开发环境

xilinx spartan6开发板、ISE14.7、modelsim10.5、verilog

DDS相关知识

DDS 是直接数字式频率合成器(Direct Digital Synthesizer)的英文缩写,是一项关键的数字化技术。与传统的频率合成器相比,DDS 具有低成本、低功耗、高分辨率和快速转换时间等优点,广泛使用在电信与电子仪器领域,一般用它产生正弦、锯齿、方波等不同波形或不同频率的信号波形。
DDS 的基本结构主要由相位累加器相位调制器波形数据表 ROMD/A 转换器等四大结构组成,其中较多设计还会在数模转换器之后增加一个低通滤波器。
其中波形数据表ROM既可以通过MATLAB进行生成,也可以利用CORDIC算法来实现。本文采用调用CORDIC IP核来实现正弦波,通过verilog编程实现方波和三角波。
FPGA--简易DDS信号发生器 (内涵DDS与CORDIC IP核详解)附源码

DDS结构示意图(M、N表示位宽)

系统时钟 CLK 为整个系统的工作时钟,频率为 fCLK;频率字输入 F_WORD,一般为整数,数值大小控制输出信号的频率大小,数值越大输出信号频率越高,反之,输出信号频率越低,后文中用 K 表示;相位字输入P_WORD,为整数,数值大小控制输出信号的相位偏移,主要用于相位的信号调制,后文用 P 表示;设输出信号为 CLK_OUT,频率为 fOUT。公式为
K = 2 N ∗ f O U T f C L K K = {2^N}*\frac{{{{\rm{f}}_{OUT}}}}{{{{\rm{f}}_{CLK}}}} K=2N∗fCLK​fOUT​​
在这里简单介绍一下DDS原理公式:

根据傅立叶变换定理可知,任何周期信号都可以分解为一系列正弦或余弦信号之和,不失一般性,以正弦信号的产生为例详细说明直接数字频率合成技术的原理。比如一个频率为fc的正弦信号,其时域表达式为:
FPGA--简易DDS信号发生器 (内涵DDS与CORDIC IP核详解)附源码
其相位表达式为:
FPGA--简易DDS信号发生器 (内涵DDS与CORDIC IP核详解)附源码
从两式可以看出,正弦信号是关于相位的一个周期函数,下图更加直观的描述相位与幅度的关系,16个相位与16个幅度值相对应,即每一个相位值对应一个幅度值,比如1100对应的相位为3π/2,对应的幅度值为-1.
FPGA--简易DDS信号发生器 (内涵DDS与CORDIC IP核详解)附源码
相位和幅值的一一对应关系就好比存储器中地址和存储内容的关系,如果把一个周期内每个相位对应的幅度值存入存储器当中,那么对于任意频率的正弦信号,在任意时刻,只要已知相位Φ(t),也就知道地址,就可通过查表得到s(t)。
相位累加器在每个时钟脉冲输入时,把频率控制字累加一次,相位累加器的输出数据就是信号的相位,用输出的数据作为波形存储器(ROM)的相位取样地址,这样就可以把存取在波形存储器内的波形抽样值经查找表查处,完成相位到幅值的转换。频率控制字相当于Φ(t)中的2πfc,相位控制字相当于Φ(t)中的 θ 0 。 {\theta _{\rm{0}}}。 θ0​。
由于相位累加器字长的限制,相位累加器累加到一定值后,其输出将会溢出,这样波形存储器的地址就会循环一次,即意味着输出波形循环一周。故改变频率控制字即相位增量,就可以改变相位累加器的溢出时间,在时钟频率不变的条件下就可以改变输出频率。改变查表寻址的时钟频率,同样也可以改变输出波形的频率。于是对应上面的公式。
为了获得较高的频率分辨率,则只有增加相位累加器的字长N,故一般N都取值较大。但是受存储器容量的限制,存储器地址线的位数w不可能很大,一般都要小于N。这样存储器的地址线一般都只能接在相位累加器输出的高w位,而相位累加器输出余下的(N-W)个低位则只能被舍弃,这就是相位截断误差的来源
当K=1时输出信号存在频率最低,称为最小分辨率,根据采样定理K应小于 2 N 2 \frac{{{{\rm{2}}^N}}}{{\rm{2}}} 22N​。

举一个实例:

设:ROM 存储单元个数为 4096,每个存储数据用 8 位二进制表示。即,ROM 地址线宽度为 12,数据线宽度为 8;相位累加器位宽 N = 32。
根据上述条件可以知道,相位调制器位宽 M = 12(需要进行相位选择,因此其位宽与ROM地址位宽一致),那么根据 DDS 原理。那么在相位调制器中与相位控制字进行累加时,应用相位累加器的高 12 位累加。而相位累加器的低20 位只与频率控制字累加。我们以频率控制字 K = 1为例,相位累加器的低 20位一直会加 1,直到低 20位溢出向高 12 位进位,此时 ROM 地址为 0,也就是读了 2 20 {{\rm{2}}^{{\rm{20}}}} 220次时钟周期,ROM地址才变化,还要读4096个 2 20 {{\rm{2}}^{{\rm{20}}}} 220才会形成一个周期的波形,K=1即输出频率为 f O U T = f C L K 2 32 {{\rm{f}}_{OUT}}{\rm{ = }}\frac{{{{\rm{f}}_{CLK}}}}{{{{\rm{2}}^{{\rm{32}}}}}} fOUT​=232fCLK​​

当K变大10倍的话,相位累加器低20位溢出时间会变成原来的0.1倍,那么最终读完整个地址的时间也会变成原来的0.1倍,频率变成: f O U T = 10 ∗ f C L K 2 32 {{\rm{f}}_{OUT}}{\rm{ = }}\frac{{{\rm{10*}}{{\rm{f}}_{CLK}}}}{{{{\rm{2}}^{{\rm{32}}}}}} fOUT​=23210∗fCLK​​
tip:这个例子是存在相位截断误差的。

CORDIC IP核相关知识

CORDIC(Coordinate Rotation Digital Computer)算法即坐标旋转数字计算方法,是J.D.Volder1于1959年首次提出,主要用于三角函数、双曲线、指数、对数的计算。该算法通过基本的加和移位运算代替乘法运算,使得矢量的旋转和定向的计算不再需要三角函数、乘法、开方、反三角、指数等函数。
FPGA是不擅长复杂的数学运算的,不支持浮点数,所以很多操作收到了限制,而cordic可以熟练实现各种比较复杂的数学计算,正好与我们的verilog语言互补。

计算正余弦(sin/cos)原理

cordic算法将正余弦计算转换为简单的迭代过程(一系列的加减和移位操作), 非常适合硬件实现,是对正余弦等数学计算的逼近。

详情请看我的另外一篇文章详细介绍CORDIC 算法

代码详解

RTL视图
FPGA--简易DDS信号发生器 (内涵DDS与CORDIC IP核详解)附源码
顶层模块sp.v如下,包含DA模块和波形控制模块

module sp6(
			input ext_clk_25m,	//外部输入25MHz时钟信号
			input ext_rst_n,	//外部输入复位信号,低电平有效
			input[3:0] switch,	//拨码开关SW3输入,ON -- 低电平;OFF -- 高电平					
			output dac_iic_sck,		//DAC5571的IIC接口SCL
			inout dac_iic_sda		//DAC5571的IIC接口SDA
		);													

//-------------------------------------
//PLL例化
wire clk_12m5;	//PLL输出12.5MHz时钟
wire clk_25m;	//PLL输出25MHz时钟
wire clk_50m;	//PLL输出50MHz时钟
wire clk_100m;	//PLL输出100MHz时钟
wire sys_rst_n;	//PLL输出的locked信号,作为FPGA内部的复位信号,低电平复位,高电平正常工作

  pll_controller uut_pll_controller
   (// Clock in ports
    .CLK_IN1(ext_clk_25m),      // IN
    // Clock out ports
    .CLK_OUT1(clk_12m5),     // OUT
    .CLK_OUT2(clk_25m),     // OUT
    .CLK_OUT3(clk_50m),     // OUT
    .CLK_OUT4(clk_100m),     // OUT
    // Status and control signals
    .RESET(~ext_rst_n),// IN
    .LOCKED(sys_rst_n));      // OUT		
	
		
//-------------------------------------
//波形数据产生
wire[7:0] dac_data;	//DAC输出数据,模块内部自动判断该数据是否发生变化,若前后有变化,则通过IIC接口发起一次DAC转换数据写入操作,建议该数据变化速率不要超过1.5KHz

wave_controller		uut_wave_controller(
						.clk(clk_25m),		//时钟信号
						.rst_n(sys_rst_n),	//复位信号,低电平有效
						.wave_set(switch),	//波形设置信号,高电平有效,bit3--输出高电平波形,bit2--方波,bit1--三角波,bit0--正弦波
						.dac_data(dac_data)	//DAC转换数据		
					);

//-------------------------------------
//DAC5571的IIC写DA转换数据模块

dac_controller		uut_dac_controller(
						.clk(clk_25m),		//时钟信号
						.rst_n(sys_rst_n),	//复位信号,低电平有效
						.dac_data(dac_data),	//DAC输出数据,模块内部自动判断该数据是否发生变化,若前后有变化,则通过IIC接口发起一次DAC转换数据写入操作,建议该数据变化速率不要超过1.5KHz
						.scl(dac_iic_sck),		//DAC5571的IIC接口SCL
						.sda(dac_iic_sda)		//DAC5571的IIC接口SDA	
					);

					
endmodule

波形控制模块wave_controller.v如下,其中正弦波信号是由CORDIC IP核产生的,方波和三角波都通过一定的逻辑进行产生。

module wave_controller(
			input clk,		//时钟信号,25MHz
			input rst_n,	//复位信号,低电平有效
			input[3:0] wave_set,	//波形设置信号,低电平有效,bit3--输出高电平波形,bit2--方波,bit1--三角波,bit0--正弦波
			output reg[7:0] dac_data	//DAC输出数据,模块内部自动判断该数据是否发生变化,若前后有变化,则通过IIC接口发起一次DAC转换数据写入操作,建议该数据变化速率不要超过1.5KHz	
		);
		
//-------------------------------------------------		
//1Hz正弦波生成
wire[11:0] sin_out;	//sin输出值:bit11-9为有符号整数部分,bit8-0为小数部分;输出值范围-1~1,即12’hc00~12'h3ff
reg[11:0] sin_tmp;	
wire[7:0] sin_wave;	//8bit DAC输出的正弦波
	
sin_controller 		uut_sin_controller (
						.clk(clk), 
						.rst_n(rst_n), 
						.sin_out(sin_out)
					);	

always @(posedge clk or negedge rst_n)
	if(!rst_n) sin_tmp <= 12'd0;
	else if((sin_out >= 12'hc00) || (sin_out < 12'h400)) sin_tmp <= sin_out + 12'h400;

assign sin_wave = sin_tmp[10:3];	
	
//-------------------------------------------------		
//1Hz三角波生成		
reg[15:0] tcnt;	//2ms计数器
reg[8:0] triangle_tmp;
reg[7:0] triangle_wave;	//三角波数据

	//1s定时
always @(posedge clk or negedge rst_n)
	if(!rst_n) tcnt <= 16'd0;
	else if(tcnt < 16'd48827) tcnt <= tcnt+1'b1;
	else tcnt <= 16'd0;		

	//512个点计数
always @(posedge clk or negedge rst_n)
	if(!rst_n) triangle_tmp <= 9'd0;
	else if(tcnt == 16'd48827) triangle_tmp <= triangle_tmp+1'b1;
	
	//三角波数据产生
always @(posedge clk or negedge rst_n)
	if(!rst_n) triangle_wave <= 8'd0;
	else if(triangle_tmp < 9'd256) triangle_wave <= triangle_tmp[7:0]; 
	else triangle_wave <= ~triangle_tmp[7:0];
	
//-------------------------------------------------		
//1Hz方波生成		
reg[24:0] scnt;	//1s计数器
reg[7:0] square_wave;	//方波数据

	//1s定时
always @(posedge clk or negedge rst_n)
	if(!rst_n) scnt <= 25'd0;
	else if(scnt < 25'd24_999_999) scnt <= scnt+1'b1;
	else scnt <= 25'd0;		

	//1000个点波形产生
always @(posedge clk or negedge rst_n)
	if(!rst_n) square_wave <= 8'h00;
	else if(scnt < 25'd12_500_000) square_wave <= 8'h00;
	else square_wave <= 8'hff;

//-------------------------------------------------
//输出波形选择

always @(posedge clk or negedge rst_n)
	if(!rst_n) dac_data <= 8'd0;
	else if(!wave_set[3]) dac_data <= 8'hff;
	else if(!wave_set[2]) dac_data <= square_wave;
	else if(!wave_set[1]) dac_data <= triangle_wave;
	else if(!wave_set[0]) dac_data <= sin_wave;
	else dac_data <= 8'd0;
	
	
endmodule

DA模块dac_controller.v如下,关于DA模块的讲解,可以看我另外一篇关于DA的文章。

module dac_controller(
			input clk,		//时钟信号,25MHz
			input rst_n,	//复位信号,低电平有效
			input[7:0] dac_data,	//DAC输出数据,模块内部自动判断该数据是否发生变化,若前后有变化,则通过IIC接口发起一次DAC转换数据写入操作,建议该数据变化速率不要超过1.5KHz
			output scl,		//DAC5571的IIC接口SCL
			inout sda		//DAC5571的IIC接口SDA	
		);

//-------------------------------------------------	
//判断DAC输出数据是否变化,若变化则发起一次IIC数据写入操作					
reg[7:0] dac_datar;		//dac_data缓存寄存器
reg dac_en;		//DAC转换使能信号,高电平有效															

always @(posedge clk or negedge rst_n)										
	if(!rst_n) dac_datar <= 8'd0;
	else dac_datar <= dac_data;

always @(posedge clk or negedge rst_n)										
	if(!rst_n) dac_en <= 1'b0;
	else if(dac_datar != dac_data) dac_en <= 1'b1;
	else dac_en <= 1'b0;

//-------------------------------------------------						
reg[8:0] cnti;		//计数器,25MHz时钟频率下,产生5KHz的IIC时钟															

always @(posedge clk or negedge rst_n)										
	if(!rst_n) cnti <= 9'd0;
	else if(cnti < 9'd499 && cstate != IDLE) cnti <= cnti + 1'b1;
	else cnti <= 9'd0;

wire scl_low = (cnti == 9'd374);											
wire scl_high = (cnti == 9'd124);											

assign scl = ~cnti[8];														

//-------------------------------------------------
//IIC写操作状态机
parameter IDLE		= 4'd0;
parameter START		= 4'd1;
parameter ADDR		= 4'd2;
parameter ACK1		= 4'd3;
parameter CMSB		= 4'd4;
parameter ACK2		= 4'd5;
parameter LSBI		= 4'd6;
parameter ACK3		= 4'd7;
parameter ACK4		= 4'd8;
parameter STOP		= 4'd9;

parameter DEVICE_ADDR	= 8'b1001_1000;										
wire[7:0] dac_mdata = {4'b0000,dac_data[7:4]};	
wire[7:0] dac_ldata = {dac_data[3:0],4'b0000};	

reg[3:0] cstate,nstate;	
reg sdar;	
reg[2:0] bcnt;																
reg sdlink;																	

always @(posedge clk or negedge rst_n)
	if(!rst_n) cstate <= IDLE;
	else cstate <= nstate;
	
always @(cstate or dac_en or scl_high or scl_low or bcnt) begin				
	case(cstate)
		IDLE: 	if(dac_en) nstate <= START;
				else nstate <= IDLE;
		START:  if(scl_high) nstate <= ADDR;
				else nstate <= START;
		ADDR: 	if(scl_low && bcnt == 3'd0) nstate <= ACK1;
				else nstate <= ADDR;
		ACK1: 	if(scl_low) nstate <= CMSB;
				else nstate <= ACK1;
		CMSB: 	if(scl_low && bcnt == 3'd0) nstate <= ACK2;
				else nstate <= CMSB;
		ACK2: 	if(scl_low) nstate <= LSBI;
				else nstate <= ACK2;
		LSBI:	if(scl_low && bcnt == 3'd0) nstate <= ACK3;
				else nstate <= LSBI;
		ACK3: 	if(scl_low) nstate <= ACK4;
				else nstate <= ACK3;
		ACK4: 	if(scl_low) nstate <= STOP;
				else nstate <= ACK4;				
		STOP: 	if(scl_high) nstate <= IDLE;
				else nstate <= STOP;
		default: nstate <= IDLE;
		endcase
end

always @(posedge clk or negedge rst_n)										
	if(!rst_n) begin
			sdar <= 1'b1;
			sdlink <= 1'b1;
		end
	else begin
		case(cstate)
			IDLE: begin
					sdar <= 1'b1;
					sdlink <= 1'b1;	
				end
			START: 	if(scl_high) begin
					sdar <= 1'b0;
					sdlink <= 1'b1;	
				end			
			ADDR: if(scl_low) begin
					sdar <= DEVICE_ADDR[bcnt];	
					sdlink <= 1'b1;	
				end					
			CMSB: if(scl_low) begin 
					sdar <= dac_mdata[bcnt];	
					sdlink <= 1'b1;	
				end
			LSBI: if(scl_low) begin
					sdar <= dac_ldata[bcnt];	
					sdlink <= 1'b1;	
				end
			ACK1,ACK2,ACK3: if(scl_low) begin
					sdar <= 1'b0;
					sdlink <= 1'b0;	
				end
			ACK4: if(scl_low) begin
					sdar <= 1'b0;
					sdlink <= 1'b1;	
				end
			STOP: if(scl_high) begin 
					sdar <= 1'b1;
					sdlink <= 1'b1;	
				end
			default: ;
			endcase
		end
	
assign sda = sdlink ? sdar : 1'bz;											

always @(posedge clk or negedge rst_n)										
	if(!rst_n) bcnt <= 3'd0;
	else begin
		case(cstate)
			ADDR,CMSB,LSBI:	begin
					if(scl_low) bcnt <= bcnt-1'b1;
					else ;
				end
			default: bcnt <= 3'd7;
			endcase
		end

endmodule

原文链接:https://blog.csdn.net/u014586651/article/details/88830115

上一篇:1、记录一个分布式延时订单收货自动处理方案


下一篇:0.A1-Air302(NB-IOT)-硬件使用说明,下载和运行第一个lua程序(Mini板)