Ist es möglich, einen IIR-Filter in einem FPGA zu erstellen, der mit der Abtastfrequenz getaktet wird?

Bei dieser Frage geht es um die Implementierung eines IIR-Filters in einem FPGA mit DSP-Slices mit sehr spezifischen Kriterien.

Angenommen, Sie erstellen einen Filter ohne Vorwärtsabgriffe und nur 1 Rückwärtsabgriff mit dieser Gleichung:

j [ n ] = j [ n 1 ] b 1 + x [ n ]

(siehe Bild)

Nehmen Sie als Beispiel den DSP48A1-Slice von Xilinx – die meisten Hard-IP-DSP-Slices sind ähnlich.

Angenommen, Sie haben analoge Daten, die mit 1 Abtastung pro Takt eingehen. Ich möchte einen IIR-Filter entwerfen, der synchron zum Abtasttakt läuft.

Das Problem ist, dass Sie, um den DSP-Slice mit der maximalen Rate laufen zu lassen, nicht im selben Zyklus multiplizieren UND addieren können. Zwischen diesen Komponenten muss ein Pipelineregister vorhanden sein.

Wenn Sie also bei jedem Takt 1 neues Sample haben, müssen Sie 1 Ausgang pro Takt erzeugen. Allerdings benötigt man die bisherige Ausgabe 2 Takte bevor man eine neue in dieser Ausführung herstellen kann.

Die offensichtliche Lösung besteht darin, die Daten entweder mit doppelter Taktrate zu verarbeiten oder das Pipeline-Register zu deaktivieren, damit Sie im selben Zyklus multiplizieren und addieren können.

Wenn Sie beispielsweise mit der maximalen Taktrate des vollständig gepipelineten DSP-Slice abtasten, ist leider keine dieser Lösungen möglich. Gibt es eine andere Möglichkeit das zu bauen?

(Bonuspunkte, wenn Sie einen IIR-Filter entwerfen können, der mit einer beliebigen Anzahl von DSP-Slices mit der halben Abtastrate arbeitet)

Das Ziel wäre, einen Kompensationsfilter für einen 1-GSPS-ADC in einem Xilinx Artix FPGA zu betreiben. Ihre DSP-Slices können bei vollständiger Pipeline mit knapp über 500 MHz laufen. Wenn es eine Lösung für 1 Sample pro Takt gibt, würde ich gerne versuchen, die Lösung für 2 Samples pro Takt zu skalieren. Mit einem FIR-Filter geht das alles ganz einfach.

Beispiel für einen IIR-Filter mit Einzelrückkopplung

Nur zur Verdeutlichung, es gibt keinen Grund, warum Sie mit der Pipeline-Methode keinen Ausgang pro Taktzyklus haben würden, oder? Sie versuchen, die Latenz auf einen Taktzyklus statt auf zwei zu minimieren, richtig? Wenn Sie eine Ganzzahl für b1 verwenden, können Sie je nach Ihrer Situation die Multiplikation in eine Riesenaddition einschließlich x[n] umwandeln.
richtig - da es einen Eingang pro Uhr gibt, muss es einen Ausgang pro Uhr geben. Latenz ist auch kein Problem. Der DSP-Slice hat nur einen Addierer mit 2 Eingängen, und die Abgriffe sind normalerweise ziemlich große Zahlen, sodass Sie b1-Zeiten nicht in einem Taktzyklus hinzufügen können. Die Hauptgrenze besteht darin, dass der Ausgang 1 Takt zurückkoppeln muss, aber 2 Takte zum Produzieren benötigt werden.
Ich glaube, Sie missverstehen immer noch, wie eine Pipeline funktioniert. Eine Pipeline erhöht möglicherweise die Latenz, ermöglicht es Ihnen jedoch, bei jedem Taktzyklus 1 Ausgang für jeden Eingang zu erhalten. Es ist nur so, dass das Ergebnis jetzt 2 Takte später statt der idealen 1 Uhr danach ist. Die Eingabe wäre die folgende Sequenz: x[0],x[1],x[2],x[3],x[4], während die Ausgabe im selben Zeitintervall y[-2],y wäre [-1],y[0],y[1],y[2]. Sie verlieren keine Proben. Außerdem befinden Sie sich auf einem FPGA. Wenn Sie also mehr Arbeit erledigen möchten, als für die DSP-Pipelines ausgelegt ist, verwenden Sie das FPGA, um die Arbeitslast zu parallelisieren.
Dieser DSP ist in der Lage, eine verschmolzene Multiplikationsakkumulation in einem Zyklus durchzuführen. Es ist mir jedoch unklar, ob der Ausgang eines DSP-Slice in einem einzigen Zyklus mit seinem eigenen Eingang mit Rückkopplung verbunden werden kann.
horta - Sie haben im Allgemeinen Recht mit dem Pipelining, aber das Problem ist, dass die Registerkarte b1 in diesem Fall eine Rückmeldung enthält - was bedeutet, dass eine Stufe in der Pipeline von der Ausgabe des vorherigen Werts abhängt. Wenn es immer 2 Takte dauert, um die nächste Ausgabe aus der vorherigen Ausgabe zu erzeugen, gibt es keine Möglichkeit, 1 Ausgabe pro Takt zu erzeugen, unabhängig davon, wie viel Latenz Sie hinzugefügt haben. jbarlow - Sie haben Recht, der DSP-Slice hat eine 1-Zyklus-Fused-Option. Allerdings kann es in diesem Fall nicht schnell genug laufen. Durch Hinzufügen des M-Registers (siehe Datenblatt) können Sie 500 MHz erreichen. Allerdings können Sie dann nicht im selben Takt multiplizieren und addieren.
Ah, jetzt verstehe ich, was du sagst, Marcus. Ich denke, die einzige Lösung besteht jetzt darin, dass Sie Ihre eigene Logik im FPGA-Teil erstellen, mit der Sie in einem einzigen Taktzyklus eine Multiplikations-Addierung durchführen können, während Sie die Ausgabe direkt an den Eingang dieses Logikblocks zurückführen.
Meine übliche Antwort auf Fragen wie diese bei der Arbeit lautet: "Sie haben eine sehr festgelegte Implementierung im Sinn ... sagen Sie mir die tatsächlichen Spezifikationen für die Verarbeitung, die Sie durchführen möchten. Mal sehen, ob wir es anders machen können."
Sie haben Recht, und ich arbeite bereits an anderen Optionen, die sich erheblich unterscheiden. Ziel ist es, ein 1-GSPS-Oszilloskop aus einem Artix-7-FPGA zu bauen. Der IIR-Filter dient hauptsächlich dazu, das Passband zu kompensieren, falls es nicht flach ist. Da das Oszilloskop jedoch mit kurzen Erfassungspuffern arbeitet, ist es kein Problem, die Daten nachträglich mit einer geringeren Geschwindigkeit zu filtern – es ist kein kontinuierliches Streamen erforderlich. Die Kompensationsfilterung im Voraus durchzuführen, ist nur eine Idee, die mehr Optionen offen lässt. Dies ist auch eine gute Leistungsübung, um genau zu sehen, wie schnell IIR laufen kann, also bin ich froh, dass ich gefragt habe.

Antworten (2)

Ich habe noch nicht mit IIR-Filtern gearbeitet, aber wenn Sie nur die angegebene Gleichung berechnen müssen

y[n] = y[n-1]*b1 + x[n]

Einmal pro CPU-Zyklus können Sie Pipelining verwenden.

In einem Zyklus führen Sie die Multiplikation durch und in einem Zyklus müssen Sie die Summierung für jeden Eingangsabtastwert durchführen. Das bedeutet, dass Ihr FPGA in der Lage sein muss, die Multiplikation in einem Zyklus durchzuführen, wenn es mit der angegebenen Abtastrate getaktet wird! Dann müssen Sie nur die Multiplikation des aktuellen Samples UND die Summation des Multiplikationsergebnisses des letzten Samples parallel durchführen. Dies verursacht eine konstante Verarbeitungsverzögerung von 2 Zyklen.

Ok, schauen wir uns die Formel an und entwerfen eine Pipeline:

y[n] = y[n-1]*b1 + x[n]

Ihr Pipeline-Code könnte so aussehen:

output <= last_output_times_b1 + last_input
last_output_times_b1 <= output * b1;
last_input <= input

Beachten Sie, dass alle drei Befehle parallel ausgeführt werden müssen und dass "output" in der zweiten Zeile daher die Ausgabe des letzten Taktzyklus verwendet!

Ich habe nicht viel mit Verilog gearbeitet, daher ist die Syntax dieses Codes höchstwahrscheinlich falsch (zB fehlende Bitbreite von Ein-/Ausgangssignalen; Ausführungssyntax für Multiplikation). Sie sollten jedoch auf die Idee kommen:

module IIRFilter( clk, reset, x, b, y );
  input clk, reset, x, b;
  output y;

  reg y, t, t2;
  wire clk, reset, x, b;

  always @ (posedge clk or posedge reset)
  if (reset) begin
    y <= 0;
    t <= 0;
    t2 <= 0;
  end else begin
    y <= t + t2;
    t <= mult(y, b);
    t2 <= x
  end

endmodule

PS: Vielleicht könnte ein erfahrener Verilog-Programmierer diesen Code bearbeiten und diesen Kommentar und den Kommentar über dem Code anschließend entfernen. Vielen Dank!

PPS: Falls Ihr Faktor "b1" eine feste Konstante ist, können Sie das Design möglicherweise optimieren, indem Sie einen speziellen Multiplikator implementieren, der nur eine skalare Eingabe akzeptiert und nur "mal b1" berechnet.

Antwort auf: "Leider entspricht dies tatsächlich y[n] = y[n-2] * b1 + x[n]. Dies liegt an der zusätzlichen Pipeline-Stufe." als Kommentar zur alten Version der Antwort

Ja, das war eigentlich richtig für die folgende alte (FALSCHE!!!) Version:

  always @ (posedge clk or posedge reset)
  if (reset) begin
    t <= 0;
  end else begin
    y <= t + x;
    t <= mult(y, b);
  end

Ich habe diesen Fehler jetzt hoffentlich behoben, indem ich die Eingabewerte auch in einem zweiten Register verzögert habe:

  always @ (posedge clk or posedge reset)
  if (reset) begin
    y <= 0;
    t <= 0;
    t2 <= 0;
  end else begin
    y <= t + t2;
    t <= mult(y, b);
    t2 <= x
  end

Um sicherzustellen, dass es diesmal richtig funktioniert, schauen wir uns an, was bei den ersten paar Zyklen passiert. Beachten Sie, dass die ersten 2 Zyklen mehr oder weniger (definierten) Müll produzieren, da keine vorherigen Ausgabewerte (z. B. y[-1] == ??) verfügbar sind. Das Register y wird mit 0 initialisiert, was der Annahme von y[-1] == 0 entspricht.

Erster Zyklus (n=0):

BEFORE: INPUT (x=x[0], b); REGISTERS (t=0, t2=0, y=0)

y <= t + t2;      == 0
t <= mult(y, b);  == y[-1] * b  = 0
t2 <= x           == x[0]

AFTERWARDS: REGISTERS (t=0, t2=x[0], y=0), OUTPUT: y[0]=0

Zweiter Zyklus (n=1):

BEFORE: INPUT (x=x[1], b); REGISTERS (t=0, t2=x[0], y=y[0])

y <= t + t2;      ==     0  +  x[0]
t <= mult(y, b);  ==  y[0]  *  b
t2 <= x           ==  x[1]

AFTERWARDS: REGISTERS (t=y[0]*b, t2=x[1], y=x[0]), OUTPUT: y[1]=x[0]

Dritter Zyklus (n=2):

BEFORE: INPUT (x=x[2], b); REGISTERS (t=y[0]*b, t2=x[1], y=y[1])

y <= t + t2;      ==  y[0]*b +  x[1]
t <= mult(y, b);  ==  y[1]   *  b
t2 <= x           ==  x[2]

AFTERWARDS: REGISTERS (t=y[1]*b, t2=x[2], y=y[0]*b+x[1]), OUTPUT: y[2]=y[0]*b+x[1]

Vierter Zyklus (n=3):

BEFORE: INPUT (x=x[3], b); REGISTERS (t=y[1]*b, t2=x[2], y=y[2])

y <= t + t2;      ==  y[1]*b +  x[2]
t <= mult(y, b);  ==  y[2]   *  b
t2 <= x           ==  x[3]

AFTERWARDS: REGISTERS (t=y[2]*b, t2=x[3], y=y[1]*b+x[2]), OUTPUT: y[3]=y[1]*b+x[2]

Wir können sehen, dass wir beginnend mit Zyklus n = 2 die folgende Ausgabe erhalten:

y[2]=y[0]*b+x[1]
y[3]=y[1]*b+x[2]

was äquivalent ist

y[n]=y[n-2]*b + x[n-1]
y[n]=y[n-1-l]*b1 + x[n-l],  where l = 1
y[n+l]=y[n-1]*b1 + x[n],  where l = 1

Wie oben erwähnt, führen wir eine zusätzliche Verzögerung von l=1 Zyklen ein. Das bedeutet, dass Ihre Ausgabe y[n] um Lag l=1 verzögert wird. Das heißt, die Ausgabedaten sind äquivalent, aber um einen "Index" verzögert. Um es klarer zu machen: Die Ausgangsdaten werden um 2 Zyklen verzögert, da ein (normaler) Taktzyklus benötigt wird und 1 zusätzlicher (Verzögerung l = 1) Taktzyklus für die Zwischenstufe hinzugefügt wird.

Hier ist eine Skizze, um den Datenfluss grafisch darzustellen:

Skizze des Datenflusses

PS: Vielen Dank, dass Sie sich meinen Code genau angesehen haben. Also habe ich auch was gelernt! ;-) Lassen Sie mich wissen, ob diese Version korrekt ist oder ob Sie weitere Probleme sehen.

Gute Arbeit! Leider ist y[n] = y[n-2] * b + x[n-1] funktional nicht äquivalent zu y[n] = y[n-1] * b + x[n] mit Latenz. Die Form einer IIR-Übertragungsfunktion sieht eigentlich so aus: y[n] = x[n] * b0 + x[n-1] * b1 - y[n-1] * a1 - y[n-2] * a2 usw. Ihr Formular setzt b0 und a1 auf 0 und verwendet stattdessen b1 und a2. Diese Transformation erzeugt jedoch tatsächlich einen ganz anderen Filter. Wenn es jedoch eine Möglichkeit gäbe, einen Filter zu berechnen, bei dem der erste Nenner (a1) auf Null gesetzt ist, würden beide Lösungen perfekt funktionieren.
Nun, Sie müssen das Problem der „eingeführten Verzögerung“ richtig verstehen. Beispielsweise sollte ein "Datenstromverarbeitungs"-Filter seine Eingabe einfach weiterleiten, da y[n] = x[n] korrekt funktionieren würde, wenn er y[n] = x[n-1] als Ausgabe erzeugt. Die Ausgabe wird nur um 1 Zyklus verzögert (z. B. der Ausgabeindex ist relativ zu allen Eingabeindizes um einen festen Wert versetzt)! In unserem Beispiel bedeutet dies, dass Ihre Funktion y[n+l] = y[n-1] * b + x[n]einen festen Wert für die Verzögerung hat l, der umgeschrieben werden kann, y[n] = y[n-1-l] * b + x[n-l]und für l = 1 ist dies y[n] = y[n-2] * b + x[n-1].
Für Ihren komplexeren IIR-Filter müssten Sie dasselbe tun: y[n+l] = x[n] * b0 + x[n-1] * b1 - y[n-1] * a1 - y[n-2] * a2=> y[n] = x[n-l]*b0 + x[n-1-l] * b1 - y[n-1-l] * a1 - y[n-2-l]*a2. Angenommen, Sie können alle drei Multiplikationen parallel durchführen (1. Schritt / 1 Zyklus) und müssen die Produkte zusammenzählen, benötigen Sie 2 Zyklen (1 Zyklus: Addieren/Subtrahieren der ersten beiden Produktergebnisse, 1 Zyklus: Addieren/Subtrahieren das Ergebnis dieser beiden Add/Subs), benötigen Sie 2 zusätzliche Zyklen. Also l=(3-1)=2 ergibt y[n]=x[n-2]*b0+x[n-1-2]*b1-y[n-1-2]*a1-y[n-2-2]*a2=>y[n]=x[n-2]*b0+x[n-3]*b1-y[n-3]*a1-y[n-4]*a2
Damit dies funktioniert, muss Ihr FPGA natürlich folgendes parallel können: 4 Multiplikationen und 3 Additionen/Subtraktionen. Das heißt, Sie benötigen Ressourcen für 4 Multiplikatoren und 3 Addierer.

Ja, Sie können mit der Abtastfrequenz takten.

Eine Lösung für dieses Problem besteht darin, den ursprünglichen Ausdruck so zu manipulieren, dass Pipeline-Register eingefügt werden können, während die gewünschte Ausgangssequenz beibehalten wird.

Gegeben: y[n] = y[n-1]*b1 +x[n];

dies kann manipuliert werden zu: y[n] = y[n-2]*b1*b1 +x[n-1]*b1 +x[n].

Um zu überprüfen, ob dies die gleiche Sequenz ist, betrachten Sie, was mit den ersten mehreren Abtastwerten x[0], x[1], x[2] usw. passiert, wobei vor x[0] alle x,y-Abtastwerte Null waren.

Für den ursprünglichen Ausdruck lautet die Sequenz:

y = x[0],

x[1] +x[0]*b1,

x[2] +x[1]*b1 +x[0]*b1*b1,

x[3] +x[2]*b1 +x[1]*b1*b1 +x[0]*b1*b1*b1, ...

Es ist klar, dass es notwendig ist, dass b1 < 1 ist, sonst wächst dies unbegrenzt.

Betrachten Sie nun den manipulierten Ausdruck:

y = x[0],

x[0]*b1 +x[1],

x[0]*b1*b1 +x[1]*b1 +x[2],

x[0]*b1*b1*b1 +x[1]*b1*b1 +x[2]*b1 +x[3], ...

Dies ist die gleiche Reihenfolge.

Eine Hardwarelösung in Xilinx-Bibliotheksprimitiven würde zwei DSP48E in Kaskade benötigen. Siehe Abbildung 1-1 in UG193 v3.6 für die Port- und Registernamen unten. Das erste Primitiv multipliziert mit b1 und addiert einen Takt später; die zweite multipliziert mit b1*b1 und addiert einen Takt später. Für diese Logik gibt es eine Pipeline-Latenzzeit von 4 Takten.

-- DSP48E #1

a_port1 := b1; -- konstanter Koeffizient, setze AREG=1

b_port1 := x; -- Attribut BREG=1 setzen

c_port1 := x; -- CREG=1 setzen

-- intern für DSP48E #1

reg_a1 <= a_port1;

reg_b1 <= b_port1;

reg_c1 ​​<= c_port1;

reg_m1 <= reg_a1 * reg_b1;

reg_p1 <= reg_m1 + reg_c1; -- Ausgabe des 1. DSP48E

-- Ende von DSP48E #1

-- DSP48E #2

a_port2 := reg_p2; -- Attribut AREG=0 setzen

                -- this means the output of register reg_p2

                -- directly feeds back to the multiplier

b_port2 := b1*b1; -- Konstante, setze BREG=1

c_port2 := reg_p1; -- CREG=1 setzen

-- intern für DSP48E #2

reg_b2 <= b_port2;

reg_c2 <= c_port2;

reg_m2 <= a_port2 * reg_b2;

reg_p2 <= reg_m2 + reg_c2;

-- Ende von DSP48E #2

Die Sequenz bei reg_p1:

x[0],

x[1] +x[0]*b1,

x[2] +x[1]*b1,

x[3] +x[2]*b1,

usw.

Die Sequenz bei reg_p2 ist das gewünschte Ergebnis. Intern im 2. DSP48E hat das Register reg_m2 eine Sequenz:

x[0]*b1*b1,

x[1]*b1*b1 +x[0]*b1*b1*b1,

x[2]*b1*b1 +x[1]*b1*b1*b1 +x[0]*b1*b1*b1*b1

Dieses Ergebnis hat eine schöne Eleganz. Natürlich multipliziert und addiert der DSP48E nicht denselben Takt, aber das ist es, was die Differenzgleichung erfordert. Die manipulierte Differenzgleichung ermöglicht es uns, die M- und P-Register im DSP48E zu tolerieren und mit voller Geschwindigkeit zu takten.