跳转到内容
新建笔记

无设备树平台驱动:设备注册、匹配与资源回收

platform 是 Linux 设备模型中的一种软件总线,常用于不能自动枚举的片上或板级设备。设备实例提供资源和配置,驱动实现具体硬件的操作逻辑,总线负责匹配和绑定。设备实例可以来自设备树、ACPI,也可以由 C 代码注册;本文用最后一种方式说明基本流程。

“接口名字相同”不足以保证驱动可复用。例如两块屏幕都连接 SPI,并不代表控制命令、初始化时序和电源管理相同。platform 模型把实例配置与驱动实现分开,但驱动仍需理解对应硬件的编程模型。

platform 总线连接设备实例与驱动的原图

platform 总线本身必须先可用。设备与驱动可以先后注册,尚未有匹配伙伴时可以等待;两者都存在且匹配后,核心才尝试调用 probe。设备已经注册、驱动已经注册以及设备成功绑定,是不同状态。

对象/接口职责
struct platform_device一台设备实例:名字、ID、资源表、属性/平台数据和内嵌 struct device
struct platform_driver一套驱动:probe、移除/关机等回调、匹配表及内嵌 device_driver
platform_driver_register注册驱动,不是“把设备挂到总线”
platform_driver_unregister撤销驱动并解绑其设备
platform_device_register...注册设备实例;具体便捷接口可代管分配和资源表复制
platform_device_unregister撤销设备并放弃注册引用,返回 void
platform_set/get_drvdata保存/取得每个已探测实例的私有状态

probe 的主要工作是取得资源、初始化实例并公开接口;不是在每次探测时重新注册一遍 platform 驱动。shutdown 服务于系统关机路径,与设备解绑时的移除回调并非同一个事件。电源管理回调则应按目标版本的 PM 机制编写。platform 驱动模型

原始平台设备名称、ID 与驱动对应示意

Linux 的匹配不限于图中的名称相等;还可能使用 override、OF、ACPI 或 platform ID 表,完整顺序见 设备树平台驱动。对于这里的无固件示例,platform ID 表提供明确匹配。

设备 .id 的两个特殊值不能混淆:

ID常量效果
-1PLATFORM_DEVID_NONE不附加实例 ID 后缀;不是自动分配
-2PLATFORM_DEVID_AUTO由框架分配自动 ID
非负整数指定实例号区分同名设备的实例;仍要避免设备模型中的名称冲突

设备的 .release 在最后一个引用放掉时调用,不保证与 platform_device_unregister 的函数返回同步发生。把静态设备配上一个空 .release 不是可以通用复制的内存管理方案;动态对象应由明确的释放回调回收。

3. 可独立构建的双模块示例

跳转到“3. 可独立构建的双模块示例”

下面用一个纯软件的 0~255 数值展示 C 代码注册设备与驱动绑定,不伪造真实板上的 MMIO 地址。设备模块和驱动模块各是一个完整文件,可分别构建和加载。

platform_device_register_simple 负责分配设备并使用框架的释放方法;本例无内存或 IRQ 资源,所以传 NULL, 0。

vlog_pdev.c
// SPDX-License-Identifier: GPL-2.0-only
#include <linux/module.h>
#include <linux/platform_device.h>
static struct platform_device *vlog_device;
static int __init vlog_device_init(void)
{
vlog_device = platform_device_register_simple("vlog_platform",
PLATFORM_DEVID_NONE,
NULL, 0);
return PTR_ERR_OR_ZERO(vlog_device);
}
static void __exit vlog_device_exit(void)
{
platform_device_unregister(vlog_device);
}
module_init(vlog_device_init);
module_exit(vlog_device_exit);
MODULE_LICENSE("GPL");
MODULE_DESCRIPTION("One software platform device without firmware resources");

驱动的状态为每个设备单独分配,sysfs 的 value 属性只接受十进制 0~255。发布属性放在初始化之后;managed 资源按逆序释放,先撤销属性再释放状态。

vlog_pdrv.c
// SPDX-License-Identifier: GPL-2.0-only
#include <linux/module.h>
#include <linux/mod_devicetable.h>
#include <linux/mutex.h>
#include <linux/platform_device.h>
#include <linux/slab.h>
#include <linux/string.h>
#include <linux/sysfs.h>
struct vlog_platform_state {
struct mutex lock;
unsigned int value;
};
static ssize_t value_show(struct device *dev, struct device_attribute *attr,
char *buf)
{
struct vlog_platform_state *s = dev_get_drvdata(dev);
unsigned int value;
mutex_lock(&s->lock);
value = s->value;
mutex_unlock(&s->lock);
return sysfs_emit(buf, "%u\n", value);
}
static ssize_t value_store(struct device *dev, struct device_attribute *attr,
const char *buf, size_t count)
{
struct vlog_platform_state *s = dev_get_drvdata(dev);
unsigned int value;
int ret;
if (memchr(buf, '\0', count))
return -EINVAL;
ret = kstrtouint(buf, 10, &value);
if (ret)
return ret;
if (value > 255)
return -ERANGE;
mutex_lock(&s->lock);
s->value = value;
mutex_unlock(&s->lock);
return count;
}
static DEVICE_ATTR_RW(value);
static struct attribute *vlog_attrs[] = { &dev_attr_value.attr, NULL };
ATTRIBUTE_GROUPS(vlog);
static int vlog_probe(struct platform_device *pdev)
{
struct vlog_platform_state *s;
s = devm_kzalloc(&pdev->dev, sizeof(*s), GFP_KERNEL);
if (!s)
return -ENOMEM;
mutex_init(&s->lock);
platform_set_drvdata(pdev, s);
/* devres removes the group before freeing the earlier allocation. */
return devm_device_add_groups(&pdev->dev, vlog_groups);
}
static const struct platform_device_id vlog_ids[] = {
{ "vlog_platform", 0 },
{ }
};
MODULE_DEVICE_TABLE(platform, vlog_ids);
static struct platform_driver vlog_driver = {
.probe = vlog_probe,
.id_table = vlog_ids,
.driver = { .name = "vlog_platform" },
};
module_platform_driver(vlog_driver);
MODULE_LICENSE("GPL");
MODULE_DESCRIPTION("Per-device software state for the platform bus example");

Makefile 写为:

obj-m += vlog_pdev.o vlog_pdrv.o

按 目标内核的外部模块构建方法 编译。在内核版本与配置相符的实验系统上,设备模块与驱动模块任一先加载都可以;两者就绪且 probe 成功后,可读取 /sys/bus/platform/devices/vlog_platform/value,或向它写入 42。

设备创建成功并不保证 probe 成功:属性冲突、内存不足等错误仍会留下未绑定的设备,需检查驱动链接和日志。驱动移除后其属性消失,设备模块可以仍在;设备模块移除后该实例消失,驱动可以继续等待其他设备。

4. 从软件示例扩展到硬件资源

跳转到“4. 从软件示例扩展到硬件资源”

无设备树的板级代码可向 platform_device 提供资源表。例如内存资源包含 start、包含式 end 和 IORESOURCE_MEM,设备的 num_resources 与数组长度一致。资源长度应用 resource_size(res);四字节寄存器的结束地址是起始地址加 3,不是加 4。

驱动按资源项调用 platform_get_resource(pdev, IORESOURCE_MEM, index),检查返回值,再对属于本设备的独占寄存器区使用 devm_platform_ioremap_resource 等帮助函数。中断应通过 platform_get_irq 取得并检查返回值,不把另一个平台的板级 IRQ 宏复制过来。

原笔记的 PIO 示例把 0x01c20890 等 GPIOE 地址作为资源;这些地址和位域已集中核对在 字符设备的 GPIOE 表。在已有 pinctrl/GPIO 控制器的 Linux 系统中,普通 LED 消费者应申请具体引脚,不能另占同一控制器的寄存器区域。

原 probe 还曾对未分配的 cdev * 直接写成员,并在硬件和失败清理尚未就绪时公开设备入口。硬件驱动需要同时检查分配、资源、映射、设备号、类、节点与回调注册各步骤,失败时逆序清理。使用 managed 资源可以减少部分清理代码,但不能代替打开文件和异步操作的寿命设计。

5. 版本与生命周期边界

跳转到“5. 版本与生命周期边界”

本组代码固定在 Linux 6.8:class_create 已是单参数接口;platform 头文件既保留旧式 int remove,也提供推荐用于过渡的 void remove_new。不能把一个版本的结构体手抄后当作其他版本的兼容声明。本例无额外停止动作,完全由属性组和内存的 managed 释放次序完成解绑,因而不必定义空移除函数。Linux 6.8 platform 声明

若增加字符设备或 misc 入口,解绑时可能仍有已打开文件;如果增加 IRQ、DMA、定时器或工作队列,也可能仍有后台访问。应先停止入口与生产者并等待在途操作,再释放资源。相关文件引用问题见 miscdevice。

两个完整模块在 Ubuntu 6.8.0-146-generic 的 x86-64 构建资料下通过编译、MODPOST 和最终模块链接,数值属性回调做了主机模型的边界与实例隔离检查。没有在当前主机加载它们,也没有声称完成真实 platform 总线绑定/解绑压力测试。