Witam!
Niedawno napisałem tutorial na temat korzystania z interface szeregowego przy użyciu arduino.
Postanowiłem jednak napisać jeszcze jeden. Tym razem jednak wykorzystanie zostanie język C.
Opiszę tutaj podstawy korzystania z asynchronicznego portu szeregowego na mikrokontrolerach AVR.
Nie będę tutaj jednak powtarzał jeszcze raz informacji na temat protokołów itp. więc polecam najpierw przeczytać mój poprzedni artykuł na ten temat :
https://majsterkowo.pl/przesylanie-danych-przez-asynchroniczny-port-szeregowy-arduino-tutorial/
Dodatkowo przydatne mogą być informacje na temat programowania AVR które umieściłem w 2 częściowym artykule 5 prostych projektów AVR w C
https://majsterkowo.pl/5-prostych-projektow-avr-w-c-czesc-1/
https://majsterkowo.pl/5-prostych-projektow-avr-w-c-czesc-2/
Po tym krótkim wstępie pora na właściwy tutorial (:
Na początek
Na początek potrzebny nam będzie układ na którym będziemy eksperymentować :
Jest to bardzo prosty układ. Za mikrokontroler służy popularna Atmega328p, taktowana z zewnętrznego oscylatora kwarcowego 16 MHz.
Jest ona zasilana bezpośrednio ze złącza programatora co dodatkowo upraszcza całość.
Jako urządzenie do którego będzie gadała atmega zastosowałem konwerter USB-UART wbudowany w arduino. W tym celu należy podłączyć pin TX atmegi do pinu RX konwertera i pin TX konwertera do pinu RX atmegi.
Ale UWAGA! Pin RX konwertera jest podłączony do pinu TX Mikrokontrolera wbudowanego w arduino, tak samo pin TX konwertera jest podłączony do pinu RX mikrokontrolera. Tak więc aby użyć arduino jako konwerter USB – UART musimy wgrać program który nie będzie używał UART (najlepiej pusty ) i
poodłączyć przewody na odwrót : RX do pinu oznaczonego na płytce jako RX i TX do TX.
Po podłączeniu wszystkiego musimy ustawić fusebity.
Ustawienia fusebitów dla tego układu to :
1 |
lfuse:w:0xc7:m -U hfuse:w:0xdf:m -U efuse:w:0xff:m |
Podałem od razu jako argumenty do AVRDUDE.
Gotowa komenda to :
1 |
avrdude -p atmega328p -c usbasp -U lfuse:w:0xc7:m -U hfuse:w:0xdf:m -U efuse:w:0xff:m |
Co do wgrywania i kompilowania zamieszczonych tu programów, to opisałem to w moim artykule https://majsterkowo.pl/5-prostych-projektow-avr-w-c-czesc-1/
Lista rejestrów
Oto lista rejestrów które służą do kontroli działania UART w mikrokontrolerze Atmega 328p.
W innych mikrokontrolerach AVR jest podobnie.
Jednak zawsze należy najpierw się upewnić w dokumentacji danego mikrokontrolera.
Szczegóły na temat przeznaczenia poszczególnych bitów rejestrów można znaleźć w dalszej części artykułu oraz w dokumentacji mikrokontrolera.
Oto lista:
UDR0 – Rejestr danych
UCSR0A – Rejestr kontrolny A
UCSR0B – Rejestr kontrolny B
UCSR0C – Rejestr kontrolny C
UBRR0H / UBRR0L – Rejestry do ustawiania prędkości przesyłu danych.
Ustawienia i inicjalizacja
Notatka : Niektóre mikrokontrolery mają więcej niż 1 sprzętowy UART, w takiej sytuacji zmieniają się nazwy rejestrów i bitów.
W nazwach bitów i rejestrów dodałem małe “n”. Zastępuje ono numer UART. (Numeracja od 0).
Pierwszą rzeczą którą trzeba zrobić przed przesłaniem lub odebraniem danych jest ustawienie parametrów przesyłu danych.
Podstawowym z nich jest prędkość przesyłu ( w bitach na sekundę).
Teorytycznie prędkość danych może być dowolna jednak w praktyce 99.9% urządzeń korzysta z jednej z tych wartości :
- 300
- 600
- 1200
- 2400
- 4800
- 9600
- 14400
- 19200
- 28800
- 38400
- 57600
- 76800
- 115200
- 230400
- 25000
- 500000
- 1000000
Prędkość przesyłu danych jest ustawiana za pomocą rejestru UBRRn.
A konkretnie UBRRnH i UBRRnL ponieważ wartość ta ma dwa bajty (unsigned int).
Do UBRRnH zapisujemy 8 najbardziej znaczących bitów a do UBRRnL pozostałe 8 bitów zmiennej. W praktyce są to tylko 4 bity bo maksymalna prędkość nie jest na tyle duża aby zająć ostatnie 4 bity.
Jednak żeby było ciekawiej nie zapisujemy tam realnej prędkości ale wartość obliczoną ze wzoru :
(F_cpu/16xBAUD)-1
Przy czym F_cpu to prędkość zegara mikrokontrolera a BAUD to żądana prędkość przesyłu danych.
Następnie musimy tą wartość zapisać w tych dwóch rejestrach.
Przykładowy kod który to robi:
1 2 3 4 5 |
void set_baud(unsigned int ubrr) { UBRR0H = (unsigned char)(ubrr>>8); UBRR0L = (unsigned char)ubrr; } |
Oczywiście wartość “ubrr” trzeba najpierw policzyć, aby uniknąć robienia tego ręcznie można ją zdefiniować w ten sposób :
1 2 3 |
#define F_CPU 16000000 #define BAUD 9600 #define MYUBRR F_CPU/16/BAUD-1 |
Jej obliczanie można również przenieść do funkcji.
1 2 3 4 5 6 7 |
void set_baud(unsigned int baud) { unsigned int ubrr; ubrr=(F_CPU/16/baud)-1; UBRR0H = (unsigned char)(ubrr>>8); UBRR0L = (unsigned char)ubrr; } |
Ale to nie wszystko!
W rejestrze UCSRnA znajduje się bit U2Xn którego ustawienie na 1 powoduje podwojenie prędkości przesyłu danych.
Po jego ustawieniu, wartość dla rejestrów UBRRn można policzyć ze wzoru :
(F_cpu/8xBAUD)-1
Następną rzeczą którą trzeba zrobić jest ustawienie interface-u do pracy w trybie asynchronicznym. (Tak istnieje możliwość pracy w trybie synchronicznym)
Robi się to za pomocą bitów UMSELn0 i UMSELn1 w rejestrze UCSRnC. Aby USART odpalił w trybie asynchronicznym trzeba je ustawić na zero.
To znaczy, że nie należy nic robić bo domyślna wartość tych bitów to 0.
Teraz trzeba ustawić pozostałe rzeczy :
- Bit parzystości
- Ilość bitów danych
- Ilość bitów stopu
Za bity parzystości odpowiadają bity UPMn0 I UPMn1 w rejestrze UCSRnC.
Możliwe konfiguracje to
UPMn1 | UPMn0
- 00 – Bit parzystości nieobecny
- 01 – Zarezerwowane ustawienie
- 10 – Bit parzystości
- 11 – Bit nieparzystości
Następnym krokiem jest ustawienie ilości bitów danych.
Ustawia je się za pomocą bitów UCSZn2/1/0 w rejestrze UCSRnC
Możliwe konfiguracje to :
UCSZn2 | UCSZn1 | UCSZn0
- 000 – 5-bitów
- 001 – 6-bitów
- 010 – 7-bitów
- 011 – 8-bitów
- 100 – Zarezerwowane ustawnie
- 101 – Zarezerwowane ustawnie
- 110 – Zarezerwowane ustawnie
- 111 – 9-bit
Ostatnią rzeczą którą należy zrobić to ustawienie ilości bitów stopu.
Za ustawienia bitów stopu odpowiada bit USBSn z rejestru UCSRnC.
Wartość 0 oznacza, że jest jeden bit stopu, wartość 1 oznacza 2 bity stopu.
Po ustawieniu wszystkiego musimy jeszcze włączyć Interface szeregowy.
Co ciekawe nadajnik i odbiornik są włączane osobno.
Za włączanie i wyłączanie obu tych elementów odpowiadają bity :
- RXENn – W przypadku odbiornika
- TXENn – W przypadku nadajnika
Oba te bity znajdują się w rejestrze UCSRnB.
No dobrze pora więc na pierwszy przykład : Prosty program który zainicjalizuje nadajnik portu szeregowego i ustawi go na takie ustawienia :
- 9600 bitów na sekundę.
- 1 bit parzystości.
- 1 bit stopu.
- 8 bitów danych.
W tym przypadku napisana przez mnie funkcja umożliwia wybór prędkości przesyłu danych oraz tego które z modułów UART mają zostać uruchomione.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
void uart_init(unsigned int baud,bool RX,bool TX) { // ustawienie prędkości unsigned int ubrr; ubrr=(F_CPU/16/baud)-1; UBRR0H = (unsigned char)(ubrr>>8); UBRR0L = (unsigned char)ubrr; // ustawienie bitów parzystości UCSR0C |= 1<<UPM00; // ustawienie ilości bitów danych UCSR0C |= (1<<UCSZ01) | (1<<UCSZ00); // ustawienie bitów stopu // domyślnie jest 1 if(RX) { UCSR0B |= 1<<RXEN0; } if(TX) { UCSR0B |= 1<<TXEN0; } } |
Wysyłanie danych
Kiedy już wszystko zainicjalizowaliśmy i ustawiliśmy, pora na wysyłanie jednego bajta danych.
Procedura wysyłania danych jest bardzo prosta i wygląda następująco :
Najpierw musimy sprawdzić stan poprzedniej transmisji.
Zgodnie z dokumentacją, nie można ładować danych do bufora zanim poprzednia transmisja się nie zakończy.
W tym celu należy sprawdzić czy flaga UDREn ma wartość ma wartość 1. Jeśli ma to wtedy bufor jest gotowy by przyjąć nowe dane.
Do tego wystarczy taka linijka kodu :
1 |
while ( !( UCSR0A & (1<<UDRE)) ); |
Bufor nadawania i bufor odbierania dzielą ten sam rejestr UDRn.
Z tym, że odebrane z niego dane pochodzą z bufora danych odebranych a dane do niego zapisane trafiają do bufora danych wysyłanych.
W drugim kroku należy po prostu zapisać żądany bajt do bufora (znajdującego się pod rejestrem UDRn) i tyle.
Nadajnik wykryje, że bufor nie jest pusty i natychmiast prześle zawarte w nim dane.
Funkcja wysyłająca dane wygląda tak :
1 2 3 4 5 |
void UART_send( unsigned char data) { while(!(UCSR0A & (1<<UDRE0))); UDR0 = data; } |
Teraz mamy już wszystko niezbędne do wysłania pierwszego bajta danych!
Oto prosty program wysyłający 10 :
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 |
#include <avr/io.h> #include <util/delay.h> #include <stdbool.h>// do zmiennej typu BOOL void UART_init(unsigned int baud,bool RX,bool TX) { // ustawienie prędkości unsigned int ubrr; ubrr=(((F_CPU / (baud * 16UL))) - 1); UBRR0H = (unsigned char)(ubrr>>8); UBRR0L = (unsigned char)ubrr; // ustawienie bitów parzystości UCSR0C |= 1<<UPM01;// 1 bit parzystości // ustawienie ilości bitów danych UCSR0C |= (1<<UCSZ01) | (1<<UCSZ00); // ustawienie bitów stopu // domyślnie jest 1 if(RX) { UCSR0B |= 1<<RXEN0; } if(TX) { UCSR0B |= 1<<TXEN0; } } void USART_send( unsigned char data){ while(!(UCSR0A & (1<<UDRE0))); UDR0 = data; } int main(void) { UART_init(9600,false,true); while(1) { USART_send(10); _delay_ms(1000); } return 0; } |
Screenshoot z analizatora stanów logicznych potwierdza, że 10 została odebrana poprawnie :
Pora na coś większego :
Nieśmiertelne Hello World! (:
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 54 55 56 57 58 59 60 61 |
#include <avr/io.h> #include <util/delay.h> #include <stdbool.h>// do zmiennej typu BOOL void UART_init(unsigned int baud,bool RX,bool TX) { // ustawienie prędkości unsigned int ubrr; ubrr=(((F_CPU / (baud * 16UL))) - 1); UBRR0H = (unsigned char)(ubrr>>8); UBRR0L = (unsigned char)ubrr; // ustawienie bitów parzystości UCSR0C |= 1<<UPM01;// 1 bit parzystości // ustawienie ilości bitów danych UCSR0C |= (1<<UCSZ01) | (1<<UCSZ00); // ustawienie bitów stopu // domyślnie jest 1 if(RX) { UCSR0B |= 1<<RXEN0; } if(TX) { UCSR0B |= 1<<TXEN0; } } void USART_send( unsigned char data){ while(!(UCSR0A & (1<<UDRE0))); UDR0 = data; } int main(void) { UART_init(9600,false,true); while(1) { char message[12]; message[0]='H'; message[1]='e'; message[2]='l'; message[3]='l'; message[4]='o'; message[5]=' '; message[6]='W'; message[7]='o'; message[8]='r'; message[9]='l'; message[10]='d'; message[11]='!'; for(short i=0;i<=11;i++) { USART_send(message[i]); } _delay_ms(1000); } return 0; } |
Efekt :
Aby trochę to uprościć można jeszcze ewentualnie dodać funkcję wysyłająca stringi :
1 2 3 4 5 6 7 8 |
void UART_putstring(char* StringPtr){ while(*StringPtr != 0x00) { UART_send(*StringPtr); StringPtr++; } } |
Finalny kod wysyłający dane :
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 |
#include <avr/io.h> #include <util/delay.h> #include <stdbool.h>// do zmiennej typu BOOL void UART_init(unsigned int baud,bool RX,bool TX) { // ustawienie prędkości unsigned int ubrr; ubrr=(((F_CPU / (baud * 16UL))) - 1); UBRR0H = (unsigned char)(ubrr>>8); UBRR0L = (unsigned char)ubrr; // ustawienie bitów parzystości UCSR0C |= 1<<UPM01;// 1 bit parzystości // ustawienie ilości bitów danych UCSR0C |= (1<<UCSZ01) | (1<<UCSZ00); // ustawienie bitów stopu // domyślnie jest 1 if(RX) { UCSR0B |= 1<<RXEN0; } if(TX) { UCSR0B |= 1<<TXEN0; } } void UART_send( unsigned char data){ while(!(UCSR0A & (1<<UDRE0))); UDR0 = data; } void UART_putstring(char* StringPtr){ while(*StringPtr != 0x00) { UART_send(*StringPtr); StringPtr++; } } int main(void) { UART_init(9600,false,true); while(1) { UART_putstring("Hello world!"); _delay_ms(1000); } return 0; } |
Następnie postanowiłem podłączyć pin TX atmegi do pinu TX płytki arduino przez co zadziała ona jako konwerter USB-UART i będę mógł odbierać/wysyłać dane przez monitor portu szeregowego.
Jednak najpierw musiałem usunąć bit parzystości ponieważ w arduino IDE nie ma opcji ustawienie takich rzeczy jak Bity parzystości czy liczba bitów stopu ): . Musiałem też dodać znak nowej linii na końcu wysyłanego stringa żeby w terminalu dane nie pokazywały się w postaci super długiego ciągu znaków.
Jednak po tej zmianie, w monitorze portu szeregowego zobaczyłem “Hello world!”:
Odbieranie danych
Skoro już wiemy jak wysyłać dane to teraz pora na ich odbieranie.
W pierwszym kroku należy sprawdzić czy do odebrania są dostępne jakieś dane.
Za to odpowiada flaga RXCn w rejestrze UCSRnA.
Kiedy w buforze pojawiają się jakieś dane to wtedy przyjmuje ona wartość 1.
Następnie musimy po prostu odczytać dane z rejestru UDRn.
Napisałem więc dwie prościutkie funkcje zwracające obie te wartości :
1 2 3 4 |
bool UART_available() { return (UCSR0A & (1<<RXC0)); } |
1 2 3 4 |
unsigned char UART_read() { return UDR0; } |
Stworzyłem również ten przykład :
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 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 |
#include <avr/io.h> #include <stdbool.h>// do zmiennej typu BOOL void UART_init(unsigned int baud,bool RX,bool TX) { // ustawienie prędkości unsigned int ubrr; ubrr=(((F_CPU / (baud * 16UL))) - 1); UBRR0H = (unsigned char)(ubrr>>8); UBRR0L = (unsigned char)ubrr; // ustawienie bitów parzystości //UCSR0C |= 1<<UPM01;// 1 bit parzystości // ustawienie ilości bitów danych UCSR0C |= (1<<UCSZ01) | (1<<UCSZ00); // ustawienie bitów stopu // domyślnie jest 1 if(RX) { UCSR0B |= 1<<RXEN0; } if(TX) { UCSR0B |= 1<<TXEN0; } } void UART_send( unsigned char data){ while(!(UCSR0A & (1<<UDRE0))); UDR0 = data; } void UART_putstring(char* StringPtr){ while(*StringPtr != 0x00) { UART_send(*StringPtr); StringPtr++; } } bool UART_available() { return (UCSR0A & (1<<RXC0)); } unsigned char UART_read() { return UDR0; } int main(void) { DDRB |= 1<<0; UART_init(9600,true,true); while(1) { if(UART_available()) { unsigned char byte=UART_read(); if(byte=='a') { PORTB |= 1<<0; UART_putstring("Dioda zapalona \n"); } if(byte=='b') { PORTB &= ~(1<<0); UART_putstring("Dioda zgaszona \n"); } } } return 0; } |
Program odbiera bajty z portu szeregowego i jeśli ich wartość wynosi ‘a’ to wtedy zapala on diodę. Wartość ‘b’ gasi diodę.
Dodatkowo wysyłane jest potwierdzenie zgaszenia lub zapalenia diody.
Kontrola odebranych danych
UART mikrokontrolera ma wbudowane mechanizmy kontroli danych.
Mechanizmy te wykrywają :
- Błąd parzystości – Bit parzystości / nieparzystości nie zgadza się z danymi.
- Błąd ramki – Bit stopu został odebrany nieprawidłowo jako 0 a nie 1.
- Przepełnienie bufora – Bufor danych odebranych się przepełnił to jest zawiera dwa bajty i został wykryty bit startu.
Każdy z tych błędów wiąże się z utratą przesyłanych danych.
Flagi tych 3 błędów znajdują się w rejestrze UCSnA.
Nazwy bitów flag to :
- FEn – Błąd ramki.
- DORn – Przepełnienie bufora.
- UPEn – Błąd parzystości.
Kiedy nastąpi błąd, odpowiednia flaga zmienia swój stan na 1.
Flagi są czyszczone (zmiana stanu na 0) kiedy bufor jest odczytywany.
Wysyłanie i odbieranie pakietów z 9 bitami danych
Podane do tej pory metody będą działały dla każdej ilości bitów danych z wyjątkiem 9.
Problem z 9 bitami pojawia się bo jeden bajt ma 8 bitów. Więc nadmiarowy bit należy odczytywać i wysyłać osobno.
Procedura wysyłania danych z 9 bitami wygląda tak :
- Podobnie jak w przypadku normalnej transmisji należy poczekać aż bit UDREn zmieni swój stan na niski.
- Następnie musimy zapisać nadmiarowy (najbardziej znaczący) bit do bitu TXB8n w rejestrze UCSRnB.
- Potem należy zapisać pozostałe 8 bitów do rejestru UDRn.
Procedura odczytu danych wygląda natomiast tak :
- Należy sprawdzić czy w buforze są nowe dane.
- Należy odczytać nadmiarowy bit (najbardziej znaczący) z bitu RXB8n.
- Należy odczytać dane z rejestru UDRn.
Jednak w przypadku korzystania z 9 bitów danych należy pamiętać, że ogranicza to pojemność bufora do 1 bajtu ( zw względu na bity RXB8n i TBB8n).
Normalnie pojemność obu buforów to zawrotne 2 bajty.
Przerwania
Z UART związane są 3 przerwania o wektorach :
- USART_RX
- USART_UDRE
- USART_TX
Pierwsze przerwanie wykonuje się kiedy zostają odebrane dane.
Przerwanie drugie wykonuje się kiedy rejestr danych UDRn jest pusty.
Trzecie przerwanie wykonuje się kiedy UART kończy wysyłać dane.
Aby włączyć dane przerwanie należy ustawić odpowiedni bit rejestru UCSRnB na 1.
- USART_RX – Bit RXCIEn
- USART_UDRE – Bit UDRIEn
- USART_TX -Bit TXCIEn
Gotowy kod definiujący wektory przerwań :
1 2 3 4 5 6 7 8 9 10 11 |
ISR(USART_RX_vect) { } ISR(USART_UDRE_vect) { } ISR(USART_TX_vect) { } |
Warto zaznaczyć, że w celu korzystania z przerwań należy dołączyć bibliotekę :
1 |
#include <avr/interrupt.h> |
Należy również włączyć globalnie przerwania za pomocą bitu I w rejestrze SREG.
Trzeba również pamiętać o tym, że dla większej ilości UART w mikrokontrolerze, różne UART będą miały różne wektory przerwań.
Zawsze należy zapoznać się z dokumentacją danego mikrokontrolera.
Kilka słów na koniec
To by było na tyle jeśli chodzi o ten krótki tutorial.
Mam nadzieję, że okazał się pomocny (:
Komentarze, zarówno pozytywne jak i negatywne mile widziane.
Dziękuję. Nie działa mi tylko jedna rzecz:
Kiedy próbuję wysłać zmienną int np 9 to nie działa, a jeśli wyślę zmienną char ‘9’ to normalnie działa