Liczenie cykli w nowoczesnych procesorach (np. ARM)


14

W wielu aplikacjach procesor, którego wykonywanie instrukcji ma znaną zależność czasową z oczekiwanymi bodźcami wejściowymi, może obsłużyć zadania wymagające znacznie szybszego procesora, gdyby związek był nieznany. Na przykład w projekcie, w którym użyłem PSOC do wygenerowania wideo, użyłem kodu do wyprowadzenia jednego bajtu danych wideo co 16 taktów procesora. Ponieważ testowanie, czy urządzenie SPI jest gotowe i rozgałęzienie, jeśli nie, IIRC zajmie 13 zegarów, a ładowanie i przechowywanie danych wyjściowych zajmie 11, nie było możliwości przetestowania urządzenia pod kątem gotowości między bajtami; zamiast tego po prostu ustawiłem, aby procesor wykonał dokładnie kod o wartości 16 cykli dla każdego bajtu po pierwszym (wydaje mi się, że użyłem rzeczywistego obciążenia indeksowanego, sztucznego obciążenia indeksowanego i magazynu). Pierwszy zapis SPI każdej linii miał miejsce przed rozpoczęciem wideo, a dla każdego kolejnego zapisu było 16-cyklowe okno, w którym zapis mógł wystąpić bez przepełnienia lub niedopełnienia bufora. Pętla rozgałęziająca wygenerowała 13-cyklowe okno niepewności, ale przewidywalne wykonanie 16-cyklowe oznaczało, że niepewność dla wszystkich kolejnych bajtów mieściłaby się w tym samym oknie 13-cyklowym (które z kolei mieszczą się w 16-cyklowym oknie, w którym zapis może być akceptowalny pojawić się).

W przypadku starszych procesorów informacje o taktowaniu instrukcji były jasne, dostępne i jednoznaczne. W przypadku nowszych układów ARM informacje o taktowaniu wydają się znacznie bardziej niejasne. Rozumiem, że kiedy kod jest wykonywany z pamięci flash, zachowanie buforowania może znacznie utrudnić przewidywanie, więc spodziewałbym się, że każdy kod liczony w cyklu powinien być wykonywany z pamięci RAM. Jednak nawet podczas wykonywania kodu z pamięci RAM specyfikacje wydają się nieco niejasne. Czy stosowanie kodu liczonego w cyklu jest nadal dobrym pomysłem? Jeśli tak, jakie są najlepsze techniki, aby działał niezawodnie? W jakim stopniu można bezpiecznie założyć, że sprzedawca mikroukładów nie zamierza po cichu wsunąć „nowego ulepszonego” układu, który w niektórych przypadkach odcina cykl wykonywania niektórych instrukcji?

Zakładając, że następująca pętla zaczyna się na granicy słów, jak określić na podstawie specyfikacji dokładnie, ile to zajmie (załóżmy, że Cortex-M3 z pamięcią stanu zerowego oczekiwania; nic innego o systemie nie powinno mieć znaczenia dla tego przykładu).

myloop:
  mov r0, r0; Krótkie proste instrukcje, aby umożliwić pobranie większej liczby instrukcji
  mov r0, r0; Krótkie proste instrukcje, aby umożliwić pobranie większej liczby instrukcji
  mov r0, r0; Krótkie proste instrukcje, aby umożliwić pobranie większej liczby instrukcji
  mov r0, r0; Krótkie proste instrukcje, aby umożliwić pobranie większej liczby instrukcji
  mov r0, r0; Krótkie proste instrukcje, aby umożliwić pobranie większej liczby instrukcji
  mov r0, r0; Krótkie proste instrukcje, aby umożliwić pobranie większej liczby instrukcji
  dodaje r2, r1, # 0x12000000; Instrukcja 2-słowowa
  ; Powtórz następujące czynności, prawdopodobnie z innymi operandami
  ; Będzie dodawał wartości, dopóki nie pojawi się przeniesienie
  itcc
  addscc r2, r2, # 0x12000000; 2-wyrazowa instrukcja plus dodatkowe „słowo” dla itcc
  itcc
  addscc r2, r2, # 0x12000000; 2-wyrazowa instrukcja plus dodatkowe „słowo” dla itcc
  itcc
  addscc r2, r2, # 0x12000000; 2-wyrazowa instrukcja plus dodatkowe „słowo” dla itcc
  itcc
  addscc r2, r2, # 0x12000000; 2-wyrazowa instrukcja plus dodatkowe „słowo” dla itcc
; ... itd., z bardziej warunkowymi instrukcjami składającymi się z dwóch słów
  sub r8, r8, # 1
  bpl myloop

Podczas wykonywania pierwszych sześciu instrukcji rdzeń będzie miał czas na pobranie sześciu słów, z których trzy zostaną wykonane, aby mogły zostać pobrane maksymalnie trzy słowa. Kolejne instrukcje składają się z trzech słów, więc rdzeń nie będzie mógł pobrać instrukcji tak szybko, jak są one wykonywane. Spodziewałbym się, że niektóre instrukcje „it” zajmą cykl, ale nie wiem, jak przewidzieć, które z nich.

Byłoby miło, gdyby ARM mógł określić pewne warunki, w których czas rozkazu „it” byłby deterministyczny (np. Jeśli nie ma stanów oczekiwania lub rywalizacji o magistralę kodową, a poprzednie dwie instrukcje są instrukcjami rejestru 16-bitowego itp.) ale nie widziałem żadnej takiej specyfikacji.

Przykładowa aplikacja

Załóżmy, że ktoś próbuje zaprojektować płytę główną dla Atari 2600 do generowania komponentowego wyjścia wideo w rozdzielczości 480P. 2600 ma zegar pikseli 3,579 MHz i zegar procesora 1,19 MHz (zegar punktowy / 3). W przypadku komponentowego wideo 480P, każda linia musi być wyprowadzona dwukrotnie, co oznacza wyjście z zegarem kropkowym 7,168 MHz. Ponieważ układ wideo Atari (TIA) generuje jeden z 128 kolorów, wykorzystując jako 3-bitowy sygnał luma plus sygnał fazowy o rozdzielczości około 18ns, trudno byłoby dokładnie określić kolor, patrząc tylko na wyjścia. Lepszym rozwiązaniem byłoby przechwytywanie zapisów do rejestrów kolorów, obserwowanie zapisanych wartości i wprowadzanie do każdego rejestru wartości luminancji TIA odpowiadającej numerowi rejestru.

Wszystko to można zrobić za pomocą FPGA, ale niektóre dość szybkie urządzenia ARM mogą być znacznie tańsze niż FPGA z wystarczającą ilością pamięci RAM, aby obsłużyć niezbędne buforowanie (tak, wiem, że w przypadku woluminów taka rzecz mogłaby zostać wyprodukowana koszt nie jest prawdziwy czynnik). Wymaganie od ARM monitorowania przychodzącego sygnału zegarowego znacznie zwiększy jednak wymaganą szybkość procesora. Przewidywalne liczby cykli mogłyby uczynić rzeczy czystszymi.

Stosunkowo proste podejście polegałoby na tym, aby CPLD obserwował procesor i TIA i generował 13-bitowy sygnał synchronizacji RGB +, a następnie kazałby ARM DMA pobierać 16-bitowe wartości z jednego portu i zapisywać je na drugim z odpowiednim taktowaniem. Ciekawym wyzwaniem projektowym byłoby sprawdzenie, czy tani ARM mógłby zrobić wszystko. DMA może być użytecznym aspektem podejścia typu „wszystko w jednym”, jeśli można przewidzieć jego wpływ na liczbę cykli procesora (szczególnie jeśli cykle DMA mogą się zdarzyć w cyklach, gdy szyna pamięci jest w przeciwnym razie bezczynna), ale w pewnym momencie procesu ARM musiałby wykonywać funkcje wyszukiwania tabeli i oglądania magistrali. Zauważ, że w przeciwieństwie do wielu architektur wideo, w których rejestry kolorów są zapisywane w odstępach czasu wygaszania, Atari 2600 często zapisuje rejestry kolorów podczas wyświetlanej części ramki,

Być może najlepszym rozwiązaniem byłoby użycie kilku dyskretnych układów logicznych do identyfikacji zapisów kolorów i wymuszenie niższych bitów rejestrów kolorów do odpowiednich wartości, a następnie użycie dwóch kanałów DMA do próbkowania danych wejściowych magistrali procesora i danych wyjściowych TIA oraz trzeci kanał DMA do generowania danych wyjściowych. Procesor będzie wtedy mógł przetwarzać wszystkie dane z obu źródeł dla każdej linii skanowania, wykonać niezbędne tłumaczenie i buforować je w celu uzyskania danych wyjściowych. Jedynym aspektem obowiązków adaptera, które musiałyby się zdarzyć w „czasie rzeczywistym”, byłoby zastąpienie danych zapisanych w COLUxx, i które można by załatwić za pomocą dwóch wspólnych układów logicznych.

Odpowiedzi:


7

Głosuję na DMA. Jest bardzo elastyczny w Cortex-M3 i nowszych - i możesz robić wszelkiego rodzaju szalone rzeczy, takie jak automatyczne pobieranie danych z jednego miejsca i przesyłanie do innego z określoną szybkością lub w niektórych przypadkach bez wydawania ŻADNYCH cykli procesora. DMA jest znacznie bardziej niezawodny.

Ale może być to trudne do zrozumienia w szczegółach.

Inną opcją są miękkie rdzenie w FPGA ze sprzętową implementacją tych ciasnych rzeczy.


1
Podoba mi się pojęcie DMA. Nie sądzę jednak, aby rdzeń Cortex M3 miał DMA - jest to funkcja układów poszczególnych producentów i wydaje się, że wszyscy wdrażają go inaczej. Jedną z rzeczy, która wydaje mi się irytująca z przynajmniej jedną implementacją, w którą faktycznie grałem (STM32L152), jest to, że nie mogę znaleźć żadnego sposobu, aby uzyskać stroboskop, kiedy dane DMA są wysyłane. Nie jest również jasne, jakie czynniki mogą wpływać na terminowość DMA.
supercat

1
W każdym razie, w odniesieniu do jednej z pierwszych aplikacji, nad którymi zastanawiałem się nad precyzyjnym cyklowaniem, zamieściłem więcej informacji w pierwotnym pytaniu. Jestem ciekawa, co myślisz. Inną sytuacją, w której zastanawiałem się nad wybijaniem cykli, byłoby wysadzanie wyświetlanych danych na kolorowy wyświetlacz LCD. Dane byłyby buforowane w pamięci RAM przy użyciu 8-bitowych kolorów, ale wyświetlacz potrzebuje kolorów 16-bitowych. Najszybszym sposobem, w jaki wymyśliłem dane wyjściowe, byłoby użycie sprzętu do wygenerowania strobów zapisu, więc procesor musiałby tylko wyrejestrować dane. Czy dobrze byłoby przetłumaczyć 8-> 16 bitów na mały bufor ...
supercat

1
... a następnie umówić DMA na przeniesienie tego lub jakie byłoby najlepsze podejście?
supercat

4

Informacje o czasie są dostępne, ale, jak zauważyłeś, czasami mogą być niejasne. W sekcji 18.2 i tabeli 18.1 Technicznej instrukcji obsługi Cortex-M3 znajduje się wiele informacji na temat czasu , na przykład ( tutaj pdf ), a fragment tutaj:

fragment 18.2

które podają listę warunków dla maksymalnego czasu. Czas dla wielu instrukcji zależy od czynników zewnętrznych, z których niektóre pozostawiają niejednoznaczności. Podkreśliłem każdą dwuznaczność, którą znalazłem w następującym fragmencie z tej sekcji:

[1] Oddziały biorą jeden cykl na instrukcję, a następnie przeładowują rurociąg dla instrukcji docelowej. Nieodebrane oddziały to łącznie 1 cykl. Natychmiastowe rozgałęzienia to zwykle 1 cykl przeładunku rurociągu (łącznie 2 cykle). Wykonane odgałęzienia z operandem rejestru są zwykle 2 cyklami przeładowania rurociągu (łącznie 3 cykle). Przeładowanie potoku jest dłuższe [Jak długo?] Podczas rozgałęziania do niewyrównanych instrukcji 32-bitowych oprócz dostępu do wolniejszej pamięci. Wskazówka magistrali jest wysyłana do szyny kodu, która pozwala wolniejszemu systemowi [O ile wolniej?] Na wstępne ładowanie. Może to [Czy to opcjonalne?] Zmniejszyć [O ile?] Karę za rozgałęzienie za wolniejszą pamięć, ale nigdy mniej niż pokazano tutaj.

[2] Zasadniczo instrukcje przechowujące ładunek wymagają dwóch cykli dla pierwszego dostępu i jednego cyklu dla każdego dodatkowego dostępu. Sklepy z bezpośrednim przesunięciem zajmują jeden cykl.

[3] UMULL / SMULL / UMLAL / SMLAL używają wcześniejszego zakończenia w zależności od wielkości wartości źródłowych [Jakie rozmiary?]. Są one przerywane (porzucone / ponownie uruchomione), przy najgorszym przypadku opóźnienia jednego cyklu. Wersje MLAL zajmują od czterech do siedmiu cykli, a wersje MULL - od trzech do pięciu cykli . W przypadku MLAL podpisana wersja jest o jeden cykl dłuższa niż niepodpisana.

[4] Instrukcje IT można złożyć . [Gdy? Zobacz komentarze.]

[5] Czasy DIV zależą od dywidendy i dzielnika . [Ten sam problem, co MUL] DIV jest przerywany (porzucony / zrestartowany), z najgorszym opóźnieniem jednego cyklu. Kiedy dywidenda i dzielnik są podobne [Jak podobne?], Podział kończy się szybko. Minimalny czas dotyczy przypadków dzielnika większego niż dywidenda i dzielnika zerowego. Dzielnik zera zwraca zero (nie jest to błąd), chociaż dostępna jest pułapka debugowania, która pozwala uchwycić ten przypadek. [Jakie są zakresy podane dla MUL?]

[6] Sen to jeden cykl instrukcji plus tyle cykli snu, ile potrzeba. WFE korzysta tylko z jednego cyklu, gdy zdarzenie minęło. WFI jest zwykle więcej niż jednym cyklem, chyba że zdarzy się przerwanie dokładnie podczas wchodzenia do WFI.

[7] ISB ma jeden cykl (działa jako gałąź). DMB i DSB zajmują jeden cykl, chyba że dane oczekują w buforze zapisu lub LSU. Jeśli przerwanie pojawi się podczas bariery, zostanie porzucone / uruchomione ponownie.

Dla wszystkich przypadków użycia będzie bardziej złożony niż „Ta instrukcja to jeden cykl, ta instrukcja to dwa cykle, to jest jeden cykl ...” licząc możliwe w prostszych, wolniejszych, starszych procesorach. W niektórych przypadkach użycia nie napotkasz żadnych dwuznaczności. Jeśli napotkasz dwuznaczności, sugeruję:

  1. Skontaktuj się ze sprzedawcą i zapytaj go, jaki jest czas instrukcji dla twojego przypadku użycia.
  2. Testuj, aby określić niejednoznaczne zachowanie
  3. Ponownie przetestuj wszystkie wersje procesora, a zwłaszcza zmiany dostawcy.

Wymagania te prawdopodobnie stanowią odpowiedź na twoje pytanie: „Nie, to nie jest dobry pomysł, chyba że napotkane trudności są warte swojej ceny” - ale już o tym wiesz.


1
Uważam, że następujące jest niejasne: „Przeładowanie potoku jest dłuższe, gdy rozgałęzienie do niewyrównanych instrukcji 32-bitowych oprócz dostępu do wolniejszej pamięci” nie mówi, czy dodaje dokładnie jeden cykl, a „instrukcje IT można złożyć” nie nie określają, na jakich warunkach będą lub nie będą.
supercat

1
Czas „IT” wydaje się szczególnie kłopotliwy, ponieważ jest to instrukcja, która często byłaby używana w ciasnej pętli liczącej cykle, i jestem pewien, że nie zawsze można ją złożyć. Domyślam się, że jeśli zawsze rozgałęzia się do początku pętli wrażliwej na taktowanie, zmusza pętlę do rozpoczęcia na granicy słowa, unika wszelkich ładunków warunkowych lub zapisuje się w pętli i nie umieszcza od razu żadnej instrukcji „IT” po załadowaniu lub rejestracji aktualizacji sklepu czasy „IT” byłyby spójne, ale specyfikacja tego nie wyjaśnia.
supercat

1
Domyślam się, że dział IT prawdopodobnie (zgodnie z prawdą) zauważył coś takiego: „W przypadku braku stanów oczekiwania lub rywalizacji o magistralę kodową, składanie IT jest gwarantowane, jeśli (1) poprzednia instrukcja była instrukcją 16-bitową, do której nie uzyskano dostępu pamięć lub licznik programu oraz (2) albo następna instrukcja jest instrukcją 16-bitową, albo poprzednia instrukcja nie była celem „niezaangażowanej” gałęzi. Składanie IT może również wystąpić w innych nieokreślonych okolicznościach. ” Taka specyfikacja pozwoliłaby pisać programy z przewidywalnym czasem instrukcji IT, upewniając się, że kod został ułożony zgodnie ze wskazaniami.
supercat

1
Wow - przyznaję, że przeszedłem jedynie proste obliczenia cyklu najgorszego przypadku, zamiast walczyć z zastrzeżeniami pod stołem. Moja zaktualizowana odpowiedź podkreśla inne niejasności.
Kevin Vermeer

1
Istnieje wiele sytuacji, w których interesuje się liczeniem najgorszych przypadków, a spora liczba, gdy interesuje się liczeniem najlepszych przypadków (np. Jeśli port SPI może wysyłać jeden bajt co 16 cykli, generowanie każdego bajtu zajęłoby 14 cykli najlepszy przypadek, a sprawdzenie gotowości zajmie 5 cykli, sprawdzenie gotowości każdego bajtu ograniczy prędkość do jednego bajtu co 19 cykli najlepszy przypadek; pisanie na ślepo z dwoma dodanymi operacjami NOP pozwoli na szybkość jednego bajtu co 16 cykli najlepszy przypadek ). Przypadki, w których potrzebny jest precyzyjny pomiar czasu, nie są tak powszechne, ale mogą się pojawić.
supercat

3

Jednym ze sposobów obejścia tego problemu jest użycie urządzeń o deterministycznych lub przewidywalnych czasach, takich jak śmigło Parallax i układy XMOS:

http://www.parallaxsemiconductor.com/multicoreconcept

http://www.xmos.com/

Liczenie cykli działa bardzo dobrze w przypadku śmigła (należy użyć języka asemblera), podczas gdy urządzenia XMOS mają bardzo potężne narzędzie programowe, XMOS Timing Analyzer, który działa z aplikacjami napisanymi w języku programowania XC:

https://www.xmos.com/download/public/XMOS-Timing-Analyzer-Whitepaper%281%29.pdf


1
Zaczynam myśleć, że Leon ma udziały w XMOS ... ;-)
Federico Russo

1
Po prostu lubię ich żetony i ludzi, którzy tam pracują. Parallax to także miła firma z dobrymi produktami.
Leon Heller,

1
Tak, bez obrazy. Uderza mnie tylko to, że wszystkie odpowiedzi (oprócz jednej), w których wspomniany jest XMOS, pochodzą od ciebie. Nie ma nic złego w byciu entuzjastycznie nastawionym do czegoś.
Federico Russo,

@Federico, @Leon - Właśnie to martwi mnie trochę w XMOS: dlaczego na świecie jest tylko 1 użytkownik (przynajmniej tak to wygląda)? Jeśli jest tak wspaniale, dlaczego nie mówi się o mieście? Nigdy nie słyszałem, żeby ktoś o tym mówił, rzadziej go używaj.
stevenvh,

Wypróbuj fora XMOS: xcore.com
Leon Heller,

2

Liczenie cykli staje się coraz bardziej problematyczne, gdy uciekasz od mikrokontrolerów niskiego poziomu i przechodzisz do bardziej ogólnych procesorów obliczeniowych. Pierwsze zwykle mają dobrze określone terminy instrukcji, częściowo z powodów, dla których umieszczasz witrynę. Jest tak również dlatego, że ich architektura jest dość prosta, więc czasy instrukcji są stałe i można je poznać.

Dobrym przykładem tego jest większość PIC Microchip. Serie 10, 12, 16 i 18 mają bardzo dobrze udokumentowane i przewidywalne czasy instrukcji. Może to być przydatna funkcja w małych aplikacjach kontrolnych, do których przeznaczone są te układy.

Kiedy unikasz bardzo niskich kosztów, a projektant może w związku z tym poświęcić nieco więcej miejsca na chipy, aby uzyskać większą prędkość z bardziej egzotycznej architektury, możesz także uniknąć przewidywalności. Spójrz na nowoczesne warianty x86 jako ekstremalne przykłady tego. Istnieje kilka poziomów pamięci podręcznej, ożywienia pamięci, pobierania z wyprzedzeniem, potokowania itp., Co sprawia, że ​​liczenie cykli instrukcji jest prawie niemożliwe. W tej aplikacji nie ma to jednak znaczenia, ponieważ klient jest zainteresowany dużą szybkością, a nie przewidywalnością czasu instrukcji.

Możesz nawet zobaczyć ten efekt w pracy w wyższych modelach Microchip. 24-bitowy rdzeń (serie 24, 30 i 33) ma w dużej mierze przewidywalne taktowanie instrukcji, z wyjątkiem kilku wyjątków, gdy istnieją treści magistrali rejestrów. Na przykład w niektórych przypadkach maszyna wstawia przeciągnięcie, gdy następna instrukcja wykorzystuje rejestr z niektórymi trybami adresowania pośredniego, których wartość została zmieniona w poprzedniej instrukcji. Ten rodzaj przeciągnięcia jest niezwykły na dsPIC i przez większość czasu można go zignorować, ale pokazuje, jak te rzeczy się wkradają, ponieważ projektanci starają się zapewnić ci szybszy i bardziej wydajny procesor.

Więc podstawowa odpowiedź jest taka, że ​​jest to część kompromisu przy wyborze procesora. W przypadku małych aplikacji sterujących możesz wybrać coś małego, taniego, o małej mocy i przewidywalnym czasie instrukcji. Gdy potrzebujesz większej mocy obliczeniowej, zmienia się architektura, więc musisz zrezygnować z przewidywalnego czasu instrukcji. Na szczęście nie stanowi to większego problemu w przypadku aplikacji wymagających większej mocy obliczeniowej i zastosowań ogólnych, więc uważam, że kompromisy działają całkiem dobrze.


Zgadzam się, że ogólnie aplikacje wymagające większej mocy obliczeniowej stają się mniej wrażliwe na mikroskopijne taktowanie, ale istnieją pewne scenariusze, w których można potrzebować nieco więcej przetwarzania niż PIC-18, ale także potrzebować przewidywalności. Zastanawiam się, w jakim stopniu powinienem starać się nauczyć takich rzeczy, jak 16-bitowe architektury PIC, lub w jakim stopniu powinienem pomyśleć, że ARM będzie prawdopodobnie odpowiedni.
supercat

0

Tak, nadal możesz to zrobić, nawet na ARM. Największy problem z ARM polega na tym, że ARM sprzedaje rdzenie, a nie układy scalone, a taktowanie rdzenia jest znane, ale to, co owija go dostawca układów, różni się od dostawcy do dostawcy, a czasem od rodziny układów do innego w obrębie dostawcy. Tak więc konkretny układ od konkretnego dostawcy może być dość deterministyczny (jeśli na przykład nie używasz pamięci podręcznych), ale trudniej go przenieść. W przypadku 5 zegarów tutaj i 11 zegarów przy użyciu timerów jest problematyczne, ponieważ liczba instrukcji potrzebnych do próbkowania timera i ustalenia, czy upłynął limit czasu. Z dźwięków z poprzednich doświadczeń programistycznych jestem skłonny założyć się, że prawdopodobnie debuguję za pomocą oscyloskopu, tak jak ja, więc możesz wypróbować ciasną pętlę na chipie z częstotliwością zegara, spojrzeć na spi lub i2c lub jakikolwiek kształt fali, dodać lub usuń nops, zmień liczbę razy w pętli i po prostu dostrój. Jak w przypadku każdej platformy, nieużywanie przerw znacznie pomaga deterministyczny charakter wykonywania instrukcji.

Nie, nie jest to tak proste jak PIC, ale wciąż całkiem wykonalne, szczególnie jeśli opóźnienie / taktowanie zbliża się do częstotliwości taktowania procesora. Wielu dostawców opartych na ARM pozwala zwielokrotnić częstotliwość zegara i uzyskać powiedzmy 60 MHz z odniesienia 8 MHz, więc jeśli potrzebujesz jakiegoś interfejsu 2 MHz zamiast robienia czegoś co 4 instrukcje, możesz zwiększyć zegar (jeśli masz budżet energetyczny), a następnie użyj timera i daj sobie mnóstwo zegarów, aby robić również inne rzeczy.

Korzystając z naszej strony potwierdzasz, że przeczytałeś(-aś) i rozumiesz nasze zasady używania plików cookie i zasady ochrony prywatności.
Licensed under cc by-sa 3.0 with attribution required.