FM DDS: SPI Mock 2

Doing the DDS calculations in full-frontal double floating point turns out to be maybe fast enough:

DDS Mock - 0 VAC - SPI
DDS Mock – 0 VAC – SPI

I set the ADC to HIGH_SPEED conversion and sampling, reducing the time between the start of conversion (first pulse in D1) and the ADC end-of-conversion interrupt (rising edge in D2) from 4.7 μs to 2.6 μs, more-or-less, kinda-sorta.

The ADC hardware can return the average of several sample taken in quick succession, so I set it to average four samples. The vertical cursors show the combination of fast conversion and averaging requires 7 μs (-ish) from start to finish: long enough to justify separating the two by an interrupt and short enough to allow calculations after fetching the result.

The purple trace shows the analog input voltage hovering close to a constant VCC/2 (about 1.6+ V), rather than the sine-wave I used earlier, again courtesy of the scope’s arbitrary function generator. The loop() dumps the min and max ADC values (minus half the ADC range (4096/2= 2048):

    -4 to     2
    -3 to     2
    -3 to     2

A span of half a dozen counts = 3 bits means the 12 bit ADC really delivers 9 bits = 0.2% resolution = 54 dB dynamic range = probably not good enough. However, the “circuit” is an open-air hairball on the bench, driven from the scope’s arbitrary waveform generator in high-Z mode, so things can only get better with more any attention to detail.

The 1.9 μs gap between the first and second burst of SPI clocks contains all the floating-point calculations required to convert an ADC sample to DDS delta-phase bits:

void adc0_isr(void) {

  int Audio;

  digitalWriteFast(PIN_ANALOG,HIGH);

  AnalogSample = adc->readSingle();             	  // fetch just-finished sample
  Audio = AnalogSample - 2048;                      // convert to AC signal

  DDSBuffer.Phase = 0;

  SPI.beginTransaction(SPISettings(8000000, MSBFIRST, SPI_MODE0));
  digitalWriteFast(PIN_DDS_FQUD, LOW);

  SPI.transfer(DDSBuffer.Phase);

  DDSBuffer.DeltaPhase = (uint32_t)((((double)Audio / 2048.0) * Deviation + Crystal) * CountPerHertz);

  SPI.transfer((uint8_t)(DDSBuffer.DeltaPhase >> 24));      // MSB first!

  if (Audio > AudioMax)                                     // ignore race conditions
    AudioMax = Audio;
  if (Audio < AudioMin) AudioMin = Audio; SPI.transfer((uint8_t)(DDSBuffer.DeltaPhase >> 16));

  SPI.transfer((uint8_t)(DDSBuffer.DeltaPhase >>  8));
  SPI.transfer((uint8_t)DDSBuffer.DeltaPhase);

  SPI.endTransaction();                         // do not raise FQ_UD until next timer tick!

  digitalWriteFast(PIN_ANALOG,LOW);
}

A closer look lets the scope decode and present the SPI data:

DDS Mock - 0 VAC - SPI detail
DDS Mock – 0 VAC – SPI detail

The program calculates and displays various “constants” I set for convenience:

FM Modulated DDS
Ed Nisley KE4ZNU
 serial wait: 890 ms

DDS clock:     180000000.000 Hz
CountPerHertz:        23.861 ct
HertzPerCount:         0.042 Hz

Crystal:    20000000.000 Hz
Deviation:      5000.000 Hz

You can confirm the SPI data by working backwards with a calculator:

  • DDS delta-phase register bytes: 1C 71 C6 E2 = 477218530 decimal
  • Multiply by 180 MHz / 2^32 to get frequency: 1999997.5506 Hz
  • Subtract nominal 20.0 MHz crystal to get modulation: -2.4494 Hz
  • Divide by nominal 5.0 kHz deviation to get fractional modulation: -4.89.9e-6
  • Multiply by half the ADC range (4096/2) to get ADC counts: -1.003
  • Add 2048 to get the actual ADC sample: 2047

Nicely inside the range of values reported by the main loop, whew.

Which means I can avoid screwing around with fixed-point arithmetic until such time as clawing back a few microseconds makes a meaningful difference.

Now, to begin paying attention to those pesky hardware details …

The TeensyDuino source code as a GitHub Gist:

// FM DDS
// Ed Nisley - KE4ZNU
// 2017-04-19 Demo 1
#include <IntervalTimer.h>
#include <ADC.h>
#include <SPI.h>
#define PIN_HEART 14
#define PIN_TIMER 15
#define PIN_ANALOG 16
#define PIN_GLITCH 17
#define PIN_AUDIO A9
#define PIN_DDS_FQUD 10
// data to DDS MOSI0 11
// no data from DDS MISO0 12
// DDS clock on SCK0 13 -- also LED
#define BUILTIN_LED 13
//---------------------
// Useful constants
int SamplePeriod = 25; // microseconds per analog sample
//---------------------
// Globals
ADC *adc = new ADC();
IntervalTimer timer;
volatile int AnalogSample;
volatile int AudioMax = -4096;
volatile int AudioMin = 4096;
typedef struct {
uint8_t Phase;
uint32_t DeltaPhase; // DDS expects MSB first!
} DDS;
DDS DDSBuffer;
double DDSClock = 180.0e6; // nominal DDS oscillator
double CountPerHertz, HertzPerCount; // DDS delta-phase increments
double Crystal = 20.0e6; // nominal DDS frequency
double Deviation = 5.0e3; // nominal FM signal deviation (one-sided)
double TestFreq;
//---------------------
// Handy routines
void FlipPin(int pin) {
digitalWriteFast(pin,!digitalRead(pin));
}
void PulsePin(int p) {
FlipPin(p);
FlipPin(p);
}
//---------------------
// Timer handler
void timer_callback(void) {
digitalWriteFast(PIN_TIMER,HIGH);
digitalWriteFast(PIN_DDS_FQUD,HIGH); // latch previously shifted bits
adc->startSingleRead(PIN_AUDIO, ADC_0); // start ADC conversion
analogWriteDAC0(AnalogSample); // show previous audio sample
digitalWriteFast(PIN_TIMER,LOW);
}
//---------------------
// Analog read handler
void adc0_isr(void) {
int Audio;
digitalWriteFast(PIN_ANALOG,HIGH);
AnalogSample = adc->readSingle(); // fetch just-finished sample
Audio = AnalogSample - 2048; // convert to AC signal
DDSBuffer.Phase = 0;
SPI.beginTransaction(SPISettings(8000000, MSBFIRST, SPI_MODE0));
digitalWriteFast(PIN_DDS_FQUD, LOW);
SPI.transfer(DDSBuffer.Phase);
DDSBuffer.DeltaPhase = (uint32_t)((((double)Audio / 2048.0) * Deviation + Crystal) * CountPerHertz);
SPI.transfer((uint8_t)(DDSBuffer.DeltaPhase >> 24)); // MSB first!
if (Audio > AudioMax) // ignore race conditions
AudioMax = Audio;
if (Audio < AudioMin)
AudioMin = Audio;
SPI.transfer((uint8_t)(DDSBuffer.DeltaPhase >> 16));
SPI.transfer((uint8_t)(DDSBuffer.DeltaPhase >> 8));
SPI.transfer((uint8_t)DDSBuffer.DeltaPhase);
SPI.endTransaction(); // do not raise FQ_UD until next timer tick!
digitalWriteFast(PIN_ANALOG,LOW);
}
//---------------------
// Hardware setup
void setup(void) {
pinMode(BUILTIN_LED,OUTPUT); // will eventually become SCK0
pinMode(PIN_HEART, OUTPUT); // show we arrived
digitalWrite(PIN_HEART,LOW);
PulsePin(PIN_HEART);
PulsePin(PIN_HEART);
pinMode(PIN_TIMER,OUTPUT);
digitalWrite(PIN_TIMER,LOW);
pinMode(PIN_GLITCH,OUTPUT);
digitalWrite(PIN_GLITCH,LOW);
pinMode(PIN_ANALOG,OUTPUT);
digitalWrite(PIN_ANALOG,LOW);
pinMode(PIN_AUDIO,INPUT);
pinMode(PIN_DDS_FQUD,OUTPUT);
digitalWriteFast(PIN_DDS_FQUD,HIGH);
Serial.begin(115200);
int waited = 0;
while (!Serial && waited < 3000) { // fall out after a few seconds
delay(1);
waited++;
if (! (waited % 50))
FlipPin(BUILTIN_LED);
}
Serial.printf("FM Modulated DDS\nEd Nisley KE4ZNU\n");
Serial.printf(" serial wait: %d ms\n\n",waited);
SPI.begin();
SPI.usingInterrupt(255); // attached through analog IRQs
adc->setAveraging(4); // choices: 0, 4, 8, 16, 32
adc->setResolution(12); // choices: 8, 10, 12, 16
adc->setConversionSpeed(ADC_CONVERSION_SPEED::HIGH_SPEED);
adc->setSamplingSpeed(ADC_SAMPLING_SPEED::HIGH_SPEED);
adc->enableInterrupts(ADC_0);
if (!timer.begin(timer_callback, SamplePeriod)) {
Serial.printf("Timer start failed\n");
while (true) {
FlipPin(BUILTIN_LED);
delay(75);
}
}
CountPerHertz = (1LL << 32) / DDSClock;
HertzPerCount = 1.0 / CountPerHertz;
Serial.printf("DDS clock: %13.3f Hz\n",DDSClock);
Serial.printf("CountPerHertz: %13.3f ct\n",CountPerHertz);
Serial.printf("HertzPerCount: %13.3f Hz\n\n",HertzPerCount);
TestFreq = Crystal;
Serial.printf("Crystal: %13.3f Hz\n",Crystal);
Serial.printf("Deviation: %13.3f Hz\n",Deviation);
Serial.printf("\nSetup done\n");
}
//---------------------
// Do things forever
void loop(void) {
digitalWrite(PIN_HEART,HIGH);
Serial.printf(" %5d to %5d\n",AudioMin,AudioMax);
AudioMax = 99*AudioMax/100; // ignore race conditions
AudioMin = 99*AudioMin/100;
digitalWrite(PIN_HEART,LOW);
delay(500);
}
view raw FMDDS.ino hosted with ❤ by GitHub