文章目录

利用fpga实现dds输出的方案详解

一.什么是dds?二.dds在fpga中是怎么实现的?1.从哪里读?2. 怎么读?:

三.软件实现:1.quartus:第一步:第二步:第三步:第四步:第五步:第六步:

2.vivado:第一步:第二步:第三步:第四步:第五步:

四.代码:五.测试结果:

最近在整理电脑文件,发现之前准备电赛时写的程序太占用内存了,准备删掉。趁删掉之前,我打算记录一些在网站上,对当时的学习历程进行一些总结和回顾。 当时电赛所采用的fpga是因特尔的CycloneIV,软件是quartus,但现在因为课程需要,打算在vivado上也进行重新实现,希望能够对vivado更加熟悉一些。这两者的方法都会在下面的文章中进行体现。当然,程序对两者是完全相同的,我们充分体会到了fpga的可移植性和便利性。 以下我的理解都很浅显,大家谨慎观看,欢迎批评指正。

利用fpga实现dds输出的方案详解

(对原理、操作不感兴趣的朋友可以直接移步代码)

一.什么是dds?

dds就是Direct Digital Synthesizer,直接数字式频率合成器。我们在电赛中常说的“利用dds进行输出”,其实指的是利用dds输出一个模拟信号,即“dds信号发生器”,目前高校实验室还在使用的许多信号发生器就是基于这种原理来实现的。 fpga通过一定数量的引脚,将给定逻辑的高低电平输出,其电平的相加之和就可以成为一个模拟信号(每个电平之间存在最小逻辑电平的差值,是不平滑的)。

上面这个图片截取自quartus的signaltap。可以看出,sine_out是一个8位二进制数字,对应从地位开始,一直到高位的数字信号也进行了显示,这样就可以成为一个模拟信号进行输出。在输出端,我们通过运放电路进行组合,滤波(即DAC电路),即可得到一个较为理想的输出模拟信号。这就是dds。

二.dds在fpga中是怎么实现的?

首先,根据上面dds的原理,fpga需要做的其实只是将确定逻辑的高低电平以一定的频率进行输出即可。我们需要的只是一个存储这些高低信号的模块,以及确定读取他们的频率。

1.从哪里读?

rom示fpga中常用的存储模块。rom即read only memory,是只读的存储模块。fpga中有一个名为rom的ip核,调用它可以直接例化一个rom模块。我们需要通过matlab或者其他小工具对我们期望输出的模拟信号进行数字化,生成一串数字数组,将他们放入rom中即可。(具体的软件实现步骤将在下面进行展示)

2. 怎么读?:

在fpga中读取rom时,是输入一个地址,以及读取的时钟,他给我们的即是对应地址下面存储的数据。我们需要严格把控的是模拟信号输出的频率,这在工程中一般是十分重要的。 如果控制频率呢?原理其实很简单,我们的fpga有一个系统时钟,如果不加处理的用系统时钟读取这个rom,地址(rom_address)每次加一,那么输出的信号频率可以这样计算:

认为系统时钟为50MHz,rom中存储的是8位二进制数,希望输出的正弦信号在一个周期内被采样了256个(根据我们期望的精度来确定) 那么,Fout=50M/256=195312.5Hz

这个结果因为256作为分母,根本不准确。因此做如下改变: 我们定义一个32位的累加器F_cnt,每次不是直接将地址加一,而是:

每次对32位累加器加1,将32位累加器的最高八位取出,赋给地址: 这时的输出频率是: Fout=50M/2^32=0.01164

我们通常称上面的数字是“最小频率”,或者说是输出频率的精度。如果我们想获得确定频率的信号(1k),那么只需要做如下改变: 不再是每次系统时钟tik时,对累加器加一,而是加一个精确的数字F_word(频率控制字)。这里的原理经常被混淆,解释是:累加器必须加够2^32 才能实现一个周期的输出,加够则需要2^32/Fword次,那么Fcnt的高八位改变的频率就可以被精确控制,从而控制输出频率,比如:

输出1k的正弦信号:定义F_word=85899,Fout=50M85899/3^32=999.999。 对应的Fword=Fout/0.01164,或者fFword=Fout2^32/Fsclk

三.软件实现:

下面将分别针对对两种软件,阐明例化rom的ip核,以及导入rom文件进行的方法。

1.quartus:

第一步:

生成mif文件:使用小工具“Guagle wave maker”,按下面的指示进行配置,生成.mif文件放在quartus的工程目录下面。

第二步:

打开quartus,新建工程。(图略)

第三步:

选择魔术棒,新建一个megafunction

第四步:

输入rom查找ip核,选择rom port1,并给输出文件写一个名字rom:

第五步:

配置rom,并且通过browse将刚刚生成的.mif文件定为输出:

第六步:

一直点击next直到完成配置。

2.vivado:

第一步:

生成coe文件,文档格式和quartus不同。 {这里的文档格式是: memory_initialization_radix(存储数值的基数,就是进制数): 只能选2,10,16 memory_initialization_vector(存储的数值): 定义其中存储的数值,数据间为逗号,末尾为;,并且不能带0x等等字符,只能是确定进制的数字!!} 我们可以用matlab生成一段数据,然后用以上格式进行修改。 也可以按照下面的代码来生成coe文件(当然mif文件也可直接这样生成)

N=2^8;

s_p=0:255;

Mem_depth = 256;

Mem_width = 8;

sin_data=sin(2*pi*s_p/N);

% plot(sin_data,'r*');

% hold on;

% plot(sin_data);

fix_p_sin_data=fix(sin_data*((N / 2) -1));

for i=1:N

if fix_p_sin_data(i)<0

fix_p_sin_data(i)=N+fix_p_sin_data(i);

else

fix_p_sin_data(i)=fix_p_sin_data(i);

end

end

fid=fopen('sp_ram_256x8.mif','w+');

fprintf(fid,'WIDTH=%d;\n', Mem_width);

fprintf(fid,'DEPTH=%d;\n',Mem_depth);

fprintf(fid,'ADDRESS_RADIX=UNS;\n');

fprintf(fid,'DATA_RADIX=UNS;\n');

fprintf(fid,'CONTENT BEGIN \n');

for i=1:N

fprintf(fid,'%d:%d; \n',i-1,fix_p_sin_data(i));

end

fprintf(fid,'END; \n');

fclose(fid);

fid=fopen('sp_ram_256x8.coe','w+');

fprintf(fid,'memory_initialization_radix=10;\n');

fprintf(fid,'memory_initialization_vector= \n');

for i=1:N

if i == N

fprintf(fid,'%d; ',fix_p_sin_data(i));

else

fprintf(fid,'%d, ',fix_p_sin_data(i));

end

end

第二步:

在vivado中新建一个工程(略),将上面的coe文件放入这个工程目录当中

第三步:

选择IP category,查找rom,选择block memory generator

第四步:

对rom进行配置,在browse中选择我们刚刚生成的coe文件:

第五步:

选择OK,然后直接开始综合生成ip核,生成成功。

四.代码:

.v文件:

`timescale 1ns / 1ps

//////////////////////////////////////////////////////////////////////////////////

// Company:

// Engineer: jeffery_meng

// Create Date: 2022/03/22 12:34:09

module sin(

//1.写出所有接出接口;

clk,

reset_n,

dac_clk,//这个dac时钟是为了配合外部dac模块进行的输出

sine_out

);

input clk;

input reset_n;

output dac_clk;

output [7:0] sine_out;

wire [31:0] Fword;

wire [7:0] Pword;//Fword和Pword相当于事先定义好的可以修改的常量,不会在程序里再做赋值,所以做成wire型变量;

assign Fword=32'd858993;

//Fword=Fout/0.011641532;

//例 10k--858993;100k--8589933;5k--429497;

assign Pword=8'b0;

wire [7:0] wave_data;

reg [7:0] wave_data_r;

reg [31:0] Fcnt;//Fcnt是相位累加器,频率控制数字对他起作用;

wire [7:0]rom_addr;//reg和wire是两种变量类型,基于时序逻辑时一定要用reg,@语句中只能够对reg变量进行赋值;

assign sine_out=wave_data_r;

always @(posedge clk or negedge reset_n)

begin

if(!reset_n)

Fcnt <= 32'd0;//Fcnt:相位累加器,Fcnt是一个32位数字;

else

Fcnt <= Fcnt + Fword;//每次时钟tik,Fcnt总是步进Fword次;

end

assign rom_addr = Fcnt[31:24] + Pword;

assign dac_clk=clk;

my_rom rom(

.addra(rom_addr),

.clka(dac_clk),

.douta(wave_data)

);

always @(posedge dac_clk)

begin

wave_data_r <= wave_data;

end

endmodule

tb文件:

`timescale 1ns / 1ps

//////////////////////////////////////////////////////////////////////////////////

// Company:

// Engineer: jeffery

module tb(

);

reg clk;

reg reset_n;

wire [7:0] sine_out;

initial

begin

clk=0;

reset_n=1;

#20 reset_n=0;

#20 reset_n=1;

end

always

begin

#10 clk=~clk;//需要获得50M时钟,周期20ps,所以延时为10

end

sin my_sin(

.clk(clk),

.reset_n(reset_n),

.dac_clk(),//这个dac时钟是为了配合外部dac模块进行的输出,测试时不需要

.sine_out(sine_out)

);

endmodule

五.测试结果:

下面是在vivado上的仿真结果