STM32 - NRF24L01





Здравствуйте.

Это описание библиотеки для работы с радио-модулями NRF24L01 с помощью микроконтроллера stm32. За основу взята ардуиновская билиотека RF24. Подробно про сами модули я писал тут.

Не смотря на то, что в статье рассказано про работу с stm32, описание функций в равной степени справедливо и для ардуино.



Итак, модуль взаимодействует с микроконтроллером по шине SPI используя программный пин CS (Chip Select), а так же задействованы ещё два пина — CE (Chip Enable) и IRQ.


Настроим их в Кубе…



F303



Включаем SPI в режиме Full-Duplex Master.

Data Size — 8 Bits.

Baud Rate — в даташите на модуль написано что максимальная скорость не должна превышать 10Mbps, а минимальная должна быть по крайней мере в два раза выше значения setDataRate() (см. ниже), в общем установите что-то близкое к тому что на картинке.

CSN — Chip Select.

CE — Chip Enable.

Оба этих пина настройте так…




IRQ — модуль умеет генерировать внешний сигнал (на своём пине irq) в момент окончания отправки, приёма, либо возникновения ошибки, а мы можем ловить этот сигнал если подключим его к stm32 и настроим вход на внешнее прерывание…


Модуль прижимает ножку к «земле», поэтому указываем Falling.

Особой надобности в этом пине нет, можете вообще его не активировать в Кубе, а у модуля оставить висеть «в воздухе».


Так же включите USART1 для вывода инфы, и какой-нибудь пин для мигалки (у меня PB11 — ledpb11).


Распиновка модуля…


С подключением всё просто — ножка к ножке.



NRF24L01 может:

Передавать не больше 32-х байт в одной посылке. То, что вы посылаете называется полезная нагрузка.

Передавать полезную нагрузку и запрашивать ответ подтверждения. То есть, передатчик посылает пакет данных приёмнику с требованием ответить при успешном получении, а приёмник получив пакет отправляет передатчику ответ (ACK-байт) — типа «всё окей, данные получил».

Если передатчик не получает подтверждения, то он повторяет посылку. Количество повторных посылок и интервал между ними можно настраивать. Все эти операции с отправкой/получением подтверждения и повторами, модуль делает сам, без нашего участия (что конечно же очень клёво), а от нас требуется только включить или отключить этот функционал.

Помимо простого подтверждения, приёмник может добавлять к ответу ещё какие-то данные, то есть добавлять к ACK-байту полезную нагрузку. Например, у нас за окном весит термометр (приёмник), а мы хотим время от времени забирать данные с него. Тогда можно просто отправлять этому термометру что угодно, например один байт, с требованием прислать подтверждение, а он будет посылать вместе с подтверждением ещё и температуру. Очень удобный функционал. Если у вас с десяток или больше таких датчиков, то можно вместо бессмысленного байта посылать идентификатор этого датчика. Таким образом можно обращаться к любому количеству приёмников-термометров.

Как уже было сказано выше, передатчик может послать не больше 32-х байт в одном пакете. Так вот, модуль можно настроить либо на фиксированный размер пакета, либо на динамический. Например вы знаете что будете обмениваться пакетами по четыре байта, тогда настраиваете модули (и приёмник и передатчик) на этот размер. Если обмен будет происходить пакетами разной длины, тогда настраиваете модули на динамический размер пакетов.

У модуля можно настраивать частотный канал, от 0 до 125. Каналы соответствуют несущей частоте, от 2.400 ГГц до 2.525 ГГц соответственно. То есть если указать нулевой канал, то модуль будет работать на частоте 2.400 ГГц, если указать канал 1, то частота будет 2.401 ГГц, и т.д. Если у вашего соседа используются такие же модули, то можете договорится с ним и указать разные каналы чтоб частоты не пересекались.

Ещё у NRF24L01 есть так называемые трубы, с помощью которых можно разделять передатчики по идентификаторам…



Каждой трубе присваивается свой числовой идентификатор, по которому приёмник определяет какой передатчик прислал данные. Частотный канал у приёмника и всех передатчиков одинаковый.

Допустим у вас есть один приёмник и несколько передатчиков, которые шлют данные приёмнику. Чтобы понять от кого пришли данные можно просто посылать приёмнику пакет данных и включать в него какой-то байт с номером передатчика, а на приёмнике разбирать эти данные «вручную» и определять от кого они пришли. Как писалось выше, этот метод подойдёт на тот случай если передатчиков очень много, собственно здесь другого варианта и нет. Однако если у вас не больше шести передатчиков, то можно каждому из них назначить одну из труб (см. картинку), а приёмник прослушивая эти трубы будет сам определять кто прислал данные.

Если у вас передатчиков меньше шести, тогда оставляйте трубу №0 не задействованной. Тут дело вот в чём: труба №0 особенная, она используется приёмником для отправки подтверждений любому из передатчиков. То есть, допустим приёмник получил пакет по трубе №3, тогда он переключает трубу №0 на передачу, присваивает ей идентификатор трубы №3 и отправляет подтверждение. После этого труба №0 переключается обратно на прослушивание с соответствующим идентификатором. Как вы понимаете, пока труба №0 переключена, она ничего принять не может, а значит можно пропустить пакет от передатчика работающего с этой трубой.

Короче говоря, если есть возможность, то не используйте трубу №0.

Обратите внимание, у труб с 1 по 5 идентификаторы должны отличатся только последними двумя цифрами.



Далее перейдём к кодингу и рассмотрим функции подробнее.

Скачайте два примера — приёмник и передачик. Если у вас плата BluePill, то можно прямо их использовать, если другая, то подключите к своему проекту файлы nrf24l01.c и RF24.h (они одинаковые и для приёмника и для передатчика), а в main.c добавьте инклюд…

/* USER CODE BEGIN Includes */
#include "RF24.h"
/* USER CODE END Includes */


Если у вас нет двух stm'ок, то можно использовать ардуину в качестве приёмника или передатчика. Разумеется с ардуиновским кодом.



Функции настройки


Устанавливается на приёмнике и передатчике:

isChipConnected(); — проверяет подключён ли модуль к SPI.

NRF_Init(); — инициализация модуля (это моя функция).

setAutoAck(true); — включить/отключить (true/false) автоподтверждение для всех труб. Включено по умолчанию.

setAutoAckPipe(1, true); — включить/отключить автоподтверждение для конкретной трубы.

enableAckPayload(); — разрешить добавлять полезную нагрузку в автоподтверждении. Автоматически включает динамический размер полезной нагрузки для труб №0 и №1, для остальных труб нужно запускать — enableDynamicPayloads().

enableDynamicPayloads(); — включить динамический размер полезной нагрузки.

disableDynamicPayloads(); — oтключает динамическую полезную нагрузку во всей системе. Также отключает полезную нагрузку в автоподтверждении — enableAckPayload().

disableCRC(); — отключить CRC, перед этим запустить setAutoAck(false) и disableDynamicPayloads().

setRetries(2, 3); — устанавливает кол-во повторных попыток при неудачной отправке, и задержку между ними (в примере 2 попытки с задержкой 1000мкс). Максимальное кол-во попыток 15. Задержка указывается кратно 250мкс — 0 = 250мкс, 1 = 500мкс, 2 = 750мкс, 3 = 1000мкс и т.д., максимум 15 = 4000мкс. По умолчанию в NRF_Init() устанавливается setRetries(5, 15).

setDataRate(RF24_1MBPS); — скорость передачи данных. По умолчанию в NRF_Init() устанавливается 1MBPS, можно установить RF24_250KBPS или RF24_2MBPS.

setPALevel(RF24_PA_MAX); — мощность передатчика. RF24_PA_MIN = -18dBm, RF24_PA_LOW = -12dBm, RF24_PA_HIGH = -6dBM, RF24_PA_MAX = 0dBm. По умолчанию в NRF_Init() устанавливается setPALevel(RF24_PA_MAX) — лупит во всю силу. Если передатчики близко к друг другу, то можно уменьшить мощность. Если работает от батарейки, то опять же имеет смысл уменьшить, с целью энергосбережения. Если используется в коммерческих целях, то надо почитать про разрешённую мощность.

setCRCLength(RF24_CRC_8); — размер CRC, по умолчанию в NRF_Init() устанавливается RF24_CRC_16 (16 бит).

setChannel(19); — частотный канал. По умолчанию в NRF_Init() устанавливается 76. Максимальное значение — 125. Должен быть одинаковый на приёмнике и на передатчике.

setPayloadSize(6); — размер полезной нагрузки в байтах при отключённой динамической полезной нагрузке. Количество должно быть одинаковое на приёмнике и на передатчике.

powerDown(); — переводит модуль в режим низкого энергопотребления.

powerUp(); — возвращает к жизни.

setAddressWidth(5); — установить длину идентификатора трубы. 3 — 24 бита, 4 — 32 бита, 5 — 40 бит. По умолчанию в NRF_Init() прописано 5. Если измените, то сами идентификаторы подкорректируйте.


Только для передатчика:

openWritingPipe(pipe1); — открытие трубы на передатчике. В скобках идентификатор, должен совпадать с какой либо из открытых труб на приёмнике.


Только для приёмника:

openReadingPipe(1, pipe1); — открыть трубу №1 на приёмнике. В скобках номер трубы и индетификатор. Если нужно открыть несколько труб, то просто добавить функции с другим номером и идентификатором.

startListening(); — включить прослушивание труб на приёмнике.



Теперь пояснения к коду приёмника

Запускаем счётчик микросекунд, используется вместо ардуиновской delayMicroseconds():

/* USER CODE BEGIN 2 */
  DWT_Init(); // счётчик для микросекундных пауз


Объявляем идентификаторы труб:

//const uint64_t pipe0 = 0x787878787878;
const uint64_t pipe1 = 0xE8E8F0F0E2LL; // адрес первой трубы
//const uint64_t pipe2 = 0xE8E8F0F0A2LL;
//const uint64_t pipe3 = 0xE8E8F0F0D1LL;
//const uint64_t pipe4 = 0xE8E8F0F0C3LL;
//const uint64_t pipe5 = 0xE8E8F0F0E7LL;

Для приёмника объявляем по количеству передатчиков (если несколько передатчиков висят на разных трубах).

Автоподтверждение включается автоматически, если его не отключить принудительно.

Разрешаем добавлять полезную нагрузку в автоподтверждении:

enableAckPayload();

При этом автоматически включится динамический размер полезной нагрузки — enableDynamicPayloads().

Указываем частотный канал:

setChannel(19);


Открываем трубу №1 для приёма:

openReadingPipe(1, pipe1);

Нулевую трубу не трогаем.

Если нужно слушать две трубы, то пишем так:

openReadingPipe(1, pipe1);
openReadingPipe(2, pipe2);
и т.д.

Раскомментируйте идентификатор второй трубы, и не забудьте что enableDynamicPayloads() включается автоматически только для труб 0 и 1, для остальных это надо делать вручную.

Запускаем прослушивание труб (не важно сколько труб открыто):

startListening();

Только для приёмника.

Далее идёт проверка соединения с модулем, инициализация и чтение параметров из модуля, которые мы настроили…




В бесконечном цикле делаем так…

/* USER CODE BEGIN WHILE */
  while (1)
  {
	///////////////////////////////////// ПРИЁМ /////////////////////////////////////////////
	uint8_t nrf_data[32] = {0,}; // буфер указываем максимального размера
	static uint8_t remsg = 0;
	uint8_t pipe_num = 0;

	if(available(&pipe_num)) // проверяем пришло ли что-то
	{
		remsg++;

		writeAckPayload(pipe_num, &remsg, sizeof(remsg)); // отправляем полезную нагрузку вместе с подтверждением

		if(pipe_num == 0) // проверяем в какую трубу пришли данные
		{
			HAL_UART_Transmit(&huart1, (uint8_t*)"pipe 0\n", strlen("pipe 0\n"), 1000);
		}

		else if(pipe_num == 1)
		{
			HAL_UART_Transmit(&huart1, (uint8_t*)"pipe 1\n", strlen("pipe 1\n"), 1000);

			uint8_t count = getDynamicPayloadSize(); // смотрим сколько байт прилетело

			read(&nrf_data, count); // Читаем данные в массив nrf_data и указываем сколько байт читать

			if(nrf_data[0] == 77 && nrf_data[1] == 86 && nrf_data[2] == 97) // проверяем правильность данных
			{
				HAL_GPIO_TogglePin(ledpb11_GPIO_Port, ledpb11_Pin);
				snprintf(str, 64, "data[0]=%d data[1]=%d data[2]=%d\n", nrf_data[0], nrf_data[1], nrf_data[2]);
				HAL_UART_Transmit(&huart1, (uint8_t*)str, strlen(str), 1000);
			}
		}

		else if(pipe_num == 2)
		{
			HAL_UART_Transmit(&huart1, (uint8_t*)"pipe 2\n", strlen("pipe 2\n"), 1000);
		}

		else
		{
			while(availableMy()) // если данные придут от неуказанной трубы, то попадут сюда
			{
				read(&nrf_data, sizeof(nrf_data));
				HAL_UART_Transmit(&huart1, (uint8_t*)"Unknown pipe\n", strlen("Unknown pipe\n"), 1000);
			}
		}
	}




У меня тут для примера сделаны условия — pipe_num == 0 и pipe_num == 2, но они нужны только если вы прослушиваете эти трубы, и если прослушиваете, тогда внутри этих условий обязательно должна присутствовать функция чтения, чтоб забрать то что пришло и очистить регистры.




Передатчик

Всё то же самое что и на приёмнике за исключением того, что нам нужен только один идентификатор трубы:

const uint64_t pipe1 = 0xE8E8F0F0E2LL;

Должен совпадать с одной из открытых труб на приёмнике.

А трубу открываем для передачи:

openWritingPipe(pipe1);


Прослушивание здесь не нужно.

В бесконечном цикле будем отправлять три байта, а вместе с подтверждением получать один байт, который инкрементируется на приёмнике. То есть в ответ будем получать постоянно увеличивающееся число…

/* USER CODE BEGIN WHILE */
  while (1)
  {
	  ///////////////////////////////////// ПЕРЕДАЧА /////////////////////////////////////////////
	  uint8_t nrf_data[32] = {0,}; // буфер
	  nrf_data[0] = 77;
	  nrf_data[1] = 86;
	  nrf_data[2] = 97;

	  uint8_t remsg = 0; // переменная для приёма байта пришедшего вместе с ответом

	  if(write(&nrf_data, strlen((const char*)nrf_data))) // отправляем данные
	  {
		if(isAckPayloadAvailable()) // проверяем пришло ли что-то вместе с ответом
		{
			read(&remsg, sizeof(remsg));
			HAL_GPIO_TogglePin(ledpb11_GPIO_Port, ledpb11_Pin);
			snprintf(str, 64, "Ack: %d\n", remsg);
			HAL_UART_Transmit(&huart1, (uint8_t*)str, strlen(str), 1000);
		}
	  }
	  else HAL_UART_Transmit(&huart1, (uint8_t*)"Not write\n", strlen("Not write\n"), 1000);

	  HAL_Delay(100);
          ...




Если отключить модуль приёмника, то будет выводиться сообщение «Not write», намекающее что что-то пошло не так



Сейчас модули работают в самом так сказать удобном режиме, можно передавать данные переменной длины, успешная посылка подтверждается ответом, и в ответе можно тоже передавать какие-то данные (в примере передаётся один байт, но можно и больше).


Если для приёмника и передатчика отключить полезную нагрузку в автоподтверждении…

//enableAckPayload();
setChannel(19);
...


Тогда подтверждение будет приходить, но без полезной нагрузки. Приход подтверждения никак не будет проявляться внешне, то есть всё происходит внутри модулей.

Для приёмника можно убрать эту часть кода…

/* USER CODE BEGIN WHILE */
  while (1)
  {
	///////////////////////////////////// ПРИЁМ /////////////////////////////////////////////
	uint8_t nrf_data[32] = {0,}; // буфер указываем максимального размера
	//static uint8_t remsg = 0;
	uint8_t pipe_num = 0;

	if(available(&pipe_num)) // проверяем пришло ли что-то
	{
		//remsg++;

		//writeAckPayload(pipe_num, &remsg, sizeof(remsg)); // отправляем полезную нагрузку вместе с подтверждением
        ...


А для передатчика эту…


          //uint8_t remsg = 0; // переменная для приёма байта пришедшего вместе с ответом

	  if(write(&nrf_data, strlen((const char*)nrf_data))) // отправляем данные
	  {
		/*if(isAckPayloadAvailable()) // проверяем пришло ли что-то вместе с ответом
		{
			read(&remsg, sizeof(remsg));
			HAL_GPIO_TogglePin(ledpb11_GPIO_Port, ledpb11_Pin);
			snprintf(str, 64, "Ack: %d\n", remsg);
			HAL_UART_Transmit(&huart1, (uint8_t*)str, strlen(str), 1000);
		}*/
	  }





Чтобы вовсе отключить подтверждение, нужно на обоих устройствах сделать так…

//enableAckPayload();
setAutoAck(false);
setChannel(19);
...



Если передаются данные фиксированного размера, тогда нужно добавить такую строчку…

//enableAckPayload();
setAutoAck(false);
setPayloadSize(3); // 3 байта
setChannel(19);
...


Размер буфера тоже сделать 3 байта…

uint8_t nrf_data[3] = {0,};


На приёмнике убрать проверку количества принятых байт, и в функции чтения указать размер буфера…


                else if(pipe_num == 1)
		{
			HAL_UART_Transmit(&huart1, (uint8_t*)"pipe 1\n", strlen("pipe 1\n"), 1000);

			//uint8_t count = getDynamicPayloadSize(); // смотрим сколько байт прилетело

			read(&nrf_data, sizeof(nrf_data)); // Читаем данные в массив nrf_data и указываем размер буфера

			if(nrf_data[0] == 77 && nrf_data[1] == 86 && nrf_data[2] == 97) // проверяем правильность данных
			{
				HAL_GPIO_TogglePin(ledpb11_GPIO_Port, ledpb11_Pin);
				snprintf(str, 64, "data[0]=%d data[1]=%d data[2]=%d\n", nrf_data[0], nrf_data[1], nrf_data[2]);
				HAL_UART_Transmit(&huart1, (uint8_t*)str, strlen(str), 1000);
			}




Приёмник у нас может прослушивать несколько труб одновременно, а вот если понадобится чтоб передатчик слал данные на разные трубы, то можно переключать исходящую трубу прямо «налету», добавив команду перед функцией записи…


          openWritingPipe(pipe1);
	  if(write(&nrf_data, strlen((const char*)nrf_data))) // отправляем данные
	  {
		if(isAckPayloadAvailable()) // проверяем пришло ли что-то вместе с ответом
		{
                   ...


          openWritingPipe(pipe2);
	  if(write(&nrf_data, strlen((const char*)nrf_data))) // отправляем данные
	  {
		if(isAckPayloadAvailable()) // проверяем пришло ли что-то вместе с ответом
		{
                   ...

Нужно объявить ещё один идентификатор.



Если нужно чтоб приёмник мог ещё и посылать данные, например работал как ретранслятор, то его можно переключать туда-сюда.

Добавляем в начале программы трубу для передачи:

...
openReadingPipe(1, pipe1);
startListening();
openWritingPipe(pipe3); // труба для передачи

Для передачи указываем какой-то другой идентификатор трубы, главное чтоб он совпадал с приёмником в который ретранслируются данные.

Когда приёмник принял что-то, отключаем прослушивание:

stopListening();


Отправляем данные как в примере передатчика.

И переключаемся обратно в режим приёмника:

startListening();


Если для прослушивания используется труба №0, тогда так:

openReadingPipe(0, pipe0);
startListening();

Повторюсь — только если используется труба №0, если любые другие, то достаточно одного startListening().



Сейчас длина идентификатора труб 40 бит, но её можно уменьшить до 24-х или 32-х бит. Чтобы сделать 32 бита нужно до открытия трубы добавить команду…

setAddressWidth(4);


Тогда идентификаторы будут короче и места займут меньше…

//const uint32_t pipe0 = 0x7FFFFFF8;
const uint32_t pipe1 = 0x7FFFFFF9; // адрес первой трубы
//const uint32_t pipe2 = 0x7FFFFFFA;
//const uint32_t pipe3 = 0x7FFFFFFB;
//const uint32_t pipe4 = 0x7FFFFFFC;
//const uint32_t pipe5 = 0x7FFFFFFD;



В процессе экспериментов с настройками могут возникнуть непонятки, если это случилось, то прежде чем паниковать попробуйте обесточить модуль. Это нужно чтоб все регистры сбросились, ибо не всякая очередная настройка отменяет предыдущую. Я запитывал модули через транзистор, который отключал питание при перезагрузке МК.

И таки да, не с какими «глюками» я не сталкивался, все опробованные мной режимы работают без нареканий, гонял модули «и в хвост, и в гриву». Главное понимать что и как вы настраиваете.




Если вы настроили и подключили пин IRQ, то все три прерывания будут приходить сюда…

void HAL_GPIO_EXTI_Callback(uint16_t GPIO_Pin)
{
	if(GPIO_Pin == IRQ_Pin)
	{
	    HAL_UART_Transmit(&huart1, (uint8_t*)"IRQ\n", strlen("IRQ\n"), 1000);
	}
}


Есть функция которая может маскировать любые из трёх прерываний, то есть внутри модуля прерывания будут происходить, но не будут выводится на пин.

maskIRQ(true, true, true); — аргумент true отключает соответствующий сигнал — bool tx, bool fail, bool rx. По умолчанию все сигналы включены.


На этом всё.


Всем спасибо


Приёмник

Передачик

Мануал на ардуиновскую библиотеку

Даташит


Форум

Телеграм-чат STM32


  • 0
  • 2463
Поблагодарить автора




Telegram-чат istarik

Задать вопрос по статье
Telegram-канал istarik

Известит Вас о новых публикациях






Комментарии (0)

Только зарегистрированные и авторизованные пользователи могут оставлять комментарии.