fbpx

Pokazałem Ci do tej pory jak korzystać z nRF24L01 przy pomocy pollingu oraz zacząłem już coś działać z przerwaniami. Tak dokładniej to użyłem przerwania od odbioru danych, aby przychodzące dane odczytać z układu zamiast ciągłego odpytywania układu, czy już coś przyszło. Niby niewiele, ale zawsze był to jakiś stracony czas na komunikację z układem. Co, gdyby wykorzystać w pełni potencjał przerwań?

Przypominam Ci również, że moduły nRF24 możesz kupić bezpośrednio u mnie jednocześnie mnie wspierając.

Cała seria o nRF24L01+:

Przerwania w nRF24

W naszym układzie mamy dostępne 3 źródła przerwań:

  • od zakończenia odebrania danych – RX_DR
  • od zakończenia transferu (wysłania) – TX_DS
  • od przekroczenia maksymalnej ilości retransmisji przy nadawaniu – MAX_RT

Ilością retransmisji się nie będę zajmował. Każdy może mieć swój pomysł na jego realizację. Czy to wyrzucenie błędu, czy dalsze próby retransmisji. W bibliotece pozostawiam miejsce (callback) na implementację obsługi tego przerwania.

Pozostają więc dwa. Jedno od odebrania danych, które mam już częściowo zaimplementowałem. Częściowo, bo trochę zmienię koncepcję obsługi całości.

Drugie to od zakończenia nadawania. To przerwanie przyda nam się do tego, aby nie czekać niepotrzebnie na koniec transferu przez nRF24. Dzięki temu odblokuję MCU do innych zadań. Przyda się to bardzo w momencie, gdy chciałbym wysłać więcej niż 32 bajty “na raz” i trzeba podzielić wiadomość na paczki, które zmieszczą się w Payload nRFa.

Jak lepiej ogarnąć przerwania?

Przede wszystkim musiałem lepiej zorganizować obsługę przerwania. Słusznie zwrócił mi uwagę Przemysław (dambo), że mogę mieć konflikt interesów, jeśli chodzi o dostęp do SPI. Ogromne Ci za to dziękuję, bo kompletnie o tym zapomniałem!

Konflikt polegałby na tym, że przyszłoby przerwanie mając “zajęte” SPI w pętli głównej programu. W przerwaniu tym również wymagałem dostępu do SPI, więc robi się zgrzyt, bo dwa miejsca w kodzie chcą jednego SPI “na raz”. Są co najmniej dwa rozwiązania:

  • Zrobić “mutexa” na zasób SPI
  • Obsłużyć tak naprawdę przerwanie w mainie

Wybrałem bramkę numer dwa. W jaki sposób? Otóż w obsłudze przerwania EXTI ustawiam jedynie flagę, że takie przerwanie wystąpiło.

void nRF24_IRQ_Handler(void)
{
	Nrf24InterruptFlag = 1;
}

I tyle. Dzięki tej fladze dopiero w evencie od nRF24 czytam z rejestrów układu, które przerwanie wystąpiło i ustawiam kolejną flagę – już konkretną od rodzaju przerwania.

void nRF24_IRQ_Read(void)
{
	if(Nrf24InterruptFlag == 1)
	{
		Nrf24InterruptFlag = 0;

		uint8_t status = nRF24_ReadStatus();
		uint8_t ClearIrq = 0;
		// RX FIFO Interrupt
		if ((status & (1 << NRF24_RX_DR)))
		{
			nrf24_rx_flag = 1;
			ClearIrq |= (1<<NRF24_RX_DR); // Interrupt flag clear
		}
		// TX Data Sent interrupt
		if ((status & (1 << NRF24_TX_DS)))
		{
			nrf24_tx_flag = 1;
			ClearIrq |= (1<<NRF24_TX_DS); // Interrupt flag clear
		}
		// Max Retransmits interrupt
		if ((status & (1 << NRF24_MAX_RT)))
		{
			nrf24_mr_flag = 1;
			ClearIrq |= (1<<NRF24_MAX_RT); // Interrupt flag clear
		}

		nRF24_WriteStatus(ClearIrq);
	}
}

Tym sposobem mam pewność, że SPI będzie zwolnione. No prawie, bo jeszcze może być zajęte przez inny układ na DMA… Jednak przy używaniu DMA według mnie warto brać cały interfejs na wyłączność dla jednego układu, więc inne mogą się wtedy nie wcisnąć 🙂

Teraz w funkcji nRF24_Event mogę się tymi flagami zająć. Na tym etapie wygląda ona tak.

void nRF24_Event(void)
{
	nRF24_IRQ_Read(); // Check if there was any interrupt

	if(nrf24_rx_flag)
	{
		nRF24_EventRxCallback();
		nrf24_rx_flag = 0;
	}

	if(nrf24_tx_flag)
	{
		nRF24_EventTxCallback();
		nrf24_tx_flag = 0;
	}

	if(nrf24_mr_flag)
	{
		nRF24_EventMrCallback();
		nrf24_mr_flag = 0;
	}
}

Mamy odczytanie przerwań oraz reakcje na nie przez callbacki.

Zarządzanie danymi

Można teraz całą obsługę zrobić w callbackach, jednak ja je pozostawię jako dodatkowe. W poprzednim wpisie na blogu przybliżyłem Ci ideę bufora cyklicznego. Napisałem, że chcę go wykorzystać przy transmisji nRF24. Tak też zrobiłem.

Stworzyłem dwa bufory – odbiorczy i nadawczy. Po nazwach można już się domyślić do czego będą one służyć.

W odbiorczym pojawią się dane zaraz po odebraniu, czyli po wystąpieniu przerwania RX_DR.

Do nadawczego z kolei to my będziemy wpisywać to, co chcemy wysłać. Biblioteka, a właściwie funkcja eventowa będzie sprawdzała, czy jest coś do wysłania i jeśli radio będzie wolne to wyśle to, co jest w buforze.

Dzięki takiemu rozwiązaniu nie przejmujemy się co w danej chwili robi nRF24L01. Pchamy do i wyciągamy z buforów dane, kiedy jest nam wygodniej. Mało tego! Pokażę Ci, że nie musi Cię martwić limit 32 bajtów w Payload! Można to obsłużyć z automatu.

Bufory kołowe

Zmodyfikowałem lekko bufor, który opisałem poprzednio. Implementacja  na potrzeby edukacyjne była bardzo prosta. Ja potrzebuję tutaj czegoś bardziej zaawansowanego, ale niekoniecznie pełnego zbędnych funkcji.

Po pierwsze zająłem się rozmiarem bufora. Zamiast stałego rozmiaru każdego z buforów pozwoliłem sobie zastosować coś, co w standardzie C99 nazywa się Flexible array member. Mówiąc w skrócie, ostatni element w strukturze danych może być tablicą dynamiczną. To właśnie będzie mój bufor na dane. Dzięki temu mogę mieć mały bufor nadawczy, a duży odbiorczy, jeśli jest taka potrzeba.

Użycie FAM sprawia, że obiekty muszą być tworzone dynamicznie z użyciem malloc. Definicja struktury wygląda w ten sposób.

typedef struct
{
	uint8_t Size;
	uint8_t Head;
	uint8_t Tail;
	uint8_t Elements;
	uint8_t Buffer[];	// Flexible Array Member
} RingBuffer;

Jak widzisz ostatni element to wskaźnik na tablicę. Co ciekawe, gdy zrobisz sizeof(RingBuffer), to zwróci on wartość 4 jakby tej tablicy w ogóle nie było 🙂

Jak stworzyć teraz taki bufor?

RB_Status RB_CreateBuffer(RingBuffer **Buffer, uint8_t Size)
{
	*Buffer = malloc(sizeof(RingBuffer) + (sizeof(uint8_t) * Size));

	if(Buffer == NULL)
	{
		return RB_NOTCREATED;
	}

	(*Buffer)->Size = Size;
	(*Buffer)->Head = 0;
	(*Buffer)->Tail = 0;
	(*Buffer)->Elements = 0;

	return RB_OK;
}

W mallocu podajesz rozmiar struktury plus liczbę elementów w tablicy. To tyle.

Jak widzisz w strukturze stworzyłem pole Size. Trzeba teraz pamiętać jakiego rozmiaru jest bufor, bo każdy może być inny. Pole Elements będzie nas informowało ile danych jest aktualnie przechowywane.

Do działania z nRF24 stworzyłem jeszcze funkcję zwracającą właśnie ile elementów przechowuje bufor.

uint8_t RB_ElementsAvailable(RingBuffer *Buffer)
{
	return Buffer->Elements;
}

Bufory tworzone są podczas inicjalizacji nRF24 i są widoczne tylko w obrębie jego biblioteki.

Odbiór danych

Odbiór danych następuje wtedy, gdy pojawi się zapalona flaga nrf24_rx_flag. Realizowany jest podobnie jak w poprzednim wpisie z tą różnicą, że dane wpisuję do wewnętrznego rejestru kołowego.

void nRF24_ReceiveData(void)
{
	uint8_t i, DataCounter;
	uint8_t RXPacket[32];
	do
	{
		nRF24_ReceivePacket(RXPacket, &DataCounter);

		for(i = 0; i < DataCounter; i++)
		{
			RB_WriteToBuffer(RXBuffer, RXPacket[i]);
		}

	}while(!nRF24_IsRxEmpty());
}

Teraz, aby użyć tych danych wystarczy je “odczytać” z nRF24. Jeśli zwrócona ilość danych równa się zero, to znaczy, że nic nie przyszło. 

nRF24_ReadData(Message, &MessageLength);
if(MessageLength > 0)
{
	HAL_UART_Transmit(&huart2, Message, MessageLength, 1000);
}

Funkcja zwraca tyle, ile jest dostępnych danych w buforze – nie ma ograniczenia związanego z Payload.

nRF24_RX_Status nRF24_ReadData(uint8_t *Data, uint8_t *Size)
{
	uint8_t i = 0;
	*Size = 0;

	  if(nRF24_RXAvailable())
	  {
		while(RB_OK == RB_ReadFromBuffer(RXBuffer, &Data[i]))
		{
			i++;
		}
		*Size = i;
	  }
	if(*Size == 0)
	{
		return NRF24_NO_RECEIVED_PACKET;
	}

	return NRF24_RECEIVED_PACKET;
}

Natomiast sprawdzenie, czy coś jest do odczytania w wersji z buforami kołowymi to sprawdzenie, czy coś jest w buforze.

uint8_t nRF24_RXAvailable(void)
{
	return nRF24_IsSomtehingToRead();
}

Nadawanie danych

Teraz działamy odwrotnie. My piszemy do bufora, a biblioteka sprawdza, czy jest coś do wysłania i to wysyła.

Wpisanie do bufora nRFa odbywa się – można by powiedzieć – “normalnie”.

MessageLength = sprintf(Message, "abcdefghijklmnopqrstuwxyz1234567890\n\r" );
nRF24_SendData(Message, MessageLength);

Wrzucone to zostaje do bufora nadawczego.

nRF24_TX_Status nRF24_SendData(uint8_t* Data, uint8_t Size)
{
	uint8_t i = 0;

	while(Size > 0)
	{
		if(RB_OK == RB_WriteToBuffer(TXBuffer, Data[i++]))
		{
			Size--;
		}
		else
		{
			return NRF24_NO_TRANSMITTED_PACKET;
		}
	}
	return NRF24_TRANSMITTED_PACKET;
}

Następnie, gdy przychodzi event nRF24, biblioteka sprawdza, czy jest coś do nadania. Musi być też zwolnione radio, więc powołałem kolejną flagę – Nrf24TXFreeFlag.

Flaga ta jest kasowana zgodnie z jej nazwą, więc wyzerowana oznacza, że radio nie jest aktualnie wolne. Następnie pobieram liczbę bajtów do wysłania. Jeśli jest większa niż maksymalnie możliwy Payload, ucinam pobór do 32 bajtów. Odczytuję potrzebną ilość danych i wysyłam do układu.

void nRF24_CheckTXAndSend(void)
{
	uint8_t i, DataCounter;
	uint8_t TXPacket[32];

	if(nRF24_IsSomtehingToSend() && Nrf24TXFreeFlag)
	{
		nRF24_TX_Mode();

		Nrf24TXFreeFlag = 0;
		DataCounter = RB_ElementsAvailable(TXBuffer);
		if(DataCounter > 32)
		{
			DataCounter = 32; // Max Payload
		}

		for(i = 0; i < DataCounter; i++)
		{
			RB_ReadFromBuffer(TXBuffer, &TXPacket[i]);
		}

		nRF24_SendPacket(TXPacket, DataCounter);
		NRF24_CE_HIGH;
		nRF24_Delay_ms(1);
		NRF24_CE_LOW;
	}
}

Możesz zauważyć, że jest tu jeden Delay. Tak naprawdę powinien on trwać ok. 130 µs, bo tyle trwa przeskok między stanami nRF24, ale rozdzielczość delaya w HALu to 1 ms. Jest on dosyć drobny, ale można się uprzeć, aby go usunąć. Pomysł na jego usunięcie przyszedł mi dopiero podczas pisania tego tekstu.

Ok posłaliśmy dane do układu i co dalej? Przecież mamy wyzerowaną flagę Nrf24TXFreeFlag, a do wysłania kolejnych danych potrzebujemy ją ustawić.

Tutaj na białym koniu wjeżdża przerwanie TX_DS, które mówi nam o tym, że transmisja po radiu została zakończona. W evencie, gdy pojawi się flaga od TX_DS, ustawiam Nrf24TXFreeFlag co powoduje, że można wysłać kolejne dane. Może to być kolejna część wiadomości, jeśli miała więcej niż 32 bajty.

Działanie i wygląd biblioteki

Aktualny kształt libki jest dosyć skomplikowany i niestety mało czytelny… Wszystko przez to, że uparłem się, aby jeden zestaw plików obsługiwał trzy wypadki:

  1. Polling
  2. Odbiór na przerwaniu, wysyłka na pollingu
  3. Wszystko na przerwaniu

Do konfiguracji całej biblioteki służy kilka define’ów

//
//	Configuration
//
#define NRF24_USE_INTERRUPT		1
#if (NRF24_USE_INTERRUPT == 1)
#define NRF24_USE_RINGBUFFER	1
#endif


#define NRF24_DYNAMIC_PAYLOAD	1

#if (NRF24_USE_RINGBUFFER == 1)
#define NRF24_RX_BUFFER_SIZE 32
#define NRF24_TX_BUFFER_SIZE 32
#endif

Myślę, że łatwo jest się domyślić co one przełączają. W artykule pominąłem w funkcjach te różne opcje dla różnych funkcji dla lepszej czytelności samej części z buforami.

Całość napisałem tak, że we wszystkich trybach używa się dokładnie tych samych funkcji do odbioru i nadawania, więc końcowe użycie biblioteki jest dosyć proste. Całość sprowadza się zawsze do inicjalizacji oraz trzech funkcji podstawowych.

#if (NRF24_USE_INTERRUPT == 1)
//
//	Main event for whole communication
//	Put it in main while(1) loop
//
void nRF24_Event(void);
#endif

//
// TRANSMITTING DATA
//
nRF24_TX_Status nRF24_SendData(uint8_t *Data, uint8_t Size);
nRF24_RX_Status nRF24_ReadData(uint8_t *Data, uint8_t *Size);

Podsumowanie

Biblioteka na ten moment jest według mnie skończona. Jest jeszcze kilka drobnych elementów do poprawy jak ten mały delay przy nadawaniu, ale nie jest to aż tak konieczne. Nadawanie i odbiór testowałem dosyć “stresowo” próbując wysyłać wielokrotność Payload na raz. Zarówno po stronie nadajnika, jak i odbiornika wszystko szło idealnie, więc uważam moją bibliotekę za sukces.

Chętnie poznam Twoją opinię na temat mojego kodu i propozycje ewentualnych poprawek czy usprawnień. Jestem otwarty na dyskusję 🙂

Na ten moment to wszystko na temat nRF24L01+. W przyszłości wrócę jeszcze z badaniem zasięgu, ale muszę zbudować układ pomiarowy. Może zrobimy to wspólnie na LIVE? 🙂

Cała seria:

Jeśli artykuł Ci się spodobał, możesz mnie wesprzeć kupując coś u mnie 🙂 https://sklep.msalamon.pl/

Pełny projekt wraz z biblioteką znajdziesz jak zwykle na moim GitHubie: NADAJNIK, ODBIORNIK

Jeśli zauważyłeś jakiś błąd, nie zgadzasz się z czymś, chciałbyś coś dodać istotnego lub po prostu uważasz, że chciałbyś podyskutować na ten temat, napisz komentarz. Pamiętaj, że dyskusja ma być kulturalna i zgodna z zasadami języka polskiego.

5/5 - (7 votes)

Podobne artykuły

.

8 komentarzy

polak27 · 18/11/2020 o 20:07

Bardzo pomocna seria artykułów. Akurat mierzę się z tym układem i Pańska praca mocno pomogła mi w uruchomieniu całości. Generalnie już przewalczyłem problemy i komunikacja działa. Tylko dorzucę jeden niuans, który odkryłem podczas testów z oscyloskopem.
NRF24_CE_HIGH;
nRF24_Delay_ms(1);
NRF24_CE_LOW;

Ten delay trwa tutaj 1 ms. Jest on zależny od ilości bajtów jaką przesyłamy. Dla 1 bajta możne wynosić 200us, a dla 32 bajtów już około 1ms. Ale to nie wszystko. Jest z nim związany pewien problem. Mianowicie jeśli w momencie startu przesyłania CE_HIGH zaczniemy cokolwiek odczytywać z rejestru statusu to transmisja się zawali (przynajmniej w moim module 🙂 ). Dlatego przez ten czas nie można nic odczytywać. Ja tego delaya wyrzuciłem w następujący sposób: wystawiam CE_HIGH aby wysłać pakiet, ustawiam czekanie na przerwanie, gdy nadejdzie sprawdzam w statusie czy to TX_complete i dopiero wtedy CE_LOW. W ten sposób nic nie blokujemy. Może komuś przyda się coś z tego co napisałem 🙂

SaS · 30/06/2020 o 08:41

Malloc w uC to proszenie się o kłopoty o czym przekonał się już nie jeden Arduinowiec, gdzie malloc jest nadużywany tak samo jak i typ zmiennoprzecinkowy, int gdzie wystarcza typ 8-bit, itp.

    Mateusz Salamon · 30/06/2020 o 09:48

    Oczywiście, że malloc() bywa niebezpieczny! Ciągłe zajmowanie pamięci może nieźle namieszać chociażby poprzez fragmentacje. Zauważ, że ja użyłem go tylko raz i te bufory żyją przez cały program. To chyba nie puści dymu z krzemu, co? 🙂

SaS · 30/06/2020 o 08:37

“Są co najmniej dwa rozwiązania:
Zrobić “mutexa” na zasób SPI
Obsłużyć tak naprawdę przerwanie w mainie
Wybrałem bramkę numer dwa.”
Złe rozwiązanie bo praktycznie niczym nie różni się od odpytywania układu w main tyle, ze zamiast sprawdzać rejestr w układzie NRF sprawdzana jest programowa flaga. Jaki to daje realne zalety w stosunku do obsługi NRF bez użycia przerwań? Żadnych! Zakładając, że main z jakiś powodów wykonuje się długo dane odbiorcze mogą zostać zgubione.
Aby w pełni wykorzystać potencjał przerwań, SPI w przerwaniach, o wyższym priorytecie niż pozostałe przerwania związane z SPI, musi obsługiwać kolejkę dostępu do interfejsu.
Aby nie komplikować sobie życia można skorzystać z RTOS o ile systick jest na tyle krótki, ze nie da się przepełnić bufora odbiorczego NRF.

    Mateusz Salamon · 30/06/2020 o 09:46

    W małym programie, gdzie nie ma czasochłonnych operacji w mainie jak np. obsługa TFT to nie ma większej różnicy. Unikam niepotrzebnej ciągłej transmisji po SPI, która zajmuje więcej czasu niż sprawdzenie komórki w RAMie. Sam nRF24 ma też swój FIFO, który na chwilę wystarczy. Gdybyś mi podał dobry przykład obsługi takiej kolejki dostępu do SPI bez RTOS to podejdę jeszcze raz do tematu 🙂
    RTOS jest też rozwiązaniem, ale czy trzeba zawsze do niego sięgać? Zauważyłem, że wiele osób podaje go jako ten złoty środek, a w nim czai się mnóstwo innych niebezpieczeństw.

      Przemek · 04/07/2020 o 17:54

      dokładnie – wytłumaczenie z tym, że odczyt z RAMu jest szybszy niż przesłanie komunikatu po SPI jest już wystarczające. Zewnętrzne przerwanie może też wybudzać procka, nie marnujemy energii na odpytywania w “lowPowerowych” rozwiązaniach (oczywiście NRF swoje i tak wtedy pobierze).

      Co do czasu wykonywania – to zawsze trzeba uwzględniać i pisać tak, żeby algorytmy wykonywały się “małymi kawałkami” w takich przypadkach nieblokująco/profilować sobie czasy obliczeń, żeby je poznać, lub spisać ładną decyzję projektową, co jest ważniejsze itp.

      Tutaj faktycznie RTOS ułatwia sprawę, ale zgadzam się z Mateuszem, że nie zawsze trzeba po niego sięgać, zawsze trzeba to przekalkulować, czy się opłaca, czy nie (aczkolwiek sam aktualnie go wpycham wszędzie 😛 ale to wynika z rozbudowania projektów).

      Sam widziałem, jak RTOSem została przykryta “nieudolność” programisty w projekcie i nie stało to nawet obok “poprawnego wykorzystania RTOSa” – ot zamiast jednego superloopa ze starego podejścia, zrobiły się 3 superloopy i to na tym samym priorytecie, więc dupę ratował algorytm round robin, który cyklicznie zajmował się przełączeniem kontekstów, a aplikacja mogła być o wiele lepiej porozdzielana na kilkanaście dedykowanych tasków i to wcale by nie było trudne – ot trochę pomyślunku przed pisaniem. RTOS nie jest złotym środkiem na zły kod 🙂

    Mateusz Salamon · 30/06/2020 o 10:06

    Wpadłem teraz na taki pomysł, aby “mutexować” tego SPI.
    HAL daje zajmuje peryferium poprzez HAL_BUSY. Można sprawdzać w przerwaniu czy jest zajęte (poprzez operacje w main0. Jeśli SPI zwolniony – normalnie odczytać, a jeśli SPI zajęty to zamiast czekać na nigdy nienadchodzące zwolnienie – ustawić flagę. Wtedy w mainie na podstawie tej flagi jeszcze raz wywołać obsługę przerwania. To mogłoby wyeliminować przypadek w którym SPI jest zagarnięte przez main i wejdzie przerwanie na ten sam SPI 🙂

    Będę wdzięczy za inne pomysły. Warto wymieniać się wiedzą 🙂

Dodaj komentarz

Avatar placeholder

Twój adres e-mail nie zostanie opublikowany. Wymagane pola są oznaczone *