目录
DDS 信号发生器实验
1、DDS 简介
2、实验任务
3、程序设计
3.1、DDS 顶层模块代码
3.2、clk_wiz IP 核
3.3、ILA IP 核(集成逻辑分析器:Integrated Logic Analyzer,ILA)
3.4、各波形参考代码
3.4.1、正弦信号波形采集参考代码(sin_wave.m):
3.4.2、方波信号波形采集参考代码(square_wave.m):
3.4.3、三角波信号波形采集参考代码(triangle_wave.m):
3.4.4、锯齿波信号波形采集参考代码(sawtooth_wave.m):
3.4.5、将四个 coe 文件整合为一个 coe 文件
3.5、ROM IP 核
ROM IP 核,加载生成的.coe 文件
3.6、DA 数据发送模块代码
3.7、AD 数据接收模块代码
3.8、按键消抖模块代码
4、硬件设计
4.1、添加约束文件.dxc
5、下载验证
DDS 信号发生器实验
DDS(Direct Digital Synthesizer)即直接数字式频率合成器,是一种新型的频率合成技术。与传统的频率合成器相比,DDS 具有相对带宽大,频率转换时间短,稳定性好,分辨率高,可灵活产生多种信号等优点。较容易实现频率、相位及幅度的数控调制,因此,在现代电子系统及设备的频率源设计中,尤其在通信领域,直接数字频率合成器的应用越来越广泛。作为设计人员,我们习惯称它为信号发生器,一般用它产生正弦、锯齿、方波等不同波形或不同频率的信号波形,在电子设计和测试中得到广泛应用。本章我们将利用高速 ad/da 模块去设计实现一个简易的 DDS 信号发生器。
1、DDS 简介
DDS(Direct Digital Synthesizer)即数字合成器,是一种新型的频率合成技术,具有低成本、低功耗、高分辨率、频率转换时间短、相位连续性好等优点,对数字信号处理及其硬件实现有着很重要的作用。DDS 的基本结构主要由相位累加器、相位调制器、波形数据表 ROM、D/A 转换器等四大结构组成,其中较多设计还会在数模转换器之后增加一个低通滤波器(LPF)。DDS 基本结构图如图 27.1.1 所示。
由上图可以看出,DDS 主要由相位累加器、相位调制器、波形数据表以及 D/A 转换器构成。其中相位累加器由 N 位加法器与 N 位寄存器构成。每来一个时钟,加法器就将频率控制字与累加寄存器输出的相位数据相加,相加的结果又反馈至累加寄存器的数据输入端,以使加法器在下一个时钟脉冲的作用下继续与频率控制字相加。这样,相位累加器在时钟的作用下,不断对频率控制字进行线性相位累加。即在每一个时钟脉冲输入时,相位累加器便把频率控制字累加一次。当相位累加器累加满量时就会产生一次溢出,完成一个周期的动作。相位累加器输出的数据就是合成信号的相位。相位累加器的溢出频率,就是 DDS 输出的信号频率。
通过改变相位控制字 P_WORD 可以控制输出信号的相位参数。令相位加法器的字长为 M,当相位控制字由 0 跃变为 P_WORD 时,波形存储器(ROM)的输入为相位累加器的输出与相位控制字 P_WORD 之和,因而其输出的幅度编码相位会增加 P_WORD/2M,从而使输出的信号产生相移。
用相位调制器输出的数据,作为波形存储器的相位采样地址,这样就可以把存储在波形存储器里的波形采样值经查表找出,完后相位到幅度的转换。N 位的寻址 ROM 相当于把 0°-360°的正弦信号离散成具有2 N 个样值的序列。若波形存储器中有 D 位数据位,则 2 N 个样值的幅值以 D 位二进制数值固化在波形存储器当中。按照地址的不同可以输出相应相位的正弦信号幅值。相位—幅度变换原理图如下图所示:
数模转换器(D/A)的作用是把合成的正弦波数字量转化为模拟量。正弦幅度量化序列经数模转换器转换后变成了包络为正弦波的阶梯波。频率合成器对数模转换器的分辨率有一定的要求,其分辨率越高,合成的正弦波台阶数就越多,输出的波形精度也就越高。DDS 信号流程图如下图所示:
2、实验任务
本节实验任务是使用 ZYNQ 开发板及高速 AD-DA 扩展模块(ATK_HS_AD_DA 模块)实现数模及模数的转换。首先 ZYNQ PL 端产生正弦波、方波、三角波、锯齿波变化的数字信号,经过 DA 芯片后转换成模拟信号,将 DA 的模拟电压输出端连接至 AD 的模拟电压输入端,AD 芯片将模拟信号转换成数字信号,通过按下按键 key0 和 key1 可以实现波形种类和波形频率的切换,然后通过示波器观察 DA 端模拟信号的波形是否出现了相应变化,也可以通过 Ila 界面去进行观察。
3、程序设计
根据本章的实验任务,ZYNQ PL 端需要连续输出正弦波、方波、三角波、锯齿波波形的数据,才能使 AD9708 连续输出相应波形的模拟电压,如果使用三角函数公式运算的方式来编写代码,然后输出正弦波、方波、三角波、锯齿波的波形数据,那么程序设计会变得非常复杂。在工程应用中,一般将波形数据存储在 RAM 或者 ROM 中,由于本次实验并不需要写数据到 RAM 中,因此我们将波形数据存储在只读的 ROM 中,直接读取 ROM 中的数据发送给 DA 转换芯片即可。
图 27.4.1 是根据本章实验任务画出的系统框图。ROM 里面事先存储好了波形的数据,DA 数据发送模块从 ROM 中读取数据,将数据和时钟送到 AD9708 的输入数据端口和输入时钟端口;AD 数据接收模块给 AD9280 输出驱动时钟信号和使能信号,并采集 AD9280 输出模数转换完成的数据。
DDS 实验的系统框图如下图所示:
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 实验,这里我们重点讲解其余的几个模块。
3.1、DDS 顶层模块代码
顶层模块的代码如下:
`timescale 1ns / 1ps
//
// Company:
// Engineer:
//
// Create Date: 2023/06/25 10:44:45
// 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 key1_flag; //key1 消抖后的按键值的有效标志
wire clk_100M; //da 芯片的驱动时钟
//**************************************************
//** main code
//**************************************************
//例化按键消抖模块
key_debounce u_key0_debounce(
.clk (clk_100M ),
.sys_rst_n (sys_rst_n ),
.key (key0 ),
.key_flag (key0_flag ),
.key_value (key0_value )
);
//例化按键消抖模块
key_debounce u_key1_debounce(
.clk (clk_100M ),
.sys_rst_n (sys_rst_n ),
.key (key1 ),
.key_flag (key1_flag ),
.key_value (key1_value )
);
//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 )
);
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 存储波形
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
);
//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 ila_0 (
.clk (ad_clk ), // input wire clk
.probe0 (ad_otr ), // input wire [0:0] probe0
.probe1 (ad_data ) // input wire [7:0] probe0
);
endmodule
DA 数据发送模块输出的读 ROM 地址(rd_addr)连接至 ROM 模块的地址输入端,ROM 模块输出的数据(rd_data)连接至 DA 数据发送模块的数据输入端,从而完成了从 ROM 中读取数据的功能。
在顶层模块代码的第 85 至 94 行例化了 clk_wiz 的 ip 核,用于生成 100M 时钟作为读 rom 地址的时钟。 注意,rom 模块的端口时钟和按键消抖模块的时钟也是 100M。
3.2、clk_wiz IP 核
clk_wiz IP 核的配置如下图所示:
接下来切换至“Output Clocks”选项卡,在“Output Clock”选项卡中,勾选第 1 个时钟,并且将其“Output Freq(MHz)”分别设置为 100,其他设置保持默认即可,如下图所示。
其余的设置保持默认,最后直接点击“OK”按钮即可。
在顶层模块代码的第 96 至 101 行例化了 ROM 模块,由 Block Memory Generator IP 核配置生成。
3.3、ILA IP 核(集成逻辑分析器:Integrated Logic Analyzer,ILA)
我们把探针数量设置为 2,并且把采样深度设置为 4096。探针宽度的设置如下图所示:
我们将两个探针的位宽设置成 1 和 8,分别对应 ad_otr 和 ad_data 的位宽,设置完成后点击“OK”按钮即可。
我们在前面说过,ROM 中存储的波形数据可以使用 MatLab 软件生成,使用 MatLab 绘制 4 种信号波形,对波形进行等间隔采样,以采样次数作为 ROM 存储地址,将采集的波形幅值数据做为存储数据写入存储地址对应的存储空间。在本次实验中我们对 4 种信号波形进行分别采样,采样次数为 100 次,采集的波形幅值数据位宽为 8bit,将采集数据保存为 coe 文件。
3.4、各波形参考代码
3.4.1、正弦信号波形采集参考代码(sin_wave.m):
% 正弦信号波形采集参考代码(sin_wave.m):
clc, clear, close all
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); % 写完了,关闭文件
3.4.2、方波信号波形采集参考代码(square_wave.m):
% 方波信号波形采集参考代码(square_wave.m):
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); % 写完了,关闭文件
图 27.4.8 MatLab 生成的方波信号波形
3.4.3、三角波信号波形采集参考代码(triangle_wave.m):
% 三角波信号波形采集参考代码(triangle_wave.m):
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); % 写完了,关闭文件
3.4.4、锯齿波信号波形采集参考代码(sawtooth_wave.m):
% 锯齿波信号波形采集参考代码(sawtooth_wave.m):
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 中。
3.4.5、将四个 coe 文件整合为一个 coe 文件
整体信号波形采集参考代码(wave_100x8.m)
clc; %清除命令行命令
clear; %清除工作区变量,释放内存空间
close all;
% F1 = 1; %信号频率
% Fs = 2^12; %采样频率
% P1 = 0; %信号初始相位
% N = 2^12; %采样点数
% t = [0:1/Fs:(N-1)/Fs]; %采样时刻
% ADC = 2^7 - 1; %直流分量
% A = 2^7; %信号幅度
F1 = 1; %信号频率
Fs = 10^2; %采样频率
P1 = 0; %信号初始相位
N = 10^2; %采样点数
t = [0:1/Fs:(N-1)/Fs]; %采样时刻
ADC = 2^7 - 1; %直流分量
A = 2^7; %信号幅度
s1 = A*sin(2*pi*F1*t + pi*P1/180) + ADC; %正弦波信号
s2 = A*square(2*pi*F1*t + pi*P1/180) + ADC; %方波信号
s3 = A*sawtooth(2*pi*F1*t + pi*P1/180,0.5) + ADC; %三角波信号
s4 = A*sawtooth(2*pi*F1*t + pi*P1/180) + ADC; %锯齿波信号
%创建 coe 文件
% fild = fopen('wave_16384x8.coe','wt');
fild = fopen('wave_400x8.coe','wt');
%写入 coe 文件头
fprintf(fild, '%s\n','MEMORY_INITIALIZATION_RADIX=10;'); %10 进制数
fprintf(fild, '%s\n','MEMORY_INITIALIZATION_VECTOR=');
for j = 1:4
for i = 1:N
if j == 1 %打印正弦信号数据
s0(i) = round(s1(i)); %对小数四舍五入以取整
end
if j == 2 %打印方波信号数据
s0(i) = round(s2(i)); %对小数四舍五入以取整
end
if j == 3 %打印三角波信号数据
s0(i) = round(s3(i)); %对小数四舍五入以取整
end
if j == 4 %打印锯齿波信号数据
s0(i) = round(s4(i)); %对小数四舍五入以取整
end
if s0(i) <0 %负 1 强制置零
s0(i) = 0;
end
if j == 4 && i == N
fprintf(fild, '%d',s0(i)); %数据写入
fprintf(fild, '%s',';'); %最后一个数使用分号结束
else
fprintf(fild, '%d',s0(i)); %数据写入
fprintf(fild, '%s\n',','); %逗号,换行
end
end
end
fclose(fild);
使用 Notepad++代码编辑器打开生成的 COE 文件后如下图所示:
3.5、ROM IP 核
ROM IP 核,加载生成的.coe 文件
工程中创建了一个单端口 ROM,并命名为“rom_400x8b”,在调用 Block Memory Generator IP 核时, “Basic”选项也配置如下图所示:
我们将其接口类型设置为“Native”、Memory Type 设置为“Single Port ROM”,即单端口 ROM。 “Port A Options”选项页的配置页面如下图所示:
图 27.4.13 Block Memory Generator IP 核的 PortA Options 配置页面
我们将 PortA 的位宽设置为 8,深度设置为 400,以存储 matlab 生成的 400 个数据。此外,将使能引脚的类型设置为 “Always Enabled” ,即 ROM 一直处于使能的状态。
接下来配置 “Other Options” 选项页,加载刚才生成的.coe 文件,如下图所示:
最后点击“OK”按钮完成 IP 核的配置。
3.6、DA 数据发送模块代码
DA 数据发送模块的代码如下:
`timescale 1ns / 1ps
//
// Company:
// Engineer:
//
// Create Date: 2023/06/25 14:05:47
// 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
//在代码的第 52 行定义了一个参数寄存器 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,
//通过锁相环倍频可以得到 100Mhz 的 da 芯片的驱动时钟,那么一个完整的波形周期的长度为 100*10ns=1000ns,
//当 freq_adj 的值为 0 时,即波形的的最快输出频率为 1s/1000ns(1s = 1000000000ns)=1Mhz,
//当我们把 freq_adj 的值设置为 1 时,一个完整的波形周期长度为 1000ns*(1+1) = 2000ns,
//频率为 500Khz。当 freq_adj 的值设为 3 或 7 时,输出波形频率也相应的变为 250khz 和 125khz。
3.7、AD 数据接收模块代码
AD 数据接收代码如下:
`timescale 1ns / 1ps
//
// Company:
// Engineer:
//
// Create Date: 2023/06/25 14:29:35
// 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, //50Mhz系统时钟(达芬奇Pro开发板)
input rst_n, //复位信号,低电平有效
input [7:0] ad_data, //AD输入数据
//模拟输入电压超出量程标志(本次试验未用到)
input ad_otr, //0:在量程范围; 1:超出量程
output reg ad_clk //AD(TLC5510)驱动时钟,最大支持20Mhz时钟(达芬奇Pro开发板)
);
//*****************************************
//** main code
//******************************************
//时钟分频(2分频,时钟频率为25Mhz),产生AD时钟(50Mhz系统时钟,达芬奇Pro开发板)
always @(posedge clk or negedge rst_n) begin
if(rst_n==1'b0)
ad_clk <=1'b0;
else
ad_clk <= ~ad_clk;
end
endmodule
3.8、按键消抖模块代码
按键消抖模块代码如下:
`timescale 1ns / 1ps
//
// Company:
// Engineer:
//
// Create Date: 2023/06/25 14:28:31
// 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, //50MHz时钟
input sys_rst_n, //复位信号,低频有效
input key, //外部按键输入
output reg key_flag, //按键数据有效信号
output reg key_value //按键消抖后的数据
);
//reg define
reg [31:0] delay_cnt;
reg key_reg;
//(系统时钟50MHz,一个时钟周期是1/50MHz=0.02us=20ns)(0.2s需计数:0.2s/20ns=10_000_000次)
//(系统时钟200MHz,一个时钟周期是1/200MHz=0.005us=5ns)(20ms需计数:20ms/5ns=4_000_000次)
//*************************************************************
//** main code
//*************************************************************
always @(posedge sys_clk or negedge sys_rst_n) begin
if (!sys_rst_n) begin
key_reg <= 1'b1;
delay_cnt <= 32'd0;
end
else begin
key_reg <= key;
if (key_reg != key) begin //一旦检测到按键状态发生变化(有按键按下或者释放)
//delay_cnt <= 32'd1000_000;//给延时计数器重新装载初始值(计数时间为20ms)
delay_cnt <= 32'd4; //仅用于仿真
end
else begin //按键状态稳定时,计数器递减,开始20ms倒计时
if (delay_cnt >32'd0)
delay_cnt <= delay_cnt - 1'b1;
else
delay_cnt <= 32'd0;
end
end
end
always @(posedge sys_clk or negedge sys_rst_n) begin
if (!sys_rst_n) begin
key_flag <= 1'b0;
key_value <= 1'b1;
end
else if (delay_cnt == 32'd1) begin //当计数器递减到1时,说明按键稳定状态维持了20ms
key_flag <= 1'b1; //此时消抖过程结束,给出一个时钟周期的标志信号
key_value <= key; //并寄存此时按键的值
end
else begin
key_flag <= 1'b0;
key_value <= key_value;
end
end
endmodule
4、硬件设计
本章节中硬件设计与“高速 AD/DA 实验”完全相同,此处不在赘述。本实验中,各端口信号的管脚分配如下表所示。
4.1、添加约束文件.dxc
对应的 .xdc 约束语句如下所示:
#时序约束
create_clock -period 20.000 -name sys_clk [get_ports sys_clk]
#IO 管脚约束
set_property -dict {IOSTANDARD LVCMOS33 PACKAGE_PIN U18} [get_ports sys_clk]
set_property -dict {IOSTANDARD LVCMOS33 PACKAGE_PIN N16} [get_ports sys_rst_n]
set_property -dict {IOSTANDARD LVCMOS33 PACKAGE_PIN L14} [get_ports key0]
set_property -dict {IOSTANDARD LVCMOS33 PACKAGE_PIN K16} [get_ports key1]
set_property -dict {IOSTANDARD LVCMOS33 PACKAGE_PIN Y18} [get_ports {da_data[7]}]
set_property -dict {IOSTANDARD LVCMOS33 PACKAGE_PIN Y19} [get_ports {da_data[6]}]
set_property -dict {IOSTANDARD LVCMOS33 PACKAGE_PIN P15} [get_ports {da_data[5]}]
set_property -dict {IOSTANDARD LVCMOS33 PACKAGE_PIN P16} [get_ports {da_data[4]}]
set_property -dict {IOSTANDARD LVCMOS33 PACKAGE_PIN W18} [get_ports {da_data[3]}]
set_property -dict {IOSTANDARD LVCMOS33 PACKAGE_PIN W19} [get_ports {da_data[2]}]
set_property -dict {IOSTANDARD LVCMOS33 PACKAGE_PIN R16} [get_ports {da_data[1]}]
set_property -dict {IOSTANDARD LVCMOS33 PACKAGE_PIN R17} [get_ports {da_data[0]}]
set_property -dict {IOSTANDARD LVCMOS33 PACKAGE_PIN R18} [get_ports da_clk]
set_property -dict {IOSTANDARD LVCMOS33 PACKAGE_PIN U17} [get_ports {ad_data[7]}]
set_property -dict {IOSTANDARD LVCMOS33 PACKAGE_PIN T16} [get_ports {ad_data[6]}]
set_property -dict {IOSTANDARD LVCMOS33 PACKAGE_PIN Y16} [get_ports {ad_data[5]}]
set_property -dict {IOSTANDARD LVCMOS33 PACKAGE_PIN Y17} [get_ports {ad_data[4]}]
set_property -dict {IOSTANDARD LVCMOS33 PACKAGE_PIN T15} [get_ports {ad_data[3]}]
set_property -dict {IOSTANDARD LVCMOS33 PACKAGE_PIN T14} [get_ports {ad_data[2]}]
set_property -dict {IOSTANDARD LVCMOS33 PACKAGE_PIN W16} [get_ports {ad_data[1]}]
set_property -dict {IOSTANDARD LVCMOS33 PACKAGE_PIN V16} [get_ports {ad_data[0]}]
set_property -dict {IOSTANDARD LVCMOS33 PACKAGE_PIN V18} [get_ports ad_clk]
set_property -dict {IOSTANDARD LVCMOS33 PACKAGE_PIN V17} [get_ports ad_otr]
5、下载验证
将高速 AD-DA 模块插入 ZYNQ 领航者开发板的扩展口(靠近 TF_CARD 接口的位置),连接时注意扩展口电源引脚方向和开发板电源引脚方向一致,然后将下载器一端连接电脑,另一端与开发板上对应端口连接,最后连接电源线并打开电源开关。
领航者开发板硬件连接实物图如图 27.5.1 所示:
将工程生成的比特流文件下载到 ZYNQ 中后,然后使用示波器测量 DA 输出通道的波形。首先将示波器带夹子的一端连接到开发板的 GND 位置(可使用杜邦线连接至开发板上的任一的 GND 管脚),然后将另一端探针插入高速 AD-DA 模块的 DA 通道中间的金属圆圈内(注意将红色的保护套拿掉),如图 27.5.2 所示。或者也可以直接测试高速 AD-DA 模块的 TP 引脚,如图 27.5.3 所示。
此时观察示波器可以看到模拟波的波形,如果观察不到波形,可查看示波器设置是否正确,可以尝试按下示波器的“AUTO”,再次观察示波器波形。示波器的显示界面如下所示:
通过按下按键 key0 可实现波形的的切换:
通过按下按键 key1 可实现频率的切换:
在示波器上观察到正确的波形后,说明 DDS 的功能已经实现了。关于生成的模拟波形除了可以在示波器上观察,也可以通过 Ila 去进行观察,在 ILA 中观察 ad_data 数据的具体的做法可以参照高速 AD/DA 实验里的步骤。