Saturday, December 27, 2014

Generating Audio PSK31 with an Arduino (Part 2)

I have completed my initial pass on PSK31 audio generation using an Arduino.  While I have not yet fixed the PSK31 character timing, fldigi is able to decode what I am generating, so there is a bit of character timing flexibility at least in the implementation of fldigi.  I have not tested with any other PSK decoder as it is still my intention to fix this part of the implementation.  However, I wanted to publish an update with what I have working regardless.

Here is the top part of the code listing where we describe the functionality and define the table that translates between 7 bit ASCII characters and the variable bit length PSK31 character set.

// PSK31 audio generation
// Jeff Whitlatch - ko7m

// 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)
};

Now we define the sinusoid data table of 513 bytes.  The first 512 bytes are 16 cycles of sine data ramping up from zero to full volume.

// 16 cycles of 32 samples each (512 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.
char data[] = {
  0x80,0x80,0x80,0x80,0x81,0x81,0x82,0x82,
  0x83,0x83,0x83,0x83,0x83,0x82,0x82,0x81,
  0x7F,0x7E,0x7D,0x7B,0x7A,0x79,0x78,0x77,
  0x76,0x76,0x76,0x77,0x78,0x79,0x7B,0x7D,
  0x80,0x82,0x85,0x87,0x89,0x8B,0x8D,0x8E,
  0x8F,0x8F,0x8F,0x8D,0x8C,0x89,0x86,0x83,
  0x7F,0x7C,0x78,0x75,0x71,0x6E,0x6C,0x6B,
  0x6A,0x6A,0x6B,0x6C,0x6F,0x72,0x76,0x7B,
  0x80,0x84,0x89,0x8E,0x92,0x96,0x99,0x9A,
  0x9B,0x9B,0x9A,0x98,0x94,0x90,0x8B,0x85,
  0x7F,0x79,0x73,0x6E,0x69,0x64,0x61,0x5F,
  0x5E,0x5E,0x60,0x62,0x66,0x6C,0x72,0x78,
  0x80,0x87,0x8E,0x95,0x9B,0xA0,0xA4,0xA6,
  0xA7,0xA7,0xA5,0xA2,0x9D,0x97,0x90,0x88,
  0x7F,0x77,0x6F,0x67,0x60,0x5A,0x56,0x53,
  0x52,0x52,0x55,0x59,0x5E,0x65,0x6D,0x76,
  0x80,0x89,0x92,0x9B,0xA3,0xA9,0xAE,0xB2,
  0xB3,0xB2,0xB0,0xAB,0xA5,0x9D,0x94,0x8A,
  0x7F,0x75,0x6A,0x61,0x58,0x51,0x4B,0x48,
  0x46,0x47,0x4A,0x4F,0x56,0x5F,0x69,0x74,
  0x80,0x8B,0x97,0xA1,0xAB,0xB3,0xB9,0xBD,
  0xBE,0xBD,0xBA,0xB4,0xAD,0xA3,0x98,0x8C,
  0x7F,0x73,0x66,0x5B,0x50,0x48,0x41,0x3D,
  0x3C,0x3D,0x40,0x46,0x4F,0x59,0x65,0x72,
  0x80,0x8D,0x9B,0xA7,0xB2,0xBC,0xC2,0xC7,
  0xC9,0xC8,0xC4,0xBD,0xB4,0xA9,0x9C,0x8E,
  0x7F,0x71,0x62,0x55,0x49,0x3F,0x38,0x33,
  0x31,0x33,0x37,0x3E,0x47,0x53,0x61,0x70,
  0x80,0x8F,0x9F,0xAD,0xB9,0xC4,0xCC,0xD1,
  0xD2,0xD1,0xCD,0xC5,0xBB,0xAE,0xA0,0x90,
  0x7F,0x6F,0x5F,0x50,0x42,0x37,0x2F,0x2A,
  0x28,0x29,0x2E,0x36,0x41,0x4E,0x5D,0x6E,
  0x80,0x91,0xA2,0xB2,0xC0,0xCB,0xD4,0xD9,
  0xDB,0xDA,0xD5,0xCD,0xC1,0xB3,0xA3,0x92,
  0x7F,0x6D,0x5B,0x4B,0x3C,0x30,0x27,0x21,
  0x1F,0x21,0x26,0x2F,0x3B,0x49,0x5A,0x6C,
  0x80,0x93,0xA5,0xB6,0xC6,0xD2,0xDC,0xE1,
  0xE4,0xE2,0xDC,0xD3,0xC7,0xB8,0xA6,0x93,
  0x7F,0x6C,0x58,0x46,0x37,0x2A,0x20,0x1A,
  0x18,0x19,0x1F,0x29,0x35,0x45,0x57,0x6B,
  0x80,0x94,0xA8,0xBB,0xCB,0xD8,0xE2,0xE9,
  0xEB,0xE9,0xE3,0xD9,0xCC,0xBC,0xA9,0x95,
  0x7F,0x6A,0x56,0x43,0x32,0x24,0x1A,0x13,
  0x11,0x13,0x19,0x23,0x31,0x42,0x55,0x6A,
  0x80,0x95,0xAB,0xBE,0xCF,0xDD,0xE8,0xEF,
  0xF1,0xEF,0xE9,0xDE,0xD0,0xBF,0xAB,0x96,
  0x7F,0x69,0x53,0x3F,0x2E,0x1F,0x15,0x0E,
  0x0B,0x0D,0x14,0x1F,0x2D,0x3F,0x53,0x69,
  0x80,0x96,0xAD,0xC1,0xD3,0xE2,0xED,0xF4,
  0xF6,0xF4,0xED,0xE2,0xD4,0xC2,0xAD,0x97,
  0x7F,0x68,0x52,0x3D,0x2B,0x1C,0x10,0x09,
  0x07,0x09,0x10,0x1B,0x2A,0x3C,0x51,0x68,
  0x80,0x97,0xAE,0xC3,0xD6,0xE5,0xF0,0xF7,
  0xFA,0xF8,0xF1,0xE6,0xD6,0xC4,0xAF,0x98,
  0x7F,0x67,0x50,0x3B,0x28,0x19,0x0D,0x06,
  0x04,0x06,0x0D,0x18,0x28,0x3A,0x50,0x67,
  0x80,0x98,0xAF,0xC5,0xD8,0xE7,0xF3,0xFA,
  0xFD,0xFA,0xF3,0xE8,0xD8,0xC5,0xB0,0x98,
  0x7F,0x67,0x4F,0x3A,0x27,0x17,0x0B,0x04,
  0x01,0x04,0x0B,0x17,0x26,0x39,0x4F,0x67,
  0x80,0x98,0xB0,0xC6,0xD9,0xE9,0xF4,0xFC,
  0xFE,0xFC,0xF5,0xE9,0xD9,0xC6,0xB0,0x98,
  0x7F,0x67,0x4F,0x39,0x26,0x16,0x0A,0x03,
  0x01,0x03,0x0A,0x16,0x26,0x39,0x4F,0x67,
  0x80
  };

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

// Useful macros for setting and resetting bits
#define cbi(sfr, bit) (_SFR_BYTE(sfr) &= ~_BV(bit))
#define sbi(sfr, bit) (_SFR_BYTE(sfr) |= _BV(bit))

The following variables are used in the interrupt service routine.  These variables define the 7 bit ASCII buffer of the text to be sent.  The idea is to maintain a head and tail index into the buffer where the head is the next character to be sent.  The tail is the place where new text will be inserted.  The buffer is circular and when head == tail, the buffer is empty and we stop the PSK31 transmission.

// 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)

The following variable holds the variable bit length character currently being sent.  Bits are sent least significant bit (LSB) first.  When two zero bits have been sent, the character is finished and the next character is fetched from the buffer above.

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

The following variable should be a constant as there is a constant number of phase points in 1/2 the PSK31 bit time of 1024 phase points.  I will fix this in the final version.

volatile int   cbHalfBit = 512;

The following variables are keeping track of the current phase point (index into the sine table) and the number of phase points that remain in the direction we are scanning the table.  Lastly, we keep track of if we are currently sending a PSK31 one bit or a zero bit.

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

The IX variable is the increment (+1 or -1) to add to the phase index to get to the next phase point to be processed.  The sign of the variable indicates whether we are processing the table in the forward or reverse direction.  Phase is the current phase of the sinusoid and is either +1 for no phase shift or -1 for 180 degree phase shift.  The fFullBit variable tells us if we are processing the first or second half of the PSK31 bit.

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

The cZeroBits counts the number of consecutive zero bits that have been sent in order to detect the end of a character.  The maxZeroBits variable tells us how many zero bits indicate the end of a character.  This is also used to send a few zero bits at the end of a transmission before turning off the tone.

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

The following code sets up timer 2 to process our phase point generation.  This still needs to be adjusted to be 31.25 microseconds rather than the current 32 microseconds.  I will fix this, I promise.  It just has not been a priority.

// Setup timer2 with prescaler = 1, PWM mode to phase correct PWM
// See th ATMega datasheet for all the gory details

// This is not quite right for PSK31 as it is 32 us vs. 31.25 us
void timer2Setup()
{
  // Clock prescaler = 1
  sbi (TCCR2B, CS20);    // 001 = no prescaling
  cbi (TCCR2B, CS21);
  cbi (TCCR2B, CS22);

  // Phase Correct PWM
  cbi (TCCR2A, COM2A0);  // 10 = clear OC2A on compare match when up counting
  sbi (TCCR2A, COM2A1);  //      set OC2A on compare match when down counting

  // Mode 1
  sbi (TCCR2A, WGM20);   // 101 = Mode 5 uses OCR2A as top value rather than 0xff
  cbi (TCCR2A, WGM21);
}

Now we have the main waveform generation state machine.  This code is still very rough and will need to be optimized once I get all functionality implemented.

// 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 512 phase points
// (16 amplitudes of 32 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 no amplitude or phase
// change, so we just play 32 phase points of
// full amplitude data 32 times for a total of 1024
// phase points.
//
// Each end of the ramp-up table starts with a zero 
// crossing byte, so there is one extra byte in
// the table (513 entries).  Ramping up plays bytes
// 0 -> 511 and ramping down plays bytes 512 -> 1
// allowing each direction to start at the zero
// crossing point.
ISR(TIMER2_OVF_vect)
{
  // Set current amplitude value for the sine wave 
  // being constructed taking care to invert the
  // phase when processing the table in reverse order.
  OCR2A = *pbSine * ix * phase;
  pbSine += ix;
  
  // At the half bit time, we need to change phase
  // if generating a zero bit
  if (0 == --cbHalfBit)
  {
    cbHalfBit = 512;
    
    // 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 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) cbi (TIMSK2,TOIE2); else maxZeroBits = 75;
      }
    }
    
    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 ? 32 : 512;
    ix = -ix;
  }
}

Setup is going to set our pin modes, set up timer 2 and put some test text into the send buffer to be processed.  Once the timer is enabled, PSK generation is automagic.

void setup() 
{
  // PWM output for timer2 is pin 10 on the ATMega2560
  // If you use an ATMega328 (such as the UNO) you need
  // to make this pin 11
  // See https://spreadsheets.google.com/pub?key=rtHw_R6eVL140KS9_G8GPkA&gid=0
  pinMode(10, OUTPUT);   // Timer 2 PWM output on mega256 is pin 10

  // Set up timer2 to a phase correct 32kHz clock
  timer2Setup();

  // Put something in the buffer to be sent
  strcpy((char *) &rgchBuf[0], "\ncq cq cq de ko7m ko7m ko7m"
                               "\ncq cq cq de ko7m ko7m ko7m pse k\n");
  tail = strlen((const char *) rgchBuf);
  head = 0;
  
  sbi (TIMSK2,TOIE2);    // Enable timer 2.
}

Nothing to do (yet) in the main loop

void loop() 
{
}

Here is a screen shot of fldigi decoding the test message I have hard coded.




As always, your mileage may vary.  If you have any questions or comments I would love to hear from you by posting here or dropping me a line at ko7m at arrl dot org and I will do my best to help you out.

7 comments:

  1. Interesting, I have bookmarked your page...

    I'm more a PIC fan that Arduino, but your code is clean and works (I use JAL) It will be interesting to try that on a PIC 18F...

    73 CO7WT

    ReplyDelete
  2. Thank you kindly for your comments. I appreciate the feedback. I glad that you find my simple efforts useful.

    73's de Jeff - ko7m

    ReplyDelete
  3. I love it! Thank you for your efforts and contribution. I'll be following progress! Ed WD4ED

    ReplyDelete
  4. I love this! Thank you for your efforts and contribution! I'll be following. Ed WD4ED

    ReplyDelete
  5. Hi jeff. I think this might be my sunday project. I will use an uno. Do I need a filter circuit on the pwm out? Couldn't see mention of one in your documentation. Chris ZS1CDG

    ReplyDelete
    Replies
    1. Yes, I would definitely filter the PWM output. If you do not, your signal will contain a large amount of high frequency energy that will contribute to distortion of your signal. It is a simple RC filter, so two inexpensive components is all that is minimally required. You can see mention of this in my article on direct digital synthesis. I will also update the PSK series to include this information.

      http://ko7m.blogspot.com/2014/08/direct-digital-synthesis-dds.html

      Delete
    2. Yes, I would definitely filter the PWM output. If you do not, your signal will contain a large amount of high frequency energy that will contribute to distortion of your signal. It is a simple RC filter, so two inexpensive components is all that is minimally required. You can see mention of this in my article on direct digital synthesis. I will also update the PSK series to include this information.

      http://ko7m.blogspot.com/2014/08/direct-digital-synthesis-dds.html

      Delete