From c52bdf4fe9d5a291d5a88f8e226a11bf3db26bd1 Mon Sep 17 00:00:00 2001 From: newell Date: Sun, 22 Dec 2024 14:06:33 -0800 Subject: [PATCH] Add driver support for individual IO_UPDATE, PROFILE, OSK, DRG, and ATT for new Urukul proto_rev 0x09. --- artiq/coredevice/ad9910.py | 515 +++++++++++++++++--------- artiq/coredevice/urukul.py | 733 ++++++++++++++++++++++++++++++------- 2 files changed, 943 insertions(+), 305 deletions(-) diff --git a/artiq/coredevice/ad9910.py b/artiq/coredevice/ad9910.py index 821411b8e..a27dda09a 100644 --- a/artiq/coredevice/ad9910.py +++ b/artiq/coredevice/ad9910.py @@ -1,13 +1,11 @@ from numpy import int32, int64 -from artiq.language.core import ( - kernel, delay, portable, delay_mu, now_mu, at_mu) -from artiq.language.units import us, ms -from artiq.language.types import TBool, TInt32, TInt64, TFloat, TList, TTuple - from artiq.coredevice import spi2 as spi from artiq.coredevice import urukul -from artiq.coredevice.urukul import DEFAULT_PROFILE +from artiq.coredevice.urukul import DEFAULT_PROFILE, _RegIOUpdate +from artiq.language.core import at_mu, delay, delay_mu, kernel, now_mu, portable +from artiq.language.types import TBool, TFloat, TInt32, TInt64, TList, TTuple +from artiq.language.units import ms, us # Work around ARTIQ-Python import machinery urukul_sta_pll_lock = urukul.urukul_sta_pll_lock @@ -15,10 +13,18 @@ urukul_sta_smp_err = urukul.urukul_sta_smp_err __all__ = [ "AD9910", - "PHASE_MODE_CONTINUOUS", "PHASE_MODE_ABSOLUTE", "PHASE_MODE_TRACKING", - "RAM_DEST_FTW", "RAM_DEST_POW", "RAM_DEST_ASF", "RAM_DEST_POWASF", - "RAM_MODE_DIRECTSWITCH", "RAM_MODE_RAMPUP", "RAM_MODE_BIDIR_RAMP", - "RAM_MODE_CONT_BIDIR_RAMP", "RAM_MODE_CONT_RAMPUP", + "PHASE_MODE_CONTINUOUS", + "PHASE_MODE_ABSOLUTE", + "PHASE_MODE_TRACKING", + "RAM_DEST_FTW", + "RAM_DEST_POW", + "RAM_DEST_ASF", + "RAM_DEST_POWASF", + "RAM_MODE_DIRECTSWITCH", + "RAM_MODE_RAMPUP", + "RAM_MODE_BIDIR_RAMP", + "RAM_MODE_CONT_BIDIR_RAMP", + "RAM_MODE_CONT_RAMPUP", ] _PHASE_MODE_DEFAULT = -1 @@ -34,12 +40,12 @@ _AD9910_REG_IO_UPDATE = 0x04 _AD9910_REG_FTW = 0x07 _AD9910_REG_POW = 0x08 _AD9910_REG_ASF = 0x09 -_AD9910_REG_SYNC = 0x0a -_AD9910_REG_RAMP_LIMIT = 0x0b -_AD9910_REG_RAMP_STEP = 0x0c -_AD9910_REG_RAMP_RATE = 0x0d -_AD9910_REG_PROFILE0 = 0x0e -_AD9910_REG_PROFILE1 = 0x0f +_AD9910_REG_SYNC = 0x0A +_AD9910_REG_RAMP_LIMIT = 0x0B +_AD9910_REG_RAMP_STEP = 0x0C +_AD9910_REG_RAMP_RATE = 0x0D +_AD9910_REG_PROFILE0 = 0x0E +_AD9910_REG_PROFILE1 = 0x0F _AD9910_REG_PROFILE2 = 0x10 _AD9910_REG_PROFILE3 = 0x11 _AD9910_REG_PROFILE4 = 0x12 @@ -92,10 +98,10 @@ class SyncDataEeprom: word = self.eeprom_device.read_i32(self.eeprom_offset) >> 16 sync_delay_seed = word >> 8 if sync_delay_seed >= 0: - io_update_delay = word & 0xff + io_update_delay = word & 0xFF else: io_update_delay = 0 - if io_update_delay == 0xff: # unprogrammed EEPROM + if io_update_delay == 0xFF: # unprogrammed EEPROM io_update_delay = 0 # With Numpy, type(int32(-1) >> 1) == int64 self.sync_delay_seed = int32(sync_delay_seed) @@ -138,13 +144,33 @@ class AD9910: to the same string value. """ - def __init__(self, dmgr, chip_select, cpld_device, sw_device=None, - pll_n=40, pll_cp=7, pll_vco=5, sync_delay_seed=-1, - io_update_delay=0, pll_en=1): - self.kernel_invariants = {"cpld", "core", "bus", "chip_select", - "pll_en", "pll_n", "pll_vco", "pll_cp", - "ftw_per_hz", "sysclk_per_mu", "sysclk", - "sync_data"} + def __init__( + self, + dmgr, + chip_select, + cpld_device, + sw_device=None, + pll_n=40, + pll_cp=7, + pll_vco=5, + sync_delay_seed=-1, + io_update_delay=0, + pll_en=1, + ): + self.kernel_invariants = { + "cpld", + "core", + "bus", + "chip_select", + "pll_en", + "pll_n", + "pll_vco", + "pll_cp", + "ftw_per_hz", + "sysclk_per_mu", + "sysclk", + "sync_data", + } self.cpld = dmgr.get(cpld_device) self.core = self.cpld.core self.bus = self.cpld.bus @@ -163,8 +189,14 @@ class AD9910: assert clk <= 60e6 assert 12 <= pll_n <= 127 assert 0 <= pll_vco <= 5 - vco_min, vco_max = [(370, 510), (420, 590), (500, 700), - (600, 880), (700, 950), (820, 1150)][pll_vco] + vco_min, vco_max = [ + (370, 510), + (420, 590), + (500, 700), + (600, 880), + (700, 950), + (820, 1150), + ][pll_vco] assert vco_min <= sysclk / 1e6 <= vco_max assert 0 <= pll_cp <= 7 else: @@ -174,15 +206,18 @@ class AD9910: self.sysclk_per_mu = int(round(sysclk * self.core.ref_period)) self.sysclk = sysclk - if isinstance(sync_delay_seed, str) or isinstance(io_update_delay, - str): + if not self.cpld.io_update: + self.cpld.io_update = _RegIOUpdate(self.cpld, self.chip_select) + + if isinstance(sync_delay_seed, str) or isinstance(io_update_delay, str): if sync_delay_seed != io_update_delay: - raise ValueError("When using EEPROM, sync_delay_seed must be " - "equal to io_update_delay") + raise ValueError( + "When using EEPROM, sync_delay_seed must be " + "equal to io_update_delay" + ) self.sync_data = SyncDataEeprom(dmgr, self.core, sync_delay_seed) else: - self.sync_data = SyncDataUser(self.core, sync_delay_seed, - io_update_delay) + self.sync_data = SyncDataUser(self.core, sync_delay_seed, io_update_delay) self.phase_mode = PHASE_MODE_CONTINUOUS @@ -236,9 +271,10 @@ class AD9910: :param addr: Register address :param data: Data to be written """ - self.bus.set_config_mu(urukul.SPI_CONFIG | spi.SPI_END, 24, - urukul.SPIT_DDS_WR, self.chip_select) - self.bus.write((addr << 24) | ((data & 0xffff) << 8)) + self.bus.set_config_mu( + urukul.SPI_CONFIG | spi.SPI_END, 24, urukul.SPIT_DDS_WR, self.chip_select + ) + self.bus.write((addr << 24) | ((data & 0xFFFF) << 8)) @kernel def write32(self, addr: TInt32, data: TInt32): @@ -247,11 +283,13 @@ class AD9910: :param addr: Register address :param data: Data to be written """ - self.bus.set_config_mu(urukul.SPI_CONFIG, 8, - urukul.SPIT_DDS_WR, self.chip_select) + self.bus.set_config_mu( + urukul.SPI_CONFIG, 8, urukul.SPIT_DDS_WR, self.chip_select + ) self.bus.write(addr << 24) - self.bus.set_config_mu(urukul.SPI_CONFIG | spi.SPI_END, 32, - urukul.SPIT_DDS_WR, self.chip_select) + self.bus.set_config_mu( + urukul.SPI_CONFIG | spi.SPI_END, 32, urukul.SPIT_DDS_WR, self.chip_select + ) self.bus.write(data) @kernel @@ -260,12 +298,16 @@ class AD9910: :param addr: Register address """ - self.bus.set_config_mu(urukul.SPI_CONFIG, 8, - urukul.SPIT_DDS_WR, self.chip_select) + self.bus.set_config_mu( + urukul.SPI_CONFIG, 8, urukul.SPIT_DDS_WR, self.chip_select + ) self.bus.write((addr | 0x80) << 24) self.bus.set_config_mu( urukul.SPI_CONFIG | spi.SPI_END | spi.SPI_INPUT, - 16, urukul.SPIT_DDS_RD, self.chip_select) + 16, + urukul.SPIT_DDS_RD, + self.chip_select, + ) self.bus.write(0) return self.bus.read() @@ -275,12 +317,16 @@ class AD9910: :param addr: Register address """ - self.bus.set_config_mu(urukul.SPI_CONFIG, 8, - urukul.SPIT_DDS_WR, self.chip_select) + self.bus.set_config_mu( + urukul.SPI_CONFIG, 8, urukul.SPIT_DDS_WR, self.chip_select + ) self.bus.write((addr | 0x80) << 24) self.bus.set_config_mu( urukul.SPI_CONFIG | spi.SPI_END | spi.SPI_INPUT, - 32, urukul.SPIT_DDS_RD, self.chip_select) + 32, + urukul.SPIT_DDS_RD, + self.chip_select, + ) self.bus.write(0) return self.bus.read() @@ -292,16 +338,19 @@ class AD9910: :return: 64-bit integer register value """ self.bus.set_config_mu( - urukul.SPI_CONFIG, 8, - urukul.SPIT_DDS_WR, self.chip_select) + urukul.SPI_CONFIG, 8, urukul.SPIT_DDS_WR, self.chip_select + ) self.bus.write((addr | 0x80) << 24) self.bus.set_config_mu( - urukul.SPI_CONFIG | spi.SPI_INPUT, 32, - urukul.SPIT_DDS_RD, self.chip_select) + urukul.SPI_CONFIG | spi.SPI_INPUT, 32, urukul.SPIT_DDS_RD, self.chip_select + ) self.bus.write(0) self.bus.set_config_mu( - urukul.SPI_CONFIG | spi.SPI_END | spi.SPI_INPUT, 32, - urukul.SPIT_DDS_RD, self.chip_select) + urukul.SPI_CONFIG | spi.SPI_END | spi.SPI_INPUT, + 32, + urukul.SPIT_DDS_RD, + self.chip_select, + ) self.bus.write(0) hi = self.bus.read() lo = self.bus.read() @@ -312,17 +361,20 @@ class AD9910: """Write to 64-bit register. :param addr: Register address - :param data_high: High (MSB) 32 data bits + :param data_high: High (MSB) 32 data bits :param data_low: Low (LSB) 32 data bits """ - self.bus.set_config_mu(urukul.SPI_CONFIG, 8, - urukul.SPIT_DDS_WR, self.chip_select) + self.bus.set_config_mu( + urukul.SPI_CONFIG, 8, urukul.SPIT_DDS_WR, self.chip_select + ) self.bus.write(addr << 24) - self.bus.set_config_mu(urukul.SPI_CONFIG, 32, - urukul.SPIT_DDS_WR, self.chip_select) + self.bus.set_config_mu( + urukul.SPI_CONFIG, 32, urukul.SPIT_DDS_WR, self.chip_select + ) self.bus.write(data_high) - self.bus.set_config_mu(urukul.SPI_CONFIG | spi.SPI_END, 32, - urukul.SPIT_DDS_WR, self.chip_select) + self.bus.set_config_mu( + urukul.SPI_CONFIG | spi.SPI_END, 32, urukul.SPIT_DDS_WR, self.chip_select + ) self.bus.write(data_low) @kernel @@ -336,15 +388,18 @@ class AD9910: :param data: Data to be written to RAM. """ - self.bus.set_config_mu(urukul.SPI_CONFIG, 8, urukul.SPIT_DDS_WR, - self.chip_select) + self.bus.set_config_mu( + urukul.SPI_CONFIG, 8, urukul.SPIT_DDS_WR, self.chip_select + ) self.bus.write(_AD9910_REG_RAM << 24) - self.bus.set_config_mu(urukul.SPI_CONFIG, 32, - urukul.SPIT_DDS_WR, self.chip_select) + self.bus.set_config_mu( + urukul.SPI_CONFIG, 32, urukul.SPIT_DDS_WR, self.chip_select + ) for i in range(len(data) - 1): self.bus.write(data[i]) - self.bus.set_config_mu(urukul.SPI_CONFIG | spi.SPI_END, 32, - urukul.SPIT_DDS_WR, self.chip_select) + self.bus.set_config_mu( + urukul.SPI_CONFIG | spi.SPI_END, 32, urukul.SPIT_DDS_WR, self.chip_select + ) self.bus.write(data[len(data) - 1]) @kernel @@ -353,43 +408,53 @@ class AD9910: The profile to read from and the step, start, and end address need to be configured before and separately using - :meth:`set_profile_ram` and the parent CPLD + :meth:`set_profile_ram` and the parent CPLD :meth:`~artiq.coredevice.urukul.CPLD.set_profile`. :param data: List to be filled with data read from RAM. """ - self.bus.set_config_mu(urukul.SPI_CONFIG, 8, urukul.SPIT_DDS_WR, - self.chip_select) + self.bus.set_config_mu( + urukul.SPI_CONFIG, 8, urukul.SPIT_DDS_WR, self.chip_select + ) self.bus.write((_AD9910_REG_RAM | 0x80) << 24) n = len(data) - 1 if n > 0: - self.bus.set_config_mu(urukul.SPI_CONFIG | spi.SPI_INPUT, 32, - urukul.SPIT_DDS_RD, self.chip_select) + self.bus.set_config_mu( + urukul.SPI_CONFIG | spi.SPI_INPUT, + 32, + urukul.SPIT_DDS_RD, + self.chip_select, + ) preload = min(n, 8) for i in range(n): self.bus.write(0) if i >= preload: data[i - preload] = self.bus.read() self.bus.set_config_mu( - urukul.SPI_CONFIG | spi.SPI_INPUT | spi.SPI_END, 32, - urukul.SPIT_DDS_RD, self.chip_select) + urukul.SPI_CONFIG | spi.SPI_INPUT | spi.SPI_END, + 32, + urukul.SPIT_DDS_RD, + self.chip_select, + ) self.bus.write(0) for i in range(preload + 1): data[(n - preload) + i] = self.bus.read() @kernel - def set_cfr1(self, - power_down: TInt32 = 0b0000, - phase_autoclear: TInt32 = 0, - drg_load_lrr: TInt32 = 0, - drg_autoclear: TInt32 = 0, - phase_clear: TInt32 = 0, - internal_profile: TInt32 = 0, - ram_destination: TInt32 = 0, - ram_enable: TInt32 = 0, - manual_osk_external: TInt32 = 0, - osk_enable: TInt32 = 0, - select_auto_osk: TInt32 = 0): + def set_cfr1( + self, + power_down: TInt32 = 0b0000, + phase_autoclear: TInt32 = 0, + drg_load_lrr: TInt32 = 0, + drg_autoclear: TInt32 = 0, + phase_clear: TInt32 = 0, + internal_profile: TInt32 = 0, + ram_destination: TInt32 = 0, + ram_enable: TInt32 = 0, + manual_osk_external: TInt32 = 0, + osk_enable: TInt32 = 0, + select_auto_osk: TInt32 = 0, + ): """Set CFR1. See the AD9910 datasheet for parameter meanings and sizes. This method does not pulse ``IO_UPDATE.`` @@ -408,33 +473,43 @@ class AD9910: :param osk_enable: Enable OSK mode. :param select_auto_osk: Select manual or automatic OSK mode. """ - self.write32(_AD9910_REG_CFR1, - (ram_enable << 31) | - (ram_destination << 29) | - (manual_osk_external << 23) | - (internal_profile << 17) | - (drg_load_lrr << 15) | - (drg_autoclear << 14) | - (phase_autoclear << 13) | - (phase_clear << 11) | - (osk_enable << 9) | - (select_auto_osk << 8) | - (power_down << 4) | - 2) # SDIO input only, MSB first + self.write32( + _AD9910_REG_CFR1, + (ram_enable << 31) + | (ram_destination << 29) + | (manual_osk_external << 23) + | (internal_profile << 17) + | (drg_load_lrr << 15) + | (drg_autoclear << 14) + | (phase_autoclear << 13) + | (phase_clear << 11) + | (osk_enable << 9) + | (select_auto_osk << 8) + | (power_down << 4) + | 2, + ) # SDIO input only, MSB first @kernel - def set_cfr2(self, - asf_profile_enable: TInt32 = 1, - drg_enable: TInt32 = 0, - effective_ftw: TInt32 = 1, - sync_validation_disable: TInt32 = 0, - matched_latency_enable: TInt32 = 0): + def set_cfr2( + self, + asf_profile_enable: TInt32 = 1, + drg_destination: TInt32 = 0, + drg_enable: TInt32 = 0, + drg_nodwell_high: TInt32 = 0, + drg_nodwell_low: TInt32 = 0, + effective_ftw: TInt32 = 1, + sync_validation_disable: TInt32 = 0, + matched_latency_enable: TInt32 = 0, + ): """Set CFR2. See the AD9910 datasheet for parameter meanings and sizes. This method does not pulse ``IO_UPDATE``. :param asf_profile_enable: Enable amplitude scale from single tone profiles. + :param drg_destination: Digital ramp destination. :param drg_enable: Digital ramp enable. + :param drg_nodwell_high: Digital ramp no-dwell high. + :param drg_nodwell_low: Digital ramp no-dwell low. :param effective_ftw: Read effective FTW. :param sync_validation_disable: Disable the SYNC_SMP_ERR pin indicating (active high) detection of a synchronization pulse sampling error. @@ -444,19 +519,24 @@ class AD9910: * matched_latency_enable = 0: in the order listed * matched_latency_enable = 1: simultaneously. """ - self.write32(_AD9910_REG_CFR2, - (asf_profile_enable << 24) | - (drg_enable << 19) | - (effective_ftw << 16) | - (matched_latency_enable << 7) | - (sync_validation_disable << 5)) + self.write32( + _AD9910_REG_CFR2, + (asf_profile_enable << 24) + | (drg_destination << 20) + | (drg_enable << 19) + | (drg_nodwell_high << 18) + | (drg_nodwell_low << 17) + | (effective_ftw << 16) + | (matched_latency_enable << 7) + | (sync_validation_disable << 5), + ) @kernel def init(self, blind: TBool = False): """Initialize and configure the DDS. Sets up SPI mode, confirms chip presence, powers down unused blocks, - configures the PLL, waits for PLL lock. Uses the ``IO_UPDATE`` + configures the PLL, waits for PLL lock. Uses the ``IO_UPDATE`` signal multiple times. :param blind: Do not read back DDS identity and do not wait for lock. @@ -476,7 +556,7 @@ class AD9910: if not blind: # Use the AUX DAC setting to identify and confirm presence aux_dac = self.read32(_AD9910_REG_AUX_DAC) - if aux_dac & 0xff != 0x7f: + if aux_dac & 0xFF != 0x7F: raise ValueError("Urukul AD9910 AUX_DAC mismatch") delay(50 * us) # slack # Configure PLL settings and bring up PLL @@ -485,9 +565,13 @@ class AD9910: # sync timing validation disable (enabled later) self.set_cfr2(sync_validation_disable=1) self.cpld.io_update.pulse(1 * us) - cfr3 = (0x0807c000 | (self.pll_vco << 24) | - (self.pll_cp << 19) | (self.pll_en << 8) | - (self.pll_n << 1)) + cfr3 = ( + 0x0807C000 + | (self.pll_vco << 24) + | (self.pll_cp << 19) + | (self.pll_en << 8) + | (self.pll_n << 1) + ) self.write32(_AD9910_REG_CFR3, cfr3 | 0x400) # PFD reset self.cpld.io_update.pulse(1 * us) if self.pll_en: @@ -520,11 +604,16 @@ class AD9910: self.cpld.io_update.pulse(1 * us) @kernel - def set_mu(self, ftw: TInt32 = 0, pow_: TInt32 = 0, asf: TInt32 = 0x3fff, - phase_mode: TInt32 = _PHASE_MODE_DEFAULT, - ref_time_mu: TInt64 = int64(-1), - profile: TInt32 = DEFAULT_PROFILE, - ram_destination: TInt32 = -1) -> TInt32: + def set_mu( + self, + ftw: TInt32 = 0, + pow_: TInt32 = 0, + asf: TInt32 = 0x3FFF, + phase_mode: TInt32 = _PHASE_MODE_DEFAULT, + ref_time_mu: TInt64 = int64(-1), + profile: TInt32 = DEFAULT_PROFILE, + ram_destination: TInt32 = -1, + ) -> TInt32: """Set DDS data in machine units. This uses machine units (FTW, POW, ASF). The frequency tuning word @@ -574,8 +663,9 @@ class AD9910: dt = int32(now_mu()) - int32(ref_time_mu) pow_ += dt * ftw * self.sysclk_per_mu >> 16 if ram_destination == -1: - self.write64(_AD9910_REG_PROFILE0 + profile, - (asf << 16) | (pow_ & 0xffff), ftw) + self.write64( + _AD9910_REG_PROFILE0 + profile, (asf << 16) | (pow_ & 0xFFFF), ftw + ) else: if not ram_destination == RAM_DEST_FTW: self.set_ftw(ftw) @@ -593,8 +683,9 @@ class AD9910: return pow_ @kernel - def get_mu(self, profile: TInt32 = DEFAULT_PROFILE - ) -> TTuple([TInt32, TInt32, TInt32]): + def get_mu( + self, profile: TInt32 = DEFAULT_PROFILE + ) -> TTuple([TInt32, TInt32, TInt32]): """Get the frequency tuning word, phase offset word, and amplitude scale factor. @@ -608,15 +699,21 @@ class AD9910: data = int64(self.read64(_AD9910_REG_PROFILE0 + profile)) # Extract and return fields ftw = int32(data) - pow_ = int32((data >> 32) & 0xffff) - asf = int32((data >> 48) & 0x3fff) + pow_ = int32((data >> 32) & 0xFFFF) + asf = int32((data >> 48) & 0x3FFF) return ftw, pow_, asf @kernel - def set_profile_ram(self, start: TInt32, end: TInt32, step: TInt32 = 1, - profile: TInt32 = _DEFAULT_PROFILE_RAM, - nodwell_high: TInt32 = 0, zero_crossing: TInt32 = 0, - mode: TInt32 = 1): + def set_profile_ram( + self, + start: TInt32, + end: TInt32, + step: TInt32 = 1, + profile: TInt32 = _DEFAULT_PROFILE_RAM, + nodwell_high: TInt32 = 0, + zero_crossing: TInt32 = 0, + mode: TInt32 = 1, + ): """Set the RAM profile settings. See also AD9910 datasheet. :param start: Profile start address in RAM (10-bit). @@ -635,8 +732,13 @@ class AD9910: :const:`RAM_MODE_RAMPUP`) """ hi = (step << 8) | (end >> 2) - lo = ((end << 30) | (start << 14) | (nodwell_high << 5) | - (zero_crossing << 3) | mode) + lo = ( + (end << 30) + | (start << 14) + | (nodwell_high << 5) + | (zero_crossing << 3) + | mode + ) self.write64(_AD9910_REG_PROFILE0 + profile, hi, lo) @kernel @@ -711,7 +813,7 @@ class AD9910: def turns_to_pow(self, turns: TFloat) -> TInt32: """Return the 16-bit phase offset word corresponding to the given phase in turns.""" - return int32(round(turns * 0x10000)) & int32(0xffff) + return int32(round(turns * 0x10000)) & int32(0xFFFF) @portable(flags={"fast-math"}) def pow_to_turns(self, pow_: TInt32) -> TFloat: @@ -723,8 +825,8 @@ class AD9910: def amplitude_to_asf(self, amplitude: TFloat) -> TInt32: """Return 14-bit amplitude scale factor corresponding to given fractional amplitude.""" - code = int32(round(amplitude * 0x3fff)) - if code < 0 or code > 0x3fff: + code = int32(round(amplitude * 0x3FFF)) + if code < 0 or code > 0x3FFF: raise ValueError("Invalid AD9910 fractional amplitude!") return code @@ -732,7 +834,7 @@ class AD9910: def asf_to_amplitude(self, asf: TInt32) -> TFloat: """Return amplitude as a fraction of full scale corresponding to given amplitude scale factor.""" - return asf / float(0x3fff) + return asf / float(0x3FFF) @portable(flags={"fast-math"}) def frequency_to_ram(self, frequency: TList(TFloat), ram: TList(TInt32)): @@ -774,8 +876,9 @@ class AD9910: ram[i] = self.amplitude_to_asf(amplitude[i]) << 18 @portable(flags={"fast-math"}) - def turns_amplitude_to_ram(self, turns: TList(TFloat), - amplitude: TList(TFloat), ram: TList(TInt32)): + def turns_amplitude_to_ram( + self, turns: TList(TFloat), amplitude: TList(TFloat), ram: TList(TInt32) + ): """Convert phase and amplitude values to RAM profile data. To be used with :const:`RAM_DEST_POWASF`. @@ -786,8 +889,9 @@ class AD9910: Suitable for :meth:`write_ram`. """ for i in range(len(ram)): - ram[i] = ((self.turns_to_pow(turns[i]) << 16) | - self.amplitude_to_asf(amplitude[i]) << 2) + ram[i] = (self.turns_to_pow(turns[i]) << 16) | self.amplitude_to_asf( + amplitude[i] + ) << 2 @kernel def set_frequency(self, frequency: TFloat): @@ -844,10 +948,16 @@ class AD9910: return self.pow_to_turns(self.get_pow()) @kernel - def set(self, frequency: TFloat = 0.0, phase: TFloat = 0.0, - amplitude: TFloat = 1.0, phase_mode: TInt32 = _PHASE_MODE_DEFAULT, - ref_time_mu: TInt64 = int64(-1), profile: TInt32 = DEFAULT_PROFILE, - ram_destination: TInt32 = -1) -> TFloat: + def set( + self, + frequency: TFloat = 0.0, + phase: TFloat = 0.0, + amplitude: TFloat = 1.0, + phase_mode: TInt32 = _PHASE_MODE_DEFAULT, + ref_time_mu: TInt64 = int64(-1), + profile: TInt32 = DEFAULT_PROFILE, + ram_destination: TInt32 = -1, + ) -> TFloat: """Set DDS data in SI units. See also :meth:`AD9910.set_mu`. @@ -861,14 +971,22 @@ class AD9910: :param ram_destination: RAM destination. :return: Resulting phase offset in turns """ - return self.pow_to_turns(self.set_mu( - self.frequency_to_ftw(frequency), self.turns_to_pow(phase), - self.amplitude_to_asf(amplitude), phase_mode, ref_time_mu, - profile, ram_destination)) + return self.pow_to_turns( + self.set_mu( + self.frequency_to_ftw(frequency), + self.turns_to_pow(phase), + self.amplitude_to_asf(amplitude), + phase_mode, + ref_time_mu, + profile, + ram_destination, + ) + ) @kernel - def get(self, profile: TInt32 = DEFAULT_PROFILE - ) -> TTuple([TFloat, TFloat, TFloat]): + def get( + self, profile: TInt32 = DEFAULT_PROFILE + ) -> TTuple([TFloat, TFloat, TFloat]): """Get the frequency, phase, and amplitude. See also :meth:`AD9910.get_mu`. @@ -880,14 +998,19 @@ class AD9910: # Get values ftw, pow_, asf = self.get_mu(profile) # Convert and return - return (self.ftw_to_frequency(ftw), self.pow_to_turns(pow_), - self.asf_to_amplitude(asf)) + return ( + self.ftw_to_frequency(ftw), + self.pow_to_turns(pow_), + self.asf_to_amplitude(asf), + ) @kernel def set_att_mu(self, att: TInt32): """Set digital step attenuator in machine units. - This method will write the attenuator settings of all four channels. See also + This method will write the attenuator settings of the channel + (Urukul proto_rev 0x08, all four channels will be updated at same time). + See also :meth:`CPLD.get_channel_att `. :param att: Attenuation setting, 8-bit digital. @@ -898,7 +1021,9 @@ class AD9910: def set_att(self, att: TFloat): """Set digital step attenuator in SI units. - This method will write the attenuator settings of all four channels. See also + This method will write the attenuator settings of the channel + (Urukul proto_rev 0x08, all four channels will be updated at same time). + See also :meth:`CPLD.get_channel_att `. :param att: Attenuation in dB. @@ -916,7 +1041,7 @@ class AD9910: @kernel def get_att(self) -> TFloat: - """Get digital step attenuator value in SI units. See also + """Get digital step attenuator value in SI units. See also :meth:`CPLD.get_channel_att `. :return: Attenuation in dB. @@ -934,10 +1059,52 @@ class AD9910: self.cpld.cfg_sw(self.chip_select - 4, state) @kernel - def set_sync(self, - in_delay: TInt32, - window: TInt32, - en_sync_gen: TInt32 = 0): + def cfg_osk(self, state: TBool): + """Set CPLD CFG OSK state. The OSK bit is controlled by the + logical or of the CPLD configuration shift register OSK bit. + + :param state: CPLD CFG OSK bit + """ + self.cpld.cfg_osk(self.chip_select - 4, state) + + @kernel + def cfg_drctl(self, state: TBool): + """Set CPLD CFG DRCTL state. The DRCTL bit is controlled by the + logical or of the CPLD configuration shift register DRCTL bit. + + :param state: CPLD CFG DRCTL bit + """ + self.cpld.cfg_drctl(self.chip_select - 4, state) + + @kernel + def cfg_drhold(self, state: TBool): + """Set CPLD CFG DRHOLD state. The DRHOLD bit is controlled by the + logical or of the CPLD configuration shift register DRHOLD bit. + + :param state: CPLD CFG DRHOLD bit + """ + self.cpld.cfg_drhold(self.chip_select - 4, state) + + @kernel + def cfg_mask_nu(self, state: TBool): + """Set CPLD CFG MASK_NU state. The MASK_NU bit is controlled by the + logical or of the CPLD configuration shift register MASK_NU bit. + + :param state: CPLD CFG MASK_NU bit + """ + self.cpld.cfg_mask_nu(self.chip_select - 4, state) + + @kernel + def cfg_att_en(self, state: TBool): + """Set CPLD CFG ATT_EN state. The ATT_EN bit is controlled by the + logical or of the CPLD configuration shift register ATT_EN bit. + + :param state: CPLD CFG ATT_EN bit + """ + self.cpld.cfg_att_en(self.chip_select - 4, state) + + @kernel + def set_sync(self, in_delay: TInt32, window: TInt32, en_sync_gen: TInt32 = 0): """Set the relevant parameters in the multi device synchronization register. See the AD9910 datasheet for details. The ``SYNC`` clock generator preset value is set to zero, and the ``SYNC_OUT`` generator is @@ -950,14 +1117,16 @@ class AD9910: (``SYNC_OUT``, cf. ``sync_sel == 1``). Should be left off for the normal use case, where the ``SYNC`` clock is supplied by the core device. """ - self.write32(_AD9910_REG_SYNC, - (window << 28) | # SYNC S/H validation delay - (1 << 27) | # SYNC receiver enable - (en_sync_gen << 26) | # SYNC generator enable - (0 << 25) | # SYNC generator SYS rising edge - (0 << 18) | # SYNC preset - (0 << 11) | # SYNC output delay - (in_delay << 3)) # SYNC receiver delay + self.write32( + _AD9910_REG_SYNC, + (window << 28) # SYNC S/H validation delay + | (1 << 27) # SYNC receiver enable + | (en_sync_gen << 26) # SYNC generator enable + | (0 << 25) # SYNC generator SYS rising edge + | (0 << 18) # SYNC preset + | (0 << 11) # SYNC output delay + | (in_delay << 3), + ) # SYNC receiver delay @kernel def clear_smp_err(self): @@ -976,8 +1145,7 @@ class AD9910: self.cpld.io_update.pulse(1 * us) @kernel - def tune_sync_delay(self, - search_seed: TInt32 = 15) -> TTuple([TInt32, TInt32]): + def tune_sync_delay(self, search_seed: TInt32 = 15) -> TTuple([TInt32, TInt32]): """Find a stable ``SYNC_IN`` delay. This method first locates a valid ``SYNC_IN`` delay at zero validation @@ -1033,8 +1201,9 @@ class AD9910: raise ValueError("no valid window/delay") @kernel - def measure_io_update_alignment(self, delay_start: TInt64, - delay_stop: TInt64) -> TInt32: + def measure_io_update_alignment( + self, delay_start: TInt64, delay_stop: TInt64 + ) -> TInt32: """Use the digital ramp generator to locate the alignment between ``IO_UPDATE`` and ``SYNC_CLK``. @@ -1106,11 +1275,9 @@ class AD9910: for j in range(repeat): t1[0] += self.measure_io_update_alignment(i, i + 1) t1[1] += self.measure_io_update_alignment(i + 1, i + 2) - if ((t1[0] == 0 and t1[1] == 0) or - (t1[0] == repeat and t1[1] == repeat)): + if (t1[0] == 0 and t1[1] == 0) or (t1[0] == repeat and t1[1] == repeat): # edge is not close to i + 1, can't interpret result - raise ValueError( - "no clear IO_UPDATE-SYNC_CLK alignment edge found") + raise ValueError("no clear IO_UPDATE-SYNC_CLK alignment edge found") else: # the good delay is period//2 after the edge return (i + 1 + period // 2) & (period - 1) diff --git a/artiq/coredevice/urukul.py b/artiq/coredevice/urukul.py index cac53499b..ebebd7185 100644 --- a/artiq/coredevice/urukul.py +++ b/artiq/coredevice/urukul.py @@ -1,15 +1,23 @@ +from abc import ABC, abstractmethod +from typing import Union + from numpy import int32, int64 -from artiq.language.core import kernel, delay, portable, at_mu, now_mu -from artiq.language.units import us, ms -from artiq.language.types import TInt32, TFloat, TBool - from artiq.coredevice import spi2 as spi +from artiq.language.core import at_mu, delay, kernel, now_mu, portable +from artiq.language.types import TBool, TFloat, TInt32, TInt64 +from artiq.language.units import ms, us -SPI_CONFIG = (0 * spi.SPI_OFFLINE | 0 * spi.SPI_END | - 0 * spi.SPI_INPUT | 1 * spi.SPI_CS_POLARITY | - 0 * spi.SPI_CLK_POLARITY | 0 * spi.SPI_CLK_PHASE | - 0 * spi.SPI_LSB_FIRST | 0 * spi.SPI_HALF_DUPLEX) +SPI_CONFIG = ( + 0 * spi.SPI_OFFLINE + | 0 * spi.SPI_END + | 0 * spi.SPI_INPUT + | 1 * spi.SPI_CS_POLARITY + | 0 * spi.SPI_CLK_POLARITY + | 0 * spi.SPI_CLK_PHASE + | 0 * spi.SPI_LSB_FIRST + | 0 * spi.SPI_HALF_DUPLEX +) # SPI clock write and read dividers SPIT_CFG_WR = 2 @@ -20,30 +28,24 @@ SPIT_ATT_RD = 16 SPIT_DDS_WR = 2 SPIT_DDS_RD = 16 -# CFG configuration register bit offsets +# Common CFG configuration register bit offsets CFG_RF_SW = 0 CFG_LED = 4 CFG_PROFILE = 8 -CFG_IO_UPDATE = 12 -CFG_MASK_NU = 13 -CFG_CLK_SEL0 = 17 -CFG_CLK_SEL1 = 21 -CFG_SYNC_SEL = 18 -CFG_RST = 19 -CFG_IO_RST = 20 -CFG_CLK_DIV = 22 -# STA status register bit offsets +# Common STA status register bit offsets STA_RF_SW = 0 STA_SMP_ERR = 4 STA_PLL_LOCK = 8 STA_IFC_MODE = 12 STA_PROTO_REV = 16 +STA_DROVER = 23 -# supported hardware and CPLD code version -STA_PROTO_REV_MATCH = 0x08 +# Supported hardware and CPLD code version +STA_PROTO_REV_8 = 0x08 +STA_PROTO_REV_9 = 0x09 -# chip select (decoded) +# Chip select (decoded) CS_CFG = 1 CS_ATT = 2 CS_DDS_MULTI = 3 @@ -56,62 +58,77 @@ CS_DDS_CH3 = 7 DEFAULT_PROFILE = 7 -@portable -def urukul_cfg(rf_sw, led, profile, io_update, mask_nu, - clk_sel, sync_sel, rst, io_rst, clk_div): - """Build Urukul CPLD configuration register""" - return ((rf_sw << CFG_RF_SW) | - (led << CFG_LED) | - (profile << CFG_PROFILE) | - (io_update << CFG_IO_UPDATE) | - (mask_nu << CFG_MASK_NU) | - ((clk_sel & 0x01) << CFG_CLK_SEL0) | - ((clk_sel & 0x02) << (CFG_CLK_SEL1 - 1)) | - (sync_sel << CFG_SYNC_SEL) | - (rst << CFG_RST) | - (io_rst << CFG_IO_RST) | - (clk_div << CFG_CLK_DIV)) - - @portable def urukul_sta_rf_sw(sta): """Return the RF switch status from Urukul status register value.""" - return (sta >> STA_RF_SW) & 0xf + return (sta >> STA_RF_SW) & 0xF @portable def urukul_sta_smp_err(sta): """Return the SMP_ERR status from Urukul status register value.""" - return (sta >> STA_SMP_ERR) & 0xf + return (sta >> STA_SMP_ERR) & 0xF @portable def urukul_sta_pll_lock(sta): """Return the PLL_LOCK status from Urukul status register value.""" - return (sta >> STA_PLL_LOCK) & 0xf + return (sta >> STA_PLL_LOCK) & 0xF @portable def urukul_sta_ifc_mode(sta): """Return the IFC_MODE status from Urukul status register value.""" - return (sta >> STA_IFC_MODE) & 0xf + return (sta >> STA_IFC_MODE) & 0xF @portable def urukul_sta_proto_rev(sta): """Return the PROTO_REV value from Urukul status register value.""" - return (sta >> STA_PROTO_REV) & 0x7f + return (sta >> STA_PROTO_REV) & 0x7F + + +@portable +def urukul_sta_drover(sta): + """Return the DROVER status from Urukul status register value.""" + return (sta >> STA_DROVER) & 0xF class _RegIOUpdate: - def __init__(self, cpld): + def __init__(self, cpld, chip_select): self.cpld = cpld + self.chip_select = chip_select @kernel - def pulse(self, t: TFloat): + def pulse_mu(self, duration): + """Pulse the output high for the specified duration + (in machine units). + + The time cursor is advanced by the specified duration.""" cfg = self.cpld.cfg_reg - self.cpld.cfg_write(cfg | (1 << CFG_IO_UPDATE)) - delay(t) + if self.cpld.proto_rev == 0x08: + self.cpld.cfg_write(cfg | (1 << ProtoRev8.CFG_IO_UPDATE)) + else: + self.cpld.cfg_write( + cfg | (int64(1) << (ProtoRev9.CFG_IO_UPDATE + (self.chip_select - 4))) + ) + delay_mu(duration) + self.cpld.cfg_write(cfg) + + @kernel + def pulse(self, duration): + """Pulse the output high for the specified duration + (in seconds). + + The time cursor is advanced by the specified duration.""" + cfg = self.cpld.cfg_reg + if self.cpld.proto_rev == 0x08: + self.cpld.cfg_write(cfg | (1 << ProtoRev8.CFG_IO_UPDATE)) + else: + self.cpld.cfg_write( + cfg | (int64(1) << (ProtoRev9.CFG_IO_UPDATE + (self.chip_select - 4))) + ) + delay(duration + 50 * ms) # Needed extra for slack self.cpld.cfg_write(cfg) @@ -124,6 +141,420 @@ class _DummySync: pass +class CPLDVersionManager(ABC): + @abstractmethod + @kernel + def cfg_write(self, cpld, cfg): + pass + + @abstractmethod + @kernel + def sta_read(self, cpld): + pass + + @abstractmethod + @kernel + def init(self, cpld): + pass + + @abstractmethod + @kernel + def io_rst(self, cpld): + pass + + def _not_implemented(self, *args, **kwargs): + raise NotImplementedError( + "This function is not implemented for this Urukul version." + ) + + @kernel + def configure_bit(self, cpld, bit_offset: TInt32, channel: TInt32, on: TBool): + self._not_implemented() + + @kernel + def configure_all_bits(self, cpld, bit_offset: TInt32, state: TInt32): + self._not_implemented() + + @kernel + def cfg_att_en(self, cpld, channel: TInt32, on: TBool): + self._not_implemented() + + @kernel + def cfg_att_en_all(self, cpld, state: TInt32): + self._not_implemented() + + @kernel + def cfg_osk(self, cpld, channel: TInt32, on: TBool): + self._not_implemented() + + @kernel + def cfg_osk_all(self, cpld, state: TInt32): + self._not_implemented() + + @kernel + def cfg_drctl(self, cpld, channel: TInt32, on: TBool): + self._not_implemented() + + @kernel + def cfg_drctl_all(self, cpld, state: TInt32): + self._not_implemented() + + @kernel + def cfg_drhold(self, cpld, channel: TInt32, on: TBool): + self._not_implemented() + + @kernel + def cfg_drhold_all(self, cpld, state: TInt32): + self._not_implemented() + + @kernel + def cfg_mask_nu(self, cpld, channel: TInt32, on: TBool): + self._not_implemented() + + @kernel + def cfg_mask_nu_all(self, cpld, state: TInt32): + self._not_implemented() + + +class ProtoRev8(CPLDVersionManager): + + # ProtoRev8 CFG configuration register bit offsets + CFG_IO_UPDATE = 12 + CFG_MASK_NU = 13 + CFG_CLK_SEL0 = 17 + CFG_CLK_SEL1 = 21 + CFG_SYNC_SEL = 18 + CFG_RST = 19 + CFG_IO_RST = 20 + CFG_CLK_DIV = 22 + + @staticmethod + @portable + def urukul_cfg( + rf_sw, + led, + profile, + io_update, + mask_nu, + clk_sel, + sync_sel, + rst, + io_rst, + clk_div, + ): + """Build Urukul CPLD configuration register""" + return ( + (rf_sw << CFG_RF_SW) + | (led << CFG_LED) + | (profile << CFG_PROFILE) + | (io_update << ProtoRev8.CFG_IO_UPDATE) + | (mask_nu << ProtoRev8.CFG_MASK_NU) + | ((clk_sel & 0x01) << ProtoRev8.CFG_CLK_SEL0) + | ((clk_sel & 0x02) << (ProtoRev8.CFG_CLK_SEL1 - 1)) + | (sync_sel << ProtoRev8.CFG_SYNC_SEL) + | (rst << ProtoRev8.CFG_RST) + | (io_rst << ProtoRev8.CFG_IO_RST) + | (clk_div << ProtoRev8.CFG_CLK_DIV) + ) + + @kernel + def cfg_write(self, cpld, cfg: TInt32): + """Write to the configuration register. + + See :func:`urukul_cfg` for possible flags. + + :param cfg: 24-bit data to be written. Will be stored at + :attr:`cfg_reg`. + """ + cpld.bus.set_config_mu(SPI_CONFIG | spi.SPI_END, 24, SPIT_CFG_WR, CS_CFG) + cpld.bus.write(cfg << 8) + cpld.cfg_reg = cfg + + @kernel + def sta_read(self, cpld) -> TInt32: + """Read the status register. + + Use any of the following functions to extract values: + + * :func:`urukul_sta_rf_sw` + * :func:`urukul_sta_smp_err` + * :func:`urukul_sta_pll_lock` + * :func:`urukul_sta_ifc_mode` + * :func:`urukul_sta_proto_rev` + + :return: The status register value. + """ + cpld.bus.set_config_mu( + SPI_CONFIG | spi.SPI_END | spi.SPI_INPUT, 24, SPIT_CFG_RD, CS_CFG + ) + cpld.bus.write(cpld.cfg_reg << 8) + return cpld.bus.read() + + @kernel + def init(self, cpld): + """Initialize and detect Urukul. + + Resets the DDS I/O interface and verifies correct CPLD gateware + version. + Does not pulse the DDS ``MASTER_RESET`` as that confuses the AD9910. + """ + cfg = cpld.cfg_reg + # Don't pulse MASTER_RESET (m-labs/artiq#940) + cpld.cfg_reg = cfg | (0 << ProtoRev8.CFG_RST) | (1 << ProtoRev8.CFG_IO_RST) + delay(100 * us) # reset, slack + cpld.cfg_write(cfg) + if cpld.sync_div: + at_mu(now_mu() & ~0xF) # align to RTIO/2 + cpld.set_sync_div(cpld.sync_div) # 125 MHz/2 = 1 GHz/16 + delay(1 * ms) # DDS wake up + + @kernel + def io_rst(self, cpld): + """Pulse IO_RST""" + cpld.cfg_write(cpld.cfg_reg | (1 << ProtoRev8.CFG_IO_RST)) + cpld.cfg_write(cpld.cfg_reg & ~(1 << ProtoRev8.CFG_IO_RST)) + + +class ProtoRev9(CPLDVersionManager): + + # ProtoRev9 CFG configuration register bit offsets + CFG_OSK = 20 + CFG_DRCTL = 24 + CFG_DRHOLD = 28 + CFG_IO_UPDATE = 32 + CFG_MASK_NU = 36 + CFG_CLK_SEL0 = 40 + CFG_CLK_SEL1 = 44 + CFG_SYNC_SEL = 41 + CFG_RST = 42 + CFG_IO_RST = 43 + CFG_CLK_DIV = 45 + CFG_ATT_EN = 47 + + @staticmethod + @portable + def urukul_cfg( + rf_sw, + led, + profile, + osk, + drctl, + drhold, + io_update, + mask_nu, + clk_sel, + sync_sel, + rst, + io_rst, + clk_div, + att_en, + ): + """Build Urukul CPLD configuration register""" + return ( + (rf_sw << CFG_RF_SW) + | (led << CFG_LED) + | (profile << CFG_PROFILE) + | (osk << ProtoRev9.CFG_OSK) + | (drctl << ProtoRev9.CFG_DRCTL) + | (drhold << ProtoRev9.CFG_DRHOLD) + | (io_update << ProtoRev9.CFG_IO_UPDATE) + | (mask_nu << ProtoRev9.CFG_MASK_NU) + | ((clk_sel & 0x01) << ProtoRev9.CFG_CLK_SEL0) + | ((clk_sel & 0x02) << (ProtoRev9.CFG_CLK_SEL1 - 1)) + | (sync_sel << ProtoRev9.CFG_SYNC_SEL) + | (rst << ProtoRev9.CFG_RST) + | (io_rst << ProtoRev9.CFG_IO_RST) + | (clk_div << ProtoRev9.CFG_CLK_DIV) + | (att_en << ProtoRev9.CFG_ATT_EN) + ) + + @kernel + def cfg_write(self, cpld, cfg: TInt64): + """Write to the configuration register. + + See :func:`urukul_cfg` for possible flags. + + :param cfg: 52-bit data to be written. Will be stored at + :attr:`cfg_reg`. + """ + cpld.bus.set_config_mu(SPI_CONFIG, 24, SPIT_CFG_WR, CS_CFG) + cpld.bus.write(((cfg >> 28) & 0xFFFFFF) << 8) + cpld.bus.set_config_mu(SPI_CONFIG | spi.SPI_END, 28, SPIT_CFG_WR, CS_CFG) + cpld.bus.write((cfg & 0xFFFFFFF) << 4) + cpld.cfg_reg = cfg + + @kernel + def sta_read(self, cpld) -> TInt64: + """Read the status register. + + Use any of the following functions to extract values: + + * :func:`urukul_sta_rf_sw` + * :func:`urukul_sta_smp_err` + * :func:`urukul_sta_pll_lock` + * :func:`urukul_sta_ifc_mode` + * :func:`urukul_sta_proto_rev` + * :func:`urukul_sta_drover` + + :return: The status register value. + """ + cpld.bus.set_config_mu(SPI_CONFIG | spi.SPI_INPUT, 24, SPIT_CFG_RD, CS_CFG) + cpld.bus.write(((cpld.cfg_reg >> 24) & 0xFFFFFF) << 8) + cpld.bus.set_config_mu( + SPI_CONFIG | spi.SPI_END | spi.SPI_INPUT, 28, SPIT_CFG_RD, CS_CFG + ) + cpld.bus.write((cpld.cfg_reg & 0xFFFFFFF) << 4) + hi = cpld.bus.read() + lo = cpld.bus.read() + return (int64(hi) << 28) | lo + + @kernel + def init(self, cpld): + """Initialize and detect Urukul. + + Resets the DDS I/O interface and verifies correct CPLD gateware + version. + Does not pulse the DDS ``MASTER_RESET`` as that confuses the AD9910. + """ + cfg = cpld.cfg_reg + # Don't pulse MASTER_RESET (m-labs/artiq#940) + cpld.cfg_reg = cfg | (0 << ProtoRev9.CFG_RST) | (1 << ProtoRev9.CFG_IO_RST) + delay(100 * us) # reset, slack + cpld.cfg_write(cfg) + if cpld.sync_div: + at_mu(now_mu() & ~0xF) # align to RTIO/2 + cpld.set_sync_div(cpld.sync_div) # 125 MHz/2 = 1 GHz/16 + delay(1 * ms) # DDS wake up + + @kernel + def io_rst(self, cpld): + """Pulse IO_RST""" + cpld.cfg_write(cpld.cfg_reg | (1 << ProtoRev9.CFG_IO_RST)) + cpld.cfg_write(cpld.cfg_reg & ~(1 << ProtoRev9.CFG_IO_RST)) + + @kernel + def _configure_bit(self, cpld, bit_offset: TInt32, channel: TInt32, on: TBool): + """Configure a single bit in the configuration register. + + :param bit_offset: Base bit offset for the configuration type + :param channel: Channel index (0-3) + :param on: Switch value + """ + c = cpld.cfg_reg + if on: + c |= int64(1) << (bit_offset + channel) + else: + c &= ~(int64(1) << (bit_offset + channel)) + cpld.cfg_write(c) + + @kernel + def _configure_all_bits(self, cpld, bit_offset: TInt32, state: TInt32): + """Configure all four bits of a specific type in the configuration register. + + :param bit_offset: Base bit offset for the configuration type + :param state: State as a 4-bit integer + """ + cpld.cfg_write( + (cpld.cfg_reg & ~(int64(0xF) << bit_offset)) | (int64(state) << bit_offset) + ) + + @kernel + def cfg_att_en(self, cpld, channel: TInt32, on: TBool): + """Configure the ATT_EN bit through the configuration register. + + :param channel: Channel index (0-3) + :param on: Switch value + """ + cpld._configure_bit(ProtoRev9.CFG_ATT_EN, channel, on) + + @kernel + def cfg_att_en_all(self, cpld, state: TInt32): + """Configure all four ATT_EN bits through the configuration register. + + :param state: OSK state as a 4-bit integer. + """ + cpld._configure_all_bits(ProtoRev9.CFG_ATT_EN, state) + + @kernel + def cfg_osk(self, cpld, channel: TInt32, on: TBool): + """Configure the OSK bit through the configuration register. + + :param channel: Channel index (0-3) + :param on: Switch value + """ + cpld._configure_bit(ProtoRev9.CFG_OSK, channel, on) + + @kernel + def cfg_osk_all(self, cpld, state: TInt32): + """Configure all four OSK bits through the configuration register. + + :param state: OSK state as a 4-bit integer. + """ + cpld._configure_all_bits(ProtoRev9.CFG_OSK, state) + + @kernel + def cfg_drctl(self, cpld, channel: TInt32, on: TBool): + """Configure the DRCTL bit through the configuration register. + + :param channel: Channel index (0-3) + :param on: Switch value + """ + cpld._configure_bit(ProtoRev9.CFG_DRCTL, channel, on) + + @kernel + def cfg_drctl_all(self, cpld, state: TInt32): + """Configure all four DRCTL bits through the configuration register. + + :param state: DRCTL state as a 4-bit integer. + """ + cpld._configure_all_bits(ProtoRev9.CFG_DRCTL, state) + + @kernel + def cfg_drhold(self, cpld, channel: TInt32, on: TBool): + """Configure the DRHOLD bit through the configuration register. + + :param channel: Channel index (0-3) + :param on: Switch value + """ + cpld._configure_bit(ProtoRev9.CFG_DRHOLD, channel, on) + + @kernel + def cfg_drhold_all(self, cpld, state: TInt32): + """Configure all four DRHOLD bits through the configuration register. + + :param state: DRHOLD state as a 4-bit integer. + """ + cpld._configure_all_bits(ProtoRev9.CFG_DRHOLD, state) + + @kernel + def cfg_mask_nu(self, cpld, channel: TInt32, on: TBool): + """Configure the MASK_NU bits through the configuration register. + + :param channel: Channel index (0-3) + :param on: Switch value + """ + cpld._configure_bit(ProtoRev9.CFG_MASK_NU, channel, on) + + @kernel + def cfg_mask_nu_all(self, cpld, state: TInt32): + """Configure all four MASK_NU bits through the configuration register. + + :param state: MASK_NU state as a 4-bit integer. + """ + cpld._configure_all_bits(ProtoRev9.CFG_MASK_NU, state) + + +class CPLDVersionManagerFactory: + @staticmethod + def get_version(proto_rev: int) -> CPLDVersionManager: + if proto_rev == STA_PROTO_REV_8: + return ProtoRev8() + elif proto_rev == STA_PROTO_REV_9: + return ProtoRev9() + else: + raise ValueError(f"Urukul unsupported proto_rev: {proto_rev}") + + class CPLD: """Urukul CPLD SPI router and configuration interface. @@ -162,13 +593,26 @@ class CPLD: front panel SMA with no clock connected), then the ``init()`` method of the DDS channels can fail with the error message ``PLL lock timeout``. """ + kernel_invariants = {"refclk", "bus", "core", "io_update", "clk_div"} - def __init__(self, dmgr, spi_device, io_update_device=None, - dds_reset_device=None, sync_device=None, - sync_sel=0, clk_sel=0, clk_div=0, rf_sw=0, - refclk=125e6, att=0x00000000, sync_div=None, - core_device="core"): + def __init__( + self, + dmgr, + spi_device, + io_update_device=None, + dds_reset_device=None, + sync_device=None, + sync_sel=0, + clk_sel=0, + clk_div=0, + rf_sw=0, + refclk=125e6, + att=0x00000000, + sync_div=None, + proto_rev=0x08, + core_device="core", + ): self.core = dmgr.get(core_device) self.refclk = refclk @@ -179,7 +623,7 @@ class CPLD: if io_update_device is not None: self.io_update = dmgr.get(io_update_device) else: - self.io_update = _RegIOUpdate(self) + self.io_update = io_update_device if dds_reset_device is not None: self.dds_reset = dmgr.get(dds_reset_device) if sync_device is not None: @@ -191,77 +635,109 @@ class CPLD: assert sync_div is None sync_div = 0 - self.cfg_reg = urukul_cfg(rf_sw=rf_sw, led=0, profile=DEFAULT_PROFILE, - io_update=0, mask_nu=0, clk_sel=clk_sel, - sync_sel=sync_sel, - rst=0, io_rst=0, clk_div=clk_div) + # proto_rev = 0x09 # urukul_sta_proto_rev(self.sta_read()) + self.proto_rev = proto_rev + self.version_manager = CPLDVersionManagerFactory.get_version(proto_rev) + + if self.proto_rev == STA_PROTO_REV_8: + self.cfg_reg = ProtoRev8.urukul_cfg( + rf_sw=rf_sw, + led=0, + profile=DEFAULT_PROFILE, + io_update=0, + mask_nu=0, + clk_sel=clk_sel, + sync_sel=sync_sel, + rst=0, + io_rst=0, + clk_div=clk_div, + ) + else: + self.cfg_reg = ProtoRev9.urukul_cfg( + rf_sw=rf_sw, + led=0, + profile=(DEFAULT_PROFILE << 9) + | (DEFAULT_PROFILE << 6) + | (DEFAULT_PROFILE << 3) + | DEFAULT_PROFILE, + osk=0, + drctl=0, + drhold=0, + io_update=0, + mask_nu=0, + clk_sel=clk_sel, + sync_sel=sync_sel, + rst=0, + io_rst=0, + clk_div=clk_div, + att_en=0, + ) self.att_reg = int32(int64(att)) self.sync_div = sync_div @kernel - def cfg_write(self, cfg: TInt32): - """Write to the configuration register. - - See :func:`urukul_cfg` for possible flags. - - :param cfg: 24-bit data to be written. Will be stored at - :attr:`cfg_reg`. - """ - self.bus.set_config_mu(SPI_CONFIG | spi.SPI_END, 24, - SPIT_CFG_WR, CS_CFG) - self.bus.write(cfg << 8) - self.cfg_reg = cfg + def cfg_write(self, cfg): + self.version_manager.cfg_write(self, cfg) @kernel - def sta_read(self) -> TInt32: - """Read the status register. - - Use any of the following functions to extract values: - - * :func:`urukul_sta_rf_sw` - * :func:`urukul_sta_smp_err` - * :func:`urukul_sta_pll_lock` - * :func:`urukul_sta_ifc_mode` - * :func:`urukul_sta_proto_rev` - - :return: The status register value. - """ - self.bus.set_config_mu(SPI_CONFIG | spi.SPI_END | spi.SPI_INPUT, 24, - SPIT_CFG_RD, CS_CFG) - self.bus.write(self.cfg_reg << 8) - return self.bus.read() + def sta_read(self): + return self.version_manager.sta_read(self) @kernel - def init(self, blind: TBool = False): - """Initialize and detect Urukul. - - Resets the DDS I/O interface and verifies correct CPLD gateware - version. - Does not pulse the DDS ``MASTER_RESET`` as that confuses the AD9910. - - :param blind: Do not attempt to verify presence and compatibility. - """ - cfg = self.cfg_reg - # Don't pulse MASTER_RESET (m-labs/artiq#940) - self.cfg_reg = cfg | (0 << CFG_RST) | (1 << CFG_IO_RST) - if blind: - self.cfg_write(self.cfg_reg) - else: - proto_rev = urukul_sta_proto_rev(self.sta_read()) - if proto_rev != STA_PROTO_REV_MATCH: - raise ValueError("Urukul proto_rev mismatch") - delay(100 * us) # reset, slack - self.cfg_write(cfg) - if self.sync_div: - at_mu(now_mu() & ~0xf) # align to RTIO/2 - self.set_sync_div(self.sync_div) # 125 MHz/2 = 1 GHz/16 - delay(1 * ms) # DDS wake up + def init(self): + self.version_manager.init(self) @kernel def io_rst(self): - """Pulse IO_RST""" - self.cfg_write(self.cfg_reg | (1 << CFG_IO_RST)) - self.cfg_write(self.cfg_reg & ~(1 << CFG_IO_RST)) + self.version_manager.io_rst(self) + + @kernel + def configure_bit(self, bit_offset: TInt32, channel: TInt32, on: TBool): + self.version_manager.configure_bit(self, bit_offset, channel, on) + + @kernel + def configure_all_bits(self, bit_offset: TInt32, state: TInt32): + self.version_manager.configure_all_bits(self, bit_offset, state) + + @kernel + def cfg_att_en(self, channel: TInt32, on: TBool): + self.version_manager.cfg_att_en(self, channel, on) + + @kernel + def cfg_att_en_all(self, state: TInt32): + self.version_manager.cfg_att_en_all(self, state) + + @kernel + def cfg_osk(self, channel: TInt32, on: TBool): + self.version_manager.cfg_osk(self, channel, on) + + @kernel + def cfg_osk_all(self, state: TInt32): + self.version_manager.cfg_osk_all(self, state) + + @kernel + def cfg_drctl(self, channel: TInt32, on: TBool): + self.version_manager.cfg_drctl(self, channel, on) + + @kernel + def cfg_drctl_all(self, state: TInt32): + self.version_manager.cfg_drctl_all(self, state) + + @kernel + def cfg_drhold(self, channel: TInt32, on: TBool): + self.version_manager.cfg_drhold(self, channel, on) + + @kernel + def cfg_drhold_all(self, state: TInt32): + self.version_manager.cfg_drhold_all(self, state) + + @kernel + def cfg_mask_nu(self, channel: TInt32, on: TBool): + self.version_manager.cfg_mask_nu(self, channel, on) + + @kernel + def cfg_mask_nu_all(self, state: TInt32): + self.version_manager.cfg_mask_nu_all(self, state) @kernel def cfg_sw(self, channel: TInt32, on: TBool): @@ -285,7 +761,7 @@ class CPLD: :param state: RF switch state as a 4-bit integer. """ - self.cfg_write((self.cfg_reg & ~0xf) | state) + self.cfg_write((self.cfg_reg & ~0xF) | state) @portable(flags={"fast-math"}) def mu_to_att(self, att_mu: TInt32) -> TFloat: @@ -294,7 +770,7 @@ class CPLD: :param att_mu: Digital attenuation setting. :return: Attenuation setting in dB. """ - return (255 - (att_mu & 0xff)) / 8 + return (255 - (att_mu & 0xFF)) / 8 @portable(flags={"fast-math"}) def att_to_mu(self, att: TFloat) -> TInt32: @@ -320,19 +796,18 @@ class CPLD: :param att: 8-bit digital attenuation setting: 255 minimum attenuation, 0 maximum attenuation (31.5 dB) """ - a = self.att_reg & ~(0xff << (channel * 8)) + a = self.att_reg & ~(0xFF << (channel * 8)) a |= att << (channel * 8) self.set_all_att_mu(a) @kernel def set_all_att_mu(self, att_reg: TInt32): - """Set all four digital step attenuators (in machine units). + """Set all four digital step attenuators (in machine units). See also :meth:`set_att_mu`. :param att_reg: Attenuator setting string (32-bit) """ - self.bus.set_config_mu(SPI_CONFIG | spi.SPI_END, 32, - SPIT_ATT_WR, CS_ATT) + self.bus.set_config_mu(SPI_CONFIG | spi.SPI_END, 32, SPIT_ATT_WR, CS_ATT) self.bus.write(att_reg) self.att_reg = att_reg @@ -340,7 +815,6 @@ class CPLD: def set_att(self, channel: TInt32, att: TFloat): """Set digital step attenuator in SI units. - This method will write the attenuator settings of all four channels. See also :meth:`set_att_mu`. :param channel: Attenuator channel (0-3). @@ -361,11 +835,9 @@ class CPLD: :return: 32-bit attenuator settings """ - self.bus.set_config_mu(SPI_CONFIG | spi.SPI_INPUT, 32, - SPIT_ATT_RD, CS_ATT) + self.bus.set_config_mu(SPI_CONFIG | spi.SPI_INPUT, 32, SPIT_ATT_RD, CS_ATT) self.bus.write(0) # shift in zeros, shift out current value - self.bus.set_config_mu(SPI_CONFIG | spi.SPI_END, 32, - SPIT_ATT_WR, CS_ATT) + self.bus.set_config_mu(SPI_CONFIG | spi.SPI_END, 32, SPIT_ATT_WR, CS_ATT) delay(10 * us) self.att_reg = self.bus.read() self.bus.write(self.att_reg) # shift in current value again and latch @@ -384,7 +856,7 @@ class CPLD: :return: 8-bit digital attenuation setting: 255 minimum attenuation, 0 maximum attenuation (31.5 dB) """ - return int32((self.get_att_mu() >> (channel * 8)) & 0xff) + return int32((self.get_att_mu() >> (channel * 8)) & 0xFF) @kernel def get_channel_att(self, channel: TInt32) -> TFloat: @@ -417,13 +889,12 @@ class CPLD: self.sync.set_mu(ftw) @kernel - def set_profile(self, profile: TInt32): - """Set the PROFILE pins. - - The PROFILE pins are common to all four DDS channels. + def set_profile(self, channel: TInt32, profile: TInt32): + """Set the CFG.PROFILE[0:2] pins for a channel. + :param channel: Channel (0-3). :param profile: PROFILE pins in numeric representation (0-7). """ - cfg = self.cfg_reg & ~(7 << CFG_PROFILE) - cfg |= (profile & 7) << CFG_PROFILE + cfg = self.cfg_reg & ~(7 << (CFG_PROFILE + channel * 3)) + cfg |= (profile & 7) << (CFG_PROFILE + channel * 3) self.cfg_write(cfg)