Thursday, December 31, 2020

Analog Devices AD9833: Accurate, Fast Sine, Triangle and Square waves using Python

This is hopefully the last of the FTDI232H posts? Not many readers for these FTDI posts so far; guess over the holidays we have better things to do (not me?). 

Really, it's a pretty cool little board.... 


To wrap up we will use affordable breakout boards to make function generator accurate to 10Mhz+. To make this go, you will need an FTDI232H breakout board, Python 3.6+ and the PYFTDI module, and an AD9833 breakout board; the Bob's are available from Amazon, AliExpress, and all the other usual places. 

You may want to first read about getting started with the FTDI 232H breakout board in the last two posts, here and here.

I hate breadboarding so a small PCB for this project may come around one of these days?


I already got basic AD9833 SPI going at the end of the post here; this time I will flesh it out so you can edit values for the waveform and frequency directly in the Python code; out comes the waveform at the frequency specified on your scope--accurately.  

(Get the python code here from Github, but here is the 12-26-20 version:)

from pyftdi.spi import SpiController

############user changes these###############

user_freq = 1000

wave = 3 #1--sine   2--pulse  3--tri

##########################################


#pinout from H232 for SPI


'''

#WE WANT TO BE ABLE TO ENTER A FREQ TO SHOW ON SCOPE.

# Instantiate a SPI controller

# We need want to use A*BUS4 for /CS, so at least 2 /CS lines should be

# reserved for SPI, the remaining IO are available as GPIOs.



def get_dec_freq(freq):

    bignum = 2**28

    f = freq

    clock=25000000 #if your clock is different enter that here./

    dec_freq = f*bignum/clock

    return int(dec_freq)



padded_binary = 0

bits_pushed = 0

d = get_dec_freq(user_freq)


print("freq int returned is: " + str(d))


#turn into binary string.

str1 = bin(d)

#print(str1)


#get rid of first 2 chars.

str2 = str1[2:]

#print(str2)


#pad whatever we have so far to 28 bits:

longer = str2.zfill(28)

#print("here is 28 bit version of string")

#print(str(longer))

#print("here is length of that string")

#print(len(str(longer)))


lm1 = "01" + longer[:6]

lm2 = longer[6:14]

rm1 = "01" + longer[14:20]

rm2 = longer[20:]

# print(lm1 + " " + lm2  + " " + rm1 + " " + rm2)



def str_2_int(strx):

    numb = int(strx, 2)

    return numb


lm1x = str_2_int(lm1)

lm2x = str_2_int(lm2)

rm1x = str_2_int(rm1)

rm2x = str_2_int(rm2)

print(str(lm1x) + " " + str(lm2x)  + " " + str(rm1x) + " " + str(rm2x))


##########

#freq0_loadlower16 = [80,199]

#freq0_loadupper16 = [64,0]

#64 0 80 198



spi = SpiController(cs_count=2)

device = 'ftdi://ftdi:232h:0:1/1'

# Configure the first interface (IF/1) of the FTDI device as a SPI master

spi.configure(device)


# Get a port to a SPI slave w/ /CS on A*BUS4 and SPI mode 2 @ 10MHz

slave = spi.get_port(cs=1, freq=8E6, mode=2)



freq0_loadlower16 = [rm1x,rm2x]

freq0_loadupper16 = [lm1x,lm2x]


cntrl_reset = [33,0]



phase0 = [207,0]


#new waveforms here


cntrl_reset = [33,0]


if wave == 3: # tri

    cntrl_write = [32,2]  #tri

    print("tri")

if wave == 2:

    cntrl_write = [32,32]  #square

    print("square")

if wave == 1:

    cntrl_write = [32,0]  #sine

    print("sin")


send2_9833 = cntrl_reset + freq0_loadlower16 + freq0_loadupper16 + phase0 + cntrl_write


print(send2_9833)


qq = bytearray(send2_9833)

# Synchronous exchange with the remote SPI slave

#write_buf = qq

#read_buf = slave.exchange(write_buf, duplex=False)

slave.exchange(out=qq, readlen=0, start=True, stop=True, duplex=False, droptail=0)

slave.flush()


The only tricky thing to getting the python to go was formulating the 14 and 12 bit words for 9833 data, then shoving that correctly into bytearrays. I tried masks, bit shifting and other things, but none of it worked (works fine in C, but not in Python). The main problem apparently is that Python treats ints as binary data behind the scenes, so 0b01110000 is always treated as 0b0111. So much for using masks--it's all fine unless you need to pad your 1's and 0's to 28 or 32 bits as you do with the 9833. 

To get around this, I ended up doing something damn kludgy or dodgey or dicey or just not smart: I changed the 1's and 0's to strings (python is good at converting data types), used standard python tools to extract and manipulate the 1's and 0's to what was needed by the 9833; used zfill to make the string 28 chars long, and added 2-3 bits to the front of each string as needed by the 9833 to signify if the data is control, frequency, phase or whatever. Then at the last minute I converted the string back to integers to shove into a byte array. 

Complex and ugly but it worked.

Is there a more elegant Python way to code this?  There must be but I can't figure it out, if you have ideas, enter it into the comments because I am curious.

To use the code, change the frequency and waveform values at the top then run it.  That's it.

It would of course be easy to use something like the argparse module to make these arguments into a command line statement but what is here is good enough for me.

The only thing left to do, to get a really cool little USB driven function generator, is to up the gain at output (easy, it's an op amp) and perhaps make it bipolar (not as easy, but certainly possible; it means creating a -5V signal somehow from the existing 5V signal).  Also the 9833 doesn't do ramp waves--sorry, but it would be possible to add more circuitry to convert the triangle wave to ramp I figure.

Of course there are less than a million AD9833 based gizmos already on the web, here and using arduino only, here for instance.  But I am not sure there is another that uses Python and is USB driven? If there already is, oh well.  I didn't look around that much and it was fun coding all this.  

I am going to not post for at least a few weeks while I take some online classes and try to sharpen some programming skills. After that more geeky posts. I feel things heading into less analog territory in 2021?  Probably. See ya next year!


No comments:

Post a Comment

EFM/KORG770 VCF--BUILD: yes, SOUND: no

Ahoy!  This time, I wanted to refine the quick prototyping idea discussed midway through this previous post : Minimal breadboarding--I hate ...