Morse code telegraph decoder on an Arduino Uno
Tap Morse code on a button and an LCD spells out the message. The sketch learns your speed, tells dots from dashes by length, and recognizes an SOS. The whole build, an Arduino Uno and 3 more parts, runs here in your browser on the firmware below; open it in the editor to change the wiring or the code and run it again.
Teaching? Assign it to your class in one click.
Intermediate About 20 minutes to build and run. Runs in your browser. Free, and no account needed.
The parts retail for about $32. On Mokxi they're free.
What you'll learn
- Sound
- Controllers
- Displays
- Reading a button
- Difficulty: Intermediate
- Time: About 20 minutes
- Board: Arduino Uno R3
- Parts: 4
How it works
Tap Morse code on a button and an LCD spells out the message. The sketch learns your speed, tells dots from dashes by length, and recognizes an SOS.
Arduino Uno R3 runs the sketch below. The circuit around it: Pushbutton, Piezo speaker, Character LCD with I2C backpack. pinMode sets KEY as an input with its pull-up on. digitalRead reads whether KEY is HIGH or LOW. Serial.begin opens the serial port at 115200 baud. tone plays a note on PIEZO. millis reads the time since power-up, so the sketch can time things without stopping.
Parts list
4 parts, plus the jumper wires. They retail for about $32; on Mokxi they're free.
- 1 × Arduino Uno R3In Starter Kit, Claw Machine Kit, Vending Machine Kit.
- 1 × PushbuttonIn Starter Kit, Christmas Lights Kit, Logic Lab Kit.
- 1 × Piezo speakerIn Starter Kit, Claw Machine Kit, Vending Machine Kit.
- 1 × Character LCD with I2C backpackIn Claw Machine Kit, Vending Machine Kit, Rhythm Game Kit.
Kits that can build it
These 3 starter kits have every part in the circuit. Each kit's page lists what else it can build.
Wiring table
Every connection, pin by pin, read from the circuit itself. Each row joins the pin on the left to the pin on the right, by a jumper wire or a breadboard strip.
| Part and pin | Connects to |
|---|---|
| Piezo speaker pin 1 | Arduino Uno R3 pin 9 |
| Pushbutton pin 1a | Arduino Uno R3 pin 2 |
| Character LCD with I2C backpack pin VCC | Arduino Uno R3 pin 5V |
| Character LCD with I2C backpack pin GND | Arduino Uno R3 pin GND |
| Character LCD with I2C backpack pin SDA | Arduino Uno R3 pin A4 |
| Character LCD with I2C backpack pin SCL | Arduino Uno R3 pin A5 |
| Pushbutton pin 2b | Ground |
| Piezo speaker pin 2 | Ground |
The code
The firmware exactly as the editor opens it. Change a line there and press Run: it compiles in the browser.
// Morse code telegraph decoder: one button, a piezo and an I2C LCD on an Uno.
//
// key (button) pin 2 to GND
// piezo pin 9 and GND (the sidetone, so you can hear yourself)
// LCD 16x2 I2C on A4 and A5, address 0x27
//
// Tap the key. A short press is a dot, a long one a dash. Pause for a moment
// and the letter is finished; pause longer and the word is. The LCD shows the
// last 16 letters on the top line and the dots and dashes so far on the
// bottom line. Send ... --- ... (S O S) and the sketch says so.
//
// # Nobody taps at the same speed
//
// A dash is three times as long as a dot, but how long a dot is depends on
// the person. So nothing here is a fixed number of milliseconds. The sketch
// keeps `unit`, its idea of one dot, starting at 200 ms. A press shorter than
// two units is a dot, anything longer a dash, and after every press the unit
// is moved halfway to what was just tapped (a dot's length, or a third of a
// dash's). Tap faster and `unit` follows within a few presses. The pauses use
// it too: a gap over two units ends a letter, over five ends a word, halfway
// between the lengths a pause can be.
//
// # A letter is a walk down a tree
//
// E is a dot and T is a dash; A is dot-dash and N is dash-dot. Every letter
// is a path of dots and dashes from the start, and the alphabet makes a
// binary tree. Number the nodes so the start is 1, a dot goes from node n to
// 2n and a dash to 2n + 1, and the whole alphabet fits in one 64-letter
// string: index 1 is the start, 2 is E, 3 is T, 5 is A (dot, dash) and so on.
// Decoding is then just doubling a number, with no list of codes to search.
#include <Wire.h>
#include <LiquidCrystal_I2C.h>
const uint8_t KEY = 2;
const uint8_t PIEZO = 9;
const int SIDETONE = 650;
// the tree, indexed as described above; * marks a path that is not a letter
const char TREE[65] = "**ETIANMSURWDKGOHVF*L*PJBXCYZQ**54*3***2*******16*******7***8*90";
LiquidCrystal_I2C lcd(0x27, 16, 2);
unsigned long unit = 200; // ms in one dot
uint8_t node = 1; // where in the tree the current letter is
char symbols[8]; // the dots and dashes so far
uint8_t symbolCount = 0;
char text[17] = ""; // the last letters, newest at the end
uint8_t textLen = 0;
char lastThree[4] = "";
bool keyDown = false;
bool reading = false;
unsigned long changedAt = 0; // when the stable state last changed
unsigned long pressedAt = 0;
unsigned long releasedAt = 0;
bool letterOpen = false; // a letter is waiting for its gap
bool wordOpen = false; // a word is waiting for its gap
void showLine() {
lcd.setCursor(0, 0);
lcd.print(text);
for (int i = textLen; i < 16; i++) lcd.print(' ');
lcd.setCursor(0, 1);
for (int i = 0; i < symbolCount; i++) lcd.print(symbols[i]);
for (int i = symbolCount; i < 16; i++) lcd.print(' ');
}
void addChar(char c) {
if (textLen == 16) {
for (int i = 0; i < 15; i++) text[i] = text[i + 1];
textLen = 15;
}
text[textLen++] = c;
text[textLen] = 0;
// the last three characters, for spotting SOS
lastThree[0] = lastThree[1];
lastThree[1] = lastThree[2];
lastThree[2] = c;
lastThree[3] = 0;
}
void finishLetter() {
char c = node < 64 ? TREE[node] : '*';
if (c == '*') c = '?';
addChar(c);
Serial.print("letter: ");
Serial.println(c);
node = 1;
symbolCount = 0;
letterOpen = false;
showLine();
if (lastThree[0] == 'S' && lastThree[1] == 'O' && lastThree[2] == 'S') {
Serial.println("SOS received: help is on the way");
lcd.setCursor(0, 1);
lcd.print("SOS! HELP COMING");
lastThree[0] = 0;
}
}
void finishWord() {
addChar(' ');
Serial.println("word end");
wordOpen = false;
showLine();
}
void pressEnded(unsigned long length) {
bool dash = length >= 2 * unit;
if (symbolCount < 6) symbols[symbolCount++] = dash ? '-' : '.';
if (node < 32) node = node * 2 + (dash ? 1 : 0);
else node = 64; // too long to be a letter
// move the unit halfway to this press, a dot's length or a third of a dash's
unsigned long sample = dash ? length / 3 : length;
unit = (unit + sample) / 2;
unit = constrain(unit, 40UL, 600UL);
Serial.print(dash ? "dash " : "dot ");
Serial.print(length);
Serial.print(" ms, unit ");
Serial.println(unit);
letterOpen = true;
wordOpen = true;
// Only the one new symbol is drawn: redrawing both rows takes the LCD tens
// of milliseconds, and the loop must be back to watching the key by then.
if (symbolCount > 0) {
lcd.setCursor(symbolCount - 1, 1);
lcd.print(symbols[symbolCount - 1]);
}
}
void setup() {
Serial.begin(115200);
pinMode(KEY, INPUT_PULLUP);
pinMode(PIEZO, OUTPUT);
Wire.setClock(400000);
lcd.init();
lcd.backlight();
showLine();
Serial.println("morse decoder ready: tap the key");
}
void loop() {
unsigned long now = millis();
// debounce: a new reading counts once it has held for 30 ms, long enough to ride out the contact bounce
bool raw = digitalRead(KEY) == LOW;
if (raw != reading) {
reading = raw;
changedAt = now;
}
if (reading != keyDown && now - changedAt >= 30) {
keyDown = reading;
if (keyDown) {
pressedAt = now;
tone(PIEZO, SIDETONE);
} else {
noTone(PIEZO);
releasedAt = now;
pressEnded(now - pressedAt);
}
}
// the gaps: more than two units end a letter (a gap is one or three), more
// than five end a word (three or seven): halfway between the lengths it can be
if (!keyDown) {
unsigned long quiet = now - releasedAt;
if (letterOpen && quiet > 2 * unit) finishLetter();
if (wordOpen && !letterOpen && quiet > 5 * unit) finishWord();
}
}
Code walkthrough
The key functions of the sketch, and what each one does.
setup sketch.ino
Runs once when the board starts. It gets the pins and libraries ready.
void setup() {
Serial.begin(115200);
pinMode(KEY, INPUT_PULLUP);
pinMode(PIEZO, OUTPUT);
Wire.setClock(400000);
lcd.init();
lcd.backlight();
showLine();
Serial.println("morse decoder ready: tap the key");
}loop sketch.ino
Runs over and over for as long as the board has power. This is where the project does its work.
void loop() {
unsigned long now = millis();
// debounce: a new reading counts once it has held for 30 ms, long enough to ride out the contact bounce
bool raw = digitalRead(KEY) == LOW;
if (raw != reading) {
reading = raw;
changedAt = now;
}
if (reading != keyDown && now - changedAt >= 30) {
keyDown = reading;
if (keyDown) {
pressedAt = now;
tone(PIEZO, SIDETONE);
} else {
noTone(PIEZO);
releasedAt = now;
pressEnded(now - pressedAt);
}
}
// the gaps: more than two units end a letter (a gap is one or three), more
// than five end a word (three or seven): halfway between the lengths it can be
if (!keyDown) {
unsigned long quiet = now - releasedAt;
if (letterOpen && quiet > 2 * unit) finishLetter();
if (wordOpen && !letterOpen && quiet > 5 * unit) finishWord();
}
// ... 1 more lines in the editorTroubleshooting
The usual mistakes with this circuit, and the fix for each.
- The input reads random HIGH and LOW when the button is not pressed. Pushbutton
A floating pin picks up noise. Add a 10 k pull-down or pull-up resistor, or use INPUT_PULLUP and wire the button between the pin and GND. Then pressed reads LOW. - The button seems always pressed, or never changes. Pushbutton
A 4-leg tactile switch has legs joined in pairs. Place it across the breadboard center gap so the pairs sit on different rows. Wired along one side, the legs are already connected. - A passive piezo stays silent when you just set the pin HIGH. Piezo speaker
A passive piezo needs a changing signal. Use tone(pin, frequency) or a PWM wave. Around 2 to 4 kHz is loudest. A buzzer with a built in oscillator makes sound from plain DC instead. - Sound is very quiet or the pin seems overloaded. Piezo speaker
Drive the piezo from a pin directly, or through a 100 ohm resistor. For a bigger buzzer, switch it with a transistor. Check that you wired the right two legs and joined grounds. - The screen stays blank or shows only a row of blocks. Character LCD with I2C backpack
The I2C address is usually 0x27 or 0x3F. Run an I2C scanner to find yours. Then turn the contrast trimmer on the backpack slowly until text appears. - The backlight is on but there is no text, or text is garbled. Character LCD with I2C backpack
Wire SDA and SCL to the right pins (A4 and A5 on an Uno), power it from 5 V, and call lcd.init() and the backlight call in setup. - Upload fails or nothing runs after wiring pins 0 and 1 to a module. Arduino Uno R3
Pins 0 and 1 are the USB serial port. Move the module to other pins, or unplug it while uploading. Also check the right board is selected. - Pins misbehave or the board resets when you power motors or servos from it. Arduino Uno R3
Each I/O pin gives about 20 mA, 40 mA max. Power motors and servos from their own supply and join its ground to the Uno's GND.
Change it and keep it
Open it in the editor, change the circuit or the code, and keep your version in a free account.