Jak szybkie są porty cyfrowe Arduino?

Sprawdzanie czasów generowania sygnałów.analizatorów Saleae.

Autorzy: KG + ?
Zaawansowanie: badania trwają, są już wstępne wyniki
Cel: wiedza, wiedza, wiedza – potem przełożona na praktykę

Motywacja

Mikrokontroler ATmega 328P znajdujący się w platformie Arduino UNO (i innych, ale ta jest najbardziej popularna) dysponuje 14-toma wejściami/wyjściami cyfrowymi (skupiam się jedynie na portach cyfrowych, pomijam analogowe). Porty te zgrupowane są w trzech bankach (B,C,D) i mamy do nich dostęp za pomocą funkcji digitalWrite() lub bezpośrednio – przez manipulację rejestrami. Powstaje pytanie, jak szybko możemy zmieniać stan wysoki na niski w danym porcie? Jak się to ma wykorzystując dedykowaną funkcję z Arduino IDE a jak programując rejstry? Kolejne pytanie, to jak szybko możemy odczytywać zmiany napięć (np. funkcjami obsługi przerwań). Jedno z drugim jest związane i często potrzebne, np. w przypadku obsługi enkoderów do silników. Takie enkodery mogą generować 64 impulsy na obrót (co jest bezproblemowe do wyłapania przez Arduino), ale często silniki mają przekładnię (np. Pollolu 37Dx70L ma przekładnię 70:1) i liczba impulsów na sekundę wynosi już 4480. Czy Arduino odczyta wszystie te impulsy?

Sprawdzamy czasy wyjścia.

Pierwszy program to prawie kopia popularnego Blink-a, jednak tym razem zmieniam stan pinu numer 7 (zdefiniowanego 1 pierwszej linijce kodu pod nazwą PIN) i robię to bardzo szybko – co 1 ms. Celowo nic nie wypisuję na port szeregowy, aby nie powodować niepotrzebnych opóźnień.

Program 1: program testowy w dialekcie C/C++ Arduino.

Oczywiście zmian napięć na porcie 7 nie widać, nawet gdybyśmy podłączyli multimetr – ten powinien wskazać około 2.5V (gdyż podczas trwania mierzenia wartości napięcia na multimetrze czas trwania napięcia 5V = czasowi trwania napięcia 0V, co średnio daje właśnie 2.5V). Najlepiej podłączyć dobry oscyloskop, ale niewiele osób takowy posiada (to wydatek przynajmniej 2000 zł). Tanie „chińskie” mini-oscyloskopy (za ~200zł) jeszcze z tym sobie poradzą, ale za chwilę przyspieszymy przełączanie pinów i takie sprzęty będą bezużyteczne. Dlatego w tym wpisie skupię się na analizatorze stanów logicznych Saleae, a dokładniej ich klonów (ze względu na cenę, ale do oryginałów jeszcze wrócę). Poniżej aukcja w popularnym serwisie pokazująca o czym mowa.

Podłączamy więc GND i CH1 analizatora Saleae do Arduino z uruchomionym pierwszym programem i sprawdzamy, co się dzieje (ja ustawiłem czas próbkowania na 0.01 s, aby pomiar był szybki).

Wykres 1 – Wynik Programu 1: sprawdzenie poprawności odczytów pierwszego programu.

Widzimy prostokątny przebieg w części Channel 0 (choć na tanim klonie analizatora Saleae naklejka mówi, że jest to port pierwszy – czyli Channel 1). Myszą sprawdzamy jakie są faktyczne czasy trwania sygnału HIGH i LOW. Na powyższym zdjęciu czas sygnału LOW to 1.008292 ms, czyli bardzo, bardzo zbliżone do 1 ms. Dokładniejsze sprawdzanie kolejnych sygnałów może pokazać, że inne sygnały LOW różnią się niewiele – nie są identyczne. Cóż, tak właśnie działa elektronika (nieidealnie). Przy okazji widzimy, że sygnał jest periodyczny z częstotliwością 496 Hz (napis Freq z lewej strony).

Zmieniamy program wyrzucając funkcje opóźniające i sprawdzamy, ile możemy maksymalnie „wycisnąć” z Arduino.

Program 2: Wyciskamy maksimum z Arduino (program w dialekcie C/C++ Arduino).

Pozbyliśmy się więc opóźnień i testujemy z wykorzystaniem analizatora Saleae. Po powiększeniu skali (kółko myszki) widzimy coś takiego:

Wykres 2 – Wynik Programu 2.

Czyli bez funkcji delay(1) sygnał HIGH trwa 3.208 us, natomiast sygnał LOW jakby trochę dłużej (6.75 us – 3.208 us = 3.542 us). Potwierdzają to kolejne fragmenty wykresu (znowu nie są identyczne, ale bardzo zbliżone). Różnica tych czasów (około 0.300 us) zostanie wyjaśniona w dalszej części wpisu (a może sam się już domyślasz, skąd się bierze?). Zauważamy, że częstotliwość wzrosła i wynosi 148.148 kHz, czyli 148148 Hz. Czy to już maksymalna wartość?

Programujemy rejestry

Dialekt C/C++ Arduino jest fajny, prosty i posiada użyteczne funkcje, jak pinMode() czy digitalWrite(). Ale nie musimy ich stosować, aby „dobrać” się do portów Arduino. Poniższy schemat przedstawia piny cyfrowe I/O w nazewnictwie Arduino UNO (kolor niebieski: ARDUINO PIN) oraz prawdziwe nazwy mikrokontrolera ATmega328 (kolor zielony: PORT PIN). Widzimy więc, że popularny Arduino pin 13, do którego standartowo podłączony jest LED to faktycznie pin PB5. Ja z kolei użyłem Arduino pin 7 w pierwszych programach, czyli PD7. Sprawdź, czy umiesz to odczytać z poniższego obrazka i nie przejmuj się, że z jakiegoś powodu piny nie są w kolejności, a czasami „skaczą” z jednej strony układu na drugą (dodatkowo: czy zauważyłeś, że są „nowe” piny 20 i 21 – do których nie masz dostępu z Arduino IDE?).

Nazewnictwo portów dla ATmegi 328 – patrz kolor niebieski i zielony.

Programowanie rejstrów polega na odwoływaniu się do konkretnych komórek pamięci w mikrokontrolerze. Na ogół zmieniamy jeden bit, które odpowiada za dany pin. Ale można od razu zmieniać cały bajt – czyli 8 portów na raz! W naszym przypadku najpierw trzeba ustalić rolę pinu – wejście czy wyjście – za to odpowiada rejestr DataDirectionRegisterX który pełni rolę wywołania funkcji pinMode(), a potem włączyć lub wyłączyć dany bit (ponownie: funkcja digitalWrite() w dialekcie Arduino, a w rejestrach to PORTX). X na końcu nazw rejestrów to właśnie banki portów, u nas B,C lub D.

Programujemy wyjście cyfrowe PD7 (czyli Arduino pin 7) bez funkcji z Arduino IDE – patrz poniższy program. Linijka 2 to odpowiednik instrukcji pinMode(), tym razem działająca dla całego „banku D” pinów (czyli od razu dla wszystkich ośmiu pinów Arduino 0,1,2,3,4,5,6,7). Każdy bit w rejestrze DDRD (Data Direction RegisterD) odpowiada jednemu portowi, ja tu wpisałem 255 czyli B11111111 (same jedynki). Jedynka oznacza OUTPUT, natomiast zero oznacza INPUT (łatwiej było by to zapamiętać, gdyby 1=INPUT, a 0=OUTPUT – ale tutaj jest na odwrót, czyli 1=OUTPUT, 0=INPUT). Cała linijka 2 programu odpowiada więc ośmiu instrukcjom pinMode(0, OUTPUT), pinMode(1, OUTPUT), …,pinMode(7, OUTPUT). Tylko jedna instrukcja przypisania pojedynczego bajta do rejestru DDRD zastępuje nam wywołanie ośmiu funkcji pinMode()! Wiem wiem, ponieważ pracuję tylko z jednym portem PD7 powinienem wykonać DDRD=B10000000; czyli OUTPUT tylko na nim, a reszta portów w tym pinie jako INPUT – ale tak mi się jakoś cały bank ustawiło na OUTPUT i tego się będę dalej trzymać.

Program 3: test programowania portów.

Linijka 6 to włączenie w stan HIGH portu PD7, inne porty w tym banku pozostają bez zmian. Robię to z wykorzystaniem instrukcji sumy bitowej tego, co jest aktualnie w rejestrze D (rejestr nazywa się PORTD) z bajtem B10000000. Odpowiada to wywołaniu funkcji digitalWrite(7, HIGH). Podobnie jak w linijce 2 mogłem zaprogramować cały bank D w jednej instrukcji przypisania, tak i tutaj mogłem (np. PORTD=B00001111 to włączenie w stan HIGH portów PD0, PD1, PD2 oraz PD3 jednocześnie włączając w stan LOW portów PD4, PD5, PD6 i PD7. Ja nie chciałem zmieniać innch portów, tylko PD7, dlatego użyłem instrukcji sumy bitowej). Następnie czekam 1 ms. Linia 8 to przełączenie PD7 w stan LOW, jednocześnie pozostawiając bez zmiany porty PD1..PD7 (zastosowałem iloczyn bitowy). Kompiluję i wgrywam. Sprawdzam na analizatorze – wynik poniżej:

Wykres 3 – Wynik Programu 3.

OK, wszystko działa jak powinno: mamy częstotliwość Freq=495.97 Hz i sygnały HIGH i LOW trwające 1 ms (z malutkim ogonkiem). Programowanie rejestrów nie jest takie trudne, prawda? 😉 Można więc pozbyć się pętli opóźniających (linijki 7 i 8) i zobaczyć, jak teraz będzie działać przełączanie portów.

Program 4: programowanie portów.

Wynik powyższego programu w analizatorze Saleae:

Wykres 4 – Wynik Programu 4.

Zaczynają się dziać ciekawe rzeczy 😉 Stan HIGH trwa 125 ns (nano sekund! a w Programie 2 było to 3.208 us = 3208 ns, czyli ponad 25x więcej!). Ale dziwne, bo stan LOW nie trwa tyle samo co stan wyoski, a jedynie 500ns-125ns = 375 ns. Skąd ten brak symetrii? Zresztą, różnica czasów trwania stanów HIGH/LOW pojawiła się już wcześniej – podczas omawiania wyników działania Programu 2 (patrz dyskusja pod Wykresem 2) i wynosiła właśnie ~0.3 us czyli ~300 ns. Dlaczego włączenie HIGH trwa krócej niż LOW? Tak być nie może, tu chodzi o coś innego… Moje przepuszczenie (robocza teza): po wywołaniu dwóch instrukcji w funkcji loop() Proramu 4 ponownie wywołuje się funkcja loop() – czyli mamy dodatkowe instrukcje skoku (dodatkowe opóźnienie). Aby to sprawdzić, rozbudowuję funkcję loop() o kolejne dwa cykle włączenia/wyłączenia tego samego pinu – wówczas loop() wywoła się po trzech przełączeniach portu PD7. Oto i program:

Program 5: trzykrotne przełączenie PD7 w jednej funkcji loop().

Jak to działa? Poniżej obraz z Saleae:

Wykres 5 – Wynik Programu 5.

Bingo! Faktycznie, teraz widać, że mamy trzy cykle włączenia/wyłączenia stanu na porcie, a potem dłuższą pauzę, czyli stan LOW się wydłużył. Widać też, że po pierwszym włączeniu PD7 na HIGH następuje stan LOW trwający dokładnie tyle samo co poprzednio stan HIGH – czyli 125 ns. Patrz rysunek poniżej.

Wykres 6a – Wynik Programu 5 – pomiar czasu trwania stanu LOW.

 

Wykres 6b – Wynik programu 5 – pomiar czasu trwania stanu HIGH.

Co z tego wynika? Wykresy 6a i 6b wskazują, że częstotliwość sygnałów HIGH/LOW wynosi 4 MHz (ponad 27x szybciej niż maksimum, jakie „wycisnąłem” programując w dialekcie C/C++ Arduino, patrz Wykres 2). Czas trwania tych sygnałów to 125 ns. Uzyskałem niezłe przyspieszenie programując porty i nie korzystając z funkcji digitalWrite().

Częstotliwość pracy ATmegi 328

Arduino UNO bazuje na ATmedze 328p, działającym z kwarcem 16 MHz.Oznacza to, że zegar „klika” co 1/16000000 s = 62.5 ns. Czas wykonania każdej instrukcji można znaleźć w specyfikacji mikrokontrolera gdzie widać, że operacje sumy/iloczynu logicznego zajmują dwa cykle zegarowe (dwa „kliki”), a przypisanie wartości tylko jeden „klik”. Jak na razie widzimy, że przełączenie portów trwa 125 ns, czyli faktycznie dwa cykle (manipulacje na banku rejestr PORTD wymagają użycia instrukcji asemblera SEI/CLI trwającej 2 „kliki”, porównaj tabela 31. Instruction Set Summary ze specyfikacji ATmegi 328p). Dlatego rozsądnie jest zmodyfikować program na okoliczność moich testów tak, że zmieniam wszystkie 8 portów (choć i tak zależy mi na jednym) co zajmie jeden cykl (instrukcja OUT w Tabeli 31). Nowy program wygląda tak:

Program 6: trzykrotne przełączenie PD7 w jednej funkcji loop(). Program bez operacji sumy/iloczynu bitów.

Po zmianie na Program 6 widzę poniższy wykres:

Wykres 8a: wynik działania Programu 6. 

 

Wykres 8b: wynik działania Programu 6. 

Widać charakterystyczne trzy przełączenia i wydłużony stan LOW na ponowne wykonanie pętli loop(). Ale stany HIGH trwa 83 ns, podczas gdy stan LOW jakby połowę tego czasu, czyli 42 ns! Ponownie uruchamiam zbieranie danych (no dobra, kilka razy aż trafiam to, co chcę poniżej przedstawić):

Wykres 9a: wynik działania Programu 6. Ponowne uruchomienie.

 

Wykres 9b: wynik działania Programu 6. Ponowne uruchomienie.

Wykresy 9a i 9b wskazują, że teraz czasy się „odwróciły”! HIGH trwa 42 ns, a LOW trwa 83 ns. Jakaś pomyłka? Nie, bo takie Arduino UNO działa z zegarem 16 MHz, czyli zegar „klika” co 1/16000000 s = 62.5 ns. Programowanie PORD=ileś_tam odbywa się w jednej instrukcji asemblera OUT PORD, ileś_tram, co zgodnie ze specyfikacją ATmegi 328p (Tabela 31. Instruction Set Summary) zajmuje właśnie 1 CYKL (jeden „klik”). Z kolei Saleae próbkuje z częstością 24 MHz, czyli w łapie sygnały w oknach po 1/24000000 = 42 ns. Sygnały z Aruino o długości 62.5 ns wchodzą nie do jednego okna, ale do „jednego i pół” 😉

Widać, że zwiększyliśmy szybkość generowania impulsów HIGH/LOW i teraz pokazuje się Freq = 8 MHz (czyli 2x więcej niż w Programie 5, oraz ponad 50x szybciej niż Program 2).

Myślę, że sprawa 42/83 ns jest już to jasne – ze względu na brak synchronizacji pracy Saleae i pracy Arduino UNO (no bo skąd miała by być taka synchronizacja) analizator raz interpretuje sygnał jako stan HIGH, a raz jako LOW. Arduino zmienia sygnały co 63 ns, a próbkowanie odbywa się co 42 ns – schematyczny rysunek powinien to jeszcze raz zobrazować:

Szkic 1: próbkowanie sygnału z Arduino analizatorem stanów logicznych.

Dodatkowe wyjaśnienie Szkicu 1: a) krzywa zielona to sygnał z Arduino (działa Program 6) – rzeczywisty (bo trwa 62.5 ns) ale trochę wyidealizowany (prostokąty są ostre! a dobrej klasy oscyloskop pokaże, że na zboczach narastających i opadających pojawiają się oscylacje); b) okno  próbkowania analizatorem trwa 42 ns i nie musi trafić w początek/koniec sygnału z Arduino – jest gdziekolwiek; analizator sprawdza, który ze stanów przeważa w oknie próbkowania – stan HIGH czy stan LOW – i na tej podstawie przypisuje odpowiednio 1 lub 0 (oczywiście w omawianym przypadku b) będzie to 1, bo przez całe okno mamy stan HIGH); c) kilka następujących po sobie próbkowań – aby rysunek był wyraźniejszy, zmieniam kolor okna próbkowania na jaśniejszy (co drugie próbkowanie); widzimy, że drugie próbkowanie dało wynik 0 (przeważał stan LOW), kolejne dało 1 (stan LOW był około 1/3 czasu, przeważał stan HIGH) a ostatnie próbkowanie ponownie dało wartość 1 (znowu przeważał stan HIGH). Ten przykład przedstawia sytuację, gdzie odczyt analizatorem pokazał by stan wysoki z czasem trwania 42 ns, potem czas niski z tym samym czasem 42 ns, a potem stan wysoki trwający 84 ns. Takie sytuacje występują podczas eksperymentów z Programem 6. Łatwo sobie też wyobrazić inny moment startu próbkowania i sytuację, gdzie zawsze trafiamy naprzemiennie 84 ns dla jednego stanu i 42 ns dla stanu przeciwnego – patrz Szkic 2 poniżej.

Szkic 2: Wyjaśnienie dwukrotnie dłuższego stanu HIGH w stosunku do czasu stanu LOW.

W powyższym obrazku wystarczy lekko przesunąć czas rozpoczęcia próbkowania aby mieć sytuację odwrotną – czas trwania sygnału LOW będzie 2x dłuższy od czasu trwania sygnału HIGH.

Prawdziwe analizatory Saleae

Ja używam taniego klona, ale oryginalne mają nie tylko analizatory cyfrowe, ale i wejścia analogowe – to już są małe oscyloskopy! Występują w trzech rodzajach a poniżej tabelka z porównaniem parametrów (ze strony Kamami – kliknij w tabelkę aby przeskoczyć do ich sklepu):

Porównanie specyfikacji analizatorów Saleae.

Super urządzenia, ale na osłodzę zapał naiwnego czytelnika ich cenami: zaczynają się od niecałych 2000 zł a kończą na 5500 zł. Może te klony za 30 zł nie takie złe? 😉

BitScope

To bardzo fajny, hobbystyczny oscyloskop + 8-mio kanałow analzator stanów logicznych za kwotę ~300 zł. Do jego pracy potrzebny jest komputer z ekranem, a podałącza się go przez USB. Najlepsze, że działa on pod Linuxem, Maciem i Windowsem (podreślę – działa pod Linuxem, więc w sieci jest sporo projektów BitScope + Raspberry Pi). Ciekawą ofertę na niego ma sklep Botland.

Oscyloskop cyfrowy BitScope – mały, ale dzik! 😉

Parametry tego urządzenia są bardzo przyzwoite jak na cele hobbystyczne (do nauki elektroniki, analizy układów LRC czy innych) i wynoszą: próbkowanie 20 MHz z 40Msps. Jednak to nie wystarcza, aby poprawnie zinterpretować wynik Programu 5:

Wynik Programu 5 na BitScopie.

Działam tutaj na maksymalnej częstości 20MHz (podziała czasowa 1 us) i choć widać trójki pików – to słabo z precyzyjnym odczytaniem ich czasów. Przełączenie BitScopa w tryb analizatora stanów logicznych pomaga – choć i tak jest słabszy niż omawiany klon Saleae ze względu na próbkowanie jedynie 20 MHz (przypominam: Saleae ma 24 MHz).

RIGOL 1204B

Skoro testuję różne sprzęty, to przedstawię profesjonalny oscyloskop stacjonarny za kwotę około ~5000 zł (choć tak wysoka cena wynika z posiadania aż czterech kanałów – podczas gdy typowo mamy dwa kanały analogowe; odpowiedni model z dwoma kanałami to już wydatek jedynie ~3500 zł). Już na obudowie można odczytać, że oscyloskop ten pracuje z 200 MHz i 2 Gsps.

Program 6 na oscyloskopie. Widać brak idealnych prostokątów w rogach sygnałów.

Nie będę robić telefonem screen-shotów tylko wykorzystam funkcję USB i zapisu przebiegu z oscyloskopu bezpośrednio na pendrv (mogę do CSV, jakiś innych formatów lub obrazka PNG – jednak jest to zrzut ekranu graficznego działającego w trybie 320×240 pikseli – i dlatego rozdzielczość plików PNG nie powala).

Tak zapisuje RIGOL swoje screenshoty do formatu PNG (szkoda, że takie to małe!). Są inne formaty (CSV, WAVEFORM).

 

Powiększenie na pojedynczy pik (skala x=20 ns) – pomiar długości trwania (tak ustawiłem kurosry, że mi wyszło 69.6 ns).

 

Pomiar ponownego wywołania funkcji loop() – wychodzi mi 296 ns.

A tani chińczyk? DSO188

Ten miniaturowy oscyloskopik „zabawka” ma kolorowy ekranik LCD 1.8 cala (nie potrzebuje więc komputera!) oraz wbudowaną baterię, czyli działa „w terenie” 😉 Jego parametry próbkowania to 1 MHz oraz 5 Msps co od razu wskazuje, że dużo tutaj nie zdziała (dodam jeszcze: to oscyloskop jednokanałowy bez analizatora stanów). Jednak za kwotę około ~200 zł mamy fajną zabawkę do przygód z Arduino – i nie musimy go podłącząć do przewodów zasilających!

DSO 188 z programem 5. Tiaaaa…

Choć nie poradził sobie, to jednak go polecam! Oczywiście po chwili zastanowienia, bo BitScope jest zdecydowanie lepszy, ale musi być podłączony do komputera. Ten działa niezależnie (szybko i na baterii). Poniżej zdjęcie z Programem 3:

DSO188 z Programem 3 – jest dobrze!

Jak widać wyraźnie odczytujemy sygnały zmieniające się co 1 ms, o czasie trwania 1ms (podziałka 500 us, a na wykresie mamy białą krzywą pokrywającą dwie podziałki – czyli 1000 us = 1ms właśnie). Gdzie go kupić? Polecam gotronika (ma też filmy z wideo-recenzjami) i aukcje w popularnych serwisach.

Minikombaj AVR

No to jeszcze jeden, ostatni (ale nie najsłabszy). Jest to dwukanałowy oscyloskop działający na bateriach, z monochromatycznym ekranikiem i próbkowaniem 0,3 MHz.

Jest to sprzęt sprzedawany przez firmę AVR jako kit 2999. To nie tylko oscyloskopik ale i generator fal, widma…

Ile trwa wywołanie funkcji loop()?

Wychodzi na to, że ponowne wywołanie loop()-a trwa 300 ns (plus jeszcze epsilon). Z czego to wynika? Na tą chwilę powiem, że trzeba wywołać instrukcję skoku do funkcji – a tych jest kilka rodzajów. Poniżej pokażę, że da się skrócić to 300 ns o 1/3! Poniższy fragment tylko dla zuchwałych 😉

Tylko dla twardzieli – programujemy w asemblerze

Nie będzie to czysty asembler, a wstawka asemblera w język Arduino. Dlaczego taka hybryda? Aby uprościć i nie zniechęcić zwykłego człowieka 😉

Program 7: programowanie ze wstrzyknięciem kodu w asemblerze.

Funkcja setup() jest taka sama jak w Programie 6, czyli ustawiamy rejestr DDRD na OUTPUT. Robię to bez asemblera, jak poprzednio. Wstawka w asemblerze pojawia się dopiero w funkcji loop() – po makrze __asm__( instrukcje ); Jednak nie widać odwołań do rejestru PORTD. Nazwa tego rejestru dla wstrzykniętego kodu asemblera nie jest rozpoznawalna (można to obejść, zdefiniować, ale nie chciałem zaciemniać programu). Dlatego pojawia się adres 0x0B, czyli właśnie PORTD (sprawdź w specyfikacji ATmegi328p, a dla leniwych podpowiem: przyjrzyj się punktowi 13.4.8). Wstawka w asemblerze to para instrukcji:

sbi 0x0B,7
cbi 0x0B,7

Instrukcje te w kodzie Arduino zakończone są przejściem do nowej linii (mogła być spacja, lub tabulator… pojawia się dodatkowe \n na koniec każdej linii). SBI to instrukcja „Set Bit in I/O regiter”, po której podajemy który bit ma być ustawiony na 1 (u mnie: ma to być 7-my bit w rejestrze 0x0B czyli PD7). Potem następuje czyszczenie tego bitu, czyli CBI – „Clear Bit in I/O register” (u mnie: czyszczę na zero bit 7, czyli właśnie PD7). Każda z tych instrukcji wymaga 2 cyklów, więc działanie Programu 7 widoczne jest na wykresach poniżej:

Wykres 10: wynik programu 7.

Sygnał HIGH trwa 2×62.5 ns czyli 125 ns, tyle samo trwa sygnał LOW, a ponowne uruchomienie loop()-a to 300 ns – razem 500 ns, co zgadza się z powyższym rysunkiem. Lekko zmodyfikujmy kod, aby jak poprzednio generowane były 3 przełączenia:

Program 7a: trzykrotne przełączenie pinu 7 w Arduino, kod w asemblerze.

Wynik jest łatwy do przewidzenia, poniżej obraz:

Wykres 10a: wynik programy 7a.

Zwróć uwagę, że widać trzy równe sygnały HIGH (każdy po 125 ns), rozdzielone doma przerwami (także po 125 ns – a trzecia przerwa, ostatnia dłuższa – o kolejne 300 ns). Ale ale, poprzednio powiedzieliśmy sobie, że operacje bitowe (SBI, CLI) trwają „aż” 2 cykle, i aby to przyspieszyć, można zastąpić to bezpośrednim zmienieniem całego rejestru (nadpisaniem). W asemblerze robi się to przez instrukcję OUT a kod powinien wyglądać tak:

OUT 0x0B, 0x80

Czyli chcemy zapisać bajt B1000000 (przypominam: 7 bit 1, reszta 0) do rejestru PORTD (0x0B). W asemblerze nie mogę użyć Arduiinowego B10000000 więc powinienem wpisać wartość 128 (dziesiętnie), czyli 0x80 (szesnastkowo). Jednak… instrukcja OUT tego nie przyjmie. Ona ładuje do rejestru 0x0B nie konkretną wartość, ale wartość innego rejestru. Trzeba więc najpierw zapisać sobie wartość 0x80 do wolnego, roboczego rejestru (ja to zrobię do rejestru r16) a następnie tak go wykorzystam:

OUT 0x0B, r16

Całość wygląda tak:

Program 8a: nadpisanie całego rejestru PORTD bez instrukcji SBI/CBI.

 

Wykres 11a: wynik działania Proramu 8a. Stan HIGH trwa krótko (w rzeczywistości 62ns, ale dla analizatora to 42 ns).

Oczywiście w kodzie pojawił się też roboczy rejestr r17, który przechowuje wartość zero. No i kolejna drobnostka: jeden raz, na początku w setup()-ie, ładuję odpowiednie wartości do rejestrów r16 i r17 (nie chcę tego robić za każdym razem, bo bym tracił kolejne cykle na wywołanie instrukcji LDI). Minimalna modyfikacja kodu o generowanie trzech przełączeń:

Program 8b: trzy przełączenia!

Wynik działania poniżej (dwie wersje, bo raz analizator informuje o 42 ns, a raz o 82 ns – mam nadzieję, że wiesz, dlaczego):

Wykres 12a: wynik programu 8b. Pomiar długości sygnału HIGHT.

 

Wykres 12b: wynik programu 8b. Pomiar długości przerwy loop(), czyli 333 ns (minus jeden OUT).

 

Wykres 12c: wynik programu 8b – kolejne uruchomienie, zamiana czasów HIGH/LOW.

Ponownie widzimy mega-szybkie przełączanie pinów (8 MHz), a czas trwania przerwy pomiędzy trójkami pików wynosi 333 ns. Ale 333 ns można jeszcze ulepszyć! Czyli skrócić przerwę pomiędzy kolejnymi wywołaniami funkcji loop() – a właściwie zrobić tak, aby loop() wywołała się tylko jeden raz, a wewnątrz niej bezwarunkowo odbywał się skok do zdefiniowanego miejsca w kodzie.

Program 9: skok wewnątrz kodu asemblera.

W tej wersji wstrzykniętego kodu asemblera definiuję (linia 10) adres kodu jako Loop a następnie, po wykonaniu trzech przełączeń (jak poprzednio, w Programie 8) wykonuję bezwarunkowy skok do tego miejsca programu (instrukcja RJMP, która trwa 2 cykle zegara – i stąd to przyspieszenie!). W takiej wersji programu funkcja loop() wykonuje się tylko jeden raz, a ponowne skoki do przełączeń – wewnątrz niej. Szybciej się nie da! Wynik poniżej.

Wykres 13a: wynik programu 9 – trójki stanów HIGH/LOW, super szybkie…

 

Wykres 13b: wynik programu 9 – najkrótsze opóźnienie pomiędzy skokiem programu!

Na koniec coś dla zakochanych w asemblerze – każdy asemblerowiec zna instrukcję NOP (no operation – pusty przebieg jednego cyklu, czyli nic-nie-robienie). Wbrew pozorom instrukcja NOP jest super użyteczna, np. w celu wprowadzenia pauzy (opóźnienia). Z tabeli 31 wiemy, że NOP trwa 1 cykl zegara, czyli wywołanie tej instrukcji to przerwa na 62.5 ns. Można zdefiniować pętlę takich instrukcji, aby mieć funkcję czekającą np. 10 us itd. Poniżej jednak ograniczę się do pojedynczego spowalniania kodu:

Program 10: kod w asemblerze z instrukcją NOP (no operation).

Jak można się spodziewać – sygnał HIGH trwa 2 cykle (bo jeden to NOP, a drugie to dziasłanie OUT-a), a ponim pojawia się sygnał LOW rozdzielony 1 cyklem (analizator wskaże czasami 42 ns, czasmi 84 ns).

Wykres 14: wynik działania programu 10.

Podsumowanie

Wykorzystując analizator stanów logicznych Saleae udało się sprawdzić, że porty Arduino możemy przełączać z częstotliwością 150 kHz gdy programujemy w dialekcie Arduino (z wykorzystaniem funkcji digitalWrite(), porównaj Program 2), albo nawet 8 MHz (czyli ponad 50x szybciej!) gdy programujemy bezpośrednio rejestry ATmegi (PORTD, porównaj Program 6). Myślę, że posługiwanie się portami nie jest trudne, choć wymaga wykorzystania operacji logicznych na bitach (ale można zyskać nawet 50x przyspieszenie!). Okazuje się także, że ponowne wywołanie funkcji loop() nie jest „za darmo” – to też przerwa ~300 ns (lub 200 ns, jeśli chcemy się bawić w asembler). Mam nadzieję, że prezentacja różnych oscyloskopów była ciekawa.

O tym, że programując bezpośrednio rejestry portów zyskujemy bardzo dużo – niech przemówią poniższe przykłady zbudowania 8-bitowego wyjścia analogowego dla Arduino i wygenerowanie przebiegu piłokształtnego i sinusa (wiadomo, jestem fizykiem, stąd tak mało wyszukane przykłady…). Jest to temat z jednego z wykładów, a tutaj przedstawię tylko kilka wyników:

Arduino z 8-bitowym analogowym wyjściem (drabinka R2R). Układ z wykładu dla studentów z fizyki.

 

Wykres 15: porównanie częstotliwości generowanych sygnałów analogowych: (lewa strona) programowanie w dialekcie Arduino, (prawa strona) programowanie portów. Przyspieszenie 25x. Dodatkowo: lewy obraz mocno zaszumiony. Wykorzystano BitScope.

 

Wykres 16: porównanie częstotliwości generowanych sygnałów analogowych: (lewa strona) programowanie w dialekcie Arduino, (prawa strona) programowanie portów. Przyspieszenie 20x. Dodatkowo: szumy dla pierwszego przypadku. Wykorzystano BitScope.

Odczytywanie sygnałów – przerwania

Niebawem…..

 
void setup() {
  Serial.begin(9600);
}
int licz=0;
void loop() {
Serial.print(millis());
Serial.print(" ");
licz++;
}