Project tutorial

Adaptive LED Morse Code Decoder and Timer Interrupt © MIT

Morse Code decoder that can adjusts to LED bright level and demonstrates how to implement timer interrupt.

  • 5 views
  • 0 comments
  • 0 respects

Components and supplies

About this project

Teaching Morse Code

Morse code is easy to understand (although hard to remember). Someone who just finished her/his first Arduino Blink project can easily write Morse code using LED.

It is a magical experience when other Arduino understands messages delivered in series of blinks that you wrote in your code.

Morse code units

Morse code has dot (1 unit), dash (3 units), inter-element gap (1 unit), letter gap (3 units), and space (7 units).

For example, A can be understood as dot, inter-element gap, dash, and letter gap. Assuming 100ms as a unit for Morse code, this is how one can send "A" in Morse code in loop:

void setup() {
    pinMode(13, OUTPUT);
}
void loop() {
    // dot
    digitalWrite(13, HIGH);
    delay(100);
    // inter-element gap
    digitalWrite(13, LOW);
    delay(100);
    // dash
    digitalWrite(13, HIGH);
    delay(300);
    // letter gap
    digitalWrite(13, LOW);
    delay(300);
}

Decoder components

For decoder, you need a photo resistor (LDR) and 1K ohm resistor to make voltage divider. The analog input A0 is used to read the voltage level.

A photo resistor is used here because it is cheap and available in most of Arduino learning kit.

Photo resistor as Morse code sensor

A photo resistor is designed to read level of ambient light and using this to read LED light level means it will be impacted by other ambient light level and reading will vary significantly.

I've built a decoder code that can dynamically adjust its light level using adaptive logic. The code is designed to read max/min value of light reading for period of time and it will adjust cut-off value dynamically every so often.

I've called this as adaptive logic-level processor. You can find the corresponding class from the source code.

While this code is doing its best to adjust dynamically, it cannot solve all situation when simply light level difference is too low. So when running this experiment, make sure you get your LED closer to the photo resistor.

Timer Interrupt to read sensor

Reading sensor value in loop is ok as long as your loop is not doing too much other work. I've opted to use timer interrupt to read sensor at every 1ms interval reliably regardless how much work is done in loop.

This requires a buffer to store the signal so that loop can consume the signal when it is ready.

Classes in Decoder program

I've decided to write classes for the decoder. This allows more organized code while increased length of code due to some boilerplate code. I'm not C++ coder usually, so excuse me on any mistakes of not following coding standards.

There are four classes:

  • AdaptiveLogicLevelProcessor is responsible for dynamically adjusting logic level
  • MorseCodeElementProcessor is responsible for detecting individual Morse code signal.
  • MorseCodeProcessor is responsible for decoding Morse
  • MorseCodeBuffer is responsible for buffering Morse code so that interrupt routine can share the code to loop.

Morse code tree

Morse code decoding conceptually just navigating binary tree. For example, from [Start], dot would go to [E]. And dash would go to [A]. And having a 3 unit gap would complete decoding, so it will return [A].

The decoder uses this binary tree encoded as an array and navigate this tree based on incoming signal.

See https://en.wikipedia.org/wiki/Morse_code for more information about Morse code.

When testing

As photo sensor is sensitive to ambient light, make sure you bring the photo sensor and LED closely and point them each other. Or you can cover them in a paper cup together so that it is less impacted by ambient light.

Give the decoder some time to adjust to the light level to start output.

You can change UNIT_LENGTH to something smaller if you want to send code faster.

Code

DecoderArduino
// Configuration
// Minimum tested unit length was 10ms and it works reliably with cheap light resistor.
const int UNIT_LENGTH = 100;
const int BUFFER_SIZE = 5;


enum class Signal: byte {
  NOISE = 0,
  DIT = 1,
  DAH = 2,
  ELEMENTGAP = 3,
  GAP = 4,
  LONGGAP = 5
};

struct MorseCodeElement {
  Signal m_signal;
  unsigned long m_duration;
};

class MorseCodeBuffer {
  int m_size;
  int m_head;
  int m_tail;
  MorseCodeElement* m_buffer;
  
public:
  MorseCodeBuffer(int size) {
    // Use extra element to distinguish empty vs full.
    size++;
    
    m_size = size;
    m_head = 0;
    m_tail = 0;
    m_buffer = new MorseCodeElement[size];
  }
  
  bool Enqueue(MorseCodeElement element) {    
    int new_tail = (m_tail + 1) % m_size;
    
    // Is full?
    if (new_tail == m_head) {
      return false;
    }
    
    m_tail = new_tail;
    m_buffer[m_tail] = element;
    
    return true;
  }
  
  bool TryDequeue(MorseCodeElement* element) {
    // Is empty?
    if (m_head == m_tail) {
      return false;
    }
    
    *element = m_buffer[m_head];
    m_head = (m_head + 1) % m_size;
    return true;
  }
  
  int GetCount() {
      if (m_head == m_tail) {
        return 0;
      }
    
      return (m_tail - m_head + m_size) % m_size;
  }
};

class AdaptiveLogicLevelProcessor {
  int m_sensorMinValue = 1023;
  int m_sensorMaxValue = 0;
  int m_sensorMedianValue = 511;
  unsigned long m_sensorCalibrationTime = 0;
  bool m_calibrated;

public:
  AdaptiveLogicLevelProcessor() {
    m_sensorMinValue = 1023;
    m_sensorMaxValue = 0;
    m_sensorMedianValue = 511;
    m_sensorCalibrationTime = 0;
  }

  bool process(int sensorValue, int* digitalInputValue) {
    unsigned long currentTime = millis();
  
    // Re-calibrate sensor value range
    if (currentTime - m_sensorCalibrationTime > 5000) {
      if (m_sensorMinValue < m_sensorMaxValue) {
  
        if (m_sensorMaxValue - m_sensorMinValue > 20) {
          m_sensorMedianValue = m_sensorMinValue + (m_sensorMaxValue - m_sensorMinValue) / 2;
          m_calibrated = true;
        } else {
          Serial.println();
          Serial.print("Unreliable LOW/HIGH: ");
          Serial.print(m_sensorMinValue);
          Serial.print(' ');
          Serial.print(m_sensorMaxValue);
          Serial.println();
          m_calibrated = false;
        }
      }
  
       m_sensorMaxValue = 0;
       m_sensorMinValue = 1023;
       m_sensorCalibrationTime = currentTime;
    }
    
    if (m_sensorMinValue > sensorValue) {
      m_sensorMinValue = sensorValue;
    }

    if (m_sensorMaxValue < sensorValue) {
      m_sensorMaxValue = sensorValue;
    }
    
    if (!m_calibrated) {
      return false;
    }
    
    *digitalInputValue = sensorValue > m_sensorMedianValue ? HIGH : LOW;
    return true;
  }
};

class MorseCodeElementProcessor {
  unsigned long m_previousTime = 0;
  int m_previousSignal = LOW;
  
  int m_oneUnitMinValue;
  int m_oneUnitMaxValue;
  int m_threeUnitMinValue;
  int m_threeUnitMaxValue;
  int m_sevenUnitMinValue;
  int m_sevenUnitMaxValue;

public:
  MorseCodeElementProcessor(int unitLengthInMilliseconds) {
    m_oneUnitMinValue = (int)(unitLengthInMilliseconds * 0.5);
    m_oneUnitMaxValue = (int)(unitLengthInMilliseconds * 1.5);
    
    m_threeUnitMinValue = (int)(unitLengthInMilliseconds * 2.0);
    m_threeUnitMaxValue = (int)(unitLengthInMilliseconds * 4.0);

    m_sevenUnitMinValue = (int)(unitLengthInMilliseconds * 5.0);
    m_sevenUnitMaxValue = (int)(unitLengthInMilliseconds * 8.0);
  }

  bool process(int newSignal, MorseCodeElement* element) {
    unsigned long currentTime = millis();
    unsigned long elapsed;
    bool shouldBuffer = false;
    
    element->m_signal = Signal::NOISE;
    
    // If previous status was OFF and now it is ON
    if (m_previousSignal == LOW && newSignal == HIGH) {
      elapsed = currentTime - m_previousTime;
      element->m_duration = elapsed;
      
      if (m_sevenUnitMinValue <= elapsed) {
        element->m_signal = Signal::LONGGAP;
        shouldBuffer = true;
      } else if (m_threeUnitMinValue <= elapsed && elapsed <= m_threeUnitMaxValue) {
        element->m_signal = Signal::GAP;
        shouldBuffer = true;
      } else if (m_oneUnitMinValue <= elapsed && elapsed <= m_oneUnitMaxValue) {
        element->m_signal = Signal::ELEMENTGAP;
        shouldBuffer = true;
      } else {
        element->m_signal = Signal::NOISE;
        shouldBuffer = true;
      }
          
      m_previousSignal = HIGH;
      m_previousTime = currentTime;
    } else if (m_previousSignal == HIGH && newSignal == LOW) {
      elapsed = currentTime - m_previousTime;
      element->m_duration = elapsed;
  
      if (m_threeUnitMinValue <= elapsed && elapsed <= m_threeUnitMaxValue) {
        element->m_signal = Signal::DAH;
        shouldBuffer = true;
      } else if (m_oneUnitMinValue <= elapsed && elapsed <= m_oneUnitMaxValue) {
        element->m_signal = Signal::DIT;
        shouldBuffer = true;
      } else {
        element->m_signal = Signal::NOISE;
        shouldBuffer = true;
      }
  
      m_previousSignal = LOW;
      m_previousTime = currentTime;  
    }
    
    return shouldBuffer;
  }
};

class MorseCodeProcessor {
  private:  
    static const int TREE_SIZE = 255;
    static constexpr char tree[TREE_SIZE] = {
      '\0', '\0', '\0', '5', '\0', '\0', '\0', 'H', '\0', '\0', '\0', '4', '\0', '\0', '\0', 'S',
      '\0', '\0', '$', '\0', '\0', '\0', '\0', 'V', '\0', '\0', '\0', '3', '\0', '\0', '\0', 'I',
      '\0', '\0', '\0', '\0', '\0', '\0', '\0', 'F', '\0', '\0', '\0', '\0', '\0', '\0', '\0', 'U',
      '\0', '?', '\0', '\0', '\0', '_', '\0', '\0', '\0', '\0', '\0', '2', '\0', '\0', '\0', 'E',
      '\0', '\0', '\0', '&', '\0', '\0', '\0', 'L', '\0', '"', '\0', '\0', '\0', '\0', '\0', 'R',
      '\0', '\0', '\0', '+', '\0', '.', '\0', '\0', '\0', '\0', '\0', '\0', '\0', '\0', '\0', 'A',
      '\0', '\0', '\0', '\0', '\0', '\0', '\0', 'P', '\0', '@', '\0', '\0', '\0', '\0', '\0', 'W',
      '\0', '\0', '\0', '\0', '\0', '\0', '\0', 'J', '\0', '\'', '\0', '1', '\0', '\0', '\0', '\0',
      '\0', '\0', '\0', '6', '\0', '-', '\0', 'B', '\0', '\0', '\0', '=', '\0', '\0', '\0', 'D',
      '\0', '\0', '\0', '/', '\0', '\0', '\0', 'X', '\0', '\0', '\0', '\0', '\0', '\0', '\0', 'N',
      '\0', '\0', '\0', '\0', '\0', '\0', '\0', 'C', '\0', ';', '\0', '\0', '\0', '!', '\0', 'K',
      '\0', '\0', '\0', '(', '\0', ')', '\0', 'Y', '\0', '\0', '\0', '\0', '\0', '\0', '\0', 'T',
      '\0', '\0', '\0', '7', '\0', '\0', '\0', 'Z', '\0', '\0', '\0', '\0', '\0', ',', '\0', 'G',
      '\0', '\0', '\0', '\0', '\0', '\0', '\0', 'Q', '\0', '\0', '\0', '\0', '\0', '\0', '\0', 'M',
      '\0', ':', '\0', '8', '\0', '\0', '\0', '\0', '\0', '\0', '\0', '\0', '\0', '\0', '\0', 'O',
      '\0', '\0', '\0', '9', '\0', '\0', '\0', '\0', '\0', '\0', '\0', '0', '\0', '\0', '\0'
    };
    
    bool m_error;
    int m_start;
    int m_end;
    int m_index;
    Signal m_previousInput;

    void reset() {
      m_error = false;
      m_start = 0;
      m_end = TREE_SIZE;
      m_index = (m_end - m_start) / 2;
    }
  
  public:
    MorseCodeProcessor() {
      reset();
      m_previousInput = Signal::NOISE;
    }

    bool process(Signal input, char* output) {
      bool completed = false;
      
      if (!m_error && input == Signal::DIT) {
        if (m_start == m_index) {
          m_error = true;
        } else {
          m_end = m_index;
          m_index = m_start + (m_end - m_start) / 2;
        }
      } else if (!m_error && input == Signal::DAH) {
        if (m_end == m_index) {
          m_error = true;
        } else {
          m_start = m_index + 1;
          m_index = m_start + (m_end - m_start) / 2;
        }
      } else if (input == Signal::GAP || input == Signal::LONGGAP) {
        completed = !m_error && tree[m_index] != 0;
        
        if (completed) {
          output[0] = tree[m_index];
          output[1] = '\0';

          if (input == Signal::LONGGAP) {
            output[1] = ' ';
            output[2] = '\0';
          }
        }
        
        reset();
      }

      m_previousInput = input;

      return completed;
    }
};

constexpr char MorseCodeProcessor::tree[];

MorseCodeBuffer buffer(BUFFER_SIZE);
MorseCodeProcessor morseCodeProcessor;
AdaptiveLogicLevelProcessor logicLevelProcessor;
MorseCodeElementProcessor morseCodeElementProcessor(UNIT_LENGTH);


/************************************************************
 * Timer interrupt function to process analog signal
 ************************************************************/
SIGNAL(TIMER0_COMPA_vect) {
  cli();
  
  int digitalInputValue;

  if (logicLevelProcessor.process(analogRead(A0), &digitalInputValue)) {
    MorseCodeElement element;
  
    if (morseCodeElementProcessor.process(digitalInputValue, &element)) {
      buffer.Enqueue(element);
    }
  }

  sei();
}

void setup() {
  Serial.begin(9600);

  // Sets up a timer interrupt to be called for every millisecond
  cli();
  OCR0A = 0xAF;
  TIMSK0 |= _BV(OCIE0A);
  sei();
}

/************************************************************
 * Helper function to dequeue an item from the buffer safely
 ************************************************************/
bool TryDequeueSafe(MorseCodeElement* element) {
  // Dequeue item from the buffer while disabling interrupt
  // so that it doesn't corrupt buffer status
  cli();
  bool result = buffer.TryDequeue(element);
  sei();

  return result;
}

char* output = new char[3];

void loop() {
  MorseCodeElement element;
  
  // Drain buffer
  while (TryDequeueSafe(&element)) {
    if (element.m_signal == Signal::DIT) {
      Serial.print(".");
    } else if (element.m_signal == Signal::DAH) {
      Serial.print("-");
    }
    
    if (morseCodeProcessor.process(element.m_signal, output)) {
      Serial.print('(');
      Serial.print(output);
      Serial.print(')');
    }

    if (element.m_signal == Signal::LONGGAP) {
      Serial.println();
    }
  }
}
EncoderArduino
#define DEBUG

const int UNIT_LENGTH = 100;
const int DIT_LENGTH = UNIT_LENGTH * 1;
const int DAH_LENGTH = UNIT_LENGTH * 3;
const int ELEMENT_GAP = UNIT_LENGTH * 1;
const int SHORT_GAP = UNIT_LENGTH * 2;
const int MEDIUM_GAP = UNIT_LENGTH * 4;

const char* codeset[] = {
  /* ! */ "-.-.--",
  /* " */ ".-..-.",
  /* # */ NULL,
  /* $ */ "...-..-",
  /* % */ NULL,
  /* & */ ".-...",
  /* ' */ ".----.",
  /* ( */ "-.--.",
  /* ) */ "-.--.-",
  /* * */ NULL,
  /* + */ ".-.-.",
  /* , */ "--..--",
  /* - */ "-....-",
  /* . */ ".-.-.-",
  /* / */ "-..-.",
  /* 0 */ "-----",
  /* 1 */ ".----",
  /* 2 */ "..---",
  /* 3 */ "...--",
  /* 4 */ "....-",
  /* 5 */ ".....",
  /* 6 */ "-....",
  /* 7 */ "--...",
  /* 8 */ "---..",
  /* 9 */ "----.",
  /* : */ "---...",
  /* ; */ "-.-.-.",
  /* < */ NULL,
  /* = */ "-...-",
  /* > */ NULL,
  /* ? */ "..--..",
  /* @ */ ".--.-.",
  /* A */ ".-",
  /* B */ "-...",
  /* C */ "-.-.",
  /* D */ "-..",
  /* E */ ".",
  /* F */ "..-.",
  /* G */ "--.",
  /* H */ "....",
  /* I */ "..",
  /* J */ ".---",
  /* K */ "-.-",
  /* L */ ".-..",
  /* M */ "--",
  /* N */ "-.",
  /* O */ "---",
  /* P */ ".--.",
  /* Q */ "--.-",
  /* R */ ".-.",
  /* S */ "...",
  /* T */ "-",
  /* U */ "..-",
  /* V */ "...-",
  /* W */ ".--",
  /* X */ "-..-",
  /* Y */ "-.--",
  /* Z */ "--..",
  /* [ */ NULL,
  /* \ */ NULL,
  /* ] */ NULL,
  /* ^ */ NULL,
  /* _ */ "..--.-"
 };

const char* getCode(char c) {
  // To uppercase if needed
  if ('a' <= c && c <= 'z') {
    c = c - ('a' - 'A');
  }

  if ('!' <= c && c <= '_') {
    return codeset[c - '!'];
  }

  return NULL;
}

void setup() {
  pinMode(LED_BUILTIN, OUTPUT);
  Serial.begin(19200);
}


void dit()
{
#ifdef DEBUG
  Serial.print(".");
#endif
  digitalWrite(LED_BUILTIN, HIGH);
  delay(DIT_LENGTH);
  digitalWrite(LED_BUILTIN, LOW);
  delay(ELEMENT_GAP);
}

void dah()
{
#ifdef DEBUG
  Serial.print("-");
#endif
  digitalWrite(LED_BUILTIN, HIGH);
  delay(DAH_LENGTH);
  digitalWrite(LED_BUILTIN, LOW);
  delay(ELEMENT_GAP);
}

void letter()
{
#ifdef DEBUG
  Serial.print(" ");
#endif
  delay(SHORT_GAP);
}

void space()
{
#ifdef DEBUG
  Serial.println();
#endif
  delay(MEDIUM_GAP);
}

void play(const char * input) {
  int inputLength = strlen(input);

  for (int i = 0; i < inputLength; i++) {
    char c = input[i];

    if (c == ' ') {
      space();
    }
    else {
      const char* code = getCode(c);
      
      if (code == NULL) {
        continue;
      }

      int codeLength = strlen(code);
  
      for (int j = 0; j < codeLength; j++) {
        if (code[j] == '.') {
          dit();  
        }
        else if (code[j] == '-') {
          dah();
        }
      }
      
      letter();  
    }
  }
}

const char* text = "Hello World ";

// the loop function runs over and over again forever
void loop() {
  play(text);
}

Schematics

Decoder
Decoder bb t65o4bfw8d
Encoder
Blink bb znq8pto7cj

Comments

Similar projects you might like

Arduino Bluetooth Basic Tutorial

by Mayoogh Girish

  • 455,615 views
  • 42 comments
  • 240 respects

Home Automation Using Raspberry Pi 2 And Windows 10 IoT

Project tutorial by Anurag S. Vasanwala

  • 285,654 views
  • 95 comments
  • 672 respects

Security Access Using RFID Reader

by Aritro Mukherjee

  • 231,168 views
  • 38 comments
  • 241 respects

OpenCat

Project in progress by Team Petoi

  • 200,118 views
  • 156 comments
  • 1,395 respects
Add projectSign up / Login