SPI 设备驱动开发

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 串起来,写一个完整的传感器驱动。

我是黒漂技术佬。

相关推荐
Dlrb12111 小时前
51单片机基础知识
单片机·嵌入式硬件·51单片机
zhou135284822672 小时前
Windows 配置 WSL (Ubuntu) 环境完整指南
linux·windows·ubuntu
FREEDOM_X2 小时前
Linux 进程间通讯(IPC)——总结
linux·c语言·前端·嵌入式硬件·struts
天空'之城2 小时前
Linux 系统编程 17:零拷贝技术全解
linux·嵌入式·零拷贝技术全解
振南的单片机世界2 小时前
地址总线定门牌号,数据总线搬货量:CPU的“寻址”与“搬运”
arm开发·stm32·单片机·嵌入式硬件
网络小白不怕黑2 小时前
11.虚拟机模拟路由器实验
linux·运维·服务器·网络
Mapleay2 小时前
Linux 内核编程基础
linux·运维·服务器
梁朝辉3 小时前
单片机中ADC直接通道和附加通道什么区别?
单片机·嵌入式硬件
sg_knight3 小时前
Claude Code 安装指南(macOS / Linux / Windows WSL)
linux·windows·macos·claude·code·anthropic·claude-code