Беспроводная передача данных, ISM-диапазон

в 7:09, , рубрики: arduino, Песочница, метки: ,

Беспроводная передача данных, ISM диапазон
В последнее время на Хабре было описано множество примеров реализации погодных термометров, систем сбора информации, управлением в системах «умный дом» — как проводных, передающих информацию по Ethernet, так и беспроводных, по WiFi™. В каждом конкретном случае — есть своя специфика, есть свои плюсы и минусы. И в данном материале речь пойдет об еще одном способе передачи данных — передаче в ISM-диапазоне 868 МГц.

В Российской Федерации к нелицензируемому диапазону частот, которые могут быть использованы без оформления разрешения ГКРЧ при условии соблюдения требований по ширине полосы, излучаемой мощности и назначению готового изделия, относят:

  1. 433.075—434.750 МГц
  2. 868.0—868.2 МГц
  3. 868.7—869.2 МГц
  4. 2400.0—2483.5 МГц

Коротко, для 434 МГц мощность передатчика должна составлять не более 10 мВт, для 868.0—868.2 МГц — до 10 мВт, для 868.7—869.2 МГц — до 25 мВт, для 2.4 ГГц — не более 100 мВт. Подробнее об ограничениях читайте в «Постановлении Правительства РФ от 12 октября 2004 г. N 539 «О порядке регистрации радиоэлектронных средств и высокочастотных устройств».

Основное различие между данными ISM-диапазонами определяется частотой излучения и как следствие, свойствами радиоволн. Применительно к задаче — сбора данных, систем беспроводного управления и контроля, наиболее оптимальным решением является использование диапазона 868 МГц. По сравнению с СВЧ диапазоном 2.4 ГГц, более длинные волны 868 МГц имеют меньшую интенсивность затухания, соответственно большая проницаемость сквозь преграды и дальность передачи сигнала гораздо выше. Для примера, кирпичная стена толщиной 89 мм поглощает около 3.5 дБ мощности волны 868 МГц и 6 дБ у 2.4 ГГц. Также в сравнении с диапазоном 433 МГц, у 868 МГц меньшая загруженность частоты, что способствует более надежной работе радиоканала.

Предельная толщина препятствия, через которую может пройти радиосигнал
Частоты Кирпичная стена, м. Бетон, м.
434 МГц 4.3 0.47
868 МГц 2.18 0.24
2.4 ГГц 0.78 0.09

Следующей важной характеристикой является скорость передачи данных. Современные ISM трансиверы имеют достаточно высокие показатели, в среднем это значение от 256 до 1000 кбит/сек, что для подобного рода задач вполне достаточно.

Таким образом, можно заключить, что в совокупности таких параметров как — высокая проницаемость, меньшая загруженность частотного диапазона, а также достаточно высокая скорость передачи данных, радиоволны 868 МГц является наиболее оптимальным решением данного рода задач по сравнению с остальным ISM-диапазоном.

Для примера передачи данных в ISM-диапазоне соберем устройство сбора показаний с удаленных датчиков. Допустим это будет температура и влажность воздуха. Т.е. нам нужно собрать 2 разнесенных устройства — первое будет выполнять роль головного и отображать сводную информацию, а второе — датчик, будет периодически производить замеры и отсылать данные на головное устройство. Причем оба устройства будут размещены вне прямой видимости, в двух разных зданиях.

В качестве платформы, позволяющей организовать радиоканал 868 МГц, воспользуемся платкой «Колибри» (Arduino Mini + RF). В ней используется трансивер EZRadioPRO Si4431 с программируемой выходной мощностью от -8 до +13dbM, что соответствует нормам ГКРЧ (20 мВт). Шаг настройки 3dbM. Чувствительность приемника –121 dBm. Энергопотребление 18.5 mA на прием и 30 mA на передачу. Допустимое питание платы от 5 или от 3.3V. Скорость передачи данных 0.123 — 256 кбит/сек. Помимо всего прочего плата программно совместима с Arduino IDE, что позволяет ее легко программировать. Принципиальная схема.

Для замера влажности и температуры воспользуемся цифровым датчиком SHT10. Он достаточно компактен и требует минимальной обвязки. Точность измерения показаний температуры ±0.5℃, а влажности 4.5%. Даташит.

Для отображения информации на головном устройстве возьмем графический ЖК-дисплей с разрешением 128*64 точки (WG12864A-TGH-VNW). Подсветка белая, цвет точки серый. И, дабы не занимать все пины микроконтроллера под дисплей, подключать его будем по SPI с помощью микросхемки MCP23S17. Но об этом чуть позже.

Ключевые компоненты системы
Для головного устройства Для удаленного датчика
«Железо»
  • Колибри — 1 шт.
  • ЖК-дисплей — 1 шт.
  • Макетная плата – 1 шт.
  • Микросхема MCP23S17 — 1 шт.
  • Источник питания на 5V – 1 шт.
  • Штыревая антенна – 1 шт.
  • Колибри — 1 шт.
  • SHT10 — 1 шт.
  • Макетная плата – 1 шт.
  • Батарейный отсек на 2AA — 1 шт.
  • Батарейки AA по 1.5V – 2 шт.
  • Штыревая антенна – 1 шт.
Софт

Головное устройство

1. Сборка

Для начала соберем схему головного устройства.
Головное устройство будет состоять из платы Колибри, которая будет принимать и отображать данные на ЖК-дисплее. И как было сказано выше, работать с ЖК-дисплеем будем через интерфейс SPI, посредством микросхемы MCP23S17.
Беспроводная передача данных, ISM диапазон
Данную схему соберем на макетной плате. Выводы обозначенные синими линиями подключим к плате Колибри — это цифровые контакты 10, 11, 12, 13 (SPI). На макетную плату и на Колибри остается подать питание 5V. Питание устройства предполагается либо от блока питания, где “честные” 5V, либо через линейный стабилизатор.
Беспроводная передача данных, ISM диапазон

2. Прошивка

Для работы с радиомодулем платы Колибри воспользуемся готовой библиотекой EZRadioPRO под Arduinо IDE. Ее нужно скачать и установить внутри IDE. Также нам понадобится библиотека I2C_graphical_LCD_display для работы с ЖК дисплеем. Ее также нужно скачать и установить.

Стартуем Arduino IDE и создаем на основе примера вот такой вот скетч.

#include <SI4431.h>
#include "SimRF.h"
#include <Wire.h>
#include <SPI.h>
#include <I2C_graphical_LCD_display.h>
I2C_graphical_LCD_display lcd;

// Локальный адрес радиомодуля (0 для мастера)
#define LOCAL_ADDR (0)
// Функция 1: приём данных с датчика
#define RF_FUNC01 (1)
// Функция 2: передача данных датчику
#define RF_FUNC02 (2)
// Буфер передаваемых данных
unsigned char RFTX_buffer[32];
// Буфер принимаемых данных
unsigned char RFRX_buffer[32];
char i2a_buf[6];
/*
Структура с 4-мя 16-битными регистрами
удаленных датчиков
*/
typedef struct {
  u16 Register0; 
  u16 Register1;
  u16 Register2;
  u16 Register3;
} tRemoteSensor;

tRemoteSensor RemoteSensorStatus[2]; // Массив регистров состояния удаленных датчиков 1 и 2
tRemoteSensor RemoteSensorCmd[2];    // Массив регистров для отправки на удаленные датчики 1 и 2

void print_P (const char* s) {
  char c;
  while ((c = pgm_read_byte(s++)) != 0)
  Serial.print(c);
}

u8 get_xor(u8* Src, u8 len) {
  u8 xoracc = 0;
  while (len--) {
    xoracc ^= *Src++;
  }
  return xoracc;
}

// Передача функции 02 с параметрами для датчиков из массива RemoteSensorCmd[];
void RFTX_FUNC02(u8 addr) {    
  RemoteSensorCmd[addr -1].Register0 = RemoteSensorStatus[addr -1].Register0;
  RemoteSensorCmd[addr -1].Register1 = RemoteSensorStatus[addr -1].Register1;
  RemoteSensorCmd[addr -1].Register2 = RemoteSensorStatus[addr -1].Register2;
  RemoteSensorCmd[addr -1].Register3 = RemoteSensorStatus[addr -1].Register3;
    
  RFTX_buffer[0] = addr;       //Адрес назначения
  RFTX_buffer[1] = LOCAL_ADDR; //Адрес отправителя  
  RFTX_buffer[2] = RF_FUNC02;	//Код функции 02
  u16* p16 = (u16*) &RFTX_buffer[3];
  *p16++ = RemoteSensorCmd[addr -1].Register0;
  *p16++ = RemoteSensorCmd[addr -1].Register1;
  *p16++ = RemoteSensorCmd[addr -1].Register2;
  *p16++ = RemoteSensorCmd[addr -1].Register3;  
  // полученный ХЭШ код передаваемых данных
  RFTX_buffer[11] = get_xor ((u8*)RFTX_buffer, 11);
  //Отправим пакет по заданному адресу
  /*print_P(PSTR("rnData to TX: "));  
  for (u8 i = 0; i< 12; i++) {
    Serial.print((u8)RFTX_buffer[i], HEX);
    print_P(PSTR(","));
  }*/   
  SI4431.TXData((u8*) RFTX_buffer, 12); 
}

void RFRX_FUNC01(u8 SlaveAddr, u16* Payload) {
  RemoteSensorStatus [SlaveAddr - 1].Register0 = *Payload++;
  RemoteSensorStatus [SlaveAddr - 1].Register1 = *Payload++;
  RemoteSensorStatus [SlaveAddr - 1].Register2 = *Payload++;
  RemoteSensorStatus [SlaveAddr - 1].Register3 = *Payload++;
}
            
/** Пример обработки принятых пакетов
*  ВХОД:
*		addr - ожидаемый адрес ведомого устройства
*		len - длина принятого пакета
*		pData - указатель на начало принятого пакета в SRAM
* ВЫХОД:
*		(0..127) код принятой функции
*		-1		 ошибка 	
*/
void RFRX_MASTER_PROCESS(u8 len) {  
  s8 funccode;
  u8 SlaveAddr;
  // Слишком короткие пакеты не рассматриваем  
  if (len < 3) return;
    //print_P(PSTR("rn Dump:"));
    /*for (u8 i = 0; i< len; i++) {
      Serial.print(RFRX_buffer[i], HEX);
      print_P(PSTR(","));
      i2a((u16)RFRX_buffer[i], i2a_buf);
      lcd.string(i2a_buf);
      lcd.string("-");
    }
    */
    SlaveAddr = RFRX_buffer[1]; // Получили адрес слейва
    if ((RFRX_buffer[0] == LOCAL_ADDR)&&(SlaveAddr != LOCAL_ADDR)) { 
      //Если пакет предназначен для MASTER и принят от SLAVE (адрес >= 1)
      // Проверим XOR
      // print_P(PSTR("rn Correct Addr!"));
      if (get_xor((u8*)RFRX_buffer, len - 1) == (u8) RFRX_buffer[len-1]) {                   
        funccode = RFRX_buffer[2]; //получили код функции
        switch (funccode) {
          case RF_FUNC01: { // Пришли данные с датчика
            //print_P(PSTR("rnData from sensor: "));
            //Serial.print(SlaveAddr, HEX);
            tRemoteSensor* pSensorData = (tRemoteSensor*)&RFRX_buffer[3];   // Получили указатель на структуру данных с датчика
          
            // Разбираем данные по переменным
            s16 Temperature_x100 = (s16) (pSensorData-> Register0);         
            u16 Humidity_x100 =    pSensorData-> Register1;         
            u16 Data1       =     pSensorData-> Register2;
            u16 Data2 =             pSensorData-> Register3;
            
            // Печать температуры
            lcd.gotoxy (0, 16);
            lcd.string("Temperature: ");
            lcd.gotoxy (80, 16);
            if (Temperature_x100 < 0) {
              lcd.string("-");
              Temperature_x100 = -Temperature_x100;// преобразем в положительное
            }
            u16 Temp = Temperature_x100 / 100; //Целая часть
            i2a(Temp, i2a_buf);
            showString(i2a_buf);
            //lcd.string(i2a_buf);
            showString(".");
            i2a(Temperature_x100 - Temp * 100, i2a_buf); //Дробная часть
            showString(i2a_buf);
            lcd.string("C ");

            // Печать влажности
            lcd.gotoxy (0, 32);
            lcd.string("Humidity: ");
            lcd.gotoxy (80, 32);
            Temp = Humidity_x100 / 100;
            i2a(Temp, i2a_buf);
            showString(i2a_buf);
            showString(".");
            i2a(Humidity_x100 - Temp * 100, i2a_buf);
            showString(i2a_buf);
            lcd.string("% ");
          

            // Печать произвольного числа
            //i2a(Data, i2a_buf);
            //lcd.string(i2a_buf);
          
            // Скопируем данные в виртуальные регистры датчика для датчиков 01 и 02
            if (SlaveAddr <= 0x02) {
              RFRX_FUNC01(SlaveAddr, (u16*) &RFRX_buffer[3]);
            }   
            // Теперь ждем 500 мс
            delay (500);
            // И отправляем посылку с командой 02
            RFTX_FUNC02(SlaveAddr);
          }
          break;
          case RF_FUNC02: {
            // Пришло подтверждение команды с датчика  
            // Пока ничего делать не будем - просто напишем в консоли, что комнда дошла
            print_P(PSTR("rnACK from sensor:"));
            Serial.print(SlaveAddr, HEX);
          }                  
          break;
          default:
          break;
        }
      }  else  print_P(PSTR("rn XOR error!"));
    }
  else  print_P(PSTR("rn ADDR error!"));
}

// Обработчик ответа для функции 01
void RFRX_RESP_FUNC02(t_cmd_servo* pcmd_servo) {
  s16 POS1, POS2, POS3, POS4;
  POS1 = pcmd_servo->POS1;
  POS2 = pcmd_servo->POS2;
  POS3 = pcmd_servo->POS3;
  POS4 = pcmd_servo->POS4;
  /*print_P(PSTR("rnServo function response OK!"));
  print_P(PSTR("rnPOS1:"));
  Serial.print(POS1, HEX);
  Serial.print(POS2, HEX);
  Serial.print(POS3, HEX);
  Serial.print(POS4, HEX); */  
}

void setup(){
  u8  ItStatus1, ItStatus2;
  lcd.begin(0x20, 0, 10);
  Serial.begin(38400);
  u8 length;
  u8 temp8;
  delay (1000);
  SI4431.begin();     
  // Распечатаем регистры радиомодуля, для проверки SPI связи
  /*for (u8 reg = 0; reg <=0x7f; reg++) {     
    print_P(PSTR("rnReg:"));
    u8 c = SI4431.ReadRegister(reg);
    Serial.print(reg, HEX);
    print_P(PSTR(", "));
    Serial.print(c, HEX);
  }*/	
  SI4431.Init(7);
  lcd.gotoxy(36, 0);
  lcd.string("I'm Ready!");
}

void loop(){
  u8  ItStatus1, ItStatus2;
  u8 len;
  //Разрешим 2 прерывания:
  // a) первое показывает прием правильного пакета:  'ipkval'
  // б) второе показывает прием пакета с неправильной КС CRC:  'icrcerror' 
  SI4431.RXIRQEnable();
  //читаем регистры состояния прерываний для очистки флагов прерываний и освобождения вывода NIRQ        
   SI4431.ReadStatus(&ItStatus1, &ItStatus2);
  /*Снова разрешаем тракт приёма*/
   SI4431.RXEnable();
  // ждём событие прерывания
  // если оно наступило, значит принят пакет либо возникла ошибка CRC    
  if(SI4431.IRQstate() == 0 ){
    //запрет тракта приёма
    SI4431.RXDisable();
    //читаем регистры состояния прерываний для очистки флагов прерываний и освобождения вывода NIRQ        
    SI4431.ReadStatus(&ItStatus1, &ItStatus2);
    // Было прерывание по ошибке CRC
    if( (ItStatus1 & 0x01) == 0x01 ){
      print_P(PSTR("rn Received CRC Error!"));
      //сброс FIFO передачи
      SI4431.FIFOReset();
      //помигаем всеми светодиодами для индикации ошибки
      /*TX_LED_SET
      RX_LED_SET
      delay(500);
      TX_LED_CLR
      RX_LED_CLR*/
    }
    // Было прерывание по приёму нормального пакета
    if( (ItStatus1 & 0x02) == 0x02 ){
      print_P(PSTR("rn Received good packet! Len = "));
      //Читаем размер полезных данных        
      len = SI4431.RXPacketLen();
      Serial.print(len, HEX);
      //убеждаемся, что число принятых байт поместится в буфер МК
      if(len <= sizeof(RFRX_buffer))
      {
        SI4431.RXData((u8 *)RFRX_buffer, len); //Читаем принятые данные 
        RFRX_MASTER_PROCESS (len);                    	                                        
     }
    }
    //сброс RX FIFO
    SI4431.FIFOReset();
    //Разрешение тракта приёма
    SI4431.RXEnable();            
  }
}

void i2a( unsigned int i, char* pOut_buf ){
  int ii;
  char int_buf[5];
  for (ii=0; ii < 5; ){
    int_buf[ii++] = '0'+ i % 10;
    i = i / 10;
  }
  do{ ii--; }while( (int_buf[ii] == '0') && (ii != 0) );
  do
    {
      *pOut_buf++= int_buf[ii--];
    } while (ii >= 0);
    *pOut_buf = 0x00;
}

Данный скетч инициализирует радиоканал, головному устройству назначается адрес 0. Выставлена максимальная мощность передатчика 13dBm: SI4431.Init(7);
В данном примере на дисплей выводятся показания от 1 удаленного датчика, при необходимости аналогичным образом можно выводить показания от множества датчиков.

Далее прошивочку нужно загрузить в плату Колибри. Сделать это можно несколькими способами.

  1. С помощью платы Ардуино
  2. С помощью USB-Serial Converter
  3. С помощью внутрисхемного программатора.

Для прошивки с помощью Ардуино, сперва нужно извлечь из него микроконтроллер. После этого нужно соединить проводками обе платы следующими пинами:

Ардуино Колибри
Pin 0 Pin 0
Pin 1 Pin 1
RESET RESET
+5V +5V
GND GND

После того как соединили и выставили на Колибри джампер питания в положение 5V, можно подключить Ардуино к ПК. В Arduino IDE укажите правильный порт и в разделе Tools -> Board выберите параметр «Arduino Nano w/ Atmega 168», после чего нажмите на кнопку «Загрузить».

Аналогичные действия предпринимаются при загрузки с помощью USB-Serial конвертера. Ну и самый легкий способ — это загрузка с помощью программатора. В Arduino IDE нажимаете кнопку «Скомпилировать», далее «находите» hex-файл с прошивкой. Подключаетесь программатором к ICSP разъему Колибри, подаете на плату питание, загружаете в нее прошивку, указав предварительно в оболочке вашего программатора МК ATMEGA168A. Фьюзы: 0xF8, 0xDF, 0xFF. Lockbit: 0xCF.

Прошили, отключили все лишние проводки от Колибри. Теперь подаем на плату питания и на ЖК дисплее должна появится надпись: I'm Ready! Головное устройство собрано, переходим к следующему шагу.

Датчик

1. Сборка

Плата с датчиком будет состоять только из одного цифрового сенсора. Питание платы будет батарейное, 3V.

Схема датчика SHT10.
Беспроводная передача данных, ISM диапазон
Соберем по схеме на макетной плате. Сенсор подключается к Колибри по 4 проводкам (цифровые пины 6 и 7 и питание).

Беспроводная передача данных, ISM диапазон

Прежде чем подключить к Колибри батарейный отсек, необходимо загрузить прошивку. А уже после этого подключаем к Колибри батарейный отсек к разъему питания на 3V и джампер также выставляем в положение 3V.

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

2. Прошивка

Помимо библиотеки EZRadioPRO для датчика понадобится библиотека SHT1x, с ее помощью будем считывать показания температуры и влажности. Качаем и устанавливаем эту библиотеку.

Стартуем Arduino IDE и создаем на основе примера вот такой вот скетч.

#include <SI4431.h>
#include "SimRF.h"
#include <avr/sleep.h>
#include <avr/wdt.h> 

#include <SHT1x.h>
#define dataPin  7
#define clockPin 6
SHT1x sht1x(dataPin, clockPin);
//#define DEBUG_MODE
// Локальный адрес радиомодуля (0 для мастера)
#define SLAVE_ADDR (1)
// Коды функций протокола
// Функция 1: передача измерений мастеру
#define RF_FUNC01 (1)
// Функция 2: прием команд от мастера
#define RF_FUNC02 (2)
// Буфер передаваемых данных
unsigned char RFTX_buffer[32];
// Буфер принимаемых данных
unsigned char RFRX_buffer[32];
unsigned char putch(unsigned char send_char) {
  while (!(UCSR0A & 0x20));
  UDR0 = (unsigned char) send_char;
  while (!(UCSR0A & 0x20));
  return(send_char);
}
/*
 Печать в консоль строки из FLASH ПЗУ
*/
void print_P (const char* s) {
  char c;
  while ((c = pgm_read_byte(s++)) != 0)
    putch(c);
}
/*
 Вычисление XOR для массива данных
 u8* Src - указатель на начало данных в ОЗУ
 u8 len - длина обрабатываемого массива
*/
u8 get_xor(u8* Src, u8 len){
  u8 xoracc = 0;
  while (len--){
    xoracc ^= *Src++;
  }
  return xoracc;
}
/*
 Передача функции 01 с параметрами измерений
 u8 addr - Адрес назначения
 u8* Data - Указатель на начало массива данных
 u8 Len - Длина массива данных
*/
void RFTX_FUNC01(u8 addr, u8* Data, u8 Len){           
  RFTX_buffer[0] = addr;       //Адрес назначения
  RFTX_buffer[1] = SLAVE_ADDR; //Адрес отправителя  
  RFTX_buffer[2] = 0x01;	//Код функции 01
  //Укладываем массив данных в пакет для передачи
  for (u8 i = 0 ; i < Len; i++){
    RFTX_buffer[3+i] = *Data++;
  } 
  // получем XOR код передаваемых данных
  RFTX_buffer[3 + Len] = get_xor ((u8*)RFTX_buffer, Len + 3);
  SI4431.TXData((u8*) RFTX_buffer, 3 + Len + 1); //Отправим пакет по заданному адресу по радиоканалу
}
/** Обработка принятых пакетов
*  ВХОД:
*		addr - ожидаемый адрес удаленного устройства
*		len - длина принятого пакета
*		pData - указатель на начало принятого пакета в SRAM
* ВЫХОД:
*		(0..127) код принятой функции
*		-1		 ошибка 	
*/
s8 RFRX_PROCESS(u8 addr, u8 len, u8* pData){
  s8 funccode;
  // Слишком короткие пакеты не рассматриваем  
  if (len < 3) return -1;
  if ((RFRX_buffer[0] == SLAVE_ADDR)&&(RFRX_buffer[1] == addr)) { //Если пакет предназначен для нас и принят от заданного адреса отправителя    
    if (get_xor((u8*)RFRX_buffer, len - 1) == RFRX_buffer[len-1]){
      funccode = RFRX_buffer[2];
      pData = (u8*) &RFRX_buffer[3];
      return  funccode;                  
    }
    else return -1;    
  }
  else return -1;
}
/*
  Обработчик ответа от мастера для функции 02
  u8* PayloadData - начало полезных данных
  u8 PayloadLen   - длина полезных данных
*/
void RFRX_RESP_FUNC02(u8* PayloadData, u8 PayloadLen){
  // Пока просто распечатаем принятые регистры от мастера в консоли
  #ifdef DEBUG_MODE 
  for (u8 i = 0; i< PayloadLen; i++){
    Serial.print(*PayloadData++, HEX);
  }   
  #endif
}
//Формирование и отправка подтверждения приёма пакета
void RFTX_ACK_FUNC02(u8 addr, u8* Data, u8 Len){
  RFTX_buffer[0] = 0x00; //Адрес мастера
  RFTX_buffer[1] = SLAVE_ADDR; //Наш адрес 
  RFTX_buffer[2] = RF_FUNC02;
  //Укладываем массив данных в пакет для передачи
  for (u8 i = 0 ; i < Len; i++){
    RFTX_buffer[3+i] = *Data++;
  } 
  // получем XOR код передаваемых данных
  RFTX_buffer[3 + Len] = get_xor ((u8*)RFTX_buffer, Len+3);        
  SI4431.TXData((u8*) RFTX_buffer, 3 + Len + 1); //Отправим пакет по заданному адресу по радиоканалу
}
u16 SensorData [4]; // Массив с 4мя 16 битными данными датчика
volatile unsigned char WDT_wake;
volatile unsigned char SLEEP_TIME = 15;
//volatile unsigned char WDT_counter;  // увеличим счетчик прерываний
/*
Обработчик прерываний по срабатыванию охранного таймера
*/
ISR(WDT_vect) {  
  static unsigned char WDT_counter;  // увеличим счетчик прерываний
  if (WDT_counter++ == SLEEP_TIME) {
    WDT_counter = 0;
    WDT_wake = 1;
  }
  //Serial.print(WDT_counter, HEX);   
  asm ("WDR");
}

void setup_WDT(void) {
 asm ("CLI");
 asm ("WDR");
 MCUSR &= ~(1<<WDRF); //Сброс флага срабатывания
 WDTCSR = (1 << WDCE)|(1<<WDE);
 WDTCSR = (1 << WDP2)|(1 << WDP1)|(1 << WDIE)|(1 << WDIF);  // Настройка сторожевого таймера на период 1 сек. Перевод в режим работы генерирования прерываний
 asm ("SEI"); 
}

void off_WDT(void){
  asm ("CLI");
  asm ("WDR");
  MCUSR &= ~(1<<WDRF); //Сброс флага срабатывания
  WDTCSR = (1 << WDCE)|(1<<WDE)|(1 << WDIF); 
  /* Turn off WDT */
  WDTCSR = 0x00;
  asm ("SEI"); 
}

void sys_sleep(void){
  asm ("CLI");       
  ADCSRA &= ~(1 << ADEN);  // Выключим АЦП  
  SMCR |= (1<<SE); // Конфигурируем режим power-down
  asm ("SEI");    
  asm ("SLEEP");
}

void sys_wake(void){
  // тут можно включить АЦП
}

void RF_sleep (void){
  u8  ItStatus1, ItStatus2;
  //читаем регистры состояния прерываний для очистки флагов прерываний и освобождения вывода NIRQ        
  SI4431.ReadStatus(&ItStatus1, &ItStatus2);
  SI4431.WriteRegister(0x08, 0x03); //Сброс FIFO
  SI4431.WriteRegister(0x08, 0x00); 
  SI4431.WriteRegister(0x07, 0x00); // Усыпляем модуль
}

void setup(){
  Serial.begin(38400);
  delay (1000);
  u8  ItStatus1, ItStatus2;
  u8 length;
  u8 temp8;   	
  #ifdef DEBUG_MODE
    print_P (PSTR("rnHello!"));
  #endif
  delay (100);
  SI4431.begin();  
  #ifdef DEBUG_MODE  
  // Распечатаем регистры радиомодуля, для проверки SPI связи
  for (u8 reg = 0; reg <=0x7f; reg++){     
      print_P(PSTR("rnReg:"));
      u8 c = SI4431.ReadRegister (reg);
      Serial.print(reg, HEX);
      print_P(PSTR(", "));
      Serial.print(c, HEX);
  }  	
  #endif
  SI4431.Init(7);  
  #ifdef DEBUG_MODE
    print_P(PSTR("rnRadio initialisation is OK"));    
  #endif
  delay (1000);
  setup_WDT();
  SMCR = (1<<SE)|(1<<SM1); // Конфигурируем режим power-down
  sys_sleep();
}

void loop(){
  u8  ItStatus1, ItStatus2;
  unsigned long Timer;
  if (WDT_wake){
  Timer = 0;
  WDT_wake = 0;
  // Просыпаемся раз в N секунд
  // Отключаем сторожевой таймер
  off_WDT();    
  //digitalWrite(13, HIGH); // Включим светодиод (индикация просыпания и работы)
  #ifdef DEBUG_MODE
    print_P(PSTR("rn WAKE UP!"));  
  #endif 
  // 1) Сначала получим данные, например от АЦП или от SHT11x 

  float Temp = sht1x.readTemperatureC();
  s16 Temp_x100 = (s16) (Temp * 100.0); // Умножаем на 100.0
  SensorData[0] = (u16) Temp_x100;
  
  float Humidity = sht1x.readHumidity();
  u16 Humidity_x100 = (s16) (Humidity * 100.0); // Умножаем на 100.0
  SensorData[1] = Humidity_x100;
  
  SensorData[2] = 0;
  SensorData[3] = 0;  
  // 2) Потом, сформируем в ОЗУ пакет для передачи в эфир  
  // 3) Потом, выводим радиомодули из спящего режима 
  // 4) Отправляем данные мастеру по адресу 0 (ответа ждать не будем... мне кажется не обязательно это)  
  for (u8 TXcnt = 0; TXcnt< 4; TXcnt++) {
  #ifdef DEBUG_MODE
    print_P(PSTR("rn TX to MASTER #"));   
  #endif  
    Serial.print(TXcnt + 1, DEC);
  RFTX_FUNC01(0x00, (u8*) SensorData, 8); // Отправим по адресу 0  8 байт информации с датчика
  // 4) Повторяем отправку данных мастеру по адресу 0 через короткое время для надежности
  }
  // 5) Переводим радиомодуль в режим RX
    //после передачи установим биты разрешения прерываний для режима приёма
    //Разрешим 2 прерывания:
    // a) первое показывает прием правильного пакета:  'ipkval'
    // б) второе показывает прием пакета с неправильной КС CRC:  'icrcerror' 
  SI4431.RXIRQEnable();
 //читаем регистры состояния прерываний для очистки флагов прерываний и освобождения вывода NIRQ        
  SI4431.ReadStatus(&ItStatus1, &ItStatus2);
  //сброс FIFO передачи
  SI4431.FIFOReset();  
 /*Снова разрешаем тракт приёма*/
  SI4431.RXEnable();   
  
  // 6) Ждем команды от мастера в течение 1 секунд
    #ifdef DEBUG_MODE
    print_P(PSTR("rn Wait Resp"));    
  #endif  
  Timer = 0;
  #define RESP_TIMEOUT (65535 * 32)
  while (Timer < RESP_TIMEOUT){
    //asm ("WDR");
    if(SI4431.IRQstate() == 0 )
    { //принят пакет либо возникла ошибка CRC
      //запрет тракта приёма
      SI4431.RXDisable();
      //читаем регистры состояния прерываний для очистки флагов прерываний и освобождения вывода NIRQ        
      SI4431.ReadStatus(&ItStatus1, &ItStatus2);
      // Было прерывание по ошибке CRC
      if( (ItStatus1 & 0x01) == 0x01 )
      {
        #ifdef DEBUG_MODE
          print_P(PSTR("rn RX CRC Error!"));
        #endif  
        //сброс FIFO передачи
        SI4431.FIFOReset();
        //помигаем всеми светодиодами для индикации ошибки
        break; // Выход из цикла ожидания ответа  
      }
      else if( (ItStatus1 & 0x02) == 0x02 )
      {
        // Было прерывание по приёму нормального пакета
        #ifdef DEBUG_MODE
          print_P(PSTR("rn RX packet Len = "));
        #endif  
        //Читаем размер полезных данных        
        u8 len = SI4431.RXPacketLen();
        #ifdef DEBUG_MODE
          Serial.print(len, HEX);        
        #endif  
        //убеждаемся, что число принятых байт поместится в буфер МК
        if(len <= sizeof(RFRX_buffer))
        {
          SI4431.RXData((u8 *)RFRX_buffer, len); //Читаем принятые данные
          s8 funccode = RFRX_PROCESS (0x00, len, (u8*)RFRX_buffer);
          switch (funccode)
          {
             case RF_FUNC02:
               {
                 // Получили ответ от мастера нам!
                 #ifdef DEBUG_MODE
                   print_P(PSTR("rn MASTER RESPONSE!"));
                 #endif  
                 u8* PayloadData = (u8*) &RFRX_buffer[3]; //Начало полезных данных
                 u8 PayloadLen =  len - 4; //Длина полезных данных
                 RFRX_RESP_FUNC02(PayloadData, PayloadLen); //Обработаем ответ
// 7) Если команда пришла, то выполняем ее и отправляем подтверждение выполнения 
                 //Формирование и отправка подтверждения приёма пакета мастеру
                  for (u8 TXcnt = 0; TXcnt< 4; TXcnt++)
                  {
                 #ifdef DEBUG_MODE
                   print_P(PSTR("rn ACK MASTER RESPONSE!"));
                 #endif                   
                 RFTX_ACK_FUNC02(0x00, PayloadData, PayloadLen);
                  }
               }
               break;
             default:
               #ifdef DEBUG_MODE
                 print_P(PSTR("rnInvalid RX packet!"));
               #endif  
               break;
          }
        }        
      }

      break; // Выход из цикла ожидания ответа      
    }
    else
    {
      Timer++;
    }
  }    
  #ifdef DEBUG_MODE
    if (Timer == RESP_TIMEOUT)   print_P(PSTR("rn Response Timeout!"));  
    print_P(PSTR("rn SLEEP..."));  
  #endif 

  // 8) Выводим радиомодуль в режим сна
  RF_sleep();
  setup_WDT();  
  //digitalWrite(13, LOW);  
  }
  // 9) Выводим атмегу в режим сна, запуская таймер на N секунд для пробуждения
  // 10) Как проснемся, идем на п. 1.    
  sys_sleep();
}

Схема работы данной программы проста. После инициализации радиоканала, где ему назначается адрес 1 и первой передачи данных на устройство с адресом 0, микроконтроллер переводит трансивер в спящий режим и потом сам засыпает на 15 секунд. По истечению данного времени по сторожевому таймеру он пробуждается, включает трансивер и вновь происходит передача данных.

К одному головному устройству можно привязать множество таких датчиков.

Загружаем этот скетч в Колибри. После загрузки отключаем все лишнее, подключаем батарейное питание и включаем питание. Через некоторое время на головном устройстве получим показания с удаленного датчика.


Уф, вроде все написал. Если что-то непонятно, спрашивайте :)

Автор: Prometheus

Источник

* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js