锐单电子商城 , 一站式电子元器件采购平台!
  • 电话:400-990-0325

如何在FPGA中构建数控振荡器 (NCO)

时间:2024-01-04 12:07:02 h60固态继电器

在某些特殊项目中,许多信号处理需要正弦波。如果设计中可以控制正弦波的相位或频率,则通常称为数控振荡器 (NCO)。今天,让我们花点时间研究如何学习FPGA中构建一个NCO。最后,我们将介绍一个 C 该方案可用于嵌入式应用程序。

我们可以生成正弦波(查找表法,四分之一波查表法,CORDIC如何通过正弦波发生器将余弦波变成 NCO。

然而,在我们深入了解细节之前,让我们花一些时间思考如何使用 NCO。我今天提出这个原因是双重的。首先,我知道有一个学生在努力理解如何构建这样的东西作为数字通信解调器的一部分。第二部分是建立一个比较 stackoverflow 文章(https://stackoverflow.com/questions/13466623/how-to-look-up-sine-of-different-frequencies-from-a-fixed-sized-lookup-table)推荐的更好的NCO。

但几乎不可能触及你想成为的人NCO想法。考虑一个例子。...

  • 任何线性信号处理系统都以其频率响应为特征。所以我以前用过。 NCO 以及某种类型的示波器来评估信号处理算法的数字输入是否正确。

  • 我们以前用过 NCO作为演示的一部分, 证明改进的 PWM 发生器(http://zipcpu.com/dsp/2017/09/04/pwm-reinvention.html)与传统音频信号生成相比 PWM工作得更好。尽管我们当时没有讨论音调发生器的细节,但我们今天将解释很多细节。

  • 还可以使用 NCO 在频率上移动信号-或从某个频率中移动 (IF)将其基带频率降低到可以在接收器中处理,或在另一个方向上作为传输的一部分。

  • 还可以将 NCO 作为数字通信调制器或解调器的一部分。

  • 可以使用 NCO创建调幅 (AM) 信号甚至调频 (FM)信号。

  • 另一个常见的用途是通过加法合成器生成音符。事实上,我们将在下面开发NCO相位累加器部分甚至可以用于减法合成——它非常常见。

  • 最后,设计师可以完全控制它 NCO因此,频率输出 你甚至可以做一些更奇怪的事情——比如构建跳频扩频信号——这应该是你想做的。

事实上,NCO是数字信号处理 (DSP) 这里很难列出算法的基本组成部分。

什么是 NCO?

527c2890f74b1b782383e2aae955b949.png图 1. NCO

NCO 为了将频率输入转换为正弦波,数控振荡器只是一个由数字逻辑创建的振荡器,可以完全控制数字逻辑。名义上,该振荡器将接收所需的频率作为输入,并在此频率下产生数字采样的正弦波。如果选择在PLL中使用 NCO,然后将调整正弦波发生器的相位。然而,现在,将 NCO视为 接受频率输入并产生采样的简单数字逻辑电路 正弦波作为输出。

图 2. 框图

基本 NCO 的组成部分.在内部, NCO 跟踪其产生的正弦波相位,并在每个采样点添加相位。

让我们看看它是如何通过三角函数工作的。

我们将从我们想要的正弦波开始 ,如图 3 所示正弦波:

图 3. 正弦波

并由等式给出:

由于数字实现只能处理采样信号,我们需要每秒采样一次正弦波Ts。为了保持符号的直接性,我们现在将按样本号索引的正弦波输出,而不是按时间索引。

就我个人而言,我发现使用数字采样比使用采样之间的时间更容易。它们相互倒数,所以我们可以表示相同的方程,fsTsfs = 1/Ts

并绘制图 4 采样函数在中间。

图 4. 正弦波采样

在该图中,采样点以圆圈显示。它们各自被一个2pi f/fs相隔开。

然而,我们在这个算法中的所有重点都集中在这个表达式的相位上——正弦波参数。在上述表达式中,频率比n给出这个相位。我们称之为变化的相位值。f/fs 2pi phi[n]

要构建 NCO,我们需要相位值phi[n]转换为我们的正弦波发生器可以处理的输入。

我们将首先使用这个相位值phi[n]重写我们的正弦波,让它捕捉正弦波的内部——除了部分。

具体来说,相位值由下面定义:

它代表了我们的正弦波绕单位的旋转次数——如果愿意,就是旋转次数。

图 5. 单位圆旋转

例如,phi[n]为 1.0.内部应用于我们的正弦波会导致正弦函数的相位参数-2pi,表明我们绕着单位转了一圈 。2.0 的Aphi[n]将产生相同的值,但表示相位已绕单位圆行进两次。然后分数将表示从 x 轴围绕单位圆的部分角度,因此 phi[n]为0.5将表示绕圆的一半, 而0.25将代表绕圈的四分之一。

让我们继续使用这个值。这个阶段可以根据前一阶段递归地定义。

这个简单的修改有两个目的。首先,它允许我们避免乘以n,将下一阶段的计算从过去转变为只需加法的计算。其次,因为这个更新NCO版本不再与距离有关。这种微妙的变化使我们能够n=0中保持累积相位偏移 ——不仅在零时间相位为零的频率。

到目前为止,这听起来很简单。诀窍是什么?

建立 NCO 的“诀窍”

建立 NCO的“诀窍” 在于phi[n]. 以上介绍的单位围绕单位圆phi[n]循环数(或旋转)。phi[n] 为 1.0 表示绕单位圆1 次,phi[n]为 2.0 表示绕单位圆2 第二,按此类推。

然而,在大多数信号处理逻辑(不是全部)中,我们并不关心正弦波绕组单元圆的次数 ,只关心角分数。因此,让我们从整数和小数的角度来检查这个数字。

具体来说,让我们把它分成整数部分, W小数点后的第一位,如下所示。

phi[n] 分为整数分量和 W 小数分量

从图形上看,删除整数部分可能如下图所示 6 所示。

图 6. 相位函数

请注意图 6 中相位如何在正弦波开始重复的同一点跳回零。当然,不需要将此值恢复为零,但这样做会创建一个有限的范围,然后我们可以将其拆分为固定数量的位-W.

由于我们不关心单位圆的整数次,我们可以从它的整数部分中减去phi[n],它将单独恢复分数——就像我们在上图一样 6 所做的。然后,我们可以乘以结果2^W,从而获得适合位长字的定点相位,表示为:

为了完成这一点,我们只保留这个值的整数部分,代表我们的小数相位W的最高位,我们将忽略小数点以外的任何其他位置。

因此PHI[n],现在是介于0和之间的数字,2^W-1表示在0和1之间旋转单位圆0的分数。

这是我们把戏的第一部分。

让我们用P作这个技能的第二部分PHI[n]的高位 , 作为正弦波发生器的输入,无论是 查表正弦波发生器 CORDIC相位输入相位值算法 。

但是我们如何用定点相位来表示其他W位呢?

这些可以用于两个目的之一。首先,它们可以用作分数表索引,随着时间的推移积累来调整我们的表索引。 7 一个示例显示了这种情况。

图 7 分数相累积

如果你仔细看这张图,你可以看到相位指针一次移动多于一个表位置。最后,这个额外的累积分数会导致表索引完全跳过表位置(位置 6)。

这将允许表示和创建正弦波,而不是通过表格的整数步长形成的频率。这种频率可能涉及跳过表格条目,如上图所示 7 所示,或在必要时重复项目。是的,跳跃项目可能导致输出失真,但它也有助于保持 比第一位允许的更好的频率分辨率。

底部的第二个用途W-P为了减少与任何表一部分,以减少与任何表单相关的相位噪声。这是一个非常重要的可能性,我们可能不得不回到它,并在未来的文章中写更多。

但是溢出呢?

这是一个重要的问题,所以让我们看看会发生什么。考虑一下,如果我们在一个 8 跟踪位字中的相位,我们的加法溢出会发生什么。假设你想从PHI[n]=8'h20(45 开始绕一个圆圈走四步 。然后在每个时钟添加8'h40(90 度)。结果序列将是8'h20(45 度)、 8'h60(135 度)、8'ha0(225 度)、8'he0(315 度)、 8'h20(45 度)。

你有没有发现刚刚发生的事情?相位累加器刚刚在 315 度和 45 程度之间溢出,但相位表示只是正确的!这意味着相位中的任何溢出都可以忽略——无论如何,它只围绕单位圆,而正弦波发生器只对相位分数感兴趣。

示例源代码

那么,在实践中会发生什么呢?让我们举个例子,看看这个。 C 和 Verilog 会是什么样子?

我们将从一个开始 C 例子开始了。我们将制作一个包含这些原则的例子。 C NCO类别。本课程包括三个基本部分。首先是类声明和表格生成。

classNCO{ public: unsignedm_lglen,m_len,m_mask,m_phase,m_dphase; float*m_table;  NCO(constintlgtblsize){ //We'lluseatable2^(lgtblize)inlength. This is
  // non-negotiable, as the rest of this algorithm depends upon
  // this property.
  m_lglen = lgtblsize;
  m_len = (1<

我们将使用任何间隔左边缘的正弦波值来构建表格本身。这不是最优的,因为它会在左边缘强制误差为零,并可能在间隔的右侧使其成为最大值,但它会很快为我们提供不错的能力。

m_table = new float[m_len];
  for(k=0; k

我们可能会在稍后的帖子中回到这一点,以最小化此查找中的最大错误。

此初始化的最后一部分是为我们的 相位 累加器 ( PHI[n]) 提供初始值以及创建已知频率输出所需的相位步长。在这种情况下,我们将初始化这一步,使其产生零频率——这不是很令人兴奋,但下一步将是修复它。

// m_phase is the variable holding our PHI[n] function from
  // above.
  // We'll initialize our initial phase and frequency to zero
  m_phase = 0;
  m_dphase = 0;
 }

 // On any object deletion, make sure we delete the table as well
 ~NCO(void) {
  delete[] m_table;
 }

这个实现的第二部分是设置频率的函数。

// Adjust the sample rate for your implementation as necessary
 const float SAMPLE_RATE= 1.0;
 const float ONE_ROTATION= 2.0 * (1u << (sizeof(unsigned)*8-1));

 float frequency(float f) {
  // Convert the frequency to a fractional difference in phase
  m_dphase = (int)(f * ONE_ROTATION / SAMPLE_RATE);
 }

作为个人实践,我从不SAMPLE_RATE参与我的NCO 实施。这样,一个 NCO 实施就可以跨多个项目工作。

你可能会发现上面逻辑中最令人困惑的部分是 ONE_ROTATION值。这是代表围绕位圆一周的相位值。它由2^W 给出 。但是,我们必须通过一些环来设置这个值,因为该值不适合我们用来保存ONE_ROTATION 的整数。或者,我们可能已经设置为 unsigned PHI[n]m_phase,但是上面的方法使编译器更容易识别这个值是一个常量,而不是需要调用数学库函数。

这个类的最后一部分将索引向前一步插入到表中,然后返回表中由m_phase 单词P中的最高位给出的索引处的值。

float operator ()(void) {
  unsigned index;

  // Increment the phase by an amount dictated by our frequency
  // m_phase was our PHI[n] value above
  m_phase += m_dphase; // PHI[n] = PHI[n-1] + (2^32 * f/fs)

  // Grab the top m_lglen bits of this phase word
  index = m_phase >> (sizeof(unsigned)*8)-m_lglen);

  // Insist that this index be found within 0... (m_len-1)
  index &= m_mask;

  // Finally return the table lookup value
  return m_table[index];
 }

可能会注意到我选择使用单精度floats,而不是double精度浮点数。我这样做有两个原因。

首先,我想鼓励你提出一个问题,即实际需要多少精度?

其次,我想指出单精度float表示只有 24 位尾数。今天的大多数 CPU都允许 32 位的整数。结果,整数相位累加器比相位累加器具有更高的精度float。可以在图 8 中以图形方式看到这一点。

图 8. 浮点与定点相位

比较固定和浮点相位表示

如果不熟悉单精度 IEEE 浮点数,第一位 S, 是符号位,接下来的七位E, 是指数位。最后的 24 位M, 是尾数位。放在一起,这些项目代表了一种类似的数字(-1)^S * 2^E * M。(是的,我在这里跳过了一些细节。)

与 IEEE 浮点数不同,我们的定点相位表示只是32尾数位,值范围从0到1。

你认为哪一个会更精确?

以类似的方式,如果unsigned long对累加器使用 an 而不是unsignedvalue,则 相位 累加器的精度将高于double精度浮点所允许的精度。

在这两种情况下,这个相位累加器优于浮点相位累加器的原因很简单,因为我们的表示具有固定的小数位置,而不是浮点小数点。这允许将每个字的更多位分配给尾数,因为浮点表示需要为符号位和指数分配额外的位。

如果您在 Verilog 中构建 NCO 实现,代码几乎相同。最大的区别首先是我们要求给定的频率已经转换为适当的单位,其次是位选择比以前更简单。

module nco(i_clk, i_ld, i_dphase, o_val);
 parameter LGTBL = 9, // Log, base two, of the table size
   W = 32, // Word-size
   OW = 8; // Output width
 localparam P = LGTBL;
 //
 input wire  i_clk;
 //
 input wire  i_ld;
 input wire [W-1:0] i_dphase;
 //
 input wire  i_ce
 output wire [OW-1:0] o_val;

任何时候请求新频率i_ld时,都会将信号设置为高电平并将新频率置于i_dphase. 此 频率值以m_dphase上面 C++ 代码中的单位为单位。

reg [W-1:0] r_step;

 initial r_step = 0;
 always @(posedge i_clk)
 if (i_ld)
  r_step <= i_dphase; // = 2^W * f/fs

同样,在任何i_ce为 high 的时钟上,我们将相位向前步进相同的频率相关量。

reg [W-1:0] r_phase;

 initial r_phase = 0;
 always @(posedge i_clk)
 if (i_ce)
  // PHI[n] = PHI[n-1] + 2^W * f / fs
  r_phase <= r_phase + r_step;

最后,在我们的查表r_phase中使用的最高位P。

sintable // #(.PW(P), .OW(OW))
  stbl(i_clk, 1'b0, i_ce, 1'b0, r_phase[(W-1):(W-P)],
  o_val, ignored);
endmodule

可能会注意到 C++ 和 Verilog 的实现非常相似。它们都是低逻辑实现,展示了创建NCO所需的基础知识 。

未来的规划

我们刚刚介绍了构建基本 NCO背后的逻辑。虽然这种方法生成的正弦波并不完美,但对于项目来说可能已经足够了。如果需要更高质量的正弦波,可能希望知道除了我们之前讨论过的简单查表方法之外,还有其他更好的正弦波发生器。

参考

http://zipcpu.com/dsp/2017/12/09/nco.html

锐单商城拥有海量元器件数据手册IC替代型号,打造电子元器件IC百科大全!

相关文章