To forum używa plików cookies
To forum wykorzystuje pliki cookies do przechowywania informacji o Twoim logowaniu, jeśli jesteś zarejestrowany, oraz informacji o Twojej ostatniej wizycie, jeśli nie jesteś zalogowany. Pliki cookies to niewielkie pliki tekstowe zapisywane na Twoim komputerze; cookies ustawiane przez to forum mogą być używane wyłącznie na tej stronie i nie stanowią zagrożenia dla bezpieczeństwa. Cookies na tym forum śledzą również, które tematy zostały przez Ciebie przeczytane oraz kiedy miało to miejsce. Prosimy o potwierdzenie, czy akceptujesz, czy odrzucasz zapisywanie tych plików cookies.

Niezależnie od wyboru w Twojej przeglądarce zostanie zapisany plik cookie, aby zapobiec ponownemu zadawaniu tego pytania. W każdej chwili będziesz mógł zmienić ustawienia cookies, korzystając z linku w stopce strony.

Ocena wątku:
  • 0 głosów - średnia: 0
  • 1
  • 2
  • 3
  • 4
  • 5
WALET – czyli jak buduję sobie transceiver SDR
#31
Kontynuując opis hardware zatrzymam się na chwilę przy portach procesora.

[Obrazek: http://lx-net.pl/walet/image/proc01.jpg]
I00, Txd, Rxd oraz Rst służą głównie do połączenia z PC podczas programowania.
Cs, Dcs, Mosi, Miso, Clk, Tcs służą do sterowania ekranem dotykowym FFT za pomocą protokołu SPI
Linp, Rinp, Lout, Rout to omawiane wcześniej wejście przetwornika A/D oraz wyjście D/A kodera
Rx/Tx to port typu 0/1 nadawanie/odbiór
Cwk to port 0/1 kluczowanie cw
Cwt to port generatora tonu 800Hz podczas pracy cw
Sda oraz Scl to linie magistrali I2C służące do komunikacji z peryferiami

Dodatkowo, pokazane styki Key, Dot/Dash oraz Ptt służą do sterowania urządzeniem w taki sposób, że w trybie SSB styk Ptt zwarty do masy przełącza urządzenie w tryb nadawania a w trybie cw Key to kluczowanie ręczne a Dot/Dash to styki klucza elektronicznego. Te elementy, choć wymagające drobnej korekty są w zasadzie oprogramowane i działają.

Widoczny przełącznik SW9 oraz leżące z lewej strony przełączniki SW1-7 mają swoje miejsce na płytce drukowanej h0 ale na razie nie mają, mimo opisów, przypisanej żadnej funkcji i mogą być wykorzystane do innych, nie wymienionych wcześniej funkcji. Wymaga to jednak dość precyzyjnego dobierania skojarzonych oporników bo wszystkie wymienione funkcje działają na linii I04, która ustawiona jest jako port analogowy z odczytem napięcia w zakresie 0-3.3V.

[Obrazek: http://lx-net.pl/walet/image/pczol02.jpg]
Ostatecznie, w mojej wersji urządzenia użyję prawdopodobnie trzech przełączników jak to pokazuje zdjęcie.
Ekran dotykowy ma w zasadzie nieograniczone możliwości konfiguracyjne i na przykład przycisk Mnu1 miał początkowo służyć jako przełączenie do kolejnego menu ale chyba nie potrzebuję innych funkcji w tak prostym urządzeniu. 
Chyba zamienię obecną funkcjonalność Mnu1 (uruchamiającą teraz Setup) na Rit a do setup-u będzie można wejść na ekranie inicjującym za pomocą jednego z pokazanych klawiszy. 
Pokazane klawisze F1-F3 zostaną prawdopodobnie użyte do dublowania funkcji, na przykład do zmiany kroku sterowania lub pasma pracy w górę i w dół zamiast klawisza dotykowego.
Użyte przeze mnie klawisze są typu tact i mają wymiary 6x6mm oraz długość trzpienia co najmniej 18mm - jeśli zastosowane zostanie zaproponowane przeze mnie rozwiązanie mechaniczne.

Początkowo też zamierzałem wyprowadzić gniazda na panel tylny ale rezygnując z czterech przycisków na płytce h0 uzyskałem miejsce na 3 gniazda (słuchawki/głośnik, mikrofon z ptt oraz klucz (zwykły i elektroniczny). 
Na zdjęciu widać przednią płytę z nową maskownicą. 
Taką maskownicę można zrobić w podstawowym punkcie poligraficznym z drukarką kolorową i laminarka termiczną. Trzeba zrobić wydruk na papierze samoprzylepnym a następnie go zafoliować.
Dla przypomnienia, na Github można znaleźć plik .pdf z rysunkiem takiej płyty czołowej. Koszt wykonania zalaminowanej strony w najbliższym punkcie u mnie  to 6PLN. Nie jest to równie trwałe rozwiązanie jak wydruk na folii ale jest w miarę estetyczne i pozwala na szybkie zmiany prototypowe zanim powstanie rozwiązanie docelowe.
Otwór wyświetlacza trzeba wyciąć nożem do tapet a inne otwory przebijakami o odpowiednim rozmiarze.

Wspominałem wcześniej o układzie formowania sygnałów heterodyny mieszacza Tayloe (F0 i F1).

[Obrazek: http://lx-net.pl/walet/image/form.jpg]
Układ ten sterowany jest (Clk) z wyjścia F0 si5351 z podwójną częstotliwością pracy urządzenia. W obwodzie działają układy 7474 oraz 7486. Układ ten rozwiązuje problem sterowania wprost z si5351 z przesunięciem fazy między sygnałami co wymaga restartu pętli PLL a to z kolei generuje stuki podczas strojenia.
Niestety, pokazany układ formowania też nie jest idealny.

[Obrazek: http://lx-net.pl/walet/image/form01.jpg]  [Obrazek: http://lx-net.pl/walet/image/form02.jpg]
Pierwsze zdjęcie pokazuje jak wyglądają sygnały przy pracy na 3.5MHz. Widać dwa ładne sygnały przesunięte w fazie o 90 stopni.
Drugie zdjęcie pokazuje "dziurę" w pracy układu formowania - znika F0 na jakiś czas od ok. 22.5MHz do ok. 23.5MHz, Na szczęście zarówno dla niższych jak i wyższych częstotliwości formowanie działa prawidłowo. Niestety, nie zgłębiałem tego problemu ze względu na ważniejsze tematy, przypuszczam, że wynika to z  tzw. "hazardu" i/lub "wyścigów" w układach cyfrowych. Z grubsza oznacza to problemy jeśli sygnały sterujące mają różne drogi (opóźnienia) na dotarcie np. do przerzutnika i docierają w niewłaściwej kolejności lub działają na układach o różnej prędkości.
Dlatego istotne jest aby zastosowane układy 7474 i 7486 powinny działać z możliwie dużą prędkością. Naturalnym wyborem są układy 74AC74 oraz 74AC86 ale jak widać i to nie wystarcza. 
Poza pokazaną dziurą w pracy układu formowania, im wyższa częstotliwość pracy, tym bardziej objawia się skończony czas narastania zboczy impulsów oraz ograniczenia propagacji co objawia się delikatnym stukotem w głośniku bez podłączonej anteny. Z sygnałem użytkowym przeszkoda ta zanika w szumie lub jest lekko zauważalna na najwyższych pasmach. 
Istnieje szybsza wersja układów tych układów, a nawet posiadam takie układy (LVC/ALVC) ale niestety, podczas wlutowania układu 7474 pomyliłem jego miejsce i wlutowałem go zamiast 7486. Po włączeniu zasilania usmażyłem ten układ więc na razie poprzestałem na wersji AC. Wrócę do tematu jak będę miał więcej czasu.

Uwaga:
Dokonałem drobnych zmian w oprogramowaniu a nowe wersje zamieściłem w obu formach na Github tak, że nie zmieniła się numeracja linii w kodzie Arduino.
Większość zmian to kosmetyka (np. wydłużenie czasu nieaktywności klawisza Save w procedurze Setup lub skrócenie funkcji intro po włączeniu urządzenia ale jedna ze zmian jest nieco ważniejsza bo dotyczy doboru w procedurze Setup korekty dla generatora si5351. 
Okazało się, że wprawdzie korekta działa ale nie można jej sprawdzić w podany wcześniej sposób na gnieździe RF płytki ha bo sygnał ten może się pojawić dopiero jeśli układ działa w trybie nadajnika. Obecnie, po wejściu do Setup i wykonywaniu zmian korekty można na bieżąco śledzić wpływ zmian mierząc częstotliwość sygnału referencyjnego 10MHz na  gnieździe RF. 
Należy jednak wcześniej włączyć urządzenie na kilka minut aby si5351 osiągnął równowagę termiczną. U mnie wartość korekty wynosi ok. 26700 (czyli ok. 267Hz). Trzeba też pamiętać, że poziom tego sygnału na gnieździe RF zależy od ustawienia rezystora nastawnego RV1 (Cw_level) na płytce ha.
We wcześniejszej,  nieskorygowanej wersji oprogramowania można ten sygnał mierzyć wprost na wyjściu F2 układu si5351 lub na stykach 5/8 przekaźnika K1 na płytce ha. Ponieważ zmiany nie są wielkie to nie modyfikowałem wersji oprogramowania.

Zgłosiłem Walet-a do tegorocznego konkursu PUK i czekam na potwierdzenie zgłoszenia. 
Prawdopodobnie pojawię się w Burzeninie w sobotę a może nawet przywiozę płytki urządzenia bo mam kolejne zapytania na ten temat.

cdn. lj

Github
Cytuj
#32
Na zakończenie omawiania części technicznej pokażę efekt regulacji i kształtowania tonu cw.
[Obrazek: https://lx-net.pl/walet/image/cwton01.jpg] [Obrazek: https://lx-net.pl/walet/image/cwton02.jpg]
Na lewym zdjęciu jest ciąg kropek przy minimalnym formowaniu obu zboczy a na prawym przy zaokrągleniu zarówno narastającego jak i opadającego zbocza.

Efekt można odsłuchać tu:
Cw ton z minimalnym formowaniem
Cw ton po formowaniu obu zboczy
Wyraźnie widać i słychać, że w pierwszym przypadku słychać nawet lekki stukot a ton po regulacji jest "miękki".

Na tym zakończę na razie opis hardware urządzenia a w kolejnych opisach większy nacisk zostanie położony na dane graficzne oraz oprogramowanie.

cdn. lj

Github
Cytuj
#33
Ekran dotykowy TFT został ogólnie opisany wcześniej. Aby go używać należy zainstalować bibliotekę TFT dla procesorów esp co opisałem wcześniej a jednym z ważniejszych  elementów tej instalacji jest konfiguracja pliku Arduino/libraries/TFT_eSPI/User_Setup.h (oczywiście uwzględniając lokalny format ścieżek oraz miejsce instalacji Arduino). 
Do celów pracy w tym projekcie można skorzystać z mojego pliku User_Setup.h zamieszczonego na Github

Przy pierwszym uruchomieniu programu Waleta z wyświetlaczem pojawi się żądanie kalibracji ekranu dotykowego
[Obrazek: https://lx-net.pl/walet/image/kalibr_dotyk.jpg]
Kalibracja wymaga zaznaczenia wszystkich, czterech rogów wyświetlacza.

Mając działający wyświetlacz można korzystać z jego definicji oraz API (czyli dostępnych poleceń linii komend) zdefiniowanych w pliku: Arduino/libraries/TFT_eSPI/TFT_eSPI.h
Kod Waleta korzysta z takich funkcji jak: tft.init,  tft.fillScreen, tft.fillCircle, tft.drawString, tft.drawRect oraz kilku innych i mam nadzieję, że nazwy tych funkcji tłumaczą co robią. 
Ale z punktu widzenia grafiki Waleta najbardziej istotne polecenie to tft.pushImage .
Ekran należy początkowo uruchomić i zorientować co ma miejsce w liniach 408 i 411 kodu, następnie można korzystać z wymienionych funkcji do woli.

Funkcja tft.pushImage(xs,ys,xw,yw,data) - to funkcja rysowania na ekranie gotowej grafiki w formie prostokąta. Jest ona zdefiniowana przez swoje parametry następująco:
xs - miejsce gdzie należy wkleić grafikę w osi X
ys - miejsce gdzie należy wkleić grafikę w osi Y
xw - szerokość grafiki w osi X
yw - wysokość grafiki w osi Y
data - wskaźnik do tablicy zawierającej dane związane z grafiką zapisane w formacie RGB565

[Obrazek: https://lx-net.pl/walet/image/ekran01.jpg]
W przyjętej orientacji ekranu (tft.setRotation(1);) każdy kolorowy pixel RGB ma współrzędne x w zakresie 0-319 oraz współrzędne y w zakresie 0-239.
Zakładając, że chcemy na przykład wkleić do ekranu grafikę  o nazwie image (zawsze w formacie prostokątnym) o rozmiarze 90x130 pixel-i  należy użyć polecenia: tft.pushImage(100,50,x90130,image) .
Grafika image musi być przedstawiona w formacie RGB565 8 bitów (nieco uproszczony format RGB) a można to zrobić korzystając z dostępnych w sieci konwerterów online (np. pliki .jpg, .png). Wystarczy takiemu konwerterowi wskazać plik w formacie .jpg i w rezultacie otrzymamy ciąg liczb heksadecymalnych ułożonych w tablicę pozwalającą sterownikowi wyświetlacza zapalenie pixeli w odpowiednim kolorze w odpowiednim miejscu.
Ja korzystam z tego konwertera
a w wyniku tego otrzymamy coś podobnego do tego:

static const uint16_t image[] PROGMEM  = {
  0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000,
  0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000,
  0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000,
  0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000,
  .
  .
  0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000,
  0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000
  };

Można to bezpośrednio wkleić do kodu programu ale wygodniej jest umieścić te dane w pliku. Ja pokazałem tu jedynie kilka linii kodu po konwersji a dla rzeczywistych plików graficznych może być ich setki lub tysiące. Dlatego w Walecie pliki grafiki wklejam do plików nazywanych właściwą nazwą - w tym przykładzie powinna to być nazwa image.h a następnie dołączam do programu poleceniem #include ścieżka_do_piku/image.h
Stąd w Arduino mam folder graph gdzie znajdują się wszystkie pliki graficzne używane w programie Waleta o czym będzie nieco dalej.

cdn. lj

Github
Cytuj
#34
Pokażę teraz jak wykonałem animowany miernik stanowiący większą część panelu dotykowego Waleta.
Do tego celu potrzebne jest narzędzie do wektorowej grafiki a w moim przypadku, w naturalny sposób (linux) jest to Inkscape (ma także wersję dla Windows) gdzie takie elementy jak na przykład tęczowy łuk daje się osiągnąć w kilka minut. Żałuję, że nie mam zmysłu graficznego więc to co udało mi się uzyskać nie dorasta do pracy zawodowego grafika.

[Obrazek: https://lx-net.pl/walet/image/gag_base_n.png]
Początkowo musi powstać baza graficzna w o rozmiarze 320x240 pikseli. Widoczne tu trzy skale dotyczą siły odbioru (S), dopasowania anteny (SWR) oraz mocy w antenie (P).
Założyłem, że skala będzie mieć 16 poziomów od umownego zera do umownej pełnego wychylenia a położenie wskazówki będzie definiowała odpowiednia część programu związana z pomiarem siły sygnału przy odbiorze i mocą lub SWR przy nadawaniu.

[Obrazek: https://lx-net.pl/walet/image/gag_02_n.png]

Dla każdego z tych poziomów należy wrysować wskazówkę na odpowiedniej pozycji. Program wektorowy ułatwia to znakomicie. Każde położenie wskazówki ma wirtualną oś obrotu w tym samym miejscu a w zależności od potrzeb, każdy element na tych rysunkach, jako obiekt wektorowy, można włączyć do obrazu lub go wyłączyć, obrócić etc.

[Obrazek: https://lx-net.pl/walet/image/g02_e.png]   [Obrazek: https://lx-net.pl/walet/image/g02_f.png]
Następnie należy wyciąć z obu widocznych rysunków dwa identyczne obszary i zapamiętać jako pliki graficzne  w taki sposób, że na jednym z nich wrysowana jest wskazówka. Oba te obszary mają ten sam rozmiar co do piksela a dodatkowo należy zapamiętać prawy górny adres pierwszego piksela na wycinanym obszarze bo obrazy te będą wklejane do obrazu podstawowego w opisany wyżej sposób za pomocą polecenia tft.pushImage.
Kolejną operacją jest, również opisana wcześniej, konwersja grafiki do postaci RGB w formie macierzy heksadecymalnej. 
Należy wykonać taką konwersję dla obrazka bazowego oraz po dwie dla obszarów odpowiadających kolejnym położeniom wskazówki od 0 do 15.
W ten sposób powstaną pliki pozwalające na animację położenia wskazówki a robi się to w taki sposób, że jeśli na przykład ostatnie położenie wskazówki było na pozycji 4 a należy ją przestawić na pozycję 6 to najpierw należy zamaskować położenie wskazówki na czwórce grafiką bez wskazówki i kolejno należy wgrać grafikę skojarzoną ze wskazówką na szóstce pamiętając oczywiście pod jakim adresem ekranu należy wkleić każdą z grafik.
Alternatywne rozwiązanie mogłoby polegać na ładowaniu całego rysunku skali ze wskazówką w odpowiednim miejscu ale to najgorsze z rozwiązań. 
Ilość danych do przesłania rośnie proporcjonalnie do pola powierzchni obrazów a więc kwadratowo z ich liniowymi rozmiarami. Pomimo połączenia wyświetlacza z procesorem za pomocą interface SPI, który działa z prędkościami znacznie powyżej 1MB/s, kwadratowa zależność złożoności grafiki szybko wyczerpuje zasoby procesora a w tym przypadku zależy nam na prędkości działania skali.  
Dla porównania, bazowy rysunek pełnej skali ma rozmiar w formacie .jpg 21.4kB ale po konwersji do formatu RGB urasta do 230.0kB!. Podobnie, dwa wycinki dla położenia 2 na skali mają w formacie .jpg rozmiary odpowiednio 2.9 i 3.3kB ale po konwersji urastają do 16.1kB. Wiąże się to z koniecznością podania dla każdego z pikseli współrzędnych dla każdego z trzech kolorów i nie ma tu żadnej kompresji danych.
W folderze Arduino/graph/ można podejrzeć każdą z użytych w programie grafik jako tablicę wartości heksadecymalnych.

Dla przykładu grafika ze wskazówką na pozycji dwa jak wyżej ma około 250 linii w edytorze tekstowym:

static const uint16_t g02f[] PROGMEM  = {
  0x0000, 0x4800, 0xcdd7, 0xad55, 0x0000, 0x0000, 0x0000, 0x0000,
  0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0841, 0x9492,
  .
  .
  0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000,
  0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000
  };

Jak kiedyś starczy czasu i nie będę miał co robić, to poprawię grafikę bo uważam, że wskazówka powinna być nieco grubsza ale wiąże się to ze żmudną manipulacją, wycinaniem, konwersją etc.

W obecnym stanie na dole ekranu Waleta znajduje się pole, które kiedyś mam nadzieję wyposażyć w funkcjonalność pseudo wodospadu i w zależności od wybranej metody będę stosował jak dla skali zestaw predefiniowanych obrazów albo cały obraz w tym obszarze będzie generowany w czasie rzeczywistym za pomocą prymitywnych funkcji takich jak jak rysowanie piksela, linii, prostokąta etc.

Inne elementy ekranu Waleta to mniej skomplikowane formaty graficzne, na przykład dioda LED w kolorze zielonym i czerwonym jest generowana za pomocą  polecenia okręgu z wypełnieniem o odpowiedniej barwie. Może wyjątkiem są klawisze z prawej strony, które zastosowałem jako gotowe podprogramy z linku prowadzącego do opisu instalacji bibliotek TFT.
cdn. lj

Github
Cytuj
#35
Pora na opis rdzenia oprogramowania Waleta a więc użycie narzędzi Arduino Audio Tools. Na początku opisów podałem link do repozytorium tego narzędzia i zainteresowanych kolegów zachęcam do studiowania tego ogromu wiedzy na temat przetwarzania sygnałów audio. Ja uszczknąłem z tego "tortu" ledwie okruszek ale i tak dało mi to możliwość rozwiązania podstawowych potrzeb programowych choć liczę, że doczytam się także brakujących mi jeszcze informacji. Podjąłem nawet dyskusję z autorem (Phil Shatzmann) na potrzebny mi temat ale różnica w wiedzy na temat omawianych spraw była tak duża, że chyba nie potrafiłem nawet nazwać dobrze swojego problemu więc i nie doczekałem się satysfakcjonującej odpowiedzi (na razie).

Niemniej, w liniach 106-115 (każdorazowo odwołuję się do pierwszej wersji programu dostępnego na Github) następują podstawowe definicje przepływu informacji w Audio Tools. Instrukcje zakomentowane oczywiście się "nie liczą" ale pokazują ścieżki po jakich chodziłem i na razie takich elementów nie usuwałem. Podobnie jak  nie usuwałem z kodu zakomentowanych alternatywnych rozwiązań, choćby na temat uzyskania przesunięcia fazy o 90 stopni wprost w si5351.
W liniach 345-353 znajdują się definicje łańcuchów filtrów cyfrowych jakich na starcie używa program a w komentarzach są alternatywne definicje używane dla innych trybów pracy.
Linie 355-376 definiują zarówno kodek jak i porty A/D i D/A oraz uruchamiają część odpowiedzialną za cyfrowe przetwarzanie audio.
Linie 752-777 to właściwa definicja przetwarzania audio, w szczególności polecenie copier.copy() to kwintesencja narzędzi Audio Tools i ono właśnie zawiaduje właściwym przepływem danych cyfrowych w porządku określonym zarówno w liniach 106-115 jak i w definicjach łańcuchów filtrów (345-353).
Jednocześnie linie 752-777 to oddzielne zadanie dla drugiego rdzenia procesora zajmujące się wyłącznie obsługą audio o czym napiszę dalej.

Czas więc na początek opisać szczegółowo jak prawidłowo zdefiniować filtry cyfrowe dla systemu Audio Tools. Aby je narzędzia przetwarzania zrozumiały muszą mieć właściwy format.

Krótko o filtrach cyfrowych. Zasadniczo wyróżnia się dwa rodzaje:
FIR (Finite Impulse Response): Posiadają skończoną odpowiedź impulsową, są zawsze stabilne i pozwalają uzyskać liniową charakterystykę fazową. Do obliczeń sygnału wyjściowego używa się zawsze ustabilizowanych danych "z przeszłości". Takim filtrem jest użyty w kodzie Waleta filtr Hilberta.
[Obrazek: http://lx-net.pl/walet/image/fir.jpg]
IIR (Infinite Impulse Response): Mają nieskończoną odpowiedź impulsową dzięki sprzężeniu zwrotnemu (rekurencji); są bardziej wydajne obliczeniowo, lecz mogą być niestabilne. W kodzie Waleta ten rodzaj filtrów służy do uzyskania charakterystyk typu LPF lub BPF. Filtry IIR korzystają za danych z "przeszłości" ale również z wartości obliczanych bieżąco stąd wynika potencjalna niestabilność.
[Obrazek: http://lx-net.pl/walet/image/iir.jpg]
W szczególności, w kodzie Waleta użyłem filtru typu BiQuad korzystający z niewielkiej ilości danych, i który jest obsługiwany prze Audio Tools
[Obrazek: http://lx-net.pl/walet/image/biq.jpg]
Do projektowania filtra Hilberta (typu FIR) używałem oprogramowania iowahills dostępnego w załączonym linku i choć oprogramowanie to nie jest rozwijane od lat to szukając właściwego rozwiązania znalazłem wiele pozytywnych jego recenzji.
Oprogramowanie Iowahills
Program działa tylko na Windows, nie wymaga instalacji a po rozpakowaniu należy uruchomić jedyny plik wykonywalny.
Istnieje na Github repozytorium pozwalające na skompilowanie programu we współczesnych warunkach ale ja nie przeszedłem tej drogi. Z pobieżnego oglądu wynika, że interface współczesny jest nieco zmodyfikowaną wersją dawnego rozwiązania ale trzeba nad nim samodzielnie popracować na początku.
[Obrazek: http://lx-net.pl/walet/image/iowa01.jpg]
Rysunek pokazuje w tle zawartość paczki oprogramowania po rozpakowaniu lokalnym oraz program HilbertFilters, którego działanie widać na pierwszym tle.
Projektując filtr należy przede wszystkim z menu PassBand wybrać Classic Hilbert a dodatkowo jego parametry a więc częstotliwość próbkowania (w moim przypadku 24kHz) ilość tzw. taps-ów (członów filtra) oraz dolną i górną granicę częstotliwościową. Ilość taps-ów powinna być nieparzysta.
Obliczenia uruchamia się klawiszem Calc a wynik otrzymuje się w formie grafiki wprost na ekranie oraz w pliku współczynników filtra przez wybranie File -> Save Coefficients.
[Obrazek: http://lx-net.pl/walet/image/iowa02.jpg]
Plik wyników zawiera tyle linii ile zostało zdefiniowanych taps-ów.
Tu pokazałem wynik dla filtra o 51 tap-sach oraz częstotliwości próbkowania 16kHz (w nazwie jest 51_16). Rysunek pokazuje środkową część pliku wyników gdzie wyraźnie widać symetrię współczynników filtra. Co drugi współczynnik jest zerowy a dodatkowo współczynniki z pierwszej połowy mają taką samą wartość ale z przeciwnym znakiem w stosunku do współczynników z drugiej połowy - względem elementu środkowego.

Tak uzyskany wynik należy doprowadzić do postaci akceptowalnej przez Audio Tools przy czym buduje się dzielony plik konfiguracyjny w taki sposób, że pierwsza połowa zawiera wiersze obliczonych właśnie współczynników a druga zawiera identyczną ilość elementów co pierwsza część z tym, że wszystkie elementy poza środkowym mają wartość 0 a element środkowy ma wartość 1.
To właśnie definiuje transformatę Hilberta, która działa na jeden z kanałów I lub Q budując dla transformowanego kanału wynik złożony z obliczeń na podstawie odpowiedniej ilości próbek a dla drugiego po prostu przenosi wartość próbki bez zmian. Ponieważ w nieformowanym kanale tylko środkowy element ma wartość 1 stąd wynika opóźnienie w procesowaniu sygnałów. Sygnał wyjściowy jest w najlepszym razie opóźniony o co najmniej połowę z ilości zdefiniowanych taps-ów.
W kodzie Waleta filtr hilberta hb_24_81.h , posiadający 81 taps-ów wygląda następująco:

float hilbert_24_81[] = {
0.000000000000000000,
0.001010454500352403,
0.000000000000000000,
0.001603255859532541,
....
-0.001603255859532541,
0.000000000000000000,
-0.001010454500352403,
0.000000000000000000,
};
float coeffs_delay_81[] = {
0.000000000000000000,
0.000000000000000000,
....
0.000000000000000000,
0.000000000000000000,

1.000000000000000000,
0.000000000000000000,
0.000000000000000000,
...
0.000000000000000000,
0.000000000000000000,
};

Każda z części filtra ma po 81 elementów, część aktywna (niebieska) zawiera obliczone wcześniej współczynniki a część pasywna (zielona)  zawiera zera poza linią środkową (41) w której figuruje wartość 1 (w formie zmiennoprzecinkowej). Zwracam uwagę, że budując strukturę definiującą filtr Hilberta dla Audio Tools należy zastosować wymaganą gramatykę a więc również przecinki na końcu każdej linii oraz znaki nawiasów { } oraz końcowe średniki.

Do budowy filtrów LPF oraz BPF można wykorzystać struktury FIR ale znacznie wydajniejsze są tu filtry IIF a w szczególności filtry typu BiQuad

[Obrazek: http://lx-net.pl/walet/image/biquad.jpg]
Ja używam kalkulatora online dla filtrów LPF i BPF typu BiQuad z tego linku
W wyniku obliczeń uzyskuje się dwie trójki parametrów a pierwszy parametr w trójce parametrów b ma zawsze wartość 1 co zapewnia stabilność takiego filtra. Filtry BiQuad mają dodatnie sprzężenie zwrotne i dobranie nieodpowiednich współczynników prowadzi do wzbudzenia filtra (odpowiedź nieskończona).

Obliczone filtry LPF oraz BPF mają po 6 współczynników i dlatego definiowane są wprost w kodzie programu (linie 95-102) oddzielnie dla każdej trójki wyliczonych współczynników a oraz b:

//-----------------------------define lp and bp filters-------------------------------
const float b_lpf24_36[] = { 0.13109923, 0.26219847, 0.13109923 }; //biquad lpf 24kHz sampling, 3600Hz f max
const float a_lpf24_36[] = { 1.0, -0.74774808, 0.27214502 };

const float b_lpf24_24[] = { 0.06745228, 0.13490457, 0.06745228 }; //biquad lpf 24kHz sampling, 2400Hz f max
const float a_lpf24_24[] = { 1.0, -1.14292982, 0.41273895 };

const float b_bpf24_8_6[] = { 0.01703090, 0.0, -0.01703090 };  //biquad bpf 24kHz sampling, 800Hz central freq
const float a_bpf24_8_6[] = { 1.0, -1.92297774, 0.96593821 };

Ostatecznie, należy dla potrzeb Audio Tools zdefiniować tzw łańcuch filtrów w postaci (linie 345-353)

//define filters chain
  //inFiltered.setFilter(0, new FIR<float>(hilbert_24_81));
  inFiltered.setFilter(0, new FilterChain<float, 2>({new BiQuadDF2<float>(b_lpf24_36,a_lpf24_36), new FIR<float>(hilbert_24_81)}));
  //inFiltered.setFilter(0, new FilterChain<float, 2>({new BiQuadDF2<float>(b_bpf24_8_6,a_bpf24_8_6), new FIR<float>(hilbert_24_81)}));

  //inFiltered.setFilter(1, new FIR<float>(hilbert_24_81));
  inFiltered.setFilter(1, new FilterChain<float, 2>({new BiQuadDF2<float>(b_lpf24_36,a_lpf24_36), new FIR<float>(coeffs_delay_81)}));
  //inFiltered.setFilter(1, new FilterChain<float, 2>({new BiQuadDF2<float>(b_lpf24_24,a_lpf24_20), new FIR<float>(coeffs_delay_81)}));
  //inFiltered.setFilter(1, new FilterChain<float, 2>({new BiQuadDF2<float>(b_bpf24_8_6,a_bpf24_8_6), new FIR<float>(coeffs_delay_81)}));

Zwracam uwagę (czerwone i purpurowe podkreślenia), że o ile nazwę dla pliku nagłówkowego .h dla danych filtra Hilberta można nadać dowolnie to przy definicji dla obu kanałów filtrowania (I oraz Q) muszą się znaleźć opisy obu części filtra tak jak je podano w pliku współczynników). Podobnie jest dla filtrów LPF i BPF ale one, że względu na małą ilość parametrów zdefiniowane są jako stałe i tej nazwy dla stałej należy używać.
Aktywne linie w powyższym opisie definiują filtrowanie dla kanałów I oraz Q więc różnią się jedynie częścią użytego filtra Hilberta bo filtrowanie LPF lub BPF są oczywiście dla obu kanałów identyczne.

Mam nadzieję, że podane opisy będą dla części kolegów inspiracją do eksperymentowania z własnymi rozwiązaniami. W szczególności dotyczy to filtra Hilberta. Cechą filtrów typu FIR jest to, że jakość filtracji wzrasta z ilością taps-ów ale wiąże się to z wydłużeniem procesu obliczeń z jednej strony ale także ze zwiększeniem opóźnienia w procesie obróbki sygnału o czym będzie także później. 
Jakkolwiek, im dłuższy (więcej taps) filtr Hilberta tym lepsze będzie tłumienie niepożądanej wstęgi ale nie można wydłużać filtra w nieskończoność bo w pewnym momencie przekroczone zostaną moce obliczeniowe procesora. 
Tu jednak jest możliwość eksperymentowania zarówno z długością filtra/filtrów jak i jego parametrami (np. okno dla filtra Hilberta) oraz z częstotliwością próbkowania. 
W tym ostatnim przypadku trzeba pamiętać, że dla konkretnej częstotliwości próbkowania można z danych wyłuskać jedynie informacje o sygnałach mających częstotliwość dwukrotnie mniejszą niż częstotliwość próbkowania (tzw twierdzenie o próbkowaniu Nyquista-Shannona) a więc kod Waleta działający z prędkością próbkowania 24kHz może ewentualnie zidentyfikować sygnały nie szybsze niż 12kHz. Ma to takie przełożenie na wyniki przetwarzania, że w ewentualnym "wodospadzie sygnałów" nie będzie takich, które są oddalonych o więcej niż 12 kHz od częstotliwości pracy.

cdn. lj

Github
Cytuj
#36
Większość procesorów esp32 ma 2 rdzenie (0 i 1) ale domyślnie programy dla Arduino działają na rdzeniu 1.
W przypadku programu obsługującego strumienie audio z wykorzystaniem protokołu I2S przetwarzanie danych odbywa się w czasie rzeczywistym z opóźnieniem czasowym związanym z próbkowaniem danych, ich filtracją oraz konwersją a a także manipulacjami.
Z tego względu korzystne jest przeniesienie całej obróbki audio na oddzielny rdzeń a nawet można powiedzieć, że próba uruchomienia wszystkiego na jednym rdzeniu na pewno zakończy się niepowodzeniem.

Dla bardziej dociekliwych wypisałem wcześniej adres w sieci skąd można otrzymać dodatkowe informacje na temat pracy zadań na oddzielnych rdzeniach. W kodzie Waleta są to następujące miejsca (z zachowaniem właściwej sekcji kodu Arduino):

linia 91 -> podstawowa komenta definiująca zadanie dla rdzenia 0
TaskHandle_t Task0;

linie 299-300 -> Szczegółowa definicja środowiska dla zadania w rdzeniu 0. Istotnym parametrem jest wielkości pamięci -> tu 100000 i z tą wartością można eksperymentować dobierając ją do zadania działającego na tym rdzeniu
//create a task that executes the Task0code() function, with priority 1 and executed on core 0
xTaskCreatePinnedToCore(Task0code, "Task0", 100000, NULL, 1, &Task0, 0);

linie 752-777 -> szczegółowa definicja poleceń działających w rdzeniu 0
//core 0 definition
void Task0code(void * pvParameters) {
  //Serial.print("Task0 running on core ");
  //Serial.println(xPortGetCoreID());
  for ( ; ; ) {
    //Serial.println("Core 0 processing");
    //delay((int)random(100, 1000));
    copier.copy();
    if (isFilterChanged == true){
      if (Filter_index == 0){
        inFiltered.setFilter(0, new FilterChain<float, 2>({new BiQuadDF2<float>(b_lpf24_36,a_lpf24_36), new FIR<float>(hilbert_24_81)}));
        inFiltered.setFilter(1, new FilterChain<float, 2>({new BiQuadDF2<float>(b_lpf24_36,a_lpf24_36), new FIR<float>(coeffs_delay_81)}));
      }
      else if (Filter_index == 1){
        inFiltered.setFilter(0, new FilterChain<float, 2>({new BiQuadDF2<float>(b_lpf24_24,a_lpf24_24), new FIR<float>(hilbert_24_81)}));
        inFiltered.setFilter(1, new FilterChain<float, 2>({new BiQuadDF2<float>(b_lpf24_24,a_lpf24_24), new FIR<float>(coeffs_delay_81)}));
      }
      else if (Filter_index == 2){
        inFiltered.setFilter(0, new FilterChain<float, 2>({new BiQuadDF2<float>(b_bpf24_8_6,a_bpf24_8_6), new FIR<float>(hilbert_24_81)}));
        inFiltered.setFilter(1, new FilterChain<float, 2>({new BiQuadDF2<float>(b_bpf24_8_6,a_bpf24_8_6), new FIR<float>(coeffs_delay_81)}));
      }
      isFilterChanged = false;
    }
    delay(1);
  }
}



Zadanie dla rdzenia 0 definiuje się jako pętla for (linia 756).W pętli działają w zasadzie trzy polecenia/funkcje:
copier.copy();
-> linia 759, wymienione wcześniej polecenie Audio Tools odpowiedzialne za organizację i przepływ danych audio
if (isFilterChanged == true) -> linia 760, polecenie spradzenia czy podczas odbiory zażądano zmiany filtra LPF lub BPF
delay(1); -> linia 775 czyli opóźnienie 1ms. Okazuje się, że bez tego polecenie całość nie działa. Opóźnienie rzędu 1ms nie ma wpływu na działanie copier.copy() ale mam zamiar przetestować inne, szczególnie mniejsze wartości opóźnienia.

Ze względu na wyjazd na kilka dni (będę dopiero po 20 sierpnia) starałem się zakończyć opis jeszcze przed wyjazdem aby umożliwić zrozumienie zarówno budowy jak i funcjonowania kodu konstrukcji Waleta.
Mam świadomość, że zadanie, choć mocno zaawansowane, ciągle jest w toku więc proszę o tym pamiętać podejmując np. krytykę pewnych rozwiązań. Na konstruktywną krytykę z resztą liczę, tym bardziej, że projest ma charakter otwarty, mocno eksperymatalny i jak wspominałem, do zrobienia jest co najmniej arw, odczyty swr i mocy, ewentualny "wodospad" a także kosmetyka całości kodu a wszczególności usunięcie niepożądanego zjawiska przenoszenia sygnału audio podczas przechodzenia z nadawania na odbiór. Ponieważ cyfrowy tor audio pracuje bez przerwy, niezależnie czy to odbiór czy nadawanie, a w systemie występuje opóźnienie związane z cyfrową obróbką sygnału to w momencie kiedy układ przechodzi elektrycznie (przekaźniki) z nadawania na odbiór podczas pracy ssb ma w buforze ostatnie kilkadziesiąt ms transmisji z nadawania i odbiornik najpierw odtwarza tą resztkę a następnie przechodzi do zwykłego odbioru korespondenta.
Mam co najmniej dwa pomysły jak to zlikwidować. Po pierwsze przez manipulację w kodzie programu narzędziami Audio Tools a po drugie w sprzęcie po prostu blokując wzmacniacz odbiornika na czas działania w torze opisanej resztki sygnału nadawania.

Opóźnienie cyfrowe przetwarzania sygnałów jest bardzo widoczne podczas odsłuchu własnego sygnału na zewnętrznym odbiorniku. W tradycyjnych transceiverach słyszy się własny głos w słuchawce odbiornika równocześnie z wypowiadanymi słowami. W urządzeniach z cyfrową obróbką może być słyszalne w odbiorniku opóżnienie własnego głosu co jest nieco irytujące dla mnie i zwykle łapię się na tym, że odruchowo spowalniam wymowę jakbym chciał doprowadzić do synchronizacji obo sygnałów co jest oczywiście niemożliwe ;-). Oczywiście z punktu widzenia korespondenta takie efekty nie mają znaczenia i nie będą widoczne a przy pracy cw nie występują.

Jak pisałem, podczas nieobecności będę mniej dostępny ale każdego wieczora postaram się tu zajrzeć więc jeśli pojawią się pytania lub jakaś dyskusja to włączę się w miarę możliwości. A jak zdążę to jeszcze dziś dopiszę w jaki sposób uzyskać w si5351 przesunięcie w fazie o 90 stopni dla sygnałów F0 i F1.

cdn.  lj

Github
Cytuj
#37
Smile 
(Wczoraj, 13:48)SP6FRE napisał(a): Z tego względu korzystne jest przeniesienie całej obróbki audio na oddzielny rdzeń a nawet można powiedzieć, że próba uruchomienia wszystkiego na jednym rdzeniu na pewno zakończy się niepowodzeniem.

Dobrze, że ekipa od UHSDR o tym nie wiedziała ... 
Ale na poważnie, wiele rdzeni tez może przyprawić o ból głowy bo pomijając, że jakoś muszę się ze sobą komunikować to jeszcze konkurują o zasoby. Tak czy owak, to poważne wyzwanie zwłaszcza, że narzędzia środowiska są nader skromne.
Praktykujący teoretyk
Cytuj
#38
Piotrze, binaria od Ciebie udało mi się bez problemu wgrać z pomocą przeglądarki Chrome i zajęło to minutę a sam program działa bez problemu. 
Różnice w interpretacji kodu u Ciebie i u mnie (choćby format ścieżek) wynikają prawdopodobnie z tego, że mamy różne środowiska linux-owe. U mnie działa Fedora (RedHat) u Ciebie Debian ale w końcu wszystko działa jak trzeba.

Udało mi się opisać generowanie sygnałów przesuniętych w fazie w samym si5351. Niestety, niefortunnie w kodzie Waleta częstotliwość robocza oznakowana jest jako F1 co może być mylące z wyjściami si5351 opisanymi jako F0, F1 i F2. Starałem się tam gdzie to może budzić wątpliwości wyraźnie te zmienne opisać dodatkowo.
Ślady po tej metodzie znaleźć można w liniach 1654-1707 choć linie odpowiedzialne za przesunięcie fazy w si5351 zostały zakomentowane. Podany niżej kod może jednak stanowić podstawę do własnej analizy tego problemu.

Dokumentacja si5351 informuje, że można uzyskać przesunięcie fazy za pomocą rejestru fazy przypisanego do każdego wyjścia układu (rejestry 165 do 167 dla układu z trzema wyjściami). Dokładniej, 7 bitowy rejestr fazy (wartości dopuszczalne to 0-127) pozwala na opóźnienie sygnału wyjściowego w stosunku do sterowania bez opóźnienia o wartość tego rejestru pomnożoną przez 1/4 okresu pętli PLL. Brzmi to trochę zawile ale najlepiej to zrobić na przykładach.

Zalecana częstotliwość pętli PLL dla si5351 to 600 do 800MHz. W tym wypadku interesuje nas ta dolna granica bo ona wyznacza minimalną częstotliwość dla jakiej można operację przesunięcia fazy o 90 stopni dokonać.
Częstotliwość 600MHz to okres ok. 0,000000001667 sekundy.  
Każdej jednostce w rejestrze fazy (0-127) można przypisać 1/4 tego okresu a więc ok. 0,000000000417 sekundy.
Maksymalna zawartość rejestru przesunięcia fazy (wymagana wartość parzysta) to 126 tak więc z tą wartością uda się opóźnić start sygnału o 0,000000000417*126 = 0,0000000525 sekundy.
Jednocześnie przesunięcie w fazie o 90 stopni sygnału wyjściowego F1-F3 to jedna czwarta okresu tego sygnału tak więc z wartością 126 w rejestrze fazy oraz częstotliwością PLL 600MHz można uzyskać pożądany efekt dla częstotliwości nie mniejszej niż około 1/(0,0000000525 *4) = 4761904,76Hz (4.761904MHz)
To oznacza, że bez modyfikacji ustawień dolnej częstotliwości PLL nie uda się uzyskać pasma 3.5MHz.

W praktyce daje się obniżyć dolną częstotliwość PLL do 400MHz a nawet niżej ale 400MHz udaje się uzyskać u niemal wszystkich egzemplarzy układów bez zrywania drgań. Jak w bibliotece Etherkit obniżyć tą dolną granicę pisałem omawiając generator si5351.
Policzmy jaką najniższą częstotliwość z przesunięciem fazy o 90 stopni uzyska się dla częstotliwości PLL równej 400MHz.
Licząc podobnie, okres dla 400MHz to 0,0000000025 sekundy a czwarta część tego czasu to 0,000000000625 sek. Dla maksymalnej wartości w rejestrze fazy (126) uzyskamy przesunięcie czasowe 0,00000007875 sekundy. To oczywiście odpowiada jednej czwartej okresu jednego z sygnałów wyjściowych F1-F3 a więc najmniejsza częstotliwość wyniesie 1/(4*0,00000007875) = 3174603,17Hz a więc ok. 3.1746MHz.

Patrząc na obliczenia widać, ze zarówno w obliczeniach dla pętli PLL jak i w obliczeniu częstotliwości wyjściowej występuje "magiczna" czwórka, która może być uproszczona.
Algorytm obliczenia przesunięcia fazy na częstotliwości roboczej F z wykorzystaniem narzędzi Etherkit może więc wyglądać następująco:

1. Podziel najniższą częstotliwość PLL przez częstotliwość roboczą F i zaokrąglij wynik w górę do najbliższej liczby parzystej N, która powinna mieścić się w granicach 2-126.
2. Oblicz częstotliwość PLL mnożąc N przez częstotliwość roboczą F czyli oblicz PLL = N * F
3. Dla tak znalezionej wartości PLL wygeneruj na wyjściach F0 i F1 si5351 żądaną częstotliwość roboczą F
4. Ustaw rejestr przesunięcia fazy dla wyjścia F0 lub F1 na obliczoną wartość N
5. Zresetuj pętlę PLL
W takich warunkach zmiana wstęgi będzie się odbywać przez wykonanie przesunięcia w fazie dla częstotliwości F0 lub F1 si5351.

Odpowiedni kod może wyglądać następująco zakładając, że niskie pll używane będzie poniżej 6.8MHz:

// jeśli częstotliwość robocza jest mniejsza niż 6.8M to pll = 400000 a jeśli wyższa to pll = 700000
long long pll_lim;
  if (F1 < 680000000ULL) {   //Tu F1 jest częstotliwością roboczą urządzenia
    pll_lim = 40000000000ULL;
  } else {
    pll_lim = 70000000000ULL;
  }
//obliczenie współczynnika podziału pll_div
  pll_div = int(pll_lim / F1);
  if (pll_div & 1 == 1) {    //jeśli podzielnik nieparzysty zwiększ go o 1
    pll_div++;
  }
//sprawdzenie czy obliczony podzielnik daje minimalną wartość przyjętą dla pll oraz ustalenie, że pll_div będzie parzyste a w końcu wyliczenie ostatecznej wartości pll_tmp = pll_div * częstotliwość pracy (w kodzie Waleta to F1)
  pll_tmp = pll_div * F1;
  while (pll_tmp < pll_lim) {
    pll_div++;
    if (pll_div & 1 == 1) {
      pll_div++;
    }
    pll_tmp = pll_div * F1;
  }
//ustawienie na obu wyjściach (F0 i F1) si5351 częstotliwości roboczej (w kodzie Waleta to F1)
  si5351->set_freq_manual(F1, pll_tmp, SI5351_CLK0);
  si5351->set_freq_manual(F1, pll_tmp, SI5351_CLK1);
//wybór przesunięcia w fazie dla F0 lub F1 w zależności od wybranej wstęgi pracy
  if (UpDn_state == true) {
    si5351->set_phase(SI5351_CLK0, 0);
    si5351->set_phase(SI5351_CLK1, pll_div);
    //si5351->pll_reset(SI5351_PLLB);
  } else {
    si5351->set_phase(SI5351_CLK0, pll_div);
    si5351->set_phase(SI5351_CLK1, 0);
    //si5351->pll_reset(SI5351_PLLA);
  }
//reset pętli PLL w celu wprowadzenia zmiam fazy w życie
  si5351->pll_reset(SI5351_PLLA);
 
Jak pisałem, początkowo stosowałem taki system generowania heterodyny dla mieszacza Tayloe ale konieczność resetowania pętli PLL objawiała się stukotem podczas strojenia dlatego obecnie używam układu formowania na układach 7474 oraz 7486. Planuję jednak dla przyszłej wersji płytki ha zastosować zwory w połączeniach tak aby częstotliwości F0 i F1 dla mieszacza mogły pochodzić zarówno z układu formowania lub wprost z si5351. Zajrzę do kodu resetowania pętli PLL w Etherkit bo może autor zastosował tam jakieś opóźnienia i da się działanie w tej formie poprawić.


O ile ktoś chciałby się zdecydować na wykonanie urządzenia w obudowie podobnej do mojej z wyprowadzeniem gniazd na przedni panel jak to pokazałem nieco wcześniej to należy wykonać w płytce h0 podpiłowania na spodzie płytek tak aby mogły przedostać się te przewody na drugą stronę tej płytki gdzie są złącza.

[Obrazek: http://lx-net.pl/walet/image/otwory01.jpg]  [Obrazek: http://lx-net.pl/walet/image/otwory02.jpg]
Widać to na pierwszym zdjęciu. Kolejne płytki: ha,hb i hc mają już z obu stron wykonane otwory jak to pokazałem na kolejnym zdjęciu, które po lekkim podpiłowaniu cieńszej krawędzi pozwalają na prowadzenie przewodów do tyłu urządzenia (o ile będzie to potrzebne). O ile wykonam kolejne płytki to będą one już posiadać odpowiednie otwory w płytce h0.

cdn. lj

Github
Cytuj


Skocz do:


Użytkownicy przeglądający ten wątek: 2 gości