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
- Select "Device Drivers"
- Select "SPI support"
- 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"
- Configure user space direct SPI interface operation
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
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 toolsAmong them,
RK_KERNEL_DTS
specifies the Device Tree Source (DTS) file for the kernel. Taking Luckfox Pico as an example, by opening theBoardConfig-EMMC-NONE-RV1103_Luckfox_Pico-IPC.mk
file, we can see that the file pointed to byRK_KERNEL_DTS
isrv1103g-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
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
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.Recompile the kernel
luckfox@luckfox:~/Luckfox-Pico/luckfox-pico$ ./build.sh kernel
2.3 Flash the Firmware Again
- After successfully compiling the kernel, the generated files can be found in the
<SDK directory>/output/image
directory. - Replace the
boot.image
andenv.txt
files in the original firmware. - 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 ofstruct 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
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 afterPATH=
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
Compile the Program Using the Cross-Compilation Tool
arm-rockchip830-linux-uclibcgnueabihf-gcc spi.c -o spi
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
- After running the program on the Slave side, it waits for the Master side to send data.
- Running the program on the Master side, it sends data to the Slave side.
- The Slave side successfully receives and outputs the data, then enters the next waiting cycle.