Hello,

I was wondering if anyone who's built an SUServo has sample code for it they could share? There's not a whole lot of SUServo-related code on GitHub, and the documentation doesn't seem to have have examples/tutorials. ARTIQ's SUServo class and its methods are well documented, but figuring out proper usage, as well as how the code interfaces with the hardware, can be quite challenging on its own.

Thank you!

8 days later

Hi. I don't have any example code right now as all the code I've got is pretty entangled with other parts of our stack. But, some general pointers.

First, make sure you've set the Urukul DIP switches correctly, built the gateware for SUServo and hooked your Urukuls and Sampler up with both EEM cables.

Then, here is some pseudo-code that should point you in the right direction (I haven't been able to test it, but this is roughly right).

from artiq.experiment import *


class ServoTester(EnvExperiment):
    def build(self):

        self.setattr_device("core")
        self.setattr_device("servo0")  # or whatever your device db entry is
        self.setattr_device("urukul_cpld0")  # or whatever your device db entry is
        self.setattr_device("servo_ch0")

    @kernel
    def run(self):

    # initialize the servo + hardware (normally only done in your startup script)
    self.servo0.init()

   # the servo is currently inactive, so active it
   # after this
   #  - the servo will continuously (once per ~us) read ADC samples and store them in RAM.
   #    These are the "x" values
   #  - it will also continuously update the DDS frequency, phase and amplitude from the values stored in RAM
   #    The amplitude is the servo "y" value
   #  - if the IIRs are enabled for a DDS channel and the RF switch is open then the servo will update the
   #    y value by integrating the x values. Otherwise, the y values are held constant (e.g. use this to manually
   #   set them
   self.servo0.set_config(enable=True)

  # now, before playing with the feedback, let's check we can get some life out of the DDSs
  
  # first, set the Urukul attenuation to desired value
  self.urukul0.set_att(0, 0)

 # since the Urukuls are programmed from the servo 
 # the servo has multiple different "profiles" for each channel (DDS)
 # a profile is set of frequency, amplitude, phase data as well as an integrator
 # switching between profiles allows us to change the DDS parameters without having
 # to reset the integrators
 self.servo_ch0.set_dds(profile=0, frequency=100*MHz, offset=0)
 self.servo_ch0.set_y(0, 1)  # DDS amplitude to max manually
 self.servo_ch0.set(en_out=1, en_iir=0, profile=0)  # turn the RF switch on, but disable the integrator

After fixing the typos in that, you should get 100MHz RF out of channel 0. Once that works, next up is getting samples out of the ADC manually and enabling the PID loop...

    hartytp Thank you very much! This is immensely clarifying. We're still having some issues though. We have some code constructed based on your example, and it runs without errors, but doesn't seem to generate an output. We're using the SUServo bitstream available in the M-Labs Anaconda repository and the device database that comes with the ARTIQ package.

    Below is our code:

    from artiq.experiment import *
    import numpy as np
    
    
    class SUServoTest(EnvExperiment):
        def build(self):
            self.setattr_device("core")
            self.setattr_device("urukul0_cpld")
            self.setattr_device("suservo0")
            self.setattr_device("suservo0_ch0")
        
        @kernel
        def run(self):
            # Prepare core
            self.core.reset()
            self.core.break_realtime()
    
            #Initialize and activate SUServo
            self.suservo0.init()
            self.suservo0.set_config(enable=1)
            
            # Configure channel 0 to output 100 MHz
            self.suservo0.cpld0.set_att(0, 0.0)
            self.suservo0_ch0.set_dds(profile=0, frequency=100000000, offset=0.0)
            self.suservo0_ch0.set_y(0, 1.0)
            self.suservo0_ch0.set(en_out=1, en_iir=0, profile=0)

    and our device_db.py file:

    core_addr = "169.254.234.70"
    
    device_db = {
        "core": {
            "type": "local",
            "module": "artiq.coredevice.core",
            "class": "Core",
            "arguments": {"host": core_addr, "ref_period": 1e-9}
        },
        "core_log": {
            "type": "controller",
            "host": "::1",
            "port": 1068,
            "command": "aqctl_corelog -p {port} --bind {bind} " + core_addr
        },
        "core_cache": {
            "type": "local",
            "module": "artiq.coredevice.cache",
            "class": "CoreCache"
        },
        "core_dma": {
            "type": "local",
            "module": "artiq.coredevice.dma",
            "class": "CoreDMA"
        },
    
        "i2c_switch0": {
            "type": "local",
            "module": "artiq.coredevice.i2c",
            "class": "PCA9548",
            "arguments": {"address": 0xe0}
        },
        "i2c_switch1": {
            "type": "local",
            "module": "artiq.coredevice.i2c",
            "class": "PCA9548",
            "arguments": {"address": 0xe2}
        },
    
        "ttl0": {
            "type": "local",
            "module": "artiq.coredevice.ttl",
            "class": "TTLInOut",
            "arguments": {"channel": 0},
        },
        "ttl1": {
            "type": "local",
            "module": "artiq.coredevice.ttl",
            "class": "TTLInOut",
            "arguments": {"channel": 1},
        },
        "ttl2": {
            "type": "local",
            "module": "artiq.coredevice.ttl",
            "class": "TTLInOut",
            "arguments": {"channel": 2},
        },
        "ttl3": {
            "type": "local",
            "module": "artiq.coredevice.ttl",
            "class": "TTLInOut",
            "arguments": {"channel": 3},
        },
    
        "ttl4": {
            "type": "local",
            "module": "artiq.coredevice.ttl",
            "class": "TTLOut",
            "arguments": {"channel": 4},
        },
        "ttl5": {
            "type": "local",
            "module": "artiq.coredevice.ttl",
            "class": "TTLOut",
            "arguments": {"channel": 5},
        },
        "ttl6": {
            "type": "local",
            "module": "artiq.coredevice.ttl",
            "class": "TTLOut",
            "arguments": {"channel": 6},
        },
        "ttl7": {
            "type": "local",
            "module": "artiq.coredevice.ttl",
            "class": "TTLOut",
            "arguments": {"channel": 7},
        },
        "ttl8": {
            "type": "local",
            "module": "artiq.coredevice.ttl",
            "class": "TTLOut",
            "arguments": {"channel": 8},
        },
        "ttl9": {
            "type": "local",
            "module": "artiq.coredevice.ttl",
            "class": "TTLOut",
            "arguments": {"channel": 9},
        },
        "ttl10": {
            "type": "local",
            "module": "artiq.coredevice.ttl",
            "class": "TTLOut",
            "arguments": {"channel": 10},
        },
        "ttl11": {
            "type": "local",
            "module": "artiq.coredevice.ttl",
            "class": "TTLOut",
            "arguments": {"channel": 11},
        },
        "ttl12": {
            "type": "local",
            "module": "artiq.coredevice.ttl",
            "class": "TTLOut",
            "arguments": {"channel": 12},
        },
        "ttl13": {
            "type": "local",
            "module": "artiq.coredevice.ttl",
            "class": "TTLOut",
            "arguments": {"channel": 13},
        },
        "ttl14": {
            "type": "local",
            "module": "artiq.coredevice.ttl",
            "class": "TTLOut",
            "arguments": {"channel": 14},
        },
        "ttl15": {
            "type": "local",
            "module": "artiq.coredevice.ttl",
            "class": "TTLOut",
            "arguments": {"channel": 15},
        },
    
        "suservo0_ch0": {
            "type": "local",
            "module": "artiq.coredevice.suservo",
            "class": "Channel",
            "arguments": {"channel": 16, "servo_device": "suservo0"}
        },
        "suservo0_ch1": {
            "type": "local",
            "module": "artiq.coredevice.suservo",
            "class": "Channel",
            "arguments": {"channel": 17, "servo_device": "suservo0"}
        },
        "suservo0_ch2": {
            "type": "local",
            "module": "artiq.coredevice.suservo",
            "class": "Channel",
            "arguments": {"channel": 18, "servo_device": "suservo0"}
        },
        "suservo0_ch3": {
            "type": "local",
            "module": "artiq.coredevice.suservo",
            "class": "Channel",
            "arguments": {"channel": 19, "servo_device": "suservo0"}
        },
        "suservo0_ch4": {
            "type": "local",
            "module": "artiq.coredevice.suservo",
            "class": "Channel",
            "arguments": {"channel": 20, "servo_device": "suservo0"}
        },
        "suservo0_ch5": {
            "type": "local",
            "module": "artiq.coredevice.suservo",
            "class": "Channel",
            "arguments": {"channel": 21, "servo_device": "suservo0"}
        },
        "suservo0_ch6": {
            "type": "local",
            "module": "artiq.coredevice.suservo",
            "class": "Channel",
            "arguments": {"channel": 22, "servo_device": "suservo0"}
        },
        "suservo0_ch7": {
            "type": "local",
            "module": "artiq.coredevice.suservo",
            "class": "Channel",
            "arguments": {"channel": 23, "servo_device": "suservo0"}
        },
    
        "suservo0": {
            "type": "local",
            "module": "artiq.coredevice.suservo",
            "class": "SUServo",
            "arguments": {
                "channel": 24,
                "pgia_device": "spi_sampler0_pgia",
                "cpld0_device": "urukul0_cpld",
                "cpld1_device": "urukul1_cpld",
                "dds0_device": "urukul0_dds",
                "dds1_device": "urukul1_dds"
            }
        },
    
        "spi_sampler0_pgia": {
            "type": "local",
            "module": "artiq.coredevice.spi2",
            "class": "SPIMaster",
            "arguments": {"channel": 25}
        },
    
        "spi_urukul0": {
            "type": "local",
            "module": "artiq.coredevice.spi2",
            "class": "SPIMaster",
            "arguments": {"channel": 26}
        },
        "urukul0_cpld": {
            "type": "local",
            "module": "artiq.coredevice.urukul",
            "class": "CPLD",
            "arguments": {
                "spi_device": "spi_urukul0",
                "refclk": 125e6,
                "clk_sel": 2
            }
        },
        "urukul0_dds": {
            "type": "local",
            "module": "artiq.coredevice.ad9910",
            "class": "AD9910",
            "arguments": {
                "pll_n": 32,
                "chip_select": 3,
                "cpld_device": "urukul0_cpld",
            }
        },
       "spi_sampler0_adc": {
            "type": "local",
            "module": "artiq.coredevice.spi2",
            "class": "SPIMaster",
            "arguments": {"channel": 27}
        },
        "spi_sampler0_cnv": {
            "type": "local",
            "module": "artiq.coredevice.ttl",
            "class": "TTLOut",
            "arguments": {"channel": 28},
        },
        "sampler0": {
            "type": "local",
            "module": "artiq.coredevice.sampler",
            "class": "Sampler",
            "arguments": {
                "spi_adc_device": "spi_sampler0_adc",
                "spi_pgia_device": "spi_sampler0_pgia",
                "cnv_device": "spi_sampler0_cnv"
            }
        },
        "spi_urukul1": {
            "type": "local",
            "module": "artiq.coredevice.spi2",
            "class": "SPIMaster",
            "arguments": {"channel": 27}
        },
        "urukul1_cpld": {
            "type": "local",
            "module": "artiq.coredevice.urukul",
            "class": "CPLD",
            "arguments": {
                "spi_device": "spi_urukul1",
                "refclk": 100e6,
                "clk_sel": 0
            }
        },
        "urukul1_dds": {
            "type": "local",
            "module": "artiq.coredevice.ad9910",
            "class": "AD9910",
            "arguments": {
                "pll_n": 40,
                "chip_select": 3,
                "cpld_device": "urukul1_cpld",
            }
        },
    
        "led0": {
            "type": "local",
            "module": "artiq.coredevice.ttl",
            "class": "TTLOut",
            "arguments": {"channel": 28}
        },
        "led1": {
            "type": "local",
            "module": "artiq.coredevice.ttl",
            "class": "TTLOut",
            "arguments": {"channel": 29}
        }
    }

    We're quite certain that our DIP switches are set correctly and that our boards are connected via the right EEMs. At a glance, is there anything that might be wrong with the code, or is it possibly a hardware/gateware issue?

    Thank you again.

      The conda package version is "4.0-0+gitbf50dcf7", so, see the corresponding source code at that revision in the ARTIQ git repository.

      aquinn sorry for the slow response, I was expecting an email notification when you replied but didn’t get one...

      that at all looks about right. One thought of the top of me head is that at one point I had some issues with setting amplitude to 1 as that overflowed and rounds down to almost zero. Try with 0.9

      aquinn also what leds do you have on now? No red led means the dds is correctly initialised (pll locked) which is a good start

      green means rf switch on which is another good sanity check

      aquinn one other suggestion: try using the get_y method and other get_ methods to read back the parameters that have been set and check everything is updating correctly

        hartytp Using the get_ methods, it looks like the Kasli is able to update the Urukul's parameters properly, but from the LEDs, it looks like the DDS are not getting initialized. This gives a possible lead! We'll continue looking into it.

        Okay. So calling the servo init isn’t initialising your urukul correctly. Probably one of:clocking issue, dip switches incorrect for servo (you need to switch one to enable servo mode) or a gateware/device_db mismatch. One thing you can try is probing the urukul eem cable and check you get spi transactions when you call the init code. Also try probing the clock chip and check the right clock gets to the ddss

        Thanks for the help @hartytp. Our best guess right now is that it's a gateware/device_db mismatch. Therefore we are going to wait until we get a new Artiq 5 gateware/device_db combo built using the new M-Labs build system before debugging further.

        20 days later
        5 days later

        @sb10q, the kasli-oregon configuration seems to be working just fine! There's something about SU servo though that I wasn't entirely clear on. Is there any way of directly controlling the set point for the Sampler? The closest I could find was the offset parameter in Channel.set_dds(), and it seems like the y-value is being set to this offset parameter regardless of what kind of signal is being fed into the Sampler. It doesn't look like the Urukul signal is being modulated.

        I just wanted to clarify whether or not the offset parameter is used to control the set point before continuing to debug our current code/hardware setup.

        Yes. E.g. set_dds(offset=-.5) controls the offset of the ADC inputs and thus the setpoint of the servo.

        And make sure you have the P/I profile coefficients and all parameters (ADC channel, channel enable, IIR enable, delay etc), set up as well, then select profile and enable the channel.

          rjo Thank you very much for the confirmation! That's very helpful to know. We were also wondering though if there's any way of converting between volts (which the ADC measures in) and the full scale units of the set point, or if we find this relationship through measurement.

          rjo We were able to get our servo system up and running. Thank you very much for your help!

          Edit: In case someone else comes along who might benefit from it, here's an example of some minimal code that gets a servo loop running. This loop will target the input voltage v_t for the Sampler channel adc_ch. The target voltage is set using the offset parameter in set_dds. This offset should be minus the target voltage scaled by the gain.

          from artiq.experiment import *
          
          
          class SUServoMinimal(EnvExperiment):
              def build(self):
                  self.setattr_device("core")
                  self.setattr_device("urukul0_cpld")
                  self.setattr_device("suservo0")
                  self.setattr_device("suservo0_ch0")
              
              @kernel
              def run(self):
                  # Prepare core
                  self.core.reset()
          
                  #Initialize and activate SUServo
                  self.suservo0.init()
                  self.suservo0.set_config(enable=1)
                  
                  # Set Sampler gain and Urukul attenuation
                  g = 0
                  A = 0.0
                  self.suservo0.set_pgia_mu(0, g)         # set gain on Sampler channel 0 to 10^g
                  self.suservo0.cpld0.set_att(0, A)       # set attenuation on Urukul channel 0 to 0
                  
                  
                  # Set physical parameters
                  v_t = 0.07                              # target input voltage (V) for Sampler channel
                  f = 100000000.0                         # frequency (Hz) of Urukul output
                  o = -v_t*(10.0**(g-1))                  # offset to assign to servo to reach target voltage
          
                  # Set PI loop parameters 
                  kp = 0.005                              # proportional gain in loop
                  ki = -10.0                              # integrator gain
                  gl = 0.0                                # integrator gain limit
                  adc_ch = 0                              # Sampler channel to read from
                  
                  # Input parameters, activate Urukul output (en_out=1),
                  # activate PI loop (en_iir=1)
                  self.suservo0_ch0.set_iir(profile=0, adc=adc_ch, kp=kp, ki=ki, g=gl)
                  self.suservo0_ch0.set_dds(profile=0, frequency=f, offset=o)
                  self.suservo0_ch0.set(en_out=1, en_iir=1, profile=0)