Morse Code Transmitter and Receiver - Working Code
Posted: 18 May 2023 07:03
I'm old enough to have learnt morse code and thought it would be fun to use some of the latest electronics to send and receive something archaic.
Both Tx and Rx use the EasyPicFusionv7 board with a PIC32MX795F512L MCU.
The Tx uses a TFT display to show Words Per Minute (WPM), pulse lengths and so on.
THE TRANSMITTER
Is the easiest to write and simply uses the function vdelay_ms() to create the 'DITS' (one unit of time) and the 'DAHS' (three units of time).
Inter-pulse delay is 1*DIT, inter-letter is 3*DIT' inter-word delay is 7*DIT.
Traditional, hand generated morse is usually in the region of 10 to 30 WPM. The word 'PARIS' is typically used as a test word.
My machine generates code works from 10 to 260 wpm where a DIT = 4[mS]
THE RECEIVER
Is a bit trickier as I've made it automatically track variations in morse speed.
Maybe of interest to coders: INT0 interrupt (on RD0 for morse input) and a rolling average to track changes in morse speed.
The key is to record 32 pulses that are either x[mS] or 3x[mS] long and average = 2x[mS]
Use the average to distinguish between DITs and DAhs, convert to 0s and 1s, convert this binary string to a decimal number and look up the appropriate morse code table.
The output is RS232 on UART2/USB UART A.
Two files below: The transmitter and the Receiver. The resources file ("Resources_Tahoma_26X29.h")for the transmitter is not included - if you want a copy let me know.
Good luck - comments, criticism and improvements welcomed.
Regards Bill Legge in Australia
Both Tx and Rx use the EasyPicFusionv7 board with a PIC32MX795F512L MCU.
The Tx uses a TFT display to show Words Per Minute (WPM), pulse lengths and so on.
THE TRANSMITTER
Is the easiest to write and simply uses the function vdelay_ms() to create the 'DITS' (one unit of time) and the 'DAHS' (three units of time).
Inter-pulse delay is 1*DIT, inter-letter is 3*DIT' inter-word delay is 7*DIT.
Traditional, hand generated morse is usually in the region of 10 to 30 WPM. The word 'PARIS' is typically used as a test word.
My machine generates code works from 10 to 260 wpm where a DIT = 4[mS]
THE RECEIVER
Is a bit trickier as I've made it automatically track variations in morse speed.
Maybe of interest to coders: INT0 interrupt (on RD0 for morse input) and a rolling average to track changes in morse speed.
The key is to record 32 pulses that are either x[mS] or 3x[mS] long and average = 2x[mS]
Use the average to distinguish between DITs and DAhs, convert to 0s and 1s, convert this binary string to a decimal number and look up the appropriate morse code table.
The output is RS232 on UART2/USB UART A.
Two files below: The transmitter and the Receiver. The resources file ("Resources_Tahoma_26X29.h")for the transmitter is not included - if you want a copy let me know.
Good luck - comments, criticism and improvements welcomed.
Regards Bill Legge in Australia
Code: Select all
////////////////////////////////////////////////////////////////////////////////
// Project: WVL_Fusion_Morse_Tx //
// File: WVL_Fusion_Morse_Tx.c //
// Function: Generate morse code 20 to 266wpm //
// MCU: P32MX795F512L with 8MHz on-board Xtal //
// Board: EasyPIC_Fusion_v7_for_PIC32 //
// Power 7-12V input. 3.3V //
// Compiler: mikroC PRO for PIC32 V4.0 //
// Oscillator: 8Mhz Xtal, Div2. PLLX20 = 80MHz //
// Programmer: On-board Mikro using PGEC2 and PGED2 on RB6 and RB7 //
// Author: WVL //
// Date: Mar 2023 //
////////////////////////////////////////////////////////////////////////////////
////////////////////////////////////////////////////////////////////////////////
// Morse code comprises tones, usually about 800Hz: //
// Dit 1 unit long //
// Dah 3 units long //
// Inter-pulse 1 //
// Inter-character 3 //
// Inter-word 7 //
// Words-per-minute are usually calculated from 1200/dot[mS] //
// So, 1 Dit[ms] = 1200/wpm. //
// Hand generated morse is about 10 to 30 wpm but machine may be much faster //
// Want 10 to 200 wpm //
// So Dit_times are: 10wpm = 120mS and 200wpm = 33mS //
////////////////////////////////////////////////////////////////////////////////
////////////////////////////////////////////////////////////////////////////////
// TFT module connections //
char TFT_DataPort at LATE; //
sbit TFT_RST at LATD7_bit; //
sbit TFT_BLED at LATD2_bit; //
sbit TFT_RS at LATD9_bit; //
sbit TFT_CS at LATD10_bit; //
sbit TFT_RD at LATD5_bit; //
sbit TFT_WR at LATD4_bit; //
char TFT_DataPort_Direction at TRISE; //
sbit TFT_RST_Direction at TRISD7_bit; //
sbit TFT_BLED_Direction at TRISD2_bit; //
sbit TFT_RS_Direction at TRISD9_bit; //
sbit TFT_CS_Direction at TRISD10_bit; //
sbit TFT_RD_Direction at TRISD5_bit; //
sbit TFT_WR_Direction at TRISD4_bit; //
////////////////////////////////////////////////////////////////////////////////
////////////////////////////////////////////////////////////////////////////////
#include "Resources_Tahoma_26X29.h" //
#define HEARTBEAT LATG0_bit=~LATG0_bit //
#define TEXT_ENVELOPE LATA3_bit //
#define WORD_ENVELOPE LATA2_bit //
#define LETTER_ENVELOPE LATA1_bit //
#define PULSE_ENVELOPE LATA0_bit //
////////////////////////////////////////////////////////////////////////////////
void main(){
// Comma count 1 3 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 18 19 20 21 22 23 24
// Morse code A B C D E F G H I J K L M N 0 P Q R S T U V W X Y Z
unsigned char CODE[] = "01,1000,1010,100,0,0010,110,0000,00,0111,101,0100,11,10,111,0110,1101,010,000,1,001,0001,011,1001,1011,1100,";
unsigned int code_number = 0; // count of CODE string
unsigned int commas = 0; // commas until desired morse code
unsigned int comma_count = 0; // commas so far
unsigned char TEXT[] = "THE QUICK BROWN FOX JUMPS OVER THE LAZY DOG"; // TEXT string 0-18, 19 characters plus '\0'
unsigned int text_number = 0; // count of TEXT string
unsigned int text_length = strlen(TEXT); // length of TEXT string
unsigned int wpm = 0; // words per minute rasge from 4 to 64
unsigned int dit_time = 0; // time of 1 dot in [mS]
unsigned int dah_time = 0; // time of 1 dah in [mS]
unsigned int ave_time = 0; // average of 1 dit and 1 dah
unsigned int ADC_reading_1 = 0; // 10bit, range 0 to 1023 for dit_time and wpm
unsigned char txt[6]; // for conversion unsigned int to string
unsigned char letter = 0; // next letter to be transmitted
// Init MCU
AD1PCFG = 0xFFFE; // configure AN0/RB0
JTAGEN_bit = 0; // disable JTAG
// Init pins
TRISA0_bit = 0; // pulse envelope
TRISA1_bit = 0; // letter envelope
TRISA2_bit = 0; // word envelope
TRISA3_bit = 0; // text envelope
TRISD3_bit = 0; // audio output using SOUND function at 800Hz
LATA = 0;
TRISG0_bit = 0; // heartbeat on RG0
TRISB0_bit = 1; // input for ADC on RB0 for dot time and wpm
// Init ADC to set morse speed
ADC1_Init();
// Init sound on RD3 for speaker on board
Sound_Init(&PORTD,3);
// Init TFT
// TFT_Init(320, 240); // old TFT controller
TFT_Init_ILI9341_8bit(320, 240); // new TFT controller Jan 2019
Delay_ms(100);
TFT_BLED = 1;
// Get new readings to set wpm and dit_time.
ADC_reading_1 = ADC1_Get_Sample(0); // RB0, to set dot time and wpm
// Calculate dit_time.
// Good fast morse is about 20wpm.
// Machine morse is about 5 to 50wpm
// ADC is 0-1023, 1024/8 = 256 (right shift >>2)
ADC_reading_1 = ADC_reading_1>>2;
wpm = 10 + ADC_reading_1; // slowest wpm is 10 max 266wpm
dit_time = 1200/wpm; // range is about 120 - 4[mS]
dah_time = 3*dit_time; // range is about 360 - 12[mS]
ave_time = 2*dit_time; // range is about 240 - 8[mS]
// Display on TFT
TFT_Fill_Screen(CL_YELLOW);
TFT_Set_Pen(CL_Black, 1);
TFT_Set_Font(&Tahoma26X29, CL_BLUE, FO_HORIZONTAL);
TFT_Write_Text("WVL_Fusion_Morse_Tx", 3, 10);
TFT_Write_Text("ADC_1", 3, 50);
TFT_Write_Text("WPM", 3, 80);
TFT_Write_Text("DIT[mS]", 3, 110);
TFT_Write_Text("DAH[mS]", 3, 140);
TFT_Write_Text("AVE[mS]", 3, 170);
WordToStr(ADC_reading_1,txt);
TFT_Write_Text(txt,130,50);
WordToStr(wpm,txt);
TFT_Write_Text(txt,130,80);
WordToStr(dit_time,txt);
TFT_Write_Text(txt,130,110);
WordToStr(dah_time,txt);
TFT_Write_Text(txt,130,140);
WordToStr(ave_time,txt);
TFT_Write_Text(txt,130,170);
while(1){
TEXT_ENVELOPE = 1;
// Get the next letter to be sent from TEXT string
for(text_number=0;text_number<=text_length-1;text_number++){
// Check if letter is a space (ASCII=32)
if(TEXT[text_number]==32)Vdelay_ms(4*dit_time); // inter-word delay 1+2+4=7
// Find the morse code for the next letter if not a space
else{
WORD_ENVELOPE = 1;
commas = TEXT[text_number] - 65; // ACII 'A' is 65
// Step through the CODE until the correct number of commas has been counted
code_number = 0;
comma_count = 0;
while(comma_count<commas){
if(CODE[code_number]==',')comma_count++;
code_number++;
}
LETTER_ENVELOPE = 1;
// Send dits and dahs until ',' is read
while(CODE[code_number] !=','){ // read string till next comma
if(CODE[code_number]=='0'){ // if next pulse is a dit
PULSE_ENVELOPE = 1;
Sound_Play(800,dit_time);
PULSE_ENVELOPE = 0;
}
else{ // if next pulse is a dah
PULSE_ENVELOPE = 1;
Sound_Play(800,dah_time);
PULSE_ENVELOPE = 0;
}
code_number++;
// Close envelopes if next code is a ','
if(CODE[code_number]==','){
LETTER_ENVELOPE = 0;
// Close word envelope if next letter is a ' '
if(TEXT[text_number+1]==' ')WORD_ENVELOPE = 0;
// Close word and text envelope if next letter is string terminator
if(TEXT[text_number+1]==0){
WORD_ENVELOPE = 0;
TEXT_ENVELOPE = 0;
}
}
// Inter-pulse delay of 1
Vdelay_ms(dit_time);
}
// Inter-word delay of 1+2=3
Vdelay_ms(2*dit_time);
}
}
// Housekeeping
HEARTBEAT;
delay_ms(1000);
}
}
Code: Select all
////////////////////////////////////////////////////////////////////////////////
// Project: WVL_Fusion_Morse_Rx2 //
// File: Main.c //
// Function: Measure pulse length and period of morse pulses //
// Use change on pin INT0 and 1mS global counter //
// Uses rolling average to get average of dits and dahs //
// MCU: P32MX795F512L with 8MHz on-board Xtal //
// Board: EasyPIC_Fusion_v7_for_PIC32 //
// Power 7-12V input. 3.3V //
// Compiler: mikroC PRO for PIC32 V4.0 //
// Oscillator: 8Mhz Xtal, Div2. PLLX20 = 80MHz //
// Peripheral bus Div by 1 = 80MHz //
// Programmer: On-board Mikro using PGEC2 and PGED2 on RB6 and RB7 //
// Author: WVL //
// Date: May 2023 //
////////////////////////////////////////////////////////////////////////////////
////////////////////////////////////////////////////////////////////////////////
// Decoding is based on the detection of a 'dit' in [mS] //
// A dit = 1 unit of time, A dah = 3 units of time. Average = 2 //
// Record 20 pulses and average then: //
// inter-pulse time = Av_time/2 low state < average //
// inter-letter time = Av_time*3/2 low state > average //
// inter-word time = Av_time*7/2 low_state > average * 3 //
// 0 or 'dit' = Av_time/2 high state < average //
// 1 or 'dah' = Av_time*3/2 high state > average //
// Using interrupts, maintain an average for the last 32 pulses //
////////////////////////////////////////////////////////////////////////////////
////////////////////////////////////////////////////////////////////////////////
// Defines //
#define TRUE 1 //
#define FALSE 0 //
////////////////////////////////////////////////////////////////////////////////
////////////////////////////////////////////////////////////////////////////////
// Global variables //
bit got_hi_time; //
bit got_lo_time; //
unsigned int count_ms = 0; //
unsigned int hi_time = 0; //
unsigned int lo_time = 0; //
////////////////////////////////////////////////////////////////////////////////
////////////////////////////////////////////////////////////////////////////////
void U2(char c){UART2_Write(c);} //
////////////////////////////////////////////////////////////////////////////////
////////////////////////////////////////////////////////////////////////////////
// System clock = 80MHz. Prescale 1:8 = 10MHz = 0.1[uS] perios //
// Preload = 10000. Overflow time = 1 ms //
void InitTimer1(){ //
T1CON = 0x8010; //
T1IP0_bit = 0; // Int priority = 110 = 6 //
T1IP1_bit = 1; //
T1IP2_bit = 1; //
T1IF_bit = 0; //
T1IE_bit = 1; //
PR1 = 10000; //
TMR1 = 0; //
} //
////////////////////////////////////////////////////////////////////////////////
////////////////////////////////////////////////////////////////////////////////
// TIMER1 interrupt every 1[mS] and increment global variables //
// Use less priority than INT0 - used to detect edges of morse pulses //
void Timer1Interrupt() iv IVT_TIMER_1 ilevel 6 ics ICS_AUTO { //
T1IF_bit = 0; // clear interrupt //
lo_time++; //
hi_time++; //
LATA0_bit = ~LATA0_bit; // flash LED //
} //
////////////////////////////////////////////////////////////////////////////////
////////////////////////////////////////////////////////////////////////////////
// INT0 set up with priority and edge (rise=1 fall=0), flag and enable //
void InitINT0(){ //
INT0IP2_bit = 1; // set up INT0 to priority 7 //
INT0IP1_bit = 1; //
INT0IP0_bit = 1; //
INT0EP_bit = 1; // rising edge //
INT0IF_bit = 0; // clear flag //
INT0IE_bit = 1; // enable interrupt //
} //
////////////////////////////////////////////////////////////////////////////////
////////////////////////////////////////////////////////////////////////////////
// INT0 interrupt of alternating rising and falling edge on RD0 pin //
// Use TIMER1 interrupts to measure hi_time and lo_time in [mS] //
void INT0Interrupt() iv IVT_EXTERNAL_0 ilevel 7 ics ICS_AUTO { //
INT0IF_bit = 0; //
INT0IE_bit = 0; // disable interrupt // //
if(PORTD.B0==1){ //
got_hi_time = FALSE; //
got_lo_time = TRUE; //
hi_time = 0; //
LATA1_bit = 1; // LED on //
INT0EP_bit = 0; // next interrupt on falling edge //
} // //
else{ //
got_hi_time = TRUE; //
got_lo_time = FALSE; //
lo_time = 0; //
LATA1_bit = 0; // LED off //
INT0EP_bit = 1; // next interrupt on rising edge //
} //
INT0IE_bit = 1; // enable interrupt //
} //
////////////////////////////////////////////////////////////////////////////////
////////////////////////////////////////////////////////////////////////////////
// Calculate average of all hi pulses. DIT=1, DAH=3, Average = 2 //
// Use 32 pulses so average may be found by right shifting only //
unsigned int Moving_Average(unsigned int next_number){ //
static unsigned int average_array[31] = {0}; // average over 32 pulses //
static unsigned short index = 0; //
static unsigned long sum = 0; //
// Subtract the oldest number from the prev sum, add the new number //
sum = sum - average_array[index] + next_number; //
// Assign the next_number to the position in the array //
average_array[index] = next_number; //
index++; //
if (index >= 31)index = 0; //
// Return the average. Same as total>>5 or divide by 32 //
return sum>>5; //
} //
////////////////////////////////////////////////////////////////////////////////
void main(){
// Block variables
// Comma count 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 1819 20 21 22 23 24
// Morse code A B C D E F G H I J K L M N 0 P Q R S T U V W X Y Z
// Dits and Dahs 01,1000,1010,100,0,0010,110,0000,00,0111,101,0100,11,10,111,0110,1101,010,000,1,001,0001,011,1001,1011,1100
unsigned char DECODE1[] = {69,84}; // E,T with 1 dit or dah
unsigned char DECODE2[] = {73,65,78,77}; // I,A,N,M with 2 dits or dahs
unsigned char DECODE3[] = {83,85,82,87,68,75,71,79}; // S,U,R,W,D,K,G,O 3 dits or dahs
unsigned char DECODE4[] = {72,86,70,99,76,99,80,74,66,88,67,89,90,81}; // H,V,F, ,L, ,P,J,B,X,C,Y,Z,Q 4 dits or dahs
unsigned int commas = 0; // commas until desired morse code
unsigned int comma_count = 0; // commas so far
unsigned short decimal = 0; // conversion binary to digital, eg 1101=13, picks out 'Q' in DECODE4[]
unsigned char in_string[5]; // holds 0's and 1's of a received morse letter
unsigned int length = 0; // number if 1's or 0's in the letter
unsigned int x = 0; // general counter
unsigned int average_time = 1; // average of last 32 pulses
//unsigned char my_string[6]; // conversion of unsigned int to string
// Init MCU
AD1PCFG = 0xFFFF; // configure all pins digital
JTAGEN_bit = 0; // disable JTAG
// Use advanced init for UART2 because peripheral bus is set to div8
// frequency[kHz] should be 10,000 but that does not work???
UART2_Init(115200);
Delay_ms(100); // wait for UART module to stabilize
UART2_Write_Text("\r\nWVL_Fusion_Morse_Rx2\r\n");
// Init pins, A0 for 1mS TIMER 1, A1 for high state
TRISA = 0; // all outputs for LEDs
TRISD0_bit = 1; // input for INT0 and morse pulses
// Check port a leds working
for(x=0;x<10;x++){LATA = ~LATA;delay_ms(100);}
LATA = 0;
InitINT0(); // on RD0
InitTimer1(); // set to interrupt every 1[mS]
got_hi_time = FALSE;
got_lo_time = FALSE;
EnableInterrupts();
x = 0;
// Collect 50 pulse times (dit=1 or dah=3) and average (average=2)
while(x<50){
if(got_hi_time==TRUE){
got_hi_time=FALSE;
Printout(U2,"%u\thi_time:%u\tMoving_Average:%u\r\n",x,hi_time,Moving_Average(hi_time));
x++;
}
}
while(1){
if(got_hi_time==TRUE){
got_hi_time=FALSE;
// use latest hi_time to update moving average of all high pulses
average_time = Moving_Average(hi_time);
if(hi_time>average_time){ // it's a dah = 1!
in_string[length] = '1';
}
else{ // it's a dit = 0!
in_string[length] = '0';
}
length++;
}
if(got_lo_time==TRUE){
got_lo_time=FALSE;
if(lo_time>average_time){ // inter-letter time so DECODE
// Convert receives 0s and 1s to decimal
switch (length){
case 1: decimal = in_string[0]-48;
UART2_Write(DECODE1[decimal]);
break;
case 2: decimal = in_string[0]-48;
decimal = decimal*2 + (in_string[1]-48);
UART2_Write(DECODE2[decimal]);
break;
case 3: decimal = in_string[0]-48;
decimal = decimal*2 + (in_string[1]-48);
decimal = decimal*2 + (in_string[2]-48);
UART2_Write(DECODE3[decimal]);
break;
case 4: decimal = in_string[0]-48;
decimal = decimal*2 + (in_string[1]-48);
decimal = decimal*2 + (in_string[2]-48);
decimal = decimal*2 + (in_string[3]-48);
UART2_Write(DECODE4[decimal]);
}
length = 0;
}
// Insert a space if lo_time is > 6*dits or 3*Average
if(lo_time>=3*average_time) UART2_write(32);
// New line if time_low is > 12 dits or 6*Average
if(lo_time>6*average_time) UART2_Write_Text("\r\n");
}
}
}