Skip to content

support@quartzcomponents.com

Free Shipping Over INR 500

Electronics Projects

Morse Code Transceiver using Arduino Nano and NRF24L01

by RISHABH JANGID 17 Apr 2026 0 Comments

This project implements a wireless Morse code communication system using Arduino Nano and NRF24L01. Morse code encodes text as timed signals using dots and dashes, allowing messages to be transmitted through simple pulses.

The system captures button input, converts it into Morse code, transmits it wirelessly, and displays the decoded text on an OLED screen. It also demonstrates how the NRF24L01 transceiver operates, including SPI-based communication, packet transmission, and switching between transmitter and receiver modes for real-time data exchange.

Components Required


About Components

NRF24L01

The NRF24L01 is a low-power wireless transceiver module designed for short-range communication between microcontrollers. It operates in the 2.4GHz ISM band and enables reliable data transmission using SPI communication.

This module functions as both a transmitter and receiver (transceiver), allowing two identical systems to communicate with each other.

Features and Specifications

  • Operating Voltage: 1.9V to 3.6V (typically 3.3V)
  • Current Consumption: Low power (varies with mode)
  • Frequency Band: 2.4GHz ISM band
  • Data Rate: 250 kbps, 1 Mbps, or 2 Mbps
  • Communication Protocol: SPI (Serial Peripheral Interface)
  • Range: Up to ~100 meters (line-of-sight)
  • Output Power: Programmable (−18 dBm to 0 dBm)
  • Multi-node Capability: Supports multiple devices using addressing system

OLED Display Module

The OLED display module is used to present real-time information such as transmitted and received Morse code. It uses organic light-emitting diodes, allowing high contrast, low power consumption, and clear visibility without backlighting.

It communicates with the microcontroller using the I2C protocol, reducing wiring complexity.

Features and Specifications

  • Operating Voltage: 3.3V to 5V
  • Communication: I2C (SDA, SCL)
  • Display Size: Typically 0.96 inch
  • Resolution: 128 × 64 pixels
  • Power Consumption: Very low
  • Visibility: High contrast, wide viewing angle

Push Buttons

Push buttons provide user input for controlling the system. In this project, they are used to input Morse code and switch between transmission and reception modes.

Features and Specifications

  • Type: Momentary tactile switch
  • Operation: Normally open (NO)
  • Voltage Rating: Suitable for low-voltage circuits (3.3V–5V)
  • Interface: Digital input to microcontroller
  • Debouncing: May require software handling to avoid false triggers

Why Arduino Nano is Used?

  • Small size makes the project compact and portable
  • Low cost and easily available
  • Supports SPI communication required for NRF24L01
  • Enough pins to connect buttons, display, and module
  • Easy to program using Arduino IDE, suitable for all levels

 

Circuit Diagram

Morse Code Translator Circuit Diagram

Connections

nRF24L01 → Arduino Nano

  • VCC → 3.3V
  • GND → GND
  • CE → D9
  • CSN (CS) → D10
  • SCK → D13
  • MOSI → D11
  • MISO → D12
  • IRQ → not connected

OLED (I2C) → Arduino Nano

  • VCC → 5V
  • GND → GND
  • SCL → A5
  • SDA → A4

Buttons

Button 1:

  • One leg → D2
  • Other leg → GND

Button 2:

  • One leg → D3
  • Other leg → GND

Power rails (breadboard)

  • Arduino 5V → breadboard + rail
  • Arduino GND → breadboard – rail
  • OLED powered from 5V rail
  • nRF24 powered from 3.3V pin (not 5V)

Code Explanation

Libraries Used

Arduino · C++
#include <SPI.h>
#include <RF24.h>
#include <Wire.h>
#include <Adafruit_GFX.h>
#include <Adafruit_SH110X.h>
#include "MorseLib.h"

Purpose of Libraries

  • SPI, RF24 → Used for wireless communication with NRF24L01
  • Wire, Adafruit_GFX, Adafruit_SH110X → Used to control the OLED display
  • MorseLib → It is a custom library made for this project that converts morse code (dots and dashes) into letters [Download]


Pin Definitions

Pin Definitions
#define BTN       3    // mode toggle (Send / Receive)
#define MORSE_BTN 2    // Morse code input
  • Button on pin 3 → changes mode (Send / Receive)
  • Button on pin 2 → used to enter Morse code


Timing Rules

Timing Constants
#define DOT_THRESHOLD  200   // ms — short press = dot
#define LETTER_GAP     600   // ms — pause between letters
#define WORD_GAP       1400  // ms — pause triggers word send
  • Short press → dot (.)
  • Long press → dash (-)
  • Small gap → next letter
  • Big gap → send full word


Wireless Module Setup

Radio Initialisation
RF24 radio(9, 10);
const byte addr[6] = "00001";

radio.setAutoAck(false);
radio.setPALevel(RF24_PA_LOW);
radio.setDataRate(RF24_250KBPS);
radio.setChannel(100);
  • Uses pins 9 and 10 for communication
  • Both devices use same address to connect


Display Setup

OLED Initialisation
Adafruit_SH1106G display(128, 64, &Wire, -1);
  • Starts 128×64 OLED display


Important Variables

State Variables
bool isTx = false;              // true = Tx mode, false = Rx mode

char currentSymbol[10] = "";   // stores dots and dashes
char txText[32]       = "";   // message to transmit
char rxText[64]       = "";   // received message

unsigned long lastHbTime = 0;  // heartbeat timer
unsigned long lastRxTime = 0;  // last received signal


Setup Function

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

  pinMode(BTN,       INPUT_PULLUP);
  pinMode(MORSE_BTN, INPUT_PULLUP);

  display.begin(0x3C, true);
  display.clearDisplay();
  display.setCursor(0, 0);
  display.println("I AM Rx");
  display.display();

  if (!radio.begin()) while (1);

  radio.openWritingPipe(addr);
  radio.openReadingPipe(0, addr);
  radio.startListening();
}
  • Starts serial, buttons, display, and wireless module
  • Device starts in receive mode


Mode Switching

TX / RX Toggle
bool currBtn = digitalRead(BTN);

if (prevBtn == HIGH && currBtn == LOW) {
  isTx = !isTx;

  if (isTx) { radio.stopListening(); }
  else       { radio.startListening(); }

  delay(300);
}
  • Press button to switch mode
  • Also changes radio to send or receive


Transmitting Mode

Heartbeat

Heartbeat Packet
char hb[] = "#";
radio.write(hb, sizeof(hb));
lastHbTime = millis();
  • Sends "#" every 300 ms
  • Used to tell receiver that sender is active


Making Letters

Morse Decode
char decoded = MorseLib::decode(currentSymbol);
txText[txIndex++] = decoded;
txText[txIndex]   = '\0';
  • Converts Morse into letter
  • Adds letter to message


Sending Message

Radio Write
radio.write(txText, strlen(txText) + 1);
  • Sends only correct data
  • Avoids random characters

 

Receiving Mode

Receive Data

Radio Read
if (radio.available()) {
  char msg[32] = "";
  radio.read(msg, sizeof(msg));
}


Heartbeat Detection

Presence Check
if (strcmp(msg, "#") == 0) {
  lastRxTime = millis();  // sender is active
}
  • Checks if sender is active


Receiving Messages

Buffer Append
strncat(rxText, msg, sizeof(rxText) - strlen(rxText) - 1);
  • Safely adds message
  • Prevents overflow and errors

Auto Clear

Stale Message Clear
if (millis() - lastRxTime > 3000) {
  rxText[0] = '\0';  // clears old message after 3 s
}
  • Clears old message after some time


Result

The implemented system successfully transmits and receives Morse code wirelessly using the NRF24L01, providing real-time communication between two modules. The OLED display module effectively displays the encoded and decoded messages, ensuring clear and accurate visual feedback.

Real-Life Applications

  • Short-Range Wireless Communication — Enables simple data exchange between two devices without internet or cellular networks.
  • Emergency Signaling Systems — Morse code provides a reliable backup communication method during network failures or disaster situations.
  • Learning and Educational Tool — Helps understand wireless communication, signal encoding/decoding, and embedded system design.
  • Secure Basic Communication — Morse-based input adds a layer of abstraction, making casual interception less intuitive.
  • Remote Control Systems — Can be adapted to send command signals wirelessly for controlling devices.
  • Low-Power Communication Devices — Suitable for battery-operated systems where efficient power usage is required.

Checkout the Full Video Tutorial :

 

Code

Complete Sketch
#include <SPI.h>
#include <RF24.h>
#include <Wire.h>
#include <Adafruit_GFX.h>
#include <Adafruit_SH110X.h>
#include "MorseLib.h"

#define BTN            3
#define MORSE_BTN      2
#define DOT_THRESHOLD  200
#define LETTER_GAP     600
#define WORD_GAP       1400

RF24 radio(9, 10);
const byte addr[6] = "00001";
Adafruit_SH1106G display(128, 64, &Wire, -1);

// ---- STATE ----
bool isTx           = false;
bool prevBtn        = HIGH;
bool txInit         = false;
bool rxInit         = false;
bool indicatorState = false;

unsigned long lastBlinkTime = 0;
bool          blinkState    = false;
unsigned long lastRxTime   = 0;
const int     holdTime      = 1500;

// ---- MORSE TX ----
char currentSymbol[10] = "";
int  symbolIndex       = 0;
char txText[32]        = "";
int  txIndex           = 0;

unsigned long pressStart     = 0;
unsigned long lastRelease    = 0;
bool          lastMorseState = HIGH;

// ---- RX ----
char rxText[64] = "";

void setup() {
  Serial.begin(9600);
  pinMode(BTN,       INPUT_PULLUP);
  pinMode(MORSE_BTN, INPUT_PULLUP);

  display.begin(0x3C, true);
  display.clearDisplay();
  display.setTextColor(SH110X_WHITE);
  display.setTextSize(1);
  display.setCursor(0, 0);
  display.println("I AM Rx");
  display.display();

  if (!radio.begin()) while (1);
  radio.setAutoAck(false);
  radio.setPALevel(RF24_PA_LOW);
  radio.setDataRate(RF24_250KBPS);
  radio.setChannel(100);
  radio.openWritingPipe(addr);
  radio.openReadingPipe(0, addr);
  radio.startListening();
}

void loop() {

  // ---- MODE TOGGLE ----
  bool currBtn = digitalRead(BTN);
  if (prevBtn == HIGH && currBtn == LOW) {
    isTx = !isTx;
    if (isTx) { radio.stopListening(); txInit = false; }
    else       { radio.startListening(); rxInit = false; }
    delay(300);
  }
  prevBtn = currBtn;

  // ================= TX =================
  if (isTx) {

    if (!txInit) {
      display.clearDisplay();
      display.setCursor(0, 0);
      display.println("I AM Tx");
      display.display();
      txInit = true;
    }

    // heartbeat
    char hb[] = "#";
    radio.write(&hb, sizeof(hb));

    bool curr = digitalRead(MORSE_BTN);

    if (lastMorseState == HIGH && curr == LOW)  { pressStart = millis(); }

    if (lastMorseState == LOW && curr == HIGH) {
      unsigned long duration = millis() - pressStart;
      if (symbolIndex < 9) {
        currentSymbol[symbolIndex++] = (duration < DOT_THRESHOLD) ? '.' : '-';
        currentSymbol[symbolIndex]   = '\0';
      }
      lastRelease = millis();
    }

    unsigned long gap = millis() - lastRelease;

    if (symbolIndex > 0 && gap > LETTER_GAP && gap < WORD_GAP) {
      char decoded = MorseLib::decode(currentSymbol);
      if (txIndex < 31) { txText[txIndex++] = decoded; txText[txIndex] = '\0'; }
      symbolIndex = 0; currentSymbol[0] = '\0';
    }

    if (txIndex > 0 && gap > WORD_GAP) {
      radio.write(&txText, sizeof(txText));
      txIndex = 0; txText[0] = '\0';
    }

    display.fillRect(0, 16, 128, 32, SH110X_BLACK);
    display.setCursor(0, 16); display.println(currentSymbol);
    display.setCursor(0, 28); display.println(txText);
    display.display();
    lastMorseState = curr;

    if (millis() - lastBlinkTime > 500) {
      lastBlinkTime = millis(); blinkState = !blinkState;
      display.fillRect(0, 48, 128, 16, SH110X_BLACK);
      if (blinkState) { display.setCursor(0, 48); display.println("[SENDING]"); }
      display.display();
    }

  // ================= RX =================
  } else {

    if (radio.available()) {
      char msg[32] = "";
      radio.read(&msg, sizeof(msg));

      if (strcmp(msg, "#") == 0) {
        lastRxTime = millis();
      } else {
        strncat(rxText, msg, sizeof(rxText) - strlen(rxText) - 1);
        display.fillRect(0, 16, 128, 20, SH110X_BLACK);
        display.setCursor(0, 20); display.println(rxText);
        display.display();
        lastRxTime = millis();
      }
    }

    if (!rxInit) {
      display.clearDisplay();
      display.setCursor(0, 0);
      display.println("I AM Rx");
      display.display();
      rxInit = true;
    }

    bool show = (millis() - lastRxTime < holdTime);
    if (show != indicatorState) {
      display.fillRect(0, 48, 128, 16, SH110X_BLACK);
      if (show) { display.setCursor(0, 48); display.println("[OTHER IS IN]"); }
      display.display();
      indicatorState = show;
    }

    if (millis() - lastRxTime > 3000) { rxText[0] = '\0'; }
  }
}

 

Prev Post
Next Post

Leave a comment

Please note, comments need to be approved before they are published.

Thanks for subscribing!

This email has been registered!

Shop the look

Choose Options

Edit Option
Back In Stock Notification
is added to your shopping cart.
this is just a warning
Login