¿Cómo implementar un oscilador digital?


20

Tengo un sistema de procesamiento de señal digital de punto flotante que funciona a una frecuencia de muestreo fija de fs=32768 muestras por segundo implementado utilizando un procesador x86-64. Suponiendo que el sistema DSP está bloqueado sincrónicamente para lo que sea que importe, ¿cuál es la mejor manera de implementar un oscilador digital a alguna frecuencia f ?

Específicamente, quiero generar la señal:

y(t)=sin(2πft)
donde t=n/fs para el número de muestran .

Una idea es hacer un seguimiento de un vector (x,y) que rotamos en un ángulo en cada ciclo de reloj.Δϕ=2πf/fs

Como una implementación de pseudocódigo de Matlab (la implementación real está en 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)]

Luego, en cada ciclo de reloj, giramos el vector un poco:

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

Esto permite que el oscilador se calcule con solo 4 multiplicaciones por ciclo. Sin embargo, me preocuparía el error de fase y la estabilidad de la amplitud. (En pruebas simples me sorprendió que la amplitud no muriera o explotara de inmediato, ¿tal vez la sincosinstrucción garantiza sin2+cos2=1 ?).

¿Cuál es la forma correcta de hacer esto?

Respuestas:


12

Tiene razón en que el enfoque estrictamente recursivo es vulnerable a la acumulación de errores a medida que aumenta el número de iteraciones. Una forma más robusta de hacer esto normalmente es usar un oscilador controlado numéricamente (NCO) . Básicamente, tiene un acumulador que realiza un seguimiento de la fase instantánea del oscilador, actualizado de la siguiente manera:

δ=2πFFs

ϕ[norte]=(ϕ[norte-1]+δ)modificación2π

En cada instante de tiempo, entonces, te queda convertir la fase acumulada en el NCO a las salidas sinusoidales deseadas. Cómo lo haga depende de sus requisitos de complejidad computacional, precisión, etc. Una forma obvia es calcular los resultados como

XC[norte]=cos(ϕ[norte])

Xs[norte]=pecado(ϕ[norte])

usando cualquier implementación de seno / coseno que tenga disponible. En los sistemas de alto rendimiento y / o integrados, la asignación de valores de fase a seno / coseno a menudo se realiza a través de una tabla de búsqueda. El tamaño de la tabla de búsqueda (es decir, la cantidad de cuantización que realiza en el argumento de fase para seno y coseno) puede usarse como una compensación entre el consumo de memoria y el error de aproximación. Lo bueno es que la cantidad de cálculos requeridos suele ser independiente del tamaño de la tabla. Además, puede limitar el tamaño de su LUT si es necesario aprovechando la simetría inherente a las funciones coseno y seno; realmente solo necesita almacenar un cuarto de un período de la sinusoide muestreada.

Si necesita una precisión mayor que la que puede proporcionarle un LUT de tamaño razonable, siempre puede ver la interpolación entre muestras de tabla (interpolación lineal o cúbica, por ejemplo).

Otro beneficio de este enfoque es que es trivial incorporar la modulación de frecuencia o fase con esta estructura. La frecuencia de la salida se puede modular variando consecuencia, y la modulación de fase se puede implementar simplemente agregando a ϕ [ n ] directamente.δϕ[norte]


2
Gracias por la respuesta. ¿Cómo se sincoscompara el tiempo de ejecución de un puñado de multiplicaciones? ¿Hay posibles dificultades a tener en cuenta con la modoperación?
nibot

Es atractivo que se pueda usar la misma LUT de fase a amplitud para todos los osciladores del sistema.
nibot

¿Cuál es el propósito del mod 2pi? También he visto implementaciones que hacen mod 1.0. ¿Puede ampliar para qué sirve la operación de módulo?
BigBrownBear00

1
ϕ[norte][0 0,2π)

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

8

g=1r2+i2

Obviously this is very expensive, however we know that the gain correction is very close to unity and we can approximate this with a simple Taylor expansion to

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

Moreover we don't need to do this on every single sample, but once every 100 or 1000 samples is more than enough to keep this stable. This is particularly useful if you do frame based processing. Updating once per frame is just fine. Here is a quick Matlab calculates 10,000,000 samples.

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

This answer is further explained by Hilmar in another question: dsp.stackexchange.com/a/1087/34576
sircolinton

7

You can avoid the unstable magnitude drift if you do not make it recursively update the vector v. Instead, rotate your prototype vector v to the current output phase. This still requires some trig functions, but only once per buffer.

No magnitude drift and arbitrary frequency

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

You can do away with the multiply, the trig functions required by cexp, and the modulus remainder over 2pi if you can tolerate a quantized frequency translation. e.g. fs/1024 for a 1024 sample phasor buffer.

Al usar nuestro sitio, usted reconoce que ha leído y comprende nuestra Política de Cookies y Política de Privacidad.
Licensed under cc by-sa 3.0 with attribution required.