写在前面的话
DDS是直接数字式频率合成器(Direct Digital Synthesizer)的英文缩写。与传统的频率合成器相比,DDS具有低成本、低功耗、高分辨率和快速转换时间等优点,广泛使用在电信与电子仪器领域,是实现设备全数字化的一个关键技术。
项目需求
设计一个相位和频率可调的波形(正弦波)发生器。
项目分析
问题1:什么是波形发生器?
波形发生器是一种数据信号发生器,在调试硬件时,常常需要加入一些信号,以观察电路工作是否正常。加入的信号有:正弦波、三角波、方波和任意波形等等。
问题2:什么是相位可调?
相位(phase)是对于一个波,特定的时刻在它循环周期中的位置:一种它是否在波峰、波谷或它们之间的某点的标度。相位描述信号波形变化的度量,通常以度 (角度)作为单位,也称作相角。 当信号波形以周期的方式变化,波形循环一周即为360°。那么相位可调也可以简单的理解为:改变初始相位。
问题3:什么是频率可调?
频率,是单位时间内完成周期性变化的次数,是描述周期运动频繁程度的量,常用符号f或ν表示,单位为秒分之一,符号为s-1。频率可调也就是改变单位时间内完成周期性变化的次数。
系统架构
我们应该先把完整的波形数据放在rom里面,然后用一个控制器把rom里面的数据读出来,设计架构图如下:
模块功能介绍
模块名 |
功能描述 |
rom |
存储波形数据 |
control |
输出有效地址 |
dds |
负责顶层的连接 |
8.9.6端口和内部连线描述
顶层端口
端口名 |
端口说明 |
clk |
系统时钟输入 |
rst_n |
系统复位 |
num[7:0] |
波形数据输出 |
内部连线
连线名 |
连线说明 |
addr[7:0] |
有效地址 |
波形数据的由来
我们应用Mif_Maker2010(mif文件生成软件)来生成我们的波形数据:
第一步:打开Mif_Maker2010
第二步:设定波形为正弦波
第三步:设置全局参数
第四步:再一次设定波形为正弦波即可
保存mif文件。
我们将正弦波分成了256个点,每个点对应一个数据,生成mif文件。
波形发生器(不可调频和调相)
代码解释
control模块代码
/**************************************************** * Engineer : 梦翼师兄 * QQ : 761664056 * The module function: 输出有效地址 *****************************************************/ 00 module control ( 01 clk, //外部输入时钟 02 rst_n,//系统复位 03 addr//有效地址 04 ); 05 //模块输入 06 input clk;//外部输入时钟 07 input rst_n;//系统复位 08 //模块输出 09 output reg [7:0] addr;//有效地址 10 11 always @ (posedge clk or negedge rst_n) 12 begin 13 if (!rst_n) //复位的时候,addr清零 14 addr <= 0; 15 else 16 if (addr < 255) 17 addr <= addr + 1;//地址在0~255之间循环 18 else 19 addr <= 0; 20 end 21 22 endmodule |
16~19行控制地址在0~255 之间循环,以此读出ROM中所有波形数据。
dds模块
/**************************************************** * Engineer : 梦翼师兄 * QQ : 761664056 * E_mail : zxopenwjf@126.com * The module function: 负责顶层连接 *****************************************************/ 00 module dds ( 01 clk, //外部输入时钟 02 rst_n,//系统复位 03 num //波形数据输出 04 ); 05 //系统输入 06 input clk;//外部输入时钟 07 input rst_n;//系统复位 08 //系统输出 09 output [7:0] num;//波形数据输出 10 //定义中间连线 11 wire [7:0] addr;//有效地址 12 //实例化control 13 control control ( 14 .clk(clk), //外部输入时钟 15 .rst_n(rst_n), //系统复位 16 .addr(addr)//有效地址 17 ); 18 //调用ip核 19 rom rom_inst ( 20 .address ( addr ), 21 .clock ( clk ), 22 .q ( num ) 23 ); 24 25 endmodule |
dds_tb模块
/**************************************************** * Engineer : 梦翼师兄 * QQ : 761664056 * E_mail : zxopenwjf@126.com * The module function: 测试dds模块 *****************************************************/ 00 `timescale 1ns/1ps //定义时间单位和精度 01 02 module dds_tb; 03 //系统输入 04 reg clk;//外部输入时钟 05 reg rst_n;//系统复位 06 //系统输出 07 wire [7:0] num;//波形数据输出 08 09 initial begin 10 clk = 1; 11 rst_n = 0; 12 # 200.1 13 rst_n = 1; 14 end 15 16 always # 10 clk = ~clk;//50M的时钟 17 18 dds dds ( 19 .clk(clk), //外部输入时钟 20 .rst_n(rst_n), //系统复位 21 .num(num)//波形数据输出 22 ); 23 24 endmodule |
仿真分析
将num设置为无符号类型的数据。
将num设置成模拟的数据。
具体的参数如下:
经过以上几步,我们就可以看到想要的正弦波。
放大后,我们测量了正弦波的一些参数,初始相位0度(梦翼师兄自己定义的),频率是195.31KHz。那么这些参数都是那里来的呢?
正弦波在相位为0度的时候开始出波形,是因为我们的地址是从0递增到255的,而0地址的数据恰好就是正弦波相位为0度的数据,故而初始相位为0度。
我们所使用的时钟为50MHz,而正弦波被分成了256个点,我们来计算一下:
结果很显然,结果等于195312.5HZ,也就是195.31KHz。
波形发生器(不可调频,但可以调相)
经过上一小节的分析,改变地址的初值就可以改变初始的相位,下面我们来验证。
代码解释
control模块代码
/**************************************************** * Engineer : 梦翼师兄 * QQ : 761664056 * The module function: 输出有效地址 *****************************************************/ 00 module control ( 01 clk, //外部输入时钟 02 rst_n,//系统复位 03 addr//有效地址 04 ); 05 //模块输入 06 input clk;//外部输入时钟 07 input rst_n;//系统复位 08 //模块输出 09 output reg [7:0] addr;//有效地址 10 //定义相位控制字 11 parameter pword = 64; 12 13 always @ (posedge clk or negedge rst_n) 14 begin 15 if (!rst_n) //复位的时候,addr被赋值为相位控制字 16 addr <= pword; 17 else 18 if (addr < 255) 19 addr <= addr + 1;//地址在0~255之间循环 20 else 21 addr <= 0; 22 end 23 24 endmodule |
第11行,定义了相位控制字。这里采用了参数的形式,读者也可以直接采用数据输入的形式。
第16行,在复位的时候,我们给地址的初值是64。正弦波被分成了256个点,第64个点刚好对应的初始相位为90度。我们可以按照下面的公式来进行计算初始相位和地址初值的关系:
dds模块代码
/**************************************************** * Engineer : 梦翼师兄 * QQ : 761664056 * The module function: 负责顶层连接 *****************************************************/ 00 module dds ( 01 clk, //外部输入时钟 02 rst_n,//系统复位 03 num //波形数据输出 04 ); 05 //系统输入 06 input clk;//外部输入时钟 07 input rst_n;//系统复位 08 //系统输出 09 output [7:0] num;//波形数据输出 10 //定义相位控制字 11 parameter pword = 64; 12 //定义中间连线 13 wire [7:0] addr;//有效地址 14 //实例化control 15 control#(.pword(pword))//传递相位控制字 16 control ( 17 .clk(clk), //外部输入时钟 18 .rst_n(rst_n), //系统复位 19 .addr(addr)//有效地址 20 ); 21 //调用ip核 22 rom rom_inst ( 23 .address ( addr ), 24 .clock ( clk ), 25 .q ( num ) 26 ); 27 28 endmodule |
第15行,实例化control模块时,可以选择改变control模块内部的参数,而不需要去底层模块进行修改,这样就可以一级一级的传递参数下去。
dds_tb模块代码
/**************************************************** * Engineer : 梦翼师兄 * QQ : 761664056 * The module function: 测试dds模块 *****************************************************/ 00 `timescale 1ns/1ps //定义时间单位和精度 01 02 module dds_tb; 03 //系统输入 04 reg clk;//外部输入时钟 05 reg rst_n;//系统复位 06 //系统输出 07 wire [7:0] num;//波形数据输出 08 //定义相位控制字 09 parameter pword = 64; 10 11 initial begin 12 clk = 1; 13 rst_n = 0; 14 # 200.1 15 rst_n = 1; 16 end 17 18 always # 10 clk = ~clk;//50M的时钟 19 20 dds #(.pword(pword))//传递相位控制字 21 dds( 22 .clk(clk), //外部输入时钟 23 .rst_n(rst_n), //系统复位 24 .num(num)//波形数据输出 25 ); 26 27 endmodule |
第20行,在编写测试文件的时候,同样可以改变参数。
有兴趣的同学可以将底层的参数设置成和测试文件中不一样的数据,然后仿真对比一下,其结果都是最顶层的参数起作用。
仿真分析
经过设置以后,我们再一次测量频率和相位这两个参数。
初始相位变成了90度,但是频率还是195.31KHz。通过改变相位控制字,就可以改变相位,达到了可调相的功能。
波形发生器(可以调频,可以调相)
经过前面两小节的分析,我们了解到,如果采用本地时钟是50M的话,那么频率就是195.31KHZ,根据我们的公式:
设想:如果想要改变输出的频率,我们可以选择改变时钟或者输出点的个数!显而易见,我们的设计不能够时时刻刻去改变时钟的频率,那么想要输出别的频率,我们只能改变输出的点的个数,也就是改变有效地址的数量。之前的设计是每个时钟沿地址增加1,频率是195.31KHz;如果我们每个时钟沿地址增加2,根据公式,输出频率应该是390.62KHz。下面我们就来验证:
代码解释
control模块
/**************************************************** * Engineer : 梦翼师兄 * QQ : 761664056 * The module function: 输出有效地址 *****************************************************/ 00 module control ( 01 clk, //外部输入时钟 02 rst_n,//系统复位 03 addr//有效地址 04 ); 05 //模块输入 06 input clk;//外部输入时钟 07 input rst_n;//系统复位 08 //模块输出 09 output reg [7:0] addr;//有效地址 10 //定义相位控制字 11 parameter pword = 64; 12 //定义频率控制字 13 parameter fword = 2; 14 15 always @ (posedge clk or negedge rst_n) 16 begin 17 if (!rst_n) //复位的时候,addr清零 18 addr <= pword; 19 else 20 addr <= addr + fword;//地址在0~255之间循环 21 end 22 23 endmodule |
dds模块代码
/**************************************************** * Engineer : 梦翼师兄 * QQ : 761664056 * The module function: 负责顶层连接 *****************************************************/ 00 module dds ( 01 clk, //外部输入时钟 02 rst_n,//系统复位 03 num //波形数据输出 04 ); 05 //系统输入 06 input clk;//外部输入时钟 07 input rst_n;//系统复位 08 //系统输出 09 output [7:0] num;//波形数据输出 10 //定义相位控制字 11 parameter pword = 64; 12 //定义频率控制字 13 parameter fword = 3; 14 //定义中间连线 15 wire [7:0] addr;//有效地址 16 //实例化control 17 control#(.pword(pword),//传递相位控制字 18 .fword(fword)//传递频率控制字 19 ) 20 control ( 21 .clk(clk), //外部输入时钟 22 .rst_n(rst_n), //系统复位 23 .addr(addr)//有效地址 24 ); 25 //调用ip核 26 rom rom_inst ( 27 .address ( addr ), 28 .clock ( clk ), 29 .q ( num ) 30 ); 31 32 endmodule |
dds_tb模块代码
/**************************************************** * Engineer : 梦翼师兄 * QQ : 761664056 * The module function: 测试dds模块 *****************************************************/ 00 `timescale 1ns/1ps //定义时间单位和精度 01 02 module dds_tb; 03 //系统输入 04 reg clk;//外部输入时钟 05 reg rst_n;//系统复位 06 //系统输出 07 wire [7:0] num;//波形数据输出 08 //定义相位控制字 09 parameter pword = 64; 10 //定义频率控制字 11 parameter fword = 2; 12 13 initial begin 14 clk = 1; 15 rst_n = 0; 16 # 200.1 17 rst_n = 1; 18 end 19 20 always # 10 clk = ~clk;//50M的时钟 21 22 dds #(.pword(pword),//传递相位控制字 23 .fword(fword)//传递频率控制字 24 ) 25 dds ( 26 .clk(clk), //外部输入时钟 27 .rst_n(rst_n), //系统复位 28 .num(num)//波形数据输出 29 ); 30 31 endmodule |
仿真分析
查看波形的频率为390.62KHz,证明我们的设计和思路是正确的。
最终设计
上一小节的设计,只能设定195.31KHz的整数倍,那就失去了我们设计的意义。为了提高精度,我们可以定义一个位宽为N(N>8)的地址计数器,让地址计数器每次增加一定的值,然后把高八位当作有效地址输送给rom,这样的话,就实现了降低地址改变的频率,进而达到降低输出波形的频率。下面我们就分析一下,这样可不可以改变频率。
地址计数器的原理就是先将pword的值作为地址的初值,然后每来一个时钟,地址计数器的值就等于地址当前值加上频率控制字fword,如此循环。例如刚开始fword = 1(假设pword=0) ,那么第一个时钟周期地址计数器的输出就是1,第二个时钟周期输出的就是2,第三个时钟周期输出的就是3。再例如,我们的频率控制字fword 刚开始等于2,那么地址计数器输出的就依次是0,2,4.....也就是说频率控制字fword 越大,地址计数器的输出值间隔也就越大,那么我们假设地址计数器的输出是32位的,如果fword越大,那么地址计数值到2^32 的时间就越短。这样的话,我们来算一下:
我们使用的是50MHz的晶振,周期为20ns,假设fword为1,地址计数器的输出为N位的,那么每20ns,地址计数器加1,要加到2^N,需要20ns x 2^N 时间,这个时间就是输出一个完整信号的周期,那么我们可以知道,输出信号的频率为 Fout = Fclk /2^N,其中,Fclk为我们的晶振频率,再假如,fword = B 的时候,地址间隔提高 B 倍,因此计满一个周期的时间缩小了 B 倍,频率提高的 B 倍。综上所述,我们得出了输出信号的频率计算公式:
我们来计算一下:当B=1,F大约是0.012Hz。所以我们改变fword的值就基本实现了所有的低于最快频率以下所有的频率值(频率值只能是0.012的倍数,因为0.012太小了,所以基本可以实现所有的频率,若这个精度还是达不到要求的话,大家可以继续增大N的值,根据公式就可以得出最小精度,也可以根据最小精度计算N的值)。
我们取地址计数器的前八位,相当于把一个波形的相位分成了256个点,每个点对应一个数据,正好和我们的波形数据点的个数是一样的。
代码分析
control模块代码
/**************************************************** * Engineer : 梦翼师兄 * QQ : 761664056 * The module function: 输出有效地址 ***************************************************** 00 module control ( 01 clk, //外部输入时钟 02 rst_n,//系统复位 03 addr//有效地址 04 ); 05 //模块输入 06 input clk;//外部输入时钟 07 input rst_n;//系统复位 08 //模块输出 09 output [7:0] addr;//有效地址 10 //定义相位控制字 11 parameter pword = 128; 12 //定义频率控制字 13 parameter fword = 10; 14 //定义中间寄存器 15 reg [31:0] addr_num;//地址计数器 16 17 assign addr = addr_num[31:24];//地址计数器的 18 19 always @ (posedge clk or negedge rst_n) 20 begin 21 if (!rst_n) //复位的时候,addr清零 22 begin 23 addr_num[31:24] <= pword; 24 addr_num[23:0] <= 24'd0; 25 end 26 else 27 addr_num <= addr_num + fword;//地址在0~255之间循环 28 end 29 30 endmodule |
第11行,相位控制字是128,初始相位应该是180度。
第13行,频率控制字是10,波形的频率是0.12Hz。
dds模块代码
/**************************************************** * Engineer : 梦翼师兄 * QQ : 761664056 * The module function: 负责顶层连接 *****************************************************/ 00 module dds ( 01 clk, //外部输入时钟 02 rst_n,//系统复位 03 num //波形数据输出 04 ); 05 //系统输入 06 input clk;//外部输入时钟 07 input rst_n;//系统复位 08 //系统输出 09 output [7:0] num;//波形数据输出 10 //定义相位控制字 11 parameter pword = 64; 12 //定义频率控制字 13 parameter fword = 100000; 14 //定义中间连线 15 wire [7:0] addr;//有效地址 16 //实例化control 17 control#(.pword(pword),//传递相位控制字 18 .fword(fword)//传递频率控制字 19 ) 20 control ( 21 .clk(clk), //外部输入时钟 22 .rst_n(rst_n), //系统复位 23 .addr(addr)//有效地址 24 ); 25 //调用ip核 26 rom rom_inst ( 27 .address ( addr ), 28 .clock ( clk ), 29 .q ( num ) 30 ); 31 32 endmodule |
第11行,相位控制字是64,初始相位应该是90度。
第13行,频率控制字是100000,波形的频率是1200Hz。
dds_tb模块代码
/**************************************************** * Engineer : 梦翼师兄 * QQ : 761664056 * The module function: 测试dds模块 *****************************************************/ 00 `timescale 1ns/1ps //定义时间单位和精度 01 02 module dds_tb; 03 //系统输入 04 reg clk;//外部输入时钟 05 reg rst_n;//系统复位 06 //系统输出 07 wire [7:0] num;//波形数据输出 08 //定义相位控制字 09 parameter pword = 64; 10 //定义频率控制字 11 parameter fword = 10000; 12 13 initial begin 14 clk = 1; 15 rst_n = 0; 16 # 200.1 17 rst_n = 1; 18 end 19 20 always # 10 clk = ~clk;//50M的时钟 21 22 dds #(.pword(pword),//传递相位控制字 23 .fword(fword)//传递频率控制字 24 ) 25 dds ( 26 .clk(clk), //外部输入时钟 27 .rst_n(rst_n), //系统复位 28 .num(num)//波形数据输出 29 ); 30 31 endmodule |
第11行,相位控制字是64,初始相位应该是90度。
第13行,频率控制字是10000,波形的频率是120Hz。
仿真分析
经过测试输出波形的频率和相位,证明我们的设计可以调频和调相,同时也验证了,最顶层的参数才能起作用。
利用上述的设计我们完成了输出波形数据的调频和调相,然后将数据经过DA转换就可以实现真实模拟信号的输出了。将读出的数据通过DA转换时,一定要注意DA芯片的转换频率,读出数据的频率必须和DA的数据转换速度相匹配。