【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提前释放片选。
相关推荐
北龙云海5 小时前
案例分享 | 面向公网业务系统境内访问白名单配置实操方案
运维·网络·安全·网络安全·告警·数据泄露·入侵攻击
FoldWinCard6 小时前
D5 Linux 网络及端口命令
linux·运维·服务器
零涂毕业设计7 小时前
毕业设计模块开发-OLED显示屏(IIC协议0.96寸)STM32 ESP32 FPGA Linux驱动免费分享
linux·stm32·单片机·嵌入式·esp32·fpga·oled
tedcloud1237 小时前
OmniRoute怎么部署?开源AI模型路由平台Linux部署教程
linux·服务器·人工智能·开源·音视频
霸道流氓气质7 小时前
KMS 密钥管理服务(Key Management Service)原理与实践
linux·服务器·数据库
SakitamaX7 小时前
LVS(Linux Virtual Server)概念详解
linux·运维·lvs
小张同学a.7 小时前
Linux系统管理
linux·运维
会飞的小新7 小时前
Windows CMD 与 PowerShell 深度解析:底层原理、命令差异、自动化选型指南
运维·windows·自动化
阿龙的工作记录8 小时前
深入理解Linux设备驱动模型
linux·嵌入式硬件·工作记录
代码村新手8 小时前
Linux的基本指令
linux·运维·服务器