Alternatywne sposoby programowania aplikacji DSP

| Technika

Duża wydajność procesorów ogólnego przeznaczenia sprawia, że coraz częściej są one wystarczające, aby wykonywać zadania uruchamiane dotąd tylko i wyłącznie na specjalizowanych jednostkach DSP. Sprawa dotyczy nie tylko najnowszych systemów Intela czy AMD, ale także układów oszczędnych energetycznie, a nawet układów FPGA. Jeszcze niedawno specjalizowane procesory DSP nie miały konkurencji. Ich zdolność do wykonywania zaawansowanych obliczeń w czasie rzeczywistym była nie do przecenienia. Jedyną konkurencję mogły stanowić najdroższe procesory ogólnego przeznaczenia, ale tylko i wyłącznie wtedy, gdy były programowane w asemblerze. Od tego czasu sytuacja się zmieniła mimo, że asembler to język programowania wciąż oferujący najwyższą wydajność tworzonego programu. Niestety, jest to jedyna zaleta tego języka.

Alternatywne sposoby programowania aplikacji DSP

Duża wydajność procesorów ogólnego przeznaczenia sprawia, że coraz częściej są one wystarczające, aby wykonywać zadania uruchamiane dotąd tylko i wyłącznie na specjalizowanych jednostkach DSP. Sprawa dotyczy nie tylko najnowszych systemów Intela czy AMD, ale także układów oszczędnych energetycznie, a nawet układów FPGA.

Problem wysokiej wydajności asemblera

Jeszcze niedawno specjalizowane procesory DSP nie miały konkurencji. Ich zdolność do wykonywania zaawansowanych obliczeń w czasie rzeczywistym była nie do przecenienia. Jedyną konkurencję mogły stanowić najdroższe procesory ogólnego przeznaczenia, ale tylko i wyłącznie wtedy, gdy były programowane w asemblerze. Od tego czasu sytuacja się zmieniła mimo, że asembler to język programowania wciąż oferujący najwyższą wydajność tworzonego programu. Niestety, jest to jedyna zaleta tego języka. Praktycznie nie ma mowy o przenośności kodu, gdyż każda nowa rodzina wyrobów posługuje się swoim własnym asemblerem. Ponadto, nawet jeżeli spróbować przenieść kod na procesor kompatybilny z poprzednio używanym, optymalność nowej implementacji będzie wysoce wątpliwa. Kolejne wersje procesorów tej samej rodziny różnią się architekturą jądra i zawierają nowe zestawy instrukcji, znacznie przyspieszających działanie procesora w pewnym zakresie potencjalnych zadań. Dobry programista, korzystający z asemblera powinien być zorientowany w specyfice programowanego procesora i tworzyć kod, który wykorzysta wszystkie zalety wybranej jednostki obliczeniowej.

Bardzo poważną wadą programowania niskopoziomowego jest również czas potrzebny na napisanie kodu. Powoduje to, że asembler jest wykorzystywany najczęściej dopiero w momencie programowania ostatecznej wersji oprogramowania, podczas, gdy wszystkie symulacje przeprowadzane są na podstawie kodu napisanego w języku wyższego poziomu. Ostatnim problemem, wręcz wykluczającym asembler w nowych projektach, jest jego nieczytelność. Ponieważ bardzo trudno jest analizować, debuggować lub modyfikować napisany wcześniej kod, uciążliwym staje się aktualizowanie oprogramowania tworzonego urządzenia. W związku z powyższym, a jednocześnie mając na uwadze fakt, że w obecnych czasach udostępnianie aktualizacji oprogramowania jest istotnym czynnikiem decydującym o zakupie urządzenia przez klienta, asembler jako język programowania wykorzystywany jest tylko wtedy, gdy jest to absolutnie konieczne.

Nie przekreśla to jednak szans procesorów ogólnego przeznaczenia na konkurowanie z jednostkami DSP. Okazuje się, że na nowych procesorach wystarczająco wydajny może być kod napisany w popularnym języku C. Ten już dosyć stary język ma bardzo wiele zalet. Po pierwsze, nie jest trudno znaleźć inżyniera umiejącego programować w C. Proces programowania jest też znacznie szybszy niż z wykorzystaniem czystego asemblera, a co więcej, język C pozwala na stosowanie wstawek asemblerowych. W związku z powyższym możliwe jest napisanie programu, który w większości składa się z kodu w C, a w kluczowych momentach obliczenia wykonywane są na podstawie źródła asemblerowego. Wśród zalet języka C należy także wymienić zdolność do operowania na niskim poziomie pojedynczych bitów, a zarazem wysoką przenośność kodu. Kompilatory języka C dostępne są prawie na każdą istniejącą platformę, choć ich wybór, jak i wybór parametrów kompilacji będą znacząco wpływać na uzyskany efekt końcowy. Konsekwencją wymienionych cech jest jeszcze jedna zaleta, polegająca na niskich kosztach debuggowania i modyfikowania napisanego kodu. Wiele z błędów zostanie wyłapanych już na etapie kompilacji, a nie dopiero podczas uruchomienia programu. Czytelność kodu jest także znacznie wyższa niż w przypadku programów napisanych w czystym asemblerze. Niestety, standardowe ANSI C nie było przeznaczone do obliczeń sygnałowych, a co za tym idzie, nie obsługuje niektórych z opcji, które są niezbędne do efektywnego programowania aplikacji DSP. Napisany kod na jednym procesorze może zostać skompilowany w taki sposób, że liczba typu int będzie miała 32 bity, a na innym 16 bitów długości. Język ANSI C nie ma wbudowanej także obsługi ułamków stałoprzecinkowych, które są jednym z niezbędnych elementów aplikacji DSP. Cechy te sprawiają, że napisanie optymalnego programu w czystym C może okazać się niemożliwe.

Arytmetyka opierająca się na ułamkach stałoprzecinkowych, różni się znacząco w precyzji wykonywanych na niej obliczeń, od arytmetyki zmiennoprzecinkowej. W pierwszej z nich liczby zapisywane są w formacie (A, B), gdzie ciąg bitów A przedstawia liczbę całości, a ciąg B część ułamkową, przy czym na obie części zarezerwowane są stałe liczby bitów. Arytmetyka zmiennoprzecinkowa operuje zapisem (E, M), w którym M to bitowy zapis mantysy, a E, to wykładnik, którego znaczenie sprowadza się do określenia pozycji przecinka w ułamku, w odniesieniu do wartości mantysy. Stosując arytmetykę zmiennoprzecinkową, nie ma możliwości odgórnego ustalenia stałej precyzji obliczeń.

Procesory DSP korzystają także z obliczeń nasyconych, które polegają na ograniczeniu od góry i od dołu zakresu wartości, w których mogą zawierać się zmienne. Przekroczenie którejś z tych wartości spowoduje, że wynik zostanie skrócony do przekraczanej granicy.

Na pomoc przychodzi język Embedded C, który jest zmodyfikowanym C, przeznaczonym do tworzenia systemów wbudowanych. O ile zachowuje on składnię i większość funkcji oryginalnego C, to rozwija je także o takie dodatki, jak liczby ułamkowe stałoprzecinkowe, czy też arytmetykę nasyconą. Dzięki nim możliwe jest pisanie kodu, który będzie znacznie bliższy optymalnemu. Co prawda, tak powstały kod będzie już mniej przenośny, a precyzyjne stosowanie usprawnień z Embedded C wymaga dodatkowych umiejętności programistycznych, ale nadal jest to opcja godna polecenia w wielu projektach.

Jeżeli zatrudnieni programiści preferują język C++, to także mają szansę spróbować swych sił w programowaniu DSP. Istnieją różne warianty kompilatorów języka C++, które obsługują zmodyfikowaną składnię, przystosowaną do zastosowań typowych dla przetwarzania sygnałów. Różnica pomiędzy kierunkiem tych modyfikacji, a zmianami dokonanymi w celu stworzenia Embedded C, polega na tym, że język C++ został raczej odchudzony, co spowodowało uproszczenie składni jak i zmniejszenie rozmiarów bibliotek. Korzyści są wyraźne. Mniejsze biblioteki, to mniejsza ilość zajętej pamięci, co jest szczególnie ważne w małych, tanich i energooszczędnych systemach. Język został też pozbawiony obsługi wyjątków, szablonów, wielokrotnego dziedziczenia, jak i niektórych przestrzeni nazw – choć dokładne zmiany zależeć będą od konkretnych implementacji kompilatorów. Kompilatory języków Embeded C/C++ tworzone są zarówno jako uniwersalne narzędzia, jak i w postaci programów przeznaczonych tylko i wyłącznie do jednej rodziny procesorów. Narzędzia te istnieją w wersjach przeznaczonych dla uniwersalnych procesorów, jak i specjalistycznych rodzin procesorów sygnałowych.

Przetwarzanie równoległe

Wybór języka programowania powinien uwzględniać jeszcze jedną cechę nowych procesorów, który może się znacznie przyczynić do wydajności programowanych aplikacji – wielordzeniowość. Dostępne od pewnego czasu, popularne procesory dwujądrowe, a nawet wcześniej produkowane jednostki obsługujące technologię hyper-threading, widziane są w systemie jako dwa oddzielne procesory. Fakt ten może być wykorzystany, ale tylko wtedy, jeśli programista napisze kod, który będzie się mógł wykonywać

Bardzo duży wpływ na wzrost wydajności aplikacji będzie miało wprowadzenie do niej instrukcji typu SIMD (ang: Single Instruction Multiple Data), które dostępne są w klasycznych procesorach w postaci pakietów MMX i kolejnych wersji SSE (Intel), 3DNow! (Amd), AltiVec (PowerPC), VIS (Sparc), czy też MAJC (Sun). Aby w pełni wykorzystać możliwości tych rozszerzeń konieczne jest albo napisanie odpowiedniego kodu asemblerowego, albo włączenie odpowiednich przełączników podczas kompilacji kodu wyższego poziomu. Niestety, rzadko które kompilatory są w stanie efektywnie korzystać z instrukcji SIMD. Producenci procesorów wspierają klientów poprzez dostarczenie zoptymalizowanych bibliotek asemblerowych, bądź też wyczerpującej pomocy na temat tego, jak skutecznie korzystać z SIMD. Przykładem są Intelowskie biblioteki IPP i pakiet wspomagający programowanie: Tunning Assistant.

równolegle na dwóch jądrach. Niestety, najbardziej popularne obecnie języki programowania nie ułatwiają takiego podejścia do tematu. Kod w C to głównie lista poleceń wykonywanych jedno po drugim. Nawet, jeśli programista wykorzysta takie mechanizmy, jak tworzenie dodatkowych wątków, kod staje się niezbyt czytelny i nie jest łatwo ustalić, jaka będzie kolejność wykonywania poszczególnych zadań. Dopóki nie powstaną nowe standardy programowania, zorientowane na równoległe przetwarzanie danych, najwygodniejszym sposobem obsługi kilku jednostek arytmetycznych naraz jest skorzystanie z języka blokowego typu data flow. Programowanie za pomocą bloków polega na wstawianiu poszczególnych elementów w postaci prostokątów i łączeniu ich graficznie – za pomocą linii i strzałek. Każdy z bloków reprezentuje jakąś funkcję lub jednostkę, a połączenia - potoki danych pomiędzy poszczególnymi elementami. Niestety, podejście to przestaje się sprawdzać wtedy, gdy poza jednostajnym przetwarzaniem sygnałów dochodzi duża liczba sygnałów sterujących lub warunków i rozgałęzień algorytmu. Problemem są też sytuacje, w których okazuje się, że w posiadanej przez programistę bibliotece nie ma funkcji blokowej, realizującej pożądaną przez niego operację. Zgodnie z pierwotnym przeznaczeniem, języki programowania blokowego miały na celu jedynie przeprowadzanie symulacji, a samo programowanie docelowego systemu miało odbywać się w jakimś języku tekstowym. Tymczasem połączenie języka typu data flow z modułami w innym języku niszczy popularne koncepcje pisania całego projektu w jednym języku programowania. Niemniej, aby wygenerować program, który następnie będzie mógł być uruchamiany na jakimś procesorze, konieczne będzie dostarczenie do programu bibliotek zawierających opisy wszystkich wykorzystanych bloków, stworzone w jakimś kompilatorze. Bardzo popularnym językiem będzie w tym przypadku Matlab, na podstawie którego został stworzony pakiet Simulink firmy MathWorks, służący do tworzenia schematów data flow. Matlab to język bardzo popularny wśród osób tworzących algorytmy przetwarzające sygnały. Możliwość wygodnej symulacji i potężne możliwości przetwarzania macierzy sprawiają, że jest on szeroko wykorzystywany, choć głównie na czas symulacji. Gotowy kod jest następnie przetwarzany na przykład na język C, za pomocą specjalnych translatorów.

Na rynku dostępne są także konkurencyjne języki programowania blokowego, które wcale niekoniecznie posługują się językiem Matlab, jako kodem wynikowym stworzonego programu. Przykładem jest środowisko RIDE, stworzone przez grupę Hyperception, należącą do National Instruments. Opisane w RIDE programy przetwarzane są bezpośrednio na asembler, przeznaczony do kompilacji na konkretnym procesorze. Warto wspomnieć, że także producenci specjalizowanych układów DSP oferują swoim klientom systemy data flow, z tym, że tylko do programowania określonej rodziny procesorów. Przykładem będzie Analog Devices, które ma w swej ofercie procesory takie, jak przeznaczony do systemów audio SHARC lub multimedialny BlackFin, a do każdego z nich można nabyć odpowiednią wersję pakietu VisualDSP++. Oczywistą konsekwencją stosowania tego typu narzędzi jest zwiększenie efektywności programowania, kosztem zmniejszenia przenośności kodu.

Moduły IP z pomocą

Wiele z dostępnych na rynku translatorów, to produkty rozwijane na poziomie akademickim – w tym również w Polsce. Starają się one dostosować charakterystyczny sposób przetwarzania danych z Matlaba tak, aby jak najskuteczniej sprawdzał się na najnowszych procesorach. Istnieją też komercyjne narzędzia, przeznaczone do konkretnych rodzin procesorów. Przykładem będzie konwerter MATLAB-to-C firmy Catalytic, który przetwarza kod Matlaba, na stałoprzecinkowy kod C, kompilowany właściwie tylko na procesorach C64x marki Texas Instruments. Co więcej, środowisko Simulink potrafi także generować bezpośrednio kod C, przeznaczony na poszczególne, lub wszystkie procesory. Pakiet Real-Tome Workshop – część pakietu Simulink, zawiera biblioteki pozwalające zarówno wygenerować uniwersalny program jak i na wcześniej wspomniane procesory C64x.

Jedną z ważniejszych zalet opisywanych powyżej języków blokowych jest szybkość, z jaką można tworzyć w nich programy i poddawać je testowaniu. Niestety, w wypadku, gdy wykorzystywany pakiet nie zawiera potrzebnego w projekcie modułu, konieczne staje się czasochłonne napisanie go od zera. Co prawda istnieją rozwiązania niosące pomoc w takich sytuacjach, czego dobrym przykładem jest pakiet SPW firmy CoWare umożliwiający dokonywanie modyfikacji dostępnych modułów bibliotecznych, ale nie zlikwiduje to wszystkich problemów. Jeśli zastosowanie techniki typowej dla SPW nie jest możliwe, pozostaje jedynie szansa wykorzystania jednego z dostępnych modułów IP, które sprzedawane są przez przeróżne firmy programistyczne.

Moduły programowe IP (Intellecutal Property) to pakiety, najczęściej skompilowanego kodu, które mogą być wykorzystane za opłatą w tworzonym projekcie. Pakiety IP mogą zawierać zarówno pojedyncze funkcje jak i większe bloki komponentów. Przykładami dostępnych algorytmów będą różnego rodzaju filtry i transformaty lub implementacje obecnych na rynku standardów przesyłania i kodowania danych. Istotną zaletą nabywanych modułów będzie ich wysoki stopień optymalizacji, którego uzyskanie poprzez pisanie oprogramowania od początku, byłoby bardzo czasochłonne. Niestety, tak jak to bywa z zakupami, nabywca nie zawsze w pełni wie co kupuje. Może się zdarzyć, że nabyty blok będzie niekompatybilny z resztą oprogramowania lub pełen ograniczeń – takich jak na przykład odgórnie ustalona precyzja przetwarzania. W celu zwiększenia szans na poprawną współpracę poszczególnych elementów, warto zaopatrzyć się w kod napisany zgodnie ze standardami wprowadzanymi przez wytwórców elektroniki. Koncerny takie jak TI czy LSI Logic ustanowiły swoje standardy pisania programów, które koncentrują się na przyjęciu jednolitej konwencji w nazewnictwie oraz sposobie odwołań do pamięci i otaczających układ podzespołów elektronicznych. Trudno też z góry ocenić wydajność nabywanych komponentów, a ewentualne poprawki w ich budowie możliwe będą tylko, jeśli sprzedawca udostępni kod źródłowy programu. Gorzej, gdy jedyne co jest dostępne, to skompilowana biblioteka. Takie sytuacje będą występować głównie w przypadku zakupów większych aplikacji – często łączących w sobie kilka różnych modułów odpowiadających np. za kompresję dźwięku lub kodowanie obrazu. Ratunkiem może być oferowana przez sprzedawcę pomoc techniczna, ale i ta nie zawsze jest dostępna. W krytycznych przypadkach, niewielkie firmy zajmujące się tworzeniem kodu IP mogą po prostu zniknąć z rynku lub zmienić obszar działalności i zaprzestać wsparcia dla sprzedanych dotąd produktów.

Na rynku dostępne są także darmowe biblioteki przeznaczone do współpracy z produktami konkretnego wytwórcy układów elektronicznych. Będą to np. zbiory funkcji takie jak IPP (Intel Performance Primitives) dostępne zarówno na procesory rodzin x86 i XScale. Bardziej wyspecjalizowane biblioteki znaleźć można w pakietach wspierających programowanie dołączanych do zestawów uruchomieniowych poszczególnych procesorów.

Problemem może być też dostępność implementacji najnowszych algorytmów. Te starsze zazwyczaj są bardziej zoptymalizowane i oferowane przez wiele konkurencyjnych firm, podczas gdy nowości szybko podlegają zmianom i nie są tak rozpowszechnione. Istnieją też bloki odpowiadające za systemy operacyjne produktów koncentrujących się na przetwarzaniu sygnałów, ale ich oferta znacząco się różni, w zależności od typu procesora, do którego jest dostosowana. Naturalnie, liczba tego typu aplikacji dostępnych na typowe procesory DSP będzie znacznie niższa niż tych, działających na uniwersalnych jednostkach obliczeniowych.

A może FPGA?

Gdy żaden z dostępnych na rynku procesorów specjalizowanych ani uniwersalnych nam nie odpowiada, dobrym pomysłem może się okazać stworzenie własnego układu, poprzez odpowiednie zaprogramowanie komórek FPGA. Dzięki możliwości elastycznej konfiguracji, jaką oferują układy programowalne, bazujące na nich aplikacje mogą znacznie przewyższać wydajnością nawet najszybsze procesory specjalizowane. Projektant może stworzyć dowolne potoki przetwarzania danych i wedle własnej woli implementować poszczególne funkcje filtrujące czy kodujące, tak aby pozbyć się „wąskich gardeł” na drodze sygnałów.

Oczywiście, gdyby stosowanie układów programowalnych miało same zalety, już dawno producenci wycofaliby się z wytwarzania jakichkolwiek innych układów. Produkty programowalne i reprogramowalne mają to do siebie, że czas potrzebny na stworzenie odpowiedniego, przeznaczonego nań oprogramowania jest znacznie dłuższy niż poświęcany na programy dla klasycznych DSP. Co więcej, języki opisu sprzętu nie posiadają cech zorientowanych na przetwarzanie DSP i tylko niewielka część inżynierów znających się na przetwarzaniu sygnałów, umie sprawnie posługiwać się VHDL-em czy Verilogiem. Niemniej, wiodący producenci układów programowalnych nie pozwoliliby stracić okazji na zyski, więc wprowadzili do swojej oferty narzędzia, znacznie ułatwiające programowanie układów przetwarzających sygnały.

Ponieważ, jak już wiadomo, jednym z wygodniejszych sposobów opisu algorytmu jest zapisanie go w języku Matlab lub za pomocą blokowego Simulinka – w pierwszej kolejności zaczęły powstawać narzędzia bazujące na środowisku firmy MathWorks. Pojawiły się specjalne bloki w postaci bibliotek Simulinka, zawierające typowe dla DSP funkcje, takie jak transformaty i filtry. Korzystając z biblioteki sygnałowej, projekt może być przetworzony na kod VHDL za pomocą pakietu DSP Builder Altery lub poprzez System Generator Xilinxa. Inni producenci nie wprowadzili swoich własnych konwerterów, ale wspierają niezależne oprogramowanie Synplify DSP stworzone przez firmę Synplicity. Entuzjastom pisania w czystym Matlabie pozostało skorzystać z narzędzi firmy AccelChip (wykupionej na początku 2006 roku przez Xilinxa), które to pozwalają na bezpośrednią konwersję do syntezowalnego kodu RTL lub do postaci bloków Simulinka.

Pomimo swej dużej popularności, Matlab to nie jedyny język konwertowany do syntezowalnego kodu FPGA. Istnieją pakiety, które posługują się własnym językiem – np. GoWare SPW lub przetwarzają kod języka C bezpośrednio na kod programujący konkretne modele układów FPGA. Jednym z takich rozwiązań jest sprzedawany wraz z płytką uruchomieniową program firmy Celoxica. Warto zaznaczyć, że możliwość zaprogramowania kodu FPGA-DSP w języku C może być bardzo korzystna, szczególnie, że w zasobach internetowych można znaleźć gotowe realizacje niektórych algorytmów kodowania, właśnie w postaci darmowych, referencyjnych programów w C.

Wygoda w implementacji algorytmów przetwarzających sygnały w układach programowalnych niestety nie idzie w parze z ich optymalnością. Przykładem jest choćby niska wydajność układów FPGA w przetwarzaniu liczb zmiennoprzecinkowych. W związku z tym, do narzędzi wprowadzono możliwość wyboru precyzji konwersji na liczby stałoprzecinkowe. Środowiska Synplify DSP i AccelChip DSP wprowadzają nawet możliwość wyboru precyzji w niektórych punktach toru przetwarzania sygnału, a w pozostałych dokładność dobierają automatycznie – przy czym ten drugi system bazuje na przeliczeniach podanych przez użytkownika wektorów testowych.

Stosując konwertery algorytmów DSP na kod FPGA bardzo trudno ustalić poziom zrównoleglenia wykonywanych operacji. Projektant zmuszony polegać na automatycznych narzędziach, które wybiorą za niego szczegóły fizycznej implementacji nie może precyzyjnie dobrać architektury połączeń wewnątrz układu. Jedną z wyższych skuteczności wyróżnia się Synplify DSP. Potrafi automatycznie wstawiać w program multipleksery i optymalizować algorytmy, a nawet generować wydajne wielokanałowe ścieżki danych, na podstawie jednokanałowych projektów. Poszczególne kanały dzielą pomiędzy sobą wszystkie dostępne zasoby, a ich całkowita wydajność jest dostosowywana do parametrów podanych przez projektanta.

Procesory w FPGA

Na rynku dostępnych jest bardzo wiele całych pakietów jak i pojedynczych funkcji, możliwych do zaprogramowania do wnętrza układów FPGA. Ich listę obejrzeć można na stronach największych producentów układów programowalnych – Altery, Xilinxa czy też Lattice. Wśród dostawców technologii znaleźć można dziesiątki firm, często oferujących także zestawy uruchomieniowe, wspomagające testowanie całych projektów DSP.

Ostatnią z możliwości zwiększenia wydajności aplikacji przetwarzających sygnały jest ich implementacja w jednym z dostępnych na rynku układów FPGA, zawierających zintegrowane jądra specjalizowanych układów DSP lub jądra klasycznych procesorów ogólnego przeznaczenia. Obecnie dostępne są na rynku produkty takie jak Virtex4 marki Xilinx, który zawiera w sobie nawet dwa zintegrowane jądra procesorów PowerPC. Dzięki temu można w wygodny sposób połączyć układ przetwarzający sygnały z systemem sterującym całością urządzenia. W razie konieczności wyboru układu programowalnego niezawierającego wewnątrz żadnego dodatkowego procesora, na rynku można znaleźć bloki IP zawierające proste jądra procesorów w postaci kodu VHDL, możliwego do wprogramowania w FPGA.

Jak się więc okazuje, ilość potencjalnych możliwości na realizację aplikacji przetwarzających sygnały, jest w obecnych czasach ogromna i pozwala wybrać takie rozwiązanie, które najlepiej spełni wszystkie oczekiwania. Nie ma już konieczności stosowania klasycznych procesorów DSP.

Marcin Karbowniczek

Zobacz również