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 ...
Odpowiedzi:
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:
A oto podobne makro dla PIC 24 lub dsPIC 30 lub 33:
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.
źródło
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 .:
lub
itp.
źródło
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:
Twoja efektywna średnia ruchoma długość jest,
decimationFactor*statesize
ale musisz tylko trzymaćstatesize
próbki. Oczywiście możesz uzyskać lepszą wydajność, jeśli posiadaszstatesize
idecimationFactor
masz 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)
źródło
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).
Ten filtr aproksymuje średnią ruchomą ostatnich K próbek, ustawiając wartość alfa na 1 / K. Czy to w poprzednim kodzie przez
#define
ingBITS
do log2 (K), czyli dla k = 16 zestawBITS
4, dla k = 4 zestawBITS
2, itd.(Sprawdzę kod wymieniony tutaj, jak tylko otrzymam zmianę i w razie potrzeby edytuję odpowiedź).
źródło
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
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).
źródło
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.
źródło
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.
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.
źródło
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.
źródło
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ść?
źródło