【Linux驱动开发】第24天:SPI 数据收发机制

Linux SPI 数据收发:spi_transfer / spi_message 机制详解(同步+示例)

一、核心结构体基础

1. struct spi_transfer:单次传输片段

描述一段连续收发缓冲区,代表一次单向/双向数据移位操作。

c 复制代码
struct spi_transfer {
    const void *tx_buf; // 发送缓冲区,NULL只收不发
    void *rx_buf;       // 接收缓冲区,NULL只发不收
    unsigned len;       // 收发字节长度(tx/rx长度一致)
    unsigned cs_change:1; // 传输完是否拉高片选
    unsigned delay_usecs; // 本次传输后延时
    u32 speed_hz; // 本次单独时钟,覆盖spi_device默认
    u8 bits_per_word; // 位宽,覆盖默认
    u8 tx_nbits; // MOSI线数(单线/双线)
    u8 rx_nbits; // MISO线数
};

关键规则:

  1. tx_buf/rx_buf 可单独置空:
    • tx非空、rx=0:只写
    • rx非空、tx=0:只读
    • 都不为空:全双工同时收发
  2. len 是字节总数,tx/rx长度必须相同;
  3. cs_change=1:该transfer结束后释放片选;默认整条message只首尾拉/拉高CS。

2. struct spi_message:一整笔SPI事务

把多个连续spi_transfer打包为一次完整SPI操作,全程片选保持低电平,仅事务结束释放。

c 复制代码
struct spi_message {
    struct list_head transfers; // transfer链表
    struct spi_device *spi;     // 所属spi设备
    unsigned status;            // 执行状态 0成功,负数错误码
    void (*complete)(void *context); // 异步完成回调
    void *context;
    unsigned actual_length;     // 实际传输字节
};

事务执行流程:

  1. 内核拉低CS;
  2. 依次执行链表内所有spi_transfer
  3. 全部transfer执行完毕后拉高CS;
  4. 同步:阻塞等待全部完成返回;异步:直接返回,完成触发complete回调。

3. 层级关系图

复制代码
spi_message(完整事务,CS全程选中)
 ├─ spi_transfer1(写指令)
 ├─ spi_transfer2(读数据)
 └─ spi_transfer3(写寄存器)

二、同步 vs 异步传输区别

同步传输 spi_sync()

  1. 调用进程阻塞,直到整条message传输完毕;
  2. 简单易写,90%普通外设(Flash、OLED、传感器)优先使用;
  3. 适合probe、read/write系统调用上下文;
  4. 接口原型:
c 复制代码
int spi_sync(struct spi_device *spi, struct spi_message *msg);

返回值:0成功,负数错误码。

异步传输 spi_async()

  1. 调用立即返回,不阻塞线程;
  2. 传输完成后执行message->complete回调函数;
  3. 适合高速大数据、DMA大批量传输(LCD、摄像头);
  4. 注意:回调运行在中断上下文,不能睡眠、不能调用阻塞接口。

三、内核封装简易同步API(日常优先用)

内核基于spi_sync封装好简易接口,不用手动构造message/transfer:

  1. spi_write_then_read():最常用,先发指令再读数据(Flash、传感器标准操作)
c 复制代码
int spi_write_then_read(struct spi_device *spi,
    const void *txbuf, unsigned n_tx,
    void *rxbuf, unsigned n_rx);
  1. spi_write():仅发送数据
c 复制代码
static inline int spi_write(struct spi_device *spi, const void *buf, size_t len)
  1. spi_read():仅读取数据
c 复制代码
static inline int spi_read(struct spi_device *spi, void *buf, size_t len)

四、底层原生同步示例(手动构造message+transfer)

场景:W25Q读取JEDEC ID,先发送0x9F指令,再读取3字节ID。

c 复制代码
#include <linux/spi/spi.h>
#include <linux/mutex>

// 互斥锁保护SPI并发访问
static DEFINE_MUTEX(spi_lock);

static int spi_read_jedec_id(struct spi_device *spi, u8 *id_buf)
{
    int ret;
    struct spi_message msg;
    struct spi_transfer xfer[2]; // 两段传输:发指令、读ID
    u8 tx_cmd = 0x9F; // W25读ID指令

    // 初始化spi_message
    spi_message_init(&msg);
    msg.spi = spi;

    // 第一段transfer:只发送指令,不接收
    memset(&xfer[0], 0, sizeof(xfer[0]));
    xfer[0].tx_buf = &tx_cmd;
    xfer[0].rx_buf = NULL;
    xfer[0].len = 1;
    spi_message_add_tail(&xfer[0], &msg);

    // 第二段transfer:只接收数据,不发送
    memset(&xfer[1], 0, sizeof(xfer[1]));
    xfer[1].tx_buf = NULL;
    xfer[1].rx_buf = id_buf;
    xfer[1].len = 3;
    spi_message_add_tail(&xfer[1], &msg);

    mutex_lock(&spi_lock);
    ret = spi_sync(spi, &msg); // 同步阻塞传输
    mutex_unlock(&spi_lock);

    if (ret) {
        dev_err(&spi->dev, "spi sync failed ret=%d\n", ret);
        return ret;
    }
    return 0;
}

代码流程说明

  1. spi_message_init() 清空初始化消息;
  2. 构造2段transfer:发指令、读数据;
  3. spi_message_add_tail() 将transfer挂入message链表;
  4. spi_sync() 提交给SPI控制器,阻塞等待传输结束;
  5. 多线程操作SPI必须加互斥锁,防止传输交叉错乱。

五、spi_write_then_read 简化等效写法

上面手动构造message的逻辑,可一行简化,功能完全一致:

c 复制代码
u8 cmd = 0x9F;
u8 id[3];
int ret = spi_write_then_read(spi, &cmd, 1, id, 3);

内核内部自动封装spi_message、transfer、spi_sync,开发优先使用该接口。

六、纯发送、纯读同步示例

1. 仅写操作(发送单字节命令)

c 复制代码
u8 cmd = 0x06; // W25写使能指令
int ret = spi_write(spi, &cmd, 1);

2. 仅读操作(无前置指令)

c 复制代码
u8 data[4];
int ret = spi_read(spi, data, 4);

七、多段连续传输示例(多transfer)

需求:先发指令(1B) → 发地址(3B) → 读16字节数据,全程CS不释放

c 复制代码
static int spi_multi_xfer_demo(struct spi_device *spi)
{
    int ret;
    struct spi_message msg;
    struct spi_transfer xfer[3];
    u8 tx_buf[4] = {0x03,0x00,0x00,0x00}; // 读指令+地址
    u8 rx_buf[16] = {0};

    spi_message_init(&msg);
    msg.spi = spi;

    // xfer0:发送4字节指令+地址
    xfer[0].tx_buf = tx_buf;
    xfer[0].len = 4;
    spi_message_add_tail(&xfer[0], &msg);

    // xfer1:接收16字节数据
    xfer[1].rx_buf = rx_buf;
    xfer[1].len = 16;
    spi_message_add_tail(&xfer[1]);

    // xfer2:可选,传输后拉高片选
    xfer[2].cs_change = 1;
    spi_message_add_tail(&xfer[2], &msg);

    mutex_lock(&spi_lock);
    ret = spi_sync(spi, &msg);
    mutex_unlock(&spi);
    return ret;
}

八、关键机制总结

  1. 分层模型
    • spi_transfer:最小数据分片;
    • spi_message:完整一次CS选中的事务,由多个transfer组成;
  2. 同步传输spi_sync阻塞,适合绝大多数外设驱动,开发成本最低;
  3. 简易封装接口spi_write_then_read覆盖90%寄存器读写场景,优先使用;
  4. 多线程访问SPI必须互斥锁保护,避免多条message交错执行;
  5. 一条message内部默认全程CS低,仅全部transfer完成后释放;单transfer末尾可通过cs_change提前释放片选。
相关推荐
吴爃9 小时前
小微企业 SRE 稳定性建设
运维·稳定性·小微企业
天空'之城9 小时前
Linux 系统编程 10:线程同步
linux·开发语言·系统编程·线程同步
河铃旅鹿10 小时前
在Ubuntu系统上为Android交叉编译OpenSSL
android·linux·ubuntu
开开心心_Every10 小时前
带OCR识别的电子发票打印工具
运维·自动化·ocr·电脑·powerpoint·音视频·lua
长孙豪翔10 小时前
引发事件的问题
java·linux·数据库
小张成长计划..10 小时前
【Linux】7:第一个系统程序-进度条
linux·运维·服务器
智者知已应修善业10 小时前
【 LM358AD方波】2024-12-31
驱动开发·经验分享·笔记·硬件架构·硬件工程
枳实-叶10 小时前
【Linux驱动开发】第23天:spi_driver 的 probe / remove 函数实现规范
linux·驱动开发·c#
李子琪。10 小时前
云计算虚拟化技术全解析:从理论到实践
linux·centos·云计算
wuminyu10 小时前
markword在高并发场景下变化剖析
java·linux·c语言·jvm·c++