I'd like to communicate using the DIO-RJ45 card with a different device in a SPI-like protocol. For that, I need to generate a clock and change the signal line at the rising edge. I'd like to achieve clock rates of 50MHz, but even 20MHz would be significant progress.

Currently, I am using bit-banging. For example, this is how I currently generate the clock (self.clk is a TTL of DIO-RJ45):
def clock(self, cycles: TInt32) -> None:
"""Output cycles of clocks."""
"""
for _ in range(cycles):
delay(self.step_s)
self.clk.on()
delay(self.step_s)
self.clk.off()

The down-side of this approach is that I run in RTIO Underflow Errors at higher clock rates. Even pre-recording the sequences using DMA only helps so much as the bandwidth of the DMA is not sufficiently high for clock rates of 20MHz.

Is there a more efficient way to generate SPI-like signals (clock + data line)? The DIO-RJ45 wiki mentions that LDVS_1 is clock-capable. Is SPI natively supported by DIO-RJ45? If not, what would need to be changed to add such support?

The dio_spi peripheral does this.

Thank you for the reply. I will try it out tomorrow. But to avoid a lot of guessing, could you tell me in this JSON example

        {
            "type": "dio_spi",
            "board": "DIO_SMA",
            "ports": [0],
            "spi": [
                {
                    "name": "sandia_dac_spi",
                    "clk": 0,
                    "mosi": 1,
                    "cs": [3]
                },
                {
                    "name": "hv209_spi",
                    "clk": 4,
                    "mosi": 5
                }
            ],
            "ttl": [
                {
                    "name": "sandia_dac_trigger",
                    "pin": 2,
                    "direction": "output"
                },
                {
                    "name": "hv209_rst",
                    "pin": 6,
                    "direction": "output",
                    "edge_counter": false
                },
                {
                    "pin": 7,
                    "direction": "input",
                    "edge_counter": true
                }
            ]
        }
  • What does the ttl block mean?
  • For clk, mosi, and cs: are the numbers referring to the pin of the channel?
  • What do the names refer to? Related question, is this how I can access the device using something like self.setattr_device("sandia_dac_spi") in this example?

What does the ttl block mean?

Not all channels of the board must be assigned to SPI signals. You can have standalone TTL lines, which then correspond to devices that use drivers from artiq.coredevice.ttl.

For clk, mosi, and cs: are the numbers referring to the pin of the channel?

The numbers correspond to the channel on the EEM port. The channel numbers are indicated on the front-panel (DIO_SMA, DIO_RJ45).

The channel direction must be configured with the direction-control switches on the board (per bank for DIO_SMA, per channel for DIO_RJ45).

is this how I can access the device using something like (...)

Yes. Same for the standalone TTLs. The SPI device is an instance of artiq.coredevice.spi2.SPIMaster.

Thank you for all your answers.

So far, I was not successful. I am trying with DIO_SMA which is on port 0. IO0 to IO3 are set to input direction, IO4 to IO7 are set as output direction. Here is the snippet of my json configuration:

        {
          "type": "dio_spi",
          "board": "DIO_SMA",
          "ports": [0],
          "spi": [
              {
                  "name": "sma_spi",
                  "clk": 7,
                  "mosi": 5
              }
          ]
        },

and here the relevant lines from device_db.py generated using artiq_ddb_template.exe:

device_db["sma_spi0"] = {
    "type": "local",
    "module": "artiq.coredevice.spi2",
    "class": "SPIMaster",
    "arguments": {"channel": 0x000000}
}

If I attach the scope to IO4 and run this script

from artiq.experiment import *
 
class SPITest(EnvExperiment):
    def build(self):
        self.setattr_device("core")
        self.setattr_device("sma_spi0")
 
    @kernel
    def run(self):
        self.core.break_realtime()
        
        data = 0xf0f0f0f0
        for i in range(1000):
            self.sma_spi0.write(data)
            delay(1*ms)

I unfortunately don't see any output.

What confuses me is that while in the json file clk is required, the channel number for clk is not used in the creation of device_db.py (see: https://github.com/m-labs/artiq/blob/376fbaafc5a4e33a956beda66ea0fbe0e198b363/artiq/frontend/artiq_ddb_template.py#L124). Same holds true for miso and mosi.

Are the channel numbers hard-coded? Or what determines which channel numbers have what functionality?

You're missing a call to sma_spi0.set_config. Given your word length (32 bits) and assuming mode-0 SPI, something like this:

from artiq.coredevice import spi2

# ...
clock_freq = 10 * MHz
cs = 1
word_size = 32
self.sma_spi0.set_config(spi2.SPI_END, word_size, clock_freq, cs)
# ...

spi2.SPI_END is only necessary if you want to de-assert CS after the transaction. You can use spi2.SPI_CLK_POLARITY and spi2.SPI_CLK_PHASE to change the SPI mode (combined with bitwise OR).

See the docstrings in the artiq.coredevice.spi2 module for more details.

Thank you for the answer. I indeed forgot to set the configuration

I was not yet successful to get SPI up and running. I don't see any change on my scope.

I don't understand how artiq knows which pins to use as CLK, MOSI etc. From the JSON configuration

        {
          "type": "dio_spi",
          "board": "DIO_SMA",
          "ports": [0],
          "spi": [
              {
                  "name": "sma_spi",
                  "clk": 4,
                  "mosi": 5
              }
          ]
        },

I generate the device_db.py file:

device_db["sma_spi0"] = {
    "type": "local",
    "module": "artiq.coredevice.spi2",
    "class": "SPIMaster",
    "arguments": {"channel": 0x000000}
}

channel in arguments seems to change only dependent on the position of the definition within the JSON configuration. More importantly, the parameters clk and mosi have no effect on device_db.py. So, how does the Sinara box know which pins to use as clock and as data output?

Do I need to rebuild firmware and/or gateware?

  • rjo replied to this.

    MichaelHartmann Do I need to rebuild firmware and/or gateware

    Yes. Each time you change peripherals. If you have a current afws subscription, just send the patch to the JSON and we'll update it.

    Then I finally understand what is going wrong. I can build the firmware/gateware. Thanks so much for helping!

    I got it running (I just checked CLK, but I am confident the rest is working as well). Thank you very much.

    For reference, here is what you need to do to get SPI running:

    1. Change JSON configuration and define the mapping between pins and functionality. For example, with a DIO-SMA card at port 0 using pin 4 (IO4) as CLK, and pin 5 (IO5) as MOSI, and using IO0 to IO3 as inputs:
      [...]
            "peripherals": [
                  {
                    "type": "dio_spi",
                    "board": "DIO_SMA",
                    "ports": [0],
                    "spi": [
                        {
                            "name": "sma_spi",
                            "clk": 4,
                            "mosi": 5
                        }
                    ],
                    "ttl": [
                        {
                            "name": "sma_in",
                            "pin": 0,
                            "direction": "input"
                        }
                    ]
                },
      [...]
    2. Rebuild firmware/gateware/runtime using the altered JSON configuration and flash Sinara box. For building you can use AFWS if you have a subscription. More information can be found in the manual: https://m-labs.hk/artiq/manual/
    3. Simple script to output data via SPI:
      from artiq.experiment import *
      import artiq.coredevice.spi2 as spi2
      
      class SPITest(EnvExperiment):
          def build(self):
              self.setattr_device("core")
              self.setattr_device("sma_spi0")
       
          @kernel
          def run(self):
              self.core.break_realtime()
              
              clock_freq = 10 * MHz
              cs = 1
              word_size = 32
              self.sma_spi0.set_config(spi2.SPI_END, word_size, clock_freq, cs)
      
              data = 0xf0f0f0f0
              self.sma_spi0.write(data)
      For more information read the documentation of spi2: https://m-labs.hk/artiq/manual/_modules/artiq/coredevice/spi2.html