Szybkie i wydajne obliczanie średniej ruchomej


33

Szukam rozwiązania efektywnego pod względem czasu i pamięci do obliczania średniej ruchomej w C. Muszę unikać dzielenia, ponieważ korzystam z PIC 16, który nie ma dedykowanej jednostki podziału.

W tej chwili po prostu przechowuję wszystkie wartości w buforze pierścieniowym i po prostu przechowuję i aktualizuję sumę za każdym razem, gdy pojawia się nowa wartość. Jest to naprawdę wydajne, ale niestety wykorzystuje większość mojej dostępnej pamięci ...


3
Nie wydaje mi się, żeby można to zrobić bardziej efektywnie pod względem przestrzeni.
Rocketmagnet

4
@JobyTaffey dobrze, jest to dość szeroko stosowany algorytm w systemach sterowania i wymaga radzenia sobie z ograniczonymi zasobami sprzętowymi. Myślę więc, że znajdzie tu więcej pomocy niż na SO.
clabacchio

3
@Joby: Istnieją pewne zmarszczki na temat tego pytania, które dotyczą małych systemów o ograniczonych zasobach. Zobacz moją odpowiedź. Zrobiłbyś to zupełnie inaczej w dużym systemie, do którego przywykli ludzie z SO. Wiele z tego pojawiło się w moim doświadczeniu w projektowaniu elektroniki.
Olin Lathrop,

1
Zgadzam się. Jest to całkiem odpowiednie dla tego forum, ponieważ dotyczy systemów wbudowanych.
Rocketmagnet

Odpowiedzi:


55

Jak wspomnieli inni, powinieneś rozważyć użycie filtra IIR (nieskończona odpowiedź impulsowa) zamiast filtra FIR (skończona odpowiedź impulsowa), którego używasz teraz. Jest w tym coś więcej, ale na pierwszy rzut oka filtry FIR są implementowane jako jawne zwoje i filtry IIR z równaniami.

Szczególnym filtrem IIR, którego często używam w mikrokontrolerach, jest jednobiegunowy filtr dolnoprzepustowy. Jest to cyfrowy odpowiednik prostego filtra analogowego RC. W przypadku większości aplikacji będą one miały lepsze właściwości niż używany filtr pudełkowy. Większość zastosowań filtru skrzynkowego, z którym się spotkałem, wynika z tego, że ktoś nie zwraca uwagi w klasie cyfrowego przetwarzania sygnałów, a nie z powodu potrzeby ich szczególnych cech. Jeśli chcesz tylko tłumić wysokie częstotliwości, o których wiesz, że są hałasem, lepiej jest zastosować jednobiegunowy filtr dolnoprzepustowy. Najlepszym sposobem zaimplementowania jednego cyfrowo w mikrokontrolerze jest zwykle:

FILT <- FILT + FF (NOWOŚĆ - FILT)

FILT to trwały stan. Jest to jedyna trwała zmienna potrzebna do obliczenia tego filtra. NOWOŚĆ to nowa wartość aktualizowana przez filtr w tej iteracji. FF to frakcja filtra , która reguluje „ciężkość” filtra. Spójrz na ten algorytm i zobacz, że dla FF = 0 filtr jest nieskończenie ciężki, ponieważ wynik nigdy się nie zmienia. Dla FF = 1 tak naprawdę nie ma żadnego filtru, ponieważ dane wyjściowe są zgodne z danymi wejściowymi. Przydatne wartości są pomiędzy. W małych systemach wybierasz FF na 1/2 Ntak, że mnożenie przez FF można osiągnąć jako przesunięcie w prawo o N bitów. Na przykład, FF może wynosić 1/16 i pomnożyć przez FF, a zatem przesunięcie w prawo o 4 bity. W przeciwnym razie ten filtr wymaga tylko jednego odjęcia i jednego dodania, chociaż liczby zwykle muszą być szersze niż wartość wejściowa (więcej na temat dokładności numerycznej w osobnej sekcji poniżej).

Zwykle wykonuję odczyty A / D znacznie szybciej niż są potrzebne i stosuję dwa z tych filtrów kaskadowo. Jest to cyfrowy odpowiednik dwóch filtrów RC połączonych szeregowo i tłumi się o 12 dB / oktawę powyżej częstotliwości dobiegu. Jednak w przypadku odczytów A / D zwykle lepiej jest spojrzeć na filtr w dziedzinie czasu, biorąc pod uwagę jego odpowiedź krokową. To mówi ci, jak szybko twój system zobaczy zmianę, gdy zmierzy się rzecz, którą mierzysz.

Aby ułatwić projektowanie tych filtrów (co oznacza tylko wybranie FF i podjęcie decyzji, ile z nich kaskadować), używam mojego programu FILTBITS. Podajesz liczbę bitów shift dla każdego FF w kaskadowej serii filtrów, a to oblicza odpowiedź krokową i inne wartości. Właściwie zwykle uruchamiam to za pomocą mojego skryptu opakowującego PLOTFILT. Spowoduje to uruchomienie FILTBITS, który tworzy plik CSV, a następnie drukuje plik CSV. Na przykład oto wynik „PLOTFILT 4 4”:

Dwa parametry PLOTFILT oznaczają, że będą dwa kaskady filtrów typu opisanego powyżej. Wartości 4 wskazują liczbę bitów shift do zrealizowania pomnożenia przez FF. Dwie wartości FF wynoszą zatem w tym przypadku 1/16.

Czerwony ślad jest odpowiedzią krokową jednostki i jest najważniejszą rzeczą do obejrzenia. Na przykład oznacza to, że jeśli dane wejściowe ulegną natychmiastowej zmianie, wydajność połączonego filtra wyrówna się do 90% nowej wartości w 60 iteracjach. Jeśli zależy Ci na 95% czasie rozliczenia, musisz poczekać około 73 iteracji, a na 50% czas rozliczenia tylko 26 iteracji.

Zielony ślad pokazuje moc wyjściową z jednego piku pełnej amplitudy. To daje ci pojęcie o losowym tłumieniu hałasu. Wygląda na to, że żadna pojedyncza próbka nie spowoduje więcej niż 2,5% zmiany wyniku.

Niebieski ślad ma dać subiektywne wrażenie tego, co ten filtr robi z białym szumem. To nie jest rygorystyczny test, ponieważ nie ma gwarancji, jaka dokładnie była zawartość liczb losowych wybranych jako sygnał szumu białego dla tej serii PLOTFILT. To tylko po to, aby dać ci szorstkie odczucie, jak bardzo będzie zgnieciony i jak gładki.

PLOTFILT, może FILTBITS i wiele innych przydatnych rzeczy, szczególnie do tworzenia oprogramowania układowego PIC, jest dostępnych w wersji oprogramowania PIC Development Tools na mojej stronie pobierania oprogramowania .

Dodano informacje o precyzji numerycznej

Widzę z komentarzy i teraz nowej odpowiedzi, że istnieje zainteresowanie omówieniem liczby bitów potrzebnych do wdrożenia tego filtra. Zauważ, że pomnożenie przez FF spowoduje utworzenie nowych bitów Log 2 (FF) poniżej punktu binarnego. W małych systemach FF jest zwykle wybierane jako 1/2 N, tak że mnożenie jest faktycznie realizowane przez prawe przesunięcie N bitów.

FILT jest zatem zwykle stałą liczbą całkowitą. Zauważ, że nie zmienia to żadnej matematyki z punktu widzenia procesora. Na przykład, jeśli filtrujesz 10-bitowe odczyty A / D i N = 4 (FF = 1/16), to potrzebujesz 4 bitów ułamkowych poniżej 10-bitowych liczb całkowitych A / D. Jednym z najbardziej procesorów byłoby 16-bitowe operacje na liczbach całkowitych z powodu 10-bitowych odczytów A / D. W takim przypadku nadal możesz wykonywać dokładnie te same 16-bitowe operacje na liczbach całkowitych, ale zacznij od odczytów A / D przesuniętych o 4 bity. Procesor nie zna różnicy i nie musi. Wykonywanie obliczeń matematycznych dla całych 16-bitowych liczb całkowitych działa niezależnie od tego, czy uważasz, że są to 12,4 stałe liczby całkowite, czy prawdziwe 16-bitowe liczby całkowite (stały punkt 16,0).

Ogólnie rzecz biorąc, musisz dodać N bitów do każdego bieguna filtra, jeśli nie chcesz dodawać szumu ze względu na reprezentację numeryczną. W powyższym przykładzie drugi filtr dwóch musiałby mieć 10 + 4 + 4 = 18 bitów, aby nie stracić informacji. W praktyce na maszynie 8-bitowej oznacza to, że używasz 24-bitowych wartości. Technicznie tylko drugi z dwóch biegunów potrzebowałby szerszej wartości, ale dla uproszczenia oprogramowania zwykle używam tej samej reprezentacji, a tym samym tego samego kodu, dla wszystkich biegunów filtra.

Zwykle piszę podprogram lub makro, aby wykonać jedną operację na filtrze, a następnie stosuję go do każdego z biegunów. To, czy podprogram lub makro zależy od tego, czy cykle czy pamięć programu są ważniejsze w tym konkretnym projekcie. Tak czy inaczej, używam jakiegoś stanu scratch, aby przekazać NEW do podprogramu / makra, które aktualizuje FILT, ale także ładuje to do tego samego stanu scratch NEW, w którym był. Ułatwia to stosowanie wielu biegunów, ponieważ zaktualizowane FILT jednego bieguna jest NOWOŚĆ następnego. Kiedy podprogram jest pożyteczny, wskazany jest wskaźnik FILT po drodze, który jest aktualizowany do zaraz po FILT po wyjściu. W ten sposób podprogram automatycznie działa na kolejnych filtrach w pamięci, jeśli wywoływany jest wiele razy. Z makrem nie potrzebujesz wskaźnika, ponieważ podajesz adres, aby operować na każdej iteracji.

Przykłady kodu

Oto przykład makra opisanego powyżej dla PIC 18:

////////////////////////////////////////////////// //////////////////////////////
//
// Filtr FILTRA Makro
//
// Zaktualizuj jeden biegun filtra o nową wartość w NEWVAL. NEWVAL został zaktualizowany do
// zawiera nową przefiltrowaną wartość.
//
// FILT to nazwa zmiennej stanu filtru. Przyjmuje się, że ma 24 bity
// szeroki i w lokalnym banku.
//
// Wzór na aktualizację filtra to:
//
// FILT <- FILT + FF (NEWVAL - FILT)
//
// Mnożenie przez FF odbywa się poprzez prawe przesunięcie bitów FILTBITS.
//
/ filtr makr
  /pisać
         dbankif lbankadr
         movf [arg 1] +0, w; NEWVAL <- NEWVAL - FILT
         subwf newval + 0
         movf [arg 1] +1, w
         subwfb newval + 1
         movf [arg 1] +2, w
         subwfb newval + 2

  /pisać
  / loop n filtbits; raz na każdy bit, aby przesunąć NEWVAL w prawo
         rlcf newval + 2, w; przesuń NEWVAL w prawo o jeden bit
         rrcf newval + 2
         rrcf newval + 1
         rrcf newval + 0
    / endloop

  /pisać
         movf newval + 0, w; dodaj przesuniętą wartość do filtra i zapisz w NEWVAL
         addwf [arg 1] +0, w
         movwf [arg 1] +0
         movwf newval + 0

         movf newval + 1, w
         addwfc [arg 1] +1, w
         movwf [arg 1] +1
         movwf newval + 1

         movf newval + 2, w
         addwfc [arg 1] +2, w
         movwf [arg 1] +2
         movwf newval + 2
  / endmac

A oto podobne makro dla PIC 24 lub dsPIC 30 lub 33:

////////////////////////////////////////////////// //////////////////////////////
//
// Makro FILTR ffbits
//
// Zaktualizuj stan jednego filtra dolnoprzepustowego. Nowa wartość wejściowa jest w W1: W0
// a stan filtra, który ma zostać zaktualizowany, wskazuje W2.
//
// Zaktualizowana wartość filtru zostanie również zwrócona w W1: W0 i W2 wskaże
// do pierwszej pamięci po stanie filtru. To makro może zatem być
// wywoływane kolejno w celu aktualizacji serii kaskadowych filtrów dolnoprzepustowych.
//
// Formuła filtra to:
//
// FILT <- FILT + FF (NOWOŚĆ - FILT)
//
// gdzie mnożenie przez FF jest wykonywane przez arytmetyczne przesunięcie w prawo o
// FFBITS.
//
// OSTRZEŻENIE: W3 jest zniszczony.
//
/ filtr makr
  / var new ffbits integer = [arg 1]; pobierz liczbę bitów do przesunięcia

  /pisać
  / write "; Wykonaj jednobiegunowe filtrowanie dolnoprzepustowe, shift bits =" ffbits
  /pisać " ;"

         sub w0, [w2 ++], w0; NOWOŚĆ - FILT -> W1: W0
         podpunkt w1, [w2--], w1

         lsr w0, # [v ffbits], w0; przesuń wynik w W1: W0 w prawo
         sl w1, # [- 16 ffbits], w3
         lub w0, w3, w0
         asr w1, # [v ffbits], w1

         dodaj w0, [w2 ++], w0; dodaj FILT, aby uzyskać końcowy wynik w W1: W0
         addc w1, [w2--], w1

         mov w0, [w2 ++]; zapisz wynik do stanu filtra, wskaźnik wyprzedzenia
         mov w1, [w2 ++]

  /pisać
  / endmac

Oba te przykłady są zaimplementowane jako makra przy użyciu mojego preprocesora asemblera PIC , który jest bardziej wydajny niż którykolwiek z wbudowanych narzędzi makr.


1
+1 - bezpośrednio na pieniądze. Jedyną rzeczą, którą dodam, jest to, że filtry średniej ruchomej mają swoje miejsce, gdy są wykonywane synchronicznie z jakimś zadaniem (takim jak wytwarzanie fali napędowej do napędzania generatora ultradźwięków), dzięki czemu filtrują harmoniczne 1 / T, gdzie T jest ruchem średni czas.
Jason S

2
Dobra odpowiedź, ale tylko dwie rzeczy. Po pierwsze: niekoniecznie brak uwagi prowadzi do wyboru złego filtra; w moim przypadku nigdy nie nauczono mnie o tej różnicy, to samo dotyczy osób, które nie ukończyły szkoły. Czasami jest to po prostu ignorancja. Ale po drugie: dlaczego kaskaduje się dwa filtry cyfrowe pierwszego rzędu zamiast używać filtrów wyższego rzędu? (tylko dla zrozumienia, nie krytykuję)
clabacchio

3
dwa kaskadowe jednobiegunowe filtry IIR są bardziej odporne na problemy numeryczne i łatwiejsze do zaprojektowania niż pojedynczy filtr IIR drugiego rzędu; kompromis polega na tym, że przy 2 kaskadowych stopniach dostajesz filtr o niskim Q (= 1/2?), ale w większości przypadków nie jest to wielka sprawa.
Jason S,

1
@clabacchio: Innym problemem, o którym powinienem wspomnieć, jest implementacja oprogramowania układowego. Możesz napisać podprogram jednopolowego filtra dolnoprzepustowego, a następnie zastosować go wiele razy. W rzeczywistości zwykle piszę taki podprogram, aby przenieść wskaźnik w pamięci do stanu filtru, a następnie przesunąć wskaźnik, aby można go było kolejno wywoływać w celu realizacji filtrów wielobiegunowych.
Olin Lathrop,

1
1. bardzo dziękuję za odpowiedzi - wszystkie. Zdecydowałem się użyć tego filtra IIR, ale ten filtr nie jest używany jako standardowy filtr LowPass, ponieważ muszę uśrednić wartości przeciwne i porównać je, aby wykryć zmiany w pewnym zakresie. ponieważ Wartości te mogą mieć bardzo różne wymiary w zależności od sprzętu, chciałem przyjąć średnią, aby móc automatycznie reagować na te zmiany specyficzne dla sprzętu.
sensslen

18

Jeśli możesz żyć z ograniczeniem mocy dwóch liczb przedmiotów do średniej (tj. 2,4,8,16,32 itd.), To podział można łatwo i skutecznie wykonać na mikroprocesorze o niskiej wydajności bez dedykowanego podziału, ponieważ można to zrobić jako odrobinę zmiany. Każde prawo zmiany to jedna potęga dwóch, np .:

avg = sum >> 2; //divide by 2^2 (4)

lub

avg = sum >> 3; //divide by 2^3 (8)

itp.


jak to pomaga? OP twierdzi, że głównym problemem jest przechowywanie przeszłych próbek w pamięci.
Jason S

W ogóle nie dotyczy to pytania PO.
Rocketmagnet

12
OP pomyślał, że ma dwa problemy, dzieląc się na PIC16 i pamięć dla bufora pierścieniowego. Ta odpowiedź pokazuje, że podział nie jest trudny. Wprawdzie nie rozwiązuje problemu pamięci, ale system SE pozwala na częściowe odpowiedzi, a użytkownicy mogą wziąć coś z każdej odpowiedzi dla siebie, a nawet edytować i łączyć odpowiedzi innych. Ponieważ niektóre inne odpowiedzi wymagają operacji dzielenia, są one podobnie niekompletne, ponieważ nie pokazują, jak skutecznie to osiągnąć na PIC16.
Martin

8

Jest to odpowiedź na średniej ruchomej (filtr prawda aka „filtra wagonu”) z mniejszymi wymaganiami pamięci, jeśli nie przeszkadza ci próbkowania. Nazywa się to kaskadowym filtrem grzebieniowym (CIC). Chodzi o to, że masz integrator, który bierzesz różnice w czasie, a kluczowym urządzeniem oszczędzającym pamięć jest to, że poprzez próbkowanie w dół nie musisz przechowywać wszystkich wartości integratora. Można go zaimplementować przy użyciu następującego pseudokodu:

function out = filterInput(in)
{
   const int decimationFactor = /* 2 or 4 or 8 or whatever */;
   const int statesize = /* whatever */
   static int integrator = 0;
   static int downsample_count = 0;
   static int ringbuffer[statesize];
   // don't forget to initialize the ringbuffer somehow
   static int ringbuffer_ptr = 0;
   static int outstate = 0;

   integrator += in;
   if (++downsample_count >= decimationFactor)
   {
     int oldintegrator = ringbuffer[ringbuffer_ptr];
     ringbuffer[ringbuffer_ptr] = integrator;
     ringbuffer_ptr = (ringbuffer_ptr + 1) % statesize;
     outstate = (integrator - oldintegrator) / (statesize * decimationFactor);
   }
   return outstate;
}

Twoja efektywna średnia ruchoma długość jest, decimationFactor*statesizeale musisz tylko trzymać statesizepróbki. Oczywiście możesz uzyskać lepszą wydajność, jeśli posiadasz statesizei decimationFactormasz moc 2, dzięki czemu operatory podziału i reszty zostaną zastąpione przesunięciami i maskami.


Postscript: Zgadzam się z Olinem, że zawsze powinieneś rozważyć proste filtry IIR przed filtrem średniej ruchomej. Jeśli nie potrzebujesz zerowych częstotliwości filtra boxcar, 1-biegunowy lub 2-biegunowy filtr dolnoprzepustowy prawdopodobnie będzie dobrze działał.

Z drugiej strony, jeśli filtrujesz w celu zdziesiątkowania (pobieranie danych o wysokiej częstotliwości próbkowania i uśrednianie ich w celu użycia przez proces o niskiej częstotliwości), to filtr CIC może być właśnie tym, czego szukasz. (szczególnie jeśli możesz użyć Statesize = 1 i całkowicie uniknąć bufora pierścieniowego za pomocą tylko jednej poprzedniej wartości integratora)


8

Istnieje pewna dogłębna analiza matematyki za pomocą filtra IIR pierwszego rzędu, który Olin Lathrop opisał już na temat cyfrowego przetwarzania sygnałów wymianie stosu (zawiera wiele ładnych zdjęć). Równanie dla tego filtra IIR jest następujące:

y [n] = αx [n] + (1 α) y [n − 1]

Można to zaimplementować tylko przy użyciu liczb całkowitych i bez dzielenia przy użyciu następującego kodu (może wymagać debugowania podczas pisania z pamięci).

/**
*  @details    Implement a first order IIR filter to approximate a K sample 
*              moving average.  This function implements the equation:
*
*                  y[n] = alpha * x[n] + (1 - alpha) * y[n-1]
*
*  @param      *filter - a Signed 15.16 fixed-point value.
*  @param      sample - the 16-bit value of the current sample.
*/

#define BITS 2      ///< This is roughly = log2( 1 / alpha )

short IIR_Filter(long *filter, short sample)
{
    long local_sample = sample << 16;

    *filter += (local_sample - *filter) >> BITS;

    return (short)((*filter+0x8000) >> 16);     ///< Round by adding .5 and truncating.
}

Ten filtr aproksymuje średnią ruchomą ostatnich K próbek, ustawiając wartość alfa na 1 / K. Czy to w poprzednim kodzie przez #defineing BITSdo log2 (K), czyli dla k = 16 zestaw BITS4, dla k = 4 zestawBITS 2, itd.

(Sprawdzę kod wymieniony tutaj, jak tylko otrzymam zmianę i w razie potrzeby edytuję odpowiedź).


6

Oto jednobiegunowy filtr dolnoprzepustowy (średnia ruchoma, z częstotliwością odcięcia = częstotliwość odcięcia). Bardzo prosty, bardzo szybki, działa świetnie i prawie nie ma narzutu pamięci.

Uwaga: Wszystkie zmienne mają zakres wykraczający poza funkcję filtrowania, z wyjątkiem przekazanych w newInput

// One-time calculations (can be pre-calculated at compile-time and loaded with constants)
DecayFactor = exp(-2.0 * PI * CutoffFrequency / SampleRate);
AmplitudeFactor = (1.0 - DecayFactor);

// Filter Loop Function ----- THIS IS IT -----
double Filter(double newInput)
{
   MovingAverage *= DecayFactor;
   MovingAverage += AmplitudeFactor * newInput;

   return (MovingAverage);
}

Uwaga: jest to filtr jednostopniowy. Można kaskadowo połączyć wiele etapów, aby zwiększyć ostrość filtra. Jeśli używasz więcej niż jednego stopnia, będziesz musiał dostosować DecayFactor (w odniesieniu do częstotliwości odcięcia), aby to zrekompensować.

I oczywiście wszystko, czego potrzebujesz, to te dwie linie umieszczone gdziekolwiek, nie potrzebują one własnej funkcji. Ten filtr ma czas narastania, zanim średnia ruchoma reprezentuje wartość sygnału wejściowego. Jeśli chcesz ominąć ten czas narastania, możesz po prostu zainicjować MovingAverage do pierwszej wartości newInput zamiast 0 i mieć nadzieję, że pierwszy newInput nie będzie wartością odstającą.

(CutoffFrequency / SampleRate) ma zakres od 0 do 0,5. DecayFactor to wartość między 0 a 1, zwykle bliska 1.

Pływaki o pojedynczej precyzji są wystarczające do większości rzeczy, po prostu wolę podwójne. Jeśli chcesz trzymać się liczb całkowitych, możesz przekonwertować DecayFactor i Amplitude Factor na ułamkowe liczby całkowite, w których licznik jest zapisany jako liczba całkowita, a mianownik jest liczbą całkowitą o wartości 2 (więc możesz przesuwać bity w prawo jako mianownik zamiast konieczności dzielenia podczas pętli filtra). Na przykład, jeśli DecayFactor = 0,99 i chcesz użyć liczb całkowitych, możesz ustawić DecayFactor = 0,99 * 65536 = 64881. A następnie za każdym razem, gdy pomnożysz przez DecayFactor w pętli filtrów, po prostu przesuń wynik >> 16.

Aby uzyskać więcej informacji na ten temat, doskonała książka dostępna online, rozdział 19 na temat filtrów rekurencyjnych: http://www.dspguide.com/ch19.htm

PS W paradygmacie średniej ruchomej, inne podejście do ustawiania DecayFactor i AmplitudeFactor, które może być bardziej odpowiednie dla twoich potrzeb, powiedzmy, że chcesz poprzednie, około 6 pozycji uśrednionych razem, robiąc to dyskretnie, dodałeś 6 pozycji i dzielisz przez 6, dzięki czemu można ustawić AmplitudeFactor na 1/6, a DecayFactor na (1,0 - AmplitudeFactor).


4

Możesz ustawić przybliżoną średnią ruchomą dla niektórych aplikacji za pomocą prostego filtra IIR.

waga to wartość 0..255, wysokie wartości = krótsza skala czasowa dla uśrednienia

Wartość = (nowa wartość * waga + wartość * (256-waga)) / 256

Aby uniknąć błędów zaokrąglania, wartość byłaby zwykle długa, z czego jako rzeczywistych wartości używa się tylko bajtów o wyższym porządku.


3

Wszyscy inni dokładnie skomentowali użyteczność IIR vs. FIR i podział potęgi dwóch. Chciałbym podać kilka szczegółów implementacji. Poniżej działa dobrze na małych mikrokontrolerach bez FPU. Nie ma mnożenia, a jeśli utrzymasz N potęgę dwóch, cały podział jest jednokierunkowy z przesuwaniem bitów.

Podstawowy bufor pierścieniowy FIR: zachowaj działający bufor ostatnich N wartości i działający SUMA wszystkich wartości w buforze. Za każdym razem, gdy pojawia się nowa próbka, odejmij najstarszą wartość w buforze od SUM, zamień ją na nową próbkę, dodaj nową próbkę do SUMY i wyślij SUM / N.

unsigned int Filter(unsigned int sample){
    static unsigned int buffer[N];
    static unsigned char oldest = 0;
    static unsigned long sum;

    sum -= buffer[oldest];
    sum += sample;
    buffer[oldest] = sample;
    oldest += 1;
    if (oldest >= N) oldest = 0;

    return sum/N;
}

Zmodyfikowany bufor pierścieniowy IIR: utrzymuj działający SUM ostatnich N wartości. Za każdym razem, gdy pojawia się nowa próbka, SUM - = SUM / N, dodaj nową próbkę i wyślij SUM / N.

unsigned int Filter(unsigned int sample){
    static unsigned long sum;

    sum -= sum/N;
    sum += sample;

    return sum/N;
}

Jeśli dobrze cię czytam, opisujesz filtr IIR pierwszego rzędu; wartość, którą odejmujesz, nie jest najstarszą wypadającą wartością, ale jest średnią z poprzednich wartości. Filtry IIR pierwszego rzędu z pewnością mogą być przydatne, ale nie jestem pewien, co masz na myśli, gdy sugerujesz, że wyjście jest takie samo dla wszystkich sygnałów okresowych. Przy częstotliwości próbkowania 10 kHz, wprowadzenie fali prostokątnej 100 Hz do 20-stopniowego filtra skrzynkowego da sygnał, który wzrośnie równomiernie dla 20 próbek, ustawi się wysoko na 30, spadnie równomiernie na 20 próbek i pozostanie niski na 30. Pierwszego rzędu Filtr IIR ...
supercat

... wytworzy falę, która gwałtownie zacznie rosnąć i stopniowo wyrówna się w pobliżu maksimum wejściowego (ale nie w jej pobliżu), a następnie gwałtownie zacznie opadać i stopniowo wyrówna się w pobliżu (ale nie w) minimum wejściowego. Bardzo różne zachowanie.
supercat

Masz rację, myliłem dwa rodzaje filtrów. Jest to rzeczywiście IIR pierwszego rzędu. Zmieniam odpowiedź, aby dopasować. Dzięki.
Stephen Collings

Jednym z problemów jest to, że prosta średnia ruchoma może, ale nie musi być przydatna. Z filtrem IIR możesz uzyskać niezły filtr ze stosunkowo niewielką ilością cieląt. Opisany przez ciebie FIR może dać ci tylko prostokąt w czasie - sinus w freq - i nie możesz zarządzać płatami bocznymi. Być może warto wrzucić kilka mnożników całkowitych, aby był to fajny symetryczny strojony FIR, jeśli możesz oszczędzić tyknięcia zegara.
Scott Seidman

@ScottSeidman: Nie ma potrzeby mnożenia, jeśli po prostu ma się każdy stopień FIR, albo wyprowadza średnią z danych wejściowych do tego stopnia i jego poprzednią zapisaną wartość, a następnie zapisuje dane wejściowe (jeśli ktoś ma zakres liczbowy, można użyć sumy raczej niż średnia). To, czy jest to lepsze niż filtr pudełkowy, zależy od aplikacji (na przykład odpowiedź krokowa filtra pudełkowego z całkowitym opóźnieniem 1 ms będzie miała nieprzyjemny skok d2 / dt, gdy zmieni się wejście, i ponownie 1 ms później, ale będzie miała minimalne możliwe d / dt dla filtra z całkowitym opóźnieniem 1 ms).
supercat

2

Jak powiedział mikeselectricstuff , jeśli naprawdę musisz zmniejszyć swoje potrzeby pamięciowe i nie masz nic przeciwko, że twoja odpowiedź impulsowa jest wykładnicza (zamiast prostokątnego impulsu), wybrałbym wykładniczą średnią ruchomą filtr . Używam ich szeroko. Przy takim filtrze nie potrzebujesz bufora. Nie musisz przechowywać N poprzednich próbek. Tylko jeden. Zatem twoje wymagania dotyczące pamięci zostaną zmniejszone o współczynnik N.

Ponadto nie potrzebujesz do tego żadnego podziału. Tylko mnożenia. Jeśli masz dostęp do arytmetyki zmiennoprzecinkowej, użyj mnożenia zmiennoprzecinkowego. W przeciwnym razie wykonaj mnożenie liczb całkowitych i przesuń w prawo. Jednak jesteśmy w 2012 roku i polecam korzystanie z kompilatorów (i MCU), które pozwalają pracować z liczbami zmiennoprzecinkowymi.

Poza tym, że jest bardziej wydajna i szybsza w pamięci (nie musisz aktualizować elementów w żadnym okrągłym buforze), powiedziałbym, że jest to również bardziej naturalne , ponieważ wykładnicza odpowiedź impulsowa lepiej pasuje do zachowania natury, w większości przypadków.


5
Nie zgadzam się z tobą zaleceniem używania liczb zmiennoprzecinkowych. OP z pewnej przyczyny prawdopodobnie używa 8-bitowego mikrokontrolera. Znalezienie 8-bitowego mikrokontrolera ze sprzętową obsługą zmiennoprzecinkową może być trudnym zadaniem (znasz jakieś?). A używanie liczb zmiennoprzecinkowych bez obsługi sprzętowej będzie zadaniem bardzo wymagającym zasobów.
PetPaulsen,

5
Mówienie, że zawsze powinieneś używać procesu z funkcją zmiennoprzecinkową, jest po prostu głupie. Poza tym każdy procesor może wykonywać zmiennoprzecinkowe, to tylko kwestia szybkości. W świecie osadzonym kilka centów kosztu budowy może mieć znaczenie.
Olin Lathrop,

@Olin Lathrop i PetPaulsen: Nigdy nie powiedziałem, że powinien używać MCU ze sprzętowym FPU. Przeczytaj ponownie moją odpowiedź. Przez „(i MCU)” rozumiem MCU wystarczająco mocne do płynnej pracy z programową arytmetyką zmiennoprzecinkową, co nie ma miejsca w przypadku wszystkich MCU.
Telaclavo,

4
Nie ma potrzeby używania zmiennoprzecinkowego (sprzętowego oprogramowania LUB) tylko dla 1-biegunowego filtra dolnoprzepustowego.
Jason S

1
Gdyby miał operacje zmiennoprzecinkowe, nie sprzeciwiłby się podziałowi.
Federico Russo,

0

Jednym z problemów z filtrem IIR, który prawie dotknął @olin i @ supercat, ale najwyraźniej został pominięty przez innych, jest to, że zaokrąglanie w dół wprowadza pewną niedokładność (i potencjalnie stronniczość / obcięcie): zakładając, że N jest potęgą dwóch, a tylko arytmetyka liczb całkowitych jest zastosowane przesunięcie w prawo systematycznie eliminuje LSB nowej próbki. Oznacza to, że jak długo może być seria, średnia nigdy nie weźmie ich pod uwagę.

Załóżmy na przykład, że seria powoli maleje (8,8,8, ..., 8,7,7,7, ... 7,6,6,) i załóżmy, że na początku średnia rzeczywiście wynosi 8. Pierwsza próbka „7” doprowadzi średnią do 7, niezależnie od siły filtra. Tylko dla jednej próbki. Ta sama historia dla 6 itd. Pomyśl teraz o czymś przeciwnym: seria idzie w górę. Średnia pozostanie na 7 na zawsze, dopóki próbka nie będzie wystarczająco duża, aby ją zmienić.

Oczywiście można poprawić „odchylenie”, dodając 1/2 ^ N / 2, ale to tak naprawdę nie rozwiąże problemu z precyzją: w takim przypadku malejąca seria pozostanie na zawsze na poziomie 8, aż próbka osiągnie 8-1 / 2 ^ (N / 2). Na przykład dla N = 4 każda próbka powyżej zera zachowa średnią bez zmian.

Uważam, że rozwiązanie tego problemu oznaczałoby przechowanie akumulatora utraconych LSB. Ale nie zrobiłem tego wystarczająco daleko, aby przygotować kod i nie jestem pewien, czy nie zaszkodziłoby to mocy IIR w niektórych innych przypadkach serii (na przykład, czy 7,9,7,9 to wtedy średnia do 8) .

@Olin, twoja dwustopniowa kaskada również potrzebuje wyjaśnienia. Czy masz na myśli utrzymywanie dwóch średnich wartości z wynikiem pierwszego wprowadzonego do drugiego w każdej iteracji? Jaka jest z tego korzyść?

Korzystając z naszej strony potwierdzasz, że przeczytałeś(-aś) i rozumiesz nasze zasady używania plików cookie i zasady ochrony prywatności.
Licensed under cc by-sa 3.0 with attribution required.