Tuesday, September 2, 2014

Direct Digital Synthesis (DDS) Part 2

Continuing on the theme of DDS in software, my previous post illustrated a simple audio sinusoidal generator using DDS techniques.  I promised to turn this baseline code into something more useful and here I will illustrate using this technique to generate a WSPR beacon message.

Here we will not be building a complete WSPR beacon, but only the components that deal with generating the WSPR audio signal in software.

I have done a similar beacon previously for the Parallax Propeller processor.  This will be a similar initial implementation that sends a pre-encoded message using Joe Taylor's wsprcode.exe application.  I encoded the message "KO7M CN87 7" and here is that message:

C:\Users\Jeff>wsprcode "KO7M CN87 7"
Message: KO7M CN87 7

Source-encoded message (50 bits, hex): 8B CC 46 9D 56 B1 C0

Data symbols:
      1 1 0 1 0 1 0 1 1 1 1 0 1 1 1 0 0 1 1 0 0 1 0 1 1 1 0 0 0 1
      1 1 1 0 0 0 1 1 0 0 1 0 1 0 0 1 1 0 1 1 1 1 0 1 1 1 1 0 0 0
      0 1 0 1 0 0 0 0 0 1 1 0 1 1 0 0 1 0 0 1 0 0 1 0 1 1 0 0 0 1
      0 1 0 1 1 0 1 0 0 0 1 1 0 1 0 1 0 1 0 1 1 1 0 1 0 1 1 0 0 1
      0 1 0 1 0 0 0 1 1 0 0 1 1 1 0 1 0 1 1 1 0 0 0 1 0 1 0 1 0 0
      1 0 1 1 1 0 1 1 0 0 0 1

Sync symbols:
      1 1 0 0 0 0 0 0 1 0 0 0 1 1 1 0 0 0 1 0 0 1 0 1 1 1 1 0 0 0
      0 0 0 0 1 0 0 1 0 1 0 0 0 0 0 0 1 0 1 1 0 0 1 1 0 1 0 0 0 1
      1 0 1 0 0 0 0 1 1 0 1 0 1 0 1 0 1 0 0 1 0 0 1 0 1 1 0 0 0 1
      1 0 1 0 1 0 0 0 1 0 0 0 0 0 1 0 0 1 0 0 1 1 1 0 1 1 0 0 1 1
      0 1 0 0 0 1 1 1 0 0 0 0 0 1 0 1 0 0 1 1 0 0 0 0 0 0 0 1 1 0
      1 0 1 1 0 0 0 1 1 0 0 0

Channel symbols:
      3 3 0 2 0 2 0 2 3 2 2 0 3 3 3 0 0 2 3 0 0 3 0 3 3 3 1 0 0 2
      2 2 2 0 1 0 2 3 0 1 2 0 2 0 0 2 3 0 3 3 2 2 1 3 2 3 2 0 0 1
      1 2 1 2 0 0 0 1 1 2 3 0 3 2 1 0 3 0 0 3 0 0 3 0 3 3 0 0 0 3
      1 2 1 2 3 0 2 0 1 0 2 2 0 2 1 2 0 3 0 2 3 3 1 2 1 3 2 0 1 3
      0 3 0 2 0 1 1 3 2 0 0 2 2 3 0 3 0 2 3 3 0 0 0 2 0 2 0 3 1 0
      3 0 3 3 2 0 2 3 1 0 0 2

Decoded message: KO7M CN87 7              ntype:  7

I grabbed the channel symbols and turned them into a byte array, thus:

uint8_t symbols[] = 
{
  3,3,0,2,0,2,0,2,3,2,2,0,3,3,3,0,0,2,3,0,0,3,0,3,3,3,1,0,0,2,
  2,2,2,0,1,0,2,3,0,1,2,0,2,0,0,2,3,0,3,3,2,2,1,3,2,3,2,0,0,1,
  1,2,1,2,0,0,0,1,1,2,3,0,3,2,1,0,3,0,0,3,0,0,3,0,3,3,0,0,0,3,
  1,2,1,2,3,0,2,0,1,0,2,2,0,2,1,2,0,3,0,2,3,3,1,2,1,3,2,0,1,3,
  0,3,0,2,0,1,1,3,2,0,0,2,2,3,0,3,0,2,3,3,0,0,0,2,0,2,0,3,1,0,
  3,0,3,3,2,0,2,3,1,0,0,2
};

// Maximum number of symbols in a WSPR message

const uint32_t iSymbolMax = (sizeof(symbols) / sizeof(uint8_t));

I then set up a couple of constants and a table of DDS tuning words for the WSPR channel symbols.  Remember that for a SSB transmitter, the audio tones that are used to modulate it for WSPR transmission are in the range of 1400-1600 Hz.  The WSPR band is 200 Hz wide and so I have chosen to put my beacon signal in the middle of the band.  Any frequency can be used in this 200 Hz range, but consideration should be made for the width of the signal when choosing to be near the band edge.

// Tuning word is set to the following multiplier times the tone frequency
const double multiplier = pow(2,32) / ref_frequency / 256;

// Tuning word delta for 1.4648 Hz delta between symbols
const double delta = 1.4648 * multiplier;

// Array of pre-calculated tuning words for each WSPR symbol spaced
// 1.4648 Hz apart. Arbitrarily chose symbol 2 for 1500 Hz as W1JT does not
// define it in his documentation.
volatile const uint32_t tuning_words[] = 
{
  (dds_frequency * multiplier - (delta * 2)),
  (dds_frequency * multiplier - delta),
  (dds_frequency * multiplier),
  (dds_frequency * multiplier + delta) 
};

I am going to use timer 2 to generate a 32 kHz clock (reference frequency) for the DDS and generate a 1500 Hz tone.

// DDS frequency is chosen to be in the middle of the 1400-1600 Hz band
// Any value in this range is appropriate, but should be chosen to stay within
// the band while allowing for 1.4648 Hz above and 2.9296 Hz below plus
// sidebands.
int dds_frequency = 1500;

// The reference frequency is subject to the accuracy of the 16 MHz
// crystal oscillator.
const double ref_frequency = (16000000/510);

So, once we enable interrupts, a 1500 Hz tone will be generated.  The four WSPR channel symbols are 1.4648 Hz apart and are of 683 ms duration.  Since we are executing an interrupt service routine every 32 us, it will take 21427 interrupts to generate one WSPR channel symbol.  Once we have seen this many interrupts, we move on to the next symbol in the WSPR message until all 162 symbols have been generated.  At this point, we disable interrupts and the tone generation stops.

// Timer 2 interrupt service routine (ISR) is used to generate
// the timebase reference clock for the DDS generator at 32kHz.
ISR(TIMER2_OVF_vect)
{
  // Keep track of how long this symbol has been playing and when to move
  // to next symbol. WSPR symbol length = 8192 / 12000 * 1000 ms = 682.6666 or
  // 683 ms.  This function is called about every 32 us.  By keeping track of
  // how many times this function is called we can calculate the length of
  // each symbol.
  //
  // Clock is 16000000 / 510 = 31372.5490 Hz = 31.875 us per tick
  // .683 / 3.1875e-5 = 21427 counts.  This may need be adjusted based on
  // accuracy of main 16 MHz clock.
  if (cTone++ >= 21427)
  {
    cTone = 0;  // Reset tone length counter
    iSymbol++;  // Carry on with next symbol
  }
  
  // Update 24 bit phase accumulator and extract the sine table index from
  // top 8 bits of phase accumulator.
  phase_accumulator += tuning_word;
  sine_table_index = phase_accumulator >> 16;  // Use upper 8 bits as index
  
  // Set current amplitude value for the sine wave being constructed.
  OCR2A = pgm_read_byte_near(sine256 + sine_table_index);
  
  // If we have sent the entire 162 symbol WSPR msg, kill the timer
  // and reset to first symbol.
  if (iSymbol >= iSymbolMax)
  {
    TIMSK2 = 0;  // Disable timer 2 to stop tone
    iSymbol = 0; // Reset back to beginning of symbol set
  }
  
  // Get the next tuning word based on the current symbol
  tuning_word = tuning_words[symbols[iSymbol]];
}

So, putting all this together, I have a functional WSPR beacon using software DDS.  This is not a complete beacon solution as WSPR messages have to be started within a couple seconds of an even minute.  But, the code if started on an even minute is easily decoded by Joe Taylor's fine software.  Here is a screen shot of an Argo waterfall display of the signal:

Here is a screen shot of the signal decode:


Where am I going with all this?  Tying all this back to recent work on the Minima transceiver, I am proposing that the CW sidetone hardware be replaced by a software DDS sidetone generator.  In doing so, not only can we provide CW sidetone services, but the audio tone can be modulated in interesting ways to easily integrate digital modes such as WSPR into the Minima project.  On transmit, the CW sidetone is used to generate CW signals by feeding the audio signal into the transmitter audio chain.  The same technique is used for digital modes.

In my next post, I will flesh out the WSPR beacon to allow specification of the message without requiring the message be pre-encoded using wsprcode.exe.  Stay tuned...

Here is the complete source code listing thus far.  If you wish to just compile it and use it, please change the symbols[] array to contain your own WSPR message.

// Sine wave generator using DDS techniques implementing WSPR beacon
// Jeff Whitlatch - ko7m

#include "avr/pgmspace.h"

// Single period sine wave table.
// Amplitudes are 0-255 with 128 as zero crossing.  256 samples per period.
PROGMEM prog_uchar sine256[] =
{
  128,131,134,137,140,143,146,149,152,155,158,162,165,167,170,173,
  176,179,182,185,188,190,193,196,198,201,203,206,208,211,213,215,
  218,220,222,224,226,228,230,232,234,235,237,238,240,241,243,244,
  245,246,248,249,250,250,251,252,253,253,254,254,254,255,255,255,
  255,255,255,255,254,254,254,253,253,252,251,250,250,249,248,246,
  245,244,243,241,240,238,237,235,234,232,230,228,226,224,222,220,
  218,215,213,211,208,206,203,201,198,196,193,190,188,185,182,179,
  176,173,170,167,165,162,158,155,152,149,146,143,140,137,134,131,
  128,124,121,118,115,112,109,106,103,100,97,93,90,88,85,82,
  79,76,73,70,67,65,62,59,57,54,52,49,47,44,42,40,
  37,35,33,31,29,27,25,23,21,20,18,17,15,14,12,11,
  10,9,7,6,5,5,4,3,2,2,1,1,1,0,0,0,
  0,0,0,0,1,1,1,2,2,3,4,5,5,6,7,9,
  10,11,12,14,15,17,18,20,21,23,25,27,29,31,33,35,
  37,40,42,44,47,49,52,54,57,59,62,65,67,70,73,76,
  79,82,85,88,90,93,97,100,103,106,109,112,115,118,121,124
};

uint8_t symbols[] = 
{
  3,3,0,2,0,2,0,2,3,2,2,0,3,3,3,0,0,2,3,0,0,3,0,3,3,3,1,0,0,2,
  2,2,2,0,1,0,2,3,0,1,2,0,2,0,0,2,3,0,3,3,2,2,1,3,2,3,2,0,0,1,
  1,2,1,2,0,0,0,1,1,2,3,0,3,2,1,0,3,0,0,3,0,0,3,0,3,3,0,0,0,3,
  1,2,1,2,3,0,2,0,1,0,2,2,0,2,1,2,0,3,0,2,3,3,1,2,1,3,2,0,1,3,
  0,3,0,2,0,1,1,3,2,0,0,2,2,3,0,3,0,2,3,3,0,0,0,2,0,2,0,3,1,0,
  3,0,3,3,2,0,2,3,1,0,0,2
};

// Maximum number of symbols in a WSPR message
const uint32_t iSymbolMax = (sizeof(symbols) / sizeof(uint8_t));

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

// DDS frequency is chosen to be in the middle of the 1400-1600 Hz band  Any value
// in this range is appropriate, but should be chosen to stay within the band while
// allowing for 1.4648 Hz above and 2.9296 Hz below plus sidebands.
int dds_frequency = 1500;

// The reference frequency is subject to the accuracy of the 16 MHz crystal oscillator.
const double ref_frequency = (16000000/510);

// The following formula will calculate the necessary tuning word for a given output freq
// tuning_word = pow(2,32) * dds_frequency / ref_frequency;

// These must all be marked as volatile as they are used in an interrupt service routine
volatile byte sine_table_index;
volatile uint32_t phase_accumulator;
volatile uint32_t tuning_word;
volatile uint16_t iSymbol;
volatile uint16_t cTone;

// Tuning word is set to the following multiplier times the tone frequency
const double multiplier = pow(2,32) / ref_frequency / 256;

// Tuning word delta for 1.4648 Hz delta between symbols
const double delta = 1.4648 * multiplier;

// Array of pre-calculated tuning words for each WSPR symbol spaced 1.4648 Hz apart.  
// Arbitrarily chose symbol 2 for 1500 Hz as W1JT does not define it in his documentation.
volatile const uint32_t tuning_words[] = 
{
  (dds_frequency * multiplier - (delta * 2)),
  (dds_frequency * multiplier - delta),
  (dds_frequency * multiplier),
  (dds_frequency * multiplier + delta) 
};

// Setup
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();

  iSymbol = cTone = 0;     // Set initial symbol and symbol length
  
  // Calculate the tuning word for the first symbol before we enable interrupts
  tuning_word = tuning_words[symbols[iSymbol]];
  
  // disable interrupts to avoid timing distortion
  //cbi (TIMSK0, TOIE0);    // Disable timer 0.  Breaks the delay() function
  sbi (TIMSK2, TOIE2);      // Enable timer 2.
}

// Nothing to do here.  Everything is interrupt driven
void loop()
{
}

// Setup timer2 with prescaler = 1, PWM mode to phase correct PWM
// See th ATMega datasheet for all the gory details
void timer2Setup()
  TIMSK2 = 0;
  TCCR2A = _BV(COM2A1) | _BV(COM2B1) | _BV(WGM20);
  TCCR2B = _BV(CS20);
}

// Timer 2 interrupt service routine (ISR) is used to generate
// the timebase reference clock for the DDS generator at 32kHz.
ISR(TIMER2_OVF_vect)
{
  // Keep track of how long this symbol has been playing and when to move to next symbol
  // WSPR symbol length = 8192 / 12000 * 1000 ms = 682.6666 or 683 ms.  This function is
  // called about every 32 us.  By keeping track of how many times this function is called
  // we can calculate the length of each symbol
  //
  // Clock is 16000000 / 510 = 31372.5490 Hz = 31.875 us per tick
  // .683 / 3.1875e-5 = 21427 counts.  This may need be adjusted based on accuracy of
  // main 16 MHz clock.
  if (cTone++ >= 21427)
  {
    cTone = 0;  // Reset tone length counter
    iSymbol++;  // Carry on with next symbol
  }
  
  // Update 24 bit phase accumulator and extract the sine table index from top 8 bits
  phase_accumulator += tuning_word;
  sine_table_index = phase_accumulator >> 16;  // Use upper 8 bits as index
  
  // Set current amplitude value for the sine wave being constructed.
  OCR2A = pgm_read_byte_near(sine256 + sine_table_index);
  
  // If we have sent the entire 162 symbol WSPR msg, kill the timer
  // and reset to first symbol.
  if (iSymbol >= iSymbolMax)
  {
    TIMSK2 = 0;  // Disable timer 2 to stop tone
    iSymbol = 0; // Reset back to beginning of symbol set
  }
  
  // Get the next tuning word based on the current symbol
  tuning_word = tuning_words[symbols[iSymbol]];

}

No comments:

Post a Comment