platform 是 Linux 设备模型中的一种软件总线,常用于不能自动枚举的片上或板级设备。设备实例提供资源和配置,驱动实现具体硬件的操作逻辑,总线负责匹配和绑定。设备实例可以来自设备树、ACPI,也可以由 C 代码注册;本文用最后一种方式说明基本流程。
“接口名字相同”不足以保证驱动可复用。例如两块屏幕都连接 SPI,并不代表控制命令、初始化时序和电源管理相同。platform 模型把实例配置与驱动实现分开,但驱动仍需理解对应硬件的编程模型。
1. 设备、驱动与总线
跳转到“1. 设备、驱动与总线”
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 驱动模型
2. 名称与 ID
跳转到“2. 名称与 ID”
Linux 的匹配不限于图中的名称相等;还可能使用 override、OF、ACPI 或 platform ID 表,完整顺序见 设备树平台驱动。对于这里的无固件示例,platform ID 表提供明确匹配。
设备 .id 的两个特殊值不能混淆:
| ID | 常量 | 效果 |
|---|---|---|
-1 | PLATFORM_DEVID_NONE | 不附加实例 ID 后缀;不是自动分配 |
-2 | PLATFORM_DEVID_AUTO | 由框架分配自动 ID |
| 非负整数 | 指定实例号 | 区分同名设备的实例;仍要避免设备模型中的名称冲突 |
设备的 .release 在最后一个引用放掉时调用,不保证与 platform_device_unregister 的函数返回同步发生。把静态设备配上一个空 .release 不是可以通用复制的内存管理方案;动态对象应由明确的释放回调回收。
3. 可独立构建的双模块示例
跳转到“3. 可独立构建的双模块示例”下面用一个纯软件的 0~255 数值展示 C 代码注册设备与驱动绑定,不伪造真实板上的 MMIO 地址。设备模块和驱动模块各是一个完整文件,可分别构建和加载。
设备文件:vlog_pdev.c
跳转到“设备文件:vlog_pdev.c”platform_device_register_simple 负责分配设备并使用框架的释放方法;本例无内存或 IRQ 资源,所以传 NULL, 0。
// 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");驱动文件:vlog_pdrv.c
跳转到“驱动文件:vlog_pdrv.c”驱动的状态为每个设备单独分配,sysfs 的 value 属性只接受十进制 0~255。发布属性放在初始化之后;managed 资源按逆序释放,先撤销属性再释放状态。
// 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。
6. 验证范围
跳转到“6. 验证范围”两个完整模块在 Ubuntu 6.8.0-146-generic 的 x86-64 构建资料下通过编译、MODPOST 和最终模块链接,数值属性回调做了主机模型的边界与实例隔离检查。没有在当前主机加载它们,也没有声称完成真实 platform 总线绑定/解绑压力测试。