其实就是两个,一个虚拟总线,一个物理总线。

1 Platform虚拟总线

虚拟总线是静态加载的,就是说这些设备一开始就是集成在SOC里面的,不需要替换也没法替换,启动的时候直接把驱动挂上去就行。

一个简单的例子如下:

DTS

my_device@10000000 {
    compatible = "demo,my-platform-device";
    reg = <0x10000000 0x1000>;
    my-value = <1234>;
};

这里的<0x10000000 0x1000>就是SoC内部IP寄存器的位置和范围,一般来说,也只有直接访问Soc寄存器的,才是Platform设备。

Driver,主要是传入的dev是platform_device。

#include <linux/module.h>
#include <linux/platform_device.h>
#include <linux/of.h>

static int my_probe(struct platform_device *pdev)
{
    u32 value;
    dev_info(&pdev->dev, "probe called\n");

    if (!of_property_read_u32(
            pdev->dev.of_node,
            "my-value",
            &value)) {
        dev_info(&pdev->dev,
                 "my-value = %d\n",
                 value);
    }

    return 0;
}

static int my_remove(struct platform_device *pdev)
{
    dev_info(&pdev->dev, "remove called\n");
    return 0;
}

static const struct of_device_id my_match_table[] = {
    {
        .compatible = "demo,my-platform-device",
    },
    {},
};

MODULE_DEVICE_TABLE(of, my_match_table);

static struct platform_driver my_driver = {
    .probe = my_probe,
    .remove = my_remove,
    .driver = {
        .name = "my_platform_driver",
        .of_match_table = my_match_table,
    },
};

module_platform_driver(my_driver);

MODULE_LICENSE("GPL");
MODULE_AUTHOR("demo");

挂载的位置是在/sys/bus/platform/。

常用的接口大概有这些。

类别函数作用常用场景
设备注册platform_device_register()注册一个 platform_device手动创建设备
platform_device_unregister()注销 platform_device模块退出
platform_device_register_simple()简单创建 platform_device测试驱动、老式代码
platform_device_register_data()创建设备并附带私有数据非DT设备
资源获取platform_get_resource()获取设备资源(resource)获取寄存器地址、IRQ
platform_get_resource_byname()根据名字获取资源多个reg资源时
寄存器映射devm_platform_ioremap_resource()获取并映射MMIO寄存器最常用
devm_platform_ioremap_resource_byname()根据名字映射寄存器多个寄存器区域
中断获取platform_get_irq()获取第N个IRQDTS interrupts
platform_get_irq_byname()根据名字获取IRQ多中断设备
DMA资源platform_get_resource() + IORESOURCE_DMA获取DMA资源DMA设备
私有数据管理platform_set_drvdata()保存driver私有数据probe保存状态
platform_get_drvdata()获取driver私有数据remove/ioctl使用
driver注册platform_driver_register()注册platform_driver模块初始化
platform_driver_unregister()注销platform_driver模块退出
module_platform_driver()快速注册driver推荐写法
设备模型pdev->dev获取通用device对象devm接口、日志、电源
pdev->name获取设备名字调试
pdev->id获取设备编号多实例设备
Device Treepdev->dev.of_node获取DT节点读取DTS属性
of_property_read_u32()读取整数属性配置参数
of_property_read_string()读取字符串属性配置参数
of_get_named_gpio()获取GPIO(旧接口)GPIO配置
gpiod_get()获取GPIO descriptor(新接口)推荐方式

不过对于现在的Linux框架来说,基本上不用自己去注册device了。

2 物理总线驱动

对于任何物理总线来说,实际上都分为两个部分。一个是控制器controller,这部分是算作platform driver,这里就不多说了。另一个部分是总线上的device,这部分这次写一下。

物理总线有很多,比如USB/PICE/I2C/SPI/UART等等,目前的流程其实是控制器controller先启动。之后controller再去挂载总线下面的设备。

这里也就是说对于设备树,首先解析最公共的部分,然后挨个加载虚拟总线platform下的设备,之后各个控制器扫描并加载自己对应的部分。

下面是一个典型的SPI设备驱动:

DTS

可以看到,节点是挂在某一个spi controller下面。首先使能该控制器,然后再描述这个节点的具体信息。

&qupv3_se0_spi {
	status = "okay";

	/* 我们的自定义 SPI 子设备 */
	my_spi_sensor: my-spi-sensor@0 {
		compatible = "demo,my-spi-sensor";
		reg = <0>;                     /* 对应 SPI Chip Select (CS0) */
		spi-max-frequency = <10000000>;/* 最大 SPI 时钟频率:10 MHz */
		
		/* 常用可选属性 */
		/* spi-cpha; */                /* 时钟相位 (Phase) */
		/* spi-cpol; */                /* 时钟极性 (Polarity) */
		
		/* 如果有中断引脚 (GPIO 中断) */
		/*
		interrupt-parent = <&tlmm>;
		interrupts = <12 IRQ_TYPE_EDGE_RISING>;
		*/
	};
};

Driver,可以看到,这里传入的是spi_device *spi,这个指针在spi子系统里面已经做了初始化,并且赋值controller,

#include <linux/init.h>
#include <linux/module.h>
#include <linux/spi/spi.h>
#include <linux/of.h>

#define DRIVER_NAME "my_spi_sensor"

/* 驱动结构体 */
struct my_sensor_data {
	struct spi_device *spi;
	/* 可在此添加 mutex、cdev、kfifo 等 */
};

/* 读取寄存器辅助函数 */
static int my_sensor_read_reg(struct spi_device *spi, u8 reg, u8 *val)
{
	int ret;
	u8 tx_buf[2] = { reg | 0x80, 0x00 }; /* 假设最高位 1 表示读操作 */
	u8 rx_buf[2] = { 0 };

	/* 标准同步读写传输 */
	ret = spi_write_then_read(spi, &tx_buf[0], 1, rx_buf, 1);
	if (ret < 0) {
		dev_err(&spi->dev, "Failed to read reg 0x%02x, err: %d\n", reg, ret);
		return ret;
	}

	*val = rx_buf[0];
	return 0;
}

/* SPI 匹配成功后调用的 Probe 函数 */
static int my_sensor_probe(struct spi_device *spi)
{
	struct my_sensor_data *data;
	u8 chip_id = 0;
	int ret;

	dev_info(&spi->dev, "Probing my_spi_sensor on Qualcomm platform...\n");

	/* 1. 设置 SPI 传输模式(如需要重写 DTS 配置) */
	spi->mode = SPI_MODE_0;
	spi->bits_per_word = 8;
	ret = spi_setup(spi);
	if (ret < 0) {
		dev_err(&spi->dev, "spi_setup failed: %d\n", ret);
		return ret;
	}

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

	data->spi = spi;
	spi_set_drvdata(spi, data);

	/* 3. 尝试与 SPI 芯片通讯(如读取 Chip ID 校验硬件是否存在) */
	ret = my_sensor_read_reg(spi, 0x00, &chip_id);
	if (ret == 0) {
		dev_info(&spi->dev, "Read Chip ID success: 0x%02x\n", chip_id);
	}

	/* 在此处注册 sysfs、cdev 或 input 设备等给用户态使用 */

	return 0;
}

/* 设备解绑/卸载时调用 */
static void my_sensor_remove(struct spi_device *spi)
{
	dev_info(&spi->dev, "Removing my_spi_sensor driver\n");
}

/* 1. 设备树匹配表 (OF Matching Table) */
static const struct of_device_id my_sensor_of_match[] = {
	{ .compatible = "demo,my-spi-sensor" },
	{ /* sentinel */ }
};
MODULE_DEVICE_TABLE(of, my_sensor_of_match);

/* 2. SPI 驱动核心结构体 */
static struct spi_driver my_sensor_driver = {
	.driver = {
		.name = DRIVER_NAME,
		.of_match_table = my_sensor_of_match,
	},
	.probe = my_sensor_probe,
	.remove = my_sensor_remove,
};

module_spi_driver(my_sensor_driver);

MODULE_AUTHOR("Qualcomm Developer");
MODULE_DESCRIPTION("Simple SPI Sensor Driver Demo");
MODULE_LICENSE("GPL v2");

这里操作的方法就五花八门了,要看各个具体的控制器定义了。

图片来源见水印。

Logo

openEuler 是由开放原子开源基金会孵化的全场景开源操作系统项目,面向数字基础设施四大核心场景(服务器、云计算、边缘计算、嵌入式),全面支持 ARM、x86、RISC-V、loongArch、PowerPC、SW-64 等多样性计算架构

更多推荐