因此,在现代电子系统及设备的频率源设计中,尤其在通信领域,直接数字频率合成器的应用越来越广泛。作为设计人员,我们习惯称它为信号发生器,一般用它产生正弦、锯齿、方波等不同波形或不同频率的信号波形,在电子设计和测试中得到广泛应用。
本节实验任务是 使用 ALINX ZYNQ开发板及高速 AD-DA扩展模块AN108模块,实现数模及模数的转换。
高速AD 转换芯片和高速DA 转换芯片都是由ADI 公司生产的,分别是AD9280/3PA9280(两款芯片兼容)和AD9708。
AD9708 芯片输出的是一对差分电流信号,为了防止受到噪声干扰,电路中接入了低通滤波器,然后通过高性能和高带宽的运放电路,实现差分变单端以及幅度调节等功能,使整个电路性能得到了最大限度的提升,最终输出的模拟电压范围是 - 5V ~ +5V。
AD9280 芯片的输入模拟电压转换范围是0V~ 2V,所以电压输入端需要先经过电压衰减电路,使输入的 - 5V ~ +5V 之间的电压衰减到0V~2V 之间,然后经过AD9280 芯片将模拟电压信号转换成数字信号。
DAC原理通过权电阻进行举例
对于实际的DA芯片,显然要更加的复杂,但是原理基本一致
如硬件结构图所示,DA 电路由高速DA 芯片、7 阶巴特沃斯低通滤波器、幅度调节电路和信号输出接口组成。
我们使用的高速DA 芯片是AD 公司推出的AD9708。AD9708 是ADI 公司(Analog Devices,Inc.,亚德诺半导体技术有限公司)生产的TxDAC 系列数模转换器,具有高性能、低功耗的特点。AD9708 的数模转换位数为8 位,最大转换速度为125MSPS(每秒采样百万次Million Samples per Second)。内置1.2V 参考电压,差分电流输出。芯片内部结构图如下图所示
AD9708 在时钟(CLOCK)的驱动下工作,内部集成了+1.2V 参考电压(+1.20V REF)、运算放大器、电流源(CURRENT SOURCE ARRAY)和锁存器(LATCHES)。
两个电流输出端IOUTA 和IOUTB 为一对差分电流,当输入数据为0(DB7~ DB0=8’h00)时,IOUTA 的输出电流为0,而IOUTB 的输出电流达到最大,最大值的大小跟参考电压有关;
当输入数据全为高点平(DB7~DB0=8’hff)时,IOUTA 的输出电流达到最大,最大值的大小跟参考电压有关,而IOUTB 的输出电流为0。
AD9708 必须在时钟的驱动下才能把数据写入片内的锁存器中,其触发方式为上升沿触发,AD9708 的时序图如下图所示:
上图中的DBO-DB7 和CLOCK 是AD9708 的8 位输入数据和输入时钟,IOUTA 和IOUTB 为AD9708输出的电流信号。由上图可知,数据在时钟的上升沿锁存,因此我们可以在时钟的下降沿发送数据。
需要注意的是,CLOCK 的时钟频率越快,AD9708 的数模转换速度越快,AD9708 的时钟频率最快为125Mhz。
IOUTA 和IOUTB 为AD9708 输出的一对差分电流信号,通过外部电路低通滤波器与运放电路输出模拟电压信号,电压范围是-5V 至+5V 之间。
当输入数据等于0 时,AD9708 输出的电压值为5V;
当输入数据等于255(8’hff)时,AD9708 输出的电压值为-5V。
AD9708 芯片差分输出以后,为了防止噪声干扰,电路中接入了7 阶巴特沃斯低通滤波器,带宽为40MHz,频率响应如下图所示
滤波器参数如下图所示
滤波器之后,我们使用了2 片高性能145MHz 带宽的运放AD8056,实现差分变单端,以及幅度调节等功能,使整个电路性能得到了最大限度的提升。
幅度调节,使用的是5K 的电位器U6,最终的输出范围是-5V~5V(10Vpp)。
注:由于电路器的精度不是很精确,最终的输出有一定误差,有可能波形幅度不能达到10Vpp,也有可能出现波形削顶等问题,这些都属正常情况。
AD9708 是一款数字信号转模拟信号的器件,内部没有集成DDS(Direct Digital Synthesizer,直接数字式频率合成器)的功能,但是可以通过控制AD9708 的输入数据,使其模拟DDS 的功能。
例如,我们使用AD9708 输出一个正弦波模拟电压信号,那么我们只需要将AD9708 的输入数据按照正弦波的波形变化即可,下图为AD9708 的输入数据和输出电压值按照正弦波变化的波形图。
由上图可知,数据在0 至255 之间按照正弦波的波形变化,最终得到的电压也会按照正弦波波形变化,当输入数据重复按照正弦波的波形数据变化时,那么AD9708 就可以持续不断的输出正弦波的模拟电压波形。
需要注意的是,最终得到的AD9708 的输出电压变化范围由其外部电路决定的,当输入数据为0时,AD9708 输出+5V 的电压;当输入数据为255 时,AD9708 输出-5V 的电压。
由此可以看出,只要输入的数据控制的得当,AD9708 可以输出任意波形的模拟电压信号,包括正弦波、方波、锯齿波、三角波等 波形。
AD电路设计如图所示
如硬件结构图中所示,AD 电路由高速AD 芯片、衰减电路和信号输入接口组成。
AD9280是 ADI公司生产 的 一款单芯片、 8位、 32MSPS Million Samples Per Second,每秒采样百万次) 模数转换器,具有高性能 、低 功耗 的 特点。内部结构图如下图所示
AD9280在 时钟( CLK )的 驱动下工作, 用于控制所有内部转换的周期; AD9280内置片内采样保持放大器( SHA),同时采用多级差分流水线架构,保证了 32MSPS的数据转换速率下全温度范围内无失码;AD9280内部集成了可编程的基准源,根据系统需要也可以选择外部高精度基准满足系统的要求。
AD9280输出的数据以二进制格式表示,当输入的模拟电压超出量程时,会拉高 OTR out-of-range信号;当输入的模拟电压在量程范围内时, OTR信号为低电平,因此可以通过 OTR信号来判断输入的模拟电压是否在测量范围内。
AD9280的 时序图 如下 图所示:
模拟信号转换成数字信号并不是当前周期就能转换完成 从采集模拟信号开始到 输出 数据需要经过 3个 时钟周期。
比如上图 中 在 时钟 CLK的上升沿 沿采集的 模拟电压 信号 S1 经过 3个 时钟周期后 (实际上再加上 25ns的时间延时),输出 转换后的数据 DATA1。需要 注意的是, AD9280芯片 的最大转换速度是32MSPS,即 输入的时钟最大频率为 32MHz。
在信号进入AD 芯片之前,我们用一片AD8056 芯片构建了衰减电路,接口的输入范围是-5V~ +5V(10Vpp)。
衰减以后,输入范围满足AD 芯片的输入范围(0~2V)。转换公式如下:
当输入信号Vin=5(V)的时候,输入到AD 的信号Vad=2(V);
当输入信号Vin=-5(V)的时候,输入到AD 的信号Vad=0(V);
AD9280 支持输入的模拟电压范围是0V 至2V,0V 对应输出的数字信号为0,2V 对应输出的数字信号为255。而AD9708 经外部电路后,输出的电压范围是-5V~ +5V,因此在AD9280 的模拟输入端增加电压衰减电路,使-5V~+5V 之间的电压转换成0V 至2V 之间。
那么实际上对我们用户使用来说,得到的是AD9280输出的8bit数据
当AD9280的模拟输入接口连接-5V 电压时,AD 输出的数据为0;
当AD9280 的模拟输入接口连接+5V 电压时,AD输出的数据为255。
当AD9280 模拟输入端接-5V 至+5V 之间变化的正弦波电压信号时,其转换后的数据也是成正弦波波形变化,转换波形如下图所示:
由上图可知,输入的模拟电压范围在-5V 至5V 之间,按照正弦波波形变化,最终得到的数据也是按照正弦波波形变化。
根据本章的实验任务,FPGA需要连续输出正弦波波形的数据,才能使AD9708连续输出正弦波波形的模拟电压,如果通过编写代码使用三角函数公式运算的方式输出正弦波数据,那么程序设计会变得非常复杂。
在工程应用中,一般将正弦波波形数据存储在RAM或者ROM中,由于本次实验并不需要写数据到RAM中,因此我们将正弦波波形数据存储在只读的ROM中,直接读取ROM中的数据发送给DA转换芯片即可。
根据本章实验任务画出的系统框图。ROM里面事先存储好了正弦波波形的数据,DA数据发送模块从ROM中读取数据,将数据和时钟送到AD9708的输入数据端口和输入时钟端口;AD数据接收模块给AD9280输出驱动时钟信号和使能信号,并采集AD9280输出模数转换完成的数据。
FPGA 通过ROM IP 产生正弦波数据输出到DA 芯片进行DA 转换,产生正选波模拟信号,把模块的AD 和DA 端口连接起来就形成环路。
生成文件:软件支持将波形转换成COE Vivado软件支持的存储格式)和 MIF Quartus软件 支持的存储格式)格式文件,这里保持默认,即选中 COE文件格式。
然后点击“一键生成 ”按钮,在弹出的界面中选择 COE文件的存放路径并输入文件名,这里将 COE文件保存在工程的 doc文件夹下。
WaveToMem转换过程中的软件界面如下图所示:
使用Notepad++代码编辑器打开生成 的 COE文件后 如下图所示:
例化 了 ROM模块,由 Block Memory Generator IP核配置生成 。
工程中创建了一个单端口ROM,并命名为 rom_256x8b”,在调用 Block Memory Generator IP核时,“ Basic”选项也配置如下图所示
我们将其接口类型设置为“Native”、 Memory Type设置为“ Single Port ROM”,即单端口 ROM。
我们将PortA的位宽设置为 8,深度设置为 256,以存储上位机生成的 256个数据。此外,将使能引脚的类型设置为“ Always Enabled”,即 ROM一直处于使能的状态。
接下来配置“Other Options”选项页,加载刚才生成的 .coe文件,如下图所示:
最后点击“OK”按钮完成 IP核的配置。
例化了一个 ILA的 IP核,用于捕获 ad_otr和 ad_data的数据。
需要注意的是, ILA的采样时钟必须使用 ad_clk,否则数据可能采集错误。
ILA IP核的配置如下图所示:
我们把探针数量设置为2,并且把采样深度设置为 4096。探针宽度的设置如下图所示:
我们将两个探针的位宽设置成1和 8,分别对应 ad_otr和 ad_data的位宽,设置完成后点击“ OK”按钮即可。
FPGA顶层模块 hs_ad_da 例化 了 以下三个 模块: DA数据 发送模块 da_wave_send)、 ROM波形存储 模块( rom_256x8b)和 AD数据 接收模块 ad_wave_rec 。
DA数据 发送模块 da_wave_send DA数据 发送模块 输出 读 ROM地址, 将 输入的 ROM数据发送至 DA转换 芯片的数据 端口。
ROM波形 存储 模块( rom_256x8b ROM波形 存储 模块 由 Vivado软件自带的 Block Memory Generator IP核 实现, 其存储 的波形数据可以 使用 波形 转 存储文件的上位机来 生成 .coe文件。
AD数据 接收模块 ad_wave_rec AD数据 接收模块 输出 AD转换 芯片的驱动时钟和使能信号,随后 接收 AD转换 完成的数据。
`timescale 1ns / 1ps
//
// Company:
// Engineer:
//
// Create Date: 2022/02/25 16:37:25
// Design Name:
// Module Name: hs_adda
// Project Name:
// Target Devices:
// Tool Versions:
// Description:
//
// Dependencies:
//
// Revision:
// Revision 0.01 - File Created
// Additional Comments:
//
//
module hs_adda(
input sys_clk, // 系统时钟
input sys_rst_n, // 系统复位,低电平有效
// DA 芯片接口
output da_clk, // DA(AD9708)驱动时钟,最大支持125MHz时钟
output [7 : 0] da_data, // 输出给DA的数据
// AD 芯片接口
input [7 : 0] ad_data, // AD输入数据
// 模拟输入电压超出量程标识(本次实验未用到)
input ad_otr, // 0:在量程范围 1:超出量程
output ad_clk // AD(AD9280)驱动时钟,最大支持32MHz时钟
);
// wire define
wire [7 : 0] rd_addr; // ROM读地址
wire [7 : 0] rd_data; // ROM读出的数据
// *************************
// ** main code
// *************************
// DA数据发送
da_wave_send u_da_wave_send (
.clk (sys_clk),
.rst_n (sys_rst_n),
.rd_data(rd_data),
.rd_addr(rd_addr),
.da_clk (da_clk),
.da_data(da_data)
);
// ROM存储波形
rom_256x8b u_rom_256x8b (
.clka (sys_clk), // input wire clka
.addra (rd_addr), // input wire [7 : 0] addra
.douta (rd_data) // output wire [7 : 0] douta
);
// AD数据接收
ad_wave_rec u_ad_wave_rec (
.clk (sys_clk),
.rst_n (sys_rst_n),
.ad_data(ad_data),
.ad_otr (ad_otr),
.ad_clk (ad_clk)
);
// ILA采集AD数据
ila_0 u_ila_0 (
.clk(ad_clk), // input wire clk
.probe0(probe0), // input wire [0:0] probe0
.probe1(ad_data) // input wire [7:0] probe1
);
endmodule
DA数据 发送模块输出的读 ROM地址 rd_addr 连接 至 ROM模块 的 地址 输入端, ROM模块输出 的数据( rd_data 连接 至 DA数据 发送模块的 数据 输入端, 从 而完成了从 ROM中 读取数据的功能。
在代码的例化 了 ROM模块,由 Block Memory Generator IP核配置生成 。
代码的例化了一个 ILA的 IP核,用于捕获 ad_otr和 ad_data的数据。需要注意的是, ILA的采样时钟必须使用 ad_clk,否则数据可能采集错误。
`timescale 1ns / 1ps
//
// Company:
// Engineer:
//
// Create Date: 2022/02/27 16:01:20
// Design Name:
// Module Name: da_wave_send
// Project Name:
// Target Devices:
// Tool Versions:
// Description:
//
// Dependencies:
//
// Revision:
// Revision 0.01 - File Created
// Additional Comments:
//
//
module da_wave_send(
input clk, // 时钟
input rst_n, // 复位信号,低电平有效
input [7 : 0] rd_data, // ROM读出数据
output reg [7 : 0] rd_addr, // 读ROM地址
// DA 芯片接口
output da_clk, // DA(AD9708)驱动时钟,最大支持125MHz时钟
output [7 : 0] da_data // 输出给DA的数据
);
// parameter
// 频率调节控制
parameter FREQ_ADJ = 8'd5; // 频率调节,FREQ_ADJ的值越大,最终输出的频率越低,范围0~255
// reg define
reg [7 : 0] freq_cnt; // 频率计数器
// *****************************
// ** main code
// *****************************
// 数据rd_data是在clk的上升沿更新的,所以DA芯片在clk的下降沿锁频存数据是最稳定的时刻
// 而DA实际上在da_clk的上升沿锁存数据,所以时钟取反,这样clk的下降沿相当于da_clk的上升沿
assign da_clk = ~clk;
assign da_data = rd_data; // 将读到的ROM数据幅值给DA数据端口
// 频率调节计数器
always @(posedge clk or negedge rst_n) begin
if(rst_n == 1'b0)
freq_cnt <= 8'd0;
else if(freq_cnt == FREQ_ADJ)
freq_cnt <= 8'd0;
else
freq_cnt <= freq_cnt + 8'd1;
end
// 读ROM地址
always @(posedge clk or negedge rst_n) begin
if(rst_n == 1'b0)
rd_addr <= 8'd0;
else begin
if(freq_cnt == FREQ_ADJ) begin
rd_addr <= rd_addr + 8'd1;
end
end
end
endmodule
在代码的定义了一个参数 FREQ_ADJ(频率 调节 ),可以 通过控制 频率调节参数 的大小 来控制最终输出 正弦波的频率 大小, 频率 调节参数的值越小正弦波频率越大 。
频率调节参数调节正弦波频率的方法是通过控制读 ROM的速度实现的, 频率调节参数越小 freq_cnt计数到频率调节参数值的时间越短,读 ROM数据的速度越快,那么正弦波输出频率也就越高;反过来频率调节参数越大 freq_cnt计数到频率调节参数值的时间越长,读 ROM数据 的速度越慢 ,那么正弦波输出频率也就越低 。
由于 freq_cnt计数器 的位宽为 8位 ,计数范围是 0~ 255 所以 频率 调节 参数 FREQ_ADJ支持的调节 范围是 0~255 可通过修改 freq_cnt计数器 的位宽 来 修改 FREQ_ADJ支持的调节 范围 。
WaveToMem软件 设置 ROM深度 为 256 倍频系数为 1,而输入 时钟为 50Mhz,那么一个 完整的正弦波 周期 长度为 256*20ns = 5120ns 当 FREQ_ADJ的 值为 0时 ,即正弦波的最快输出频率为 1s/5120ns(1s = 1000000000ns) ≈ 195.3Khz。
当 我们 把 FREQ_ADJ的 值设置为 5时 一个 完整的正弦波 周期 长度 为5120ns*(5+1) = 30720ns,频率约 为 32.55KHz。 也可以在 WaveToMem软件设置中增加 倍频 系数 或者 增加AD的 驱动时钟来提高正弦波输出频率 。
`timescale 1ns / 1ps
//
// Company:
// Engineer:
//
// Create Date: 2022/02/27 17:09:28
// Design Name:
// Module Name: ad_wave_rec
// Project Name:
// Target Devices:
// Tool Versions:
// Description:
//
// Dependencies:
//
// Revision:
// Revision 0.01 - File Created
// Additional Comments:
//
//
module ad_wave_rec(
input clk, // 时钟
input rst_n, // 复位信号,低电平有效
input [7 : 0] ad_data, // AD输入数据
// 模拟输入电压超出量程标志(本次实验为用到)
input ad_otr, // 0:在量程范围 1:超出量程
output reg ad_clk // AD(AD9280)驱动时钟,最大支持32Mhz时钟
);
// *****************************
// ** main code
// *****************************
// 时钟分频(2分频,时钟频率为25MHz),产生AD时钟
always @(posedge clk or negedge rst_n) begin
if(rst_n == 1'b0)
ad_clk <= 1'b0;
else
ad_clk <= ~ad_clk;
end
endmodule
由于AD转换 芯片 支持 的最大时钟频率为 32Mhz,而 ZYNQ PL的 系统时钟频率为 50Mhz,所以需要先对时钟进行分频 ,将分频后的时钟作为AD转换芯片的驱动时钟 。时钟分频(2分频,时钟频率为25MHz),产生AD时钟。
需要说明的是,AD数据接收 模块 没有对输入的 ad_otr(输入的模拟电压超出量程指示)和 ad_dataAD输入的数据)做任何处理,这两个信号是在 ILA中观察信号的变化的。
############## clock and reset define##################
create_clock -period 20.000 [get_ports sys_clk]
set_property IOSTANDARD LVCMOS33 [get_ports sys_clk]
set_property PACKAGE_PIN U18 [get_ports sys_clk]
set_property IOSTANDARD LVCMOS33 [get_ports sys_rst_n]
set_property PACKAGE_PIN N15 [get_ports sys_rst_n]
########AN108 ON AX7020 and AX7010 J11##################
set_property PACKAGE_PIN F20 [get_ports da_clk]
set_property PACKAGE_PIN F19 [get_ports {da_data[7]}]
set_property PACKAGE_PIN G20 [get_ports {da_data[6]}]
set_property PACKAGE_PIN G19 [get_ports {da_data[5]}]
set_property PACKAGE_PIN H18 [get_ports {da_data[4]}]
set_property PACKAGE_PIN J18 [get_ports {da_data[3]}]
set_property PACKAGE_PIN L20 [get_ports {da_data[2]}]
set_property PACKAGE_PIN L19 [get_ports {da_data[1]}]
set_property PACKAGE_PIN M20 [get_ports {da_data[0]}]
set_property PACKAGE_PIN L17 [get_ports {ad_data[0]}]
set_property PACKAGE_PIN L16 [get_ports {ad_data[1]}]
set_property PACKAGE_PIN M18 [get_ports {ad_data[2]}]
set_property PACKAGE_PIN M17 [get_ports {ad_data[3]}]
set_property PACKAGE_PIN D20 [get_ports {ad_data[4]}]
set_property PACKAGE_PIN D19 [get_ports {ad_data[5]}]
set_property PACKAGE_PIN E19 [get_ports {ad_data[6]}]
set_property PACKAGE_PIN E18 [get_ports {ad_data[7]}]
set_property PACKAGE_PIN G18 [get_ports ad_clk]
set_property IOSTANDARD LVCMOS33 [get_ports da_clk]
set_property IOSTANDARD LVCMOS33 [get_ports {da_data[7]}]
set_property IOSTANDARD LVCMOS33 [get_ports {da_data[6]}]
set_property IOSTANDARD LVCMOS33 [get_ports {da_data[5]}]
set_property IOSTANDARD LVCMOS33 [get_ports {da_data[4]}]
set_property IOSTANDARD LVCMOS33 [get_ports {da_data[3]}]
set_property IOSTANDARD LVCMOS33 [get_ports {da_data[2]}]
set_property IOSTANDARD LVCMOS33 [get_ports {da_data[1]}]
set_property IOSTANDARD LVCMOS33 [get_ports {da_data[0]}]
set_property IOSTANDARD LVCMOS33 [get_ports {ad_data[0]}]
set_property IOSTANDARD LVCMOS33 [get_ports {ad_data[1]}]
set_property IOSTANDARD LVCMOS33 [get_ports {ad_data[2]}]
set_property IOSTANDARD LVCMOS33 [get_ports {ad_data[3]}]
set_property IOSTANDARD LVCMOS33 [get_ports {ad_data[4]}]
set_property IOSTANDARD LVCMOS33 [get_ports {ad_data[5]}]
set_property IOSTANDARD LVCMOS33 [get_ports {ad_data[6]}]
set_property IOSTANDARD LVCMOS33 [get_ports {ad_data[7]}]
set_property IOSTANDARD LVCMOS33 [get_ports ad_clk]
set_property IOSTANDARD LVCMOS33 [get_ports ad_otr]
set_property C_CLK_INPUT_FREQ_HZ 300000000 [get_debug_cores dbg_hub]
set_property C_ENABLE_CLK_DIVIDER false [get_debug_cores dbg_hub]
set_property C_USER_SCAN_CHAIN 1 [get_debug_cores dbg_hub]
connect_debug_port dbg_hub/clk [get_nets ad_clk_OBUF]
因为代码主要参考了正点原子,但AD108是黑金的,虽然大体大同小异,但是对于AD_OTR引脚,黑金的AN108没有引出,因此,这里悬空没有配置
ctrl+shift+p,输入Testbench,在命令行就会自动输出Testbench代码
//~ `New testbench
`timescale 1ns / 1ps
module tb_dds_ip;
// dds_ip Parameters
parameter PERIOD = 10;
// dds_ip Inputs
reg sys_clk = 0 ;
reg sys_rst_n = 0 ;
reg [7 : 0] ad_data = 0 ;
reg ad_otr = 0 ;
// dds_ip Outputs
wire da_clk ;
wire [7 : 0] da_data ;
wire ad_clk ;
initial
begin
forever #(PERIOD/2) sys_clk=~sys_clk;
end
initial
begin
#(PERIOD*2) sys_rst_n = 1;
end
dds_ip u_dds_ip (
.sys_clk ( sys_clk ),
.sys_rst_n ( sys_rst_n ),
.ad_data ( ad_data [7 : 0] ),
.ad_otr ( ad_otr ),
.da_clk ( da_clk ),
.da_data ( da_data [7 : 0] ),
.ad_clk ( ad_clk )
);
initial
begin
sys_clk = 0;
sys_rst_n = 0;
#20
sys_rst_n = 1;
$finish;
end
endmodule
连接AN108 的 DAC 输入到信号发生器的输出, 这里使用的是专用屏蔽线,如果使用其他线可能会有较大干扰 。
调节信号发生的频率和幅度, AN108 输入范围 5V 5V ,为了便于观察波形数据,建议信号输入频率 200Khz 到 1Mhz 。
将工程生成的比特流文件下载到 ZYNQ中后,然后使用 示波器测量 DA输出 通道的波形。
首先将示波器带 夹子的一 端连 接到开发板的 GND位置 可 使用杜邦线连接至开发板上的任一 的 GND管脚 )),然后 将另一端 探针插入高速 AD-DA模块 的 DA通道中间 的金属圆圈内
观察到正弦波波形后,说明DA 已经正确输出模拟电压波形了,接下来我们来验证AD 的功能
连接后在ILA中 观察 ad_data数据 的变化 观察到的波形如下图所示 。
由上图可知, 输入的 ad_data数据为正弦波变化的波形,说明 AD-DA实验验证成功。
DDS Direct Digital Synthesizer)即数字合成器,是一种新型的频率合成技术,具有低成本、低功耗、高分辨率、频率转换时间短、相位连续性好等优点,对数字信号处理及其硬件实现有着很重要的作用。
DDS的基本结构主要由相位累加器、相位调制器、波形数据表 ROM、 D/A转换器等四大结构组成,其中较多设计还会在数模转换器之后增加一个低通滤波器( LPF)。 DDS基本结构图如 图
其中相位累加器由 N位加法器与 N位寄存器构成。每来一个时钟,加法器就将频率控制字与累加寄存器输出的相位数据相加,相加的结果又反馈至累加寄存器的数据输入端,以使加法器在下一个时钟脉冲的作用下继续与频率控制字相加。这样,相位累加器在时钟的作用下,不断对频率控制字进行线性相位累加。即在每一个时钟脉冲输入时,相位累加器便把频率控制字累加一次。当相位累加器累加满量时就会产生一次溢出,完成一个周期的动作。
相位累加器输出的数据就是合成信号的相位。相位累加器的溢出频率,就是 DDS输出的信号频率。
通过改变相位控制字P_WORD可以控制输出信号的相位参数。
令相位加法器的字长为 M,当相位控制字由 0跃变为 P_WORD时,波形存储器( ROM)的输入为相位累加器的输出与相位控制字 P_WORD之和,因而其输出的幅度编码相位会增加 P_WORD/2^M,从而使输出的信号产生相移。
用相位调制器输出的数据,作为波形存储器的相位采样地址,这样就可以把存储在波形存储器里的波形采样值经查表找出,完后相位到幅度的转换。 N位的寻址 ROM相当于把 0 360 °的正弦信号离散成具有2N个样值的序列。若波形存储器中有 D位数据位,则 2^N个样值的幅值以 D位二进制数值固化在波形存储器当中。按照地址 的不同可以输出相应相位的正弦信号幅值。相位 幅度变换原理图如下图所示:
数模转换器(D/A)的作用是把合成的正弦波数字量转化为模拟量。正弦幅度量化序列经数模转换器转换后变成了包络为正弦波的阶梯波。频率合成器对数模转换器的分辨率有一定的要求,其分辨率越高,合成的正弦波台阶数就越多,输出的波形精度也就越高。DDS信号流程图如下图所示:
使用 ALINX ZYNQ 7020开发板及高速 AD-DA扩展模块 AN108模块)实现数模及模数的转换。
首先 ZYNQ PL端 产生正弦波 、方波、三角波、锯齿波 变化的数字信号, 经过 DA芯片后转换成模拟信号, 将 DA的 模拟 电压输出端 连接至 AD的 模拟 电压输入端 AD芯片 将 模拟信号转换成数字信号 通过按下按键 key0和 key1可以实现波形种类和波形频率的切换, 然后 通过示波器 观察 DA端模拟 信号的 波形 是否 出现了相应变化,也可以通过 Ila界面去进行观察 。
硬件设计与上述“高速AD/DA实验”完全相同,此处不在赘述。 主要不同就是根据实验要求添加了两个按键
根据本章的实验任务, ZYNQ PL端 需要 连续输出正弦波 、方波、三角波、锯齿波 波形 的数据,才能使AD9708连续输出 相应 波形的 模拟 电压 ,如果 使用三角函数公式运算的方式来编写代码,然后输出正弦波、方波、三角波、锯齿波的波形数据, 那么 程序设计会变得非常复杂 。
在工程 应用中一般将波形数据存储在RAM或者 ROM中,由于 本次实验并不需要 写 数据到 RAM中,因此我们 将波形数据存储在只读的 ROM中 ,直接读取 ROM中的数据 发送给DA转换芯片即可。
ROM里面 事先存储好了波形的数据, DA数据 发送模块 从 ROM中读取 数据,将数据 和 时钟送 到 AD9708的 输入数据端口 和 输入时钟端口; AD数据 接收 模块给AD9280输出 驱动 时钟 信号 和使能信号 ,并采集 AD9280输出模数 转换完成 的数据 。
FPGA顶层模块 dds 例化 了 以下 六 个 模块: 时钟模块( clk_wiz_0)、两个按键消抖模块 key_debounce)、DA数据 发送模块 da_wave_send)、 ROM波形 存储 模块( rom_400x8b)和 AD数据 接收模块 ad_wave_rec 。
时钟模块(clk_wiz_0)):为按键消抖模块、 DA数据发送模块、 ROM波形存储模块提供驱动时钟。
按键消抖模块(key_debounce):对按键信号延时采样,将消抖后的按键信号和按键数据有效信号输出至da_wave_send模块。
DA数据 发送模块 da_wave_send DA数据 发送模块 输出 读 ROM地址, 将 输入的 ROM数据发送 至DA转换 芯片的数据 端口。
ROM波形 存储 模块( rom_400x8b ROM波形 存储 模块 由 Vivado软件自带的 Block Memory Generator IP核 实现, 其存储 的波形数据 可以 使用 matlab生成 的 .coe文件。
AD数据 接收模块 ad_wave_rec AD数据 接收模块 输出 AD转换 芯片的驱动时钟和使能信号,随后接收 AD转换 完成的数据。
顶层模块里的按键消抖模块可以参考按键控制蜂鸣器实验,AD数据接收模块可以参考高速 AD/DA实验,这里我们重点讲解其余的几个模块。
我们把探针数量设置为2,并且把采样深度设置为 4096。探针宽度的设置如下图所示:
我们将两个探针的位宽设置成1和 8,分别对应 ad_otr和 ad_data的位宽,设置完成后点击“ OK”按钮即可。
ROM中 存储的波形数据 可以 使用 MatLab软件生成 使用 MatLab绘制 4种信号波形,对波形进行等间隔采样,以采样次数作为 ROM存储地址,将采集的波形幅值数据做为存储数据写入存储地址对应的存储空间。在本次实验中我们对 4种信号波形进行分别采样,采样次数为 100次,采集的波形幅值数据位宽为 8bit,将采集数据保存为 coe文件。
各波形参考代码如下。
F1=1; %信号频率
Fs=10^2; %采样频率
P1=0; %信号初始相位
N=10^2; %采样点数
t=[0:1/Fs:(N-1)/Fs]; %采样时刻
ADC=2^7 - 1; %直流分量
A=2^7; %信号幅度
%生成正弦信号
s=A*sin(2*pi*F1*t + pi*P1/180) + ADC;
plot(s); %绘制图形
%创建 coe 文件
fild = fopen('sin_wave_100x8.coe','wt');
%写入 coe 文件头
%固定写法,表示写入的数据是10进制表示
fprintf(fild, '%s\n','memory_initialization_radix=10;');
%固定写法,下面开始写入数据
fprintf(fild, '%s\n\n','memory_initialization_vector =');
for i = 1:N
s2(i) = round(s(i)); %对小数四舍五入以取整
if s2(i) <0 %负 1 强制置零
s2(i) = 0
end
fprintf(fild, '%d',s2(i)); %数据写入
if i==N
fprintf(fild, '%s\n',';'); %最后一个数据用;
else
fprintf(fild,',\n'); % 其他数据用,
end
end
fclose(fild); % 写完了,关闭文件
F1=1; %信号频率
Fs=10^2; %采样频率
P1=0; %信号初始相位
N=10^2; %采样点数
t=[0:1/Fs:(N-1)/Fs]; %采样时刻
ADC=2^7 - 1; %直流分量
A=2^7; %信号幅度
%生成方波信号
s=A*square(2*pi*F1*t + pi*P1/180) + ADC;
plot(s); %绘制图形
%创建 coe文件
fild = fopen('squ_wave_100x8.coe','wt');
%写入 coe文件头
%固定写法,表示写入的数据是10进制表示
fprintf(fild, '%s\n','memory_initialization_radix=10;');
%固定写法,下面开始写入数据
fprintf(fild, '%s\n\n','memory_initialization_vector =');
for i = 1:N
s2(i) = round(s(i)); %对小数四舍五入以取整
if s2(i) <0 %负 1 强制置零
s2(i) = 0
end
fprintf(fild, '%d',s2(i)); %数据写入
if i==N
fprintf(fild, '%s\n',';'); %最后一个数据用分号
else
fprintf(fild,',\n'); % 其他数据用 ,
end
end
fclose(fild); % 写完了,关闭文件
F1=1; %信号频率
Fs=10^2; %采样频率
P1=0; %信号初始相位
N=10^2; %采样点数
t=[0:1/Fs:(N-1)/Fs]; %采样时刻
ADC=2^7 - 1; %直流分量
A=2^7; %信号幅度
%生成三角波信号
s=A*sawtooth(2*pi*F1*t + pi*P1/180,0.5) + ADC;
plot(s); %绘制图形
%创建 coe 文件
fild = fopen('tri_wave_100x8.coe','wt');
%写入coe文件头
%固定写法,表示写入的数据是10进制表示
fprintf(fild, '%s\n','memory_initialization_radix=10;');
%固定写法,下面开始写入数据
fprintf(fild, '%s\n\n','memory_initialization_vector =');
for i = 1:N
s2(i) = round(s(i)); %对小数四舍五入以取整
if s2(i) <0 %负 1 强制置零
s2(i) = 0
end
fprintf(fild, '%d',s2(i)); %数据写入
if i==N
fprintf(fild, '%s\n',';'); %最后一个数据用分号
else
fprintf(fild,',\n'); %其他数据用 ,
end
end
fclose(fild); % 写完了,关闭文件
F1=1; %信号频率
Fs=10^2; %采样频率
P1=0; %信号初始相位
N=10^2; %采样点数
t=[0:1/Fs:(N-1)/Fs]; %采样时刻
ADC=2^7 - 1; %直流分量
A=2^7; %信号幅度
%生成锯齿波信号
s=A*sawtooth(2*pi*F1*t + pi*P1/180) + ADC;
plot(s); %绘制图形
%创建 coe 文件
fild = fopen('saw_wave_100x8.coe','wt');
%写入 coe 文件头
%固定写法,下面开始写入数据
fprintf(fild, '%s\n','memory_initialization_radix=10;');
%固定写法,下面开始写入数据
fprintf(fild, '%s\n\n','memory_initialization_vector =');
for i = 1:N
s2(i) = round(s(i)); %对小数四舍五入以取整
if s2(i) <0 %负 1 强制置零
s2(i) = 0
end
fprintf(fild, '%d',s2(i)); %数据写入
if i==N
fprintf(fild, '%s\n',';'); %最后一个数据用分号
else
fprintf(fild,',\n'); % 其他数据用 ,
end
end
fclose(fild); % 写完了,关闭文件
使用MatLab对 4种波形进行采样后,生成 4个 coe文件,分别对应 4种波形,我们 通过 调用一个深度为 100*4,位宽为 8bit的 ROM,将 四 个 coe文件整合为一个 coe文件 。在配置 rom的 ip核时将整合的 coe文件导入到 rom中。
memory_initialization_radix=10;
memory_initialization_vector =
127,
135,
143,
151,
159,
167,
174,
181,
189,
196,
202,
209,
215,
220,
226,
231,
235,
239,
243,
246,
249,
251,
253,
254,
255,
255,
255,
254,
253,
251,
249,
246,
243,
239,
235,
231,
226,
220,
215,
209,
202,
196,
189,
181,
174,
167,
159,
151,
143,
135,
127,
119,
111,
103,
95,
87,
80,
73,
65,
58,
52,
45,
39,
34,
28,
23,
19,
15,
11,
8,
5,
3,
1,
0,
0,
0,
0,
0,
1,
3,
5,
8,
11,
15,
19,
23,
28,
34,
39,
45,
52,
58,
65,
73,
80,
87,
95,
103,
111,
119,
255,
255,
255,
255,
255,
255,
255,
255,
255,
255,
255,
255,
255,
255,
255,
255,
255,
255,
255,
255,
255,
255,
255,
255,
255,
255,
255,
255,
255,
255,
255,
255,
255,
255,
255,
255,
255,
255,
255,
255,
255,
255,
255,
255,
255,
255,
255,
255,
255,
255,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
4,
9,
14,
19,
25,
30,
35,
40,
45,
50,
55,
60,
66,
71,
76,
81,
86,
91,
96,
101,
107,
112,
117,
122,
127,
132,
137,
142,
147,
153,
158,
163,
168,
173,
178,
183,
188,
194,
199,
204,
209,
214,
219,
224,
229,
235,
240,
245,
250,
255,
250,
245,
240,
235,
229,
224,
219,
214,
209,
204,
199,
194,
188,
183,
178,
173,
168,
163,
158,
153,
147,
142,
137,
132,
127,
122,
117,
112,
107,
101,
96,
91,
86,
81,
76,
71,
66,
60,
55,
50,
45,
40,
35,
30,
25,
19,
14,
9,
4,
0,
2,
4,
7,
9,
12,
14,
17,
19,
22,
25,
27,
30,
32,
35,
37,
40,
43,
45,
48,
50,
53,
55,
58,
60,
63,
66,
68,
71,
73,
76,
78,
81,
83,
86,
89,
91,
94,
96,
99,
101,
104,
107,
109,
112,
114,
117,
119,
122,
124,
127,
130,
132,
135,
137,
140,
142,
145,
147,
150,
153,
155,
158,
160,
163,
165,
168,
171,
173,
176,
178,
181,
183,
186,
188,
191,
194,
196,
199,
201,
204,
206,
209,
211,
214,
217,
219,
222,
224,
227,
229,
232,
235,
237,
240,
242,
245,
247,
250,
252;
我们将其接口类型设置为“Native”、 Memory Type设置为“ Single Port ROM”,即单端口 ROM。“Port A Options”选项页的配置页面如下图所示
我们将PortA的位宽设置为 8,深度设置为 400,以存储 matlab生成的 400个数据。此外,将使能引脚的类型设置为“ Always Enabled”,即 ROM一直处于使能的状态。
接下来配置“Other Options”选项页,加载刚才生成的 .coe文件,如下图所示:
在顶层模块代码例化了 clk_wiz的 ip核,用于生成 100M时钟作为读 rom地址的时钟。
注意, rom模块的端口时钟和按键消抖模块的时钟也是 100M。 clk_wiz IP核的配置如下图所示:
接下来切换至 Output Clocks”选项卡,在 Output Clock 选项卡 中,勾选 第 1个时钟,并且将其“ Output Freq(MHz)”分别设置为 100,其他设置保持默认即可,如 下图 所示。
其余的设置保持默认,最后直接点击“ OK”按钮即可 。
`timescale 1ns / 1ps
//
// Company:
// Engineer:
//
// Create Date: 2022/02/28 19:57:11
// Design Name:
// Module Name: da_wave_send
// Project Name:
// Target Devices:
// Tool Versions:
// Description:
//
// Dependencies:
//
// Revision:
// Revision 0.01 - File Created
// Additional Comments:
//
//
module da_wave_send(
input rst_n, // 复位信号,低电平有效
input clk, // 连接到clk_100M
input key0_value, // 消抖后的按键值
input key0_flag, // 消抖后的按键值的有效标志
input key1_value, // 消抖后的按键值
input key1_flag, // 消抖后的按键值的有效标志
input [7 : 0] rd_data, // ROM读出的数据
output reg [8 : 0] rd_addr, // 读ROM地址
// DA芯片接口
output da_clk, // DA(AD9708)驱动时钟,最大支持125Mhz时钟
output [7 : 0] da_data // 输出给DA的数据
);
// parameter
// 波形调节控制
parameter sine_wave_addr = 9'd0; // 正弦波起始位置
parameter square_wave_addr = 9'd100; // 方波起始位置
parameter triangle_wave_addr = 9'd200; // 三角波起始位置
parameter sawtooth_wave_addr = 9'd300; // 锯齿波起始位置
//频率调节控制,FREQ_ADJ的越大,最终输出的频率越低,范围0~255
parameter FREQ_ADJ0 = 8'd0; //参数0对应输出1Mhz波形频率
parameter FREQ_ADJ1 = 8'd1; //参数1对应输出500khz波形频率
parameter FREQ_ADJ2 = 8'd3; //参数3对应输出250khz波形频率
parameter FREQ_ADJ3 = 8'd7; //参数7对应输出125khz波形频率
//reg define
reg [7 : 0] freq_adj; //频率调节参数寄存器
reg [7 : 0] freq_cnt; //频率调节计数器
reg [1 : 0] wave_select; //切换波形地址寄存器
reg [1 : 0] freq_select; //切换波形频率寄存器
//*****************************************************
//** main code
//*****************************************************
// 数据rd_data是在clk_100M的上升沿更新的,
// 所以DA芯片在clk_100M的下降沿锁存数据是稳定的时刻。
// 而DA实际上在da_clk的上升沿锁存数据,所以时钟取反,
// 这样 clk_100M 的下降沿相当于 da_clk 的上升沿。
assign da_clk = ~clk;
assign da_data = rd_data; //将读到的ROM数据赋值给DA数据端口
// 切换波形种类
always @(posedge clk or negedge rst_n) begin
if(rst_n == 1'b0)
wave_select <= 2'd0;
else if((key0_flag == 1) && (key0_value == 0)) begin //确保按键key0确实被有效按下
if(wave_select < 2'd3)
wave_select <= wave_select+1'd1;
else
wave_select <= 0;
end
else
wave_select <= wave_select;
end
//切换波形频率
always @(posedge clk or negedge rst_n) begin
if(rst_n == 1'b0)
freq_select <= 2'd0;
else if((key1_flag ==1) && (key1_value ==0)) begin //确保按键key1确实被有效按下
if(freq_select < 2'd3)
freq_select <= freq_select+1'd1;
else
freq_select <= 0;
end
else
freq_select <= freq_select;
end
always @(posedge clk or negedge rst_n) begin
if(rst_n == 1'b0)
freq_adj <= 8'd0;
else case(freq_select)
2'd0:freq_adj <= FREQ_ADJ0;
2'd1:freq_adj <= FREQ_ADJ1;
2'd2:freq_adj <= FREQ_ADJ2;
2'd3:freq_adj <= FREQ_ADJ3;
default:freq_adj <= FREQ_ADJ0;
endcase
end
//频率调节计数器
always @(posedge clk or negedge rst_n) begin
if(rst_n == 1'b0)
freq_cnt <= 8'd0;
else if(freq_cnt == freq_adj)
freq_cnt <= 8'd0;
else
freq_cnt <= freq_cnt + 8'd1;
end
//读ROM地址,按照100M的频率去读
always @(posedge clk or negedge rst_n) begin
if(rst_n == 1'b0)
rd_addr <= 9'd0;
else if(freq_cnt == freq_adj) begin
case(wave_select)
2'd0:
if(rd_addr >= sine_wave_addr && rd_addr <= sine_wave_addr+9'd99)
if(rd_addr == sine_wave_addr+9'd99)
rd_addr <= sine_wave_addr;
else
rd_addr <= rd_addr+9'd1;
else
rd_addr <= sine_wave_addr;
2'd1:
if(rd_addr >= square_wave_addr && rd_addr <= square_wave_addr+9'd99)
if(rd_addr == square_wave_addr+9'd99)
rd_addr <= square_wave_addr;
else
rd_addr <= rd_addr+9'd1;
else
rd_addr <= square_wave_addr;
2'd2:
if(rd_addr >= triangle_wave_addr && rd_addr <= triangle_wave_addr+9'd99)
if(rd_addr == triangle_wave_addr+9'd99)
rd_addr <= triangle_wave_addr;
else
rd_addr <= rd_addr+9'd1;
else
rd_addr <= triangle_wave_addr;
2'd3:
if(rd_addr >= sawtooth_wave_addr && rd_addr <= sawtooth_wave_addr+9'd99)
if(rd_addr == sawtooth_wave_addr+9'd99)
rd_addr <= sawtooth_wave_addr;
else
rd_addr <= rd_addr+9'd1;
else
rd_addr <= sawtooth_wave_addr;
default:
if(rd_addr >= sine_wave_addr && rd_addr <= sine_wave_addr+9'd99)
if(rd_addr == sine_wave_addr+9'd99)
rd_addr <= sine_wave_addr;
else
rd_addr <= rd_addr+9'd1;
else
rd_addr <= sine_wave_addr;
endcase
end
else
rd_addr <= rd_addr;
end
endmodule
在代码的定义了 一个 参 数寄存器 freq_adj(频率 调节 ),可以 通过控制 频率 调节 参数 的大小 来控制最终输出 波形 的 频率大小, 频率 调节 参数 的值 越小 波形 频率越大 。频率 调节 参数调节 波形 频率的方法是通过 控制 读 ROM的 速度 实现 的, 频率 调节 参数越小 freq_cnt计数 到 频率 调节 参数 值的 时间越 短,读 ROM数据 的速度越快,那么 正弦波输出 频率 也就 越 高;反过来 频率 调节 参数越大 freq_cnt计数 到 频率 调节 参数 值的 时间 越长,读 ROM数据 的速度越 慢 ,那么 正弦波输出 频率 也就 越低 。由于 freq_cnt计数器 的位宽为8位 ,计数范围是 0~ 255 所以 频率 调节 参数 freq_adj支持的调节 范围是 0~255 可通过修改 freq_cnt计数器 的位宽 来 修改 freq_adj支持的调节 范围 。
通过matlab生成的 coe文件的数据深度为 100 而 输入时钟为 50 Mhz 通过锁相环倍频可以得到 100 Mhz的 da芯片的驱动时钟, 那么一个完整的波形周期的长度 为 10010ns=1000ns,当 freq_adj的值为 0时,即波形的的最快输出频率为 1s/1000ns(1s = 1000000000ns)=1Mhz 当 我们 把 freq_adj的 值设置为 1时 一个完整的 波形 周期 长度 为 1000ns(1+1) = 2000ns,频率 为 500Khz。 当 freq_adj的值设为 3或 7时,输出波形频率也相应的变为 250khz和 125khz。
`timescale 1ns / 1ps
//
// Company:
// Engineer:
//
// Create Date: 2022/02/28 17:54:35
// Design Name:
// Module Name: dds
// Project Name:
// Target Devices:
// Tool Versions:
// Description:
//
// Dependencies:
//
// Revision:
// Revision 0.01 - File Created
// Additional Comments:
//
//
module dds(
input sys_clk, // 系统时钟
input sys_rst_n, // 系统复位,低电平有效
input key0, // 按键key0
input key1, // 按键key1
// DA芯片接口
output da_clk, // DA(AD9708)驱动时钟,最大支持125Mhz时钟
output [7 : 0] da_data, // 输出给DA的数据
// AD芯片接口
input [7 : 0] ad_data, // AD输入数据
// 模拟输入电压超出量程标志(本次实验未用到)
input ad_otr, // 0:在量程范围 1:超出量程
output ad_clk // AD(AD9280)驱动时钟,最大支持32Mhz时钟
);
// wire define
wire [8 : 0] rd_addr; // ROM读地址
wire [7 : 0] rd_data; // ROM读出的数据
wire key0_value; // key0消抖后的按键值
wire key0_flag; // key0消抖后的按键值的有效标志
wire key1_value; // key1消抖后的按键值
wire key_flag; // key1消抖后的按键值的有效标志
wire clk_100M; // da芯片的驱动时钟
// *****************************
// ** main code
// *****************************
// 例化按键消抖模块
key_debounce u_key0_debounce(
.sys_rst_n (sys_rst_n),
.clk (clk_100M),
.key (key0),
.key_value (key0_value),
.key_flag (key0_flag)
);
// 例化按键消抖模块
key_debounce u_key1_debounce(
.sys_rst_n (sys_rst_n),
.clk (clk_100M),
.key (key1),
.key_value (key1_value),
.key_flag (key1_flag)
);
// DA数据发送
da_wave_send u_da_wave_send(
.clk (clk_100M),
.rst_n (sys_rst_n),
.key0_value (key0_value),
.key0_flag (key0_flag),
.key1_value (key1_value),
.key1_flag (key1_flag),
.rd_data (rd_data),
.rd_addr (rd_addr),
.da_clk (da_clk),
.da_data (da_data)
);
// AD数据接收
ad_wave_rec u_ad_wave_rec(
.clk (sys_clk),
.rst_n (sys_rst_n),
.ad_data (ad_data),
.ad_otr (ad_otr),
.ad_clk (ad_clk)
);
clk_wiz_0 u_clk_wiz_0 (
// Clock out ports
.clk_out1(clk_100M), // output clk_out1
// Status and control signals
.reset(~sys_rst_n), // input reset
.locked(locked), // output locked
// Clock in ports
.clk_in1(sys_clk)
); // input clk_in1
rom_400x8b u_rom_400x8b (
.clka(clk_100M), // input wire clka
.addra(rd_addr), // input wire [8 : 0] addra
.douta(rd_data) // output wire [7 : 0] douta
);
ila_0 u_ila_0 (
.clk(ad_clk), // input wire clk
.probe0(ad_otr), // input wire [0:0] probe0
.probe1(ad_data) // input wire [7:0] probe1
);
endmodule
`timescale 1ns / 1ps
//
// Company:
// Engineer:
//
// Create Date: 2022/02/28 20:53:15
// Design Name:
// Module Name: key_debounce
// Project Name:
// Target Devices:
// Tool Versions:
// Description:
//
// Dependencies:
//
// Revision:
// Revision 0.01 - File Created
// Additional Comments:
//
//
module key_debounce(
input sys_clk ,
input sys_rst_n ,
input key , //外部输入的按键值
output reg key_value , //消抖后的按键值
output reg key_flag //消抖后的按键值的效标志
);
//reg define
reg [19:0] cnt ;
reg key_reg ;
//*****************************************************
//** main code
//*****************************************************
//按键值消抖
always @ (posedge sys_clk or negedge sys_rst_n) begin
if(!sys_rst_n) begin
cnt <= 20'd0;
key_reg <= 1'b1;
end
else begin
key_reg <= key; //将按键值延迟一拍
if(key_reg != key) begin //检测到按键状态发生变化
cnt <= 20'd100_0000; //则将计数器置为20'd100_0000
//即延时100_0000 * 20ns(1s/50MHz) = 20ms
end
else begin //如果当前按键值和前一个按键值一样,即按键没有发生变化
if(cnt > 20'd0) //则计数器递减到0
cnt <= cnt - 1'b1;
else
cnt <= 20'd0;
end
end
end
//将消抖后的最终的按键值送出去
always @ (posedge sys_clk or negedge sys_rst_n) begin
if(!sys_rst_n) begin
key_value <= 1'b1;
key_flag <= 1'b0;
end
//在计数器递减到1时送出按键值
else if(cnt == 20'd1) begin
key_value <= key;
key_flag <= 1'b1;
end
else begin
key_value <= key_value;
key_flag <= 1'b0;
end
end
endmodule
每检测到按键被按下或松开,就让计数器从 100_0000开始 递减,时长 20ms。在这 20ms期间,每当有抖动产生,计数器就被重置回 100_0000,即重新开始计时 20ms。代码中的第 46行,只有在计数器递减到 1时,即此时计数器计时完了20ms,才会寄存按键的值。这样,每当按键被按下或松开, 20ms内的抖动就被消除了。
`timescale 1ns / 1ps
//
// Company:
// Engineer:
//
// Create Date: 2022/02/28 17:54:35
// Design Name:
// Module Name: dds
// Project Name:
// Target Devices:
// Tool Versions:
// Description:
//
// Dependencies:
//
// Revision:
// Revision 0.01 - File Created
// Additional Comments:
//
//
module dds(
input sys_clk, // 系统时钟
input sys_rst_n, // 系统复位,低电平有效
input key0, // 按键key0
input key1, // 按键key1
// DA芯片接口
output da_clk, // DA(AD9708)驱动时钟,最大支持125Mhz时钟
output [7 : 0] da_data, // 输出给DA的数据
// AD芯片接口
input [7 : 0] ad_data, // AD输入数据
// 模拟输入电压超出量程标志(本次实验未用到)
input ad_otr, // 0:在量程范围 1:超出量程
output ad_clk // AD(AD9280)驱动时钟,最大支持32Mhz时钟
);
// wire define
wire [8 : 0] rd_addr; // ROM读地址
wire [7 : 0] rd_data; // ROM读出的数据
wire key0_value; // key0消抖后的按键值
wire key0_flag; // key0消抖后的按键值的有效标志
wire key1_value; // key1消抖后的按键值
wire key_flag; // key1消抖后的按键值的有效标志
wire clk_100M; // da芯片的驱动时钟
// *****************************
// ** main code
// *****************************
// 例化按键消抖模块
key_debounce u_key0_debounce(
.sys_rst_n (sys_rst_n),
.clk (clk_100M),
.key (key0),
.key_value (key0_value),
.key_flag (key0_flag)
);
// 例化按键消抖模块
key_debounce u_key1_debounce(
.sys_rst_n (sys_rst_n),
.clk (clk_100M),
.key (key1),
.key_value (key1_value),
.key_flag (key1_flag)
);
// DA数据发送
da_wave_send u_da_wave_send(
.clk (clk_100M),
.rst_n (sys_rst_n),
.key0_value (key0_value),
.key0_flag (key0_flag),
.key1_value (key1_value),
.key1_flag (key1_flag),
.rd_data (rd_data),
.rd_addr (rd_addr),
.da_clk (da_clk),
.da_data (da_data)
);
// AD数据接收
ad_wave_rec u_ad_wave_rec(
.clk (sys_clk),
.rst_n (sys_rst_n),
.ad_data (ad_data),
.ad_otr (ad_otr),
.ad_clk (ad_clk)
);
clk_wiz_0 u_clk_wiz_0 (
// Clock out ports
.clk_out1(clk_100M), // output clk_out1
// Status and control signals
.reset(~sys_rst_n), // input reset
.locked(locked), // output locked
// Clock in ports
.clk_in1(sys_clk)
); // input clk_in1
rom_400x8b u_rom_400x8b (
.clka(clk_100M), // input wire clka
.addra(rd_addr), // input wire [8 : 0] addra
.douta(rd_data) // output wire [7 : 0] douta
);
ila_0 u_ila_0 (
.clk(ad_clk), // input wire clk
.probe0(ad_otr), // input wire [0:0] probe0
.probe1(ad_data) // input wire [7:0] probe1
);
endmodule
############## clock and reset define##################
create_clock -period 20.000 [get_ports sys_clk]
set_property IOSTANDARD LVCMOS33 [get_ports sys_clk]
set_property PACKAGE_PIN U18 [get_ports sys_clk]
set_property IOSTANDARD LVCMOS33 [get_ports sys_rst_n]
set_property PACKAGE_PIN K16 [get_ports sys_rst_n]
########key N15 N16##################
set_property PACKAGE_PIN N15 [get_ports key1]
set_property PACKAGE_PIN N16 [get_ports key0]
set_property IOSTANDARD LVCMOS33 [get_ports key1]
set_property IOSTANDARD LVCMOS33 [get_ports key0]
########AN108 ON AX7020 and AX7010 J11##################
set_property PACKAGE_PIN F20 [get_ports da_clk]
set_property PACKAGE_PIN F19 [get_ports {da_data[7]}]
set_property PACKAGE_PIN G20 [get_ports {da_data[6]}]
set_property PACKAGE_PIN G19 [get_ports {da_data[5]}]
set_property PACKAGE_PIN H18 [get_ports {da_data[4]}]
set_property PACKAGE_PIN J18 [get_ports {da_data[3]}]
set_property PACKAGE_PIN L20 [get_ports {da_data[2]}]
set_property PACKAGE_PIN L19 [get_ports {da_data[1]}]
set_property PACKAGE_PIN M20 [get_ports {da_data[0]}]
set_property PACKAGE_PIN L17 [get_ports {ad_data[0]}]
set_property PACKAGE_PIN L16 [get_ports {ad_data[1]}]
set_property PACKAGE_PIN M18 [get_ports {ad_data[2]}]
set_property PACKAGE_PIN M17 [get_ports {ad_data[3]}]
set_property PACKAGE_PIN D20 [get_ports {ad_data[4]}]
set_property PACKAGE_PIN D19 [get_ports {ad_data[5]}]
set_property PACKAGE_PIN E19 [get_ports {ad_data[6]}]
set_property PACKAGE_PIN E18 [get_ports {ad_data[7]}]
set_property PACKAGE_PIN G18 [get_ports ad_clk]
set_property IOSTANDARD LVCMOS33 [get_ports da_clk]
set_property IOSTANDARD LVCMOS33 [get_ports {da_data[7]}]
set_property IOSTANDARD LVCMOS33 [get_ports {da_data[6]}]
set_property IOSTANDARD LVCMOS33 [get_ports {da_data[5]}]
set_property IOSTANDARD LVCMOS33 [get_ports {da_data[4]}]
set_property IOSTANDARD LVCMOS33 [get_ports {da_data[3]}]
set_property IOSTANDARD LVCMOS33 [get_ports {da_data[2]}]
set_property IOSTANDARD LVCMOS33 [get_ports {da_data[1]}]
set_property IOSTANDARD LVCMOS33 [get_ports {da_data[0]}]
set_property IOSTANDARD LVCMOS33 [get_ports {ad_data[0]}]
set_property IOSTANDARD LVCMOS33 [get_ports {ad_data[1]}]
set_property IOSTANDARD LVCMOS33 [get_ports {ad_data[2]}]
set_property IOSTANDARD LVCMOS33 [get_ports {ad_data[3]}]
set_property IOSTANDARD LVCMOS33 [get_ports {ad_data[4]}]
set_property IOSTANDARD LVCMOS33 [get_ports {ad_data[5]}]
set_property IOSTANDARD LVCMOS33 [get_ports {ad_data[6]}]
set_property IOSTANDARD LVCMOS33 [get_ports {ad_data[7]}]
set_property IOSTANDARD LVCMOS33 [get_ports ad_clk]
set_property IOSTANDARD LVCMOS33 [get_ports ad_otr]
set_property PACKAGE_PIN J14 [get_ports ad_otr]
set_property C_CLK_INPUT_FREQ_HZ 300000000 [get_debug_cores dbg_hub]
set_property C_ENABLE_CLK_DIVIDER false [get_debug_cores dbg_hub]
set_property C_USER_SCAN_CHAIN 1 [get_debug_cores dbg_hub]
connect_debug_port dbg_hub/clk [get_nets ad_clk_OBUF]
将工程生成的比特流文件下载到 ZYNQ中 后, 然后 使用 ila测量 DA输出 通道的波形。
首先按下PL_KEY2,切换波形,如下失踪波形图所示