W tym artykule, przedstawię zastosowanie bufora kołowego na przykładzie UARTa, działającego w oparciu o przerwania. Zakładam, że wiesz, czym są i jak działają przerwania oraz jakie korzyści płyną z ich stosowania.
Kod z tego przykładu powinieneś bez większego problemu przenieść na dowolny mikrokontroler, jakiego używasz. Ja wykorzystam tutaj biblioteki SPL dla mikrokontrolerów STM32, ponieważ wydaje mi się, że dla osób, które programują inne mikrokontrolery, kod będzie bardziej czytelny, niż w przypadku operacji na rejestrach. W artykule pominę proces inicjalizacji i opiszę wyłącznie fragmenty dotyczące bufora kołowego. Całkowite kody źródłowe, wraz z funkcjami inicjalizującymi i główną funkcją programu, dla mikrokontrolera STM32F103, a także dla mikrokontrolera AVR ATmega328, znajdziesz na moim githubie.
Odbiór danych
Zaczniemy od danych przychodzących. Przy inicjalizacji należy pamiętać o włączeniu przerwania Rx UARTa.
Na początku definiujemy tablicę oraz nasz bufor cykliczny. Uwaga, przypominam, że jeśli korzystamy z jakiejś zmiennej w programie głównym i przerwaniu należy zaopatrzyć ją w przydomek volatile!
1 2 3 |
volatile char uart_rxBuff[UART_RX_BUF_SIZE]; volatile circ_buffer_t uart_rx_circBuff = { uart_rxBuff, 0, 0 }; |
Słowo kluczowe volatile przed strukturą informuje kompilator, że wszystkie elementy struktury są ulotne, dzięki czemu nie musimy tego słowa pisać przed każdą zmienną w deklaracji struktury:
1 2 3 4 5 |
typedef struct { volatile char * const buffer; uint8_t head; uint8_t tail; } circ_buffer_t; |
Trzeba jednak dodać volatile przed zmienną wskaźnikową, ponieważ bez tego wskaźnik byłby traktowany jako volatile, ale dane na które wskazuje już nie.
Dane do bufora odbiorczego dodajemy w procedurze obsługi przerwania od danych przychodzących (Rx), a odczytujemy je z bufora dopiero wtedy, gdy zajdzie taka potrzeba, np. gdy przyjdzie ich określona ilość, przyjdzie określony znak lub gdy procesor nie będzie wykonywał innych ważniejszych zadań. Nie stosując buforów, gdy dane przychodzą szybciej niż możemy je obsłużyć dochodzi do ich nieodwracalnej utraty.
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 |
void USART2_IRQHandler(void) { // Identyfikujemy źródło przerwania // Jeśli jest to przerwanie od danych przychodzących if (USART_GetITStatus(USART2, USART_IT_RXNE) != RESET) { uint8_t head_temp = uart_rx_circBuff.head + 1; if ( head_temp == UART_RX_BUF_SIZE ) head_temp = 0; // Sprawdzamy czy jest miejsce w buforze if ( head_temp == uart_rx_circBuff.tail ) { // Jeśli bufor jest pełny to możemy tu jakoś na to zareagować // W procedurze obsługi przerwania nie można czekać na zwolnienie miejsca! // Ja w tym wypadku zdecydowałem się pominąć nowe dane. // Czyszczenie flagi USART_IT_RXNE: USART_ClearITPendingBit(USART2, USART_IT_RXNE); } // Jeśli jest miejsce w buforze to przechodzimy dalej: else { uart_rx_circBuff.buffer[head_temp] = USART_ReceiveData(USART2); uart_rx_circBuff.head = head_temp; } } // Jeśli jest to przerwanie od danych wychodzących if (USART_GetITStatus(USART2, USART_IT_TXE) != RESET) { // tym zajmiemy sie potem } } |
Na początku ISR identyfikujemy źródło naszego przerwania:
1 |
if(USART_GetITStatus(USART2, USART_IT_RXNE) != RESET) |
Jeśli jest to przerwanie od danych przychodzących to zajmujemy się ich wrzucaniem do bufora. Cały kod dotyczący bufora kołowego jest niemal identyczny jak ten, który był zamieszczony w poprzednim artykule w funkcji circ_buffer_put_char(), więc pominąłem większość komentarzy, które tam były, aby nie zaciemniać kodu. Zamiast wywołania funkcji wrzuciłem jej ciało do procedury obsługi przerwania. Jedyną różnicą, jaka została wprowadzona, jest odwoływanie się bezpośrednio do pól bufora cyklicznego jako zmiennej globalnej, a nie przez wskaźnik. Stąd znaki „.” zamiast „->”. Jest jeszcze jedna linia, która może Cię zastanawiać:
1 |
USART_ClearITPendingBit(USART2, USART_IT_RXNE); // czyścimy flagę USART_IT_RXNE |
Flaga USART_IT_RXNE pochodzi od „Receive Data register not empty”. Jest ona automatycznie czyszczona po wywołaniu funkcji USART_ReceiveData(). Jednak jak wiadomo, w buforze może nie być miejsca i tej funkcji nie uda się nam wywołać. W takim wypadku, gdybyśmy nie wyczyścili tej flagi, przerwanie byłoby cały czas wyzwalane, bo w rejestrze odbiorczym cały czas znajduje się bajt, którego nie możemy odczytać.
Do odczytania danych z bufora odbiorczego posłuży nam funkcja uart_get_char(), która różni się od znanej już Tobie funkcji circ_buffer_get_char() tylko tym, że tutaj, tak jak w procedurze obsługi przerwania, odwołujemy się do zmiennej globalnej bufora cyklicznego.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
int8_t uart_get_char(char *data) { if (uart_rx_circBuff.head == uart_rx_circBuff.tail) return -1; uart_rx_circBuff.tail++; if (uart_rx_circBuff.tail == UART_RX_BUF_SIZE) uart_rx_circBuff.tail = 0; *data = uart_rx_circBuff.buffer[uart_rx_circBuff.tail]; return 0; } |
Dobrze, to odbiór danych mamy już za sobą. Teraz zajmiemy się wysyłaniem.
Wysyłanie danych
Może zastanawiasz się po co buforować dane wychodzące i korzystać z przerwań podczas wysyłania? W celu automatyzacji! Już wyjaśniam. Każdy interfejs komunikacyjny potrzebuje określonej ilości czasu na wysłanie pojedynczego bajta danych. Dlatego, podczas wysyłania bloku danych, chcąc wysyłać kolejne bajty, musielibyśmy za każdym razem czekać, aż zostanie zakończone wysłanie poprzedniego. Zamiast tego, możemy wykorzystać bufor, do którego wrzucamy cały blok danych i przejść do innych czynności. Cała reszta będzie realizowana w przerwaniu od Tx, które jest wyzwalane za każdym razem, gdy zwolni się miejsce w rejestrze nadawczym UART i można wysłać kolejny bajt.
Podobnie jak przy odbiorze danych, definiujemy tablicę oraz bufor cykliczny.
1 2 3 |
volatile char uart_txBuff[UART_TX_BUF_SIZE]; volatile circ_buffer_t uart_tx_circBuff = { uart_txBuff, 0, 0, 0, }; |
Następnie tworzymy funkcję służącą do wysyłania pojedynczych znaków, a w zasadzie – do wrzucania ich do bufora.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
int8_t uart_put_char(char data) { uint8_t head_temp = uart_tx_circBuff.head + 1; if ( head_temp == UART_TX_BUF_SIZE ) head_temp = 0; if ( head_temp == uart_tx_circBuff.tail ) return -1; uart_tx_circBuff.buffer[head_temp] = data; uart_tx_circBuff.head = head_temp; USART_ITConfig(USART2, USART_IT_TXE, ENABLE); return 0; } |
Jak widzisz, niewiele się ona różni od znanej już Ci funkcji queue_put_char(). W przypadku wysyłania, przerwań nie włączamy przy inicjalizacji. Robimy to dopiero po dodaniu znaku do bufora, przed wyjściem z funkcji. Odpowiada za to linia:
1 |
USART_ITConfig(USART2, USART_IT_TXE, ENABLE); |
Pozostało już tylko napisanie procedury obsługi przerwania od Tx.
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 |
void USART2_IRQHandler(void) { // Identyfikujemy źródło przerwania // Jeśli jest to przerwanie od danych przychodzących if (USART_GetITStatus(USART2, USART_IT_RXNE) != RESET) { // To zostało już napisane wcześniej } // Jeśli jest to przerwanie od danych wychodzących if (USART_GetITStatus(USART2, USART_IT_TXE) != RESET) { // Sprawdzamy czy jest coś w buforze // Jeśli bufor jest pusty tzn, że wszystkie dane zostały wysłane // i możemy wyłączyć przerwanie od TX, które włączyliśmy w funkcji uart_put_char() if (uart_tx_circBuff.head == uart_tx_circBuff.tail) { USART_ITConfig(USART2, USART_IT_TXE, DISABLE); } else { uart_tx_circBuff.tail++; if (uart_tx_circBuff.tail == UART_TX_BUF_SIZE) uart_tx_circBuff.tail = 0; // Wysyłamy znak odczytany z bufora USART_SendData(USART2, uart_tx_circBuff.buffer[uart_tx_circBuff.tail]); } } } |
Flaga USART_IT_TXE pochodzi od „Transmit Data register empty” i jest ona automatycznie ustawiana przy wywołaniu funkcji USART_SendData() przez co przerwanie będzie cały czas wyzwalane dopóki nie wyślemy wszystkich znaków z bufora i go nie wyłączymy.
Podsumowanie
Tak oto dobiegamy do końca cyklu o buforze kołowym. Mam nadzieję, że po przeczytaniu całości wszystko stało się w miarę jasne. Jak widać, podczas korzystania z UARTa całość sprowadza się tylko do korzystania z dwóch funkcji:
1 2 |
int8_t uart_put_char( char data ); int8_t uart_get_char( char *data ); |
Inne, będą opierały się właśnie na nich. Dla przykładu – funkcja do wysyłania stringów:
1 2 3 4 5 |
void uart_put_string( char *s ) { while(*s) uart_put_char(*s++); } |
Poniżej przedstawiam jeszcze główną funkcję programu:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
#include "stm32f10x.h" #include "config.h" #include "../UART/uart.h" int main(void){ GPIO_Config(); UART_Config(); NVIC_Config(); while(1){ uart_put_string("www.EmbeddedDev.pl\n\r"); } } |
Jak widać, na początku musimy pamiętać jedynie o inicjalizacji UARTa i kontrolera przerwań, a potem już dowolnie korzystamy z opisywanych tu funkcji 🙂 Przypominam, że w razie potrzeby, ciała funkcji inicjalizujących oraz całkowite kody źródłowe z tego projektu znajdziesz na moim githubie.
Zostaw komentarz!
Podobała Ci się seria o buforze kołowym? Masz jakieś pytania lub uwagi? Zostaw mi proszę komentarz, a jeśli uważasz artykuły za wartościowe – podaj je dalej 🙂 Fajnie by było gdyby trafiły do większej ilości osób. Pozdro i do następnego! 😉