Продолжаем работу с беспроводным приёмо-передатчиком NRF24L01. И сегодня та плата, которая у нас выступала в роли приёмника, будет выступать уже в роли передатчика. И задачу мы уже будем решать обратную. Передать с модуля, подключенного к данной плате пакеты, наоборот, на несколько приемников. Приемников у нас будет три и наша задача будет состоять в том, чтобы с передатчика мы могли в любой момент передать пакет на любой из этих трех приёмников. Данная задача оказалась выполнимой, поковыряться, конечно пришлось, но цель была достигнута, поэтому я незамедлительно решил поделиться с вами своими наработками.
Проект для нашего передатчика на плате Nucleo F401RE был создан из проекта приёмника урока 113 NRF24_RX_LCD и назван был, соответственно, NRF24_TX_LCD.
Запустим наш проект в Cube MX и включим там какой-нибудь таймер, думаю, он нам сегодня пригодится
Перейдём в configuration и настроим там период нашего таймер приблизительно на 1 милисекунду
Также настроим прерывания от таймера
Теперь зайдём в настройки NVIC и уменьшим приоритет прерываний от таймера. Самый высокий приоритет у нас должен быть у прерываний от ножки модуля
Сохраним настройки, сгенерируем проект для Keil, запустим его, настроим программатор на автоперезагрузку, а также включим уровень оптимизации в 1. Также подключим в дерево проекта файлы NRF24.c и lcd.c, а затем попробуем собрать наш проект.
Если всё нормально, то двигаемся дальше.
Уберем в функции main() из бесконечного цикла вызов функции приёма пакета
/* USER CODE BEGIN 3 */
NRF24L01_Receive();
Не забываем запустить наш таймер
NRF24_ini();
HAL_TIM_Base_Start_IT(&htim1);
Переходим в файл NRF24.c и в самом низу добавим функцию-обработчик для событий таймера
//--------------------------------------------------
void TIM1_Callback(void)
{
}
//--------------------------------------------------
Создадим для данной функции прототип, вернёмся в файл main.c и создадим глобальную переменную для подсчёта тиков нашего таймера
uint8_t buf1[20]={0};
uint32_t TIM1_Count=0;
Создадим и в данном файле также обработчик прерывания от таймера после функции-обработчика внешних прерываний. В данной функции мы будем подсчитывать тики нашего таймера и вызывать обработчик из библиотеки
//-------------------------------------------------------------
void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim)
{
if(htim==&htim1)
{
TIM1_Callback();
TIM1_Count++;
if(TIM1_Count>3000000) TIM1_Count=0;
}
}
//-------------------------------------------------------------
Ограничение до 3000000 было вызвано тем, что мы будем отслеживать разные временные периоды, число было подобрано такое, чтобы на наши периоды оно делилось без остатка.
Возвращаемся теперь в наш библиотечный файл NRF24.c. Остальной код теперь будет только там.
Добавим флаг для передачи
volatile uint8_t rx_flag = 0, tx_flag = 0;
Убавим размер буфера, так как передавать количество непереданных пакетов мы теперь не будет. Мы с этим наигрались, убедились, что у нас нормально всё передаётся
#define TX_PLOAD_WIDTH 5
Так как мы будем передавать пакеты на разные приёмники, то адрес передачи будет у нас в определённом месте меняться, также нам придётся менять адреса в приёмных каналах, так как в pipe0 всегда должен находиться адрес передачи. Поэтому добавим ещё некоторые макросы адресов
#define TX_PLOAD_WIDTH 5
#define TX_ADDRESS0_MSB 0xb5
#define TX_ADDRESS1_MSB 0xb7
#define TX_ADDRESS2 0xb6
uint8_t TX_ADDRESS0[TX_ADR_WIDTH] = {0xb5,0xb5,0xa1};
uint8_t TX_ADDRESS1[TX_ADR_WIDTH] = {0xb7,0xb5,0xa1};
uint8_t TX_ADDRESS2_FULL[TX_ADR_WIDTH] = {0xb6,0xb5,0xa1};
Мы также изменили адрес нулевого передатчика, так как он теперь не всегда будет нулевым и ему теперь придётся играть под общую дудку, то есть у него также все байты кроме самого младшего быть должны такими же, как и у других адресов.
А адреса мы менять будем в функции, которая включает режим передатчика. Думаю, здесь для этого самое подходящее место. Поэтому мы в данную функцию добавим ещё один аргумент — номер приёмника, на который мы собираемся передавать пакеты. После этого функция приобретёт следующий вид
void NRF24L01_TX_Mode(uint8_t TX_num, uint8_t *pBuf)
{
switch(TX_num)
{
case 1:
NRF24_Write_Buf(TX_ADDR, TX_ADDRESS0, TX_ADR_WIDTH);
NRF24_Write_Buf(RX_ADDR_P0, TX_ADDRESS0, TX_ADR_WIDTH);
NRF24_Write_Buf(RX_ADDR_P1, TX_ADDRESS1, TX_ADR_WIDTH);
NRF24_WriteReg(RX_ADDR_P2, TX_ADDRESS2);
break;
case 2:
NRF24_Write_Buf(TX_ADDR, TX_ADDRESS1, TX_ADR_WIDTH);
NRF24_Write_Buf(RX_ADDR_P0, TX_ADDRESS1, TX_ADR_WIDTH);
NRF24_Write_Buf(RX_ADDR_P1, TX_ADDRESS0, TX_ADR_WIDTH);
NRF24_WriteReg(RX_ADDR_P2, TX_ADDRESS2);
break;
case 3:
NRF24_Write_Buf(TX_ADDR, TX_ADDRESS2_FULL, TX_ADR_WIDTH);
NRF24_Write_Buf(RX_ADDR_P0, TX_ADDRESS2_FULL, TX_ADR_WIDTH);
NRF24_Write_Buf(RX_ADDR_P1, TX_ADDRESS0, TX_ADR_WIDTH);
NRF24_WriteReg(RX_ADDR_P2, TX_ADDRESS1_MSB);
break;
default:
break;
}
CE_RESET;
// Flush buffers
NRF24_FlushRX();
NRF24_FlushTX();
}
Думаю, здесь ничего сложного нет. Мы просто меняем местами адреса в зависимости от того, на какой именно адрес мы собираемся передавать пакеты.
Как мы помним, мы из функции отправки пакета удалили вообще весь код. Теперь мы его добавим заново, только уже с учётом того, что на ножке прерываний изменения уровня мы уже будем отслеживать аппаратно. Также в нашей функции добавится ещё один входной аргумент — номер приёмника, на который будет отправляться пакет
uint8_t NRF24L01_Send(uint8_t TX_num, uint8_t *pBuf)
{
uint8_t regval=0x00;
NRF24L01_TX_Mode(TX_num, pBuf);
regval = NRF24_ReadReg(CONFIG);
//если модуль ушел в спящий режим, то разбудим его, включив бит PWR_UP и выключив PRIM_RX
regval |= (1<<PWR_UP);
regval &= ~(1<<PRIM_RX);
NRF24_WriteReg(CONFIG,regval);
DelayMicro(150); //Задержка минимум 130 мкс
//Отправим данные в воздух
NRF24_Transmit(WR_TX_PLOAD, pBuf, TX_PLOAD_WIDTH);
CE_SET;
DelayMicro(15); //minimum 10us high pulse (Page 21)
CE_RESET;
return 0;
}
В прототипе также добавим новый аргумент.
Весь практически код у нас остался, только до тех пор, пока мы ждали, когда провалимся в условия обнуления уровня на ножке прерываний, теперь у нас это будет обрабатываться в специальной функции-обработчике.
Также в некоторых местах, где мы обращались к адресам более 5-го подправим код, чтобы не было при сборке даже предупреждений.
Сначала подправим это в функции NRF24L01_Receive, а также уберём лишний аргумент в вызове функции sprintf
LCD_SetPos(0, *(RX_BUF+5));
sprintf(str1,"%5u %5u ", *(int16_t*)RX_BUF, *(int16_t*)(RX_BUF+2));
Теперь в функции IRQ_Callback
*(RX_BUF+5) = pipe;
После функции NRF24L01_Receive добавим функцию вывода на дисплей номера отправляемого пакета в соответствующую строку. Причём, отправлять на дисплей данные мы будем только при включенном флаге
//------------------------------------------------
void NRF24L01_LCD(uint16_t num, uint16_t pos)
{
if(tx_flag==1)
{
LCD_SetPos(0, pos);
sprintf(str1,"%5u ", num);
LCD_String(str1);
tx_flag = 0;
}
}
//------------------------------------------------
Теперь идём в функцию обработки прерываний от ножки прерываний IRQ_Callback. Первым делом удалим оттуда невостребованную переменную
uint16_t dt=0;
Уберём мигание светодиодом, мы будем им мигать теперь только при возникновении определённых прерываний
LED_TGL;
Мигнём светодиодом в случае возникновения прерывания в случае приёма пакета
if(status & 0x40)
{
LED_TGL;
Добавим в условия прерываний передачи и ошибки пакетов
rx_flag = 1;
}
if(status&TX_DS) //tx_ds == 0x20
{
LED_TGL;
NRF24_WriteReg(STATUS, 0x20);
NRF24L01_RX_Mode();
tx_flag = 1;
}
else if(status&MAX_RT)
{
NRF24_WriteReg(STATUS, 0x10);
NRF24_FlushTX();
}
}
Подключим глобальную переменную подсчёта тиков таймера, также буфер для подготовки отправляемого пакета, а также добавим три раздельных счётчика для наших трёх приёмников в виде глобальных переменных
uint8_t ErrCnt_Fl = 0;
extern uint32_t TIM1_Count;
extern uint8_t buf1[20];
uint16_t cnt1=0, cnt2=0, cnt3=0;
Ну и осталось нам лишь разослать пакеты по нашим приёмникам в функции-обработчике от таймера
void TIM1_Callback(void)
{
if(TIM1_Count%1000==0)
{
cnt1++;
NRF24L01_LCD(cnt1, 0);
memcpy(buf1,(uint8_t*)&cnt1,2);
NRF24L01_Send(1, buf1);
HAL_Delay(1);
}
if(TIM1_Count%600==0)
{
cnt2++;
NRF24L01_LCD(cnt2, 1);
memcpy(buf1,(uint8_t*)&cnt2,2);
NRF24L01_Send(2, buf1);
HAL_Delay(1);
}
if(TIM1_Count%1500==0)
{
cnt3++;
NRF24L01_LCD(cnt3, 2);
memcpy(buf1,(uint8_t*)&cnt3,2);
NRF24L01_Send(3, buf1);
HAL_Delay(1);
}
}
Здесь мы определяем, на какой именно нам приёмник пора отправлять пакет. Определяем мы это за счёт достижения определённого количества тиков таймера. На первый приёмник мы отправляем пакет приблизительно раз в одну секунду, на второй — раз в 600 милисекунд, на третий — раз в полторы секунды. Задержка в условиях на 1 милисекунду нужна для случая совпадения условий, так как без задержки мы одновременно на несколько передатчиков пакеты не передадим. Я пробовал. Одна милисекунда была выбрана примерно, возможно, достаточно будет и меньшей задержки, так что можете поиграть с задержками в микросекундах, создав прототип на функцию задержки в файле lcd.h.
Соберём проект, прошьём контроллер. Результат мы, скорей всего, никакой не увидим, так как у нас ещё нет ни одного приёмника. Поэтому отключим наш передатчик от ПК и подключим его к независимому источнику питания.
В качестве первого приёмника мы будем использовать схему первого передатчика на контроллере F103, расположенном на недорогой плате. Только ко всему прочему мы к нему подключим для наглядности 8-разрядный индикатор на микросхеме MAX7219.
Выглядеть наш первый приёмник будет следующим образом
Проект для него мы сделаем из проекта передатчика урока 113 NRF24_TX_PIPE0 и назовём его NRF24_RX_00.
Запустим наш проект в проектогенераторе Cube MX и включим SPI2 для 8-разрядного индикатора
Также включим ножку PB12 для ножки CS
Зайдём в Configuration и настроим наш SPI2
Сохраним настройки, сгерерируем проект для Keil, откроем его в нём, настроим программатор на авторезет, уровень оптимизации установим в 1.
Файлы max7219.c и max7219.h скопируем в наш проект из проекта урока 107 по датчику температуры DHT22 с одноимённым названием.
Вернёмся в проект и подключим файлы NRF24.c и max7219.c к дереву проекта.
Попробуем собрать наш проект.
Если всё нормально, подключим библиотеку для индикатора в файле main.c
#include "NRF24.h"
#include "max7219.h"
Из функции main() удалим все локальные переменные
//uint8_t dt_reg=0;
uint8_t dt;
uint16_t i=1,retr_cnt_full=0, cnt_lost=0;
В этой же функции инициализируем наш индикатор
/* USER CODE BEGIN 2 */
HAL_Delay(100);
Init_7219();
NRF24_ini();
Из бесконечного цикла удалим полностью весь код.
Идём в файл NRF24.h и подключим туда тоже библиотеку индикатора
#include "stm32f1xx_hal.h"
#include "max7219.h"
Переходим теперь в файл NRF24.c.
Изменим там количество байт в пакете, а также адрес, так как мы его изменили в передатчике
#define TX_PLOAD_WIDTH 5
uint8_t TX_ADDRESS[TX_ADR_WIDTH] = {0xb5,0xb5,0xa1};
Добавим после функции передачи пакета NRF24L01_Send функцию приёма пакета, для нас она уже стандартная
//------------------------------------------------
void NRF24L01_Receive(void)
{
uint8_t status=0x01;
while((GPIO_PinState)IRQ == GPIO_PIN_SET) {}
status = NRF24_ReadReg(STATUS);
DelayMicro(10);
status = NRF24_ReadReg(STATUS);
if(status & 0x40)
{
NRF24_Read_Buf(RD_RX_PLOAD,RX_BUF,TX_PLOAD_WIDTH);
LED_TGL;
Clear_7219();
Number_7219(*(uint16_t*)RX_BUF);
NRF24_WriteReg(STATUS, 0x40);
}
}
//------------------------------------------------
Добавим прототип для данной функции в заголовочном файле.
Вернёмся в файл main.c и в функции main() в бесконечном цикле вызовем функцию приёма пакета
/* USER CODE BEGIN 3 */
NRF24L01_Receive();
}
Соберём код, прошьём контроллер и увидим, что пакеты от передатчика нам приходят нормально
На дисплее передатчика пока пакеты отображаются не в той строке, но, думаю, когда мы подключим все приёмники, то всё будет нормально.
Теперь подключим наш первый приёмник от независимого источника питания, причём через USB OTG разъём, так как ST-LINK нам потребуется для второго передатчика. У меня есть ещё две платы F103, но нет ST-Link-ов дешевых, через которые можно питать платы. Вот одну из этих плат мы и возьмём в качестве второго приёмника. Выглядит она несколько по-другому, чем наша привычная, но смысл тот же, причём такая даже удобнее
Подключим к плате модуль NRF и индикатор к тем же ножкам, что и у первого передатчика, а также подключим ST-Link
Подключим ST-Link к ПК.
А проект мы будем к данной схеме создавать в следующей части нашего занятия, в которой мы также создадим и напишем проект и для третьего приёмника, а также проверим как работает адресная передача на несколько приёмников.
Предыдущий урок Программирование МК STM32 Следующая часть
Отладочную плату STM32F103C8T6 можно приобрести здесь STM32F103C8T6
Программатор недорогой можно купить здесь ST-Link V2
Модуль NRF24L01+ с антенной можно купить здесь NRF24L01+
Модуль NRF24L01+ без антенны можно купить здесь NRF24L01+
Адаптер для NRF24L01 можно купить здесь (5 штук) Адаптер для NRF24L01
Индикатор светодиодный семиразрядный с драйвером MAX7219
Смотреть ВИДЕОУРОК в RuTube (нажмите на картинку)
Смотреть ВИДЕОУРОК в YouTube (нажмите на картинку)
«Задержка в условиях на 1 милисекунду нужна для случая совпадения условий»
Вместо этого лучше отслеживать прерывания от ножки irq по окончании передачи. Но для этого нужно немного логику изменить.