在 Linux 内核中,platform 、I2C 和 SPI 是三种典型的设备驱动模型。它们都基于 Linux 的设备驱动模型(总线、设备、驱动),但各自适用的硬件场景和驱动框架有所不同。下面分别介绍它们的原理及驱动代码结构。
一、Platform 总线
1. 原理
Platform 是一种虚拟总线,用于管理那些不依附于传统物理总线(如 PCI、USB、I2C、SPI)的片上设备,例如:
-
片上外设:GPIO、UART、I2C 控制器、SPI 控制器、RTC、看门狗等;
-
直接映射在内存地址空间的设备。
Platform 总线将设备分为两类:
-
platform_device:描述设备的硬件资源(寄存器地址、中断号、时钟等);
-
platform_driver:描述驱动程序的初始化、probe、remove 等操作。
当内核启动或动态加载驱动时,总线会匹配设备与驱动,匹配成功则调用驱动的 probe 函数,驱动进而初始化硬件。
2. 驱动代码结构
(1) 定义 platform_driver 结构体
cpp
#include <linux/platform_device.h>
static int my_platform_probe(struct platform_device *pdev)
{
struct resource *res;
void __iomem *base;
int irq;
// 获取寄存器资源
res = platform_get_resource(pdev, IORESOURCE_MEM, 0);
base = devm_ioremap_resource(&pdev->dev, res);
if (IS_ERR(base))
return PTR_ERR(base);
// 获取中断资源
irq = platform_get_irq(pdev, 0);
if (irq < 0)
return irq;
// 硬件初始化,注册中断等
// ...
dev_info(&pdev->dev, "probe success\n");
return 0;
}
static int my_platform_remove(struct platform_device *pdev)
{
// 释放资源,注销中断等
// ...
return 0;
}
static const struct of_device_id my_platform_of_match[] = {
{ .compatible = "vendor,my-device", },
{ /* sentinel */ }
};
MODULE_DEVICE_TABLE(of, my_platform_of_match);
static struct platform_driver my_platform_driver = {
.probe = my_platform_probe,
.remove = my_platform_remove,
.driver = {
.name = "my_platform_driver",
.of_match_table = my_platform_of_match, // 设备树匹配
},
};
module_platform_driver(my_platform_driver);
(2) 设备描述方式
内核通过 i2c_adapter 表示一个 I2C 总线控制器,通过 i2c_client 表示一个连接到该总线上的设备。设备驱动的 probe 会在 i2c_client 与 i2c_driver 匹配时被调用。
-
静态注册 (旧方式):通过
platform_device_register()或platform_add_devices()在板级文件中注册。 -
设备树(DT) (推荐):在
.dts文件中描述:my_device: my-device@12345678 {compatible = "vendor,my-device";
reg = <0x12345678 0x1000>;
interrupts = <0 42 4>;
clocks = <&clk 10>;
};内核启动时,设备树节点会被转换为
platform_device,并与驱动匹配。
二、I2C 子系统
1. 原理
I2C 子系统分为两部分:
-
I2C 总线控制器驱动(Adapter):负责控制具体 I2C 控制器的硬件操作(产生起始信号、传输字节等)。
-
I2C 设备驱动(Client Driver):针对挂载在 I2C 总线上的特定设备(如传感器、EEPROM)的驱动。
2. 驱动代码结构
(1) I2C 设备驱动(Client Driver)
cpp
#include <linux/i2c.h>
static int my_i2c_probe(struct i2c_client *client,
const struct i2c_device_id *id)
{
struct device *dev = &client->dev;
// 初始化设备,如分配私有数据结构,注册中断等
// 可以使用 i2c_transfer() 或 i2c_smbus_* 函数与设备通信
dev_info(dev, "I2C device probed\n");
return 0;
}
static int my_i2c_remove(struct i2c_client *client)
{
// 清理工作
return 0;
}
static const struct i2c_device_id my_i2c_id_table[] = {
{ "my_i2c_device", 0 },
{ }
};
MODULE_DEVICE_TABLE(i2c, my_i2c_id_table);
static const struct of_device_id my_i2c_of_match[] = {
{ .compatible = "vendor,my-i2c-device", },
{ }
};
MODULE_DEVICE_TABLE(of, my_i2c_of_match);
static struct i2c_driver my_i2c_driver = {
.driver = {
.name = "my_i2c_driver",
.of_match_table = my_i2c_of_match,
},
.probe = my_i2c_probe,
.remove = my_i2c_remove,
.id_table = my_i2c_id_table,
};
module_i2c_driver(my_i2c_driver);
(2) 设备树中描述 I2C 设备
dts
&spi0 {
cs-gpios = <&gpio 10 0>;
status = "okay";
my_device: my-spi-device@0 {
compatible = "vendor,my-spi-device";
reg = <0>; // 片选编号
spi-max-frequency = <10000000>;
spi-cpol; // 可选属性
spi-cpha;
};
};
(3) SPI 数据传输
// 同步传输
int spi_write_then_read(struct spi_device *spi,
const void *txbuf, unsigned n_tx,
void *rxbuf, unsigned n_rx);
int spi_sync(struct spi_device *spi, struct spi_message *message);
// 异步传输
int spi_async(struct spi_device *spi, struct spi_message *message);
四、三者关系与对比
| 特性 | Platform | I2C | SPI |
|---|---|---|---|
| 总线类型 | 虚拟总线(无物理总线) | 物理总线(两线制) | 物理总线(四线制 + 片选) |
| 匹配方式 | 设备树 compatible / 名称 | 设备树 compatible + reg 地址 | 设备树 compatible + reg 片选 |
| 核心结构体 | platform_driver, platform_device | i2c_driver, i2c_client, i2c_adapter | spi_driver, spi_device, spi_master |
| 主要用途 | 片上外设、内存映射设备 | 低速传感器、EEPROM | 高速传感器、显示屏、ADC |
| 数据传输 函数 | 直接访问内存映射寄存器 | i2c_transfer / SMBus 函数 | spi_sync / spi_async |
共同点
-
都遵循 Linux 设备驱动模型:驱动注册到对应总线,总线负责匹配设备与驱动。
-
都支持设备树,用于描述硬件资源。
-
都提供
probe/remove接口,驱动在probe中完成硬件初始化、中断注册、创建设备文件等。 -
都支持电源管理回调(
suspend/resume)。
五、总结
-
Platform 是 Linux 为"无总线"设备提供的通用总线模型,使得片上外设能够以统一的方式被管理。
-
I2C/SPI 是具体的物理总线子系统,内核将控制器驱动(Adapter/Master)与设备驱动(Client/Slave)分离,提高了代码复用性。
-
编写驱动时,只需关注设备侧驱动,填充相应的
*_driver结构体,实现probe和数据传输函数,并通过module_*_driver宏注册即可。
理解这三种驱动模型,是掌握 Linux 嵌入式驱动开发的基础。实际项目中,通常会结合设备树和内核提供的辅助函数(如 devm_* 资源管理)来简化驱动代码。
-
module_platform_driver宏是在较新的内核中引入的(大约是2.6.30+?),但更常见于3.x及以后。 -
设备树支持在ARM架构中从2.6.38开始引入,但广泛使用是在3.x。
-
devm_*资源管理函数是在2.6.30左右引入的。 -
of_match_table和module_platform_driver等在现代内核中普遍使用。 -
上述代码示例主要适用于 Linux 内核 3.x 及以上版本 ,也兼容部分 2.6.30+ 的较新 2.6 内核,但无法直接应用于早期的 2.6 内核(如 2.6.20 之前)或 2.4 内核。
具体版本依赖点
-
module_platform_driver()/module_i2c_driver()/module_spi_driver()这些简化注册的宏是在内核 2.6.30 左右引入的。早期版本需要手动调用
platform_driver_register()/i2c_add_driver()并在module_exit中注销。 -
设备树支持 (
of_match_table)设备树在 ARM 架构中从 2.6.38 开始引入,在 3.x 后才成为主流。早期 2.6 内核通常使用板级文件(
arch/arm/mach-xxx)静态注册设备,代码中不会出现of_match_table。 -
devm_*资源管理函数例如
devm_ioremap_resource()、devm_request_irq()等是在 2.6.30 左右加入的。早期内核需要手动申请释放资源,容易出错。 -
of_device_id和MODULE_DEVICE_TABLE(of, ...)与设备树配套的匹配表和宏,同样需要 2.6.38+ 的支持。
总结
-
如果你的目标内核是 3.x、4.x、5.x 等,代码无需修改即可使用。
-
如果使用 2.6.35 ~ 2.6.39 的部分版本,代码可能仍可编译,但需要确保内核配置开启了设备树(如果使用了
of_match_table)。 -
如果使用 2.6.30 之前的版本,上述代码需要重写为老式注册方式,并移除设备树相关部分,改用板级文件静态描述设备。
在实际开发中,建议基于较新内核(如 4.x 或 5.x),并参考内核源码中的 Documentation/ 目录获取对应版本的正确写法。