SPI 设备驱动开发
SPI 是另一种常用的串行外设总线,速度比 I2C 快,适合高速传输。这篇讲 Linux SPI 子系统架构、spi_driver 框架、四种 SPI 模式、读写传输 API,以及完整的 SPI 设备驱动示例。
大家好,我是黒漂技术佬。
I2C 简单但速度慢,SPI 速度快(几十 MHz 很常见),适合高速传输的场景,比如显示屏、Flash、高速 ADC、OLED 屏。
SPI 是四线制(MOSI、MISO、SCLK、CS),全双工,没有地址概念,靠片选信号选设备。
这篇讲 SPI 驱动怎么写:SPI 模式、spi_driver 框架、设备树配置、读写传输。
一、SPI 总线简介
四线制
| 线 | 名称 | 方向 | 说明 |
|---|---|---|---|
| SCLK | 串行时钟 | 主机→从机 | 时钟信号 |
| MOSI | 主发从收 | 主机→从机 | 主机输出,从机输入 |
| MISO | 主收从发 | 从机→主机 | 从机输出,主机输入 |
| CS / SS | 片选 | 主机→从机 | 低电平选中设备 |
- 每个从机一根独立的 CS 线
- 同一时间只有一个 CS 有效(选中一个设备)
- 全双工:发数据的同时也收数据
四种模式(CPOL + CPHA)
SPI 有时钟极性和相位两个参数,组合成四种模式:
| 模式 | CPOL(时钟极性) | CPHA(时钟相位) | 说明 |
|---|---|---|---|
| Mode 0 | 0(空闲低) | 0(第一个边沿采样) | 最常用 |
| Mode 1 | 0(空闲低) | 1(第二个边沿采样) | |
| Mode 2 | 1(空闲高) | 0(第一个边沿采样) | |
| Mode 3 | 1(空闲高) | 1(第二个边沿采样) | 也比较常用 |
- CPOL:空闲时 SCLK 的电平。0=低,1=高
- CPHA:在第几个时钟沿采样数据。0=第一个沿,1=第二个沿
驱动里要跟从设备的 datasheet 对应上,模式不对读出来的数据全是错的。
速度
SPI 速度比 I2C 快很多,常见 10MHz、20MHz,有的能到 50MHz 以上。
二、Linux SPI 子系统架构
跟 I2C 类似,三层结构:
SPI 设备驱动(我们写的)
↓
SPI 核心(spi-core)
↓
SPI 主机驱动(控制器驱动,SoC厂商提供)
spi_master
SPI 主机控制器,对应 SoC 的 SPI 外设。
spi_device
一个 SPI 从设备,包含片选、速度、模式等信息。
spi_driver
SPI 设备驱动,我们写的部分。
三、SPI 驱动框架
spi_driver 结构体
c
#include <linux/spi/spi.h>
static int my_probe(struct spi_device *spi)
{
// 初始化
return 0;
}
static void my_remove(struct spi_device *spi)
{
// 清理
}
static const struct of_device_id my_of_match[] = {
{ .compatible = "vendor,my-spi-device" },
{}
};
MODULE_DEVICE_TABLE(of, my_of_match);
static struct spi_driver my_driver = {
.probe = my_probe,
.remove = my_remove,
.driver = {
.name = "my-spi-device",
.of_match_table = my_of_match,
},
};
module_spi_driver(my_driver);
module_spi_driver 宏
自动注册注销,省得写 module_init/exit。
spi_device
probe 的参数,代表一个 SPI 从设备:
c
struct spi_device {
struct device dev;
struct spi_master *master;
u32 max_speed_hz; // 最大时钟频率
u8 chip_select; // 片选号
u8 mode; // SPI 模式(CPOL|CPHA 等)
u8 bits_per_word; // 每字位数,一般8
// ...
};
常用的 mode 标志:
SPI_MODE_0~SPI_MODE_3:四种模式SPI_CS_HIGH:片选高电平有效(默认低有效)SPI_LSB_FIRST:低位先发(默认高位先发)SPI_3WIRE:三线模式(MOSI/MISO 共用一根线)
四、设备树中的 SPI 设备
SPI 控制器节点
dts
spi0: spi@20000000 {
compatible = "vendor,spi-controller";
reg = <0x20000000 0x1000>;
#address-cells = <1>;
#size-cells = <0>;
status = "okay";
};
SPI 设备子节点
dts
&spi0 {
flash@0 {
compatible = "vendor,spi-flash";
reg = <0>; // 片选号(第几个CS)
spi-max-frequency = <50000000>; // 最大频率 50MHz
spi-cpol; // 时钟极性(CPOL=1)
spi-cpha; // 时钟相位(CPHA=1)→ Mode3
status = "okay";
};
lcd@1 {
compatible = "vendor,lcd";
reg = <1>; // 第二个片选
spi-max-frequency = <10000000>;
// 没写 spi-cpol/cpha 就是 Mode 0
dc-gpios = <&gpio0 3 GPIO_ACTIVE_HIGH>;
reset-gpios = <&gpio0 4 GPIO_ACTIVE_LOW>;
};
};
常用属性
| 属性 | 说明 |
|---|---|
reg |
片选号(第几个 CS) |
spi-max-frequency |
最大时钟频率(Hz) |
spi-cpol |
CPOL=1(空闲时钟高) |
spi-cpha |
CPHA=1(第二个沿采样) |
spi-cs-high |
片选高电平有效 |
spi-lsb-first |
低位先发 |
spi-3wire |
三线模式 |
两个都不写就是 Mode 0,都写就是 Mode 3。
片选 GPIO
有些 SPI 控制器的片选用 GPIO 模拟,设备树里配置:
dts
spi0 {
cs-gpios = <&gpio0 5 GPIO_ACTIVE_LOW>,
<&gpio0 6 GPIO_ACTIVE_LOW>;
};
五、SPI 读写传输 API
1. 最简单的读写:spi_write / spi_read
c
// 写
int spi_write(struct spi_device *spi, const void *buf, size_t len);
// 读(先发0,收数据)
int spi_read(struct spi_device *spi, void *buf, size_t len);
2. 写+读:spi_write_then_read
先写一段,再读一段,最常用的模式(发寄存器地址,读数据):
c
int spi_write_then_read(struct spi_device *spi,
const void *txbuf, unsigned n_tx,
void *rxbuf, unsigned n_rx);
- 先发送 n_tx 字节 txbuf
- 紧接着接收 n_rx 字节到 rxbuf
- 片选一直保持有效
传感器、寄存器型设备常用这个。
3. 通用传输:spi_sync
最灵活的方式,构造 spi_message:
c
#include <linux/spi/spi.h>
struct spi_transfer t = {
.tx_buf = tx_buf,
.rx_buf = rx_buf,
.len = len,
.speed_hz = 10000000, // 可以单独指定这次的速度
};
struct spi_message msg;
spi_message_init(&msg);
spi_message_add_tail(&t, &msg);
int ret = spi_sync(spi, &msg);
多个 transfer 组合
一次消息可以包含多个 transfer,片选保持:
c
struct spi_transfer t1 = { .tx_buf = cmd, .len = 1 };
struct spi_transfer t2 = { .rx_buf = data, .len = 4 };
spi_message_init(&msg);
spi_message_add_tail(&t1, &msg);
spi_message_add_tail(&t2, &msg);
spi_sync(spi, &msg);
spi_transfer 常用字段
| 字段 | 说明 |
|---|---|
| tx_buf | 发送缓冲区 |
| rx_buf | 接收缓冲区 |
| len | 传输长度 |
| speed_hz | 本次速度(可选,默认用设备的) |
| bits_per_word | 每字位数 |
| delay_usecs | 传输后延时 |
| cs_change | 传输后是否改变片选 |
4. 异步传输:spi_async
异步版本,不阻塞,完成后回调:
c
int spi_async(struct spi_device *spi, struct spi_message *msg);
msg 里设置 complete 回调函数。一般驱动用 spi_sync 同步的就够了。
六、封装寄存器读写
跟 I2C 类似,SPI 设备一般也封装成读写寄存器的函数:
c
// 写寄存器:cmd + data
static int reg_write(struct spi_device *spi, u8 reg, u8 val)
{
u8 buf[2] = { reg, val };
return spi_write(spi, buf, sizeof(buf));
}
// 读寄存器:先发reg,再读data
static int reg_read(struct spi_device *spi, u8 reg, u8 *val)
{
u8 cmd = reg | 0x80; // 假设读标志是最高位
return spi_write_then_read(spi, &cmd, 1, val, 1);
}
// 连续读多个
static int reg_read_bulk(struct spi_device *spi, u8 reg, u8 *buf, int len)
{
u8 cmd = reg | 0x80;
return spi_write_then_read(spi, &cmd, 1, buf, len);
}
具体的命令格式要看设备 datasheet,每个芯片不一样。
七、完整示例:SPI 传感器驱动
设备树
dts
&spi0 {
sensor@0 {
compatible = "vendor,spi-sensor";
reg = <0>;
spi-max-frequency = <10000000>;
status = "okay";
};
};
驱动代码
c
#include <linux/module.h>
#include <linux/spi/spi.h>
#include <linux/of.h>
#define REG_ID 0x00
#define REG_DATA 0x01
#define REG_CTRL 0x02
#define READ_FLAG 0x80
struct sensor_data {
struct spi_device *spi;
// ...
};
static int reg_read(struct spi_device *spi, u8 reg, u8 *val)
{
u8 cmd = reg | READ_FLAG;
return spi_write_then_read(spi, &cmd, 1, val, 1);
}
static int reg_write(struct spi_device *spi, u8 reg, u8 val)
{
u8 buf[2] = { reg, val };
return spi_write(spi, buf, 2);
}
static int sensor_read_data(struct sensor_data *data, u16 *result)
{
u8 buf[2];
u8 cmd = REG_DATA | READ_FLAG;
int ret;
ret = spi_write_then_read(data->spi, &cmd, 1, buf, 2);
if (ret) return ret;
*result = (buf[0] << 8) | buf[1];
return 0;
}
static int sensor_probe(struct spi_device *spi)
{
struct sensor_data *data;
u8 id;
int ret;
data = devm_kzalloc(&spi->dev, sizeof(*data), GFP_KERNEL);
if (!data) return -ENOMEM;
data->spi = spi;
spi_set_drvdata(spi, data);
// 设置 SPI 模式(设备树也可以配,这里也可以再设置)
spi->mode = SPI_MODE_0;
spi->bits_per_word = 8;
ret = spi_setup(spi);
if (ret) return ret;
// 读 ID 寄存器验证芯片
ret = reg_read(spi, REG_ID, &id);
if (ret) {
dev_err(&spi->dev, "读取ID失败\n");
return ret;
}
dev_info(&spi->dev, "传感器 ID: 0x%02x\n", id);
// 初始化配置
reg_write(spi, REG_CTRL, 0x01);
return 0;
}
static void sensor_remove(struct spi_device *spi)
{
// 清理
}
static const struct of_device_id sensor_of_match[] = {
{ .compatible = "vendor,spi-sensor" },
{}
};
MODULE_DEVICE_TABLE(of, sensor_of_match);
static struct spi_driver sensor_driver = {
.probe = sensor_probe,
.remove = sensor_remove,
.driver = {
.name = "spi-sensor",
.of_match_table = sensor_of_match,
},
};
module_spi_driver(sensor_driver);
MODULE_LICENSE("GPL");
MODULE_AUTHOR("Heipiao");
MODULE_DESCRIPTION("SPI 传感器驱动示例");
八、SPI 驱动和 I2C 驱动对比
| I2C | SPI | |
|---|---|---|
| 线数 | 2根(SCL+SDA) | 4根(SCLK+MOSI+MISO+CS) |
| 寻址 | 7/10位地址 | 片选线,每设备一根 |
| 速度 | 100k/400k | 几M到几十M |
| 全双工 | 半双工 | 全双工 |
| 多设备 | 地址区分,总线共用 | 每设备独立CS |
| 驱动结构 | i2c_driver / i2c_client | spi_driver / spi_device |
| 常用读写 | i2c_smbus_* | spi_write_then_read |
整体框架很像,就是 API 不一样。会写 I2C 驱动,SPI 也差不多。
九、常见坑
坑 1:SPI 模式不对
CPOL/CPHA 跟设备不匹配,读出来全是乱码。仔细看 datasheet,确认模式。
坑 2:速度太快
设的频率超过设备支持的最大值,通信不稳定。从低速开始调,慢慢往上加。
坑 3:片选极性搞反
默认低电平有效,设备是高有效就加 spi-cs-high 属性。
坑 4:读命令格式不对
不同芯片读命令不一样,有的是最高位设1,有的是地址+0x80,有的先发dummy字节。严格按 datasheet 来。
坑 5:DMA 对齐问题
用 spi_sync 的时候,tx_buf/rx_buf 最好用 DMA 安全的内存(dma_alloc 或者 kmalloc 对齐的),不然可能出问题。
坑 6:CS 脚没配对
设备树 reg 是片选号,不是 GPIO 号。cs-gpios 里第几个对应 reg 是几。
十、本篇小结
- SPI:四线制全双工串行总线,速度快,靠片选选设备
- 四种模式:CPOL(时钟极性)+ CPHA(时钟相位)组合,Mode 0 最常用
- Linux SPI 子系统:spi_master 主机、spi_device 设备、spi_driver 驱动
- 设备树:SPI 设备作为子节点,reg 是片选号,spi-max-frequency 设速度,spi-cpol/cpha 设模式
- 常用 API:spi_write、spi_read、spi_write_then_read(最常用)、spi_sync(灵活)
- spi_message + spi_transfer 可以组合多次传输,片选保持
- 驱动框架跟 I2C 很像,probe/remove + of_match_table + module_spi_driver
- 常见问题:模式不对、速度太快、片选极性、命令格式错误
下一篇是系列收尾实战篇:传感器驱动从 0 到 1,把前面讲的字符设备、中断、定时器、I2C/SPI 串起来,写一个完整的传感器驱动。
我是黒漂技术佬。