Sunday, March 8, 2015

Arduino Due - USB keyboard with PSK31 encoder

Updated: I have updated the code below to fix a compiler warning error.  The code below also requires the USBHost library be installed.  See the comments at the end of the posting to get the details of how to do this if you have any questions.

I have spent a few minutes today building an example PSK31 keyboard based on the Arduino Due and the recent work I posted to generate PSK31 from an Arduino Uno or Mega2560.

Firstly, I will discuss the necessary changes to implement my PSK31 encoder on the Due and then the changes needed to incorporate a USB keyboard as the beginnings of a full-function PSK31 encoder.

Readers of my previous blog posts on Generating PSK31 with an Arduino (a total of 4 posts, the other three of which are linked with the referenced article) will know that I generate PSK31 tones using a timer interrupt to construct the audio waveform from (un-optimized) memory tables of sinusoidal information.  If interested, you should also check out the recent excellent work by Jim Harvey (WB8NBS) to optimize the size of memory-resident sinusoidal data from my previous work on software DDS on the Arduino.

Generating timer interrupts on the Due is quite flexible and I have previously posted an extensive article on the topic of using Timer Counters on the Due.  Please have a look for all the gory details.

In porting my PSK31 Arduino code over to the Due, the main change that was necessary was surrounding how timers work.

// Set up Timer/Counter (TC)
void TimerStart(Tc *tc, uint32_t channel, IRQn_Type irq, uint32_t freq)
{
   pmc_set_writeprotect(false);
   pmc_enable_periph_clk(irq);
   TC_Configure(tc, channel, TC_CMR_WAVE | TC_CMR_WAVSEL_UP_RC |
                             TC_CMR_TCCLKS_TIMER_CLOCK1);
   uint32_t rc = VARIANT_MCK / 2 / freq;
   TC_SetRC(tc, channel, rc);
   TC_Start(tc, channel);
   tc->TC_CHANNEL[channel].TC_IER=  TC_IER_CPCS; // | TC_IER_CPAS;
   tc->TC_CHANNEL[channel].TC_IDR=~(TC_IER_CPCS); // | TC_IER_CPAS);
   NVIC_EnableIRQ(irq);

}

My previous article on Timers on the Due uses a variant of this function to set up a timer to interrupt at an appropriate rate to generate all phase points of a sinusoid at the desired freqency.

The only change here is to use a different timer clock.  In this case I chose to use TIMER_CLOCK1 which is the master clock (84 MHz) divided by 2.  We divide this by the desired frequency of 31.25 kHz and set a counter match interrupt at this value.  The counter will now count (at 42 MHz) up to 1344 and fire an interrupt, effectively dividing 42 MHz down to 31.250 kHz.

The timer handler as described in my article on Due timers is called TC3_Handler because we are using Timer/Counter 1 channel 0.  Please see the table in my previous article for the mapping of Timer/Counter channels to handler function names.

// Grab the next phase point from the table and 
// set the amplitude value of the sinusoid being
// constructed.  For a one bit, set 500 phase points
// (20 amplitudes of 25 samples each) to ramp
// down to zero and then immediately back up to full
// amplitude for a total of 1024 phase points.
//
// For a zero bit, there is not amplitude or phase
// change, so we just play 20 phase points of
// full amplitude data 25 times for a total of 1000
// phase points.
//
// Each end of the ramp-up table starts with a zero 
// crossing byte, so there is one extra byte in
// the table (501 entries).  Ramping up plays bytes
// 0 -> 499 and ramping down plays bytes 500 -> 1
// allowing each direction to start at the zero
// crossing point.

//TC1 ch 0
void TC3_Handler()
{
   TC_GetStatus(TC1, 0);  // Reset TC
   
  // Set current amplitude value for the sine wave 
  // being constructed taking care to invert the
  // phase when processing the table in reverse order.
  analogWrite(DAC0, (*pbSine * ix * phase) & 0xff);
  pbSine += ix;
  
  // At the half bit time, we need to change phase
  // if generating a zero bit
  if (0 == --cbHalfBit)
  {
    cbHalfBit = 500;  // Reset 1/2 PSK bit time phase counter
    
    // Get the next varichar bit to send
    if (fFullBit)
    {
      // Count the number of sequential zero bits
      if (fSendOne = vcChar & 1) cZeroBits = 0; else cZeroBits++;

      // Shift off the most least significant bit.
      vcChar >>= 1;
      
      // If we have sent two zero bits, end of character has occurred
      if (cZeroBits > maxZeroBits)
      {
        cZeroBits = 0;
        
        // If send buffer not empty, get next varicode character
        if (head != tail)
        {
          // Assumes a 256 byte buffer as index increments modulo 256
          vcChar = varicode[rgchBuf[head++]];
        }
        else
          if (maxZeroBits > 2) stopTX(); else maxZeroBits = 4;
      }
    }
    fFullBit = !fFullBit;  // Toggle end of full bit flag
    
    // When we get done ramping down, phase needs to
    // change unless we are sending a one bit
    if (ix < 0 &&!fSendOne) phase = -phase;
  }
  
  // At the end of the table for the bit being
  // generated, we need to change direction
  // and process the table in the other direction.
  if (0 == --cbDirection)
  {
    cbDirection = fSendOne ? 20 : 500;
    ix = -ix;
  }

}

I added two functions to start and stop the PSK engine as appropriate.

// True if the PSK engine is currently sending data
uint8_t fIsSending = false;

// Start the PSK engine transmitting if it is not already
void startTX()
{
  if (fIsSending) return;
    
  // Set timer to 31.25 kHz for a period of 32 us and start it
  TimerStart(TC1, 0, TC3_IRQn, 31250);
  fIsSending = true;
}

// Stop the PSK engine transmitting
void stopTX()
{
  TC_Stop(TC1, 0);
  fIsSending = false;

}

I also put the DAC into 8 bit mode to be compatible with my sinusoidal tables, though 12 bit mode could be easily accomodated.


  // Use 8 bits in analogue conversions
  analogWriteResolution(8);
  analogReadResolution(8);

So, the remaining details are in the integration of a USB keyboard to the Due.  Fortunately, the out-of-box experience with the Due supports USB host directly, so there was precious little for me to do here.


// Include the keyboard controller library to add USB keyboard support

#include <KeyboardController.h>

// Initialize the USB controller
USBHost usb;

// Attach keyboard controller to USB

KeyboardController keyboard(usb);

I then, added some example keyboard support functions to detect when a key is pressed and to enable some simple example keyboard macros.

// Keyboard functionality follows
// Intercept key release
void keyReleased()
{
  char rgchTemp[] = {0, 0};
  const char *szText = &rgchTemp[0];
  int  cchText = 0;
  
  switch (keyboard.getOemKey())
  {
    case 58:  // F1
      szText = "\nCQ CQ CQ de ko7m ko7m ko7m"
               "\nCQ CQ CQ de ko7m ko7m ko7m"
               "\nCQ CQ CQ de ko7m ko7m ko7m CN87xp pse k\n";
      break;
      
    case 59:  // F2
      szText = "\n Me: Jeff - ko7m"
               "\nLoc: Redmond, Washington USA"
               "\nGrd: CN87xp"
               "\nQSL: eQSL, direct or bureau"
               "\nRig: Homebrew SSB transceiver"
               "\nAnt: G5RV at about 8 metres"
               "\nPSK: Homebrew Arduino Due software"
               "\n";
      break;

    case 60:  // F3
      szText = "\n\nSo, back to you!\n";
      break;
      
    case 61:  // F4
      szText = "\nTnx for the nice QSO"
               "\nBest of 73's to you and yours!"
               "\n";
      break;
      
    case 62:  // F5
      szText = " de jeff -ko7m\n\n";
      break;
    
    case 40:  // CR
      szText = "\r\n";
      break;
      
    default:
      rgchTemp[0] = keyboard.getKey();
      rgchTemp[1] = 0;
      break;
  }
  
  cchText = strlen(szText);
  while (cchText--)
    rgchBuf[tail++] = *szText++;

  startTX();  // Start the PSK engine if it is stopped

}

That is about it!  A couple of things are left to the reader to provide as desired.  I have not created a complete turn-key solution, but rather have provided the main building blocks for you to base a complete solution of your own design on as desired.

  • You get to type blind.  There is no display currently on my Due, nor am I using the serial terminal in order to provide feedback as to what you are typing.  This could be done with a local LCD display, the serial monitor screen or a separate display connected to the second USB port or connected to any of the serial ports.
  • You must provide the necessary DAC buffer electronics in order to not load the DAC pin of your Due as it will not source much current before the output FET blows up.  I am using a simple LM386-based powered computer speaker with a DC blocking capacitor in series with a current limiting resistor.

    IMPORTANT: You will blow the output FET on your Due if you hook low impedance speakers or headphones directly to the DAC pin - don't do it.


The Due outputs a true analogue signal, so no external integrator is required.  I bring the audio out directly from the DAC0 pin to the buffering circuitry (yellow wire).  Brown wire is the ground connection.




The keyboard connects to the Due on the "Native USB Port" (right hand USB connector, furthest from the power connector) and the PC connects to the programming port (left hand USB connector, nearest the power connector).


Normally, you would use a USB OTG (USB on-the-go) cable to connect a keyboard to the Due.

However, since my cable has this nice right-angle connector on the micro-USB end of the cable, it prevented me from using it on the Due when the programming cable is plugged in.  So I instead used a standard USB to micro-USB cable with a female-to-female adapter between that cable and the keyboard cable.


PS2 keyboards could be easily implemented on the Arduino UNO and Mega2560.  Some reference information can be found here.  Any USB host functionality required to support HID devices such as a USB keyboard would be a rather large effort to implement.  PS2 support is much simpler and would be a good option for non-Due Arduino boards.

There is a small start-up delay (of a couple seconds) on the USB host functionality, so if you power on the Due and start typing immediately, there may be some keystrokes that are lost.  I have not investigated yet why this occurs in the standard USB library.

Have fun with this and let me know if you adapt any of this to your purposes.  I have included the complete listing of my code below for your reference.

As always, if you encounter problems or questions, I am happy to try and help.  Drop me a note by replying here or by email at ko7m at arrl dot net.

// PSK31 audio generation - Arduino Due version
// Jeff Whitlatch - ko7m

// Include the keyboard controller library to add USB keyboard support
#include <KeyboardController.h>

// We are going to generate a 1 kHz centre frequency tone.  
// Each 1 kHz cycle of the sinusoid will be generated from
// 32 eight bit amplitude samples.  The period of a 1 kHz tone
// is 1 ms.  Each of the 32 samples per cycle has a period
// of 31.25 us.  We will construct each sinusoid from a 32 byte
// per cycle lookup table of amplitude values ranging from
// 0x00 to 0xff where the zero crossing value is 0x80.

// The PSK31 character bit time is 31.25 ms constructed of 1024
// samples.  A binary zero is represented by a phase reversal
// while a binary 1 is represented by the lack of a phase reversal.

// Characters are encoded with a variable bit length code (varicode)
// where the length of each character is inversely
// proportional to the frequency of use in the english language 
// of that character.  Characters are encoded with a bit
// pattern where there are no sequential zero bits.  Two zero bits
// in a row signify the end of a character.

// Varicode lookup table
//
// This table defines the PKS31 varicode.  There are 128 entries,
// corresponding to ASCII characters 0-127 with two bytes for each entry.
// The bits for the varicode are to be shifted out LSB-first.
//
// More than one zero in sequence signifies the end of the character.
// For modulation, a 0 represents a phase reversal while a 1 
// represents a steady-state carrier.

uint16_t varicode[] = {
  0x0355,  // 0 NUL
  0x036d,  // 1 SOH
  0x02dd,  // 2 STX
  0x03bb,  // 3 ETX
  0x035d,  // 4 EOT
  0x03eb,  // 5 ENQ
  0x03dd,  // 6 ACK
  0x02fd,  // 7 BEL
  0x03fd,  // 8 BS
  0x00f7,  // 9 HT
  0x0017,  // 10 LF
  0x03db,  // 11 VT
  0x02ed,  // 12 FF
  0x001f,  // 13 CR
  0x02bb,  // 14 SO
  0x0357,  // 15 SI
  0x03bd,  // 16 DLE
  0x02bd,  // 17 DC1
  0x02d7,  // 18 DC2
  0x03d7,  // 19 DC3
  0x036b,  // 20 DC4
  0x035b,  // 21 NAK
  0x02db,  // 22 SYN
  0x03ab,  // 23 ETB
  0x037b,  // 24 CAN
  0x02fb,  // 25 EM
  0x03b7,  // 26 SUB
  0x02ab,  // 27 ESC
  0x02eb,  // 28 FS
  0x0377,  // 29 GS
  0x037d,  // 30 RS
  0x03fb,  // 31 US
  0x0001,  // 32 SP
  0x01ff,  // 33 !
  0x01f5,  // 34 @
  0x015f,  // 35 #
  0x01b7,  // 36 $
  0x02ad,  // 37 %
  0x0375,  // 38 &
  0x01fd,  // 39 '
  0x00df,  // 40 (
  0x00ef,  // 41 )
  0x01ed,  // 42 *
  0x01f7,  // 43 +
  0x0057,  // 44 ,
  0x002b,  // 45 -
  0x0075,  // 46 .
  0x01eb,  // 47 /
  0x00ed,  // 48 0
  0x00bd,  // 49 1
  0x00b7,  // 50 2
  0x00ff,  // 51 3
  0x01dd,  // 52 4
  0x01b5,  // 53 5
  0x01ad,  // 54 6
  0x016b,  // 55 7
  0x01ab,  // 56 8
  0x01db,  // 57 9
  0x00af,  // 58 :
  0x017b,  // 59 ;
  0x016f,  // 60 <
  0x0055,  // 61 =
  0x01d7,  // 62 >
  0x03d5,  // 63 ?
  0x02f5,  // 64 @
  0x005f,  // 65 A
  0x00d7,  // 66 B
  0x00b5,  // 67 C
  0x00ad,  // 68 D
  0x0077,  // 69 E
  0x00db,  // 70 F
  0x00bf,  // 71 G
  0x0155,  // 72 H
  0x007f,  // 73 I
  0x017f,  // 74 J
  0x017d,  // 75 K
  0x00eb,  // 76 L
  0x00dd,  // 77 M
  0x00bb,  // 78 N
  0x00d5,  // 79 O
  0x00ab,  // 80 P
  0x0177,  // 81 Q
  0x00f5,  // 82 R
  0x007b,  // 83 S
  0x005b,  // 84 T
  0x01d5,  // 85 U
  0x015b,  // 86 V
  0x0175,  // 87 W
  0x015d,  // 88 X
  0x01bd,  // 89 Y
  0x02d5,  // 90 Z
  0x01df,  // 91 [
  0x01ef,  // 92 
  0x01bf,  // 93 ]
  0x03f5,  // 94 ^
  0x016d,  // 95 _
  0x03ed,  // 96 `
  0x000d,  // 97 a
  0x007d,  // 98 b
  0x003d,  // 99 c
  0x002d,  // 100 d
  0x0003,  // 101 e
  0x002f,  // 102 f
  0x006d,  // 103 g
  0x0035,  // 104 h
  0x000b,  // 105 i
  0x01af,  // 106 j
  0x00fd,  // 107 k
  0x001b,  // 108 l
  0x0037,  // 109 m
  0x000f,  // 110 n
  0x0007,  // 111 o
  0x003f,  // 112 p
  0x01fb,  // 113 q
  0x0015,  // 114 r
  0x001d,  // 115 s
  0x0005,  // 116 t
  0x003b,  // 117 u
  0x006f,  // 118 v
  0x006b,  // 119 w
  0x00fb,  // 120 x
  0x005d,  // 121 y
  0x0157,  // 122 z
  0x03b5,  // 123 {
  0x01bb,  // 124 |
  0x02b5,  // 125 }
  0x03ad,  // 126 ~
  0x02b7   // 127 (del)
};

// 20 cycles of 25 samples each (500 bytes) of ramp-up
// sinusoid information.  There is an extra byte at the
// end of the table with the value 0x80 which allows the
// first byte to always be at the zero crossing point
// whether ramping up or down.
//
// Ramp profile is cosine shape.
char data[] =
{
  0x80,0x82,0x85,0x86,0x88,0x88,0x88,0x86,0x85,0x82,
  0x80,0x7E,0x7B,0x7A,0x78,0x78,0x78,0x7A,0x7B,0x7E,
  0x80,0x85,0x89,0x8D,0x8F,0x90,0x8F,0x8D,0x89,0x85,
  0x80,0x7B,0x77,0x73,0x71,0x70,0x71,0x73,0x77,0x7B,
  0x80,0x87,0x8E,0x93,0x97,0x98,0x97,0x93,0x8E,0x87,
  0x80,0x79,0x72,0x6D,0x69,0x68,0x69,0x6D,0x72,0x79,
  0x80,0x8A,0x92,0x99,0x9D,0x9F,0x9D,0x99,0x92,0x8A,
  0x80,0x76,0x6E,0x67,0x63,0x61,0x63,0x67,0x6E,0x76,
  0x80,0x8C,0x97,0xA0,0xA5,0xA7,0xA5,0xA0,0x97,0x8C,
  0x80,0x74,0x69,0x60,0x5B,0x59,0x5B,0x60,0x69,0x74,
  0x80,0x8F,0x9C,0xA6,0xAD,0xAF,0xAD,0xA6,0x9C,0x8F,
  0x80,0x71,0x64,0x5A,0x53,0x51,0x53,0x5A,0x64,0x71,
  0x80,0x91,0xA0,0xAC,0xB3,0xB6,0xB3,0xAC,0xA0,0x91,
  0x80,0x6F,0x60,0x54,0x4D,0x4A,0x4D,0x54,0x60,0x6F,
  0x80,0x93,0xA4,0xB1,0xBA,0xBD,0xBA,0xB1,0xA4,0x93,
  0x80,0x6D,0x5C,0x4F,0x46,0x43,0x46,0x4F,0x5C,0x6D,
  0x80,0x95,0xA8,0xB7,0xC1,0xC4,0xC1,0xB7,0xA8,0x95,
  0x80,0x6B,0x58,0x49,0x3F,0x3C,0x3F,0x49,0x58,0x6B,
  0x80,0x97,0xAC,0xBD,0xC7,0xCB,0xC7,0xBD,0xAC,0x97,
  0x80,0x69,0x54,0x43,0x39,0x35,0x39,0x43,0x54,0x69,
  0x80,0x99,0xB0,0xC2,0xCD,0xD1,0xCD,0xC2,0xB0,0x99,
  0x80,0x67,0x50,0x3E,0x33,0x2F,0x33,0x3E,0x50,0x67,
  0x80,0x9B,0xB3,0xC6,0xD3,0xD7,0xD3,0xC6,0xB3,0x9B,
  0x80,0x65,0x4D,0x3A,0x2D,0x29,0x2D,0x3A,0x4D,0x65,
  0x80,0x9D,0xB7,0xCB,0xD8,0xDD,0xD8,0xCB,0xB7,0x9D,
  0x80,0x63,0x49,0x35,0x28,0x23,0x28,0x35,0x49,0x63,
  0x80,0x9E,0xBA,0xCF,0xDD,0xE2,0xDD,0xCF,0xBA,0x9E,
  0x80,0x62,0x46,0x31,0x23,0x1E,0x23,0x31,0x46,0x62,
  0x80,0xA0,0xBD,0xD3,0xE2,0xE7,0xE2,0xD3,0xBD,0xA0,
  0x80,0x60,0x43,0x2D,0x1E,0x19,0x1E,0x2D,0x43,0x60,
  0x80,0xA1,0xBF,0xD7,0xE6,0xEB,0xE6,0xD7,0xBF,0xA1,
  0x80,0x5F,0x41,0x29,0x1A,0x15,0x1A,0x29,0x41,0x5F,
  0x80,0xA2,0xC1,0xDA,0xEA,0xEF,0xEA,0xDA,0xC1,0xA2,
  0x80,0x5E,0x3F,0x26,0x16,0x11,0x16,0x26,0x3F,0x5E,
  0x80,0xA4,0xC4,0xDD,0xED,0xF3,0xED,0xDD,0xC4,0xA4,
  0x80,0x5C,0x3C,0x23,0x13,0x0D,0x13,0x23,0x3C,0x5C,
  0x80,0xA4,0xC5,0xDF,0xF0,0xF6,0xF0,0xDF,0xC5,0xA4,
  0x80,0x5C,0x3B,0x21,0x10,0x0A,0x10,0x21,0x3B,0x5C,
  0x80,0xA5,0xC7,0xE2,0xF3,0xF9,0xF3,0xE2,0xC7,0xA5,
  0x80,0x5B,0x39,0x1E,0x0D,0x07,0x0D,0x1E,0x39,0x5B,
  0x80,0xA6,0xC8,0xE4,0xF5,0xFB,0xF5,0xE4,0xC8,0xA6,
  0x80,0x5A,0x38,0x1C,0x0B,0x05,0x0B,0x1C,0x38,0x5A,
  0x80,0xA7,0xC9,0xE5,0xF7,0xFD,0xF7,0xE5,0xC9,0xA7,
  0x80,0x59,0x37,0x1B,0x09,0x03,0x09,0x1B,0x37,0x59,
  0x80,0xA7,0xCA,0xE6,0xF8,0xFE,0xF8,0xE6,0xCA,0xA7,
  0x80,0x59,0x36,0x1A,0x08,0x02,0x08,0x1A,0x36,0x59,
  0x80,0xA7,0xCB,0xE7,0xF9,0xFF,0xF9,0xE7,0xCB,0xA7,
  0x80,0x59,0x35,0x19,0x07,0x01,0x07,0x19,0x35,0x59,
  0x80,0xA7,0xCB,0xE7,0xF9,0xFF,0xF9,0xE7,0xCB,0xA7,
  0x80,0x59,0x35,0x19,0x07,0x01,0x07,0x19,0x35,0x59,
  0x80
  };

// The last 20 bytes (21 with the extra on the end)
// define a single cycle of full amplitude sinusoid.
#define one  (&data[24*20])      // Sine table pointer for a one bit
#define zero (&data[25*20])      // Sine table pointer for a zero bit

// Variables used by the timer ISR to generate sinusoidal information.
volatile char    rgchBuf[256];    // Buffer of text to send
volatile uint8_t head = 0;        // Buffer head (next character to send)
volatile uint8_t tail = 0;        // Buffer tail (next insert point)

volatile uint16_t vcChar = 0;     // Current varicode char being sent

volatile int   cbHalfBit = 500;   // 500 phase points required for PSK 1/2 bit time

volatile char *pbSine = zero;
volatile int   cbDirection = 500;
volatile char  fSendOne = false;

volatile int ix       = -1;
volatile int phase    = 1;
volatile char fFullBit = 0;

volatile char cZeroBits = 0;
volatile char maxZeroBits = 2;

// Initialize the USB controller
USBHost usb;

// Attach keyboard controller to USB
KeyboardController keyboard(usb);

// True if the PSK engine is currently sending data
uint8_t fIsSending = false;

// Start the PSK engine transmitting if it is not already
void startTX()
{
  if (fIsSending) return;
    
  // Set timer to 31.25 kHz for a period of 32 us and start it
  TimerStart(TC1, 0, TC3_IRQn, 31250);
  fIsSending = true;
}

// Stop the PSK engine transmitting
void stopTX()
{
  TC_Stop(TC1, 0);
  fIsSending = false;
}
  
// Set up Timer/Counter (TC)
void TimerStart(Tc *tc, uint32_t channel, IRQn_Type irq, uint32_t freq)
{
   pmc_set_writeprotect(false);
   pmc_enable_periph_clk(irq);
   TC_Configure(tc, channel, TC_CMR_WAVE | TC_CMR_WAVSEL_UP_RC |
                             TC_CMR_TCCLKS_TIMER_CLOCK1);
   uint32_t rc = VARIANT_MCK / 2 / freq;
   TC_SetRC(tc, channel, rc);
   TC_Start(tc, channel);
   tc->TC_CHANNEL[channel].TC_IER=  TC_IER_CPCS; // | TC_IER_CPAS;
   tc->TC_CHANNEL[channel].TC_IDR=~(TC_IER_CPCS); // | TC_IER_CPAS);
   NVIC_EnableIRQ(irq);
}

// Timer 2 interrupt service routine (ISR).
//
// Grab the next phase point from the table and 
// set the amplitude value of the sinusoid being
// constructed.  For a one bit, set 500 phase points
// (20 amplitudes of 25 samples each) to ramp
// down to zero and then immediately back up to full
// amplitude for a total of 1024 phase points.
//
// For a zero bit, there is not amplitude or phase
// change, so we just play 20 phase points of
// full amplitude data 25 times for a total of 1000
// phase points.
//
// Each end of the ramp-up table starts with a zero 
// crossing byte, so there is one extra byte in
// the table (501 entries).  Ramping up plays bytes
// 0 -> 499 and ramping down plays bytes 500 -> 1
// allowing each direction to start at the zero
// crossing point.

//TC1 ch 0
void TC3_Handler()
{
   TC_GetStatus(TC1, 0);  // Reset TC
   
  // Set current amplitude value for the sine wave 
  // being constructed taking care to invert the
  // phase when processing the table in reverse order.
  analogWrite(DAC0, (*pbSine * ix * phase) & 0xff);
  pbSine += ix;
  
  // At the half bit time, we need to change phase
  // if generating a zero bit
  if (0 == --cbHalfBit)
  {
    cbHalfBit = 500;  // Reset 1/2 PSK bit time phase counter
    
    // Get the next varichar bit to send
    if (fFullBit)
    {
      // Count the number of sequential zero bits
      if (fSendOne = vcChar & 1) cZeroBits = 0; else cZeroBits++;

      // Shift off the most least significant bit.
      vcChar >>= 1;
      
      // If we have sent two zero bits, end of character has occurred
      if (cZeroBits > maxZeroBits)
      {
        cZeroBits = 0;
        
        // If send buffer not empty, get next varicode character
        if (head != tail)
        {
          // Assumes a 256 byte buffer as index increments modulo 256
          vcChar = varicode[rgchBuf[head++]];
        }
        else
          if (maxZeroBits > 2) stopTX(); else maxZeroBits = 4;
      }
    }
    fFullBit = !fFullBit;  // Toggle end of full bit flag
    
    // When we get done ramping down, phase needs to
    // change unless we are sending a one bit
    if (ix < 0 &&!fSendOne) phase = -phase;
  }
  
  // At the end of the table for the bit being
  // generated, we need to change direction
  // and process the table in the other direction.
  if (0 == --cbDirection)
  {
    cbDirection = fSendOne ? 20 : 500;
    ix = -ix;
  }
}

// Keyboard functionality follows
// Intercept key release
void keyReleased()
{
  char rgchTemp[] = {0, 0};
  const char *szText = &rgchTemp[0];
  int  cchText = 0;
  
  switch (keyboard.getOemKey())
  {
    case 58:  // F1
      szText = "\nCQ CQ CQ de ko7m ko7m ko7m"
               "\nCQ CQ CQ de ko7m ko7m ko7m"
               "\nCQ CQ CQ de ko7m ko7m ko7m CN87xp pse k\n";
      break;
      
    case 59:  // F2
      szText = "\n Me: Jeff - ko7m"
               "\nLoc: Redmond, Washington USA"
               "\nGrd: CN87xp"
               "\nQSL: eQSL, direct or bureau"
               "\nRig: Homebrew SSB transceiver"
               "\nAnt: G5RV at about 8 metres"
               "\nPSK: Homebrew Arduino Due software"
               "\n";
      break;

    case 60:  // F3
      szText = "\n\nSo, back to you!\n";
      break;
      
    case 61:  // F4
      szText = "\nTnx for the nice QSO"
               "\nBest of 73's to you and yours!"
               "\n";
      break;
      
    case 62:  // F5
      szText = " de jeff -ko7m\n\n";
      break;
    
    case 40:  // CR
      szText = "\r\n";
      break;
      
    default:
      rgchTemp[0] = keyboard.getKey();
      rgchTemp[1] = 0;
      break;
  }
  
  cchText = strlen(szText);
  while (cchText--)
    rgchBuf[tail++] = *szText++;

  startTX();  // Start the PSK engine if it is stopped
}

// Set up our pins, analogue conversion, buffers and timers 
void setup()
{
  // Use 8 bits in analogue conversions
  analogWriteResolution(8);
  analogReadResolution(8);

  tail = head = 0;
 }

// Process USB tasks in main loop
void loop() 
{
  usb.Task();
}

11 comments:

  1. Hello Jeff, thanks for all your amazing programming work you did for this PSK31-engine. I have the PSK31-engine working on my Arduini-UNO and MEGA. Now I like to make it work with my DUE. But I can't find the USB-host library with keyboard-controller. I want to make a PSK31-encoder for my FTDX3000, who can decode PSK31 and RTTY and CW. Thanks in advandge for your answer, and keep the good programming going. Hpe cuagn 73's Jan PA2JJB

    ReplyDelete
    Replies
    1. Hello Jan. I am pleased that you have found my doodling in code useful.

      At the time that I wrote this code a year ago, the experimental USBHost library was distributed with the Arduino IDE. While it is still available, it is not installed by default. You can install it easily from the Sketch -> Include Library -> Manage Libraries menu. Open up the library manager from those menu selections and search for USBHost and just click Install.

      Also, the newer IDE requires you to install support for the Due separately from the "Boards Manager" menu in the IDE. This is found in the Cortex-M3 package. It will show the Arduino Due in the list of supported boards. Just click install.

      Once these steps were performed, I was able to build the code above. I copy/pasted it into the IDE and hit compile. I see that my string constants are being flagged as deprecated by the newer compilers. You will see warning messages such as this:

      C:\Users\Jeff\Documents\Arduino\PSK31Keyboard\PSK31Keyboard.ino:386:14: warning: deprecated conversion from string constant to 'char*' [-Wwrite-strings]

      szText = "\nCQ CQ CQ de ko7m ko7m ko7m"

      Fair enough... :) It seems that string constants are now being flagged if you don't tag them as constants. These warnings can be eliminated by changing one line in the keyReleased() function.

      const char *szText = &rgchTemp[0];

      Just add the "const" specification on the front of the line. I will edit the code above to include this change and make a note of the need to install the USBHost library.

      Thanks for the feedback!

      73's de Jeff - ko7m

      Delete
  2. This is really great work! Thanks for sharing.

    ReplyDelete
  3. You mentioned: "There is a small start-up delay (of a couple seconds) on the USB host functionality, so if you power on the Due and start typing immediately, there may be some keystrokes that are lost. I have not investigated yet why this occurs in the standard USB library."

    I have not used the Due specifically, but the startup delay may be related to the serial bootloader mechanism that allows the Arduino IDE to gain control of the processor for loading new code to the Arduino.

    While working on the Arduino Nano there were a few changes available to modify the default startup behavior. One was to add a low resistance pullup (~120 ohms) to the USB handshaking line that was connected to the processor's RESET line. Another alternative was to remove the capacitor that formed the RC circuit that was also part of the USB to RESET circuit. Either way, this would prevent the USB port (and thus the IDE) from resetting the controller remotely. To load new software, you would have to start the IDE firmware upload and simultaneously reset the processor manually. The boot loader would then sync to the IDE and upload as expected. I used the pullup method with a toggle switch so I could enable/disable the reset line at the cost of a bit more power drain. (>120 ohms and the reset like would still assert; <120 ohms and the resistor would draw excessive current and heat up.) Removing the surface mount capacitor would not use extra power, but it could not be easily replaced or overridden for future development under IDE control. The primary point for me was to prevent someone from easily reprogramming the processor in the field using their own Arduino IDE, but they could still monitor and interact with the serial interface under normal program operation. But I still had a startup delay every time because the bootloader was checking to see of the IDE wanted to talk to it.

    http://playground.arduino.cc/Main/DisablingAutoResetOnSerialConnection

    The second alternative was to eliminate the bootloader firmware altogether, and program the part directly with an In Circuit Programmer. This would not only free up a bit of space for more user code, but it also eliminates the initial startup delay entirely. But you would then need extra hardware to program your device.

    ReplyDelete
  4. Hi, ide like to use adruino uno r3 is it possible, have anybody change the sketch for it.
    My email is dl1esz@gmx.de

    ReplyDelete
    Replies
    1. The challenge here is that is is not a simple matter of using a different sketch. You also need the supporting hardware and firmware. There is also some confusion around the difference between using the Arduino as a HID device as opposed to accepting input from a HID device. For example, an Arduino R3 has a second AVR chip that is a USB device that provides the USB to serial functionality used to program the Uno R3. By changing the firmware on this second AVR chip, you can create sketches that will emulate a USB keyboard or mouse. Basically, the Arduino Uno looks like a keyboard to an external PC or Mac. This is a different matter than being the host for a USB keyboard or mouse where the Arduino is accepting input from a USB keyboard.

      Short of having a USB shield (such as the example shown here at https://www.arduino.cc/en/Main/ArduinoUSBHostShield) or using one of the newer Arduino devices such as the Arduino Due that includes USB hardware and firmware support, my suggestion is to use a PS2 keyboard with the Arduino Uno. There is a link in my posting above to an example implementation. Thank you kindly for your interest in my work.

      Delete
    2. Another possible USB host shield you may want to look at may be found at https://www.circuitsathome.com/products-page/arduino-shields.

      Delete
  5. this can be made also for decoder of bpsk31 ?

    ReplyDelete
    Replies
    1. This article is only describing an encoder for BPSK. The task of decoding these signals is not quite as simple as encoding them. Nevertheless, I will be doing a series of articles on various signal processing topics so you may find these of interest. I cannot promise at this time when I will publish them as my work schedule keeps me quite busy of late, so I have not had time to complete them. Hopefully soon!

      Delete
  6. Dear Jeff:
    Congratulations fpr your work with the Arduino and the Psk31. I have one qestion: how is possble send one telemetry data, GPS signal, for example with your code? What line I have to modify?
    73,s de jabi, ea2aru.

    ReplyDelete
    Replies
    1. Hello Jabi,

      Thank you kindly for your comments. It is very possible to send GPS telemetry data as GPS uses NMEA textual sentences in general. For example, the following is an example GPS message (sentence):

      $GPRMC,123519,A,4807.038,N,01131.000,E,022.4,084.4,230394,003.1,W*6A

      This could be accomplished in many ways. One example would be to add support for the sentence to be sent when a function key is pressed on the keyboard.

      case 63: // F6
      szText = "$GPRMC,123519,A,4807.038,N,01131.000,E,022.4,084.4,230394,003.1,W*6A";
      break;

      If these lines are added near the bottom of the code with the other function keys, this sentence could be sent.

      I suspect you are really wanting to understand how to interface a GPS into the code directly where the data sent by the GPS is encoded as PSK31 by this code. I am not prepared to produce a working example for you, but the general flow would be to read and enqueue the GPS data for transmission and the dequeue it for encoding as PSK31.

      As a general statement, the encoder takes a circular buffer called rgchBuf. This is a 256 byte buffer and you would write to it using the variable "tail" as the index of the next character that should be written. The code will empty the queue reading from the index using the variable "head".

      Add to the tail, remove from the head. When tail==head, the queue is empty.

      I trust that will give you sufficient information to proceed on your own. My best to you and do let me know how you get on modifying this code to suit your needs.

      73's de Jeff - ko7m

      Delete