Skip to main content

SPI Master-Slave Communication

In this chapter, we will learn how to configure the SPI devices, using RK chips as Master and Slave, and directly manipulate the SPI interface in user space to achieve communication between the Master and Slave.

Sample program : Code.zip

1. Modify Kernel Configuration

1.1 Save the file and clean, then recompile.

cd ~/SDK_directory/sysdrv/source/kernel
cp ./arch/arm/configs/luckfox_rv1106_linux_defconfig .config
make ARCH=arm menuconfig

1.2 Modify the configuration

  1. Select "Device Drivers"
  2. Select "SPI support"
  3. Modify the configuration
    • Configure user space direct SPI interface operation
    • Enable "Rockchip SPI controller driver" for the Master by selecting "Y"
    • Enable "SPI slave protocol handlers" for the Slave by selecting "Y"

1.3 Save the config and return to the SDK directory to recompile the kernel.

make ARCH=arm savedefconfig
cp defconfig arch/arm/configs/luckfox_rv1106_linux_defconfig
cd ~/SDK_directory
./build kernel

2. Modify Device Tree

2.1 Modify the Device Tree file

  1. Device Tree File Paths

    • Development Board Configuration File

      In the <SDK directory>/project/cfg/BoardConfig_IPC/ directory, there are configuration files for different models of development boards. These files primarily include configuration parameters for various Luckfox Pico board models, covering aspects such as target architecture, boot medium, Uboot and kernel configurations, and partition settings. The SDK directory structure is as follows:

      ├── build.sh -> project/build.sh ---- SDK compilation script
      ├── media --------------------------- Multimedia encoding, ISP, and related algorithms (can be compiled independently in the SDK)
      ├── sysdrv -------------------------- U-Boot, kernel, rootfs directory (can be compiled independently in the SDK)
      ├── project ------------------------- Reference applications, compilation configurations, and script directories
      │ ├── cfg
      │ ├── BoardConfig_IPC
      │ ├── BoardConfig-EMMC-NONE-RV1103_Luckfox_Pico-IPC.mk
      │ ├── BoardConfig-EMMC-NONE-RV1103_Luckfox_Pico_Mini_A-IPC.mk
      │ ├── BoardConfig-SPI_NAND-NONE-RV1103_Luckfox_Pico_Mini_B-IPC.mk
      │ ├── BoardConfig-SPI_NAND-NONE-RV1103_Luckfox_Pico_Plus-IPC.mk
      │ ├── BoardConfig-SPI_NAND-NONE-RV1106_Luckfox_Pico_Pro_Max-IPC.mk
      │ └── ...
      ├── output -------------------------- Directory for storing SDK compiled image files
      ├── docs ---------------------------- SDK documentation directory
      └── tools --------------------------- Image burning packaging tools and burning tools

      Among them, RK_KERNEL_DTS specifies the Device Tree Source (DTS) file for the kernel. Taking Luckfox Pico as an example, by opening the BoardConfig-EMMC-NONE-RV1103_Luckfox_Pico-IPC.mk file, we can see that the file pointed to by RK_KERNEL_DTS is rv1103g-luckfox-pico.dts

    • Device Tree File Path

      Based on RK_KERNEL_DTS, the device tree file path for Luckfox Pico can be determined as follows:

      <SDK directory>/sysdrv/source/kernel/arch/arm/boot/dts/rv1103g-luckfox-pico.dts
  2. Modify the Device Tree file

    • Master side

      &spi0 {
      status = "okay";
      spi_test@00 {
      compatible = "rockchip,spidev";
      reg = <0>;
      spi-cpha;
      spi-cpol;
      spi-lsb-first;
      spi-max-frequency = <49000000>;
      status = "okay";
      };
      };
    • Slave side

      &spi0 {
      status = "okay";
      spi-slave;
      slave {
      compatible ="rockchip,spidev";
      reg = <0>;
      id = <0>;
      };
      };

2.2 Compile the Kernel

  1. Compile by selecting branches for LuckFox Pico, LuckFox Pico Mini A, LuckFox Pico Mini B, LuckFox Pico Plus, and LuckFox Pico Pro/Max.

    luckfox@luckfox:~/luckfox-pico$ ./build.sh lunch
    ls: cannot access 'BoardConfig*.mk': No such file or directory

    You're building on Linux
    Lunch menu...pick a combo:

    BoardConfig-*.mk naming rules:
    BoardConfig-"启动介质"-"电源方案"-"硬件版本"-"应用场景".mk
    BoardConfig-"boot medium"-"power solution"-"hardware version"-"applicaton".mk

    ----------------------------------------------------------------
    0. BoardConfig_IPC/BoardConfig-EMMC-NONE-RV1103_Luckfox_Pico-IPC.mk
    boot medium(启动介质): EMMC
    power solution(电源方案): NONE
    hardware version(硬件版本): RV1103_Luckfox_Pico
    applicaton(应用场景): IPC
    ----------------------------------------------------------------

    ----------------------------------------------------------------
    1. BoardConfig_IPC/BoardConfig-EMMC-NONE-RV1103_Luckfox_Pico_Mini_A-IPC.mk
    boot medium(启动介质): EMMC
    power solution(电源方案): NONE
    hardware version(硬件版本): RV1103_Luckfox_Pico_Mini_A
    applicaton(应用场景): IPC
    ----------------------------------------------------------------

    ----------------------------------------------------------------
    2. BoardConfig_IPC/BoardConfig-SPI_NAND-NONE-RV1103_Luckfox_Pico_Mini_B-IPC.mk
    boot medium(启动介质): SPI_NAND
    power solution(电源方案): NONE
    hardware version(硬件版本): RV1103_Luckfox_Pico_Mini_B
    applicaton(应用场景): IPC
    ----------------------------------------------------------------

    ----------------------------------------------------------------
    3. BoardConfig_IPC/BoardConfig-SPI_NAND-NONE-RV1103_Luckfox_Pico_Plus-IPC.mk
    boot medium(启动介质): SPI_NAND
    power solution(电源方案): NONE
    hardware version(硬件版本): RV1103_Luckfox_Pico_Plus
    applicaton(应用场景): IPC
    ----------------------------------------------------------------

    ----------------------------------------------------------------
    4. BoardConfig_IPC/BoardConfig-SPI_NAND-NONE-RV1106_Luckfox_Pico_Pro_Max-IPC.mk
    boot medium(启动介质): SPI_NAND
    power solution(电源方案): NONE
    hardware version(硬件版本): RV1106_Luckfox_Pico_Pro_Max
    applicaton(应用场景): IPC
    ----------------------------------------------------------------

    Which would you like? [0]: 0
    [build.sh:info] switching to board: /home/luckfox/luckfox-pico/project/cfg/BoardConfig_IPC/BoardConfig-EMMC-NONE-RV1103_Luckfox_Pico-IPC.mk
    [build.sh:info] Running build_select_board succeeded.
  2. Recompile the kernel

    luckfox@luckfox:~/Luckfox-Pico/luckfox-pico$ ./build.sh kernel

2.3 Flash the Firmware Again

  1. After successfully compiling the kernel, the generated files can be found in the <SDK directory>/output/image directory.
  2. Replace the boot.image and env.txt files in the original firmware.
  3. Recreate the SD card. For Luckfox Pico Plus, you may only need to modify the relevant partition.

3. SPI Communication (C Program)

3.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.

3.2 Sample Program

Through the following two programs, SPI communication between the Master and Slave can be achieved.

Master side

#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[255];
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 = 49000000, // SPI speed in Hz
.bits_per_word = 8,
};

/* Send Data */
for (int i = 0; i < transfer.len; i++)
tx_buffer[i] = i;

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

printf("Send %d bytes of data max speed: %d Hz.\n",transfer.len,transfer.speed_hz);

/* Close the SPI device */
close(spi_file);

return 0;
}

Slave side

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

#define SPI_DEVICE_PATH "/dev/spidev0.0"

int main() {
int spi_file,ret;
uint8_t tx_buffer[50];
uint8_t rx_buffer[255];

/* 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(rx_buffer),
.delay_usecs = 0,
.speed_hz = 49000000, // SPI speed in Hz
.bits_per_word = 8,
};

while(1)
{
/* Clear buffer */
memset(rx_buffer,0,sizeof(rx_buffer));

/* Waiting for data */
do {
ret = ioctl(spi_file, SPI_IOC_MESSAGE(1), &transfer);
} while (ret < 0);

/* Print rx_buffer*/
printf("---------Receive %d bytes of data max speed:%d Hz---------\n",ret,transfer.speed_hz);
printf("SPI RX: 0x%08X:", 0);
for (int i = 0; i < ret; i++) {
printf(" %02X",rx_buffer[i] );
if ((i + 1) % 16 == 0){
printf("\nSPI RX: 0x%08X:", i+1);
}
}
printf("\n");
}

/* Close the SPI device */
close(spi_file);

return 0;
}

3.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"

3.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;
}

3.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(rx_buffer),
.delay_usecs = 0,
.speed_hz = 49000000, // SPI speed in Hz
.bits_per_word = 8,
};

3.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.

/* Send Data */
for (int i = 0; i < transfer.len; i++)
tx_buffer[i] = i;

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

3.7 Slave-Side Data Reception

This code segment starts by clearing the data buffer and then checks the return value of the ioctl function to determine if any data has been received. If no data has been received, it enters a loop to wait for data from the master. Upon receiving data, the ioctl function returns the length of the received data. Subsequently, it prints the received data along with relevant information in hexadecimal format, clears the data buffer, and proceeds to the next round of waiting for data.

while(1)
{
/* Clear rx_buffer */
memset(rx_buffer,0,sizeof(rx_buffer));

/* Waiting for data */
do {
ret = ioctl(spi_file, SPI_IOC_MESSAGE(1), &transfer);
} while (ret < 0);

/* Print rx_buffer */
printf("---------Receive %d bytes of data max speed:%d Hz---------\n",ret,transfer.speed_hz);
printf("SPI RX: 0x%08X:", 0);
for (int i = 0; i < ret; i++) {
printf(" %02X",rx_buffer[i] );
if ((i + 1) % 16 == 0){
printf("\nSPI RX: 0x%08X:", i+1);
}
}
printf("\n");
}

3.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. Running the Program

4.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
or
adb -s device_serial_number 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 /
or
adb -s d48936ed7d155xxx push spi /

4.2 Hardware Connection

1. Connect SPI0_MISO, SPI0_MOSI, SPI0_CLK, and CPI0_CS0.
2. Common ground.

4.3 Running the Program

Change the permissions of the spi file and then execute the program.

# chmod +x spi
# ./spi

4.4 Experimental Observations

  1. After running the program on the Slave side, it waits for the Master side to send data.
  2. Running the program on the Master side, it sends data to the Slave side.
  3. The Slave side successfully receives and outputs the data, then enters the next waiting cycle.