The Smell of Molten Projects in the Morning

Ed Nisley's Blog: Shop notes, electronics, firmware, machinery, 3D printing, laser cuttery, and curiosities. Contents: 100% human thinking, 0% AI slop.

The New Hotness

  • 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