Showing posts with label ATMega2560. Show all posts
Showing posts with label ATMega2560. Show all posts

Sunday, April 24, 2016

Satellite tracking

The beginnings of a new project.  Software is coming together, but now it's time to pull together the hardware.  This will be an antenna azimuth/elevation  satellite tracker.  In previous posts I have explored the calculations necessary and the details of using hobby servos to create a simple proof of concept.


For now I am experimenting with the ATMega2560 board (on the left) as it has plenty of flash and RAM space for the task at hand.  I plan to control the device via a web page that it serves up, so for now I have an ethernet shield attached, though in the end the plan is to use WiFi.

Calculating satellite passes requires an accurate position for the observer in terms of latitude and longitude, but also elevation and time-of-day.  I am using a GPS module to provide this information (lower right).

While the mighty ATMega2560 is certainly capable of generating the necessary PWM signals to position servos, I have decided to offload that to separate controller based on the PCA9685 which provides I2C interfacing of 16 channels of 12 bit PWM data.  This particular board was originally intended to be used as an RGB LED controller, but my plan is to re-purpose it for this task.  It has its own internal 25 MHz oscillator that offloads the ATMega2560 from having to generate these signals.  That, plus the fact that I happened to have one on hand drove the decision to use this component to drive my servos.

After spending some time in the Avionics industry and working on electronic aviation attitude indicators, the math involved in Euler angle solutions to the orientation of a rigid body, plus the mapping of raw gyro, accelerometer and magnetometer sensor data into 3D space is a bit tedious.  Fortunately for me, Bosch has done an amazing job of taking MEMS data from on-die sensors and adding an ARM Cortex M0 processor to produce a single chip solution that abstracts away all the raw sensor fusion and spits out data directly in quaternions, Euler angles or vectors.

The plan is to use this sensor mounted on the antenna fixture to measure azimuth and elevation relative to the calculated position on earth with no careful alignment of antenna orientation required.  The Arduino will accept keplerian elements for the desired satellite and then automagically track the spacecraft as it passes overhead.

More updates as this comes together into a demonstrable solution.

Monday, March 2, 2015

Generating Audio PSK31 with an Arduino (Part 3) UPDATE

UPDATED: Complete code listing added to end of posting.  Apologies for previously posting an incorrect (and non-functional) version of the code.  The corrected code is now included at the end of this posting.

In three previous posts (part 1updatepart 2), I have described a working PSK31 encoder implemented on an Arduino ATMega328 or ATMega2560 device.  While this implementation worked for the most part, it was based on some incorrect PSK32 bit timing.  Most PSK31 decoders are sufficiently robust to allow successful decoding of my generated signal, but I wanted to write a follow-up posting to describe the error and propose a fix.

By way of review, PSK31 is a communication protocol with a transmission rate of 31.25 baud.  So, what does this mean?  From google, we see the following definition of "baud".



So, a 31.25 baud transmission is 31.25 bits per second or a bit time of 32 ms.  The technique described in my previous postings (above) results in a bit time of 32.768 ms which apparently is still decode-able, but I would still like to get this right.

My error is in how I constructed the waveform of the PSK31 signal.  I used a 1 kHz tone which of course has a 1 ms period (1/1000 = .001 seconds).  To construct this 1 kHz tone, I generated 32 phase points per cycle.  This means that I needed to generate a new phase point every 31.25 us.  (.001 / 32 = .00003125 seconds = 31.25 us).  To do this, I used a timer driven at 16 MHz in phase correct PWM mode.  This means that the timer counts from zero to 255 and then back down to zero at which time it generates an interrupt.  Therefore, the interrupt would happen every 512 ticks of the 16 MHz clock which is every 32 us.  (16000000 / 512 = 31250 = .000032 seconds = 32 us)

So, instead of generating a phase point every 31.25 us, I was generating one every 32 us. (Hey, what's 0.75 us amongst friends?)  This in and of itself is not really a problem, but the error I made was that I was using a model where I generated 1024 phase points per PSK31 bit time.  Therefore, my bit time ended up being 32.768 ms rather than the desired 32 ms.  (1024 * .000032 = .032768 seconds = 32.768 ms).

In an ideal world, I would run the timer at 16 MHz / 500 = 32 kHz instead of 16 MHz / 512 = 31.25 kHz and my current scheme would work fine.  The counter would run from zero to 250 and then back down to zero instead of counting up to 255.  However, there are a limited number of prescaler divisors available on Arduino timers and they are all powers of two, so this is not an option.  Additionally, I need phase correct PWM mode, so the counter has to run from minimum (zero) to max (255) and back down for each phase point.  The 1 kHz tone output goes high at the current phase amplitude value when the counter gets to it on the way up to 255 and sets the output low when the counter gets to the phase amplitude value on the way back down to zero.  This allows the generation of phase correct PWM output.  Once the PWM output is integrated, a nice phase continuous sine wave output is produced.

So, the fix appears to be to use 1000 phase points per PSK31 bit time rather than 1024.  (1000 * .000032 = .032 = 32 ms)  I will need to re-generate the sine tables to use possibly 20 phase points per cycle rather than 32 and make sure the state machine knows the new number of phase points per cycle, so the code changes should be pretty trivial (famous last words...)

The implementation does not separate the bit timing from the timing of the generation of the 1 kHz audio tone.  We count cycles to know when the bit has ended.  If these timing tasks were separated, then we could set the bit time independently of the frequency of the tone being generated.  But, since I coupled these two things together, I have to change the number of phase points per cycle in order to generate the correct character timing.

Here is the updated code listing with the changes discussed above.  I have verified the bit timing is now correct.

As always, your mileage may vary.  I am happy to try and help you if you have questions or comments about anything you read here.  Post a reply here or drop me a note at ko7m at arrl dot net.

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

// 25 cycles of 20 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.
//
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

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

// 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 char *pbSine = one;
//volatile int   cbDirection = 32;
//volatile char  fSendOne = true;

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

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

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

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);   // 01 = Mode 1 uses 0xff as TOP value
  cbi (TCCR2A, WGM21);
}

// 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 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 (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.
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 = 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) 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 ? 20 : 500;
    ix = -ix;
  }
}

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"
                               "\nCQ CQ CQ de ko7m ko7m ko7m CN87xp pse k\n");
  tail = strlen((const char *) rgchBuf);
  head = 0;
  
  sbi (TIMSK2,TOIE2);    // Enable timer 2.
}

void loop() 
{
}

Saturday, August 23, 2014

Direct Digital Synthesis (DDS)

UPDATED

With all the recent work on software for the Si570 device, this got me thinking a bit about the process of direct digital synthesis.  I went back and read again a great tutorial on the topic by Analog Devices MT-085 Fundamentals of Direct Digital Synthesis and decided to experiment with a software DDS using the Arduino as a test platform.

Basically the architecture of a DDS system has a stable clock driving a look up of sinusoidal information where one or more cycles of a sine wave (or for that matter, any other arbitrary waveform) are fed in sequence to a digital to analog converter to produce the final output.  Each of these look ups can be stored in a read-only memory and the value fetched represents the corresponding digital amplitude of the signal at each clock tick.

For my experimentation, I will construct a sinusoidal waveform using 8 bit data representing the amplitude of the signal at each clock tick.  To construct the table of sinusoidal amplitude information, I will retrieve a value from 0-255 which will map to a signal where the sinusoidal waveform crosses zero at the middle of this range (e.g. 128) and 256 samples for one period of the waveform.

Each address in the look up table corresponds to a phase point on the sine wave from 0 to 360 degrees.  In reality only data for 90 degrees (one quadrant of a circle) would be required as the quadrant is indicated by the two most significant bits.  The look up table contains the corresponding digital amplitude information for one complete cycle of a sine wave used to drive a DAC (digital to analogue converter).

If the look up table contained an entry for each bit of a 32 bit index into the table, there would be 2^32 output values before the index overflowed.  A sine wave thus constructed would have a frequency equal to the input clock frequency divided by 2^32 as it would take that many data points to reconstruct the sine wave.  Let's call this the M=1 case (one increment per clock tick).

If we increment through the table a little faster, say M=2 then the table index will roll over twice as fast and the output frequency would be doubled.  For an n-bit index (in DDS systems this is called a phase accumulator) there are 2^n possible phase points (data points in our look up table).  Let's call M the amount that the phase accumulator (index) is incremented on each clock cycle.  If fc is the clock frequency, then the frequency of the output sine wave is determined by the following formulae, also known as the DDS tuning equation.

    fout = (M * fc) / (2^n)

The frequency resolution of a DDS system is equal to fc/2^n.  If n = 32, this represents a resolution greater than one part in four billion. (2^32 = 4,294,967,296).  This is rather impractical and in general unnecessary.  In most DDS systems not all of the bits of the phase accumulator are used in the look up table and only the upper (most significant) bits are used.  This can significantly reduce the size of the look up table without affecting the frequency resolution.  This truncation of the phase does however add a small amount of phase noise to the final output.

There is a lot more information in the reference I mention above (MT-085) and well worth a read by anyone interested in DDS technology.

The first task at hand is to construct a table of 256 sine values representing the amplitude of the sinusoid for one sine period.  I wrote a little C# application to generate the necessary data structure text:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace GenSineData
{
  class Program
  {
    static void Main(string[] args)
    {
      Console.WriteLine("PROGMEM prog_uchar sine256[] = ");
      Console.WriteLine("{");
      Console.Write("  ");

      for (int i = 0; i < 256; i++)
      {
        double angle = 360.0 / 256.0 * Convert.ToDouble(i);
        double radians = angle * Math.PI / 180;
        double sin = Math.Sin(radians);
        
        // Map range from -1 to +1 to 0 to 255
        int amplitude = Convert.ToInt32(map(sin, -1, 1, 0, 255));
        Console.Write(amplitude.ToString() + (i != 255 ? "," : ""));
        if (i > 0 && (i+1) % 16 == 0)
        {
          Console.WriteLine();
          Console.Write("  ");
        }
      }
      Console.WriteLine("};");
    }

    // Map from one range to another
    static double map(double x, double in_min,
                      double in_max, double out_min, double out_max)
    {
      return (x - in_min) * (out_max - out_min) / (in_max - in_min) + out_min;
    }
  }
}

The output of this application can be pasted into my Arduino code producing a program memory-based lookup table.

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

Importing this data into Excel and graphing it, we find a nice sinusoidal waveform with zero crossings at the value 128 and all amplitude values between 0 and 255 using only 256 data points and 8 bit amplitude values.



So far, so good...

To compute the necessary tuning word for the DDS for the desired output frequency:

  M = (2^32) * desired_frequency / clock_frequency

For my purposes, I intend to use a clock_frequency of approximately 32kHz by dividing down the Arduino master clock of 16MHz.  I will use a timer interrupt at 32kHz and a 32 bit phase accumulator.  The upper 8 bits will be used as the index into the above sine wave table used to drive the Arduino PWM DAC.  Of course a low pass filter will be required to remove high frequency components of the audio output.

Ok, so now we put together a little Arduino test application to pull all these concepts together.  I am using an ATMega2560 for my testing but any Arduino would work so long as the pin differences are taken into account between the platforms.  More on this later.


Here is my DDSTest sketch:

// Sine wave generator using DDS techniques
// 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
  };

// 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 and reference clock
double dds_frequency = 0.0;
const double ref_frequency = (16000000/500);

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

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 spreadsheet here
  pinMode(10, OUTPUT);      // Timer 2 PWM output on mega256 is pin 10
  
  // Set up timer2 to a phase correct 32kHz clock
  timer2Setup();

  // disable interrupts to avoid timing distortion
  cbi (TIMSK0,TOIE0);    // Disable timer 0.  Breaks the delay() function
  sbi (TIMSK2,TOIE2);    // Enable timer 2.

  // Set up initial DDS frequency and calculate the timing word
  dds_frequency = 1000;
  tuning_word = pow(2,32) * dds_frequency / ref_frequency;
}

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

// Setup timer2 with prescaler = 1, PWM mode to phase correct PWM
// See the ATMega2560 datasheet for all the gory details
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 compare match
  sbi (TCCR2A, COM2A1);

  // Mode 1
  sbi (TCCR2A, WGM20);     // See table 20-8 in datasheet
  cbi (TCCR2A, WGM21);
  cbi (TCCR2B, WGM22);
}

// Timer 2 interrupt service routine (ISR) is used to generate
// the timebase reference clock for the DDS generator at 32kHz.
ISR(TIMER2_OVF_vect)
{
  // Update phase accumulator and extract the sine table index from it
  phase_accumulator += tuning_word;
  sine_table_index = phase_accumulator >> 24;  // 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); 

}

Connecting a powered speaker to pin 10 allows me to hear the 1kHz tone.  The PWM signal on pin 10 looks like this, though when this picture was taken, a 1.5kHz tone was being generated.





So, far so good...  A low pass filter will be required or at least some level of integration to turn this into a sine wave.  Since we are sampling at 32kHz, Nyquist says that the highest frequency we can produce is 16kHz.  So by setting a simple RC low pass filter cutoff frequency near this frequency should be adequate.  I will choose 15kHz for simplicity.

  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.  (.01 uF will do thanks...)

The following is the result using this simple integrator:





Fun!  Ok, so next installment, I hope to do something useful with all this...

More to come...

Tuesday, July 15, 2014

Minima hardware build

I have spent some time today organizing my Minima hardware so that it is not quite so fragile and breadboard-ish in preparation for starting to put together my own rig.

I am comfortable that I have the software in good shape and it is time to think about pulling together my own build.

Here is what I have for a front panel.  There is a 20 column by 4 line display, three push-buttons and a rotary encoder.  Readers of my blog may recognize this as the panel for my beacon project which is being re-purposed for this project.


I have mounted an I2C daughter board on the LCD in order to reduce the number of pins required to support the LCD.  I am not going to use the plethora of buttons I have seen on other designs.  I am also using a commercially available Arduino Uno board rather than build a controller board.  I have mounted it on the back of the LCD.  The remainder of the electronics of the radio will be in the bottom of the box.


I am replacing the potentiometer tuning with a rotary encoder and adding my iambic keyer code to the main Minima sketch.  If sufficient flash is available, I will also add my Arduino beacon code to the mix.  This may require an ATMega2560 device with its larger flash and RAM.  There may be sufficient space, but RAM in particular is getting a bit tight.