FPGA学习(五)——DDS信号发生器设计

FPGA学习(五)------DDS信号发生器设计

目录

一、FPGA开发中常用IP核------ROM/RAM/FIFO

常见的FPGA存储器有3种,RAM(随机访问内存)ROM(只读存储器)FIFO(先入先出)

这三种存储器的区别如下

其中RAM 通常都是在掉电之后就丢失数据,ROM在系统停止供电的时候仍然可以保持数据

可以向RAM和ROM中的任意位置写入数据,也可以读取任意的位置的数据

FIFO的数据先入先出,先进去的数据先出来,只能顺序写入数据,顺序的读出数据,其数据地址由内部读写指针自动加1完成,不能像普通存储器那样可以由地址线决定读取或写入某个指定的地址。

这三种存储器的应用场合

RAM和ROM常用于存储指令或者中间的数据

FIFO常用于数据传输通道中用于缓存数据,避免数据丢失,如不同速率时钟模块间的数据传输就需要用到异步FIFO

1、ROM简介

​ ROM 是只读存储器(Read-Only Memory)的简称,是一种只能读出事先所存数据的固态半导体存储器。其特性是一旦储存资料就无法再将之改变或删除,且资料不会因为电源关闭而消失。而事 实上在 FPGA 中通过 IP 核生成的 ROM 或 RAM(RAM 将在下一节为大家讲解)调用的都是FPGA 内部的 RAM 资源,掉电内容都会丢失(这也很容易解释,FPGA 芯片内部本来就没有掉电非易失存储器单元)。用 IP 核生成的 ROM 模块只是提前添加了数据文件(.coe 格式)(.mif/.hex格式),在 FPGA 运行时通过数据文件给 ROM 模块初始化,才使得 ROM 模块像个"真正"的掉电非易失存储器;也正是这个原因,ROM 模块的内容必须提前在数据文件中写死,无法在电路中修改。

最简单的使用有效时钟CLKA、有效地址ADDRA和有效使能EA,就可以输出DOUTA

单端口ROM:只提供一个独立的地址端口核一个读数据端口

双端口ROM:有两个地址端口,和两个读数据端口

2、ROM文件的设置

(1)直接编辑法

在Edit找到Custom Fill Cells

设置完成后,将文件保存,然后用编辑器打开看一下

(2)用C语言等软件生成初始化文件

当需要初始化的存储单元变多时,上述手工输入数据的方法就不太实用了。在了解mif文件的格式的基础上,可以自己编写C语言程序或者使用MATLAB程序自动生成mif文件。

以下是产生128X8位正弦波形数据的C语言源程序 :

c 复制代码
#include<stdio.h>
#include<math.h>
#define PI 3.141592
#define DEPTH 128 //数据深度,即存储单元的个数
#define WIDTH 8 //存储单元的宽度
int main(void)
{ 
int n, temp;
float v;
FILE *fp;
/*建立名为Data_sine.mif的新文件,允许写入数据*/
fp= fopen("Data_sine.mif","w+");
if(NULL==fp)
printf("Can not creat file!\r\n");
else
{printf("File created successfully\n");
/*生成文件头,注意不要忘了分号*/

fprintf (fp, "DEPTH = %d;\n ",DEPTH) ;
fprintf (fp,"WIDTH = %d;\n",WIDTH);
fprintf (fp,"ADDRESS_RADIX = HEX;\n");
fprintf(fp, "DATA_RADIX = HEX;\n");
fprintf(fp,"CONTENT\n");
fprintf (fp," BEGIN\n");
/*以十六进制输出地址和数据 */
for (n=0;n<DEPTH;n++)
{
/*周期为128个点的正弦波*/
v= sin(2*PI*n/DEPTH);
/*将-1~1之间的正弦波的值扩展到0~255之间*/
temp=(int)((v+1)*255/2);//v+1将数值平移到lO~2之间
/*以十六进制形式输出地址和数据*/
fprintf(fp, "%x\t:\t%x;\n",n, temp);
}
fprintf(fp,"END;\n");
fclose(fp);
}
}

编辑上述代码后生成myimf.exe文件,在资源管理器中双击运行exe文件,生成Data_sine.mif文件

3、ROM IP核配置调用

在Tools->IP Catalog->搜索ROM

1-PORT单端口,2-PORT双端口,此次以单端口为例

How wide should the 'q' output bus be?

How many x-bit words of memory?

配置ROM的位宽和深度

What should the memory block type be?

使用什么类型的内存

在官方资料中,有提及什么类型的芯片适合用什么类型的内存

寄存器时钟和复位信号的设置,设置输出端q直接输出,不通过寄存器,设置时钟使能端(clken)

Create one dock enable signal for each dock signal. Note: All registered ports are controlled by the enable signal(s)

是否创建一个时钟使能信号

Create an 'adr'asynchronous dear for the registered ports

是否创建一个清零信号

存储器初始化,并指明初始化ROM的数据文件

使用mif文件对ROM进行初始化,文件名为黑色则为添加成功,红色则为失败

ROM子模块参数设置完成后,为了测试其功能,新建一个顶层文件进行功能测试。代码如下:

复制代码
module Sine_Signal(q_sig,address_sig,clock_sig,clken_sig);

	output [7:0] q_sig;
	output [7:0]  address_sig;
	input clken_sig;
	input clock_sig;
	
IPROM	IPROM_inst (
	.address ( address_sig ),
	.clken ( clken_sig ),
	.clock ( clock_sig ),
	.q ( q_sig )
	);


endmodule

注意实例化部分应该与生成的_inst.v文件内容一致。

仿真波形如下:

二、DDS信号发生器设计

DDS是直接数字式频率合成器(Direct Digital Synthesizer)的英文缩写,是一项关键的数字化技术。与传统的频率合成器相比,DDS具有低成本、低功耗、高分辨率和快速转换时间等优点,广泛使用在电信与电子仪器领域,是实现设备全数字化的一个关键技术。作为设计人员,我们习惯称它为信号发生器,一般用它产生正弦、锯齿、方波等不同波形或不同频率的信号波形,在电子设计和测试中得到广泛应用。

DDS的基本结构主要由相位累加器、相位调制器、波形数据表ROM、D/A转换器等四大结构组成,其中较多设计还会在数模转换器之后增加一个低通滤波器。

1、相位累加器设计

复制代码
//=====相位累加器和数据锁存器===== 
module addr_cnt(CPi,K,ROMaddr, Address); 
	input CPi; //系统基准时钟(100MHz) 
	input [12:0] K; //13位频率控制字 
	output reg [9:0] ROMaddr; //10位ROM地址 
	output reg [16:0] Address; //17位相位累加器地址信号 
always @(posedge CPi) 
begin 
	Address = Address + K; 
	ROMaddr = Address[16:7]; 
end 
 
endmodule

2、波形存储器ROM设计

(1)方波模块

由于方波的实现算法相对简单,可以不用ROM表,直接用寄存器来保存方波的输出值。方波只有高、低电平两种状态,因此只需要在一个周期的中间位置翻转电平即可。其实现原理如下:由于相位累加器的值是线性累加的,因此地址值(Address)也是线性累加的,对地址值Address进行判断,当地址值的最高位为0时,便将存储波形幅值的存储器的每一位赋值为1,否则赋值为0。具体源程序如下:

复制代码
//=====方波产生模块:squwave. v ====== 
module squwave(CPi,RSTn, Address, Qsquare); 
	input CPi; //系统基准时钟(100MHz)
	input RSTn;
	//同步清零
	input [16:0]Address;//17位地址输入信号
	output reg[11:0] Qsquare; //输出方波信号,12位
always @(posedge CPi)
if(!RSTn) Qsquare=12'h000;//同步清零
else begin
	if(Address<=17'hOFFFF)
		Qsquare=12'hFFF;//输出高电平
	else Qsquare=12'h000;//输出低电平
end
endmodule
(2)正弦波形存储器模块
c 复制代码
#include <stdio.h>
#include <math.h>
#define PI 3.141592
#define DEPTH 1024 //数据深度,即存储单元的个数
#define WIDTH 12//存储单元的宽度
int main(void)
{
	int n, temp;
	float v;
	FILE*fp;
	/*建立文件名为Sine1024.mif的新文件,允许写入数
	据,对文件名没有特殊要求,但扩展名必须为.
	mif*/fp=fopen("'Sine1024. mif", "w+");
	if(NULL== fp)
		printf(" Can not cr eat file!r\n");
	else
	{ 
		printf(" File created successfully!\n");
		/*生成文件头,注意不要忘了";" */
		fprintf(fp, "DEPTH =%od;\n",DEPTH);
		fprintf(fp, "WIDTH =%od;\n",WIDTH);
		fprintf(fp, "ADDRESS RADIX=HEX;\n");
		fprintf(fp, "DATA RADIX=HEX;(n");
		fprintf(fp, "CONTENT\n");
		fprintf(fp,"BEGIN\n");
		/*以十六进制输出地址和数据*/
		for(n=0;n<DEPTH;n++) 
		{/*周期为1024个点的正弦波*/
		v= sin(2*PI*n/DEPTH);
		/*将-1~1之间的正弦波的值扩展到0~4095之间*/
		temp=(int)((v+1)*4095/2);//v+1将数值平移到0~2之间
		/*以十六进制输出地址和数据*/
		fprintf(fp, "%x\t:t%x;\n",n, temp);
		} 
		fprintf(fp, "END;\n");
		fclose(fp);//关闭文件
	} 
} 

3、锁相环倍频电路设计

使用QuartusPrime软件调用宏模块定制一个100MHz的锁相环模块。其过程是:选择Tool一MegaWizardPlug-InManager命令,启动MegaWizard工具,选择左栏I/O项目下的ALTPLL(嵌入式锁相环),定制一个名称为PLL100M_CP的时钟模块,该模块的输入inclk0为50MHz时钟信号,输出c0为100MHz的脉冲信号,占空比为50%,带有相位锁定指示输出端locked,模块符号如图8.7.9所示。

4、顶层电路设计

将上述各个模块逐个级联起来就可以得到波形产生器的顶层模块,其代码如下:

复制代码
//=DDS的顶层模块:DDS top.V =
module DDS top(CLOCK 50, RSTn, WaveSel,K,WaveValue, LEDG, CLOCK 100);
input CLOCK 5O;
input RSTn;
input [1:0] WaveSel;
input [12:0] K;
output reg [1l:0] WaveValue;
wire [9:0] ROMaddr;
wire [16:0]Address;
wire[11:0]Qsine,Qquar;
output [0:0]LEDG;
output CLOCK_100;
wire CPi = CLOCK_100;
PLL100M_CP PLL100M_CP_inst (
	.inclk0(CLOCK 50 );
//50MHz时钟输入
	.c0(CLOCK_100),
//100MHz时钟输出
	.locked (LEDG[O])
);
addr_cnt U0_instance(CPi,K,ROMaddr, Address);
SineROM ROM_inst (
	.adres (ROMaddr),
	.clock(CPi ),
	.q(Qsine)
);
	squwave U1(CPi,RSTn,Address,Qsquare);
	always @(posedge CPi)
begin
	case(WaveSel)
	2'b01:WaveValue= Qsine;
	2'b10:WaveValue=Qsquare;
	default:WaveValue= Qsine;
	endcase
end
endmodule

5、最终实现

使用DE2-115开发板来验证上述设计。用析出的50MHz晶振作为时钟输入,用KEY3控制方波清零,用SW12~SW0设置频率控制字,SW7、SW6用来选择输出波形的各类,用LEDGe作为 PLL的相位锁定指示。为方便引脚分配,新建一个顶层文件:

复制代码
module DE2_115_DDS_top(CLOCK_50,KEY,SW,GPIO_0,LEDG);
	input CLOCK_50;
	input [3:3] KEY;
	input [17:0] SW;
	output [12:0] GPIO_0;
	output [0:0] LEDG;
	wire CLOCK_100;
	assign GPIO_0[12]=CLOCK_100;
	wire RSTn=KEY[3];
	wire[1:0] WaveSel=SW[17:16];
	wire [12:0] K=SW[12:0];
	wire [11:0] WaveValue;
	assign GPIO_0[11:0]=WaveValue;
	DDS_top DE2(CLOCK_50,RSTn,WaveSel,K,WaveValue,LEDG,CLOCK_100);
endmodule

6、仿真波形

相关推荐
菜一头包1 小时前
GNU,GDB,GCC,G++是什么?与其他编译器又有什么关系?
linux·c++·学习·gnu
暴富奥利奥2 小时前
基于 OpenCV 的图像与视频处理
opencv·学习
夏季疯2 小时前
学习笔记:黑马程序员JavaWeb开发教程(2025.3.23)
java·笔记·学习
虾球xz3 小时前
游戏引擎学习第238天:让 OpenGL 使用我们的屏幕坐标
学习·游戏引擎
爱吃羊的老虎4 小时前
【verilog】多个 if 控制同一个变量(后面会覆盖前面)非阻塞赋值真的并行吗?
fpga开发
苦逼IT运维5 小时前
Git LFS 学习笔记:原理、配置、实践与心路历程
笔记·git·学习
码银5 小时前
【Java】接口interface学习
java·开发语言·学习
DKPT6 小时前
重构之去除多余的if-else
java·开发语言·笔记·学习·面试
pumpkin845146 小时前
学习笔记十五——rust柯里化,看不懂 `fn add(x) -> impl Fn(y)` 的同学点进来!
笔记·学习·rust