Kursy • Poradniki • Inspirujące DIY • Forum
Głównym tematem będzie jednak stworzenie aplikacji na PC, mogącej komunikować się z mikrokontrolerem za pomocą portu szeregowego. Aplikacja jest stworzona dla układów wykorzystujących właśnie USART. Takie rozwiązanie umożliwia nie tylko przesyłanie danych za pomocą RS232, ale również przez USB i konwerter FT232, bluetooth, IrDA i inne interfejsy, używające wirtualnego portu COM.
Interfejs USART
USART - Universal Synchronous and Asynchronous Reciever and Transmitter
Jak już wspomniałem, jest to interfejs komunikacyjny, dostępny w większości popularnych mikrokontrolerów. W niektórych można znaleźć nawet kilka pracujących niezależnie USARTów.
W artykule będę się posługiwał ATmegą16, ale programy powinny być łatwe do przeniesienia na inne pokrewne procesory. Do transferu danych za pomocą USARTu służą sygnały TxD (transmisja danych) oraz RxD (odbiór danych).
Prędkość transmisji (baud rate) jest z góry ustalona dla danego połączenia. Dokonuje się tego przez wpisanie odpowiedniej wartości do rejestru UBRR (USART Baud Rate Register), wyliczonej w oparciu o częstotliwość taktowania mikrokontrolera.
Warto zwrócić uwagę na pewną rzecz. Istnieją standardowe prędkości transmisji: 2400, 4800, 9600 itd. Aby zapewnić transmisję z błędem na poziomie 0.00%, częstotliwość taktowania mikrokontrolera musi być oczywiście znana i stabilna, ale także musi być wielokrotnością 1,8432MHz. Wybór innej częstotliwości spowoduje pogorszenie dokładności.
W datasheecie Atmela można znaleźć dokładny opis użytego interfejsu, a w tym m.in. wzór do obliczania baud rate oraz tabelkę z popularnymi częstotliwościami taktowania, prędkościami transmisji, odpowiednimi wartościami rejestru UBRR oraz odpowiadającymi tym ustawieniom błędami.
Ramka danych w transmisji UART wygląda następująco:
Zawiera bit startu, sygnalizujący początek kolejnej ramki (od 5 do 9 bitów danych), bit parzystości, służący do kontroli błędów odbioru. Może działać w trzech trybach - brak kontroli (no), suma zawsze parzysta (even), suma zawsze nieparzysta (odd).
Popularną konfiguracją, w której pracuje interfejs USART, jest podłączenie do portu RS232 komputera. Aby tego dokonać, niezbędna jest konwersja stanów logicznych, której można dokonać za pomocą popularnego scalaka MAX232.
Poniżej schemat połączenia uC do RSa:
Inne, częste konfiguracje to USART, pracujący z FT232RL i podłączony do portu USB, moduł Bluetooth, np. BTM-222 (należy przy nim pamiętać o napięciu zasilania 3,3 V), czy połączenie ze sobą dwóch mikrokontrolerów. Nie będę się nad nimi rozpisywać. Schematy połączeń można bez problemu znaleźć w internecie. Przejdźmy zatem do programu od strony PC.
Środowisko programistyczne
Głównym kryterium wyboru środowiska programistycznego jest obecność dedykowanych bibliotek do obsługi portów szeregowych.
Dlatego nie ma sensu komplikować sobie życia. Poza tym wiele środowisk zdecydowanie ułatwia tworzenie prostych w obsłudze i ładnie wyglądających aplikacji okienkowych. Na pewno jest to miła odmiana dla topornych programów konsolowych.
Do napisania swojego programu wybrałem platformę .NET i Visual C# 2010, ale nic nie stoi na przeszkodzie, aby napisać taki program na przykład w Javie. Proces powstawania aplikacji, a nawet potrzebne funkcje w Javie są bardzo podobne, więc nie powinno być problemu z przeniesieniem tego artykułu na inny język.
Jeśli ktoś miał już do czynienia z C, C++ czy Javą, przesiadka na C# nie powinna stanowić dużego problemu. Do niektórych nowych rzeczy trzeba się przyzwyczaić, ale reszta jest całkiem podobna. Osoby, które preferują Bascoma, Pascala i inne podobne języki może zainteresują się Visual Basic. Jednak im również polecam C# ze względu na to, że jest reprezentantem dużo popularniejszej rodziny języków oraz łatwiej znaleźć pomoc w Internecie.
Program Visual C# Express 2010 można pobrać darmowo ze strony microsoftu, studenci mogą darmowo pobrać Visual Studio 2010 z MSDNAA. Dla pozostałych licencja na Visual Studio niestety jest płatna. Ja używam wersji Express.
Po zakończonej, udanej instalacji otwieramy Visual C# 2010 Express i wybieramy:
File->New Project
Ukazuje nam się poniższe okno (w pełnej wersji Visual Studio 2010 mamy do wyboru więcej rodzajów projektów).
Wybieramy Windows Forms Aplication Visual C#, a na dole wybieramy nazwę dla naszego projektu i klikamy OK. Powinien nam się ukazać widok Designera:
Widzimy w nim, jak wygląda nasza aplikacja okienkowa. Jeżeli klikniemy teraz zielony przycisk Play na górnym pasku lub Debug->Start Debugging zobaczymy, że aktualnie nasz program jest pustym okienkiem, a jedyną jego funkcjonalnością są 3 przyciski w prawym górnym rogu.
Dobrze jest od razu rozmieścić panele opcji w celu wygodniejszej pracy z programem. Najpierw klikamy na zaznaczony kółkiem młotek. Uaktywnia on panel z elementami, które możemy dodawać do naszej formy.
Ja ustawiłem go po prawej stronie, a następnie kliknąłem w pinezkę na górze, czyli Auto Hide. Teraz, kiedy najadę myszką na pasek po prawej stronie, otworzy się toolbox.
Po kliknięciu na nasze okienko Form1 prawym przyciskiem i wybranie Properties, ukażą nam się opcje obiektu. Ta zakładka będzie nam często potrzebna. Dlatego przesunąłem ją na prawo bez autoukrywania. Drzewo projektu przeniosłem za to na lewą stronę ekranu. Ostatecznie wszystko wygląda tak:
Wprowadzenie do C#
Teraz parę słów o języku C#. Jest to język obiektowy, czyli składający się z elementów (obiektów), należących do klas. Każda klasa posiada swoje parametry - zmienne oraz metody - funkcje, działające na tych parametrach, zwracające ich wartości, konwertujące do innych typów itp.
Poszczególne części składowe klas mogą mieć atrybuty private (dostępne tylko dla metod danej klasy) lub public (dostępne również dla innych klas). Atrybutów tych używa się w celu zwiększenia bezpieczeństwa kodu. Powszechną praktyką jest ustawianie zmiennych klasy jako prywatne, a metod jako publiczne.
Szczególne metody to konstruktor (funkcja o takiej samej nazwie jak klasa, klasy mogą mieć więcej niż jeden konstruktor. Poszczególne z nich mają taką samą nazwę, ale różne argumenty) oraz destruktor. Odpowiadają one za inicjalizację oraz zakończenie życia obiektu, należącego do danej klasy.
Poza tym klasy mogą po sobie dziedziczyć parametry i metody, parametrem jednej klasy mogą być inne itp. Z początku może się to wydawać dość zagmatwane, ale w rzeczywistości jest ogromnym ułatwieniem. Typy zmiennych, takie, jak int czy double również są w C# klasami. Można zatem wykorzystywać metody, w które są wyposażone. Przykładowo ToString() daje na wyjściu zapis np. liczby int jako łańcucha znaków. Nie zmienia to oczywiście dotychczasowych zastosowań tych rodzajów zmiennych. W związku z tym inty można dalej normalnie dodawać, odejmować itp. ale oprócz tego mają dodatkowe funkcjonalności.
Taka czysta teoria dotycząca języka i tak nie ma większego sensu, więc przejdziemy już do tworzenia naszej aplikacji. Jeżeli kogoś bardziej interesują podstawy języka C# oraz programowania na platformie .NET polecam bardzo obszerną bibliotekę microsoftu oraz przeszukanie Internetu, gdzie istnieje ogromna ilość tutoriali i kursów dla początkujących, a także artykułów na każdym poziomie zaawansowania.
Tworzenie szablonu aplikacji
Najpierw zastanówmy się, jak nasza aplikacja ma wyglądać i co ma robić. Ten etap projektowania jest znacznie uproszczony dzięki Designerowi i możliwości podglądu okna na bieżąco. Na pewno będzie potrzebny panel konfigurujący połączenie oraz jakiś sposób wysyłania i odbierania wiadomości.
Na początku warto zrobić zwykły terminal z dwoma polami tekstowymi. W jednym będą ukazywać się wiadomości wysłane, a w drugim odebrane albo jedno pole tekstowe z całą "rozmową". Później będzie można rozszerzyć funkcjonalność na transmisję według jakiegoś bardziej zaawansowanego protokołu np. do odbierania w sposób ciągły danych z czujników robota, wczytywania zawartości EEPROM albo do sterowania serwomechanizmami.
Nasza aplikacja będzie się składać z trzech zakładek - terminala ogólnego przeznaczenia, protokołu komunikacji z konkretnym mikrokontrolerem, a także opcji połączenia.
Powiększmy nasze okienko. W opcjach można również zmienić nazwę, tekst i rozmiar czcionki w celu ułatwienia sobie późniejszej pracy i lepszej estetyki. Następnie po odszukaniu w Toolboxie w grupie Containers TabControl, dodajmy go do naszego okna. W opcjach Taba należy znaleźć parametr Dock i przestawić na Fill. Wtedy wypełni całe okno. Następnie szukamy opcji Tab Pages i klikamy na przycisk "...". Tam dodajemy trzeciego taba i zmieniamy nazwy oraz wyświetlamy tekst zgodnie z poniższym obrazkiem:
Nazwy i tekstu drugiego taba jeszcze nie zmieniałem. Najpierw zrobimy terminal, a tab zarezerwowany na bardziej wysublimowany sposób komunikacji, zostawię sobie na ewentualną drugą część. Równie dobrze można zrobić jedynie taby z opcjami i terminalem, bo, jak widać, zarządzanie tabami jest bardzo proste i wymaga jedynie użycia przycisków Add i Remove. Teraz przechodzimy do taba opcji i dodajemy elementy typu label, combobox i button. Docelowo chcemy uzyskać coś takiego:
Za każdym razem pamiętamy o zmianie pól Text i Name w opcjach. Nazwy zmieniamy na takie, żeby było wiadomo do czego służy dany element. Ja na przykład używam cbName, cbBaud, butRefresh itp. Przy ComboBoxach należy jeszcze w opcji DropDownStyle wybrać DropDownList, aby można było wybierać wartości tylko ze zdefiniowanego przez nas zbioru.
Dla ComboBoxów dotyczących prędkości transmisji i ilości bitów danych należy jeszcze ręcznie zdefiniować elementy tego zbioru. Odnajdujemy opcję Items i klikamy na kropki przy napisie Collection. Tam wpisujemy dla bitów danych wartości od 5 do 9, każda odzielona enterem. Natomiast w drugim wpisujemy wszystkie standardowe prędkości transmisji czyli:
1 2 3 4 5 6 7 8 9 10 11 |
2400 4800 9600 14400 19200 28800 38400 57600 76800 115200 230400 |
Po zrobieniu zakładki opcji, podobnej do tej na rysunku albo według własnego uznania, możemy przejść do projektowania terminala. Mój wygląda tak:
Składa się z dwóch Labeli, dwóch Buttonów, jednego Rich Text Boxa, jednego NumericUpDown i z jednego PictureBoxa. Czerwony kwadracik będzie przyciskiem do rozpoczynania/kończenia połączenia i jednocześnie będzie pokazywać aktualny status.
W dużym polu tekstowym będą wyświetlane dotychczas wysłane/odebrane dane w kodzie szesnastkowym. Za pomocą małego pola Numeric będzie można wybierać wartość nowego bajtu do wysłania w kodzie szesnastkowym.
Naciśnięcie przycisku Wyślij będzie powodować wysłanie aktualnej wartości przez port COM, a Wyczyść będzie czyścić Log. Należy pamiętać o kilku ustawieniach w opcjach. W PictureBoxie ustawiamy BackColor na Red, w RichTextBoxie ustawiamy ReadOnly na True, aby przypadkowo nie zmieniać zawartości loga.
W NumericUpDown zmieniamy opcję Hexadecimal na true, aby liczby były wyświetlane szesnastkowo, a wartości Minimum i Maximum odpowiednio na 0 i 255, aby zamykały się w jednym bajcie.
W ten sposób udało nam się zakończyć fazę projektowania "designu" naszej aplikacji. Do tej pory wszystko było proste i nie miało nic wspólnego z prawdziwym programowaniem. Nasza aplikacja już wygląda całkiem nieźle i po kompilacji nawet ma niektóre funkcje takie jak wybieranie wartości bajtu czy możliwość wyboru niektórych opcji z rozwijanej listy.
Następnym krokiem będzie dodanie funkcjonalności odpowiednim przyciskom i implementacja połączenia szeregowego. Ale to już zrobimy za pomocą zwykłego programowania a nie przeciągania elementów i zmiany ich właściwości.
Programowanie
Aby zobaczyć prawdziwy kod naszego programu musimy w Solution Explorerze znaleźć główny plik (domyślnie jest to Form1.cs), kliknąć prawym przyciskiem i wybrać View Code. Naszym oczom ukaże się zapis:
1 2 3 4 5 6 7 8 9 10 |
namespace TutorialCOM { public partial class tutorialCOM : Form { public tutorialCOM() { InitializeComponent(); } } } |
Pierwsza linijka określa przestrzeń nazw do której należą definiowane przez nas klasy. Projekt może korzystać z wielu plików i nie trzeba ich scalać za pomocą popularnego w innych językach słowa include. Wystarczy, że wszystkie pliki w projekcie będą miały tę samą przestrzeń nazw.
Dalej mamy deklarację naszej głównej klasy oraz jej konstruktor, w którym znajduje się funkcja InitializeComponent. To ona tworzy okienko jakie widzimy po kompilacji programu.
Nasuwa się pytanie, co to za magiczna funkcja, która nie przyjmuje żadnych parametrów i dokładnie wie jakie okienko ma dla nas stworzyć. Odpowiedź na to pytanie kryje się pod słowem kluczowym partial oraz w innym pliku należącym do naszego projektu - Form1.Designer.cs. Jeśli go otworzymy, widzimy również definicję naszej klasy ze słówkiem partial w przestrzeni nazw TutorialCOM. Jest to oczywiście druga część deklaracji naszej głównej klasy. Właśnie tam znajduje się funkcja InitializeComponent, aktualizowana automatycznie, kiedy dodajemy coś do designera.
Na początku pliku z kodem możemy znaleźć linijki, rozpoczynające się od słowa using. Informują one kompilator o standardowych przestrzeniach nazw, jakich będziemy używali w naszym programie.
Te aktualnie wygenerowane w naszym pliku odnoszą się do klas używanych w designerze. Co daje użycie słowa using? Najłatwiej będzie to przedstawić na przykładzie.
Do naszego programu będziemy potrzebować namespace System.IO.Ports, gdzie znajduje się klasa SerialPort, obsługująca port szeregowy. Do naszego programu musimy dodać element powyższej klasy, aby móc realizować połączenie. We wnętrzu klasy możemy zdefiniować sobie zmienną:
System.IO.Ports.SerialPort port;
Jeżeli dalej będziemy chcieli odwoływać się do elementów tej przestrzeni nazw, za każdym razem od nowa musimy pisać System.IO.Ports. Przy okazji widzimy po wpisaniu kropki, że Visual Studio wyświetla rozwijaną listę z dostępnymi elementami do wyboru co wielce ułatwia pisanie. Mimo wszystko umieszczanie za każdym razem tej ścieżki znacząco przedłuża pisanie. Dlatego właśnie w początkowej części pliku możemy dodać linijkę:
using System.IO.Ports;
Wtedy nasza definicja zmiennej może zostać skrócona do samego:
SerialPort port;
Od tej pory nie musimy za każdym razem pisać pełnej ścieżki. Dzięki słowu kluczowemu using, kompilator wie, gdzie szukać używanych przez nas klas. Mamy dodaną zmienną typu SerialPort. Teraz musimy ją zainicjalizować w konstruktorze za pomocą następującego kodu:
1 2 3 4 5 |
//inicjalizacja zmiennej port z domyślnymi wartościami port = new SerialPort(); //ustawienie timeoutów aby program się nie wieszał port.ReadTimeout = 500; port.WriteTimeout = 500; |
Opcje połączenia będziemy modyfikować w odpowiedniej zakładce. Musimy więc zadbać o to, żeby wyświetlały tam się poprawne wartości, zarówno aktualne, jak i dostępne na rozwijanych listach. W tym celu musimy utworzyć obsługę zdarzenia, które będzie się uruchamiać zawsze, kiedy wejdziemy do zakładki opcji. W tym celu w konstruktorze dodajemy event:
1 |
Opcje.Enter += new EventHandler(Opcje_Enter); |
a pod spodem funkcję go obsługującą:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
void Opcje_Enter(object sender, EventArgs e) { //aktualizacja list this.cbName.Items.Clear(); this.cbParity.Items.Clear(); this.cbStop.Items.Clear(); foreach (String s in SerialPort.GetPortNames()) this.cbName.Items.Add(s); foreach (String s in Enum.GetNames(typeof(Parity))) this.cbParity.Items.Add(s); foreach (String s in Enum.GetNames(typeof(StopBits))) this.cbStop.Items.Add(s); //aktualizacja nazw cbName.Text = port.PortName.ToString(); cbBaud.Text = port.BaudRate.ToString(); cbData.Text = port.DataBits.ToString(); cbParity.Text = port.Parity.ToString(); cbStop.Text = port.StopBits.ToString(); } |
Opcje.Enter jest eventem przypisanym do obiektu o nazwie opcje i uruchamianym zawsze, kiedy wejdziemy w tę zakładkę. Za każdym razem, gdy zajdzie ten event wykonywane są wszystkie EventHandlery (czyli funkcje), jakie się w nim znajdują.
Na evencie można wykonać operację dodania Event Handlera += lub odjęcia -=. Natomiast w funkcji obsługującej event, poza prostym przypisaniem Stringów do tekstów danych pól, mamy ciekawą konstrukcję foreach. Służy ona do operacji na wszystkich możliwych elementach jakie znajdziemy w danym zbiorze, kiedy jeszcze nie wiemy, ile ich będzie.
W naszym wypadku dodajemy do listy każdy element typu String, jaki znajdziemy w zbiorach nazw portów, opcji parzystości i opcji stopu. Dzięki temu jeśli podłączymy coś do 5 portów COM, to na liście znajdzie się 5 elementów, a jeżeli nic nie podłączymy, to lista będzie pusta. Jest to niezwykle przydatna komenda.
Jeśli teraz skompilujemy program to możemy łatwo sprawdzić działanie rozwijanych list. Przed użyciem foreach, czyścimy zawartość list, aby ewentualnie nie duplikować, ani żeby nie było tam nieaktualnych wartości. Ważne jest, by najpierw użyć konstrukcji foreach, a dopiero potem przypisać teksty, ponieważ funkcja Clear kasuje zawartość listy włącznie z aktualną wartością.
Dlatego właśnie dodałem przycisk Odśwież, za pomocą którego będziemy mogli aktualizować od razu te parametry. Jeżeli otworzymy designera i klikniemy dwukrotnie na przycisk Odśwież, automatycznie wygeneruje nam się cały kod do obsługi eventu kliknięcia przycisku. U nas jednak ten event będzie wyglądał dokładnie tak samo jak w przypadku uaktywnienia opcji. Więc można usunąć wygenerowaną funkcję handlera, a w Form1.Designer.cs odszukać wiersz:
1 |
this.butRefresh.Click += new System.EventHandler(this.butRefresh_Click); |
i zamienić go na:
1 |
this.butRefresh.Click += new System.EventHandler(this.Opcje_Enter); |
Utwórzmy teraz event kliknięcia przycisku Domyślnie tak, aby automatycznie zmieniał wartości na takie, jakie chcemy. Ja dałem następujące:
1 2 3 4 5 6 7 8 |
private void butDomyslne_Click(object sender, EventArgs e) { this.cbName.Text = "COM3"; this.cbBaud.Text = "9600"; this.cbData.Text = "8"; this.cbParity.Text = "None"; this.cbStop.Text = "One"; } |
Natomiast dla przycisku Anuluj sprawmy, aby przyjmował takie wartości, jak przy aktywacji zakładki:
1 2 3 4 5 6 7 8 |
private void butCancel_Click(object sender, EventArgs e) { cbName.Text = port.PortName.ToString(); cbBaud.Text = port.BaudRate.ToString(); cbData.Text = port.DataBits.ToString(); cbParity.Text = port.Parity.ToString(); cbStop.Text = port.StopBits.ToString(); } |
Mamy już gotową całą zakładkę opcji. Możemy wybierać właściwości połączenia z listy, a kliknięcie przycisku wywołuje odpowiednią akcję. Zróbmy teraz porządek z zakładką Terminal. Czerwony kwadracik ma służyć do ustanawiania połączenia. W tym celu należy stworzyć event od kliknięcia i umieścić tam następujący kod:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 |
private void pbStatus_Click(object sender, EventArgs e) { //jeżeli połączenie jest aktywne to je kończymy, zmieniamy kolor na red i zmieniamy napis if (port.IsOpen) { pbStatus.BackColor = System.Drawing.Color.Red; port.Close(); labStatus.Text = "Brak połączenia (teraz można zmieniać opcje połączenia)"; DodajKolorowy(rtbTerminal, "\nZakończono połączenie z " + port.PortName + "\n", System.Drawing.Color.Orange); } //w przeciwnym wypadku włączamy połączenie, zmieniamy kolor na zielony i zmieniamy napis else { //połączenie może nie być możliwe dlatego należy się zabezpieczyć na wypadek błędu try { //najpierw przepisujemy do portu parametry z opcji port.PortName = this.cbName.Text; port.BaudRate = Int32.Parse(this.cbBaud.Text); port.DataBits = Int32.Parse(this.cbData.Text); port.Parity = (Parity)Enum.Parse(typeof(Parity), this.cbParity.Text); port.StopBits = (StopBits)Enum.Parse(typeof(StopBits), this.cbStop.Text); //a następnie uruchamiamy port port.Open(); //po uruchomieniu zmieniamy elementy graficzne interfejsu pbStatus.BackColor = System.Drawing.Color.Green; labStatus.Text = "Aktywne połączenie (port:" + port.PortName.ToString() + ", prędkość: " + port.BaudRate.ToString() + ", bity danych: " + port.DataBits.ToString() + "\n bity stopu: " + port.StopBits.ToString() + ", parzystość: " + port.Parity.ToString() + ")"; DodajKolorowy(rtbTerminal, "Rozpoczęto połączenie z " + port.PortName + "\n", System.Drawing.Color.Orange); } //jeżeli nastąpi błąd to go przechwycimy i wyświetlimy stosowny komunikat catch(Exception exc) { MessageBox.Show("Błąd połączenia:\n" + exc.Message); } } } |
Na pierwszy rzut oka może on wydawać się straszny, dlatego teraz dokładnie go przeanalizujemy.
Jeżeli port jest otwarty, kończymy połączenie, a picturebox i label ustawiamy jak na początku. Jeżeli natomiast port jest zamknięty i chcemy go otworzyć, musimy się liczyć z możliwościami błędów. Dlatego właśnie używamy struktury try-catch.
Najpierw wykonywane są polecenia z nawiasu try. Jeżeli program napotka jakiś błąd, przechodzi do sekcji catch i wyświetla na ekranie informacje o błędzie. Jeśli nie byłoby tego fragmentu, program by się zawieszał. Jeżeli natomiast wykona bez problemów całą zawartość nawiasu try, pomija część catch i przechodzi dalej.
Sama zawartość nawiasu try składa się najpierw z przepisania wartości portu z opcji, otwarcia portu i ustawienia koloru kwadratu na zielony, a tekstu na informację o aktywnym połączeniu. Funkcja wyrzuca również na terminal odpowiednią wiadomość statusową w kolorze pomarańczowym. Do tego służy funkcja DodajKolorowy, do której jeszcze wrócimy.
Wysyłanie danych
Kiedy klikniemy przycisk Wyślij, aktualna wartość pola NumericUpDown powinna zostać wysłana przez COMa. W tym celu tworzymy kolejny event poprzez dwukrotne kliknięcie w designerze. Jego obsługa wygląda następująco:
1 2 3 4 5 6 7 8 9 10 |
private void butSend_Click(object sender, EventArgs e) { if (port.IsOpen) { DodajKolorowy(rtbTerminal, ((Int32)numericSend.Value).ToString("X") + " ", System.Drawing.Color.Black); Byte[] tosend = { (Byte) numericSend.Value}; port.Write(tosend, 0, 1); } else System.Windows.Forms.MessageBox.Show("Aby wysłać bajt musisz ustanowić połączenie"); } |
Warto tu zwrócić uwagę na kilka rzeczy. Po pierwsze - funkcja ToString z argumentem "X" zamienia liczby int na postać szesnastkową. Jest to dużo szybsze w porównaniu z samodzielnie napisaną podobną funkcją.
Konstrukcja warunkowa if-else zabezpiecza nas przed próbą wysłania wiadomości przy nieaktywnym połączeniu. Skupmy się teraz na funkcji DodajKolorowy. Zmiana koloru czcionki jest niezbędna, ponieważ w jednym polu tekstowym mają być wyświetlane wiadomości wychodzące, przychodzące i statusowe. Jeżeli jednak będziemy to próbowali robić w konwencjonalny sposób, a tekst dodamy xxx.Text += "string"; to poprzedni tekst również zmieni kolor psując kompletnie efekt. Dlatego trzeba obejść problem za pomocą dodatkowej funkcji, wykorzystującej polecenie AppendText:
1 2 3 4 5 6 7 8 |
private void DodajKolorowy(System.Windows.Forms.RichTextBox RichTextBox, string Text, System.Drawing.Color Color) { var StartIndex = RichTextBox.TextLength; RichTextBox.AppendText(Text); var EndIndex = RichTextBox.TextLength; RichTextBox.Select(StartIndex, EndIndex - StartIndex); RichTextBox.SelectionColor = Color; } |
Znając długość pierwotną i końcową tekstu w boxie, zaznaczamy daną część Stringa, a następnie zmieniamy kolor tylko zaznaczonej części. W ten sposób obchodzimy problem kolorowania całości tekstu przez standardową funkcję dodawania Stringa. Jako że będzie nam to potrzebne w kilku miejscach, stworzenie funkcji odpowiedniej do tego jest dla nas ułatwieniem.
Odbieranie wiadomości
Na koniec zostało nam najtrudniejsze zadanie. Musimy dodać event, wyświetlający przychodzącą wiadomość w TextBoxie. Może się wydawać, że zadanie nie będzie się bardzo różniło od poprzednich eventów, jednak rzeczywistość jest nieco inna. Rozpoczynamy od dodania w konstruktorze klasy eventu od przychodzących danych do portu COM:
1 |
port.DataReceived += new SerialDataReceivedEventHandler(DataRecievedHandler); |
Teraz wystarczy dorobić funkcję Handlera do tego eventa. I tu właśnie pojawia się trudność.
Dzieje się tak dlatego, że składowe części aplikacji tworzą oddzielne wątki. Bezpośrednia zmiana przez jeden wątek części składowej drugiego może powodować błędy. Aby uniknąć tego problemu należy użyć konstrukcji zwanej delegatem i operacji invoke. Więcej informacji o tych dwóch tworach znajdziecie tutaj oraz tutaj.
Delegat kryje w sobie odwołanie do jakichś funkcji. Aby móc go używać, należy stworzyć deklarację delegata oraz obiekt typu zadeklarowanego delegata. Kod umieszczamy w ciele klasy, obok deklaracji zmiennej port. W naszym przypadku będzie to wyglądać następująco:
1 2 |
delegate void Delegat1(); Delegat1 moj_del1; |
Zadeklarowaliśmy w ten sposób delegata, zwracającego wartość typu void i niewymagającego żadnych argumentów oraz obiekt typu Delegat1 o nazwie moj_del1. Następnie, w konstruktorze funkcji, należy dokonać inicjalizacji:
1 |
moj_del1 = new Delegat1(WpiszOdebrane); |
WpiszOdebrane to funkcja, którą dopiero utworzymy i która będzie miała za zadanie dopisywanie odebranego tekstu do textboxa. W inicjalizacji delegata widać wielkie podobieństwo do eventów. Gdybyśmy nazwali naszego delegata xxxEventHandler zamiast Delegat1, nie byłoby żadnej różnicy.
Eventy to tak naprawdę delegaty utworzone już wcześniej w standardowych bibliotekach i poddane standaryzacji zwracanych wartości i argumentów wejściowych. Poza tym klasy, których eventy dotyczą, mają już zaimplementowany odpowiedni kod. Dzięki niemu event jest wywoływany. Przejdźmy teraz do utworzenia funkcji WpiszOdebrane:
1 2 3 4 |
private void WpiszOdebrane() { DodajKolorowy(rtbTerminal, port.ReadByte().ToString("X") + " ", System.Drawing.Color.Blue); } |
Jak widać, ogranicza się ona do dodania do terminala tekstu w odpowiednim kolorze. Teraz musimy jeszcze dodać wywołanie naszego delegata po wykryciu przychodzącego bitu:
1 2 3 4 |
private void DataRecievedHandler(object sender, SerialDataReceivedEventArgs e) { rtbTerminal.Invoke(moj_del1); } |
Zawiera ono wspomniane wcześniej słówko Invoke. Odpowiada ono za bezpieczne wywołanie funkcji delegata tak, aby nie zakłócić pracy innych wątków. Po dodaniu tej linijki mamy już w pełni sprawny terminal. Teraz wystarczy wybrać z górnego menu: debug->build solution, odnaleźć na dysku folder projektu, wejść w katalog bin/Release i znaleźć tam plik exe z naszym programem.
Zdaję sobie sprawę, że niektóre poruszone tutaj rzeczy mogą być trudne, dlatego w załączniku dodaję cały kod źródłowy programu. Przy okazji polecam samodzielne studiowanie materiałów z internetu, szczególnie z msdn, jeżeli chcecie w przyszłości wykorzystać C# do innych projektów.
Podsumowanie
Napisanie takiego terminala może bezpośrednio nie jest zbyt przydatne robotykowi. Tym bardziej, że wiele tego typu programów można po prostu ściągnąć, a mają one dużo więcej opcji. Jest to jednak doskonała okazja do poznania wszystkich funkcji sterujących portem szeregowym w naszym środowisku programistycznym i wstęp do trudniejszych projektów.
Sama aplikacja posiada jedną niewykorzystaną zakładkę. Zostanie ona wykorzystana, jeśli uda mi się napisać drugą część tego poradnika. Chciałbym w nim poruszyć zagadnienie prostego programu obsługującego USART ze strony mikrokontrolera oraz pomyśleć nad jakimś standardem przesyłania danych i jego implementacją zarówno od strony PC jak i uC.
To nie koniec, sprawdź również
Przeczytaj powiązane artykuły oraz aktualnie popularne wpisy lub losuj inny artykuł »
avr, komunikacja, RS232, terminal, USART
Trwa ładowanie komentarzy...