from numpy import int32, int64

from artiq.language.core import kernel, delay_mu, delay
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


PHASER_BOARD_ID = 19

PHASER_GW_BASE = 1
PHASER_GW_MIQRO = 2

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

# servo registers
PHASER_ADDR_SERVO_CFG0 = 0x30
PHASER_ADDR_SERVO_CFG1 = 0x31

# 0x32 - 0x71 servo coefficients + offset data
PHASER_ADDR_SERVO_DATA_BASE = 0x32

# 0x72 - 0x78 Miqro channel profile/window memories
PHASER_ADDR_MIQRO_MEM_ADDR = 0x72
PHASER_ADDR_MIQRO_MEM_DATA = 0x74

# Miqro profile memory select
PHASER_MIQRO_SEL_PROFILE = 1 << 14

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

SERVO_COEFF_WIDTH = 16
SERVO_DATA_WIDTH = 16
SERVO_COEFF_SHIFT = 14
SERVO_T_CYCLE = (32+12+192+24+4)*ns  # Must match gateware ADC parameters


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 RTIO PHY and the Phaser gateware come in different modes
    that have different features. Phaser mode and coredevice PHY mode are both
    selected at their respective gateware compile-time and need to match.

    ===============  ==============  ===================================
    Phaser gateware  Coredevice PHY  Features per :class:`PhaserChannel`
    ===============  ==============  ===================================
    Base <= v0.5     Base            Base (5 :class:`PhaserOscillator`)
    Base >= v0.6     Base            Base + Servo
    Miqro >= v0.6    Miqro           :class:`Miqro`
    ===============  ==============  ===================================

    The coredevice driver (this class and :class:`PhaserChannel`) exposes
    the superset of all functionality regardless of the Coredevice RTIO PHY
    or Phaser gateware modes. This is to evade type unification limitations.
    Features absent in Coredevice PHY/Phaser gateware will not work and
    should not be accessed.

    **Base mode**

    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. If desired, these features my be
    configured via the `dac` dictionary.

    The latency/group delay from the RTIO events setting
    :class:`PhaserOscillator` or :class:`PhaserChannel` DUC parameters all the
    way to the DAC outputs is deterministic. This enables deterministic
    absolute phase with respect to other RTIO input and output events
    (see `get_next_frame_mu()`).

    **Miqro mode**

    See :class:`Miqro`

    Here the DAC operates in 4x interpolation.

    **Analog flow**

    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.

    **Servo**

    Each phaser output channel features a servo to control the RF output amplitude
    using feedback from an ADC. The servo consists of a first order IIR (infinite
    impulse response) filter fed by the ADC and a multiplier that scales the I
    and Q datastreams from the DUC by the IIR output. The IIR state is updated at
    the 3.788 MHz ADC sampling rate.

    Each channel IIR features 4 profiles, each consisting of the [b0, b1, a1] filter
    coefficients as well as an output offset. The coefficients and offset can be
    set for each profile individually and the profiles each have their own ``y0``,
    ``y1`` output registers (the ``x0``, ``x1`` inputs are shared). To avoid
    transient effects, care should be taken to not update the coefficents in the
    currently selected profile.

    The servo can be en- or disabled for each channel. When disabled, the servo
    output multiplier is simply bypassed and the datastream reaches the DAC unscaled.

    The IIR output can be put on hold for each channel. In hold mode, the filter
    still ingests samples and updates its input ``x0`` and ``x1`` registers, but
    does not update the ``y0``, ``y1`` output registers.

    After power-up the servo is disabled, in profile 0, with coefficients [0, 0, 0]
    and hold is enabled. If older gateware without ther servo is loaded onto the
    Phaser FPGA, the device simply behaves as if the servo is disabled and none of
    the servo functions have any effect.

    .. 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, gw_rev=PHASER_GW_BASE,
                 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.frame_tstamp = int64(0)
        self.clk_sel = clk_sel
        self.tune_fifo_offset = tune_fifo_offset
        self.sync_dly = sync_dly
        self.gw_rev = gw_rev  # verified in init()

        self.dac_mmap = DAC34H84(dac).get_mmap()

        self.channel = [PhaserChannel(self, ch, trf)
                        for ch, trf in enumerate([trf0, trf1])]

    @staticmethod
    def get_rtio_channels(channel_base, gw_rev=PHASER_GW_BASE, **kwargs):
        if gw_rev == PHASER_GW_MIQRO:
            return [(channel_base, "base"), (channel_base + 1, "ch0"), (channel_base + 2, "ch1")]
        elif gw_rev == PHASER_GW_BASE:
            return [(channel_base, "base"),
                    (channel_base + 1, "ch0 frequency"),
                    (channel_base + 2, "ch0 phase amplitude"),
                    (channel_base + 3, "ch1 frequency"),
                    (channel_base + 4, "ch1 phase amplitude")]
        raise ValueError("invalid gw_rev `{}`".format(gw_rev))

    @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)
        if debug:
            print("gw_rev:", self.gw_rev)
            self.core.break_realtime()
        assert gw_rev == self.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

        # determine the origin for frame-aligned timestamps
        self.measure_frame_timestamp()
        if self.frame_tstamp < 0:
            raise ValueError("frame timestamp measurement timed out")
        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_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(120*us)
        self.dac_sync()
        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:", 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:", alarms)
                self.core.break_realtime()
                # ignore alarms
            else:
                raise ValueError("DAC alarm")

        # 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.dac_sync()
        delay(100*us)
        self.dac_write(self.dac_mmap[2] >> 16, self.dac_mmap[2])
        delay(40*us)
        self.dac_sync()
        delay(100*us)

        # 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)  # maximum attenuation

            channel.set_servo(profile=0, enable=0, hold=1)

            if self.gw_rev == PHASER_GW_BASE:
                # 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*us)
                channel.oscillator[0].set_amplitude_phase_mu(asf=0, pow=0xc000,
                                                            clr=1)
                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 self.gw_rev == PHASER_GW_MIQRO:
                channel.miqro.reset()

            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)
            channel.cal_trf_vco()

            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)
            channel.en_trf_out()

        # 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 write16(self, addr, data: TInt32):
        """Write 16 bit to a sequence of FPGA registers."""
        self.write8(addr, data >> 8)
        self.write8(addr + 1, data)

    @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 measure_frame_timestamp(self):
        """Measure the timestamp of an arbitrary frame and store it in `self.frame_tstamp`.

        To be used as reference for aligning updates to the FastLink frames.
        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)

    @kernel
    def get_next_frame_mu(self):
        """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

    @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 dac_sync(self):
        """Trigger DAC synchronisation for both output channels.

        The DAC sif_sync is de-asserts, then asserted. The synchronisation is
        triggered on assertion.

        By default, the fine-mixer (NCO) and QMC are synchronised. This
        includes applying the latest register settings.

        The synchronisation sources may be configured through the `syncsel_x`
        fields in the `dac` configuration dictionary (see `__init__()`).

        .. note:: Synchronising the NCO clears the phase-accumulator
        """
        config1f = self.dac_read(0x1f)
        delay(.4*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):
        """Set the DAC coarse mixer frequency for both channels

        Use of the coarse mixer requires the DAC mixer to be enabled. The mixer
        can be configured via the `dac` configuration dictionary (see
        `__init__()`).

        The selected coarse mixer frequency becomes active without explicit
        synchronisation.

        :param fs_8_step: coarse mixer frequency shift in 125 MHz steps. This
            should be an integer between -3 and 4 (inclusive).
        """
        # values recommended in data-sheet
        #         0       1       2       3       4       -3      -2      -1
        vals = [0b0000, 0b1000, 0b0100, 0b1100, 0b0010, 0b1010, 0b0001, 0b1110]
        cmix = vals[fs_8_step%8]
        config0d = self.dac_read(0x0d)
        delay(.1*ms)
        self.dac_write(0x0d, (config0d & ~(0b1111 << 12)) | (cmix << 12))

    @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 pattern: 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
        if count == 0:
            raise ValueError("no good fifo offset")
        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 :class:`PhaserOscillator` (in the coredevice phy),
    * an interpolation chain and digital upconverter (DUC) on Phaser,
    * a :class:`Miqro` instance 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`.
    * :attr:`miqro`: A :class:`Miqro`.

    .. 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 a 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.
        Miqro is not affected by this. But both the oscillators and Miqro can
        be affected by intrinsic overshoot of the interpolator on the DAC.
    """
    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)]
        self.miqro = Miqro(self)

    @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.

        This method stages the new NCO frequency, but does not apply it.

        Use of the DAC-NCO requires the DAC mixer and NCO to be enabled. These
        can be configured via the `dac` configuration dictionary (see
        `__init__()`).

        :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.

        This method stages the new NCO frequency, but does not apply it.

        Use of the DAC-NCO requires the DAC mixer and NCO to be enabled. These
        can be configured via the `dac` configuration dictionary (see
        `__init__()`).

        :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.

        By default, the new NCO phase applies on completion of the SPI
        transfer. This also causes a staged NCO frequency to be applied.
        Different triggers for applying NCO settings may be configured through
        the `syncsel_mixerxx` fields in the `dac` configuration dictionary (see
        `__init__()`).

        Use of the DAC-NCO requires the DAC mixer and NCO to be enabled. These
        can be configured via the `dac` configuration dictionary (see
        `__init__()`).

        :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.

        By default, the new NCO phase applies on completion of the SPI
        transfer. This also causes a staged NCO frequency to be applied.
        Different triggers for applying NCO settings may be configured through
        the `syncsel_mixerxx` fields in the `dac` configuration dictionary (see
        `__init__()`).

        Use of the DAC-NCO requires the DAC mixer and NCO to be enabled. These
        can be configured via the `dac` configuration dictionary (see
        `__init__()`).

        :param phase: NCO phase in turns
        """
        pow = int32(round(phase*(1 << 16)))
        self.set_nco_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)

    @kernel
    def cal_trf_vco(self):
        """Start calibration of the upconverter (hardware variant) VCO.

        TRF outputs should be disabled during VCO calibration.
        """
        self.trf_write(self.trf_mmap[1] | (1 << 31))

    @kernel
    def en_trf_out(self, rf=1, lo=0):
        """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)
        # set RF and LO output bits
        data = data | (1 << 12) | (1 << 13) | (1 << 14)
        # clear to enable output
        if rf == 1:
            data = data ^ (1 << 14)
        if lo == 1:
            data = data ^ ((1 << 12) | (1 << 13))
        self.trf_write(data)

    @kernel
    def set_servo(self, profile=0, enable=0, hold=0):
        """Set the servo configuration.

        :param enable: 1 to enable servo, 0 to disable servo (default). If disabled,
            the servo is bypassed and hold is enforced since the control loop is broken.
        :param hold: 1 to hold the servo IIR filter output constant, 0 for normal operation.
        :param profile: Profile index to select for channel. (0 to 3)
        """
        if (profile < 0) or (profile > 3):
            raise ValueError("invalid profile index")
        addr = PHASER_ADDR_SERVO_CFG0 + self.index
        # enforce hold if the servo is disabled
        data = (profile << 2) | (((hold | ~enable) & 1) << 1) | (enable & 1)
        self.phaser.write8(addr, data)

    @kernel
    def set_iir_mu(self, profile, b0, b1, a1, offset):
        """Load a servo profile consiting of the three filter coefficients and an output offset.

        Avoid setting the IIR parameters of the currently active profile.

        The recurrence relation is (all data signed and MSB aligned):

        .. math::
            a_0 y_n = a_1 y_{n - 1} + b_0 x_n + b_1 x_{n - 1} + o

        Where:

            * :math:`y_n` and :math:`y_{n-1}` are the current and previous
              filter outputs, clipped to :math:`[0, 1[`.
            * :math:`x_n` and :math:`x_{n-1}` are the current and previous
              filter inputs in :math:`[-1, 1[`.
            * :math:`o` is the offset
            * :math:`a_0` is the normalization factor :math:`2^{14}`
            * :math:`a_1` is the feedback gain
            * :math:`b_0` and :math:`b_1` are the feedforward gains for the two
              delays

        .. seealso:: :meth:`set_iir`

        :param profile: Profile to set (0 to 3)
        :param b0: b0 filter coefficient (16 bit signed)
        :param b1: b1 filter coefficient (16 bit signed)
        :param a1: a1 filter coefficient (16 bit signed)
        :param offset: Output offset (16 bit signed)
        """
        if (profile < 0) or (profile > 3):
            raise ValueError("invalid profile index")
        # 32 byte-sized data registers per channel and 8 (2 bytes * (3 coefficients + 1 offset)) registers per profile
        addr = PHASER_ADDR_SERVO_DATA_BASE + (8 * profile) + (self.index * 32)
        for data in [b0, b1, a1, offset]:
            self.phaser.write16(addr, data)
            addr += 2

    @kernel
    def set_iir(self, profile, kp, ki=0., g=0., x_offset=0., y_offset=0.):
        """Set servo profile IIR coefficients.

        Avoid setting the IIR parameters of the currently active profile.

        Gains are given in units of output full per scale per input full scale.

        .. note:: Due to inherent constraints of the fixed point datatypes and IIR
            filters, the ``x_offset`` (setpoint) resolution depends on the selected
            gains. Low ``ki`` gains will lead to a low ``x_offset`` resolution.

        The transfer function is (up to time discretization and
        coefficient quantization errors):

        .. math::
            H(s) = k_p + \\frac{k_i}{s + \\frac{k_i}{g}}

        Where:
            * :math:`s = \\sigma + i\\omega` is the complex frequency
            * :math:`k_p` is the proportional gain
            * :math:`k_i` is the integrator gain
            * :math:`g` is the integrator gain limit

        :param profile: Profile number (0-3)
        :param kp: Proportional gain. This is usually negative (closed
            loop, positive ADC voltage, positive setpoint). When 0, this
            implements a pure I controller.
        :param ki: Integrator gain (rad/s). Equivalent to the gain at 1 Hz.
            When 0 (the default) this implements a pure P controller.
            Same sign as ``kp``.
        :param g: Integrator gain limit (1). When 0 (the default) the
            integrator gain limit is infinite. Same sign as ``ki``.
        :param x_offset: IIR input offset. Used as the negative
            setpoint when stabilizing to a desired input setpoint. Will
            be converted to an equivalent output offset and added to y_offset.
        :param y_offset: IIR output offset.
        """
        NORM = 1 << SERVO_COEFF_SHIFT
        COEFF_MAX = 1 << SERVO_COEFF_WIDTH - 1
        DATA_MAX = 1 << SERVO_DATA_WIDTH - 1

        kp *= NORM
        if ki == 0.:
            # pure P
            a1 = 0
            b1 = 0
            b0 = int(round(kp))
        else:
            # I or PI
            ki *= NORM*SERVO_T_CYCLE/2.
            if g == 0.:
                c = 1.
                a1 = NORM
            else:
                c = 1./(1. + ki/(g*NORM))
                a1 = int(round((2.*c - 1.)*NORM))
            b0 = int(round(kp + ki*c))
            b1 = int(round(kp + (ki - 2.*kp)*c))
            if b1 == -b0:
                raise ValueError("low integrator gain and/or gain limit")

        if (b0 >= COEFF_MAX or b0 < -COEFF_MAX or
                b1 >= COEFF_MAX or b1 < -COEFF_MAX):
            raise ValueError("high gains")

        forward_gain = (b0 + b1) * (1 << SERVO_DATA_WIDTH - 1 - SERVO_COEFF_SHIFT)
        effective_offset = int(round(DATA_MAX * y_offset + forward_gain * x_offset))

        self.set_iir_mu(profile, b0, b1, a1, effective_offset)



class PhaserOscillator:
    """Phaser IQ channel oscillator (NCO/DDS).

    .. note:: Latencies between oscillators within a channel and between
        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"}

    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)


class Miqro:
    """
    Miqro pulse generator.

    A Miqro instance represents one RF output. The DSP components are fully
    contained in the Phaser gateware. The output is generated by with
    the following data flow:

    **Oscillators**

    * There are n_osc = 16 oscillators with oscillator IDs 0..n_osc-1.
    * Each oscillator outputs one tone at any given time

        * I/Q (quadrature, a.k.a. complex) 2x16 bit signed data
          at tau = 4 ns sample intervals, 250 MS/s, Nyquist 125 MHz, bandwidth 200 MHz
          (from f = -100..+100 MHz, taking into account the interpolation anti-aliasing
          filters in subsequent interpolators),
        * 32 bit frequency (f) resolution (~ 1/16 Hz),
        * 16 bit unsigned amplitude (a) resolution
        * 16 bit phase offset (p) resolution

    * The output phase p' of each oscillator at time t (boot/reset/initialization of the
      device at t=0) is then p' = f*t + p (mod 1 turn) where f and p are the (currently
      active) profile frequency and phase offset.
    * Note: The terms  "phase coherent" and "phase tracking" are defined to refer to this
      choice of oscillator output phase p'. Note that the phase offset p is not relative to
      (on top of previous phase/profiles/oscillator history).
      It is "absolute" in the sense that frequency f and phase offset p fully determine
      oscillator output phase p' at time t. This is unlike typical DDS behavior.
    * Frequency, phase, and amplitude of each oscillator are configurable by selecting one of
      n_profile = 32 profiles 0..n_profile-1. This selection is fast and can be done for
      each pulse. The phase coherence defined above is guaranteed for each
      profile individually.
    * Note: one profile per oscillator (usually profile index 0) should be reserved
      for the NOP (no operation, identity) profile, usually with zero amplitude.
    * Data for each profile for each oscillator can be configured
      individually. Storing profile data should be considered "expensive".
    * Note: The annotation that some operation is "expensive" does not mean it is
      impossible, just that it may take a significant amount of time and
      resources to execute such that it may be impractical when used often or
      during fast pulse sequences. They are intended for use in calibration and
      initialization.

    **Summation**

    * The oscillator outputs are added together (wrapping addition).
    * The user must ensure that the sum of oscillators outputs does not exceed the
      data range. In general that means that the sum of the amplitudes must not
      exceed one.

    **Shaper**

    * The summed complex output stream is then multiplied with a the complex-valued
      output of a triggerable shaper.
    * Triggering the shaper corresponds to passing a pulse from all oscillators to
      the RF output.
    * Selected profiles become active simultaneously (on the same output sample) when
      triggering the shaper with the first shaper output sample.
    * The shaper reads (replays) window samples from a memory of size n_window = 1 << 10.
    * The window memory can be segmented by choosing different start indices
      to support different windows.
    * Each window memory segment starts with a header determining segment
      length and interpolation parameters.
    * The window samples are interpolated by a factor (rate change) between 1 and
      r = 1 << 12.
    * The interpolation order is constant, linear, quadratic, or cubic. This
      corresponds to interpolation modes from rectangular window (1st order CIC)
      or zero order hold) to Parzen window (4th order CIC or cubic spline).
    * This results in support for single shot pulse lengths (envelope support) between
      tau and a bit more than r * n_window * tau = (1 << 12 + 10) tau ~ 17 ms.
    * Windows can be configured to be head-less and/or tail-less, meaning, they
      do not feed zero-amplitude samples into the shaper before and after
      each window respectively. This is used to implement pulses with arbitrary
      length or CW output.

    **Overall properties**

    * The DAC may upconvert the signal by applying a frequency offset f1 with
      phase p1.
    * In the Upconverter Phaser variant, the analog quadrature upconverter
      applies another frequency of f2 and phase p2.
    * The resulting phase of the signal from one oscillator at the SMA output is
      (f + f1 + f2)*t + p + s(t - t0) + p1 + p2 (mod 1 turn)
      where s(t - t0) is the phase of the interpolated
      shaper output, and t0 is the trigger time (fiducial of the shaper).
      Unsurprisingly the frequency is the derivative of the phase.
    * Group delays between pulse parameter updates are matched across oscillators,
      shapers, and channels.
    * The minimum time to change profiles and phase offsets is ~128 ns (estimate, TBC).
      This is the minimum pulse interval.
      The sustained pulse rate of the RTIO PHY/Fastlink is one pulse per Fastlink frame
      (may be increased, TBC).
    """

    def __init__(self, channel):
        self.channel = channel
        self.base_addr = (self.channel.phaser.channel_base + 1 +
                self.channel.index) << 8

    @kernel
    def reset(self):
        """Establish no-output profiles and no-output window and execute them.

        This establishes the first profile (index 0) on all oscillators as zero
        amplitude, creates a trivial window (one sample with zero amplitude,
        minimal interpolation), and executes a corresponding pulse.
        """
        for osc in range(16):
            self.set_profile_mu(osc, profile=0, ftw=0, asf=0)
            delay(20*us)
        self.set_window_mu(start=0, iq=[0], order=0)
        self.pulse(window=0, profiles=[0])

    @kernel
    def set_profile_mu(self, oscillator, profile, ftw, asf, pow_=0):
        """Store an oscillator profile (machine units).

        :param oscillator: Oscillator index (0 to 15)
        :param profile: Profile index (0 to 31)
        :param ftw: Frequency tuning word (32 bit signed integer on a 250 MHz clock)
        :param asf: Amplitude scale factor (16 bit unsigned integer)
        :param pow_: Phase offset word (16 bit integer)
        """
        if oscillator >= 16:
            raise ValueError("invalid oscillator index")
        if profile >= 32:
            raise ValueError("invalid profile index")
        self.channel.phaser.write16(PHASER_ADDR_MIQRO_MEM_ADDR,
                (self.channel.index << 15) | PHASER_MIQRO_SEL_PROFILE |
                (oscillator << 6) | (profile << 1))
        self.channel.phaser.write32(PHASER_ADDR_MIQRO_MEM_DATA, ftw)
        self.channel.phaser.write32(PHASER_ADDR_MIQRO_MEM_DATA,
            (asf & 0xffff) | (pow_ << 16))

    @kernel
    def set_profile(self, oscillator, profile, frequency, amplitude, phase=0.):
        """Store an oscillator profile.

        :param oscillator: Oscillator index (0 to 15)
        :param profile: Profile index (0 to 31)
        :param frequency: Frequency in Hz (passband -100 to 100 MHz).
            Interpreted in the Nyquist sense, i.e. aliased.
        :param amplitude: Amplitude in units of full scale (0. to 1.)
        :param phase: Phase in turns. See :class:`Miqro` for a definition of
            phase in this context.
        :return: The quantized 32 bit frequency tuning word
        """
        ftw = int32(round(frequency*((1 << 30)/(62.5*MHz))))
        asf = int32(round(amplitude*0xffff))
        if asf < 0 or asf > 0xffff:
            raise ValueError("amplitude out of bounds")
        pow_ = int32(round(phase*(1 << 16)))
        self.set_profile_mu(oscillator, profile, ftw, asf, pow_)
        return ftw

    @kernel
    def set_window_mu(self, start, iq, rate=1, shift=0, order=3, head=1, tail=1):
        """Store a window segment (machine units)

        :param start: Window start address (0 to 0x3ff)
        :param iq: List of IQ window samples. Each window sample is an integer
            containing the signed I part in the 16 LSB and the signed Q part in
            the 16 MSB. The maximum window length is 0x3fe. The user must
            ensure that this window does not overlap with other windows in the
            memory.
        :param rate: Interpolation rate change (1 to 1 << 12)
        :param shift: Interpolator amplitude gain compensation in powers of 2 (0 to 63)
        :param order: Interpolation order from 0 (corresponding to
            constant/rectangular window/zero-order-hold/1st order CIC interpolation)
            to 3 (corresponding to cubic/Parzen window/4th order CIC interpolation)
        :param head: Update the interpolator settings and clear its state at the start
            of the window. This also implies starting the envelope from zero.
        :param tail: Feed zeros into the interpolator after the window samples.
            In the absence of further pulses this will return the output envelope
            to zero with the chosen interpolation.
        :return: Next available window memory address after this segment.
        """
        if start >= 1 << 10:
            raise ValueError("start out of bounds")
        if len(iq) >= 1 << 10:
            raise ValueError("window length out of bounds")
        if rate < 1 or rate > 1 << 12:
            raise ValueError("rate out of bounds")
        if shift > 0x3f:
            raise ValueError("shift out of bounds")
        if order > 3:
            raise ValueError("order out of bounds")
        self.channel.phaser.write16(PHASER_ADDR_MIQRO_MEM_ADDR,
                (self.channel.index << 15) | start)
        self.channel.phaser.write32(PHASER_ADDR_MIQRO_MEM_DATA,
            (len(iq) & 0x3ff) |
            ((rate - 1) << 10) |
            (shift << 22) |
            (order << 28) |
            ((head & 1) << 30) |
            ((tail & 1) << 31)
        )
        for iqi in iq:
            self.channel.phaser.write32(PHASER_ADDR_MIQRO_MEM_DATA, iqi)
            delay(20*us)  # slack for long windows
        return (start + 1 + len(iq)) & 0x3ff

    @kernel
    def set_window(self, start, iq, period=4*ns, order=3, head=1, tail=1):
        """Store a window segment

        :param start: Window start address (0 to 0x3ff)
        :param iq: List of IQ window samples. Each window sample is a pair of
            two float numbers -1 to 1, one for each I and Q in units of full scale.
            The maximum window length is 0x3fe. The user must ensure that this window
            does not overlap with other windows in the memory.
        :param period: Desired window sample period in SI units (4*ns to (4 << 12)*ns).
        :param order: Interpolation order from 0 (corresponding to
            constant/zero-order-hold/1st order CIC interpolation) to 3 (corresponding
            to cubic/Parzen/4th order CIC interpolation)
        :param head: Update the interpolator settings and clear its state at the start
            of the window. This also implies starting the envelope from zero.
        :param tail: Feed zeros into the interpolator after the window samples.
            In the absence of further pulses this will return the output envelope
            to zero with the chosen interpolation.
        :return: Actual sample period in SI units
        """
        rate = int32(round(period/(4*ns)))
        gain = 1.
        for _ in range(order):
            gain *= rate
        shift = 0
        while gain >= 2.:
            shift += 1
            gain *= .5
        scale = ((1 << 15) - 1)/gain
        iq_mu = [
            (int32(round(iqi[0]*scale)) & 0xffff) |
            (int32(round(iqi[1]*scale)) << 16)
            for iqi in iq
        ]
        self.set_window_mu(start, iq_mu, rate, shift, order, head, tail)
        return (len(iq) + order)*rate*4*ns

    @kernel
    def encode(self, window, profiles, data):
        """Encode window and profile selection

        :param window: Window start address (0 to 0x3ff)
        :param profiles: List of profile indices for the oscillators. Maximum
            length 16. Unused oscillators will be set to profile 0.
        :param data: List of integers to store the encoded data words into.
            Unused entries will remain untouched. Must contain at least three
            lements if all oscillators are used and should be initialized to
            zeros.
        :return: Number of words from `data` used.
        """
        if len(profiles) > 16:
            raise ValueError("too many oscillators")
        if window > 0x3ff:
            raise ValueError("window start out of bounds")
        data[0] = window
        word = 0
        idx = 10
        for profile in profiles:
            if profile > 0x1f:
                raise ValueError("profile out of bounds")
            if idx > 32 - 5:
                word += 1
                idx = 0
            data[word] |= profile << idx
            idx += 5
        return word + 1

    @kernel
    def pulse_mu(self, data):
        """Emit a pulse (encoded)

        The pulse fiducial timing resolution is 4 ns.

        :param data: List of up to 3 words containing an encoded MIQRO pulse as
            returned by :meth:`encode`.
        """
        word = len(data)
        delay_mu(-8*word)  # back shift to align
        while word > 0:
            word -= 1
            delay_mu(8)
            # final write sets pulse stb
            rtio_output(self.base_addr + word, data[word])

    @kernel
    def pulse(self, window, profiles):
        """Emit a pulse

        This encodes the window and profiles (see :meth:`encode`) and emits them
        (see :meth:`pulse_mu`).

        :param window: Window start address (0 to 0x3ff)
        :param profiles: List of profile indices for the oscillators. Maximum
            length 16. Unused oscillators will select profile 0.
        """
        data = [0, 0, 0]
        words = self.encode(window, profiles, data)
        self.pulse_mu(data[:words])
