2017-06-26

ASK Baseband Decoder (433.92MHz)

/*
ASK Baseband Decoder  Copyright (C) 2017  Marcus Andersson

This program comes with ABSOLUTELY NO WARRANTY.
This is free software, and you are welcome to redistribute it
under certain conditions; GPLv3.

This is a proof of concept 433MHz ASK baseband decoder for messages sent by
remote controls, weather stations, car keys and so on. It is written for Arduino,
but the algorithm can run on anything that has GPIO IN with interrupt and a
microsecond time function.

You need a 433Mhz receiver. There are plenty of cheap receivers on the market [1].
A good, compact antenna that I recommend is the DIY coil loaded antenna [2].

|   Receiver       Arduino
8  ----------     -----------
+--|Ant Data|-----|D2     TX|--- Serial Out
   |      V+|-----|5V    Raw|--- 5-12V
   |      V-|-----|Gnd   Gnd|--- Gnd
   ----------     -----------

Connect the data out from the 433MHz receiver to digital 2 on Arduino. Upload
this program using the Arduino IDE. Open a serial terminal set to 115200 bps
to start message reception. The output format is similar to homeduino [3],
with a list of microsecond intervals followed by the message which consists
of indexes referencing the list.

Without an ongoing transmission, the receiver will pick up noise. We want
to ignore noise and only detect proper messages. A proper message consists
of a sequence of high/low signal pairs. The signals varies between 1 to N
periods in length. A period is around 300 to 600 microseconds.

1_  _
0_ | |_.       = 11

1_  _
0_ | |_._.     = 12

1_  _._
0_ |   |_._._. = 23

...and so on.

A low signal that is longer than N periods is a sync. The high signal sets
the period time for the message. A sync is sent before and after a message.
The sync signal can be shared by two adjacent messages, which means it marks
both the end of the first message and the start of the next.

1_  _
0_ | |_._._._._._._._._._._._._._._._._._._._._._. = sync

When a sync signal is detected the message recording starts. As long as no signal
has a shorter duration than half a period, the reception continues until a new sync
signal is detected. There is a minimum length for a proper message and there is
also a minimum period time. This lowers the risk of interpreting noise as proper messages.

Incoming messages are written to a circular buffer by the interrupt routine. If
the reception buffer becomes full, the message being received is discarded. When
a complete message has been received the writer index is advanced to the position
after the message and the main loop can start to consume the message using the reader
index. The first datum in the buffer is the period time in microseconds. The following
data is the number of periods for all signals. The main loop transmits the message over
the serial port until (reader == writer) or until the number of periods of a datum is
larger than N, which means that a new message starts.

References
----------
[1] https://www.electrokit.com/rx433n-mottagarmodul-ask-433-9-mhz.45095
[2] https://arduinodiy.wordpress.com/2015/07/25/coil-loaded-433-mhz-antenna
[3] https://github.com/pimatic/homeduino
*/

// Circular buffer length
// Must be 256 for modulo arithmetic to work on byte index variables without using %.
#define BUF_LEN 256

// Noise filter
#define MIN_MSG_LEN 16

// Max length of data signal. Longer signals are treated as sync.
#define MAX_SIGNAL_PERIODS 20

// Minimum signal period time for a proper message
#define MIN_PERIOD_TIME 30 // * 4 microseconds

// Remembers the time of the last interrupt
volatile unsigned int lastTime;

// Signal period time of message being received
volatile unsigned int periodTime;

// Signal counter for message being received
volatile byte streak;

// Buffer pointer where the next message will be stored
volatile byte writer;

// Buffer pointer for the main loop reader
volatile byte reader;

// Circular message buffer
volatile unsigned int msgbuf[BUF_LEN];

void setup()
{
  Serial.begin(115200);
  // Scale down time by 4 to fit in 16 bit unsigned int
  lastTime = micros() / 4;
  periodTime = 1;
  writer = 0;
  reader = 0;
  streak = 0;
  pinMode(2, INPUT_PULLUP);
  attachInterrupt(0, isr, CHANGE);
}

void writeNum(unsigned long num, char* tail)
{
  char buf[10];
  String( num ).toCharArray(buf, 10);
  Serial.write(buf);
  Serial.write(tail);
}

unsigned int insertSort(unsigned int list[8], unsigned int val)
{
  byte i;
  for (i = 0; i < 8; i++) {
    if (list[i] == val) {
      // No duplicates
      val = list[7];
      break;
    }
    if (list[i] > val) {
      unsigned int tmp = list[i];
      list[i] = val;
      val = tmp;
    }
  }
  return val;
}

byte listpos(unsigned int list[8], unsigned int val)
{
  byte i;
  for (i = 0; i < 8; i++) {
    if (list[i] == val) {
      break;
    }
  }
  return i;
}

void loop()
{
  while (reader != writer) {
    unsigned int periodMap[8] = {-1, -1, -1, -1, -1, -1, -1, -1};
    unsigned int pt = msgbuf[reader++];
    byte prereader = reader;
    byte i;

    while (prereader != writer) {
      unsigned int periods = msgbuf[prereader++];
      if (insertSort(periodMap, periods) != -1) {
        // Too many different signals
        reader = writer;
        break;
      }
      if (periods > MAX_SIGNAL_PERIODS) {
        // End of message
        break;
      }
    }
  
    if (reader != writer) {
      for (i = 0; i < 8; i++) {
        if (periodMap[i] != -1) {
          writeNum(periodMap[i]*pt*4, " ");
        }
        else {
          writeNum(0, " ");
        }
      }
    }
  
    while (reader != writer) {
      unsigned int periods = msgbuf[reader++];
      writeNum(listpos(periodMap, periods), "");
      if (periods > MAX_SIGNAL_PERIODS) {
        // End of message
        Serial.write("\n");
        break;
      }
    }
  }
}

void isr()
{
  // Scale down time by 4 to fit in 16 bit unsigned int
  unsigned int now = micros() / 4;
  unsigned int signalTime = now - lastTime;
  unsigned int periods = (signalTime + periodTime/2) / periodTime;
  byte lowSignal = digitalRead(2);

  lastTime = now;
 
  if (periods == 0) {
    // Noise, ignore message
    streak = 0;
  }
  if (streak > 0) {
    // Receive message
    byte index = (writer + streak++); // % 256
    if (index == reader) {
      // Reception buffer is full, drop message
      streak = 0;
    }
    else {
      msgbuf[index] = periods;
    }
  }

  if (lowSignal) {
    if (periodTime > MIN_PERIOD_TIME && periods > MAX_SIGNAL_PERIODS) {
      // Sync detected
      if (streak > MIN_MSG_LEN) {
        // Message complete
        msgbuf[writer] = periodTime;
        writer = (writer + streak); // % 256
      }
      // Start new message
      streak = 1;
    }
  }
  else {
    // high signal
    if (periods > MAX_SIGNAL_PERIODS) {
      // Noise, ignore message
      streak = 0;
    }
    if (streak > 0) {
      if (periods == 1) {
        // Approximate average of single period high signals in message
        periodTime = (periodTime*streak + 2*signalTime) / (streak + 2);
      }
    }
    else {
      // Initiate search for new period time and sync
      periodTime = signalTime;
    }
  }
}