简易DDS信号发生器的设计与验证,产生所需的正弦波、方波锯齿波,并进行verilog验证。
可以通过按键控制来输出不同的波形。
xilinx spartan6开发板、ISE14.7、modelsim10.5、verilog
DDS 是直接数字式频率合成器(Direct Digital Synthesizer)的英文缩写,是一项关键的数字化技术。与传统的频率合成器相比,DDS 具有低成本、低功耗、高分辨率和快速转换时间等优点,广泛使用在电信与电子仪器领域,一般用它产生正弦、锯齿、方波等不同波形或不同频率的信号波形。
DDS 的基本结构主要由相位累加器、相位调制器、波形数据表 ROM、D/A 转换器等四大结构组成,其中较多设计还会在数模转换器之后增加一个低通滤波器。
其中波形数据表ROM既可以通过MATLAB进行生成,也可以利用CORDIC算法来实现。本文采用调用CORDIC IP核来实现正弦波,通过verilog编程实现方波和三角波。
系统时钟 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∗fCLKfOUT
在这里简单介绍一下DDS原理公式:
根据傅立叶变换定理可知,任何周期信号都可以分解为一系列正弦或余弦信号之和,不失一般性,以正弦信号的产生为例详细说明直接数字频率合成技术的原理。比如一个频率为fc的正弦信号,其时域表达式为:
其相位表达式为:
从两式可以看出,正弦信号是关于相位的一个周期函数,下图更加直观的描述相位与幅度的关系,16个相位与16个幅度值相对应,即每一个相位值对应一个幅度值,比如1100对应的相位为3π/2,对应的幅度值为-1.
相位和幅值的一一对应关系就好比存储器中地址和存储内容的关系,如果把一个周期内每个相位对应的幅度值存入存储器当中,那么对于任意频率的正弦信号,在任意时刻,只要已知相位Φ(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(Coordinate Rotation Digital Computer)算法即坐标旋转数字计算方法,是J.D.Volder1于1959年首次提出,主要用于三角函数、双曲线、指数、对数的计算。该算法通过基本的加和移位运算代替乘法运算,使得矢量的旋转和定向的计算不再需要三角函数、乘法、开方、反三角、指数等函数。
FPGA是不擅长复杂的数学运算的,不支持浮点数,所以很多操作收到了限制,而cordic可以熟练实现各种比较复杂的数学计算,正好与我们的verilog语言互补。
cordic算法将正余弦计算转换为简单的迭代过程(一系列的加减和移位操作), 非常适合硬件实现,是对正余弦等数学计算的逼近。
RTL视图
顶层模块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