Introduction
About three years ago, I spent some time generating Lissajous figures1 with a couple of AD98502 DDS synthesizers.
Today (October 2018) the AD9850 boards on eBay go for about £10, but you can buy a similar product based around the AD98333 for about £2.50 or £5 with buffers and fancy connectors.
The AD9850 handles higher-frequencies (up to 60MHz) whilst the AD8933 is limited to 12.5MHz. That’s fine for me though: I’m interested in frequencies below 100kHz. To be pedantic the AD9833 can handle frequencies up to half the frequency of the reference clock, but all the eBay boards use a 25MHz oscillator.
I was interested in using these to build a swept-frequency generator i.e. to create a signal which is basically sinusoidal with slowly increasing frequency.
Incidentally you can also get similar boards based around the Silicon Labs Si53514 chip. These generate multiple clocks and cost about £10.
A conceptual view
At the heart of the AD9833 is a phase accumulator which increments at a rate determined by both the external master clock and the value loaded into one of the chip’s registers. The official block diagram shows this:
You can see there is more than I’ve described. In particular:
- The AD9833 lets you store two different frequencies and then switch between them easily. This makes it easier to implement frequency-shift keying5.
- The phase of the output can also be tweaked. This makes it easier to implement phase-shift keying6.
However, we can ignore these if we just want to generate a slowly changing sine wave.
Arduino interfacing
Google will furnish many articles on ad9833 arduino7, but it’s not quite clear of their relative merits.
Most of the libraries implement a C++ class which hides the functionality of the chip behind a nice API. Sadly the one I tried seemed to reset the phase accumulator when the frequency changed, leading to discontinuities in the output.
Given the datasheet, It is simple to program the chip to generate a single frequency, so I thought it better to just drive it directly.
There is one wrinkle though: although the interface to the AD9833 is essentially SPI, blog posts commonly talk about it finding it hard to use the system SPI and instead use bespoke bit-banging code. I followed that advice here, and stole the SPI code from Marco Colli’s library8.
// for chip info see https://www.analog.com/en/products/ad9833.html
// SPI code taken from https://github.com/MajicDesigns/MD_AD9833/
const uint8_t _dataPin = 11;
const uint8_t _clkPin = 13;
const uint8_t _fsyncPin = 10;
// send raw 16-bit word
void spiSend(const uint16_t data)
{
digitalWrite(_fsyncPin, LOW);
uint16_t m = 1UL << 15;
for (uint8_t i = 0; i < 16; i++)
{
digitalWrite(_dataPin, data & m ? HIGH : LOW);
digitalWrite(_clkPin, LOW); //data is valid on falling edge
digitalWrite(_clkPin, HIGH);
m >>= 1;
}
digitalWrite(_dataPin, LOW); //idle low
digitalWrite(_fsyncPin, HIGH);
}
void setFreq(double f)
{
const uint16_t b28 = (1UL << 13);
const uint16_t freq = (1UL << 14);
const double f_clk = 25e6;
const double scale = 1UL << 28;
const uint32_t n_reg = f * scale / f_clk;
const uint16_t f_low = n_reg & 0x3fffUL;
const uint16_t f_high = (n_reg >> 14) & 0x3fffUL;
spiSend(b28);
spiSend(f_low | freq);
spiSend(f_high | freq);
}
void setup() {
pinMode(_clkPin, OUTPUT);
pinMode(_fsyncPin, OUTPUT);
pinMode(_dataPin, OUTPUT);
digitalWrite(_fsyncPin, HIGH);
digitalWrite(_clkPin, LOW);
}
void loop() {
setFreq(800.0);
delay(50);
setFreq(1600.0);
delay(50);
}
Raspberry Pi
The code above is entirely self-contained, so it is easy to port it to the Raspberry Pi. I switched the code from C to Python, targetting the gpiozero9 API.
import gpiozero
class AD9833:
def __init__(self, data, clk, fsync):
self.dataPin = gpiozero.OutputDevice(pin = data)
self.clkPin = gpiozero.OutputDevice(pin = clk)
self.fsyncPin = gpiozero.OutputDevice(pin = fsync)
self.fsyncPin.on()
self.clkPin.on()
self.dataPin.off()
self.clk_freq = 25.0e6
def set_freq(self, f):
flag_b28 = 1 << 13
flag_freq = 1 << 14
scale = 1 << 28
n_reg = int(f * scale / self.clk_freq)
n_low = n_reg & 0x3fff
n_hi = (n_reg >> 14) & 0x3fff
self.send16(flag_b28)
self.send16(flag_freq | n_low)
self.send16(flag_freq | n_hi)
def send16(self, n):
self.fsyncPin.off()
mask = 1 << 15
for i in range(0, 16):
self.dataPin.value = bool(n & mask)
self.clkPin.off()
self.clkPin.on()
mask = mask >> 1
self.dataPin.off()
self.fsyncPin.on()
ad = AD9833(10, 11, 8)
while 1:
for f in range(10,10000):
ad.set_freq(f)
Code
You can get the code from GitHub10.
References
- 1. ../../2015/06/ad9850-lissajous.html
- 2. https://www.analog.com/en/products/ad9850.html
- 3. https://www.analog.com/en/products/ad9833.html
- 4. https://www.silabs.com/documents/public/data-sheets/Si5351-B.pdf
- 5. https://en.wikipedia.org/wiki/Frequency-shift_keying
- 6. https://en.wikipedia.org/wiki/Phase-shift_keying
- 7. https://www.google.com/search?q=arduino+ad9833
- 8. https://github.com/MajicDesigns/MD_AD9833
- 9. https://gpiozero.readthedocs.io/en/stable/
- 10. https://github.com/mjoldfield/ad9833