1、理论

        DDS 技术是一种全新的频率合成方法,其具有低成本、低功耗、高分辨率和快速转换时间等优点,对数字信号处理及其硬件实现有着很重要的作用。 DDS 的基本结构主要由相位累加器、相位调制器、波形数据表 ROM、D/A 转换器等四大结构组成,其中较多设计还会在数模转换器之后增加一个低通滤波器。DDS结构示意图见下:

        现在看这张图可能看不太懂,没事有个印象就行。

2、DDS实现

        接下来一步步实现一个正弦波发生器。

2.1、相位累加器

        首先使用一个上位机软件生成一个正弦波的MIF文件,我这里使用的是正点原子的上位机WaveToMif:

        位宽设置为8,深度设置为4096。这样就把一个正弦波形拆成了4096个数据,每个数据的大小就是当前位的值,然后我们再构建一个8位宽、深度为4096的ROM,把这个MIF文件存到ROM里面去,到时候再把他读取出来,就构成了一幅完整的正弦波。

        先写如下Verilog代码来输出一个正弦波:

module  dds
(
    input               sys_clk     ,   //系统时钟,50MHz
    input               sys_rst_n   ,   //复位信号,低电平有效

    output      [7:0]   data_out        //波形输出
);
//parameter定义
parameter   FREQ_CTRL  = 12'd1;			//相位累加器单次累加值

//reg 定义
reg	[11:0]	rom_addr	;				//ROM地址
reg [11:0]	fre_adder	;				//累加器

//相位累加器赋值
always@(posedge sys_clk or negedge sys_rst_n)begin
    if(sys_rst_n == 1'b0)
        fre_adder <=  12'd0;
    else
		fre_adder <=  fre_adder + FREQ_CTRL;	
end

//ROM读地址赋值
always@(posedge sys_clk or negedge sys_rst_n)begin
    if(sys_rst_n == 1'b0)
		rom_addr <= 12'd0;
	else
		rom_addr <= fre_adder[11:0];
end

//------------------------- 模块例化------------------------
//例化ROM模块
rom_dds	rom_dds_inst (
	.address 	( rom_addr )	,	//ROM读地址
	.clock 		( sys_clk )		,   //读时钟
	.rden 		( 1'b1 )		,   //读使能
	.q 			( data_out )        //读出波形数据
);
endmodule

        在这段代码中:

  • 第1个always块对定义的相位累加器进行了赋值,每个时钟周期(20ns)累加FREQ_CTRL(这里设置的1)
  • 第2个always块将相位累加器的值赋给了ROM的读取地址

        可以分析,ROM的值就是每个时钟周期增加1,一直到地址溢出(4096),然后重新开始循环,这样每隔20ns✖4096=81920ns就能输出一个周期的正弦波,该正弦波的频率为                    1 / 81920 ns = 12207Hz。

        使用如下Testbench进行仿真(仅仅是实例了该模块,后续仿真依然使用这个Testbench):

`timescale  1ns/1ns
module  tb_dds();

//wire  define
wire    [7:0]   data_out    ;

//reg   define
reg             sys_clk     ;
reg             sys_rst_n   ;

initial
    begin
        sys_clk     = 1'b0	;
        sys_rst_n   <= 1'b0	;
        #200;
        sys_rst_n   <= 1'b1	;
    end

always #10 sys_clk = ~sys_clk;

//------------- top_dds_inst -------------
dds	dds_inst
(
    .sys_clk     (sys_clk)		,   //系统时钟,50MHz
    .sys_rst_n   (sys_rst_n)	,   //复位信号,低电平有效

    .data_out    (data_out)    		//波形输出
);

endmodule

        仿真结果如下:输出了12.20KHZ频率的正弦波(这里的误差来源于不好精准放置参考线,下同)

        

        以上输出一个波形的正弦波可以推导出一些公式:20 ns * 4096 = 81920ns;20 ns是采样时钟Fclk的倒数1 / Fclk,81920 ns是输出时钟的周期,也是输出频率Fout的倒数1 / Fout,4096是相位累加器(12位宽:N)的最大值,所以有:1 / Fclk * 2 ^ N= 1 / Fout----公式(1)

        那么在这个频率的基础上怎么进行调节?这个波形是每个时钟周期ROM的读取地址+1,假设每隔两个时钟周期,才将ROM的地址+1,那么输出波形的周期不就变成之前的两倍,频率变为一半了嘛(6100Hz)!

        稍微更改Verilog文件:把相位累加器的位宽从12位改为13位,但是将相位累加器的值赋值给ROM地址的时候只使用高12位;这样相位累加器的低1位每隔2才会向高12位进位,高12位每隔两个时钟周期才会+1,从而实现了每隔两个时钟周期ROM的读取地址+1。

module  dds
(
    input               sys_clk     ,   //系统时钟,50MHz
    input               sys_rst_n   ,   //复位信号,低电平有效

    output      [7:0]   data_out        //波形输出
);
//parameter定义
parameter   FREQ_CTRL  = 12'd1;			//相位累加器单次累加值

//reg 定义
reg	[11:0]	rom_addr	;				//ROM地址
reg [12:0]	fre_adder	;				//累加器

//相位累加器赋值
always@(posedge sys_clk or negedge sys_rst_n)begin
    if(sys_rst_n == 1'b0)
        fre_adder <=  12'd0;
    else
		fre_adder <=  fre_adder + FREQ_CTRL;	
end

//ROM读地址赋值
always@(posedge sys_clk or negedge sys_rst_n)begin
    if(sys_rst_n == 1'b0)
		rom_addr <= 12'd0;
	else
		rom_addr <= fre_adder[12:1];
end

//------------------------- 模块例化------------------------
//例化ROM模块
rom_dds	rom_dds_inst (
	.address 	( rom_addr )	,	//ROM读地址
	.clock 		( sys_clk )		,   //读时钟
	.rden 		( 1'b1 )		,   //读使能
	.q 			( data_out )        //读出波形数据
);
endmodule

 

        使用modelsim进行联合仿真,结果如下:

 

        可以看到将相位累加器的位宽增加就可以将频率相应的缩小了。如果使用20位宽的相位累加器,并使用高12位(因为ROM地址位宽是12位)对ROM地址赋值,且每个时钟周期低8位累加1,那么低8位需要2的8次方(256)个时钟周期才会向高12位进位(高12位+1,ROM地址+1),那么输出信号的频率应该就是12位位宽相位累加器输出频率(12207Hz)的256分之一,即47.68Hz,更改Verilog,并进行仿真,仿真结果如下,与推论的一致:

module  dds
(
    input               sys_clk     ,   //系统时钟,50MHz
    input               sys_rst_n   ,   //复位信号,低电平有效

    output      [7:0]   data_out        //波形输出
);
//parameter定义
parameter   FREQ_CTRL  = 12'd1;			//相位累加器单次累加值

//reg 定义
reg	[11:0]	rom_addr	;				//ROM地址
reg [19:0]	fre_adder	;				//累加器

//相位累加器赋值
always@(posedge sys_clk or negedge sys_rst_n)begin
    if(sys_rst_n == 1'b0)
        fre_adder <=  12'd0;
    else
		fre_adder <=  fre_adder + FREQ_CTRL;	
end

//ROM读地址赋值
always@(posedge sys_clk or negedge sys_rst_n)begin
    if(sys_rst_n == 1'b0)
		rom_addr <= 12'd0;
	else
		rom_addr <= fre_adder[19:8];
end

//------------------------- 模块例化------------------------
//例化ROM模块
rom_dds	rom_dds_inst (
	.address 	( rom_addr )	,	//ROM读地址
	.clock 		( sys_clk )		,   //读时钟
	.rden 		( 1'b1 )		,   //读使能
	.q 			( data_out )        //读出波形数据
);
endmodule

 

        到这里看似通过改变相位累加器的位宽从而实现了控制波形的输出频率,但是在实际模块调用中,相位累加器的位宽都是固定的----相当于位宽被BAN了,那该当如何?

        编写Verilog模块时候,每次相位累加器的值都是每个时钟周期+1,这个1还是用参数FREQ_CTRL 定义的,之所以用参数定义,就是为了方便更改。

        设想一下,一个20位宽的相位累加器,并使用高12位(因为ROM地址位宽是12位)对ROM地址赋值,如果每个时钟周期加的不是1,而是其他值(比如2的4次方16),那么低8位是不是只要2 ^ 4 = 16(2 ^ 8 / 2 ^ 4)就会溢出,向高12位进位?所以在这种情况下,输出频率应该是47.68Hz的16倍,即762.93Hz。

        照例更改代码,再进行仿真:

module  dds
(
    input               sys_clk     ,   //系统时钟,50MHz
    input               sys_rst_n   ,   //复位信号,低电平有效

    output      [7:0]   data_out        //波形输出
);
//parameter定义
parameter   FREQ_CTRL  = 12'd16;		//相位累加器单次累加值

//reg 定义
reg	[11:0]	rom_addr	;				//ROM地址
reg [19:0]	fre_adder	;				//累加器

//相位累加器赋值
always@(posedge sys_clk or negedge sys_rst_n)begin
    if(sys_rst_n == 1'b0)
        fre_adder <=  12'd0;
    else
		fre_adder <=  fre_adder + FREQ_CTRL;	
end

//ROM读地址赋值
always@(posedge sys_clk or negedge sys_rst_n)begin
    if(sys_rst_n == 1'b0)
		rom_addr <= 12'd0;
	else
		rom_addr <= fre_adder[19:8];
end

//------------------------- 模块例化------------------------
//例化ROM模块
rom_dds	rom_dds_inst (
	.address 	( rom_addr )	,	//ROM读地址
	.clock 		( sys_clk )		,   //读时钟
	.rden 		( 1'b1 )		,   //读使能
	.q 			( data_out )        //读出波形数据
);
endmodule

 

        这样在公式(1)----1 / Fclk * 2 ^ N = 1 / Fout的基础上,将频率控制字K加进去 ----1 / Fclk * 2 ^ N / K= 1 / Fout,即公式(2)----K = 2 ^ N * fOUT / fCLK  ,这样就得到了DDS的频率控制字的算法。

2.2、相位调制器

        从上面的分析可以知道,从ROM里面读取数据(地址从0到最大)就可以输出一个完整的波形,那么一个周期就是2的N次(N表示ROM地址位宽)个点,假如在读取ROM地址的时候赋一个值初值PHASE_CTRL,那么波形的相位就一定会发生变化。

        可以推出如下对应关系:一个周期 2π = 4096(2的位宽次方----2 ^ N,即2 ^ 12),那么 PHASE_CTRL = 相位偏移量 / 2π * 2 ^ N。

        在上条仿真的基础上,增加一个 π/2(90度)相位的偏移,可以算出PHASE_CTRL应该设置为 π / 2 / 2π * 2 ^ 12 = 1024。

        重新编写Verilog文件,并进行仿真:

module  dds
(
    input               sys_clk     ,   //系统时钟,50MHz
    input               sys_rst_n   ,   //复位信号,低电平有效

    output      [7:0]   data_out        //波形输出
);
//parameter定义
parameter   FREQ_CTRL  = 12'd16;		//相位累加器单次累加值
parameter   PHASE_CTRL  = 12'd1024;		//相位偏移量 

//reg 定义
reg	[11:0]	rom_addr	;				//ROM地址
reg [19:0]	fre_adder	;				//累加器

//相位累加器赋值
always@(posedge sys_clk or negedge sys_rst_n)begin
    if(sys_rst_n == 1'b0)
        fre_adder <=  12'd0;
    else
		fre_adder <=  fre_adder + FREQ_CTRL;	
end

//ROM读地址赋值
always@(posedge sys_clk or negedge sys_rst_n)begin
    if(sys_rst_n == 1'b0)
		rom_addr <= 12'd0;
	else
		rom_addr <= fre_adder[19:8] + PHASE_CTRL; //读取地址还要加上相位偏移量
end

//------------------------- 模块例化------------------------
//例化ROM模块
rom_dds	rom_dds_inst (
	.address 	( rom_addr )	,	//ROM读地址
	.clock 		( sys_clk )		,   //读时钟
	.rden 		( 1'b1 )		,   //读使能
	.q 			( data_out )        //读出波形数据
);
endmodule

        可以看到最新输出的波形频率仍然是764赫兹,但是初始相位与之前的相差了90度。

 3、总结

  • 通过改变频率控制字的大小,可以改变相位累加器的累加速度,间接控制ROM读取速度,从而控制输出波形频率;
  • 通过改变相位控制字的大小,可以改变相位调制器,间接控制ROM读取的初值,从而控制输出波形的初值;
  • 假设存储波形的ROM地址位宽为N,设定的相位累加器的位宽为M,采样时钟Fclk,想要输出的时钟为Fout,初始相位为θ,则有如下等式:
    • 频率控制字:K = 2^{M}*Fout/Fclk
    • 相位控制字:P = \Theta /2\pi *2^{N}

  • 📣您有任何问题,都可以在评论区和我交流📃!
  • 📣本文由 孤独的单刀 原创,首发于CSDN平台🐵,博客主页:wuzhikai.blog.csdn.net
  • 📣您的支持是我持续创作的最大动力!如果本文对您有帮助,还请多多点赞👍、评论💬和收藏⭐!

Logo

开放原子开发者工作坊旨在鼓励更多人参与开源活动,与志同道合的开发者们相互交流开发经验、分享开发心得、获取前沿技术趋势。工作坊有多种形式的开发者活动,如meetup、训练营等,主打技术交流,干货满满,真诚地邀请各位开发者共同参与!

更多推荐