{"id":28807,"date":"2024-09-02T05:00:00","date_gmt":"2024-09-02T03:00:00","guid":{"rendered":"https:\/\/sii.pl\/blog\/?p=28807"},"modified":"2024-09-02T08:52:41","modified_gmt":"2024-09-02T06:52:41","slug":"rozwoj-oprogramowania-audio-dla-systemow-embedded","status":"publish","type":"post","link":"https:\/\/sii.pl\/blog\/rozwoj-oprogramowania-audio-dla-systemow-embedded\/","title":{"rendered":"Rozw\u00f3j oprogramowania audio dla system\u00f3w embedded"},"content":{"rendered":"\n<p>Obecnie s\u0142uchawki bezprzewodowe s\u0105 bardziej popularne ni\u017c jeszcze kilka lat temu ze wzgl\u0119du na to, \u017ce technologia w nich u\u017cywana oraz d\u0142ugo\u015b\u0107 dzia\u0142ania na jednym \u0142adowaniu zosta\u0142y znacznie poprawione. Jednym z podstawowych element\u00f3w technologii w nich zastosowanych jest <strong>aktywna redukcja ha\u0142asu<\/strong> (ANC, ang. Active Noise Cancellation).<\/p>\n\n\n\n<p>ANC to technologia, kt\u00f3ra zmniejsza d\u017awi\u0119ki otoczenia poprzez generowanie d\u017awi\u0119k\u00f3w o przeciwnej fazie, co skutecznie je niweluje. System ANC wykorzystuje mikrofony do rejestrowania ha\u0142asu otoczenia, a nast\u0119pnie przetwarza je, aby stworzy\u0107 anty-ha\u0142as odtwarzany przez g\u0142o\u015bniki. T\u0119 technologi\u0119 stosuje si\u0119 powszechnie w s\u0142uchawkach, samochodach i innych urz\u0105dzeniach, zapewniaj\u0105c u\u017cytkownikom cichsze i bardziej komfortowe warunki do s\u0142uchania ulubionej muzyki.<\/p>\n\n\n\n<p>ANC najlepiej dzia\u0142a na d\u017awi\u0119ki o niskiej cz\u0119stotliwo\u015bci, kt\u00f3re s\u0105 wolno-zmienne, jest za to mniej skuteczna wobec nieprzewidywalnych, szybko-zmiennych ha\u0142as\u00f3w.<\/p>\n\n\n\n<figure class=\"wp-block-image aligncenter size-full\"><a href=\"https:\/\/sii.pl\/blog\/wp-content\/uploads\/2024\/08\/1-1.jpg\"><img decoding=\"async\" width=\"707\" height=\"466\" src=\"https:\/\/sii.pl\/blog\/wp-content\/uploads\/2024\/08\/1-1.jpg\" alt=\"Schemat blokowy nowoczesnych s\u0142uchawek bezprzewodowych\" class=\"wp-image-28810\" srcset=\"https:\/\/sii.pl\/blog\/wp-content\/uploads\/2024\/08\/1-1.jpg 707w, https:\/\/sii.pl\/blog\/wp-content\/uploads\/2024\/08\/1-1-300x198.jpg 300w\" sizes=\"(max-width: 707px) 100vw, 707px\" \/><\/a><figcaption class=\"wp-element-caption\">Ryc. 1 Schemat blokowy nowoczesnych s\u0142uchawek bezprzewodowych<\/figcaption><\/figure>\n\n\n\n<p>W tym artykule przybli\u017c\u0119 wam <strong>podstawy tworzenia aplikacji audio<\/strong>, omawiaj\u0105c mikrokontrolery dedykowane do tego typu zastosowa\u0144 oraz <strong>metody optymalizacji kodu, zw\u0142aszcza algorytm\u00f3w DSP<\/strong>. Poka\u017c\u0119, jak krok po kroku <strong>zbudowa\u0107 sw\u00f3j pierwszy system audio<\/strong> oraz na co warto zwr\u00f3ci\u0107 uwag\u0119 podczas jego projektowania. Na zako\u0144czenie przedstawi\u0119 i om\u00f3wi\u0119 osi\u0105gni\u0119te wyniki.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\"><strong>Jaki powinien by\u0107 fundament systemu audio?<\/strong><\/h2>\n\n\n\n<p>Rozw\u00f3j oprogramowania audio dla przeno\u015bnych system\u00f3w embedded zasilanych z baterii niesie ze sob\u0105 dodatkowe wyzwania zwi\u0105zane z optymalizacj\u0105 wydajno\u015bci. Systemy embedded, stosowane m.in.: w s\u0142uchawkach, cechuj\u0105 si\u0119 ograniczonymi zasobami sprz\u0119towymi, co wymaga tworzenia oprogramowania o minimalnym zu\u017cyciu procesora i pami\u0119ci, przy jednoczesnym utrzymaniu wysokiej jako\u015bci d\u017awi\u0119ku. Kluczowe znaczenie ma r\u00f3wnie\u017c zarz\u0105dzanie latencj\u0105, poniewa\u017c ka\u017cde op\u00f3\u017anienie jest niepo\u017c\u0105dane, zw\u0142aszcza w aplikacjach czasu rzeczywistego.<\/p>\n\n\n\n<p>Aby sprosta\u0107 tym wyzwaniom, mo\u017cemy wykorzysta\u0107 m.in.: nast\u0119puj\u0105ce mikrokontrolery.<\/p>\n\n\n\n<ol class=\"wp-block-list\">\n<li><strong>Procesor DSP<\/strong> \u2013 pierwsz\u0105 opcj\u0105 jest wyb\u00f3r dedykowanego procesora DSP. Przyk\u0142adem mo\u017ce by\u0107 <a href=\"https:\/\/www.analog.com\/en\/product-category\/processors-dsp.html\" target=\"_blank\" aria-label=\" (opens in a new tab)\" rel=\"noreferrer noopener\" class=\"ek-link\" rel=\"nofollow\" >linia SHARC<\/a> od firmy Analog Devices. Procesory te s\u0105 taktowane zegarem do 400 MHz oraz oferuj\u0105 FPU obs\u0142uguj\u0105cy liczby zmiennoprzecinkowe w formacie 32 oraz 40 bitowym. Dodatkowo wspiera on operacje SIMD i posiada 5 Mb pami\u0119ci RAM L1 on-chip oraz 8 Mb RAM L2 \u00ad\u2013 dzi\u0119ki temu b\u0119dziemy mie\u0107 szybki dost\u0119p do danych naszych algorytm\u00f3w, co bezpo\u015brednio prze\u0142o\u017cy si\u0119 na wydajno\u015b\u0107 systemu.<\/li>\n\n\n\n<li><strong style=\"color: initial;\">Mikrokontroler posiadaj\u0105cy SIMD i FPU<\/strong><span style=\"color: initial;\"> \u2013 drug\u0105 opcj\u0105, kt\u00f3ra jest ta\u0144sza i prostsza, jest konwencjonalny mikrokontroler, kt\u00f3ry wspiera operacje SIMD oraz posiada FPU. Mo\u017ce by\u0107 to ESP32-S3 lub mikrokontroler wyposa\u017cony w rdze\u0144 ARM-Cortex-M4 lub wy\u017cszy. Dzi\u0119ki temu mo\u017cemy u\u017cy\u0107 tylko jednego kontrolera i \u201eprzyspieszy\u0107\u201d nasze algorytmy, u\u017cywaj\u0105c operacji wektorowych.<\/span><\/li>\n\n\n\n<li><strong>Mikrokontroler z zewn\u0119trznym modu\u0142em DSP<\/strong> \u00ad\u2013 trzeci\u0105 opcj\u0105 jest wykorzystanie mikrokontrolera opartego np. na rdzeniu ARM z dodatkowym koprocesorem DSP. W tym przypadku modu\u0142 DSP b\u0119dzie pre-programowany do wykonywania konkretnych algorytm\u00f3w (np. filtrowania), a sam mikrokontroler b\u0119dzie odpowiedzialny tylko za obs\u0142ug\u0119 stworzonej aplikacji.<\/li>\n<\/ol>\n\n\n\n<h2 class=\"wp-block-heading\"><strong>Zbudujmy sw\u00f3j pierwszy pipeline audio<\/strong><\/h2>\n\n\n\n<p>W celu stworzenia podstawowej aplikacji audio u\u017cyj\u0119 popularnego mikrokontrolera ESP32-S3. Posiada on bardzo dobry SDK, kt\u00f3ry pozwala na szybkie prototypowanie aplikacji. Dodatkowo jest szeroko dost\u0119pnym i tanim mikrokontrolerem z du\u017cym wsparciem spo\u0142eczno\u015bci. Dzi\u0119ki temu mo\u017cemy skupi\u0107 si\u0119 g\u0142\u00f3wnie nad rozwi\u0105zaniami, kt\u00f3re mo\u017cna zastosowa\u0107 w aplikacji.<\/p>\n\n\n\n<p>Kolejnym krokiem w tworzeniu naszego systemu jest wyb\u00f3r rodzaju aplikacji, kt\u00f3r\u0105 chcemy zaimplementowa\u0107 na pocz\u0105tek. Mo\u017cemy wyr\u00f3\u017cni\u0107 <strong>kilka typ\u00f3w przetwarzania audio<\/strong>:<\/p>\n\n\n\n<ol class=\"wp-block-list\">\n<li><strong>Generowanie sygna\u0142u<\/strong> \u2013 system otrzymuje dane z zewn\u0119trznego \u017ar\u00f3d\u0142a i przekszta\u0142ca je na d\u017awi\u0119k. Przyk\u0142adem jest klawiatura MIDI, kt\u00f3ra przekszta\u0142ca naci\u015bni\u0119cia klawiszy w odpowiednie d\u017awi\u0119ki.<\/li>\n\n\n\n<li><strong>Analizowanie sygna\u0142u<\/strong> \u2013 system przyjmuje sygna\u0142 audio i analizuje go, np. za pomoc\u0105 FFT. Przyk\u0142adem mo\u017ce by\u0107 analizator widma sygna\u0142u.<\/li>\n\n\n\n<li><strong>Przetwarzanie sygna\u0142u<\/strong> \u2013 system pobiera sygna\u0142 audio, przetwarza go za pomoc\u0105 algorytm\u00f3w DSP, a nast\u0119pnie przesy\u0142a przetworzony sygna\u0142 na zewn\u0105trz aplikacji, np. do g\u0142o\u015bnik\u00f3w. Przyk\u0142adem mo\u017ce by\u0107 system ANC lub efekty gitarowe.<\/li>\n<\/ol>\n\n\n\n<p>W tym artykule b\u0119dziemy budowa\u0107 aplikacj\u0119, kt\u00f3ra <strong>b\u0119dzie przetwarza\u0107 sygna\u0142 audio<\/strong>. Jak w takim razie nasz system b\u0119dzie wiedzia\u0142, co przetwarza\u0107? Mo\u017cemy u\u017cy\u0107 przetwornika ADC, cyfrowego mikrofonu, modu\u0142u SAI, karty d\u017awi\u0119kowej USB\u2026 \u2013 odpowiedzi na to pytanie jest wiele.<\/p>\n\n\n\n<p><strong>Kt\u00f3ry z wymienionych b\u0119dzie najlepszy?<\/strong> U\u017cyj\u0119 ulubionej odpowiedzi ka\u017cdego in\u017cyniera \u2013 to zale\u017cy \ud83d\ude42 Zale\u017cy Ci na niskiej latencji sygna\u0142u, a Tw\u00f3j algorytm nie wymaga przetwarzania blokowego? Wybierz dobry przetwornik ADC i przetwarzaj sygna\u0142 pr\u00f3bka po pr\u00f3bce. Mo\u017cesz zaakceptowa\u0107 wy\u017csz\u0105 latencj\u0119 sygna\u0142u lub Twoje algorytmy przetwarzaj\u0105 sygna\u0142 blokowo? W tym przypadku mo\u017cesz wybra\u0107 zar\u00f3wno przetwornik ADC jak i rozwi\u0105zania cyfrowe wykorzystuj\u0105ce I2S czy SAI.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\"><strong>Czym jest I2S?<\/strong><\/h2>\n\n\n\n<p>I2S (ang. Inter-IC Sound) to standardowy protok\u00f3\u0142 komunikacyjny zaprojektowany do cyfrowego przesy\u0142ania sygna\u0142\u00f3w audio mi\u0119dzy uk\u0142adami scalonymi, takimi jak mikrokontrolery, kodeki audio i cyfrowe przetworniki sygna\u0142\u00f3w (DSP).<\/p>\n\n\n\n<p>G\u0142\u00f3wne cechy I2S to:<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li><strong>Szeregowy przesy\u0142 danych \u2013<\/strong> I2S wykorzystuje szeregowe po\u0142\u0105czenie do przesy\u0142ania danych audio, co pozwala na uproszczenie uk\u0142ad\u00f3w i zmniejszenie liczby \u015bcie\u017cek. W tym protokole wykorzystujemy 4 sygna\u0142y:\n<ul class=\"wp-block-list\">\n<li>SCK \u2013 sygna\u0142 zegarowy,<\/li>\n\n\n\n<li>SDIN \u2013 dane wej\u015bciowe z urz\u0105dzenia I2S,<\/li>\n\n\n\n<li><span style=\"color: initial;\">SDOUT \u2013 dane wyj\u015bciowe, kt\u00f3re chcemy odtworzy\u0107,<\/span><\/li>\n\n\n\n<li>LRCK \u2013 sygna\u0142 wyboru kana\u0142u L\/R.<\/li>\n<\/ul>\n<\/li>\n\n\n\n<li><strong>Synchronizacja<\/strong> \u2013 dane s\u0105 przesy\u0142ane w synchronizacji z sygna\u0142em zegarowym (SCK) oraz sygna\u0142em wyboru kana\u0142u (WS), co umo\u017cliwia precyzyjne odwzorowanie d\u017awi\u0119ku.<\/li>\n\n\n\n<li><strong>Format danych<\/strong> \u2013 protok\u00f3\u0142 obs\u0142uguje r\u00f3\u017cne formaty bitowe, cz\u0119sto u\u017cywaj\u0105c 16-bitowych, 24-bitowych lub 32-bitowych ramki danych.<\/li>\n\n\n\n<li><strong>Oddzielne linie danych<\/strong> \u2013 dane dla lewego i prawego kana\u0142u audio s\u0105 przesy\u0142ane na osobnych liniach danych, co zapewnia niezale\u017cno\u015b\u0107 i jako\u015b\u0107 d\u017awi\u0119ku.<\/li>\n<\/ul>\n\n\n\n<figure class=\"wp-block-image aligncenter size-large\"><a href=\"https:\/\/sii.pl\/blog\/wp-content\/uploads\/2024\/08\/image2-5.png\"><img decoding=\"async\" width=\"1024\" height=\"536\" src=\"https:\/\/sii.pl\/blog\/wp-content\/uploads\/2024\/08\/image2-5-1024x536.png\" alt=\"Przyk\u0142adowe sygna\u0142y ramki audio w formacie I2S\" class=\"wp-image-28813\" srcset=\"https:\/\/sii.pl\/blog\/wp-content\/uploads\/2024\/08\/image2-5-1024x536.png 1024w, https:\/\/sii.pl\/blog\/wp-content\/uploads\/2024\/08\/image2-5-300x157.png 300w, https:\/\/sii.pl\/blog\/wp-content\/uploads\/2024\/08\/image2-5-768x402.png 768w, https:\/\/sii.pl\/blog\/wp-content\/uploads\/2024\/08\/image2-5-1536x804.png 1536w, https:\/\/sii.pl\/blog\/wp-content\/uploads\/2024\/08\/image2-5-2048x1072.png 2048w\" sizes=\"(max-width: 1024px) 100vw, 1024px\" \/><\/a><figcaption class=\"wp-element-caption\">Ryc. 2 <a href=\"https:\/\/infocenter.nordicsemi.com\/index.jsp?topic=%2Fps_nrf9160%2Fi2s.html\" target=\"_blank\" aria-label=\" (opens in a new tab)\" rel=\"noreferrer noopener\" class=\"ek-link\" rel=\"nofollow\" >Przyk\u0142adowe sygna\u0142y ramki audio w formacie I2S<\/a><\/figcaption><\/figure>\n\n\n\n<p>W naszym przyk\u0142adzie wykorzystamy protok\u00f3\u0142 I2S, aby do mikrokontrolera pod\u0142\u0105czy\u0107 cyfrowy mikrofon I2S SPH0645LM4H jako sygna\u0142 wej\u015bciowy oraz cyfrowy kodek audio I2S UDA1334A, kt\u00f3ry b\u0119dzie dekodowa\u0142 nasz przetworzony sygna\u0142 na sygna\u0142 analogowy.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\"><strong>Jak efektywnie odczytywa\u0107 sygna\u0142 wej\u015bciowy?<\/strong><\/h2>\n\n\n\n<p>Skoro ju\u017c wiemy, \u017ce b\u0119dziemy u\u017cywa\u0107 protoko\u0142u I2S do nagrywania i odtwarzania sygna\u0142\u00f3w audio, to jak zrobi\u0107 to najlepiej? Istniej\u0105 dwie najbardziej popularne metody.<\/p>\n\n\n\n<p>Pierwsz\u0105 z nich jest <strong>synchroniczna obs\u0142uga protoko\u0142u I2S<\/strong>. Jest ona bardzo podobna do obs\u0142ugi protoko\u0142u I2C. Najpierw przygotowujemy dane, a nast\u0119pnie wywo\u0142ujemy funkcj\u0119 <em>i2s_write<\/em> lub <em>i2s_read, <\/em>aby skomunikowa\u0107 si\u0119 z naszym modu\u0142em I2S. Funkcje te s\u0105 blokuj\u0105ce, przez co zmuszamy nasz procesor do czekania, a\u017c wszystkie dane zostan\u0105 przes\u0142ane lub odczytane. Rozwi\u0105zanie to jest najprostsze, jednak <strong>nieefektywne<\/strong> ze wzgl\u0119du na nieoptymalne u\u017cycie procesora.<\/p>\n\n\n\n<p>Drug\u0105 opcj\u0105 jest <strong>wykorzystanie DMA do przesy\u0142u naszych danych<\/strong>. Przygotowujemy wtedy blok pami\u0119ci, kt\u00f3ry b\u0119dzie s\u0142u\u017cy\u0142 za <em>po\u015brednika <\/em>mi\u0119dzy naszym CPU a modu\u0142em I2S. Dzi\u0119ki temu odci\u0105\u017camy procesor, kt\u00f3ry w czasie przesy\u0142ania danych mi\u0119dzy tym blokiem a I2S b\u0119dzie m\u00f3g\u0142 zaj\u0105\u0107 si\u0119 innymi rzeczami, np. zas\u0142u\u017conym odpoczynkiem w Idle\u2019u \ud83d\ude09 \u2013 w ko\u0144cu zrobi\u0142 kawa\u0142 dobrej roboty.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\"><strong>Wymagaj\u0105cy algorytm<\/strong><\/h3>\n\n\n\n<p>Lecz co w przypadku, gdy nasz algorytm b\u0119dzie na tyle wymagaj\u0105cy dla procesora, \u017ce nie da mu nawet nanosekundy na odpoczynek? W takim przypadku mo\u017ce doj\u015b\u0107 do sytuacji, w kt\u00f3rej procesor nadal b\u0119dzie przetwarza\u0142 dane, a DMA b\u0119dzie ju\u017c \u0142adowa\u0142 nowe dane do tego samego bloku pami\u0119ci \u2013 co poskutkuje problemami ze sp\u00f3jno\u015bci\u0105 danych.<\/p>\n\n\n\n<p>Aby temu zapobiec, musimy zastosowa\u0107 technik\u0119 zwan\u0105 podw\u00f3jnym buforowaniem (ang. Double-buffering). Na przyk\u0142ad, aby u\u0142atwi\u0107 przetwarzanie bufora o d\u0142ugo\u015bci N, wystarczy utworzy\u0107 dwa bufory o d\u0142ugo\u015bci N. Jak pokazano na rysunku poni\u017cej, CPU przetwarza bufor in1 i przechowuje wynik w buforze out1, podczas gdy silnik DMA przesy\u0142a dane z wyj\u015bcia out0. Na rysunku mo\u017cna zobaczy\u0107, \u017ce gdy silnik DMA sko\u0144czy prac\u0119 z lew\u0105 po\u0142ow\u0105 podw\u00f3jnych bufor\u00f3w, rozpoczyna przesy\u0142anie danych do wej\u015bcia in1 i wyj\u015bcia out1, podczas gdy rdze\u0144 przetwarza dane z wej\u015bcia in0 i out0. Oczywi\u015bcie mo\u017cna zastosowa\u0107 wi\u0119cej ni\u017c 2 bufory, co da nam wi\u0119cej czasu na przetworzenie danych przez algorytm bez utraty zebranych danych z urz\u0105dzenia I2S.<\/p>\n\n\n\n<figure class=\"wp-block-image aligncenter size-full\"><a href=\"https:\/\/sii.pl\/blog\/wp-content\/uploads\/2024\/08\/2.jpg\"><img decoding=\"async\" width=\"933\" height=\"412\" src=\"https:\/\/sii.pl\/blog\/wp-content\/uploads\/2024\/08\/2.jpg\" alt=\"Schemat podw\u00f3jnego buforowania DMA do przetwarzania strumieniowego\" class=\"wp-image-28819\" srcset=\"https:\/\/sii.pl\/blog\/wp-content\/uploads\/2024\/08\/2.jpg 933w, https:\/\/sii.pl\/blog\/wp-content\/uploads\/2024\/08\/2-300x132.jpg 300w, https:\/\/sii.pl\/blog\/wp-content\/uploads\/2024\/08\/2-768x339.jpg 768w, https:\/\/sii.pl\/blog\/wp-content\/uploads\/2024\/08\/2-370x162.jpg 370w\" sizes=\"(max-width: 933px) 100vw, 933px\" \/><\/a><figcaption class=\"wp-element-caption\">Ryc. 3 <a href=\"https:\/\/www.eetimes.com\/fundamentals-of-embedded-audio-part-3\/\" target=\"_blank\" aria-label=\" (opens in a new tab)\" rel=\"noreferrer noopener\" class=\"ek-link\" rel=\"nofollow\" >Schemat podw\u00f3jnego buforowania DMA do przetwarzania strumieniowego<\/a><\/figcaption><\/figure>\n\n\n\n<p>U\u017cycie DMA jest bardziej skomplikowane ni\u017c pierwsza metoda, wymaga znajomo\u015bci platformy oraz dog\u0142\u0119bnego debugowania, aby upewni\u0107 si\u0119, \u017ce wszystkie dane audio b\u0119d\u0105 odpowiednio przetworzone. Jednak jest to opcja najcz\u0119\u015bciej wybierana, ze wzgl\u0119du na <strong>odpowiednie rozdysponowanie czasu procesora<\/strong> oraz czas przetwarzania ramek audio.<\/p>\n\n\n\n<p>W naszym przyk\u0142adzie b\u0119dziemy u\u017cywa\u0107 drugiej metody obs\u0142ugi danych \u2013 czyli DMA oraz podw\u00f3jnego buforowania do odczytu i zapisu danych audio.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\"><strong>Jak efektywnie przetworzy\u0107 nasz sygna\u0142?<\/strong><\/h2>\n\n\n\n<p>Skoro ju\u017c zoptymalizowali\u015bmy nagrywanie i odtwarzanie sygna\u0142u audio, nadszed\u0142 czas na optymalizacj\u0119 przetwarzania sygna\u0142\u00f3w. Ka\u017cda aplikacja audio b\u0119dzie zawiera\u0142a pewien rodzaj ci\u0105gu po\u0142\u0105czonych procesor\u00f3w audio (ang. pipeline). Taki procesor mo\u017cemy zaimplementowa\u0107 na dwa sposoby:<\/p>\n\n\n\n<ol class=\"wp-block-list\">\n<li><strong>Wykorzystuj\u0105c przetwarzanie szeregowe<\/strong> \u2013 gdzie ka\u017cda instancja, kt\u00f3ra ma za zadanie przetworzy\u0107 sygna\u0142 jest po\u0142\u0105czona ze sob\u0105 w spos\u00f3b szeregowy. W tym przypadku procesor audio o numerze N+1, nie mo\u017ce rozpocz\u0105\u0107 swojego przetwarzania, dop\u00f3ki procesor poprzedzaj\u0105cy (N) nie zako\u0144czy swojej pracy. Uk\u0142ad taki jest korzystny, gdy ka\u017cdy z procesor\u00f3w jest przystosowany do pracy na takiej samej ilo\u015bci danych w bloku. Dodatkowo nie wykorzystuje on du\u017co pami\u0119ci RAM mikrokontrolera, poniewa\u017c ca\u0142o\u015b\u0107 przetwarzania wykonywana b\u0119dzie na tym samym tasku. Niestety uk\u0142ad taki ma te\u017c du\u017cy minus, kt\u00f3rym jest latencja przetwarzania, kt\u00f3ra b\u0119dzie sum\u0105 czas\u00f3w przetwarzania poszczeg\u00f3lnych procesor\u00f3w w pipeline\u2019ie.<\/li>\n\n\n\n<li><strong>Wykorzystuj\u0105c przetwarzanie r\u00f3wnoleg\u0142e<\/strong> \u2013 gdzie ka\u017cda instancja przetwarzaj\u0105ca audio b\u0119dzie dzia\u0142a\u0142a na osobnym tasku, a po\u0142\u0105czona b\u0119dzie buforem ko\u0142owym z poprzedni\u0105 i kolejn\u0105 instancj\u0105 w stworzonym pipeline audio. Uk\u0142ad taki b\u0119dzie potrzebowa\u0142 o wiele wi\u0119cej pami\u0119ci RAM mikrokontrolera, ale ka\u017cda instancja mo\u017ce operowa\u0107 na r\u00f3\u017cnych wielko\u015bciach bloku audio, a ca\u0142kowita latencja takiego systemu b\u0119dzie wynosi\u0142a tyle, ile czas przetwarzania najwolniejszej z instancji (w najlepszym przypadku, kiedy ka\u017cda instancja przetwarzaj\u0105ca b\u0119dzie dzia\u0142a\u0142a na osobym rdzeniu mikrokontrolera).<\/li>\n<\/ol>\n\n\n\n<h3 class=\"wp-block-heading\"><strong>Usprawnienia instancji przetwarzaj\u0105cych<\/strong><\/h3>\n\n\n\n<p>Opr\u00f3cz optymalizacji sposobu przetwarzania sygna\u0142u audio mo\u017cemy r\u00f3wnie\u017c usprawni\u0107 same instancje przetwarzaj\u0105ce. Mog\u0105 nimi by\u0107:<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li>Filtry cyfrowe,\n<ul class=\"wp-block-list\">\n<li>FIR (ang. Finite Impulse Response),<\/li>\n\n\n\n<li>IIR (ang. Infinite Impulse Response),<\/li>\n\n\n\n<li>Biquad (IIR 2-go rz\u0119du),<\/li>\n<\/ul>\n<\/li>\n\n\n\n<li>FFT,<\/li>\n\n\n\n<li>Algorytmy AEC (ang. Acoustic Echo Cancellation),<\/li>\n\n\n\n<li>Algorytmy NS (ang. Noise Suppression),<\/li>\n\n\n\n<li>Sieci neuronowe.<\/li>\n<\/ul>\n\n\n\n<h2 class=\"wp-block-heading\"><strong>Filtr FIR<\/strong><\/h2>\n\n\n\n<p>W tym artykule skoncentrujemy si\u0119 na filtrze FIR, kt\u00f3ry jest cyfrowym filtrem o sko\u0144czonej odpowiedzi impulsowej, co oznacza, \u017ce jego reakcja na impuls wej\u015bciowy zanika po okre\u015blonym czasie.<\/p>\n\n\n\n<p>Do jego g\u0142\u00f3wnych cech nale\u017c\u0105:<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li><strong>Liniowa faza \u2013<\/strong> FIR mo\u017ce by\u0107 zaprojektowany tak, aby mia\u0142 liniow\u0105 charakterystyk\u0119 fazow\u0105, co oznacza, \u017ce wszystkie cz\u0119stotliwo\u015bci sygna\u0142u s\u0105 op\u00f3\u017anione o t\u0119 sam\u0105 ilo\u015b\u0107 czasu. Jest to istotne w aplikacjach audio, gdzie nieliniowo\u015b\u0107 fazy mo\u017ce prowadzi\u0107 do zniekszta\u0142ce\u0144 sygna\u0142u (np. w przypadku algorytmu ANC czy AEC).<\/li>\n\n\n\n<li><strong>Stabilno\u015b\u0107 \u2013<\/strong> filtry FIR s\u0105 zawsze stabilne, poniewa\u017c nie maj\u0105 sprz\u0119\u017cenia zwrotnego w swojej implementacji.<\/li>\n\n\n\n<li><strong>Projektowanie<\/strong> \u2013 s\u0105 \u0142atwe do projektowania przy u\u017cyciu metod takich jak metoda okien czasowych, odpowiedzi impulsowej czy metody cz\u0119stotliwo\u015bciowej.<\/li>\n<\/ul>\n\n\n\n<p>Filtr FIR oblicza ka\u017cd\u0105 pr\u00f3bk\u0119 wyj\u015bciow\u0105 jako sum\u0119 wa\u017con\u0105 okre\u015blonej liczby pr\u00f3bek wej\u015bciowych. Matematycznie wyra\u017caj\u0105c, je\u015bli mamy filtr FIR rz\u0119du N, jego wyj\u015bciowa pr\u00f3bka y[n] mo\u017ce by\u0107 wyra\u017cona jako:<\/p>\n\n\n\n<figure class=\"wp-block-image size-full\"><img decoding=\"async\" width=\"270\" height=\"77\" src=\"https:\/\/sii.pl\/blog\/wp-content\/uploads\/2024\/08\/3.jpg\" alt=\"\" class=\"wp-image-28821\"\/><\/figure>\n\n\n\n<p>gdzie:<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li><em>x[n]<\/em> to pr\u00f3bki wej\u015bciowe,<\/li>\n\n\n\n<li><em>y[n]<\/em> to pr\u00f3bki wyj\u015bciowe,<\/li>\n\n\n\n<li><em>b<sub>k<\/sub><\/em>&nbsp; to wsp\u00f3\u0142czynniki filtru,<\/li>\n\n\n\n<li><em>N<\/em> to rz\u0105d filtru FIR.<\/li>\n<\/ul>\n\n\n\n<p>Powy\u017csze r\u00f3wnanie u\u017cyte do obliczenia pojedynczej pr\u00f3bki w j\u0119zyku C++ mo\u017ce wygl\u0105da\u0107 nast\u0119puj\u0105co:<\/p>\n\n\n<div class=\"wp-block-syntaxhighlighter-code \"><pre class=\"brush: plain; title: ; notranslate\" title=\"\">\ninline T ProcessSample(T sample) {\n    T   acc{ 0 };\n    int coeffsPos{ 0 };\n    mDelayLine&#x5B;mDelayPos] = sample;\n    ++mDelayPos;\n    if (mDelayPos &gt;= mN) { mDelayPos = 0; }\n\n    for (int n = mDelayPos; n &amp;lt; mN; n++) {\n        acc += mCoeffs&#x5B;coeffsPos++] * mDelayLine&#x5B;n];\n    }\n    for (int n = 0; n &amp;lt; mDelayPos; n++) {\n        acc += mCoeffs&#x5B;coeffsPos++] * mDelayLine&#x5B;n];\n    }\n\n    return acc;\n}\n<\/pre><\/div>\n\n\n<p>W tym przypadku u\u017cywamy tablicy mDelayLine jako bufora ko\u0142owego do kt\u00f3rego zapisujemy pr\u00f3bki audio, a nast\u0119pnie przemna\u017camy z naszymi wsp\u00f3\u0142czynnikami filtra.<\/p>\n\n\n\n<p>Mo\u017cemy przyspieszy\u0107 wykonywanie tej funkcji, u\u017cywaj\u0105c instrukcji SIMD (ang. Single Instruction Multiple Data), aby przetworzy\u0107 wektor danych u\u017cywaj\u0105c jednej instrukcji.<\/p>\n\n\n\n<p>Poni\u017cej <strong>implementacja filtra FIR z u\u017cyciem loop-unrolling<\/strong>, aby zaprezentowa\u0107 spos\u00f3b przetwarzania.<\/p>\n\n\n<div class=\"wp-block-syntaxhighlighter-code \"><pre class=\"brush: plain; title: ; notranslate\" title=\"\">\ninline T ProcessSample(T sample) {\n    T   acc{ 0 };\n    int coeffsPos{ 0 };\n\n    mDelayLine&#x5B;mDelayPos++] = sample;\n    if (mDelayPos &gt;= mN) { mDelayPos = 0; }\n    int n;\n\n    for (n = mDelayPos; n &amp;lt; mN; n += 4) {\n        acc += mCoeffs&#x5B;coeffsPos++] * mDelayLine&#x5B;n];\n        acc += mCoeffs&#x5B;coeffsPos++] * mDelayLine&#x5B;n + 1];\n        acc += mCoeffs&#x5B;coeffsPos++] * mDelayLine&#x5B;n + 2];\n        acc += mCoeffs&#x5B;coeffsPos++] * mDelayLine&#x5B;n + 3];\n    }\n    for (n = 0; n &amp;lt; mDelayPos; n += 4) {\n        acc += mCoeffs&#x5B;coeffsPos++] * mDelayLine&#x5B;n];\n        acc += mCoeffs&#x5B;coeffsPos++] * mDelayLine&#x5B;n + 1];\n        acc += mCoeffs&#x5B;coeffsPos++] * mDelayLine&#x5B;n + 2];\n        acc += mCoeffs&#x5B;coeffsPos++] * mDelayLine&#x5B;n + 3];\n    }\n    for (; n &amp;lt; mDelayPos; n++) {\n        acc += mCoeffs&#x5B;coeffsPos++] * mDelayLine&#x5B;n];\n    }\n\n    return acc;\n}\n<\/pre><\/div>\n\n\n<p>Wykorzystuj\u0105c za\u0142o\u017cenia loop-unrolling&#8217;u, mo\u017cemy <strong>zaimplementowa\u0107 filtr FIR z u\u017cyciem instrukcji SSE\/AVX dla procesor\u00f3w x86<\/strong>.<\/p>\n\n\n<div class=\"wp-block-syntaxhighlighter-code \"><pre class=\"brush: plain; title: ; notranslate\" title=\"\">\ninline T ProcessSample(T sample) {\n    int    coeffsPos{ 0 };\n    int    n;\n    __m128 sum = _mm_setzero_ps(); \/\/ SSE - ustawienie sumy na zero\n\n    mDelayLine&#x5B;mDelayPos++] = sample;\n    if (mDelayPos &gt;= mN) { mDelayPos = 0; }\n\n    for (n = mDelayPos; n &amp;lt; mN; n += 4) {\n        \/\/ Za\u0142aduj 4 pr\u00f3bki wej\u015bciowe (zak\u0142adaj\u0105c wyr\u00f3wnane dane)\n        __m128 x = _mm_load_ps(&amp;amp;mDelayLine&#x5B;n]);\n        \/\/ Za\u0142aduj 4 wyr\u00f3wnane wsp\u00f3\u0142czynniki\n        __m128 c = _mm_load_ps(&amp;amp;mCoeffs&#x5B;coeffsPos]);\n        \/\/ Mno\u017cenie wej\u015b\u0107 przez wsp\u00f3\u0142czynniki\n        __m128 mul = _mm_mul_ps(x, c);\n        \/\/ Sumowanie wynik\u00f3w\n        sum = _mm_add_ps(sum, mul);\n\n        coeffsPos += 4;\n    }\n<\/pre><\/div>\n\n<div class=\"wp-block-syntaxhighlighter-code \"><pre class=\"brush: plain; title: ; notranslate\" title=\"\">\n    for (n = 0; n &amp;lt; mDelayPos; n += 4) {\n       __m128 x = _mm_load_ps(&amp;amp;mDelayLine&#x5B;n]);\n       __m128 c = _mm_load_ps(&amp;amp;mCoeffs&#x5B;coeffsPos]);\n       __m128 mul = _mm_mul_ps(x, c);\n       sum = _mm_add_ps(sum, mul);\n\n        coeffsPos += 4;\n    }\n    for (; n &amp;lt; mDelayPos; n++) {\n        acc += mCoeffs&#x5B;coeffsPos++] * mDelayLine&#x5B;n];\n    }\n\n    \/\/ Dodawanie horyzontalne, aby uzyska\u0107 pojedyncz\u0105 sum\u0119\n    sum = _mm_hadd_ps(sum, sum);\n    sum = _mm_hadd_ps(sum, sum);\n\n    \/\/ Zapisz wynik do zmiennej\n    T result;\n    _mm_store_ss(&amp;amp;result, sum);\n\n    return result;\n}\n<\/pre><\/div>\n\n\n<h3 class=\"wp-block-heading\"><strong>Generowanie wsp\u00f3\u0142czynnika<\/strong><\/h3>\n\n\n\n<p>Jak w takim razie wygenerowa\u0107 wsp\u00f3\u0142czynniki do takiego filtru? Mo\u017cemy do tego wykorzysta\u0107 metod\u0119 okien czasowych. Polega na <strong>wykorzystaniu funkcji okienkowych<\/strong> (np. okna Hamming&#8217;a, Hanning\u2019a) do przyci\u0119cia niesko\u0144czonej odpowiedzi impulsowej idealnego filtru. Idealne filtry cyfrowe maj\u0105 niesko\u0144czon\u0105 odpowied\u017a impulsow\u0105, co jest niepraktyczne do realizacji w rzeczywistych systemach. Na przyk\u0142ad idealny filtr dolnoprzepustowy ma funkcj\u0119 <em>sinc<\/em> jako swoj\u0105 odpowied\u017a impulsow\u0105:<\/p>\n\n\n\n<figure class=\"wp-block-image size-full\"><img decoding=\"async\" width=\"393\" height=\"91\" src=\"https:\/\/sii.pl\/blog\/wp-content\/uploads\/2024\/08\/4.jpg\" alt=\"\" class=\"wp-image-28823\" style=\"object-fit:cover\" srcset=\"https:\/\/sii.pl\/blog\/wp-content\/uploads\/2024\/08\/4.jpg 393w, https:\/\/sii.pl\/blog\/wp-content\/uploads\/2024\/08\/4-300x69.jpg 300w\" sizes=\"(max-width: 393px) 100vw, 393px\" \/><\/figure>\n\n\n\n<p>Aby uzyska\u0107 sko\u0144czon\u0105 odpowied\u017a impulsow\u0105, przycinamy idealn\u0105 odpowied\u017a impulsow\u0105 do d\u0142ugo\u015bci M. Jednak\u017ce bez dodatkowej modyfikacji, takie przyci\u0119cie wprowadza oscylacje w odpowiedzi cz\u0119stotliwo\u015bciowej (tzw. <strong>efekty Gibbsa<\/strong>). Aby zminimalizowa\u0107 efekty Gibbsa, stosujemy funkcj\u0119 okna czasowego w(n), kt\u00f3ra wyg\u0142adza przej\u015bcia na ko\u0144cach przyci\u0119tej odpowiedzi impulsowej.<\/p>\n\n\n\n<figure class=\"wp-block-image size-full\"><img decoding=\"async\" width=\"248\" height=\"56\" src=\"https:\/\/sii.pl\/blog\/wp-content\/uploads\/2024\/08\/5.jpg\" alt=\"\" class=\"wp-image-28825\"\/><\/figure>\n\n\n\n<p>Poni\u017cej znajduje si\u0119 rysunek ukazuj\u0105cy cztery najcz\u0119\u015bciej stosowane okna czasowe oraz ich transformaty Fouriera.<\/p>\n\n\n\n<figure class=\"wp-block-image aligncenter size-full\"><a href=\"https:\/\/sii.pl\/blog\/wp-content\/uploads\/2024\/08\/6.jpg\"><img decoding=\"async\" width=\"776\" height=\"316\" src=\"https:\/\/sii.pl\/blog\/wp-content\/uploads\/2024\/08\/6.jpg\" alt=\"Cztery powszechnie stosowane funkcje okien czasowych (w(n), u g\u00f3ry) i odpowiadaj\u0105ce im kwadratowe transformaty Fouriera (|W(k)|2, u do\u0142u)\" class=\"wp-image-28829\" srcset=\"https:\/\/sii.pl\/blog\/wp-content\/uploads\/2024\/08\/6.jpg 776w, https:\/\/sii.pl\/blog\/wp-content\/uploads\/2024\/08\/6-300x122.jpg 300w, https:\/\/sii.pl\/blog\/wp-content\/uploads\/2024\/08\/6-768x313.jpg 768w\" sizes=\"(max-width: 776px) 100vw, 776px\" \/><\/a><figcaption class=\"wp-element-caption\">Ryc. 4 <a href=\"http:\/\/arxiv.org\/pdf\/1607.03579\" target=\"_blank\" aria-label=\" (opens in a new tab)\" rel=\"noreferrer noopener\" class=\"ek-link\" rel=\"nofollow\" >Cztery powszechnie stosowane funkcje okien czasowych (w(n), u g\u00f3ry) i odpowiadaj\u0105ce im kwadratowe transformaty Fouriera (|W(k)|<sup>2<\/sup>, u do\u0142u)<\/a><\/figcaption><\/figure>\n\n\n\n<p>Aby dodatkowo zoptymalizowa\u0107 proces generowania wsp\u00f3\u0142czynnik\u00f3w takiego filtru, mo\u017cemy u\u017cy\u0107 specyfikatora <em>constexpr<\/em> z j\u0119zyka C++, kt\u00f3ry obliczy i wygeneruje nam wsp\u00f3\u0142czynniki podczas kompilacji programu, a same wsp\u00f3\u0142czynniki b\u0119d\u0105 umieszczone w tablicy, kt\u00f3ra bezpo\u015brednio trafi do cz\u0119\u015bci kodu naszego programu.<\/p>\n\n\n<div class=\"wp-block-syntaxhighlighter-code \"><pre class=\"brush: plain; title: ; notranslate\" title=\"\">\nstruct HanningWindow {\n    template&amp;lt;std::size_t N&gt;\n    static constexpr std::array&amp;lt;float, N&gt; Get() {\n        std::array&amp;lt;float, N&gt; tHanningWindow{};\n        uint16_t             i = 0;\n        for (auto &amp;amp;el : tHanningWindow) {\n            el = std::sin((kPi * i) \/ (N - 1)) * std::sin((kPi * i) \/ (N - 1));\n            ++i;\n        }\n        return tHanningWindow;\n    }\n};\n\ntypedef struct {\n    double f0;\n    double fs;\n} kEqParams;\n\n\ntemplate&amp;lt;class WindowType&gt;\nstruct LPF {\n    template&amp;lt;std::size_t N, typename T = float&gt;\n    constexpr std::array&amp;lt;T, N&gt; GetCoeffs(const kEqParams &amp;amp;params) {\n        const auto            fc     = params.f0 \/ params.fs;\n        const auto            window = WindowType::template Get&amp;lt;N&gt;();\n        std::array&amp;lt;double, N&gt; coeffs{};\n        std::array&amp;lt;T, N&gt;      retCoeffs{};\n\n        for (int i = 0; i &amp;lt; N; i++) {\n            coeffs&#x5B;i] = dsp::math::sinc(2.0 * fc * (i - (N - 1) \/ 2.0));\n        }\n\n        int i = 0;\n        for (auto &amp;amp;coeff : coeffs) { coeff *= window&#x5B;i++]; }\n\n        \/\/ Normalize to get unity gain\n        auto maxAbsMultiplier = 0.0;\n        for (auto &amp;amp;coeff : coeffs) { maxAbsMultiplier += coeff; }\n\n        for (auto &amp;amp;coeff : coeffs) { coeff \/= maxAbsMultiplier; }\n\n        for (i = 0; i &amp;lt; N; ++i) { retCoeffs&#x5B;i] = static_cast&amp;lt;T&gt;(coeffs&#x5B;i]); }\n\n        return retCoeffs;\n    }\n};\n<\/pre><\/div>\n\n\n<h2 class=\"wp-block-heading\"><strong>Ograniczmy nasz\u0105 rozdzielczo\u015b\u0107 przetwarzania<\/strong><\/h2>\n\n\n\n<p>Typowo do oblicze\u0144 cyfrowego przetwarzania sygna\u0142\u00f3w (DSP) u\u017cywamy <strong>liczb zmiennoprzecinkowych<\/strong> typu float i double. Je\u017celi chcieliby\u015bmy dodatkowo zoptymalizowa\u0107 obliczenia, bo np. nie potrzebujemy a\u017c tak du\u017cej rozdzielczo\u015bci oblicze\u0144, mo\u017cemy u\u017cy\u0107 liczb sta\u0142oprzecinkowych. Liczba sta\u0142oprzecinkowa (ang. fixed-point number) to metoda reprezentowania liczb u\u0142amkowych (nieca\u0142kowitych) poprzez przechowywanie sta\u0142ej liczby cyfr ich cz\u0119\u015bci u\u0142amkowej.<\/p>\n\n\n\n<p><strong>Liczby sta\u0142oprzecinkowe<\/strong> s\u0105 cz\u0119sto u\u017cywane w systemach wbudowanych i cyfrowym przetwarzaniu sygna\u0142\u00f3w (DSP) z powodu swojej efektywno\u015bci obliczeniowej i prostoty implementacji. W systemach tych operacje na liczbach sta\u0142oprzecinkowych mog\u0105 by\u0107 wykonane szybciej i z mniejszym zapotrzebowaniem na zasoby procesora ni\u017c operacje na liczbach zmiennoprzecinkowych.<\/p>\n\n\n\n<p>Przyk\u0142adowo, je\u015bli mamy 8-bitow\u0105 liczb\u0119 sta\u0142oprzecinkow\u0105 z przecinkiem dziesi\u0119tnym umieszczonym po czterech bitach, to liczba ta reprezentuje format Q4.4, gdzie:<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li>4 bity s\u0105 przed przecinkiem (cz\u0119\u015b\u0107 ca\u0142kowita),<\/li>\n\n\n\n<li>4 bity s\u0105 po przecinku (cz\u0119\u015b\u0107 u\u0142amkowa).<\/li>\n<\/ul>\n\n\n\n<p>Dla liczby 8-bitowej (0101 0110) w formacie Q4.4:<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li>Cz\u0119\u015b\u0107 ca\u0142kowita:&nbsp; 0101 (czyli 5 w formacie dziesi\u0119tnym),<\/li>\n\n\n\n<li>Cz\u0119\u015b\u0107 u\u0142amkowa: 0110 (czyli 6\/16 = 0.375 w formacie dziesi\u0119tnym).<\/li>\n<\/ul>\n\n\n\n<p>Ca\u0142a liczba wynosi natomiast<\/p>\n\n\n\n<p>5 + 0.375 = 5.375.<\/p>\n\n\n\n<p>Skalowanie jest kluczowym konceptem w liczbach sta\u0142oprzecinkowych. Polega na pomno\u017ceniu warto\u015bci rzeczywistej przez okre\u015blony czynnik skaluj\u0105cy (moc dw\u00f3ch) w celu reprezentacji liczby w formacie ca\u0142kowitoliczbowym.<\/p>\n\n\n\n<p>Przyk\u0142adowo, je\u015bli chcemy reprezentowa\u0107 liczb\u0119 5.375 w formacie Q4.4:<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li>Pomn\u00f3\u017c 5.375 przez 2<sup>4<\/sup>=16 (czynnik skaluj\u0105cy dla formatu Q4.4),<\/li>\n\n\n\n<li>Otrzymujemy 5.375*16=86,<\/li>\n\n\n\n<li>86 w postaci binarnej to 0101 0110.<\/li>\n<\/ul>\n\n\n\n<h3 class=\"wp-block-heading\"><strong>Zalety i wady liczb sta\u0142oprzecinkowych<\/strong><\/h3>\n\n\n\n<p>Zalety:<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li><strong>Wydajno\u015b\u0107<\/strong> \u2013 operacje na liczbach sta\u0142oprzecinkowych s\u0105 zazwyczaj szybsze i wymagaj\u0105 mniej zasob\u00f3w ni\u017c operacje na liczbach zmiennoprzecinkowych.<\/li>\n\n\n\n<li><strong>Prostota<\/strong> \u2013 mniej skomplikowane jednostki arytmetyczne s\u0105 wymagane do przetwarzania liczb sta\u0142oprzecinkowych.<\/li>\n\n\n\n<li><strong>Deterministyczno\u015b\u0107<\/strong> \u2013 liczby sta\u0142oprzecinkowe maj\u0105 deterministyczne zachowanie pod wzgl\u0119dem czasu wykonania operacji.<\/li>\n<\/ul>\n\n\n\n<p>Wady:<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li><strong>Ograniczony zakres i precyzja<\/strong> \u2013 ze wzgl\u0119du na sta\u0142\u0105 pozycj\u0119 przecinka, liczby sta\u0142oprzecinkowe maj\u0105 ograniczony zakres reprezentacji i precyzj\u0119 w por\u00f3wnaniu do liczb zmiennoprzecinkowych.<\/li>\n\n\n\n<li><strong>Potencjalne przepe\u0142nienie<\/strong> \u2013 operacje mog\u0105 \u0142atwo prowadzi\u0107 do przepe\u0142nienia, je\u015bli wyniki przekraczaj\u0105 zakres reprezentowanych warto\u015bci.<\/li>\n\n\n\n<li><strong>Z\u0142o\u017c<\/strong><strong>ono\u015b\u0107 skalowania<\/strong> \u2013 programista musi starannie zarz\u0105dza\u0107 skalowaniem, aby unikn\u0105\u0107 utraty precyzji i przepe\u0142nienia.<\/li>\n<\/ul>\n\n\n\n<p>Implementuj\u0105c bibliotek\u0119 sta\u0142oprzecinkow\u0105, kt\u00f3ra wspiera operacje constexpr j\u0119zyka C++ oraz posiada odpowiednie przeci\u0105\u017cenia operator\u00f3w matematycznych, mo\u017cemy generowa\u0107 dane (np. wsp\u00f3\u0142czynniki filtra FIR), zmieniaj\u0105c tylko typ danych.<\/p>\n\n\n<div class=\"wp-block-syntaxhighlighter-code \"><pre class=\"brush: plain; title: ; notranslate\" title=\"\">\nconstexpr auto                                kFilterSize   = 256u;\nconstexpr auto                                kSamplingFreq = 44.1_kHz;\n\nstatic constexpr __attribute__((aligned(16))) std::array&amp;lt;float, kFilterSize&gt; kFloatFilterCoeffs{\n    dsp::fir::LPF&amp;lt;dsp::HammingWindow&gt;{}.GetCoeffs&amp;lt;kFilterSize, float&gt;(\n        dsp::fir::kEqParams{ .f0 = 4000, .fs = kSamplingFreq })\n};\n\nstatic constexpr __attribute__((aligned(16))) std::array&amp;lt;QFormat&amp;lt;Q15&gt;, kFilterSize&gt; kQ15FilterCoeffs{\n    dsp::fir::LPF&amp;lt;dsp::HammingWindow&gt;{}.GetCoeffs&amp;lt;kFilterSize, QFormat&amp;lt;Q15&gt;&gt;(\n        dsp::fir::kEqParams{ .f0 = 4000, .fs = kSamplingFreq })\n};\n<\/pre><\/div>\n\n\n<h2 class=\"wp-block-heading\"><strong>Jak to si\u0119 sprawdza w rzeczywisto\u015bci?<\/strong><\/h2>\n\n\n\n<p>Tak jak w poprzedniej cz\u0119\u015bci tego artyku\u0142u, tak samo w tej u\u017cyjemy mikrokontrolera ESP32-S3 z instrukcjami SIMD. Na pocz\u0105tku sprawd\u017amy, jakie wyniki osi\u0105gnie ten mikrokontroler przy podstawowych operacjach matematycznych w zale\u017cno\u015bci od typu danych.<\/p>\n\n\n\n<figure class=\"wp-block-image aligncenter size-full\"><a href=\"https:\/\/sii.pl\/blog\/wp-content\/uploads\/2024\/08\/7.jpg\"><img decoding=\"async\" width=\"911\" height=\"225\" src=\"https:\/\/sii.pl\/blog\/wp-content\/uploads\/2024\/08\/7.jpg\" alt=\"Wyniki testu sprawdzaj\u0105cego szybko\u015b\u0107 operacji arytmetycznych mikrokontrolera ESP32-S3\" class=\"wp-image-28831\" srcset=\"https:\/\/sii.pl\/blog\/wp-content\/uploads\/2024\/08\/7.jpg 911w, https:\/\/sii.pl\/blog\/wp-content\/uploads\/2024\/08\/7-300x74.jpg 300w, https:\/\/sii.pl\/blog\/wp-content\/uploads\/2024\/08\/7-768x190.jpg 768w\" sizes=\"(max-width: 911px) 100vw, 911px\" \/><\/a><figcaption class=\"wp-element-caption\">Tab. 1 Wyniki testu sprawdzaj\u0105cego szybko\u015b\u0107 operacji arytmetycznych mikrokontrolera ESP32-S3<\/figcaption><\/figure>\n\n\n\n<p>Jak wida\u0107 w przedstawionej tabeli, operacje dodawania oraz mno\u017cenia dla typu integer oraz float s\u0105 por\u00f3wnywalne, natomiast typ float wypada o wiele lepiej przy operacji mno\u017cenia i akumulacji (kt\u00f3ra jest wykorzystywana w filtrze FIR \u2013 co prze\u0142o\u017cy si\u0119 na dalsze wyniki ;)). <strong>Najgorzej wypada<\/strong> typ double, ze wzgl\u0119du na brak bezpo\u015bredniego wsparcia w FPU mikroprocesora. Najgorszym typem operacji jest dzielenie, kt\u00f3re w ka\u017cdym przypadku zabiera\u0142o najwi\u0119cej czasu procesora.<\/p>\n\n\n\n<p><strong>Rozwi\u0105zaniem na pozbycie si\u0119 dzielenia<\/strong>, a tym samym przyspieszenie kodu, jest mno\u017cenie przez odwrotno\u015b\u0107. Zamiast dzieli\u0107 ka\u017cd\u0105 liczb\u0119 w tablicy, lepiej jest jednorazowo obliczy\u0107 odwrotno\u015b\u0107 dzielnika i przemno\u017cy\u0107 ka\u017cd\u0105 liczb\u0119.<\/p>\n\n\n<div class=\"wp-block-syntaxhighlighter-code \"><pre class=\"brush: plain; title: ; notranslate\" title=\"\">\nstd::array&amp;lt;float, N&gt; data;\n    for(auto &amp;amp;el : data)\n    {\n        data \/= 4;\n    }\n\n    \/\/ Calculate 1\/divider and multiply data\n    std::array&amp;lt;float, N&gt; data;\n    const auto oneOverDivider = 1 \/ 4;\n    for(auto &amp;amp;el : data)\n    {\n        data *= oneOverDivider;\n    }\n<\/pre><\/div>\n\n\n<h3 class=\"wp-block-heading\"><strong>Por\u00f3wnanie wydajno\u015bci<\/strong><\/h3>\n\n\n\n<p>Przejd\u017amy teraz do por\u00f3wnania wydajno\u015bci opisywanego w poprzedniej cz\u0119\u015bci filtra FIR. Sprawd\u017amy, ile czasu b\u0119dzie potrzebowa\u0142 nasz mikrokontroler, aby przefiltrowa\u0107 sygna\u0142 audio \u2013 por\u00f3wnajmy czas przetwarzania w zale\u017cno\u015bci od rozmiaru filtra, rozmiaru ramki danych audio oraz typu przetwarzanych danych.<\/p>\n\n\n\n<figure class=\"wp-block-image aligncenter size-full\"><a href=\"https:\/\/sii.pl\/blog\/wp-content\/uploads\/2024\/08\/8.jpg\"><img decoding=\"async\" width=\"977\" height=\"467\" src=\"https:\/\/sii.pl\/blog\/wp-content\/uploads\/2024\/08\/8.jpg\" alt=\"Por\u00f3wnanie czas\u00f3w przetwarzania filtra FIR operuj\u0105cego na danych typu float w zale\u017cno\u015bci od rozmiaru filtra oraz rozmiaru bloku audio\" class=\"wp-image-28833\" srcset=\"https:\/\/sii.pl\/blog\/wp-content\/uploads\/2024\/08\/8.jpg 977w, https:\/\/sii.pl\/blog\/wp-content\/uploads\/2024\/08\/8-300x143.jpg 300w, https:\/\/sii.pl\/blog\/wp-content\/uploads\/2024\/08\/8-768x367.jpg 768w\" sizes=\"(max-width: 977px) 100vw, 977px\" \/><\/a><figcaption class=\"wp-element-caption\">Tab. 2 Por\u00f3wnanie czas\u00f3w przetwarzania filtra FIR operuj\u0105cego na danych typu float w zale\u017cno\u015bci od rozmiaru filtra oraz rozmiaru bloku audio<\/figcaption><\/figure>\n\n\n\n<figure class=\"wp-block-image aligncenter size-full\"><a href=\"https:\/\/sii.pl\/blog\/wp-content\/uploads\/2024\/08\/9.jpg\"><img decoding=\"async\" width=\"1018\" height=\"466\" src=\"https:\/\/sii.pl\/blog\/wp-content\/uploads\/2024\/08\/9.jpg\" alt=\"Por\u00f3wnanie czas\u00f3w przetwarzania filtra FIR operuj\u0105cego na danych typu Q15 w zale\u017cno\u015bci od rozmiaru filtra oraz rozmiaru bloku audio\" class=\"wp-image-28835\" srcset=\"https:\/\/sii.pl\/blog\/wp-content\/uploads\/2024\/08\/9.jpg 1018w, https:\/\/sii.pl\/blog\/wp-content\/uploads\/2024\/08\/9-300x137.jpg 300w, https:\/\/sii.pl\/blog\/wp-content\/uploads\/2024\/08\/9-768x352.jpg 768w\" sizes=\"(max-width: 1018px) 100vw, 1018px\" \/><\/a><figcaption class=\"wp-element-caption\">Tab. 3 Por\u00f3wnanie czas\u00f3w przetwarzania filtra FIR operuj\u0105cego na danych typu Q15 w zale\u017cno\u015bci od rozmiaru filtra oraz rozmiaru bloku audio<\/figcaption><\/figure>\n\n\n\n<p>Obrabiaj\u0105c dane z tabeli powy\u017cej, mo\u017cemy uzyska\u0107 wykres pokazuj\u0105cy w jakim stopniu operacje SIMD przyspieszy\u0142y nasz algorytm dla danego rozmiaru filtra oraz bloku audio.<\/p>\n\n\n\n<figure class=\"wp-block-image aligncenter size-full\"><a href=\"https:\/\/sii.pl\/blog\/wp-content\/uploads\/2024\/08\/image5-7.png\"><img decoding=\"async\" width=\"779\" height=\"479\" src=\"https:\/\/sii.pl\/blog\/wp-content\/uploads\/2024\/08\/image5-7.png\" alt=\"Procentowy wykres akceleracji filtru FIR wykorzystuj\u0105c operacje SIMD. Por\u00f3wnanie wzgl\u0119dem rozmiaru filtru dla danych typu float\" class=\"wp-image-28837\" srcset=\"https:\/\/sii.pl\/blog\/wp-content\/uploads\/2024\/08\/image5-7.png 779w, https:\/\/sii.pl\/blog\/wp-content\/uploads\/2024\/08\/image5-7-300x184.png 300w, https:\/\/sii.pl\/blog\/wp-content\/uploads\/2024\/08\/image5-7-768x472.png 768w\" sizes=\"(max-width: 779px) 100vw, 779px\" \/><\/a><figcaption class=\"wp-element-caption\">Ryc. 5 Procentowy wykres akceleracji filtru FIR wykorzystuj\u0105c operacje SIMD. Por\u00f3wnanie wzgl\u0119dem rozmiaru filtru dla danych typu float<\/figcaption><\/figure>\n\n\n\n<p>Drugi wykres przedstawia wyniki uzyskane, gdy filtr FIR korzysta\u0142 z danych typu Q15:<\/p>\n\n\n\n<figure class=\"wp-block-image aligncenter size-full\"><a href=\"https:\/\/sii.pl\/blog\/wp-content\/uploads\/2024\/08\/image6-4.png\"><img decoding=\"async\" width=\"764\" height=\"472\" src=\"https:\/\/sii.pl\/blog\/wp-content\/uploads\/2024\/08\/image6-4.png\" alt=\"Procentowy wykres akceleracji filtru FIR wykorzystuj\u0105c operacje SIMD. Por\u00f3wnanie wzgl\u0119dem rozmiaru filtru dla danych typu Q15\" class=\"wp-image-28839\" srcset=\"https:\/\/sii.pl\/blog\/wp-content\/uploads\/2024\/08\/image6-4.png 764w, https:\/\/sii.pl\/blog\/wp-content\/uploads\/2024\/08\/image6-4-300x185.png 300w\" sizes=\"(max-width: 764px) 100vw, 764px\" \/><\/a><figcaption class=\"wp-element-caption\">Ryc. 6 Procentowy wykres akceleracji filtru FIR wykorzystuj\u0105c operacje SIMD. Por\u00f3wnanie wzgl\u0119dem rozmiaru filtru dla danych typu Q15<\/figcaption><\/figure>\n\n\n\n<p>Jak wida\u0107 na wykresie, <strong>w przypadku danych typu float nie op\u0142aca si\u0119 u\u017cywa\u0107 operacji SIMD<\/strong>, gdy rozmiar filtru jest ma\u0142y lub gdy ramka audio ma ma\u0142y rozmiar. W przypadku danych Q15, akceleracja jest o wiele ni\u017csza ni\u017c w przypadku danych typu float. Czemu tak jest? Pami\u0119tacie jakie wyniki uzyskali\u015bmy podczas stress testu operacji matematycznych? Wysz\u0142o nam, \u017ce operacja MAC (ang. Multiply and Accumulate) zosta\u0142a wykonywana najszybciej dla danych typu float. W\u0142a\u015bnie to ma prze\u0142o\u017cenie na uzyskane przez nas wyniki.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\"><strong>Pora na podsumowanie<\/strong><\/h2>\n\n\n\n<p>Artyku\u0142 z pewno\u015bci\u0105 nie wyczerpuje wszystkich temat\u00f3w zwi\u0105zanych z cyfrowym przetwarzaniem sygna\u0142\u00f3w. Moim celem by\u0142o pokazanie, <strong>jak w prosty spos\u00f3b z wykorzystaniem \u0142atwo dost\u0119pnych element\u00f3w mo\u017cemy rozpocz\u0105\u0107 swoja przygod\u0119 z audio<\/strong>.<\/p>\n\n\n\n<p>Dowiedzieli\u015bmy si\u0119, jaki mikrokontroler nale\u017cy wybra\u0107, je\u017celi chcemy stworzy\u0107 aplikacj\u0119 audio, jak dzia\u0142a protok\u00f3\u0142 I2S, kt\u00f3ry jest szeroko stosowany w przetwarzaniu sygna\u0142\u00f3w i jak wykorzysta\u0107 DMA do efektywnego przetwarzania danych.<\/p>\n\n\n\n<p>Om\u00f3wili\u015bmy te\u017c wykorzystanie operacji SIMD do zr\u00f3wnolegnienia naszych oblicze\u0144 matematycznych oraz dowiedzieli\u015bmy si\u0119, jak wykorzysta\u0107 typ sta\u0142oprzecinkowy. Uzyskane wyniki jednoznacznie pokaza\u0142y, \u017ce wykorzystanie operacji SIMD jest kluczowe w aplikacjach DSP.<\/p>\n\n\n\n<p>Kod przedstawiony w artykule oraz u\u017cyty do pomiar\u00f3w czasu przetwarzania algorytm\u00f3w <a href=\"https:\/\/github.com\/michalber\/EmbeddedAudioBlogPost\" target=\"_blank\" aria-label=\" (opens in a new tab)\" rel=\"noreferrer noopener\" class=\"ek-link\" rel=\"nofollow\" >jest dost\u0119pny na GitHubie<\/a>.<\/p>\n\n\n<div class=\"kk-star-ratings kksr-auto kksr-align-left kksr-valign-bottom\"\n    data-payload='{&quot;align&quot;:&quot;left&quot;,&quot;id&quot;:&quot;28807&quot;,&quot;slug&quot;:&quot;default&quot;,&quot;valign&quot;:&quot;bottom&quot;,&quot;ignore&quot;:&quot;&quot;,&quot;reference&quot;:&quot;auto&quot;,&quot;class&quot;:&quot;&quot;,&quot;count&quot;:&quot;5&quot;,&quot;legendonly&quot;:&quot;&quot;,&quot;readonly&quot;:&quot;&quot;,&quot;score&quot;:&quot;5&quot;,&quot;starsonly&quot;:&quot;&quot;,&quot;best&quot;:&quot;5&quot;,&quot;gap&quot;:&quot;11&quot;,&quot;greet&quot;:&quot;&quot;,&quot;legend&quot;:&quot;5\\\/5 ( votes: 5)&quot;,&quot;size&quot;:&quot;18&quot;,&quot;title&quot;:&quot;Rozw\u00f3j oprogramowania audio dla system\u00f3w embedded&quot;,&quot;width&quot;:&quot;139.5&quot;,&quot;_legend&quot;:&quot;{score}\\\/{best} ( {votes}: {count})&quot;,&quot;font_factor&quot;:&quot;1.25&quot;}'>\n            \n<div class=\"kksr-stars\">\n    \n<div class=\"kksr-stars-inactive\">\n            <div class=\"kksr-star\" data-star=\"1\" style=\"padding-right: 11px\">\n            \n\n<div class=\"kksr-icon\" style=\"width: 18px; height: 18px;\"><\/div>\n        <\/div>\n            <div class=\"kksr-star\" data-star=\"2\" style=\"padding-right: 11px\">\n            \n\n<div class=\"kksr-icon\" style=\"width: 18px; height: 18px;\"><\/div>\n        <\/div>\n            <div class=\"kksr-star\" data-star=\"3\" style=\"padding-right: 11px\">\n            \n\n<div class=\"kksr-icon\" style=\"width: 18px; height: 18px;\"><\/div>\n        <\/div>\n            <div class=\"kksr-star\" data-star=\"4\" style=\"padding-right: 11px\">\n            \n\n<div class=\"kksr-icon\" style=\"width: 18px; height: 18px;\"><\/div>\n        <\/div>\n            <div class=\"kksr-star\" data-star=\"5\" style=\"padding-right: 11px\">\n            \n\n<div class=\"kksr-icon\" style=\"width: 18px; height: 18px;\"><\/div>\n        <\/div>\n    <\/div>\n    \n<div class=\"kksr-stars-active\" style=\"width: 139.5px;\">\n            <div class=\"kksr-star\" style=\"padding-right: 11px\">\n            \n\n<div class=\"kksr-icon\" style=\"width: 18px; height: 18px;\"><\/div>\n        <\/div>\n            <div class=\"kksr-star\" style=\"padding-right: 11px\">\n            \n\n<div class=\"kksr-icon\" style=\"width: 18px; height: 18px;\"><\/div>\n        <\/div>\n            <div class=\"kksr-star\" style=\"padding-right: 11px\">\n            \n\n<div class=\"kksr-icon\" style=\"width: 18px; height: 18px;\"><\/div>\n        <\/div>\n            <div class=\"kksr-star\" style=\"padding-right: 11px\">\n            \n\n<div class=\"kksr-icon\" style=\"width: 18px; height: 18px;\"><\/div>\n        <\/div>\n            <div class=\"kksr-star\" style=\"padding-right: 11px\">\n            \n\n<div class=\"kksr-icon\" style=\"width: 18px; height: 18px;\"><\/div>\n        <\/div>\n    <\/div>\n<\/div>\n                \n\n<div class=\"kksr-legend\" style=\"font-size: 14.4px;\">\n            5\/5 ( votes: 5)    <\/div>\n    <\/div>\n","protected":false},"excerpt":{"rendered":"<p>Obecnie s\u0142uchawki bezprzewodowe s\u0105 bardziej popularne ni\u017c jeszcze kilka lat temu ze wzgl\u0119du na to, \u017ce technologia w nich u\u017cywana &hellip; <a class=\"continued-btn\" href=\"https:\/\/sii.pl\/blog\/rozwoj-oprogramowania-audio-dla-systemow-embedded\/\">Continued<\/a><\/p>\n","protected":false},"author":664,"featured_media":28842,"comment_status":"open","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"_acf_changed":false,"_editorskit_title_hidden":false,"_editorskit_reading_time":0,"_editorskit_is_block_options_detached":false,"_editorskit_block_options_position":"{}","inline_featured_image":false,"footnotes":""},"categories":[1314],"tags":[2647,1512,563,249,1161],"class_list":["post-28807","post","type-post","status-publish","format-standard","has-post-thumbnail","hentry","category-development-na-twardo","tag-anc","tag-poradnik","tag-embedded","tag-audio","tag-mikrokontroler"],"acf":[],"aioseo_notices":[],"republish_history":[],"featured_media_url":"https:\/\/sii.pl\/blog\/wp-content\/uploads\/2024\/08\/Rozwoj-oprogramowania-audio-dla-systemow-embedded.jpg","category_names":["Development na twardo"],"_links":{"self":[{"href":"https:\/\/sii.pl\/blog\/wp-json\/wp\/v2\/posts\/28807"}],"collection":[{"href":"https:\/\/sii.pl\/blog\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/sii.pl\/blog\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/sii.pl\/blog\/wp-json\/wp\/v2\/users\/664"}],"replies":[{"embeddable":true,"href":"https:\/\/sii.pl\/blog\/wp-json\/wp\/v2\/comments?post=28807"}],"version-history":[{"count":3,"href":"https:\/\/sii.pl\/blog\/wp-json\/wp\/v2\/posts\/28807\/revisions"}],"predecessor-version":[{"id":28945,"href":"https:\/\/sii.pl\/blog\/wp-json\/wp\/v2\/posts\/28807\/revisions\/28945"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/sii.pl\/blog\/wp-json\/wp\/v2\/media\/28842"}],"wp:attachment":[{"href":"https:\/\/sii.pl\/blog\/wp-json\/wp\/v2\/media?parent=28807"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/sii.pl\/blog\/wp-json\/wp\/v2\/categories?post=28807"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/sii.pl\/blog\/wp-json\/wp\/v2\/tags?post=28807"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}