基于FPGA频率可调的信号发生器设计
1赞今天做了一个信号发生器的实验,可以产生任意频率的信号发生器(需要通过改参数实现),在总结信号发生器的设计之前,我们还需要介绍两个小东西:单口rom和mif文件。
1、单口rom
rom,read only memory,中文名:只读存储器,不知道会不会有童鞋rom和ram傻傻分不清楚,其实只要知道他们的英文名称还是很好区分的嘛。在我们本次实验中ROM的作用就是用来存放波形数据的。大家都知道,我们的正弦波,余弦波,三角波等等都是连续信号,也叫模拟信号。而我们的FPGA是属于数字逻辑器件,只能处理数字信号。所以我们在处理这些连续波形的时候需要做的第一件事就是将波形离散化。
就拿图上这个正玄波来说,如果我们设定采样深度是256的话,也就是把一个周期的正弦波均分为0-255个点,然后求每个点对应的值,把这个值按顺序保存到rom中。当然不用我们自己来求这个函数值,现在有很多工具都可以进行这样的一个离散化,像MATLAB,以及网络上很多mif文件小工具。
2、mif文件
mif文件,全名memory initialization file。意思是存储器初始化文件,像我们的rom是只读存储器,我们在使用的的时候是没办法对他写入数据的,所以我们在调用rom的IP核的时候需要对他进行初始化设置,存入我们需要的值。前面介绍正弦波离散化的时候,离散得到各个点的值,我们就是通过生成mif文件最后保存到rom中。
这就是我们将sin函数离散化后生成的一个mif文件,从左到右,从上到下依次保存着0-255各个点的值。
系统框架
下面我们来画一下整个系统的一个框图。
注:图中addr、三条data线、以及data_out线都是多位宽的总线
我们用两个按键来分别控制产生信号的种类以及信号的频率。三个rom中分别存储着三个信号离散采样后的值。我们可以定义一个标志信号,当按键按下一次,标志信号加1,标志信号从0-1循环变化,当信号取不同的值时,就输出不同的波形,由于一次只输出一种波形,所以我们只用一个地址信号就可以了,对三个rom输出同一个地址,取出来的值,我们根据标志信号来决定输出哪一个。
频率可调的原理
这个实验的难点在于,我们如何得到任意想要的频率?虽然我在代码中只定义了4种频率,但是其实通过更改参数,我们可以得到任意想要的频率。这个该怎么实现,其实是非常有趣的一个问题。
我们每给rom一个地址,就能读出一个数据。我们定义采样深度256,那么rom的地址范围就是0-255。所以我们让控制器循环的输出0-255的地址,就能把rom里的数据读出来。现在假设系统时钟是50MHZ,如果每一个时钟地址加一,那么我们输出的信号频率就是50M/256 = 195.3K。是这样吧?
现在我们要改变这个频率有哪些方法?
从上面这个公式看,可以分别从分子和分母两方面入手。先来说说分子,刚才我们是每个时钟让地址加一,如果我们对时钟进行一个计数,让每两个时钟让地址加一,那么输出信号的频率是不是为原来的1/2,如果是三个时钟让地址加一,那么结果就是1/3。可以理解吧?当然也可以用pll倍频,倍频的结果就是2倍,3倍啦。
现在再看分子,256是事先已经存在ROM里的数据的深度,由于ROM的只读性,显然256是不能改变的。但是这并不代表我们不能在分母上做文章。你有256个数据,我可不可以只读128个呢?根据奈奎斯特采样定理:在进行模拟/数字信号的转换过程中,当采样频率fs.max大于信号中,最高频率fmax的2倍时,即:fs.max>=2fmax,则采样之后的数字信号完整地保留了原始信号中的信息。现在对于这一个周期的信号,如果我们采两个点,是不是就已经是原始信号频率的两倍了。虽然点数越少,波形失真越厉害,但是并不是完全不能做的。所以我们让每个时钟周期地址加二,那么输出的信号频率是不是就是195.3K的两倍了?加3就是3倍,依次类推。
我想各位一定已经有所眉目了吧?哈哈,把改变分子分母结合起来不就可以随意增大和减小输出信号的频率了吗?too young too simple。这样的话,不管我们怎么改变是不是都是和195.3k有关,怎么都摆脱不了这个梗。如果我们需要一个500k,800k的任意信号呢?
今天我就介绍一种更简单的方法。前面那个公式说了输出信号 = 50M/256。这里的256 = 2^8。所以我们的地址之前定义[7:0]就够了。现在回顾一下,我们刚刚从改变分子分母分别说了两种方法,一种是让时钟计数,计多次,地址加一下;另一种是地址每加一下加的不止1。我们现在换个角度想一下。我们不通过计数的方式,我们把地址位宽拓展一下,原来8位,假设我们拓展为32位,最终输出的地址取高8位。那么输出信号的最低频率是不是 = 50M/2^32 = 0.01164HZ。如果我们想要其他的频率,再在这个基础上乘就行了。怎么乘前面是不是已经讲过了,如果我要得到1K的,1K/0.01164 = 85911,我们让地址计数器每个时钟来了加85911。这种方法你们还满意吗?
代码解析
最后看代码,我就只贴ROM控制器的代码了
对照着前面的系统框架来看,我们的控制器模块,除了时钟和复位之外,还有两个按键输入,三个data信号输入,一个地址输出,一个最终的波形输出。
模块中定义了两个flag,分别为flag_style 和flag_freq,由按键控制,分别决定这输出信号的种类和频率。当按键按下,flag的值改变,当flag_style 等于不同的值时,data_out分别连接到不同的data输入上,就可以实现输出不同的信号波形。当flag_freq等于不同的值时,就让地址计数器每个时钟周期加上的值不同。由于定义了4种频率,所以flag_freq有4种可能。而且每当有按键按下时,我们都将地址恢复到初始相位,让波形从头开始输出。
modelsim波形分析
原来modelsim也能输出正弦波,选中波形信号,先右键—>radix将它改为无符号十进制数,或者十六进制数,然后再右键—>format—>analog(automatic),波形就粗来啦。
第一张图,正弦波切换为方波,波形上方的11、10,表示按键值,按键默认高电平,按下时为0。
key[0]键按下时,正弦波切换为方波,第二张图key[1]键按下时,方波频率改变。第三张图该三角波露脸了,两条红线之间是三角波的一个周期,黑色字体显示的是三角波的频率——500K,与设定的一致,还是蛮准的嘛。