diff --git a/artiq/coredevice/suservo.py b/artiq/coredevice/suservo.py index 653f0b683..e642afcec 100644 --- a/artiq/coredevice/suservo.py +++ b/artiq/coredevice/suservo.py @@ -1,4 +1,4 @@ -from artiq.language.core import kernel, delay, portable, now_mu +from artiq.language.core import kernel, delay, portable, now_mu, delay_mu from artiq.language.units import us, ms from artiq.coredevice.rtio import rtio_output, rtio_input_data @@ -39,8 +39,18 @@ class SUServo: @kernel def init(self): + """Initialize the Servo, Sampler and both Urukuls. + + Leaves the Servo disabled (see :meth:`set_config`), resets all DDS. + + Urukul initialization is performed blindly as there is no readback from + the DDS or the CPLDs. + + This method does not alter the profile configuration memory + or the channel controls. + """ self.set_config(0) - delay(2*us) # pipeline flush + delay(3*us) # pipeline flush self.pgia.set_config_mu( sampler.SPI_CONFIG | spi.SPI_END, @@ -60,29 +70,62 @@ class SUServo: @kernel def write(self, addr, value): + """Write to Servo memory. + + This method advances the timeline by one coarse RTIO cycle. + + :param addr: Memory location address. + :param value: Data to be written. + """ rtio_output(now_mu(), self.channel, addr | WE, value) delay_mu(self.ref_period_mu) @kernel def read(self, addr): + """Read from Servo memory. + + This method does not advance the timeline but consumes all slack. + + :param addr: Memory location address. + """ rtio_output(now_mu(), self.channel, addr, 0) return rtio_input_data(self.channel) @kernel - def set_config(self, start): - self.write(CONFIG_ADDR, start) + def set_config(self, enable): + """Set SU Servo configuration. + + This method advances the timeline by one Servo memory access. + + :param enable: Enable Servo operation. Disabling takes up to 2 Servo + cycles (~2.2 µs). + """ + self.write(CONFIG_ADDR, enable) @kernel def get_status(self): + """Get current SU Servo status. + + This method does not advance the timeline but consumes all slack. + + :return: Status. Bit 0: enabled, bit 1: done + """ return self.read(CONFIG_ADDR) @kernel def get_adc_mu(self, adc): + """Get an ADC reading (IIR filter input X0). + + This method does not advance the timeline but consumes all slack. + + :param adc: ADC channel number (0-7) + :return: 16 bit signed Y0 + """ return self.read(STATE_SEL | (adc << 1) | (1 << 8)) @kernel - def set_gain_mu(self, channel, gain): - """Set instrumentation amplifier gain of a channel. + def set_pgia_mu(self, channel, gain): + """Set instrumentation amplifier gain of a ADC channel. The four gain settings (0, 1, 2, 3) corresponds to gains of (1, 10, 100, 1000) respectively. @@ -96,6 +139,10 @@ class SUServo: self.pgia.write(gains << 16) self.gains = gains + @kernel + def get_adc(self, adc): + raise NotImplementedError # FIXME + class Channel: kernel_invariants = {"channel", "core", "servo", "servo_channel"} @@ -105,28 +152,123 @@ class Channel: self.core = dmgr.get(core_device) self.servo = dmgr.get(servo_device) self.channel = channel - self.servo_channel = self.channel + 8 - self.servo.channel # FIXME + # FIXME: this assumes the mem channel is right after the control + # channels + self.servo_channel = self.channel + 8 - self.servo.channel @kernel def set(self, en_out, en_iir=0, profile=0): + """Operate channel. + + This method does not advance the timeline. + + :param en_out: RF switch enable + :param en_iir: IIR updates enable + :param profile: Active profile (0-31) + """ rtio_output(now_mu(), self.channel, 0, en_out | (en_iir << 1) | (profile << 2)) @kernel - def set_profile_mu(self, profile, ftw, adc, offset, - a1, b0, b1, delay, pow=0): + def set_dds_mu(self, profile, ftw, offset, pow=0): + """Set profile DDS coefficients. + + This method advances the timeline by four Servo memory accesses. + + :param profile: Profile number (0-31) + :param ftw: Frequency tuning word (32 bit unsigned) + :param offset: IIR offset (setpoint) + :param pow: Phase offset word (16 bit unsigned) + """ base = (self.servo_channel << 8) | (profile << 3) - data = [ftw >> 16, b1, pow, adc | (delay << 8), offset, a1, ftw, b0] - for i in range(8): - self.servo.write(base + i, data[i]) + self.servo.write(base + 0, ftw >> 16) + self.servo.write(base + 6, ftw) + self.servo.write(base + 4, offset) + self.servo.write(base + 2, pow) + + @kernel + def set_dds(self, profile, frequency, offset, phase=0.): + raise NotImplementedError # FIXME + + @kernel + def set_iir_mu(self, profile, adc, a1, b0, b1, delay=0): + """Set profile IIR coefficients. + + This method advances the timeline by four Servo memory accesses. + + :param profile: Profile number (0-31) + :param adc: ADC channel to use (0-7) + :param a1: 18 bit signed A1 coefficient (Y1 coefficient, + feedback, integrator gain) + :param b0: 18 bit signed B0 coefficient (recent, + X0 coefficient, feed forward, proportional gain) + :param b1: 18 bit signed B1 coefficient (old, + X1 coefficient, feed forward, proportional gain) + :param delay: Number of Servo cycles (~1.1 µs each) to suppress + IIR updates for after either (1) enabling or disabling RF output, + (2) enabling or disabling IIR updates, or (3) setting the active + profile number: i.e. after invoking :meth:`set`. + """ + base = (self.servo_channel << 8) | (profile << 3) + self.servo.write(base + 1, b1) + self.servo.write(base + 3, adc | (delay << 8)) + self.servo.write(base + 5, a1) + self.servo.write(base + 7, b0) + + @kernel + def set_iir(self, profile, adc, i_gain, p_gain, delay=0.): + raise NotImplementedError # FIXME @kernel def get_profile_mu(self, profile, data): + """Retrieve profile data. + + The data is returned in the `data` argument as: + `[ftw >> 16, b1, pow, adc | (delay << 8), offset, a1, ftw, b0]`. + + This method advances the timeline by 32 µs and consumes all slack. + + :param profile: Profile number (0-31) + :param data: List of 8 integers to write the profile data into + """ base = (self.servo_channel << 8) | (profile << 3) - for i in range(8): + for i in range(len(data)): data[i] = self.servo.read(base + i) - delay(2*us) + delay(4*us) @kernel - def get_asf_mu(self, profile): + def get_y_mu(self, profile): + """Get a profile's IIR state (filter output, Y0). + + The IIR state is also know as the "integrator", or the DDS amplitude + scale factor. It is 18 bits wide and unsigned. + + This method does not advance the timeline but consumes all slack. + + :param profile: Profile number (0-31) + :return: 18 bit unsigned Y0 + """ return self.servo.read(STATE_SEL | (self.servo_channel << 5) | profile) + + @kernel + def get_y(self, profile): + raise NotImplementedError # FIXME + + @kernel + def set_y_mu(self, profile, y): + """Set a profile's IIR state (filter output, Y0). + + The IIR state is also know as the "integrator", or the DDS amplitude + scale factor. It is 18 bits wide and unsigned. + + This method advances the timeline by one Servo memory access. + + :param profile: Profile number (0-31) + :param y: 18 bit unsigned Y0 + """ + return self.servo.write( + STATE_SEL | (self.servo_channel << 5) | profile, y) + + @kernel + def set_y(self, profile, y): + raise NotImplementedError # FIXME diff --git a/artiq/examples/kasli_suservo/repository/suservo.py b/artiq/examples/kasli_suservo/repository/suservo.py index ace00485c..09e3f7653 100644 --- a/artiq/examples/kasli_suservo/repository/suservo.py +++ b/artiq/examples/kasli_suservo/repository/suservo.py @@ -24,33 +24,48 @@ class SUServo(EnvExperiment): self.suservo0.init() delay(1*us) - self.suservo0.cpld0.set_att_mu(0, 255) + # ADC PGIA gain + self.suservo0.set_pgia_mu(0, 0) + # DDS attenuator + self.suservo0.cpld0.set_att_mu(0, 64) delay(1*us) + assert self.suservo0.get_status() == 2 - print(self.suservo0.get_status()) - delay(3*ms) - - self.suservo0_ch0.set_profile_mu( - profile=0, ftw=0x12345667, adc=0, offset=0x10, - a1=-0x2000, b0=0x1ffff, b1=0, delay=0, pow=0xaa55) + # set up profile 0 on channel 0 + self.suservo0_ch0.set_y_mu(0, 0) + self.suservo0_ch0.set_iir_mu( + profile=0, adc=0, a1=-0x800, b0=0x1000, b1=0, delay=0) + self.suservo0_ch0.set_dds_mu( + profile=0, ftw=0x12345667, offset=0x1, pow=0xaa55) + # enable channel self.suservo0_ch0.set(en_out=1, en_iir=1, profile=0) - - delay(10*ms) + # enable servo iterations self.suservo0.set_config(1) - delay(10*ms) + + # read back profile data data = [0] * 8 self.suservo0_ch0.get_profile_mu(0, data) self.p(data) delay(10*ms) + + # check servo status + assert self.suservo0.get_status() == 1 + + # reach back ADC data print(self.suservo0.get_adc_mu(0)) delay(10*ms) - print(self.suservo0.get_adc_mu(1)) + + # read out IIR data + print(self.suservo0_ch0.get_y_mu(0)) delay(10*ms) - print(self.suservo0_ch0.get_asf_mu(0)) - delay(10*ms) - print(self.suservo0_ch0.get_asf_mu(0)) - delay(10*ms) - print(self.suservo0.get_status()) + + # repeatedly clear the IIR state/integrator + # with the ADC yielding 0's and given the profile configuration, + # this will lead to a slow ram up of the amplitude over about 200ms + # followed by saturation and repetition. + while True: + self.suservo0_ch0.set_y_mu(0, 0) + delay(.2*s) @kernel def led(self): diff --git a/doc/manual/core_drivers_reference.rst b/doc/manual/core_drivers_reference.rst index bdfd93519..c54a44c0d 100644 --- a/doc/manual/core_drivers_reference.rst +++ b/doc/manual/core_drivers_reference.rst @@ -132,3 +132,13 @@ DAC/ADC drivers .. automodule:: artiq.coredevice.novogorny :members: + + +Compound drivers +---------------- + +:mod:`artiq.coredevice.suservo` module +++++++++++++++++++++++++++++++++++++++ + +.. automodule:: artiq.coredevice.suservo + :members: