quisk-kc4upr/gpiokeyer.c

593 lines
20 KiB
C
Raw Normal View History

//#if defined(ENABLE_GPIO_KEYER)
/***********************************************************************
* gpiokeyer.c
* by Robert A. French (KC4UPR)
*
* This file implements a General Purpose Input Output (GPIO) based
* Morse Code / Continuous Wave (CW) keyer for Quisk. This was derived
* from Richard (N1GP) Koch's Raspberry Pi iambic keyer code, available
* at: github.com/n1gp/iambic-keyer
*
* Modifications included:
* - Removing the GPIO sidetone functionality. Quisk will be creating
* both the sidetone and the actual quadrature CW signal to feed to
* the SDR.
* - Modifying the keying logic (state machine) slightly. I don't know
* if I improved it or not, but changes were based on my tracing thru
* the code and updating things that seemed reasonable.
* - Implementing GPIO key and T/R switching outputs, that could be
* used to drive both an external CW generator, as well a T/R
* switching with a configurable QSK/semi-QSK delay.
* - Making parameters configurable/updateable during runtime by calls
* from Quisk.
* - Making the GPIO pins configurable at startup.
* - Incorporating the keyer into Quisk's overall keying logic logic.
*
* Original N1GP changelog and licensing are below, as well as the notes
* from the even previous source, a Verilog keyer for Hermes Lite.
**********************************************************************/
/*
3/1/2020, Rob French / KC4UPR, I converted the code for use as a
component of Quisk.
10/12/2016, Rick Koch / N1GP, I adapted Phil's verilog code from
the openHPSDR Hermes iambic.v implementation to build
and run on a raspberry PI 3.
1/7/2017, N1GP, adapted to work with Jack Audio, much better
timing.
------------------------------------------------------------------------
This library is free software; you can redistribute it and/or
modify it under the terms of the GNU Library General Public
License as published by the Free Software Foundation; either
version 2 of the License, or (at your option) any later version.
This library is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
Library General Public License for more details.
You should have received a copy of the GNU Library General Public
License along with this library; if not, write to the
Free Software Foundation, Inc., 51 Franklin St, Fifth Floor,
Boston, MA 02110-1301, USA.
------------------------------------------------------------------------
------------------------------------------------------------------------
Copywrite (C) Phil Harman VK6PH May 2014
------------------------------------------------------------------------
The code implements an Iambic CW keyer. The following features are supported:
* Variable speed control from 1 to 60 WPM
* Dot and Dash memory
* Straight, Bug, Iambic Mode A or B Modes
* Variable character weighting
* Automatic Letter spacing
* Paddle swap
Dot and Dash memory works by registering an alternative paddle closure whilst a paddle is pressed.
The alternate paddle closure can occur at any time during a paddle closure and is not limited to being
half way through the current dot or dash. This feature could be added if required.
In Straight mode, closing the DASH paddle will result in the output following the input state. This enables a
straight morse key or external Iambic keyer to be connected.
In Bug mode closing the dot paddle will send repeated dots.
The difference between Iambic Mode A and B lies in what the keyer does when both paddles are released. In Mode A the
keyer completes the element being sent when both paddles are released. In Mode B the keyer sends an additional
element opposite to the one being sent when the paddles are released.
This only effects letters and characters like C, period or AR.
Automatic Letter Space works as follows: When enabled, if you pause for more than one dot time between a dot or dash
the keyer will interpret this as a letter-space and will not send the next dot or dash until the letter-space time has been met.
The normal letter-space is 3 dot periods. The keyer has a paddle event memory so that you can enter dots or dashes during the
inter-letter space and the keyer will send them as they were entered.
Speed calculation - Using standard PARIS timing, dot_period(mS) = 1200/WPM
*/
// NEED TO FIGURE OUT A SIMPLER WAY TO DO THIS... THIS IS JUST TO GET ENABLE_GPIO_KEYER!
//#include <Python.h> // used by quisk.h
//#include <complex.h> // Used by quisk.h
//#include "quisk.h"
#define DEBUG
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <errno.h>
#include <unistd.h>
#include <stdint.h>
#include <fcntl.h>
#include <poll.h>
#include <sched.h>
#include <time.h>
#include <sys/mman.h>
#include <pthread.h>
#include <signal.h>
#include <semaphore.h>
#include <wiringPi.h>
static void* keyer_thread(void *arg);
static pthread_t keyer_thread_id;
// GPIO pins
//#define LEFT_PADDLE_GPIO 22
//#define RIGHT_PADDLE_GPIO 27
//#define KEY_OUT_GPIO 23
//#define PTT_OUT_GPIO 24
static int left_paddle_gpio = 22;
static int right_paddle_gpio = 27;
static int keyer_out_gpio = 23;
static int tr_switch_gpio = 24;
// Keyer modes
#define KEYER_STRAIGHT 0
#define KEYER_MODE_A 1
#define KEYER_MODE_B 2
#define NUM_KEYER_MODES 3
#define NSEC_PER_SEC (1000000000)
enum {
CHECK = 0,
PREDOT,
PREDASH,
SENDDOT,
SENDDASH,
DOTDELAY,
DASHDELAY,
DOTHELD,
DASHHELD,
LETTERSPACE,
HANGTIME,
EXITLOOP
};
static int dot_memory = 0;
static int dash_memory = 0;
static int key_state = 0;
static int kdelay = 0;
static int dot_delay = 0;
static int dash_delay = 0;
static int kcwl = 0;
static int kcwr = 0;
static int *kdot;
static int *kdash;
static int cw_keyer_speed = 20;
static int cw_keyer_weight = 55;
static int cw_keys_reversed = 0;
static int cw_keyer_mode = KEYER_MODE_A;
static int cw_keyer_spacing = 0;
static int cw_active_state = 0;
static sem_t cw_event;
#if defined(DEBUG)
static int cw_event_value;
#endif
static int cw_hangtime_msec = 250;
static int running, keyer_out = 0;
static int cw_keyer_enabled = 1;
// Function prototypes
void keyer_update(void);
void keyer_event(int, int, uint32_t);
void keyer_event_left(void);
void keyer_event_right(void);
void clear_memory(void);
static void set_keyer_out(int);
static void* keyer_thread(void*);
/////
void keyer_update() {
dot_delay = 1200 / cw_keyer_speed;
// will be 3 * dot length at standard weight
dash_delay = (dot_delay * 3 * cw_keyer_weight) / 50;
if (cw_keys_reversed) {
kdot = &kcwr;
kdash = &kcwl;
} else {
kdot = &kcwl;
kdash = &kcwr;
}
// need to actually dynamically set this at some point...
cw_hangtime_msec = (int)(0.25f * 1000.0f);
}
void keyer_event(int gpio, int level, uint32_t tick) {
int state = (cw_active_state == 0) ? (level == 0) : (level != 0);
if (gpio == left_paddle_gpio)
kcwl = state;
else // RIGHT_PADDLE_GPIO
kcwr = state;
if (state) // || cw_keyer_mode == KEYER_STRAIGHT)
sem_post(&cw_event);
}
// Added to support WiringPi, which uses a different type of callback.
void keyer_event_left()
{
int level = digitalRead(left_paddle_gpio);
keyer_event(left_paddle_gpio, level, 0);
#if defined(DEBUG)
sem_getvalue(&cw_event, &cw_event_value);
fprintf(stdout, "left paddle pressed - %d\n", cw_event_value);
#endif
}
// Added to support WiringPi, which uses a different type of callback.
void keyer_event_right()
{
int level = digitalRead(right_paddle_gpio);
keyer_event(right_paddle_gpio, level, 0);
#if defined(DEBUG)
sem_getvalue(&cw_event, &cw_event_value);
fprintf(stdout, "right paddle pressed - %d\n", cw_event_value);
#endif
}
void clear_memory() {
dot_memory = 0;
dash_memory = 0;
}
static void set_keyer_out(int state) {
if (state && tr_switch_gpio) {
digitalWrite(tr_switch_gpio, 1 & cw_keyer_enabled);
}
keyer_out = state;
digitalWrite(keyer_out_gpio, keyer_out & cw_keyer_enabled);
}
static void* keyer_thread(void *arg) {
struct timespec loop_delay;
int interval = 1000000; // 1 ms
int hangtime_elapsed = 0;
while (running) {
#if defined(DEBUG)
sem_getvalue(&cw_event, &cw_event_value);
fprintf(stdout, "waiting - %d\n", cw_event_value);
#endif
sem_wait(&cw_event);
key_state = CHECK;
while (key_state != EXITLOOP) {
if (keyer_out)
hangtime_elapsed = 0;
switch(key_state) {
case HANGTIME:
if (hangtime_elapsed >= cw_hangtime_msec) {
if (tr_switch_gpio) {
#if defined(DEBUG)
sem_getvalue(&cw_event, &cw_event_value);
fprintf(stdout, "hangtime complete, %d msec - %d\n", cw_hangtime_msec, cw_event_value);
#endif
digitalWrite(tr_switch_gpio, 0);
key_state = EXITLOOP;
}
}
// INTENTIONALLY FALLING THROUGH TO 'CHECK' STATE (no 'break')
case CHECK: // check for key press
if (cw_keyer_mode == KEYER_STRAIGHT) { // Straight/External key or bug
if (*kdash) { // send manual dashes
if (!keyer_out) {
set_keyer_out(1);
key_state = HANGTIME; //EXITLOOP;
}
}
else if (*kdot) // and automatic dots
key_state = PREDOT;
else if (key_state == HANGTIME) {
if (keyer_out)
set_keyer_out(0);
} else {
// NOTE: not working right
//set_keyer_out(0);
key_state = EXITLOOP;
}
}
else {
if (*kdot)
key_state = PREDOT;
else if (*kdash)
key_state = PREDASH;
else if (key_state != HANGTIME) {
// Do we really need to do this? Aren't these covered in other states?
//set_keyer_out(0);
key_state = EXITLOOP;
}
}
break;
case PREDOT: // need to clear any pending dots or dashes
clear_memory();
key_state = SENDDOT;
break;
case PREDASH:
clear_memory();
key_state = SENDDASH;
break;
// dot paddle pressed so set keyer_out high for time dependant on speed
// also check if dash paddle is pressed during this time
case SENDDOT:
set_keyer_out(1);
if (kdelay == dot_delay) {
kdelay = 0;
set_keyer_out(0);
key_state = DOTDELAY; // add inter-character spacing of one dot length
}
else kdelay++;
// if Mode A and both paddels are relesed then clear dash memory
if (cw_keyer_mode == KEYER_MODE_A) {
if (!*kdot & !*kdash)
dash_memory = 0;
}
if (*kdash) { // set dash memory
dash_memory = 1;
}
break;
// dash paddle pressed so set keyer_out high for time dependant on 3 x dot delay and weight
// also check if dot paddle is pressed during this time
case SENDDASH:
set_keyer_out(1);
if (kdelay == dash_delay) {
kdelay = 0;
set_keyer_out(0);
key_state = DASHDELAY; // add inter-character spacing of one dot length
}
else kdelay++;
// if Mode A and both padles are relesed then clear dot memory
if (cw_keyer_mode == KEYER_MODE_A) {
if (!*kdot & !*kdash)
dot_memory = 0;
}
if (*kdot) { // set dot memory
dot_memory = 1;
}
break;
// add dot delay at end of the dot and check for dash memory, then check if paddle still held
case DOTDELAY:
if (kdelay == dot_delay) {
kdelay = 0;
if(!*kdot && cw_keyer_mode == KEYER_STRAIGHT) // just return if in bug mode
key_state = HANGTIME;
else if (dash_memory) // dash has been set during the dot so service
key_state = PREDASH;
else key_state = DOTHELD; // dot is still active so service
}
else kdelay++;
if (*kdash) { // set dash memory
dash_memory = 1;
}
break;
// add dot delay at end of the dash and check for dot memory, then check if paddle still held
case DASHDELAY:
if (kdelay == dot_delay) {
kdelay = 0;
if (dot_memory) // dot has been set during the dash so service
key_state = PREDOT;
else key_state = DASHHELD; // dash is still active so service
}
else kdelay++;
if (*kdot) { // set dot memory
dot_memory = 1;
}
break;
// check if dot paddle is still held, if so repeat the dot. Else check if Letter space is required
case DOTHELD:
if (*kdot) // dot has been set during the dash so service
key_state = PREDOT;
else if (*kdash) // has dash paddle been pressed
key_state = PREDASH;
else if (cw_keyer_spacing) { // Letter space enabled so clear any pending dots or dashes
clear_memory();
key_state = LETTERSPACE;
}
else key_state = HANGTIME;
break;
// check if dash paddle is still held, if so repeat the dash. Else check if Letter space is required
case DASHHELD:
if (*kdash) // dash has been set during the dot so service
key_state = PREDASH;
else if (*kdot) // has dot paddle been pressed
key_state = PREDOT;
else if (cw_keyer_spacing) { // Letter space enabled so clear any pending dots or dashes
clear_memory();
key_state = LETTERSPACE;
}
else key_state = HANGTIME;
break;
// Add letter space (3 x dot delay) to end of character and check if a paddle is pressed during this time.
// Actually add 2 x dot_delay since we already have a dot delay at the end of the character.
case LETTERSPACE:
if (kdelay == 2 * dot_delay) {
kdelay = 0;
if (dot_memory) // check if a dot or dash paddle was pressed during the delay.
key_state = PREDOT;
else if (dash_memory)
key_state = PREDASH;
else key_state = HANGTIME; // no memories set so restart
}
else kdelay++;
// save any key presses during the letter space delay
if (*kdot) dot_memory = 1;
if (*kdash) dash_memory = 1;
break;
default:
key_state = EXITLOOP;
}
if (!keyer_out)
hangtime_elapsed++;
if (key_state != EXITLOOP) {
clock_gettime(CLOCK_MONOTONIC, &loop_delay);
loop_delay.tv_nsec += interval;
while (loop_delay.tv_nsec >= NSEC_PER_SEC) {
loop_delay.tv_nsec -= NSEC_PER_SEC;
loop_delay.tv_sec++;
}
clock_nanosleep(CLOCK_MONOTONIC, TIMER_ABSTIME, &loop_delay, NULL);
}
}
}
#if defined(DEBUG)
fprintf(stdout, "[GPIO Keyer] thread should be ending now\n");
#endif
return arg; // did this to ditch compiler warnings... bad idea?
}
int open_key_gpiokeyer(const char * name)
{
int i;
if (sscanf(name, "gpio:%d,%d,%d,%d", &left_paddle_gpio, &right_paddle_gpio, &keyer_out_gpio, &tr_switch_gpio) < 4) {
fprintf(stderr, "Insufficient parameters for GPIO Keyer: %s\n", name);
return -1;
}
#if defined(DEBUG)
fprintf(stdout, "GPIO Keyer selected:\n - left paddle GPIO: %d\n - right paddle GPIO: %d\n - keyer out GPIO: %d\n - T/R switch GPIO: %d\n",
left_paddle_gpio, right_paddle_gpio, keyer_out_gpio, tr_switch_gpio);
#endif
if (wiringPiSetupGpio () < 0) {
fprintf(stderr, "Unable to setup wiringPi: %s\n", strerror (errno));
return -1;
}
pinMode(right_paddle_gpio, INPUT);
pullUpDnControl(right_paddle_gpio, PUD_UP);
usleep(100000);
wiringPiISR(right_paddle_gpio, INT_EDGE_BOTH, keyer_event_right);
pinMode(left_paddle_gpio, INPUT);
pullUpDnControl(left_paddle_gpio, PUD_UP);
usleep(100000);
wiringPiISR(left_paddle_gpio, INT_EDGE_BOTH, keyer_event_left);
if (keyer_out_gpio) {
pinMode(keyer_out_gpio, OUTPUT);
digitalWrite(keyer_out_gpio, 0);
}
if (tr_switch_gpio) {
pinMode(tr_switch_gpio, OUTPUT);
digitalWrite(tr_switch_gpio, 0);
}
keyer_update();
i = sem_init(&cw_event, 0, 0);
running = 1;
i |= pthread_create(&keyer_thread_id, NULL, keyer_thread, NULL);
if(i < 0) {
fprintf(stderr,"pthread_create for keyer_thread failed %d\n", i);
return -1;
}
return 0;
}
void close_key_gpiokeyer(void)
{
#if defined(DEBUG)
fprintf(stdout, "[GPIO Keyer] closing key\n");
#endif
running = 0;
sem_post(&cw_event);
pthread_join(keyer_thread_id, 0);
sem_destroy(&cw_event);
#if defined(DEBUG)
fprintf(stdout, "[GPIO Keyer] keyer closed\n");
#endif
}
int is_key_down_gpiokeyer(void)
{
static int retval;
retval = keyer_out & cw_keyer_enabled;
return retval;
}
void quisk_set_gpio_keyer_mode(int mode)
{
#if defined(DEBUG)
fprintf(stdout, "MODE CHANGE\n");
#endif
if ((mode > -1) && (mode < NUM_KEYER_MODES))
cw_keyer_mode = mode;
}
void quisk_set_gpio_keyer_speed(int wpm)
{
#if defined(DEBUG)
fprintf(stdout, "SPEED CHANGE\n");
#endif
cw_keyer_speed = wpm;
keyer_update();
}
void quisk_set_gpio_keyer_weight(int weight)
{
if ((weight > 32) && (weight < 67))
cw_keyer_weight = weight;
keyer_update();
}
void quisk_set_gpio_keyer_reversed(int flag)
{
cw_keys_reversed = (flag == 0 ? 0 : 1);
keyer_update();
}
void quisk_set_gpio_keyer_strict(int flag)
{
cw_keyer_spacing = (flag == 0 ? 0 : 1);
}
void quisk_set_gpio_keyer_enabled(int flag)
{
cw_keyer_enabled = (flag == 0 ? 0 : 1);
}
/***********************************************************************
* EOF
**********************************************************************/
//#endif