Zegar biurkowy na atmega328 i woltomierzach analogowych – akt czwarty

Zegar biurkowy na atmega328 i woltomierzach analogowych – akt czwarty

Witam Was w ostatniej części swojego cyklu. Wykorzystamy w niej przejściówkę UART<->USB i napiszemy program pozwalający nam na przestawianie zegara z poziomu komputera, a także zmianę jego ustawień wewnętrznych. Zaczynamy!

Po pierwsze potrzebujemy (oczywiście) wspomnianej już przejściówki UART<->USB. Przejściówkę tego typu możemy nabyć np. za pośrednictwem eBay’a z Chin (około 5 złotych z przesyłką do kraju, czas oczekiwania ok. 4 tygodnie), lub (identyczną)  na Allegro (ok. 12-13 zł, jednak jest u nas po 3-4 dniach).

Większość przejściówek, oprócz komunikacji z układem zapewnia także możliwość zasilania układu z portu USB – ja projektując płytkę zdecydowałem, że nie chcę korzystać z tej opcji. Aby wykorzystywać przejściówkę tylko w celu komunikacji musimy połączyć trzy piny – pin TX układu z pinem RX mikrokontrolera, odwrotnie – pin RX układu do pinu TX mikrokontrolera oraz musimy połączyć masy układów.

Po podpięciu przejściówki do układu podpinamy ją do portu USB (ja w tym celu użyłem przedłużacza USB, ale można też zrobić odwrotnie – przejściówkę wpiąć do USB i „na kablach” podłączyć ją do układu).

Opublikuj swój projekt i odbierz 50% rabatu do wykorzystania w sklepie Nettigo.pl

Po podłączeniu wypadałoby sprawdzić działanie przejściówki i jej podpięcia. W tym celu wgrywamy prosty program, który inicjalizuje port szeregowy i nadaje przez niego komunikat służący nam za potwierdzenie działania konwertera.

Po wgraniu kodu wybieramy w programie Arduino port (Narzędzia->Port szeregowy), pod którym znajduje się przejściówka (numer portu możemy sprawdzić w menadżerze urządzeń). Po wybraniu portu uruchamiamy Monitor portu szeregowego (również w menu Narzędzia) i (o ile prędkości transmisji w poleceniu Serial.begin oraz w oknie monitora portu szeregowego będą takie same) to powinniśmy zobaczyć komunikaty nadawane przez nasz mikrokontroler. Po odtańczeniu tańca zwycięstwa bierzemy się do dalszej roboty.

Oczywiście moglibyśmy zadowolić się konfiguracją za pomocą odpowiednio wpisywanych komend w oknie monitora portu szeregowego, ale zdecydowanie bardziej eleganckim rozwiązaniem będzie napisanie aplikacji okienkowej, która ubierze wszystkie komendy w przyjazny dla oka interfejs.

Postanowiłem skorzystać z programu Microsoft Visual Studio 2013 (dostępna jest w Internecie bezpłatna, 30 dniowa wersja próbna o pełnej funkcjonalności). Od razu uprzedzam – niniejszy opis nie jest nauką programowania w C i opisując kod i poszczególne czynności zakładam, że osoby czytające mają chociaż jako-takie pojęcie o programowaniu w C i miały już kiedyś styczność z tworzeniem aplikacji okienkowych.

Po pobraniu i zainstalowaniu programu uruchamiamy go i tworzymy nowy projekt (ja wybrałem projekt C#, ale w przypadku naszego w miarę prostego programu nie zrobi większej różnicy wybranie C++ czy innego języka z rodziny C). Następnym krokiem jest rozmieszczenie na ekranie naszej aplikacji wszystkich kontrolek, które chcemy na nim zawrzeć.

Wygląd naszego programu to oczywiście kwestia indywidualna. Ja, po uwzględnieniu wszystkich funkcjonalności, które chciałem zawrzeć w programie i zebraniu tego do kupy otrzymałem program o takim wyglądzie:

wyglad programu

Ważne są komponenty SerialPort, Timer (a właściwie dwa) oraz statusStrip znajdujące się POD samym oknem aplikacji. Oprócz nich program składa się z EditBox’ów (pola służące do wprowadzania danych), Label’i (czyli tekstu), CheckBox’ów (pola z ptaszkami do zaznaczania), ComboBox’ów (wprowadzanie danych, lub ich wybór ze zdefiniowanej listy), Buttonów (przycisków), a całość jest ogarnięta „tematycznie” przy pomocy GroupBox’ów.

Podstawą naszego programu będzie oczywiście odbieranie i wysyłanie danych z portu COM, dlatego pierwszym komponentem, który oprogramujemy będzie komponent SerialPort (który nazwałem po prostu „port”). Jednak zanim zabierzemy się za programowanie musimy zaplanować, w jaki sposób komputer i układ będą się z sobą porozumiewać. Po przemyśleniu i przyjrzeniu się podobnym programom w Internecie postanowiłem zrobić to następująco:

Komputer wysyła flagę, informującą układ o tym, czego komputer od niego oczekuje (w przypadku chęci odczytu danych), lub jakie dane zaraz mu wyśle (w przypadku wysyłania danych). Jeżeli mamy do czynienia z odczytem danych z układu układ odpowiada tą samą flagą, którą otrzymał z komputera, a po fladze nadaje żądane dane, jeżeli natomiast dane są wysyłane do układu – układ wie ilu danych jakiego typu się spodziewać i co z nimi zrobić.

Przykładowo:

Komputer wysyła flagę „1”.

Układ po otrzymaniu danych odczytuje flagę i sprawdza co ona oznacza (w tym przypadku jest to pobranie aktualnych ustawień zegara).

Układ wie już, że komputer oczekuje na dane od niego, więc tym razem to on wysyła flagę „1”.

Po nadaniu flagi układ nadaje żądane od niego dane.

Mając zaplanowaną obsługę danych możemy napisać funkcję, która będzie wywoływana w momencie odebrania danych przez komputer, a także funkcje dalej obsługujące odebrane dane.

Wyjaśnienia może wymagać wywoływanie funkcji w następujący sposób – this.Invoke(new EventHandler(update)). Z poziomu funkcji DataRecived nie mamy niestety możliwości bezpośredniego wpływania na elementy naszego arkusza (zmienianie napisów itd.) – wynika to z tego, że obsługa portu COM jest wykonywana w wątku niezależnym od obsługi samej formatki. Najprostszym sposobem obejścia tego problemu jest wpisanie danych, które chcemy przesłać do formatki do zmiennej globalnej i wywołanie (przy użyciu metody Invoke) funkcji mającej za zadanie przepisanie wartości tej zmiennej w pożądane miejsce.

Wiemy już, jak obsłużyć dane, które przyjdą do komputera, ale nasz komputer nie otrzyma żadnych danych, dopóki układ nie będzie ich nadawał, a on nie będzie ich nadawał dopóki nie otrzyma z komputera takiego polecenia. Wobec tego pod trzy przyciski (odpowiednio „Pobierz dane”, „Pobierz aktualny” i „Wyślij dane”) podpinamy następujące funkcje:

Ostatnia z funkcji będzie podpięta nie pod przycisk, a pod akcję CheckedChanged pola CheckBox o nazwie „ZEGAR” (w sekcji GODZINA).

Jak widać kod (w pierwszych dwóch i w ostatnim przypadku) ma za zadanie sprawdzić, czy port służący do komunikacji z układem został otwarty prawidłowo i, jeżeli tak, nadanie odpowiedniej flagi i zapisanie informacji o wysłanych danych do okna richTextBox1 (które będzie nam służyło do podglądania informacji przechodzących przez port z i do komputera). W trzecim przypadku sprawa jest trochę bardziej skomplikowana – będziemy tutaj nadawali dane, które najpierw przygotowywane są do wysłania w stringu „data”, a dopiero po jego przygotowaniu, sprawdzeniu poprawności jego długości i faktu otwarcia portu – nadane. Ale zaraz, zaraz. Napisałem że powyższy kod sprawdzi, czy port komunikacji został już otwarty, a nigdzie go nie otwieram. Wobec tego szybko dodajemy kod do przycisku „POŁĄCZ”:

Kod ten pobierze nazwę portu z pola port_name, pożądaną prędkość z pola port_speed, wpisze te dane do konfiguracji naszego portu i spróbuje go otworzyć. Spróbuje? Tak, spróbuje – port może być niedostępny z wielu powodów (może być zajęty, chociażby przez monitor portu szeregowego Arduino), lub też zwyczajnie może być podana zła jego nazwa – z tego właśnie powodu cały kod ujęty jest w sekcji try{(…)}catch{(…)}. Taka konstrukcja działa następująco – wykonywany zostaje kod z sekcji try, a jeżeli przy jego wykonywaniu zostanie napotkany jakiś błąd to wykonana zostaje sekcja catch. W moim przypadku jeżeli nie uda się z jakiegoś powodu połączyć z układem kod błędu zostanie umieszczony w polu richTextBox1 (o którym już wspominałem). Nasz program potrafi już nadawać i odbierać dane, teraz musimy tej samej sztuki nauczyć nasz mikrokontroler. W tym celu modyfikujemy kod  naszego zegara do następującej postaci:

W kodzie nastąpiło parę zmian (pomijając dopisanie kodu obsługującego Serial Port) od jego poprzedniej wersji, która zamykała drugą część mojego artykułu. Zmieniły się piny, pod które podpięte są przyciski (zmiana ta wyniknęła ze względów praktycznych – łatwiej mi było zaprojektować płytkę drukowaną, a dodatkowo zwolniło to piny RX oraz TX układu), niektóre ze zmiennych globalnych utraciły status „const”, zmienił się także sposób wyświetlania godziny oraz wyliczania podświetlenia. Przy wyświetlaniu godziny układ sprawdza, czy godzina właśnie pobrana z zegara RTC różni się od tej, aktualnie wyświetlonej i zmienia wyświetlaną godzinę, tylko jeżeli faktycznie nastąpiła zmiana. Zmiana podświetlenia wyniknęła ze względów praktycznych – zegar stoi na biurku i jest skierowany frontem w kierunku łóżka – ciągłe jego podświetlenie przez całą noc trochę mi przeszkadzało, wobec tego dodałem konfigurowalny przedział godzin, w trakcie których podświetlenie w ogóle się nie uruchamia.

W głównej pętli loop(){(…)} pojawiła się część odpowiedzialna za obsługę komunikacji z komputerem. Po odebraniu danych (do zmiennej int) program porównuje jej wartość z oczekiwanymi znakami – po odczytaniu któregoś z pożądanych znaków układ oczekuje na kolejne dane, albo sam rozpoczyna ich nadawanie. Co do odczytu danych – Arduino standardowo pozwala na odczytywanie danych (np. liczb) po jednym znaku (dokładnie po jednym bajcie), dlatego napisałem prostą funkcję składającą cztery kolejne odebrane znaki w jedną wartość int – jest to funkcja int SerialRead4Int(){(…)} sposób jej działania jest dość prosty, a w przypadku odebrania nieprawidłowych danych funkcja zwróci w wyniku -1.

W związku z możliwością konfigurowania niektórych parametrów zegara pojawiła się konieczność zachowywania tych ustawień, nawet w momencie odłączenia zasilania układu. Z pomocą przychodzi nam tutaj wbudowana w mikrokontroler pamięć EEPROM, która zachowuje zapisane dane nawet w przypadku zaniku zasilania. Podobnie jak w przypadku odczytywania danych z Serial Port’u – dane do EEPROMU zapisywane są po jednym bajcie na raz. Krótkie poszukiwania w Internecie naprowadziły mnie na gotowy kod, który służy do zapisywania tam zmiennych int do wielkości 16bitów (dla wartości bez znaku – 65535, co nam w zupełności wystarczy) – są to funkcje EEPROMWriteInt oraz EEPROMReadInt. Przy używaniu tych funkcji musimy pamiętać o tym, że każda wartość zapełnia dwie komórki danych, czyli pierwszy zapis robimy na komórkę 0, a drugi na komórkę 2!

Trochę inaczej od pozostałych flag, obsługiwana jest flaga 4 – służy ona do przekazania układowi informacji, że komputer oczekuje od niego ciągłego przesyłania aktualnej godziny. Po odebraniu tej flagi układ zmienia sobie globalną flagę, której stan jest osobno sprawdzany w głównej pętli i jeżeli jest ona ustawiona – następuje wysłanie godziny. Ponowne otrzymanie flagi 4 powoduje przestawienie globalnej flagi i zatrzymanie nadawania godziny.

Po wgraniu powyższego kodu nie powinniśmy zauważyć różnicy w działaniu zegara, powinniśmy za to być już w stanie „porozmawiać” z całym układem przy pomocy naszego programu. Wobec tego uruchamiamy nasz program i sprawdzamy poprawność jego działania.

Następnym elementem, który oprogramujemy będzie możliwość przesłania aktualnej godziny do układu. Będzie można w tym celu albo skorzystać z czasu w zegarze systemowym, albo pójść na całość i zaprząc do tego atomowy zegar cezowy. Brzmi fajnie, ale podejrzewam, że niewielu z nas ma w domu taki zegar. Na szczęście w dobie Internetu nie stanowi to żadnego problemu – możemy skorzystać z tzw. serwerów czasu, które służą do synchronizowania zegarów przy użyciu specjalnego protokołu NTP.

Najpierw oprogramujemy jednak pobieranie czasu z systemu, jako że jest to prostsze. Pod metodę CheckedChanged pola CheckBox o opisie SYSTEM podpinamy następujący kod:

Jak widać, kod ten, w zależności od stanu pola CheckBox włącza lub wyłącza komponent o nazwie gettimesystem_ovf. Komponentem tym jest zwykły Timer, który wywołuje zdarzenie Tick() co 100ms (10 razy na sekundę). Kod dla zdarzenia Tick tego timera wygląda następująco:

Kod ten (oprócz odświeżania zegara na formatce) pokazuje także na osobnym polu tekstowym różnicę czasu między zegarem systemowym, a czasem pobranym z układu.

Wiemy już która dokładnie jest godzina na naszym zegarze systemowym, teraz bierzemy się za pobieranie czasu z Internetu.

Wspomniałem już, że do tego celu służy protokół NTP. Moglibyśmy oczywiście oprogramować cały ten protokół we własnym zakresie, ale zajęłoby nam to sporo czasu i duplikowalibyśmy rozwiązania już dostępne w Internecie. Po niedługich poszukiwaniach zdecydowałem się wykorzystać bardzo wygodną i dopracowaną klasę służącą właśnie do pobierania czasu z serwerów NTP napisaną przez Pana Valera Bocana, którą można pobrać w formie paczki zip (wraz z przykładowym programem, pokazującym sposób obsługi) z jego strony internetowej.

Po przeanalizowaniu załączonego przykładu, skutecznym dołączeniu pliku zawierającego wspomnianą już klasę do całego projektu Visual Studia doszedłem do następującego kodu zdarzenia CheckedChanged pola INTERNET, oraz pola Tick przewidzianego dla niego Timera:

Kod działa następująco – podobnie jak w przypadku czasu z systemu, zaznaczenie pola powoduje uruchomienie Timera, który w swoim kodzie sprawdza (poprzez zmienną globalną bool gottime), czy został już pobrany prawidłowo czas z Internetu. Jeżeli nie – zostaje on pobrany z serwera o nazwie znajdującej się w polu webtimeserver i wpisany do zmiennej globalnej TimeSpan recivedtime. Oprócz tego zostaje też zapisana godzina, o której nastąpił poprawny odczyt czasu z Internetu. W jakim celu? Otóż serwery NTP nie lubią ciągłego odpytywania o aktualną godzinę (a nasz kod robiłby to 10 razy na sekundę) – sugerowaną praktyką jest więc pobranie dokładnego czasu raz, zapisanie dokładnej godziny, o której nastąpiło pobranie tego czasu i dalej używanie różnicy zapisanej godziny z godziną aktualną (w ten sposób wiemy ile czasu upłynęło od pobrania czasu) i dodania tej wartości do poprzednio pobranej godziny. Dokładność, dla naszego rozwiązania, niemalże identyczna, a serwer nie jest niepotrzebnie zarzucany zapytaniami 10 razy na sekundę. Podobnie jak w przypadku czasu z systemu, tutaj także wyliczana jest różnica pomiędzy wzorcem z Internetu i aktualnie ustawioną godziną na zegarze.

Wiemy już (bardzo) dokładnie, która jest godzina, do pełni szczęścia brakuje nam możliwości podzielenia się tą informacją z naszym zegarem. W tym celu oprogramowujemy dwa przyciski SEND (jeden będzie wysyłał godzinę systemową, a drugi – godzinę z Internetu):

Kod, podobnie jak jeden z poprzednich zbiera dane do wysłania do jednego stringa i dopiero po skompletowaniu danych wysyła je do układu. Zastosowałem tutaj jeszcze jedną małą sztuczkę. Dane przygotowuję, dodając do nich jedną sekundę i wysyłam je dopiero w momencie gdy będą one poprawne (czyli gdy minie ta jedna sekunda) – dzięki temu uzyskuję (z grubsza, oczywiście) synchronizację co do dziesiątych części sekundy.

Kod aplikacji przewiduje już możliwość przesyłania do układu godziny, ale kod układu jeszcze nie wie, co z tak otrzymanymi danymi powinien zrobić. Szybko to poprawiamy modyfikując kod do następującej postaci: (część kodu usunąłem, ponieważ jest identyczny z tą z tym zamieszczonym powyżej)

Jak widać, pojawiła się nowa funkcja tmElements_t SerialReadTime(){(…)} – służy ona do odczytania z Serial Portu 6 kolejnych cyfr w formacie HHMMSS i stworzenia z nich obiektu klasy tmElements_t, który może zostać wysłany do układu RTC w celu przestawienia ustawionej w nim godziny. Funkcja ta jest wywoływana w pętli loop() w sposób widoczny powyżej.

Wgrywamy program do układu, uruchamiamy nasz program i sprawdzamy czy wszystko działa. Działa? Działa!

Ostatnią rzeczą, którą postanowiłem dodać do swojego projektu jest ukłon w stronę wrześniowej tematyki. Załóżmy, że w naszej pracy często współpracujemy z firmami zza oceanu, lub z kraju kwitnącej wiśni. W ramach naszej współpracy często przeprowadzamy konferencje telefoniczne, bądź wideo. Wygodnie byłoby móc szybko sprawdzić która jest „tam u nich” godzina, więc dodamy możliwość uwzględnienia przesunięcia strefy czasowej.

Po przemyśleniu kwestii postanowiłem załatwić to przy pomocy 4 kontrolek – jednego pola CheckBox, w którym zaznaczymy, czy uwzględnienie strefy czasowej naprawdę nas interesuje, dwóch pól ComboBox (do wyboru aktualnej strefy czasowej i strefy docelowej) i pola tekstowego, które wyświetli nam różnicę czasu między wybranymi strefami.

Kod dla metody SelectedIndexChanged obu pól (oba pola wywołują ten sam kod!) prezentuje się następująco:

Kod ten ogranicza się do prostej matematyki i wylicza różnicę czasu pomiędzy dwoma wybranymi strefami czasowymi, a następnie zapisuje ją jako zmienną typu TimeSpan do zmiennej globalnej timezonemod. W celu uwzględnienia ustawień strefy czasowej w każdym miejscu, w którym robimy coś z czasem (wyświetlamy go, wysyłamy itp.) musimy dodać kod, który sprawdzi, czy pole CheckBox informujące o konieczności uwzględnienia strefy czasowej jest zaznaczone i (jeżeli tak) dodaniu do przetwarzanej godziny wartości zmiennej timezonemod.

Oprócz tego, cała reszta rzeczy uwzględnionych w kodzie programu to już kosmetyka – dodałem dolną belkę w oknie programu, na której od razu widać, czy program jest połączony z portem COM, którym i z jaką prędkością. Dodałem też kod, który przy uruchomieniu programu sprawdzi w systemie jakie porty COM są dostępne i doda ich listę do rozwijalnego menu (domyślnie wybierając z tej listy pierwszy z nich). Do rozwijalnej listy dopisałem także parę polskich serwerów NTP (domyślnie ustawiłem serwer znajdujący się pod Poznaniem – w trakcie testów nie miałem z nim żadnych problemów, a z innymi potrafiły takie problemy być). Ostateczny kod całego programu wygląda następująco:

A kod dla mikrokontrolera: