Morse Code Reader

In a previous post, I have shown how to send Morse code. Wouldn’t it be nice if we could read it and convert it back to text? This is what I intend to demonstrate in this blog post. We will see how to program the Arduino to read from a digital input pin and look at how to convert digital on/off information into Morse code sequences of dots and dashes, and then into text. The featured image at the beginning of this blog post is Copyright Museums Victoria / CC BY. The image was downloaded from Museums Victoria.

As mentioned in my previous post, Morse Code Generator, Morse code is a method of transmitting information as a series of on-off tones, lights, or clicks. Basically, letters, numbers, and punctuation marks are translated to a variable length collection of dots and dashes, of shorter and longer bursts of sound or light. The duration of the dot, the short burst, is the unit of time by which all other elements of Morse code are defined. A dash, the long burst, is a signal whose duration is three times that of the dot. The time between dots and dashes within an encoded character is one unit of time. the time between characters is three units of time and the time between words is seven units of time.

International Morse code is thus composed of five elements:

  • short mark, dot or ‘dit’: one time unit long
  • longer mark, dash or ‘dah’: three time units long
  • gap between the dots and dashes within a character: one time unit long
  • gap between letters of a word: three time units long
  • gap between words: seven time units long

Following, is the international Morse code equivalent, in dot (.) and dash (-) notation for each alphabetical and numerical character:

315px-International_Morse_Code.svg

A Question of Timing

Decoding Morse code is all about measuring time. Measuring the amount of time the tone, light or electrical signal is on and the amount of time the tone, light, or electrical signal is off. Take the following diagram depicting the Morse code signal for the text “A TEST”.

Morse Code Timing

It is the on signal time that determines whether a dot or dash was transmitted. As shown above, whenever the signal is on for one unit of time, a dot (or dit) was transmitted and whenever the signal is on for three units of time, a dash (or dah) was transmitted. The off signal time determines if we are within a character being transmitted, if we are between characters, if we are between words, or if we are at the end of a transmission. If the signal is off for one unit of time, we are within a character, if the signal is off for three units of time, we are between characters of a word, if the signal is off for seven units of time, we are between words, and if the signal is off for more than seven units of time, then the transmission is finished.

Measuring Time

In order to measure for how long the signal stays on or off, we must first determine when a signal starts to be on and when the signal starts to be off. In a previous blog post, LED Toggle with a Push-Button Switch, I introduced a piece of code that detects when a signal goes from the off to the on state or from the on to the off state. This piece of code is called an edge detector because it detects the moment the signal changes state, that is the moment an edge (vertical line) occurs in the digital signal. The piece of code introduced was the following:

// Wait for an edge and return state - LED Toggle post
bool waitForEdge() {
  bool startValue = digitalRead(INPORT);
  bool newValue = startValue;
  while (newValue == startValue) {
    newValue = digitalRead(INPORT);
    }
  delay(DEBOUNCE_DELAY);
  return newValue;
}

In the code, we first read the actual value of the signal, then read its value until it changes. Once the value changes, we wait a bit to let the signal settle. The function then returns the last value read. This works except for one thing: we need to detect end-of transmission, which occurs if a low signal lasts for significantly more than seven time units. We need to add a timeout to the logic and try to detect a state change, but only for a certain amount of time. Also, because we need to detect a state change from a previously known state, we want to pass the previous state to the function instead of doing an initial signal read in the function. This will allow for a state change occurring outside the function.

Following is the new detectEdge() function. The first modification to the edge detection function is that three parameters were added to the function: the Arduino digital input port to read from, a timeout value in milliseconds, and a Boolean value, startingState, containing the current state of the signal, on or off. At the beginning of the function body, we declare Boolean variable signalState that will be used to hold the current signal state. We then get the current process time in milliseconds.

Note that signalState is initialized to the startingState value, ensuring that signalState is always valid. It is a good practice to initialize variables upon their declaration as it prevents them from having unknown values. In the following code, if signalState was not initialized, it would be possible to exit the while loop upon a timeout, without ever assigning a value to signalState causing the function to return an unitialized value that may randomly take any value.

// Wait for an edge or timeout.
bool detectEdge(int port, int timeout, bool startingState) {
  unsigned long timerStart = millis();
  bool signalState = startingState;
  while (true) {
    if ((millis() - timerStart) > timeout) {
      break;
    }
    signalState = digitalRead(INPORT);
    if (signalState != startingState) {
      break;
    }
  }
  return signalState;
}

A while(true) infinite loop follows. Within the loop, we first check if we have timed-out and if so, exit the loop. If not, we read the signal value from the input port and if the signal value is different from the starting state, we also exit the loop. The loop continues indefinitely until either a timeout occurs or a signal value change is detected. The current signal state value is returned at the end of the function.

Evaluating Morse Code Time Units

Now that we are detecting edges, we can measure the time a signal remains on or off, true or false. We need to determine if a portion of the signal lasted for 1, 3, or 7 units of time or longer. We could simply take the elapsed time between edges and divide it by the time 1 unit of time takes. This would give us the exact number of time units a portion of the signal lasted, which could be, say, 0.9, 3.1, or 6.8. Intuitively, we can see that 0.9 time units is really 1 time unit, 3.1 is 3 time units, and 6.8 time units is 7 time units. When measuring signal time, we have to allow for a bit of error in the measurement of time or in the signal production. After all, Morse code can be manually produced by a key operator and speed will vary with time.

A solution to this problem could be to round the measured time unit to the nearest integer. Hence, 0.5 to 1.49 time units would be considered to be 1 time unit and 2.5 to 3.49 time units would be considered 3 time units. Now what would we do if we had some portions of signal that were measured to last 2, 4, 5, 6, 8, or 9 time units in this manner? Would we generate an error? Again, intuitively, 4 units of time is a lot closer to 3 units of time than it is to 7 units of time and 6 units of time is closer to 7 units of time. We are less interested in in-between values than we are in deciding if a portion of signal is closer to 1, 3, or 7 units of time, or if it is completely off the scale.

Since we want the number of units of time to be either 1, 3, or 7, we need to define ranges that translate into these values. Let’s use, as a first rule, that the time a portion of signal lasts must be within 50 percent of its nominal value in time units. That is a 1 time unit signal must be between 0.5 and 1.5 time units. For any portion of a measured signal, the measured time T, in time units, must then be within the ranges

1 time unit: 0.5 ≤ T < 1.5
3 time units: 1.5 ≤ T < 4.5
7 time units: 3.5 ≤ T < 10.5

Using this rule, all time measurements that are within 0.5 and 10.5 time units are covered. There is an overlap between 3.5 and 4.5 time units, however. This can be resolved by splitting the difference and specifying the following ranges

1 time unit: 0.5 ≤ T < 1.5
3 time units: 1.5 ≤ T < 4
7 time units: 4 ≤ T < 10.5

Anything below 0.5 time units is thus considered as if there was no signal, or as if there was noise on the line; anything above 10.5 time units is considered a time out, or an end of transmission. The following code translates an elapsed time in Morse code time units according to the rules just stated.

// Convert time in milliseconds to Morse code time units
int timeUnits(int elapsedTime, int timeUnit, int timeout) {
  int value = 15;
  if (elapsedTime < 0.5*timeUnit) {
    value = 0;
  }
  else if (elapsedTime < 1.5*timeUnit) {
    value = 1;
  }
  else if (elapsedTime < 4*timeUnit) {
    value = 3;
  }
  else if (elapsedTime < timeout) {
    value = 7;
  }
  return value;
}

The function timeUnits() takes three parameters: the elapsed time in milliseconds to be converted to Morse code time units; the number of milliseconds corresponding to one Morse code unit of time; and the number of milliseconds corresponding to a timeout. This last parameter could have been set to 10.5 time units in the code, but this timeout value is also used when detecting edges and in order to ensure that values are coherent throughout the code, it is set once and passed as a parameter.

The body of the function starts by assuming that there will be a timeout and sets the number of time units to 15, arbitrarily chosen to represent a timeout. Then the unit time value is set according to where the number of elapsed milliseconds fall within the range of acceptable values. Finally, the function returns the value of time units, either 0, 1, 3, 7, or 15.

Sending Text Back to the PC

Before continuing with Morse code signal analysis and decoding, let’s have a look at a very useful class in the Arduino library: the Serial class. It is used to allow the Arduino board to communicate with a computer or other devices. All Arduino boards have at least one serial port that communicate on digital pins 0 and 1 as well as with a computer through the board’s USB connector. You cannot use pins 0 and 1 for input/output and serial communication at the same time.

The Arduino Serial class supports several methods to configure, send and receive information from the serial port. In this project, I made use of three of these methods:

  • The begin() method is used to initialize the serial port with serial communication speed, called a baud rate, and communication options including the number of data bits, from 5 to 8; the parity type, odd, even, or none; and the number of stop bits.
  • The print() method sends ASCII characters corresponding to the method argument. If the argument is a String, the characters of the string are sent as is. If the argument is an integer or a floating-point number, the numerical value is first converted to a string and then sent as ASCII characters.
  • The println() method behaves the same as the print() method, but appends a carriage return character (ASCII 13 or ‘\r’) and a new line character (ASCII 10 or ‘\n’) to the information sent.

Converting Morse Code to Text

Now that we measure time and are evaluating the number of time units the signal is on or off, we can convert the signal to Morse code and then to text. First, we declare global information used throughout the code. In the following code snippet, we set the Morse code signal to be read from the Arduino’s board pin 8, INPORT. We set the unit of time, TIME_UNIT, to 50 milliseconds, corresponding to the CODEX speed standard of 20 Words Per Minute (WPM), and we set the timeout value, READ_TIMEOUT, at 10.5 times unit of time. There are three global variables used to hold information between calls to the loop() function: currentCode holds a Morse code character being read, currentLine holds the text string being decoded from Morse code, and currentState holds the current state of the signal being decoded.

/*
 Morse Code Reader
 Program that reads Morse code and outputs decoded information to the
 Serial interface. It is associated with the Morse Code decoder blog
 post on https://lagacemichel.com
 MIT License
 Copyright (c) 2019, Michel Lagace
 */

 define INPORT 8
 define TIME_UNIT 50
 define READ_TIMEOUT 10.5*TIME_UNIT
 String currentCode;
 String currentLine;
 bool currentState;

The next code snippet contains the setup() function that initializes the digital input pin to receive the Morse code signal, initializes global variables, and sets up the serial communication to communicate with the PC at 9600 baud, or approximately 10 characters per second.

// Setup the board.
 void setup() {
   pinMode(INPORT, INPUT);
   currentState = false;
   currentCode = "";
   currentLine = "";
   Serial.begin(9600);
 }

The decodeSequence() function converts a series of dots and dashes representing the signal sequence of a Morse code character into the equivalent ASCII character. In the code below, we first initialize a String, characters, that contains the collection of all supported characters, from ‘a’ to ‘z’ and from ‘0’ to ‘9’. We then have an array of String objects, codedCharacters, that contains the Morse code corresponding to each character in the first String. The decodeSequence() function takes one parameter, sequence, a String containing the Morse code to decode. The function returns a single ASCII character corresponding to the decoded Morse code signal. It starts by initializing character, the variable containing the decoded Morse code to an asterisk. This is the character returned if the Morse code cannot be found in codedCharacters. It loops through the list of Morse codes in codedCharacters comparing each one to the sequence to be decoded. If found, the index of the sequence found is used to identify the character in characters. The character found or an asterisk is returned at the end of the function.

// List of valid alphanumeric characters
static String characters = "abcdefghijklmnopqrstuvwxyz0123456789";

// Morse code sequences for each alphanumeric character
static String codedCharacters[] = { 
   ".-", "-…", "-.-.", "-..", ".", "..-.", "--.", "….",
   "..", ".---", "-.-", ".-..", "--", "-.", "---", ".--.",
   "--.-", ".-.", "…", "-", "..-", "…-", ".--", "-..-",
   "-.--", "--..", "-----", ".----", "..---", "…--",
   "….-", "…..", "-….", "--…", "---..", "----."};

 // Convert Morse code signal to a character
 char decodeSequence(String sequence) {
   int i;
   char character = '*';
   for (i = 0; i < sizeof(codedCharacters)/sizeof(String); i++) {
     if (codedCharacters[i] == sequence) {
       character = characters[i];
       break;
     }
   }
   return character;
 }

The Main Loop

The following piece of code contains the loop() function of the program. It waits for a signal’s edge, accumulates Morse code dashes and dots, decodes the dots and dashes into characters and assembles the characters into words to form a sentence. The loop() function is split into three sections: the edge detection section, the falling edge processing section and the rising edge processing section.

// This is the main loop. It monitors INPORT digital IO
// pin for Morse code, decodes the signal and sends decoded
// text to the attached PC.
void loop() {
  // Wait for a rising edge, falling edge, or time out
  unsigned long startTime = millis();
  bool previousState = currentState;
  currentState = detectEdge(INPORT, READ_TIMEOUT, previousState);
  int elapsedTimeUnits =
    timeUnits(millis() - startTime, TIME_UNIT, READ_TIMEOUT);

  // On a falling edge, append dot or dash to character
  if (previousState && !currentState) {
      if (elapsedTimeUnits == 1) {
        currentCode += ".";
      }
      else if (elapsedTimeUnits == 3) {
        currentCode += "-";
      }
  }

  // On a rising edge or time out, handle end of
  // character, word, or end of transmission. 
  else if (currentCode != "") {
    if (elapsedTimeUnits > 1) {
       currentLine += decodeSequence(currentCode);
      currentCode = "";
    }
    if (elapsedTimeUnits > 3) {
      currentLine += ' ';
    }
    if (elapsedTimeUnits > 7) {
      Serial.println(currentLine);
      currentLine = "";
    }
  }
}

In the edge detection section, we save the current time since the program started in milliseconds using the millis() function provided by the system. We then call the detectEdge() function which returns the current signal state after detecting an edge or a timeout. We measure the elapsed time in milliseconds by subtracting the current time since the program started from the time saved before detecting the edge. The time in milliseconds is converted to Morse code time units through a call to the timeUnits() function.

In the second section, if the previous signal state was on and the new state is off, we have a falling edge. Any other signal condition is either a rising edge or a timeout. In the case of a falling edge, if the on signal lasted for 1 time unit, add a ‘dot’ to currentCode. If the on signal lasted for 3 units of time, add a ‘dash’ to currentCode. Do not do anything if the signal lasted for any other amount of time.

In the third section, we process information only if some dots and dashes have been accumulated in the currentCode variable. If the off signal lasted 1 unit of time or less, do nothing and continue accumulating dots and dashes in the currentCode variable. If the off signal lasted for more than 1 time unit, decode the dots and dashes sequence into its corresponding character and add it to the sentence being formed; reset the sequence of dots and dashes. In addition, if the off signal lasted more than 3 time units, add a space character to the sentence. Finally, if the off signal lasted more than 7 time units, return the decoded sentence to the serial line through a call to Serial.println() and reset the current line.

Putting it All Together

Let’s use the same setup as was used in my previous post Raspberry Pi Speaks Arduino. Connect the different parts using a solderless breadboard, jump wires, a BC337-40 transistor, and a 10K, a 220K, and a 150K resistor as depicted. Through one of the breadboard’s ground rail, we connect the Raspberry Pi and Arduino’s ground pins. The Raspberry Pi’s 3.3-volt supply is connected to the transistor’s base through the 220K resistor. The Raspberry Pi’s GPIO4 output is connected directly to the transistor’s emitter. The Arduino’s 5-volt supply is connected to the transistor’s collector through a 10K resistor. The Arduino’s digital input 8 is connected directly to the transistor’s collector.

Get the Python and Arduino code for the Morse Code Reader demonstration by accessing the MorseCodeReader repository on Github at https://github.com/lagacemichel/MorseCodeReader and download a copy of MorseCodeTutorial.py on the Raspberry Pi and a copy of MorseDecodeTutorial.ino on your PC. On the PC, using the Arduino IDE, load the MorseDecodeTutorial.ino file and load it on the Arduino board. On the Raspberry Pi, run one of the Python 3 IDEs, load the MorseCodeTutorial.py file and run it.

On the PC, within the Arduino IDE, press the control, shift, and M keys simultaneously. This will make the Serial Monitor window appear. As the Python program on the Raspberry Pi sends Morse code signals, the Arduino board decodes it until an end of transmission is detected, then it sends the text, through the serial port, back to the PC. The text is displayed on the Serial Monitor window and the output should look like the following:

We have just demonstrated Morse code sent by a Raspberry Pi Python program, received by an Arduino digital input pin through a level shifting transistor, decoded back to text within the Arduino and sent to the PC for display through the serial communication port.

Raspberry Pi Speaks Arduino

For a project of mine, I needed to connect a Raspberry Pi’s output to an Arduino Uno’s input. The Raspberry Pi’s digital input/output uses 3.3-volt logic while the Arduino Uno digital input/output uses 5-volt logic. How does one convert from one logic level to the other? There are several ways to do this and most methods use a transistor switch controlled by 3.3-volt logic to drive 5-volt logic.

We must first define what constitutes a HIGH and a LOW on the Arduino Uno and the Raspberry Pi. For the Arduino Uno, a digital pin, whether input or output, is considered in a LOW state for voltages below 1.5 volts and in a HIGH state for voltages between 3 and 5 volts. The Raspberry Pi’s digital input and output pins are considered in a LOW state for voltages below 1 volt and in a HIGH state for voltages between 2 and 3.3 volts.

From 3.3 Volts to 5 Volts

In order to convert the Raspberry Pi’s digital output voltage levels to the Arduino’s digital input levels, we can use a transistor switch, similar to the one used to control a relay in a previous post. I will describe two transistor circuits: the logic inverter and the level converter using the transistor’s emitter as input.

Logic Inverter

In the circuit below, we want that if the input voltage is between 0 and 1 volt, the transistor be in cutoff mode, completely off, and the output voltage be between 3 to 5 volts. We also want that if the input voltage is between 2 and 3.3 volts, the transistor be in saturation mode, completely on, and the output voltage be between 0 to 1.5 volts. In other words, if the input is LOW, the output will be HIGH and if the input is HIGH, then the output will be LOW. This circuit is called a logic inverter. It converts logic levels but it inverts logical values. Because the input is considered LOW at or below 1 volt and HIGH at or above 2 volts, we want for the transistor to go from cutoff to saturation mode between input values of 1 volt and 2 volts. To ensure that LOW and HIGH values are met, let’s make the transistor be in cutoff mode for input values at of below 1.4 volts and let it be in saturation mode for values at or above 1.6 volts.

3.3V to 5V Inverted Transistor General

In cutoff mode, there is no transistor base current and the base-emitter voltage (Vbe) is at or below 0.7 volts. Hence, the current in Rbase and Rbias are the same and the voltage across Rbias is at or below 0.7 volts. When the input voltage is 1.4 volts, the following equation holds.

Vbe / RBias = (Vin – Vbe) / RBase

0.7 V / RBias = (1.4 V – 0.7 V) / RBase = 0.7 V / RBase

RBias = RBase

In saturation mode, the voltage across the collector and emitter is 0 volts and collector current is constant at Vcc / RLoad. We want the collector current to be large enough for the transistor to work properly, but small enough to consume the least power possible. Using a 10K resistor, the collector current is 5 V / 10K, or 0.5 mA which is within the BC337-40 bipolar junction transistor working values. According to its specification, the hfe value, the collector-base current amplification, is 400. The base current is thus

Ib = Ic / hfe

Ib = 0.5 mA / 400 = 1.25 μA

The current through RBase is equal to the currents through both RBias and the transistor’s base.  When the input voltage is 1.6 volts, the following equation holds.

(Vin – Vbe) / RBase = Vbe / RBias + Ib

(1.6 V – 0.7 V) / RBase = 0.7 V / RBias + 1.25 μA

Since, as we have seen above, RBias has the same value as RBase, the equation becomes

0.9 V / RBase = 0.7 V / RBase + 1.25 μA

RBase = RBias = 160K ≅ 220K

The circuit becomes

3.3V to 5V B input 2 base resistors Values

The following graph plots the input and output computed voltage values, in blue, and the values measured on an actual circuit implemented with the resistor values in the schematic above, in orange.

3.3V to 5V Biased Base Input

As can be seen, logical values are inverted and voltage values correspond to acceptable values for both the Raspberry Pi and Arduino’s HIGH and LOW voltage levels. Notice the shift of the target voltage values slightly to the left of 1.5 volts. This is caused by the fact that we are not using the exact resistor values as computed, because the saturated Vbe is not exactly 0.7 volts and the actual hfe is not exactly 400. Still, the results are more than acceptable, if we wanted an inverted signal.

Using the Emitter as Input

Let’s use the same circuit, but connect the base resistor to the Raspberry Pi’s supply, VccP, and the emitter to the Raspberry Pi digital output as in the following circuit.

3.3V to 5V Converted Transistor General

This is like rotating the previous circuit clockwise by 90 degrees. Now we want that if the input voltage is LOW, below 1 volt, that the transistor be in saturation and the output be at Vin, the Raspberry Pi digital output value.  We also want that if the input voltage is HIGH, at 2 volts or higher, that the transistor be in cutoff and the output be at the Arduino’s supply voltage. Because the permissible voltages for the LOW value on the Arduino, between 0 and 1.5 volts, are higher than those for the Raspberry Pi, between 0 and 1 volt, it is acceptable to use the Raspberry Pi digital output voltages as the Arduino’s input for LOW values.

In this circuit, as it was in the previous one, in cutoff mode there is no transistor base current and the base-emitter voltage is at or below 0.7 volts. Hence, the current in RBase and RBias are the same and the voltage across RBias is at or below 0.7 volts. Hence, when the input voltage is 1.6 volts, the following equation holds.

(VccP – Vbe – Vin) / RBase = Vbe / RBias

(3.3V – 0.7V – 1.6) / RBase = 0.7V / RBias

RBias = 0.7 • RBase

For saturation mode, the same reasoning as for the logic inverter holds about the base current to collector current relationship. Since we want saturation of the transistor to occur at 1.4 volts, the collector current is equal to the current going through RLoad.

Ic = (VccA – Vin) / RLoad

Ic = (5V – 1.4V) / 10K = 0.36 mA

The base current is thus

Ib = Ic / hfe

Ib = 0.36 mA / 400 = 0.9 μA

The current through the base is the current through RBase minus the current through RBias. When the input voltage is 1.4 volts, when we want transistor saturation to occur, the following equations hold.

Ib = (VccP – Vbe – Vin) / RBase – Vbe / RBias

0.9 μA = (3.3V – 0.7V – 1.4V) / RBase – 0.7V / RBias

Since, as we have seen above, RBias is 0.7 • RBase, we have

0.9 μA = (3.3V – 0.7V – 1.4V) / RBase – 0.7V / (0.7 • RBase)

0.9 μA = 1.2V / RBase – 1V / RBase

RBase = 0.2V / 0.9 μA = 222.222K ≅ 220K

We then compute RBias from the cutoff equations

RBias = 0.7 • RBase = 155.556K ≅ 150K

The circuit becomes

3.3V to 5V E input 2 base resistors Values

The following graph plots the input and output computed voltage values, in blue, and the values measured on an actual circuit implemented with the resistor values in the schematic above, in orange.

3.3V to 5V Biased Emitter Input

In the graph, logical values are converted to the same levels this time, and voltage values correspond to acceptable values for both the Raspberry Pi and Arduino’s HIGH and LOW voltage levels. Notice the shift of the target voltage values slightly to the right of 1.5 volts. This is caused by the fact that we are not using the exact resistor values as computed, because the saturated Vbe value is not exactly 0.7 volts and the actual transistor current amplification is not exactly 400. Nevertheless, results are more than acceptable.

Putting it Together

Now that we have a method to convert 3.3-volt logic to 5-volt logic, we can connect a Raspberry Pi 3 GPIO output to an Arduino digital input. As a test, we will program the Raspberry Pi to output Morse Code, read the output using an Arduino digital input and output the value read to the Arduino’s built-in LED. The code within this post can be found on GitHub by clicking here.

Breadboarding

First, let’s have a look at the complete circuit. The following picture depicts how to connect the different parts using a solderless breadboard, jump wires, a BC337-40 transistor, and a 10K, a 220K and a 150K resistor. Through one of the breadboard’s ground rail, we connect the Raspberry Pi and Arduino’s ground pins. The Raspberry Pi’s 3.3-volt supply is connected to the transistor’s base through the 220K resistor. The Raspberry Pi’s GPIO4 output is connected directly to the transistor’s emitter. The Arduino’s 5-volt supply is connected to the transistor’s collector through a 10K resistor. The Arduino’s digital input 8 is connected directly to the transistor’s collector.

3.3to5_bb

Morse Code Generator in Python

We have seen the morse code generator in a previous post. I have ported the Morse Code program to Python 3 for it to be run on the Raspberry Pi. I used IDLE (Integrated Development and Learning Environment) to code and run the Python 3 code on the Raspberry Pi. If you are new to the Raspberry Pi and want to set it up, please visit “Get started with Raspberry Pi” on the www.raspberrypi.org site.

# Morse Code Generator Python Program that translates a
# text string to Morse Code and outputs it to a GPIO pin.
# This program is used as part of a demonstration to show
# connectivity between a Raspberry Pi and an Arduino UNO.
# It is associated with the Raspberry Pi Speaks Arduino
# blog post on https://lagacemichel.com
# MIT License
# Copyright (c) 2019, Michel Lagace

import RPi.GPIO as IO
import time
UNIT_TIME = 0.1
BOARD_PIN = 7
# Characters to be encoded
characters = "abcdefghijklmnopqrstuvwxyz"

# Morse code sequences for each character
codedCharacters = [
".-", "-...", "-.-.", "-..", ".", "..-.", "--.", "....",
"..", ".---", "-.-", ".-..", "--", "-.", "---", ".--.",
"--.-", ".-.", "...", "-", "..-", "...-", ".--", "-..-",
"-.--", "--.." ]

# Setup the board. Digital port LED_BUILTIN in output mode
def setup():
    IO.setwarnings(False)
    IO.setmode (IO.BOARD)
    IO.setup(BOARD_PIN,IO.OUT)

# Function to output a dot: one unit on, one unit off
def outputDot():
    IO.output(BOARD_PIN,1)
    time.sleep(UNIT_TIME)
    IO.output(BOARD_PIN,0)
    time.sleep(UNIT_TIME)

# Function to output a dash: three units on, one unit off
def outputDash():
    IO.output(BOARD_PIN,1)
    time.sleep(UNIT_TIME*3)
    IO.output(BOARD_PIN,0)
    time.sleep(UNIT_TIME)

# Function to output a single character
def outputCharacter(c):
    # Find index of character to encode
    index = characters.find(c)
    # Ignore unencodable characters
    if (index >= 0):
        # Encode morse codeand output it
        code = codedCharacters[index]
        for ch in code:
            if ch == '-':
                outputDash()
            else: # if not '-', must be '.'
                outputDot()
        # Wait 3 units at the end of the letter
        # (2 units assuming previous dot or dash)
        time.sleep(UNIT_TIME*2)

# Function to encode a whole string
def sentence (text):
    # Output each character in turn
    for ch in text:
        # Only lower-case characters are encoded
        if ch != ' ':
            outputCharacter(ch.lower())
        # Spaces are encoded as 7 units,
        # (4 units assuming a previous character)
        else:
            time.sleep(UNIT_TIME*4)

# Function looped indefinitely
def loop():
    sentence("Mikes Electro Shack")
    time.sleep(UNIT_TIME*25) # Wait 4 spaces at the end

# Main program. Setup board then loop forever
setup()
while(True):
    loop()

Raspberry Pi Reader on the Arduino

The code on the Arduino is quite simple. It loops forever, reading the digital input connected to the Raspberry Pi, writes the value to the built-in LED, then waits 10 milliseconds befor starting over.

// Main RaspberryPiReader sketch
// Program that reads a digital input from a Raspberry Pi
// GPIO pin and outputs its value to an Arduino digital output
// pin. This sketch is used as part of a demonstration to show
// connectivity between a Raspberry Pi and an Arduino UNO.
// It is associated with the Raspberry Pi Speaks Arduino
// blog post on https://lagacemichel.com
// MIT License
// Copyright (c) 2019, Michel Lagace

// Define input and output ports
#define INPORT 8    // Input port connected to Raspberry Pi
#define OUTPORT 13  // Output port to built-in 

// Time to wait in milliseconds between samples
#define SAMPLE_DELAY 10

// Setup the board.
void setup() {
    pinMode(INPORT, INPUT);
    pinMode(OUTPORT,OUTPUT);
    digitalWrite(OUTPORT,LOW);
}

// Repeat forever
void loop() {
    // Read value from Raspberry Pi
    bool value = digitalRead(INPORT);
    
    // Output value to built-in LED
    digitalWrite(OUTPORT, value);
    
    // Wait before next sample
    delay(SAMPLE_DELAY);
}

Download the sketch on the Arduino then run the Python program on the Raspberry Pi. You will notice Morse Code being output by the Arduino’s buit-in LED.