01 kwietnia 2017

Komunikacja przez port szeregowy

Dawno dawno temu RS-232C był obowiązkowym wyposażeniem każdego szanującego się PCta. Te czasy dawno odeszły w zapomnienie, USB całkowicie wyparło dawne interfejsy komunikacyjne. Jednak w przypadku prostych mikrokontrolerów port szeregowy ma się bardzo dobrze i raczej jeszcze długo pozostanie w użyciu. Co prawda zamiast RS-232C używany jest często UART oraz bezpośrednie konwertery na USB, ale działanie portu jest praktycznie takie jak wiele lat temu. Płytka Nucleo posiada wbudowany konwerter UART-USB, można więc go wykorzystać do komunikacji z komputerem.

Interfejs szeregowy jest prosty i jak zaraz zobaczymy jego użycie na STM32F103 jest również bardzo proste. Najpierw musimy ustalić, który UART wykorzystamy do komunikacji. W instrukcji płytki Nucleo znajdziemy odpowiednią informację - jest to UART2. Teraz w dokumentacji mikrokontrolera musimy doczytać, do której magistrali jest dołączony - okazuje się że APB1. Pozostaje więc uruchomić taktowanie naszego portu:

RCC->APB1ENR |= RCC_APB1ENR_USART2EN;

Jak pamiętamy, nasz układ pracuje z zegarem 8MHz. Nie ustawialiśmy żadnych dzielników, więc sam procesor, jak i obie magistrale APB1 oraz APB2 pracują z tą częstotliwością.
Komunikacja z zewnętrznym interfejsem wymaga konfiguracji pinów, PA2 jest wyjściem (TX), a PA3 wejściem (RX). Wejście powinno być skonfigurowane bez rezystorów podciągających, a wyjście należy ustawić jako funkcję alternatywną:

// PA2 - USART2TX
GPIOA->CRL &= ~GPIO_CRL(GPIO_MASK, 2);
GPIOA->CRL |= GPIO_CRL(GPIO_AF_PP_50MHZ, 2);

// PA3 - USART2RX
GPIOA->CRL &= ~GPIO_CRL(GPIO_MASK, 3);

GPIOA->CRL |= GPIO_CRL(GPIO_IN_FLOAT, 3);

Teraz musimy skonfigurować sam UART. Najpierw wybieramy prędkość komunikacji - powiedzmy standardowe 115200. Do rejestru BRR musimy wpisać wartość podzielnika, czyli dzielimy częstotliwość pracy UART2 przez oczekiwaną prędkość transmisji:

USART2->BRR = 8000000 / 115200;

Pozostaje już tylko uruchamić moduł:

USART2->CR1 = USART_CR1_UE | USART_CR1_TE | USART_CR1_RE;

Flaga UE uruchamia moduł (UART enable), TE uruchamia nadawanie, a RE odbieranie danych. Wszystko gotowe, czas wysłać dane. Aby to zrobić, wystarczy zapisać dane do rejestru DR, czyli:

USART2->DR = data;

Jednak musimy sprawdzić, czy UART nie jest zajęty nadawaniem. Określa to flaga TXE w rejestrze SR. Cała procedura zapisu danej wygląda więc tak:

while ((USART->SR & USART_SR_TXE) == 0);
USART->DR = data;

Najpierw czekamy na zwolnienie bufora transmisji, a następnie wstawiamy do niego daną do wysłania. Ponieważ będzimy wysyłać napisy, a nie pojedyncze bajty, piszemy pomocniczą funkcję usart_send.

void usart_send(USART_TypeDef *USART, const char *s)
{
while (*s) {
while ((USART->SR & USART_SR_TXE) == 0);
USART->DR = *s++;
}
}

Teraz mamy już wszystko co potrzebujemy do napisania programu. Będzie on wysyłał komunikat powitalny po uruchomieniu oraz kolejne po naciśnięciu przycisku. Kod wygląda następująco:

void usart_send(USART_TypeDef *USART, const char *s)
{
while (*s) {
while ((USART->SR & USART_SR_TXE) == 0);
USART->DR = *s++;
}
}

int main(void)
{
/* Enable the GPIO Clock */
RCC->APB2ENR |= RCC_APB2ENR_IOPAEN | RCC_APB2ENR_IOPCEN;
RCC->APB1ENR |= RCC_APB1ENR_USART2EN;

// PA5 - LED
GPIOA->CRL &= ~GPIO_CRL(GPIO_MASK, 5);
GPIOA->CRL |= GPIO_CRL(GPIO_OUT_PP_50MHZ, 5);

// PC13 - button
GPIOC->CRH &= ~GPIO_CRH(GPIO_MASK, 13);
GPIOC->CRH |= GPIO_CRH(GPIO_IN_FLOAT, 13);

// PA2 - USART2TX
GPIOA->CRL &= ~GPIO_CRL(GPIO_MASK, 2);
GPIOA->CRL |= GPIO_CRL(GPIO_AF_PP_50MHZ, 2);

// PA3 - USART2RX
GPIOA->CRL &= ~GPIO_CRL(GPIO_MASK, 3);
GPIOA->CRL |= GPIO_CRL(GPIO_IN_FLOAT, 3);

USART2->BRR = 8000000 / 115200;
USART2->CR1 = USART_CR1_UE | USART_CR1_TE | USART_CR1_RE;
usart_send(USART2, "Hello world!\n");

while (1) {
if ((GPIOC->IDR & GPIO_IDR_IDR13) == 0) {
usart_send(USART2, "Button pressed!\n");
while ((GPIOC->IDR & GPIO_IDR_IDR13) == 0);
}
}
}

Uruchamiamy terminal na PC i sprawdzamy jak nasz program działa:

Wysyłanie danych przez port szeregowy mamy rozpracowane i widzimy, że to bardzo łatwe zadanie. Czas coś odebrać. Okazuje się, że nie jest to wcale dużo trudniejsze. Moglibyśmy użyć przerwań, albo najlepiej DMA, ale na początek po prostu odbierajmy dane w pętli głównej.
W tym celu musimy sprawdzić flagę RXNE w rejestrze SR, która jest ustawiana po odebraniu danych. Wtedy wystarczy odczytać wartość z rejestru DR (tego samego do którego wstawiamy dane do wysłania).

if (USART2->SR & USART_SR_RXNE) {
char data = USART2->DR;

Program wygląda więc następująco:


int main(void)
{
int i;

/* Enable the GPIO Clock */
RCC->APB2ENR |= RCC_APB2ENR_IOPAEN | RCC_APB2ENR_IOPCEN;
RCC->APB1ENR |= RCC_APB1ENR_USART2EN;

// PA5 - LED
GPIOA->CRL &= ~GPIO_CRL(GPIO_MASK, 5);
GPIOA->CRL |= GPIO_CRL(GPIO_OUT_PP_50MHZ, 5);

// PC13 - button
GPIOC->CRH &= ~GPIO_CRH(GPIO_MASK, 13);
GPIOC->CRH |= GPIO_CRH(GPIO_IN_FLOAT, 13);

// PA2 - USART2TX
GPIOA->CRL &= ~GPIO_CRL(GPIO_MASK, 2);
GPIOA->CRL |= GPIO_CRL(GPIO_AF_PP_50MHZ, 2);
// PA3 - USART2RX
GPIOA->CRL &= ~GPIO_CRL(GPIO_MASK, 3);
GPIOA->CRL |= GPIO_CRL(GPIO_IN_FLOAT, 3);

USART2->BRR = 8000000 / 115200;
USART2->CR1 = USART_CR1_UE | USART_CR1_TE | USART_CR1_RE;

usart_send(USART2, "Hello world!\n");

while (1) {
if (USART2->SR & USART_SR_RXNE) {
char message[] = "Received:  \n";
message[10] = USART2->DR;
usart_send(USART2, message);
}
if ((GPIOC->IDR & GPIO_IDR_IDR13) == 0) {
usart_send(USART2, "Button pressed!\n");
while ((GPIOC->IDR & GPIO_IDR_IDR13) == 0);
}
}
}

Sprawdzamy, czy działa:
Mamy więc działającą komunikację z komputerem. Na koniec warto jeszcze wspomnieć o znanej i lubianej funkcji printf. Jest ona bardzo wygodna, ale niestety potwornie zwiększa objętość kodu.
Program w opisanej wersji zajmuje 2,3KB pamięci, a po dodaniu printf 26,4KB - ponad 10 razy więcej. Skoro programujemy niskopoziomowo, a mikrokontroler nie ma 1MB pamięci, lepiej chyba zrezygnować z printf. Nawet jeśli będzie to nieco niewygodne.







Brak komentarzy:

Prześlij komentarz