/* * SibKeyerQSK.ino * The Simpler Is Better Morse Code Keyer * (But not so simple that it doesn't do everything your want it to do) * This is the full QSK version with all of the bells and whistles, * the SibKeyer.ino source code has a much-simplified version. * See https://www.solorb.com/elect/hamcirc/SibKeyer/ for the schematic * and other project details. * * Author: G. Forrest Cook, W0RIO * License: GPL Version 3. * * This is a basic keyer design with only a few features including: * - Dot and Dash paddle inputs * - A manual key input which doubles as a TX tune control. * - Buffered inputs via a 74LS14 to keep static zaps away from the * microprocessor and invert the logic for pull-down key inputs * - dot and dash memories for (mode B) iambic keying * - Separate TX and RX outputs with a programmable delay for QSK operation * - A Keyer Active output which can be used for non-QSK T/R switching, * muting a receiver and activating a VFO * - A dash-on output for implementing optional two-tone CW (FSCW) * - A dot-on output for implementing optional two-tone CW (FSCW) * - The Dash and Dot outputs can also be used for optional eye-candy LEDs * - An Analog speed control for smooth speed adjustment * - button activated automatic message (CQ call), stops with paddle/key press * * This has been developed on an Arduino Nano but should work with * other Atmega328P based Arduinos such as the Uno, it will also work * with a standalone Atmega328P IC. * * Project Started: Dec 13, 2024 * Version: see the Serial.println statement below * * The keyer is clocked by an interrupt signal on digital pin 2 (IRQ 0) * which is generated by an NMOS or CMOS 555 timer chip. * This produces a wide-range analog-adjustable speed control. * The first clock pulse from the 555 takes longer than the rest, * by counting multiple clock pulses for dot, dash and space timing, the * first pulse timing error is minimized. * * A completely optional, but fun addition to this keyer involves connecting * differently colored LEDs to the following output pins using 1K current * limit resistors to ground: Active, RxOut, TxOut, DotOut, and Dashout. * This will produce a light show when the keyer is running. * */ // Define the I/O pins const int ClockIrq = 2; // 555 timer output (interrupt 0) const int Message = 3; // CQ message button input const int DotKey = 4; // Dot paddle input (buffered) const int DashKey = 5; // Dash paddle input (buffered) const int TuneKey = 6; // Tune switch input (buffered) const int Active = 7; // 555 timer control and keyer active signal const int TxOut = 8; // Transmit output const int RxOut = 9; // Receive output for QSK const int DashOut = 10; // dash output for two-tone CW const int DotOut = 11; // dot output for two-tone CW const int Dout12 = 12; const int NanoLED = 13; // Red LED on the Arduino Nano // Define the time constants const int Dotcount = 10; // clock pulses for dot on time const int Dashcount = 30; // clock pulses for dash on time const int Spacecount = 10; // clock pulses for space after dot and dash const int Letterspace = 20; // additional clock pulses after sending letter const int Wordspace = 60; // clock pulses for time after a word const int TuneDeb = 10; // manual key debounce time (mSec) const int MsgPause = 350; // timeout after message end const int QSKtime = 5; // delay between !RX and TX for QSK (mSec) // adjust for your qsk hardware, set to // a max of about 15mS, 5 mS is typical const long ActiveTout = 500; // stop the 555 clock after idle time (mSec) const int Blinktime = 70; // Led blink on/off time // Global variables unsigned long Mstime; // running milliseconds counter volatile byte Tcount = 0; // interrupt routine element time counter byte Dotmem = 0; // dot paddle pressed memory byte Dashmem = 0; // dash paddle pressed menory // CQ message: 0=char space, 1=dot, 2=dash, 3=word space. // Adjust the array size for the char count of your message. byte Msgtable[102] = { 2, 1, 2, 1, 0, 2, 2, 1, 2, 3, // CQ (10 char) 2, 1, 2, 1, 0, 2, 2, 1, 2, 3, // CQ (10 char) 2, 1, 2, 1, 0, 2, 2, 1, 2, 3, // CQ (10 char) 2, 1, 2, 1, 0, 2, 2, 1, 2, 3, // CQ (10 char) 2, 1, 1, 0, 1, 3, // DE (6 char) // call sign URCALL (26 char) 1, 1, 2, 0, 1, 2, 1, 0, 2, 1, 2, 1, 0, 1, 2, 0, 1, 2, 1, 1, 0, 1, 2, 1, 1, 3, // call sign URCALL (26 char) 1, 1, 2, 0, 1, 2, 1, 0, 2, 1, 2, 1, 0, 1, 2, 0, 1, 2, 1, 1, 0, 1, 2, 1, 1, 3, 2, 1, 2, 4 // K (4 char) }; int Msg_size = sizeof (Msgtable); void setup() { // Set the parallel lines for input and output pinMode (ClockIrq, INPUT); pinMode (Message, INPUT); pinMode (DotKey, INPUT); pinMode (DashKey, INPUT); pinMode (TuneKey, INPUT); pinMode (Active, OUTPUT); pinMode (TxOut, OUTPUT); pinMode (RxOut, OUTPUT); pinMode (DashOut, OUTPUT); pinMode (DotOut, OUTPUT); pinMode (Dout12, OUTPUT); pinMode (NanoLED, OUTPUT); // Set initial output values digitalWrite(Active, LOW); digitalWrite(TxOut, LOW); digitalWrite(RxOut, HIGH); digitalWrite(DashOut, LOW); digitalWrite(DotOut, LOW); Blink13 (5); // blink the Nano's onboard LED at startup attachInterrupt(0, timerint, RISING); // Int 0 runs timerint() Serial.begin (19200); // Initialize serial port, set the baud rate Serial.println (F("W0RIO SibKeyerQSK, rev: 8 Apr, 2025")); Mstime = millis(); // mark the real time at startup } void loop() { if (Dotmem || digitalRead (DotKey)) // send dot if dot mem or paddle on { sendDot(); Dotmem=0; } if (Dashmem || digitalRead (DashKey)) // send dash if dash mem or paddle on { sendDash(); Dashmem=0; } if (digitalRead (TuneKey)) // manual key and TX tune button tuneTX(); if (digitalRead (Message)) // check for the CQ message button sendMessage(); // Turn off Active state and stop the 555 timer after the timeout period. if (millis() - Mstime >= ActiveTout) digitalWrite(Active, LOW); } void sendDot() // send a dot and a following space { digitalWrite(Active, HIGH); // start the 555 timer and active sig digitalWrite(DotOut, HIGH); // sig only active during dots digitalWrite(RxOut, LOW); delay(QSKtime); // qsk delay digitalWrite(TxOut, HIGH); Tcount=Dotcount; while (Tcount) // delay for dot, check for dash paddle if (digitalRead (DashKey)) Dashmem=1; Tcount=Spacecount; digitalWrite(TxOut, LOW); digitalWrite(DotOut, LOW); delay(QSKtime); // qsk delay digitalWrite(RxOut, HIGH); while (Tcount); // delay for trailing space Mstime = millis(); // mark real time for Active timeout } void sendDash() // send a dash and a following space { digitalWrite(Active, HIGH); // start the 555 timer and active sig digitalWrite(DashOut, HIGH); // sig only active during dashes digitalWrite(RxOut, LOW); delay(QSKtime); // qsk delay digitalWrite(TxOut, HIGH); Tcount=Dashcount; while (Tcount) // delay for dash, check for dot paddle if (digitalRead (DotKey)) Dotmem=1; Tcount=Spacecount; digitalWrite(TxOut, LOW); digitalWrite(DashOut, LOW); delay(QSKtime); // qsk delay digitalWrite(RxOut, HIGH); while (Tcount); // delay for trailing space Mstime = millis(); // mark real time for Active timeout } void tuneTX() { { digitalWrite(Active, HIGH); // start active signal and 555 timer digitalWrite(RxOut, LOW); delay(QSKtime); // qsk delay digitalWrite(TxOut, HIGH); while (digitalRead (TuneKey)) // loop while tune mode is pressed delay (TuneDeb); digitalWrite(TxOut, LOW); delay(QSKtime); // qsk delay digitalWrite(RxOut, HIGH); Mstime = millis(); // mark the real time for active timer } } void sendMessage() // send a memory message { int msg_step = 0; byte stop_msg = 0; byte msg_char; digitalWrite(Active, HIGH); // start the 555 timer and active sig Tcount=Dotcount; while (msg_step < Msg_size) // loop through the canned message { msg_char = Msgtable[msg_step]; switch (msg_char) // deal with the various message chars { case 0: // if 0, delay for a letter space. { Tcount=Letterspace; while (Tcount) // delay, exit on paddle/key activity if (digitalRead(DotKey)||digitalRead(DashKey)||digitalRead(TuneKey)) stop_msg = 1; } break; case 1: // if 1, send a dot { digitalWrite(DotOut, HIGH); // sig only active during dots digitalWrite(RxOut, LOW); delay(QSKtime); // qsk delay digitalWrite(TxOut, HIGH); Tcount=Dotcount; while (Tcount) // delay, exit on paddle/key activity if (digitalRead(DotKey)||digitalRead(DashKey)||digitalRead(TuneKey)) stop_msg = 1; digitalWrite(TxOut, LOW); delay(QSKtime); // qsk delay digitalWrite(RxOut, HIGH); digitalWrite(DotOut, LOW); Tcount=Spacecount; while (Tcount) // delay, exit on paddle/key activity if (digitalRead(DotKey)||digitalRead(DashKey)||digitalRead(TuneKey)) stop_msg = 1; } break; case 2: // if 2, send a dash { digitalWrite(DashOut, HIGH); // sig only active during dashes digitalWrite(RxOut, LOW); delay(QSKtime); // qsk delay digitalWrite(TxOut, HIGH); Tcount=Dashcount; while (Tcount) // delay, exit on paddle/key activity if (digitalRead(DotKey)||digitalRead(DashKey)||digitalRead(TuneKey)) stop_msg = 1; digitalWrite(TxOut, LOW); delay(QSKtime); // qsk delay digitalWrite(RxOut, HIGH); digitalWrite(DashOut, LOW); Tcount=Spacecount; while (Tcount) // delay, exit on paddle/key activity if (digitalRead(DotKey)||digitalRead(DashKey)||digitalRead(TuneKey)) stop_msg = 1; } break; case 3: // if 3, send a word space { Tcount=Wordspace; while (Tcount) // delay, exit on paddle/key activity if (digitalRead(DotKey)||digitalRead(DashKey)||digitalRead(TuneKey)) stop_msg = 1; } break; } if (stop_msg) { digitalWrite(TxOut, LOW); delay(QSKtime); // qsk delay digitalWrite(RxOut, HIGH); delay (MsgPause); // pause after message was stopped break; // end the while loop. } msg_step++; // point to next element } if (!stop_msg) // the message completed normally { digitalWrite(TxOut, LOW); delay(QSKtime); // qsk delay digitalWrite(RxOut, HIGH); } Mstime = millis(); // mark real time for Active timeout } void timerint() { if (Tcount) Tcount--; // count element counter down to 0 } void Blink13(byte count) // blink the red LED on Digital 13 { do { digitalWrite(NanoLED, HIGH); delay(Blinktime); digitalWrite(NanoLED, LOW); delay(Blinktime); } while (count--); }