Easy Step-By-Step DIY Arduino Morse Code Decoder For CW Ham Radio Operators
As I mentioned on here before, I have been studying both the arduino and electronic circuits. The 48th video in Paul McWhorter’s youtube series on arduinos interfaced with an LCD display. While following the tutorial and watching my inspirational messages such “Hello World” and “I like pizza” transfer from the desktop arduino IDE to the the little screen, the first independent break out project that I wanted to build popped in my mind. How tough would it be to build a morse code decoder? Or otherwise known as a cw (for continuous wave) decoder for the ham radio operators out there.
The internet search engines led me to these instructions by Hjalmar Hansen OZ1JHM and this video on the “learnelectronics” youtube channel. There are no shortages of internet sources for these projects however.
Below is the overall schematic for my version of Hjalmar OZ1JHM’s design. I made it using Tinkercad.
[Note: Thanks to Jeff AC1JR (https://blahg.josefsipek.net/) for noticing that the original schematic I posted was incorrect. Hopefully fixed now!]
The parts list is fairly simple:
- Arduino Uno
- A 2×16 LCD display with a pin header like this one. I’ll discuss this more down below.
- A 5-volt active buzzer like this one.
- An LED (color of choice)
- One 10KΩ potentiometer (I used a Piher PT15 type)
- One 330Ω resistor
- Two 10KΩ resistors
- One 100nF capacitor
- TRS jack (I repurposed one from a dead set of headphones; a TS jack may work fine as well)
- Hookup wire (22awg, solid copper)
- One solderless breadboard
- Small piece of protoboard (optional)
- Solder (optional)
- Soldering iron (optional)
- Heat shrink tubing (optional)
- Heat gun (optional) or other method of heat shrinking like a hair dryer
- Multimeter
THREE PARTS TO CONSTRUCTION:
- Construct audio input module.
- Wire components on breadboard and arduino
- Program arduino
PART 1: CONSTRUCT AUDIO INPUT MODULE:
(1) Remove TRS jack from a dead pair of headphones, or acquire the jack in some other way.
(2) Solder about 10 inches of hookup wire onto the appropriate posterior part of the TRS jack that is in continuity with the tip. Check for continuity with tip, and no continuity with the ring or sleeve.
(3) Use heat shrink tubing to cover the initial wire to the tip.
(4) Solder a 10″ length of wire to the posterior sleeve area of the TRS jack.
(5) Consider twisting the wires together for neatness sake.
(6) Add heat shrink tubing over the sleeve wire connection.
This is an idea of how the audio input module will look after it is all constructed. You will notice that I twisted the audio connector wires in the final version.
Note: My understanding of this circuit is that audio from a cw source enters as AC current. The capacitor is to stop stray DC current from passing through. The AC current from the audio input is then subjected to the voltage divider created by the 10KΩ resistors. Apparently this biases the audio input current so that the current is always positive with respect to ground. The reason to include this part is because the arduino apparently cannot handle negative currents. This is the explanation provided in the learnelectronics video.
(7) Use a protoboard (or go ahead and just use a breadboard) to wire the components together. You can see how I did it in the sketch below. The tip of the TRS jack is wired to the capacitor (labeled “Audio in”), while the sleeve of the TRS jack is wired to ground (not shown).
And how it actually looks when soldered to the protoboard. I drew in the green lines to show how the components are connected on the underside of the board. The TRS sleeve is wired to ground as shown below.
And this is how the board looks without any additional mark-ups. If you would rather use a breadboard for the audio part, the wiring cartoon is shown below.
PART 2: WIRE COMPONENTS ON BREADBOARD AND ARDUINO
(1) Refer to the cartoon circuit diagram below.
(2) Establish a ground rail and a 5V rail on your breadboard. Connect the ground rail to one of the ground pins on the arduino. Connect the 5V rail to the 5V pin on the arduino. All subsequent ground connections should go into the ground rail of the breadboard. All subsequent 5V connections should go into the 5V rail of the breadboard.
(3) The connections on the LCD display that I used for this project are listed in the table below. I believe that the “A” and “K” refer to the power and ground to the LED of the display respectively. This seems to be labeled “LED” and “LED” on other LCD displays, including the one shown in the cartoon schematic above. Check your LCD display documentation to see how these pins are arranged.
(4) You may need to solder a pin header on to your LCD display in order to secure it in a breadboard. Settle the LCD into the breadboard when you are able.
(5) Use hookup wire to wire the appropriate pins of the LCD display to the arduino as shown below.
Here is a close up of the LCD display I used for this project. You can see the soldering along the top where a pin header had been placed (not by me). If you do not want to modify this project, make sure you get a matching display. Searching LCD1602 on amazon shows a lot of similar products.
(6) Lets wire up the potentiometer next. One leg goes to the ground rail, one leg goes to the 5V rail, and the center pin connects with V0 of the LCD display. This simply controls the brightness of the screen.
(7) Next lets wire in an LED and its 330Ω current limiting resistor. Wire the cathode of the LED (long leg) to pin 13 of the arduino. Use the 330Ω resistor to connect the anode to the ground rail of the breadboard. The LED will light up with dits and dahs.
(8) Finally I added an active buzzer. Wire the positive terminal to pin 12 of the arduino. Attach the negative terminal of the buzzer to ground. The buzzer will allow for audio monitoring of the code.
Here is a view of the completely wired hardware.
PART 3: PROGRAM THE ARDUINO
(1) Open your arduino IDE, copy and paste the arduino code below, make sure it compiles. That’s it! If you follow my notes and use identical hardware, you should now have a working cw decoder.
Just some notes about the arduino code for this process. Hjalmar Hansen OZ1JHM describes the process here. Hjalmar includes tips on how to modify the code to work better. However, for me, just copying and pasting his code would not compile. I cleaned up the code, including removing any of the commentary, and programmed in the buzzer.
You can test it by receiving cw from an online morse code generator through the audio jack. It is definitely not perfect…not by a long shot. But holy tamole! It works!
Well, that’s it…Get to it!
Always yours,
KM1NDY
#include <LiquidCrystal.h>
LiquidCrystal lcd(8, 9, 4, 5, 6, 7);
const int colums = 16;
const int rows = 2;
int lcdindex = 0;
int line1[colums];
int line2[colums];
byte U_umlaut[8] = {B01010,B00000,B10001,B10001,B10001,B10001,B01110,B00000}; // 'Ü'
byte O_umlaut[8] = {B01010,B00000,B01110,B10001,B10001,B10001,B01110,B00000}; // 'Ö'
byte A_umlaut[8] = {B01010,B00000,B01110,B10001,B11111,B10001,B10001,B00000}; // 'Ä'
byte AE_capital[8] = {B01111,B10100,B10100,B11110,B10100,B10100,B10111,B00000}; // 'Æ'
byte OE_capital[8] = {B00001,B01110,B10011,B10101,B11001,B01110,B10000,B00000}; // 'Ø'
byte fullblock[8] = {B11111,B11111,B11111,B11111,B11111,B11111,B11111,B11111};
byte AA_capital[8] = {B00100,B00000,B01110,B10001,B11111,B10001,B10001,B00000}; // 'Å'
byte emtyblock[8] = {B00000,B00000,B00000,B00000,B00000,B00000,B00000,B00000};
int audioInPin = A1;
int audioOutPin = 10;
int buzzPin=12;
int ledPin = 13;
float magnitude ;
int magnitudelimit = 100;
int magnitudelimit_low = 100;
int realstate = LOW;
int realstatebefore = LOW;
int filteredstate = LOW;
int filteredstatebefore = LOW;
float coeff;
float Q1 = 0;
float Q2 = 0;
float sine;
float cosine;
float sampling_freq=8928.0;
float target_freq=558.0;
float n=48.0;
int testData[48];
int nbtime = 6;
long starttimehigh;
long highduration;
long lasthighduration;
long hightimesavg;
long lowtimesavg;
long startttimelow;
long lowduration;
long laststarttime = 0;
char code[20];
int stop = LOW;
int wpm;
void setup() {
int k;
float omega;
k = (int) (0.5 + ((n * target_freq) / sampling_freq));
omega = (2.0 * PI * k) / n;
sine = sin(omega);
cosine = cos(omega);
coeff = 2.0 * cosine;
lcd.createChar(0, U_umlaut);
lcd.createChar(1, O_umlaut);
lcd.createChar(2, A_umlaut);
lcd.createChar(3, AE_capital);
lcd.createChar(4, OE_capital);
lcd.createChar(5, fullblock);
lcd.createChar(6, AA_capital);
lcd.createChar(7, emtyblock);
lcd.clear();
Serial.begin(115200);
pinMode(ledPin, OUTPUT);
pinMode(buzzPin, OUTPUT);
lcd.begin(colums, rows);
for (int index = 0; index < colums; index++){
line1[index] = 32;
line2[index] = 32;
}
}
void loop() {
for (char index = 0; index < n; index++)
{
testData[index] = analogRead(audioInPin);
}
for (char index = 0; index < n; index++){
float Q0;
Q0 = coeff * Q1 - Q2 + (float) testData[index];
Q2 = Q1;
Q1 = Q0;
}
float magnitudeSquared = (Q1*Q1)+(Q2*Q2)-Q1*Q2*coeff;
magnitude = sqrt(magnitudeSquared);
Q2 = 0;
Q1 = 0;
if (magnitude > magnitudelimit_low){
magnitudelimit = (magnitudelimit +((magnitude - magnitudelimit)/6));
}
if (magnitudelimit < magnitudelimit_low)
magnitudelimit = magnitudelimit_low;
if(magnitude > magnitudelimit*0.6)
realstate = HIGH;
else
realstate = LOW;
if (realstate != realstatebefore){
laststarttime = millis();
}
if ((millis()-laststarttime)> nbtime){
if (realstate != filteredstate){
filteredstate = realstate;
}
}
if (filteredstate != filteredstatebefore){
if (filteredstate == HIGH){
starttimehigh = millis();
lowduration = (millis() - startttimelow);
}
if (filteredstate == LOW){
startttimelow = millis();
highduration = (millis() - starttimehigh);
if (highduration < (2*hightimesavg) || hightimesavg == 0){
hightimesavg = (highduration+hightimesavg+hightimesavg)/3;
}
if (highduration > (5*hightimesavg) ){
hightimesavg = highduration+hightimesavg;
}
}
}
if (filteredstate != filteredstatebefore){
stop = LOW;
if (filteredstate == LOW){
if (highduration < (hightimesavg*2) && highduration > (hightimesavg*0.6)){
strcat(code,".");
Serial.print(".");
}
if (highduration > (hightimesavg*2) && highduration < (hightimesavg*6)){
strcat(code,"-");
Serial.print("-");
wpm = (wpm + (1200/((highduration)/3)))/2;
}
}
if (filteredstate == HIGH){
float lacktime = 1;
if(wpm > 25)lacktime=1.0;
if(wpm > 30)lacktime=1.2;
if(wpm > 35)lacktime=1.5;
if (lowduration > (hightimesavg*(2*lacktime)) && lowduration < hightimesavg*(5*lacktime)){
docode();
code[0] = '\0';
Serial.print("/");
}
if (lowduration >= hightimesavg*(5*lacktime)){
docode();
code[0] = '\0';
printascii(32);
Serial.println();
}
}
}
if ((millis() - startttimelow) > (highduration * 6) && stop == LOW){
docode();
code[0] = '\0';
stop = HIGH;
}
if(filteredstate == HIGH){
digitalWrite(ledPin, HIGH);
digitalWrite(buzzPin, HIGH);
tone(audioOutPin,target_freq);
}
else{
digitalWrite(ledPin, LOW);
digitalWrite(buzzPin, LOW);
noTone(audioOutPin);
}
updateinfolinelcd();
realstatebefore = realstate;
lasthighduration = highduration;
filteredstatebefore = filteredstate;
}
void docode(){
if (strcmp(code,".-") == 0) printascii(65);
if (strcmp(code,"-...") == 0) printascii(66);
if (strcmp(code,"-.-.") == 0) printascii(67);
if (strcmp(code,"-..") == 0) printascii(68);
if (strcmp(code,".") == 0) printascii(69);
if (strcmp(code,"..-.") == 0) printascii(70);
if (strcmp(code,"--.") == 0) printascii(71);
if (strcmp(code,"....") == 0) printascii(72);
if (strcmp(code,"..") == 0) printascii(73);
if (strcmp(code,".---") == 0) printascii(74);
if (strcmp(code,"-.-") == 0) printascii(75);
if (strcmp(code,".-..") == 0) printascii(76);
if (strcmp(code,"--") == 0) printascii(77);
if (strcmp(code,"-.") == 0) printascii(78);
if (strcmp(code,"---") == 0) printascii(79);
if (strcmp(code,".--.") == 0) printascii(80);
if (strcmp(code,"--.-") == 0) printascii(81);
if (strcmp(code,".-.") == 0) printascii(82);
if (strcmp(code,"...") == 0) printascii(83);
if (strcmp(code,"-") == 0) printascii(84);
if (strcmp(code,"..-") == 0) printascii(85);
if (strcmp(code,"...-") == 0) printascii(86);
if (strcmp(code,".--") == 0) printascii(87);
if (strcmp(code,"-..-") == 0) printascii(88);
if (strcmp(code,"-.--") == 0) printascii(89);
if (strcmp(code,"--..") == 0) printascii(90);
if (strcmp(code,".----") == 0) printascii(49);
if (strcmp(code,"..---") == 0) printascii(50);
if (strcmp(code,"...--") == 0) printascii(51);
if (strcmp(code,"....-") == 0) printascii(52);
if (strcmp(code,".....") == 0) printascii(53);
if (strcmp(code,"-....") == 0) printascii(54);
if (strcmp(code,"--...") == 0) printascii(55);
if (strcmp(code,"---..") == 0) printascii(56);
if (strcmp(code,"----.") == 0) printascii(57);
if (strcmp(code,"-----") == 0) printascii(48);
if (strcmp(code,"..--..") == 0) printascii(63);
if (strcmp(code,".-.-.-") == 0) printascii(46);
if (strcmp(code,"--..--") == 0) printascii(44);
if (strcmp(code,"-.-.--") == 0) printascii(33);
if (strcmp(code,".--.-.") == 0) printascii(64);
if (strcmp(code,"---...") == 0) printascii(58);
if (strcmp(code,"-....-") == 0) printascii(45);
if (strcmp(code,"-..-.") == 0) printascii(47);
if (strcmp(code,"-.--.") == 0) printascii(40);
if (strcmp(code,"-.--.-") == 0) printascii(41);
if (strcmp(code,".-...") == 0) printascii(95);
if (strcmp(code,"...-..-") == 0) printascii(36);
if (strcmp(code,"...-.-") == 0) printascii(62);
if (strcmp(code,".-.-.") == 0) printascii(60);
if (strcmp(code,"...-.") == 0) printascii(126);
if (strcmp(code,".-.-") == 0) printascii(3);
if (strcmp(code,"---.") == 0) printascii(4);
if (strcmp(code,".--.-") == 0) printascii(6);
}
void printascii(int asciinumber){
int fail = 0;
if (lcdindex > colums-1){
lcdindex = 0;
if (rows==4){
for (int i = 0; i <= colums-1 ; i++){
lcd.setCursor(i,rows-3);
lcd.write(line2[i]);
line2[i]=line1[i];
}
}
for (int i = 0; i <= colums-1 ; i++){
lcd.setCursor(i+fail,rows-2);
lcd.write(line1[i]);
lcd.setCursor(i+fail,rows-1);
lcd.write(32);
}
}
line1[lcdindex]=asciinumber;
lcd.setCursor(lcdindex+fail,rows-1);
lcd.write(asciinumber);
lcdindex += 1;
}
void updateinfolinelcd(){
int place;
if (rows == 4){
place = colums/2;}
else{
place = 2;
}
if (wpm<10){
lcd.setCursor((place)-2,0);
lcd.print("0");
lcd.setCursor((place)-1,0);
lcd.print(wpm);
lcd.setCursor((place),0);
lcd.print(" WPM");
}
else{
lcd.setCursor((place)-2,0);
lcd.print(wpm);
lcd.setCursor((place),0);
lcd.print(" WPM ");
}
}
What you got from the headphones is a PLUG, not a jack. A jack is the socket that the plug goes into.