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线数
};
关键规则:
tx_buf/rx_buf可单独置空:- tx非空、rx=0:只写
- rx非空、tx=0:只读
- 都不为空:全双工同时收发
len是字节总数,tx/rx长度必须相同;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; // 实际传输字节
};
事务执行流程:
- 内核拉低CS;
- 依次执行链表内所有
spi_transfer; - 全部transfer执行完毕后拉高CS;
- 同步:阻塞等待全部完成返回;异步:直接返回,完成触发complete回调。
3. 层级关系图
spi_message(完整事务,CS全程选中)
├─ spi_transfer1(写指令)
├─ spi_transfer2(读数据)
└─ spi_transfer3(写寄存器)
二、同步 vs 异步传输区别
同步传输 spi_sync()
- 调用进程阻塞,直到整条message传输完毕;
- 简单易写,90%普通外设(Flash、OLED、传感器)优先使用;
- 适合probe、read/write系统调用上下文;
- 接口原型:
c
int spi_sync(struct spi_device *spi, struct spi_message *msg);
返回值:0成功,负数错误码。
异步传输 spi_async()
- 调用立即返回,不阻塞线程;
- 传输完成后执行
message->complete回调函数; - 适合高速大数据、DMA大批量传输(LCD、摄像头);
- 注意:回调运行在中断上下文,不能睡眠、不能调用阻塞接口。
三、内核封装简易同步API(日常优先用)
内核基于spi_sync封装好简易接口,不用手动构造message/transfer:
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);
spi_write():仅发送数据
c
static inline int spi_write(struct spi_device *spi, const void *buf, size_t len)
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;
}
代码流程说明
spi_message_init()清空初始化消息;- 构造2段transfer:发指令、读数据;
spi_message_add_tail()将transfer挂入message链表;spi_sync()提交给SPI控制器,阻塞等待传输结束;- 多线程操作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;
}
八、关键机制总结
- 分层模型
spi_transfer:最小数据分片;spi_message:完整一次CS选中的事务,由多个transfer组成;
- 同步传输 :
spi_sync阻塞,适合绝大多数外设驱动,开发成本最低; - 简易封装接口
spi_write_then_read覆盖90%寄存器读写场景,优先使用; - 多线程访问SPI必须互斥锁保护,避免多条message交错执行;
- 一条message内部默认全程CS低,仅全部transfer完成后释放;单transfer末尾可通过
cs_change提前释放片选。