4. SPI设备驱动基本框架:module_init/exit、spi_driver结构体、probe/remove回调函数实现
SPI设备驱动在Linux内核中遵循标准的设备驱动模型(Driver Model)。驱动开发者需要注册一个spi_driver结构体,并实现其核心回调函数。本章将详细拆解驱动入口/出口函数、驱动结构体定义以及probe/remove回调的完整实现流程。
4.1 驱动入口与出口:module_init / module_exit
每个SPI设备驱动都必须通过module_init和module_exit宏向内核注册/注销驱动实例。这两个宏是驱动加载和卸载的“大门”。
| 宏 | 触发时机 | 典型操作 |
|---|---|---|
module_init(x) |
insmod 或内核启动自动加载时 | 调用 spi_register_driver() |
module_exit(x) |
rmmod 或内核关闭时 | 调用 spi_unregister_driver() |
// 驱动入口函数
static int __init my_spi_driver_init(void)
{
int ret;
ret = spi_register_driver(&my_spi_driver);
if (ret)
pr_err("Failed to register SPI driver: %d\n", ret);
return ret;
}
module_init(my_spi_driver_init);
// 驱动出口函数
static void __exit my_spi_driver_exit(void)
{
spi_unregister_driver(&my_spi_driver);
}
module_exit(my_spi_driver_exit);
MODULE_LICENSE("GPL");
MODULE_AUTHOR("Embedded Linux Expert");
MODULE_DESCRIPTION("SPI device driver example");
关键点:
__init 和 __exit 标记用于优化内存布局,驱动加载后 __init 函数占用的内存会被释放。
4.2 spi_driver 结构体:驱动的“身份证”
spi_driver 是SPI设备驱动的核心结构体,定义在 include/linux/spi/spi.h 中。它包含了驱动名称、设备ID表以及最重要的 probe 和 remove 回调函数指针。
struct spi_driver {
const struct spi_device_id *id_table; // 设备ID匹配表
int (*probe)(struct spi_device *spi); // 设备匹配成功时调用
int (*remove)(struct spi_device *spi); // 设备移除时调用
void (*shutdown)(struct spi_device *spi);
struct device_driver driver; // 通用驱动结构体
};
| 成员 | 作用 | 必须实现? |
|---|---|---|
id_table |
定义驱动支持的设备列表,用于和设备树/平台数据匹配 | 是(推荐) |
probe |
设备与驱动匹配成功后执行初始化 | 是 |
remove |
设备移除或驱动卸载时执行清理 | 是 |
shutdown |
系统关机或重启时调用 | 否 |
driver |
内嵌通用驱动结构体,包含name、of_match_table等 | 是(需填充name) |
// 设备ID表
static const struct spi_device_id my_spi_ids[] = {
{ "my_spi_dev", 0 },
{ }
};
MODULE_DEVICE_TABLE(spi, my_spi_ids);
// 设备树匹配表(可选,现代驱动推荐)
static const struct of_device_id my_spi_of_match[] = {
{ .compatible = "vendor,my-spi-device" },
{ }
};
MODULE_DEVICE_TABLE(of, my_spi_of_match);
// spi_driver 实例定义
static struct spi_driver my_spi_driver = {
.driver = {
.name = "my_spi_driver",
.of_match_table = my_spi_of_match,
},
.id_table = my_spi_ids,
.probe = my_spi_probe,
.remove = my_spi_remove,
};
注意: 如果同时提供
id_table 和 of_match_table,内核优先使用设备树匹配。务必保证 driver.name 唯一,避免与其它驱动冲突。
4.3 probe 回调函数:设备初始化“工厂”
当内核发现一个SPI设备与驱动的 id_table 或 of_match_table 匹配时,自动调用 probe 函数。这是驱动完成硬件初始化、分配资源、注册字符设备/输入设备/网络设备等核心操作的入口。
static int my_spi_probe(struct spi_device *spi)
{
struct my_spi_dev *dev;
int ret;
// 1. 设置SPI通信参数
spi->mode = SPI_MODE_0;
spi->max_speed_hz = 10 * 1000 * 1000; // 10MHz
spi->bits_per_word = 8;
ret = spi_setup(spi);
if (ret)
return ret;
// 2. 分配私有数据结构
dev = devm_kzalloc(&spi->dev, sizeof(*dev), GFP_KERNEL);
if (!dev)
return -ENOMEM;
dev->spi = spi;
spi_set_drvdata(spi, dev);
// 3. 初始化硬件(如复位、读取ID等)
ret = my_spi_hw_init(dev);
if (ret)
return ret;
// 4. 注册设备(如misc设备、IIO设备等)
ret = my_spi_register_device(dev);
if (ret)
return ret;
dev_info(&spi->dev, "SPI device probed successfully\n");
return 0;
}
probe 函数典型流程:
- 配置SPI控制器参数(mode、speed、bits_per_word)并调用
spi_setup()。 - 使用
devm_kzalloc分配驱动私有数据,利用设备资源管理自动释放。 - 执行硬件初始化序列(复位、读取设备ID、配置寄存器)。
- 向内核注册设备节点(如字符设备、IIO设备、输入设备等)。
- 使用
spi_set_drvdata()保存私有数据指针,供后续回调使用。
4.4 remove 回调函数:优雅的“退场”
remove 函数在设备拔出或驱动卸载时被调用,负责释放 probe 中分配的所有资源。如果使用了 devm_ 系列API(如 devm_kzalloc、devm_request_irq),大部分资源会自动释放,但需要显式注销设备节点。
static int my_spi_remove(struct spi_device *spi)
{
struct my_spi_dev *dev = spi_get_drvdata(spi);
// 1. 注销设备节点
my_spi_unregister_device(dev);
// 2. 关闭硬件(如禁用中断、进入低功耗模式)
my_spi_hw_deinit(dev);
// 3. 清除驱动数据(可选,devm_ 会自动处理)
spi_set_drvdata(spi, NULL);
dev_info(&spi->dev, "SPI device removed\n");
return 0;
}
最佳实践: 优先使用
devm_ 系列API(设备资源管理),这样即使 probe 中途失败或 remove 被跳过,内核也能自动回收资源,避免内存泄漏。
4.5 驱动框架完整流程图
下图展示了从驱动加载到设备移除的完整生命周期,以及 spi_driver 各组件之间的协作关系。
4.6 驱动框架核心要点总结
- 入口/出口:
module_init调用spi_register_driver,module_exit调用spi_unregister_driver。 - 匹配机制: 内核优先使用设备树
of_match_table匹配,其次使用id_table匹配。 - probe 职责: 配置SPI参数、分配私有数据、初始化硬件、注册设备节点。
- remove 职责: 注销设备节点、关闭硬件、释放资源(推荐使用
devm_系列API简化管理)。 - 数据传递: 使用
spi_set_drvdata()和spi_get_drvdata()在回调之间传递私有数据。
调试检查清单:
- 确认
spi_driver.driver.name与设备树中compatible或spi-device节点名称匹配。 - 检查
probe中是否调用了spi_setup(),否则SPI控制器参数不会生效。 - 验证
remove中是否注销了probe中注册的所有设备节点,避免访问空指针。 - 使用
devm_kzalloc替代kzalloc,减少资源泄漏风险。