撰写时间:2023/10/24
1 简介
SPI(串行外设接口)是一种常用的通信协议,用于高速全双工通信。Linux内核提供了一种名为spidev的用户空间接口,用于访问SPI设备。
spidev是一个位于Linux内核空间的接口程序。其作用是充当用户程序与内核空间SPI核心底层驱动之间的桥梁。通过spidev接口,开发者可以直接在用户空间进行程序开发以调用底层SPI驱动,这大大降低了SPI驱动开发的门槛。通常情况下,设备驱动的开发需要在内核空间中进行,这对大多数开发者来说是一项相对复杂和陌生的任务。然而,由于spidev的存在,开发者无需深入了解或学习内核开发的各种方法与流程,可以直接使用熟悉的开发工具和语言,来轻松地完成SPI驱动的开发。这样,即便是不熟悉内核开发的人员也能更方便地进行SPI通讯开发。
1.1 主要优点
- 安全性:因为用户空间与内核空间的隔离,即使代码存在错误,也不会导致整个Linux系统崩溃或不稳定。
- 易用性:用户空间提供了更多易用和强大的开发工具和库,比如各种语言的SDK、调试工具等。可以使用C、C++、Python等语言进行开发。
- 快速迭代:由于在用户空间中进行开发,代码的编译和测试周期通常更短,这有助于快速迭代和优化。
1.2 应用场景
- 原型开发:如果你只是需要进行一些基础的功能测试或者原型设计,spidev是一个很好的选择。
- 简单驱动程序的开发:对于那些不需要访问内核高级功能(如中断处理)的项目,使用spidev能快速地完成驱动程序的开发。
1.3 spidev的局限性
虽然spidev提供了一个相对简便的方式来访问SPI设备,它有一些情况下不能完全替代专门的SPI设备驱动。这是因为spidev主要用于在Linux内核与SPI主设备之间进行通信,但当需要访问内核空间的高级接口功能,如中断处理等,spidev就显得力不从心。在这种情况下,开发者应当考虑直接在内核空间开发SPI驱动程序,而不是依赖于spidev在用户空间进行编程。
2 spidev的安装
以openwrt系统为例,介绍启用spidev模块的方法。在openwrt系统中,默认是没有启用spidev模块的,有两种启用方法:一种是在openwrt系统中安装spidev模块包,这种方法较为简单;另一种方法是使用make menuconfig
配置openwrt的源代码,然后重新编译,此方法较复杂,涉及到linux源码的配置与编译等内容。
2.1 安装spidev包的方法
这种方法不需要重新编译openwrt系统,非常方便。
1)获取kmod-spi-dev
ipk安装包
在http://openwrt.jaru.eu.org/gargoyle-pl/chaos_calmer/ramips/packages/中根据openwrt的版本下载”kmod-spi-dev-… .ipk”的安装包。这个源比官网更全。
我的openwrt版本是 Chaos Calmer 15.05.1
。在这里查看其它openwrt版本。http://openwrt.jaru.eu.org/gargoyle-pl/
openwrt的版本以及linux内核版本号,可以在luci界面查看。在浏览器中输入openwrt的IP地址,登陆后就能看到。
2)安装ipk包
将ipk包拷贝到openwrt系统后,执行opkg install
命令安装。
opkg install kmod-spi-dev*.ipk
2.2 配置并编译源代码的方法
在openwrt源代码的根目录下,使用make menuconfig
命令打开配置窗口,在Kernel modules->SPI Support
中,选中kmod-spi-dev
。这里的kmod-spi-dev
即是需要开启的spidev模块。
保存退出后。在openwrt源代码的根目录下,执行make命令重新编译openwrt系统:
make V=s j=2
其中 V=s j=2
是make编译命令的参数。使用了2个线程编译。如果编译出现问题,需要查看具体的出错情况时,需要把j = 2
去掉,即使用make V=s
重新编译重新编译,才能看到出错详情。
make 参数V
与j
的解释:
1). V=s
是一个环境变量设置,通常用于控制构建过程中的输出详细程度。
V=0
或者没有设置V
通常意味着只显示必要的信息。V=1
通常用于显示更详细的构建信息。V=s
在显示详细的调试信息,但这取决于具体的Makefile
如何处理这个变量。
2). j=2
是用于 make
的一个标准选项,表示并行编译,即同时进行两个任务。j
参数后面的数字 2
指定了可以同时进行的任务数量。这在多核或多线程的CPU上是有用的,因为它可以加速整个构建过程。
在使用上述的方法安装spidev模块或编译源码后,可以使用下面的方法查看spidev包是否已经安装。
opkg list-installed | grep spi
执行后会看到所有已经安装的名称包含spi的包。其中有kmod-spi-dev说明已经安装成功了。
3 在DTS中添加spidev
在安装spidev模块后,还需要在dts(设备树)文件中加入spidev的配置。以mt7628芯片为例,在mt7628.dts文件中的palmbus@10000000
属性下的spi@b00
中加入:
spidev@1{
compatible = "linux,spidev";
reg = <1 0>;
spi-max-frequency = <10000000>;
};
完整的dts文件见文后。
在openwrt系统的/dev
目录中看到spidevX.Y
形式的文件名称,说明spidev已经在加载到内核中,用户空间程序就可以通过标准的open/read/write/ioctl接口来访问这个SPI设备了。
4 spidev通讯参数的设定(C/C++)
在完成了安装以及dts文件的修改后,就可以编程了。这里以C/C++开发为例说明。如果你使用python,可以去这里查看python调用spidev接口的方法。https://pypi.org/project/spidev/
在编写具体的通讯代码之前,先要对spi通讯的参数进行设定。ioctl()
函数提供了一种强大的方式来与SPI设备进行交互。在本节中,将详细介绍几种重要的ioctl()
请求,这些请求允许开发者读取或更改设备当前用于数据传输的参数设置。
4.1 SPI传输模式
使用SPI_IOC_RD_MODE
和SPI_IOC_WR_MODE
请求,可以获取或设置SPI传输模式。这些请求需要一个指向字节(byte)的指针,该字节将存储或接收SPI模式。
- 读取模式(RD):获取当前的SPI模式。
- 写入模式(WR):设置新的SPI模式。
可以使用预定义的常量,如SPI_MODE_0
、SPI_MODE_1
等,或者直接使用SPI_CPOL
和SPI_CPHA
标志来设置时钟极性和相位。
4.2 位对齐方式
SPI_IOC_RD_LSB_FIRST
和SPI_IOC_WR_LSB_FIRST
允许获取或设置位对齐方式——MSB-first或LSB-first。
- MSB-first:最高有效位优先。
- LSB-first:最低有效位优先。
数值为0,表示MSB-first,也是默认的设定值。当数值为非0时,表示很少使用的LSB-first。
SPI(串行外设接口)通信中,数据传输的位对齐方式是一个重要的参数。位对齐方式决定了在SPI字(通常为8位或16位)传输过程中,是最高有效位(MSB)优先还是最低有效位(LSB)优先。
MSB-first(最高有效位优先):在这种模式下,每个SPI字的最高有效位(MSB)将首先被发送或读取。这是SPI通信中最常见的配置。例如,如果你有一个8位的字节0b11001010,那么最左边的位(即最高有效位,这里是1)将首先被发送。
LSB-first(最低有效位优先):与MSB-first相反,在LSB-first模式下,每个SPI字的最低有效位(LSB)会首先被发送或读取。在某些特定应用或特定类型的SPI设备中,这种配置可能会更有用。例如,如果你有相同的8位字节0b11001010,最右边的位(即最低有效位,这里是0)将首先被发送。
4.3 每字位数
通过SPI_IOC_RD_BITS_PER_WORD
和SPI_IOC_WR_BITS_PER_WORD
,可以读取或设置每个SPI传输字中的位数。通常情况下,默认值为8位。
4.4 最大传输速度
使用SPI_IOC_RD_MAX_SPEED_HZ
和SPI_IOC_WR_MAX_SPEED_HZ
,你可以获取或设置SPI设备的最大传输速度。这需要一个指向u32
类型的指针,该指针用于存储或接收速度值(以Hz为单位)。
需要注意的是,虽然你可以设置一个理想的传输速度,但硬件和驱动程序可能不一定支持你设置的确切速度。
通过掌握这些ioctl()
请求,你将能更灵活地控制SPI设备,从而实现更高效和可定制的数据传输。希望这篇文章能够帮助你更深入地了解如何使用spidev
进行SPI编程。
4.5 在C++中使用spidev设置SPI设备:一个实例解析
下面是一段C++示例程序,使用spidev
库和ioctl()
函数来初始化SPI设备。
#include <fcntl.h>
#include <unistd.h>
#include <sys/ioctl.h>
#include <linux/spi/spidev.h>
uint8_t mode = SPI_MODE_0;
uint8_t bits = 8;
uint32_t speed = 100000;
const char *device = "/dev/spidev32766.1";
bool SPIDevice::Open_Initialize() {
fd = open(device, O_RDWR);
if (fd < 0 || ioctl(fd, SPI_IOC_WR_MODE, &mode) == -1 ||
// ioctl(fd, SPI_IOC_RD_MODE, &mode) == -1 ||
ioctl(fd, SPI_IOC_WR_BITS_PER_WORD, &bits) == -1 ||
// ioctl(fd, SPI_IOC_RD_BITS_PER_WORD, &bits) == -1 ||
ioctl(fd, SPI_IOC_WR_MAX_SPEED_HZ, &speed) == -1 ||
// ioctl(fd, SPI_IOC_RD_MAX_SPEED_HZ, &speed) == -1
) {
return false;
}
return true;
}
程序中包含了spidev编程时要引用的头文件。
#include <fcntl.h>
#include <unistd.h>
#include <sys/ioctl.h>
#include <linux/spi/spidev.h>
我们的程序示例定义了一个SPIDevice
类,其中包含一个Open_Initialize()
成员函数。这个函数执行以下几个主要任务:
- 打开SPI设备。
- 设置SPI传输模式。
- 设置SPI字的位数。
- 设置SPI的最大传输速度。
4.5.1 打开SPI设备
fd = open(device, O_RDWR);
这里使用Linux的open()
系统调用来打开SPI设备。O_RDWR
标志表示我们将以读写模式打开设备。打开标志的详细内容可以看这里:[open - open a file] https://pubs.opengroup.org/onlinepubs/7908799/xsh/open.html
4.5.2 设置SPI传输模式
ioctl(fd, SPI_IOC_WR_MODE, &mode)
使用ioctl()
函数和SPI_IOC_WR_MODE
请求来设置SPI的传输模式。我们选择的是SPI_MODE_0
。除此之外,还可使用SPI_MODE_1
、SPI_MODE_2
、SPI_MODE_3
。
4.5.3 设置SPI字的位数
ioctl(fd, SPI_IOC_WR_BITS_PER_WORD, &bits)
这里,使用ioctl()
函数和SPI_IOC_WR_BITS_PER_WORD
请求来设置每个SPI字的位数,这里设为8位。
4.6 设置SPI最大传输速度
ioctl(fd, SPI_IOC_WR_MAX_SPEED_HZ, &speed)
最后,设置SPI的最大传输速度为100,000 Hz。
5 编写spidev通讯程序(C/C++)
通过read、write实现半双工的通讯,使用ioctl实现全双工通讯。
5.1 半双工spi通讯
下面是一个使用spidev的简单示例。可以实现半双工的读写以及关闭spidev的通道。
// 执行半双工读写
read(fd, rxbuf, len);
write(fd, txbuf, len);
//
close(fd); //关闭spidev的通讯通道。
上面的代码是一个简单的示例,用于读写数据到一个通用设备(通常是一个SPI设备)通过文件描述符(fd)。
read(fd, rxbuf, len);
read
是一个系统调用,用于从文件(在这个情况下是一个设备)读取数据。
fd
是文件描述符,标识了我们要操作的文件或设备。
rxbuf
是一个缓冲区(buffer),用于存储读取到的数据。
len
是读取的字节数。
当这一行代码执行后,rxbuf
将会填充从文件描述符fd
读取到的len
个字节的数据。
write(fd, txbuf, len);
write
是一个系统调用,用于向文件(在这个情况下是一个设备)写入数据。
txbuf
是一个缓冲区,包含我们想要写入的数据。
len
是写入的字节数。
当这一行代码执行后,txbuf
里面的数据(len
字节数)将会被写入到文件描述符 fd
指向的文件或设备。
close(fd);
这个调用用于关闭之前通过文件描述符 fd
打开的文件或设备,释放系统资源。
注意:这里是半双工操作,意味着读和写是分开进行的。这与全双工(同时进行读和写)不同。这里先进行读操作,然后进行写操作。
简单来说,这段代码从一个设备读取数据,然后写入数据,最后关闭与该设备的连接。
5.2 实现全双工spi通讯
这是一个函数,使用ioctl()
函数和SPI_IOC_MESSAGE
请求实现全双工的通讯。
#include <fcntl.h>
#include <unistd.h>
#include <sys/ioctl.h>
#include <linux/spi/spidev.h>
// 执行全双工读写的函数,可被调用以进行SPI通讯。
bool SPIDevice::transfer(const uint8_t *tx, uint8_t *rx, size_t len) {
struct spi_ioc_transfer tr;
memset(&tr, 0, sizeof(tr));
tr.tx_buf = (unsigned long)tx;
tr.rx_buf = (unsigned long)rx;
tr.len = len;
tr.delay_usecs = 0;
tr.speed_hz = speed;
tr.bits_per_word = bits;
bool a = (ioctl(fd, SPI_IOC_MESSAGE(1), &tr)==-1);
if (a)
{
std::cout<<"transfer error"<<std::endl; //如果ioctl()调用返回-1,则输出一个错误消息。
}
return !a;
}
其中用到了SPI_IOC_MESSAGE
,这是一个非常重要的ioctl()
请求,用于执行实际的SPI数据传输。该命令允许应用程序在单个调用中执行一个或多个SPI消息的完整传输。
参数结构:spi_ioc_transfer
该命令通常与一个或多个spi_ioc_transfer
结构体一同使用。每个spi_ioc_transfer
结构体都描述了一个独立的SPI传输消息,包括以下几个重要的字段:
tx_buf
:指向待发送数据的缓冲区。
rx_buf
:指向用于接收数据的缓冲区。
len
:传输的字节数。
speed_hz
:传输速率。
bits_per_word
:每个数据字的位数。
delay_usecs
:传输完成后的延迟时间,单位为微秒。
在使用ioctl()
进行SPI传输时,通常会像下面这样调用:
struct spi_ioc_transfer tr;
// 初始化 tr 结构体...
int ret = ioctl(fd, SPI_IOC_MESSAGE(1), &tr);
其中,fd
是已打开的SPI设备的文件描述符,SPI_IOC_MESSAGE(1)
指示我们将进行一个SPI消息的传输,&tr
是指向已初始化的spi_ioc_transfer
结构体的指针。
执行多个SPI传输
SPI_IOC_MESSAGE
也支持一次执行多个SPI传输。例如,如果你有一个spi_ioc_transfer
数组,你可以这样做:
struct spi_ioc_transfer trs[2];
// 初始化 trs 数组...
int ret = ioctl(fd, SPI_IOC_MESSAGE(2), trs);
这样,trs
数组中的两个spi_ioc_transfer
结构体会被依次用于SPI数据传输。
5.3 小结
上文主要讲述了如何在C/C++中使用spidev库实现SPI(Serial Peripheral Interface)通信。文章首先介绍了如何进行半双工通信,然后详细解释了如何使用ioctl()
函数实现全双工通信。
(1)半双工SPI通信:
- 使用
read()
和write()
函数进行数据的读取和写入。 - 最后通过
close(fd)
来关闭SPI的通信通道。
(2)全双工SPI通信:
- 使用
ioctl()
和特定的请求SPI_IOC_MESSAGE
来进行全双工通信。 - 具体通过一个名为
SPIDevice::transfer
的函数来实现。 - 使用了
spi_ioc_transfer
结构体来描述SPI传输的各个参数,如待发送和接收的数据缓冲区、传输字节数、速度等。
(3)多消息SPI传输:
SPI_IOC_MESSAGE
也支持一次执行多个SPI传输,通过构建一个spi_ioc_transfer
结构体数组来实现。
文章还深入介绍了spi_ioc_transfer
结构体的各个字段和它们的意义,便于理解并实现基于spidev的SPI通信。
6 总结
在这篇文章中,我们深入探讨了spidev——Linux内核中的用户空间SPI接口。我们了解到,spidev作为一个桥梁,连接了用户程序与内核空间SPI核心底层驱动,使得开发者可以直接在用户空间进行程序开发以调用底层SPI驱动,大大降低了SPI驱动开发的门槛。
同时,我们也了解到,虽然spidev提供了一个相对简便的方式来访问SPI设备,但在需要访问内核空间的高级接口功能时,如中断处理等,spidev就显得力不从心。在这种情况下,开发者应当考虑直接在内核空间开发SPI驱动程序。
此外,我们还介绍了如何在openwrt系统中启用spidev模块,包括配置并编译源代码的方法,以及安装spidev包的方法。我们还详细介绍了如何使用ioctl()函数来与SPI设备进行交互,以及如何设置SPI通讯的参数。
最后,通过C/C++示例程序,展示了如何使用spidev库和ioctl()函数来初始化SPI设备并进行通讯。
总的来说,spidev是一个强大而灵活的工具,它为开发者提供了一个简单而直接的方式来访问和控制SPI设备。无论你是一个经验丰富的内核开发者,还是一个刚刚开始接触SPI编程的新手,相信你都能从这篇文章中找到有用的信息和启发。
参考资料
[1] https://www.kernel.org/doc/Documentation/spi/spidev
[2] https://blog.csdn.net/chinazhangzhong123/article/details/54707387
[3] https://juejin.cn/post/7153440681615163423
[4] https://blog.csdn.net/qq_37037348/article/details/130564284
[5] https://blog.csdn.net/yangguoyu8023/article/details/122636162
[6] https://blog.csdn.net/u011006622/article/details/124102680
[7] https://www.cnblogs.com/panda-w/p/11137887.html
[8] https://wiki.t-firefly.com/AIO-3399ProC/driver_spi.html
[9] https://www.eet-china.com/mp/a209080.html
[10] http://www.linkedkeeper.com/mobile/1410.html
[11] https://wiki.t-firefly.com/zh_CN/AIO-3128C/driver_spi.html
[12] https://blog.csdn.net/jia_weihui/article/details/129239470
[13] https://jb51.net/article/186517.htm
[14] https://doc.embedfire.com/linux/imx6/quick_start/zh/latest/quick_start/spi_bus/spi_bus.html
[15] https://elecfans.com/article/2052674.html
[17] http://xilinx.eetrend.com/d6-xilinx/blog/2017-02/10922.html
[18] https://wiki.phytec.com/plugins/servlet/mobile?contentId=158152813
[19] https://community.infineon.com/t5/USB-EZ-PD-Type-C/在-SPI-MOSI-和-MISO-上发送和接收字节的示例代码/td-p/623509
[21] https://hackmd.io/@amberchung/linux-spidev
[22] https://github.com/apache/dubbo-website/issues/575
[23] https://wiki.sipeed.com/soft/maixpy3/zh/usage/hardware/SPI.html
附录
mt7628.dts 文件
/dts-v1/;
/include/ "mt7628an.dtsi"
/ {
compatible = "mediatek,mt7628an-eval-board", "mediatek,mt7628an-soc";
model = "Mediatek MT7628AN evaluation board";
chosen {
bootargs = "console=ttyS0,115200";
};
memory@0 {
device_type = "memory";
reg = <0x0 0x10000000>; //256MB RAM
// reg = <0x0 0x8000000>; //128MB RAM
// reg = <0x0 0x4000000>; //64MB RAM
};
pinctrl {
state_default: pinctrl0 {
gpio {
// ralink,group = "i2c";
// ralink,function = "gpio";
};
p0_led {
ralink,group = "p0_led_an";
ralink,function = "ephy";
};
p1_led {
ralink,group = "p1_led_an";
ralink,function = "ephy";
};
p2_led {
ralink,group = "p2_led_an";
ralink,function = "ephy";
};
p3_led {
ralink,group = "p3_led_an";
ralink,function = "gpio";
};
p4_led {
ralink,group = "p4_led_an";
ralink,function = "gpio";
};
spis {
ralink,group = "spis";
ralink,function = "spis";
};
pwm1 {
ralink,group = "pwm1";
ralink,function = "gpio";
};
pwm0 {
ralink,group = "pwm0";
ralink,function = "gpio";
};
refclk {
ralink,group = "refclk";
ralink,function = "gpio";
};
gpio_0 {
ralink,group = "gpio";
// ralink,function = "refclk_fc";
ralink,function = "gpio";
};
i2s {
ralink,group = "i2s";
ralink,function = "i2s";
};
};
uart2_pins: uart2 {
uart2 {
ralink,group = "uart2";
ralink,function = "gpio";
};
};
};
palmbus@10000000 {
i2c@900 {
status = "okay";
};
spi@b00 {
status = "okay";
m25p80@0 {
#address-cells = <1>;
#size-cells = <1>;
compatible = "en25q64";
reg = <0 0>;
linux,modalias = "w25q64", "w25q128", "w25q256";
spi-max-frequency = <10000000>;
m25p,chunked-io = <32>;
partition@0 {
label = "u-boot";
reg = <0x0 0x30000>;
read-only;
};
partition@30000 {
label = "u-boot-env";
reg = <0x30000 0x10000>;
read-only;
};
factory: partition@40000 {
label = "factory";
reg = <0x40000 0x10000>;
read-only;
};
partition@50000 {
label = "firmware";
// reg = <0x50000 0x7b0000>; //8MB flash
// reg = <0x50000 0xfb0000>; //16MB flash
reg = <0x50000 0x1fb0000>; //32MB flash
};
};
spidev@1{
compatible = "spidev", "rohm,dh2228fv";
reg = <1 0>;
spi-max-frequency = <10000000>;
};
};
uart1@d00 {
status = "okay";
};
uart2@e00 {
status = "okay";
};
};
sdhci@10130000 {
status = "okay";
mediatek,cd-low;
};
ethernet@10100000 {
mtd-mac-address = <&factory 0x28>;
};
gpio-leds {
compatible = "gpio-leds";
system {
label = "mediatek:green:system";
gpios = <&gpio1 5 1>;
default-state = "off";
};
wifi {
label = "mediatek:green:wifi";
gpios = <&gpio1 12 1>;
default-state = "off";
};
};
gpio-keys-polled {
compatible = "gpio-keys-polled";
#address-cells = <1>;
#size-cells = <0>;
poll-interval = <20>;
button_1 {
label = "button_1";
gpios = <&gpio1 6 0>;
linux,code = <0x101>;
};
button_2{
label = "button_2";
gpios = <&gpio1 8 0>;
linux,code = <0x102>;
};
};
};