spidev:Linux内核中的用户空间SPI接口

撰写时间:2023/10/24

1 简介

SPI(串行外设接口)是一种常用的通信协议,用于高速全双工通信。Linux内核提供了一种名为spidev的用户空间接口,用于访问SPI设备。

spidev是一个位于Linux内核空间的接口程序。其作用是充当用户程序与内核空间SPI核心底层驱动之间的桥梁。通过spidev接口,开发者可以直接在用户空间进行程序开发以调用底层SPI驱动,这大大降低了SPI驱动开发的门槛。通常情况下,设备驱动的开发需要在内核空间中进行,这对大多数开发者来说是一项相对复杂和陌生的任务。然而,由于spidev的存在,开发者无需深入了解或学习内核开发的各种方法与流程,可以直接使用熟悉的开发工具和语言,来轻松地完成SPI驱动的开发。这样,即便是不熟悉内核开发的人员也能更方便地进行SPI通讯开发。

1.1 主要优点

  1. 安全性:因为用户空间与内核空间的隔离,即使代码存在错误,也不会导致整个Linux系统崩溃或不稳定。
  2. 易用性:用户空间提供了更多易用和强大的开发工具和库,比如各种语言的SDK、调试工具等。可以使用C、C++、Python等语言进行开发。
  3. 快速迭代:由于在用户空间中进行开发,代码的编译和测试周期通常更短,这有助于快速迭代和优化。

1.2 应用场景

  1. 原型开发:如果你只是需要进行一些基础的功能测试或者原型设计,spidev是一个很好的选择。
  2. 简单驱动程序的开发:对于那些不需要访问内核高级功能(如中断处理)的项目,使用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-devipk安装包

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模块。

Image.png
Image.png

保存退出后。在openwrt源代码的根目录下,执行make命令重新编译openwrt系统:

make V=s j=2

其中 V=s j=2 是make编译命令的参数。使用了2个线程编译。如果编译出现问题,需要查看具体的出错情况时,需要把j = 2去掉,即使用make V=s重新编译重新编译,才能看到出错详情。

make 参数Vj的解释:

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_MODESPI_IOC_WR_MODE请求,可以获取或设置SPI传输模式。这些请求需要一个指向字节(byte)的指针,该字节将存储或接收SPI模式。

  • 读取模式(RD):获取当前的SPI模式。
  • 写入模式(WR):设置新的SPI模式。

可以使用预定义的常量,如SPI_MODE_0SPI_MODE_1等,或者直接使用SPI_CPOLSPI_CPHA标志来设置时钟极性和相位。

4.2 位对齐方式

SPI_IOC_RD_LSB_FIRSTSPI_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_WORDSPI_IOC_WR_BITS_PER_WORD,可以读取或设置每个SPI传输字中的位数。通常情况下,默认值为8位。

4.4 最大传输速度

使用SPI_IOC_RD_MAX_SPEED_HZSPI_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()成员函数。这个函数执行以下几个主要任务:

  1. 打开SPI设备。
  2. 设置SPI传输模式。
  3. 设置SPI字的位数。
  4. 设置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_1SPI_MODE_2SPI_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

[16] https://e2echina.ti.com/support/microcontrollers/msp430/f/msp-low-power-microcontroller-forum/206161/msp430fr2433-spi

[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

[20] https://wx.comake.online/doc/doc/SigmaStarDocs-Pudding-0120/customer/development/reference/spi dev.html

[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>;
                };
        };  
};