diff --git a/artiq/coredevice/dac34h84.py b/artiq/coredevice/dac34h84.py new file mode 100644 index 000000000..155096a1e --- /dev/null +++ b/artiq/coredevice/dac34h84.py @@ -0,0 +1,276 @@ +class DAC34H84: + """DAC34H84 settings and register map. + + For possible values, documentation, and explanation, see the DAC datasheet + at https://www.ti.com/lit/pdf/slas751 + """ + qmc_corr_ena = 0 # msb ab + qmc_offset_ena = 0 # msb ab + invsinc_ena = 0 # msb ab + interpolation = 1 # 2x + fifo_ena = 1 + alarm_out_ena = 1 + alarm_out_pol = 1 + clkdiv_sync_ena = 1 + + iotest_ena = 0 + cnt64_ena = 0 + oddeven_parity = 0 # even + single_parity_ena = 1 + dual_parity_ena = 0 + rev_interface = 0 + dac_complement = 0b0000 # msb A + alarm_fifo = 0b111 # msb 2-away + + dacclkgone_ena = 1 + dataclkgone_ena = 1 + collisiongone_ena = 1 + sif4_ena = 1 + mixer_ena = 0 + mixer_gain = 1 + nco_ena = 0 + revbus = 0 + twos = 1 + + coarse_dac = 9 # 18.75 mA, 0-15 + sif_txenable = 0 + + mask_alarm_from_zerochk = 0 + mask_alarm_fifo_collision = 0 + mask_alarm_fifo_1away = 0 + mask_alarm_fifo_2away = 0 + mask_alarm_dacclk_gone = 0 + mask_alarm_dataclk_gone = 0 + mask_alarm_output_gone = 0 + mask_alarm_from_iotest = 0 + mask_alarm_from_pll = 0 + mask_alarm_parity = 0b0000 # msb a + + qmc_offseta = 0 # 12b + fifo_offset = 2 # 0-7 + qmc_offsetb = 0 # 12b + + qmc_offsetc = 0 # 12b + + qmc_offsetd = 0 # 12b + + qmc_gaina = 0 # 11b + + cmix_fs8 = 0 + cmix_fs4 = 0 + cmix_fs2 = 0 + cmix_nfs4 = 0 + qmc_gainb = 0 # 11b + + qmc_gainc = 0 # 11b + + output_delayab = 0b00 + output_delaycd = 0b00 + qmc_gaind = 0 # 11b + + qmc_phaseab = 0 # 12b + + qmc_phasecd = 0 # 12b + + phase_offsetab = 0 # 16b + phase_offsetcd = 0 # 16b + phase_addab_lsb = 0 # 16b + phase_addab_msb = 0 # 16b + phase_addcd_lsb = 0 # 16b + phase_addcd_msb = 0 # 16b + + pll_reset = 0 + pll_ndivsync_ena = 1 + pll_ena = 1 + pll_cp = 0b01 # single charge pump + pll_p = 0b100 # p=4 + + pll_m2 = 1 # x2 + pll_m = 8 # m = 8 + pll_n = 0b0001 # n = 2 + pll_vcotune = 0b01 + + pll_vco = 0x3f # 4 GHz + bias_sleep = 0 + tsense_sleep = 0 + pll_sleep = 0 + clkrecv_sleep = 0 + dac_sleep = 0b0000 # msb a + + extref_ena = 0 + fuse_sleep = 1 + atest = 0b00000 # atest mode + + syncsel_qmcoffsetab = 0b1001 # sif_sync and register write + syncsel_qmcoffsetcd = 0b1001 # sif_sync and register write + syncsel_qmccorrab = 0b1001 # sif_sync and register write + syncsel_qmccorrcd = 0b1001 # sif_sync and register write + + syncsel_mixerab = 0b1001 # sif_sync and register write + syncsel_mixercd = 0b1001 # sif_sync and register write + syncsel_nco = 0b1000 # sif_sync + syncsel_fifo_input = 0b10 # external lvds istr + sif_sync = 1 + + syncsel_fifoin = 0b0010 # istr + syncsel_fifoout = 0b0100 # ostr + clkdiv_sync_sel = 0 # ostr + + path_a_sel = 0 + path_b_sel = 1 + path_c_sel = 2 + path_d_sel = 3 + # swap dac pairs (CDAB) for layout + # swap I-Q dacs for spectral inversion + dac_a_sel = 3 + dac_b_sel = 2 + dac_c_sel = 1 + dac_d_sel = 0 + + dac_sleep_en = 0b1111 # msb a + clkrecv_sleep_en = 1 + pll_sleep_en = 1 + lvds_data_sleep_en = 1 + lvds_control_sleep_en = 1 + temp_sense_sleep_en = 1 + bias_sleep_en = 1 + + data_dly = 2 + clk_dly = 0 + + ostrtodig_sel = 0 + ramp_ena = 0 + sifdac_ena = 0 + + grp_delaya = 0x00 + grp_delayb = 0x00 + + grp_delayc = 0x00 + grp_delayd = 0x00 + + sifdac = 0 + + def __init__(self, updates=None): + if updates is None: + return + for key, value in updates.items(): + if not hasattr(self, key): + raise KeyError("invalid setting", key) + setattr(self, key, value) + + def get_mmap(self): + mmap = [] + mmap.append( + (0x00 << 16) | + (self.qmc_offset_ena << 14) | (self.qmc_corr_ena << 12) | + (self.interpolation << 8) | (self.fifo_ena << 7) | + (self.alarm_out_ena << 4) | (self.alarm_out_pol << 3) | + (self.clkdiv_sync_ena << 2) | (self.invsinc_ena << 0)) + mmap.append( + (0x01 << 16) | + (self.iotest_ena << 15) | (self.cnt64_ena << 12) | + (self.oddeven_parity << 11) | (self.single_parity_ena << 10) | + (self.dual_parity_ena << 9) | (self.rev_interface << 8) | + (self.dac_complement << 4) | (self.alarm_fifo << 1)) + mmap.append( + (0x02 << 16) | + (self.dacclkgone_ena << 14) | (self.dataclkgone_ena << 13) | + (self.collisiongone_ena << 12) | (self.sif4_ena << 7) | + (self.mixer_ena << 6) | (self.mixer_gain << 5) | + (self.nco_ena << 4) | (self.revbus << 3) | (self.twos << 1)) + mmap.append((0x03 << 16) | (self.coarse_dac << 12) | (self.sif_txenable << 0)) + mmap.append( + (0x07 << 16) | + (self.mask_alarm_from_zerochk << 15) | (1 << 14) | + (self.mask_alarm_fifo_collision << 13) | + (self.mask_alarm_fifo_1away << 12) | + (self.mask_alarm_fifo_2away << 11) | + (self.mask_alarm_dacclk_gone << 10) | + (self.mask_alarm_dataclk_gone << 9) | + (self.mask_alarm_output_gone << 8) | + (self.mask_alarm_from_iotest << 7) | (1 << 6) | + (self.mask_alarm_from_pll << 5) | (self.mask_alarm_parity << 1)) + mmap.append( + (0x08 << 16) | (self.qmc_offseta << 0)) + mmap.append( + (0x09 << 16) | (self.fifo_offset << 13) | (self.qmc_offsetb << 0)) + mmap.append((0x0a << 16) | (self.qmc_offsetc << 0)) + mmap.append((0x0b << 16) | (self.qmc_offsetd << 0)) + mmap.append((0x0c << 16) | (self.qmc_gaina << 0)) + mmap.append( + (0x0d << 16) | + (self.cmix_fs8 << 15) | (self.cmix_fs4 << 14) | + (self.cmix_fs2 << 12) | (self.cmix_nfs4 << 11) | + (self.qmc_gainb << 0)) + mmap.append((0x0e << 16) | (self.qmc_gainc << 0)) + mmap.append( + (0x0f << 16) | + (self.output_delayab << 14) | (self.output_delaycd << 12) | + (self.qmc_gaind << 0)) + mmap.append((0x10 << 16) | (self.qmc_phaseab << 0)) + mmap.append((0x11 << 16) | (self.qmc_phasecd << 0)) + mmap.append((0x12 << 16) | (self.phase_offsetab << 0)) + mmap.append((0x13 << 16) | (self.phase_offsetcd << 0)) + mmap.append((0x14 << 16) | (self.phase_addab_lsb << 0)) + mmap.append((0x15 << 16) | (self.phase_addab_msb << 0)) + mmap.append((0x16 << 16) | (self.phase_addcd_lsb << 0)) + mmap.append((0x17 << 16) | (self.phase_addcd_msb << 0)) + mmap.append( + (0x18 << 16) | + (0b001 << 13) | (self.pll_reset << 12) | + (self.pll_ndivsync_ena << 11) | (self.pll_ena << 10) | + (self.pll_cp << 6) | (self.pll_p << 3)) + mmap.append( + (0x19 << 16) | + (self.pll_m2 << 15) | (self.pll_m << 8) | (self.pll_n << 4) | + (self.pll_vcotune << 2)) + mmap.append( + (0x1a << 16) | + (self.pll_vco << 10) | (self.bias_sleep << 7) | + (self.tsense_sleep << 6) | + (self.pll_sleep << 5) | (self.clkrecv_sleep << 4) | + (self.dac_sleep << 0)) + mmap.append( + (0x1b << 16) | + (self.extref_ena << 15) | (self.fuse_sleep << 11) | + (self.atest << 0)) + mmap.append( + (0x1e << 16) | + (self.syncsel_qmcoffsetab << 12) | + (self.syncsel_qmcoffsetcd << 8) | + (self.syncsel_qmccorrab << 4) | + (self.syncsel_qmccorrcd << 0)) + mmap.append( + (0x1f << 16) | + (self.syncsel_mixerab << 12) | (self.syncsel_mixercd << 8) | + (self.syncsel_nco << 4) | (self.syncsel_fifo_input << 2) | + (self.sif_sync << 1)) + mmap.append( + (0x20 << 16) | + (self.syncsel_fifoin << 12) | (self.syncsel_fifoout << 8) | + (self.clkdiv_sync_sel << 0)) + mmap.append( + (0x22 << 16) | + (self.path_a_sel << 14) | (self.path_b_sel << 12) | + (self.path_c_sel << 10) | (self.path_d_sel << 8) | + (self.dac_a_sel << 6) | (self.dac_b_sel << 4) | + (self.dac_c_sel << 2) | (self.dac_d_sel << 0)) + mmap.append( + (0x23 << 16) | + (self.dac_sleep_en << 12) | (self.clkrecv_sleep_en << 11) | + (self.pll_sleep_en << 10) | (self.lvds_data_sleep_en << 9) | + (self.lvds_control_sleep_en << 8) | + (self.temp_sense_sleep_en << 7) | (1 << 6) | + (self.bias_sleep_en << 5) | (0x1f << 0)) + mmap.append( + (0x24 << 16) | (self.data_dly << 13) | (self.clk_dly << 10)) + mmap.append( + (0x2d << 16) | + (self.ostrtodig_sel << 14) | (self.ramp_ena << 13) | + (0x002 << 1) | (self.sifdac_ena << 0)) + mmap.append( + (0x2e << 16) | (self.grp_delaya << 8) | (self.grp_delayb << 0)) + mmap.append( + (0x2f << 16) | (self.grp_delayc << 8) | (self.grp_delayd << 0)) + mmap.append((0x30 << 16) | self.sifdac) + return mmap diff --git a/artiq/coredevice/phaser.py b/artiq/coredevice/phaser.py new file mode 100644 index 000000000..bcd83926c --- /dev/null +++ b/artiq/coredevice/phaser.py @@ -0,0 +1,945 @@ +from artiq.language.core import kernel, delay_mu, delay +from artiq.coredevice.rtio import rtio_output, rtio_input_data +from artiq.language.units import us, ns, ms, MHz, dB +from artiq.language.types import TInt32 +from artiq.coredevice.dac34h84 import DAC34H84 +from artiq.coredevice.trf372017 import TRF372017 + + +PHASER_BOARD_ID = 19 +PHASER_ADDR_BOARD_ID = 0x00 +PHASER_ADDR_HW_REV = 0x01 +PHASER_ADDR_GW_REV = 0x02 +PHASER_ADDR_CFG = 0x03 +PHASER_ADDR_STA = 0x04 +PHASER_ADDR_CRC_ERR = 0x05 +PHASER_ADDR_LED = 0x06 +PHASER_ADDR_FAN = 0x07 +PHASER_ADDR_DUC_STB = 0x08 +PHASER_ADDR_ADC_CFG = 0x09 +PHASER_ADDR_SPI_CFG = 0x0a +PHASER_ADDR_SPI_DIVLEN = 0x0b +PHASER_ADDR_SPI_SEL = 0x0c +PHASER_ADDR_SPI_DATW = 0x0d +PHASER_ADDR_SPI_DATR = 0x0e +PHASER_ADDR_SYNC_DLY = 0x0f + +PHASER_ADDR_DUC0_CFG = 0x10 +# PHASER_ADDR_DUC0_RESERVED0 = 0x11 +PHASER_ADDR_DUC0_F = 0x12 +PHASER_ADDR_DUC0_P = 0x16 +PHASER_ADDR_DAC0_DATA = 0x18 +PHASER_ADDR_DAC0_TEST = 0x1c + +PHASER_ADDR_DUC1_CFG = 0x20 +# PHASER_ADDR_DUC1_RESERVED0 = 0x21 +PHASER_ADDR_DUC1_F = 0x22 +PHASER_ADDR_DUC1_P = 0x26 +PHASER_ADDR_DAC1_DATA = 0x28 +PHASER_ADDR_DAC1_TEST = 0x2c + +PHASER_SEL_DAC = 1 << 0 +PHASER_SEL_TRF0 = 1 << 1 +PHASER_SEL_TRF1 = 1 << 2 +PHASER_SEL_ATT0 = 1 << 3 +PHASER_SEL_ATT1 = 1 << 4 + +PHASER_STA_DAC_ALARM = 1 << 0 +PHASER_STA_TRF0_LD = 1 << 1 +PHASER_STA_TRF1_LD = 1 << 2 +PHASER_STA_TERM0 = 1 << 3 +PHASER_STA_TERM1 = 1 << 4 +PHASER_STA_SPI_IDLE = 1 << 5 + +PHASER_DAC_SEL_DUC = 0 +PHASER_DAC_SEL_TEST = 1 + +PHASER_HW_REV_VARIANT = 1 << 4 + + +class Phaser: + """Phaser 4-channel, 16-bit, 1 GS/s DAC coredevice driver. + + Phaser contains a 4 channel, 1 GS/s DAC chip with integrated upconversion, + quadrature modulation compensation and interpolation features. + + The coredevice produces 2 IQ (in-phase and quadrature) data streams with 25 + MS/s and 14 bit per quadrature. Each data stream supports 5 independent + numerically controlled IQ oscillators (NCOs, DDSs with 32 bit frequency, 16 + bit phase, 15 bit amplitude, and phase accumulator clear functionality) + added together. See :class:`PhaserChannel` and :class:`PhaserOscillator`. + + Together with a data clock, framing marker, a checksum and metadata for + register access the streams are sent in groups of 8 samples over 1.5 Gb/s + FastLink via a single EEM connector from coredevice to Phaser. + + On Phaser in the FPGA the data streams are buffered and interpolated + from 25 MS/s to 500 MS/s 16 bit followed by a 500 MS/s digital upconverter + with adjustable frequency and phase. The interpolation passband is 20 MHz + wide, passband ripple is less than 1e-3 amplitude, stopband attenuation + is better than 75 dB at offsets > 15 MHz and better than 90 dB at offsets + > 30 MHz. + + The four 16 bit 500 MS/s DAC data streams are sent via a 32 bit parallel + LVDS bus operating at 1 Gb/s per pin pair and processed in the DAC (Texas + Instruments DAC34H84). On the DAC 2x interpolation, sinx/x compensation, + quadrature modulator compensation, fine and coarse mixing as well as group + delay capabilities are available. + + The latency/group delay from the RTIO events setting + :class:`PhaserOscillator` or :class:`PhaserChannel` DUC parameters all they + way to the DAC outputs is deterministic. This enables deterministic + absolute phase with respect to other RTIO input and output events. + + The four analog DAC outputs are passed through anti-aliasing filters. + + In the baseband variant, the even/in-phase DAC channels feed 31.5 dB range + attenuators and are available on the front panel. The odd outputs are + available at MMCX connectors on board. + + In the upconverter variant, each IQ output pair feeds one quadrature + upconverter (Texas Instruments TRF372017) with integrated PLL/VCO. This + digitally configured analog quadrature upconverter supports offset tuning + for carrier and sideband suppression. The output from the upconverter + passes through the 31.5 dB range step attenuator and is available at the + front panel. + + The DAC, the analog quadrature upconverters and the attenuators are + configured through a shared SPI bus that is accessed and controlled via + FPGA registers. + + .. note:: Various register settings of the DAC and the quadrature + upconverters are available to be modified through the `dac`, `trf0`, + `trf1` dictionaries. These can be set through the device database + (`device_db.py`). The settings are frozen during instantiation of the + class and applied during `init()`. See the :class:`DAC34H84` and + :class:`TRF372017` source for details. + + .. note:: To establish deterministic latency between RTIO time base and DAC + output, the DAC FIFO read pointer value (`fifo_offset`) must be + fixed. If `tune_fifo_offset=True` (the default) a value with maximum + margin is determined automatically by `dac_tune_fifo_offset` each time + `init()` is called. This value should be used for the `fifo_offset` key + of the `dac` settings of Phaser in `device_db.py` and automatic + tuning should be disabled by `tune_fifo_offset=False`. + + :param channel: Base RTIO channel number + :param core_device: Core device name (default: "core") + :param miso_delay: Fastlink MISO signal delay to account for cable + 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 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 + dictionary. + :param trf1: Channel 1 TRF372017 quadrature upconverter settings as a + dictionary. + + Attributes: + + * :attr:`channel`: List of two :class:`PhaserChannel` + To access oscillators, digital upconverters, PLL/VCO analog + quadrature upconverters and attenuators. + """ + kernel_invariants = {"core", "channel_base", "t_frame", "miso_delay", + "dac_mmap"} + + 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, + core_device="core"): + self.channel_base = channel_base + self.core = dmgr.get(core_device) + # TODO: auto-align miso-delay in phy + self.miso_delay = miso_delay + # frame duration in mu (10 words, 8 clock cycles each 4 ns) + # self.core.seconds_to_mu(10*8*4*ns) # unfortunately this returns 319 + assert self.core.ref_period == 1*ns + self.t_frame = 10*8*4 + self.clk_sel = clk_sel + self.tune_fifo_offset = tune_fifo_offset + self.sync_dly = sync_dly + + self.dac_mmap = DAC34H84(dac).get_mmap() + + self.channel = [PhaserChannel(self, ch, trf) + for ch, trf in enumerate([trf0, trf1])] + + @kernel + def init(self, debug=False): + """Initialize the board. + + Verifies board and chip presence, resets components, performs + communication and configuration tests and establishes initial + conditions. + """ + board_id = self.read8(PHASER_ADDR_BOARD_ID) + if board_id != PHASER_BOARD_ID: + raise ValueError("invalid board id") + delay(.1*ms) # slack + + hw_rev = self.read8(PHASER_ADDR_HW_REV) + delay(.1*ms) # slack + is_baseband = hw_rev & PHASER_HW_REV_VARIANT + + gw_rev = self.read8(PHASER_ADDR_GW_REV) + 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 + + # 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_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 + + # crossing dac_clk (reference) edges with sync_dly + # changes the optimal fifo_offset by 4 + self.set_sync_dly(self.sync_dly) + + # 4 wire SPI, sif4_enable + self.dac_write(0x02, 0x0080) + if self.dac_read(0x7f) != 0x5409: + raise ValueError("DAC version readback invalid") + delay(.1*ms) + if self.dac_read(0x00) != 0x049c: + raise ValueError("DAC config0 reset readback invalid") + delay(.1*ms) + + t = self.get_dac_temperature() + 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) + + # pll_ndivsync_ena disable + config18 = self.dac_read(0x18) + delay(.1*ms) + self.dac_write(0x18, config18 & ~0x0800) + + patterns = [ + [0xf05a, 0x05af, 0x5af0, 0xaf05], # test channel/iq/byte/nibble + [0x7a7a, 0xb6b6, 0xeaea, 0x4545], # datasheet pattern a + [0x1a1a, 0x1616, 0xaaaa, 0xc6c6], # datasheet pattern b + ] + # A data delay of 2*50 ps heuristically and reproducibly matches + # FPGA+board+DAC skews. There is plenty of margin (>= 250 ps + # either side) and no need to tune at runtime. + # Parity provides another level of safety. + for i in range(len(patterns)): + delay(.5*ms) + errors = self.dac_iotest(patterns[i]) + if errors: + raise ValueError("DAC iotest failure") + + delay(2*ms) # let it settle + lvolt = self.dac_read(0x18) & 7 + 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) + self.core.break_realtime() + + # self.dac_write(0x20, 0x0000) # stop fifo sync + # alarm = self.get_sta() & 1 + # delay(.1*ms) + self.clear_dac_alarms() + 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) + if debug: + print(alarms) + self.core.break_realtime() + # ignore alarms + else: + raise ValueError("DAC alarm") + + # power up trfs, release att reset + self.set_cfg(clk_sel=self.clk_sel, dac_txena=0) + + for ch in range(2): + channel = self.channel[ch] + # test attenuator write and readback + channel.set_att_mu(0x5a) + if channel.get_att_mu() != 0x5a: + raise ValueError("attenuator test failed") + delay(.1*ms) + channel.set_att_mu(0x00) # minimum attenuation + + # test oscillators and DUC + for i in range(len(channel.oscillator)): + oscillator = channel.oscillator[i] + asf = 0 + if i == 0: + asf = 0x7fff + # 6pi/4 phase + oscillator.set_amplitude_phase_mu(asf=asf, pow=0xc000, clr=1) + delay(1*us) + # 3pi/4 + channel.set_duc_phase_mu(0x6000) + channel.set_duc_cfg(select=0, clr=1) + self.duc_stb() + delay(.1*ms) # settle link, pipeline and impulse response + data = channel.get_dac_data() + delay(.1*ms) + sqrt2 = 0x5a81 # 0x7fff/sqrt(2) + data_i = data & 0xffff + data_q = (data >> 16) & 0xffff + # allow ripple + if (data_i < sqrt2 - 30 or data_i > sqrt2 or + abs(data_i - data_q) > 2): + raise ValueError("DUC+oscillator phase/amplitude test failed") + + if is_baseband: + continue + + if channel.trf_read(0) & 0x7f != 0x68: + raise ValueError("TRF identification failed") + delay(.1*ms) + + delay(.2*ms) + for data in channel.trf_mmap: + channel.trf_write(data) + + 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: + raise ValueError("TRF R_SAT_ERR") + delay(.1*ms) + + # enable dac tx + self.set_cfg(clk_sel=self.clk_sel) + + @kernel + def write8(self, addr, data): + """Write data to FPGA register. + + :param addr: Address to write to (7 bit) + :param data: Data to write (8 bit) + """ + rtio_output((self.channel_base << 8) | (addr & 0x7f) | 0x80, data) + delay_mu(int64(self.t_frame)) + + @kernel + def read8(self, addr) -> TInt32: + """Read from FPGA register. + + :param addr: Address to read from (7 bit) + :return: Data read (8 bit) + """ + rtio_output((self.channel_base << 8) | (addr & 0x7f), 0) + response = rtio_input_data(self.channel_base) + return response >> self.miso_delay + + @kernel + def write32(self, addr, data: TInt32): + """Write 32 bit to a sequence of FPGA registers.""" + for offset in range(4): + byte = data >> 24 + self.write8(addr + offset, byte) + data <<= 8 + + @kernel + def read32(self, addr) -> TInt32: + """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 + return data + + @kernel + def set_leds(self, leds): + """Set the front panel LEDs. + + :param leds: LED settings (6 bit) + """ + self.write8(PHASER_ADDR_LED, leds) + + @kernel + def set_fan_mu(self, pwm): + """Set the fan duty cycle. + + :param pwm: Duty cycle in machine units (8 bit) + """ + self.write8(PHASER_ADDR_FAN, pwm) + + @kernel + def set_fan(self, duty): + """Set the fan duty cycle. + + :param duty: Duty cycle (0. to 1.) + """ + pwm = int32(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): + """Set the configuration register. + + Each flag is a single bit (0 or 1). + + :param clk_sel: Select the external SMA clock input + :param dac_resetb: Active low DAC reset pin + :param dac_sleep: DAC sleep pin + :param dac_txena: Enable DAC transmission pin + :param trf0_ps: Quadrature upconverter 0 power save + :param trf1_ps: Quadrature upconverter 1 power save + :param att0_rstn: Active low attenuator 0 reset + :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)) + + @kernel + def get_sta(self): + """Get the status register value. + + Bit flags are: + + * :const:`PHASER_STA_DAC_ALARM`: DAC alarm pin + * :const:`PHASER_STA_TRF0_LD`: Quadrature upconverter 0 lock detect + * :const:`PHASER_STA_TRF1_LD`: Quadrature upconverter 1 lock detect + * :const:`PHASER_STA_TERM0`: ADC channel 0 termination indicator + * :const:`PHASER_STA_TERM1`: ADC channel 1 termination indicator + * :const:`PHASER_STA_SPI_IDLE`: SPI machine is idle and data registers + can be read/written + + :return: Status register + """ + return self.read8(PHASER_ADDR_STA) + + @kernel + def get_crc_err(self): + """Get the frame CRC error counter. + + :return: The number of frames with CRC mismatches sind the reset of the + device. Overflows at 256. + """ + return self.read8(PHASER_ADDR_CRC_ERR) + + @kernel + def set_sync_dly(self, dly): + """Set SYNC delay. + + :param dly: DAC SYNC delay setting (0 to 7) + """ + if dly < 0 or dly > 7: + raise ValueError("SYNC delay out of bounds") + self.write8(PHASER_ADDR_SYNC_DLY, dly) + + @kernel + def duc_stb(self): + """Strobe the DUC configuration register update. + + Transfer staging to active registers. + This affects both DUC channels. + """ + 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): + """Set the SPI machine configuration + + :param select: Chip selects to assert (DAC, TRF0, TRF1, ATT0, ATT1) + :param div: SPI clock divider relative to 250 MHz fabric clock + :param end: Whether to end the SPI transaction and deassert chip select + :param clk_phase: SPI clock phase (sample on first or second edge) + :param clk_polarity: SPI clock polarity (idle low or high) + :param half_duplex: Read MISO data from MOSI wire + :param lsb_first: Transfer the least significant bit first + :param offline: Put the SPI interfaces offline and don't drive voltages + :param length: SPI transfer length (1 to 8 bits) + """ + if div < 2 or div > 257: + raise ValueError("divider out of bounds") + if length < 1 or length > 8: + raise ValueError("length out of bounds") + 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)) + + @kernel + def spi_write(self, data): + """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): + """Read from the SPI input data register.""" + return self.read8(PHASER_ADDR_SPI_DATR) + + @kernel + def dac_write(self, addr, data): + """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) + 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_write(data) + delay_mu(t_xfer) + + @kernel + def dac_read(self, addr, div=34) -> TInt32: + """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) + 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.spi_write(0) + delay_mu(t_xfer) + data |= self.spi_read() + return data + + @kernel + def get_dac_temperature(self) -> TInt32: + """Read the DAC die temperature. + + :return: DAC temperature in degree Celsius + """ + return self.dac_read(0x06, div=257) >> 8 + + @kernel + def get_dac_alarms(self): + """Read the DAC alarm flags. + + :return: DAC alarm flags (see datasheet for bit meaning) + """ + return self.dac_read(0x05) + + @kernel + def clear_dac_alarms(self): + """Clear DAC alarm flags.""" + self.dac_write(0x05, 0x0000) + + @kernel + def dac_iotest(self, pattern) -> TInt32: + """Performs a DAC IO test according to the datasheet. + + :param patterm: List of four int32 containing the pattern + :return: Bit error mask (16 bits) + """ + if len(pattern) != 4: + raise ValueError("pattern length out of bounds") + for addr in range(len(pattern)): + self.dac_write(0x25 + addr, pattern[addr]) + # repeat the pattern twice + self.dac_write(0x29 + addr, pattern[addr]) + delay(.1*ms) + for ch in range(2): + channel = self.channel[ch] + channel.set_duc_cfg(select=1) # test + # dac test data is i msb, q lsb + data = pattern[2*ch] | (pattern[2*ch + 1] << 16) + channel.set_dac_test(data) + if channel.get_dac_data() != data: + raise ValueError("DAC test data readback failed") + delay(.1*ms) + cfg = self.dac_read(0x01) + 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 + # 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 + errors = self.dac_read(0x04) + delay(.1*ms) # slack + else: + errors = 0 + self.dac_write(0x01, cfg) # clear config + self.dac_write(0x04, 0x0000) # clear iotest_result + return errors + + @kernel + def dac_tune_fifo_offset(self): + """Scan through `fifo_offset` and configure midpoint setting. + + :return: Optimal `fifo_offset` setting with maximum margin to write + pointer. + """ + # expect two or three error free offsets: + # + # read offset 01234567 + # write pointer w + # distance 32101234 + # error free x xx + config9 = self.dac_read(0x09) + 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 + alarms = self.get_dac_alarms() + delay(.1*ms) # slack + if (alarms >> 11) & 0x7 == 0: # any fifo alarm + good |= 1 << o + # if there are good offsets accross the wrap around + # offset for computations + if good & 0x81 == 0x81: + good = ((good << 4) & 0xf0) | (good >> 4) + offset = 4 + else: + offset = 0 + # calculate mean + sum = 0 + count = 0 + for o in range(8): + if good & (1 << o): + sum += o + count += 1 + best = ((sum // count) + offset) % 8 + self.dac_write(0x09, (config9 & 0x1fff) | (best << 13)) + return best + + +class PhaserChannel: + """Phaser channel IQ pair. + + A Phaser channel contains: + + * multiple oscillators (in the coredevice phy), + * an interpolation chain and digital upconverter (DUC) on Phaser, + * several channel-specific settings in the DAC: + * quadrature modulation compensation QMC + * numerically controlled oscillator NCO or coarse mixer CMIX, + * the analog quadrature upconverter (in the Phaser-Upconverter hardware + variant), and + * a digitally controlled step attenuator. + + Attributes: + + * :attr:`oscillator`: List of five :class:`PhaserOscillator`. + + .. note:: The amplitude sum of the oscillators must be less than one to + avoid clipping or overflow. If any of the DDS or DUC frequencies are + non-zero, it is not sufficient to ensure that the sum in each + quadrature is within range. + + .. note:: The interpolation filter on Phaser has an intrinsic sinc-like + overshoot in its step response. That overshoot is an direct consequence + of its near-brick-wall frequency response. For large and wide-band + changes in oscillator parameters, the overshoot can lead to clipping + 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"} + + def __init__(self, phaser, index, trf): + self.phaser = phaser + self.index = index + self.trf_mmap = TRF372017(trf).get_mmap() + self.oscillator = [PhaserOscillator(self, osc) for osc in range(5)] + + @kernel + def get_dac_data(self) -> TInt32: + """Get a sample of the current DAC data. + + The data is split accross multiple registers and thus the data + is only valid if constant. + + :return: DAC data as 32 bit IQ. I/DACA/DACC in the 16 LSB, + Q/DACB/DACD in the 16 MSB + """ + return self.phaser.read32(PHASER_ADDR_DAC0_DATA + (self.index << 4)) + + @kernel + def set_dac_test(self, data: TInt32): + """Set the DAC test data. + + :param data: 32 bit IQ test data, I/DACA/DACC in the 16 LSB, + Q/DACB/DACD in the 16 MSB + """ + self.phaser.write32(PHASER_ADDR_DAC0_TEST + (self.index << 4), data) + + @kernel + def set_duc_cfg(self, clr=0, clr_once=0, select=0): + """Set the digital upconverter (DUC) and interpolator configuration. + + :param clr: Keep the phase accumulator cleared (persistent) + :param clr_once: Clear the phase accumulator for one cycle + :param select: Select the data to send to the DAC (0: DUC data, 1: test + data, other values: reserved) + """ + self.phaser.write8(PHASER_ADDR_DUC0_CFG + (self.index << 4), + ((clr & 1) << 0) | ((clr_once & 1) << 1) | + ((select & 3) << 2)) + + @kernel + def set_duc_frequency_mu(self, ftw): + """Set the DUC frequency. + + :param ftw: DUC frequency tuning word (32 bit) + """ + self.phaser.write32(PHASER_ADDR_DUC0_F + (self.index << 4), ftw) + + @kernel + def set_duc_frequency(self, frequency): + """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)))) + self.set_duc_frequency_mu(ftw) + + @kernel + def set_duc_phase_mu(self, pow): + """Set the DUC phase offset. + + :param pow: DUC phase offset word (16 bit) + """ + addr = PHASER_ADDR_DUC0_P + (self.index << 4) + self.phaser.write8(addr, pow >> 8) + self.phaser.write8(addr + 1, pow) + + @kernel + def set_duc_phase(self, phase): + """Set the DUC phase in SI units. + + :param phase: DUC phase in turns + """ + pow = int32(round(phase*(1 << 16))) + self.set_duc_phase_mu(pow) + + @kernel + def set_nco_frequency_mu(self, ftw): + """Set the NCO frequency. + + :param ftw: NCO frequency tuning word (32 bit) + """ + self.phaser.dac_write(0x15 + (self.index << 1), ftw >> 16) + self.phaser.dac_write(0x14 + (self.index << 1), ftw) + + @kernel + def set_nco_frequency(self, frequency): + """Set the NCO frequency in SI units. + + :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)))) + self.set_nco_frequency_mu(ftw) + + @kernel + def set_nco_phase_mu(self, pow): + """Set the NCO phase offset. + + :param pow: NCO phase offset word (16 bit) + """ + self.phaser.dac_write(0x12 + self.index, pow) + + @kernel + def set_nco_phase(self, phase): + """Set the NCO phase in SI units. + + :param phase: NCO phase in turns + """ + pow = int32(round(phase*(1 << 16))) + self.set_duc_phase_mu(pow) + + @kernel + def set_att_mu(self, data): + """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) + self.phaser.spi_cfg(select=PHASER_SEL_ATT0 << self.index, div=div, + end=1) + self.phaser.spi_write(data) + delay_mu(t_xfer) + + @kernel + def set_att(self, att): + """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)) + if data < 0 or data > 0xff: + raise ValueError("attenuation out of bounds") + self.set_att_mu(data) + + @kernel + def get_att_mu(self) -> TInt32: + """Read current attenuation. + + The current attenuation value is read without side effects. + + :return: Current attenuation in machine units + """ + div = 34 + t_xfer = self.phaser.core.seconds_to_mu((8 + 1)*div*4*ns) + self.phaser.spi_cfg(select=PHASER_SEL_ATT0 << self.index, div=div, + end=0) + self.phaser.spi_write(0) + delay_mu(t_xfer) + data = self.phaser.spi_read() + delay(20*us) # slack + self.phaser.spi_cfg(select=PHASER_SEL_ATT0 << self.index, div=div, + end=1) + self.phaser.spi_write(data) + delay_mu(t_xfer) + return data + + @kernel + def trf_write(self, data, readback=False): + """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) + read = 0 + end = 0 + clk_phase = 0 + if readback: + clk_phase = 1 + for i in range(4): + if i == 0 or i == 3: + if i == 3: + end = 1 + self.phaser.spi_cfg(select=PHASER_SEL_TRF0 << self.index, + div=div, lsb_first=1, clk_phase=clk_phase, + end=end) + self.phaser.spi_write(data) + data >>= 8 + delay_mu(t_xfer) + if readback: + read >>= 8 + read |= self.phaser.spi_read() << 24 + delay(20*us) # slack + return read + + @kernel + def trf_read(self, addr, cnt_mux_sel=0) -> TInt32: + """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)) + # single clk pulse with ~LE to start readback + self.phaser.spi_cfg(select=0, div=34, end=1, length=1) + self.phaser.spi_write(0) + delay((1 + 1)*34*4*ns) + return self.trf_write(0x00000008 | (cnt_mux_sel << 27), + readback=True) + + +class PhaserOscillator: + """Phaser IQ channel oscillator (NCO/DDS). + + .. note:: Latencies between oscillators within a channel and between + oscillator paramters (amplitude and phase/frequency) are deterministic + (with respect to the 25 MS/s sample clock) but not matched. + """ + kernel_invariants = {"channel", "base_addr"} + + def __init__(self, channel, index): + 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): + """Set Phaser MultiDDS frequency tuning word. + + :param ftw: Frequency tuning word (32 bit) + """ + rtio_output(self.base_addr, ftw) + + @kernel + def set_frequency(self, frequency): + """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)))) + self.set_frequency_mu(ftw) + + @kernel + def set_amplitude_phase_mu(self, asf=0x7fff, pow=0, clr=0): + """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) + rtio_output(self.base_addr | (1 << 8), data) + + @kernel + def set_amplitude_phase(self, amplitude, phase=0., clr=0): + """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)) + if asf < 0 or asf > 0x7fff: + raise ValueError("amplitude out of bounds") + pow = int32(round(phase*(1 << 16))) + self.set_amplitude_phase_mu(asf, pow, clr) diff --git a/artiq/coredevice/trf372017.py b/artiq/coredevice/trf372017.py new file mode 100644 index 000000000..40957db86 --- /dev/null +++ b/artiq/coredevice/trf372017.py @@ -0,0 +1,133 @@ +class TRF372017: + """TRF372017 settings and register map. + + For possible values, documentation, and explanation, see the datasheet. + https://www.ti.com/lit/gpn/trf372017 + """ + rdiv = 21 # 13b + ref_inv = 0 + neg_vco = 1 + icp = 0 # 1.94 mA, 5b + icp_double = 0 + cal_clk_sel = 12 # /16, 4b + + ndiv = 420 # 16b + pll_div_sel = 0 # /1, 2b + prsc_sel = 1 # 8/9 + vco_sel = 2 # 2b + vcosel_mode = 0 + cal_acc = 0b00 # 2b + en_cal = 1 + + nfrac = 0 # 25b + + pwd_pll = 0 + pwd_cp = 0 + pwd_vco = 0 + pwd_vcomux = 0 + pwd_div124 = 0 + pwd_presc = 0 + pwd_out_buff = 1 + pwd_lo_div = 1 + pwd_tx_div = 0 + pwd_bb_vcm = 0 + pwd_dc_off = 0 + en_extvco = 0 + en_isource = 0 + ld_ana_prec = 0 # 2b + cp_tristate = 0 # 2b + speedup = 0 + ld_dig_prec = 1 + en_dith = 1 + mod_ord = 2 # 3rd order, 2b + dith_sel = 0 + del_sd_clk = 2 # 2b + en_frac = 0 + + vcobias_rtrim = 4 # 3b + pllbias_rtrim = 2 # 2b + vco_bias = 8 # 460 µA, 4b + vcobuf_bias = 2 # 2b + vcomux_bias = 3 # 2b + bufout_bias = 0 # 300 µA, 2b + vco_cal_ib = 0 # PTAT + vco_cal_ref = 2 # 1.04 V, 2b + vco_ampl_ctrl = 3 # 2b + vco_vb_ctrl = 0 # 1.2 V, 2b + en_ld_isource = 0 + + ioff = 0x80 # 8b + qoff = 0x80 # 8b + vref_sel = 4 # 0.85 V, 3b + tx_div_sel = 1 # div2, 2b + lo_div_sel = 3 # div8, 2b + tx_div_bias = 1 # 37.5 µA, 2b + lo_div_bias = 2 # 50 µA, 2b + + vco_trim = 0x20 # 6b + vco_test_mode = 0 + cal_bypass = 0 + mux_ctrl = 1 # lock detect, 3b + isource_sink = 0 + isource_trim = 4 # 3b + pd_tc = 0 # 2b + ib_vcm_sel = 0 # ptat + dcoffset_i = 2 # 150 µA, 2b + vco_bias_sel = 1 # spi + + def __init__(self, updates=None): + if updates is None: + return + for key, value in updates.items(): + if not hasattr(self, key): + raise KeyError("invalid setting", key) + setattr(self, key, value) + + def get_mmap(self): + mmap = [] + mmap.append( + 0x9 | + (self.rdiv << 5) | (self.ref_inv << 19) | (self.neg_vco << 20) | + (self.icp << 21) | (self.icp_double << 26) | + (self.cal_clk_sel << 27)) + mmap.append( + 0xa | + (self.ndiv << 5) | (self.pll_div_sel << 21) | (self.prsc_sel << 23) | + (self.vco_sel << 26) | (self.vcosel_mode << 28) | + (self.cal_acc << 29) | (self.en_cal << 31)) + mmap.append(0xb | (self.nfrac << 5)) + mmap.append( + 0xc | + (self.pwd_pll << 5) | (self.pwd_cp << 6) | (self.pwd_vco << 7) | + (self.pwd_vcomux << 8) | (self.pwd_div124 << 9) | + (self.pwd_presc << 10) | (self.pwd_out_buff << 12) | + (self.pwd_lo_div << 13) | (self.pwd_tx_div << 14) | + (self.pwd_bb_vcm << 15) | (self.pwd_dc_off << 16) | + (self.en_extvco << 17) | (self.en_isource << 18) | + (self.ld_ana_prec << 19) | (self.cp_tristate << 21) | + (self.speedup << 23) | (self.ld_dig_prec << 24) | + (self.en_dith << 25) | (self.mod_ord << 26) | + (self.dith_sel << 28) | (self.del_sd_clk << 29) | + (self.en_frac << 31)) + mmap.append( + 0xd | + (self.vcobias_rtrim << 5) | (self.pllbias_rtrim << 8) | + (self.vco_bias << 10) | (self.vcobuf_bias << 14) | + (self.vcomux_bias << 16) | (self.bufout_bias << 18) | + (1 << 21) | (self.vco_cal_ib << 22) | (self.vco_cal_ref << 23) | + (self.vco_ampl_ctrl << 26) | (self.vco_vb_ctrl << 28) | + (self.en_ld_isource << 31)) + mmap.append( + 0xe | + (self.ioff << 5) | (self.qoff << 13) | (self.vref_sel << 21) | + (self.tx_div_sel << 24) | (self.lo_div_sel << 26) | + (self.tx_div_bias << 28) | (self.lo_div_bias << 30)) + mmap.append( + 0xf | + (self.vco_trim << 7) | (self.vco_test_mode << 14) | + (self.cal_bypass << 15) | (self.mux_ctrl << 16) | + (self.isource_sink << 19) | (self.isource_trim << 20) | + (self.pd_tc << 23) | (self.ib_vcm_sel << 25) | + (1 << 28) | (self.dcoffset_i << 29) | + (self.vco_bias_sel << 31)) + return mmap diff --git a/artiq/frontend/artiq_ddb_template.py b/artiq/frontend/artiq_ddb_template.py index 052673e03..3f208c4a7 100755 --- a/artiq/frontend/artiq_ddb_template.py +++ b/artiq/frontend/artiq_ddb_template.py @@ -485,6 +485,21 @@ class PeripheralManager: channel=rtio_offset) return 1 + def process_phaser(self, rtio_offset, peripheral): + self.gen(""" + device_db["{name}"] = {{ + "type": "local", + "module": "artiq.coredevice.phaser", + "class": "Phaser", + "arguments": {{ + "channel_base": 0x{channel:06x}, + "miso_delay": 1, + }} + }}""", + name=self.get_name("phaser"), + channel=rtio_offset) + return 2 + def process(self, rtio_offset, peripheral): processor = getattr(self, "process_"+str(peripheral["type"])) return processor(rtio_offset, peripheral) diff --git a/artiq/frontend/artiq_sinara_tester.py b/artiq/frontend/artiq_sinara_tester.py index 5aed406e4..48b095729 100755 --- a/artiq/frontend/artiq_sinara_tester.py +++ b/artiq/frontend/artiq_sinara_tester.py @@ -51,6 +51,7 @@ class SinaraTester(EnvExperiment): self.samplers = dict() self.zotinos = dict() self.fastinos = dict() + self.phasers = dict() self.grabbers = dict() ddb = self.get_device_db() @@ -77,6 +78,8 @@ class SinaraTester(EnvExperiment): self.zotinos[name] = self.get_device(name) elif (module, cls) == ("artiq.coredevice.fastino", "Fastino"): self.fastinos[name] = self.get_device(name) + elif (module, cls) == ("artiq.coredevice.phaser", "Phaser"): + self.phasers[name] = self.get_device(name) elif (module, cls) == ("artiq.coredevice.grabber", "Grabber"): self.grabbers[name] = self.get_device(name) @@ -111,6 +114,7 @@ class SinaraTester(EnvExperiment): self.samplers = sorted(self.samplers.items(), key=lambda x: x[1].cnv.channel) self.zotinos = sorted(self.zotinos.items(), key=lambda x: x[1].bus.channel) self.fastinos = sorted(self.fastinos.items(), key=lambda x: x[1].channel) + self.phasers = sorted(self.phasers.items(), key=lambda x: x[1].channel_base) self.grabbers = sorted(self.grabbers.items(), key=lambda x: x[1].channel_base) @kernel @@ -391,6 +395,57 @@ class SinaraTester(EnvExperiment): [card_dev for _, (__, card_dev) in enumerate(self.fastinos)] ) + @kernel + def set_phaser_frequencies(self, phaser, duc, osc): + self.core.break_realtime() + phaser.init() + delay(1*ms) + phaser.channel[0].set_duc_frequency(duc) + phaser.channel[0].set_duc_cfg() + phaser.channel[0].set_att(6*dB) + phaser.channel[1].set_duc_frequency(-duc) + phaser.channel[1].set_duc_cfg() + phaser.channel[1].set_att(6*dB) + phaser.duc_stb() + delay(1*ms) + for i in range(len(osc)): + phaser.channel[0].oscillator[i].set_frequency(osc[i]) + phaser.channel[0].oscillator[i].set_amplitude_phase(.2) + phaser.channel[1].oscillator[i].set_frequency(-osc[i]) + phaser.channel[1].oscillator[i].set_amplitude_phase(.2) + delay(1*ms) + + @kernel + def phaser_led_wave(self, phasers): + while not is_enter_pressed(): + self.core.break_realtime() + # do not fill the FIFOs too much to avoid long response times + t = now_mu() - self.core.seconds_to_mu(.2) + while self.core.get_rtio_counter_mu() < t: + pass + for phaser in phasers: + for i in range(6): + phaser.set_leds(1 << i) + delay(100*ms) + phaser.set_leds(0) + delay(100*ms) + + def test_phasers(self): + print("*** Testing Phaser DACs and 6 USER LEDs.") + print("Frequencies:") + for card_n, (card_name, card_dev) in enumerate(self.phasers): + duc = (card_n + 1)*10*MHz + osc = [i*1*MHz for i in range(5)] + print(card_name, + " ".join(["{:.0f}+{:.0f}".format(duc/MHz, f/MHz) for f in osc]), + "MHz") + self.set_phaser_frequencies(card_dev, duc, osc) + print("Press ENTER when done.") + # Test switching on/off USR_LEDs at the same time + self.phaser_led_wave( + [card_dev for _, (__, card_dev) in enumerate(self.phasers)] + ) + @kernel def grabber_capture(self, card_dev, rois): self.core.break_realtime() @@ -443,6 +498,8 @@ class SinaraTester(EnvExperiment): self.test_zotinos() if self.fastinos: self.test_fastinos() + if self.phasers: + self.test_phasers() if self.grabbers: self.test_grabbers() diff --git a/artiq/gateware/eem.py b/artiq/gateware/eem.py index 19fbe7ea8..627bc29e8 100644 --- a/artiq/gateware/eem.py +++ b/artiq/gateware/eem.py @@ -5,7 +5,7 @@ from migen.genlib.io import DifferentialOutput from artiq.gateware import rtio from artiq.gateware.rtio.phy import spi2, ad53xx_monitor, grabber from artiq.gateware.suservo import servo, pads as servo_pads -from artiq.gateware.rtio.phy import servo as rtservo, fastino +from artiq.gateware.rtio.phy import servo as rtservo, fastino, phaser def _eem_signal(i): @@ -613,7 +613,8 @@ class Fastino(_EEM): Subsignal("clk", Pins(_eem_pin(eem, 0, pol))), Subsignal("mosi", Pins(*(_eem_pin(eem, i, pol) for i in range(1, 7)))), - Subsignal("miso", Pins(_eem_pin(eem, 7, pol))), + Subsignal("miso", Pins(_eem_pin(eem, 7, pol)), + Misc("DIFF_TERM=TRUE")), IOStandard(iostandard), ) for pol in "pn"] @@ -626,3 +627,33 @@ class Fastino(_EEM): log2_width=log2_width) target.submodules += phy target.rtio_channels.append(rtio.Channel.from_phy(phy, ififo_depth=4)) + + +class Phaser(_EEM): + @staticmethod + def io(eem, iostandard="LVDS_25"): + return [ + ("phaser{}_ser_{}".format(eem, pol), 0, + Subsignal("clk", Pins(_eem_pin(eem, 0, pol))), + Subsignal("mosi", Pins(*(_eem_pin(eem, i, pol) + for i in range(1, 7)))), + Subsignal("miso", Pins(_eem_pin(eem, 7, pol)), + Misc("DIFF_TERM=TRUE")), + IOStandard(iostandard), + ) for pol in "pn"] + + @classmethod + def add_std(cls, target, eem, iostandard="LVDS_25"): + cls.add_extension(target, eem, iostandard=iostandard) + + phy = phaser.Phaser( + target.platform.request("phaser{}_ser_p".format(eem)), + target.platform.request("phaser{}_ser_n".format(eem))) + target.submodules += phy + target.rtio_channels.extend([ + rtio.Channel.from_phy(phy, ififo_depth=4), + rtio.Channel.from_phy(phy.ch0.frequency), + rtio.Channel.from_phy(phy.ch0.phase_amplitude), + rtio.Channel.from_phy(phy.ch1.frequency), + rtio.Channel.from_phy(phy.ch1.phase_amplitude), + ]) diff --git a/artiq/gateware/rtio/phy/fastino.py b/artiq/gateware/rtio/phy/fastino.py index 14d9d9cf1..ba8a7b5c9 100644 --- a/artiq/gateware/rtio/phy/fastino.py +++ b/artiq/gateware/rtio/phy/fastino.py @@ -4,132 +4,7 @@ from migen.genlib.io import DifferentialOutput, DifferentialInput, DDROutput from misoc.cores.liteeth_mini.mac.crc import LiteEthMACCRCEngine from artiq.gateware.rtio import rtlink - - -class SerDes(Module): - def transpose(self, i, n): - # i is n,m c-contiguous - # o is m,n c-contiguous - m = len(i)//n - assert n*m == len(i) - - def __init__(self, pins, pins_n): - n_bits = 16 # bits per dac data word - n_channels = 32 # channels per fastino - n_div = 7 # bits per lane and word - assert n_div == 7 - n_frame = 14 # word per frame - n_lanes = len(pins.mosi) # number of data lanes - n_checksum = 12 # checksum bits - n_addr = 4 # readback address bits - n_word = n_lanes*n_div - n_body = n_word*n_frame - (n_frame//2 + 1) - n_checksum - - # dac data words - self.dacs = [Signal(n_bits) for i in range(n_channels)] - # dac update enable - self.enable = Signal(n_channels) - # configuration word - self.cfg = Signal(20) - # readback data - self.dat_r = Signal(n_frame//2*(1 << n_addr)) - # data load synchronization event - self.stb = Signal() - - # # # - - # crc-12 telco - self.submodules.crc = LiteEthMACCRCEngine( - data_width=2*n_lanes, width=n_checksum, polynom=0x80f) - - addr = Signal(4) - body_ = Cat(self.cfg, addr, self.enable, self.dacs) - assert len(body_) == n_body - body = Signal(n_body) - self.comb += body.eq(body_) - - words_ = [] - j = 0 - for i in range(n_frame): # iterate over words - if i == 0: # data and checksum - k = n_word - n_checksum - elif i == 1: # marker - words_.append(C(1)) - k = n_word - 1 - elif i < n_frame//2 + 2: # marker - words_.append(C(0)) - k = n_word - 1 - else: # full word - k = n_word - # append corresponding frame body bits - words_.append(body[j:j + k]) - j += k - words_ = Cat(words_) - assert len(words_) == n_frame*n_word - n_checksum - words = Signal(len(words_)) - self.comb += words.eq(words_) - - clk = Signal(n_div, reset=0b1100011) - clk_stb = Signal() - i_frame = Signal(max=n_div*n_frame//2) # DDR - frame_stb = Signal() - sr = [Signal(n_frame*n_div - n_checksum//n_lanes, reset_less=True) - for i in range(n_lanes)] - assert len(Cat(sr)) == len(words) - # DDR bits for each register - ddr_data = Cat([sri[-2] for sri in sr], [sri[-1] for sri in sr]) - self.comb += [ - # assert one cycle ahead - clk_stb.eq(~clk[0] & clk[-1]), - # double period because of DDR - frame_stb.eq(i_frame == n_div*n_frame//2 - 1), - - # LiteETHMACCRCEngine takes data LSB first - self.crc.data[::-1].eq(ddr_data), - self.stb.eq(frame_stb & clk_stb), - ] - miso = Signal() - miso_sr = Signal(n_frame, reset_less=True) - self.sync.rio_phy += [ - # shift 7 bit clock pattern by two bits each DDR cycle - clk.eq(Cat(clk[-2:], clk)), - [sri[2:].eq(sri) for sri in sr], - self.crc.last.eq(self.crc.next), - If(clk[:2] == 0, # TODO: tweak MISO sampling - miso_sr.eq(Cat(miso, miso_sr)), - ), - If(~frame_stb, - i_frame.eq(i_frame + 1), - ), - If(frame_stb & clk_stb, - i_frame.eq(0), - self.crc.last.eq(0), - # transpose, load - Cat(sr).eq(Cat(words[mm::n_lanes] for mm in range(n_lanes))), - Array([self.dat_r[i*n_frame//2:(i + 1)*n_frame//2] - for i in range(1 << len(addr))])[addr].eq(miso_sr), - addr.eq(addr + 1), - ), - If(i_frame == n_div*n_frame//2 - 2, - # inject crc - ddr_data.eq(self.crc.next), - ), - ] - - clk_ddr = Signal() - miso0 = Signal() - self.specials += [ - DDROutput(clk[-1], clk[-2], clk_ddr, ClockSignal("rio_phy")), - DifferentialOutput(clk_ddr, pins.clk, pins_n.clk), - DifferentialInput(pins.miso, pins_n.miso, miso0), - MultiReg(miso0, miso, "rio_phy"), - ] - for sri, ddr, mp, mn in zip( - sr, Signal(n_lanes), pins.mosi, pins_n.mosi): - self.specials += [ - DDROutput(sri[-1], sri[-2], ddr, ClockSignal("rio_phy")), - DifferentialOutput(ddr, mp, mn), - ] +from .fastlink import SerDes, SerInterface class Fastino(Module): @@ -139,9 +14,31 @@ class Fastino(Module): rtlink.OInterface(data_width=max(16*width, 32), address_width=8, enable_replace=False), - rtlink.IInterface(data_width=32)) + rtlink.IInterface(data_width=14)) - self.submodules.serializer = SerDes(pins, pins_n) + self.submodules.serializer = SerDes( + n_data=8, t_clk=7, d_clk=0b1100011, + n_frame=14, n_crc=12, poly=0x80f) + self.submodules.intf = SerInterface(pins, pins_n) + self.comb += [ + Cat(self.intf.data[:-1]).eq(Cat(self.serializer.data[:-1])), + self.serializer.data[-1].eq(self.intf.data[-1]), + ] + + # dac data words + dacs = [Signal(16) for i in range(32)] + header = Record([ + ("cfg", 4), + ("leds", 8), + ("reserved", 8), + ("addr", 4), + ("enable", len(dacs)), + ]) + body = Cat(header.raw_bits(), dacs) + assert len(body) == len(self.serializer.payload) + self.comb += self.serializer.payload.eq(body) + + # # # # Support staging DAC data (in `dacs`) by writing to the # DAC RTIO addresses, if a channel is not "held" by its @@ -164,37 +61,36 @@ class Fastino(Module): # LSBs of the RTIO address for a DAC channel write must be zero and the # address space is sparse. - hold = Signal.like(self.serializer.enable) + hold = Signal.like(header.enable) - # TODO: stb, timestamp - read_regs = Array([ - self.serializer.dat_r[i*7:(i + 1)*7] - for i in range(1 << 4) - ]) + read_regs = Array([Signal.like(self.serializer.readback) + for _ in range(1 << len(header.addr))]) cases = { # update - 0x20: self.serializer.enable.eq(self.serializer.enable | self.rtlink.o.data), + 0x20: header.enable.eq(header.enable | self.rtlink.o.data), # hold 0x21: hold.eq(self.rtlink.o.data), # cfg - 0x22: self.serializer.cfg[:4].eq(self.rtlink.o.data), + 0x22: header.cfg.eq(self.rtlink.o.data), # leds - 0x23: self.serializer.cfg[4:12].eq(self.rtlink.o.data), + 0x23: header.leds.eq(self.rtlink.o.data), # reserved - 0x24: self.serializer.cfg[12:].eq(self.rtlink.o.data), + 0x24: header.reserved.eq(self.rtlink.o.data), } - for i in range(0, len(self.serializer.dacs), width): + for i in range(0, len(dacs), width): cases[i] = [ - Cat(self.serializer.dacs[i:i + width]).eq(self.rtlink.o.data), + Cat(dacs[i:i + width]).eq(self.rtlink.o.data), [If(~hold[i + j], - self.serializer.enable[i + j].eq(1), + header.enable[i + j].eq(1), ) for j in range(width)] ] self.sync.rio_phy += [ If(self.serializer.stb, - self.serializer.enable.eq(0), + header.enable.eq(0), + read_regs[header.addr].eq(self.serializer.readback), + header.addr.eq(header.addr + 1), ), If(self.rtlink.o.stb & ~self.rtlink.o.address[-1], Case(self.rtlink.o.address[:-1], cases), diff --git a/artiq/gateware/rtio/phy/fastlink.py b/artiq/gateware/rtio/phy/fastlink.py new file mode 100644 index 000000000..1a9317f77 --- /dev/null +++ b/artiq/gateware/rtio/phy/fastlink.py @@ -0,0 +1,133 @@ +from migen import * +from migen.genlib.io import (DifferentialOutput, DifferentialInput, + DDROutput, DDRInput) +from misoc.cores.liteeth_mini.mac.crc import LiteEthMACCRCEngine + +from artiq.gateware.rtio import rtlink + + +class SerDes(Module): + # crc-12 telco: 0x80f + def __init__(self, n_data=8, t_clk=7, d_clk=0b1100011, + n_frame=14, n_crc=12, poly=0x80f): + """DDR fast link. + + * One word clock lane with `t_clk` period. + * Multiple data lanes at DDR speed. + * One return data lane at slower speed. + * n_frame//2 - 1 marker bits are used to provide framing. + + * `n_data` lanes + * `t_clk` bits per clk cycle with pattern `d_clk` + * `n_frame` words per frame + * `n_crc` CRC bits per frame for divisor poly `poly` + """ + # pins + self.data = [Signal(2, reset_less=True) for _ in range(n_data)] + n_mosi = n_data - 2 # mosi lanes + n_word = n_mosi*t_clk # bits per word + t_frame = t_clk*n_frame # frame duration + n_marker = n_frame//2 + 1 + n_body = n_word*n_frame - n_marker - n_crc + t_miso = 0 # miso sampling latency TODO + assert n_crc % n_mosi == 0 + + # frame data + self.payload = Signal(n_body) + # readback data + self.readback = Signal(n_frame, reset_less=True) + # data load synchronization event + self.stb = Signal() + + # # # + + self.submodules.crca = LiteEthMACCRCEngine( + data_width=n_mosi, width=n_crc, polynom=poly) + self.submodules.crcb = LiteEthMACCRCEngine( + data_width=n_mosi, width=n_crc, polynom=poly) + + words_ = [] + j = 0 + # build from LSB to MSB because MSB first + for i in range(n_frame): # iterate over words + if i == 0: # data and checksum + words_.append(C(0, n_crc)) + k = n_word - n_crc + elif i == 1: # marker + words_.append(C(1)) + k = n_word - 1 + elif i < n_frame//2 + 2: # marker + words_.append(C(0)) + k = n_word - 1 + else: # full word + k = n_word + # append corresponding frame body bits + words_.append(self.payload[j:j + k]) + j += k + words_ = Cat(words_) + assert len(words_) == n_frame*n_word + words = Signal(len(words_)) + self.comb += words.eq(words_) + + clk = Signal(t_clk, reset=d_clk) + i = Signal(max=t_frame//2) + # big shift register for mosi and + sr = [Signal(t_frame, reset_less=True) for i in range(n_mosi)] + assert len(Cat(sr)) == len(words) + crc_insert = Cat(([d[0] for d in self.data[1:-1]] + + [d[1] for d in self.data[1:-1]])[:n_crc]) + miso_sr = Signal(t_frame, reset_less=True) + miso_sr_next = Signal.like(miso_sr) + self.comb += [ + self.stb.eq(i == t_frame//2 - 1), + # LiteETHMACCRCEngine takes data LSB first + self.crca.data.eq(Cat([sri[-1] for sri in sr[::-1]])), + self.crcb.data.eq(Cat([sri[-2] for sri in sr[::-1]])), + self.crcb.last.eq(self.crca.next), + miso_sr_next.eq(Cat(self.data[-1], miso_sr)), + # unload miso + # TODO: align to marker + self.readback.eq(Cat([miso_sr_next[t_miso + i*t_clk] + for i in range(n_frame)])), + ] + self.sync.rio_phy += [ + # shift everything by two bits + [di.eq(sri[-2:]) for di, sri in zip(self.data, [clk] + sr)], + clk.eq(Cat(clk[-2:], clk)), + [sri.eq(Cat(C(0, 2), sri)) for sri in sr], + miso_sr.eq(miso_sr_next), + self.crca.last.eq(self.crcb.next), + i.eq(i + 1), + If(self.stb, + i.eq(0), + clk.eq(clk.reset), + self.crca.last.eq(0), + # transpose, load + [sri.eq(Cat(words[i::n_mosi])) for i, sri in enumerate(sr)], + # inject crc for the last cycle + crc_insert.eq(self.crca.next if n_crc // n_mosi <= 1 + else self.crca.last), + ), + ] + + +class SerInterface(Module): + def __init__(self, pins, pins_n): + self.data = [Signal(2) for _ in range(2 + len(pins.mosi))] + + for d, pp, pn in zip(self.data, + [pins.clk] + list(pins.mosi), + [pins_n.clk] + list(pins_n.mosi)): + ddr = Signal() + self.specials += [ + # d1 closer to q + DDROutput(d[1], d[0], ddr, ClockSignal("rio_phy")), + DifferentialOutput(ddr, pp, pn), + ] + ddr = Signal() + self.specials += [ + DifferentialInput(pins.miso, pins_n.miso, ddr), + # q1 closer to d + DDRInput(ddr, self.data[-1][0], self.data[-1][1], + ClockSignal("rio_phy")), + ] diff --git a/artiq/gateware/rtio/phy/phaser.py b/artiq/gateware/rtio/phy/phaser.py new file mode 100644 index 000000000..bb299ab0c --- /dev/null +++ b/artiq/gateware/rtio/phy/phaser.py @@ -0,0 +1,89 @@ +from migen import * +from misoc.cores.duc import MultiDDS + +from artiq.gateware.rtio import rtlink +from .fastlink import SerDes, SerInterface + + +class Phy(Module): + def __init__(self, regs): + self.rtlink = rtlink.Interface( + rtlink.OInterface(data_width=32, address_width=4, + enable_replace=True)) + self.sync.rtio += [ + If(self.rtlink.o.stb, + Array(regs)[self.rtlink.o.address].eq(self.rtlink.o.data) + ) + ] + + +class DDSChannel(Module): + def __init__(self, share_lut=None): + to_rio_phy = ClockDomainsRenamer("rio_phy") + self.submodules.dds = to_rio_phy(MultiDDS( + n=5, fwidth=32, xwidth=16, z=19, zl=10, share_lut=share_lut)) + self.submodules.frequency = Phy([i.f for i in self.dds.i]) + self.submodules.phase_amplitude = Phy( + [Cat(i.a, i.clr, i.p) for i in self.dds.i]) + + +class Phaser(Module): + def __init__(self, pins, pins_n): + self.rtlink = rtlink.Interface( + rtlink.OInterface(data_width=8, address_width=8, + enable_replace=False), + rtlink.IInterface(data_width=10)) + + # share a CosSinGen LUT between the two channels + self.submodules.ch0 = DDSChannel() + self.submodules.ch1 = DDSChannel(share_lut=self.ch0.dds.cs.lut) + n_channels = 2 + n_samples = 8 + n_bits = 14 + body = Signal(n_samples*n_channels*2*n_bits, reset_less=True) + self.sync.rio_phy += [ + If(self.ch0.dds.valid, # & self.ch1.dds.valid, + # recent:ch0:i as low order in body + Cat(body).eq(Cat(self.ch0.dds.o.i[2:], self.ch0.dds.o.q[2:], + self.ch1.dds.o.i[2:], self.ch1.dds.o.q[2:], + body)), + ), + ] + + self.submodules.serializer = SerDes( + n_data=8, t_clk=8, d_clk=0b00001111, + n_frame=10, n_crc=6, poly=0x2f) + self.submodules.intf = SerInterface(pins, pins_n) + self.comb += [ + Cat(self.intf.data[:-1]).eq(Cat(self.serializer.data[:-1])), + self.serializer.data[-1].eq(self.intf.data[-1]), + ] + + header = Record([ + ("we", 1), + ("addr", 7), + ("data", 8), + ("type", 4) + ]) + assert len(Cat(header.raw_bits(), body)) == \ + len(self.serializer.payload) + self.comb += self.serializer.payload.eq(Cat(header.raw_bits(), body)) + + re_dly = Signal(3) # stage, send, respond + self.sync.rtio += [ + header.type.eq(1), # body type is baseband data + If(self.serializer.stb, + self.ch0.dds.stb.eq(1), # synchronize + self.ch1.dds.stb.eq(1), # synchronize + header.we.eq(0), + re_dly.eq(re_dly[1:]), + ), + If(self.rtlink.o.stb, + re_dly[-1].eq(~self.rtlink.o.address[-1]), + header.we.eq(self.rtlink.o.address[-1]), + header.addr.eq(self.rtlink.o.address), + header.data.eq(self.rtlink.o.data), + ), + self.rtlink.i.stb.eq(re_dly[0] & self.serializer.stb), + self.rtlink.i.data.eq(self.serializer.readback), + ] diff --git a/artiq/gateware/targets/kasli_generic.py b/artiq/gateware/targets/kasli_generic.py index 2fcd299a1..577b93dad 100755 --- a/artiq/gateware/targets/kasli_generic.py +++ b/artiq/gateware/targets/kasli_generic.py @@ -113,6 +113,12 @@ def peripheral_fastino(module, peripheral): peripheral.get("log2_width", 0)) +def peripheral_phaser(module, peripheral): + if len(peripheral["ports"]) != 1: + raise ValueError("wrong number of ports") + eem.Phaser.add_std(module, peripheral["ports"][0]) + + peripheral_processors = { "dio": peripheral_dio, "urukul": peripheral_urukul, @@ -123,6 +129,7 @@ peripheral_processors = { "grabber": peripheral_grabber, "mirny": peripheral_mirny, "fastino": peripheral_fastino, + "phaser": peripheral_phaser, } diff --git a/artiq/gateware/test/rtio/test_fastlink.py b/artiq/gateware/test/rtio/test_fastlink.py new file mode 100644 index 000000000..df8840951 --- /dev/null +++ b/artiq/gateware/test/rtio/test_fastlink.py @@ -0,0 +1,84 @@ +import unittest + +from migen import * +from artiq.gateware.rtio.phy.fastlink import * + + + +class TestPhaser(unittest.TestCase): + def setUp(self): + self.dut = SerDes(n_data=8, t_clk=8, d_clk=0b00001111, + n_frame=10, n_crc=6, poly=0x2f) + + def test_init(self): + pass + + def record_frame(self, frame): + clk = 0 + marker = 0 + stb = 0 + while True: + if stb == 2: + frame.append((yield self.dut.data)) + clk = (clk << 2) & 0xff + clk |= (yield self.dut.data[0]) + if clk == 0x0f: + if marker == 0x01: + stb += 1 + if stb >= 3: + break + # 10/2 + 1 marker bits + marker = (marker << 1) & 0x3f + marker |= (yield self.dut.data[1]) & 1 + yield + + def test_frame(self): + frame = [] + self.dut.comb += self.dut.payload.eq((1 << len(self.dut.payload)) - 1) + run_simulation(self.dut, self.record_frame(frame), + clocks={n: 2 for n in ["sys", "rio", "rio_phy"]}) + self.assertEqual(len(frame), 8*10//2) + self.assertEqual([d[0] for d in frame], [0, 0, 3, 3] * 10) + self.assertEqual([d[1] & 1 for d in frame[4*4 - 1:10*4 - 1:4]], + [0, 0, 0, 0, 0, 1]) + + +class TestFastino(unittest.TestCase): + def setUp(self): + self.dut = SerDes( + n_data=8, t_clk=7, d_clk=0b1100011, + n_frame=14, n_crc=12, poly=0x80f) + + def test_init(self): + pass + + def record_frame(self, frame): + clk = 0 + marker = 0 + stb = 0 + while True: + if stb == 2: + frame.append((yield self.dut.data)) + clk = (clk << 2) & 0xff + clk |= (yield self.dut.data[0]) + if clk in (0b11100011, 0b11000111): + if marker == 0x01: + stb += 1 + if stb >= 3: + break + # 14/2 + 1 marker bits + marker = (marker << 1) & 0xff + if clk & 0b100: + marker |= (yield self.dut.data[1]) >> 1 + else: + marker |= (yield self.dut.data[1]) & 1 + yield + + def test_frame(self): + frame = [] + self.dut.comb += self.dut.payload.eq((1 << len(self.dut.payload)) - 1) + run_simulation(self.dut, self.record_frame(frame), + clocks={n: 2 for n in ["sys", "rio", "rio_phy"]}) + self.assertEqual(len(frame), 7*14//2) + self.assertEqual([d[0] for d in frame], [3, 0, 1, 3, 2, 0, 3] * 7) + self.assertEqual(frame[-1], [3, 3, 1, 1, 1, 2, 1, 0]) # crc12 diff --git a/artiq/test/coredevice/test_phaser.py b/artiq/test/coredevice/test_phaser.py new file mode 100644 index 000000000..18aac6a57 --- /dev/null +++ b/artiq/test/coredevice/test_phaser.py @@ -0,0 +1,34 @@ +import unittest +from artiq.experiment import * +from artiq.test.hardware_testbench import ExperimentCase +from artiq.language.core import kernel, delay +from artiq.language.units import us + + +class PhaserExperiment(EnvExperiment): + def build(self): + self.setattr_device("core") + self.setattr_device("phaser0") + + @kernel + def run(self): + self.core.reset() + # The Phaser initialization performs a comprehensive test: + # * Fastlink bringup + # * Fastlink error counter + # * Board identification + # * Hardware identification + # * SPI write, readback, timing + # * Temperature readout + # * DAC identification, IOTEST, alarm sweep, PLL configuration, FIFO + # alignmend + # * DUC+Oscillator configuration, data end-to-end verification and + # readback + # * Attenuator write and readback + # * TRF bringup PLL locking + self.phaser0.init() + + +class PhaserTest(ExperimentCase): + def test(self): + self.execute(PhaserExperiment) diff --git a/doc/manual/core_drivers_reference.rst b/doc/manual/core_drivers_reference.rst index b0026b6ac..baa1b59e4 100644 --- a/doc/manual/core_drivers_reference.rst +++ b/doc/manual/core_drivers_reference.rst @@ -130,6 +130,11 @@ RF generation drivers .. automodule:: artiq.coredevice.basemod_att :members: +:mod:`artiq.coredevice.phaser` module ++++++++++++++++++++++++++++++++++++++ + +.. automodule:: artiq.coredevice.phaser + :members: DAC/ADC drivers ---------------