Is there a way to sweep output frequency of a DDS channel linearly between two values?
Sweep frequency DDS
RAM mode on AD9910 Urukul mode may do what you want, depending on sweep specifications.
- Edited
There's an example frequency ramp using the AD9910's RAM in #1378.
An alternative would be to use the integrated digital ramp generator (DRG, see datasheet). This functionality is not (yet) exposed in the coredevice API (#1344) and needs CPLD gateware changes to get full support for Urukul v1.4+ (no access to the physical DRG pins, in particular DRCTL
). Some features are nevertheless already available (and used in AD9910.measure_io_update_alignment()
). Below is a quick example for a one-shot ramp centered on 100 MHz (barely tested, see the datasheet for an explanation of the parameters):
from artiq.experiment import *
from artiq.coredevice import ad9910
class AD9910DRG(EnvExperiment):
def build(self):
self.setattr_device("core")
self.setattr_device("urukul0_ch0")
self.dds = self.urukul0_ch0
self.ramp_low = self.dds.frequency_to_ftw(97.5*MHz)
self.ramp_high = self.dds.frequency_to_ftw(102.5*MHz)
self.ramp_step = self.dds.frequency_to_ftw(5*kHz)
self.ramp_rate = 1 << 15
@kernel
def run(self):
self.core.reset()
# init DDS
self.dds.cpld.init()
self.dds.init()
self.dds.sw.on()
self.dds.set_att(20.)
self.dds.set_amplitude(1.0)
# setup enable DRG
cfr2 = (
(1 << 19) # enable DRG
| (0 << 20) # DRG destination = FTW, see datasheet table 11
)
self.dds.write32(ad9910._AD9910.REG_CFR2, cfr2)
# set ramp limits
self.dds.write64(
ad9910._AD9910_REG_RAMP_LIMIT,
self.ramp_high,
self.ramp_low,
)
# set ramp step
# DRCTL = 0 so only the negative slope increment matters
# one can however work around that limitation by purposely overflowing
# the 32-bit FTW decrement
self.dds.write64(
ad9910._AD9910_REG_RAMP_STEP,
-self.ramp_step, # destination parameter (here FTW) decrement (DRCTL = 0)
0, # destination parameter increment (DRCTL = 1)
)
# set ramp rate
self.dds.write32(
ad9910._AD9910_REG_RAMP_RATE,
self.ramp_rate << 16, # set negative slope rate
)
# load registers = start ramp
self.dds.cpld.io_update.pulse_mu(8)
- Edited
airwoodix Thanks alot for the reply and sorry for my late response. I am actually looking to perform continous sinusoidal frequency sweep modulation e.g. CF =100Mhz , depth = 2 Mhz and modulation frequency = 50 Khz.
- From the data sheet it seems that the DRG will only produce a linear sweep and therefore would the use of RAM Continuous Recirculate Mode be the most appropriate?
- I have read on the analog device forums that in the case the DRG is used to produce the FTWs, the phase is not reset upon recirculation.
"When you frequency modulate via the DRG, the DRG generates time varying FTWs via its accumulator. The DRG's output FTWs become the input to the DDS accumulator. The DDS accumulator, meanwhile, just keeps "humming along" independent of the FTWs generated by the DRG. It just keeps on accumulating phase, unless you clear it manually.
The only way to ensure zero phase at the start of each sweep is to somehow keep track of the timing and clear the DDS accumulator when a sweep starts. Synchronizing the clearing operation, however, will be tricky."
https://ez.analog.com/dds/f/q-a/28396/ad9910-frequency-sweep-initial-phase/156351#156351
Would you know how to overcome this issue?
I have been able to produce a working code, however when I set the address step rate to be 25 (it is defined in t_step) the DDS output cuts out. It seems that below 1e4 the step address causes problems. In the documentation on the ARTIQ website it states that the step address is defined in units of 4ns. the code is below, could someone please advise me
`from artiq.experiment import *
from artiq.coredevice import ad9910
import numpy as np
#there are 1024 values for each profile, and the control parameters are the CF, MF, and MD
#The standard value has been set as N = 1024
#first write a simple script and then looking to optimise it.
N = 1000
f1 = 99.MHz
f2 = 100.MHz
#CF, MD, and MF are all in MHz
CF = 100
MF = 0.01
MD = 1.
#Converting to time period in seconds.
time_period = (1/MF)*1000
print(time_period)
time_step= (time_period/N)
print(time_step)
t_step = int(round(time_step/4))
print(t_step)
T = int(1e8)
class DDS_freq_ramp(EnvExperiment):
def build(self):
self.setattr_device("core")
self.setattr_device("urukul1_cpld") #2nd Urukul module
self.setattr_device("urukul1_ch3") #Urukul module
self.u = self.urukul1_ch3
def prepare(self):
#create list of frequencies in FTW format to write to RAM
self.f = [0.]*N
self.f_ram = [0]*N
time = np.arange(0,1000,1)
amplitude = np.sin(2*np.pi*time/999)
amplitude2 = amplitude*MD
amplitude3 = amplitude2+CF
amplitude4 = amplitude3*MHz
f_span = f2 - f1
f_step = f_span / N
#print(f_step)
print(amplitude4)
#I want to see the first step
print(f1+f_step)
for i in range(N):
self.f[i] = amplitude4[i]
self.u.frequency_to_ram(self.f,self.f_ram)
@kernel
def run(self):
self.core.reset()
'''initialize DDS channel'''
self.u.cpld.init()
self.u.init()
self.u.cpld.io_update.pulse(100*ns)
self.core.break_realtime()
self.u.set_amplitude(1.)
self.u.set_att(0.*dB)
self.u.set(100.*MHz)
'''prepare RAM profile:'''
self.u.set_cfr1() #disable RAM for writing data
self.u.cpld.io_update.pulse_mu(8) #I/O pulse to enact RAM change
self.u.set_profile_ram(start=0, end=N-1, step=t_step, profile=0, mode=ad9910.RAM_MODE_CONT_RAMPUP)
self.u.cpld.set_profile(0)
self.u.cpld.io_update.pulse_mu(8)
'''write data to RAM:'''
delay(50*us)
self.u.write_ram(self.f_ram)
delay(100*us)
'''enable RAM mode (enacted by IO pulse) and fix other parameters:'''
self.u.set_cfr1(internal_profile=0,ram_destination=ad9910.RAM_DEST_FTW, ram_enable=1)
self.u.set_amplitude(1.)
self.u.set_att(10.*dB)
self.u.cpld.io_update.pulse_mu(8)
'''switch on DDS channel'''
self.u.sw.on() `
Could some one please advise me on how to correct this issue with regards to the signal cutting out after decreasing the time step for the step address rate to below 1e4? I would really appreciate some guidance.
- Edited
I am running into problems with the time step between the samples, in the example code given above the time step is 1e8 which would correspond to 1e8x4ns = 0.4 seconds however going below 1e3x4ns the signal cuts out. I don't understand why this is a problem given that the minimum time to playback ram samples is 4ns and the minimum time for I/O update is in nanoseconds. If I could get this simple ramp up mode working then I should be able to carry onto sinusoidal frequency sweep, could someone please advise me.
I'm just starting to play around with the ram_mode functionality. I'm wondering if you've resolved your issue and how?
Also, is there a way to individually set the DDS profiles on a particular urukul? It seems that self.u.cpld.set_profile(0)
sets the profile to 0 for all of the dds chips on the board corresponding to that cpld??
- Edited
jbroz Yes I have created a completed and tested script. You should note there was an issue with the amplitude decreasing with decreasing time step between the RAM samples. This can be fixed from the solution proposed in this github issue. https://github.com/m-labs/artiq/issues/1554. I have attached my tested code below so feel free to test and improve this code.
`
from artiq.experiment import *
from artiq.coredevice import ad9910
import numpy as np
#there are 1024 values for each profile, and the control parameters are the CF, MF, and MD
#The standard value has been set as N = 1024
#first write a simple script and then looking to optimise it.
N = 25
#f1 = 99.*MHz
#f2 = 100.*MHz
#CF, MD, and MF are all in MHz
CF = 100
MF = 0.05
MD = 0.5
#Converting to time period in seconds.
time_period = (1/(MF*MHz))*(1e9)
print(time_period)
time_step= (time_period/N)
print(time_step)
t_step = int(round(time_step/4))
print(t_step, "t_step")
T = int(1e2)
class DDS_freq_ramp(EnvExperiment):
def build(self):
self.setattr_device("core")
self.setattr_device("urukul1_cpld") #2nd Urukul module
self.setattr_device("urukul1_ch3") #Urukul module
self.u = self.urukul1_ch3
def prepare(self):
#create list of frequencies in FTW format to write to RAM
self.f = [0.]*N
self.f_ram = [0]*N
time = np.arange(0,N,1)
amplitude = np.sin(2*np.pi*time/(N-1))
amplitude2 = amplitude*MD
amplitude3 = amplitude2+CF
amplitude4 = amplitude3*MHz
#f_span = f2 - f1
#f_step = f_span / N
#print(f_step)
print(amplitude4)
#I want to see the first step
#print(f1+f_step)
for i in range(N):
self.f[i] = amplitude4[i]
self.u.frequency_to_ram(self.f,self.f_ram)
@kernel
def run(self):
#self.core.reset()
self.core.break_realtime()
'''initialize DDS channel'''
self.u.cpld.init()
self.u.init()
self.u.cpld.io_update.pulse(100*ns)
#self.core.break_realtime()
self.u.set_amplitude(1.)
self.u.set_att(0.*dB)
self.u.set(100.*MHz)
'''prepare RAM profile:'''
self.u.set_cfr1(ram_enable=0) #disable RAM for writing data
self.u.cpld.io_update.pulse(100*ns) #I/O pulse to enact RAM change
self.u.set_profile_ram(start=0, end=N-1, step=t_step, profile=0, mode=ad9910.RAM_MODE_CONT_RAMPUP)
self.u.cpld.set_profile(0)
delay(10 * us)
self.u.cpld.io_update.pulse(100*ns)
'''write data to RAM:'''
delay(10*us)
self.u.write_ram(self.f_ram)
delay(10*us)
'''enable RAM mode (enacted by IO pulse) and fix other parameters:'''
self.u.set_cfr1(internal_profile=0, ram_enable = 1, ram_destination=ad9910.RAM_DEST_FTW, manual_osk_external=0, osk_enable=1, select_auto_osk=0)
self.u.sw.on()
delay(10 *us)
self.u.cpld.io_update.pulse_mu(8)
delay(1 * ms)
self.u.set_cfr1(ram_enable =0)
`
han94ros Will do. So far, I've just been playing with the amplitude modulation. But for the most part I'm doing everything the same way as you and it seems to be working fine. I don't know how much there is to improve.
I'm still surprised that you can't set the profiles individually for the different DDS chips on the board (if this is, in fact, the case -- though, I haven't tested it yet).
Also, thanks for pointing out the osk_enable hack! I'm sure that will save me some time tomorrow.
jbroz seems to work if you do not disable the RAM when writing the waveform to another chip/channel. This allows for the setting of independent frequency modulated signals to the different chips.
It says in the manual however that it is strongly recommended that RAM_enable = 0 when performing load/retrieve operations - do you believe writing to the RAM whilst it is still enabled would cause any damage.
Ahh, that makes sense. I think the warning in the manual doesn't apply since the load/retrieve operations are just being performed on a single DDS (even though we've switched the profile setting on all of them). But I could be wrong.
No additional interest or development that I know of.