From c8d91b297d44cf4ad4cf73e594c322b026de845b Mon Sep 17 00:00:00 2001 From: Sebastien Bourdeauducq Date: Sun, 13 May 2018 22:30:33 +0800 Subject: [PATCH] coredevice: add new ad9914 driver --- artiq/coredevice/ad9914.py | 307 +++++++++++++++++++++++++++++++++++++ 1 file changed, 307 insertions(+) create mode 100644 artiq/coredevice/ad9914.py diff --git a/artiq/coredevice/ad9914.py b/artiq/coredevice/ad9914.py new file mode 100644 index 000000000..a291cf6d6 --- /dev/null +++ b/artiq/coredevice/ad9914.py @@ -0,0 +1,307 @@ +""" +Driver for the AD9914 DDS (with parallel bus) on RTIO. +""" + + +from artiq.language.core import * +from artiq.language.types import * +from artiq.language.units import * +from artiq.coredevice.rtio import rtio_output + +from numpy import int32, int64 + + +_PHASE_MODE_DEFAULT = -1 +PHASE_MODE_CONTINUOUS = 0 +PHASE_MODE_ABSOLUTE = 1 +PHASE_MODE_TRACKING = 2 + +AD9914_REG_CFR1L = 0x01 +AD9914_REG_CFR1H = 0x03 +AD9914_REG_CFR2L = 0x05 +AD9914_REG_CFR2H = 0x07 +AD9914_REG_CFR3L = 0x09 +AD9914_REG_CFR3H = 0x0b +AD9914_REG_CFR4L = 0x0d +AD9914_REG_CFR4H = 0x0f +AD9914_REG_DRGFL = 0x11 +AD9914_REG_DRGFH = 0x13 +AD9914_REG_DRGBL = 0x15 +AD9914_REG_DRGBH = 0x17 +AD9914_REG_DRGAL = 0x19 +AD9914_REG_DRGAH = 0x1b +AD9914_REG_FTWL = 0x2d +AD9914_REG_FTWH = 0x2f +AD9914_REG_POW = 0x31 +AD9914_REG_ASF = 0x33 +AD9914_REG_USR0 = 0x6d +AD9914_FUD = 0x80 +AD9914_GPIO = 0x81 + + +class AD9914: + """Driver for one AD9914 DDS channel. + + The time cursor is not modified by any function in this class. + + Output event replacement is not supported and issuing commands at the same + time is an error. + + :param sysclk: DDS system frequency. The DDS system clock must be a + phase-locked multiple of the RTIO clock. + :param bus_channel: RTIO channel number of the DDS bus. + :param channel: channel number (on the bus) of the DDS device to control. + """ + + kernel_invariants = {"core", "sysclk", "bus_channel", "channel", + "rtio_period_mu", "sysclk_per_mu", "write_duration_mu", + "dac_cal_duration_mu", "init_duration_mu", "init_sync_duration_mu", + "set_duration_mu", "set_x_duration_mu" + "continuous_phase_comp"} + + def __init__(self, dmgr, sysclk, bus_channel, channel, core_device="core"): + self.core = dmgr.get(core_device) + self.sysclk = sysclk + self.bus_channel = bus_channel + self.channel = channel + self.phase_mode = PHASE_MODE_CONTINUOUS + + self.rtio_period_mu = int64(8) + self.sysclk_per_mu = int32(self.sysclk * self.core.ref_period) + + self.write_duration_mu = 5 * self.rtio_period_mu + self.dac_cal_duration_mu = 147000 * self.rtio_period_mu + self.init_duration_mu = 10 * self.write_duration_mu + self.dac_cal_duration_mu + self.init_sync_duration_mu = 18 * self.write_duration_mu + 2 * self.dac_cal_duration_mu + self.set_duration_mu = 6 * self.write_duration_mu + self.set_x_duration_mu = 7 * self.write_duration_mu + + self.continuous_phase_comp = 0 + + @kernel + def write(self, addr, data): + rtio_output(now_mu(), self.bus_channel, addr, data) + delay_mu(self.write_duration_mu) + + @kernel + def init(self): + """Resets and initializes the DDS channel. + + This needs to be done for each DDS channel before it can be used, and + it is recommended to use the startup kernel for this purpose. + """ + delay_mu(-self.init_duration_mu) + self.write(AD9914_GPIO, (1 << self.channel) << 1); + + self.write(AD9914_REG_CFR1H, 0x0000) # Enable cosine output + self.write(AD9914_REG_CFR2L, 0x8900) # Enable matched latency + self.write(AD9914_REG_CFR2H, 0x0080) # Enable profile mode + self.write(AD9914_REG_DRGBH, 0x8000) # Programmable modulus B == 2**31 + self.write(AD9914_REG_DRGBL, 0x0000) + self.write(AD9914_REG_ASF, 0x0fff) # Set amplitude to maximum + self.write(AD9914_REG_CFR4H, 0x0105) # Enable DAC calibration + self.write(AD9914_FUD, 0) + delay_mu(self.dac_cal_duration_mu) + self.write(AD9914_REG_CFR4H, 0x0005) # Disable DAC calibration + self.write(AD9914_FUD, 0) + + @kernel + def init_sync(self, sync_delay): + """Resets and initializes the DDS channel as well as configures + the AD9914 DDS for synchronisation. The synchronisation procedure + follows the steps outlined in the AN-1254 application note. + + This needs to be done for each DDS channel before it can be used, and + it is recommended to use the startup kernel for this. + + This function cannot be used in a batch; the correct way of + initializing multiple DDS channels is to call this function + sequentially with a delay between the calls. 10ms provides a good + timing margin. + + :param sync_delay: integer from 0 to 0x3f that sets the value of + SYNC_OUT (bits 3-5) and SYNC_IN (bits 0-2) delay ADJ bits. + """ + delay_mu(-self.init_sync_duration_mu) + self.write(AD9914_GPIO, (1 << self.channel) << 1) + + self.write(AD9914_REG_CFR4H, 0x0105) # Enable DAC calibration + self.write(AD9914_FUD, 0) + delay_mu(self.dac_cal_duration_mu) + self.write(AD9914_REG_CFR4H, 0x0005) # Disable DAC calibration + self.write(AD9914_FUD, 0) + self.write(AD9914_REG_CFR2L, 0x8b00) # Enable matched latency and sync_out + self.write(AD9914_FUD, 0) + # Set cal with sync and set sync_out and sync_in delay + self.write(AD9914_REG_USR0, 0x0840 | (sync_delay & 0x3f)) + self.write(AD9914_FUD, 0) + self.write(AD9914_REG_CFR4H, 0x0105) # Enable DAC calibration + self.write(AD9914_FUD, 0) + delay_mu(self.dac_cal_duration_mu) + self.write(AD9914_REG_CFR4H, 0x0005) # Disable DAC calibration + self.write(AD9914_FUD, 0) + self.write(AD9914_REG_CFR1H, 0x0000) # Enable cosine output + self.write(AD9914_REG_CFR2H, 0x0080) # Enable profile mode + self.write(AD9914_REG_DRGBH, 0x8000) # Programmable modulus B == 2**31 + self.write(AD9914_REG_DRGBL, 0x0000) + self.write(AD9914_REG_ASF, 0x0fff) # Set amplitude to maximum + self.write(AD9914_FUD, 0) + + @kernel + def set_phase_mode(self, phase_mode): + """Sets the phase mode of the DDS channel. Supported phase modes are: + + * ``PHASE_MODE_CONTINUOUS``: the phase accumulator is unchanged when + switching frequencies. The DDS phase is the sum of the phase + accumulator and the phase offset. The only discrete jumps in the + DDS output phase come from changes to the phase offset. + + * ``PHASE_MODE_ABSOLUTE``: the phase accumulator is reset when + switching frequencies. Thus, the phase of the DDS at the time of + the frequency change is equal to the phase offset. + + * ``PHASE_MODE_TRACKING``: when switching frequencies, the phase + accumulator is set to the value it would have if the DDS had been + running at the specified frequency since the start of the + experiment. + """ + self.phase_mode = phase_mode + + @kernel + def set_mu(self, frequency, phase=0, phase_mode=_PHASE_MODE_DEFAULT, + amplitude=0x0fff, ref_time=-1): + """Sets the DDS channel to the specified frequency and phase. + + This uses machine units (FTW and POW). The frequency tuning word width + is 32, whereas the phase offset word width depends on the type of DDS + chip and can be retrieved via the ``pow_width`` attribute. The amplitude + width is 12. + + The "frequency update" pulse is sent to the DDS with a fixed latency + with respect to the current position of the time cursor. + + :param frequency: frequency to generate. + :param phase: adds an offset, in turns, to the phase. + :param phase_mode: if specified, overrides the default phase mode set + by ``set_phase_mode`` for this call. + :param ref_time: reference time used to compute phase. Specifying this + makes it easier to have a well-defined phase relationship between + DDSes on the same bus that are updated at a similar time. + """ + if phase_mode == _PHASE_MODE_DEFAULT: + phase_mode = self.phase_mode + if ref_time < 0: + ref_time = now_mu() + delay_mu(-self.set_duration_mu) + + self.write(AD9914_GPIO, (1 << self.channel) << 1) + + self.write(AD9914_REG_FTWL, ftw & 0xffff) + self.write(AD9914_REG_FTWH, (ftw >> 16) & 0xffff) + + # We need the RTIO fine timestamp clock to be phase-locked + # to DDS SYSCLK, and divided by an integer self.sysclk_per_mu. + if phase_mode == PHASE_MODE_CONTINUOUS: + # Do not clear phase accumulator on FUD + # Disable autoclear phase accumulator and enables OSK. + self.write(AD9914_REG_CFR1L, 0x0108) + pow += self.continuous_phase_comp + else: + # Clear phase accumulator on FUD + # Enable autoclear phase accumulator and enables OSK. + self.write(AD9914_REG_CFR1L, 0x2108) + fud_time = now_mu() + 2 * self.write_duration_mu + pow -= int32((ref_time - fud_time) * self.sysclk_per_mu * ftw >> (32 - 16)) + if phase_mode == PHASE_MODE_TRACKING: + pow += int32(ref_time * self.sysclk_per_mu * ftw >> (32 - 16)) + self.continuous_phase_comp = pow + + self.write(AD9914_REG_POW, pow) + self.write(AD9914_REG_ASF, amplitude) + self.write(AD9914_FUD, 0) + + @portable(flags={"fast-math"}) + def frequency_to_ftw(self, frequency): + """Returns the frequency tuning word corresponding to the given + frequency. + """ + return round(float(int64(2)**32*frequency/self.sysclk)) + + @portable(flags={"fast-math"}) + def ftw_to_frequency(self, ftw): + """Returns the frequency corresponding to the given frequency tuning + word. + """ + return ftw*self.sysclk/int64(2)**32 + + @portable(flags={"fast-math"}) + def turns_to_pow(self, turns): + """Returns the phase offset word corresponding to the given phase + in turns.""" + return round(float(turns*2**16)) + + @portable(flags={"fast-math"}) + def pow_to_turns(self, pow): + """Returns the phase in turns corresponding to the given phase offset + word.""" + return pow/2**16 + + @portable(flags={"fast-math"}) + def amplitude_to_asf(self, amplitude): + """Returns amplitude scale factor corresponding to given amplitude.""" + return round(float(amplitude*0x0fff)) + + @portable(flags={"fast-math"}) + def asf_to_amplitude(self, asf): + """Returns the amplitude corresponding to the given amplitude scale + factor.""" + return asf/0x0fff + + @kernel + def set(self, frequency, phase=0.0, phase_mode=_PHASE_MODE_DEFAULT, + amplitude=1.0): + """Like ``set_mu``, but uses Hz and turns.""" + self.set_mu(self.frequency_to_ftw(frequency), + self.turns_to_pow(phase), phase_mode, + self.amplitude_to_asf(amplitude)) + + # Extended resolution functions + @kernel + def set_mu_x(self, xftw, amplitude=0x0fff): + delay_mu(-self.set_x_duration_mu) + + self.write(AD9914_GPIO, (1 << self.channel) << 1) + + # Enable programmable modulus. + # Note another undocumented "feature" of the AD9914: + # Programmable modulus breaks if the digital ramp enable bit is + # not set at the same time. + self.write(AD9914_REG_CFR2H, 0x0089) + self.write(AD9914_REG_DRGAL, xftw & 0xffff) + self.write(AD9914_REG_DRGAH, (xftw >> 16) & 0x7fff) + self.write(AD9914_REG_DRGFL, (xftw >> 31) & 0xffff) + self.write(AD9914_REG_DRGFH, (xftw >> 47) & 0xffff) + self.write(AD9914_REG_ASF, amplitude) + + self.write(AD9914_FUD, 0) + + @portable(flags={"fast-math"}) + def frequency_to_xftw(self, frequency): + """Returns the frequency tuning word corresponding to the given + frequency (extended resolution mode). + """ + return round(float(int64(2)**63*frequency/self.sysclk)) + + @portable(flags={"fast-math"}) + def xftw_to_frequency(self, xftw): + """Returns the frequency corresponding to the given frequency tuning + word (extended resolution mode). + """ + return xftw*self.sysclk/int64(2)**63 + + @kernel + def set_x(self, frequency, amplitude=1.0): + """Like ``set_mu_x``, but uses Hz and turns.""" + self.set_mu_x(self.frequency_to_xftw(frequency), + self.amplitude_to_asf(amplitude)) +