Cheap New Ham

home

Arduino I/Q Audio Frequency DDS

21 Jul 2015

I have a long-term dream to design a single-signal direct-conversion receiver using a microcontroller for the audio processing. In trying to figure out how I—a relative newbie to digital and RF design—could design a transceiver, I figured that I needed some basic test gear and a way to gain experience with microcontroller programming. My first attempt was to turn an eBay AD9850 module into a VFO using an Arduino Nano as the controller and various displays and buttons and encoders for the interface. I may document that one eventually, but that’s a project that’s been done a million times before.

For my next project, I thought that while the VFO could be used to produce known input to the RF stages of a receiver design, if I wanted to start working on the audio processing parts of the code, then I needed a way to inject input into the audio stages of the receiver. A receiver of this type uses two audio frequency (or baseband) input channels known as in-phase and quadrature or I/Q for short. So what I needed was not just a tool to generate audio frequency input, but one that could make two channels of audio with a particular phase relationship.

One of the advantages of this receiver architecture is that the I/Q signals are not particularly high frequency so they can be generated by a fairly modest microcontroller such as the 16 MHz Arduino Nano. The techniques for generating DDS signals are fairly well-known. I borrowed some code for initializing the Arduino timers from http://interface.khm.de/index.php/lab/interfaces-advanced/arduino-dds-sinewave-generator/ and the Analog Devices tutorial http://www.analog.com/media/en/training-seminars/tutorials/MT-085.pdf is a very fine document for explaining the general idea behind DDS

Pulse Width Modulation and low-pass filtering

An Arduino cannot output analog voltages. It has no Digital-to-Analog Converter (DAC). Instead, when one uses analogWrite on an Arduino, it uses a technique called Pulse Width modulation (PWM) to fake it. PWM uses one of the Arduino’s timer routines to count clock cycles and turn the output pin ON for some cycles and then OFF for the rest of the cycles. The timer is an 8-bit timer so there are only 256 levels of analog voltages or 8 bits of PWM DAC resolution. By turning off any prescaling, it is possible to run the PWM clock as fast as the system clock so that it sets the output voltage every 256 clock cycles. Since the system clock runs at 16 MHz, this means that the PWM signal is a square wave at 65.5 kHz and a duty cycle that depends on the value specified in analogWrite.

This means that the PWM-based DAC cannot be used to output voltages that change at a frequency higher than that. (Don’t worry, that ends up being a fairly light requirement on output frequency.) The other thing that PWM means is that we need to filter out the high-frequency variations to see the analog voltages that we are looking for. This means that our output channels will need some sort of low-pass filtering. In this project, we use two stages of cascaded RC low-pass filters with cutoff frequencies of 4 kHz and 7 kHz.

DDS basics

The basic idea of Direct Digital Synthesis (DDS) is that we store a table of values of the sine function and then we use some clever timing based on our desired frequency to decide when to set the output voltage to the next level in the table. The Analog Devices tutorial linked above gives a good explanation of the ideas involved. The details in this project are that I use a table of 1024 sine values which gives the output in levels from 0 to 255, the same 8 bits of resolution that our PWM DAC has. The DDS has its own clock frequency which is specified by how many system clock cycles before it should update again. It isn’t worth updating faster than the PWM clock and we have to be sure that the update routine has enough time to complete before we call it again. Anything between 500 and 800 clock cycles has been working fine for me.

This provides another limit on the highest frequency that the DDS can generate. The DDS can’t generate frequencies higher than one-fourth to one-third of the DDS clock frequency. In this case, that comes out to around 5 kHz. By that frequency, the waveforms start to look pretty rough. A better low-pass filter could provide better performance up to that frequency limit.

Schematic and Construction

In a recent mailing list message, Ashhar Farhan, the designer of the BitX and Minima transceivers, mentioned that he couldn’t spend the time to draw schematics, but in less than an hour, he could write up a new project and photograph the schematic from out of his notebook. Following that sage advice, I offer the above quick and dirty version of the schematic.

I built the circuit on a breadboard which made it easy to add the filters and to probe the output with the oscilloscope. Power comes from the USB port on the Arduino Nano.

Pudding

They say that the proof is in the pudding, so here it is on the scope. This is what the waveform looks like for an I/Q pair of 4 kHz sine waves. The waveforms look much better at lower frequencies around 1-2 kHz. Since this is intended for testing a CW receiver, that range of frequencies is plenty for feeding into the audio processing stage of the receiver. It wouldn’t work for making a musical instrument, but it should be good enough for this purpose.

Code

dds_test.ino

This is the main file for the Arduino project.

#include <avr/pgmspace.h>
#include <avr/interrupt.h>
#include <avr/io.h>
#include "sine_data.h"

// pin definitions
#define OUTPUT1 3
#define OUTPUT2 11

// How often should the DDS clock tick in units of processor clock ticks
const uint16_t DDS_TICKS = 800;

volatile uint32_t accum1 = 0;
volatile uint32_t accum2 = 1L << 30; // one quarter period ahead
volatile uint32_t tuning_word;
volatile uint16_t table_index; // N=1024 entries in the sine table

////////////////////////////
// output frequency in hertz
const double frequency = 1250;
//
////////////////////////////

void setup() {
  pinMode(OUTPUT1, OUTPUT);
  pinMode(OUTPUT2, OUTPUT);
  
  //Fast PWM mode
  TCCR2A = _BV(COM2A1) | _BV(COM2B1) | _BV(WGM21) | _BV(WGM20);
  //No pre-scaler
  TCCR2B = _BV(CS20);

  // Set up Timer 1 for CTC mode interrupts every DDS_TICKS cycles
  cli();   // disable interrupts  
  // Set CTC mode (Section 15.9.2 Clear Timer on Compare Match)
  // WGM = 0b0100, TOP = OCR1A, Update 0CR1A Immediate (Table 15-4)
  // Have to set OCR1A *after*, otherwise it gets reset to 0!
  TCCR1B = (TCCR1B & ~_BV(WGM13)) | _BV(WGM12);
  TCCR1A = TCCR1A & ~(_BV(WGM11) | _BV(WGM10));
  // No prescaler, CS = 0b001 (Table 15-5)
  TCCR1B = (TCCR1B & ~(_BV(CS12) | _BV(CS11))) | _BV(CS10);
  // Set the compare register (OCR1A).
  // OCR1A is a 16-bit register, so we have to do this with
  // interrupts disabled to be safe.
  OCR1A = DDS_TICKS;
  // Enable interrupt when TCNT1 == OCR1A (p.136)
  TIMSK1 |= _BV(OCIE1A);

  // Figure out tuning word
  tuning_word = pow(2, 32) * frequency / (16000000.0 / DDS_TICKS);

  // set initial pulse widths
  table_index = accum1 >> 22; // Keep 10 most significant bits
  OCR2A = pgm_read_byte(&sinewave_data[table_index]);
  table_index = accum2 >> 22; // Keep 10 most significant bits
  OCR2B = pgm_read_byte(&sinewave_data[table_index]);

  // Check setup constants
  Serial.begin(9600);
  Serial.print("Output frequency (Hz): ");
  Serial.println(frequency);
  Serial.print("DDS clock frequency (Hz): ");
  Serial.println(16000000. / DDS_TICKS);
  Serial.print("Tuning word: ");
  Serial.println(tuning_word);
  Serial.print("initial accum1: ");
  Serial.println(accum1);
  Serial.print("initial accum2: ");
  Serial.println(accum2);
  sei();
}

void loop() {
  // put your main code here, to run repeatedly:
  //Serial.print("accum1: ");
  //Serial.println(accum1);
  //Serial.print("accum2: ");
  //Serial.println(accum2);
  //delay(1000);
}

ISR(TIMER1_COMPA_vect) {
  accum1 = accum1 + tuning_word;
  accum2 = accum2 + tuning_word;

  table_index = accum1 >> 22; // Keep 10 most significant bits
  OCR2A = pgm_read_byte(&sinewave_data[table_index]);
  table_index = accum2 >> 22; // Keep 10 most significant bits
  OCR2B = pgm_read_byte(&sinewave_data[table_index]);
}

sine_data.h

This a header file for the sine table entries.

/* Sinewave table
   Using N=1024 in order to use every possible value in the dynamic
   8-bit dynamic range
 */
 
const unsigned char sinewave_data[] PROGMEM = {
127, 128, 129, 129, 130, 131, 132, 132, 133, 134, 135, 136, 136, 137,
138, 139, 139, 140, 141, 142, 143, 143, 144, 145, 146, 146, 147, 148,
149, 150, 150, 151, 152, 153, 153, 154, 155, 156, 156, 157, 158, 159,
159, 160, 161, 162, 163, 163, 164, 165, 166, 166, 167, 168, 168, 169,
170, 171, 171, 172, 173, 174, 174, 175, 176, 177, 177, 178, 179, 179,
180, 181, 182, 182, 183, 184, 184, 185, 186, 186, 187, 188, 188, 189,
190, 191, 191, 192, 193, 193, 194, 195, 195, 196, 197, 197, 198, 198,
199, 200, 200, 201, 202, 202, 203, 204, 204, 205, 205, 206, 207, 207,
208, 208, 209, 210, 210, 211, 211, 212, 213, 213, 214, 214, 215, 215,
216, 217, 217, 218, 218, 219, 219, 220, 220, 221, 221, 222, 223, 223,
224, 224, 225, 225, 226, 226, 227, 227, 228, 228, 228, 229, 229, 230,
230, 231, 231, 232, 232, 233, 233, 233, 234, 234, 235, 235, 236, 236,
236, 237, 237, 238, 238, 238, 239, 239, 239, 240, 240, 241, 241, 241,
242, 242, 242, 243, 243, 243, 244, 244, 244, 244, 245, 245, 245, 246,
246, 246, 247, 247, 247, 247, 248, 248, 248, 248, 249, 249, 249, 249,
249, 250, 250, 250, 250, 250, 251, 251, 251, 251, 251, 252, 252, 252,
252, 252, 252, 252, 253, 253, 253, 253, 253, 253, 253, 253, 254, 254,
254, 254, 254, 254, 254, 254, 254, 254, 254, 254, 254, 254, 254, 254,
254, 254, 254, 254, 255, 254, 254, 254, 254, 254, 254, 254, 254, 254,
254, 254, 254, 254, 254, 254, 254, 254, 254, 254, 254, 253, 253, 253,
253, 253, 253, 253, 253, 252, 252, 252, 252, 252, 252, 252, 251, 251,
251, 251, 251, 250, 250, 250, 250, 250, 249, 249, 249, 249, 249, 248,
248, 248, 248, 247, 247, 247, 247, 246, 246, 246, 245, 245, 245, 244,
244, 244, 244, 243, 243, 243, 242, 242, 242, 241, 241, 241, 240, 240,
239, 239, 239, 238, 238, 238, 237, 237, 236, 236, 236, 235, 235, 234,
234, 233, 233, 233, 232, 232, 231, 231, 230, 230, 229, 229, 228, 228,
228, 227, 227, 226, 226, 225, 225, 224, 224, 223, 223, 222, 221, 221,
220, 220, 219, 219, 218, 218, 217, 217, 216, 215, 215, 214, 214, 213,
213, 212, 211, 211, 210, 210, 209, 208, 208, 207, 207, 206, 205, 205,
204, 204, 203, 202, 202, 201, 200, 200, 199, 198, 198, 197, 197, 196,
195, 195, 194, 193, 193, 192, 191, 191, 190, 189, 188, 188, 187, 186,
186, 185, 184, 184, 183, 182, 182, 181, 180, 179, 179, 178, 177, 177,
176, 175, 174, 174, 173, 172, 171, 171, 170, 169, 168, 168, 167, 166,
166, 165, 164, 163, 163, 162, 161, 160, 159, 159, 158, 157, 156, 156,
155, 154, 153, 153, 152, 151, 150, 150, 149, 148, 147, 146, 146, 145,
144, 143, 143, 142, 141, 140, 139, 139, 138, 137, 136, 136, 135, 134,
133, 132, 132, 131, 130, 129, 129, 128, 127, 126, 125, 125, 124, 123,
122, 122, 121, 120, 119, 118, 118, 117, 116, 115, 115, 114, 113, 112,
111, 111, 110, 109, 108, 108, 107, 106, 105, 104, 104, 103, 102, 101,
101, 100, 99, 98, 98, 97, 96, 95, 95, 94, 93, 92, 91, 91, 90, 89, 88,
88, 87, 86, 86, 85, 84, 83, 83, 82, 81, 80, 80, 79, 78, 77, 77, 76,
75, 75, 74, 73, 72, 72, 71, 70, 70, 69, 68, 68, 67, 66, 66, 65, 64,
63, 63, 62, 61, 61, 60, 59, 59, 58, 57, 57, 56, 56, 55, 54, 54, 53,
52, 52, 51, 50, 50, 49, 49, 48, 47, 47, 46, 46, 45, 44, 44, 43, 43,
42, 41, 41, 40, 40, 39, 39, 38, 37, 37, 36, 36, 35, 35, 34, 34, 33,
33, 32, 31, 31, 30, 30, 29, 29, 28, 28, 27, 27, 26, 26, 26, 25, 25,
24, 24, 23, 23, 22, 22, 21, 21, 21, 20, 20, 19, 19, 18, 18, 18, 17,
17, 16, 16, 16, 15, 15, 15, 14, 14, 13, 13, 13, 12, 12, 12, 11, 11,
11, 10, 10, 10, 10, 9, 9, 9, 8, 8, 8, 7, 7, 7, 7, 6, 6, 6, 6, 5, 5, 5,
5, 5, 4, 4, 4, 4, 4, 3, 3, 3, 3, 3, 2, 2, 2, 2, 2, 2, 2, 1, 1, 1, 1,
1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1,
1, 1, 1, 1, 1, 1, 1, 2, 2, 2, 2, 2, 2, 2, 3, 3, 3, 3, 3, 4, 4, 4, 4,
4, 5, 5, 5, 5, 5, 6, 6, 6, 6, 7, 7, 7, 7, 8, 8, 8, 9, 9, 9, 10, 10,
10, 10, 11, 11, 11, 12, 12, 12, 13, 13, 13, 14, 14, 15, 15, 15, 16,
16, 16, 17, 17, 18, 18, 18, 19, 19, 20, 20, 21, 21, 21, 22, 22, 23,
23, 24, 24, 25, 25, 26, 26, 26, 27, 27, 28, 28, 29, 29, 30, 30, 31,
31, 32, 33, 33, 34, 34, 35, 35, 36, 36, 37, 37, 38, 39, 39, 40, 40,
41, 41, 42, 43, 43, 44, 44, 45, 46, 46, 47, 47, 48, 49, 49, 50, 50,
51, 52, 52, 53, 54, 54, 55, 56, 56, 57, 57, 58, 59, 59, 60, 61, 61,
62, 63, 63, 64, 65, 66, 66, 67, 68, 68, 69, 70, 70, 71, 72, 72, 73,
74, 75, 75, 76, 77, 77, 78, 79, 80, 80, 81, 82, 83, 83, 84, 85, 86,
86, 87, 88, 88, 89, 90, 91, 91, 92, 93, 94, 95, 95, 96, 97, 98, 98,
99, 100, 101, 101, 102, 103, 104, 104, 105, 106, 107, 108, 108, 109,
110, 111, 111, 112, 113, 114, 115, 115, 116, 117, 118, 118, 119, 120,
121, 122, 122, 123, 124, 125, 125, 126};

Questions or Comments

This is the first project that I have designed and built and published online. If you have any comments or suggestions for improvements, please let me know via email at neilmartinsenburrell@gmail.com.