Linux: SPI应用编程


  在 Linux 中,SPI(Serial Peripheral Interface)是一种串行通信协议,用于在主设备(如 CPU)和一个或多个从设备(如传感器、存储器等)之间传输数据。SPI 通信通常通过四个基本信号线进行:时钟(SCLK)、主输出从输入(MOSI)、主输入从输出(MISO)和片选(CS)。

  • SCLK(Serial Clock):由主设备生成的时钟信号。
  • MOSI(Master Out Slave In):主设备发送数据到从设备。
  • MISO(Master In Slave Out):从设备发送数据到主设备。
  • CS(Chip Select):选择具体的从设备。主设备通过拉低某个从设备的 CS 线来激活对应的从设备。

一、基础知识

SPI 设备文件:

  • 在 Linux 中,SPI 设备通常表现为 /dev/spidevX.Y 形式的设备文件。
  • X 表示 SPI 主机控制器的编号。
  • Y 表示SPI 总线上设备的编号。
  • 可以通过以下命令检查 /dev/spidev 设备:
ls /dev/spidev*

SPI 控制结构:

  • 使用 struct spi_ioc_transfer 结构体来描述一次 SPI 传输操作。
  • 这个结构体定义了 SPI传输的数据方向、长度和速度等参数。
/**
 * struct spi_ioc_transfer - 描述单个 SPI 传输
 * @tx_buf: 持有指向用户空间缓冲区的指针,用于传输数据,或者为 null。
 *          如果没有提供数据,则发送零值。
 * @rx_buf: 持有指向用户空间缓冲区的指针,用于接收数据,或者为 null。
 * @len: 发送和接收缓冲区的长度,以字节为单位。
 * @speed_hz: 临时覆盖设备的比特率。
 * @bits_per_word: 临时覆盖设备的字长。
 * @delay_usecs: 如果非零,则在最后一位传输后等待的时间(微秒),然后可选择在下一次传输前取消选
 * 择设备。
 * @cs_change: 如果为真,则在开始下一次传输之前取消选择设备。
 * @word_delay_usecs: 如果非零,单次传输中每个字之间的等待时间(微秒)。此属性需要 SPI 控制器
 * 显式支持,否则将被静默忽略。
 *
 * 这个结构体直接映射到内核的 spi_transfer 结构体;
 * 字段具有相同的含义,只不过指针位于不同的地址空间(在某些情况下,例如 32 位 i386 用户空间与 
 * 64 位 x86_64 内核,可能会有不同的大小)。
 * 零初始化结构体,包括当前未使用的字段,以适应潜在的未来更新。
 *
 * SPI_IOC_MESSAGE 使用户空间能够执行类似于内核 spi_sync() 的操作。
 * 传递一个相关传输的数组,它们将一起执行。
 * 每个传输可以是半双工(单向)或全双工(双向)。
 *
 * 例如:
 *	struct spi_ioc_transfer mesg[4];
 *	...
 *	status = ioctl(fd, SPI_IOC_MESSAGE(4), mesg);
 *
 * 举例来说,一个传输可以发送一个九位命令(在 16 位字中右对齐),下一个传输可以读取一个 8 位数据块,
 * 然后通过临时取消选择芯片来结束该命令;下一个传输可以发送另一个九位命令(重新选择芯片),最后一个
 * 传输可能会写入一些寄存器值。
 */
struct spi_ioc_transfer {
    // 用户空间发送缓冲区指针,用于存储发送数据,若为null,则发送0
    __u64		tx_buf;
    // 用户空间接收缓冲区指针,用于存储接收数据,若为null,则不接收数据
    __u64		rx_buf;
    
    // 发送和接收缓冲区的长度(以字节为单位)
    __u32		len;
    // 此次传输临时覆盖设备的比特率
    __u32		speed_hz;
    
    // 最后一位传输后延迟的时间(微秒),在下一个传输前可选地取消选择设备
    __u16		delay_usecs;
    // 此次传输临时覆盖设备的字长
    __u8		bits_per_word;
    // 在开始下一个传输前取消选择设备
    __u8		cs_change;
    // 发送数据时每个字的位数
    __u8		tx_nbits;
    // 接收数据时每个字的位数
    __u8		rx_nbits;
    // 在同一个传输中各字之间延迟的时间(微秒),需要SPI控制器显式支持,否则会被忽略
    __u8		word_delay_usecs;
    // 填充字段,保持结构体对齐
    __u8		pad;
    
    /*
     * 如果结构体spi_ioc_transfer的内容发生不兼容的更改,
     * 则ioctl编号(当前为0)必须更改;
     * 具有固定大小字段的ioctl会进行更多的错误检查,
     * 而像这样字段大小可变的ioctl则不会。
     *
     * 注意:该结构体在64位和32位用户空间中的布局相同。
     */
};

  “临时覆盖设备”指的是在单次 SPI 传输过程中,暂时性地改变 SPI 设备的一些配置参数,而不是永久性地修改设备的基本设置。具体来说:

  • speed_hz:临时覆盖设备的比特率(频率)。这意味着在本次传输过程中,SPI 通信将使用指定的频率,而不是设备默认或之前设置的频率。
  • bits_per_word:临时覆盖设备的字长。这意味着在本次传输过程中,SPI 通信将使用指定的位宽,而不是设备默认或之前设置的位宽。

  这些参数允许在不同的 SPI 传输中灵活调整通信参数,而不需要频繁地修改设备的基本配置。这样做可以提高效率,并且更好地适应不同类型的 SPI 传输需求。

  delay_usecs 字段表示在最后一次位传输之后的延迟时间(以微秒为单位)。这个延迟时间是在下一个传输开始之前的一个可选等待时间。具体来说:

  1. 延迟时间

    • delay_usecs 指定了在最后一个位传输完成后,SPI 控制器需要等待的时间(以微秒为单位)。
    • 如果 delay_usecs 为零,则没有额外的等待时间。
    • 如果 delay_usecs 不为零,则 SPI 控制器会在最后一个位传输完成后等待指定的时间。
  2. 取消选择设备

    • cs_change 字段用于控制是否在下一个传输开始前取消选择设备(即释放片选信号)。
    • 如果 cs_change 为真(1),则在延迟时间结束后,SPI 控制器会取消选择设备。
    • 如果 cs_change 为假(0),则不会取消选择设备。

示例场景:

假设我们有以下传输序列:

  1. 第一次传输:

    • 发送命令字。
    • 接收响应字。
    • 设置 delay_usecs 为 100 微秒。
    • 设置 cs_change 为 1。
  2. 第二次传输:

    • 发送新的命令字。
    • 接收新的响应字。
    • 设置 delay_usecs 为 0 微秒。
    • 设置 cs_change 为 0。
第一次传输过程:

1. 发送命令字。
2. 接收响应字。
3. 等待 100 微秒。
4. 取消选择设备(释放片选信号)。

第二次传输过程:

1. 重新选择设备(激活片选信号)。
2. 发送新的命令字。
3. 接收新的响应字。
4. 不取消选择设备(保持片选信号激活状态)。

  通过这种方式,可以在不同的传输之间引入必要的延迟,并根据需要选择或取消选择设备,从而实现更复杂的 SPI 通信逻辑。

SPI 系统调用:

  使用标准的文件 I/O 操作(如 open(), read(), write()ioctl())来控制 SPI 设备。
特别地,ioctl() 常用于设置 SPI 参数和发起数据传输。

SPI 参数配置:

  可以通过 ioctl() 调用来设置 SPI 速度、模式等参数。部分示例如下:

1. 设置 SPI 模式

SPI 模式(Mode)定义了 SPI 设备如何同步数据传输。共有四种模式:

  • Mode 0: CPOL=0, CPHA=0
  • Mode 1: CPOL=0, CPHA=1
  • Mode 2: CPOL=1, CPHA=0
  • Mode 3: CPOL=1, CPHA=1

使用 SPI_IOC_WR_MODE 来设置 SPI 模式,使用 SPI_IOC_RD_MODE 来读取当前模式。

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

int fd = open("/dev/spidev0.0", O_RDWR);
if (fd < 0) {
    perror("Failed to open SPI device");
    return -1;
}

// 设置 SPI 模式
uint8_t mode = SPI_MODE_0;
if (ioctl(fd, SPI_IOC_WR_MODE, &mode) < 0) {
    perror("Failed to set SPI mode");
    return -1;
}

// 读取 SPI 模式
if (ioctl(fd, SPI_IOC_RD_MODE, &mode) < 0) {
    perror("Failed to get SPI mode");
    return -1;
}

2. 设置 SPI 位宽

  SPI 位宽(Bits per Word)定义了每次传输的数据位数。使用 SPI_IOC_WR_BITS_PER_WORD 来设置位宽,使用 SPI_IOC_RD_BITS_PER_WORD 来读取当前位宽。

// 设置 SPI 位宽
uint8_t bits = 8;
if (ioctl(fd, SPI_IOC_WR_BITS_PER_WORD, &bits) < 0) {
    perror("Failed to set bits per word");
    return -1;
}

// 读取 SPI 位宽
if (ioctl(fd, SPI_IOC_RD_BITS_PER_WORD, &bits) < 0) {
    perror("Failed to get bits per word");
    return -1;
}

3. 设置 SPI 速度

   SPI 速度(Speed)定义了数据传输的速率。使用 SPI_IOC_WR_MAX_SPEED_HZ 来设置速度,使用 SPI_IOC_RD_MAX_SPEED_HZ 来读取当前速度。

// 设置 SPI 速度
uint32_t speed = 500000;
if (ioctl(fd, SPI_IOC_WR_MAX_SPEED_HZ, &speed) < 0) {
    perror("Failed to set max speed Hz");
    return -1;
}

// 读取 SPI 速度
if (ioctl(fd, SPI_IOC_RD_MAX_SPEED_HZ, &speed) < 0) {
    perror("Failed to get max speed Hz");
    return -1;
}

4. 进行 SPI 数据传输

  数据传输是通过 spi_ioc_transfer 结构体完成的。你可以使用 SPI_IOC_MESSAGE(n) 来发送和接收数据,其中 n 是传输的消息数量。

#include <linux/spi/spidev.h>

struct spi_ioc_transfer transfer = {
    .tx_buf = (uintptr_t)tx_buf,
    .rx_buf = (uintptr_t)rx_buf,
    .len = sizeof(tx_buf),
    .speed_hz = speed,
    .bits_per_word = bits,
    .delay_usecs = 0,
};

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

  当你使用 SPI_IOC_MESSAGE(1) 时,表示你将执行一个 SPI 消息传输,而 SPI_IOC_MESSAGE(2) 则表示你将连续执行两个 SPI 消息传输。

SPI_IOC_MESSAGE(1):

  • 功能:执行一个 SPI 消息传输。
  • 使用场景:当你只需要进行一次读写操作时,可以使用这个命令。
  • 示例:你已经有了一个定义好的 spi_ioc_transfer 结构体,其中包含了发送和接收缓冲区的地址、传输长度、传输速度等参数。通过 SPI_IOC_MESSAGE(1),你可以将这个结构体传递给 SPI 驱动,从而完成一次 SPI 传输。

SPI_IOC_MESSAGE(2):

  • 功能:连续执行两个 SPI 消息传输。
  • 使用场景:当你需要连续进行两次读写操作时,可以使用这个命令。这可以用于一些特殊的 SPI 通信协议,或者在需要发送多个命令/数据对时提高效率。
  • 示例:你有两个 spi_ioc_transfer 结构体,每个都定义了一个 SPI 传输。通过 SPI_IOC_MESSAGE(2),你可以将这两个结构体作为一个数组传递给 SPI 驱动,从而连续完成两次 SPI 传输。

区别与注意事项:

  • 效率:使用 SPI_IOC_MESSAGE(2) 进行连续两次传输可能比分别使用两次 SPI_IOC_MESSAGE(1) 更高效,因为它减少了与内核空间的交互次数。
  • 复杂性:使用 SPI_IOC_MESSAGE(2) 可能会稍微增加代码的复杂性,因为你需要管理两个 spi_ioc_transfer 结构体而不是一个。
  • 灵活性:虽然 SPI_IOC_MESSAGE(2) 提供了连续传输的便利,但如果你需要在两次传输之间执行其他操作(如检查状态、处理数据等),则可能需要使用两次
    SPI_IOC_MESSAGE(1)
    硬件支持:并非所有的 SPI 硬件都支持连续的多消息传输。在使用 SPI_IOC_MESSAGE(2) 之前,你需要确认你的硬件和驱动支持这种操作模式。

5. ioctl支持的方法汇总

// 定义SPI_IOC_MESSAGE宏,用于SPI设备的I/O控制操作,以发送和接收SPI协议数据
#define SPI_IOC_MESSAGE(N) _IOW(SPI_IOC_MAGIC, 0, char[SPI_MSGSIZE(N)])

/* 以下定义用于读取和设置SPI工作模式(SPI_MODE_0到SPI_MODE_3,共8位) */
#define SPI_IOC_RD_MODE			_IOR(SPI_IOC_MAGIC, 1, __u8) // 读取SPI工作模式
#define SPI_IOC_WR_MODE			_IOW(SPI_IOC_MAGIC, 1, __u8) // 设置SPI工作模式

/* 以下定义用于读取和设置SPI的位顺序(最小有效位或最大有效位先传输) */
#define SPI_IOC_RD_LSB_FIRST		_IOR(SPI_IOC_MAGIC, 2, __u8) // 读取SPI位顺序
#define SPI_IOC_WR_LSB_FIRST		_IOW(SPI_IOC_MAGIC, 2, __u8) // 设置SPI位顺序

/* 以下定义用于读取和设置SPI设备的字长(每个字的数据位数,范围1到N) */
#define SPI_IOC_RD_BITS_PER_WORD	_IOR(SPI_IOC_MAGIC, 3, __u8) // 读取SPI设备字长
#define SPI_IOC_WR_BITS_PER_WORD	_IOW(SPI_IOC_MAGIC, 3, __u8) // 设置SPI设备字长

/* 以下定义用于读取和设置SPI设备的默认最大传输速率(以Hz为单位) */
#define SPI_IOC_RD_MAX_SPEED_HZ		_IOR(SPI_IOC_MAGIC, 4, __u32) // 读取SPI设备最大传输速率
#define SPI_IOC_WR_MAX_SPEED_HZ		_IOW(SPI_IOC_MAGIC, 4, __u32) // 设置SPI设备最大传输速率

/* 以下定义用于读取和设置SPI模式字段的32位值,用于更复杂的配置需求 */
#define SPI_IOC_RD_MODE32		_IOR(SPI_IOC_MAGIC, 5, __u32) // 读取SPI模式字段(32位)
#define SPI_IOC_WR_MODE32		_IOW(SPI_IOC_MAGIC, 5, __u32) // 设置SPI模式字段(32位)

二、示例程序

示例1:同时读写

  使用 C 语言编写用户空间程序来与 SPI 设备通信。以下是一个简单的 SPI 通信示例程序:

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

int main() {
    int fd = open("/dev/spidev0.0", O_RDWR);
    if (fd < 0) {
        perror("Failed to open SPI device");
        return -1;
    }

    // 设置 SPI 模式
    uint8_t mode = SPI_MODE_0;
    if (ioctl(fd, SPI_IOC_WR_MODE, &mode) < 0) {
        perror("Failed to set SPI mode");
        return -1;
    }

    // 设置 SPI 位宽
    uint8_t bits = 8;
    if (ioctl(fd, SPI_IOC_WR_BITS_PER_WORD, &bits) < 0) {
        perror("Failed to set bits per word");
        return -1;
    }

    // 设置 SPI 速度
    uint32_t speed = 500000;
    if (ioctl(fd, SPI_IOC_WR_MAX_SPEED_HZ, &speed) < 0) {
        perror("Failed to set max speed Hz");
        return -1;
    }

    // 定义要发送和接收的数据
    uint8_t tx_buf[] = {0xAA, 0xBB, 0xCC}; // 要发送的数据
    uint8_t rx_buf[sizeof(tx_buf)] = {0};  // 接收的数据缓存

    // 设置 SPI 传输参数
    struct spi_ioc_transfer transfer = {
        .tx_buf = (uintptr_t)tx_buf,  // 发送缓冲区
        .rx_buf = (uintptr_t)rx_buf,  // 接收缓冲区
        .len = sizeof(tx_buf),        // 数据长度
        .speed_hz = speed,            // SPI 速度
        .bits_per_word = bits,        // 每字节位数
        .delay_usecs = 0,             // 延迟(微秒)
    };

    // 执行 SPI 传输
    if (ioctl(fd, SPI_IOC_MESSAGE(1), &transfer) < 0) {
        perror("Failed to transfer SPI messages");
        return -1;
    }

    // 输出接收到的数据
    printf("Received data:");
    for (size_t i = 0; i < sizeof(rx_buf); i++) {
        printf(" 0x%02X", rx_buf[i]);
    }
    printf("\n");

    close(fd);
    return 0;
}

  保存代码为 spi_example.c,然后使用以下命令编译:

gcc spi_example.c -o spi_example

  然后运行编译后的程序:

./spi_example

示例2:先写后读

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

int main() {
    int fd = open("/dev/spidev0.0", O_RDWR);
    if (fd < 0) {
        perror("Failed to open SPI device");
        return -1;
    }

    uint8_t mode = SPI_MODE_0;
    if (ioctl(fd, SPI_IOC_WR_MODE, &mode) < 0) {
        perror("Failed to set SPI mode");
        return -1;
    }

    uint8_t bits = 8;
    if (ioctl(fd, SPI_IOC_WR_BITS_PER_WORD, &bits) < 0) {
        perror("Failed to set bits per word");
        return -1;
    }

    uint32_t speed = 500000;
    if (ioctl(fd, SPI_IOC_WR_MAX_SPEED_HZ, &speed) < 0) {
        perror("Failed to set max speed Hz");
        return -1;
    }

    // 1. 写入数据
    uint8_t tx_buf[] = {0xAA, 0xBB, 0xCC}; // 要发送的数据
    struct spi_ioc_transfer transfer_write = {
        .tx_buf = (uintptr_t)tx_buf,
        .rx_buf = 0, // 不接收数据
        .len = sizeof(tx_buf),
        .speed_hz = speed,
        .bits_per_word = bits,
        .delay_usecs = 0,
    };

    if (ioctl(fd, SPI_IOC_MESSAGE(1), &transfer_write) < 0) {
        perror("Failed to write SPI data");
        return -1;
    }

    // 2. 读取数据
    uint8_t rx_buf[3] = {0}; // 接收缓冲区
    struct spi_ioc_transfer transfer_read = {
        .tx_buf = 0, // 不发送数据
        .rx_buf = (uintptr_t)rx_buf,
        .len = sizeof(rx_buf),
        .speed_hz = speed,
        .bits_per_word = bits,
        .delay_usecs = 0,
    };

    if (ioctl(fd, SPI_IOC_MESSAGE(1), &transfer_read) < 0) {
        perror("Failed to read SPI data");
        return -1;
    }

    printf("Received data:");
    for (size_t i = 0; i < sizeof(rx_buf); i++) {
        printf(" 0x%02X", rx_buf[i]);
    }
    printf("\n");

    close(fd);
    return 0;
}

示例3:先读后写

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

int main() {
    int fd = open("/dev/spidev0.0", O_RDWR);
    if (fd < 0) {
        perror("Failed to open SPI device");
        return -1;
    }

    uint8_t mode = SPI_MODE_0;
    if (ioctl(fd, SPI_IOC_WR_MODE, &mode) < 0) {
        perror("Failed to set SPI mode");
        return -1;
    }

    uint8_t bits = 8;
    if (ioctl(fd, SPI_IOC_WR_BITS_PER_WORD, &bits) < 0) {
        perror("Failed to set bits per word");
        return -1;
    }

    uint32_t speed = 500000;
    if (ioctl(fd, SPI_IOC_WR_MAX_SPEED_HZ, &speed) < 0) {
        perror("Failed to set max speed Hz");
        return -1;
    }

    // 1. 读取数据
    uint8_t rx_buf[3] = {0}; // 接收缓冲区
    struct spi_ioc_transfer transfer_read = {
        .tx_buf = 0, // 不发送数据
        .rx_buf = (uintptr_t)rx_buf,
        .len = sizeof(rx_buf),
        .speed_hz = speed,
        .bits_per_word = bits,
        .delay_usecs = 0,
    };

    if (ioctl(fd, SPI_IOC_MESSAGE(1), &transfer_read) < 0) {
        perror("Failed to read SPI data");
        return -1;
    }

    printf("Received data:");
    for (size_t i = 0; i < sizeof(rx_buf); i++) {
        printf(" 0x%02X", rx_buf[i]);
    }
    printf("\n");

    // 2. 写入数据
    uint8_t tx_buf[] = {0xAA, 0xBB, 0xCC}; // 要发送的数据
    struct spi_ioc_transfer transfer_write = {
        .tx_buf = (uintptr_t)tx_buf,
        .rx_buf = 0, // 不接收数据
        .len = sizeof(tx_buf),
        .speed_hz = speed,
        .bits_per_word = bits,
        .delay_usecs = 0,
    };

    if (ioctl(fd, SPI_IOC_MESSAGE(1), &transfer_write) < 0) {
        perror("Failed to write SPI data");
        return -1;
    }

    close(fd);
    return 0;
}

示例4:内核源码提供的demo

// SPDX-License-Identifier: GPL-2.0
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <fcntl.h>
#include <string.h>

#include <sys/ioctl.h>
#include <sys/types.h>
#include <sys/stat.h>

#include <linux/types.h>
#include <linux/spi/spidev.h>

static int verbose;

/**
 * 从指定文件描述符中读取数据
 * 
 * 此函数的目的是读取指定数量的数据,但不超过缓冲区的大小
 * 它至少会尝试读取2个字节,以确保有最小量的数据用于后续处理
 * 
 * @param fd 要读取的文件描述符
 * @param len 请求读取的字节数
 */
static void do_read(int fd, int len)
{
	unsigned char buf[32]; // 定义一个字节缓冲区,最大存储32个字节
	unsigned char *bp; // 指向缓冲区的指针,用于遍历读取的数据
	int status; // 存储read函数的返回状态

	// 确保读取的字节数不超过缓冲区大小,且至少为2字节
	if (len < 2)
		len = 2;
	else if (len > sizeof(buf))
		len = sizeof(buf);

	// 清空缓冲区,为读取新数据做准备
	memset(buf, 0, sizeof buf);

	// 尝试从文件描述符fd中读取数据
	status = read(fd, buf, len);
	if (status < 0) {
		// 如果读取失败,输出错误信息并返回
		perror("read");
		return;
	}
	if (status != len) {
		// 如果实际读取的字节数少于预期,输出提示信息并返回
		fprintf(stderr, "short read\n");
		return;
	}

	// 打印读取到的数据,前两个字节单独打印
	printf("read(%2d, %2d): %02x %02x,", len, status, buf[0], buf[1]);
	status -= 2;
	bp = buf + 2;
	// 遍历并打印剩余的字节数据
	while (status-- > 0)
		printf(" %02x", *bp++);
	printf("\n");
}

/**
 * 执行消息传输
 * 本函数通过 ioctl 接口实现 SPI (Serial Peripheral Interface) 设备的数据收发
 * 
 * @param fd 文件描述符,标识 SPI 设备
 * @param len 指定本次传输的数据长度
 */
static void do_msg(int fd, int len)
{
	// 定义结构体数组,用于描述 SPI 数据传输过程中的发送和接收操作
	struct spi_ioc_transfer xfer[2];
	// 定义缓冲区和缓冲区指针,用于数据存储和操作
	unsigned char buf[32], *bp;
	// 定义变量,用于存储 ioctl 操作的状态
	int status;

	// 初始化 xfer 和 buf,确保内存清零,避免垃圾数据影响
	memset(xfer, 0, sizeof xfer);
	memset(buf, 0, sizeof buf);

	// 确保传输长度不超过缓冲区大小,防止溢出
	if (len > sizeof buf)
		len = sizeof buf;

	//下面是一次配置两个传输的一种示例操作
	// 设置发送数据的起始字节,这是一个常见的 SPI 通信握手字节
	buf[0] = 0xaa;
	// 配置第一个 xfer 元素为发送操作,指定发送缓冲区和长度,没有配置rxbuf
	//表示只进行发送
	xfer[0].tx_buf = (unsigned long)buf;
	xfer[0].len = 1;

	// 配置第二个 xfer 元素为接收操作,指定接收缓冲区和长度
	//没有配置txbuf,表示只进行接受
	xfer[1].rx_buf = (unsigned long)buf;
	xfer[1].len = len;

	// 调用 ioctl 函数,执行 SPI 数据传输
	status = ioctl(fd, SPI_IOC_MESSAGE(2), xfer);
	// 检查 ioctl 操作状态,如果出错则打印错误信息并返回
	if (status < 0) {
		perror("SPI_IOC_MESSAGE");
		return;
	}

	// 打印接收的数据信息,包括长度和数据内容
	printf("response(%2d, %2d): ", len, status);
	for (bp = buf; len; len--)
		printf(" %02x", *bp++);
	printf("\n");
}

/**
 * 打印SPI设备的状态信息
 * 
 * @param name SPI设备的名称,用于打印输出时标识设备
 * @param fd SPI设备的文件描述符,用于执行ioctl操作获取设备状态
 * 
 * 本函数通过文件描述符fd使用ioctl系统调用,获取并打印SPI设备的当前配置和状态信息
 * 包括SPI模式、每个字的数据位数、是否先发送LSB(低位在前)以及最大传输速率
 */
static void dumpstat(const char *name, int fd)
{
	// 定义变量以存储SPI设备的状态信息
	__u8 lsb, bits;
	__u32 mode, speed;

	// 读取并打印SPI设备的模式
	if (ioctl(fd, SPI_IOC_RD_MODE32, &mode) < 0) {
		perror("SPI rd_mode");
		return;
	}
	// 读取并打印SPI设备是否配置为低位在先(LSB first)
	if (ioctl(fd, SPI_IOC_RD_LSB_FIRST, &lsb) < 0) {
		perror("SPI rd_lsb_fist");
		return;
	}
	// 读取并打印SPI设备每个字的数据位数
	if (ioctl(fd, SPI_IOC_RD_BITS_PER_WORD, &bits) < 0) {
		perror("SPI bits_per_word");
		return;
	}
	// 读取并打印SPI设备的最大传输速率
	if (ioctl(fd, SPI_IOC_RD_MAX_SPEED_HZ, &speed) < 0) {
		perror("SPI max_speed_hz");
		return;
	}

	// 打印SPI设备的状态信息
	printf("%s: spi mode 0x%x, %d bits %sper word, %d Hz max\n", name, mode,
	       bits, lsb ? "(lsb first) " : "", speed);
}

// 主函数,处理命令行参数并执行相应操作
int main(int argc, char **argv)
{
	int c;
	int readcount = 0;
	int msglen = 0;
	int fd;
	const char *name;

	// 处理命令行选项
	while ((c = getopt(argc, argv, "hm:r:v")) != EOF) {
		switch (c) {
		case 'm':
			msglen = atoi(optarg);
			if (msglen < 0)
				goto usage;
			continue;
		case 'r':
			readcount = atoi(optarg);
			if (readcount < 0)
				goto usage;
			continue;
		case 'v':
			verbose++;
			continue;
		case 'h':
		case '?':
		usage:
			fprintf(stderr,
				"usage: %s [-h] [-m N] [-r N] /dev/spidevB.D\n",
				argv[0]);
			return 1;
		}
	}

	// 检查剩余的参数
	if ((optind + 1) != argc)
		goto usage;
	name = argv[optind];

	// 打开设备文件
	fd = open(name, O_RDWR);
	if (fd < 0) {
		perror("open");
		return 1;
	}

	// 打印设备文件的状态
	dumpstat(name, fd);

	// 根据指定的消息长度执行操作
	if (msglen)
		do_msg(fd, msglen);

	// 根据指定的读取次数执行读取操作
	if (readcount)
		do_read(fd, readcount);

	// 关闭设备文件
	close(fd);
	return 0;
}

三、知识补充

“双线”、“四线”和“三线”

  在SPI (Serial Peripheral Interface) 通信中,“双线”、“四线”和“三线”指的是数据传输方式的不同变体:

  • 双线 (Dual SPI):
    • 在双线模式下,SPI 设备可以同时通过两条数据线进行数据传输。
    • 通常情况下,一条数据线用于发送数据 (MOSI), 另一条用于接收数据 (MISO)。
    • 在双线模式中,除了标准的 MOSI 和 MISO 外,还会额外使用一条数据线来增加数据吞吐量。
    • 例如,在读操作中,一条数据线用于发送地址或命令,另一条数据线用于接收数据;而在写操作中,两条数据线都用于发送数据。
  • 四线 (Quad SPI):
    • 四线模式进一步扩展了数据传输能力,使用四条数据线进行数据传输。
    • 这种模式可以在单次时钟周期内传输更多的数据,从而显著提高数据传输速率。
    • 例如,在读操作中,三条数据线用于发送地址或命令,第四条数据线用于接收数据;而在写操作中,四条数据线都用于发送数据。
  • 三线 (Three-Wire SPI):
    • 三线SPI是一种特殊的SPI模式,它减少了所需的信号线数量,仅使用三条信号线:SCK (串行时钟)、MOSI (主出从入) 和 CS (片选)。
    • 在这种模式下,MISO (主入从出) 信号线被省略了。
    • 通常用于不需要双向数据流的应用场景,比如只读或只写的存储器芯片。

  这些不同的数据传输方式提供了不同的性能和成本权衡,可以根据具体的应用需求选择最合适的传输模式。

8位、16位、32位

  当提到 SPI 的 8 位、16 位或 32 位时,这指的是 SPI 数据帧的大小,即每次传输的数据量。具体来说,这些术语指的是 SPI 通信中数据字的长度。例如,8 位 SPI 表示每次传输 8 位数据;16 位 SPI 表示每次传输 16 位数据;而 32 位 SPI 则表示每次传输 32 位数据。

SPI 数据帧大小的意义:

  1. 数据宽度:决定了每次 SPI 事务中传输的数据量。
  2. 兼容性:不同的设备可能支持不同的数据宽度,因此选择正确的数据宽度对于确保设备间的正确通信至关重要。
  3. 性能:更宽的数据帧可以提高数据传输速率,但这也取决于设备的能力和 SPI 总线的最大频率。

  位数与接线的关系:SPI的接线并不直接与数据帧的大小有关。

  SPI 通信通常使用以下四条线:SCK (Serial Clock)MOSI (Master Out Slave In)MISO (Master In Slave Out)SS (Slave Select)

  这些线路不论是在 8 位、16 位还是 32 位 SPI 中都是相同的。数据帧的大小通过软件配置来确定,而不是通过硬件接线。这些设置不会影响 SPI 的物理接线。

8位、16位等位数与硬件的关系

  1. 硬件支持:

    • 不同的 SPI 控制器硬件可能支持不同的数据位数。例如,一些控制器可能仅支持 8 位数据帧,而其他高级控制器则可能支持 8 位、16 位甚至 32 位数据帧。
  2. 寄存器配置:

    • 在硬件层面上,SPI 控制器通常会有一些寄存器用来配置数据帧的位数。例如,在 STM32 微控制器中,SPI 控制寄存器 SPI_CR1 中的 DFF 位(Data Frame Format)可以用来选择数据帧是 8 位还是 16 位。
  3. 时钟管理:

    • 数据帧的位数会影响 SPI 时钟的管理。例如,如果选择了 16 位数据帧,那么 SPI 时钟将在 16 个时钟周期内完成一次数据传输。
示例:STM32 的 SPI 控制器
	在 STM32 微控制器中,SPI 控制器支持 8 位或 16 位的数据帧,并且可以通过 SPI 
控制寄存器.

SPI_CR1 中的 DFF 位进行配置:
	如果 DFF 位被清零(0),则数据帧长度为 8 位。
	如果 DFF 位被置位(1),则数据帧长度为 16 位。

  个人水平有限,欢迎大家在评论区进行指导和交流!!!😁😁😁

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

小嵌同学

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值