Driving the AD9850 signal generator with a picaxe

matherp

Senior Member
If you look on ebay you will see lots of AD9850 signal generator modules available from £3.25. These are capable of up to 40Mhz operation. The issue with these is that to set the frequency you need to load a 32 bit number and to calculate it accurately you really need 64 bit precision arithmetic.

However, I found it was just about possible using a picaxe and a hacked version of Jeremy Leach's 32 bit routines.

The attached snippet calculates the 4 data bytes needed (the control word should be set to 0). These can be loaded via a parallel load or by a clocked serial load.
The code allows for frequencies from 1 to 99,990,000 Hz accurate to 1 in 10,000.

I haven't included the hardware interfacing code as this will be implementation specific - but, as is, the code runs in the simulator and you can compare the output with AD's online calculator at:

http://designtools.analog.com/dt/dds/ad9850.html

NB: the code assumes a 125Mhz crystal which seems to be the default. It would need modifying for another crystal frequency.

Its amazing what these little chips are capable of given a little ingenuity :)

Hope this is useful

Peter

Code:
#picaxe20x2
setfreq m64
#simspeed 0
'
' Calculates 32 bit data load for frequency on AD9850
' accurate to 0.01%
' 32-Bit (unsigned) maths on a PICAXE   
' J.Leach 2006 , modified PM 26/10/2012  
'
Symbol Procedure0StartAddress = 0
Eeprom 0,("+E*F*G/H=I")
'Word 0 (b0 and b1)
Symbol OperandLSW = w0
Symbol OperandLSWLSB = b0
Symbol OperandLSWMSB = b1
'Word 1 (b2 and b3)
Symbol T = w1
Symbol Temp2Word = w1
Symbol InstructionOperand = b2
'Word 2 (b4 and b5)
Symbol AccumulatorLSW = w2
Symbol AccumulatorLSWLSB = b4
Symbol AccumulatorLSWMSB = b5
'Word 3 (b6 and b7)
Symbol AccumulatorMSW = w3
'Word 4 (b8 and b9)
Symbol S = w4
Symbol Temp1Word = w4
'Word 5 (b10 and b11)
Symbol OperandMSW = w5
Symbol Address = b10
Symbol Address1 = b10
Symbol Address2 = b11
'Word 6 (b12 and b13)
Symbol InstructionCode = b12
Symbol Temp1Byte = b12
Symbol ProgramCounter = b13
Symbol Temp2Byte = b13
Symbol ErrorFlag = b13
Symbol Index = b13
'words 7-11
symbol E=w7
symbol F=w8
symbol G=w9
symbol H=w10
symbol I=w11
symbol Decade =w12
symbol byte0=0
symbol byte1=b4
symbol byte2=b5
symbol byte3=b6
symbol byte4=b7
symbol Crystalmult=10000
Symbol LCDOutPin = 6 'LCD used for alerting, but other methods can be used.
Symbol RTStartAddress = 80
Symbol RTEndAddress = 83
Symbol DenominatorLSBAddress = 84
Symbol DenominatorMSBAddress = 85
Symbol ErrorFlagAddress = 86
Symbol GPRStartAddress = 88
Symbol ProgramCounterAddress = 87
Symbol ERROR_Overflow = 0
Symbol ERROR_NegativeResult = 1
Symbol ERROR_DivideByZero = 2
Symbol RTStartLessb4Address = 76 '(80 - 4)
Symbol GPRStartAddressLessb0Address = 88 '(87 - 0)

Main:
Decade=100 'Multiply base frequency by this to get target frequency
'1, 10, 100, 1000, 10000 are valid values for the decade multiplier
E = 5000 ' base frequency set for 500Khz (5000 * 100)
'1-9999  are valid values for the base frequency
F = 42950 'approx 2^32/1000000
G = 8 'scale to use max precision for 125MHz crystal (1,000,000,000/125,000,000)
H = 10000/Decade 'scale the answer based on the decade selected
ProgramCounter = Procedure0StartAddress
Gosub ExecuteProcedure
'
'substitute serial or parallel chip load here based on hardware I/F selected
'
b0=byte4
gosub writehex 
b0=byte3
gosub writehex
b0=byte2
gosub writehex
b0=byte1
gosub writehex
End

 '*************************************************
 '**** VIRTUAL ARITHMETIC AND LOGIC UNIT (ALU) ****
 '*************************************************

Add:

      Poke ErrorFlagAddress,ERROR_Overflow

      'Add LSW
      AccumulatorLSW = AccumulatorLSW + OperandLSW
      If AccumulatorLSW >= OperandLSW Then Add_1
      'Add Carry to MSW and jump to error routine if overflow
      AccumulatorMSW = AccumulatorMSW + 1
      If AccumulatorMSW = 0 Then CPU_Error

      'Add MSW
      Add_1:
      AccumulatorMSW = AccumulatorMSW + OperandMSW
      'Jump to error routine if overflow
      If AccumulatorMSW < OperandMSW Then CPU_Error
Return

Subtract:
      Poke ErrorFlagAddress,ERROR_NegativeResult

      'Subtract LSW
      Temp1Word = AccumulatorLSW
      AccumulatorLSW = AccumulatorLSW - OperandLSW

      If Temp1Word >= AccumulatorLSW Then Subtract_1
      'Borrow from MSW and jump to error routine if this will make the overall result
      'negative.
      If AccumulatorMSW = 0 Then CPU_Error
      AccumulatorMSW = AccumulatorMSW - 1

      'Note: No need to Subtract MSW as only OperandLSW is being used
      Subtract_1:
Goto Fetch

Multiply:

      Poke ErrorFlagAddress,ERROR_Overflow

      'Calculate the higher multiple and keep in Accumulator
      Temp1Word = AccumulatorLSW
      Temp2Word = AccumulatorMSW
      AccumulatorLSW = 0
      AccumulatorMSW = OperandLSW * Temp2Word
      Temp2Word = OperandLSW ** Temp2Word
      'Check for overflow
      If Temp2Word > 0 Then CPU_Error

      'Calculate the lower multiple and put in Operand
      OperandMSW = Temp1Word ** OperandLSW
      OperandLSW = Temp1Word * OperandLSW

      'Add the multiples to get the final result
      Gosub Add
Goto Fetch

Divide:
      Poke ErrorFlagAddress,ERROR_DivideByZero

      'Check for error
      If OperandLSW = 0 Then CPU_Error

      'Zero the Running Total
      For Address = RTStartAddress To RTEndAddress
            Poke Address,0
      Next

      'Calculate the quotient and remainder for 65535/OperandLSW
      S = 65535 / OperandLSW
      T = 65535 // OperandLSW

      'Store the denominator
      Poke DenominatorLSBAddress,OperandLSWLSB
      Poke DenominatorMSBAddress,OperandLSWMSB

      Divide_1:
      'Calculate S * AccumulatorMSW in the Operand, and add to Running Total.
      'Note: uses variables very carefully !
      OperandLSW = AccumulatorMSW
      Gosub SwapAccumulatorWithRT
      OperandMSW = S ** OperandLSW
      OperandLSW = S * OperandLSW
      Gosub Add
      'Update the running total
      Gosub SwapAccumulatorWithRT

      'Calculate the new Numerator
      OperandMSW = 0
      OperandLSW = AccumulatorLSW + AccumulatorMSW
      If OperandLSW > AccumulatorMSW Then Divide_2
      OperandMSW = 1
      Divide_2:
      AccumulatorLSW = AccumulatorMSW * T
      AccumulatorMSW = AccumulatorMSW ** T
      Gosub Add

      'Check to see if the new numerator is a single word. Loop back if it isn't
      If AccumulatorMSW > 0 Then Divide_1

      Temp1Word = AccumulatorLSW
      'Retrieve the running total
      Gosub SwapAccumulatorWithRT
      'Retrieve the denominator
      Peek DenominatorLSBAddress,OperandLSWLSB
      Peek DenominatorMSBAddress,OperandLSWMSB
      'Calculate AccumulatorLSW/Demominator and store in Operand
      OperandLSW = Temp1Word / OperandLSW
      OperandMSW = 0
      'and add to the Running total to give the final result
      Gosub Add
Goto Fetch

SwapAccumulatorWithRT:
      'Swaps the Accumulator value with the Running Total value
      For Address1 = RTStartAddress To RTEndAddress
            Address2 = Address1 - RTStartLessb4Address
            Peek Address1,Temp1Byte
            Peek Address2,Temp2Byte
            Poke Address1,Temp2Byte
            Poke Address2,Temp1Byte
      Next
Return

StoreAccumulatorLSW:

      Address = InstructionOperand - "E" * 2 +14
      Poke Address,AccumulatorLSWLSB
      Address = Address + 1
      Poke Address,AccumulatorLSWMSB
Return

CPU_Error:
      Peek ErrorFlagAddress,ErrorFlag
      Sertxd("ERROR: ",#ErrorFlag," ")
End

ExecuteProcedure:
      'ON ENTRY: ProgramCounter has been set to the start of the Procedure

      'Save the Program Counter
      Poke ProgramCounterAddress,ProgramCounter 'Save the ProgramCounter

      'Load General Purpose registers with corresponding E to I4 values
      For Address1 = 14 To 23
            Address2 = Address1 + GPRStartAddress -14
            Peek Address1,Temp1Byte
            Poke Address2,Temp1Byte
      Next

      'Set the Accumulator to 0
      AccumulatorMSW = 0
      AccumulatorLSW = 0

Fetch:
      'Fetch an Instruction code and operand from Program Memory
      Peek ProgramCounterAddress,ProgramCounter 'Retrieve the ProgramCounter
      Read ProgramCounter,InstructionCode
      ProgramCounter = ProgramCounter + 1
      Read ProgramCounter,InstructionOperand
      ProgramCounter = ProgramCounter + 1
      Poke ProgramCounterAddress,ProgramCounter 'Save the ProgramCounter

      'Load the contents of the specified Register into OperandLSW
      Address = InstructionOperand - "E" * 2 + GPRStartAddress
      Peek Address,OperandLSWLSB
      Address = Address + 1
      Peek Address,OperandLSWMSB
      OperandMSW = 0

Execute:
      'Execute the loaded Instruction

      Lookdown InstructionCode,("+","-","*","/","="),Index
      Branch Index,(Instruction_Add,Subtract,Multiply,Divide,StoreAccumulatorLSW)

Instruction_Add:
      Gosub Add
      Goto Fetch 

writehex:
     b1=b0>>4
     gosub hexit
     sertxd (b1)
     b1=b0 & $0F
     gosub hexit
     sertxd (b1,cr,lf)
return

hexit:
     lookup b1,("0","1","2","3","4","5","6","7","8","9","A","B","C","D","E","F"),b1
return
 
Last edited:

MPep

Senior Member
Hi Matherp,

Good work I am sure. I haven't heard of the AD9850 before.
I suggest you post this in the COde Snippets part of the forum, for easier to access, and finding.

Or A Moderator might shift this for you.

MPep.
 

John West

Senior Member
Good timing. I just received a couple of these modules today and I jumped in here to see if anyone had high-rez code written for them. Providing answers even before I ask the question is definitely above and beyond the call of duty.
 

hippy

Technical Support
Staff member
I'm not familiar with the AD9850 but I'm guessing that what one is trying to solve is the equation from the datasheet ...

freqoutput = ( N * freqcrystal ) / 232

Or, to find the 32-bit value 'N' to send for a desired frequency ...

N = ( freqoutput * 232 ) / freqcrystal

The "* 232" can be a shift leaving just a 64-bit by 32-bit division which may be simpler to achieve than the existing code. That could be done by repeated subtraction if time were not important.

Use a 'power of two' value crystal and the maths becomes easy.
 

John West

Senior Member
I was wondering if Matherp had finished his AD9850 project and had anything more to say about how well it works and any problems encountered.
 

matherp

Senior Member
John

All working very well - will post full code and circuit in the next few days

Best regards

Peter
 

John West

Senior Member
Thanks a lot, Peter. Code support for my AD8950 via PICAXE is a bigger project than I thought. Especially getting it to sweep, and other features. The integer math gets awkward. I can see where it might not be the best chip for the job, but the PICAXE series of chips is about as far as I really want to go in the microcontroller field. There are just too many other chips and programming languages, and too many other demands on my time. Too much of everything for an old techie. Our cups runneth over these days, but sometimes that just makes a big mess.
 

radiogareth

Senior Member
Any progress on this one? I have two modules on their way over and would much prefer something picaxe based rather than native PIC that I just programme and hope.....
 

John West

Senior Member
For those of you who are interested in this sig gen module, (and have a bit more money available,) also check out the "new improved" AD9851 version, which goes to still higher frequencies, (70MHz,) yet uses a lower xtal ref frequency due to a built-in 6X frequency multiplier.
 

Paix

Senior Member
Excellent piece of work. I had just been looking at the AD9850 chip, for use as a frequency source with a Rigol DS1102E scope as part of a lab.

Exceedingly timely. Many thanks
 

ferrymanr

Member
Just a thought about modifying for the AD9851. It looks as if the calculation uses the clock frequency for the AD9850 as 125*8 (MHz) which is 1000. The AD9851 has a 180 MHz clock which would give 1440. I haven't yet managed to get my old brain around the math routines to see what this involves.
The AD9851 needs the control bit 32 (W32) of the 40 bit word to be a 1 to turn on the clock multiplier. Also the recommendation is that directly after turning on the serial mode the register is loaded with 40 zeros to flush spurious data before starting normal loads. Other than a top frequency of 70MHz this should be the only mod required for the AD9851.
Richard (Dick) G4BBH
 

ferrymanr

Member
I have used the code from "User Projects - Miscellaneous" running on an AD8591. So far have changed the control byte to enable the AD8951 x6 multiplier so the clock is 180MHz. Line 43 - symbol controlbyte = %10000000 ' or decimal 128
I now have to alter the maths to use a 180MHz clock instead of 125 MHz and ensure I can get up to 70MHz.
One observation is that the LCD display driver only uses line 1 and does not include any decimal points or MHz/kHz.
Dick
 

matherp

Senior Member
The LCD gives the full value of the frequency in HZ - no decimal place needed (1 - 99990000 if the chip supported it). It is specifically designed to use a 8 x 1 LCD
 

matherp

Senior Member
I've just posted a code update in "miscellaneous" that uses 56 bit arithmetic to finally give full accurate 8 digit resolution in frequency setting (1-40,000,000 HZ). The Arduino LCD keypad shield makes it very easy to prototype this with a AXE401 and gives quite a nice user interface. (the shield uses a voltage divider network on A.0 analogue input to encode 5 separate switches - select, up, down, right, left)
One gotcha on most versions of this shield, do not use pwm to control the backlight, the shield has a hardware bug that shorts the pwm output pin to ground via the transistor base/emitter when the pin is driven high. It is OK if the pin is set as an input and OK if it is set to zero to turn the backlight off. Pic shows the prototype generating a 39,999,999hz sign wave displayed on my lovely old Hameg 604 scope

pinout of the AD9850 module is:
VCC pins 1,18,19,20
GND pins 6,11,17
A.1 pin 4 (Data)
A.2 pin 3 (Latch)
A.3 pin 2 (Clock)
output to scope pin 10

All other required wiring is on the shield or AXE401
 

Attachments

premelec

Senior Member
@ matherp - do you think I could work this on a 20M2 assuming I use a serial LCD module - I'm not sure if you have used special 28X commands but admit I haven't looked over all the code - just tried some syntax checks calling your code for a 20M2 - some obvious variable name problems but I'm not sure if there are special commands needed... Thanks very much for this code!
 

matherp

Senior Member
Hi

Should be fine other than the routine hexout - however this is only used to output to the terminal the hex digits that will be sent to the AD9850 and is purely diagnostic so the output can be checked against the AD design tool http://designtools.analog.com/dt/dds/ad9850.html
Should be easy enough to re-write it to avoid missing M2 facilities (>> and rev) if you want to keep the check.

The only other thing is that you will be limited to 32Meg rather than 64 on the 20X2 so the calcs for the update will take about 3-400 msecs longer.
 

premelec

Senior Member
@matherp - Thanks! - that's encouraging and the speed shouldn't be a problem for what I've got in mind - I did notice it uses almost all the M2 memory but plenty of space left with X products...
 
Top