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

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:

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