Li-Fi коммуникация на Arduino Uno

negavoid
Offline
Зарегистрирован: 09.07.2016

Предыстория: Li-Fi - способ передачи данных посредством света. На текущий момент существуют промышленные устройства для домашних персональных компьютеров, работающие, как будто Wi-Fi, только вместо роутера в потолок вкручивается "лампочка", а в usb-порт вставляется "аналог сетевой карты", только световой. Стоит аналог роутера что-то вроде 900$, а сетевушка около 300$. Немного офигев от цен, погуглил, и таки да, господин Джон Пьет реализовал вариант Li-Fi коммуникацию для ардуин, и его проект я и хочу здесь представить, а то всё радио да радио. Умеющие читать на английском могут сразу обратиться к оригиналу: 

https://github.com/jpiat/arduino/wiki/Arduino-simple-Visible-Light-Communication

Необходимое оборудование:

2x Arduino UNO - одна передатчик, вторая приёмник

2x светодиода одинакового цвета (у меня работало лучше всего с красными)

1х резистор 1 МОм

1х резистор 120 Ом (не обязательно)

Схема соединения:

Передатчик

D2  ------[resistor]----- led -------------- GND

Резистор очень желателен, но не обязателен.

Приёмник

           |----1Mohm-----|
  A3 ------|--- +led- ----|-------GND

Код передатчика:

/*
  LiFi Emitter

  The purpose of this demos is to demonstrate data communication using
  a pair of blue LED (one led as emitter one led as receiver).
  Communication can go at up to 600 bps (can depend on led quality)

  Emitter hardware is the following:

  I/O D2  ------[resistor]----- led -------------- GND

  Using a blue led should not require resistor, one may be needed for red or green

  A byte is sent as follow :

  Start(0) 8bit data Stop(1)

  Each bit is coded in manchester with
  0 -> 10
  1 -> 01

  A data frame is formatted as follow :

  0xAA : sent a number of time to help the received compute a signal average for the thresholding of analog values
  0xD5 : synchronization byte to break preamble
  0x02 : STX start of frame
  N times Effective data excluding command symbols, with N < 32
  0x03 : ETX end of frame
*/

#include <TimerOne.h>
#include <util/atomic.h>
//Start of what should be an include ...

//#define TRANSMIT_SERIAL

// change to alter communication speed,
// will lower values will result in faster communication
// the receiver must be tuned to the same value
#define SYMBOL_PERIOD 500 /* Defined a symbol period in us*/

#define WORD_LENGTH 10 /* Each byte is encoded on 10-bit with start, byte, stop */
#define SYNC_SYMBOL 0xD5 /* Synchronization symbol to send after a preamble, before data communication happens */
#define ETX 0x03
#define STX 0x02

//Fast manipulation of LED IO.
//These defines are for a LED connected on D13
/*#define OUT_LED() DDRB |= (1 << 5);
  #define SET_LED() PORTB |= (1 << 5)
  #define CLR_LED() PORTB &= ~(1 << 5)
*/

//These defines are for a RGB led connected to D2, D3, D4
/*#define OUT_LED() DDRD |= ((1 << 2) | (1 << 3) | (1 << 4))
  #define SET_LED() PORTD |= ((1 << 2) | (1 << 3) | (1 << 4))
  #define CLR_LED() PORTD &= ~((1 << 2) | (1 << 3) | (1 << 4))
*/

//These defines are for a single led connected to D2
#define OUT_LED() DDRD |= ((1 << 2))
#define SET_LED() PORTD |= ((1 << 2))
#define CLR_LED() PORTD &= ~((1 << 2))

unsigned char frame_buffer [38] ; //buffer for frame
char frame_index = -1; // index in frame
char frame_size = -1  ; // size of the frame to be sent

//state variables of the manchester encoder
unsigned char bit_counter = 0 ;
unsigned short data_word = 0 ;  //8bit data + start + stop
unsigned char half_bit = 0 ;
unsigned long int manchester_data ;

void to_manchester(unsigned char data, unsigned long int * data_manchester) {
  unsigned int i ;
  (*data_manchester) = 0x02 ; // STOP symbol
  (*data_manchester) = (*data_manchester) << 2 ;
  for (i = 0 ; i < 8; i ++) {
    if (data & 0x80) (*data_manchester) |=  0x02  ; // data LSB first
    else (*data_manchester) |= 0x01 ;
    (*data_manchester) = (*data_manchester) << 2 ;
    data = data << 1 ; // to next bit
  }
  (*data_manchester) |= 0x01 ; //START symbol
}

//emitter interrupt
void emit_half_bit() {
  if (manchester_data & 0x01) {
    SET_LED();
  } else {
    CLR_LED();
  }
  bit_counter -- ;
  manchester_data = (manchester_data >> 1);
  if (bit_counter == 0) {
    // is there still bytes to send in the frame ?
    manchester_data = 0xAAAAAAAA ; // keep sending ones if nothing to send
    if (frame_index >= 0 ) {
      if (frame_index < frame_size) {
        /*
          Serial.println(frame_index, DEC);
          Serial.println(frame_buffer[frame_index], HEX);
        */
        to_manchester(frame_buffer[frame_index], &manchester_data);
        frame_index ++ ;
      } else {
        frame_index = -1 ;
        frame_size = -1 ;
      }
    }
    bit_counter = WORD_LENGTH * 2 ;
    //Serial.println(manchester_data, BIN);
  }
}

void init_frame(unsigned char * frame) {
  memset(frame, 0xAA, 3);
  frame[3] = SYNC_SYMBOL ;
  frame[4] = STX;
  frame_index = -1 ;
  frame_size = -1 ;
}

int create_frame(char * data, int data_size, unsigned char * frame) {
  memcpy(&(frame[5]), data, data_size);
  frame[5 + data_size] = ETX;
  return 1 ;
}

int write(char * data, int data_size) {
  if (frame_index >=  0) return -1 ;
  if (data_size > 32) return -1 ;
  create_frame(data, data_size, frame_buffer);
  ATOMIC_BLOCK(ATOMIC_RESTORESTATE) {
    frame_index = 0 ;
    frame_size = data_size + 6 ;
  }
  return 0 ;
}

int transmitter_available() {
  if (frame_index >=  0) return 0 ;
  return 1 ;
}

void init_emitter() {
  manchester_data = 0xFFFFFFFF ;
  bit_counter = WORD_LENGTH * 2 ;
}

// the setup routine runs once when you press reset:
void setup()
{
  // use D4 as ground [ by megavoid ]
  //pinMode(4, OUTPUT);
  //digitalWrite(4, LOW);

  // initialize serial communication at 115200 bits per second:
  Serial.begin(115200);
  OUT_LED();
  init_frame(frame_buffer);
  init_emitter();
  Timer1.initialize(SYMBOL_PERIOD); //1200 bauds
  Timer1.attachInterrupt(emit_half_bit);
}

// the loop routine runs over and over again forever:
char * msg = "Hello World";
char com_buffer [32];
char com_buffer_nb_bytes = 0;

void loop()
{
#ifdef TRANSMIT_SERIAL
  // constructing the data frame only if transmitter is ready to transmit
  if (Serial.available() && transmitter_available()) {
    char c = Serial.read();
    com_buffer[com_buffer_nb_bytes] = c ;
    com_buffer_nb_bytes ++ ;
    if (com_buffer_nb_bytes >= 32 || c == '\n') {
      if (write(com_buffer, com_buffer_nb_bytes) < 0) {
        Serial.println("Transmitter is busy");
      } else {
        com_buffer_nb_bytes = 0 ;
      }
    }
  }
  delay(10);
#else
  static int i = 0 ;
  memcpy(com_buffer, msg, 11);
  com_buffer[11] = i + '0' ;
  if (write(com_buffer, 12) < 0) {
    delay(10);
  } else {
    i ++ ;
    if (i > 9) i = 0 ;
  }
#endif
}

Код приёмника:

/*
  LiFi Receiver

  The purpose of this demos is to demonstrate data communication using
  a pair of blue LED (one led as emitter one led as receiver).
  Communication can go at up to 600 bps (can depend on led quality)

  Receiver hardware :

           |----1Mohm-----|
  A3 ------|--- +led- ----|-------GND

  A byte is sent as follow :

  Start(0) 8bit data Stop(1), LSB first : 0 b0 b1 b2 b3 b4 b5 b6 b7 1

  Each bit is coded in manchester with
  time is from left to right
  0 -> 10
  1 -> 01

  A data frame is formatted as follow :

  0xAA : sent a number of time to help the receiver compute a signal average for the thresholding of analog values
  0xD5 : synchronization byte to indicate start of a frame, breaks the regularity of the 0x55 pattern to be easily
  0x02 : STX start of frame
  N times Effective data excluding command symbols, max length 32 bytes
  0x03 : ETX end of frame
*/

#include <TimerOne.h>

enum receiver_state {
  IDLE, //waiting for sync
  SYNC, //synced, waiting for STX
  START, //STX received
  DATA //receiving DATA
};

//#define DEBUG
//#define DEBUG_ANALOG

#define INT_REF /* Comment this to use AVCC reference voltage. To be used when the receiver LED generate low levels */

enum receiver_state frame_state = IDLE ;

//This defines receiver properties
#define SENSOR_PIN 3
#define SYMBOL_PERIOD 500
#define SAMPLE_PER_SYMBOL 4
#define WORD_LENGTH 10 // a byte is encoded as a 10-bit value with start and stop bits
#define SYNC_SYMBOL 0xD5 // this symbol breaks the premanble of the frame
#define ETX 0x03 // End of frame symbol
#define STX 0x02 //Start or frame symbol 

// global variables for frame decoding
char frame_buffer[38] ;
int frame_index  = -1 ;
int frame_size = -1 ;

//state variables of the thresholder
unsigned int signal_mean = 0 ;
unsigned long acc_sum = 0 ; //used to compute the signal mean value
unsigned int acc_counter = 0 ;

//manechester decoder state variable
long shift_reg = 0;

//Start of ADC managements functions
void ADC_setup() {
  ADCSRA =  bit (ADEN);                      // turn ADC on
  ADCSRA |= bit (ADPS0) |  bit (ADPS1) | bit (ADPS2);  // Prescaler of 128
#ifdef INT_REF
  ADMUX  =  bit (REFS0) | bit (REFS1);    // internal 1.1v reference
#else
  ADMUX  =  bit (REFS0) ;   // external 5v reference
#endif
}

void ADC_start_conversion(int adc_pin) {
  ADMUX &= ~(0x07) ; //clearing enabled channels
  ADMUX  |= (adc_pin & 0x07) ;    // AVcc and select input port
  bitSet (ADCSRA, ADSC) ;
}

int ADC_read_conversion() {
  while (bit_is_set(ADCSRA, ADSC));
  return ADC ;
}
//End of ADC management functions

#define START_SYMBOL 0x02
#define STOP_SYMBOL 0x01
#define START_STOP_MASK  ((STOP_SYMBOL << 20) | (START_SYMBOL << 18) | STOP_SYMBOL) //STOP/START/16bits/STOP
#define SYNC_SYMBOL_MANCHESTER  (0x6665) /* Sync symbol, encoded as a 16-bit Manchester value to help the decoding */
inline int is_a_word(long  * manchester_word, int time_from_last_sync, unsigned int * detected_word) {
  if (time_from_last_sync >= 20  || frame_state == IDLE) { // we received enough bits to test the sync
    if (((*manchester_word) & START_STOP_MASK) == (START_STOP_MASK)) { // testing first position
      (*detected_word) = ((*manchester_word) >> 2) & 0xFFFF;
      if (frame_state == IDLE) {
        if ((*detected_word) == SYNC_SYMBOL_MANCHESTER) return 2 ;
      }
      return 1 ;
      // byte with correct framing
    } else if (frame_state != IDLE && time_from_last_sync == 20) {
      (*detected_word) = ((*manchester_word) >> 2) & 0xFFFF;
      return 1 ;
    }
  }
  return 0 ;
}

inline int insert_edge( long  * manchester_word, char edge, int edge_period, int * time_from_last_sync, unsigned int * detected_word) {
  int new_word = 0 ;
  int is_a_word_value = 0 ;
  int sync_word_detect = 0 ;
  if ( ((*manchester_word) & 0x01) != edge ) { //mak sure we don't have same edge ...
    if (edge_period > (SAMPLE_PER_SYMBOL + 1)) {
      unsigned char last_bit = (*manchester_word) & 0x01 ;
      (*manchester_word) = ((*manchester_word) << 1) | last_bit ; // signal was steady for longer than a single symbol,
      (*time_from_last_sync) += 1 ;
      is_a_word_value = is_a_word(manchester_word, (*time_from_last_sync), detected_word);
      if (is_a_word_value > 0) { //found start stop framing
        new_word = 1 ;
        (*time_from_last_sync) =  0 ;
        if (is_a_word_value > 1) sync_word_detect = 1 ; //we detected framing and sync word in manchester format
      }
    }
    //storing edge value in word
    if (edge < 0) {
      (*manchester_word) = ( (*manchester_word) << 1) | 0x00 ; // signal goes down
    } else {
      (*manchester_word) = ( (*manchester_word) << 1) | 0x01 ; // signal goes up
    }
    (*time_from_last_sync) += 1 ;
    is_a_word_value = is_a_word(manchester_word, (*time_from_last_sync), detected_word);
    if (sync_word_detect == 0 && is_a_word_value > 0) { //if sync word was detected at previous position, don't take word detection into account
      new_word = 1 ;
      (*time_from_last_sync) =  0 ;
    }
  } else {
    new_word = -1 ;
  }
  return new_word ;
}

#define EDGE_THRESHOLD 4 /* Defines the voltage difference between two samples to detect a rising/falling edge. Can be increased depensing on the environment */
int oldValue = 0 ;
int steady_count = 0 ;
int dist_last_sync = 0 ;
unsigned int detected_word = 0;
int new_word = 0;
char old_edge_val = 0 ;
void sample_signal_edge() {
  char edge_val ;
  //int sensorValue = analogRead(SENSOR_PIN); // this is too slow and should be replaced with interrupt-driven ADC
  int sensorValue  = ADC_read_conversion(); // read result of previously triggered conversion
  ADC_start_conversion(SENSOR_PIN); // start a conversion for next loop
#ifndef DEBUG
#ifdef DEBUG_ANALOG
  Serial.println(sensorValue, DEC);
#endif
#endif
  if ((sensorValue - oldValue) > EDGE_THRESHOLD) edge_val = 1 ;
  else if ((oldValue - sensorValue) > EDGE_THRESHOLD) edge_val = -1;
  else edge_val = 0 ;
  oldValue = sensorValue ;
  if (edge_val == 0 || edge_val == old_edge_val || (edge_val != old_edge_val && steady_count < 2)) {
    if ( steady_count < (4 * SAMPLE_PER_SYMBOL)) {
      steady_count ++ ;
    }
  } else {
    new_word = insert_edge(&shift_reg, edge_val, steady_count, &(dist_last_sync), &detected_word);
    if (dist_last_sync > (8 * SAMPLE_PER_SYMBOL)) { // limit dist_last_sync to avoid overflow problems
      dist_last_sync = 32 ;
    }
    //if(new_word >= 0){
    steady_count = 0 ;
    //}
  }
  old_edge_val = edge_val ;
}

int add_byte_to_frame(char * frame_buffer, int * frame_index, int * frame_size, enum receiver_state * frame_state , unsigned char data) {
  if (data == SYNC_SYMBOL/* && (*frame_index) < 0*/) {
    (*frame_index) = 0 ;
    (*frame_size) = 0 ;
    (*frame_state) = SYNC ;
    //Serial.println("SYNC");
    return 0 ;
  }
  if ((*frame_state) != IDLE) { // we are synced
    frame_buffer[*frame_index] = data ;
    (*frame_index) ++ ;
    if (data == STX) {
      //Serial.println("START");
      (*frame_state) = START ;
      return 0 ;
    } else if (data == ETX) {
      //Serial.println("END");
      (*frame_size) = (*frame_index) ;
      (*frame_index) = -1 ;
      (*frame_state) = IDLE ;
      //Serial.println("END");
      return 1 ;
    } else if ((*frame_index) >= 38) { //frame is larger than max size of frame ...
      (*frame_index) = -1 ;
      (*frame_size) = -1 ;
      (*frame_state) = IDLE ;
      return -1 ;
    } else {
      (*frame_state) = DATA ;
    }
    return 0 ;
  }
  return -1 ;
}

// the setup routine runs once when you press reset:
void setup()
{
  // use A1 as groung [ by megavoid ]
  //pinMode(A1, OUTPUT);
  //digitalWrite(A1, LOW);

  // initialize serial communication at 115200 bits per second:
  int i;
  Serial.begin(115200);
  Serial.println("Start of receiver program");
  ADC_setup();
  ADC_start_conversion(SENSOR_PIN);
  //analogReference(INTERNAL); // internal reference is 1.1v, should give better accuracy for the mv range of the led output.
  Timer1.initialize(SYMBOL_PERIOD / SAMPLE_PER_SYMBOL); //1200 bauds oversampled by factor 4
  Timer1.attachInterrupt(sample_signal_edge);

}


// the loop routine runs over and over again forever:
void loop()
{
  int i;
  unsigned char received_data;
  char received_data_print ;
  int nb_shift ;
  int byte_added = 0 ;
  if (new_word == 1) {
    received_data = 0 ;
    for (i = 0 ; i < 16 ; i = i + 2) { //decoding Manchester
      received_data = received_data << 1 ;
      if (((detected_word >> i) & 0x03) == 0x01) {
        received_data |= 0x01 ;
      } else {
        received_data &= ~0x01 ;
      }
    }
    received_data = received_data & 0xFF ;
#ifdef DEBUG
    Serial.print(received_data & 0xFF, HEX);
    Serial.print(", ");
    Serial.println((char) received_data);
#endif
    new_word = 0 ;
    if ((byte_added = add_byte_to_frame(frame_buffer, &frame_index, &frame_size, &frame_state, received_data)) > 0) {
      frame_buffer[frame_size - 1] = '\0';
      Serial.println(&(frame_buffer[1]));
    }
    //if(frame_state != IDLE) Serial.println(received_data, HEX);
  }
}

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

ЕвгенийП
ЕвгенийП аватар
Offline
Зарегистрирован: 25.05.2015

Мы с внуком делали на лазере от брелока для ключей, что в каждом кисоске продаются и фототранзисторе. Передавали аналоговый сигнал (модулировали лазер звуком с линейного выхода проигрывателя). Принятый сигнал пускали на аудио-усилитель и слушали на колонках. Качество звука воплне на уровне. Практическая дальность - противоположные углы моей мастерской (она 6х4 метра) - работает отлично, дальше не проверяли. Минус использования лазера - чем дальше, тем труднее попадать в фототранзистор - мельчайшее дрожание рук - лучик уходит. Но если не с рук, а в тиски вставить, то нормально. Да, игрушка была чисто аналоговая - без ардуины.

negavoid
Offline
Зарегистрирован: 09.07.2016

Аха, амплитудную модуляцию делали. А тут чувак заморочился с суровой цифрой, даже Манчестер прикрутил.

Мне сей проект понравился тем, что его может собрать любой за 5 минут из трёх копеечных деталей, которые есть у каждого, и он сразу заработает. А дальше, может поразвивают идею и прикрутят куда к своим поделиям.

dmitron1036
Offline
Зарегистрирован: 10.01.2016

Кул. В чём соль?

negavoid
Offline
Зарегистрирован: 09.07.2016

Просто ещё один способ обмена данными между ардуинами, но основанный не на радио, а на свете.

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

Но для себя я практического применения так и не нашёл. :)

ЕвгенийП
ЕвгенийП аватар
Offline
Зарегистрирован: 25.05.2015

negavoid пишет:
Например, ставим
возле своего дома светодиодный уличный фонарь и в определенное время мигаем им на все секреты, а шпион дядя Вася в это время вдоль улицы с собачкой прогуливается. Никаких прямых контактов, информация передана :)

negavoid
Offline
Зарегистрирован: 09.07.2016

Ага :)) Или наоборот, защищаем секреты, 433 МГц и 2.4 ГГц прорвутся сквозь стены комнаты без окон к шпиёну, а свет точно нет.