Adding SPI to Yocto Linux

In this example you will add three SPI of different kinds: PL with EMIO, PS with MIO, PL AXI.

Procedure

  1. Add new hardware with SPI in Vivado and generate XSA file.
  1. Add new XSA file in Yocto Linux.
  1. Configure Linux kernel to use SPIDEV (setting CONFIG_SPI_SPIDEV=y). We need to add the support for SPIDEV in Linux for SPI to be used from user memory space. Otherwise you would need to write kernel driver for SPI, which is difficult!
  1. Add information to Device Tree that SPIDEV device is connected at SPI node.
  1. Build Yocto SD card image.
  1. Add udev rule in Linux to bind kernel binds spidev to each spi*.0 device.
  1. Basic test of SPI.
  1. [Optional] Add permanently udev rule to Yocto image build process.

Results

Finally, after these steps you will have in Yocto:

  • spi0 = QSPI (flash) → not for spidev
  • spi1 = PS SPI1 (@ e0006000) - Zynq PS: SPI0,EMIO, it will be configured for spidev1 in PMOD JB (pin 1=SS, pin 2=MOSI, pin 3=MISO, pin 4=SCLK).
  • spi2 = PS SPI2 (@ e0007000) - Zynq PS: SPI1,MIO, it will be configured for spidev2 in PMOD JF (pin1=MIO13=SS[0], pin2=MIO10=MOSI, pin3=MIO11=MISO, MIO12=SCLK).
  • spi3 = PL AXI Quad SPI (@ 41e00000) → SPI added in PL by you, it will be configured for spidev3 in PMOD JA (pin 1=SS, pin 2=MOSI, pin 3=MISO, pin 4=SCLK).

1. In Vivado: add new SPI hardware to obtain XSA file

Add SPI in Zynq connected to EMIO (via PL)

Set Zynq block/MIO Configuration/SPI0: set SPI0 to EMIO.

Add SPI in Zynq connected to MIO (via PS)

Set Zynq block/MIO Configuration/SPI0: set SPI1 to MIO 10..15

i.e.:

  • MIO10 = MOSI
  • MIO11 = MISO
  • MIO12 = SCLK
  • MIO13 = SS[0]

For single SPI device, we do not use SS[1] or SS[2].

Add SPI in FPGA

  • Add AXI Quad SPI Ip core.
  • Do NOT use XIP mode nor Performance mode.
  • Mode Standard.
  • Trans. width: 8.
  • Frequency ration: 16x1.
  • No of slaves: 1.
  • Byte level Interrupt Enable: UNCHECKED .
  • Enable FIFO.
  • Fifo Depth 16.
  • Enable STARTUP PRIMITIVE: UNCHECKED.
  • Connect interrupt:
  • Enable interrupts in Zynq (in not already enabled): Zynq/Interrupts/Fabric interrupts/PL-PS Interrupt Ports/IRQ_F2P[15:0].
  • Connect interrupt wire from AXI Quad SPI pin: ip2intc_irpt to Zynq pin: IRQ_F2P. If you already have something connected to Zynq's interrupt input (i.e. UART), then add Concat IP block and use it to join interrupts from various sources. If you already have Concat block, increment its input size, then connect interrupt by dragging a wire connection with the mouse.

Example of XDC (watch out of pin assignments!)

There is no need to add constraints for SPI connected to MIO10…15: it is already constrained and connected to PMOD JF.

##Pmod Header JA (spidev1, Zynq SPI0,EMIO)
set_property -dict { PACKAGE_PIN N15   IOSTANDARD LVCMOS33 } [get_ports { spi_rtl_ss_io }]; #IO_L21P_T3_DQS_AD14P_35 JA-1 SPI:SS
set_property -dict { PACKAGE_PIN L14   IOSTANDARD LVCMOS33 } [get_ports { spi_rtl_io0_io }]; #IO_L22P_T3_AD7P_35 JA-2 SPI:MOSI        
set_property -dict { PACKAGE_PIN K16   IOSTANDARD LVCMOS33 } [get_ports { spi_rtl_io1_io }]; #IO_L24P_T3_AD15P_35 JA-3 SPI:MISO            
set_property -dict { PACKAGE_PIN K14   IOSTANDARD LVCMOS33 } [get_ports { spi_rtl_sck_io }]; #IO_L20P_T3_AD6P_35 JA-4 SPI:CLK
##Pmod Header JB (spidev3, AXI Quad SPI in Zynq PL)
set_property -dict { PACKAGE_PIN V8    IOSTANDARD LVCMOS33     } [get_ports { SPI_0_0_ss_io }]; #IO_L15P_T2_DQS_13 Sch=jb_p[1]   JB-1 SPI:SS            
set_property -dict { PACKAGE_PIN W8    IOSTANDARD LVCMOS33     } [get_ports { SPI_0_0_io0_io }]; #IO_L15N_T2_DQS_13 Sch=jb_n[1]  JB-2 SPI:MOSI       
set_property -dict { PACKAGE_PIN U7    IOSTANDARD LVCMOS33     } [get_ports { SPI_0_0_io1_io }]; #IO_L11P_T1_SRCC_13 Sch=jb_p[2] JB-3 SPI:MISO       
set_property -dict { PACKAGE_PIN V7    IOSTANDARD LVCMOS33     } [get_ports { SPI_0_0_sck_io }]; #IO_L11N_T1_SRCC_13 Sch=jb_n[2] JB-4 SPI:CLK       
set_property -dict { PACKAGE_PIN Y7    IOSTANDARD LVCMOS33     } [get_ports { SPI_0_0_ss1_o }]; #IO_L13P_T2_MRCC_13 Sch=jb_p[3]  JB-7 SPI:SS1 (not used)      
set_property -dict { PACKAGE_PIN Y6    IOSTANDARD LVCMOS33     } [get_ports { SPI_0_0_ss2_o }]; #IO_L13N_T2_MRCC_13 Sch=jb_n[3]  JB-8 SPI:SS2 (not used)              
  • Generate bitstream
  • Export hardware to XSA file

2. Configure Yocto Linux - add new hardware

  • Copy XSA to:
lab_msw_spi/yocto-scarthgap/poky/meta-custom-zybo-z7/recipes-bsp/external-hdf/files/<your_XSA_FILE>
  • Edit file: lab_msw_spi/yocto-scarthgap/poky/meta-custom-zybo-z7/recipes-bsp/external-hdf/external-hdf_%.bbappend

and update the XSA filename (if changed after adding SPI hardware) :

FILESEXTRAPATHS:prepend := "${THISDIR}/files:"
HDF_EXT  = "xsa"
HDF_URI = file://zybo-z7-20-hw-platform_spi.xsa

3. Configure Yocto Linux - enable CONFIG_SPI_SPIDEV in the Yocto kernel

Find the name for the recipe

  • Find the name of virtual provider for the kernel in your project:
bitbake -e core-image-minimal-dev | grep ^PREFERRED_PROVIDER_virtual/kernel=
à PREFERRED_PROVIDER_virtual/kernel="linux-xlnx"
  • Use the  name of the provider (here linux-xlnx) to find the name of the kernel recipe used in your project:
bitbake -e linux-xlnx | grep ^FILE=
à FILE="/home2/ldap/Users/wujek_ldap/lab_msw_spi/yocto-scarthgap/poky/meta-xilinx/meta-xilinx-core/recipes-kernel/linux/linux-xlnx_6.1-v2023.2.bb"
  • Use the name of the recipe (here linux-xlnx_6.1-v2023.2.bb) to create BBAPPEND file to include it in your kernel recipe.

Create BBAPPEND file

  • Create file with the name of  the recipe found above:
nano recipes-kernel/linux/linux-xlnx_6.1-v2023.2.bbappend

with line:

FILESEXTRAPATHS:prepend := "${THISDIR}:"
SRC_URI:append = " file://spidev.cfg"
do_configure:append() {
    bbnote "Merging spidev.cfg (XSCT post-process)"
    KCONFIG_CONFIG="${B}/.config" \
    bash ${S}/scripts/kconfig/merge_config.sh -m -O ${B} \
         ${B}/.config ${WORKDIR}/spidev.cfg
    oe_runmake -C ${S} O=${B} olddefconfig
}

The function do_configure:append() injects spidev.cfg after the XSCT tools from Vivado builds the kernel for Zybo and sets Zybo defaults. The function provides, that the setting CONFIG_SPI_SPIDEV=y will be applied at the end and will not be overwritten by defaults.

Create kernel configuration fragment file

  • Create file:
nano recipes-kernel/linux/linux-xlnx/spidev.cfg

with line:

CONFIG_SPI_SPIDEV=y

Final check

You should have the following files:

meta-custom-zybo-z7/
├── conf
│   ├── layer.conf
│   └── machine
│       └── zybo-z7.conf
├── COPYING.MIT
├── README
├── recipes-bsp
│   ├── bootfiles
│   │   ├── bootfiles.bb
│   │   └── files
│   │       ├── boot.cmd
│   │       └── uEnv.txt
│   ├── device-tree
│   │   ├── device-tree.bbappend
│   │   └── files
│   │       └── system-user.dtsi
│   └── external-hdf
│       ├── external-hdf_%.bbappend
│       └── files
│           └── zybo-z7-20-hw-platform_spi.xsa (updated XSA)
├── recipes-core
│   └── base-files
│   |   └── base-files_%.bbappend
│   └── spidev-udev (new, optional)
│   |   └── spidev-udev_1.0.bb
│   │   └── files
│   │       └── 99-spidev-bind.rules
├── recipes-kernel (new)
│   ├── linux (new)
│   │   ├── linux-xlnx_6.1-v2023.2.bbappend ß(added some lines)
│   │   └── spidev.cfg ß(added CONFIG_SPI_SPIDEV=y)
└── wks
    └── wic-zynq.wks

Check if setting will work (replace ~/yocto_tmp with your $TMPDIR):

bitbake -c cleansstate linux-xlnx
bitbake -c configure linux-xlnx
grep CONFIG_SPI_SPIDEV $(find ~/yocto_tmp/work -type f -name ".config" -path "*/linux-xlnx/*"); 

You should get from the grep:

CONFIG_SPI_SPIDEV=y

4. Configure Yocto Linux - add a spidev@N child node in Device Tree

  • Build your device tree:
bitbake device-tree

and decode your current device tree to find the names and addresses of SPI nodes:

cp ~/yocto_tmp/deploy/images/zybo-z7/devicetree/system-top.dtb .
dtc -I dtb -O dts system-top.dtb  > system-top.dts 

Example content of this file (use your SPI node names from __symbols__):

spi@e0006000 {
           compatible = "xlnx,zynq-spi-r1p6";
           reg = <0xe0006000 0x1000>;
           status = "okay";
           interrupt-parent = <0x04>;
           interrupts = <0x00 0x1a 0x04>;
           clocks = <0x01 0x19 0x01 0x22>;
           clock-names = "ref_clk\0pclk";
           #address-cells = <0x01>;
           #size-cells = <0x00>;
           is-decoded-cs = <0x00>;
           num-cs = <0x03>;
           phandle = <0x20>;
spi@e0007000 {
           compatible = "xlnx,zynq-spi-r1p6";
           reg = <0xe0007000 0x1000>;
           status = "okay";
           interrupt-parent = <0x04>;
           interrupts = <0x00 0x31 0x04>;
           clocks = <0x01 0x1a 0x01 0x23>;
           clock-names = "ref_clk\0pclk";
           #address-cells = <0x01>;
           #size-cells = <0x00>;
           is-decoded-cs = <0x00>;
           num-cs = <0x01>;
           phandle = <0x21>;
       };
spi@e000d000 {
           compatible = "xlnx,zynq-qspi-1.0";
           reg = <0xe000d000 0x1000>;
           interrupt-parent = <0x04>;
           interrupts = <0x00 0x13 0x04>;
           clocks = <0x01 0x0a 0x01 0x2b>;
           clock-names = "ref_clk\0pclk";
           status = "okay";
           #address-cells = <0x01>;
           #size-cells = <0x00>;
           is-dual = <0x00>;
           num-cs = <0x01>;
           spi-rx-bus-width = <0x04>;
           spi-tx-bus-width = <0x04>;
           phandle = <0x22>;
       };
aliases {
       ethernet0 = "/axi/ethernet@e000b000";
       serial0 = "/axi/serial@e0001000";
       spi0 = "/axi/spi@e000d000";
       spi1 = "/axi/spi@e0006000";
       spi2 = "/axi/spi@e0007000";
       spi3 = "/amba_pl/axi_quad_spi@41e00000";
   };
__symbols__ {
       (. . . )
       spi0 = "/axi/spi@e0006000";
      spi1 = "/axi/spi@e0007000";
      qspi = "/axi/spi@e000d000";
      axi_quad_spi_0 = "/amba_pl/axi_quad_spi@41e00000";
       (. . . )
   };
  • Add new SPIDEV sub-nodes to your Device Tree in your existing DSTI file:
nano recipes-bsp/device-tree/files/system-user.dtsi 

with the content:

/* ============================
 * PS SPI @ e0006000
 * Labels per __symbols__: &spi0
 * Will appear as: /dev/spidev1.0 (because of 'aliases' mapping)
 * ============================ */
&spi0 {
    spidev_ps0_cs0: spidev@0 {
        compatible = "spidev";
        reg = <0>;                  // CS0
        spi-max-frequency = <1000000>;
        status = "okay";
    };
};
/* ============================
 * PS SPI @ e0007000
 * Labels per __symbols__: &spi1
 * Will appear as: /dev/spidev2.0 (per 'aliases')
 * ============================ */
&spi1 {
    status = "okay";
    spidev_ps1_cs0: spidev@0 {
        compatible = "spidev";
        reg = <0>;                  // CS0
        spi-max-frequency = <1000000>;
        status = "okay";
    };
};
/* ============================
 * PL AXI Quad SPI @ 0x41e00000
 * Label per __symbols__: &axi_quad_spi_0
 * Will appear as: /dev/spidev3.0 (per 'aliases')
 * ============================ */
&axi_quad_spi_0 {
    #address-cells = <1>;
    #size-cells = <0>;
    status = "okay";
    spidev_pl0_cs0: spidev@0 {
        compatible = "spidev";
        reg = <0>;                  // CS0
        spi-max-frequency = <1000000>;
        status = "okay";
    };
};

5. Build Yocto

Build the WIC SC card image:

bitbake core-image-minimal-dev

and burn the WIC image

core-image-minimal-dev-zybo-z7.rootfs-*.wic.gz

using Balena Etcher to SD card.

After building and booting, in Yocto Linux, you should have the following spidev devices in /dev:

ls /dev/spi*
/dev/spidev1.0  /dev/spidev2.0  /dev/spidev3.0

After success, you can perform further test of the new SPIs described in the separate document.

6. Add udev rule in Linux to bind kernel binds spidev to each spi*.0 device [ON ZYBO BOARD]

This step is not needed when you add permanently udev rule to Yocto image build process (described above).

  • On RUNNING Zybo board, create a udev rule in file (create this file):
nano /etc/udev/rules.d/99-spidev-bind.rules

with the following content:

ACTION=="add", SUBSYSTEM=="spi", KERNEL=="spi*.0", \
    RUN+="/bin/sh -c 'echo spidev > /sys/bus/spi/devices/%k/driver_override; \
                      echo %k > /sys/bus/spi/drivers/spidev/bind'"
  • Make sure the file is readable:
chmod 644 /etc/udev/rules.d/99-spidev-bind.rules
  • Finally:
reboot

After this you should see the files in /dev:

/dev/spidev1.0  
/dev/spidev2.0  
/dev/spidev3.0

7. Generic test of SPI in Python

It is required to have running Python3 and python3-spidev in Zybo board to run this test. See another document to get information how to install it).

  • Create file ./spi_test.py:
#!/usr/bin/env python3
# SPI Loopback Tester for Zybo Z7-20
import sys
bus = 3
device = 0
if len(sys.argv) > 1:
    try:
        arg = sys.argv[1]
        bus = int(arg)
    except:
        print("Usage: spi_test.py [bus_number]")
        sys.exit(1)
print(f"Using SPI bus {bus}: /dev/spidev{bus}.0")
print(f"  /dev/spidev1.0 PMOD JB (PS Zynq SPI0)")
print(f"  /dev/spidev2.0 PMOD JF (PS Zynq SPI1)")
print(f"  /dev/spidev3.0 PMOD JA (PL AXI QuadSPI)")
TEST_BYTES = [0x01, 0x02, 0x03, 0x04]
print(f"  pin2 MISO - pin 3 MOSI disconnected: [255, 255, 255, 255]")
print(f"  pin2 MISO - pin 3 MOSI connected:    {TEST_BYTES}")
try:
    import spidev
    spi = spidev.SpiDev()
    spi.open(bus, device)   # /dev/spidev{bus}.{device}
    spi.max_speed_hz = 500000
    resp = spi.xfer2(TEST_BYTES)
    print("TEST RESULT:")
    print("  TX:", TEST_BYTES)
    print("  RX:", resp)
except Exception as e:
    print("SPI test failed:", e)
  • Make it executable:
chmod u+x spi_test.py
  • Connect with the wire the pin 2 (MISO) and pin 3 (MOSI) of PMOD connector:
  • spidev1: PMOD JB
  • spidev2: PMOD JF
  • spidev3: PMOD JA
  • Run the test:
./spi_test.py <spidev_number_1...3>

You should get:

[1, 2, 3, 4] when pins MISO and MOSI are connected.

[255, 255, 255, 255] when pins MISO and MOSI pins are disconnected.

For example:

root@mw-linux:~# ./spi_test.py 2
Using SPI bus 2: /dev/spidev2.0
  /dev/spidev1.0 PMOD JB (PS Zynq SPI0)
  /dev/spidev2.0 PMOD JF (PS Zynq SPI1)
  /dev/spidev3.0 PMOD JA (PL AXI QuadSPI)
  pin2 MISO - pin 3 MOSI disconnected: [255, 255, 255, 255]
  pin2 MISO - pin 3 MOSI connected:    [1, 2, 3, 4]
TEST RESULT:
  TX: [1, 2, 3, 4]
  RX: [1, 2, 3, 4]

8. [Optional] Add permanently udev rule to Yocto image build process

Advantage: no need to add udev rule after each rebuild of Yocto image.

  • Copy udev rule (created eariler in Zybo board:/etc/udev/rules.d/99-spidev-bind.rules) to:
poky/meta-custom-zybo-z7/recipes-core/spidev-udev/files/99-spidev-bind.rules
  • Create bb file:
poky/meta-custom-zybo-z7/recipes-core/spidev-udev/spidev-udev_1.0.bb

with the content:

SUMMARY = "udev rule to auto-bind spidev to SPI devices"
LICENSE = "CLOSED"
SRC_URI = "file://99-spidev-bind.rules"
S = "${WORKDIR}"
do_install() {
    install -d ${D}${sysconfdir}/udev/rules.d
    install -m 0644 ${WORKDIR}/99-spidev-bind.rules \
        ${D}${sysconfdir}/udev/rules.d/99-spidev-bind.rules
}
FILES:${PN} += "${sysconfdir}/udev/rules.d/99-spidev-bind.rules"
  • Add to file poky/build/conf/local.conf:
IMAGE_INSTALL:append = " spidev-udev"