Advertisements

AD9850 DDS Module: Hardware Assisted SPI and Fixed-point Frequency Stepping

Having conjured fixed-point arithmetic into working, the next step is to squirt data to the AD9850 DDS chip. Given that using the Arduino’s hardware-assisted SPI doesn’t require much in the way of software, the wiring looks like this:

Nano to DDS schematic

Nano to DDS schematic

Not much to it, is there? For reference, it looks a lot like you’d expect:

AD9850 DDS Module - swapped GND D7 pins

AD9850 DDS Module – swapped GND D7 pins

There’s no point in building an asynchronous interface with SPI interrupts and callbacks and all that rot, because squirting one byte at 1 Mb/s (a reasonable speed for hand wiring; the AD9850 can accept bits at 140+ MHz) doesn’t take all that long and it’s easier to have the low-level code stall until the hardware finishes:

#define PIN_HEARTBEAT    9          // added LED

#define PIN_RESET_DDS    7          // Reset DDS module
#define PIN_LATCH_DDS    8          // Latch serial data into DDS

#define PIN_SCK        13          // SPI clock (also Arduino LED!)
#define PIN_MISO      12          // SPI data input
#define PIN_MOSI      11          // SPI data output
#define PIN_SS        10          // SPI slave select - MUST BE OUTPUT = HIGH

void EnableSPI(void) {
  digitalWrite(PIN_SS,HIGH);        // set SPI into Master mode
  SPCR |= 1 << SPE;
}

void DisableSPI(void) {
  SPCR &= ~(1 << SPE);
}

void WaitSPIF(void) {
  while (! (SPSR & (1 << SPIF))) {
    TogglePin(PIN_HEARTBEAT);
    TogglePin(PIN_HEARTBEAT);
    continue;
  }
}

byte SendRecSPI(byte Dbyte) {           // send one byte, get another in exchange
  SPDR = Dbyte;
  WaitSPIF();
  return SPDR;                          // SPIF will be cleared
}

With that in hand, turning on the SPI hardware and waking up the AD9850 looks like this:

void EnableDDS(void) {

  digitalWrite(PIN_LATCH_DDS,LOW);          // ensure proper startup

  digitalWrite(PIN_RESET_DDS,HIGH);         // minimum reset pulse 40 ns, not a problem
  digitalWrite(PIN_RESET_DDS,LOW);
  delayMicroseconds(1);                     // max latency 100 ns, not a problem

  DisableSPI();                             // allow manual control of outputs
  digitalWrite(PIN_SCK,LOW);                // ensure clean SCK pulse
  PulsePin(PIN_SCK);                        //  ... to latch hardwired config bits
  PulsePin(PIN_LATCH_DDS);                  // load hardwired config bits = begin serial mode

  EnableSPI();                              // turn on hardware SPI controls
  SendRecSPI(0x00);                         // shift in serial config bits
  PulsePin(PIN_LATCH_DDS);                  // load serial config bits
}

Given 32 bits of delta phase data and knowing the DDS output phase angle is always zero, you just drop five bytes into a hole in the floor labeled “SPI” and away they go:

void WriteDDS(uint32_t DeltaPhase) {

  SendRecSPI((byte)DeltaPhase);             // low-order byte first
  SendRecSPI((byte)(DeltaPhase >> 8));
  SendRecSPI((byte)(DeltaPhase >> 16));
  SendRecSPI((byte)(DeltaPhase >> 24));

  SendRecSPI(0x00);                         // 5 MSBs = phase = 0, 3 LSBs must be zero

  PulsePin(PIN_LATCH_DDS);                  // write data to DDS
}

In order to have something to watch, the loop() increments the output frequency in steps of 0.1 Hz between 10.0 MHz ± 3 Hz, as set by the obvious global variables:

      PrintFixedPtRounded(Buffer,ScanFreq,1);

      TestCount.fx_64 = MultiplyFixedPt(ScanFreq,CtPerHz);
      printf("%12s -> %9ld\n",Buffer,TestCount.fx_32.high);

      WriteDDS(TestCount.fx_32.high);

      ScanFreq.fx_64 += ScanStep.fx_64;

      if (ScanFreq.fx_64 > (ScanTo.fx_64 + ScanStep.fx_64 / 2)) {
        ScanFreq = ScanFrom;
        Serial.println("Scan restart");
      }

Which produces output like this:

DDS SPI exercise
Ed Nisley - KE4ZNU - May 2017

Inputs: 124999656 = 125000000-344
Osc freq: 124999656.000000000
Hz/Ct: 0.029103750
Ct/Hz: 34.359832926
0.1 Hz Ct: 3.435983287
Test frequency:  10000000.0000
Delta phase: 343598329

Scan limits
 from:   9999997.0
   at:  10000000.0
   to:  10000003.0

Sleeping for a while ...

Startup done!

Begin scanning

  10000000.0 -> 343598329
  10000000.1 -> 343598332
  10000000.2 -> 343598336
  10000000.3 -> 343598339
  10000000.4 -> 343598343
  10000000.5 -> 343598346
  10000000.6 -> 343598349
  10000000.7 -> 343598353
  10000000.8 -> 343598356
  10000000.9 -> 343598360
  10000001.0 -> 343598363
  10000001.1 -> 343598367
  10000001.2 -> 343598370
  10000001.3 -> 343598373
<<< snippage >>>

The real excitement happens while watching the DDS output crawl across the scope screen in relation to the 10 MHz signal from the Z8301 GPS-locked reference:

DDS GPS - 10 MHz -48 Hz offset - zero beat

DDS GPS – 10 MHz -48 Hz offset – zero beat

The DDS sine in the upper trace is zero-beat against the GPS reference in the lower trace. There’s no hardware interlock, but they’re dead stationary during whatever DDS output step produces exactly 10.0000000 MHz. The temperature coefficient seems to be around 2.4 Hz/°C, so the merest whiff of air changes the frequency by more than 0.1 Hz.

It’s kinda like watching paint dry or a 3D printer at work, but it’s my paint: I like it a lot!

The Arduino source code as a GitHub Gist:

Advertisements

,