Regular readers of this blog and my Hackster projects will note that I tend to use either bare metal applications or PYNQ when developing software as part of them.
Of course, we also develop solutions using PetaLinux and that can be very useful when we want to run a minimal embedded Linux solution. The embedded Linux solution provides easy access to networking, along with many higher levels of software abstraction and drivers. This can make the development of software applications much faster and easier than if we had to start from scratch with a bare metal approach.
If you are more familiar with bare metal development, you may be a little intimidated at first by developing a PetaLinux solution. However, it is not as daunting as you might think and has several benefits when it comes to working with sensor interfaces etc. It also is a very similar approach to working with both I2C and SPI.
For this blog, I am going to use the ZUBoard because it has a I2C temperature sensor which is accessible via the PL and a SPI pressure sensor. This also reports temperature which is connected to the PS MIO.
This project used Vivado, Vitis and PetaLinux version 2023.1 and was developed on a Ubuntu Linux development machine.
To get started, we first need to create a new project which targets the ZUBoard. Once the project has been created, we need to create a new block diagram and add the following:
Zynq MPSOC processing module configured for the ZUBoard
Re-customize the MPSoC to enable SPI1 as available via EMIO
Enable PL to PS interrupts
AXI IIC temperature interface (this can be added from the boards tab of Vivado)
AXI IIC SYZYGY DNA (this can be added from the boards tab of Vivado)
AXI IIC Click I2C (this can be added from the board tab of Vivado)
This will provide us with the ability to use the platform to talk with the AXI temperature sensor on board, SPI, and I2C Click interfaces and also obtain SYZYGY DNA information.
Use a concatenate block to connect the interrupts from the AXI IIC modules to the MPSOC interrupt connection. Once the block diagram has been created, it should look like the example below. A TCL script is attached to recreate this if desired.
With the design completed, the next step is the build the bitstream and export the XSA so we can use it to create a new PetaLinux project.
Within PetaLinux, we need to do the following:
Create a project targeting the Zynq MPSOC.
Configure the project using the XSA from Vivado.
Configure the Kernel to include user mode SPI device driver support.
Configure the root FS to include I2C tools.
Modify the system-user.dsti device tree to enable spidev on both SPI ports and enable the AXI IIC.
The instructions to do this are as follows:
petalinux-create -t project -n ZUBoard_sensors --template zynqMP
petalinux-config --get-hw-description=/home/adiuvo/hdl_projects/zuboard_petalinux
petalinux-config -c kernel
petalinux-config -c rootf
petalinux-build
petalinux-package --boot --fsbl zynqmp_fsbl.elf --u-boot u-boot.elf --pmufw pmufw.elf --fpga system.bit --force
The device tree is
/include/ "system-conf.dtsi"
/ {
};
&spi0 {
status = "okay";
spidev@0 {
compatible = "rohm,dh2228fv";
spi-max-frequency = <50000000>;
reg = <0>;
};
};
&spi1 {
status = "okay";
spidev@0 {
compatible = "rohm,dh2228fv";
spi-max-frequency = <50000000>;
reg = <0>;
};
};
&axi_iic_0 {
clock-frequency = <100000>;
status = "okay";
};
&axi_iic_1 {
clock-frequency = <100000>;
status = "okay";
};
&axi_iic_2 {
clock-frequency = <100000>;
status = "okay";
};
An important thing to note is that we have used the binding “spidev” in previous blogs and projects in the device tree. This is not actually correct because post-2022 versions need to bind in the device tree to a supported device listed in spidev.c. For this, I used the Rohm component. Another alternative is to patch the spidev.c to support spidev as a valid binding.
This will build the PetaLinux project and create the boot.bin file along with the image.ub. Image.ub is a uboot wrapped Flattened Image Tree (FIT) image. This FIT image contains the kernel, Device Tree Blob (DTB) and in this case the root file system. This means we are working with a RAM-based root file system for this application and any changes will not persist between power cycles or resets.
Having obtained the boot.bin and the image.ub, we can copy them onto a SD Card and boot the ZUBoard.
With a PetaLinux image running on the ZUBoard and with the ZUBoard connected to a network via its ethernet connector, we can begin to check that the PetaLinux image has everything we need in it.
The first thing to do is open a terminal and obtain the IP address of the board running the command ifconfig. We will need this later for our software debugging.
The next thing is to check that all of the I2C and SPI devices are recognized with the correct drivers.
In the terminal issue, the command ls /dev. Make sure and check that you see the I2C-x and SPIDEVx.x listed.
One thing you will notice on the spidev is the numbering. SPI0 in the MPSoC has become SPI1 and SPI1 has become SPI2. This is because the master device tree for the ZUBoard defines the QSPI as SPI0 in the device tree alias list.
In the spidev binding, the first number relates to the SPI device which, in this case, is 1 or 2. The number following the decimal point represents the chip select for that SPI device.
It’s good to be able to see the I2C and spidev because it indicates we will be able to create applications to work with them both.
At this point we can also use I2C tools to detect what is connected to the I2C buses and interact with them if we require.
Running the command sudo i2cdetect -l will list all of the I2C controllers registered in the system.
If we want to see what is connected to a particular I2C bus, we can run the command sudo i2cdetect -y -r <0/1/2/3> where we select bus 1-3 for probing.
In this case, we can see the temperature sensor connected to I2C-3. If we want to interact with the sensor at this address, we can read the whoami register. This can be achieved using the command sudo i2cget 3 0x3f 0x01. This is getting from I2C bus 3, at device address 0x3F the contents of register 0x01 which is the whoami register on the temp sensor. According to the data sheet the value of 0xA0 is the correct result.
Now we know we can read data from I2C buses, we are ready to start developing an application which measures both the temperatures available via SPI and I2C.
To get started, we need to create a new Vitis application targeting the XSA which was exported from Vivado. When you select the processor in the new application creation process, select PSU_Cortexa53_SMP. This will indicate that you are developing a Linux-based application.
With the application created, we are going to create a SW application which does the following:
Uses spidev and IOCTL to talk to the LPS22CH pressure sensor which also has a temperature channel
Uses I2CDev and IOCTL to talk to the STTS22HTR temperature sensor
IOCTL is one of the elements we can use from the Linux user space to access real world sensors and drives. It is short for input / output control and is one of the most common ways for us to interface with device drivers.
Each IOCTL device we want to interact with provides its own data structures, masks etc. to work with the underlying device.
In our software application, we will do the following:
Open the SPI and I2C devices for reading and writing using IOCTL.
Configure the SPI port via its IOCTL settings. We need mode 3 according to the datasheet.
Read the SPI pressure sensor whoami register to ensure we detect it properly.
Write to the SPI pressure sensors register using IOTCL to enable the sensor sampling at a define rate of 10Hz.
Read the I2C temperature sensors whoami register using i2cdev IOCTL to ensure we detect it properly.
Write to the I2C temperature sensor registers to enable operation using IOCTL.
Enter a loop where the SPI and I2C temperature sensors are sampled every second, outputting them both and the difference between the two to the terminal.
The SPI and I2C temperature sensors are located at different corners of the board. One is close to the power supplies the other not.
#include <stdint.h>
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <getopt.h>
#include <fcntl.h>
#include <time.h>
#include <sys/ioctl.h>
#include <linux/ioctl.h>
#include <sys/stat.h>
#include <linux/types.h>
#include <linux/spi/spidev.h>
#include <linux/i2c-dev.h>
#include <linux/i2c.h>
#define ARRAY_SIZE(array) sizeof(array)/sizeof(array[0])
static const char *spi_device = "/dev/spidev1.0";
static const char *i2c_device = "/dev/i2c-3";
static const char address = 0x3F;
static uint8_t bits = 8;
static uint32_t speed = 1000000;
static uint32_t mode =SPI_MODE_3 ;
static uint16_t delay;
int main(){
int spi;
int i2c;
int ret = 0;
unsigned char spi_tx_buf[2];
unsigned char spi_rx_buf[2];
unsigned char i2c_tx_buf[2];
unsigned char i2c_rx_buf[2];
int temp;
float spi_temp_degC;
int16_t spi_temperature;
float i2c_temp_degC;
int16_t i2c_temperature;
char temp_l;
char temp_h;
struct i2c_msg msgs[2];
struct i2c_rdwr_ioctl_data msgset[1];
struct spi_ioc_transfer tr = {
.tx_buf = (unsigned long)spi_tx_buf,
.rx_buf = (unsigned long)spi_rx_buf,
.len = ARRAY_SIZE(spi_tx_buf),
.delay_usecs = delay,
.speed_hz = 0,
.bits_per_word = 0,
};
//Open SPI and I2C
spi = open(spi_device, O_RDWR);
if (spi < 0)
printf("can't open SPI device\n\r");
i2c = open(i2c_device, O_RDWR);
if (i2c < 0)
printf("can't open I2C device\n\r");
//configure SPI
ret = ioctl(spi, SPI_IOC_WR_MODE32, &mode);
if (ret == -1)
printf("can't set spi mode\n\r");
ret = ioctl(spi, SPI_IOC_RD_MODE32, &mode);
if (ret == -1)
printf("can't get spi mode\n\r");
ret = ioctl(spi, SPI_IOC_WR_BITS_PER_WORD, &bits);
if (ret == -1)
printf("can't set bits per word\n\r");
ret = ioctl(spi, SPI_IOC_RD_BITS_PER_WORD, &bits);
if (ret == -1)
printf("can't get bits per word\n\r");
ret = ioctl(spi, SPI_IOC_WR_MAX_SPEED_HZ, &speed);
if (ret == -1)
printf("can't set max speed hz\n\r");
ret = ioctl(spi, SPI_IOC_RD_MAX_SPEED_HZ, &speed);
if (ret == -1)
printf("can't get max speed hz\n\r");
//enable SPI sensor sampling
spi_tx_buf[0] = (char) 0x10; //enable sampling
spi_tx_buf[1] = (char) 0x20;
ret = ioctl(spi, SPI_IOC_MESSAGE(1), &tr);
if (ret == -1)
printf("IOCTL error\n\r");
//check SPI temp sensor can be detected
spi_tx_buf[0] = (char) 0x8F; // read who am I
spi_tx_buf[1] = (char) 0x00;
ret = ioctl(spi, SPI_IOC_MESSAGE(1), &tr);
if (ret == -1)
printf("IOCTL error\n\r");
if (spi_rx_buf[1] == 0xb3)
printf("LPS22 Detected\n\r");
//check I2C temp sensor can be detected
i2c_tx_buf[0] = 0x01;
msgs[0].addr = address;
msgs[0].flags = 0;
msgs[0].len = 1;
msgs[0].buf = i2c_tx_buf;
msgs[1].addr = address;
msgs[1].flags = I2C_M_RD;
msgs[1].len = 1;
msgs[1].buf = i2c_rx_buf;
msgset[0].msgs = msgs;
msgset[0].nmsgs = 2;
if (ioctl(i2c, I2C_RDWR, &msgset) < 0) {
perror("ioctl(I2C_RDWR) in i2c_read");
}
if (i2c_rx_buf[0] == 0xa0)
printf("STTS22H Detected\n\r");
//set sampling on the I2C temp sensor
i2c_tx_buf[0] = 0x04;
i2c_tx_buf[1] = 0x0c;
msgs[0].addr = address;
msgs[0].flags = 0;
msgs[0].len = 2;
msgs[0].buf = i2c_tx_buf;
msgset[0].msgs = msgs;
msgset[0].nmsgs = 1;
if (ioctl(i2c, I2C_RDWR, &msgset) < 0)
perror("ioctl(I2C_RDWR) in i2c_write");
while(1){
//read temperature from the SPI sensor
spi_tx_buf[0] = (char) 0xab;
spi_tx_buf[1] = (char) 0x00;
ret = ioctl(spi, SPI_IOC_MESSAGE(1), &tr);
if (ret == -1)
printf("IOCTL error\n\r");
temp_l = spi_rx_buf[1];
spi_tx_buf[0] = (char) 0xac;
spi_tx_buf[1] = (char) 0x00;
ret = ioctl(spi, SPI_IOC_MESSAGE(1), &tr);
if (ret == -1)
printf("IOCTL error\n\r");
temp_h = spi_rx_buf[1];
temp = ((temp_h <<8)|(temp_l));
if ((temp & 0x8000) == 0) //msb = 0 so not negative
{
spi_temperature = temp;
}
else
{
// Otherwise perform the 2's complement math on the value
spi_temperature = (~(temp - 0x01)) * -1;
}
spi_temp_degC = (float) spi_temperature /100.0;
//Read temperature from the I2C sensor
i2c_tx_buf[0] = 0x06;
msgs[0].addr = address;
msgs[0].flags = 0;
msgs[0].len = 1;
msgs[0].buf = i2c_tx_buf;
msgs[1].addr = address;
msgs[1].flags = I2C_M_RD;
msgs[1].len = 2;
msgs[1].buf = i2c_rx_buf;
msgset[0].msgs = msgs;
msgset[0].nmsgs = 2;
if (ioctl(i2c, I2C_RDWR, &msgset) < 0) {
perror("ioctl(I2C_RDWR) in i2c_read");
}
i2c_temperature = i2c_rx_buf[1] << 8 | i2c_rx_buf[0];
i2c_temp_degC = (float) i2c_temperature / 100;
printf("SPI Temp Deg C %4.2f I2C Temp Deg C %4.2f Difference Temp Deg C %4.2f \n\n\r",spi_temp_degC, i2c_temp_degC, (spi_temp_degC - i2c_temp_degC) );
usleep(1000000);
}
}
With the application created, we need to debug it on the target. Here is where we need to remember the IP address of the ZUBoard. In Vitis, create a new system debug which attaches to a Linux target. Select a new target connection, enter in the IP address and select check connection.
When we debug the application, this will then download the elf to the ZUBoard and set it up for debugging in Vitis so we can step through the application if we wish.
Running the application will show the output of the application in the console window.
Here we have a pretty in depth look at how we can work with embedded Linux, IOCTL, I2C,and SPI. I am sure this will be useful to readers and serve as a great memory aid to myself also.
I am going to take a closer look at PetaLinux over the next few weeks and months because it is an incredibly useful element in our tool box.
All of the files necessary to recreate this, are located here https://github.com/ATaylorCEngFIET/MZ507
Workshops and Webinars
If you enjoyed the blog why not take a look at the free webinars, workshops and training courses we have created over the years. Highlights include
Introduction to Vivado learn how to use AMD Vivado
Ultra96, MiniZed & ZU1 three day course looking at HW, SW and Petalinux
Arty Z7-20 Class looking at HW, SW and Petalinux
Mastering MicroBlaze learn how to create MicroBlaze solutions
HLS Hero Workshop learn how to create High Level Synthesis based solutions
Perfecting Petalinux learn how to create and work with petalinux OS
Embedded System Book Do you want to know more about designing embedded systems from scratch? Check out our book on creating embedded systems. This book will walk you through all the stages of requirements, architecture, component selection, schematics, layout, and FPGA / software design. We designed and manufactured the board at the heart of the book! The schematics and layout are available in Altium here Learn more about the board (see previous blogs on Bring up, DDR validation, USB, Sensors) and view the schematics here.
Sponsored by AMD
Comments