Komunikacja mikrokontrolerów AVR przez interfejs UART i połączenie z komputerem
UART to interfejs, za pomocą którego możemy przesyłać dane asynchronicznie w obie strony pomiędzy dwoma urządzeniami. Większość mikrokontrolerów AVR ma wbudowaną obsługę UART, jednak nie wszystkie ale i to da się obejść.
Jak odbywa się transmisja UART?
Przede wszystkim potrzebujemy dwóch urządzeń, które będą się ze sobą komunikować. W naszym przypadku będzie to jeden z mikrokontrolerów AVR attiny84a lub atmega32a oraz komputer PC.
Oczywiście nic nie stoi na przeszkodzie aby to były dwa mikrokontrolery lub jakieś inne urządzenia obsługujące interfejs UART. Urządzenia te nie muszą mieć takiego samego taktowania ponieważ prędkość ustalamy.
Transmisja polega na przesyłaniu ramek danych składających się z bitów w określonej prędkości, która musi być taka sama na obydwu urządzeniach. Oprócz czasu przesyłania takiego jednego bitu obydwa urządzenia muszą też wiedzieć jak ramka jest zbudowana. Poniższy schemat przedstawia ramkę danych.
Załóżmy że chcielibyśmy przesłać taką ramkę. Wówczas musielibyśmy na pinie TXD ustawiać stany tak jak to zostało przedstawione na obrazku, czyli:
- Stan wysoki – kiedy żadne dane nie są przesyłane, na linii mamy po prostu stan wysoki
- Bit startu – kiedy chcemy poinformować drugie urządzenie że chcemy przesłać taką ramkę danych ustawiamy na tej linii stan niski na określony dla jednego bitu danych czas.
- 8 bitów danych – następnie manipulując stanem pinu w określonych równych okresach czasu przesyłam kolejno 8 bitów danych. Ilość ta jest konfigurowalna i może mieć też na przykład 7 lub 9 bitów ale najczęściej przesyła się jeden bajt
- 1 bit parzystości jest opcjonalny. Aby określić bit parzystości musimy zsumować ilość jedynek w bitach danych i zwrócić resztę z dzielenia przez dwa. Dzięki temu rozwiązaniu jesteśmy w stanie wyłapać większość błędów, ale nie wszystkie. Jeśli wynik rozjedzie się na tyle ze dwie jedynki nie będą się zgadzać to tego nie wyłapiemy, ale najczęściej drobne zakłócenia powoduję niezgodność w jednym bicie przesyłanych danych.
- Bit stopu – logiczny stan 1 po przesłaniu wszystkich bitów.
W drugą stronę jeśli chcielibyśmy odebrać dane przez pin RXD to wówczas czekamy aż pojawi się stan niski. Będzie to oznaczało że pojawił się bit startu, czekamy określoną ilość czasu i sprawdzamy stan ponownie, będzie to pierwszy bit danych. Powtarzamy tę czynność określoną ilość razy (przesyłamy najczęściej jeden bajt czyli 8 bitów). Dalej jeśli ustaliliśmy że przesyłamy bit parzystości możemy jeszcze zweryfikować poprawność danych i obsłużyć. Później jest jeden lub dwa bity stopu, w tym czasie możemy się przygotować do odbioru kolejnej ramki danych.
Jak ustalić prędkość przesyłania danych UART?
Prędkość przesyłania danych w BAUDach to najkrócej mówiąc ilość bitów jakie możemy przesłać w ciągu jednej sekundy. Tutaj do tej puli wchodzą też bity startu, stopu czy parzystości dlatego ilość realnie przesłanych danych będzie trochę mniejsza.
Domyślnie mikrokontroler jest taktowany z prędkością 1Mhz czyli 1000 000 razy na sekundę. Jeśli mamy wbudowaną obsługę wykorzystamy wartość z rejestru UBRR jeśli nie możemy spróbować określić opóźnienie w mikrosekundach wzorem F_CPU / BAUD. Na przykład jeśli chcemy przesyłać z prędkością 9600 baudów. 1000 000 / 9600 = 104,1666 US. Jeśli przesyłanie danych nie działa zbyt dobrze możemy spróbować podkręcić taktowanie procesora lub co jest prostsze ale nie zawsze możliwe zejść z prędkości. Gdy testowałem dla attiny84a najwidoczniej czas oczekiwania nie był zbyt dokładny, zadziałało dopiero jak zszedłem do 1200 baudów.
Spróbujmy rozpracować przykład dla mikrokontrolera atmega32a.
Połączenie mikrokontrolera Attiny84a z komputerem
Aby skomunikować się z komputerem przez interfejs UART możemy podłączyć mikrokontroler na co najmniej dwa sposoby. Pierwszy to złącze RS232, bardzo popularne, choć z mojego punktu widzenia już dość archaiczne. Nie widziałem go w laptopach i moim nowszym PC (można stosunkowo łatwo dodać, ale po co), spotkałem je w co najmniej jednym z moich starszych około 10 letnich (albo i lepiej) komputerów).
USB ma każdy i dzisiaj wykorzystamy konwerter USB-UART CP2102. Dość popularny i wygodny w użyciu.
Dziwnie się złożyło że zacząłem rozpracowywać tą komunikację z attiny84a, który jak się okazało nie ma wbudowanej obsługi tego interfejsu, ale myślę że dobrym wstępem będzie podjęcie próby samodzielnego napisania wysyłania i odbierania danych. Później wykorzystamy większy mikrokontroler aby wykorzystać rozwiązania jakie oferuje.
Jak podłączyć konwerter USB-UART CP2102 do mikrokontrolera Attiny84a?
Od strony konwertera interesują nas trzy wyjścia: GND, TxD i RxD. Gdybyśmy teraz korzystali na przykład z atmega32a co za chwilę zrobimy, szukalibyśmy wejść oznaczonych podobnie czyli RxD i TxD, w attiny84a wykorzystamy po prostu wejścia PB0 i PB1.
Co koniecznie musisz wiedzieć to oznaczenia skrótów TxD i RxD:
- TxD – Transmit Data – ten pin służy do wysyłania danych.
- RxD – Receive Data – ten pin służy do odbierania danych z innego urządzenia.
Z powyższego logiczne wydaje się że wyjścia te należy podłączyć krzyżowo. Czyli pin TxD z jednego urządzenia podłączamy do pinu RxD drugiego urządzenia ponieważ dane wysyłane z jednej strony są odbierane z drugiej. Analogicznie RxD z jednej strony łączymy do TxD z drugiej.
W naszym przykładzie łączę TxD do PB0 a RxD do PB1, GND bez niespodzianek z GND.
Jeśli chodzi o połączenie fizyczne to w zasadzie wszystko, wsadzamy wtyczkę usb do gniazda i podłączamy zasilanie do mikrokontrolera. Teraz trzeba jeszcze skonfigurować kilka rzeczy na komputerze.
Sterowniki i Putty
Po pierwsze będziemy potrzebować jakiegoś programu do obsługi połączeń szeregowych (serial), dzięki któremu będziemy mogli odbierać i wysyłać dane do mikrokontrolera. Ja korzystam chyba z najpopularniejszego choć trochę wiekowego Putty, który na ten moment jest w pełni wystarczający.
Na początek będziemy potrzebować port. Aby go znaleźć trzeba wejść do menadżera urządzeń. Bardzo możliwe że będzie pod Porty COM i LPT.
Jeśli jeszcze nie zainstalowałeś sterowników to najprawdopodobniej musisz wejść na stronę producenta w moim przypadku była to ta strona https://www.silabs.com/developer-tools/usb-to-uart-bridge-vcp-drivers?tab=downloads
Po zainstalowaniu sterowników, jeśli osiągniesz widok jak powyżej kliknij prawym i właściwości. Tutaj znajdziesz interesujący nas port i najprawdopodobniej też będzie to COM3.
Teraz należy otworzyć program Putty:
- Zaznaczyć Connection type: Serial
- Serial line – tutaj należy wpisać port, który chwilę temu odczytaliśmy czyli COM3
- Speed – czyli prędkość przesyłania danych są to bity na sekundę, za chwilę dojdziemy do tego jak to wyliczyć. Zaczniemy od 1200 chociaż najpopularniejsza prędkość to 9600 i zapewne będzie domyślna. Później klika się Open i powinna się pojawić konsola, za pomocą której będziemy mogli się komunikować z mikrokontrolerem (o ile będziemy mieli wgrany na nim odpowiedni program.
Co zrobić jeśli terminal jest zablokowany?
Może się zdarzyć tak że po otworzeniu terminala sprawia wrażenie zablokowanego. Czyli nie da się nic wpisać. Aby odblokować terminal przed otwarciem połączenia należy przejść do zakładki terminal i wybrać local echo na Force on. To powinno ułatwić wpisywanie i wysyłanie komend przez terminal.
Po połączeniu powinniśmy uzyskać możliwość komunikacji przez terminal jak na obrazku poniżej. Ale żeby uzyskać taki efekt potrzebujemy jeszcze oprogramować mikrokontroler.
Kod dla mikrokontrolera bez obsługi UART attiny84a (tylko do testów)
Może się zdarzyć że masz mikrokontroler, który nie obsługuje interfejsu UART. Niby nie warto ale da się to zrobić chociażby dla jakiegoś testu. Spróbujmy napisać program gdzie po wciśnięciu microSwitch wyślemy jakiś komunikat do konsoli. W drugą stronę jeśli wpiszemy na konsoli 1 i wyślemy to dioda się zapali a jeśli 0 to zgaśnie.
W tym przykładzie diodę LED podpinam po pin PB2 a microSwitch pod PA1. Wejście PB0 będzie robiło za TXD czyli wysyłka danych a PB0 za odbiór danych czyli RXD.
Zacznijmy od definicji prędkości taktowania, prędkości przesyłania, czasu opóźnienia dla wysyłania jednego bitu i określenia wejść dla LED i KEY.
1 2 3 4 5 6 7 8 9 |
#include <avr/io.h> #define F_CPU 1000000L // Zegar 1 MHz #include <util/delay.h> #include <avr/interrupt.h> #define BAUD 1200 //szybkość transmicji na 1200 baudów #define BIT_DELAY_US (1000000 / BAUD) // Opóźnienie między bitami w mikrosekundach #define LED (1<<PB2) #define KEY (1<<PA1) |
Teraz funkcja inicjalizująca, tutaj ustawimy stany na pinach
1 2 3 4 5 6 |
// Konfiguracja jako UART void USI_UART_init(void) { DDRB |= (1<<PB1); // Ustaw PB1 jako wyjście (TXD) DDRB &= ~(1 << PB0); //Ustaw BP0 jsko wejście (RXD) PORTB |= (1 << PB1); // Ustawienie stanu wysokiego } |
Teraz napiszemy funkcję, która będzie wysyłała jeden bajt danych według ramki z 1 bitem startu, 8 bitów danych i 1 bitem stopu. Czyli najprostsza możliwa.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
// Funkcja wysyłająca znak przez UART void USI_UART_transmit(char data) { // Start bit PORTB &= ~(1 << PB1); // Ustawienie stanu niskiego na początku transmisji _delay_us(BIT_DELAY_US); //wyślij 60 // Dane (8 bitów) for (uint8_t i = 0; i < 8; i++) { if (data & (1 << i)) { PORTB |= (1 << PB1); // Ustawienie stanu wysokiego dla bitu 1 } else { PORTB &= ~(1 << PB1); // Ustawienie stanu niskiego dla bitu 0 } _delay_us(BIT_DELAY_US); } // Stop bit PORTB |= (1 << PB1); // Ustawienie stanu wysokiego na końcu transmisji _delay_us(BIT_DELAY_US); } |
Zazwyczaj będziemy chcieli wysyłać całe stringi znaków, na przykład będzie to jakiś komunikat. Zróbmy do tego prostą funkcję z pętlą.
1 2 3 4 5 6 |
// Funkcja wysyłająca string przez USI UART void USI_UART_transmitString(const char* str) { while (*str) { USI_UART_transmit(*str++); } } |
Bardzo prawdopodobne że będziemy chcieli odebrać jakąś komendę z konsoli na komputerze. Możemy podjąć próbę napisania funkcji, która pobierze bajt danych czyli jeden znak char.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
// Funkcja odbierająca znak przez UART zmieniając stan na pinie char UART_receive() { char data = 0; // Czekaj na start bit (stan niski) while (PINB & (1 << PB0)); _delay_us(BIT_DELAY_US / 2); // Opóźnienie na połowę okresu bitu // Odczyt danych (8 bitów) for (uint8_t i = 0; i < 8; i++) { _delay_us(BIT_DELAY_US); if (PINB & (1 << PB0)) { data |= (1 << i); } } // Oczekiwanie na stop bit _delay_us(BIT_DELAY_US); return data; } |
Pozostało upchać to wszystko do funkcji głównej. Lepiej byłoby tutaj wykorzystać jeszcze przerwania ale skoro ma być najprościej jak się da to prawdopodobnie kod będzie wyglądał podobnie do tego poniżej.
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 38 39 40 41 42 43 44 |
int main(void) { DDRB |= LED; //ustawiam pin z diodą na wyjście PORTA |= KEY; //przycisk na stan wysoki żeby napięcie nie pływało int keyIsPressed = 0; USI_UART_init(); //inicjalizuję UART while (1) { //Sprawdzam czy guzik wciśnięty if(!(PINA & KEY)){ _delay_ms(5); if(!(PINA & KEY)){ if(keyIsPressed == 0){ PORTB ^= LED; if(PORTB & LED){ USI_UART_transmitString("Zapalam diode \n\r"); } else { USI_UART_transmitString("Gaszę diodę \n\r"); } } keyIsPressed = 1; } } else { keyIsPressed = 0; } if (!(PINB & (1 << PB0))) { // Jeśli PB0 jest niskie (dane odebrane) char received = UART_receive(); if (received == '0') { PORTB &= ~LED; //zgaś diode USI_UART_transmitString("Odebrano 0\n\r"); } else if (received == '1') { PORTB |= LED; //zapal diode USI_UART_transmitString("Odebrano 1\n\r"); } else { USI_UART_transmitString("Odebrano inny znak\n\r"); } } } } |
Ok pobawiliśmy się i mamy kawałek kodu, który powinno się dać odpalić na praktycznie każdym mikrokontrolerze. Sposób nie zalecany ale powinien być skuteczny.
Guzik zapala/gasi diodę i wysyła komunikat do terminala w putty. Z drugiej strony jeśli wpiszemy w terminalu zero i wyślemy enterem to dioda powinna zgasnąć, jeśli wyślemy 1 to powinna się zapalić. U mnie zaczęło to działać przy prędkości dopiero 1200 baudów, przy większych trochę mi się krzaczyło albo wysyłało błędne znaki. Miałem problem że konsola była zablokowana ale rozwiązanie przedstawiłem powyżej.
Obsługa USART w mikrokontrolerze atmega32a (lub innego mikrokontrolera AVR z obsługą UART)
Jeśli masz mikrokontroler z obsługą UART to najlepiej zapomnij o kodzie powyżej. Zaorajmy i zróbmy to jak należy na przykładzie Atmaga32a, podobnie będzie w atmega8 lub atmega88 i podobnych. (w tych dwóch wymienionych widziałem że chyba nawet piny będą te same).
Jak najprościej rozpoznać czy mikrokontroler ma obsługę UART?
Wystarczy spojrzeć na dokumentację i opis poszczególnych pinów. Jeśli mamy oznaczenia RXD i TXD to myślę że możemy działać śmiało. W każdym razie dokumentacja będzie potrzebna do określenia rejestrów ponieważ w różnych mikrokontrolerach bywają drobne różnice w nazwach.
Tak jak wcześniej w Attiny84a także i tutaj w Atmega32A podepnę diodę pod PB2 i przycisk microswitch pod PA1. I zaraz spróbujemy napisać program jak wcześniej, czyli wciśnięcie guzika wyśle komunikat i zapali diodę oraz komenda z konsoli zmieni stan diody.
Na początek definiujemy między innymi szybkość transmisji danych.
1 2 3 4 5 6 7 8 9 |
#include <avr/io.h> #define F_CPU 1000000L // Zegar 1 MHz #include <util/delay.h> #include <avr/interrupt.h> #define BAUD 9600 //szybkość transmicji na 1200 baudów #define MYUBRR F_CPU/16/BAUD-1 // Wyliczona wartość UBRR #define LED (1<<PB2) #define KEY (1<<PA1) |
Piszemy funkcję inicjalizującą gdzie przekażemy wartość ubrr do określenia prędkości przesyłania danych
1 2 3 4 5 6 7 8 9 |
void USART_Init(unsigned int ubrr) { // Ustawienie wartości UBRR UBRRH = (unsigned char)(ubrr>>8); UBRRL = (unsigned char)ubrr; // Włączenie transmisji i odbioru UCSRB = (1<<RXEN) | (1<<TXEN); // Ustawienie formatu ramki: 8 bitów danych, 1 bit stopu UCSRC = (1<<URSEL) | (1<<UCSZ1) | (1<<UCSZ0); } |
Będziemy też potrzebować funkcji do nadawania znaku i odbierania znaku
1 2 3 4 5 6 7 8 9 10 11 |
void USART_Transmit(unsigned char data) { // Czekaj, aż bufor nadawczy będzie pusty while (!(UCSRA & (1<<UDRE))) ; // Umieść dane w rejestrze bufora nadawczego UDR = data; } unsigned char USART_Receive(void) { // Czekaj na dane while (!(UCSRA & (1<<RXC))) ; // Odczytaj i zwróć dane z rejestru bufora odbiorczego return UDR; } |
I funkcja główna wysyłająca i odbierająca jakieś znaki.
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 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 |
int main(void) { DDRB |= LED; //ustawiam pin z diodą na wyjście PORTA |= KEY; //przycisk na stan wysoki żeby napięcie nie pływało int keyIsPressed = 0; unsigned int ubrr = MYUBRR; USART_Init(ubrr); // Inicjalizuj UART while (1) { //Sprawdzam czy guzik wciśnięty if(!(PINA & KEY)){ _delay_ms(5); if(!(PINA & KEY)){ if(keyIsPressed == 0){ PORTB ^= LED; if(PORTB & LED){ USART_Transmit('o'); USART_Transmit('n'); } else { USART_Transmit('o'); USART_Transmit('f'); } } keyIsPressed = 1; } } else { keyIsPressed = 0; } if (!(PIND & (1 << PD0))) { unsigned char received_char = USART_Receive(); // Odbierz znak USART_Transmit('t'); //wyświetl t USART_Transmit(received_char); // Wyślij odebrany znak z powrotem // Jeśli PB0 jest niskie (dane odebrane) /** char received = UART_receive(); if (received == '0') { PORTB &= ~LED; //zgaś diode USI_UART_transmitString("Odebrano 0\n\r"); } else if (received == '1') { PORTB |= LED; //zapal diode USI_UART_transmitString("Odebrano 1\n\r"); } else { //USI_UART_transmitString(received); USI_UART_transmitString("Odebrano inny znak\n\r"); } **/ } } } |