IIR-Filter auf einer 16-Bit-MCU (PIC24F)

Ich versuche, einen einfachen IIR-Filter erster Ordnung auf einer MCU (PIC24FJ32GA002) zu implementieren, bisher ohne Erfolg. Der Filter ist ein DC-Tracking-Filter (Tiefpassfilter), dessen Zweck es ist, die DC-Komponente eines 1,5-Hz-Signals zu verfolgen. Die Differenzgleichung wurde einem TI Application Note entnommen:

y(n)=K x(n)+y(n-1) (1-K)
mit K = 1/2^8

Ich habe ein MATLAB-Skript erstellt, um es zu testen, und es funktioniert gut in der Simulation. Verwendeter Code:

K=1/2^8
b = K
a = [1 -(1-K)]

Fs=200; // sampling frequency
Ts=1/Fs;
Nx=5000; // number of samples
nT=Ts*(0:Nx-1);
fin=1.5; // signal frequency

randn('state',sum(100*clock));
noise=randn(1,Nx);
noise=noise-mean(noise);

xin=200+9*(cos(2*pi*fin*nT));
xin=xin+noise;

out = filter(b,a,xin);

Simulation Frequenzgang

Ich kann es jedoch nicht auf einem PIC24F-Mikrocontroller implementieren. Ich stelle die Koeffizienten im Format Q15 (1.15) dar, speichere sie in kurzen Variablen und verwende eine lange für Multiplikationen. Hier ist der Code:

short xn;
short y;
short b0 = 128, a1 = 32640; // Q15
long aux1, aux2;

// (...)

while(1){

    xn = readADC(adc_ch);

    aux1 = ((long)b0*xn) << 1;
    aux2 = ((long)a1*y) << 1;
    y = ((aux1 + aux2) >> 16);

    delay_ms(5);
}

Long Cast wird verwendet, um das Signal zu verlängern, damit die Multiplikationsoperation korrekt ausgeführt wird. Nach jeder Multiplikation verschiebe ich ein Bit nach links, um das erweiterte Signalbit zu entfernen. Beim Summieren verschiebe ich 16 Bit nach rechts, um y im Q15-Format zu erhalten.

Ich debugge die MCU mit Pickit2 und dem Fenster "View-> Watch" (MPLAB IDE 8.53) und teste den Filter mit einem DC-Signal (ich ändere das DC-Signal mit einem Potenziometer, um verschiedene Werte zu testen). Der ADC hat eine Auflösung von 10 Bit und die MCU wird mit 3,3 V versorgt. Einige Ergebnisse:

1V --> xn = 312 (richtig), yn = 226 (falsch)
1,5V --> xn = 470 (richtig), yn = 228 (völlig falsch)

Was mache ich falsch? Irgendwelche Vorschläge zur Implementierung dieses IIR-Filters auf einer 16-Bit-MCU?

Vielen Dank im Voraus :)

Antworten (6)

Ich bin nicht sehr tief in Ihr Filterdesign eingetaucht, aber ein Blick auf den Quellcode bringt ein paar Dinge hervor. Zum Beispiel diese Zeilen:

aux1 = ((long)b0*xn) << 1;
aux2 = ((long)a1*y) << 1;
y = ((aux1 + aux2) >> 16);

Das erste Problem, das ich sehe, ist das ((long)b0*xn). Ich bin auf Compiler gestoßen, die dies falsch als ((long)(b0*xn)) kompilieren würden, was völlig falsch ist. Sicherheitshalber würde ich das schreiben als (((long)b0)*((long)xn)). Sicher, das ist paranoide Programmierung, aber...

Wenn Sie als nächstes "<<1" ausführen, ist dies NICHT dasselbe wie "*2". Für die meisten Dinge ist es nah, aber nicht für DSP. Es hat damit zu tun, wie der MCU/DSP mit Überlaufbedingungen umgeht und Erweiterungen signiert usw. Aber selbst wenn es als *2 funktioniert, entfernen Sie ein Bit der Auflösung, das Sie nicht entfernen müssen. Wenn Sie wirklich ein *2 machen müssen, dann machen Sie ein *2 und lassen Sie den Compiler herausfinden, ob er stattdessen ein <<1 ersetzen könnte.

Auch die >>16 ist problematisch. Aus dem Kopf heraus weiß ich nicht, ob es eine logische oder arithmetische Verschiebung sein wird. Sie wollen eine arithmetische Verschiebung. Arithmetische Verschiebungen behandeln das Vorzeichenbit korrekt, während eine logische Verschiebung Nullen für die neuen Bits einfügt. Außerdem können Sie Bits an Auflösung sparen, indem Sie die <<1 loswerden und die >>16 in >>15 ändern. Nun, und all diese in normale Multiplikationen und Divisionen umzuwandeln.

Also hier ist der Code, den ich verwenden würde:

aux1 = ((long)b0) * ((long)xn);
aux2 = ((long)a1) * ((long)y);
y = (short)((aux1+aux2) / 32768);

Nun, ich behaupte nicht, dass dies Ihr Problem lösen wird. Es kann oder auch nicht, aber es verbessert Ihren Code.

Es hat sich nicht gelöst, aber es hat geholfen, den Code und die "Tricks" des Compilers besser zu verstehen. Das Problem war mit den Koeffizienten. Es sollte a1*xn und b0*y sein. Ein wirklich lahmer Fehler in der Tat eheh Danke für deine Hilfe!
+1: Technisch gesehen definiert der C-Standard nicht, ob >> eine arithmetische oder logische Verschiebung ist ... oder zumindest sagten mir das die Leute von Microchip vor ein paar Jahren, als ich berichtete, dass ich Probleme mit >> zum Einschalten habe -2-teilt wegen fehlender Vorzeichenerweiterungen.

Ich schlage vor, dass Sie "short" durch "int16_t" und "long" durch "int32_t" ersetzen. Speichern Sie "short", "int", "long" usw. für Stellen, an denen die Größe keine Rolle spielt, z. B. Loop-Indizes.

Dann funktioniert der Algorithmus genauso, wenn Sie auf Ihrem Desktop-Computer kompilieren. Es wird viel einfacher sein, auf dem Desktop zu debuggen. Wenn es auf dem Desktop zufriedenstellend funktioniert, verschieben Sie es auf den Mikroprozessor. Um dies zu vereinfachen, fügen Sie die knifflige Routine in eine eigene Datei ein. Sie werden wahrscheinlich eine einfache main()-Funktion für den Desktop schreiben wollen, die die Routine ausführt und Null (für Erfolg) oder Nicht-Null (für Misserfolg) verlässt.

Integrieren Sie dann das Erstellen und Ausführen des Tests in das Buildsystem für Ihr Projekt. Jedes erwägenswerte Entwicklungssystem lässt Sie dies tun, aber Sie müssen möglicherweise das Handbuch lesen. Ich mag GNUMake .

Noch besser, Sie müssen diese Typedefs nicht selbst definieren, wenn Ihr System eine <stdint.h> hat: pubs.opengroup.org/onlinepubs/007904975/basedefs/stdint.h.html

David Kessner erwähnte die >> 16 als nicht zuverlässig, was wahr ist. Das hat mich schon einmal gebissen, also habe ich jetzt immer ein statisches Assertion in meinem Code, das überprüft, ob >> so funktioniert, wie ich es erwarte.

Er schlug stattdessen eine tatsächliche Spaltung vor. Aber hier erfahren Sie, wie Sie richtig rechnen >> 16, wenn sich herausstellt, dass Ihre es nicht ist.

union
{
     int16_t i16s[2];
    uint16_t i16u[2];

     int32_t i32s;
    uint32_t i32u;
}union32;

union32 x;
x.i32s = -809041920;

// now to divide by 65536

x.i16u[0] = x.i16u[1];        // The shift happens in one go.

                              // Now we do the sign extension
if (x.i16u[0] & 0x8000)       // Was this a negative number?
    x.i16u[1] = 0xFFFF;       // Then make sure the final result is still negative
else
    x.i16u[1] = 0x0000;       // Otherwise make sure the final result is still positive

// Now x.i32s = -12345

Überprüfen Sie, ob Ihr Compiler guten Code für (x.i16u[0] & 0x8000) generiert. Mikrochip-Compiler nutzen in solchen Fällen nicht immer die BTFSC-Anweisungen des PIC. Besonders die 8-Bit-Compiler. Manchmal bin ich gezwungen, dies in Asm zu schreiben, wenn ich die Leistung wirklich brauche.

Das Ausführen der Verschiebung auf diese Weise ist bei PICs, die jeweils nur ein Bit verschieben können, viel viel schneller.

+1 für die Verwendung von intNN_t- und uintNN_t-Typedefs, auch den Union-Ansatz, die ich beide in meinen Systemen verwende.
@JasonS - Danke. Der Union-Ansatz unterscheidet Embedded-Entwickler von PC-Entwicklern. Die PC-Jungs scheinen keine Ahnung zu haben, wie ganze Zahlen funktionieren.

Die Verwendung eines dsPIC anstelle des PIC24 könnte die Dinge viel einfacher machen (sie sind Pin-kompatibel). Der dsPIC-Compiler wird mit einer DSP-Bibliothek geliefert, die IIR-Filter enthält, und die Hardware unterstützt direkt Festkomma-Arithmetik.

Ich muss diese MCU (PIC24F) verwenden, da sie in einer tragbaren Low-Power-Anwendung verwendet werden soll und dsPIC diese Anforderung nicht erfüllt. Der Filter ist einfach, es ist ein Filter 1. Ordnung. Ich nehme an, mein Problem liegt beim Konvertieren des Double-> Fixed-Point-Formats. Wenn ich jedoch den Code lese, denke ich, dass ich das Richtige tue, aber es funktioniert nicht ... Ich frage mich, warum.
"wird in einer tragbaren Low-Power-Anwendung verwendet und dsPIC erfüllt diese Anforderung nicht" <-- diese Art von Pauschalaussage löst bei mir Alarmglocken aus. Der Stromverbrauch ist anwendungsabhängig, und Hardwareunterstützung bedeutet oft einen geringeren Gesamtstromverbrauch. Jede Low-Power-Anwendung wird den Prozessor die meiste Zeit im Ruhezustand halten, daher ist die Reduzierung der "On"-Zeit der Weg zu einem geringeren Stromverbrauch ...
ist das festkomma oder gleitkomma? Floating wäre ein Plus, aber Sie brauchen keinen DSP, um einen Bi-Quad-Filter zu implementieren. Sie benötigen viel mehr eine Filterdesign-Software, um die Parameter zu finden.

Es sieht so aus, als würdest du dir nie einen Wert geben, bevor du es benutzt. Sie verwenden y in der Berechnung für aux2, was richtig ist, da es sich um das gefilterte vorherige Sample handelt, aber Sie haben keine Ahnung, wie es in dieser Situation beginnen wird.

Ich würde vorschlagen, dass Sie ein ADC-Sample nehmen, bevor Sie Ihre while(1)-Schleife eingeben. Weisen Sie diesen Wert y zu, verzögern Sie 5 ms und geben Sie dann Ihre Schleife ein. Dadurch kann Ihr Filter normal funktionieren.

Ich würde auch empfehlen, dass Sie weitermachen und Ihre Shorts ändern, um längere Variablen zu sein. Wenn Ihnen nicht der Speicherplatz ausgeht, haben Sie viel einfacheren Code, wenn Sie nicht in jeder Schleife cast eingeben müssen.

Viele frühe MCUs haben nicht einmal einen Multiplikationsbefehl, geschweige denn einen Divisionsbefehl. Der IIR-Filter mit dem magischen Wert mit K = 1/2^8 wurde speziell für solche MCUs entwickelt.

// filter implements
// y(n)=K*x(n)+y(n-1)*(1-K)
// with K = 1/2^8.
// based on ideas from
// http://techref.massmind.org/techref/microchip/math/filter.htm
// WARNING: untested code.
// Uses no multiplies or divides.
// Eventually y converges to a kind of "average" of the x values.
short average; // global variable
// (...)
while(1){
    short xn = readADC(adc_ch);
    // hope that we don't overflow the subtract --
    // -- perhaps use saturating arithmetic here?
    short aux1 = xn - average;
    short aux2 = aux1 / 256;
    // compiler should use an arithmetic shift,
    // or perhaps an unaligned byte read, not an actual divide
    average += aux2;
    delay_ms(5);
}