4. SPI设备驱动编写:spi_device结构体、spi_driver注册、probe/remove回调函数实现

在SPI子系统框架中,设备驱动位于最上层,直接与具体的SPI外设(如ADC、DAC、传感器、显示控制器)交互。本章聚焦于如何编写一个标准的SPI设备驱动,涵盖核心数据结构、驱动注册流程以及生命周期回调函数的实现。

4.1 核心数据结构:spi_device

struct spi_device 是内核为每个SPI从设备创建的抽象描述,它包含了设备与主控制器之间的连接参数、传输模式以及私有数据指针。驱动开发者通过该结构体获取硬件配置并执行数据传输。

成员 类型 说明
dev struct device 内核通用设备结构体,用于设备模型管理
master struct spi_master * 指向所属SPI主控制器的指针
max_speed_hz u32 SPI通信时钟频率(Hz)
chip_select u8 片选信号编号(由控制器分配)
mode u8 SPI模式(CPOL/CPHA组合,如SPI_MODE_0)
bits_per_word u8 每字位数,通常为8
irq int 设备中断号(若使用中断)
controller_state void * 控制器私有状态指针
controller_data void * 板级特定配置数据(由平台代码设置)
driver_data void * 驱动私有数据指针(最常用,用于挂载自定义结构体)
关键提示: driver_data 是驱动与设备实例之间的桥梁。在 probe 回调中,通常分配一个包含设备状态、锁、DMA缓冲区指针的自定义结构体,并通过 spi_set_drvdata() 将其挂载到 spi_device 上。

4.2 spi_driver 注册与注销

SPI设备驱动通过 struct spi_driver 结构体向内核注册。该结构体封装了驱动名称、设备ID表以及生命周期回调函数。

#include <linux/spi/spi.h>
#include <linux/module.h>
#include <linux/of.h>  /* 设备树支持 */

static const struct of_device_id my_spi_of_match[] = {
    { .compatible = "vendor,my-spi-device" },
    { /* sentinel */ }
};
MODULE_DEVICE_TABLE(of, my_spi_of_match);

static const struct spi_device_id my_spi_id[] = {
    { "my-spi-device", 0 },
    { }
};
MODULE_DEVICE_TABLE(spi, my_spi_id);

static struct spi_driver my_spi_driver = {
    .driver = {
        .name   = "my_spi_driver",
        .of_match_table = my_spi_of_match,
    },
    .probe    = my_spi_probe,
    .remove   = my_spi_remove,
    .id_table = my_spi_id,
};

module_spi_driver(my_spi_driver);
注册要点:
  • module_spi_driver() 宏展开为 module_init 和 module_exit,自动完成 spi_register_driver 与 spi_unregister_driver 的调用。
  • of_match_table 用于设备树匹配,id_table 用于传统平台匹配(如ACPI或板级代码)。
  • 驱动名称应具有唯一性,避免与内核其他驱动冲突。

4.3 probe 回调函数实现

probe 是驱动初始化的核心入口,在SPI核心检测到匹配的设备时被调用。典型实现步骤包括:

  1. 获取设备配置: 从 spi_device 读取 max_speed_hz、mode、bits_per_word,必要时从设备树解析附加属性。
  2. 分配私有数据结构: 使用 devm_kzalloc 分配包含互斥锁、缓冲区、DMA描述符指针的结构体。
  3. 初始化硬件资源: 请求GPIO(如复位引脚)、注册中断、初始化DMA通道。
  4. 设置驱动数据: 调用 spi_set_drvdata(spi, priv) 将私有数据挂载到设备。
  5. 创建设备接口: 注册字符设备、输入设备或IIO设备,使能用户空间访问。
static int my_spi_probe(struct spi_device *spi)
{
    struct my_spi_priv *priv;
    int ret;

    /* 1. 配置SPI设备参数 */
    spi->mode = SPI_MODE_0;
    spi->max_speed_hz = 10 * 1000 * 1000; /* 10 MHz */
    spi->bits_per_word = 8;
    ret = spi_setup(spi);
    if (ret)
        return ret;

    /* 2. 分配私有数据 */
    priv = devm_kzalloc(&spi->dev, sizeof(*priv), GFP_KERNEL);
    if (!priv)
        return -ENOMEM;

    /* 3. 初始化锁与缓冲区 */
    mutex_init(&priv->lock);
    priv->spi = spi;
    priv->tx_buf = devm_kmalloc(&spi->dev, BUF_SIZE, GFP_KERNEL);
    priv->rx_buf = devm_kmalloc(&spi->dev, BUF_SIZE, GFP_KERNEL);
    if (!priv->tx_buf || !priv->rx_buf)
        return -ENOMEM;

    /* 4. 挂载私有数据 */
    spi_set_drvdata(spi, priv);

    /* 5. 注册字符设备(示例) */
    ret = my_register_cdev(priv);
    if (ret)
        return ret;

    dev_info(&spi->dev, "SPI device probed at %d Hz\n", spi->max_speed_hz);
    return 0;
}
常见错误: 在 probe 中忘记调用 spi_setup() 会导致控制器未按预期配置模式与速率;使用 devm_ 系列API可自动管理资源释放,避免 remove 中遗漏清理。

4.4 remove 回调函数实现

remove 回调在设备移除或驱动卸载时执行,负责反向清理 probe 中分配的资源。由于大量使用 devm_ 托管资源,remove 通常只需执行非托管操作:

  • 注销字符设备或输入设备(cdev_del / device_destroy)
  • 释放中断(若使用 request_irq 而非 devm_request_irq)
  • 关闭DMA通道(若使用 dma_request_channel 而非托管版本)
  • 关闭电源或复位硬件(若需要)
static int my_spi_remove(struct spi_device *spi)
{
    struct my_spi_priv *priv = spi_get_drvdata(spi);

    /* 注销字符设备 */
    my_unregister_cdev(priv);

    /* 关闭硬件(示例) */
    gpio_set_value(priv->reset_gpio, 0);

    dev_info(&spi->dev, "SPI device removed\n");
    return 0;
}
最佳实践: 如果 probe 中所有资源均使用 devm_ 系列函数分配,remove 甚至可以只返回0,内核会自动释放托管资源。但显式清理非托管资源(如字符设备号)仍是安全编码的必要步骤。

4.5 设备驱动与控制器驱动的协作流程

下图展示了SPI设备驱动在probe阶段与SPI核心、控制器驱动之间的交互关系:

SPI设备驱动 Probe 协作流程 设备树/平台代码 SPI核心 (spi.c) SPI控制器驱动 spi_device 创建 匹配 spi_driver 调用 probe 回调 spi_setup() 配置 分配私有数据 注册设备接口 说明:设备驱动 probe 在 SPI 核心完成设备创建与匹配后触发,驱动负责配置参数、分配资源并注册对外接口

4.6 设备树匹配与板级配置

现代嵌入式Linux平台广泛使用设备树(Device Tree)来描述SPI从设备。一个典型的设备树节点如下:

&spi1 {
    status = "okay";
    pinctrl-names = "default";
    pinctrl-0 = <&pinctrl_spi1>;

    my_device@0 {
        compatible = "vendor,my-spi-device";
        reg = <0>;               /* 片选编号 */
        spi-max-frequency = <10000000>;  /* 10 MHz */
        spi-cpol;                /* 设置CPOL=1 */
        spi-cpha;                /* 设置CPHA=1 */
        reset-gpios = <&gpio3 15 GPIO_ACTIVE_LOW>;
    };
};
设备树属性 对应spi_device成员 说明
compatible 驱动匹配 与 of_match_table 中的字符串匹配
reg chip_select 片选索引,由SPI控制器分配
spi-max-frequency max_speed_hz 最大时钟频率
spi-cpol / spi-cpha mode 设置SPI模式位
reset-gpios 驱动私有解析 通过 of_get_named_gpio 获取
驱动解析设备树属性示例:
static int my_spi_parse_dt(struct spi_device *spi, struct my_spi_priv *priv)
{
    struct device_node *np = spi->dev.of_node;

    if (!np)
        return 0; /* 非设备树平台 */

    priv->reset_gpio = of_get_named_gpio(np, "reset-gpios", 0);
    if (gpio_is_valid(priv->reset_gpio)) {
        devm_gpio_request_one(&spi->dev, priv->reset_gpio,
                              GPIOF_OUT_INIT_HIGH, "spi-reset");
    }
    return 0;
}

4.7 驱动编写检查清单

在完成SPI设备驱动编写后,建议对照以下清单进行验证:

  • 结构体完整性: spi_driver 是否包含 probe、remove 和至少一个ID表?
  • 参数配置: probe 中是否调用 spi_setup() 使配置生效?
  • 资源管理: 是否优先使用 devm_ 系列API简化错误路径?
  • 并发保护: 共享数据(如缓冲区、状态标志)是否使用互斥锁或自旋锁保护?
  • 错误处理: 每一步分配或注册失败后是否返回正确的负值错误码?
  • 设备树兼容: 是否添加了 of_match_table 并导出 MODULE_DEVICE_TABLE?
  • 模块化: 是否包含 MODULE_LICENSE、MODULE_AUTHOR、MODULE_DESCRIPTION?
性能陷阱: 在 probe 中执行耗时操作(如固件加载、长时间复位等待)会阻塞内核设备枚举流程。对于需要长时间初始化的设备,考虑使用工作队列或异步probe机制。