基于DDS的任意波形、信号发生器设计


实验任务

实验目的

前面章节我们学习了旋转编码器的工作原理及驱动方法,本实验主要学习DDS技术的原理及实现,IP核rom模块的例化使用,串行DAC芯片DAC081S101的驱动设计。

设计框图

根据前面的实验解析我们可以得知,该设计总体可以拆分成如下功能模块实现,

Top-Down层次设计 模块结构设计

实验原理

DAC及DDS介绍

上一节我们学习了ADC的相关知识,DACADC功能相反,DAC是数字模拟转换器(英语:Digital to analog converter,英文缩写:DAC)是一种将数字信号转换为模拟信号(以电流、电压或电荷的形式)的设备。在很多数字系统中(例如计算机),信号以数字方式存储和传输,而数字模拟转换器可以将这样的信号转换为模拟信号,从而使得它们能够被外界(人或其他非数字系统)识别。

DAC模型

上图两个都是8位DAC模型,转换精度为 2的8次方等于256,即将Vref分成256份,DAC转换输出模拟电压最小步进为Vref / 256,模拟电压 Vout = N * Vref / 256 。

DDS是直接数字式频率合成器(Direct Digital Synthesizer)的英文缩写。与传统的频率合成器相比,DDS具有低成本、低功耗、高分辨率和快速转换时间等优点,广泛使用在电信与电子仪器领域,是实现设备全数字化的一个关键技术。DDS是一种全数字化的频率合成器,由相位累加器、波形ROM、D/A转换器和低通滤波器构成。时钟频率给定后,输出信号的频率取决于频率控制字,频率分辨率取决于累加器位数,相位分辨率取决于ROM的地址线位数,幅度量化噪声取决于ROM的数据位字长和D/A转换器位数。

DDS框图

DAC模块电路连接

这里我们以STEP BaseBoard V3.0底板上的DAC模块电路,其电路图如下:

DAC模块电路

DAC模块电路所示,FPGA直接连接DAC081S101芯片的控制端,ADC有6个管脚,2脚VCC为VCC和Vref功能复用,即VCC = Vref。DAC后端是运放电路LMV721,运放模块为电压跟随电路,再往后端射频端子输出。

DAC模块驱动设计

前面我们了解DAC081S101芯片和FPGA之间连接有三根线(sync、clk、din),兼容三线SPI总线,关于SPI串行总线上节已详细介绍,这里我们直接查看DAC081S101的芯片手册。

DAC081S101管脚说明表:

DAC081S101管脚说明表

注:Din信号在SCLK的节拍下传输数据,当SCLK下降沿时Din数据被锁存到移位寄存器,所以FPGA控制在上升沿更新Din数据。

DAC081S101串行通信时序如下图:

DAC081S101串行通信时序

注:SCLK空闲时为低电平,CPOL = 0,上升沿(第二个边沿)采样,CPHA = 1,如果例化通用SPI核完成设计,需要采用SPI的第二种工作模式。

DAC081S101输入寄存器

注: 16个时钟完成一次DAC转换,传输的16位数据,最高2位为无效数据,次2位为模式口控制数据,再次8位为DAC有效数据(DB7~DB0),最低4位为无效数据。

针对DAC081S101时序,我们用Verilog设计一个计数器,当计数器值不同时完成不同操作,实现一次DAC转换,程序实现如下:

reg [7:0] cnt;
always @(posedge clk or negedge rst_n)
	if(!rst_n) cnt <= 1'b0;
	else if(cnt >= 8'd34) cnt <= 1'b0;
	else cnt <= cnt + 1'b1;
 
reg [7:0] data;
always @(posedge clk or negedge rst_n)
	if(!rst_n) begin
		dac_sync <= HIGH; dac_clk <= LOW; dac_dat <= LOW;
	end else case(cnt)
		8'd0 : begin dac_sync <= HIGH; dac_clk <= LOW; data <= dac_data; end
		8'd1,8'd3,8'd5,8'd7,8'd9,8'd11,8'd13,8'd15,
		8'd17,8'd19,8'd21,8'd23,8'd25,8'd27,8'd29,8'd31,
		8'd33: begin dac_sync <= LOW; dac_clk <= LOW; end
		8'd2 : begin dac_sync <= LOW; dac_clk <= HIGH; dac_dat <= LOW; end //15
		8'd4 : begin dac_sync <= LOW; dac_clk <= HIGH; dac_dat <= LOW; end //14
		8'd6 : begin dac_sync <= LOW; dac_clk <= HIGH; dac_dat <= LOW; end //13
		8'd8 : begin dac_sync <= LOW; dac_clk <= HIGH; dac_dat <= LOW; end //12
		8'd10: begin dac_sync <= LOW; dac_clk <= HIGH; dac_dat <= data[7]; end //11
		8'd12: begin dac_sync <= LOW; dac_clk <= HIGH; dac_dat <= data[6]; end //10
		8'd14: begin dac_sync <= LOW; dac_clk <= HIGH; dac_dat <= data[5]; end //9
		8'd16: begin dac_sync <= LOW; dac_clk <= HIGH; dac_dat <= data[4]; end //8
		8'd18: begin dac_sync <= LOW; dac_clk <= HIGH; dac_dat <= data[3]; end //7
		8'd20: begin dac_sync <= LOW; dac_clk <= HIGH; dac_dat <= data[2]; end //6
		8'd22: begin dac_sync <= LOW; dac_clk <= HIGH; dac_dat <= data[1]; end //5
		8'd24: begin dac_sync <= LOW; dac_clk <= HIGH; dac_dat <= data[0]; end //4
		8'd26: begin dac_sync <= LOW; dac_clk <= HIGH; dac_dat <= LOW; end //3
		8'd28: begin dac_sync <= LOW; dac_clk <= HIGH; dac_done <= HIGH; end //2
		8'd30: begin dac_sync <= LOW; dac_clk <= HIGH; dac_done <= LOW; end //1
		8'd32: begin dac_sync <= LOW; dac_clk <= HIGH; end //0
		8'd34: begin dac_sync <= HIGH; dac_clk <= LOW; end
		default : begin dac_sync <= HIGH; dac_clk <= LOW;  end
	endcase

到这我们就完成了串行DAC芯片DAC081S101的驱动设计,整个采样周期用了35个系统时钟,如果我们采用12MHz时钟作为该模块系统时钟,转换率Fs = 12M / 35 = 342.86 Ksps,DAC主频Fsclk = 12 MHz / 2 = 6MHz,DAC081S101芯片手册Fsclk最高频率为30MHz,所以想要更高的转换率,可以将系统时钟的频率从12 MHz倍频到60 MHz。

模块接口如下:clk和rstn为系统时钟及复位,dacsync,dacclk和dacdat为DAC控制管脚,dacdata为DAC转换数据,dacdone脉冲对应一次DAC转换的完成

module DAC081S101_driver
(
input				clk,		//系统时钟
input				rst_n,  	//系统复位,低有效
output	reg			dac_done,	//DAC采样完成标志
input			[7:0]	dac_data,	//DAC采样数据
output	reg			dac_sync,	//SPI总线CS
output	reg			dac_clk,	//SPI总线SCLK
output	reg			dac_dat		//SPI总线MOSI
);
DDS设计实现

波表数据示意图

如上图所示,以8位DAC为例,将一个周期的正弦波分割成256份,得到256个相位波形量化数据,设计一个存储器,宽度为8,深度为256,将256个相位波形量化数据放入存储器中,我们就得到了一个正弦波波表。我们可以通过例化rom的IP核实现波表的设计,具体的操作如下:

第一步我们需要有波表数据初始化文件(.mif文件),方便再例化rom核时配置需要的初始化数据,得到波表数据初始化文件的方法很多,简单介绍两种:

1.在Quartus软件中,选择File → New → Memory Files → Memory Initialization File,配置存储深度和位宽,然后将Matlab或Excel等计算的波表数据填入,如下:

MIF文件创建

2.安装正弦波数据生成器(网上搜索下载)工具软件,配置相应的参数,直接导出生成初始化.mif文件。

第二步例化rom核,打开Tools菜单下的IP Catalog工具,依次找到Libraty → Basic Functions → On Chip Memory → ROM:1-PORT,配置存储器宽度和深度,选择刚刚生成的初始化文件,其他选项默认即可,点击Finish完成rom的IP核例化。

ROM ROM

第三步更改配置模式,因为例化了rom的IP核,需要初始化数据,所以需要更改配置模式。打开Assignments菜单下的Device选项,点击Device and Pin Opetions,找到Configuration → Configuration mode,下拉列表中选择最后一项,点击OK保存。

设置FPGA配置模式

打开rom核中的Verilog文件,端口声明如下:

input   [7:0]  address;
input          clock;
output  [7:0]  q;

其中clock为存储器时钟,address为存储器地址,q对应address地址中的数据输出,到这里我们的正弦波表就创建好了。

并行DAC应用框图

如果我们有上图电路的硬件,当FPGA按照时钟的节拍产生波形数据,就可以得到对应的模拟输出了,我们来看一个简单的DDS产生锯齿波例子:

module SimpleDDS(clk, dac_dat);
input clk;
output [7:0] dac_dat;
//24位相位累加器
reg [23:0] phase_acc;
always @(posedge clk) phase_acc <= phase_acc + 24'b1;
//相位地址
wire [23:0] phase = phase_acc;
assign dac_dat = phase[23:16];  //锯齿波
endmodule

程序中可以看到DDS模块在每个clk周期都会产生一个8位数据,并行DAC每个clk进行一次DAC转换,主频等于转换率,程序是成立的。对串行DAC(以DAC081S101为例),需要16个clk才能将DDS模块1次产生的8位数据转换,所以逻辑上,DDS模块中的clk端口信号应该跟DAC驱动模块的转换率同步,而不是和系统时钟或主频同步。

波形选择

上面程序中累加器phase_acc随时钟clk自加1,传输给并行DAC的数据同样自加,最后得到锯齿波输出,当clk为100MHz时,锯齿波频率为100MHz / 2^24 = 5.96Hz。

锯齿波输出:锯齿波

想要输出三角波,只需要把DAC数据输出赋值的语句改为:

assign dac_dat = phase[23]? ~phase[22:15]:phase[22:15];     //三角波

三角波输出:三角波

想要输出方波,只需要把DAC数据输出赋值的语句改为:

assign dac_dat = phase[23]? 8'hff : 8'h00;     //方波

方波输出:方波

想要输出正弦波,只需要把DAC数据输出赋值的语句改为波表存储器的例化:

rom u1
(
.clock		(clk		),
.address	(phase[23:16]	),
.q		(sin_dat	)
);

我们知道了各种波形的产生方法,我们可以同时产生多种波形信号,然后根据波形选择端口变量wave的值选择输出的波形数据,程序实现如下:

wire [7:0] sin_dat; //正弦波
wire [7:0] saw_dat = phase[23:16];  //锯齿波
wire [7:0] tri_dat = phase[23]? (~phase[22:15]) : phase[22:15]; //三角波
wire [7:0] squ_dat = phase[23]? 8'hff : 8'h00;  //方波
 
always @(*) begin
    case(wave)
        2'b00: dac_dat = sin_dat;   //正弦波
        2'b01: dac_dat = saw_dat;   //锯齿波
        2'b10: dac_dat = tri_dat;   //三角波
        2'b11: dac_dat = squ_dat;   //方波
        default: dac_dat = sin_dat; //正弦波
    endcase
end
 
rom u1
(
.clock		(clk		),
.address	(phase[23:16]	),
.q		(sin_dat	)
);

频率调节

上面简单的DDS设计中, 将相位累加器的控制改为:

always @(posedge clk) phase_acc <= phase_acc + 24'd2;

这样原来需要2^24个clk周期完成的相位增量,现在只需要2^23个clk周期,同样的相位增量花费时间减少一半,即模拟输出信号频率是原来的2倍,改变被加数就会改变频率,如果我们定义一个端口变量finc作为被加数,使用逻辑调节finc的值就可以调节频率,f_inc被称为频率控制字

always @(posedge clk) phase_acc <= phase_acc + f_inc;

相位调节

上面简单的DDS设计中, 将相位地址的控制改为:

wire [23:0] phase = phase_acc + 24'b1;

相位累加器每次加一个固定的偏移得到全新的相位地址,使波形数据整体上产生相位偏移,不改变输出的频率,如果我们定义一个端口变量pinc作为被加数,使用逻辑调节pinc的值就可以调节相位,pinc被称为相位控制字 <code verilog> wire [23:0] phase = phaseacc + p_inc; </code>

系统总体实现

底板上旋转编码器带有按键功能,我们使用按键消抖模块完成按键信号的消抖处理,前面章节我们还学习了旋转编码器旋转功能的驱动,刚刚讲到控制波形信号发生器的波形选择和频率输出其实就是控制两个端口参数wave和f_inc,所以从旋转编码器的驱动输出到DDS端口参数的控制,我们还需要一个逻辑模块。

按动旋转编码器的按键切换信号发生器的波形输出,依次为正弦波、锯齿波、三角波、方波,波形选择功能程序实现如下:

localparam  SIN = 2'b00, SAW = 2'b01, TRI = 2'b10, SQU = 2'b11;
//波形输出选择
always @(posedge clk or negedge rst_n) begin
    if(!rst_n) wave <= SIN;
    else if(O_pulse)begin
        case(wave)
            SIN: wave <= SAW;
            SAW: wave <= TRI;
            TRI: wave <= SQU;
            SQU: wave <= SIN;
            default: wave <= SIN;
        endcase
    end else wave <= wave;
end

转动旋转编码器调节信号发生器的输出频率,左旋(逆时针)频率减小,左旋(逆时针)频率增加,频率控制功能程序实现如下:

//频率控制
always@(posedge clk or negedge rst_n) begin
    if(!rst_n) f_inc <= 24'ha0000;
    else if(L_pulse) begin
        if(f_inc <= 24'h10000) f_inc <= f_inc;
        else f_inc <= f_inc - 24'h10000;
    end else if(R_pulse) begin
        if(f_inc >= 24'h140000) f_inc <= f_inc;
        else f_inc <= f_inc + 24'h10000;
    end else f_inc <= f_inc;
end

上电默认情况下,频率控制字f_inc的值为24’ha0000,那么在模拟信号周期内采样点n = 2^24 / 10 / 2^16 = 25.6。我们设计的DAC驱动转换率为342.86 Ksps,那么对应模拟信号频率 f = 342.86K / 25.6 = 13393Hz = 13.4KHz左右。

旋转编码器调节频率控制字的步进及频率控制字最小值都为24’h10000,对应频率步进值及输出模拟信号最小频率都等于1.34KHz(运算方法同上)。

将设计中所有模块例化连线,完成整体设计,再次强调:DDS模块的时钟信号与DAC模块中的转换率信号同步,所以DDS模块中的clk要和DAC081S101driver模块中的dacdone信号连接。

综合后的设计框图如下:

RTL设计框图

实验步骤

  1. 双击打开Quartus Prime工具软件;
  2. 新建工程:File → New Project Wizard(工程命名,工程目录选择,设备型号选择,EDA工具选择);
  3. 新建文件:File → New → Verilog HDL File,键入设计代码并保存;
  4. 设计综合:双击Tasks窗口页面下的Analysis & Synthesis对代码进行综合;
  5. 管脚约束:Assignments → Assignment Editor,根据项目需求分配管脚;
  6. 设计编译:双击Tasks窗口页面下的Compile Design对设计进行整体编译并生成配置文件;
  7. 程序烧录:点击Tools → Programmer打开配置工具,Program进行下载;
  8. 观察设计运行结果。

实验现象

将设计加载到FPGA中,使用示波器观察底板DAC射频端子信号输出,按动旋转编码器观察不同的波形输出,转动旋转编码器观察频率的变化。

默认输出

正弦波输出 锯齿波输出 三角波输出 方波输出