To implement a SPI interface from the Linux user space running on a Zynq FPGA, one would more than likely use the SPIdev driver. I recently went to use it in a project again and was reminded of some of the subtleties in its deployment, which made me realize it was worth a tutorial of its own.
I am using the ZynqberryZero FPGA development board in this particular instance, but this writeup is focused on the Zynq-7000 itself so it can be applied to any Zynq-7000 FPGA development board.
I am also using Vivado and PetaLinux version 2022.1, but since I'm focused on the core concept of SPIdev on Zynq-7000 this tutorial should be pretty directly applicable to any version of Vivado/PetaLinux unless explicitly stated otherwise.
Note: even though this tutorial is fairly universal across Vivado/PetaLinux types, this does not mean one should ever mix & match Vivado and PetaLinux versions (for example - DO NOT import a Vivado 2022.1 XSA into a PetaLinux 2022.2 project). Always use the same version of Vivado and PetaLinux for a given project, otherwise you're on your own summoning whatever error demon appears as a result....
PS -Autocorrect was pretty determined to "correct" all instances of "spidev" to "spider". My apologies if I missed one of these "corrections".
Enable SPI in Zynq PSFirst things first, the physical SPI interface needs to be instantiated in the hardware design. There are a few options for this. The Zynq-7000 processing system (PS) has two SPI interfaces built into it, or a SPI interface can be deployed in the programmable logic of the Zynq using either the AXI Quad SPI IP or some custom user SPI IP.
The simplest and easiest to use are the SPI interfaces built into the Zynq PS. They are found in the configuration window of the Zynq PS under MIO Configuration > I/O Peripherals.
Note there are two SPI interfaces available, SPI0 and SPI1, make note of which number is selected, as this will have an impact later on as to how the SPIdev device number enumerates in the Linux user space.
Furthermore, the SPI interface can be routed via the Zynq's MIO or EMIO pins. The MIO pins are dedicated pins the SPI interfaces are hardwired to (saves the step of specifying package pins in the constraints file). Routing it though the EMIO allows for the user to assign them to the desired package pin on the Zynq FPGA chip.
Enable SPIdev Kernel ModuleThe SPIdev kernel module is how a SPI interface is able to be used as a device in the Linux user space (ie - showing up in the /dev directory of the filesystem). It is not enabled in PetaLinux projects by default though.
Launch the kernel configuration editor:
~/zynqberryzero_prj/zynqberryzero_os$ petalinux-config -c kernel
Then navigate to Device Drivers > SPI Support, and enable User mode SPI device driver support:
While the SPIdev kernel module is the main core driver needed, it's worth noting that some devices may still require their own specific kernel drivers to be enabled as well in order to bind to the SPIdev device. A clue of this is if you see this type of message during the Linux boot process on the Zynq target:
SPI driver fb_uc1701 has no spi_device_id for UltraChip,uc1701
This specific instance is from me enabling the SPIdev kernel (CONFIG_SPI_SPIDEV
), but not the framebuffer kernel module for the UC1701 TFT display (CONFIG_FB_TFT_UC1701
). Other device tree properties may be required as well for specific devices using SPIdev.
The device tree module is the glue between the hardware and the kernel module. Below is the device tree node I use. This was also where I hit an odd stumbling block with PetaLinux 2022.1 that I hadn't encountered in any previous versions I've used the SPIdev driver in. Usually the compatible option is "spidev"
, however PetaLinux 2022.1 refused to work with that (it would build ok, but no SPIdev devices would should up in the /dev
directory on the Zynq). I quickly found others where having the same issue and the fix was to change the compatible option to "rohm,dh2228fv"
.
I can't fully explain this since DH2228FV is a part number for a DAC chip from ROHM Semiconductor, but it's the only thing that worked. I left the spidev compatible option in my device tree node, but just commented it out in the likely case that not all readers are using PetaLinux 2022.1 and need to change it back.
&spi0 {
#address-cells=<1>;
#size-cells=<0>;
status = "okay";
num-cs = <1>;
bus-num = <0>;
is-decoded-cs = <0>;
spidev@0x00 {
//compatible = "spidev";
compatible = "rohm,dh2228fv";
/* Max SPI clock frequency via Zynq EMIO is 25MHz */
spi-max-frequency = <25000000>;
/* slave number - CS */
reg = <0>;
/* Set SPI mode = 0 */
spi-cpol = <0>;
spi-cpha = <0>;
};
};
The node above is assigning the SPI interface a device bus number of 0 with interface number 0.
As I mentioned previously, the SPI number selected in the Zynq PS hardware design in Vivado has an impact on how the SPIdev device number enumerates in the Linux user space. I originally thought whatever number I assigned here in the device tree module would/should override that. I learned the hard way however that this is not the case.
Originally I enabled SPI1 in the Zynq PS in Vivado and assigned it SPIdev device number zero with the device tree node above. This resulted in the SPIdev actually enumerating as device 3... So instead of getting /dev/spidev0.0
as I am assigning with the above device tree node, it was instead /dev/spidev3.0
(I have no idea why it enumerated as device number 3 instead of device number 2).
zynqberryzero_os:~$ ls /dev/spi*
/dev/spidev3.0
Thus I went back and enabled SPI0 in the Zynq PS in Vivado, then with the device tree node above, the SPIdev enumerated as expected:
zynqberryzero_os:~$ ls /dev/spi*
/dev/spidev0.0
SPIdev Test ApplicationWith the SPIdev driver, devices have a limited user space API that supports basic half-duplex read and write access to SPI slave devices. Full duplex transfers and device I/O configurations can be done via ioctl system calls.
There is a really great SPIdev test C application in Linus Torvalds' Github that demonstrates how to use these read/write and ioctl functions. The main concept is opening the SPIdev device from the /dev directory as a file:
static const char *device = "/dev/spidev0.0";
fd = open(device, O_RDWR);
Then write/read to the device the desired settings/functions such as SPI mode:
ret = ioctl(fd, SPI_IOC_WR_MODE32, &mode);
if (ret == -1)
pabort("can't set spi mode");
ret = ioctl(fd, SPI_IOC_RD_MODE32, &mode);
if (ret == -1)
pabort("can't get spi mode");
I recommend building this test application into your PetaLinux project when using SPIdev on a target as a good sanity check for the first boot. Create a new application in the target PetaLinux project and enable it:
~/zynqberryzero_prj/zynqberryzero_os$ petalinux-create -t apps --template c --name spidev-test --enable
This will create a blank spidev-test.c
source file in /<PetaLinux project path>/project-spec/meta-user/recipes-apps/spidev-test/files/
. Either replace it with the spidev-test.c
source file attached below or copy+paste its contents into the generated one. Build the root filesystem before building/rebuilding the PetaLinux project:
~/zynqberryzero_prj/zynqberryzero_os$ petalinux-build -c rootfs
~/zynqberryzero_prj/zynqberryzero_os$ petalinux-build
This test application is a loopback self-test, so the MOSI and MISO pins of the target SPI interface need to be connected to each other. Then the SPIdev test application can be run on the Zynq passing the target SPIdev number as an argument (-D is the device flag):
zynqberryzero_os:~$ /usr/bin/spidev-test -D /dev/spidev0.0 -v
The verbose flag (-v) is also handy here to see the bytes being clocked out of MOSI and read back in on MISO.
And that should pretty well cover the use of SPIdev in Linux on the Zynq-7000!
Comments