Arduino Morse Code Telegraph: Build a Sender and a Key Decoder
Build a working Morse code telegraph with an Arduino Uno, a push button, a piezo buzzer and an LED. Sketch 1 turns text you type into Morse sound and light. Sketch 2 reads your key presses and prints the letters. Both follow the ITU timing rules, and we tested them end to end: the decoder reads back everything the sender sends, including numbers.
What You Will Build
Morse sender
Type a message in the Arduino Serial Monitor and the board sends it as a 600 Hz tone and a flashing LED at 15 WPM.
Key decoder
Press a button or a real straight key. The board times each press, decides dot or dash, and prints the decoded text.
A modern telegraph
Together they repeat what Samuel Morse and Alfred Vail did in 1844: a key, a timed signal and a receiver that turns it back into letters.
Parts You Need
| Part | Qty | Notes |
|---|---|---|
| Arduino Uno R3 (or Nano) | 1 | Any 5 V board that supports tone() |
| Passive piezo buzzer | 1 | Needed for tone(). An active buzzer plays one fixed pitch |
| LED | 1 | Or use the built-in LED on pin 13 |
| 220 Ω resistor | 1 | Limits current through the LED |
| Momentary push button | 1 | Or a straight key with two terminals |
| Breadboard and jumper wires | 1 set | Male-to-male wires are enough |
Total cost is usually the price of the Arduino plus a few small parts from any starter kit.
Wiring
| Component | Arduino pin | Other side |
|---|---|---|
| Buzzer + | D8 | Buzzer − to GND |
| LED long leg (through 220 Ω) | D13 | LED short leg to GND |
| Push button or key | D2 | Other leg to GND |
The button uses INPUT_PULLUP, so pin 2 reads HIGH when the button is open and LOW when it is pressed. No external resistor is needed. Sketch 1 needs only the buzzer and LED; sketch 2 uses all three.
Sketch 1: Text to Morse Code Sender
Upload this sketch, open the Serial Monitor at 9600 baud, set the line ending to Newline, type a message and press Enter. Letters and numbers are sent; other characters are skipped.
// QuickMorse: Arduino Morse code sender
// Type a message in the Serial Monitor (9600 baud) and press Enter.
const int LED_PIN = 13; // LED with 220 ohm resistor, or the built-in LED
const int BUZZER_PIN = 8; // passive piezo buzzer
const int TONE_HZ = 600; // sidetone pitch in hertz
const int WPM = 15; // sending speed in words per minute
const unsigned long UNIT = 1200UL / WPM; // one unit in milliseconds (80 ms at 15 WPM)
// Morse code for A to Z, then 0 to 9 (ITU-R M.1677-1)
const char* LETTERS[] = {
".-", "-...", "-.-.", "-..", ".", "..-.", "--.", "....", "..", ".---",
"-.-", ".-..", "--", "-.", "---", ".--.", "--.-", ".-.", "...", "-",
"..-", "...-", ".--", "-..-", "-.--", "--.."
};
const char* DIGITS[] = {
"-----", ".----", "..---", "...--", "....-",
".....", "-....", "--...", "---..", "----."
};
void setup() {
pinMode(LED_PIN, OUTPUT);
Serial.begin(9600);
Serial.println("Type a message and press Enter.");
}
// One dot (1 unit) or dash (3 units), followed by the 1-unit gap inside a letter
void keyDown(unsigned long units) {
digitalWrite(LED_PIN, HIGH);
tone(BUZZER_PIN, TONE_HZ);
delay(units * UNIT);
digitalWrite(LED_PIN, LOW);
noTone(BUZZER_PIN);
delay(UNIT);
}
void sendCode(const char* code) {
for (int i = 0; code[i] != '\0'; i++) {
keyDown(code[i] == '.' ? 1 : 3);
}
delay(2 * UNIT); // 1 + 2 = 3-unit gap between letters
}
void loop() {
if (Serial.available() > 0) {
String msg = Serial.readStringUntil('\n');
msg.toUpperCase();
for (unsigned int i = 0; i < msg.length(); i++) {
char c = msg[i];
if (c >= 'A' && c <= 'Z') {
sendCode(LETTERS[c - 'A']);
} else if (c >= '0' && c <= '9') {
sendCode(DIGITS[c - '0']);
} else if (c == ' ') {
delay(4 * UNIT); // 3 + 4 = 7-unit gap between words
}
}
Serial.print("Sent: ");
Serial.println(msg);
}
}The key line is UNIT = 1200UL / WPM. A dot is 1 unit, a dash 3, the gap inside a letter 1, between letters 3 and between words 7, exactly as in the Morse timing guide.
Sketch 2: Telegraph Key Decoder
This sketch turns the Arduino into a receiver for your own keying. Hold the button for a short beat for a dot and about three times longer for a dash.
// QuickMorse: Arduino Morse code key decoder
// Press the button like a telegraph key. Decoded text appears in the Serial Monitor.
const int KEY_PIN = 2; // push button or straight key to GND (internal pull-up)
const int LED_PIN = 13;
const int BUZZER_PIN = 8; // passive piezo buzzer for sidetone
const int TONE_HZ = 600;
const int WPM = 12; // the speed you key at
const unsigned long UNIT = 1200UL / WPM; // 100 ms at 12 WPM
const unsigned long DEBOUNCE_MS = 15; // ignore contact bounce shorter than this
const char* LETTERS[] = {
".-", "-...", "-.-.", "-..", ".", "..-.", "--.", "....", "..", ".---",
"-.-", ".-..", "--", "-.", "---", ".--.", "--.-", ".-.", "...", "-",
"..-", "...-", ".--", "-..-", "-.--", "--.."
};
const char* DIGITS[] = {
"-----", ".----", "..---", "...--", "....-",
".....", "-....", "--...", "---..", "----."
};
char pattern[8]; // dots and dashes of the current letter
int len = 0;
bool keyWasDown = false;
bool spaceSent = true;
unsigned long changedAt = 0;
void setup() {
pinMode(KEY_PIN, INPUT_PULLUP);
pinMode(LED_PIN, OUTPUT);
Serial.begin(9600);
Serial.println("Start keying...");
}
char decode(const char* p) {
for (int i = 0; i < 26; i++) {
if (strcmp(p, LETTERS[i]) == 0) return 'A' + i;
}
for (int i = 0; i < 10; i++) {
if (strcmp(p, DIGITS[i]) == 0) return '0' + i;
}
return '?';
}
void loop() {
bool keyDown = (digitalRead(KEY_PIN) == LOW);
unsigned long now = millis();
if (keyDown != keyWasDown) {
if (now - changedAt < DEBOUNCE_MS) return;
unsigned long held = now - changedAt;
if (keyDown) { // key pressed: start the tone
tone(BUZZER_PIN, TONE_HZ);
digitalWrite(LED_PIN, HIGH);
} else { // key released: was it a dot or a dash?
noTone(BUZZER_PIN);
digitalWrite(LED_PIN, LOW);
if (len < 7) pattern[len++] = (held < 2 * UNIT) ? '.' : '-';
}
keyWasDown = keyDown;
changedAt = now;
spaceSent = false;
return;
}
if (!keyDown) {
unsigned long gap = now - changedAt;
if (len > 0 && gap > 2 * UNIT) { // gap longer than 2 units: letter finished
pattern[len] = '\0';
Serial.print(decode(pattern));
len = 0;
} else if (len == 0 && !spaceSent && gap > 5 * UNIT) { // longer than 5 units: word finished
Serial.print(' ');
spaceSent = true;
}
}
}The decoder measures time instead of listening for exact lengths. A press shorter than 2 units is a dot, longer is a dash. A pause longer than 2 units ends a letter, and longer than 5 units adds a space. Those thresholds sit halfway between the ideal values, so normal human timing still decodes.
Morse Timing Used in the Code
| Speed | One unit (dot) | Dash | Letter gap | Word gap |
|---|---|---|---|---|
| 5 WPM | 240 ms | 720 ms | 720 ms | 1680 ms |
| 10 WPM | 120 ms | 360 ms | 360 ms | 840 ms |
| 12 WPM | 100 ms | 300 ms | 300 ms | 700 ms |
| 15 WPM | 80 ms | 240 ms | 240 ms | 560 ms |
| 20 WPM | 60 ms | 180 ms | 180 ms | 420 ms |
| 25 WPM | 48 ms | 144 ms | 144 ms | 336 ms |
Beginners usually key at 10 to 13 WPM. If you are slower than the sketch expects, your dots start reading as dashes, so set WPM in sketch 2 to match yourself.
Troubleshooting
No sound
Check that the buzzer is passive and on pin 8. On the Uno, tone() also disables PWM on pins 3 and 11, so do not use those pins for the buzzer.
Letters come out as ?
Your keying speed and the WPM setting do not match. Slow down, or lower WPM in sketch 2.
Extra dots appear
Cheap buttons bounce. Raise DEBOUNCE_MS from 15 to 25.
Nothing in the Serial Monitor
Set the baud rate to 9600 and the line ending to Newline, then press the reset button on the board.
From the 1844 Telegraph to Arduino
Washington to Baltimore, 1844
On 24 May 1844 Morse sent “What hath God wrought” over an experimental line between Washington, D.C. and Baltimore. Read more in the history of Morse code.
From paper tape to sound
Early receivers marked dots and dashes on paper tape. Operators soon learned to read the clicks of the sounder by ear, which is what sketch 2 imitates with timing.
Same code, new hardware
Your Arduino uses the international code standardised by the ITU, the version still used by radio amateurs today.
Frequently Asked Questions
Can I use an active buzzer instead of a passive one?
Yes, but replace tone() and noTone() with digitalWrite(BUZZER_PIN, HIGH) and LOW. An active buzzer has its own oscillator, so it beeps at one fixed pitch.
How do I change the Morse code speed on Arduino?
Change the WPM constant. The sketch calculates one unit as 1200 divided by WPM, so 15 WPM gives 80 ms per dot and 240 ms per dash.
Why does the decoder print question marks?
A ? means the pattern did not match any letter. Usually you are keying much faster or slower than the WPM set in the sketch. Set WPM close to your real speed.
Can I connect a real telegraph key to the Arduino?
Yes. A straight key is just a switch: connect one terminal to pin 2 and the other to GND. The internal pull-up resistor does the rest.
Can this Arduino sketch decode Morse code from audio?
No. The decoder reads a key or button. To decode Morse from a microphone or a recording, use the QuickMorse audio decoder.
What is the PARIS standard in Morse code timing?
The word PARIS is exactly 50 units long, so sending it once per minute is 1 WPM. That is where the formula unit = 1200 / WPM milliseconds comes from.
