2. SPI驱动核心结构:spi_master/spi_device结构体解析、spi_transfer消息机制、SPI模式与时钟极性配置

SPI驱动开发,说白了就是跟三个核心结构体打交道:spi_master、spi_device 和 spi_transfer。你把这几个东西搞明白了,写驱动就顺了。我刚开始接触SPI时,光看内核文档看得一头雾水,后来自己动手调了几个板子,才真正理解它们之间的关系。

核心关系一句话:spi_master 是控制器,spi_device 是外设,spi_transfer 是它们之间的消息载体。

2.1 spi_master 结构体:控制器的灵魂

spi_master 代表一个 SPI 控制器。每个 SPI 总线都有一个 master。在嵌入式 Linux 里,它通常对应 SoC 内部的 SPI 硬件模块。

我个人习惯先看这个结构体的几个关键字段:

字段 类型 说明
bus_num u16 总线编号,系统唯一标识
num_chipselect u16 支持的片选数量
mode_bits u16 支持的 SPI 模式位掩码
min_speed_hz u32 最低时钟频率
max_speed_hz u32 最高时钟频率
transfer_one_message 函数指针 核心传输函数,驱动必须实现
setup 函数指针 配置硬件参数(时钟、模式等)

注册一个 master 的典型流程:

struct spi_master *master;
struct platform_device *pdev;

// 1. 分配 master 结构体
master = spi_alloc_master(&pdev->dev, sizeof(struct my_spi_data));
if (!master)
    return -ENOMEM;

// 2. 设置硬件参数
master->bus_num = 1;
master->num_chipselect = 2;
master->mode_bits = SPI_MODE_0 | SPI_MODE_1 | SPI_MODE_2 | SPI_MODE_3;
master->min_speed_hz = 100000;
master->max_speed_hz = 50000000;

// 3. 实现核心回调
master->transfer_one_message = my_spi_transfer_one_message;
master->setup = my_spi_setup;

// 4. 注册到内核
spi_register_master(master);
我的经验:bus_num 一定要跟设备树里的 alias 对应上,否则设备匹配会出问题。我曾经在这上面浪费过半天时间。

2.2 spi_device 结构体:外设的化身

spi_device 代表挂在 SPI 总线上的一个外设。它由内核在解析设备树或板级文件时自动创建,驱动开发者主要用它来获取外设的配置信息。

关键字段:

字段 说明
master 指向所属的 spi_master
chip_select 片选编号,决定拉低哪根 CS 线
mode SPI 模式(CPOL/CPHA 组合)
max_speed_hz 该外设支持的最大时钟频率
bits_per_word 每字位数,通常是 8
irq 外设的中断号(如果有)
controller_data 控制器私有数据,平台相关

设备树中描述一个 SPI 外设的典型写法:

&spi1 {
    status = "okay";
    cs-gpios = &gpio3 10 GPIO_ACTIVE_LOW;

    spidev@0 {
        compatible = "my,spi-sensor";
        reg = <0>;          // chip_select = 0
        spi-max-frequency = <10000000>;
        spi-cpol;            // 设置 CPOL=1
        spi-cpha;            // 设置 CPHA=1
        // 等效于 SPI_MODE_3
    };
};
注意:spi_device 不是驱动自己创建的,而是内核根据设备树或板级信息创建的。驱动要做的是在 probe 里拿到这个结构体,然后配置硬件。

2.3 spi_transfer 消息机制:数据怎么跑起来

SPI 的数据传输,靠的是 spi_transfer 结构体。它描述了一次完整的 SPI 收发操作。

核心字段:

字段 说明
tx_buf 发送缓冲区,可以是 DMA 地址
rx_buf 接收缓冲区,可以是 DMA 地址
len 传输字节数
speed_hz 本次传输的时钟频率(可覆盖默认值)
bits_per_word 本次传输的每字位数
cs_change 传输完成后是否切换片选
delay_usecs 传输完成后的延迟(微秒)

一次典型的数据收发:

struct spi_device *spi;
struct spi_transfer t = {
    .tx_buf = tx_data,
    .rx_buf = rx_data,
    .len    = 32,
    .speed_hz = 5000000,
    .cs_change = 0,
};
struct spi_message msg;

spi_message_init(&msg);
spi_message_add_tail(&t, &msg);

// 同步传输,等待完成
spi_sync(spi, &msg);
关键点:SPI 是同步全双工协议。发送一个字节的同时,一定会收到一个字节。如果你只发不收,rx_buf 可以为 NULL;只收不发,tx_buf 可以为 NULL,内核会自动填充 0xFF。

为什么用 spi_message 包裹 spi_transfer?因为一次 SPI 操作可能包含多个 transfer 序列。比如:先发命令,再发地址,最后读数据。把它们串成一个 message,内核保证原子性执行。

2.4 SPI 模式与时钟极性配置

SPI 模式由两个参数决定:CPOL(时钟极性)和 CPHA(时钟相位)。组合起来就是 4 种模式。

我画了一张图,帮你直观理解:

SPI 四种模式时序对比 模式0 (CPOL=0, CPHA=0) SCLK 数据采样在上升沿 模式1 (CPOL=0, CPHA=1) SCLK 数据采样在下降沿 模式2 (CPOL=1, CPHA=0) SCLK 数据采样在下降沿 模式3 (CPOL=1, CPHA=1) SCLK 数据采样在上升沿 时钟极性对比 CPOL=0:空闲时 SCLK 为低电平 CPOL=1:空闲时 SCLK 为高电平 时钟相位对比 CPHA=0:第一个边沿采样数据 CPHA=1:第二个边沿采样数据 ⚠ 常见误区:CPOL 决定空闲电平,CPHA 决定采样边沿。两者独立配置,不要混淆。 我曾经遇到过外设手册写的是模式0,结果实际要配模式3,因为手册把 CPOL/CPHA 定义反了。

配置模式时,直接设置 spi_device->mode 即可:

// 内核预定义的宏
#define SPI_MODE_0 (0|0)          // CPOL=0, CPHA=0
#define SPI_MODE_1 (0|SPI_CPHA)   // CPOL=0, CPHA=1
#define SPI_MODE_2 (SPI_CPOL|0)   // CPOL=1, CPHA=0
#define SPI_MODE_3 (SPI_CPOL|SPI_CPHA) // CPOL=1, CPHA=1

// 在驱动中设置
spi->mode = SPI_MODE_2;
spi_setup(spi);  // 应用配置到硬件
避坑指南:我曾经在一个项目里,外设要求模式1,但板子默认配成了模式0。现象是数据能发出去,但读回来的全是乱码。用逻辑分析仪一看,采样边沿错了。所以,调 SPI 的第一步,先拿示波器或逻辑分析仪确认时钟极性和数据采样点对不对。

2.5 整体结构关系图

最后,我把这几个核心概念串起来,画一张关系图:

SPI 驱动核心结构关系 spi_master SPI 控制器 spi_device SPI 外设 spi_transfer 数据传输单元 管理 发起 spi_message 传输队列(含多个 transfer) 封装 包含 💡 工作流程:spi_master 管理总线 → spi_device 代表外设 → 驱动构造 spi_message → 添加一个或多个 spi_transfer → 调用 spi_sync() 或 spi_async() 发起传输

嗯,到这里,SPI 驱动的核心骨架就清楚了。你写驱动时,无非就是:注册 master、匹配 device、构造 transfer、发起 message。把这几个结构体玩熟,剩下的就是具体硬件的适配了。