Skocz do zawartości

Elvis

Użytkownicy
  • Zawartość

    2537
  • Rejestracja

  • Ostatnio

  • Wygrane dni

    187

Wszystko napisane przez Elvis

  1. Jak łatwo było przewidzieć zabrałem się za WS2812B stanowczo zbyt późno - miało być piękne oświetlenie choinki, a są standardowe lampki, czyli jak zwykle. Właściwie to w tym roku jest nawet gorzej niż w poprzednie Święta, bo wtedy zrobiłem sterowanie używając ESP32 i przez WiFi można było przesyłać dane do wyświetlenia. Niestety gdzieś mi się ten program zagubił, a nowego nie napisałem, więc żeby w przyszłym roku nie zaczynać przygody z WS2812 znowu od początku, dopiszę jeszcze trochę moich przemyśleń odnośnie tych diodek. Na początek starałem się pokazać, że Arduino UNO w zupełności wystarcza do sterowania oświetleniem choinki - przynajmniej do czasu, aż będziemy chcieli bardziej rozbudować program i dodać do niego więcej funkcji niż tylko sterowanie ws2812b. Teraz spróbuję użyć jednego z typowych modułów peryferyjnych, czyli UART-a do sterowania WS2812B. Przy okazji zmieniam mikrokontroler na stm32l476 - jest do tego kilka powodów. Po pierwsze znacznie lepiej znam stm32 niż atmegę, więc jak dla mnie jest łatwiej. Po drugie Arduino i atmega328p ma raptem 2KB pamięci RAM. To pewnie wystarczyłoby do sterowania lotem na księżyc, ale w dzisiejszych czasach programy są zasobożerne, a sama biblioteka Adafruit_Neopixel zużywa po 3 bajty na każdą diodę w łańcuchu - więc jak dodamy rozbudowane sterowanie i wymagania biblioteki, może zacząć brakować pamięci. Użycie UART-a też nie ułatwia oszczędzania pamięci, bo jeden bit jest reprezentowany za pomocą trzech, więc ilość danych rośnie, albo konieczne jest kolejne komplikowanie kodu. W każdym razie wybrałem STM32L476 bo mam go aktualnie "na tapecie", mikrokontroler posiada 128KB pamięci RAM, spory wybór peryferiów, DMA oraz możliwość inwersji wyjścia Tx z UART, co będzie za chwilę przydatne. Jednak sterowanie, które opisuję można równie dobrze zastosować do innych mikrokontrolerów, nawet do Arduino. O samym UART-cie nie będę za wiele pisał, był już omawiany przy okazji kursu STM32F1 (https://forbot.pl/blog/kurs-stm32-f1-hal-komunikacja-z-komputerem-uart-id22896) oraz STM32F4 (https://forbot.pl/blog/kurs-stm32-f4-7-komunikacja-przez-uart-id13472) Na początek szybkie utworzenie projektu w STM32CubeIDE, konfiguracja zegara oraz UART1. Zaczynam od standardowych ustawień czyli prędkość 115200, 8 bitów danych itd. Taka konfiguracja jest bardzo łatwa do przetestowania, można też upewnić się, że wszystkie niuanse odnośnie komunikacji przez uart działają tak jak myślimy. Ponieważ dane są przesyłane w kolejności od najniższego bitu, wyślę wartość 0x0f, czyli najpierw powinny być cztery bity o wartości "1", a następnie cztery "0". Analizator poprawnie odczytuje przesłane dane, na diagramie widać bit startu, cztery bity "1", a następnie zera. Mniej to widać, ale na końcu jest bit stopu. Wszystko działa, jak powinno, ale do sterowania WS2812B potrzebny jest taki przebieg: Czyli najpierw stan wysoki przez czas zależny od wartości bitu, a później stan niski. UART domyślnie działa zupełnie odwrotnie - start ma wartość "0", a stop "1". Można sobie z tym poradzić sprzętowo dodając negację wysyłanego sygnału, ale w przypadku stm32l476 wystarczy zmienić ustawienia modułu: Teraz wysyłane dane muszą być zanegowane, więc zamiast 0x0f wysyłam 0xf0. Efekt jest dużo bliższy oczekiwaniom ws2812: Trzeba jeszcze poprawić czasy, można też trochę zoptymalizować sterowanie. Wysyłając wartość 0xf0 uzyskałem sygnał o odpowiednim kształcie, ale "marnuję" cały bajt na wysłanie jednego bitu. Okazuje się, że w jednej ramce UART-a bez problemu zmieścimy aż trzy bity. Ponieważ zanegowaliśmy wyjście bit startu jest automatycznie dodawany i ma wartości "1". Następnie transmitujemy (również zanegowany) bit danych, a na koniec bit zerowy. Na końcu ramki jest bit stopu, który podobnie jak start jest dodawany sprzętowo. To oznacza, że potrzebujemy wysłać raptem 7 bitów - używając UART najczęściej przesyłamy 8-bitowe dane, ale bez problemu można zmienić ustawienia na 7 bitów. Zostaje jeszcze korekta czasu transmisji. Jak pamiętamy przesłanie całego bitu do ws2812b zajmuje 1,25 us, co odpowiada 800 b/s. Ponieważ UART musi wysyłać 3 bity na jeden odbierany przez ws2812b, więc i prędkość musimy zwiększyć trzykrotnie. Daje nam to wartość 2,4Mb/s, która często pojawia się w poradnikach internetowych. Teraz już tylko zmiana ustawień CubeMX Konfiguracja peryferiów jest już gotowa, teraz trzeba napisać program. UART wysyła 7-bitowe dane, które i tak będziemy przechowywać jako 8-bitowe bajty. Ponieważ wysyłamy 3 bity na każdą ramkę UART, więc możliwych jest 8 wartości. Jako dane wejściowe przyjmujemy wartość 0-7, którą możemy zapisać binarnie jako b2b1b0. Wynik to 8-bitowa liczba, ale najwyższy bit nie ma znaczenia (więc niech będzie zerem). Pamiętając o tym, że UART wysyła dane zaczynając od najniższego bitu możemy przygotować następującą tabelkę: Teraz możemy napisać funkcję, która zakoduje nam dane w sposób oczekiwany przez UART. Jako wejście dostajemy 24-bity czyli jasność dla składowej czerwonej, zielonej i błękitnej. W wyniku otrzymamy 8 bajtów, bo tyle wymaga nasz sposób kodowania. Program może wyglądać następująco: static const uint8_t ws_code[] = { ~0x24, ~0x64, ~0x2c, ~0x6c, ~0x25, ~0x65, ~0x2d, ~0x6d }; void ws_encode(uint8_t r, uint8_t g, uint8_t b, uint8_t *buf) { uint32_t value = (g << 16) | (r << 8) | b; for (int i = 0; i < 8; i++) { uint32_t tmp = (value >> 21) & 0x7; *buf++ = ws_code[tmp]; value <<= 3; } } To już właściwie wszystko - resztę robi za nas biblioteka HAL. Jedno co musimy zrobić to zadeklarować bufor, w którym będziemy mieli po 8 bajtów na każdą diodę ws2812b: #define PIXELS 1*144 static uint8_t ws2812b_buf[PIXELS * 8]; Następnie wypełniamy bufor używając wcześniej napisanej funkcji ws_encode (oczywiście warto ją dostosować i zoptymalizować w zależności od potrzeb). Na koniec musimy wysłać zawartość naszego bufora przez UART. Na początek możemy użyć: HAL_UART_Transmit(&huart1, ws2812b_buf, sizeof(ws2812b_buf), HAL_MAX_DELAY); Jednak ta metoda niewiele nam da w porównaniu z "machaniem pinami" używanym wcześniej. Okazuje się, że obsługa przerwań i tym razem może mieć wpływ na komunikację. W rzeczywistości ten kod działa, ale opóźnienia wywołane przerwaniami mogą zakłócić transmisję. Aby tego uniknąć należy wyłączać przerwania, to jak wiemy nic dobrego. Inną możliwością jest wykorzystanie DMA do przesyłania danych. Ponieważ używamy standardowego modułu UART, wystarczy że użyjemy: HAL_UART_Transmit_DMA(&huart1, ws2812b_buf, sizeof(ws2812b_buf)); Na zakończenie jeszcze uwaga odnośnie użycia pamięci - zaprezentowane podejście jest bardzo proste, ale też mocno "naiwne". Dla długich pasków zużywamy mnóstwo pamięci, a wcale nie jest to konieczne. Mając DMA można obsługiwać przerwanie po przesłaniu połowy danych i wypełniać wówczas bufor danymi. Dzięki temu zaoszczędzimy mnóstwo RAM-u. Ale ta i inne optymalizacje to temat na kolejny wpis. Tym razem chciałem tylko zaprezentować jak łatwe jest użycie UART-a do sterowania WS2812B i jak można przygotować dane w formacie oczekiwanym przez ten moduł. .
  2. Widzę, że pisząc cokolwiek w internecie trzeba niesamowicie uważać na słówka - faktycznie źle się wyraziłem, na pewno z Arduino i atmegii 328p da się jeszcze niejedno wycisnąć. Co do obsługi przerwań to nie byłbym pewien, czy to dobry kierunek - standardowe przerwanie od timera zajmuje ponad 6us, a transfer jednego bitu - 1,25us. Więc nawet mając 3 bity do przesłania (np. używając UART), zaczyna brakować czasu. Tak jak napisałem wcześniej - ws2812b są o wiele bardziej tolerancyjne na przerwy w komunikacji niż podaje specyfikacja, więc użycie przerwań i SPI, PWM lub UART pewnie zadziała, tyle że to nie będzie sterowanie zgodne z dokumentacją i to miałem na myśli pisząc o zmianie platformy. W tym co napisałem chciałem pokazać że za pomocą Arduino UNO można sterować nawet dość długim łańcuchem diod ws2812b. Nie jest to jedyne możliwe rozwiązanie i postaram się jeszcze opisać inne możliwości - dlatego wspomniałem o innych platformach, chociaż może niezbyt dobrze dobrałem słowa.
  3. Choinki jeszcze nie kupiłem, więc są ostatnie chwile na przygotowania Natomiast czy wrażenia artystyczne są takie ważne nie jestem pewien - w każdym razie oświetlenie choinki to dla mnie dobry pretekst do zajęcia się ws2812b, których trochę nazbierałem a jakoś ciągle brakowało czasu.
  4. Uwaga - od tej chwili kończy się część inżynierska, a zaczyna "czarna magia" Testując własny program oraz szukając informacji w internecie znalazłem dość ciekawy wpis: https://wp.josh.com/2014/05/13/ws2812-neopixels-are-not-so-finicky-once-you-get-to-know-them/ Autor przebadał działanie ws2812 i podzielił się dość interesującymi przemyśleniami. Z jego doświadczeń wyszło, że bardzo ważny jest czas stanu wysokiego podczas transmisji bitu 0, natomiast pozostałe czasy mogą być przekroczone bez szkody dla działania układu. Postanowiłem jego badania przetestować - i muszę powiedzieć, że nie wyszło mi tak optymistycznie, ale nadal ciekawie. Na początek dodajmy do naszego pierwszego programu opóźnienie przy wysyłaniu bitu "0": static inline void send_bit_zero(void) { PORTD |= _BV(6); _NOP(); delayMicroseconds(10); PORTD &= ~_BV(6); for (uint8_t i = 0; i < 4; i++) _NOP(); } Taka zmiana zupełnie psuje komunikację. Diody świecą z pełną jasnością, zupełnie nie o to chodziło. Ale już wstawienie opóźnienia podczas stanu niskiego działa poprawnie: static inline void send_bit_zero(void) { PORTD |= _BV(6); _NOP(); PORTD &= ~_BV(6); for (uint8_t i = 0; i < 4; i++) _NOP(); delayMicroseconds(10); } W przypadku transmisji bitu o wartości "1" można dodać opóźnienie w dowolnym momencie i nadal komunikacja będzie działała. Testowałem różne opóźnienia i do 30us wszystkie diody ws2812b jakie posiadam działały bez problemu. Wstawianie sztucznych opóźnień jest ciekawe, jednak niewiele w tym sensu. Można natomiast zdobyte doświadczenie wykorzystać inaczej - jak widzieliśmy wcześniej obsługa przerwania dla millis() zajmuje mniej niż 7us. Skoro nawet 30us opóźnienie nie psuje komunikacji, to może dałoby się włączyć przerwania od czasu do czasu? To właśnie zrobił autor bloga, do którego link wstawiałem wcześniej. Postanowiłem przetestować następujący program: #define LED_PIN 6 void setup() { DDRD |= _BV(6); } static inline void send_bit_zero(void) { cli(); // wylacz przerwania PORTD |= _BV(6); _NOP(); PORTD &= ~_BV(6); sei(); // wlacz przerwania for (uint8_t i = 0; i < 4; i++) _NOP(); } static inline void send_bit_one(void) { PORTD |= _BV(6); for (uint8_t i = 0; i < 8;i++) _NOP(); PORTD &= ~_BV(6); } static inline void send(uint8_t value) { for (uint8_t i = 0; i < 8; i++ ) { if (value & 0x80) send_bit_one(); else send_bit_zero(); value <<= 1; } } void loop() { for (int i = 0; i < 24; i++) { send(0x01); send(0); send(0); } delay(10); } Tym razem przerwania wyłączane na raptem 0.4us, używając analizatora możemy "zobaczyć" przerwy w komunikacji: Ale o dziwo diody działają bez najmniejszych problemów. Od razu zaznaczam - to nie jest poprawne, inżynierskie podejście. Jeśli planujecie produkcję seryjną urządzeń, albo pracujecie w sektorze safety-critical to proszę nawet tego nie czytajcie. Ale jeśli chcecie mieć tylko oświetlenie na choinkę i wszystko działa poprawnie, to jest to co najmniej ciekawostka Co więcej można nieco zmienić bibliotekę Adafruit_Neopixel. W pliku Adafruit_NeoPixel.cpp, linii 222 znajdziemy wywołanie noInterrupts(). Po usunięciu tej linii zobaczymy błędy w komunikacji z modułami ws2812b. Możemy jednak dodać wyłączanie przerwań tylko na czas stanu wysokiego (to ten fragment w asemblerze) - i będziemy mieli wówczas w pełni działający port szeregowy, millis() oraz przykłady dołączone do Adafruit_Neopixel. Tak jak napisałem wcześniej - takie sterowanie nie jest zgodne z dokumentacją producenta. "U mnie działa", ale to tylko ciekawostka. Natomiast jeśli chcemy mieć poprawne sterowanie ws2812b oraz możliwość obsługi przerwań musimy zmienić platformę sprzętową. Arduino UNO nie ma niestety odpowiednich modułów peryferyjnych. Jednak jak wielokrotnie wspominałem - do przygotowania lampek na choinkę na pewno wystarczy.
  5. Pisanie w języku C lub C++ programów, które mają działać z dokładnością do cykli procesora jest w sumie bez sensu, ale postanowiłem chociaż spróbować. Używając analizatora oraz dobierając mocno empirycznie wartości udało mi się przygotować taki kod na wysyłanie bitu zerowego: static inline void send_bit_zero(void) { PORTD |= _BV(6); _NOP(); PORTD &= ~_BV(6); for (uint8_t i = 0; i < 4; i++) _NOP(); } Najważniejsze jest małe opóźnienie między ustawieniem stanu wysokiego na wyjściu, a powrotem do stanu niskiego - to raptem jeden _NOP(). Aby wysłać bit o wartości "1" trzeba dodać większe opóźnienie: static inline void send_bit_one(void) { PORTD |= _BV(6); for (uint8_t i = 0; i < 8;i++) _NOP(); PORTD &= ~_BV(6); } Teraz już można napisać cały program. Jak wspominałem bity są wysyłane zaczynając od najbardziej znaczącego, kolejność kolorów to GRB #define LED_PIN 6 void setup() { DDRD |= _BV(6); } static inline void send_bit_zero(void) { PORTD |= _BV(6); _NOP(); PORTD &= ~_BV(6); for (uint8_t i = 0; i < 4; i++) _NOP(); } static inline void send_bit_one(void) { PORTD |= _BV(6); for (uint8_t i = 0; i < 8;i++) _NOP(); PORTD &= ~_BV(6); } static inline void send(uint8_t value) { for (uint8_t i = 0; i < 8; i++ ) { if (value & 0x80) send_bit_one(); else send_bit_zero(); value <<= 1; } } void loop() { cli(); for (int i = 0; i < 24; i++) { send(0x01); send(0); send(0); } sei(); delay(10); } Gdy podłączymy analizator zobaczymy, że efekt jest dużo gorszy niż biblioteka Adafruit_Neopixel, ale co ważne działa poprawnie - testowałem nawet z 4 x 144 diodami. Tutaj wynik dla 24 diod: Problemy pojawiają się po transmisji bajtu - powroty z funkcji, obsługa pętli itd zajmuje czas To niestety widać podczas transmisji: Jak widać napisanie dobrego programu do sterowania ws2812b nie jest łatwe - ten który przygotowałem działa, ale nie jestem z niego dumny. Chciałem natomiast przetestować moje rozumienie komunikacji z ws2812 i pokazać że to nie czarna magia. Po prostu trzeba dbać o dokładne czasy generowanych przebiegów. Co więcej sprzęt potrafi nam wybaczyć nawet więcej niż gwarantuje dokumentacja.
  6. Żeby lepiej zrozumieć sterowanie ws2812b postanowiłem spróbować napisać własną obsługę tych diod. Od razu chciałbym wyjaśnić, że nie mam aspiracji do napisania lepszej biblioteki niż Adafruit_Neopixel - kod tej biblioteki został wielokrotnie przetestowany, napisany w asemblerze i świetnie zoptymalizowany. Jest więc pod prawie każdym względem lepszy - tym czego mu brakuje to czytelność, ale nie można mieć o to chyba pretensji. Ja chciałbym tylko napisać prosty kod prototypowy, który pozwoli mi zrozumieć sterowanie ws2812b. Na początek samo sterowanie liniami GPIO - ws2812b wymaga raczej szybkiego sterowania, więc zanim zabiorę się do pracy trzeba upewnić się, że jest to wykonalne. Każdy, kto miał do czynienia z Arduino wie jak proste jest używanie funkcji digitalWrite() i sterowanie pinami. Zacznę więc od wręcz banalnego programu: #define LED_PIN 6 void setup() { pinMode(LED_PIN, OUTPUT); } void loop() { digitalWrite(LED_PIN, HIGH); digitalWrite(LED_PIN, LOW); } Podłaczam analizator i mogę zobaczyć co udało mi się uzyskać: Szerokość stanu wysokiego to 3,8 us, a do sterowania ws2812b konieczne są ~0,4us - więc digitalWrite() jest miłe i sympatyczne, ale zupełnie się nie nadaje. Na szczęście pod Arduino są dostępne też inne biblioteki. Spróbuję użyć FastGPIO (https://github.com/pololu/fastgpio-arduino) Nie znam tej biblioteki za bardzo, więc jeśli program jest niepoprawny to proszę śmiało pisać. W każdym razie udało mi się przygotować coś takiego: #include <FastGPIO.h> #define LED_PIN 6 void setup() { FastGPIO::Pin<LED_PIN>::setOutput(1); } void loop() { FastGPIO::Pin<LED_PIN>::setOutput(1); FastGPIO::Pin<LED_PIN>::setOutput(0); FastGPIO::Pin<LED_PIN>::setOutput(1); FastGPIO::Pin<LED_PIN>::setOutput(0); } Efekt działania programu wygląda następująco: Jak widać impuls ma teraz szerokość 0,252us - to 15 razy lepiej niż przy użyciu digitalWrite ! Poprzednio sterowanie wyjściem było tak powolne, że czas spędzony poza pętlą loop() nie miał praktycznie znaczenia. Używając szybszej biblioteki możemy jednak zobaczyć, że dwa impulsy są generowane z odstępem równym ich szerokości, ale kolejna przerwa jest trochę większa (ma 0,5us). Wywołanie funkcji loop() oraz pętli głównej trochę zajmuje, więc mierząc czasy trzeba o tym pamiętać. Na koniec spróbujmy zupełnie pominąć biblioteki Arduino i bezpośrednio odwołać się do rejestrów mikrokontrolera #define LED_PIN 6 void setup() { DDRD |= _BV(6); } void loop() { PORTD |= _BV(6); PORTD &= ~_BV(6); PORTD |= _BV(6); PORTD &= ~_BV(6); } Teraz wyniki są już prawie takie jak powinny: Arduino UNO jest taktowane zegarem o częstotliwości 16MHz, co oznacza że czas wykonywania instrukcji to 62,5ns (albo więcej). Skoro uzyskaliśmy szerokość impulsu 126ns to znaczy że zmiana stanu wyjścia wymaga dwóch cykli maszynowych. Nie jest źle, ale w asemblerze biblioteki Adafruit_Neopixel pewnie mogą osiągnąć więcej. W każdym razie ja postanowiłem zostać przy tej wersji - nadal jest to język C, ale jak widać wydajność starej szkoły bezpośredniego dostępu do rejestrów jest trudna do pobicia nawet przez C++. Przy okazji mała dygresja odnośnie przerwań - jak wspominałem wcześniej biblioteka Neopixel wyłącza obsługę przerwań na czas całej komunikacji z ws2812b. Mając prosty program testowy można ładnie zobaczyć dlaczego: Jak widzimy obsługa przerwania zajmuje prawie 6,5us i wówczas program nie może "machać" pinem. Dlatego obsługując ws2812b autorzy biblioteki wyłączają przerwania - żeby w trakcie transmisji nie pojawiały się przerwy trwające kilka mikrosekund.
  7. Skoro zasilanie pasków ws2812b nie stanowi już problemu, czas poświęcić kilka chwil na sterowanie nimi. Jak już wcześniej wspominałem, użycie Arduino oraz biblioteka Adafruit_Neopixel (https://github.com/adafruit/Adafruit_NeoPixel) niesamowicie ułatwia zadanie - właściwie wystarczy podłączyć przewody, wgrać przykładowy program ewentualnie dostosowując liczbę sterowanych diod i gotowe. Oczywiście nie wszystko jest takie idealne, więc może warto dokładniej przyjrzeć się takiemu rozwiązaniu, poznać jego zalety i wady, a przede wszystkim poznać lepiej sposób sterowania ws2812b. Ma początek trochę dokumentacji. Okazuje się że do WS2812B znajdziemy kilka różnych datasheet-ów. Przykładowo: https://cdn-shop.adafruit.com/datasheets/WS2812B.pdf https://www.seeedstudio.com/document/pdf/WS2812B Datasheet.pdf https://www.kitronik.co.uk/pdf/WS2812B-LED-datasheet.pdf Na pierwszy rzut oka różnice nie są duże, ale w pewnych momentach istotne. Każdy moduł ws2812b składa się z układu sterującego oraz trzech diod świecących (czerwonej, zielonej i niebieskiej). Moduł nie dość że jest mały to posiada raptem 4 wyprowadzenia. Dwa służą do zasilania, a pozostałe to wejście i wyjście danych. Dzięki posiadaniu również wyjścia danych możliwe jest łatwe łączenie ws2812b w długie łańcuchy - wyjścia modułów są po prostu łączone z wejściami kolejnych. Oznacza to, że cała komunikacja z mikrokontrolerem ogranicza się do jednego sygnału, czyli wejścia pierwszego modułu w całym łańcuchu. W dokumentacji znajdziemy opis sterowania ws2812b, która opiera się na przesyłaniu trzech możliwych kodów: bitu o wartości 0, 1 oraz sygnału reset. Jak widać diagramy są raczej proste, pozostaje rozszyfrować ile wynoszą zaznaczone na nich czasy, czyli T0H, T0L, T1H, T1L oraz Treset. Okazuje się że każda dokumentacja podaje trochę inne wartości: Na szczęście różnice nie są ogromne, widzimy więc że dla bitu 0 mamy mieć przez ok. 0,4us stan wysoki, a następnie przez 0,8us stan niski. Dla bitu o wartości 1, najpierw przez 0,8us stan wysoki, a następnie przez 0,4us stan niski. W przypadku resetu, stan niski ma trwać co najmniej 50us - ale może być dowolnie długo. Teraz możemy zobaczyć jak wygląda sterowanie 24 diodami za pomocą Arduino oraz biblioteki Adafruit_NeoPixel. Kod programu jest banalnie prosty: #include <Adafruit_NeoPixel.h> #define NUMPIXELS 24 #define PIN 6 Adafruit_NeoPixel pixels(NUMPIXELS, PIN, NEO_GRB + NEO_KHZ800); void setup() { } void loop() { pixels.begin(); pixels.clear(); for (int i = 0; i < 24; i++) pixels.setPixelColor(i, pixels.Color(0x00, 0x01, 0x00)); pixels.show(); } Jego działanie polega na bardzo słabym wysterowaniu wszystkich 24 diod - wartość 0x01 oznacza 1/256 wypełnienia PWM dla diody zielonej. Okazuje się że nawet tak małe wysterowanie zapewnia całkiem widoczne świecenie diody. Wartość wybrałem jednak z innego powodu, chodziło o łatwiejsze przeanalizowanie wyników odczytanych analizatorem. Na początek cała transmisja: Jak widzimy po prawej stronie, zajmuje ona ok. 720us. Policzmy, mamy 24 diody, każda ma 3 składowe po 8 bitów dla każdej. Czyli 24 x 3 x 8 = 576 bitów danych. Transmisja każdego bitu ma według dokumentacji trwać 1,25us (+-0,6us), więc wychodzi nam idealnie to co zmierzyliśmy. Między transmisjami jest dużo wolnego czasu, kiedy na linii danych jest stan niski - dzięki temu układy ws2812b wykrywają reset transmisji i wiedzą kiedy rozpocząć przesyłanie nowych danych. Teraz możemy przyjrzeć się transmisji bitów Ogólnie większość danych testowych to zera, przy pierwszym przesyłanym zerze widzimy czasy: stan wysoki, czyli T0H ma 312us, a stan niski to pozostałe 9,938us. Wartości jak widzimy odpowiadają dokumentacji. Teraz dwa słowa o kolejności danych: najpierw przesyłane jest 8 bitów dla diody zielonej pierwszego modułu ws2812b, następnie 8 bitów dla czerwonej, a na koniec dla niebieskiej. Jeśli mamy więcej niż jeden ws2812b w łańcuchu to bezpośrednio po danych dla pierwszego, przesyłane są dane dla drugiego i kolejnych. Kolejność barw jest więc trochę nietypowa to GRB. Dane dla każdej diody przesyłane są zaczynając od najbardziej znaczącego bitu. W ramach testów używany był kod: pixels.setPixelColor(i, pixels.Color(0x00, 0x01, 0x00)); Biblioteka Neopixel używa zanacznie popularniejszej kolejności kolorów, czyli RGB, więc nasz przykład oznacza R=0, G=1, B=0. Skoro najpierw przesyłana jest składowa dla zielonej diody, powinniśmy zobaczyć pierwszy transmitowany bajt o wartości 1: Zgodnie z oczekiwaniami, transmisja zaczyna się od najwyższego bitu, a my wysyłamy 7 bitów zerowych. Ósmy bit ma wartość "1" i widzimy jego czasy podświetlone na obrazku. Tym razem stan wysoki T1H trwał 0,814 us, a niski 0,432 us - czyli zgodnie z oczekiwaniami. Mając takie wyniki nie powinniśmy być zaskoczeni, że wszystko pięknie działa. Testowałem bibliotekę Neopixel używając do 432 diod ws2812b i nie było najmniejszych problemów. Pewnie można byłoby użyć więcej, ale pojawiają się pierwsze ograniczenia Arduino UNO - mała ilość pamięci RAM. Biblioteka Adafruit_Neopixel przechowuje kolory wszystkich diod w pamięci, więc dla każdej diody konieczne są 3 bajty. Oznacza to że 432 diody wymagały 1296 z dostępnych 2048. Trochę pamięci trzeba zostawić dla programu, więc przy tak prostym sterowaniu raczej nie można liczyć na więcej niż 500 diod - co i tak jest chyba niezłym wynikiem. Czy wszystko jest tak idealnie jak się wydaje? Okazuje się że niestety nie do końca. Po pierwsze nasz mikrokontroler ma mnóstwo pracy przy sterowaniu diodami ws2812b - wszystko wykonywane jest programowo, więc wymaga mnóstwo pracy. To nie jest jakaś ogromna wada, bo i tak nasze Arduino nie ma nic lepszego do roboty w Święta. Druga wada jest poważniejsza - dla 24 diodek transmisja trwała 720us, ale przy 432 będzie to już 12960 us, czyli prawie 13 ms. Okazuje się że biblioteka Adafruit_Neopixel aby zapewnić niezbędne przebiegi czasowe na cały czas transmisji wyłącza obsługę przerwań. Oznacza to że większość modułów używających przerwań jak np. port szeregowy, a nawet funkcja millis() może nie działać prawidłowo. Możemy sobie z tym poradzić, ale takie proste rozwiązanie nie będzie niestety działało z każdą biblioteką i kodem. Jeśli więc chcemy przygotować pięknie migający łańcuch diod na choinkę, możemy po prostu użyć Arduino i biblioteki Neopixel. Natomiast jeśli chcemy zrobić coś więcej, trzeba będzie poznać diody ws2812b nieco lepiej.
  8. Do testów jak poprzednio wykorzystuję krążek 24 diod oraz Arduino UNO. Ponieważ będę testował tylko pojedyncze diody, więc wróciłem do zasilania z USB, podłączam oscyloskop i na początek pomiar prądu, gdy diody nie świecą: Niby nic ciekawego ale warto się upewnić, że nowa metoda daje te wyniki co poprzednio (wcześniej było 21,6mA, teraz 23,4mA - na większą dokładność i tak nie mam co liczyć). Kolejny prosty test to jedna dioda świecąca maksymalnie na biało, czyli PWM 100%: Wcześniej wyszło mi 52,9mA, teraz 54,9mA - czyli zarówno przy zerowym, jak i pełnym wypełnieniu pomiary oscyloskopem pokrywają się z wynikami otrzymanymi multimetrem. Czas więc przejść do czegoś ciekawszego. Wypełnienie 50%, czyli PWM=128: Jak widać i w sumie można się było spodziewać, prąd płynący w obwodzie wcale nie jest stały. Widzimy też częstotliwość PWM, która wynosi ok. 1,2kHz. Co ciekawe każda dioda WS2812B ma nieco inną częstotliwość, podobną ale inną. Jeszcze dla upewnienia się, że wszystko działa PWM=25%, czyli to co chciałbym mieć na choince: Dotychczas była używana jedna dioda, ale co się stanie gdy wysterujemy dwie? Pierwszy pomiar jest nieco zaskakujący: Okazało się jednak że winne było uśrednianie używane wcześniej, po jego wyłączeniu lepiej widać co się dzieje: Jednak częstotliwość PWM każdej diody jest nieco inna, więc przebiegi nakładają się na siebie w przeróżny sposób: Chyba już widać jak strasznie ws2812b zaśmiecają linie zasilania. Jeśli ktoś nie wierzy, poniżej wyniki dla 8 sztuk (zmieniona skala): To chyba wyjaśnia częste problemy z zasilaniem pasków WS2812B. Jak widać nawet przy wypełnieniu 25% i akceptowalnym prądzie średnim, wartości chwilowe mogą osiągać maksimum. Dlatego bardzo ważne jest filtrowanie oraz używanie zasilacza z odpowiednim zapasem mocy. Trochę mnie to wszystko zmartwiło muszę przyznać, bo okazuje się że prosty łańcuch lampek na choinkę może być nieco trudniejszy niż myślałem. Z drugiej strony już wiem dlaczego każdy odcinek LED ma własne zasilanie - nawet nie chcę myśleć jak wyglądałyby oscylogramy przy tych 23A, które mi wyszły z wcześniejszych obliczeń. Wydaje mi się, że o zasilaniu WS2812B wiem już wystarczająco dużo, teraz czas zabrać się za to co proste i przyjemne - czyli sterowanie.
  9. Nazbierałem już trochę wyników, czas je wykorzystać. Spróbowałem oszacować jakie prądy będą konieczne do zasilenia różnej liczby diodek na choince: 5 metrowy pasek świecący na biało to ponad 23A - dla mnie trochę za dużo. Ale już zejście z pwm do 25% daje jakieś 6,4A - znacznie lepiej. Ale zanim zacznę cokolwiek podłączać postanowiłem porównać moje "wyniki teoretyczne" dla 24 diodek z pomiarami. Poniżej to co wyszło: Tak jak napisałem na wstępie, nie dysponuję sprzętem laboratoryjnym, ale wygląda na to że szacunki całkiem nieźle wyszły. Oczywiście błąd będzie na pewno większy dla większej liczby diod, ale widać że dla 24 sztuk wyszło w miarę dobrze. Niestety podczas wykonywania pomiarów spotkała mnie kolejna niespodzianka. Dla wypełnienia PWM powyżej 50% musiałem zmienić zakres w multimetrze... i otrzymałem zupełnie inne wyniki niż wcześniej. Nie pozostało nic innego jak wygrzebać z szafy stary oscyloskop i przyjrzeć się o wiele dokładniej zasilaniu WS2812B.
  10. W zeszłym roku planowałem przygotować oświetlenie choinki na diodach ws2812. Plan był świetny, ale jak to zwykle z planami bywa nie do końca się powiódł. Niestety po świętach diody ws2812 nie były już aż tak ciekawe i musiały przeleżeć w szafie do następnego roku. W momencie kiedy piszę te słowa jest już mało czasu do gwiazdki, ale nie zostaje mi nic poza optymizmem - może tym razem się uda. Tym razem postanowiłem zabrać się do tematu nieco solidniej niż w zeszłym roku. Jak ogólnie wiadomo, układy elektroniczne działają lepiej gdy są zasilane, stąd na początek spróbowałem wykonać trochę pomiarów i testów związanych z tą częścią projektu. Docelowo mam kilka taśm z diodami ws2812, każda po 144 sztuki na metr, coś jak https://botland.com.pl/pl/paski-led-adresowane/5814-pasek-led-rgb-ws2812b-cyfrowy-adresowany-ip65-144-ledm-9wm-5v-1m.html W zeszłym roku już się do nich przymierzałem, więc wiem że wymagają solidnego zasilania. Dla ułatwienia najpierw używam małego krążka z 24 diodami (https://botland.com.pl/pl/lancuchy-i-matryce-led/6246-pierscien-led-rgb-ws2812b-5050-x-24-diody-86mm.html) oraz Arduino UNO. Pierwsze podłączenie i uruchomienie przykładowego programu jest banalnie proste. Biblioteka Adafriut_Neopixel działa przepięknie, a diody nawet zasilane z Arduino dają radę (całość podłączona przez USB do komputera). Teoretycznie na tym można byłoby poprzestać, w końcu działa. Jednak wiem, że jeśli podłączę np. 4 taśmy po 144 diody to już pewnie tak łatwo nie będzie. Zacznijmy więc od ustalenia jakie będzie potrzebne zasilanie dla takiego układu oraz jego docelowej wersji. Z góry zaznaczam, że moje pomiary nie są wykonywane z niebotyczną dokładnością, powiedziałbym że wiele z nich to wręcz oszacowania. Ale to i tak lepsze niż nic nie wiedzieć i po prostu liczyć że jakoś to będzie. Jako pierwszy test podłączam multimetr jako amperomierz między Arduino i krążek LED-ów. Ku mojemu zaskoczeniu w obwodzie płynie całkiem spory prąd, bo 21,7mA a na razie wszystkie diody są wyłączone. To skłoniło mnie do wykonania całej serii pomiarów - przy każdym teście steruję tylko jedną diodą, tzn. jedną składową (czerwoną, zieloną, albo niebieską) z jednej diody ws2812. W kolejnych pomiarach zmieniane jest wypełnienie PWM od 0 do 255: Jak widać niezależnie od koloru diody, zmierzone prądy są praktycznie takie same. Natomiast dla mnie zaskoczeniem był znaczny prąd przy wyłączonych diodach oraz niewielki pobór prądu gdy diody świecą. Odejmując od wyników prąd pobierany przy zerowym wypełnieniu dostajemy: Nie wiem, czy to widać z samych tabelek, ale wyniki są w pełni liniowe. Poniżej wykres dla pierwszej tabelki, czyli całkowitego poboru prądu przez krążek: Natomiast pod odjęciu prądu dla PWM=0: Sprawdziłem pobór prądu dla paska 144 wyłączonych diod. Wynik był zbliżony do 140 mA, wygląda więc na to że każda dioda ws2812b pobiera prawie 1mA nawet jeśli nie świeci. To niemiłe zaskoczenie. Kolejna sprawa to szacowanie ile maksymalnie może pobrać dioda. Wcześniej testowałem włączając tylko jedną składową w danym momencie. Poniżej są wyniki gdy jedna dioda świeci światłem białym, czyli PWM dla każdej składowej jest identyczny: Podobnie jak poprzednio można odjąć pierwszą wartość: Teraz już chyba widać jak można oszacować pobór prądu jednej diody ws2812b. Wartość stała to 1mA, następnie w zależności od wypełnienia PWM oraz liczby składowych koloru jest to 0-32mA. Więc w najgorszym przypadku pasek 144 diod powinien wymagać prawie 5A prądu... dużo. Na szczęście diody nie muszą być zawsze sterowane z pełnym wypełnieniem. Na moje potrzeby PWM=64, czyli 25% to i tak bardzo wysoka jasność. Wówczas zamiast 5A powinno wystarczyć 1,3A, ale pojawią się inne niespodzianki - o których napiszę za chwilę. Postanowiłem również przetestować napięcie zasilania WS2812B. Ogólnie dokumentacja do tych diod jest delikatnie mówiąc niezbyt obszerna (https://www.seeedstudio.com/document/pdf/WS2812B Datasheet.pdf). Znajdziemy w niej jednak chociaż wskazówkę dotyczącą napięć zasilania: Czyli od 3,5V do 5,3V. Oczywiście nie byłbym sobą gdybym nie sprawdził co będzie jak podłączymy mniej niż 3,5V (więcej niż 5,3V jednak nie miałem odwagi sprawdzać). WS2812B działają już przy napięciu 2,3V. Poniżej tabelka z poborem prądu gdy diody są wyłączone, czyli PWM=0: Przy Vin=5V mamy to co w poprzednich pomiarach, czyli trochę ponad 21mA prądu zużywanego nawet gdy diody nie świecą. Ale co warto zauważyć ten prąd zależy od napięcia, więc przy 3,5V tracilibyśmy połowę tego co przy standardowym napięciu. Ten test postanowiłem przeprowadzić jednak z innego powodu - nie chodziło mi o znalezienie minimalnego napięcia gdy ws2812 działają, ale o sprawdzenie, czy prąd wyjściowy zależy od napięcia zasilania. Inaczej mówiąc o ustalenie, czy ws2812 mają źródła prądowe. Wysterowałem zieloną składową jednej diody na 25%, czyli PWM=64. Poniżej wyniki pomiarów: Jak widzimy prąd pobierany przez układ zależy od napięcia, ale spróbujmy odjąć od tych wyników zmierzone wcześniej prądy pobierane gdy diody nie świecą: Teraz chyba już widać o co chodziło z napięciem minimalnym oraz prądami. Co prawda pobór prądu zależy od napięcia zasilania, ale to o ile zwiększa się prąd przy zmianach PWM jest stałe o ile tylko zasilimy układ z co najmniej 3,5V. Poniżej tego progu ws2812 też działają, ale prądy są mniejsze niż powinny.
  11. I jeszcze w kwestii bezpieczeństwa IoT, już tutaj masz buffer overflow: data[30] = '\0'; Reszty nawet nie komentuję, ale pamiętaj proszę że Internet to nie piaskownica, to co udostępniasz widzą wszyscy - zarówno dobrzy, jak i źli użytkownicy sieci...
  12. Jak chodzi o pomysł na artykuł, to ja chętnie poczytałbym o bezpieczeństwie IoT. Mamy teraz coraz więcej projektów urządzeń podłączonych przez WiFi, czy inne łącza do internetu, a popularność układów ESP ma tutaj pewnie istotne znaczenie. Jednak nie zawsze takie beztroskie udostępnianie urządzeń w internecie jest całkiem bezpieczne. Wydaje mi się, że artykuł o związanych z tym ryzykach oraz chociaż podstawowych środkach zaradczych byłby bardzo interesujący.
  13. W pierwszej chwili faktycznie posiadanie 6 rdzeni wydaje się mocną przesadą. Mając jednak takie możliwości, można wymyślić dla nich ciekawe zastosowania - pierwsze powinien podać @RFM, bo sam o nim pisał. Chodzi mi o "akcelerator graficzny". DMA jak najbardziej wystarczy do przesłania danych z pamięci RAM do wyświetlacza, ale konieczne jest posiadanie dużej ilości pamięci żeby zmieścić wszystkie dane. No i samo przygotowanie danych dla kolejnej ramki też może sporo czasu zajmować. Natomiast mając 6 rdzeni, można po prostu jeden przeznaczyć na generowanie obrazu - i w RAM trzymać znacznie mniej zasobożerną reprezentację, np. przez redukcję liczby kolorów, albo użycie palety. Mając zapas mocy obliczeniowej można zrobić o wiele więcej, np. dodać sprzętowe "duszki", albo własny generator fontów - właściwie wszystko to co 8- i 16- bitowce miały w dedykowanych układach tutaj może zrobić jeden, a może dwa rdzenie. Kolejne zastosowanie to DSP dla odtwarzania oraz rejestracji dźwięku. Znowu DMA w pełni wystarczy do przesłania danych np. z bufora do DAC, ale jeśli chcemy dodać filtrowanie, to już samo DMA nie wystarczy. Niby można to wykonać mając jeden rdzeń, ale czas poświęcony na liczenie filtrów zajmuje wtedy procesor. Natomiast mając tyle rdzeni, można dać po jednym na lewy i prawy kanał, a jednocześnie wykonywać program z pełną prędkością. A jak to nie wystarczy, to jeszcze kolega @ethanak mógłby uruchomić syntezator mowy na dedykowanym rdzeniu. Na płytce spresense znajdziemy również odbiornik GPS. Każdy kto spróbował odczytywać "surowe" dane z takiego odbiornika na pewno zauważył, że są one zupełnie inne od tego co widzimy używając np. smartfona - odczytywane wartości skaczą sobie o kilka metrów i to nawet przy silnym sygnale. I tutaj znowu pomocne jest filtrowanie, np. filtr Kalmana jest (albo chociaż był) popularny w nawigacjach samochodowych. Dzięki niemu można łatwo odfiltrować chwilowe zakłócenia, a jednocześnie otrzymywać dobre przybliżenie aktualnego położenia. I znowu, wszystko może robić jeden układ, ale w pewnym momencie może brakować mocy obliczeniowej - a przy 6 rdzeniach zawsze można jeden "oddać" do obsługi GPS. Można wymyślić o wiele więcej zastosowań dla wieloprocesorowości - bo wbrew pozorom jest ona popularna w zastosowaniach embedded. Co prawda najczęściej wiele rdzeni realizowanych jest przez użycie wielu mikrokontrolerów, ale mając 6-rdzeniowy układ można kilka modułów zintegrować w jeden.
  14. Zamiast tej literatury matematycznej i matematyki wyższej proponuję powtórkę z podstawówki https://matematykawpodstawowce.pl/kolejnosc-wykonywania-dzialan/
  15. Jak chodzi o wybór wersji Raspberry Pi, to 4 ma ważną zaletę. O ile rozumiem, jednym z celów pracy ma być zbudowanie pięknego interfejsu. Raspberry Pi 4 pozwala na podłączenie dwóch monitorów - więc efekt może być podwójnie piękny
  16. @mw9501 nie udostępniłeś całego kodu, więc próba pomocy to niestety zgadywanie. Zacznę więc od najprostszego pytania - włączyłeś taktowanie portu z pinem RX?
  17. Faktycznie, nie warto się tłumaczyć. Może i nie warto nawet nic pisać.
  18. Ja nie mam siły dalej dyskutować - tak jak napisałem wcześniej, zachęcam do poczytania dlaczego w Linuxie jest oddzielny zegar czasu rzeczywistego, a oddzielny monotoniczny.
  19. Ja też trochę eksperymentowałem, najpierw z MSP430, później LPC2138 - w sumie to ja nawet nie miałem czasu na testowanie RTC, ale na produkcji narzekali, że wskazania zegara różnią się o kilka godzin zanim urządzenie zostanie wysłane do klienta. Z tego co pamiętam kwarce używane do taktowania mikrokontrolerów mają o wiele wyższą dokładność niż używane w typowych modułach RTC, stąd było moje zaskoczenie dlaczego miałoby być inaczej. Ale niezależnie od wszystkiego, po pewnym czasie każdy, nawet drogi moduł wskazuje coś innego niż wzorzec czasu.
  20. To zostawmy tą dyskusję w takim stanie, że każdy ma swoje zdanie i niech sobie z nim żyje jeśli mu tak wygodnie.
  21. Ja już wykonywałem podobne eksperymenty - co prawda bez arduino, ale za to z większą ilością egzemplarzy. I zapewniam, że po kilku tygodniach zegary RTC oparte o tanie rezonatory kwarcowe wskazują niesamowite bzdury.
  22. Zostawmy może teoretyczne gdybania - do obliczania przedziałów czasu potrzebny jest zegar monotoniczny, jak go zbudujemy to oddzielny temat. Faktycznie korekta o kilka sekund dziennie nie będzie przez nikogo zauważona, ale już np. używanie takich zegarów w programach może mieć opłakane rezultaty (a z takim błędem dopiero co walczyłem, może jestem przewrażliwiony ). Chodziło mi tylko o to, że z dwojga złego lepszym zegarem do liczenia czasu pracy było użycie millis() niż RTC. Chociaż oczywiście tworząc zestawienia będziemy potrzebowali godzin z zegara czasu rzeczywistego. Ale aby dokładnie ustalić ile czasu coś trwało (np. praca), zegar nie może być przestawiany.
  23. Rezonatory kwarcowe na 32kHz znane są ze słabej dokładności - można ją kompensować ustawianiem pojemności itd. Ale nie oszukujmy się, po kilku dniach, czy miesiącach czas wskazywany przez taki RTC będzie odbiegał od wzorca. W zależności czy ta różnica będzie dodatnia, czy ujemna - musimy skorygować wskazania zegara, inaczej to już nie będzie czas rzeczywisty. Dochodzi jeszcze cofanie o godzinę, albo dodawanie godziny co roku ale to inna historia. Ja rozumiem RTC jako aktualny czas, czyli czas wskazywany przez zegar z kukułką wiszący na ścianie. Natomiast to liczenie liczby sekund od chwili zero to dokładnie czas monotoniczny. Nie ma sensu żebym tutaj robił własne wykłady, @ethanak lubisz bawić się linuxem, poczytaj ile rodzajów zegarów jest w systemie, rozróżnienie między RTC, a czasem monotonicznym jest chociażby tutaj: https://stackoverflow.com/questions/3523442/difference-between-clock-realtime-and-clock-monotonic Właśnie dlatego że rozliczanie czasu pracy jest poważną sprawą, nie można użyć do tego zegara który ma przeskoki, albo się cofa bo wymaga korekty. Można użyć modułów RTC do zbudowania zegara monotonicznego - wystarczy taki moduł raz ustawić i już więcej nie przestawiać. Ale to nie będzie już moduł przechowujący czas rzeczywisty, ale jakiś bliżej nieokreślony "czas odniesienia". Natomiast chcąc znać aktualną godzinę będziemy musieli wskazania odpowiednio przeliczyć.
  24. A możesz podać skąd pomysł że zegar RTC ma większą precyzję niż np. mikrokontroler? Bo o ile ja wiem jest dokładnie odwrotnie - tanie kwarce "zegarkowe" o częstotliwości 32kHz mają dużo niższą dokładność niż stosowane do taktowania MCU. No i chyba nie zrozumiałeś co to jest zegar monotoniczny - dokładność to oddzielna sprawa. Ale RTC z założenia można przestawiać, a czasem pojawiają się przeskoki lub cofnięcia czasu. I dlatego do odmierzania przedziałów czasu trzeba stosować coś innego - to coś może być oparte o RTC, ale sam RTC bez odpowiednich zabiegów się nie sprawdzi.
  25. RTC nie nadaje się do zliczania czasu pracy z prostej przyczyny, nie jest monotoniczny. Pierwszy i najbardziej drastyczny przypadek to zmiana czasu - z jednej strony nikt nie chciałby za darmo pracować dłużej, z drugiej kierownictwo mogłoby nie być zachwycone płaceniem za dodatkową godzinę, której w rzeczywistości nie było. Ale nawet jeśli pominiemy zmiany czasu, problem wcale nie zniknie. Na ogół oczekujemy że RTC wskazuje aktualną godzinę. Niestety jeśli uruchomimy dowolny w miarę tani (czyli np. nie-atomowy) zegar i zostawimy na jakiś czas, okaże się że jego wskazania nie są już idealne. I wtedy pojawia się problem - jeśli skorygujemy wskazania zegara, to albo damy komuś w prezencie dodatkowe godziny, albo je zabierzemy. Z drugiej strony jeśli nie będziemy aktualizować wskazań zegara, to R w nazwie RTC niewiele będzie miało wspólnego z "Rzeczywistością". Dlatego np. w systemie Linux, ale pewnie i innych mamy jednocześnie wiele zegarów. Jeden z nich to tzw. zegar monotoniczny - odpowiada on millis() na arduino i czyli w sposób stały odmierza czas np. od uruchomienia systemu. Taki zegar bardzo dobrze nadaje się do odmierzania przedziałów czasu, bo niezależnie od zmian pór roku, czy korekty wskazań RTC jego odczyty zmieniają się jednostajnie, bez cofnięć, czy przeskoków.
×
×
  • Utwórz nowe...