如何实现数字振荡器?


20

我有一个浮点数字信号处理系统,它使用x86-64处理器以每秒样本的固定样本速率运行。假设DSP系统被同步锁定在任何问题上,那么在某个频率实现数字振荡器的最佳方法是什么?ffs=32768f

具体来说,我想生成信号: 其中,对于样本编号, 。= ñ / ˚F 小号 Ñ

y(t)=sin(2πft)
t=n/fsn

一种想法是跟踪矢量,在每个时钟周期我们旋转角度。Δ φ = 2 π ˚F / ˚F 小号(x,y)Δϕ=2πf/fs

作为Matlab伪代码实现(真正的实现在C中):

%% Initialization code

f_s = 32768;             % sample rate [Hz]
f = 19.875;              % some constant frequency [Hz]

v = [1 0];               % initial condition     
d_phi = 2*pi * f / f_s;  % change in angle per clock cycle

% initialize the rotation matrix (only once):
R = [cos(d_phi), -sin(d_phi) ; ...
     sin(d_phi),  cos(d_phi)]

然后,在每个时钟周期,我们将向量旋转一点:

%% in-loop code

while (forever),
  v = R*v;        % rotate the vector by d_phi
  y = v(1);       % this is the sine wave we're generating
  output(y);
end

这样就可以在每个周期仅用4个乘法来计算振荡器。但是,我担心相位误差和幅度稳定性。(在简单的测试中,我惊讶于振幅没有立即消失或爆炸-也许sincos指令可以保证?)。sin2+cos2=1

什么是正确的方法?

Answers:


12

没错,严格的递归方法很容易随着迭代次数的增加而累积错误。通常完成的一种更健壮的方法是使用数控振荡器(NCO)。基本上,您有一个累加器,可以跟踪振荡器的瞬时相位,其更新如下:

δ=2πFFs

ϕ[ñ]=ϕ[ñ-1个]+δ2π

然后,在每个时刻,您都需要将NCO中的累积相位转换为所需的正弦输出。如何执行此操作取决于您对计算复杂性,准确性等的要求。一种显而易见的方法是将输出计算为

XC[ñ]=cosϕ[ñ]

Xs[ñ]=ϕ[ñ]

使用任何可用的正弦/余弦实现。在高通量和/或嵌入式系统中,从相位到正弦/余弦值的映射通常是通过查找表完成的。查找表的大小(即您对正弦和余弦的相位参数执行的量化数量)可以用作内存消耗和近似误差之间的折衷。令人高兴的是,所需的计算量通常与表的大小无关。此外,如果需要,您可以利用余弦和正弦函数固有的对称性来限制LUT的大小。您只需要存储采样的正弦曲线的四分之一周期即可。

如果您需要的精度比合理大小的LUT所不能提供的精度高,那么您始终可以查看表样本之间的插值(例如,线性或三次插值)。

这种方法的另一个好处是将频率或相位调制与这种结构结合起来是微不足道的。可以通过相应地改变来调制输出的频率,并且可以通过直接加到来实现相位调制。φ [ Ñ ]δϕ[ñ]


2
感谢您的回答。sincos与少数乘法相比,执行时间如何?在操作过程中是否需要注意一些陷阱mod
nibot 2011年

吸引人的是,系统中的所有振荡器都可以使用相同的相幅LUT。
nibot 2011年

mod 2pi的目的是什么?我也看到过执行mod 1.0的实现。您能扩展一下模运算的作用吗?
BigBrownBear00

1
@ BigBrownBear00:模运算使处于可管理的范围内。实际上,如果您没有模数,则随着时间的流逝,它会增长为非常大的正数或负数(累计相的总量)。这可能是不好的,原因有几个,包括最终溢出或算术精度损失,以及余弦和正弦函数评估的性能降低。如果不需要首先将参数减少到范围内则典型的实现会更快。[ 0 2 π ϕ[ñ][02π
Jason R

1
与1.0 的因数是实现细节。这取决于平台的三角函数的作用域。如果他们期望值在范围内(即,角度以周期测量),则将调整的方程式以反映该不同的单位。上面答案中的解释假定使用弧度的典型角度单位。[ 0 1.0 2π[0,1.0)ϕ[ñ]
杰森R

8

您所拥有的是一个非常有效的振荡器。潜在的数值漂移问题实际上可以解决。您的状态变量v有两部分,一方面是实际部分,另一方面是虚部。让我们叫r和i。我们知道r ^ 2 + i ^ 2 =1。随着时间的流逝,它可能会上下漂移,但是可以通过乘以

G=1个[R2+一世2

显然,这非常昂贵,但是我们知道增益校正非常接近于单位,我们可以通过简单的泰勒展开将其近似为

G=1个[R2+一世21个23-[R2+一世2

此外,我们不需要对每个样本都执行此操作,但是每100或1000个样本一次足以保持其稳定性。如果您执行基于帧的处理,这将特别有用。每帧更新一次就可以了。这是一个快速的Matlab计算10,000,000个样本。

%% seed the oscillator
% set parameters
f0 = single(100); % say 100 Hz
fs = single(44100); % sample rate = 44100;
nf = 1024; % frame size

% initialize phasor and state
ph =  single(exp(-j*2*pi*f0/fs));
state = single(1 + 0i); % real part 1, imaginary part 0

% try it
x = zeros(nf,1,'single');
testRuns = 10000;
for k = 1:testRuns
  % overall frames
  % sample: loop
  for i= 1:nf
    % phasor multiply
    state = state *ph;
    % take real part for cosine, or imaginary for sine
    x(i) = real(state);
  end
  % amplitude corrections through a taylor exansion aroud
  % abs(state) very close to 1
  g = single(.5)*(single(3)-real(state)*real(state)-imag(state)*imag(state) );
  state = state*g;
end
fprintf('Deviation from unity amplitude = %f\n',g-1);


7

如果不让它递归更新向量v,则可以避免幅度波动的不稳定。相反,请将原型向量v旋转到当前输出相位。这仍然需要一些触发功能,但每个缓冲区仅需要一次。

无幅度漂移和任意频率

伪代码:

init(freq)
  precompute Nphasor samples in phasor
  phase=0

gen(Nsamps)
    done=0
    while done < Nsamps:
       ndo = min(Nsamps -done, Nphasor)
       append to output : multiply buf[done:done+ndo) by cexp( j*phase )
       phase = rem( phase + ndo * 2*pi*freq/fs,2*pi)
       done = done+ndo

如果可以忍受量化的频率转换,则可以省去乘法,cexp所需的trig函数以及超过2pi的模余数。例如fs / 1024,用于1024个采样相量缓冲区。

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.