MicroZed Chronicles: Ultra96, PYNQ, Click Mezzanine, SPI and I2C

Several times in this series we have looked at working with I2C and SPI in both bare metal and Linux (for example spidev). These…

Adam Taylor
4 years ago

Several times in this series we have looked at working with I2C and SPI in both bare metal and Linux (for example, spidev). These interfaces are great for interfacing with sensors and displays, so it stands to reason we want to use these interfaces in our PYNQ and Python applications.

In this blog, we are going to examine how we can use PYNQ(2v4) running on the Ultra96 to interface with I2C and SPI peripherals connected via the Click Mezzanine.

There are a range of sensors we can interface with on the Click Mezzanine; for this example, I will be using the LSM6DSL and the Heart Beat 3 sensor. These use SPI and I2C interfaces, respectively.

The first thing we need to do is configure the Ultra96 and its PYNQ environment to work with I2C and SPI interfaces.

First, let’s address the I2C interface. If we open a terminal window in PYNQ, we can examine the dev directory using the command:

ls dev

This command will list the 10 I2C interfaces available in the system — exactly what these 10 interfaces are mapped to in the Ultra96, I will show you how to determine in a little while.

To begin to work with these interfaces from our Python/PYNQ environment, we want to install the python3-smbuspackage.

sudo apt install python3-smbus

We may also want to install i2ctools, as this will enable us to issue some very powerful terminal commands to understand the I2C structure on the board.

sudo apt install i2c-tools

We can then start using the I2C-tools features in a terminal to understand the I2C structure. The Ultra96 uses I2C1 within the processing system to connect to a I2C expander.

From this expander, channels 0 and 1 are connected to the low speed connector and hence to the click positions. Channel 0 is connected to Click Slot 1 and Channel 1 is connected to Click Slot 2.

However, which one of the 10 I2C devices is connected to which channel on the expander? The answer to this is to use I2C-Tools to output the list of all installed I2C buses with the command:

i2cdetect -l

From this, we can deduce that channel 0 of the expander is i2c-2 and channel 1 is i2c-3.

We can further use the I2C-Tools to detect the addressed of the devices connected to each bus.

i2cdetect -r -y

For example, i2cdetect -r -y 2 will show all of the devices connected to i2c-2.

We can see that we have one device connected i2c-3 at address 0x58, which is the address of the Heart Rate 3 Click. The UU shown at address 0x75 is the i2c expander address and shows it being used to scan the remainder of the bus.

We are now ready to be able to create a new notebook that can read and write over the I2C network.

Within our notebook, we can use the following:

import smbus — Imports the smbus package

i2c_bus = smbus.SMBus(3) — Create an I2C bus for i2c-3

data = i2c_bus.read_byte_data(0x58,0x00) — Read from register 0x00 data = 0xff & data print(hex(data))

i2c_bus.write_byte_data(DA,0x22,0x02) — write updated value

data = i2c_bus.read_byte_data(DA,0x00) — read back updated value data = 0xff & data print(hex(data))

Now that we understand how the I2C works, we can now get the SPI interfaces up and working.

The first step in this is to install the spidev package:

sudo pip3 install spidev

This will allow us to use SPI from our Python / PYNQ developments.

Checking the dev directory once this is installed should show the spidev.x.y

The mapping between our Ultra96 and the Mezzanine uses PS SPI0 connected via the low speed connector. As SPI is muli-drop with separate chip selects for each peripheral the Click Mezzanine assigns CS0 to Click Slot 0 and CS1 to Click Slot 1.

For this example, we will be interfacing with LSM6DSL in Click Slot 1.

import spidev — Import the spidev package

spi = spidev.SpiDev() spi.open(0,0) — Open SPI0 device 0 this maps to SPIDEV X.Y

def readRegister(regAddr): address = 0x80 | regAddr — set the read bit resp = spi.xfer2([address,0x00]) return resp[1] — return read results

def writeRegister(regAddr,value): spi.xfer2([regAddr,value])

whoAmI = readRegister(0x0F) print(hex(whoAmI))

This code example above will read the who am I register on the LSM6DSL and should respond with 0x6A according to the data sheet. Which was confirmed by running the script.

When we work with SPI, we can use either the Xfer or Xfer2 function. The difference between the two types of transfer is if the CS is de-asserted:

  • Xfer — Chip Select is de-asserted between blocks
  • Xfer2 — Chip Select is not de-asserted between blocks

Remember with SPI, we need to provide clocks for the read packets as well, hence the packet of 0x00 in the ReadRegister function.

Now we know how we can work with SPI and I2C from our Python / PYNQ environment. This makes developing our applications much faster and easier.

See My FPGA / SoC Projects: Adam Taylor on Hackster.io

Get the Code: ATaylorCEngFIET (Adam Taylor)

Access the MicroZed Chronicles Archives with over 300 articles on the FPGA / Zynq / Zynq MpSoC updated weekly at MicroZed Chronicles.

Adam Taylor
Adam Taylor is an expert in design and development of embedded systems and FPGA’s for several end applications (Space, Defense, Automotive)
Latest articles
Sponsored articles
Related articles
Latest articles
Read more
Related articles