From ea55c29568bacddf7e2faedc5c686fe351ac5b12 Mon Sep 17 00:00:00 2001 From: Sebastien Bourdeauducq Date: Mon, 28 Feb 2022 17:00:24 +0800 Subject: [PATCH] phaser: port to NAC3 --- artiq/coredevice/phaser.py | 369 +++++++++++--------- artiq/examples/nac3devices/nac3devices.json | 4 + artiq/examples/nac3devices/nac3devices.py | 3 + 3 files changed, 210 insertions(+), 166 deletions(-) diff --git a/artiq/coredevice/phaser.py b/artiq/coredevice/phaser.py index 5f7cd7108..5f3c4c334 100644 --- a/artiq/coredevice/phaser.py +++ b/artiq/coredevice/phaser.py @@ -1,13 +1,28 @@ +from __future__ import annotations from numpy import int32, int64 -from artiq.language.core import kernel, delay_mu, delay +from artiq.language.core import * +from artiq.coredevice.core import Core from artiq.coredevice.rtio import rtio_output, rtio_input_data, rtio_input_timestamp from artiq.language.units import us, ns, ms, MHz -from artiq.language.types import TInt32 from artiq.coredevice.dac34h84 import DAC34H84 from artiq.coredevice.trf372017 import TRF372017 +# NAC3TODO work around https://git.m-labs.hk/M-Labs/nac3/issues/189 +@nac3 +class ValueError(Exception): + pass + +# NAC3TODO https://git.m-labs.hk/M-Labs/nac3/issues/201 +@portable +def abs(x: int32) -> int32: + if x > 0: + return x + else: + return -x + + PHASER_BOARD_ID = 19 PHASER_ADDR_BOARD_ID = 0x00 PHASER_ADDR_HW_REV = 0x01 @@ -59,6 +74,7 @@ PHASER_DAC_SEL_TEST = 1 PHASER_HW_REV_VARIANT = 1 << 4 +@nac3 class Phaser: """Phaser 4-channel, 16-bit, 1 GS/s DAC coredevice driver. @@ -133,7 +149,7 @@ class Phaser: and buffer round trip. Tuning this might be automated later. :param tune_fifo_offset: Tune the DAC FIFO read pointer offset (default=True) - :param clk_sel: Select the external SMA clock input (1 or 0) + :param clk_sel: Select the external SMA clock input. :param sync_dly: SYNC delay with respect to ISTR. :param dac: DAC34H84 DAC settings as a dictionary. :param trf0: Channel 0 TRF372017 quadrature upconverter settings as a @@ -147,11 +163,20 @@ class Phaser: To access oscillators, digital upconverters, PLL/VCO analog quadrature upconverters and attenuators. """ - kernel_invariants = {"core", "channel_base", "t_frame", "miso_delay", - "dac_mmap"} + + core: KernelInvariant[Core] + channel_base: KernelInvariant[int32] + t_frame: KernelInvariant[int32] + miso_delay: KernelInvariant[int32] + frame_tstamp: Kernel[int64] + clk_sel: Kernel[bool] + tune_fifo_offset: Kernel[bool] + sync_dly: Kernel[int32] + dac_mmap: KernelInvariant[list[int32]] + channel: Kernel[list[PhaserChannel]] def __init__(self, dmgr, channel_base, miso_delay=1, tune_fifo_offset=True, - clk_sel=0, sync_dly=0, dac=None, trf0=None, trf1=None, + clk_sel=False, sync_dly=0, dac=None, trf0=None, trf1=None, core_device="core"): self.channel_base = channel_base self.core = dmgr.get(core_device) @@ -172,7 +197,7 @@ class Phaser: for ch, trf in enumerate([trf0, trf1])] @kernel - def init(self, debug=False): + def init(self, debug: bool = False): """Initialize the board. Verifies board and chip presence, resets components, performs @@ -182,40 +207,40 @@ class Phaser: board_id = self.read8(PHASER_ADDR_BOARD_ID) if board_id != PHASER_BOARD_ID: raise ValueError("invalid board id") - delay(.1*ms) # slack + self.core.delay(.1*ms) # slack hw_rev = self.read8(PHASER_ADDR_HW_REV) - delay(.1*ms) # slack - is_baseband = hw_rev & PHASER_HW_REV_VARIANT + self.core.delay(.1*ms) # slack + is_baseband = hw_rev & PHASER_HW_REV_VARIANT != 0 gw_rev = self.read8(PHASER_ADDR_GW_REV) if debug: - print("gw_rev:", gw_rev) + # NAC3TODO print("gw_rev:", gw_rev) self.core.break_realtime() - delay(.1*ms) # slack + self.core.delay(.1*ms) # slack # allow a few errors during startup and alignment since boot if self.get_crc_err() > 20: raise ValueError("large number of frame CRC errors") - delay(.1*ms) # slack + self.core.delay(.1*ms) # slack # determine the origin for frame-aligned timestamps self.measure_frame_timestamp() - if self.frame_tstamp < 0: + if self.frame_tstamp < int64(0): raise ValueError("frame timestamp measurement timed out") - delay(.1*ms) + self.core.delay(.1*ms) # reset - self.set_cfg(dac_resetb=0, dac_sleep=1, dac_txena=0, - trf0_ps=1, trf1_ps=1, - att0_rstn=0, att1_rstn=0) + self.set_cfg(dac_resetb=False, dac_sleep=True, dac_txena=False, + trf0_ps=True, trf1_ps=True, + att0_rstn=False, att1_rstn=False) self.set_leds(0x00) self.set_fan_mu(0) # bring dac out of reset, keep tx off - self.set_cfg(clk_sel=self.clk_sel, dac_txena=0, - trf0_ps=1, trf1_ps=1, - att0_rstn=0, att1_rstn=0) - delay(.1*ms) # slack + self.set_cfg(clk_sel=self.clk_sel, dac_txena=False, + trf0_ps=True, trf1_ps=True, + att0_rstn=False, att1_rstn=False) + self.core.delay(.1*ms) # slack # crossing dac_clk (reference) edges with sync_dly # changes the optimal fifo_offset by 4 @@ -225,25 +250,25 @@ class Phaser: self.dac_write(0x02, 0x0080) if self.dac_read(0x7f) != 0x5409: raise ValueError("DAC version readback invalid") - delay(.1*ms) + self.core.delay(.1*ms) if self.dac_read(0x00) != 0x049c: raise ValueError("DAC config0 reset readback invalid") - delay(.1*ms) + self.core.delay(.1*ms) t = self.get_dac_temperature() - delay(.1*ms) + self.core.delay(.1*ms) if t < 10 or t > 90: raise ValueError("DAC temperature out of bounds") for data in self.dac_mmap: self.dac_write(data >> 16, data) - delay(40*us) + self.core.delay(40.*us) self.dac_sync() - delay(40*us) + self.core.delay(40.*us) # pll_ndivsync_ena disable config18 = self.dac_read(0x18) - delay(.1*ms) + self.core.delay(.1*ms) self.dac_write(0x18, config18 & ~0x0800) patterns = [ @@ -256,33 +281,33 @@ class Phaser: # either side) and no need to tune at runtime. # Parity provides another level of safety. for i in range(len(patterns)): - delay(.5*ms) + self.core.delay(.5*ms) errors = self.dac_iotest(patterns[i]) - if errors: + if errors != 0: raise ValueError("DAC iotest failure") - delay(2*ms) # let it settle + self.core.delay(2.*ms) # let it settle lvolt = self.dac_read(0x18) & 7 - delay(.1*ms) + self.core.delay(.1*ms) if lvolt < 2 or lvolt > 5: raise ValueError("DAC PLL lock failed, check clocking") if self.tune_fifo_offset: fifo_offset = self.dac_tune_fifo_offset() if debug: - print("fifo_offset:", fifo_offset) + # NAC3TODO print("fifo_offset:", fifo_offset) self.core.break_realtime() # self.dac_write(0x20, 0x0000) # stop fifo sync # alarm = self.get_sta() & 1 - # delay(.1*ms) + # self.core.delay(.1*ms) self.clear_dac_alarms() - delay(2*ms) # let it run a bit + self.core.delay(2.*ms) # let it run a bit alarms = self.get_dac_alarms() - delay(.1*ms) # slack - if alarms & ~0x0040: # ignore PLL alarms (see DS) + self.core.delay(.1*ms) # slack + if alarms & ~0x0040 != 0: # ignore PLL alarms (see DS) if debug: - print("alarms:", alarms) + # NAC3TODO print("alarms:", alarms) self.core.break_realtime() # ignore alarms else: @@ -290,16 +315,16 @@ class Phaser: # avoid malformed output for: mixer_ena=1, nco_ena=0 after power up self.dac_write(self.dac_mmap[2] >> 16, self.dac_mmap[2] | (1 << 4)) - delay(40*us) + self.core.delay(40.*us) self.dac_sync() - delay(100*us) + self.core.delay(100.*us) self.dac_write(self.dac_mmap[2] >> 16, self.dac_mmap[2]) - delay(40*us) + self.core.delay(40.*us) self.dac_sync() - delay(100*us) + self.core.delay(100.*us) # power up trfs, release att reset - self.set_cfg(clk_sel=self.clk_sel, dac_txena=0) + self.set_cfg(clk_sel=self.clk_sel, dac_txena=False) for ch in range(2): channel = self.channel[ch] @@ -307,7 +332,7 @@ class Phaser: channel.set_att_mu(0x5a) if channel.get_att_mu() != 0x5a: raise ValueError("attenuator test failed") - delay(.1*ms) + self.core.delay(.1*ms) channel.set_att_mu(0x00) # minimum attenuation # test oscillators and DUC @@ -317,18 +342,18 @@ class Phaser: if i == 0: asf = 0x7fff # 6pi/4 phase - oscillator.set_amplitude_phase_mu(asf=asf, pow=0xc000, clr=1) - delay(1*us) + oscillator.set_amplitude_phase_mu(asf=asf, pow=0xc000, clr=True) + self.core.delay(1.*us) # 3pi/4 channel.set_duc_phase_mu(0x6000) - channel.set_duc_cfg(select=0, clr=1) + channel.set_duc_cfg(select=0, clr=True) self.duc_stb() - delay(.1*ms) # settle link, pipeline and impulse response + self.core.delay(.1*ms) # settle link, pipeline and impulse response data = channel.get_dac_data() - delay(1*us) + self.core.delay(1.*us) channel.oscillator[0].set_amplitude_phase_mu(asf=0, pow=0xc000, - clr=1) - delay(.1*ms) + clr=True) + self.core.delay(.1*ms) sqrt2 = 0x5a81 # 0x7fff/sqrt(2) data_i = data & 0xffff data_q = (data >> 16) & 0xffff @@ -342,27 +367,27 @@ class Phaser: if channel.trf_read(0) & 0x7f != 0x68: raise ValueError("TRF identification failed") - delay(.1*ms) + self.core.delay(.1*ms) - delay(.2*ms) + self.core.delay(.2*ms) for data in channel.trf_mmap: channel.trf_write(data) channel.cal_trf_vco() - delay(2*ms) # lock + self.core.delay(2.*ms) # lock if not (self.get_sta() & (PHASER_STA_TRF0_LD << ch)): raise ValueError("TRF lock failure") - delay(.1*ms) - if channel.trf_read(0) & 0x1000: + self.core.delay(.1*ms) + if channel.trf_read(0) & 0x1000 != 0: raise ValueError("TRF R_SAT_ERR") - delay(.1*ms) + self.core.delay(.1*ms) channel.en_trf_out() # enable dac tx self.set_cfg(clk_sel=self.clk_sel) @kernel - def write8(self, addr, data): + def write8(self, addr: int32, data: int32): """Write data to FPGA register. :param addr: Address to write to (7 bit) @@ -372,7 +397,7 @@ class Phaser: delay_mu(int64(self.t_frame)) @kernel - def read8(self, addr) -> TInt32: + def read8(self, addr: int32) -> int32: """Read from FPGA register. :param addr: Address to read from (7 bit) @@ -383,7 +408,7 @@ class Phaser: return response >> self.miso_delay @kernel - def write32(self, addr, data: TInt32): + def write32(self, addr: int32, data: int32): """Write 32 bit to a sequence of FPGA registers.""" for offset in range(4): byte = data >> 24 @@ -391,17 +416,17 @@ class Phaser: data <<= 8 @kernel - def read32(self, addr) -> TInt32: + def read32(self, addr: int32) -> int32: """Read 32 bit from a sequence of FPGA registers.""" data = 0 for offset in range(4): data <<= 8 data |= self.read8(addr + offset) - delay(20*us) # slack + self.core.delay(20.*us) # slack return data @kernel - def set_leds(self, leds): + def set_leds(self, leds: int32): """Set the front panel LEDs. :param leds: LED settings (6 bit) @@ -409,7 +434,7 @@ class Phaser: self.write8(PHASER_ADDR_LED, leds) @kernel - def set_fan_mu(self, pwm): + def set_fan_mu(self, pwm: int32): """Set the fan duty cycle. :param pwm: Duty cycle in machine units (8 bit) @@ -417,19 +442,19 @@ class Phaser: self.write8(PHASER_ADDR_FAN, pwm) @kernel - def set_fan(self, duty): + def set_fan(self, duty: float): """Set the fan duty cycle. :param duty: Duty cycle (0. to 1.) """ - pwm = int32(round(duty*255.)) + pwm = round(duty*255.) if pwm < 0 or pwm > 255: raise ValueError("duty cycle out of bounds") self.set_fan_mu(pwm) @kernel - def set_cfg(self, clk_sel=0, dac_resetb=1, dac_sleep=0, dac_txena=1, - trf0_ps=0, trf1_ps=0, att0_rstn=1, att1_rstn=1): + def set_cfg(self, clk_sel: bool = False, dac_resetb: bool = True, dac_sleep: bool = False, dac_txena: bool = True, + trf0_ps: bool = False, trf1_ps: bool = False, att0_rstn: bool = True, att1_rstn: bool = True): """Set the configuration register. Each flag is a single bit (0 or 1). @@ -444,13 +469,13 @@ class Phaser: :param att1_rstn: Active low attenuator 1 reset """ self.write8(PHASER_ADDR_CFG, - ((clk_sel & 1) << 0) | ((dac_resetb & 1) << 1) | - ((dac_sleep & 1) << 2) | ((dac_txena & 1) << 3) | - ((trf0_ps & 1) << 4) | ((trf1_ps & 1) << 5) | - ((att0_rstn & 1) << 6) | ((att1_rstn & 1) << 7)) + (int32(clk_sel) << 0) | (int32(dac_resetb) << 1) | + (int32(dac_sleep) << 2) | (int32(dac_txena) << 3) | + (int32(trf0_ps) << 4) | (int32(trf1_ps) << 5) | + (int32(att0_rstn) << 6) | (int32(att1_rstn) << 7)) @kernel - def get_sta(self): + def get_sta(self) -> int32: """Get the status register value. Bit flags are: @@ -468,7 +493,7 @@ class Phaser: return self.read8(PHASER_ADDR_STA) @kernel - def get_crc_err(self): + def get_crc_err(self) -> int32: """Get the frame CRC error counter. :return: The number of frames with CRC mismatches sind the reset of the @@ -484,21 +509,21 @@ class Phaser: See `get_next_frame_mu()`. """ rtio_output(self.channel_base << 8, 0) # read any register - self.frame_tstamp = rtio_input_timestamp(now_mu() + 4 * self.t_frame, self.channel_base) - delay(100 * us) + self.frame_tstamp = rtio_input_timestamp(now_mu() + int64(4) * int64(self.t_frame), self.channel_base) + self.core.delay(100. * us) @kernel - def get_next_frame_mu(self): + def get_next_frame_mu(self) -> int64: """Return the timestamp of the frame strictly after `now_mu()`. Register updates (DUC, DAC, TRF, etc.) scheduled at this timestamp and multiples of `self.t_frame` later will have deterministic latency to output. """ - n = int64((now_mu() - self.frame_tstamp) / self.t_frame) - return self.frame_tstamp + (n + 1) * self.t_frame + n = int64((now_mu() - self.frame_tstamp) / int64(self.t_frame)) + return self.frame_tstamp + (n + int64(1)) * int64(self.t_frame) @kernel - def set_sync_dly(self, dly): + def set_sync_dly(self, dly: int32): """Set SYNC delay. :param dly: DAC SYNC delay setting (0 to 7) @@ -517,8 +542,8 @@ class Phaser: self.write8(PHASER_ADDR_DUC_STB, 0) @kernel - def spi_cfg(self, select, div, end, clk_phase=0, clk_polarity=0, - half_duplex=0, lsb_first=0, offline=0, length=8): + def spi_cfg(self, select: int32, div: int32, end: bool, clk_phase: bool = False, clk_polarity: bool = False, + half_duplex: bool = False, lsb_first: bool = False, offline: bool = False, length: int32 = 8): """Set the SPI machine configuration :param select: Chip selects to assert (DAC, TRF0, TRF1, ATT0, ATT1) @@ -538,63 +563,63 @@ class Phaser: self.write8(PHASER_ADDR_SPI_SEL, select) self.write8(PHASER_ADDR_SPI_DIVLEN, (div - 2 >> 3) | (length - 1 << 5)) self.write8(PHASER_ADDR_SPI_CFG, - ((offline & 1) << 0) | ((end & 1) << 1) | - ((clk_phase & 1) << 2) | ((clk_polarity & 1) << 3) | - ((half_duplex & 1) << 4) | ((lsb_first & 1) << 5)) + (int32(offline) << 0) | (int32(end) << 1) | + (int32(clk_phase) << 2) | (int32(clk_polarity) << 3) | + (int32(half_duplex) << 4) | (int32(lsb_first) << 5)) @kernel - def spi_write(self, data): + def spi_write(self, data: int32): """Write 8 bits into the SPI data register and start/continue the transaction.""" self.write8(PHASER_ADDR_SPI_DATW, data) @kernel - def spi_read(self): + def spi_read(self) -> int32: """Read from the SPI input data register.""" return self.read8(PHASER_ADDR_SPI_DATR) @kernel - def dac_write(self, addr, data): + def dac_write(self, addr: int32, data: int32): """Write 16 bit to a DAC register. :param addr: Register address :param data: Register data to write """ div = 34 # 100 ns min period - t_xfer = self.core.seconds_to_mu((8 + 1)*div*4*ns) - self.spi_cfg(select=PHASER_SEL_DAC, div=div, end=0) + t_xfer = self.core.seconds_to_mu((8. + 1.)*float(div)*4.*ns) + self.spi_cfg(select=PHASER_SEL_DAC, div=div, end=False) self.spi_write(addr) delay_mu(t_xfer) self.spi_write(data >> 8) delay_mu(t_xfer) - self.spi_cfg(select=PHASER_SEL_DAC, div=div, end=1) + self.spi_cfg(select=PHASER_SEL_DAC, div=div, end=True) self.spi_write(data) delay_mu(t_xfer) @kernel - def dac_read(self, addr, div=34) -> TInt32: + def dac_read(self, addr: int32, div: int32 = 34) -> int32: """Read from a DAC register. :param addr: Register address to read from :param div: SPI clock divider. Needs to be at least 250 (1 µs SPI clock) to read the temperature register. """ - t_xfer = self.core.seconds_to_mu((8 + 1)*div*4*ns) - self.spi_cfg(select=PHASER_SEL_DAC, div=div, end=0) + t_xfer = self.core.seconds_to_mu((8. + 1.)*float(div)*4.*ns) + self.spi_cfg(select=PHASER_SEL_DAC, div=div, end=False) self.spi_write(addr | 0x80) delay_mu(t_xfer) self.spi_write(0) delay_mu(t_xfer) data = self.spi_read() << 8 - delay(20*us) # slack - self.spi_cfg(select=PHASER_SEL_DAC, div=div, end=1) + self.core.delay(20.*us) # slack + self.spi_cfg(select=PHASER_SEL_DAC, div=div, end=True) self.spi_write(0) delay_mu(t_xfer) data |= self.spi_read() return data @kernel - def get_dac_temperature(self) -> TInt32: + def get_dac_temperature(self) -> int32: """Read the DAC die temperature. :return: DAC temperature in degree Celsius @@ -617,12 +642,12 @@ class Phaser: .. note:: Synchronising the NCO clears the phase-accumulator """ config1f = self.dac_read(0x1f) - delay(.1*ms) + self.core.delay(.1*ms) self.dac_write(0x1f, config1f & ~int32(1 << 1)) self.dac_write(0x1f, config1f | (1 << 1)) @kernel - def set_dac_cmix(self, fs_8_step): + def set_dac_cmix(self, fs_8_step: int32): """Set the DAC coarse mixer frequency for both channels Use of the coarse mixer requires the DAC mixer to be enabled. The mixer @@ -640,11 +665,11 @@ class Phaser: vals = [0b0000, 0b1000, 0b0100, 0b1100, 0b0010, 0b1010, 0b0001, 0b1110] cmix = vals[fs_8_step%8] config0d = self.dac_read(0x0d) - delay(.1*ms) + self.core.delay(.1*ms) self.dac_write(0x0d, (config0d & ~(0b1111 << 12)) | (cmix << 12)) @kernel - def get_dac_alarms(self): + def get_dac_alarms(self) -> int32: """Read the DAC alarm flags. :return: DAC alarm flags (see datasheet for bit meaning) @@ -657,7 +682,7 @@ class Phaser: self.dac_write(0x05, 0x0000) @kernel - def dac_iotest(self, pattern) -> TInt32: + def dac_iotest(self, pattern: list[int32]) -> int32: """Performs a DAC IO test according to the datasheet. :param pattern: List of four int32 containing the pattern @@ -669,7 +694,7 @@ class Phaser: self.dac_write(0x25 + addr, pattern[addr]) # repeat the pattern twice self.dac_write(0x29 + addr, pattern[addr]) - delay(.1*ms) + self.core.delay(.1*ms) for ch in range(2): channel = self.channel[ch] channel.set_duc_cfg(select=1) # test @@ -678,20 +703,20 @@ class Phaser: channel.set_dac_test(data) if channel.get_dac_data() != data: raise ValueError("DAC test data readback failed") - delay(.1*ms) + self.core.delay(.1*ms) cfg = self.dac_read(0x01) - delay(.1*ms) + self.core.delay(.1*ms) self.dac_write(0x01, cfg | 0x8000) # iotest_ena self.dac_write(0x04, 0x0000) # clear iotest_result - delay(.2*ms) # let it rip + self.core.delay(.2*ms) # let it rip # no need to go through the alarm register, # just read the error mask # self.clear_dac_alarms() alarms = self.get_dac_alarms() - delay(.1*ms) # slack - if alarms & 0x0080: # alarm_from_iotest + self.core.delay(.1*ms) # slack + if alarms & 0x0080 != 0: # alarm_from_iotest errors = self.dac_read(0x04) - delay(.1*ms) # slack + self.core.delay(.1*ms) # slack else: errors = 0 self.dac_write(0x01, cfg) # clear config @@ -699,7 +724,7 @@ class Phaser: return errors @kernel - def dac_tune_fifo_offset(self): + def dac_tune_fifo_offset(self) -> int32: """Scan through `fifo_offset` and configure midpoint setting. :return: Optimal `fifo_offset` setting with maximum margin to write @@ -712,15 +737,15 @@ class Phaser: # distance 32101234 # error free x xx config9 = self.dac_read(0x09) - delay(.1*ms) + self.core.delay(.1*ms) good = 0 for o in range(8): # set new fifo_offset self.dac_write(0x09, (config9 & 0x1fff) | (o << 13)) self.clear_dac_alarms() - delay(.1*ms) # run + self.core.delay(.1*ms) # run alarms = self.get_dac_alarms() - delay(.1*ms) # slack + self.core.delay(.1*ms) # slack if (alarms >> 11) & 0x7 == 0: # any fifo alarm good |= 1 << o # if there are good offsets accross the wrap around @@ -734,7 +759,7 @@ class Phaser: sum = 0 count = 0 for o in range(8): - if good & (1 << o): + if good & (1 << o) != 0: sum += o count += 1 best = ((sum // count) + offset) % 8 @@ -742,6 +767,7 @@ class Phaser: return best +@nac3 class PhaserChannel: """Phaser channel IQ pair. @@ -772,9 +798,15 @@ class PhaserChannel: or overflow after the interpolation. Either band-limit any changes in the oscillator parameters or back off the amplitude sufficiently. """ - kernel_invariants = {"index", "phaser", "trf_mmap"} + + core: KernelInvariant[Core] + phaser: KernelInvariant[Phaser] + index: KernelInvariant[int32] + trf_mmap: KernelInvariant[list[int32]] + oscillator: Kernel[list[PhaserOscillator]] def __init__(self, phaser, index, trf): + self.core = phaser.core self.phaser = phaser self.index = index self.trf_mmap = TRF372017(trf).get_mmap() @@ -782,7 +814,7 @@ class PhaserChannel: self.oscillator = [PhaserOscillator(self, osc) for osc in range(5)] @kernel - def get_dac_data(self) -> TInt32: + def get_dac_data(self) -> int32: """Get a sample of the current DAC data. The data is split accross multiple registers and thus the data @@ -794,7 +826,7 @@ class PhaserChannel: return self.phaser.read32(PHASER_ADDR_DAC0_DATA + (self.index << 4)) @kernel - def set_dac_test(self, data: TInt32): + def set_dac_test(self, data: int32): """Set the DAC test data. :param data: 32 bit IQ test data, I/DACA/DACC in the 16 LSB, @@ -803,7 +835,7 @@ class PhaserChannel: self.phaser.write32(PHASER_ADDR_DAC0_TEST + (self.index << 4), data) @kernel - def set_duc_cfg(self, clr=0, clr_once=0, select=0): + def set_duc_cfg(self, clr: bool = False, clr_once: bool = False, select: int32 = 0): """Set the digital upconverter (DUC) and interpolator configuration. :param clr: Keep the phase accumulator cleared (persistent) @@ -812,11 +844,11 @@ class PhaserChannel: data, other values: reserved) """ self.phaser.write8(PHASER_ADDR_DUC0_CFG + (self.index << 4), - ((clr & 1) << 0) | ((clr_once & 1) << 1) | + (int32(clr) << 0) | (int32(clr_once) << 1) | ((select & 3) << 2)) @kernel - def set_duc_frequency_mu(self, ftw): + def set_duc_frequency_mu(self, ftw: int32): """Set the DUC frequency. :param ftw: DUC frequency tuning word (32 bit) @@ -824,17 +856,17 @@ class PhaserChannel: self.phaser.write32(PHASER_ADDR_DUC0_F + (self.index << 4), ftw) @kernel - def set_duc_frequency(self, frequency): + def set_duc_frequency(self, frequency: float): """Set the DUC frequency in SI units. :param frequency: DUC frequency in Hz (passband from -200 MHz to 200 MHz, wrapping around at +- 250 MHz) """ - ftw = int32(round(frequency*((1 << 30)/(125*MHz)))) + ftw = round(frequency*(float(1 << 30)/(125.*MHz))) self.set_duc_frequency_mu(ftw) @kernel - def set_duc_phase_mu(self, pow): + def set_duc_phase_mu(self, pow: int32): """Set the DUC phase offset. :param pow: DUC phase offset word (16 bit) @@ -844,16 +876,16 @@ class PhaserChannel: self.phaser.write8(addr + 1, pow) @kernel - def set_duc_phase(self, phase): + def set_duc_phase(self, phase: float): """Set the DUC phase in SI units. :param phase: DUC phase in turns """ - pow = int32(round(phase*(1 << 16))) + pow = round(phase*float(1 << 16)) self.set_duc_phase_mu(pow) @kernel - def set_nco_frequency_mu(self, ftw): + def set_nco_frequency_mu(self, ftw: int32): """Set the NCO frequency. This method stages the new NCO frequency, but does not apply it. @@ -868,7 +900,7 @@ class PhaserChannel: self.phaser.dac_write(0x14 + (self.index << 1), ftw) @kernel - def set_nco_frequency(self, frequency): + def set_nco_frequency(self, frequency: float): """Set the NCO frequency in SI units. This method stages the new NCO frequency, but does not apply it. @@ -880,11 +912,11 @@ class PhaserChannel: :param frequency: NCO frequency in Hz (passband from -400 MHz to 400 MHz, wrapping around at +- 500 MHz) """ - ftw = int32(round(frequency*((1 << 30)/(250*MHz)))) + ftw = round(frequency*(float(1 << 30)/(250.*MHz))) self.set_nco_frequency_mu(ftw) @kernel - def set_nco_phase_mu(self, pow): + def set_nco_phase_mu(self, pow: int32): """Set the NCO phase offset. By default, the new NCO phase applies on completion of the SPI @@ -902,7 +934,7 @@ class PhaserChannel: self.phaser.dac_write(0x12 + self.index, pow) @kernel - def set_nco_phase(self, phase): + def set_nco_phase(self, phase: float): """Set the NCO phase in SI units. By default, the new NCO phase applies on completion of the SPI @@ -917,36 +949,36 @@ class PhaserChannel: :param phase: NCO phase in turns """ - pow = int32(round(phase*(1 << 16))) + pow = round(phase*float(1 << 16)) self.set_nco_phase_mu(pow) @kernel - def set_att_mu(self, data): + def set_att_mu(self, data: int32): """Set channel attenuation. :param data: Attenuator data in machine units (8 bit) """ div = 34 # 30 ns min period - t_xfer = self.phaser.core.seconds_to_mu((8 + 1)*div*4*ns) + t_xfer = self.core.seconds_to_mu((8. + 1.)*float(div)*4.*ns) self.phaser.spi_cfg(select=PHASER_SEL_ATT0 << self.index, div=div, - end=1) + end=True) self.phaser.spi_write(data) delay_mu(t_xfer) @kernel - def set_att(self, att): + def set_att(self, att: float): """Set channel attenuation in SI units. :param att: Attenuation in dB """ # 2 lsb are inactive, resulting in 8 LSB per dB - data = 0xff - int32(round(att*8)) + data = 0xff - round(att*8.) if data < 0 or data > 0xff: raise ValueError("attenuation out of bounds") self.set_att_mu(data) @kernel - def get_att_mu(self) -> TInt32: + def get_att_mu(self) -> int32: """Read current attenuation. The current attenuation value is read without side effects. @@ -954,39 +986,39 @@ class PhaserChannel: :return: Current attenuation in machine units """ div = 34 - t_xfer = self.phaser.core.seconds_to_mu((8 + 1)*div*4*ns) + t_xfer = self.core.seconds_to_mu((8. + 1.)*float(div)*4.*ns) self.phaser.spi_cfg(select=PHASER_SEL_ATT0 << self.index, div=div, - end=0) + end=False) self.phaser.spi_write(0) delay_mu(t_xfer) data = self.phaser.spi_read() - delay(20*us) # slack + self.core.delay(20.*us) # slack self.phaser.spi_cfg(select=PHASER_SEL_ATT0 << self.index, div=div, - end=1) + end=True) self.phaser.spi_write(data) delay_mu(t_xfer) return data @kernel - def trf_write(self, data, readback=False): + def trf_write(self, data: int32, readback: bool = False) -> int32: """Write 32 bits to quadrature upconverter register. :param data: Register data (32 bit) containing encoded address :param readback: Whether to return the read back MISO data """ div = 34 # 50 ns min period - t_xfer = self.phaser.core.seconds_to_mu((8 + 1)*div*4*ns) + t_xfer = self.core.seconds_to_mu((8. + 1.)*float(div)*4.*ns) read = 0 - end = 0 - clk_phase = 0 + end = False + clk_phase = False if readback: - clk_phase = 1 + clk_phase = True for i in range(4): if i == 0 or i == 3: if i == 3: - end = 1 + end = True self.phaser.spi_cfg(select=PHASER_SEL_TRF0 << self.index, - div=div, lsb_first=1, clk_phase=clk_phase, + div=div, lsb_first=True, clk_phase=clk_phase, end=end) self.phaser.spi_write(data) data >>= 8 @@ -994,22 +1026,22 @@ class PhaserChannel: if readback: read >>= 8 read |= self.phaser.spi_read() << 24 - delay(20*us) # slack + self.core.delay(20.*us) # slack return read @kernel - def trf_read(self, addr, cnt_mux_sel=0) -> TInt32: + def trf_read(self, addr: int32, cnt_mux_sel: int32 = 0) -> int32: """Quadrature upconverter register read. :param addr: Register address to read (0 to 7) :param cnt_mux_sel: Report VCO counter min or max frequency :return: Register data (32 bit) """ - self.trf_write(0x80000008 | (addr << 28) | (cnt_mux_sel << 27)) + self.trf_write(int32(int64(0x80000008)) | (addr << 28) | (cnt_mux_sel << 27)) # single clk pulse with ~LE to start readback - self.phaser.spi_cfg(select=0, div=34, end=1, length=1) + self.phaser.spi_cfg(select=0, div=34, end=True, length=1) self.phaser.spi_write(0) - delay((1 + 1)*34*4*ns) + self.core.delay((1. + 1.)*34.*4.*ns) return self.trf_write(0x00000008 | (cnt_mux_sel << 27), readback=True) @@ -1022,24 +1054,25 @@ class PhaserChannel: self.trf_write(self.trf_mmap[1] | (1 << 31)) @kernel - def en_trf_out(self, rf=1, lo=0): + def en_trf_out(self, rf: bool = True, lo: bool = False): """Enable the rf/lo outputs of the upconverter (hardware variant). :param rf: 1 to enable RF output, 0 to disable :param lo: 1 to enable LO output, 0 to disable """ data = self.trf_read(0xc) - delay(0.1 * ms) + self.core.delay(0.1 * ms) # set RF and LO output bits data = data | (1 << 12) | (1 << 13) | (1 << 14) # clear to enable output - if rf == 1: + if rf: data = data ^ (1 << 14) - if lo == 1: + if lo: data = data ^ ((1 << 12) | (1 << 13)) self.trf_write(data) +@nac3 class PhaserOscillator: """Phaser IQ channel oscillator (NCO/DDS). @@ -1047,15 +1080,19 @@ class PhaserOscillator: oscillator parameters (amplitude and phase/frequency) are deterministic (with respect to the 25 MS/s sample clock) but not matched. """ - kernel_invariants = {"channel", "base_addr"} + + core: KernelInvariant[Core] + channel: KernelInvariant[PhaserChannel] + base_addr: KernelInvariant[int32] def __init__(self, channel, index): + self.core = channel.core self.channel = channel self.base_addr = ((self.channel.phaser.channel_base + 1 + 2*self.channel.index) << 8) | index @kernel - def set_frequency_mu(self, ftw): + def set_frequency_mu(self, ftw: int32): """Set Phaser MultiDDS frequency tuning word. :param ftw: Frequency tuning word (32 bit) @@ -1063,36 +1100,36 @@ class PhaserOscillator: rtio_output(self.base_addr, ftw) @kernel - def set_frequency(self, frequency): + def set_frequency(self, frequency: float): """Set Phaser MultiDDS frequency. :param frequency: Frequency in Hz (passband from -10 MHz to 10 MHz, wrapping around at +- 12.5 MHz) """ - ftw = int32(round(frequency*((1 << 30)/(6.25*MHz)))) + ftw = round(frequency*(float(1 << 30)/(6.25*MHz))) self.set_frequency_mu(ftw) @kernel - def set_amplitude_phase_mu(self, asf=0x7fff, pow=0, clr=0): + def set_amplitude_phase_mu(self, asf: int32 = 0x7fff, pow: int32 = 0, clr: bool = False): """Set Phaser MultiDDS amplitude, phase offset and accumulator clear. :param asf: Amplitude (15 bit) :param pow: Phase offset word (16 bit) :param clr: Clear the phase accumulator (persistent) """ - data = (asf & 0x7fff) | ((clr & 1) << 15) | (pow << 16) + data = (asf & 0x7fff) | (int32(clr) << 15) | (pow << 16) rtio_output(self.base_addr + (1 << 8), data) @kernel - def set_amplitude_phase(self, amplitude, phase=0., clr=0): + def set_amplitude_phase(self, amplitude: float, phase: float = 0., clr: bool = False): """Set Phaser MultiDDS amplitude and phase. :param amplitude: Amplitude in units of full scale :param phase: Phase in turns :param clr: Clear the phase accumulator (persistent) """ - asf = int32(round(amplitude*0x7fff)) + asf = round(amplitude*float(0x7fff)) if asf < 0 or asf > 0x7fff: raise ValueError("amplitude out of bounds") - pow = int32(round(phase*(1 << 16))) + pow = round(phase*float(1 << 16)) self.set_amplitude_phase_mu(asf, pow, clr) diff --git a/artiq/examples/nac3devices/nac3devices.json b/artiq/examples/nac3devices/nac3devices.json index 1b547d26d..45c946d3e 100644 --- a/artiq/examples/nac3devices/nac3devices.json +++ b/artiq/examples/nac3devices/nac3devices.json @@ -40,6 +40,10 @@ { "type": "fastino", "ports": [7] + }, + { + "type": "phaser", + "ports": [8] } ] } diff --git a/artiq/examples/nac3devices/nac3devices.py b/artiq/examples/nac3devices/nac3devices.py index ea46f72e5..396da3fd5 100644 --- a/artiq/examples/nac3devices/nac3devices.py +++ b/artiq/examples/nac3devices/nac3devices.py @@ -10,6 +10,7 @@ from artiq.coredevice.sampler import Sampler from artiq.coredevice.edge_counter import EdgeCounter from artiq.coredevice.grabber import Grabber from artiq.coredevice.fastino import Fastino +from artiq.coredevice.phaser import Phaser @nac3 @@ -25,6 +26,7 @@ class NAC3Devices(EnvExperiment): ttl0_counter: KernelInvariant[EdgeCounter] grabber0: KernelInvariant[Grabber] fastino0: KernelInvariant[Fastino] + # NAC3TODO segfault phaser0: KernelInvariant[Fastino] def build(self): self.setattr_device("core") @@ -38,6 +40,7 @@ class NAC3Devices(EnvExperiment): self.setattr_device("ttl0_counter") self.setattr_device("grabber0") self.setattr_device("fastino0") + # NAC3TODO segfault self.setattr_device("phaser0") @kernel def run(self):