Linux SPI 子系统详解 文章封面

Linux SPI 子系统详解

一、什么是 SPI

SPI(Serial Peripheral Interface) 是一种同步串行全双工总线协议,由 Motorola 提出,广泛用于嵌入式系统中连接传感器、Flash、DAC、显示屏等外设。

1.1 物理信号线

信号方向说明
SCLKMaster → Slave时钟,由 Master 产生
MOSIMaster → SlaveMaster Out Slave In,主发从收
MISOSlave → MasterMaster In Slave Out,从发主收
CS/SSMaster → Slave片选,低有效(通常)

多个 Slave 时,每个 Slave 独享一根 CS 线,其余三线共享。

1.2 与 I²C 对比

维度SPII²C
线数4(+N个CS)2
速度几 MHz ~ 百 MHz100K / 400K / 1MHz
全双工✅❌(半双工)
寻址CS 硬件片选7/10-bit 软件地址
多主不标准✅ 支持仲裁
适用场景高速、传感器、Flash低速、多设备、配置总线

二、SPI 时序模式(CPOL / CPHA)

SPI 有 4 种模式,由时钟极性(CPOL)和相位(CPHA)决定:

模式CPOLCPHA空闲时钟采样沿
MODE 000低上升沿
MODE 101低下降沿
MODE 210高下降沿
MODE 311高上升沿
MODE 0 (CPOL=0, CPHA=0):
SCLK  ─┐ ┌─┐ ┌─┐ ┌─
        └─┘ └─┘ └─┘
MOSI  ═══X═══X═══X══  在时钟上升沿被 Slave 采样
              ↑采样

ICM-20608 使用 SPI_MODE_0(CPOL=0,CPHA=0)。


三、Linux SPI 子系统架构

┌──────────────────────────────────────────┐
│              用户空间                     │
│  spidev(/dev/spidevX.Y) 或 内核驱动      │
└──────────────────┬───────────────────────┘
                   │
┌──────────────────▼───────────────────────┐
│           SPI 核心层(spi.c)             │
│  spi_register_driver / spi_sync          │
│  设备-驱动匹配(of_match / id_table)     │
└──────────────────┬───────────────────────┘
                   │
┌──────────────────▼───────────────────────┐
│         SPI 控制器驱动(spi-imx.c)       │
│  struct spi_master / spi_controller      │
│  DMA / PIO 传输实现                       │
└──────────────────┬───────────────────────┘
                   │ 寄存器操作
┌──────────────────▼───────────────────────┐
│            硬件 SPI 控制器               │
│  iMX6ULL ECSPI1~4                        │
└──────────────────────────────────────────┘


四、核心数据结构

4.1 struct spi_device — SPI 从设备

代表总线上的一个 Slave 设备,由内核根据 DTS 自动创建。

struct spi_device {
    struct device      dev;
    struct spi_master *master;      /* 所属控制器 */
    u32                max_speed_hz; /* 最大时钟频率 */
    u8                 chip_select;  /* CS 编号 */
    u8                 bits_per_word;/* 字长,通常 8 */
    u16                mode;         /* SPI_MODE_0~3 等 */
    int                irq;          /* 中断号(DTS 配置后自动填充)*/
    /* ... */
};

常用 mode 标志:

标志说明
SPI_MODE_0~3时序模式(CPOL/CPHA 组合)
SPI_CS_HIGHCS 高有效(非标准)
SPI_LSB_FIRST低位先发
SPI_3WIRE半双工(MOSI/MISO 共用)

4.2 struct spi_transfer — 单次传输描述

struct spi_transfer {
    const void  *tx_buf;    /* 发送缓冲区(NULL = 发全0)*/
    void        *rx_buf;    /* 接收缓冲区(NULL = 丢弃)*/
    unsigned     len;       /* 字节数 */
    u32          speed_hz;  /* 本次传输的时钟(0=用设备默认)*/
    u8           bits_per_word;
    u16          delay_usecs; /* 传输后延迟 */
    unsigned     cs_change:1; /* 传输后是否切换 CS */
    struct list_head transfer_list;
};

4.3 struct spi_message — 一次完整事务

一个 spi_message 包含一组 spi_transfer,CS 在整个 message 期间保持激活:

struct spi_message {
    struct list_head transfers;  /* spi_transfer 链表 */
    struct spi_device *spi;
    int status;                  /* 完成后的错误码 */
    /* ... */
};

/* 使用示例 */
struct spi_message  msg;
struct spi_transfer xfer[2];

spi_message_init(&msg);

xfer[0].tx_buf = cmd_buf;
xfer[0].len    = 1;
spi_message_add_tail(&xfer[0], &msg);

xfer[1].rx_buf = rx_buf;
xfer[1].len    = 14;
spi_message_add_tail(&xfer[1], &msg);

ret = spi_sync(spi, &msg);  /* 同步等待完成 */

4.4 struct spi_driver — 驱动描述

struct spi_driver {
    const struct spi_device_id *id_table;
    int  (*probe)(struct spi_device *spi);
    int  (*remove)(struct spi_device *spi);
    struct device_driver driver;
};

4.5 数据结构全景类图

classDiagram
    class spi_device {
        +spi_master *master
        +u32 max_speed_hz
        +u8 chip_select
        +u8 bits_per_word
        +u16 mode
        +int irq
    }
    class spi_master {
        +u16 bus_num
        +int (*transfer_one_message)()
        +int num_chipselect
    }
    class spi_transfer {
        +const void *tx_buf
        +void *rx_buf
        +unsigned len
        +u32 speed_hz
    }
    class spi_message {
        +list_head transfers
        +spi_device *spi
        +int status
    }
    class spi_driver {
        +probe() int
        +remove() int
        +id_table
        +of_match_table
    }
    class DriverPrivData {
        <<驱动私有数据>>
        +spi_device *spi
        +mutex lock
    }

    spi_device --> spi_master : .master
    spi_message "1" --> "N" spi_transfer : transfers 链表
    spi_message --> spi_device : .spi
    spi_driver ..> spi_device : probe 匹配
    DriverPrivData --> spi_device : .spi 指针

五、常用 API

5.1 便捷函数(推荐优先使用)

/* 只写 N 字节 */
int spi_write(struct spi_device *spi, const void *buf, size_t len);

/* 只读 N 字节 */
int spi_read(struct spi_device *spi, void *buf, size_t len);

/*
 * 先写 n_tx 字节,再读 n_rx 字节,同一 CS 事务内完成。
 * 最适合"写寄存器地址 + 读数据"的读操作模式。
 */
int spi_write_then_read(struct spi_device *spi,
                        const void *txbuf, unsigned n_tx,
                        void       *rxbuf, unsigned n_rx);

5.2 手动构造 spi_message(需要精细控制时)

int spi_sync(struct spi_device *spi, struct spi_message *message);
int spi_async(struct spi_device *spi, struct spi_message *message);
/* spi_async 不阻塞,完成后调用 message-&gt;complete 回调 */

5.3 probe 中必须调用 spi_setup

spi-&gt;bits_per_word = 8;
spi-&gt;mode          = SPI_MODE_0;
ret = spi_setup(spi);   /* 将配置写入控制器硬件 */

5.4 驱动数据绑定

/* probe 中:把 iio_dev 绑定到 spi_device */
spi_set_drvdata(spi, indio_dev);

/* remove 中:取回 */
struct iio_dev *indio_dev = spi_get_drvdata(spi);


六、SPI 驱动开发模板

/* 1. 定义私有数据 */
struct mydev_data {
    struct spi_device *spi;
    struct mutex       lock;
};

/* 2. 寄存器读写 */
static int mydev_read(struct spi_device *spi, u8 reg, u8 *val)
{
    u8 cmd = reg | BIT(7);  /* 读标志(视芯片协议而定)*/
    return spi_write_then_read(spi, &amp;cmd, 1, val, 1);
}

static int mydev_write(struct spi_device *spi, u8 reg, u8 val)
{
    u8 buf[2] = { reg, val };
    return spi_write(spi, buf, 2);
}

/* 3. probe */
static int mydev_probe(struct spi_device *spi)
{
    struct mydev_data *data;
    int ret;

    /* 配置 SPI 参数 */
    spi-&gt;bits_per_word = 8;
    spi-&gt;mode          = SPI_MODE_0;
    ret = spi_setup(spi);
    if (ret) return ret;

    data = devm_kzalloc(&amp;spi-&gt;dev, sizeof(*data), GFP_KERNEL);
    if (!data) return -ENOMEM;

    data-&gt;spi = spi;
    mutex_init(&amp;data-&gt;lock);
    spi_set_drvdata(spi, data);

    /* 硬件初始化 ... */
    return 0;
}

static int mydev_remove(struct spi_device *spi)
{
    /* devm_ 资源自动释放,这里只需关闭硬件 */
    return 0;
}

/* 4. 驱动注册 */
static const struct of_device_id mydev_of_match[] = {
    { .compatible = "vendor,mydev" },
    { }
};
MODULE_DEVICE_TABLE(of, mydev_of_match);

static struct spi_driver mydev_driver = {
    .driver = {
        .name           = "mydev",
        .of_match_table = mydev_of_match,
    },
    .probe  = mydev_probe,
    .remove = mydev_remove,
};
module_spi_driver(mydev_driver);


七、iMX6ULL DTS 配置

7.1 最小配置

/* iomuxc 中定义引脚复用 */
&amp;iomuxc {
    pinctrl_ecspi3: ecspi3grp {
        fsl,pins = &lt;
            MX6UL_PAD_UART2_TX_DATA__GPIO1_IO20   0x10b0  /* CS (GPIO) */
            MX6UL_PAD_UART2_RX_DATA__ECSPI3_SCLK  0x10b1
            MX6UL_PAD_UART2_CTS_B__ECSPI3_MOSI    0x10b1
            MX6UL_PAD_UART2_RTS_B__ECSPI3_MISO    0x10b1
        &gt;;
    };
};

/* ECSPI3 控制器节点 */
&amp;ecspi3 {
    fsl,spi-num-chipselects = &lt;1&gt;;
    cs-gpios = &lt;&amp;gpio1 20 GPIO_ACTIVE_LOW&gt;;  /* 软件 GPIO 片选 */
    pinctrl-names = "default";
    pinctrl-0 = &lt;&amp;pinctrl_ecspi3&gt;;
    status = "okay";

    mydev@0 {
        compatible = "vendor,mydev";
        reg = &lt;0&gt;;                           /* CS 编号 */
        spi-max-frequency = &lt;8000000&gt;;       /* 8 MHz */
    };
};

7.2 带 INT 引脚的配置

mydev@0 {
    compatible = "vendor,mydev";
    reg = &lt;0&gt;;
    spi-max-frequency = &lt;8000000&gt;;
    interrupt-parent = &lt;&amp;gpio4&gt;;
    interrupts = &lt;16 IRQ_TYPE_EDGE_RISING&gt;; /* INT 引脚 → GPIO4_IO16 */
};

内核会将 INT 引脚的 GPIO 转换为 spi->irq,驱动直接使用。

7.3 硬件 CS vs 软件 GPIO CS

方式说明
硬件 CSDTS 中用 ECSPI3_SS0 复用,控制器自动控制
软件 GPIO CS(推荐)DTS 中用 GPIO1_IO20 复用 + cs-gpios 属性,内核 SPI 核心控制

iMX6ULL 的 ECSPI 硬件 CS 存在时序问题(CS 在每个字节之间会短暂拉高),推荐使用软件 GPIO CS。


八、spi_write_then_read 内部原理

      CS ──┐                              ┌──
           └──────────────────────────────┘
     SCLK    ┌┐┌┐┌┐┌┐┌┐┌┐┌┐┌┐ ┌┐┌┐┌┐┌┐┌┐┌┐┌┐┌┐
     MOSI  ══╪══ reg|0x80 ══╪══╪══ dummy ═╪═
     MISO  ══╪══  (忽略)   ══╪══╪══ data  ═╪═
              ←── n_tx=1B ──→  ←── n_rx=NB ──→
              └──────────── 同一 CS 事务 ───────┘

内部会构造两个 spi_transfer,通过一个 spi_message 一次性提交,CS 全程不拉高,保证读写操作的原子性。


九、常见问题

Q1:probe 不执行(driver not bound)

  • DTS 的 compatible 字符串与驱动 of_match_table 不一致
  • DTS 节点 status = "disabled" 或未写
  • 父节点(ecspi3)的 status 未设为 "okay"
  • 引脚被其他节点(如 uart2)占用导致 pinctrl 申请失败

Q2:SPI 读取全是 0xFF

  • SPI MODE 不匹配(检查 CPOL/CPHA)
  • spi_setup 未调用
  • CS 极性错误(active high vs active low)
  • spi-max-frequency 设置过高,信号质量差

Q3:spi_sync 返回 -EINVAL

  • spi_transfer.len 为 0
  • tx_buf 和 rx_buf 同时为 NULL
  • bits_per_word 控制器不支持(一般用 8)

Q4:不能在中断上下文调用 spi_sync

spi_sync 内部可能睡眠等待 DMA 完成,禁止在中断处理程序中直接调用。
解决方案:使用 Workqueue 下半部(见 17_ICM20608_SPI_IIO驱动开发)。


十、参考资料

  • Documentation/spi/ — 内核官方 SPI 文档
  • drivers/spi/spi-imx.c — iMX6ULL SPI 控制器驱动
  • include/linux/spi/spi.h — SPI 核心头文件
  • 17_ICM20608_SPI_IIO驱动开发 — SPI IIO 驱动实战

延伸阅读

理解 SPI 核心结构后,可进入 ICM-20608 实战,并与通用设备模型建立联系。

发表评论

您的邮箱地址不会被公开。 必填项已用 * 标注

滚动至顶部