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:
- 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. - 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
Development boards typically have the SPI0 interface enabled by default. You can determine the corresponding pins using the pinout diagram. For example, on the LuckFox Pico, the SPI0 interface corresponds to pin numbers 48, 49, 50, 51, and 58 (with pin 58 initially disabled).
LuckFox Pico Diagram:
LuckFox Pico Mini A/B Diagram:
LuckFox Pico Plus Diagram:
LuckFox Pico Pro/Max Diagram:
LuckFox Pico Ultra/Ultra W 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
Use the vi tool to open the file, paste the code, and save it.
# nanoo i2c.py
Run the program.
# python3 i2c.py
Experimental Observations
Connect the MOSI and MISO of SPI and run the program:
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 ofstruct 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
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.8 Running the Program
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 /Running the Program
Modify the permissions of the
spi
file and then run the program:# chmod 777 spi
# ./spiExperimental Observations
Connect SPI's MOSI and MISO and run the program:
5. Modifying the Device Tree
The SPI interface is already enabled by default in the SDK's device tree files, and you can use it directly. However, you can modify the SPI pin configuration as described below. When connecting multiple SPI devices, you can choose to use the CS pin as a general-purpose IO and control the CS pin level independently. For example, you can configure GPIO1_C0_d as a general-purpose IO.
5.1 Modifying 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
Define GPIO
Defining a GPIO typically requires adding two code segments. Here's an example showing how to add the definition for the GPIO1_C0_d pin in the device tree:
The code segments to be added are as follows:
/{
gpio1pc:gpio1pc0 {
compatible = "regulator-fixed";
pinctrl-names = "default";
pinctrl-0 = <&gpio1_pc0>;
regulator-name = "gpio1_pc0";
regulator-always-on;
};
};
&pinctrl {
gpio1-pc0 {
gpio1_pc0:gpio1-pc0 {
rockchip,pins = <1 RK_PC0 RK_FUNC_GPIO &pcfg_pull_none>;
};
};
};Comment out the peripheral function of the pin
According to the interface diagram, you can see that the default function of GPIO1_C0_d is PWM. In the program, we want to configure this pin as a general-purpose IO, so you need to comment out the peripheral function of the pin. Commenting out the peripheral function of the pin can be done by commenting out the corresponding peripheral node in the device tree. Here's an example showing how to disable the PWM function for the GPIO1_C0_d pin in the device tree:
Configure SPI
The SPI0 interface is already enabled by default in the SDK's device tree files. Here, we won't configure GPIO1_C0_d as the CS pin for SPI0 (see comment on line 167). Below is an example showing how to configure SPI in the device tree:
The code segment to be added is as follows:
&pinctrl {
spi0 {
/omit-if-no-ref/
spi0m0_pins: spi0m0-pins {
rockchip,pins =
/* spi0_clk_m0 */
<1 RK_PC1 4 &pcfg_pull_none>,
/* spie_miso_m0 */
<1 RK_PC3 6 &pcfg_pull_none>,
/* spi_mosi_m0 */
<1 RK_PC2 6 &pcfg_pull_none>;
};
};
};
5.2 Compiling 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
5.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.