デジタル発振器を実装する方法は?


20

x86-64プロセッサを使用して実装されたサンプル/秒の固定サンプルレートで動作する浮動小数点デジタル信号処理システムがあります。DSPシステムが重要なものに同期してロックされていると仮定すると、ある周波数でデジタル発振器を実装する最良の方法は何ですか?fs=32768f

具体的には、信号を生成したい: ここで 、サンプル番号です。

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

1つのアイデアは、各クロックサイクルで角度だけ回転するベクトルを追跡することです。(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

これを行う正しい方法は何ですか?

回答:


12

当然のことながら、厳密に再帰的なアプローチは、反復回数が増えるにつれてエラーが蓄積されやすくなります。これが通常行われるより堅牢な方法の1つは、数値制御発振器(NCO)を使用することです。基本的に、次のように更新された発振器の瞬間位相を追跡するアキュムレータがあります。

δ=2πffs

ϕ[n]=(ϕ[n1]+δ)mod2π

各時点で、NCOの累積位相を目的の正弦波出力に変換する必要があります。これを行う方法は、計算の複雑さ、精度などの要件によって異なります。1つの明らかな方法は、出力を次のように計算することです。

xc[n]=cos(ϕ[n])

xs[n]=sin(ϕ[n])

使用可能なサイン/コサインの実装を使用します。高スループットおよび/または組み込みシステムでは、位相からサイン/コサイン値へのマッピングは、多くの場合ルックアップテーブルを介して行われます。ルックアップテーブルのサイズ(つまり、サインとコサインの位相引数で行う量子化の量)は、メモリ消費と近似誤差のトレードオフとして使用できます。良い点は、必要な計算量が通常、テーブルのサイズに依存しないことです。さらに、余弦関数と正弦関数に固有の対称性を利用して、必要に応じてLUTサイズを制限できます。サンプリングされた正弦波の4分の1の期間を保存するだけで十分です。

適切なサイズのLUTで得られるよりも高い精度が必要な場合は、テーブルサンプル間の補間(線形補間や3次補間など)を常に確認できます。

このアプローチのもう1つの利点は、この構造に周波数変調または位相変調を組み込むのが簡単なことです。出力の周波数は、それに応じて変化させることで変調でき、位相変調は、直接追加するだけで実装できます。δϕ[n]


2
答えてくれてありがとう。実行時間はsincos、少数の乗算と比較してどうですか?mod操作で注意すべき落とし穴はありますか?
nibot

システム内のすべての発振器に同じ位相から振幅のLUTを使用できることは魅力的です。
-nibot

mod 2piの目的は何ですか?また、mod 1.0を実行する実装を見てきました。モジュロ演算の目的を拡張できますか?
BigBrownBear00

1
ϕ[n][0,2π)

1
2π[0,1.0)ϕ[n]

8

あなたが持っているのは、非常に優れた効率的な発振器です。潜在的な数値ドリフトの問題は実際に解決できます。状態変数vには2つの部分があり、1つは本質的に実部で、もう1つは虚部です。rとiを呼び出しましょう。私たちは、r ^ 2 + i ^ 2 = 1であることを知っています。時間が経つにつれて、これは上下にドリフトする可能性がありますが、このようなゲイン補正係数を掛けることで簡単に補正できます。

g=1r2+i2

明らかにこれは非常に高価ですが、ゲイン補正が1に非常に近く、への単純なテイラー展開でこれを近似できることがわかっています。

g=1r2+i212(3(r2+i2))

さらに、すべてのサンプルでこれを行う必要はありませんが、100または1000サンプルごとに1回行うだけで、この安定性を維持できます。これは、フレームベースの処理を行う場合に特に便利です。フレームごとに1回更新するだけで十分です。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);

この答えは、ヒルマーが別の質問でさらに説明しています:dsp.stackexchange.com/a/1087/34576
sircolinton

7

ベクトルvを再帰的に更新しない場合、不安定なマグニチュードドリフトを回避できます。代わりに、プロトタイプベクトルvを現在の出力フェーズに回転させます。これにはまだいくつかのトリガー関数が必要ですが、バッファーごとに1回だけです。

マグニチュードドリフトと任意の周波数

擬似コード:

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で必要なトリガー関数、および2piを超える剰余を廃止できます。たとえば、1024サンプルフェーザーバッファーの場合はfs / 1024。

弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.