学习内容
简易DDS信号发生器的设计与验证,产生所需的正弦波、方波锯齿波,并进行verilog验证。
实现功能
可以通过按键控制来输出不同的波形。
开发环境
xilinx spartan6开发板、ISE14.7、modelsim10.5、verilog
DDS相关知识
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 IP核相关知识
CORDIC(Coordinate Rotation Digital Computer)算法即坐标旋转数字计算方法,是J.D.Volder1于1959年首次提出,主要用于三角函数、双曲线、指数、对数的计算。该算法通过基本的加和移位运算代替乘法运算,使得矢量的旋转和定向的计算不再需要三角函数、乘法、开方、反三角、指数等函数。
FPGA是不擅长复杂的数学运算的,不支持浮点数,所以很多操作收到了限制,而cordic可以熟练实现各种比较复杂的数学计算,正好与我们的verilog语言互补。
计算正余弦(sin/cos)原理
cordic算法将正余弦计算转换为简单的迭代过程(一系列的加减和移位操作), 非常适合硬件实现,是对正余弦等数学计算的逼近。
详情请看我的另外一篇文章详细介绍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