Skip to content

Getting Started

To begin working with the software API, make sure you have setup your environment for your hardware target:

  • Hardware Setup : Install drivers, setup your user and/or system for the target hardware
  • Software Setup : Install python and required dependencies, setup the environment

This page provides a step-by-step guide to go through the most important functionalities of the API required to be able to interact with a chip.

Identify your hardware

The firmware is build for both Gecco with Nexys Video FPGA board and Astep FPGA Board with a Digilent CMOD A35 fpga board.

If you are using a Gecco board, you have to ensure the FPGA is flashed with a firmware that matches your hardware configuration:

  • Which Chip Carrier Board is connected: V2/V3/V4 , Telescope ... ?
  • Which signal types the Configuration and clock signals to the carriers are expected to be: LVDS or Single ended CMOS.

Consult the Configuration Summary Page to check your hardware.

Prepare the terminal environment

The sw/ folder contains a set of modules providing the python API to work with the firmware (config, readout etc..). To allow flexibility, it is recommenced to setup the terminal environment to add the sw/ folder to the PYTHONPATH variable. This way any python import requiring a driver present in this repository will work out of the box.

On Linux:

1
2
# load.sh is located in the root of the repository
source load.sh 

On Windows CMD:

1
2
# load.sh is located in the root of the repository
.\load.bat

On Windows PowerShell:

1
2
# load.sh is located in the root of the repository
.\load.ps1

Choose a work folder for your scripts

To work on python scripts, you can place your scripts directly in the sw/ folder, and install a virtual environment there. There is a basic requirements.txt file provided which should cover the basic needs.

However, if you have sourced the load script, your environment is setup properly and the PYTHONPATH variable includes the sw/ folder. You can then work from any other folder, just make sure to install the dependencies provided in the requirements.txt file in your virtual environment

Opening a Board Driver

Before writing/reading to/from the firmware, the user must create a BoardDriver class instance configured with the right I/O interface for the target hardware.

The BoardDriver class provides high level methods to drive the main functionalities of the firmware.

Possible configurations are:

  • Gecco over FTDI FIFO (Fast USB)
  • Gecco over Uart (max. 921600bps serial, recommened if the user needs to use a debugging ILA core while interacting with the firmware)
  • Gecco over SPI (not supported yet)
  • CMOD over Uart
  • CMOD over SPI (flight configuration, not tested yet)

Most users doing chip measurement will be using the Gecco over FTDI FIFO interface:

1
2
3
4
5
import drivers.boards

## Open FTDI Driver for Gecco
boardDriver = drivers.boards.getGeccoFTDIDriver()
boardDriver.open()

Closing the connections to the fpga board doesn't have to be explicit, the low-level drivers are detecting the python program exit to properly close all driver handles.

Reading/Writing to the FPGA

To Read and write to the FPGA, we have to send read/write requests to the Firmware's Register File, which is a memory map used to set configurations and read back data, like performance counters or data frames from sensors.

The list of registers with their description and addresses is documented on this page

The python API is using a module generated by KIT's Register File generator tool, which provides a set of read and/or write methods for users to easily access the RegisterFile while hidding low-level details.

Let's look at how to read the firmware version, which is a 32 bits integer present at the beginning of the register map:

1
2
3
4
5
6
7
import drivers.boards

## Open FTDI Driver for Gecco
boardDriver = drivers.boards.getGeccoFTDIDriver()
boardDriver.open()

version = asyncio.run(boardDriver.readFirmwareVersion())

The BoardDriver is providing a helper method to read the firmware version:

1
2
3
4
5
    ...
    async def readFirmwareVersion(self):
        """Returns the raw integer with the firmware Version"""
        return (await (self.rfg.read_hk_firmware_version()))
    ...
Explanation:

  • self.rfg: The rfg class member is a class instance of main_rfg type, which is the generated class containing all the methods to access the registers. This rfg value is created and configured when creating the BoardDriver in the previous step.

  • self.rfg.read_hk_firmware_version(): This call prepares a read request to the firmware version register address and sends it straight to the firmware. This method creates the read request so that it will return 4 consecutive bytes from the register file (32 bit value), then return an integer constructed from the 4 bytes.

  • async and await: All the method calls that require a request to the hardware are marked async, so that they must be called using the await keyword. This is required because the underlying drivers are using the Python AsyncIO library.

AsyncIO requirement

All the low-level method calls must be run in an asyncio loop. You can read the AsyncIO page to learn more about asyncio.

In a nutshell, going back to our previous example:

1
2
3
...
version = asyncio.run(boardDriver.readFirmwareVersion())
...

A python script by default does not run in an asyncio loop, which means users must pass method calls sending requests to the hardware to asyncio:

  • wrap any method marked async in asyncio.run(xxx)
  • Create an async definition passed to asyncio.run(xxx), in which you can simply await any async method

Tip

Whenever you see the await keyword, if you are not running in an asyncIO loop, just wrap the code line in an asyncio.run() call

Firmware Startup / Reset State

Upon reset, the firmware will set the signals to the layer in the following configuration:

  • No Timestamp and Sample Clock output enabled to Carriers/Layers
  • Each Carrier/Layer reset and hold (resn signal set to 0, hold to 1)
  • Layer Readout modules are not self-triggering readout if the sensor pulls the interrupt signal low

To begin working with sensors, the user must first release the layer from reset and hold (if desired), and enable the clock outputs.

The BoardDriver class provides the necessary utility for this:

1
2
3
4
5
# Deassert reset right now (flush is true)
await boardDriver.setLayerReset(layer = 0 , reset = False , flush = True)

# Deassert hold right now (flush is true)
await boardDriver.holdLayer(layer = 0 , hold = False , flush = True )

The user can also start scripts with a reset cycle to ensure you are beginning with a fully reset chip:

1
2
# This method asserts the reset signal for .5s by default then deasserts it
await boardDriver.resetLayer(layer = 0)

Enabling / Setting up Clocks

Next, we will want to enable the Timestamp and Sample clocks, and configure the SPI Clock for the Layers

For the sample clock, check the hardware target for the exact configuration, but by default on Gecco, the sample clock is routed to the main differential input of Astropix (v2/v3)

1
2
# Enable TS and Sample clock right now
await boardDriver.enableSensorClocks(flush = True)

To configure the SPI Clock, a utility method can calculate the required clock divider to specify a certain SPI clock freqency.

A value of 1 Mhz should be fine to get started:

1
2
# Set the SPI Clock to 1Mhz (the value must be passed in Hz)
await boardDriver.configureLayerSPIFrequency(1000000)

Chip Configuration Setup

The default method to configure chip is first to read a configuration file containing for the matching astropix, then configure the asic.

The configuration file is the same YAML file type as the one used historically:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
# Gecco with single chip carrier board: 

# Setup Astropix 2, 1 Layer (1 readout row), 1 chip in the daisy chain, path to the config File
boardDriver.setupASICS( version = 2 , rows = 1 , chipsPerRow = 1 , configFile = "configv2.yml")

# Setup Astropix 3, 1 Layer (1 readout row), 1 chip in the daisy chain, path to the config File
boardDriver.setupASICS( version = 3 , rows = 1 , chipsPerRow = 1 , configFile = "configv3.yml")

# Gecco with 1 Quad chip:

# Setup Astropix 2, 1 Layer (1 readout row), 1 chip in the daisy chain, path to the config File
boardDriver.setupASICS( version = 3 , rows = 1 , chipsPerRow = 4 , configFile = "configv3.yml")

# CMOD with 3 Quad Chip layers: 
boardDriver.setupASICS( version = 3 , rows = 3 , chipsPerRow = 4 , configFile = "configv3.yml")

Warning

The number of chips in the row can be set in the yaml file, but will be overriden by the chipsPerRow parameter. This way only a basic config file is required for all cases

Configuring the Chip

To prepare a chip configuration, the API is the same as in the historical python.

To get the Asic class instance to configure, just call the getAsic method on the BoardDriver:

1
2
# Get the ASic class for the first row 
asic = boardDriver.getAsic(row = 0 )

Note

There is one Asic class instance per row, each containing configuration for the whole daisy chain. If the user wants to configure all the chips in the same way, there should be a loop over the number of configured rows so that all Asic classes are configured.

Now you can proceed as usual:

1
2
3
4
5
6
7
8
# Enable a Pixel
asic.enable_pixel(col = 0 , row = 0 )

# Write the configuration via SPI 
await asic.writeConfigSPI()

# Or write via ShiftRegister
await asic.writeConfigSR()

Gecco Card Configuration

If you are using Cards on Gecco, you can get the instances from the BoardDriver class.

For example:

1
2
3
4
5
## Get and configure
voltageBoard = boardDriver.geccoGetVoltageBoard()
voltageBoard.dacvalues = ... 
## Update card
await voltageBoard.update()

Sensor Readout Basics

By default, the firmware won't auto-read the sensor. The user must send some dummy bytes to the sensor so that some data will be read back if any available.

For example:

1
2
# Write 16 NULL bytes to the sensor
await boardDriver.writeBytesToLayer(layer = 0 , bytes = [0x00]*16)
Depending on the status of the sensor, the firmware will:

  • Discard received IDLE bytes, and increase a counter by one for each
  • Decode and encapsulate any data frame, increase a counter by one, then write them to the output buffer when complete

For example, a simple functionality check could be:

  • Take a layer out of reset, keep hold set
  • Send some dummy bytes
  • The IDLE bytes counter should increase by twice the number of send bytes (because the sensor output is twice as fast as the input)
  • The Frame counter will stay 0 as Sensors in Hold mode are not returning any data
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
# Reset = False, Hold = True , Send 16 Dummy bytes
await boardDriver.setLayerReset(layer = 0 , reset = False , flush = False)
await boardDriver.holdLayer(layer = 0 , hold = True , flush = True )
await boardDriver.writeBytesToLayer(layer = 0 , bytes = [0x00]*16)

# Reads the Idle counter  register for the layer 0
idleCount = await boardDriver.rfg.read_layer_0_stat_idle_counter()
print(f"Actual IDLE counter: {idleCount}")

# Reads the Frame Counter register for the layer 0 
framesCount = await boardDriver.rfg.read_layer_0_stat_frame_counter()
print(f"Actual Frame counter: {framesCount}")

One you are ready to read some actual data, make sure you are running a meaniful configuration: Noise Run, Injections etc...

Don't forget to remove the hold mode so that actual data will be send by the sensor:

1
await boardDriver.holdLayer(layer = 0 , hold = False , flush = True )

The Readout Buffer

If you are expecting some data from the sensor, and you have either send NULL bytes to the firmware to trigger some SPI byte readouts, or the firmwar is configured to automatically readout data upon interrupt, you should be able to read some bytes from the readout buffer.

The Readout buffer is only filled with payload data, following the layer interface framing described on that page - Idle bytes are always discarded.

There are two methods in the BoardDriver class to read some data:

  • boardDriver.readoutGetBufferSize(): Returns the number of bytes available in the buffer
  • boardDriver.readoutReadBytes(count): Reads count bytes from the buffer.
1
2
# Read 4K bytes from the readout buffer
await boardDriver.readoutReadBytes(count = 4096)

When reading out from the buffer, there are two options:

  • One can read the buffer size and read only the available number of bytes. This method is likely to be inefficient, creating a lot of small-sized reads. However it can be useful to read the buffer size at the end of a script to finalize reading all the data for example.
  • One can read larger number of bytes, however this method is likely to return a lot of empty bytes "0xFF" since reading may be faster than sensor data fetching. You should be careful to process the bytes stream to discard empty bytes, but not discard 0xFF bytes which are not empty bytes.
1
2
3
4
5
# Read 4K bytes from the readout buffer
bufferBytes = await boardDriver.readoutReadBytes(count = 4096)

# Process bytes according to the frame format here
# Empty bytes will be 0xFF in the stream, beware of real 0xFF data bytes