Wie implementiere ich einen digitalen Oszillator?


20

Ich habe ein digitales Gleitkomma-Signalverarbeitungssystem, das mit einer festen Abtastrate von Abtastungen pro Sekunde arbeitet und mit einem x86-64-Prozessor implementiert ist. Unter der Annahme, dass das DSP-System synchron zu allen wichtigen Punkten verriegelt ist, wie lässt sich ein digitaler Oszillator mit einer bestimmten Frequenz am besten implementieren ?ffs=32768f

Insbesondere möchte ich das Signal erzeugen: wobei für die .t = n / f s n

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

Eine Idee ist, einen Vektor zu verfolgen den wir in jedem Taktzyklus um einen Winkel drehen .Δ ϕ = 2 π f / f s(x,y)Δϕ=2πf/fs

Als Matlab-Pseudocode-Implementierung (die eigentliche Implementierung befindet sich in 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)]

Dann drehen wir den Vektor bei jedem Taktzyklus ein wenig:

%% 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

Dadurch kann der Oszillator mit nur 4 Multiplikationen pro Zyklus berechnet werden. Ich würde mir jedoch Sorgen um Phasenfehler und Amplitudenstabilität machen. (In einfachen Tests war ich überrascht, dass die Amplitude nicht sofort starb oder explodierte - vielleicht sincosgarantiert der Befehl ?).sin2+cos2=1

Was ist der richtige Weg, um dies zu tun?

Antworten:


12

Sie haben Recht, dass der streng rekursive Ansatz für die Häufung von Fehlern anfällig ist, wenn die Anzahl der Iterationen zunimmt. Eine robustere Art und Weise, wie dies normalerweise durchgeführt wird, ist die Verwendung eines numerisch gesteuerten Oszillators (NCO) . Grundsätzlich haben Sie einen Akkumulator, der die momentane Phase des Oszillators wie folgt aktualisiert:

δ=2πffs

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

Zu jedem Zeitpunkt müssen Sie dann die akkumulierte Phase im NCO in die gewünschten sinusförmigen Ausgänge umwandeln. Wie Sie dies tun, hängt von Ihren Anforderungen an Rechenaufwand, Genauigkeit usw. ab. Eine naheliegende Möglichkeit besteht darin, nur die Ausgaben als zu berechnen

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

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

mit welcher Implementierung von Sinus / Cosinus Sie zur Verfügung haben. In Hochdurchsatz- und / oder eingebetteten Systemen erfolgt die Zuordnung von Phasen- zu Sinus- / Cosinus-Werten häufig über eine Nachschlagetabelle. Die Größe der Nachschlagetabelle (dh der Quantisierungsbetrag, den Sie für das Phasenargument für Sinus und Cosinus ausführen) kann als Kompromiss zwischen Speicherverbrauch und Approximationsfehler verwendet werden. Das Schöne ist, dass der Rechenaufwand in der Regel unabhängig von der Tabellengröße ist. Darüber hinaus können Sie Ihre LUT-Größe bei Bedarf begrenzen, indem Sie die Symmetrie nutzen, die den Cosinus- und Sinusfunktionen eigen ist. Sie müssen wirklich nur ein Viertel einer Periode der abgetasteten Sinuskurve speichern.

Wenn Sie eine höhere Genauigkeit benötigen, als eine LUT mit angemessener Größe Ihnen bieten kann, können Sie immer die Interpolation zwischen Tabellenabtastwerten betrachten (z. B. lineare oder kubische Interpolation).

Ein weiterer Vorteil dieses Ansatzes besteht darin, dass es trivial ist, eine Frequenz- oder Phasenmodulation in diese Struktur einzubauen. Die Frequenz des Ausgangs kann moduliert werden, indem dementsprechend ; variiert wird , und die Phasenmodulation kann implementiert werden, indem einfach direkt zu addiert wird .ϕ [ n ]δϕ[n]


2
Danke für die Antwort. Wie vergleicht sich die Ausführungszeit sincosmit einer Handvoll Multiplikationen? Gibt es mögliche Fallstricke, auf die Sie bei der modOperation achten müssen?
Nibot

Es ist ansprechend, dass für alle Oszillatoren im System dieselbe Phase-zu-Amplitude-LUT verwendet werden kann.
Nibot

Was ist der Zweck des Mods 2pi? Ich habe auch Implementierungen gesehen, die Mod 1.0 tun. Können Sie erläutern, wozu die Modulo-Operation dient?
BigBrownBear00

1
@ BigBrownBear00: Die Modulo-Operation hält in einem überschaubaren Bereich. Wenn Sie in der Praxis das Modulo nicht hätten, würde es mit der Zeit zu einer sehr großen positiven oder negativen Zahl (der Gesamtmenge der akkumulierten Phase) werden. Dies kann aus mehreren Gründen schlecht sein, einschließlich eines möglichen Überlaufs oder eines Verlusts der arithmetischen Genauigkeit und einer verringerten Leistung der Kosinus- und Sinusfunktionsbewertungen. Typische Implementierungen sind schneller, wenn sie nicht zuerst eine Argumentreduktion in den Bereich . [ 0 , 2 π )ϕ[n][0,2π)
Jason R

1
Der Faktor gegenüber 1.0 ist ein Implementierungsdetail. Dies hängt von der Domäne der trigonometrischen Funktionen Ihrer Plattform ab. Wenn sie einen Wert im Bereich erwarten (dh der Winkel wird in Zyklen gemessen), würde die Gleichung für angepasst, um diese unterschiedliche Einheit widerzuspiegeln. Die Erklärung in der obigen Antwort setzt voraus, dass die typische Winkeleinheit des Radiant verwendet wird. [ 0 , 1.0 ) ϕ [ n ]2π[0,1.0)ϕ[n]
Jason R

8

Was Sie haben, ist ein sehr guter und effizienter Oszillator. Das potenzielle numerische Driftproblem kann tatsächlich gelöst werden. Ihre Zustandsvariable v besteht aus zwei Teilen, wobei einer im Wesentlichen der Realteil und der andere der Imaginärteil ist. Nennen wir dann r und ich. Wir wissen, dass r ^ 2 + i ^ 2 = 1. Im Laufe der Zeit kann dies auf und ab driften, dies kann jedoch leicht durch Multiplikation mit einem Verstärkungskorrekturfaktor wie diesem korrigiert werden:

g=1r2+i2

Offensichtlich ist dies sehr teuer, aber wir wissen, dass die Verstärkungskorrektur sehr nahe bei Eins liegt, und wir können dies mit einer einfachen Taylor-Erweiterung auf approximieren

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

Darüber hinaus müssen wir dies nicht bei jeder einzelnen Probe tun, aber einmal alle 100 oder 1000 Proben ist mehr als genug, um diese stabil zu halten. Dies ist besonders nützlich, wenn Sie eine rahmenbasierte Verarbeitung durchführen. Einmal pro Frame zu aktualisieren ist in Ordnung. Hier ist eine kurze Beschreibung, mit der Matlab 10.000.000 Proben berechnet.

%% 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);

Diese Antwort wird von Hilmar in einer anderen Frage näher erläutert: dsp.stackexchange.com/a/1087/34576
sircolinton

7

Sie können die Drift der instabilen Größe vermeiden, wenn Sie den Vektor v nicht rekursiv aktualisieren. Drehen Sie stattdessen den Prototypvektor v in die aktuelle Ausgabephase. Dies erfordert immer noch einige Triggerfunktionen, jedoch nur einmal pro Puffer.

Keine Magnitudendrift und willkürliche Frequenz

Pseudocode:

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

Sie können die Multiplikation, die Triggerfunktionen, die für cexp erforderlich sind, und den Restmodul über 2 pi beseitigen, wenn Sie eine quantisierte Frequenzumsetzung tolerieren können. zB fs / 1024 für einen 1024-Beispiel-Zeigerpuffer.

Durch die Nutzung unserer Website bestätigen Sie, dass Sie unsere Cookie-Richtlinie und Datenschutzrichtlinie gelesen und verstanden haben.
Licensed under cc by-sa 3.0 with attribution required.