Prelude
This article is a work in progress. I have not yet posted the complete code below as I am trying to decide if I should make it a single file, leave it as multiple files or provide a link to a public github location. I am actively updating this post and will remove these comments when the posting is complete. I appreciate your patience meanwhile.
Overview
Back in 2012 I published a project that implemented an iambic keyer/transmitter using the Parallax Propeller. This was a simple project and worked well though the chipset used to implement it is not mainstream. The code is still available in the github link posted in that article should you be interested. This article will describe the architecture and implementation details of a new and expanded version of the concepts originally implemented using the propeller but using a more commonly used micro-controller.
Here, I intend to use a garden variety Arduino based on the lowly 8-bit AVR processors as found in the Uno, Nano and other variants (ATMega328). My plan is to publish an annotated source code discussion and at the end the full source that can be simply copied and pasted if you are using an Arduino Uno or equivalent. Porting to a different board should be as simple as remapping I/O pins used for paddles, transmitter keying output, sidetone PWM and CW speed control. If you need help with this, drop me a note at ko7m at arrl dot net and I will help you out.
The code describe here is implemented as a C++ class named (oddly enough) "keyer". Since this class will be reading and controlling several GPIO pins, in general it makes little sense to have more than a single instance of the class although you certainly could implement a multi-operator keyer (single arduino controlling multiple transmitters, keyed by multiple operators), it seems unlikely in general that this would be necessary. I have therefore implemented a singleton static instance of the class within the keyer::getInstance() method that will return a pointer to the singleton class object. This should be sufficient for 99.9% of the use cases in a typical ham radio station.
File Structure
There is a single ko7mKeyer.ino file containing the normal Arduino sketch setup and loop functions. Beyond this file, there is an analog conversion bit of code that handles setup of the ADC (analogue to digital conversion) clocks (analog.h and analog.cpp). There is a timer module that also provides base handlers for pin change interrupts (timer.h and timer.cpp). The rest of the code is in ko7mKeyer.h and ko7mKeyer.cpp. Each of these files will show up as separate text tabs in the Arduino IDE. When you build the project all files will be built as necessary.
Code Walk-through
Here we will visit the code and describe its function section by section. We pull in keyer definitions from ko7mKeyer.h and declare a pointer to the keyer class.
// ko7mKeyer.ino - Iambic ko7m keyer
//
// Copyright 2014-2020 - Jeff Whitlatch - ko7m
//
#include "ko7mKeyer.h"
keyer *pKeyer = NULL;
During the normal Arduino setup, we initialize the serial port for debug output. We also speed up the ADC by modifying the clock prescaler. We want analogRead operations to be a quick as possible. This modification of the clock will significantly reduce the time required to determine the position of the CW speed pot at the cost of a minor amount of noise. We then set up pin change interrupts on the keyer paddle pins and the debounce timeout timer. At that point we are ready to grab the keyer class object pointer and initialize the keyer using default settings.
// Application start initialization
//
void setup()
{
Serial.begin(115200);
Serial.println();
Serial.println("ko7m keyer");
// Change the ADC clock prescaler to speed up ADC conversion. This will help
// the performance of the CW speed potentiometer.
//
adcInit(rate38k46Hz);
// Common timer and pin change interrupt initialization
//
timer::getInstance()->Init();
// Retrieve the singleton instance of the keyer object and hang if it cannot be found
//
pKeyer = keyer::getInstance();
if (pKeyer != NULL)
{
pKeyer->Init();
pKeyer->setSpeed(defaultWPM);
}
else
while(1); // Hang the processor as there is no keyer object
}
Now we have the main program loop which keeps track of the period between calls to the keyer check method to move the keyer state machines along. If the CW speed pot has moved, the new words per minute (WPM) value is returned and displayed on the debug output monitor.
// Main program loop
//
void loop()
{
static unsigned long lastms = 0;
unsigned long msCounter = millis();
unsigned long ms = msCounter - lastms;
static int wpmLast = 0;
// Pass the number of milliseconds since the last call to control the period between
// reads of the CW speed pot to a reasonable rate
//
int wpm = pKeyer->check(ms);
if (wpm != wpmLast)
{
Serial.print(wpm, DEC);
Serial.println(" WPM");
wpmLast = wpm;
}
lastms = msCounter;
}
I will not go into the function of the analog.cpp and analog.h files. They basically provide the ability to change the analog to digital (ADC) conversion clock prescaler which controls how long analogRead takes to return. We want it to be fast without introducing too much noise so that checking the CW speed pot every 1/2 second or so does not interfere with the timing of the keyer. I leave the analysis of these two files to the interested reader. If you have questions, drop me a note at ko7m at arrl dot net and I will do my best to help.
The timer files provide two services, a 3 millisecond debounce timer and pin change interrupt handlers for the keyer paddles. It must be remembered that these two files are very specific to the particular Arduino that is in use, in this case the Arduino Uno.
// timer.h - Timer handler
//
#pragma once
#include <Arduino.h>
class timer
{
public:
static timer *getInstance();
void Init();
};
Timer.h declares the timer class and Timer.cpp provide the implementation of the class. The basic idea behind this code is that certain digital I/O pins are set to use pin change interrupts so that anytime the state of these pins change, an interrupt is called to handle the change. This eliminates polling of the paddles to detect when they are pressed. Since the paddle contacts can bounce, the interrupt handler will reset a 3 ms timeout anytime any of the pins change. If the timer itself times out, this means that none of the pins (paddles) has changed for 3 ms. The handler for the timer timeout event will then update the new debounced pin state.
// timer.cpp - Timer1 handler
//
#include "timer.h"
// Externals
//
// Callback functions for pin change and timer interrupt service routines
//
bool keyerPinChange();
void keyerTimer();
// Create a static singleton instance of this class and return a pointer
// to the instance
//
timer *timer::getInstance()
{
static timer _timerInstance;
return &_timerInstance;
}
// Pin change interrupts for Arduino Uno ATMega328
//
// Digital pin PC Pin Vector Ctl Reg Port PCICR Bit Pin Mask Reg
// D0-D7 PCINT16-23 PCINT2_vect PCIR2 PD PCIE2 PCMSK2
// D8-D13 PCINT0-5 PCINT0_vect PCIR0 PB PCIE0 PCMSK0
// D14-D19 (A0-A5) PCINT8-13 PCINT1_vect PCIR1 PC PCIE1 PCMSK1
//
// Initialize pin change interrupts for rotary encoder, keyer paddles
// and straight key. Obviously this code is hardware configuration specific.
// PCMSK0 needs to be set to enable each pin that will use pin change interrupts.
//
void timer::Init()
{
cli(); // Disable interrupts
// Port B configuration
// Set pin mask for paddle pins 11 and 12 on Port B
//
PCMSK0 |= B00011000;
// Set pin change interrupt enable for port B (paddles)
//
PCICR |= (1<<PCIE0);
// Set timer 1 for 3 ms debounce timeout
//
TCCR1A = 0; // set entire TCCR1A register to 0
TCCR1B = 0; // same for TCCR1B
TCNT1 = 0; // initialize counter value to 0
// set compare match register 3 ms
//
OCR1A = 48000;
// turn on CTC mode
//
TCCR1B |= (1 << WGM12);
// Set bits for no prescaler
//
TCCR1B |= (1 << CS10);
// enable timer compare interrupt
//
TIMSK1 |= (1 << OCIE1A);
sei(); // Re-enable interrupts
}
// Pin change interrupt handlers for any GPIO pin that enables pin
// change interrupts. Calls each handler for each pin or pin group
//
// Pin change ISR for port B
//
ISR(PCINT0_vect)
{
// Call handler and reset timer as long as any handler returns true.
//
if (keyerPinChange())
{
TCNT1 = 0; // Zero the timer
TIMSK1 |= (1 << OCIE1A); // Re-enable it to start the debounce time over
}
}
// Timer ISR - We get here if the debounce timer times out. Call back the paddle
// and key handlers to process debounced digital inputs. Disables Timer 1 until
// the next pin change event.
//
ISR(TIMER1_COMPA_vect)
{
// Call the handlers
//
keyerTimer();
// Disable Timer
//
TIMSK1 &= ~(1 << OCIE1A);
}
Now that we have debug output and the ability to detect and debounce changes in the keyer paddles, we are ready to take on the main keyer code. The default class constructor just initializes some of the state variables to sane values. If you provide a separate I/O pin for a straight key input, it will need be initialized as an input with a pull-up resistor. If a pull-up is provided in hardware, you can comment out the line. A static instance of the class is instantiated in the getInstance method and an accessor function is provided to return a pointer to that class instance.
// keyer.cpp - Iambic keyer
//
// Copyright 2014 - Jeff Whitlatch - ko7m
//
// http://morse-rss-news.sourceforge.net/keyerdoc/modeab.pdf
// http://morse-rss-news.sourceforge.net/keyerdoc/K7QO_Iambic_Paddle.pdf
// https://ag6qr.net/index.php/2017/01/06/iambic-a-or-b-or-does-it-matter/
// https://www.qsl.net/pa2ohh/iambic.htm
//
#include "ko7mKeyer.h"
// Default constructor
//
keyer::keyer()
{
dahState = ditState = 1;
dahBuffer = ditBuffer = dahMemory = ditMemory = dahIambicBuf = ditIambicBuf=0;
pinMode(defaultStraightKeyPin, INPUT_PULLUP);
}
// Create a static singleton instance of this class and return a pointer to the
// instance
//
keyer *keyer::getInstance()
{
static keyer _keyerInstance;
return &_keyerInstance;
}
Read an analogue input connected to speed control pot and return the current WPM value. Ideally the speed control would be a linear pot. If using a log taper pot, you should wire it so the slowest changing end of the taper is at the end of the speed range that you typically spend your time. If you spend most of your time at 30 WPM or more, you will want the slowest tuning of the speed at that end of the range. If on the other hand, you spend most of your time at 15 WPM, you will want the slowest tuning of the speed at the low end of the range. Use a linear taper pot if at all possible for best results at all speeds. We also provide set/get accessor methods for things like current WPM speed, sidetone frequency, etc.
// Read the CW speed pot and return the value in words-per-minute
//
int keyer::readSpeedControl()
{
int wpm = defaultWPM;
int speed = analogRead(speedPin);
// Handle noisy ADC value at extremes. cap anything above or below the
// min/max value
//
if (speed < analogMin) speed = analogMin;
if (speed > analogMax) speed = analogMax;
// Remap analogue input range to min/max WPM
// Speed control min/max should be customized in ko7mKeyer.h to the
// desired range.
// Direct mapping of the pot range to the CW speed range
//
wpm = map(speed, analogMin, analogMax, minWPM, maxWPM);
// Linear taper mapping of a log taper pot
//
//wpm = multiMap(speed, inVal, outVal, 11);
return wpm;
}
// Return current WPM speed value
//
int keyer::getSpeed()
{
return WPM;
}
// Set new WPM speed value, constrained to the min/max values
//
void keyer::setSpeed(int newWPM)
{
WPM = min( max( newWPM, minWPM ), maxWPM );
ditTime = 1200 / WPM;
}
// Returns the current sidetone frequency
//
int keyer::getSidetoneFreq()
{
return sidetoneFreq;
}
Sidetone is a raw PWM output and should be minimally filtered with an R/C low pass filter. Since the Arduino PWM samples at 32 Khz, Nyquist says that the highest frequency we can produce is 16 Khz. In reality Ardunino tone range limits are 31 - 65535 Hz. So putting a low pass filter cutoff around 15 KHz will be more than adequate.
cutoff frequency = 1 / (2 * pi * R * C)
Using a 1000 ohm resistor for R:
15000 = 1/(2000 * pi * C)
C = 1/(30000000 * pi)
C = 1.06103295e-8 farad or 0.0106103295 microfarads.
I suggest using .01 uF with a 1K0 resistor
// Set the generated sidetone frequency.
//
void keyer::setSidetoneFreq(int freq)
{
sidetoneFreq = freq;
}
// Swap the keyer paddles from default of dit on the left and dah on the right
//
void keyer::swapPaddles()
{
ditPin ^= dahPin;
dahPin = ditPin ^ dahPin;
ditPin ^= dahPin;
}
Initialize the Arduino pins used by the keyer. Full control is provide for all pins. speedControl pin must be analogue input. The rest can be either digital or analogue. If using pin change interrupts on paddles, you will need to tend to the pin enable mask and for convenience limit the paddles to the same port on the Arduino. Sidetone pin must support PWM output. If you modify the dit and dah paddle pins, you will need to change the code in keyerPinChange and keyerTimer callback functions at the bottom of this file. You will also need to tend to the timer::Init method to set the pins change interrupt mask to enable interrupts on the new paddle pins and port in the timer.h and timer.cpp files. Contact me for assistance if you have issues and need to change which pins are used.
// Initialize the I/O pins used by the keyer
//
void keyer::initPins(int dit, int dah, int key, int sidetone, int speedControl)
{
ditPin = dit;
dahPin = dah;
keyPin = key;
sidetonePin = sidetone;
speedPin = speedControl;
// If using external pull-up resistors, change INPUT_PULLUP to INPUT
//
pinMode(ditPin, INPUT_PULLUP);
pinMode(dahPin, INPUT_PULLUP);
pinMode(speedPin, INPUT);
pinMode(keyPin, OUTPUT);
pinMode(sidetonePin, OUTPUT);
WPM = defaultWPM;
CPM = 0;
ditBuffer = dahBuffer = 0;
keyerMode = keyerModeSave = defaultMode;
ditTime = 1200 / WPM;
keyFlags = activeHigh;
sidetoneFreq = defaultSidetoneFreq;
If during initPins we find that the dah key is stuck low, this will be interpreted to mean that a TS (tip-sleeve) straight key plug has been inserted in the paddle TRS (tip-ring-sleeve) jack. Since there is no ring contact on a TS plug, the effect will be to short the DAH key to ground. In this event, we will set the keyerMode to keyerModeStraight where the DAH key will be ignored. We are only checking at init time (power-up) so the straight key would need be plugged in before powering on the keyer device in order for this detection to properly function.
if (!digitalRead(dahPin))
{
delay(stuckKeyDelay);
if (!digitalRead(dahPin))
keyerMode = keyerModeSave = keyerModeStraight;
}
}
The keyer design has provision for two mechanisms for shifting from receive to transmit and sending a carrier. A 3.5mm TRS jack is provided for standard iambic paddles along with a simple TR jack for a PTT, transmit key, foot pedal or whatever. Pulling any of these inputs low will start transmitting. The keyer supports Iambic A, Iambic B, straight key, bug and single paddle modes.
Iambic A and B behave as in the description in the links at the top of this posting. Straight key mode allows either the dit paddle or a straight key plugged into it or a separate straight key jack to key the transmitter. Bug mode allows the dit paddle to repeat while the dah paddle must be pressed manually for every dah sent. Single paddle mode is (currently) the same as Iambic A, though in theory no Iambic output can be generated as only the dit or dah paddle switch physically can close at any given time.
TODO: Single paddle and bug mode need to prevent any iambic action while allowing a current symbol to complete. For example if you are using a squeeze paddle while in either of these modes, the closure of both paddle switches should not be physically possible on a single paddle or bug. So, we need to simulate that if possible by detecting the mode and the fact that both paddles are closed and allowing the currently playing symbol to complete and then sending the other symbol. If for example while in bug mode, if you hold the dah paddle and then tap the dit paddle, what should happen? The same thing as on a mechanical bug of course. Tapping the dit paddle would force the release of the dah paddle and play dits as long as the dit paddle was closed, complete the last dit playing and then go key up. This should be doable. A single paddle should do likewise, except pressing the opposite paddle should force the original string of symbols to stop and the opposite symbol to play until the original paddle opens and then closes again.
If we are in straight key mode, ignore the dah paddle. This should allow the straight key to be connected either with a TRS or a TS 3.5mm plug into the paddle TRS jack. The dit paddle should be wired to the tip and the dah paddle to the ring. So, if the dah paddle is continually shorted to ground, we will ignore it and not generate any iambic output as long as it is physically plugged in when the keyer is powered up.
When the straight key (PTT) input is used, the keyer automatically switches to straight key mode. We restore the keyer mode whenever either paddle is pressed to whatever mode we were in before pressing the separate straight key input.
The following code provides for the ability to check debounced paddle state and generate dit and dah symbols as determined by the current mode and state of the keyer.
// Initialize the Arduino pins used by the keyer. The default set of pins is used.
//
void keyer::Init()
{
initPins(defaultDitPin, defaultDahPin, defaultKeyPin, defaultSidetonePin, defaultSpeedPin);
}
// Initialize the Arduino pins used by the keyer. The default set of pins is used.
//
void keyer::initPins()
{
initPins(defaultDitPin, defaultDahPin, defaultKeyPin, defaultSidetonePin, defaultSpeedPin);
}
// If the debounced dit paddle is depressed, or if we have buffered a dit, then
// we should generate a dit.
//
void keyer::checkDitPaddle()
{
bool fSendit = false;
// Save the (inverted) state of the paddle so it will not change while
// we process it
//
if (!ditState)
ditBuffer = 1; // This indicates we need to send a dit as that
// paddle is pressed
fSendit = (ditBuffer || ditMemory);
switch (keyerModeSave)
{
case keyerModeStraight:
if (ditBuffer)
keyDown();
else
keyUp();
break;
default:
if (fSendit)
sendDit();
break;
}
ditBuffer = 0;
}
void keyer::checkDahPaddle()
{
bool fSendit = false;
// There is no dah paddle if a straight key is plugged into the paddle jack
//
if (keyerModeSave == keyerModeStraight)
return;
// Save the (inverted) state of the paddle so it will not change while
// we process it
//
if (!dahState)
dahBuffer = 1;
fSendit = (dahBuffer || dahMemory);
switch (keyerModeSave)
{
// TODO: This needs a fix
//
case keyerModeBug:
if (dahBuffer)
keyDown();
else
keyUp();
break;
default:
if (fSendit)
sendDah();
break;
}
dahBuffer = 0;
}
// This assumes there is a separate straight key or transmit key input
// on the defaultStraightKeyPin pin that will send a carrier as long as
// the input is low
//
void keyer::checkStraightKey()
{
if (!digitalRead(defaultStraightKeyPin))
{
ditBuffer = 1;
keyerMode = keyerModeStraight;
}
}
// Check both paddles and the separate straight key input
//
void keyer::checkPaddles()
{
checkDitPaddle();
checkDahPaddle();
checkStraightKey();
}
// Create dit or dah duration while checking for paddles to change.
// In Iambic modes, we always look for the opposite paddle to the
// element currently being sent. If no current element is being sent
// we look for both paddles.
//
void keyer::delayAndWatchKey(int duration)
{
if (duration > 0)
{
endTime = millis() + duration;
while (millis() < endTime)
{
// Whichever symbol is being sent, watch the other key
//
switch(beingSent)
{
case sendingDit:
if (!dahState) dahMemory = 1;
break;
case sendingDah:
if (!ditState) ditMemory = 1;
// Intentional fall thru
default:
break;
}
}
}
}
// Send a dit while checking for paddles to change
//
void keyer::sendDit()
{
beingSent = sendingDit;
keyDown();
delayAndWatchKey(ditTime); // Wait 1 dit time while checking paddles
keyUp();
beingSent = sendingNothing;
delayAndWatchKey(ditTime);
ditMemory = 0;
}
// Send a dah while checking for paddles to change
//
void keyer::sendDah()
{
beingSent = sendingDah;
keyDown();
delayAndWatchKey(3 * ditTime); // wait 3 dit timees while checking paddles
keyUp();
beingSent = sendingNothing;
delayAndWatchKey(ditTime);
dahMemory = 0;
}
The following methods will provide for keying the I/O pin responsible for keying the transmitter. Both active high and active low output is possible as desired and indicated by keyFlags.
// Key the transmitter by setting the keyPin value high or low as indicated.
// Additionally, sidetone is generated. This can be disable by commenting out
// the appropriate digitalWrite or tone function call below.
//
void keyer::keyDown()
{
switch (keyFlags)
{
case activeLow:
digitalWrite(keyPin, LOW);
break;
case activeHigh:
digitalWrite(keyPin, HIGH);
break;
}
tone(sidetonePin, sidetoneFreq);
}
// Unkey the transmitter and sidetone
//
void keyer::keyUp()
{
switch (keyFlags)
{
case activeLow:
digitalWrite(keyPin, HIGH);
break;
case activeHigh:
digitalWrite(keyPin, LOW);
break;
}
noTone(sidetonePin);
}
The keyer check method is to be called from the main loop passing the number of milliseconds since the last call. This argument is intended to allow timing periodic events by accumulating the amount of time that has passed until periodic intervals are reached. Currently used to limit how often we check the status of the CW speed pot for changes. We check the paddles and send any buffered dits and dahs. We read the CW speed pot and update the keyer speed anytime it changes and return the current wpm value to the caller.
// Conventional keyer processing from the main loop.
//
int keyer::check(uint32_t msCounter)
{
int wpm = getSpeed();
static uint32_t last_time = 0;
// Check for any change in the paddles
checkPaddles();
if (msCounter - last_time >= SPEEDCHECKMS)
{
last_time = 0;
setSpeed(wpm = readSpeedControl());
}
last_time += msCounter;
return wpm; // Pass back any changes in CW speed
}
The last set of methods in the keyer are call-back functions from interrupt handlers whenever the keyer paddes are opened or closed or when the debounce timer expires. This code is very hardware specific. As written, both dit and dah paddle pins must be on PORTB pins 10 and 11 on the UNO. The trick here is to minimize interrupt latency, the code is written to assume a paticular port and set of two pins. If the default pins are changed, the logic below must change to match the new pins and port. Drop me a note if you have questions
// Callback function for pin change interrupt handler. Save the last state of the
// paddle I/O pins. Update the last value from the pin port. Return true if
// either of the pins has changed.
//
bool keyerPinChange()
{
static uint8_t last;
uint8_t prior;
prior = last;
last = (PINB >> 3) & 0x03;
return (prior != last);
}
// Timer 1 callback handler for keyer paddles. If this timer interrupt fires, then
// there have been no changes in either keyer paddle for 3 ms. Update the in-memory
// state variables to represent the position of both paddles.
//
void keyerTimer()
{
keyer *pKeyer = keyer::getInstance();
uint8_t newCode = (PINB >> 3) & 0x3;
uint8_t oldCode = ((pKeyer->dahState << 1) | (pKeyer->ditState));
if (newCode != oldCode)
{
// ditState and dahState will represent debounced paddle state in real time
//
pKeyer->ditState = (newCode >> 0) & 0x01;
pKeyer->dahState = (newCode >> 1) & 0x01;
}
}
The keyer header file defines the various I/O pins used and the keyer class.
// ko7mKeyer.h - Morse code iambic keyer library
//
// Copyright 2014 - Jeff Whitlatch - ko7m
//
#pragma once
#include <Arduino.h>
#include "analog.h"
#include "timer.h"
// Enumerations
//
enum { keyerModeA, keyerModeB, keyerModeSinglePaddle, keyerModeBug, keyerModeStraight, keyerModeFence };
enum { activeHigh, activeLow };
enum { sendingNothing, sendingDit, sendingDah };
// Constants
//
const int defaultDitPin = 11;
const int defaultDahPin = 12;
const int defaultSidetonePin = 10;
const int defaultKeyPin = A1;
const int defaultSpeedPin = A2;
const int defaultStraightKeyPin = PD4;
const int defaultSidetoneFreq = 700;
const int defaultWPM = 35;
const uint8_t maxWPM = 60;
const uint8_t minWPM = 10;
const int cwOffset = 700; // 700 Hz default CW offset
const int defaultMode = keyerModeA;
const int analogMin = 10;
const int analogMax = 1010;
const int stuckKeyDelay = 500; // dahKey stuck for 1/2 second at boot, switch to straight key
const uint32_t SPEEDCHECKMS = 500; // Check speed changes every 1/2 second
class keyer
{
public:
keyer();
keyer(int dit, int dah, int key, int sidetone, int speedControl);
static keyer *getInstance();
void Init();
void initPins();
void initPins(int dit, int dah, int key, int sidetone, int speedControl);
int check(uint32_t msCounter);
int readSpeedControl();
int getSpeed();
void setSpeed(int newWPM);
int getSidetoneFreq();
void setSidetoneFreq(int newFreq);
void swapPaddles();
void checkPaddles();
void sendBuffer();
int keyerModeSave;
uint8_t ditState; // The actual status of the paddle I/O pins in real time, debounced
uint8_t dahState;
private:
uint8_t ditBuffer; // Buffer of paddle state during processing
uint8_t dahBuffer;
uint8_t ditMemory; // Single symbol buffers
uint8_t dahMemory;
uint8_t ditIambicBuf; // Iambic B symbol buffers
uint8_t dahIambicBuf;
int WPM;
int CPM;
int keyerMode;
int ditTime;
int keyFlags;
int beingSent;
int ditPin;
int dahPin;
int keyPin;
int sidetonePin;
int speedPin;
int sidetoneFreq;
uint32_t endTime;
void checkDitPaddle();
void checkDahPaddle();
void checkStraightKey();
void delayAndWatchKey(int duration);
void sendDit();
void sendDah();
void keyDown();
void keyUp();
// CW speed potentiometer interpolation table
// The speed pot is a log taper device. Here we create an interpolation table to remap
// the output to be more linear. The following table is used to build the interpolation
// table used below.
//Position Resistance ADC WPM
// 0.00% 0.00% 0 10
// 10.00% 2.00% 20 13
// 20.00% 4.00% 41 16
// 30.00% 6.00% 61 19
// 40.00% 8.00% 82 22
// 50.00% 10.00% 102 25
// 60.00% 20.00% 205 28
// 70.00% 35.00% 358 31
// 80.00% 55.00% 563 34
// 90.00% 82.00% 839 37
// 100.00% 100.00% 1023 40
// Input values (ADC values)
int inVal[11] = { 0, 10, 20, 41, 82, 102, 205, 358, 563, 829, 1023 };
// Output values (WPM values)
int outVal[11] = { 10, 13, 16, 19, 22, 25, 28, 31, 34, 37, 40 };
};