(DDS)正弦波形发生器——幅值、频率、相位可调(一)
(DDS)正弦波形发生器——幅值、频率、相位可调
一、项目任务:
- 设计一个幅值、频率、相位均可调的正弦波发生器。
- 频率每次增加1kHz。
- 相位每次增加 2*PI/256
- 幅值每次增加两倍
二、文章内容:
- DDS的核心原理。
- 分别使用两种方式完成频率可调(a、b),并且进行对比(c),最后对b进行优化(d)。
- 完成赋值、频率、相位可调的正弦波形发生器。(文章二)
1、DDS核心原理:
读取ROM中存储的波形数据获得一个基础波形(基频),之后不断进行循环读取。
幅值——ROM中取得数据使用乘法进行放大。
相位——改变从ROM中读取时,地址的初值。
调频——ROM时钟固定,控制读取ROM的地址来控制输出频率:
- 系统时钟为50MHz,ROM位宽为8,深度256。
- 有一个思路就是:先确定一个最小的频率,然后不断的对该频率进行放大,即可以控制频率的大小。反过来讲,就是先使用一种最慢的控制地址的读取ROM数据的方式,然后缩小读取的时间即可放大频率。
- 那么如何确定基频的大小呢?根据采取方法不同基频大小不定,但是为了频率的准确有以下几点需要注意:
- 基频小些比较好
- 基频越接近1、0.1、0.01、0.001······越好
2、两种产生基频的方式
a、32位寄存器基频:
使用一个32为寄存器,将高8位作为ROM的地址位,其余24位无特殊作用,寄存器按照系统时钟进行自加,此时高8位的变化频率与用于自加的系统时钟频率有了成倍的关系,很像计数器计数频率变化但存在区别,之后会讨论到。
系统时钟为50MHz,ROM位宽为8,深度256。那么基础频率为:
\[% MathType!MTEF!2!1!+-
% feaahqart1ev3aaatCvAUfeBSjuyZL2yd9gzLbvyNv2CaerbuLwBLn
% hiov2DGi1BTfMBaeXatLxBI9gBaerbd9wDYLwzYbItLDharqqtubsr
% 4rNCHbWexLMBbXgBd9gzLbvyNv2CaeHbl7mZLdGeaGqiVu0Je9sqqr
% pepC0xbbL8F4rqqrFfpeea0xe9Lq-Jc9vqaqpepm0xbba9pwe9Q8fs
% 0-yqaqpepae9pg0FirpepeKkFr0xfr-xfr-xb9adbaqaaeGaciGaai
% aabeqaamaabaabauaajqgaG9FaceaapgGaamOzaOWaaSbaaSqaaiaa
% dkgaaeqaaOGaeyypa0ZaaSaaaeaacaaI1aGaaGimaiabgEna0kaaig
% dacaaIWaWaaWbaaSqabeaacaaI2aaaaaGcbaGaaGOmamaaCaaaleqa
% baGaaGOmaiaaisdaaaGccqGHxdaTcaaIYaGaaGynaiaaiAdaaaGaey
% ypa0JaaGimaiaac6cacaaIWaGaaGymaiaaigdacaaI2aGaaGinaiaa
% igdacaaI1aGaamisaiaadQhaaaa!5ABC!
{f_b} = \frac{{50 \times {{10}^6}}}{{{2^{24}} \times 256}} = 0.0116415Hz
\]所以产生一个1kHz的正弦波(1kHz的ROM地址变化速度)只需要将基频放大1000/0.0116415 = 85899.34592,即扩大85899倍。
所以若想频率每次增加1kHz,在低24位每次自加85899即可,此时85899为频率控制字。
module addr_ctrl(
input clk,
input rst_n, output [7:0] addr
); reg [31:0] address;
assign addr = address[31:24]; always @(posedge clk,negedge rst_n)
begin
if(rst_n == 0)
address <= 32'd0;
else
//address <= address + 32'd858996; //10kHz
address <= address + 32'd85899;
end endmodule

通过仿真可以看到准确的产生了1kHz的正弦波
//顶层
module digital_adds(
input clk,
input rst_n, output [7:0] data
); wire [7:0] addr; addr_ctrl addr_ctrl_inst(
.clk (clk),
.rst_n(rst_n), .addr (addr)
); rom1 rom1_inst (
.address ( addr ),
.clock ( clk ),
.q ( data )
); endmodule
//测试文件
`timescale 1ns/1ps
module digital_adds_tb();
reg clk;
reg rst_n; wire [7:0] data; digital_adds digital_adds_inst(
.clk (clk),
.rst_n(rst_n), .data (data)
); initial clk = 1;
always #10 clk = !clk; initial begin //同步复位信号需要时钟上升沿检测
rst_n = 0;
#200
rst_n = 1;
#5000000 $stop;
end endmodule
b、计数器产生基频计算:
系统时钟为50MHz,ROM位宽为8,深度1024。
使用计数器产生1Hz的基频,方便之后扩频计算
\[% MathType!MTEF!2!1!+-
% feaahqart1ev3aaatCvAUfeBSjuyZL2yd9gzLbvyNv2CaerbuLwBLn
% hiov2DGi1BTfMBaeXatLxBI9gBaerbd9wDYLwzYbItLDharqqtubsr
% 4rNCHbWexLMBbXgBd9gzLbvyNv2CaeHbl7mZLdGeaGqiVu0Je9sqqr
% pepC0xbbL8F4rqqrFfpeea0xe9Lq-Jc9vqaqpepm0xbba9pwe9Q8fs
% 0-yqaqpepae9pg0FirpepeKkFr0xfr-xfr-xb9adbaqaaeGaciGaai
% aabeqaamaabaabauaajqgaG9FaceaapgGaamOzaOWaaSbaaSqaaiaa
% dkgaaeqaaOGaeyypa0ZaaSaaaeaacaaIXaGaaGimamaaCaaaleqaba
% GaaGyoaaaaaOqaaiaaikdacaaIWaGaey41aqRaaGymaiaaicdacaaI
% YaGaaGinaiabgEna0kaaisdacaaI4aGaaGioaiaaisdacaaI4aaaai
% abg2da9iaaigdacaGGUaGaaGimaiaaicdacaaIWaGaaGimaiaaicda
% caaIYaGaaGynaiaaiAdacaaIWaGaaGimaiaaicdacaaI2aGaaGynai
% aadIeacaWG6baaaa!6128!
{f_b} = \frac{{{{10}^9}}}{{20 \times 1024 \times 48828}} = 1.0000025600065Hz
\]使用1024个数据,每个为20ns,共计48848次。
则频率控制字为1000时即可产生1kHz的正弦波。
//频率控制模块
module addr_ctrl(
input clk,
input rst_n, output reg [9:0] addr
); reg [15:0] cnt; always @(posedge clk,negedge rst_n)
begin
if(rst_n == 0)
cnt <= 16'd0;
else
if(16'd48848 <= cnt)
cnt <= 16'd0;
else
cnt <= cnt + 16'd1000; //频率控制字1kHz的基频
end always @(posedge clk,negedge rst_n)
begin
if(rst_n == 0)
addr <= 10'd0;
else
if(16'd48848 <= cnt)
addr <= addr + 10'd1;
else
addr <= addr;
end endmodule

图中可以看到,得到了977Hz的正弦波,存在着较大的误差,实际效果与理论计算严重不符,这是为什么呢?
c、对比
32位寄存器使用低位向高位进位可以确保每次加的数据都产生了效果,都向高位产生了进位。
相比使用计数器产生的基频很准确但是有着非常致命的缺点:
当频率控制字不能被计数器最大值整除,即当频率控制字即将累加到计数器最大值时,由于不能整除。可能还差一点点计数器就符合判断要求了,即已经非常接近我们预设的地址变化频率了,但是仍然不满足判断标准,必须等到下一次频率控制字的累加才可以使地址加一,这里就产生了理论计算和实际情况不同的问题。
其关键就是计数器进位是严格按照条件执行的少一点会不执行而多一点会被吞掉直接置零,舍弃掉了余数,并且由于cnt的初值从零开始且每次增加1000,说以实际上计算为:
\[% MathType!MTEF!2!1!+-
% feaahqart1ev3aaatCvAUfeBSjuyZL2yd9gzLbvyNv2CaerbuLwBLn
% hiov2DGi1BTfMBaeXatLxBI9gBaerbd9wDYLwzYbItLDharqqtubsr
% 4rNCHbWexLMBbXgBd9gzLbvyNv2CaeHbl7mZLdGeaGqiVu0Je9sqqr
% pepC0xbbL8F4rqqrFfpeea0xe9Lq-Jc9vqaqpepm0xbba9pwe9Q8fs
% 0-yqaqpepae9pg0FirpepeKkFr0xfr-xfr-xb9adbaqaaeGaciGaai
% aabeqaamaabaabauaajqgaG9FaceaapgGaamOzaOWaaSbaaSqaaiaa
% dkgaaeqaaOGaeyypa0ZaaSaaaeaacaaIXaGaaGimamaaCaaaleqaba
% GaaGyoaaaaaOqaaiaaikdacaaIWaGaey41aqRaaGymaiaaicdacaaI
% YaGaaGinaiabgEna0kaacIcacaaI0aGaaGyoaiabgUcaRiaaigdaca
% GGPaGaey41aqRaaGymaiaaicdacaaIWaGaaGimaaaacqGH9aqpcaaI
% WaGaaiOlaiaaiMdacaaI3aGaaGOnaiaaiwdacaaI2aGaaGOmaiaaiw
% dacaWGibGaamOEaaaa!6290!
{f} = \frac{{{{10}^{12}}}}{{20 \times 1024 \times (49 + 1) \times 1000}} = 976.5625Hz
\]
该计算结果和仿真得到的结果相同,并且可以看到仿真保留了三位有效数字。
也就是说b法误差有两点原因:
- 忘记对cnt从零开始进行处理。
- 算法本身带来的一定误差。
- 忘记对cnt从零开始进行处理。
而低位向高位进位得到的计数器:
- 从零开始几乎不影响最终效果因为不参与循环不会累计误差
- 不会舍弃余数而是累加了起来
传统方法b还是好用一些,,,
我竟然品出了一点连续和离散的感觉出来。
d、对b进行修改
理论计算:
\[% MathType!MTEF!2!1!+-
% feaahqart1ev3aaatCvAUfeBSjuyZL2yd9gzLbvyNv2CaerbuLwBLn
% hiov2DGi1BTfMBaeXatLxBI9gBaerbd9wDYLwzYbItLDharqqtubsr
% 4rNCHbWexLMBbXgBd9gzLbvyNv2CaeHbl7mZLdGeaGqiVu0Je9sqqr
% pepC0xbbL8F4rqqrFfpeea0xe9Lq-Jc9vqaqpepm0xbba9pwe9Q8fs
% 0-yqaqpepae9pg0FirpepeKkFr0xfr-xfr-xb9adbaqaaeGaciGaai
% aabeqaamaabaabauaajqgaG9FaceaapgGaamOzaOWaaSbaaSqaaiaa
% dkgaaeqaaOGaeyypa0ZaaSaaaeaacaaIXaGaaGimamaaCaaaleqaba
% GaaGymaiaaikdaaaaakeaacaaIYaGaaGimaiabgEna0kaaigdacaaI
% WaGaaGOmaiaaisdacqGHxdaTcaaI0aGaaGyoaiabgEna0kaaigdaca
% aIWaGaaGimaiaaicdaaaGaeyypa0JaaGyoaiaaiMdacaaI2aGaaiOl
% aiaaisdacaaI5aGaaGOmaiaaiodacaaI0aGaaGOnaiaaiMdacaWGib
% GaamOEaaaa!61D7!
{f_b} = \frac{{{{10}^{12}}}}{{20 \times 1024 \times 49 \times 1000}} = 996.4923469Hz
\]
可以看到经过改进后:
- 理论计算和仿真验真结果相符。
- 产生波形的频率精度尚可。
module addr_ctrl(
input clk,
input rst_n, output reg [9:0] addr
); reg [15:0] cnt; always @(posedge clk,negedge rst_n)
begin
if(rst_n == 0)
cnt <= 16'd0;
else
if(16'd48 <= cnt) //只要取倍数就可以了,并且要注意cnt初值为零已经多循环了一次
cnt <= 16'd0;
else
cnt <= cnt + 16'd1; //频率控制字1kHz的基频
end always @(posedge clk,negedge rst_n)
begin
if(rst_n == 0)
addr <= 10'd0;
else
if(16'd48 <= cnt)
addr <= addr + 10'd1;
else
addr <= addr;
end endmodule
至此文章1、2部分已经完成,第三部分的整体代码见下文
备注:
- ROM可以通过时钟和地址两种控制方式来使用。
- 第一个方法的图中我们可以看到准确的产生1kHz的正弦波,但是基频并非整数,为什么能够准确的产生1kHz的正弦波呢?是在哪一步忽略的,由于没有AD/DA也没有示波器没办法探究真实情况。
作者:野客居/13tree
出处:https://www.cnblogs.com/13tree/
本文版权归作者所有,如需转载请保留此段声明。
(DDS)正弦波形发生器——幅值、频率、相位可调(一)的更多相关文章
- (DDS)正弦波形发生器——幅值、频率、相位可调(二)
(DDS)正弦波形发生器--幅值.频率.相位可调(二) 主要关于调相方面 一.项目任务: 设计一个幅值.频率.相位均可调的正弦波发生器. 频率每次增加10kHz 相位每次增加 PI/2 幅值每次增加两 ...
- 基于FPGA的DDS任意波形发生器设计
一.简介 DDS技术最初是作为频率合成技术提出的,由于其易于控制,相位连续,输出频率稳定度高,分辨率高, 频率转换速度快等优点,现在被广泛应用于任意波形发生器(AWG).基于DDS技术的任 ...
- 基于DDS的任意波形发生器
实验原理 DDS的原理 DDS(Direct Digital Frequency Synthesizer)直接数字频率合成器,也可叫DDFS. DDS是从相位的概念直接合成所需波形的一种频率合成技术. ...
- FFT之频率与幅值的确定(转)
FFT之后得到的是什么数 FFT之后得到的那一串复数是波形对应频率下的幅度特征,注意这个是幅度特征不是复制,下面要讲两个问题:1.如何获取频率,2.如何获取幅值 获取频率 FFT变换如何获取频率?傅里 ...
- STM32 基DMA的DAC波形发生器
DAC是STM32系列的一个基本外设,可以将数字信号转化成模拟信号,这次我将使用DAC来输出一个特定波形. 首先确定工作方法,由于我目前在做的简易示波器在输出波形的同时还需要显示输入信号,所以不能占用 ...
- 在Modelsim波形中查看值
在Modelsim的波形中查看值时,可以利用右键选择变量的数据类型.如果变量值为0,可以选择unsigned类型观察,可以1位显示0. 长度较大的数据以十六进制显示时,即使值为0,也依然显示为长度较长 ...
- Matlab绘制幅值谱和相位谱
1. 对于直接给出频响函数的情况 这里以滑动平均的频响函数作为例子,滑动窗口为[0, 4]. 上式中M2=4. >> w=0:0.001:2*pi; >> h1=1-exp(- ...
- numpy 傅立叶得到幅值和频率
做个备份,对 numpy 不熟,每次都找函数找半天. 代码里分几块: 1. 从 argc[1] 的文档中读取数据,并转化为 float.文档中有 2001 行,第一行为头,后面 2000 个是采样数据 ...
- H5录音音频可视化-实时波形频谱绘制、频率直方图
这段时间给GitHub Recorder开源库添加了两个新的音频可视化功能,比以前单一的动态波形显示丰富了好多(下图后两行是不是比第一行看起来丰满些):趁热打铁写了一个音频可视化相关扩展测试代码,下面 ...
随机推荐
- 第六个知识点:我们怎么把NP问题解释成一组可以在多项式内证明的命题
第六个知识点:我们怎么把NP问题解释成一组可以在多项式内证明的命题 原文地址:http://bristolcrypto.blogspot.com/2014/11/52-things-number-6- ...
- 「双串最长公共子串」SP1811 LCS - Longest Common Substring
知识点: SAM,SA,单调栈,Hash 原题面 Luogu 来自 poj 的双倍经验 简述 给定两字符串 \(S_1, S_2\),求它们的最长公共子串长度. \(|S_1|,|S_2|\le 2. ...
- MySql各事务隔离级别及锁问题
聊事务隔离级别和锁问题之前首先得理解事务的隔离级别和共享锁及独占锁的概念: 事务的隔离级别: 脏读 不可重复读 幻读 Read uncommitted √ √ √ Read committed × ...
- 【计理05组01号】R 语言基础入门
R 语言基本数据结构 首先让我们先进入 R 环境下: sudo R 赋值 R 中可以用 = 或者 <- 来进行赋值 ,<- 的快捷键是 alt + - . > a <- c(2 ...
- git -remote: Support for password authentication was removed on August 13, 2021
克隆代码时,报错: Support for password authentication was removed on August 13, 2021. Please use a personal ...
- Go语言实战爬虫项目
Go语言爬虫框架之Colly和Goquery Python爬虫框架比较多有requests.urllib, pyquery,scrapy等,解析库有BeautifulSoup.pyquery.Scra ...
- Sentry 企业级数据安全解决方案 - Relay 操作指南
内容整理自官方文档 本篇回顾了我们在自托管外部使用 Relay 时的操作指南,即在您的硬件上运行的 Relay 并将事件转发到 sentry.io. 系列 Sentry 企业级数据安全解决方案 - R ...
- Golang 常见设计模式之选项模式
熟悉 Python 开发的同学都知道,Python 有默认参数的存在,使得我们在实例化一个对象的时候,可以根据需要来选择性的覆盖某些默认参数,以此来决定如何实例化对象.当一个对象有多个默认参数时,这个 ...
- fastjson字符串转JSON的$ref问题
先说结论: fastjson在把对象转换成字符串的时候,如果遇到相同的对象的时候,默认开启引用检测将相同的对象写成引用的形式. 官网文档:https://github.com/alibaba/fast ...
- (转)Make 命令
Make 命令 代码变成可执行文件,叫做编译(compile):先编译这个,还是先编译那个(即编译的安排),叫做构建(build). Make是最常用的构建工具,诞生于1977年,主要用于C语言的项目 ...