Come implementare un oscillatore digitale?


20

Ho un sistema di elaborazione del segnale digitale a virgola mobile che funziona a una frequenza di campionamento fissa di campioni al secondo implementata utilizzando un processore x86-64. Supponendo che il sistema DSP sia bloccato in modo sincrono su qualsiasi questione, qual è il modo migliore per implementare un oscillatore digitale a una certa frequenza ?fs=32768f

In particolare, voglio generare il segnale: dove per il numero di campione .

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

Un'idea è quella di tenere traccia di un vettore che ruotiamo di un angolo su ciascun ciclo di clock.(x,y)Δϕ=2πf/fs

Come implementazione di pseudocodice Matlab (la vera implementazione è 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)]

Quindi, ad ogni ciclo di clock, ruotiamo un po 'il vettore intorno:

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

Ciò consente all'oscillatore di essere calcolato con solo 4 moltiplicazioni per ciclo. Tuttavia, mi preoccuperei dell'errore di fase e della stabilità dell'ampiezza. (In semplici test sono rimasto sorpreso dal fatto che l'ampiezza non sia morta o esplosa immediatamente - forse l' sincosistruzione garantisce ?).sin2+cos2=1

Qual è il modo giusto per farlo?

Risposte:


12

Hai ragione sul fatto che l'approccio rigorosamente ricorsivo è vulnerabile all'accumulo di errori all'aumentare del numero di iterazioni. Un modo più robusto in genere è quello di utilizzare un oscillatore a controllo numerico (NCO) . Fondamentalmente, hai un accumulatore che tiene traccia della fase istantanea dell'oscillatore, aggiornato come segue:

δ=2πffs

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

Ad ogni istante, quindi, ti rimane la conversione della fase accumulata nell'NCO nelle uscite sinusoidali desiderate. Il modo in cui lo fai dipende dai tuoi requisiti di complessità computazionale, precisione, ecc. Un modo ovvio è semplicemente calcolare gli output come

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

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

usando qualunque implementazione di seno / coseno che hai a disposizione. Nei sistemi ad alto rendimento e / o incorporati, la mappatura dai valori fase a seno / coseno viene spesso eseguita tramite una tabella di ricerca. La dimensione della tabella di ricerca (ovvero la quantità di quantizzazione eseguita sull'argomento di fase su seno e coseno) può essere utilizzata come un compromesso tra consumo di memoria ed errore di approssimazione. La cosa bella è che la quantità di calcoli richiesti è in genere indipendente dalla dimensione della tabella. Inoltre, è possibile limitare le dimensioni della LUT, se necessario, sfruttando la simmetria inerente alle funzioni del coseno e del seno; hai davvero solo bisogno di memorizzare un quarto di un periodo della sinusoide campionata.

Se hai bisogno di una precisione maggiore di quella che ti può offrire un LUT di dimensioni ragionevoli, puoi sempre guardare all'interpolazione tra i campioni di tabella (interpolazione lineare o cubica, ad esempio).

Un altro vantaggio di questo approccio è che è banale incorporare la modulazione di frequenza o fase con questa struttura. La frequenza dell'uscita può essere modulata variando di conseguenza e la modulazione di fase può essere implementata semplicemente aggiungendo direttamente a .δϕ[n]


2
Grazie per la risposta. Come si confronta il tempo di esecuzione sincoscon una manciata di moltiplicazioni? Ci sono possibili insidie ​​a cui prestare attenzione durante l' modoperazione?
nibot,

È interessante che la stessa LUT da fase ad ampiezza possa essere utilizzata per tutti gli oscillatori nel sistema.
nibot,

Qual è lo scopo della mod 2pi? Ho visto anche implementazioni che fanno mod 1.0. Puoi espandere a cosa serve l'operazione modulo?
BigBrownBear00,

1
@ BigBrownBear00: l'operazione modulo è ciò che mantiene in un intervallo gestibile. In pratica, se non avessi il modulo, diventerebbe un numero molto positivo o negativo (la quantità totale di fase accumulata) nel tempo. Ciò può essere negativo per diversi motivi, tra cui l'eventuale overflow o perdita di precisione aritmetica e la riduzione delle prestazioni delle valutazioni del coseno e della funzione seno. Le implementazioni tipiche sono più veloci se non devono prima eseguire la riduzione degli argomenti nell'intervallo . ϕ[n][0,2π)
Jason R,

1
Il fattore contro 1.0 è un dettaglio di implementazione. Dipende dal dominio delle funzioni trigonometriche della tua piattaforma. Se si aspettano un valore nell'intervallo (ovvero l'angolo viene misurato in cicli), l'equazione per verrebbe regolata per riflettere quella diversa unità. La spiegazione nella risposta sopra presuppone che venga utilizzata la tipica unità angolare dei radianti. 2π[0,1.0)ϕ[n]
Jason R,

8

Quello che hai è un oscillatore molto buono ed efficiente. Il potenziale problema di deriva numerica può effettivamente essere risolto. La tua variabile di stato v ha due parti, una è potenzialmente la parte reale e l'altra la parte immaginaria. Chiamiamo quindi r e i. Sappiamo che r ^ 2 + i ^ 2 = 1. Nel tempo questo può andare su e giù, tuttavia ciò può essere facilmente corretto mediante la moltiplicazione con un fattore di correzione del guadagno come questo

g=1r2+i2

Ovviamente questo è molto costoso, tuttavia sappiamo che la correzione del guadagno è molto vicina all'unità e possiamo approssimarla con una semplice espansione di Taylor a

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

Inoltre non è necessario farlo su ogni singolo campione, ma una volta ogni 100 o 1000 campioni è più che sufficiente per mantenerlo stabile. Ciò è particolarmente utile se si esegue l'elaborazione basata su frame. L'aggiornamento una volta per fotogramma va bene. Ecco un rapido Matlab calcola 10.000.000 di campioni.

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

Questa risposta è ulteriormente spiegata da Hilmar in un'altra domanda: dsp.stackexchange.com/a/1087/34576
sircolinton

7

È possibile evitare la deriva di magnitudo instabile se non si esegue l'aggiornamento ricorsivo del vettore v. Al contrario, ruotare il vettore prototipo v nella fase di output corrente. Ciò richiede ancora alcune funzioni di trigger, ma solo una volta per buffer.

Nessuna deriva di magnitudo e frequenza arbitraria

pseudocodice:

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

È possibile eliminare la moltiplicazione, le funzioni di trigger richieste da cexp e il resto del modulo oltre 2pi se è possibile tollerare una traduzione di frequenza quantizzata. es. fs / 1024 per un buffer phasor campione 1024.

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.