Kurs Qt – #2 – komunikacja z Arduino przez UART

Kurs Qt – #2 – komunikacja z Arduino przez UART

Pora na kolejny artykuł omawiający podstawy Qt. Tym razem zajmiemy się komunikacją przez port szeregowy. Dzięki temu połączymy komputer PC z Arduino (lub innym mikrokontrolerem).

W ramach ćwiczeń stworzymy własny monitor portu szeregowego, który będzie mógł sterować pracą Arduino.


Tym razem zajmiemy się budową aplikacji obsługującej port szeregowy komputera, z którym będzie komunikował się nasz mikrokontroler. Program będzie umożliwiał ustanawianie połączenia oraz jego przerywanie. Aplikacja będzie również zapisywała informacje pochodzące z mikrokontrolera. Możliwa będzie również komunikacja w drugą stronę (np. sterowanie z PC diodą świecącą wpiętą do Arduino).

Prosty schemat budowanej aplikacji

Prosty schemat budowanej aplikacji

Interfejs aplikacji

Tworzymy nowy projekt - robimy to identycznie jak poprzednio. Następnie otwieramy plik formularza z interfejsem w trybie Design. Usuwamy zbędne elementy i dodajemy 3 przyciski PushButton:

  • Szukaj,
  • Połącz,
  • Zamknij.

Dodaj przyciski, nazwij je tak jak na poniższym zrzucie ekranu. Pamiętaj też o zmianie nazw obiektów dodanych przycisków. Od razu wprowadzimy nowy element interfejsuGroupBox - pozwoli on nam na tematyczne pogrupowanie interfejsu. Dodajemy GroupBox i zmieniamy jego etykietę na Połączenie.

Łączenie elementów w grupę tematyczną

Łączenie elementów w grupę tematyczną

Następnie umieść w nowym GroupBox'ie wszystkie trzy przyciski, tak aby przycisk Szukaj znajdował się w górnym wierszu, a przyciski Połącz i Rozłącz znajdowały się w dolnym wierszu. Następnie włącz opcję Rozmieść w siatce dla obiektów centralWidget i groupBox.

Efekt powyższych operacji powinien prezentować się następująco:

Odpowiednie rozmieszczenie elementów interfejsu

Odpowiednie rozmieszczenie elementów interfejsu

Interfejs może nie jest zbyt atrakcyjny, ale tym zabiegiem zapewniliśmy sobie skalowanie okna aplikacji do różnych wielkości.

Dodajmy teraz element typu ComboBox obok przycisku Szukaj. Nazwijmy go jako comboBoxDevices, element ten będzie przechowywał nazwy portów COM urządzeń podłączonych do naszego komputera. Ostatecznie efekt powinien być następujący:

Dodanie listy rozwijanej do interfejsu aplikacji

Dodanie listy rozwijanej do interfejsu aplikacji

Wyszukiwanie podłączonych urządzeń

Teraz, gdy nasz interfejs jest już częściowo gotowy, możemy zająć się programowaniem. Kliknij prawym przyciskiem myszy (PPM) na przycisk Szukaj, a następnie Przejdź do slotu i wybierz clicked(). Znajdziesz się wtedy w pliku mainwindow.cpp, w którym należy zaimportować klasę QDebug (tak jak poprzednio). Następnie, w utworzonym przed chwilą slocie, dodaj linijkę QDebug, która wyświetli nam informacje:

W tym miejscu warto przetestować działanie aplikacji. Po jej uruchomieniu i wciśnięciu przycisku Szukaj powinniśmy zobaczyć stosowny komunikat.

Teraz musimy zapoznać się z klasą QSerialPortInfo, która zapewni nam szereg informacji o urządzeniach podłączonych do komputera przez port szeregowy. W dokumentacji podane są informacje na temat załączania tej biblioteki do naszego projektu:

Header, mówi nam jak załączyć bibliotekę do naszych plików .h/.cpp, natomiast qmake mówi co musimy zrobić, żeby dołączyć moduł z QSerialPortInfo do projektu. Otwieramy więc plik konfiguracyjny projektu serialport (plik ten ma rozszerzenie .pro) i szukamy w nim poniższego fragmentu:

Według dokumentacji musimy dodać tutaj fragment:

Możemy to zrobić na jeden z dwóch sposobów:

Gdybyśmy tego nie zrobili to przy próbie kompilacji otrzymalibyśmy komunikat mówiący o tym, że plik wybranej klasy nie istnieje:

Spójrzmy teraz dokładniej na dokumentację QSerialPortInfo, a dokładnie na metodę availablePorts():

Metoda ta zwraca nam listę dostępnych portów szeregowych w systemie. Zatrzymajmy się na chwilę w tym miejscu, zwraca listę czyli de facto inną klasę, która implementuje nam listy. Listy są bardzo wygodne w użyciu i na pewno będziesz z nich jeszcze korzystać. W naszym przypadku jest to lista obiektów typu QSerialPortInfo, czyli obiektów z używanej przez nas klasy.

Przejdźmy do przykładu i dodajmy potrzebne dyrektywy #include:

Następnie w utworzonym wcześniej slocie on_pushButtonSearch_clicked dodajmy nowe linie kodu:

Najpierw tworzymy listę obiektów typu QSerialPortInfo, następnie do tej listy przypisujemy wartość zwracaną przez metodę QSerialPortInfo::availablePorts(), metoda availablePorts() jest metodą statyczną, dlatego wywołaliśmy ją przez nazwę klasy, a nie obiekt.

Następnie wypisujemy w pętli, za pomocą qDebug(), nazwy portów i opis tego portu, używając metod klasy QSerialPortInfo: portName() i description(). Metody te zwracają obiekty typu QString, więc nie musimy już nic z nimi robić przed przesłaniem ich do qDebug().

Nazwy i opisy poszczególnych portów możemy wydobyć z listy za pomocą metody at, która przyjmuje jako argument index interesującego nas elementu:

Musimy mieć jednak pewność, że na liście znajdują się jakieś obiekty. Dlatego bezpieczniej będzie jeśli napiszemy to w następujący sposób:

Powyższy kod zwróci nam obiekt QSerialPortInfo o indeksie 0. W naszym przypadku, wewnątrz pętli, poniższy fragment zwraca nam nie obiekt typu QSerialPortInfo, ale już nazwę portu.

Podłącz teraz do komputera swój mikrokontroler. W moim przypadku jest to Arduino Uno, które było bohaterem kursu podstaw Arduino. Uruchom aplikację i naciśnij przycisk Szukaj. Jeśli wszystko zadziała poprawnie to efekt będzie następujący:

Pierwszy efekt działania programu napisanego w Qt - lista dostępnych portów COM

Pierwszy efekt działania programu napisanego w Qt - lista dostępnych portów COM

Przetestuj wyszukiwanie urządzeń przez aplikację podłączając i odłączając płytkę od PC. Wypełnijmy teraz ComboBox'a nazwami portów podłączonych urządzeń. W stworzonej wcześniej pętli for dodaj:

Taki zabieg wstawi do naszego ComboBoxa nazwy podłączonych do komputera urządzeń, dzięki temu będziemy mogli wybrać, ten z którym chcemy się połączyć. Sprawdź teraz działanie aplikacji, efekt po kliknięciu przycisku Szukaj będzie tym razem następujący:

Lista podłączonych urządzeń

Lista podłączonych urządzeń

Rozbudowa interfejsu

Uzupełnijmy nasz interfejs o nowe elementy. Dodaj kolejnego GroupBoxa i umieść w nim nowy element TextEdit – będzie to nasz monitor portu szeregowego oraz konsola z informacjami i logami. Pamiętaj o odpowiedniej nazwie dla nowego obiektu TextEdit. Włącz opcję Rozmieszczania w siatce dla nowego GroupBoxa. Następnie dodaj jeszcze jednego GroupBoxa i 2 nowe przyciski:

  • Włącz Diodę,
  • Wyłącz diodę.

Nowa wersja interfejsu powinna wyglądać następująco:

Rozbudowana wersja interfejsu programu w Qt

Rozbudowana wersja interfejsu programu w Qt

Zajmiemy się teraz zapisywaniem odebranych informacji. Najpierw poznamy jednak kolejną przydatną klasę Qt – QDateTime, a konkretnie jej metodę currentDateTime().

Na początku pliku mainwindow.cpp dodajmy:

Od teraz poniższy fragment kodu, spowoduje wypisanie w konsoli aktualnej daty i czasu w formacie podanym w metodzie toString():

Efektem działania tej linijki będzie wypisanie tekstu tego typu:

Stwórzmy nową prywatną metodę w naszej klasie MainWindow. W tym celu w pliku mainwindow.h, pod słowem kluczowym private, dodajemy:

Wewnątrz metody addToLogs(QString message) napisz następujące linie kodu:

Stworzyliśmy w ten sposób wygodną metodę, do której będziemy przesyłać naszą informacją, którą umieścimy w konsoli stworzonej z textEditLogs.

Poniższy fragment dodaje łańcuchy znakowe przechowywane przez obiekty typu QString, dodając znak nowej linii do każdej takiej wiadomości.

Teraz zmień nieco implementację metody on_pushButtonSearch_clicked() zastępując qDebug() niedawno stworzoną metodą addToLogs():

Efekt powinien być następujący:

Podgląd logów systemu

Podgląd logów systemu

Implementacja nawiązywania połączenia

Mamy już zaimplementowane wyszukiwanie i wybieranie urządzeń, przygotowaliśmy również interfejs. Zajmiemy się teraz implementacją połączenia. Skorzystamy z klasy QSerialPort, która odpowiada za obsługę portu szeregowego. Jak zwykle pomocna okaże się odpowiednia dokumentacja.

W pliku mainwindow.h dodaj dyrektywę importu klasy QSerialPort:

Następnie, pod słowem kluczowym private, dodaj wskaźnik na obiekt QSerialPort:

Teraz w pliku mainwindow.cpp w konstruktorze umieść:

Konstruktor powinien wyglądać więc następująco:

Tym sposobem zadeklarowaliśmy wskaźnik na obiekt QSerialPort i stworzyliśmy (oraz jednocześnie przypisaliśmy do wskaźnika) obiekt QSerialPort. Zwróć uwagę na fakt, że w taki sposób stworzyliśmy obiekt na stercie (ang. heap), więc zadaniem programisty jest też zwolnienie pamięci po tym obiekcie. Moglibyśmy zrobić to w destruktorze klasy MainWindow, dodając tam jedną linijkę:

Możemy to jednak zrobić też nieco bardziej automatycznie, zostawiając takie zadanie frameworkowi. Wystarczy, że zamiast:

napiszemy:

Skorzystamy z faktu, że klasa QSerialPort (jak i wszystkie obiekty Qt) dziedziczą po klasie QObject, które można ustawiać w relacjach rodzic <- dziecko. Wtedy, gdy rodzic jest usuwany z pamięci, usuwane są najpierw jego dzieci. W opisywanym przypadku, przekazujemy do konstruktora wskaźnik this rodzica, którym jest nasza klasa MainWindow, co zapewni, że w chwili zamykania aplikacji, usunięty zostanie najpierw obiekt device (QSerialPort), a następnie obiekt okna naszej aplikacji - MainWindow.

Stwórzmy teraz slot dla przycisku Połącz i umieśćmy w nim następujący fragment kodu:

Omówmy teraz ten fragment. Pierwsza jest poniższa instrukcja warunkowa:

Sprawdza ona czy mamy jakieś podłączone urządzenia, jeśli nie, to logujemy odpowiednią informację i wychodzimy z metody.

Następny fragment jest nieco bardziej skomplikowany, ponieważ robimy kilka operacji w jednej linii:

  • tworzymy obiekt QString, który będzie zawierał nazwę wybranego przez nas portu, przypisujemy mu zawartość wybranej przez nas opcji (z ComboBoxa) w postaci łańcucha znakowego,
  • moglibyśmy zrobić QString portName = ui−>comboBoxDevices−>currentText(), ale klasa QSerialPort przyjmuje nazwę portu w postaci łańcucha znakowego w formacie COMx (gdzie x jest numerem tego portu). Natomiast my do ComboBoxa wpisaliśmy nazwę portu i jego opis:

Dlatego musimy uzyskać nazwę portu w odpowiednim formacie, w tym celu:

  • łańcuch znakowy z ComboBoxa dzielimy za pomocą metody split() z klasy QString, używając separatora w postaci \t - za pomocą znaku tabulacji oddzieliliśmy nazwę portu od jego opisu, taka operacja zwraca nam listę obiektów typu QString,
  • nas interesuje pierwszy element tej listy, więc używamy metody first(), która zwróci nam ten element.

Gdybyśmy chcieli rozdzielić te instrukcje na kilka linii, to całość wyglądałaby tak:

Jeśli masz wątpliwości lub problemy ze zrozumieniem, wykorzystaj qDebug() po każdej operacji.

Skoro sprawę nazwy portu mamy załatwioną, możemy ją podać naszemu obiektowi QSerialPort, w tym celu wykonujemy:

Teraz omówmy fragment dotyczący otwarcia i konfiguracji portu. Otwarcie portu implementujemy za pomocą metody open(), podając jako argument typ połączenia. W naszym wypadku interesuje nas komunikacja dwukierunkowa więc wybieramy opcję ReadWrite.

Metoda open() zwraca wartość true, jeśli otwarcie portu się powiodło, w przeciwnym wypadku zwraca false. Zatem wykorzystajmy to w ten sposób:

Jeśli otwarcie portu się powiodło to musimy teraz ustawić parametry połączenia:

Nie ma tutaj nic szczególnego, ustawiamy:

  • baudrate = 9600,
  • liczbę bitów danych = 8,
  • parzystość = bez kontroli parzystości,
  • kontrolę przepływu = bez kontroli przepływu.

Teraz uruchom aplikację i spróbuj nawiązać połączenie. Następnie spróbuj nawiązać połączenie po raz drugi, czy udało się nawiązać połączenie za drugim razem? Taka próba raczej się nie uda. Dlaczego? Ponieważ próbujesz otworzyć port, który został już otwarty.

Zabezpieczmy się przed tym korzystając z metody isOpen() klasy QSerialPort:

Zajmijmy się teraz zamykaniem połączenia. W tym celu stwórz slot dla przycisku Zamknij i napisz w nim następujący fragment kodu:

Połączenie zamykamy korzystając z metody close() klasy QSerialPort. Metoda ta nic nie zwraca, więc nie możemy się (w prosty sposób) dowiedzieć czy port został faktycznie zamknięty.

Program dla mikrokontrolera

Przejdźmy na chwilę do programu na nasze Arduino. Używany tu kod jest połączeniem przykładów BlinkWithoutDelay i SerialEvent. Zadaniem programu jest wysyłanie czasu procesora lub innych komunikatów oraz odbiór danych przez UART.

Wgrajmy teraz program do naszego Arduino i sprawdźmy jego działanie za pomocą SerialMonitora zintegrowanego z Arduino IDE. Przykładowy wynik działania powyższego kodu:

Efekt działania programu z Arduino

Efekt działania programu z Arduino

4 alternatywy dla monitora portu szeregowego Arduino
4 alternatywy dla monitora portu szeregowego Arduino

Komunikacja Arduino z komputer przez UART to jedna z częściej wybieranych metod. Opcja ta jest wygodna, szczególnie biorąc pod uwagę, że... Czytaj dalej »

Implementacja wysyłania i odbierania danych

Zajmiemy się teraz ważnym fragmentem naszego kodu. Połączymy sygnał możliwości czytania z portu ze slotem odczytania danych z portu. Za każdym razem, gdy w buforze odbiorczym portu znajdą się nowe dane, odczytamy je automatycznie w naszym własnym slocie. Gdy połączymy te dwie rzeczy reszta będzie działa się już nieco za kulisami naszej aplikacji. Do dzieła:

Schemat blokowy nowego mechanizmu

Schemat blokowy nowego mechanizmu

Najpierw w klasie MainWindow stwórzmy swój slot:

Stwórzmy teraz jego implementację w pliku mainwindow.cpp:

W slocie nie robimy nic szczególnego poza odczytaniem wszystkich linii łańcucha znakowego zakończonego frazą \n w pętli while. Pozbywamy się fragmentu \r\n i logujemy dane w konsoli. Jeśli chcesz dokładnie przeanalizować ten fragment to odkomentuj fragmenty z użyciem qDebug().

Następnie skorzystamy (tak jak w poprzedniej części) z metody connect(). Umieścimy ją w metodzie pushButtonConnect_clicked():

Jeśli udało nam się otworzyć port, to łączymy sygnał readyRead() klasy QSerialPort ze slotem readFromPort() za pomocą metody connect(). Sygnał readyRead() jest emitowany w momencie, gdy w buforze odbiorczym znajdują się nowe dane do odczytu (możemy to sprawdzić w dokumentacji).

Sprawdźmy naszą aplikację, podłącz mikrokontroler, uruchom program i nawiąż połączenie, odebrane dane powinny pojawiać się w naszej konsoli. Efekt poniżej:

Odbieranie danych przez aplikację napisaną w QT

Odbieranie danych przez aplikację napisaną w QT

Może się zdarzyć, że po otwarciu portu w buforze odbiorczym będą znajdować się jakieś dane i wtedy możesz odebrać np. części jednej linii. W slocie readFromPort() możemy odebrane dane przepisać do tabeli lub przesłać dalej, możemy je też przeanalizować itp.

Czytanie jednej linii, nie jest zbyt optymalne i przydaje się wtedy kiedy operujemy na łańcuchach znakowych zakończonych znakiem nowej linii. Gdybyśmy przesyłali dane w jakiś usystematyzowany sposób (np.: protokół), wtedy interesowała by nas metoda readAll() klasy QSerialPort.

W naszym przypadku wyglądałoby to w ten sposób:

Mamy już zaimplementowane odbieranie danych, teraz możemy zająć się wysyłaniem danych do mikrokontrolera. W pliku mainwindow.h, pod słowem kluczowym private, stwórz nową metodę:

W pliku mainwindow.cpp stwórz następującą implementację, którą za chwilę dokładnie omówimy:

Nie robimy tutaj nic szczególnego, sprawdzamy tylko czy wybrany port COM jest otwarty i czy możemy wysyłać do niego nasze dane:

Następnie logujemy dane, które wysyłamy, a później kolejno następuje fizyczne przesłanie danych do urządzenia (dzięki metodzie write). Metoda write() jako argument przyjmuje tablicę elementów typu char, stąd musimy dokonać konwersji naszej wiadomości message z typu QString do tablicy typu char:

Teraz zajmiemy się podłączeniem przycisków sterujących diodą. Najpierw stwórzmy dla nich sloty. W implementacjach wpisujemy:

W implementacjach używamy stworzonej przed chwilą metody i wysyłamy odpowiednie wiadomości do mikrokontrolera. Ostatecznie powinniśmy uzyskać poniższy efekt, klikanie w przyciski sterujące diodą, co powinno przekładać się na faktyczny stan diody wbudowanej w Arduino (pin 13).

Działanie przycisków sterujących diodą

Działanie przycisków sterujących diodą

Na sam koniec, polecam dodanie przycisku pod oknem logów, który można połączyć z obiektem TextEdit, aby możliwe było jego czyszczenie. Służy do tego sygnał clear() obiektu TextEdit. Połączenie sygnału kliknięcia przycisku ze slotem czyszczenia okna logów możesz dokonać w trybie Design.

Ostateczna wersja programu z przyciskiem czyszczącym logi

Ostateczna wersja programu z przyciskiem czyszczącym logi

Dodatek dla chętnych

Przeczytaj dokumentację dla klasy QSerialPortInfo i zapoznaj się z jej dodatkowymi metodami np.: manufacturer(), productIdentifier(), vendorIdentifier(). Możesz je wykorzystać np. do automatycznego łączenia się z jedną, konkretną płytką. Poniżej wyniki dla moich płytek:

  • Funduino Uno (klon Arduino Uno),
  • oryginalne Arduino Due,
  • Teensy 3.6.

W przypadku Teensy na tej stronie podany jest numer Vendor ID (16C0HEX 5824DEC), co zgadza się z tym, który otrzymałem z aplikacji. Co ciekawe, w przypadku Funduino otrzymałem ten sam VendorID, co dla oryginalnej płytki Arduino Due.


Przesyłanie danych w postaci łańcuchów znakowych nie jest zbyt optymalne. W swoich rozwiązaniach stosuję własny protokół przypominający trochę protokół MODBUS. Komunikacja w ogólnym przypadku odbywa się w sposób ustalony i na zasadzie zapytań, czyli jedna jednostka (master) odpytuje inną jednostkę lub jednostki (slave).

Dane przesyłam w postaci zdefiniowanych i opisanych pakietów złożonych z bajtów. Każdy pakiet posiada również sumę kontrolną w postaci CRC, dzięki czemu wiem, że odebrane dane są poprawne. Ramka pakietu (złożona z bajtów) składa się z kilku fragmentów np.: preambuły, danych i CRC. Polecam, a wręcz nakłaniam do napisania czegoś podobnego. Jest to ciekawe ćwiczenie, które może przydać się w późniejszych projektach.

Podsumowanie

W tej części zbudowaliśmy aplikację desktopową i zaprojektowaliśmy interfejs, który pozwolił nam na uzyskanie połączenia poprzez port szeregowy z Arduino. W konsekwencji zbudowaliśmy własny panel operatorski, który umożliwił sterowanie diodą podłączoną do mikrokontrolera.

Czy artykuł był pomocny? Oceń go:

Średnia ocena 4.8 / 5. Głosów łącznie: 76

W kolejnej części zajmiemy się konfiguracją Qt do budowania aplikacji na urządzenia z Androidem, następnie zbudujemy prostą aplikację, którą uruchomimy właśnie na telefonie.

Autor: Mateusz Patyk

Nawigacja kursu


O Autorze

Autorem tej serii wpisów jest Mateusz Patyk, który zawodowo zajmuje się programowaniem systemów wbudowanych oraz rozwijaniem aplikacji na desktopy i urządzenia mobilne. Jego głównymi obszarami zainteresowań są systemy sterowania, egzoszkielety i urządzenia do wspomagania chodu człowieka. Prywatnie miłośnik dobrego kina i gier strategicznych.

arduino, programowanie, qt, uart

Trwa ładowanie komentarzy...