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:
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-smbus package.
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:
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 <number>
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
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
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
address = 0x80 | regAddr — set the read bit
resp = spi.xfer2([address,0x00])
return resp — return read results
whoAmI = readRegister(0x0F)
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.