Skip to main content

SPI Communication

In this chapter, we will learn how to communicate with external devices using the SPI bus at the application layer.

Sample program : Code.zip

1. SPI Subsystem

In the Linux operating system, the SPI subsystem is a crucial driver framework used to manage and control various external devices connected via the SPI bus. For more detailed information about the SPI subsystem, you can refer to the <Linux kernel source>/Documentation/spi directory. Key components of the SPI subsystem include:

  1. sysfs Interface: The SPI subsystem provides a set of files and directories through sysfs for configuring and managing SPI buses and SPI devices. These files and directories are typically located under /sys/class/spi_master and /sys/bus/spi/devices, allowing users to view and modify the properties of SPI devices.
  2. Device Nodes: Each connected SPI device creates a device node under the /dev directory, enabling communication with the device from user-space programs using standard file I/O operations. Typically, these device nodes have names like /dev/spidevX.Y, where X represents the SPI bus number, and Y represents the SPI device number.

2. Viewing SPI (Shell)

2.1 Pinout

To enable I2C, refer to the PWM section.

  1. LuckFox Pico Diagram:

  2. LuckFox Pico Mini A/B Diagram:

  3. LuckFox Pico Plus Diagram:

2.2 Viewing Devices

In the /sys/bus/spi/devices directory, each SPI device has its own folder. These folders typically include "spi" and the device number in their names. For example, /sys/bus/spi/devices/spi0.0 represents device 0 on SPI bus 0. To view the available SPI buses in the system, you can use the following command:

# ls /sys/bus/spi/devices/
spi2.0 spi0.0

3. SPI Communication (Python Program)

3.1 Complete Code

With the following program, SPI communication can be implemented.

import spidev

def main():
tx_buffer = [ord(char) for char in "hello world!"]
rx_buffer = [0] * len(tx_buffer)

try:
spi = spidev.SpiDev()
spi.open(0, 0)
spi.max_speed_hz = 1000000

rx_buffer = spi.xfer2(tx_buffer[:])
print("tx_buffer:\n\r", ''.join(map(chr, tx_buffer)))
print("rx_buffer:\n\r", ''.join(map(chr, rx_buffer)))

except Exception as e:
print(f"An error occurred: {e}")

finally:
if spi:
spi.close()

if __name__ == "__main__":
main()

3.2 Open SPI Device

This code uses the SpiDev class from the spidev library to create an SPI object. By calling the open method, the SPI bus and device number are specified, which are SPI bus 0 and device 0 here. The maximum transfer speed of the SPI is set to 1,000,000 Hz, i.e., 1 MHz. This step is to configure the basic parameters of the SPI device for subsequent data transmission.

spi = spidev.SpiDev()
spi.open(0, 0)
spi.max_speed_hz = 1000000

3.3 Data Transmission and Reception

This code uses the xfer2 method for SPI data transfer. In this example, tx_buffer is the data to be sent, and rx_buffer will store the received data. Note that to ensure the original value of tx_buffer remains unchanged, a copy of tx_buffer is passed to xfer2, i.e., tx_buffer[:]. Finally, the print statement prints the sent and received data as strings for user inspection.

rx_buffer = spi.xfer2(tx_buffer[:])
print("tx_buffer:\n\r", ''.join(map(chr, tx_buffer)))
print("rx_buffer:\n\r", ''.join(map(chr, rx_buffer)))

3.4 Run the Program

  1. Use the vi tool to open the file, paste the code, and save it.

    # nanoo i2c.py
  2. Run the program.

    # python3 i2c.py
  3. Experimental Observations

    Connect the MOSI and MISO of SPI and run the program:
    image

4. SPI Communication (C Program)

4.1 ioctl Function

When writing application programs, the ioctl function is used to configure SPI-related settings. Its function prototype is as follows:

 #include <sys/ioctl.h>

int ioctl(int fd, unsigned long request, ...);

When using the ioctl function for SPI communication, commonly used request parameters include:

  • SPI_IOC_RD_MODE: Used to read the current mode settings of SPI communication. This request parameter reads mode information into an integer variable for checking the current polarity and phase settings of SPI communication.
  • SPI_IOC_WR_MODE: Used to set the mode of SPI communication. You need to provide an integer value, typically composed of two binary digits, to represent the polarity and phase of SPI communication.
  • SPI_IOC_RD_BITS_PER_WORD: Used to read the number of bits per data word. This request parameter reads the bit count information into an integer variable.
  • SPI_IOC_WR_BITS_PER_WORD: Used to set the number of bits per data word. You need to provide an integer value to specify the number of bits per data word to be sent and received.
  • SPI_IOC_RD_MAX_SPEED_HZ: Used to read the maximum speed of the SPI bus. This request parameter reads the speed information into an integer variable to check the maximum transfer speed of the current SPI bus.
  • SPI_IOC_WR_MAX_SPEED_HZ: Used to set the maximum speed of the SPI bus. You need to provide an integer value to specify the maximum transfer speed to be used.
  • SPI_IOC_MESSAGE(N): Used to perform read and write operations for SPI transfers. This request parameter requires a pointer to an array of struct spi_ioc_transfer elements, with each element describing an SPI transfer operation, allowing multiple operations to be executed.

4.2 Sample Program

The following program demonstrates SPI communication.

#include <stdio.h>
#include <stdlib.h>
#include <stdint.h>
#include <fcntl.h>
#include <unistd.h>
#include <linux/spi/spidev.h>
#include <sys/ioctl.h>

#define SPI_DEVICE_PATH "/dev/spidev0.0"

int main() {
int spi_file;
uint8_t tx_buffer[50] = "hello world!";
uint8_t rx_buffer[50];

// Open the SPI device
if ((spi_file = open(SPI_DEVICE_PATH, O_RDWR)) < 0) {
perror("Failed to open SPI device");
return -1;
}

// Configure SPI mode and bits per word
uint8_t mode = SPI_MODE_0;
uint8_t bits = 8;
if (ioctl(spi_file, SPI_IOC_WR_MODE, &mode) < 0) {
perror("Failed to set SPI mode");
close(spi_file);
return -1;
}
if (ioctl(spi_file, SPI_IOC_WR_BITS_PER_WORD, &bits) < 0) {
perror("Failed to set SPI bits per word");
close(spi_file);
return -1;
}

// Perform SPI transfer
struct spi_ioc_transfer transfer = {
.tx_buf = (unsigned long)tx_buffer,
.rx_buf = (unsigned long)rx_buffer,
.len = sizeof(tx_buffer),
.delay_usecs = 0,
.speed_hz = 1000000, // SPI speed in Hz
.bits_per_word = 8,
};

if (ioctl(spi_file, SPI_IOC_MESSAGE(1), &transfer) < 0) {
perror("Failed to perform SPI transfer");
close(spi_file);
return -1;
}

/* Print tx_buffer and rx_buffer*/
printf("\rtx_buffer: \n %s\n ", tx_buffer);
printf("\rrx_buffer: \n %s\n ", rx_buffer);

// Close the SPI device
close(spi_file);

return 0;
}

4.3 File Paths

This line of code defines a macro used to store the path to the SPI device file.

#define SPI_DEVICE_PATH "/dev/spidev0.0"

4.4 Opening the SPI Device

This section of code attempts to open the specified SPI device file.

// Open the SPI device
if ((spi_file = open(SPI_DEVICE_PATH, O_RDWR)) < 0) {
perror("Failed to open SPI device");
return -1;
}

4.5 Configuring SPI

This code is used to configure the SPI communication mode as SPI Mode 0 (clock polarity 0, clock phase 0) and set the word size to 8 bits to ensure the correctness and consistency of SPI communication.

// Configure SPI mode and bits per word
uint8_t mode = SPI_MODE_0;
uint8_t bits = 8;
if (ioctl(spi_file, SPI_IOC_WR_MODE, &mode) < 0) {
perror("Failed to set SPI mode");
close(spi_file);
return -1;
}
if (ioctl(spi_file, SPI_IOC_WR_BITS_PER_WORD, &bits) < 0) {
perror("Failed to set SPI bits per word");
close(spi_file);
return -1;
}

It defines a spi_ioc_transfer structure variable named transfer for configuring the parameters of the SPI transfer. It specifies the data buffers for the transfer, the length of the transfer, the delay, the SPI speed in Hertz, and the word size. This structure will be passed to the SPI_IOC_MESSAGE ioctl function to perform the SPI transfer.

// Perform SPI transfer
struct spi_ioc_transfer transfer = {
.tx_buf = (unsigned long)tx_buffer,
.rx_buf = (unsigned long)rx_buffer,
.len = sizeof(tx_buffer),
.delay_usecs = 0,
.speed_hz = 1000000, // SPI speed in Hz
.bits_per_word = 8,
};

4.6 Sending Data

This code uses the ioctl function to perform the SPI transfer. It specifies the number of transfers as 1 using the SPI_IOC_MESSAGE(1) macro, with the third argument being the address of the transfer structure variable configured earlier. If the SPI transfer fails, an error message will be printed, and the SPI device file descriptor will be closed.

if (ioctl(spi_file, SPI_IOC_MESSAGE(1), &transfer) < 0) {
perror("Failed to perform SPI transfer");
close(spi_file);
return -1;
}

4.7 Cross-Compilation

  1. Specify the Cross-Compilation Tool

    First, you need to add the path to the cross-compilation tool to the system's PATH environment variable so that you can use the cross-compilation tool from anywhere. You can add the following line to your shell configuration file (usually ~/.bashrc or ~/.bash_profile or ~/.zshrc, depending on your shell). Note that the path after PATH= should point to the directory where the cross-compilation tool is located.

    • gcc path

      <SDK Directory>/tools/linux/toolchain/arm-rockchip830-linux-uclibcgnueabihf/bin/arm-rockchip830-linux-uclibcgnueabihf-gcc
    • Open the shell configuration file.

      nano ~/.bashrc 
    • Add the path of the cross-compilation tool to the system's PATH environment variable. Replace <SDK Directory> with your own SDK path, such as /home/luckfox/luckfox-pico/.

      export PATH=<SDK Directory>/tools/linux/toolchain/arm-rockchip830-linux-uclibcgnueabihf/bin:$PATH
    • Reload the shell configuration file to apply the changes:

      source ~/.bashrc  
  2. Compile the Program Using the Cross-Compilation Tool

    arm-rockchip830-linux-uclibcgnueabihf-gcc spi.c -o spi
  3. After successful cross-compilation, an executable file that can run on the development board will be generated in the current directory.

    # ls
    spi spi.c

4.8 Running the Program

  1. File Transfer

    First, transfer the spi program from the virtual machine to Windows, and then transfer it to the development board via TFTP or ADB. Here are the steps to transfer the file from Windows to the development board using ADB:

    adb push path_to_file destination_on_development_board

    eg: (Transferring the `spi` file from the current directory to the root directory of the development board)
    adb push spi /
  2. Running the Program

    Modify the permissions of the spi file and then run the program:

    # chmod 777 spi
    # ./spi
  3. Experimental Observations

    Connect SPI's MOSI and MISO and run the program:
    image