W tej części zajmiemy się implementacją bufora kołowego w języku c. Jak wspomniałem w poprzednim artykule, do wydzielenia obszaru pamięci na potrzeby bufora wykorzystamy tablicę. Będzie to bardzo wygodne, ponieważ tablica zajmuje ciągły obszar w pamięci.
Struktura bufora kołowego
Typ zmiennej tablicowej możesz dobrać według swoich potrzeb, ja zdecydował się na jednobajtowy typ char. Podobnie jest z ilością elementów tablicy. Musisz dobrać odpowiednią wielkość, w zależności od tego, jaką ilość danych i z jaką częstotliwością będziesz chciał do niej zapisywać – tak, aby nadążać z ich odczytem i przetwarzaniem.
1 2 3 |
#define BUF_SIZE 10 extern char buffer[BUF_SIZE]; |
Jak już wiesz, potrzebne są nam jeszcze dwie zmienne do przechowywania indeksów head i tail. W celu zapewnienia przejrzystości kodu wszystkie niezbędne elementy do prawidłowego działania bufora ubierzemy w strukturę i utworzymy specjalny typ strukturalny.
1 2 3 4 5 |
typedef struct { char * const buffer; uint8_t head; uint8_t tail; } circ_buffer_t; |
Jak widzisz, oprócz znanych już Tobie zmiennych head, tail pojawiła się jeszcze jedna zmienna – zmienna wskaźnikowa. Podczas pisania programów, możliwe, że będziesz potrzebował więcej niż jednego bufora, a każdy z nich będzie potrzebował oddzielnej tablicy i dlatego potrzebujemy tej zmiennej. Posłuży ona do przechowywania adresu danej tablicy, aby móc do niej dopisywać i odczytywać z niej wartości. Słowo const informuje kompilator, aby ten nie pozwolił nam przez nieuwagę zmienić przechowywanego adresu tablicy, ponieważ w tym wypadku byłoby to bardzo niepożądane – moglibyśmy np. nadpisać jakieś inne dane. Przy takim zapisie stały jest wskaźnik, ale wartości w komórkach pamięci na które wskazuje mogą być zmieniane. Poniżej deklaracja bufora wraz z inicjacją: podajemy adres tablicy, a pozostałym zmiennym przypisujemy wartość 0.
1 |
circ_buffer_t circBuff = { buffer, 0, 0 }; |
Pozostało już tylko napisać dwie funkcje: jedną do dodawania danych do bufora, drugą do odczytywania danych z bufora. Jeśli dokładnie przeczytałeś poprzedni artykuł nie powinieneś mieć najmniejszego problemu z ich zrozumieniem. Przy każdej linii kody dodam komentarz, aby nie było żadnych wątpliwości 🙂
Wpisywanie danych do bufora
1 |
int8_t circ_buffer_put_char(circ_buffer_t *q, char data); |
Jak sama nazwa wskazuje, funkcja służy do dodawania znaku do bufora kołowego. Jako argumenty przyjmuje wskaźnik do utworzonej struktury oraz wartość, którą chcemy do niej dodać i zwraca wartość typu int8_t – może nią być 0, jeśli dodawanie elementu przebiegło pomyślnie lub -1, jeśli bufor jest pełny. W tym przypadku nie zdecydowałem się na nadpisywanie danych, ani oczekiwanie na zwolnienie miejsca w buforze. W razie potrzeby możesz to łatwo zmienić. Poniżej ciało funkcji:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
int8_t circ_buffer_put_char(circ_buffer_t *q, char data) { uint8_t head_temp = q->head + 1; // Przypisujemy do zmiennej następny indeks head // Jeśli był to ostatni element tablicy to ustawiamy wskaźnik na jej początek if ( head_temp == BUF_SIZE ) head_temp = 0; // Sprawdzamy czy jest miejsce w buforze. // Jeśli bufor jest pełny to wychodzimy z funkcji i zwracamy błąd. if ( head_temp == q->tail ) return -1; // Można zamiast tego czekać na zwolnienie miejsca: while (head_temp == q->tail); // Jeśli jest miejsce w buforze to przechodzimy dalej: q->buffer[head_temp] = data; // Wpisujemy wartość do bufora q->head = head_temp; // Zapisujemy nowy indeks head return 0; // wszystko przebiegło pozytywnie, więc zwracamy 0 } |
Odczytywanie danych z bufora
1 |
int8_t circ_buffer_get_char(circ_buffer_t *q, char *data ); |
Jak można się domyślić, ta funkcja służy do pobierania danych z bufora. Funkcja, podobnie jak poprzednia, zwraca 0, jeśli pobieranie przebiegło pomyślnie lub -1, jeśli wystąpił błąd – bufor jest pusty. Jako argumenty przyjmuje wskaźnik do utworzonej struktury oraz wskaźnik do zmiennej, do której chcemy zapisać odczytaną wartość.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
int8_t circ_buffer_get_char(circ_buffer_t *q, char *data) { // Sprawdzamy czy w buforze jest coś do odczytania // Jeśli bufor jest pusty to wychodzimy z funkcji i zwracamy błąd. if (q->head == q->tail) return -1; // Jeśli jest coś do odczytania to przechodzimy dalej: q->tail++; // Inkrementujemy indeks tail // Jeśli był to ostatni element tablicy to ustawiamy wskaźnik na jej początek if (q->tail == BUF_SIZE) q->tail = 0; *data = q->buffer[q->tail]; // Odczytujemy wartość z bufora return 0; // wszystko przebiegło pozytywnie, więc zwracamy 0 } |
Podsumowanie
Mam nadzieję, że jesteś miło zaskoczony prostotą tego kodu. Jeżeli pracujesz w środowisku wielowątkowym to należy jeszcze pamiętać o zapewnieniu atomowości operacji zapisu i odczytu danych z bufora. W kolejnym i już ostatnim artykule z serii o buforze kołowym, wykorzystamy powyższą implementację i w oparciu o nią stworzymy nieblokującą obsługę UART, korzystając z ogromnych zalet jakie daje nam obsługa przerwań w mikrokontrolerach. Rezultatem będzie wygodna i prosta w użyciu biblioteka, którą stosuję w niemal każdym projekcie.