Witam!
Port szeregowy w arduino to bardzo przydatne a przy tym bardzo proste w obsłudze narzędzie. Umożliwia łatwą komunikacje z komputerem, przesyłanie danych pomiędzy mikrokontrolerami a nawet obsługę niektórych modułów (np. modułów GPS).
Tak więc postanowiłem napisać krótki tutorial na temat podstaw korzystania z tego interface-u.
No i napisałem (:
Podstawowe informacje o portach szeregowym w arduino :
Port szeregowy jest to port komputerowy, przez który dane są przekazywane w formie jednego ciągu bitów.
Jest to bardzo szerokie pojęcie i odnosi się ono do wielu różnych standardów komunikacji, na przykład do RS-232.
W arduino występują dwa rodzaje portów szeregowych : asynchroniczne i synchroniczne. W synchronicznych równolegle z danymi, podawany jest sygnał zegarowy który synchronizuje odczyt danych. W asynchronicznych takiego sygnału nie ma.
Do portów synchronicznych należą na przykład SPI i I2C, (sygnał zegarowy na SCL i SCK).
Port którego dotyczy ten tutorial jest nazywany po prostu portem szeregowym, (nie udało mi się znaleźć nazwy tego portu, UART to nazwa urządzenia przesyłającego dane a nie portu Universal asynchronous receiver/transmitter = UART )
Przesył danych za pomocą tego portu odbywa się poprzez dwa piny :
- RX – Do danych wchodzących do urządzenia.
- TX – Do danych wychodzących z urządzenia.
Generalnie jest to jeden z najprostszych w użytkowaniu interface-ów komunikacyjnych jakie można znaleźć w płytkach arduino.
Podłączenia i kwestie sprzętowe
Pod względem sprzętowym użytkowanie tego interface-u jest proste.
Jeśli chodzi o najczęściej spotykaną konfigurację połączeń to jest to konfiguracja dwu-urządzeniowa.
Pin TX pierwszego urządzenia jest podłączony do pinu RX drugiego urządzenia. Natomiast pin TX drugiego urządzenia jest podłączony do pinu RX pierwszego urządzenia.
W tej konfiguracji kiedy jedno urządzenie wysyła dane, drugie je odbiera i na odwrót.
Nic nie stoi jednak na przeszkodzie aby podłączyć kilka urządzeń peryferyjnych do jednego urządzenia nadrzędnego.
W takiej sytuacji wszystkie piny RX urządzeń podrzędnych są podłączone do pinu TX urządzenia nadrzędnego a piny TX urządzeń podrzędnych są podłączone do pinu RX urządzenia nadrzędnego.
W takiej konfiguracji kiedy urządzenie nadrzędne nadaje, wszystkie urządzenia podrzędne słuchają. Natomiast kiedy nadaje urządzenie podrzędne, informacje trafiają do urządzenia nadrzędnego.
Jednak przy łączeniu większej ilości urządzeń trzeba mieć na uwadze możliwość kolizji danych ( kilka urządzeń nadaje w tym samym czasie) i jej zapobiegać.
Należy mieć również na uwadze kilka innych kwestii :
- Domyślnie na obu szynach musi panować stan wysoki. Musi to zostać zagwarantowane za pomocą rezystorów podciągających (nie wolno podłączać szyn bezpośrednio do VCC). Zazwyczaj są one wbudowane w UART ale warto to sprawdzić żeby uniknąć niespodzianek.
- Ponieważ przesył danych jest asynchroniczny (bez sygnału zegarowego) jest on również wrażliwy na niestabilności działania zegara mikrokontrolera. Dotyczy to zarówno nadajnika jak i odbiornika. Należy to uwzględnić przy projektowaniu układu.
- Pojemność i indukcyjność połączenia w istotny sposób wpływa na jakość przesyłu danych. Oba te parametry powinny być jak najmniejsze.
- Zbyt duża prędkość przesyłu może wywołać błędy.
- Zbyt duża długość przewodów również może wywołać problemy.
- Linie do przesyłu danych przewodzą szybkozmienne sygnału elektryczne. Mogą one wywoływać szum na pobliskich połączeniach. Koniecznie należy je odseparować od wrażliwych na zakłócenia elementów (na przykład od części analogowej układu).
Sposób przesyłu danych
Teraz przyszła pora na objaśnienie w jaki sposób dane są kodowane i przesyłane.
Informacje są przesyłane w postaci pakietów.
Każdy pakiet wygląd tak :
Pakiet danych zaczyna się od bitu startu który zawsze wynosi 0 ( stan niski). Następnie idzie 5 do 9 bitów danych (zależy od urządzenia). Potem mamy opcjonalny bit użyty do weryfikacji poprawności danych. Jest to tak zwany bit parzystości. Jeśli liczba “1” w bitach danych jest parzysta wtedy ten bit wynosi 1. Jeśli nie to wynosi on 0. Chociaż w tym miejscu może również stać bit nieparzystości, który jest “1” przy nieparzystej liczbie “1” w bitach danych, i “0” w przypadku parzystej liczby “1”.
Następnie mamy 1 lub 2 (czasem więcej ) bity stopu (zawsze 1), sygnalizują one koniec pakietu.
I tak wygląda cały pakiet. Jeśli przesyłamy więcej danych to następny pakiet zacznie się zaraz po bicie stopu.
Ważna uwaga : Bity danych są przesyłane w kolejności od najmniej znaczącego bitu do najbardziej znaczącego bitu.
Oto rysunek 8 bitów z zaznaczonym najmniej znaczącym bitem danych :
To znaczy, że bity zostaną wysłane w kolejności : 10101001 a nie 10010101 jak podpowiada intuicja.
Chociaż czasami (ale bardzo rzadko) stosowane są rozwiązania w których bity są przesyłane w odwrotnej kolejności.
Pora na trochę praktyki.
W pierwszej demonstracji działania interface-u szeregowego postanowiłem wysłać jeden pakiet zawierający literę “A”.
Jako nadajnik wykorzystałem Arduino Uno. Do pinu TX (wysyłającego dane) podłączyłem sondę oscyloskopu w celu zarejestrowania sygnału.
Następnie wgrałem taki oto program :
1 2 3 4 5 6 |
void setup() { Serial.begin(9600); delay(1000); Serial.print("A"); } void loop() {} |
Kod ten ustawia prędkość przesyłu danych na 9600 bitów na sekundę a następnie czeka sekundę i wysyła znak “A”.
Oto zarejestrowany sygnał z oscyloskopu :
Ale zanim będzie można to zdekodować, trzeba znać ustawienia przesyłania danych :
- Prędkość 9600 bitów na sekundę (ustawiłem w programie).
- 8 Bitów danych (Domyślne ustawienie).
- 1 bit stopu. (Domyślne ustawienie)
- Brak bitu parzystości. (Domyślne ustawienie)
UWAGA NOTATKA : Znaki ASCII Mają po 7 bitów więc każdy najbardziej znaczący bit danych będzie zawsze zerem.
Do zdekodowania transmisji pakietu musimy jeszcze poznać czas jaki przypada na jeden bit. W tym celu należy podzielić 1 sekundę przez ilość bitów na sekundę.
W naszym przypadku jest to :
1/9600 ~ 0,000104167 s
Czyli ok : 104.16 uS.
Należy jednak zwrócić uwagę, że czas przypadający na bit będzie się nieco różnił od obliczonego z powodu niedokładności w działaniu zegara mikrokontrolera.
W tym wypadku w praktyce wynosił on 105 – 106 uS.
Teraz wszystko co trzeba zrobić to odczytać bity danych :
Bit startu to zawsze 0
Najmniej znaczący bit danych (oznaczę go jako 0) : 1
1 bit danych : 0
2 bit danych : 0
3 bit danych : 0
4 bit danych : 0
5 bit danych : 0
6 bit danych : 1
7 danych (najbardziej znaczący) : 0 zawsze w przypadku znaku ASCII.
Bit stopu ( nie widać go bo dalej nie ma żadnej transmisji).
Po odrzuceniu bitu stopu i startu, gotowy bajt danych to 01000001
Odpowiada to znakowi o kodzie 100 0001 (po odrzuceniu bitu najbardziej znaczącego) co odpowiada w tablicy ASCII znakowi : A
Pora na coś większego : Na przykład Hello World!
Niestety dekodowanie tak długiej transmisji za pomocą oscyloskopu byłoby koszmarem więc stwierdziłem, że użyję do tego celu analizatora stanów logicznych :
Jego oprogramowanie zawiera również gotowy dekoder różnych transmisji co ułatwi sprawę.
Sygnał z pinu TX został podłączony do kanału 0 (który nie wiadomo dlaczego na analizatorze jest oznaczony jako kanał 1 a w programie jako kanał 0).
Na początku dla porównania z odczytem z oscyloskopu, zarejestrowałem przesyłanie znaku “A”.
Jak widać odczyt z analizatora wygląda tak samo jak odczyt z oscyloskopu.
Fajną opcją jest możliwość dekodowania sygnału za pomocą różnych analizatorów.
Dodałem więc analizę asynchronicznego sygnału szeregowego do kanału 0 :
I zgodnie z przewidywaniami, sygnał został poprawnie zdekodowany jako litera “A”
Notatka : Białe kropki oznaczają kolejne bity danych a niebieski pasek oznacza długość całego pakietu.
Teraz pora na przesłanie Hello world! .
Kod jest taki :
1 2 3 4 5 6 |
void setup() { Serial.begin(9600); delay(1000); Serial.print("Hello world!"); } void loop() {} |
Tym razem zarejestrowany sygnał jest nieco dłuższy :
Teraz widoczny staje się bit stopu pomiędzy kolejnymi pakietami danych.
Programowanie w Arduino IDE
Jeśli chodzi o arduino to użytkowanie portu szeregowego jest proste.
Służy do tego klasa Serial i podchodzące pod nią funkcję.
Cała dokumentacja wraz z listą tych funkcji znajduje się tutaj : https://www.arduino.cc/en/Reference/Serial
Przytoczę tutaj tylko kilka najczęściej używanych. Reszta również może być pomocna jednak są zdecydowanie rzadziej używane.
begin()
Funkcja ta uruchamia wewnętrzny UART mikrokontrolera i dokonuje ustawień. Po wywołaniu tej funkcji na pinach RX i TX pojawia się stan wysoki a interface szeregowy jest gotowy do pracy.
Argumenty :
- Prędkość przesyłu danych.
- Konfiguracja
Sposób użycia :
- Serial.begin(speed)
- Serial.begin(speed, config)
Przyjmowane wartości konfiguracji to :
- SERIAL_5N1 – 5 Bitów danych, brak bitu parzystości, 1 bit stopu.
- SERIAL_6N1 – 6 Bitów danych, brak bitu parzystości, 1 bit stopu.
- SERIAL_7N1 – 7 Bitów danych, brak bitu parzystości, 1 bit stopu.
- SERIAL_8N1 – 8 Bitów danych, brak bitu parzystości, 1 bit stopu. (Domyślne ustawienie)
- SERIAL_5N2 – 5 Bitów danych, brak bitu parzystości, 2 bity stopu.
- SERIAL_6N2 – 6 Bitów danych, brak bitu parzystości, 2 bity stopu.
- SERIAL_7N2 – 7 Bitów danych, brak bitu parzystości, 2 bity stopu.
- SERIAL_8N2 – 8 Bitów danych, brak bitu parzystości, 2 bity stopu.
- SERIAL_5E1 – 5 Bitów danych, bit parzystości, 1 bit stopu.
- SERIAL_6E1 – 6 Bitów danych, bit parzystości, 1 bit stopu.
- SERIAL_7E1 – 7 Bitów danych, bit parzystości, 1 bit stopu.
- SERIAL_8E1 – 8 Bitów danych, bit parzystości, 1 bit stopu.
- SERIAL_5E2 – 5 Bitów danych, bit parzystości, 2 bity stopu.
- SERIAL_6E2 – 6 Bitów danych, bit parzystości, 2 bity stopu.
- SERIAL_7E2 – 7 Bitów danych, bit parzystości, 2 bity stopu.
- SERIAL_8E2 – 8 Bitów danych, bit parzystości, 2 bity stopu.
- SERIAL_5O1 – 5 Bitów danych, bit nieparzystości, 1 bit stopu.
- SERIAL_6O1- 6 Bitów danych, bit nieparzystości, 1 bit stopu.
- SERIAL_7O1- 7 Bitów danych, bit nieparzystości, 1 bit stopu.
- SERIAL_8O1- 8 Bitów danych, bit nieparzystości, 1 bit stopu.
- SERIAL_5O2 – 5 Bitów danych, bit nieparzystości 2 bity stopu.
- SERIAL_6O2- 6 Bitów danych, bit nieparzystości 2 bity stopu.
- SERIAL_7O2- 7 Bitów danych, bit nieparzystości 2 bity stopu.
- SERIAL_8O2- 8 Bitów danych, bit nieparzystości 2 bity stopu.
Pierwsza cyfra kodu to ilość bitów danych, Litera oznacza ustawienie bitu parzystości, ostatnia cyfra to ilość bitów stopu.
Podziękowania dla użytkownika ethanak za wyjaśnienie co oznaczają poszczególne cyfry kodu. (:
Prędkość może teorytycznie zostać ustawiona na dowolną wartość (ograniczoną przez sprzętowe możliwości UART mikrokontrolera) jednak jeśli chcemy skomunikować się z komputerem (i z 99% innych urządzeń) to musi ona być jedną z tych wartości :
- 300
- 600
- 1200
- 2400
- 4800
- 9600
- 14400
- 19200
- 28800
- 38400
- 57600
- 115200
Przykład użycia :
1 2 3 |
Serial.begin(9600); // z ustawieniem Serial.begin(9600,SERIAL_8E2); |
Dla płytek arduino posiadających więcej niż jeden asynchroniczny port szeregowy :
1 2 3 4 |
Serial.begin(9600); Serial1.begin(38400); Serial2.begin(19200); Serial3.begin(4800); |
Każdy port ma przypisaną jedną klasę.
end()
Funkcja ta wyłącza port szeregowy. Po jej wywołaniu piny przypisane do portu mogą być użyte jako zwykłe piny cyfrowe. Aby włączyć ponownie port szeregowy, należy użyć funkcji begin.
Funkcja ta nie posiada żadnych argumentów ani nic nie zwraca.
Przykłady użycia :
1 2 3 4 5 |
Serial.end(); // Dla większej ilości portów Serial1.end(); Serial2.end(); Serial3.end(); |
available()
Funkcja która zwraca ilość bajtów znajdujących się w buforze odebranych danych. Bufor przechowuje 64 bajty. Jeśli bufor jest pusty, wtedy zwraca zero.
Argumenty : Brak.
Przykład użycia :
1 2 3 4 |
if (Serial.available() > 0) { // odbierz dane } |
W przypadku płytek z większą ilością portów szeregowych :
1 2 3 |
Serial1.available() Serial2.available() Serial3.available() |
write()
Funkcja ta wysyła bajty danych przez port szeregowy.,
3 możliwości użycia :
- Serial.write(val)
- Serial.write(str)
- Serial.write(buf, len)
W pierwszej opcji podajemy zmienną / wartość która ma zostać wysłana.
UWAGA! To może być jedynie zmienna/ wartość o wielkości jednego bajta.
Możliwe zmienne to :
- byte
- char
- unsigned char
- signed char
- bool
Przykład :
1 2 3 4 5 6 7 |
void setup() { Serial.begin(9600); delay(1000); char a=100; Serial.write(a); } void loop() {} |
Odczyt z analizatora (musiałem go przestawić z ASCII na wartości dziesiętne) :
W opcji numer dwa argumentem funkcji jest wartość typu string.
Przykład :
1 2 3 4 5 6 |
void setup() { Serial.begin(9600); delay(1000); Serial.write("Hello!"); } void loop() {} |
Technicznie rzecz biorąc, ten sam efekt można uzyskać używając funkcji print() nie mniej jednak taka opcja istnieje.
Efekt:
Trzecią opcją jest wysłanie tablicy zmiennych.
UWAGA! Każda zmienna musi mieć wielkość jednego bajta!
W takim przypadku jako pierwszy argument funkcji wpisujemy nazwę tablicy a jako drugi jej wielkość.
Przykład:
1 2 3 4 5 6 7 8 9 10 11 12 |
void setup() { Serial.begin(9600); delay(1000); char a[5]; a[0]=10; a[1]=11; a[2]=12; a[3]=13; a[4]=14; Serial.write(a,5); } void loop() {} |
Efekt :
Opcję tą można wykorzystać do wysłania zmiennych o rozmiarze większym niż jeden bajt, np float czy long.
print()
Funkcja która wysyła dane pod postacią znaków ASCII.
Istnieją dwie opcje jej użycia :
- Serial.print(val)
- Serial.print(val, format)
W pierwszej opcji po prostu podajemy zmienną / wartość i zostaje ona wysłana.
Przykład :
1 2 3 4 5 6 7 |
void setup() { Serial.begin(9600); delay(1000); long a=12345; Serial.print(a); } void loop() {} |
Efekt :
Możemy również ustalić jaka liczba cyfr po przecinku jest przesłana :
1 2 3 4 5 6 7 |
void setup() { Serial.begin(9600); delay(1000); double a=123.12333; Serial.print(a,3); } void loop() {} |
efekt:
1 2 3 4 5 6 7 |
void setup() { Serial.begin(9600); delay(1000); double a=123.12333; Serial.print(a,5); } void loop() {} |
Drugą opcją jest ustawienie formatu w jakim są wysyłane dane. Należy go wpisać jako drugi argument funkcji.
Dostępne formaty to :
- BIN – Format binarny
- OCT – Format ósemkowy
- DEC – Format dziesiętny
- HEX – Format szesnastkowy
Przykład :
- Serial.print(78, BIN) daje “1001110”
- Serial.print(78, OCT) daje”116″
- Serial.print(78, DEC) daje “78”
- Serial.print(78, HEX) daje “4E”
println()
Działanie i opcje takie same jak w przypadku print ale z tą różnicą, że na końcu dodawane są dwa znaki :
- Znak powrotu karetki : (ASCII 13) \r
- Znak nowej linii : (ASCII 10) \n
Powodują one przejście do następnej linii.
Przykład :
1 2 3 4 5 6 |
void setup() { Serial.begin(9600); delay(1000); Serial.println("X"); } void loop() {} |
Po X znajdują się dwa dodatkowe znaki :
read()
Funkcja która zwraca pierwszy bajt z bufora odebranych danych . Po odczytaniu, dany bajt jest czyszczony więc wywołując funkcję raz za razem możemy odczytać cały bufor. Odebrane dane są dopisywane na końcu bufora więc bajty są odczytywane w kolejności ich odebrania.
Jeśli bufor jest pusty, zwraca -1.
Przykład :
1 2 3 4 |
if (Serial.available() > 0) { char bajt = Serial.read(); } |
flush()
Funkcja która pauzuje program do czasu aż zostaną wysłane wszystkie dane. W niektórych przypadkach może to rozwiązać problemy z przesyłaniem danych.
Przykład :
1 2 |
Serial.println("Hello"); Serial.flush(); |
I to tyle jeśli chodzi o najczęściej używane funkcje, pełna lista funkcji wraz z opisami i przykładami użycia znajduje się w dokumentacji arduino : https://www.arduino.cc/en/Reference/Serial
Zachęcam do zapoznania się z nią.
Przykłady użycia portu szeregowego są wbudowane w arduino IDE i znajdują się w Przykłady -> communication
Kilka słów na koniec
Mam nadzieję, że ten tutorial okazał się pomocny.
Komentarze pozytywne jak i negatywne jak zwykle mile widziane (:
I to tyle (;
Czyli software.serial robi nam na dowolnym cyfrowym pinie uart? Piny cyfrowe bedą zmieniać swój stan wysoki/niski(bity)?
Idąc dalej, dodając na liniach danych transoptory lub tranzystory można zrobić konwerter stanów logicznych? Dobrze to wszystko sobie dopowiedziałem?
Tak, Software Serial symuluje programowo UART na dowolnym pinie cyfrowym.
Nie jestem tylko pewien czy Software serial włącza rezystory podciągające czy po prostu podpina pin do VCC, trzeba by było to sprawdzić.
Natomiast jeśli chodzi o konwerter poziomów logicznych to jest dużo rozwiązań ( poszukaj w internecie), możesz też kupić gotowy moduł. Na transoptorach też się da choć zazwyczaj używa się ich do fizycznej izolacji dwóch układów.
Generalnie potencjalnym problemem przy konwersji UART może być to, że układ nie wyrobi przy wyższych prędkościach (a czasem i nawet przy niższych) i pojawią się błędy transmisji.
Owe tajemnicze słowa których nie ma w dokumentacji to po prostu skrót trybu transmisji.
Pierwsza cyfra to ilość bitów danych
Litera to bit parzystości (N – brak, E – parzysty, O – nieparzysty)
Druga cyfra to ilość bitów stopu.
Popraw to i nie zasłaniaj się brakiem dokumentacji, bo to podstawy.
Dzięki za podpowiedź, już poprawiam!
No to poszła piątka – dobra robota!
Whoa! Bardzo dobry tutorial! :D Dobra robota!
Niestety, ale nie chce mi to działać. Na mega nadaje z Serial1, na Uno odbieram, poprzez SoftwareSerial. Prędkości takie same, a jednak nie chcą się komunikować. Nawet najprostsze hello!
A pamiętałeś, żeby połączyć Tx z Rx i Rx z Tx?
Dobrze i logicznie napisane, Tego szukałem. Pełny profesjonalizm, Dziękuję