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核心检测到匹配的设备时被调用。典型实现步骤包括:
- 获取设备配置: 从
spi_device读取max_speed_hz、mode、bits_per_word,必要时从设备树解析附加属性。 - 分配私有数据结构: 使用
devm_kzalloc分配包含互斥锁、缓冲区、DMA描述符指针的结构体。 - 初始化硬件资源: 请求GPIO(如复位引脚)、注册中断、初始化DMA通道。
- 设置驱动数据: 调用
spi_set_drvdata(spi, priv)将私有数据挂载到设备。 - 创建设备接口: 注册字符设备、输入设备或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核心、控制器驱动之间的交互关系:
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机制。