Obsługa interfejsu UART
Asynchroniczny interfejs szeregowy UART jest jednym z kilku najpopularniejszych standardów wymiany danych w systemach wbudowanych. Nic więc dziwnego, że także w strukturze naszego bohatera – mikrokontrolera MG32F103RBT6 – znajdziemy trzy takie bloki, przy czym w naszych eksperymentach posłużymy się peryferium o numerze 1. Do komunikacji posłużą nam dwie linie portu GPIOA:
PA9 – wyjście danych (TX),
PA10 – wejście danych (RX).
Aby przeprowadzić ćwiczenia opisane w niniejszym artykule, potrzebować będziemy dowolnego konwertera USB-UART pracującego z poziomami logicznymi 3,3 V. Konwerter podłączamy do złącza goldpin oznaczonego – a jakże – napisem UART, pamiętając o skrzyżowaniu linii danych (oznaczenia RX, TX znajdujące się na naszej płytce ewaluacyjnej odnoszą się do kierunków linii procesora).
Projekt 06 realizuje bodaj najprostszą funkcjonalność – „odbija” echo znaków odebranych z terminala komputera z powrotem do niego. Główny program jest zatem bardzo prosty (listing 1) – po wywołaniu funkcji konfigurującej blok UART oraz wyświetleniu komunikatu powitalnego procesor przechodzi do oczekiwania na znaki z użyciem funkcji blokującej UART_getchar(). Każdy odebrany bajt jest od razu przekazywany do funkcji UART_print(), która jako argument przyjmuje wskaźnik na zmienną typu char.
/* Kurs programowania mikrokontrolerow Megawin
Projekt 06 – obsluga interfejsu UART */
#include "mg32f10x.h"
#include "hardware.h"
int main(void)
{
char c;
/* inicjalizacja pozostalych peryferiow */
initPeripherals();
/* konfiguracja UART-a: 9600 bps @ 72 MHz */
UART_config(72000000, 9600);
/* wyswietlenie komunikatu powitalnego */
UART_print("Kurs EP – programowanie MCU Megawin!\n");
UART_print("UART echo\n");
UART_print(">>");
while (1)
{
/* odbior znaku z terminala */
c = UART_getchar();
/* echo z powrotem na terminal */
UART_print(&c);
}
}
Listing 1. Kod programu głównego UART echo
Na listingu 2 pokazano ciała wszystkich trzech funkcji służących do obsługi UART-a. Dokładne komentarze dopisane do każdej linii wyjaśniają znaczenie poszczególnych funkcji bibliotecznych – zrozumienie kolejnych realizowanych operacji nie powinno zatem nastręczać większych problemów. Zwróćmy jednak uwagę na kilka mniej oczywistych aspektów.
void UART_config(uint32_t apbclk_hz, uint32_t baudrate_bps){
/* zmienna do obliczenia rejestrow timingu */
uint32_t div;
/* wlaczenie taktowania interfejsu UART1 */
RCC->APB1PRE |= RCC_APB1PRE_SRCEN;
RCC->APB1ENR |= (RCC_APB1ENR_BMX1EN | RCC_APB1ENR_UART1EN);
/* reset interfejsu UART1 */
RCC->APB1RSTR |= RCC_APB1RSTR_UART1RST;
RCC->APB1RSTR &= ~RCC_APB1RSTR_UART1RST;
/* wyzerowanie rejestru funkcji modemowych */
UART1->MCR = 0x00;
/* obliczenie wartosci dzielnika */
div = apbclk_hz / baudrate_bps;
/* ulamkowa czesc dzielnika – tylko 4 najmlodsze bity */
UART1->DLF = div & 0x0F;
/* zezwolenie na zapis calkowitej czesci dzielnika */
UART1->LCR = UART_LCR_DLAB;
/* zapis calkowitej czesci dzielnika */
/* mlodsze polslowo */
UART1->DLL = (uint8_t)(div >> 4);
/* starsze polslowo */
UART1->DLH = (uint8_t)(div >> 12);
/* konfiguracja trybu transmisji: 8N1 */
UART1->LCR = UART_LCR_WLS_8BIT | UART_LCR_SBS_1BIT | UART_LCR_PARITY_NONE;
/* wlaczenie FIFO */
UART1->FCR = UART_FCR_FIFOE;
}
void UART_print(char *str)
{
/* petla po wszystkich znakach */
/* az do wystapienia znaku konca stringa = 0x00 */
while(*str)
{
/* oczekiwanie na zwolnienie bufora nadawczego */
while(!(UART1->LSR & UART_LSR_THRE)){};
/* zapis kolejnego bajtu do bufora */
UART1->THR = *str;
/* inkrementacja wskaznika */
str++;
}
/* oczekiwanie na wyslanie ostatniego bajtu */
while(!(UART1->LSR & UART_LSR_TEMT));
}
char UART_getchar(void){
/* oczekiwanie na odbior bajtu */
while(!(UART1->LSR & UART_LSR_DR));
/* zwrocenie odebranego bajtu */
return (UART1->RBR);
}
Listing 2. Funkcje do obsługi interfejsu UART
Dość mało intuicyjny może wydawać się sposób konfiguracji szybkości transmisji (baudrate). Nota katalogowa naszego mikrokontrolera podaje, że wartość dzielnika (obniżającego częstotliwość sygnału taktowania, czyli – w naszym przypadku – 72 MHz-owego przebiegu dostarczanego przez wewnętrzną pętlę PLL) można obliczyć poprzez podzielenie tejże częstotliwości przez 16-krotność docelowej prędkości transmisji (czyli 9600 bps). Ponieważ jednak dzielnik uwzględnia nie tylko część całkowitą, ale także 4-bitową część ułamkową, to w rzeczywistości wystarczy jedynie podzielić 72 MHz przez 9600 Hz, a ów wynik najpierw poddać funkcji iloczynu logicznego z maską 0x0F (ekstrakcja części ułamkowej), a następnie odpowiednio poprzesuwać w prawo – w celu wypełnienia zawartości młodszego i starszego rejestru przechowującego część całkowitą (DLL, DLH). Uważni Czytelnicy zauważą, że „w międzyczasie” musimy także odblokować dostęp do rejestrów dzielnika ustawiając bit UART_LCR_DLAB, co zdaniem producenta ma... wprowadzić dodatkowe zabezpieczenie.
W przypadku funkcji UART_print() warto zwrócić uwagę, że pętla while() automatycznie kończy swoje działanie po napotkaniu znaku końca stringa, czyli bajtu o wartości 0x00. Należy o tym pamiętać wykonując manipulacje na tablicach typu char[]. W przypadku używania funkcji UART_print() z „natywnymi” ciągami znaków (ujętymi w cudzysłowy), kompilator wprowadzi znak końca za nas, dzięki czemu nie musimy się martwić o niekontrolowane zapętlenie programu.
Obsługa zegara czasu rzeczywistego
Kolejny program – Projekt 07 – pozwala zapoznać się z metodami obsługi wbudowanego zegara RTC naszego mikrokontrolera. Przed przystąpieniem do eksperymentów należy upewnić się, że w gnieździe na płytce ewaluacyjnej znajduje się sprawna bateria CR2032, zaś jumper VBATSET jest ustawiony w pozycji BAT.06