Proste filtry cyfrowe cz. 3. – filtr dolnoprzepustowy

W dwóch poprzednich materiałach poznaliśmy filtr uśredniający oraz medianowy. Oba z nich wykorzystywały tylko informacje wejściowe. Dzisiaj chciałbym przedstawić Ci prosty filtr dolnoprzepustowy, który korzysta ze sprzężenia od wyjścia. Zobaczymy z czym to się wiąże i kiedy warto go stosować.

Czym jest filtr dolnoprzepustowy

Filtr dolnoprzepustowy (ang. low-pass filter) to rodzaj filtru, który przepuszcza sygnały o niższych częstotliwościach i tłumi sygnały o wyższych częstotliwościach. Jest szeroko stosowany w dziedzinie przetwarzania sygnałów, takich jak przetwarzanie dźwięku, obrazu, telekomunikacja i wiele innych aplikacji.

My często stosując filtr dolnoprzepustowy nie dobieramy współczynników na podstawie wymagań, takich jak częstotliwość odcięcia. Do prostych zastosowań stroimy filtr zazwyczaj „na oko”. Dlatego nie będę się mocno zagłębiał w teorię dotyczącą filtru, a skupię się na implementacji w kodzie. Jeżeli chciałbyś przeczytać więcej teorii na temat filtrów dolnoprzepustowych i tego, jak dokładnie wyprowadza się wzór, zajrzyj na Wikipedię.

Końcowa forma wzoru na filtr ma postać:

gdzie:

  • y – sygnał wyjściowy
  • x – sygnał wejściowy
  • α – współczynnik filtru (może przyjmować wartości z zakresu od 0 do 1)

Do głównych zalet filtrów dolnoprzepustowych zaliczamy to, że poprzez tłumienie sygnałów o wyższych częstotliwościach, filtry dolnoprzepustowe pozwalają skoncentrować się na sygnałach o bardziej istotnych niższych częstotliwościach. Dodatkowo filtry dolnoprzepustowe mogą być wykorzystywane do wygładzania sygnałów, eliminując nagłe zmiany i fluktuacje. To przydatne w przypadkach, gdy zależy nam na uzyskaniu bardziej stabilnego i jednostajnego sygnału. W tym materiale bardziej skupiamy się na drugim zastosowaniu.

Jednak filtry dolnoprzepustowe mają także wady. Przejście między pasmem przepustowym a pasmem zaporowym może powodować pewne zniekształcenia w sygnale. Zazwyczaj w okolicach częstotliwości granicznej można zaobserwować tzw. efekt wyciemniania (ang. roll-off), który prowadzi do stopniowego zmniejszania się amplitudy sygnału. Filtry dolnoprzepustowe wprowadzają opóźnienie grupowe, co oznacza, że różne składowe częstotliwości sygnału będą przesunięte w czasie w zależności od swojej częstotliwości. Może to mieć znaczenie w aplikacjach, w których wymagana jest synchronizacja sygnałów. Poza tym może tłumić nie tylko niepożądane składowe sygnału, ale także pewne części sygnału, które mogą być istotne. To może prowadzić do utraty informacji lub pogorszenia jakości sygnału.

Pomimo pewnych wad, filtry dolnoprzepustowe są bardzo popularne i często stosowane zarówno do prostszych, jak i bardziej wymagających zastosowań.

Implementacja i przykład dla STM32

Poznaliśmy niezbędną dawkę teorii potrzebną do zrozumienia działania filtru. Przyjrzyjmy się przykładowi w języku C. Filtr medianowy zastosujemy w analogiczny sposób, jak filtr uśredniający – użyjemy go, aby przefiltrować dane otrzymane z pomiarów sygnałów analogowych, czyli wyjście z konwertera ADC. Jako źródło sygnału również wykorzystamy potencjometr.

Konfiguracja mikrokontrolera

Do przykładu wykorzystamy mikrokontroler STM32L476RG dostępny w zestawie Nucleo-L476RG. Program napiszemy przy pomocy środowiska STM32CubeIDE.

Tworzymy nowy projekt wybierając „File->New->STM32 Project”. Przechodzimy przez wstępną konfigurację projektu i zabieramy się za konfigurację wyjść mikrokontrolera. Ja wygenerowałem projekt z domyślną konfiguracją dla płytki Nucleo, dlatego część pinów mam już skonfigurowane. Do obsługi potencjometru będziemy potrzebowali wejścia analogowego. W tej roli wykorzystamy wejście 5 przetwornika ADC1 (ADC1_IN5) podłączone do pinu PA0. Aby poprawnie skonfigurować pin, przechodzimy do zakładki po lewej stronie i wybieramy „Analog->ADC1”, a następnie przy wejściu IN5 wybieramy tryb „IN5 Single-ended” (tryb pomiaru na kanale ADC w odniesieniu do masy).

Teraz możemy przejść do konfiguracji ustawień przetwornika. Konwerter ADC w mikrokontrolerach STM32 to zaawansowany i dość rozbudowany system. Daje nam wiele możliwości, ale co za tym idzie, na początku trudniej jest się odnaleźć w „gąszczu” ustawień. Podstawowym wyborem jest wybór rodzaju konwersji. Mamy do dyspozycji dwa tryby: regularny (regular) i wstrzykiwany (injected). Podstawową różnicą jest to, że tryb wstrzykiwany ma wyższy priorytet i w przypadku wywołania jednoczesnej konwersji na kanale regularnym i wstrzykiwanym, to kanał wstrzykiwany będzie obsłużony w pierwszej kolejności. Poza wyniki konwersji kanałów wstrzykiwanych są przechowywane w indywidualnych rejestrach, a kanałów regularnych w jednym wspólnym rejestrze, który trzeba dostatecznie szybko odczytać, aby nie został nadpisany przez kolejny pomiar. Do naszego zastosowania w zupełności wystarczy nam konwersja w trybie regularnym.

Wśród podstawowych ustawień przetwornika ADC możemy wyróżnić:

  • Ustawienia ogólne ADC (ADC Settings):
    • Clock Prescaler – dzielnik zegara taktującego przetwornik. Wyższa wartość spowoduje, że pomiary będą wykonywane wolniej.
    • Resolution – rozdzielczość pomiaru (6, 8, 10 lub 12 bitów).
    • Data alignment – sposób wyrównania bitów danych w rejestrze wyjściowym (do prawej lub do lewej).
    • Scan Conversion Mode – tryb skanowania dostępny w przypadku wykonywania pomiarów na kilku kanałach. Powoduje, że pomiar wykonywany jest na całej grupie kanałów jeden po drugi, czyli po wykonaniu pomiaru na jednym kanale, przetwornik automatycznie wykona pomiar na kolejnym (i tak aż przejdzie wszystkie kanały).
    • Continuous Conversion Mode – tryb ciągły pomiarów, umożliwia automatyczne wystartowanie pomiarów na kanale (grupie kanałów) zaraz po ukończeniu poprzedniego.
    • Discontinuous Conversion Mode – umożliwia wykonanie pojedynczo pomiarów w grupie. Powoduje, że po wykonaniu pomiaru na jednym kanale, przetwornik czeka na start konwersji i dopiero wykonuje pomiar na kolejnym.
    • DMA Continuous Request – ustawia przesyłanie danych przez DMA w trybie ciągłym, czyli ADC generuje żądanie transferu DMA za każdym razem, gdy nowe dane są dostępne w rejestrze, nawet jeśli DMA wykonał ostatni transfer w grupie.
    • End Of Conversion Selection – określa, jakie zdarzenie generuje ustawienie flagi końca konwersji (pojedyncza konwersja czy konwersja całej grupy).
  • Ustawienia konwersji w trybie regularnym (ADC Regular Conversion Mode):
    • Enable Regular Conversion – włącza tryb konwersji regularnej
    • Nubmer of Conversion – określa liczbę konwersji do wykonania
    • External Trigger Conversion Source – wybór zdarzenia, które będzie rozpoczynało konwersję
    • External Trigger Conversion Edge – wybór zbocza, na którym będzie następowało wywołanie konwersji

Dla każdej konwersji możemy indywidualnie ustawić takie parametry, jak:

  • Channel – kanał pomiaru, możemy tutaj zdecydować w jakiej kolejności będą się wykonywały pomiary
  • Sampling Time – czas pomiaru zapisany w cyklach zegara, im dłuższy czas, tym dane są dokładniejsze

Powyżej przedstawiłem tylko najważniejsze parametry, z których będziemy korzystali najczęściej przy konfiguracji ADC.

W naszym przypadku będziemy wykonywali pomiar tylko na jednym kanale. Aby zautomatyzować odczyt danych o odległości, wykorzystamy zewnętrzne źródło wyzwalania pomiarów w postaci timera, który co 100 ms generował sygnał dla ADC.

W celu poprawnego skonfigurowania przetwornika wybieramy zatem brak dodatkowego Prescalera, rozdzielczość 12-bitów oraz wyrównanie danych do prawej. Ponieważ mamy tylko jeden pomiar, nie będziemy potrzebowali trybu skanowania. Pomiar ADC będziemy chcieli mieć wywoływany co 10 ms przez timera, dlatego tryb ciągły też nie będzie nam potrzebny. Analogicznie nie potrzebujemy też trybu Discontinuous. Włączamy natomiast DMA Continuous Request Request i End Of Conversion Mode jako zakończenie pojedynczej konwersji.

W ustawieniach trybu regularnej konwersji, wybieramy zewnętrzne zdarzenie jako Timer 3 Trigger Out Event (czyli przepełnienie od timera 3 – lista dostępnych źródeł przerwań jest dostępna w liście rozwijanej obok parametru External Trigger Conversion Source oraz w dokumentacji „Reference Manual” na stronie 528 w tabeli 108.) oraz zbocze narastające. W ustawieniach pomieru wybieramy kanał 5 (wejście na pinie PA0) oraz najdłuższy czas konwersji, czyli 640,5 cykli, co zapewni nam większą dokładność. Pełna konfiguracja widoczna jest poniżej.

Żeby wykorzystać przesyłanie danych za pomocą DMA, w zakładce DMA Settings wybieramy ADC1 i DMA1 Channel 1. W ustawieniach może pozostać tryb Circular, bez inkrementacji adresów (mamy tylko jeden pomiar) oraz długość danych Half Word.

Na koniec włączamy jeszcze przerwania od DMA, dzięki czemu będziemy wiedzieli kiedy zakończył się pomiar, dane są gotowe w naszej zmiennej i można już wykonać filtrację.

Teraz powinniśmy skonfigurować jeszcze Timer 3 w taki sposób, aby wywoływał nam pomiar na ADC co 10 ms. Wybieramy zatem Timers->TIM3. W górnej części zaznaczamy Clock Source jako Internal Clock.

Aby skonfigurować licznik, musimy odpowiednio ustawić wartości: Prescaler i Counter Period. Potrzebujemy zatem informacji o częstotliwości taktowania Timera 3. Zgodnie z dokumentacją (Datasheet) mikrokontrolera na stronie 17, TIM3 podłączony jest do szyny taktującej APB1 i zgodnie z konfiguracją zegara, taktowany będzie z częstotliwością 80 MHz.

Zgodnie ze wzorem:

TIM_Freq = APB1_Freq / (Prescaler * Counter Period )

Aby uzyskać częstotliwość 100 Hz musimy podzielić zegar przez 800 000. Biorąc pod uwagę maksymalne wartości, jakie możemy wpisać do ustawień Prescaler i Counter Period, konfiguracja Timer 3 będzie wyglądała następująco:

Aby Timer mógł generować zdarzenie dla ADC, zaznaczamy jeszcze opcję Trigger Event Selection TRGO jako Update Event. Widok wyjść mikrokontrolera w konfiguratorze będzie się przedstawiał jak na poniższym obrazku.

Przy tak skonfigurowanych peryferiach możemy wygenerować projekt („Project->Generate Code” lub „Alt+K„) i przejść do napisania kodu programu.

Filtr dolnoprzepustowy w języku C

Implementacja kodu filtra dolnoprzepustowego nie jest skomplikowana. Sprowadza się do zapisania równania, które już wcześniej omówiliśmy w formie jednej linijki w języku C. Ale zanim o tym, najpierw przygotujemy podstawy pod użycie filtra. Zawsze starajmy się, aby kod był czytelny, dzięki czemu potem łatwo będzie go modyfikować oraz wrócić do niego po dłuższym czasie.

W przypadku filtra dolnoprzepustowego nie będziemy potrzebowali bufora do przechowywania danych. Wystarczy, że wiemy jaki jest współczynnik filtru oraz zapiszemy poprzednie wyjście filtru. Dlatego stworzymy strukturę, która będzie zawierała te dwa elementy.

typedef struct
{
	float out;
	float alpha;
} lowpass_filter_t;

Jak pewnie zauważyłeś, użyłem zmiennych typu float. Aby filtr dobrze działał, będzie potrzebował przechowywać ułamkowe części wyjścia, dzięki czemu nie utracimy płynności jego działania. Poza tym współczynnik alfa może przyjmować wartości od 0 do 1, dlatego również musi być typu float. Użycie typów zmiennoprzecinkowych wiąże się z większym obciążeniem mikrokontrolera. Szczególnie jeżeli nie mamy do dyspozycji jednostki FPU. Oczywiście, jeżeli filtrujemy dane np. co sekundę, to nie będzie to żadnym problemem. Ale już wywoływanie obliczeń z dużą częstotliwością może spowodować pewne problemy. Warto o tym pamiętać dodając obliczenia na zmiennych typu float lub double.

Mamy już strukturę. Warto dodać również definicję ze współczynnikiem alfa.

#define LOW_PASS_ALPHA	(0.01f)

W funkcji inicjalizującej ustawimy wartości początkowe elementów filtru.

void filter_lowpass_init(void)
{
	filter.out = 0.0f;
	filter.alpha = LOW_PASS_ALPHA;
}

Sam proces filtrowania danych będzie sprowadzał się do wywołania jednej linii kodu z implementacją równania filtru.

int32_t filter_lowpass(int32_t new_data)
{
	filter.out = filter.alpha * new_data + (1.0f-filter.alpha) * filter.out;

	return filter.out;
}

W funkcji main() analogicznie jak w przypadku poprzednich filtrów dodajemy inicjalizację.

filter_lowpass_init();

Filtrowanie wywołujemy w przerwaniu od zakończenia pomiaru ADC.

void HAL_ADC_ConvCpltCallback(ADC_HandleTypeDef *AdcHandle)
{
	adc_filtered = filter_lowpass(adc_raw);
}

I to tyle. Wydaje się znacznie prostszy niż poprzednie filtry. No i zajmuje mniej RAM-u, bo nie musimy tworzyć tablicy do przechowywania zmiennych.

Teraz przyjrzymy się wykresom i przeanalizujemy działanie filtra w zależności od współczynnika alfa. Najpierw zobaczmy jak wygląda sygnał bezpośrednio z przetwornika ADC.

Sygnał ADC

Wykresy przefiltrowanego sygnału dla trzech różnych współczynników alfa będą wyglądały jak poniżej.

Jak można łatwo zauważyć, im mniejszy współczynnik, tym lepsza jakość filtrowania. Przypomnę, że współczynnik alfa może przyjmować wartości od 0 do 1. Sprawdźmy jeszcze, jak współczynnik wpływa na dynamikę zmian.

Jak można było się spodziewać, im mniejszy współczynnik, tym dłużej trwa reakcja na zmianę wartości. Filtr zachowuje się bardzo podobnie jak filtr uśredniający i medianowy.

Podsumowanie

Trzeci z przedstawionych przeze mnie prostych filtrów cyfrowych to jeden z najpopularniejszych i najczęściej stosowanych filtrów w przetwarzaniu sygnałów. Może przyjmować różne – mniej lub bardziej zaawansowane formy. Filtr opisany w materiale to najprostsza, ale skuteczna odmiana filtru dolnoprzepustowego, która sprawdzi się m.in. do filtracji sygnałów analogowych. Mam nadzieję, że przedstawiona implementacja pozwoli Ci łatwo dodać filtr dolnoprzepustowy do Twojego projektu.

Jeżeli wpis Ci się podobał, polub mój profil na Facebook-u oraz zasubskrybuj kanał na YouTube. Projekt wykonany w ramach artykułu znajdzie na moim repozytorium GitHub.

Repozytorium GitHub

  1. Piotrze, bardzo fajny pomysł, żeby troszkę podrożyć temat filtra dolnoprzepustowego, ubogacając go o określenie czest. próbkowania oraz czest. graniczną. W przypadku tego typu filtrów nie jest to zbyt trudne. A dzięki takim modyfikacjom będzie wiadomo co filtr nam filtruje 🙂 Pozdrowienia!

  2. Przydałoby się dorobic:
    Określenie częstotliwości próbkowania,
    Określenie częstotliwości granicznej filtra
    Określenie nachylenia filtra

    1. Dzięki za uwagi. Chciałem poruszyć temat przy jak najmniejszej dawce teorii, bo sporo osób odstrasza, jak widzą na początku dużo wzorów i wykresów. Ale pełne zrozumienie działania filtru rzeczywiście wymaga, żeby te rzeczy znać. Pomyślę o drugim artykule na ten temat, żeby uzupełnić braki.

Dodaj komentarz

Twój adres e-mail nie zostanie opublikowany. Wymagane pola są oznaczone *