Kursy • Poradniki • Inspirujące DIY • Forum
RS-232, UART, USART - odrobina teorii
Kiedyś właściwie wszystkie komputery PC były wyposażone w interfejs RS-232. Obecnie USB prawie zupełnie wyparło starszy standard. Złącze szeregowe miało jedną ogromną zaletę – było proste w obsłudze (szczególnie porównując z USB).
Dlatego w przypadku mikrokontrolerów port szeregowy jest nadal popularny, chociaż coraz częściej w nieco innej postaci. RS-232 działał na dość nietypowych napięciach ±11V (właściwie tolerował od 3V do 15V), dlatego do jego obsługi potrzebny był dodatkowy układ konwertera napięcia np. MAX232.
W typowych mikrokontrolerach stosuje się uproszczoną wersję interfejsu, działającą na napięciach 0V i 3.3V (zamiast wspomnianych wcześniej). Moduł odpowiedzialny za obsługę takiej komunikacji nazywany jest UART (ang. Universal Asynchronous Receiver and Transmitter).
Transmisja rozpoczyna się od bitu startu, zaznaczonego na rysunku jako BS. Zawsze jest to bit będący logicznym zerem. Następnie, zależnie od konfiguracji, następuje po sobie 7, 8 lub 9 bitów danych (tutaj zaznaczone jako B0-B7), które są wysyłaną informacją. Bit stopu (zaznaczony tutaj jako bit BK) to bit będący logiczną jedynką - mówi o końcu transmisji.
Format ramki oraz sposób transmisji jest właściwie niezmieniony względem RS-232.
Płytka Nucleo wyposażona jest w konwerter z UART na USB. Nie mamy więc po drodze RS-232, a po podłączeniu do PC, przejściówka będzie widziana jako port COM, czyli nasz port szeregowy.
Różnice między UART, a USART
UART - Universal Asynchronous Receiver and Transmitter
USART - Universal Synchronus and Asynchronous Receiver and Transmitter
W przypadku komputerów PC za obsługę portu szeregowego odpowiedzialny był układ UART, czyli: Universal Asynchronous Receiver and Transmitter.
Nasz STM32 posiada moduł, który może pracować zarówno synchronicznie, jak i asynchronicznie, więc nazwa stała się jeszcze bardziej skomplikowana - USART, Universal Synchronus and Asynchronous Receiver and Transmitter. Używamy jednak tylko komunikacji asynchronicznej, więc obie nazwy możemy traktować jako synonimy. Jeśli chcesz dowiedzieć się więcej o UART oraz jego zastosowaniach przeczytaj również artykuł:
Port szeregowy i interfejs USART, czyli komunikacja z PC
Zauważyłem, że problem komunikacji z komputerem za pomocą portu szeregowego jest często poruszany na naszym forum. Napisałem kilka programów... Czytaj dalej »
Możliwość przesyłania danych jest nieoceniona podczas uruchamiania i testowania układów. Można w tym celu wykorzystywać debugger, np. przez interfejs SWD, jednak komunikaty testowe często są niezastąpioną metodą wyszukiwania błędów.
W kolejnych częściach kursu jeszcze wielokrotnie wykorzystamy komunikację przez UART, np. do testowania przetwornika ADC. Dodatkowym plusem komunikacji szeregowej jest możliwość łatwego zastąpienia przejściówki UART - USB modułem radiowym i przesyłanie informacji np. za pomocą Bluetooth lub WiFi.
Gotowe zestawy do kursów Forbota
Komplet elementów Gwarancja pomocy Wysyłka w 24h
Zestaw ponad 120 elementów do przeprowadzenia wszystkich ćwiczeń z kursu można nabyć u naszych dystrybutorów! Dostępne są wersje z płytką Nucleo lub bez niej!
Zamów w Botland.com.pl »Do obserwowania efektów pracy programów z tej części kursu konieczny jest dodatkowy program, terminal, przykładowo może to być darmowy: Tera Term lub Realterm.
Konfiguracja UART na STM32
Jak zwykle pierwszy krok to konfiguracja zegara. STM32 posiada kilka modułów USART, my wykorzystamy USART2, ponieważ jest on podłączony do przejściówki, która znajduje się na płytce.
1 2 |
__HAL_RCC_GPIOA_CLK_ENABLE(); __HAL_RCC_USART2_CLK_ENABLE(); |
Pierwsza instrukcja uruchamia zegar linii I/O portu A, druga uruchamia USART2.
Zanim uruchomimy UART, zmienimy konfigurację linii RX oraz TX. Linia wyjściowa (TX) jest obecna na wyprowadzeniu PA2, a wejście (RX) na PA3. Wyjście skonfigurujemy jako funkcję alternatywną w trybie push-pull, natomiast dla wejścia wystarczy ustawić pracę w trybie funkcji alternatywnej:
1 2 3 4 5 6 7 8 9 10 |
GPIO_InitTypeDef gpio; gpio.Mode = GPIO_MODE_AF_PP; gpio.Pin = GPIO_PIN_2; gpio.Pull = GPIO_NOPULL; gpio.Speed = GPIO_SPEED_FREQ_LOW; HAL_GPIO_Init(GPIOA, &gpio); gpio.Mode = GPIO_MODE_AF_INPUT; gpio.Pin = GPIO_PIN_3; HAL_GPIO_Init(GPIOA, &gpio); |
Gdy mamy już skonfigurowane linie, czas przygotować moduł USART2:
1 2 3 4 5 6 7 8 9 10 11 |
UART_HandleTypeDef uart; uart.Instance = USART2; uart.Init.BaudRate = 115200; uart.Init.WordLength = UART_WORDLENGTH_8B; uart.Init.Parity = UART_PARITY_NONE; uart.Init.StopBits = UART_STOPBITS_1; uart.Init.HwFlowCtl = UART_HWCONTROL_NONE; uart.Init.OverSampling = UART_OVERSAMPLING_16; uart.Init.Mode = UART_MODE_TX_RX; HAL_UART_Init(&uart); |
Prędkość transmisji 115200 jest jedną ze standardowych wartości. Jeśli pojawiałyby się błędy podczas komunikacji, można wykorzystać niższe, np.: 9600 lub 38400.
UART na STM32 - wysyłanie danych
Do wysyłania danych służy funkcja HAL_UART_Transmit. Przyjmuje ona cztery parametry: wykorzystywany interfejs (USART2), adres i wielkość bufora danych do wysłania oraz timeout.
Funkcja HAL_UART_Transmit pozwala na wysyłanie dowolnych danych, np. w formacie binarnym. My jednak będziemy chcieli wysyłać głównie napisy, możemy więc przygotować funkcję pomocniczą, która sama ustali długość napisu oraz dokona niezbędnych konwersji typów:
1 2 3 4 |
void send_string(char* s) { HAL_UART_Transmit(&uart, (uint8_t*)s, strlen(s), 1000); } |
Parametr 1000 to timeout w milisekundach. Oczywiście nie powinniśmy na sztywno kodować tzw. "magicznych liczb", ale to kod przykładowy, a akurat ten parametr nie jest nam w tej chwili potrzebny.
Wykorzystujemy najprostszą wersję funkcji wysyłającej dane przez UART. Działa ona blokująco, tzn. program jest wstrzymywany do zakończenia transmisji. Biblioteka HAL dostarcza jeszcze dwie podobne funkcje: HAL_UART_Transmit_IT oraz HAL_UART_Transmit_DMA, które pozwalają na transmisję bez blokowania używając odpowiednio przerwań i DMA.
Teraz możemy napisać program, który będzie wysyłał przez UART cały napis:
1 2 3 4 |
while (1) { send_string("Hello world!\r\n"); HAL_Delay(100); } |
Znak końca wiersza to nieustający problem w przypadku różnych systemów operacyjnych. System Windows używa dwóch znaków, CR LF (czyli \r\n) natomiast Linux tylko jednego LF (\n). Ponieważ przykłady uruchamiamy na komputerze z systemem Windows, wysyłamy dwa znaki.
Więcej o tym problemie można przeczytać na Wikipedii.
Cały kod realizujący wysyłanie napisu wygląda w sposób następujący:
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 |
#include <string.h> #include "stm32f1xx.h" UART_HandleTypeDef uart; void send_string(char* s) { HAL_UART_Transmit(&uart, (uint8_t*)s, strlen(s), 1000); } int main(void) { SystemCoreClock = 8000000; // taktowanie 8Mhz HAL_Init(); __HAL_RCC_GPIOA_CLK_ENABLE(); __HAL_RCC_USART2_CLK_ENABLE(); GPIO_InitTypeDef gpio; gpio.Mode = GPIO_MODE_AF_PP; gpio.Pin = GPIO_PIN_2; gpio.Pull = GPIO_NOPULL; gpio.Speed = GPIO_SPEED_FREQ_LOW; HAL_GPIO_Init(GPIOA, &gpio); gpio.Mode = GPIO_MODE_AF_INPUT; gpio.Pin = GPIO_PIN_3; HAL_GPIO_Init(GPIOA, &gpio); uart.Instance = USART2; uart.Init.BaudRate = 115200; uart.Init.WordLength = UART_WORDLENGTH_8B; uart.Init.Parity = UART_PARITY_NONE; uart.Init.StopBits = UART_STOPBITS_1; uart.Init.HwFlowCtl = UART_HWCONTROL_NONE; uart.Init.OverSampling = UART_OVERSAMPLING_16; uart.Init.Mode = UART_MODE_TX_RX; HAL_UART_Init(&uart); while (1) { send_string("Hello world!\r\n"); HAL_Delay(100); } } |
Rezultat powinien wyglądać tak:
UART na STM32 - odbieranie danych
Potrafimy już wysyłać dane. Teraz czas odebrać transmisję z PC - np.: instrukcje sterujące robotem lub innym urządzeniem. Do odbioru bajtu wykorzystamy funkcję HAL_UART_Receive.
Parametry funkcji odbierającej dane są bardzo podobne do omówionej wcześniej funkcji HAL_UART_Transmit. Jedyna różnica to znaczenie bufora - tym razem znajdą się w nim dane, które odbierzemy przez port szeregowy.
Aby ułatwić sobie życie i przygotować czytelne przykłady będziemy odczytywać dane po bajcie. Wystarczy więc tylko ustawić kiedy w buforze odbiorczym jest już bajt do odebrania. Wykorzystamy do tego makro __HAL_UART_GET_FLAG. Fragment odbierający dane wygląda następująco:
1 2 3 4 5 |
if (__HAL_UART_GET_FLAG(&uart, UART_FLAG_RXNE) == SET) { uint8_t value; HAL_UART_Receive(&uart, &value, 1, 100); } |
Oczywiście w tym momencie powinniśmy do czegoś wykorzystać zawartość zmiennej value – np. zapisać ją w buforze. Jeśli wystarczy nam bardzo proste sterowanie, po odebraniu znaku możemy wykonać czynność, przykładowo skręcić robotem, włączyć diodę, albo wysłać komunikat.
Prosty przykład wyglądałby następująco:
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 <string.h> #include "stm32f1xx.h" UART_HandleTypeDef uart; void send_string(char* s) { HAL_UART_Transmit(&uart, (uint8_t*)s, strlen(s), 1000); } int main(void) { SystemCoreClock = 8000000; // taktowanie 8Mhz HAL_Init(); __HAL_RCC_GPIOA_CLK_ENABLE(); __HAL_RCC_USART2_CLK_ENABLE(); GPIO_InitTypeDef gpio; gpio.Mode = GPIO_MODE_AF_PP; gpio.Pin = GPIO_PIN_2; gpio.Pull = GPIO_NOPULL; gpio.Speed = GPIO_SPEED_FREQ_LOW; HAL_GPIO_Init(GPIOA, &gpio); gpio.Mode = GPIO_MODE_AF_INPUT; gpio.Pin = GPIO_PIN_3; HAL_GPIO_Init(GPIOA, &gpio); uart.Instance = USART2; uart.Init.BaudRate = 115200; uart.Init.WordLength = UART_WORDLENGTH_8B; uart.Init.Parity = UART_PARITY_NONE; uart.Init.StopBits = UART_STOPBITS_1; uart.Init.HwFlowCtl = UART_HWCONTROL_NONE; uart.Init.OverSampling = UART_OVERSAMPLING_16; uart.Init.Mode = UART_MODE_TX_RX; HAL_UART_Init(&uart); while (1) { if (__HAL_UART_GET_FLAG(&uart, UART_FLAG_RXNE) == SET) { uint8_t value; HAL_UART_Receive(&uart, &value, 1, 100); switch (value) { case 'a': send_string("Odebrano: a\r\n"); break; case 'b': send_string("Odebrano: b\r\n"); break; case 'c': send_string("Odebrano: c\r\n"); break; } } } } |
Działanie programu w praktyce widoczne jest na poniższym zrzucie ekranu:
Zadanie domowe 5.1
Napisz program do sterowania diodami led, np. "A" włącza diodę 1, "a" wyłącza, "B" włącza diodę 2, itd. Diody podłącz do pinów, które wybierzesz samodzielnie.
Zadanie domowe 5.2
Napisz program, który odbiera kilka znaków, np. 3, zapisuje w buforze, a następnie wykonuje odebrane polecenie, np. "on1" włącza diodę 1, "of2" wyłącza diodę 2.
Zadanie domowe 5.3
Napisz program odbierający znaki, aż do znaku końca linii (\n), a następnie wykonaj polecenie zależne od odebranego ciągu, np. "on 1", "off 2".
Przekierowanie printf
Wykorzystując własną funkcję send_string możemy przygotować całkiem sprawną komunikację z PC lub innym mikrokontrolerem. Jednak o wiele wygodniej byłoby wykorzystać standardową instrukcję printf do wypisywania komunikatów na złącze szeregowe.
Funkcja send_string jest bardzo prosta i potrafi jedynie wysyłać ciągi znaków. Funkcja printf daje natomiast możliwość formatowania napisów oraz wyświetlania liczb zarówno całkowitych jak i zmiennopozycyjnych. Dzięki przekierowaniu wyjścia, będziemy mogli używać printf, a wynik działania pojawi się w oknie terminala portu szeregowego.
Dzięki temu będziemy mogli przetestować nieśmiertelny przykład:
1 |
printf("Hello world!\n"); |
UWAGA!
Wykorzystanie printf, chociaż wygodne, powoduje znaczne zwiększenie objętości programu. Jeśli nie jest to absolutnie niezbędne najlepiej unikać wykorzystywania tej funkcji w programach.
Okazuje się, że gdy wywołujemy printf, biblioteka wykonuje za nas mnóstwo pracy związanej z formatowaniem i przetwarzaniem parametrów, a na koniec wywołuje funkcję __io_putchar dla każdego wysyłanego bajtu. Wystarczy więc że napiszemy tę funkcję i printf będzie wysyłał dane na nasz port szeregowy:
1 2 3 4 5 |
int __io_putchar(int ch) { send_char(ch); return ch; } |
Funkcja send_char to nieco okrojona wersja wcześniej napisanej funkcji send_string. Zamiast napisu wysyła ona po prostu pojedynczy znak:
1 2 3 4 |
void send_char(char c) { HAL_UART_Transmit(&uart, (uint8_t*)&c, 1, 1000); } |
Cały kod będzie więc wyglądał jak poniższy:
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 |
#include <string.h> #include "stm32f1xx.h" UART_HandleTypeDef uart; void send_char(char c) { HAL_UART_Transmit(&uart, (uint8_t*)&c, 1, 1000); } int __io_putchar(int ch) { send_char(ch); return ch; } int main(void) { SystemCoreClock = 8000000; // taktowanie 8Mhz HAL_Init(); __HAL_RCC_GPIOA_CLK_ENABLE(); __HAL_RCC_USART2_CLK_ENABLE(); GPIO_InitTypeDef gpio; gpio.Mode = GPIO_MODE_AF_PP; gpio.Pin = GPIO_PIN_2; gpio.Pull = GPIO_NOPULL; gpio.Speed = GPIO_SPEED_FREQ_LOW; HAL_GPIO_Init(GPIOA, &gpio); gpio.Mode = GPIO_MODE_AF_INPUT; gpio.Pin = GPIO_PIN_3; HAL_GPIO_Init(GPIOA, &gpio); uart.Instance = USART2; uart.Init.BaudRate = 115200; uart.Init.WordLength = UART_WORDLENGTH_8B; uart.Init.Parity = UART_PARITY_NONE; uart.Init.StopBits = UART_STOPBITS_1; uart.Init.HwFlowCtl = UART_HWCONTROL_NONE; uart.Init.OverSampling = UART_OVERSAMPLING_16; uart.Init.Mode = UART_MODE_TX_RX; HAL_UART_Init(&uart); printf("Hello world!\n"); while (1) { if (__HAL_UART_GET_FLAG(&uart, UART_FLAG_RXNE) == SET) { uint8_t value; HAL_UART_Receive(&uart, &value, 1, 100); printf("Odebrano: %c\n", value); } } } |
Teraz już możemy uruchomić nasz przykład!
W przypadku Windows możemy zaobserwować dziwne zachowanie na końcu wiersza. W zależności od wykorzystywanego programu do komunikacji, zamiast napisów w kolejnych wierszach możemy zobaczyć schodki. Wynika to z innego sposobu kodowania końców linii w systemach Windows oraz Unix, o którym była mowa wcześniej.
Możemy zmienić komunikat dodając \r\n na końcu (tak jak robiliśmy poprzednio), albo udoskonalić procedurę wysyłającą dane:
1 2 3 4 5 6 7 |
int __io_putchar(int ch) { if (ch == '\n') send_char('\r'); send_char(ch); return ch; } |
Teraz komunikaty powinny pojawić się prawidłowo.
Poprawiony kod programu:
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 |
#include <string.h> #include "stm32f1xx.h" UART_HandleTypeDef uart; void send_char(char c) { HAL_UART_Transmit(&uart, (uint8_t*)&c, 1, 1000); } int __io_putchar(int ch) { if (ch == '\n') send_char('\r'); send_char(ch); return ch; } int main(void) { SystemCoreClock = 8000000; // taktowanie 8Mhz HAL_Init(); __HAL_RCC_GPIOA_CLK_ENABLE(); __HAL_RCC_USART2_CLK_ENABLE(); GPIO_InitTypeDef gpio; gpio.Mode = GPIO_MODE_AF_PP; gpio.Pin = GPIO_PIN_2; gpio.Pull = GPIO_NOPULL; gpio.Speed = GPIO_SPEED_FREQ_LOW; HAL_GPIO_Init(GPIOA, &gpio); gpio.Mode = GPIO_MODE_AF_INPUT; gpio.Pin = GPIO_PIN_3; HAL_GPIO_Init(GPIOA, &gpio); uart.Instance = USART2; uart.Init.BaudRate = 115200; uart.Init.WordLength = UART_WORDLENGTH_8B; uart.Init.Parity = UART_PARITY_NONE; uart.Init.StopBits = UART_STOPBITS_1; uart.Init.HwFlowCtl = UART_HWCONTROL_NONE; uart.Init.OverSampling = UART_OVERSAMPLING_16; uart.Init.Mode = UART_MODE_TX_RX; HAL_UART_Init(&uart); printf("Hello world!\n"); while (1) { if (__HAL_UART_GET_FLAG(&uart, UART_FLAG_RXNE) == SET) { uint8_t value; HAL_UART_Receive(&uart, &value, 1, 100); printf("Odebrano: %c\n", value); } } } |
Formatowanie tekstu z printf
Osoby, które nie spotkały się wcześniej z printf mogą nie widzieć jej zalet. Możliwości tej pozornie prostej funkcji są całkiem spore. Po więcej informacji warto zajrzeć np.: do manuala. Na zachętę przeprowadźmy szybkie doświadczenie.
Załóżmy, że naszym celem będzie pomiar napięcia przez ADC (o czym w kolejnym odcinku), wynik zaokrąglamy do części całkowitych i wyświetlamy w formie napisu:
Odczytana wartosc to X V!
Oczywiście, za X podstawiamy otrzymaną liczbę. Normalnie musielibyśmy konwertować liczbę, łączyć ją z tekstem lub wysyłać całość w trzech osobnych krokach (napis, wartość i jednostka). Korzystając z właściwości printf możemy zrobić to w jednej linii:
1 |
printf("Odczytana wartosc to %d V!\n", 2); |
Funkcja ta zamieni wystąpienie %d na cyfrę 2, która została podana jako argument funkcji. Printf pozwala na znacznie więcej operacji, zainteresowanych jeszcze raz odsyłamy do manuala.
Zadanie domowe 5.4
Wykorzystaj właściwości funkcji printf do wyświetlania na ekranie komputera napisu, w którego treści podstawiane są dwie liczby całkowite:
To jest XX cz. kursu STM32, a to jest mój X dzien nauki!
Zadanie domowe 5.5
Wykorzystaj właściwości funkcji printf do wyświetlania na ekranie komputera napisu, w którego treści podstawiana jest liczba zmiennoprzecinkowa oraz całkowita:
To zadanie domowe nr XX, a ten napis wyswietlono juz X razy
Podsumowanie
Poznaliśmy postawy komunikacji za pomocą portu szeregowego. Potrafimy wysyłać oraz odbierać komunikaty. W kolejnych częściach kursu wykorzystamy nabyte umiejętności do przesyłania wyników pomiarów, np. wartości odczytanych z przetwornika analogowo-cyfrowego.
Nawigacja kursu
Następna część naszego kursu STM32 będzie omawiała wykorzystanie przetwornika ADC. Jeśli nie chcesz przeoczyć kolejnego odcinka, to skorzystaj z poniższego formularza i zapisz się na powiadomienia o nowych publikacjach!
Autor kursu: Piotr Bugalski
Testy: Piotr Adamczyk
Redakcja: Damian Szymański
Powiązane wpisy
komunikacja, kursSTM32F1HAL, Nucleo, stm32, uart
Trwa ładowanie komentarzy...