A while back, my Amazon account listed a little DDS signal generator module PCB based on the AD9850 in my "You May Also Like..." list. Well of course my Amazon account knows me all to well at this point and of course I purchased one out of curiosity as to what mini SDR projects that I could create with it.
Once it arrived, the first order of business was to figure out the digital interface of the AD9850 chip to be able to set the frequency/phase of the output sine wave so I needed to decide on some sort of development board and programming language to interface with it.
Ever since my Harry Potter Newspaper project based on the Raspberry Pi Zero W, I rediscovered my love for the Pi Zero W board. Because with the Raspberry Pi Imager utility, I can image an SD card with SSH and everything enabled to be able to run it headless quickly by having it boot up the first time already able to connection to my Wi-Fi with SSH enabled (Pi Zero W set up outlined here). So given my choice of using a Raspberry Pi to communicate with the AD9850 DDS module, Python was a natural choice for the programming of the interface.
AD9850 InterfaceLooking at the datasheet for the AD9850, there was a choice between a serial or parallel load of the data word setting the frequency/phase value of the output sine wave. The serial option is fairly similar to a SPI interface, but just different enough that implementing the standard SPI library with dedicated hardware interface in the Pi's microcontroller didn't quite work as desired.
The main culprit was the FQ_UD (frequency update) signal. It's function is similar to that of the chip select signal in SPI, but it needed to be pulsed before and after a data transfer, instead of being held high before/after a data transfer and held low during the data transfer.
This lead me to manually creating the digital interface outlined in the timing diagram above using bit manipulation in software using a technique known as bit banging.
What is Bit Banging?Bit banging is a method of manually creating an interface in software in lieu of using the dedicated hardware in a given microcontroller. So for example, taking a look at the RP2040 microcontroller in the Raspberry Pis, they contain dedicated hardware for SPI, I2C, PWM, UART, and other specific interfaces.
If one wanted to implement a SPI interface to talk to an external device such as an OLED display, external ADC/DAC chip, etc. they would connect the device to the GPIO pins on the Raspberry Pi's header that are routed to the SPI peripherals,
then use the SPI Python library to handle the creation of the physical SPI interface of outputting a clock signal, toggling the chip select signal at the appropriate times, and latching the data lines in/out. With code like would look similar to the following:
import time
import busio
spi = busio.SPI(board.SCK_0, MOSI=board.MOSI_0, MISO=board.MISO_0)
spi.configure(baudrate=100000)
spi.write(b'\x01')
time.sleep(0.1)
x = spi.read(f)
Bit banging on the other hand would skip the connection to the SPI interface hardware and would rely on the code in the software to manually toggle plain old GPIO lines at the appropriate times to create that physical interface of outputting a clock signal, toggling the chip select signal at the appropriate times, and latching the data lines in/out.
And while it's easier to use the dedicated hardware when possible since that means their corresponding software libraries handle everything for you under the hood so you just have to worry about what to do with said data to/from the interface, it suddenly becomes a nightmare trying to manipulate one of these pre-defined libraries to retrofit a custom interface. This is where the art of bit banging comes in handy. It's also handy for extremely resource-constrained microcontrollers, as well as in the microprocessor world where your peripherals aren't all neatly packaged up with your CPU.
Python CodeAs I mentioned, bit banging is a matter of manually toggling GPIO to create a series of pulses that are a desired interface to an external peripheral. So the only library needed for the actual interfacing in my Python code is the RPi.GPIO library.
A nice thing about the AD9850 interface is that I didn't need to worry about any data coming back in, so I set the interface clk (W_CLK), frequency update (FQ_UD), reset, and data GPIO lines to all outputs. Since I'm doing single pulses quite a bit for each clock cycle of W_CLK and FQ_UD, I threw those three lines of Python code into their own function:
def pulsePin(pin):
GPIO.output(pin, True)
GPIO.output(pin, True)
GPIO.output(pin, False)
return
Then, since I chose to use the serial load of the control word (so there was only one data line vs 8), the main body of my code was just a matter of shifting one bit out of the control word at a time. Since the control word for the frequency value is 32 bits and the control word for the phase value was 5 bits, with a 2 bit control code in the middle, my code implemented two for loops of a single bit right shift for the respective number of bits for the control words
for i in range (0,34):
GPIO.output(DATA, freq & 0x01)
pulsePin(W_CLK)
freq = freq >> 1
GPIO.output(DATA, 0x00 & 0x01)
pulsePin(W_CLK)
for j in range (0,4):
GPIO.output(DATA, phas & 0x01)
pulsePin(W_CLK)
phas = phas >> 1
GPIO.output(DATA, 0x00 & 0x01)
pulsePin(W_CLK)
pulsePin(FQ_UD)
And
before that, the calculation of the control words based on the desired output frequency/phase (as found in the AD9850 datasheet):
frequency = 10000000 # 10MHz
freq = int((frequency*(2**32))/CLKIN)
phase = 0 # phase offset of 0
phas = int ((phase*(2**5))/360)
I also found it was best to toggle the reset line followed my 3 clock cycles of W_CLK before everything transaction:
pulsePin(RESET)
pulsePin(W_CLK)
pulsePin(W_CLK)
pulsePin(W_CLK)
I will note that since I'm using Python here, there is no fine tune control of the timing here. Since the DDS module doesn't need a digital interface in the low microsecond to nanosecond range and it has some tolerance in the timing specs, I was able to get away with it here. But had I had any timing issues, I would have switched to C/C++.
import os
import sys
import RPi.GPIO as GPIO
W_CLK = 23
FQ_UD = 17
DATA = 27
RESET = 22
CLKIN = 125000000
GPIO.setmode(GPIO.BCM)
GPIO.setup(W_CLK, GPIO.OUT)
GPIO.setup(FQ_UD, GPIO.OUT)
GPIO.setup(DATA, GPIO.OUT)
GPIO.setup(RESET, GPIO.OUT)
GPIO.output(W_CLK, False)
GPIO.output(FQ_UD, False)
GPIO.output(DATA, False)
GPIO.output(RESET, False)
def pulsePin(pin):
GPIO.output(pin, True)
GPIO.output(pin, True)
GPIO.output(pin, False)
return
// start of interface code
pulsePin(RESET)
pulsePin(W_CLK)
pulsePin(W_CLK)
pulsePin(W_CLK)
pulsePin(FQ_UD)
frequency = 10000000
freq = int((frequency*(2**32))/CLKIN)
phase = 0
phas = int ((phase*(2**5))/360)
for i in range (0,34):
GPIO.output(DATA, freq & 0x01)
pulsePin(W_CLK)
freq = freq >> 1
GPIO.output(DATA, 0x00 & 0x01)
pulsePin(W_CLK)
for j in range (0,4):
GPIO.output(DATA, phas & 0x01)
pulsePin(W_CLK)
phas = phas >> 1
GPIO.output(DATA, 0x00 & 0x01)
pulsePin(W_CLK)
pulsePin(FQ_UD)
GPIO.cleanup()
Something thing that did trip me up a bit was that in the AD9850 datasheet, all of the equations calculated the frequency values in MHz. So I initially represented all of the frequency numbers in my Python code in MHz, however I found that I didn't get an accurate frequency output from the DDS module until I changed the frequency to Hz (ie instead of 125 for 125MHz for the clock crystal, 125000000 for 125000000 Hz).
Test on HardwareTo test my Python code, I hooked up the AD9850 DDS module to the Pi's GPIO header with the sine wave outputs connected to one of my USB scopes.
According to the AD9850's datasheet, it can be powered at either 5v or 3.3v so I initially had the DDS module's Vcc line connected to the 5v power pin on the Pi's header. However, I eventually found that powering the DDS module at 5v caused it to just output a 1v DC signal on the sine wave output regardless of whatever control word I sent it.
Once I changed the Vcc to 3.3v, the sine wave outputs were correct and correlated to the frequency and phase of the control words I was sending the AD9850 from the Pi:
Which made sense when I googled the part number (CET-ECJ-116) of the 125MHz oscillator chip on the DDS module that is creating the reference clock for the AD9850 (not to be confused with the digital interface clock, W_CLK) to see that it only operates on 3.3v with a 3.63v maximum:
I was saturating the oscillator so the AD9850 was just getting a DC signal as a reference clock, which it therefore created another DC signal with as its output. This is a great lesson in remembering to make sure to double-check the specs of all the components in a system before powering it. Luckily, this wasn't a magic smoke type of error.
This little DDS module can output sine waves from 0Hz to 40MHz, so I'm going to keep testing it to see what little baseband SDR applications I can make with it now that I can effectively control it's sine wave output.
Comments