一、概述
在看到platform总线时,不知道你心里有没有疑问?platform总线是什么,有什么用?在解答这个问题之前,我们先聊点linux驱动的分离。
1.1 linux驱动的分离
我们知道linux操作系统可以运行在各种SOC上,比如我们熟知的SOC芯片S3C2440 S3C6410和S5PV210等,由于处理器芯片的不同,那么就会导致即使对于同一硬件设备,其驱动程序也会略有不同,这就造成内核代码很冗余。
以SOC通过I2C控制三轴加速度陀螺仪MPU6550为例:
从图中可以看到不同的SOC都有一自己的I2C控制器驱动和设备驱动,因为每个SOC的I2C控制器不同,所以每个SOC必须有自己的I2C控制器驱动,但是MPU6050是一样的,所以完全可以共用一套设备驱动代码。
完善后的框架如下:
当然,这只是对于 I2C 下的 MPU6050 这个设备,实际情况下,I2C 下肯定会挂载很多设备,根据这个思路,我们可以得到框架为:
然而在实际开发中,I2C控制器驱动半导体厂家会编好,设备驱动也会由设备厂家编好,我们只需要提供设备信息即可,如设备接到哪个I2C接口上,I2C速度为多少。
这样就相当于把设备信息从设备驱动中剥离出来,而设备驱动也会用标准方法去获取设备信息(如从设备树中获取设备信息),这样就相当于驱动只负责驱动,设备(信息)只负责设备,想办法将两者进行匹配即可,来做这个匹配工作的就是总线,这就构成了linux中的总线-驱动-设备模型。结构图如下:
1.2 platform驱动模型
上面我们介绍了设备驱动的分离,得到总线-驱动-设备模型,这个总线就是我们平常所说的I2C、SPI、USB、UART等总线,这些也都是物理总线。
然而并不是所有设备都是通过总线和SOC连接的,为此linux从2.6就加入了platform设备驱动,也就做平台设备驱动,它是一条虚拟的总线,并不是一个物理的总线。如下图所示:
这里需要注意的是,platform 总线是区别于 USB、SPI、I2C 这些总线的虚拟总线。说它虚拟是因为 SOC 与一些外设如 LED、定时器、蜂鸣器是通过内存的寻址空间来进行寻址的,所以 CPU 与这些设备通信压根就不需要总线,那么硬件上也就没有这样一个总线。但内核有对这些设备做统一管理的需求,所以就对这些直接通过内存寻址的设备虚拟了一条 platform 总线,所有直接通过内存寻址的设备都映射到这条虚拟总线上。
platform 总线的优点:
- 通过 platform 总线,可以遍历所有挂载在 platform 总线上的设备;
- 设备和驱动的分离,通过 platform 总线,设备和驱动是分开注册的,因为有 probe 函数,可以随时检测与设备匹配的驱动,匹配成功就会把这个驱动向内核注册;
- 一个驱动可供同类的几个设备使用,这个功能的实现是因为驱动注册过程中有一个遍历设备的操作。
二、platform总线
2.1 platform总线类型定义
在linux 设备模型中,总线类型由bus_type 结构表示,我们所用的 I2C、SPI、USB 都是用这个结构体来定义的。该结构体定义在 include/linux/device.h文件中:
/**
* struct bus_type - The bus type of the device
*
* @name: The name of the bus.
* @dev_name: Used for subsystems to enumerate devices like ("foo%u", dev->id).
* @dev_root: Default device to use as the parent.
* @bus_groups: Default attributes of the bus.
* @dev_groups: Default attributes of the devices on the bus.
* @drv_groups: Default attributes of the device drivers on the bus.
* @match: Called, perhaps multiple times, whenever a new device or driver
* is added for this bus. It should return a positive value if the
* given device can be handled by the given driver and zero
* otherwise. It may also return error code if determining that
* the driver supports the device is not possible. In case of
* -EPROBE_DEFER it will queue the device for deferred probing.
* @uevent: Called when a device is added, removed, or a few other things
* that generate uevents to add the environment variables.
* @probe: Called when a new device or driver add to this bus, and callback
* the specific driver's probe to initial the matched device.
* @remove: Called when a device removed from this bus.
* @shutdown: Called at shut-down time to quiesce the device.
*
* @online: Called to put the device back online (after offlining it).
* @offline: Called to put the device offline for hot-removal. May fail.
*
* @suspend: Called when a device on this bus wants to go to sleep mode.
* @resume: Called to bring a device on this bus out of sleep mode.
* @num_vf: Called to find out how many virtual functions a device on this
* bus supports.
* @dma_configure: Called to setup DMA configuration on a device on
* this bus.
* @pm: Power management operations of this bus, callback the specific
* device driver's pm-ops.
* @iommu_ops: IOMMU specific operations for this bus, used to attach IOMMU
* driver implementations to a bus and allow the driver to do
* bus-specific setup
* @p: The private data of the driver core, only the driver core can
* touch this.
* @lock_key: Lock class key for use by the lock validator
* @need_parent_lock: When probing or removing a device on this bus, the
* device core should lock the device's parent.
*
* A bus is a channel between the processor and one or more devices. For the
* purposes of the device model, all devices are connected via a bus, even if
* it is an internal, virtual, "platform" bus. Buses can plug into each other.
* A USB controller is usually a PCI device, for example. The device model
* represents the actual connections between buses and the devices they control.
* A bus is represented by the bus_type structure. It contains the name, the
* default attributes, the bus' methods, PM operations, and the driver core's
* private data.
*/
struct bus_type {
const char *name;
const char *dev_name;
struct device *dev_root;
const struct attribute_group **bus_groups;
const struct attribute_group **dev_groups;
const struct attribute_group **drv_groups;
int (*match)(struct device *dev, struct device_driver *drv);
int (*uevent)(struct device *dev, struct kobj_uevent_env *env);
int (*probe)(struct device *dev);
int (*remove)(struct device *dev);
void (*shutdown)(struct device *dev);
int (*online)(struct device *dev);
int (*offline)(struct device *dev);
int (*suspend)(struct device *dev, pm_message_t state);
int (*resume)(struct device *dev);
int (*num_vf)(struct device *dev);
int (*dma_configure)(struct device *dev);
const struct dev_pm_ops *pm;
const struct iommu_ops *iommu_ops;
struct subsys_private *p;
struct lock_class_key lock_key;
bool need_parent_lock;
};
其中部分字段的含义如下:
- name:总线名称;
- bus_groups:总线属性;
- dev_groups:该总线上所有设备的默认属性;
- drv_groups:该总线上所有驱动的默认属性;
- match:当有新的设备或驱动添加到总线上时match函数被调用,如果设备和驱动可以匹配,返回0;
- uevent:当一个设备添加、移除或添加环境变量时,函数调用;
- probe:当有新设备或驱动添加时,probe函数调用,并且回调该驱动的probe函数来初始化相关联的设备;
- remove:设备移除时调用remove函数;
- shutdown:设备关机时调用shutdown函数;
- suspend:设备进入睡眠时调用suspend函数;
- resume:设备唤醒时调用resume函数;
- pm:总线的电源管理选项,并回调设备驱动的电源管理模块;
platform 总线是 bus_type 类型的全局变量,这个变量已经被 linux 内核赋值好了,其结构体成员对应的函数也已经在内核里面写好,定义在drivers/base/platform.c:
struct bus_type platform_bus_type = {
.name = "platform",
.dev_groups = platform_dev_groups,
.match = platform_match,
.uevent = platform_uevent,
.dma_configure = platform_dma_configure,
.pm = &platform_dev_pm_ops,
};
这里我们重点关注platform匹配函数platform_match 即可。
2.2 platform设备和驱动匹配
platform_bus_type 中的 platform_match 就是我们前面所说的做驱动和设备匹配的函数,不同的总线对应的 match 函数肯定不一样,这个我们不用管,内核都会写好。我们所用的 platform 总线对应的 match 函数是 platform_match 函数,该函数定义在drivers/base/platform.c:
/**
* platform_match - bind platform device to platform driver.
* @dev: device.
* @drv: driver.
*
* Platform device IDs are assumed to be encoded like this:
* "<name><instance>", where <name> is a short description of the type of
* device, like "pci" or "floppy", and <instance> is the enumerated
* instance of the device, like '0' or '42'. Driver IDs are simply
* "<name>". So, extract the <name> from the platform_device structure,
* and compare it against the name of the driver. Return whether they match
* or not.
*/
static int platform_match(struct device *dev, struct device_driver *drv)
{
struct platform_device *pdev = to_platform_device(dev);
struct platform_driver *pdrv = to_platform_driver(drv);
/* When driver_override is set, only bind to the matching driver */
if (pdev->driver_override)
return !strcmp(pdev->driver_override, drv->name);
/* Attempt an OF style match first */
if (of_driver_match_device(dev, drv))
return 1;
/* Then try ACPI style match */
if (acpi_driver_match_device(dev, drv))
return 1;
/* Then try to match against the id table */
if (pdrv->id_table)
return platform_match_id(pdrv->id_table, pdev) != NULL;
/* fall-back to driver name match */
return (strcmp(pdev->name, drv->name) == 0);
}
该函数有两个参数:设备和设备驱动,该函数主要做了一下事情:
- 将设备转为平台设备类型;
- 将驱动转为平台驱动类型;
- 调用of_driver_match_device进行设备树OF类型匹配;
- 调用acpi_driver_match_device进行ACPI类型匹配;
- 如果设置值了pdrv->id_table,进行id_table匹配;
- 最后比较platform的驱动和设备里面的name信息;
通过对上面匹配函数的一个简单分析,我们知道匹配函数做匹配的顺序是先匹配设备树,然后匹配 id_table 表,然后才是匹配name字段。对于支持设备树的 Linux 版本,我们一上来做设备树匹配就完事,不支持设备树时,我们就得定义 platform 设备,再用id_tabale表或name匹配,一般情况下都是选用name匹配。
2.3 platform总线注册
platform模块的初始化是由platform_bus_init函数完成的。该函数在内核启动阶段被调用,我们来简单看下调用过程:
-
start_kernel-> rest_init ->kernel_init-> do_basic_setup -> driver_init-> platform_bus_init;
platform_bus_init定义在drivers/base/platform.c文件中:
int __init platform_bus_init(void)
{
int error;
early_platform_cleanup();
error = device_register(&platform_bus);
if (error) {
put_device(&platform_bus);
return error;
}
error = bus_register(&platform_bus_type);
if (error)
device_unregister(&platform_bus);
of_platform_register_reconfig_notifier();
return error;
}
我们重点关注bus_register总线注册函数,传入的参数就是我们上面介绍的platform_bus_type。
bus_register函数调用后,就会在用户空间生成platform相关文件,执行如下命令:
root@zhengyang:/work/sambashare/linux-5.2.8# ls /sys/bus/platform/
devices drivers drivers_autoprobe drivers_probe uevent
/sys/bus/platform/devices里用来存放的是platform设备,/sys/bus/platform/drivers里用来存放的是platform驱动:
root@zhengyang:/work/sambashare/linux-5.2.8# ls /sys/bus/platform/devices/
ACPI0003:00 Fixed MDIO bus.0 pcspkr PNP0001:00 reg-dummy
alarmtimer i8042 platform-framebuffer.0 PNP0800:00 serial8250
root@zhengyang:/work/sambashare/linux-5.2.8# ls /sys/bus/platform/drivers
acpi-fan clk-lpt ehci-platform parport_pc syscon uart-sccnxp
alarmtimer clk-pmc-atom gpio-clk poweroff-restart tpm_tis vesa-framebuffer
amd_gpio crystal_cove_pwm i2c_designware rc5t583-gpio tps6586x-gpio virtio-mmio
byt_gpio dwc2 i8042 reg-dummy tps65910-gpio
cannonlake-pinctrl dw-pcie lp_gpio serial8250 tps68470-gpio
charger-manager e820_pmem ohci-platform simple-framebuffer tps68470_pmic_opregion
cherryview-pinctrl efi-framebuffer palmas-gpio sram twl4030-audio
三、platform驱动
3.1 platform驱动定义
在linux 设备模型中,platform驱动由platform_driver 结构表示,该结构体定义在 include/linux/platform_device.h文件中:
struct platform_driver {
int (*probe)(struct platform_device *);
int (*remove)(struct platform_device *);
void (*shutdown)(struct platform_device *);
int (*suspend)(struct platform_device *, pm_message_t state);
int (*resume)(struct platform_device *);
struct device_driver driver;
const struct platform_device_id *id_table;
bool prevent_deferred_probe;
};
其中部分成员的含义如下:
- probe:当驱动和硬件信息匹配成功之后,就会调用probe函数,驱动所有的资源的注册和初始化全部放在probe函数中;
- remove:硬件信息被移除了,或者驱动被卸载了,全部要释放,释放资源的操作就放在该函数中;
- driver:驱动基类,内核维护的所有的驱动必须包含该成员,通常driver->name用于和设备进行匹配;
- id_table:往往一个驱动可能能同时支持多个硬件,这些硬件的名字都放在该结构体数组中;
platform_driver中的id_table在platform总线进行驱动和设备匹配时使用,id_table是一个数组,里面每个元素类型都是pltform_device_id,platform_device_id结构定义在include/linux/mod_devicetable.h文件中:
struct platform_device_id {
char name[PLATFORM_NAME_SIZE];
kernel_ulong_t driver_data;
};
platform_driver中driver是一个驱动基类,相当于驱动具有的最基础的属性,driver是struct device_driver类型,定义在include/linux/device.h文件中:
/**
* struct device_driver - The basic device driver structure
* @name: Name of the device driver.
* @bus: The bus which the device of this driver belongs to.
* @owner: The module owner.
* @mod_name: Used for built-in modules.
* @suppress_bind_attrs: Disables bind/unbind via sysfs.
* @probe_type: Type of the probe (synchronous or asynchronous) to use.
* @of_match_table: The open firmware table.
* @acpi_match_table: The ACPI match table.
* @probe: Called to query the existence of a specific device,
* whether this driver can work with it, and bind the driver
* to a specific device.
* @remove: Called when the device is removed from the system to
* unbind a device from this driver.
* @shutdown: Called at shut-down time to quiesce the device.
* @suspend: Called to put the device to sleep mode. Usually to a
* low power state.
* @resume: Called to bring a device from sleep mode.
* @groups: Default attributes that get created by the driver core
* automatically.
* @pm: Power management operations of the device which matched
* this driver.
* @coredump: Called when sysfs entry is written to. The device driver
* is expected to call the dev_coredump API resulting in a
* uevent.
* @p: Driver core's private data, no one other than the driver
* core can touch this.
*
* The device driver-model tracks all of the drivers known to the system.
* The main reason for this tracking is to enable the driver core to match
* up drivers with new devices. Once drivers are known objects within the
* system, however, a number of other things become possible. Device drivers
* can export information and configuration variables that are independent
* of any specific device.
*/
struct device_driver {
const char *name;
struct bus_type *bus;
struct module *owner;
const char *mod_name; /* used for built-in modules */
bool suppress_bind_attrs; /* disables bind/unbind via sysfs */
enum probe_type probe_type;
const struct of_device_id *of_match_table;
const struct acpi_device_id *acpi_match_table;
int (*probe) (struct device *dev);
int (*remove) (struct device *dev);
void (*shutdown) (struct device *dev);
int (*suspend) (struct device *dev, pm_message_t state);
int (*resume) (struct device *dev);
const struct attribute_group **groups;
const struct dev_pm_ops *pm;
void (*coredump) (struct device *dev);
struct driver_private *p;
};
driver中of_match_table也是一个匹配表,这个匹配表是platform总线给驱动和设备做匹配时使用设备树匹配时用的,也是一个数组,数组元素都为 of_device_id 类型,该类型结构体定义在include/linux/mod_devicetable.h文件中:
struct of_device_id {
char name[32];
char type[32];
char compatible[128]; /* 使用设备树匹配时就是把设备节点的 compatible 属性值和 of_match_table 中
每个项目的这个 compatible 作比较,如果有相等的就表示设备和驱动匹配成功 */
const void *data;
};
3.2 platform驱动注册
用platform_driver 结构体定义好platform驱动后,用platform_driver_register函数向linux内核注册platform驱动,函数定义在drivers/base/platform.c:
/**
* __platform_driver_register - register a driver for platform-level devices
* @drv: platform driver structure
* @owner: owning module/driver
*/
int __platform_driver_register(struct platform_driver *drv,
struct module *owner)
{
drv->driver.owner = owner;
drv->driver.bus = &platform_bus_type;
drv->driver.probe = platform_drv_probe;
drv->driver.remove = platform_drv_remove;
drv->driver.shutdown = platform_drv_shutdown;
return driver_register(&drv->driver);
}
这里首先初始化platform驱动里的驱动基类driver,然后调用driver_register进行驱动注册,关于驱动注册函数driver_register下一篇我们会详细介绍:。
这里简单介绍一下driver_register函数执行流程:
- 将platform驱动添加到platform总线上;
- 遍历platform总线上的所有platform设备,然后进行驱动和设备匹配;
- 匹配成功后最终会执行platform驱动的probe函数,过程中的驱动基类driver的probe函数和 platform_drv_probe 函数都是达到这个目的的中转函数而已。
3.3 platform驱动卸载
用platform_driver_unregister函数卸载platform驱动,函数定义在drivers/base/platform.c:
/**
* platform_driver_unregister - unregister a driver for platform-level devices
* @drv: platform driver structure
*/
void platform_driver_unregister(struct platform_driver *drv)
{
driver_unregister(&drv->driver);
}
可以看到最终调用的driver_unregister,这里就不具体介绍了。
四、platform设备
如果我们用的 Linux 版本支持设备树,那就在设备树中去描述设备,如果不支持设备树,就要定义好 platform 设备。
这里我们需要考虑的一个点是:
- 总线下的匹配函数match在做匹配时是先设备树匹配,然后id_table表匹配,然后才是 name 字段匹配;
- 支持设备树时,直接在设备树节点里面改设备信息,内核启动时会自动遍历设备树节点,匹配成功就会自动生成一个platform_device,给下一步来使用。不是设备树的话,这个platform_device就是由开发者来写。
这里我们先不用设备树,自己来定义 platform 设备。
4.1 platform设备定义
在linux 设备模型中,platform设备由platform_device结构表示,该结构体定义在 include/linux/platform_device.h文件中:
struct platform_device {
const char *name;
int id;
bool id_auto;
struct device dev;
u32 num_resources;
struct resource *resource;
const struct platform_device_id *id_entry;
char *driver_override; /* Driver name to force a match */
/* MFD cell pointer */
struct mfd_cell *mfd_cell;
/* arch specific additions */
struct pdev_archdata archdata;
};
其中部分成员的含义如下:
- name:设备的名字,需要和platform_driver.driver里的name一样,否则就无法匹配到对应驱动;
- id:设备id,插入总线下相同name的设备编号(一个驱动可以有多个设备),如果只有一个设备的话填-1;
- dev:设备基类,内核维护的所有的设备必须包含该成员,其中成员platform_data,是个void *类型,可以给平台driver提供各种数据(比如:GPIO引脚等等);
- num_resources:资源的数目;
- resource:资源,该设备的资源描述,由struct resource(include/linux/ioport.h)结构抽象;
其中struct resource结构也是我们platform平台设备的重点,用于存放设备的资源信息,如IO地址、中断号等。定义如下:
/*
* Resources are tree-like, allowing
* nesting etc..
*/
struct resource {
resource_size_t start; //起始资源,如果是地址的话,必须是物理地址
resource_size_t end; //结束资源,如果是地址的话,必须是物理地址
const char *name; //资源名
unsigned long flags; //资源的标志 //比如IORESOURCE_MEM,表示内存资源, IORESOURCE_IRQ表示中断引脚... ...
unsigned long desc;
struct resource *parent, *sibling, *child; //资源拓扑指针父、兄、子,可以构成链表
};
4.2 platform设备注册
用platform_device结构体定义好platform设备后,用platform_device_register函数向linux内核注册platform设备,函数定义在drivers/base/platform.c:
/**
* platform_device_register - add a platform-level device
* @pdev: platform device we're adding
*/
int platform_device_register(struct platform_device *pdev)
{
device_initialize(&pdev->dev);
arch_setup_pdev_archdata(pdev);
return platform_device_add(pdev);
}
这里先初始化platform设备里的设备基类dev,然后调用platform_device_add添加platform设备:
/**
* platform_device_add - add a platform device to device hierarchy
* @pdev: platform device we're adding
*
* This is part 2 of platform_device_register(), though may be called
* separately _iff_ pdev was allocated by platform_device_alloc().
*/
int platform_device_add(struct platform_device *pdev)
{
int i, ret;
if (!pdev)
return -EINVAL;
if (!pdev->dev.parent)
pdev->dev.parent = &platform_bus;
pdev->dev.bus = &platform_bus_type;
switch (pdev->id) {
default:
dev_set_name(&pdev->dev, "%s.%d", pdev->name, pdev->id);
break;
case PLATFORM_DEVID_NONE:
dev_set_name(&pdev->dev, "%s", pdev->name);
break;
case PLATFORM_DEVID_AUTO:
/*
* Automatically allocated device ID. We mark it as such so
* that we remember it must be freed, and we append a suffix
* to avoid namespace collision with explicit IDs.
*/
ret = ida_simple_get(&platform_devid_ida, 0, 0, GFP_KERNEL);
if (ret < 0)
goto err_out;
pdev->id = ret;
pdev->id_auto = true;
dev_set_name(&pdev->dev, "%s.%d.auto", pdev->name, pdev->id);
break;
}
for (i = 0; i < pdev->num_resources; i++) {
struct resource *p, *r = &pdev->resource[i];
if (r->name == NULL)
r->name = dev_name(&pdev->dev);
p = r->parent;
if (!p) {
if (resource_type(r) == IORESOURCE_MEM)
p = &iomem_resource;
else if (resource_type(r) == IORESOURCE_IO)
p = &ioport_resource;
}
if (p) {
ret = insert_resource(p, r);
if (ret) {
dev_err(&pdev->dev, "failed to claim resource %d: %pR\n", i, r);
goto failed;
}
}
}
pr_debug("Registering platform device '%s'. Parent at %s\n",
dev_name(&pdev->dev), dev_name(pdev->dev.parent));
ret = device_add(&pdev->dev);
if (ret == 0)
return ret;
failed:
if (pdev->id_auto) {
ida_simple_remove(&platform_devid_ida, pdev->id);
pdev->id = PLATFORM_DEVID_AUTO;
}
while (--i >= 0) {
struct resource *r = &pdev->resource[i];
if (r->parent)
release_resource(r);
}
err_out:
return ret;
}
platform_device_add内部调用了device_add 来进行添加设备,关于device_add 函数下一篇我们会详细介绍:。
这里简单介绍一下device_add 函数执行流程:
- 将platform设备添加到platform总线上;
- 遍历platform总线上的所有platform驱动,然后进行驱动和设备匹配;
- 匹配成功后最终会执行platform驱动的probe函数;
4.3 platform设备卸载
用platform_device_unregister函数卸载platform设备,函数定义在drivers/base/platform.c:
/**
* platform_device_unregister - unregister a platform-level device
* @pdev: platform device we're unregistering
*
* Unregistration is done in 2 steps. First we release all resources
* and remove it from the subsystem, then we drop reference count by
* calling platform_device_put().
*/
void platform_device_unregister(struct platform_device *pdev)
{
platform_device_del(pdev);
platform_device_put(pdev);
}
可以看到最终调用了platform_device_del和platform_device_put:
/**
* platform_device_del - remove a platform-level device
* @pdev: platform device we're removing
*
* Note that this function will also release all memory- and port-based
* resources owned by the device (@dev->resource). This function must
* _only_ be externally called in error cases. All other usage is a bug.
*/
void platform_device_del(struct platform_device *pdev)
{
int i;
if (!IS_ERR_OR_NULL(pdev)) {
device_del(&pdev->dev);
if (pdev->id_auto) {
ida_simple_remove(&platform_devid_ida, pdev->id);
pdev->id = PLATFORM_DEVID_AUTO;
}
for (i = 0; i < pdev->num_resources; i++) {
struct resource *r = &pdev->resource[i];
if (r->parent)
release_resource(r);
}
}
}
/**
* platform_device_put - destroy a platform device
* @pdev: platform device to free
*
* Free all memory associated with a platform device. This function must
* _only_ be externally called in error cases. All other usage is a bug.
*/
void platform_device_put(struct platform_device *pdev)
{
if (!IS_ERR_OR_NULL(pdev))
put_device(&pdev->dev);
}
这里就不具体介绍了。
五、platform总线设备驱动案例
在具体的开发过程中我们并不需要真的去写一个platform总线模型,内核中都已经给我们定义好了。我们对platform总线模型的分析主要是搞清楚如何将驱动和设备匹配的,即当我们插入设备是如何找到对应驱动或插入驱动如何找到对应设备的,并最终调用probe函数。
其实不管是先有驱动后有设备、还是先有设备后有驱动,最终匹配成功后第一件事都是执行驱动的probe函数,所以我们尽可放心的忽略中间曲折的情感纠葛,直接把注意力放在最终的probe函数。
接下来以实际例子来使用platform总线设备驱动。我们在/work/sambashare/drivers路径下创建项目10.platform_led_dev。
5.1 LED硬件资源
查看Mini2440原理图、S3C2440数据手册,了解如何点亮LED。在Mini2440裸机开发之点亮LED中我们已经介绍了Mini2440 LED1~LED4的接线方式,以及寄存器的设置,这里简单说一下,就不具体介绍了:
- LED1~LED4对应引脚GPB5~GPB8,以点亮LED1为例;
- 配置控制寄存器GPBCON(0x56000010)的bit[11:10]=01,使GPB5引脚为输出模式;
- 配置数据寄存器GPBDAT(0x56000014)的bit5=0,使GPB5引脚输出低电平;
5.2 platform设备
在10.platform_led_dev项目路径下创建platform设备程序led_dev.c:
#include <linux/module.h>
#include <linux/fs.h>
#include <linux/platform_device.h>
/*
* 设备资源
*/
static struct resource led_resources[] = {
[0] = {
.start = 0x56000010, // GPBCON寄存器4个字节
.end = 0x56000013,
.flags = IORESOURCE_MEM, // 表示地址资源
},
[1] = {
.start = 5, //表示GPB第几个引脚开始
.end = 8, //结束引脚
.flags = IORESOURCE_IRQ, //表示中断资源
}
};
/* 注销设备时,调用 */
static void led_release(struct device * dev)
{
printk("call %s\n",__func__);
}
/*
* platform设备
*/
static struct platform_device led_device = {
.name = "led_test",
.id = -1,
.num_resources = ARRAY_SIZE(led_resources),
.resource = led_resources,
.dev = {
.release = led_release,
}
};
/*
* platform设备模块入口
*/
static int led_dev_init(void)
{
// platform设备注册
int err = platform_device_register(&led_device);
if (err)
{
printk("platform device registered failed\n");
}
else
{
printk("platform device registered successfully\n");
}
return 0;
}
/*
* platform设备模块出口
*/
static void __exit led_dev_exit(void)
{
printk("platform device unregistered\n");
// platform设备卸载
platform_device_unregister(&led_device);
}
module_init(led_dev_init);
module_exit(led_dev_exit);
MODULE_LICENSE("GPL");
可以看到该段程序指定了led设备的引脚地址,当更换SOC芯片时时,只需要修改这个地址即可。
5.3 platform驱动
#include <linux/module.h>
#include <linux/fs.h>
#include <linux/cdev.h>
#include <linux/io.h>
#include <linux/uaccess.h>
#include <linux/platform_device.h>
#include <linux/device.h>
/* GPB寄存器 */
static volatile unsigned long *gpbcon = NULL;
static volatile unsigned long *gpbdata = NULL;
/* 保存操作结构体的字符设备 */
static struct cdev led_cdev;
/* 设备类 */
static struct class *led_cls;
/* 字符设备编号 */
static dev_t devid;
/* led引脚 */
static int start_pin;
static int end_pin;
/* GPB5~GPB8配置为输出 */
static int led_open(struct inode *inode, struct file *file)
{
int i;
for(i=start_pin;i<end_pin;i++)
{
*gpbcon &= ~(0x01 << (i*2));
*gpbcon |= (0x01 << (i*2));
}
return 0;
}
/* 点亮/熄灭 LED01~LED4 */
static ssize_t led_write(struct file *file, const char __user *buf, size_t count, loff_t *ppos)
{
int val,i;
copy_from_user(&val, buf, count); // 用户空间到内核空间传递数据
printk("value %d",val);
if(val == 1)
{
/* 点亮 */
for(i=start_pin;i<end_pin;i++)
{
*gpbdata &= ~(0x01 << i);
}
}
else
{
/* 熄灭 */
for(i=start_pin;i<end_pin;i++)
{
*gpbdata |= (0x01 << i);
}
}
return 0;
}
static struct file_operations led_fops = {
.owner = THIS_MODULE,
.open = led_open,
.write = led_write,
};
/*
* 当驱动和硬件信息匹配成功之后,就会调用probe函数,驱动所有的资源的注册和初始化全部放在probe函数中
*/
static int led_probe(struct platform_device *pdev)
{
struct resource *res;
int err;
struct device *dev;
printk("led_probe, found led\n");
/* 获取platform设备某个资源 */
res = platform_get_resource(pdev,IORESOURCE_MEM, 0);
/* 用来将I/O内存资源的物理地址映射到核心虚地址空间 */
gpbcon = (volatile unsigned long *)ioremap(res->start, res->end - res->start + 1);
gpbdata = gpbcon + 1;
res = platform_get_resource(pdev,IORESOURCE_IRQ, 0);
start_pin = res->start;
end_pin = res->end;
/* 动态分配字符设备编号: (major,0) */
err = alloc_chrdev_region(&devid, 0, 1,"led");
if(err)
{
printk("register_chrdev_region error\n");
return err;
}
else
{
printk("register_chrdev_region ok\n");
}
/* 初始化字符设备,并将字符设备添加到系统 */
cdev_init(&led_cdev, &led_fops);
cdev_add(&led_cdev, devid, 1);
/* 创建类,它会在sys目录下创建/sys/class/led这个类 */
led_cls = class_create(THIS_MODULE, "led");
if(IS_ERR(led_cls)){
printk("class_create erro\n");
return -1;
}
else
{
printk("class_create ok\n");
}
/* 在/sys/class/led下创建led设备,然后mdev通过这个自动创建/dev/led这个设备节点 */
dev = device_create(led_cls, NULL, devid, NULL, "led");
if(IS_ERR(dev))
{
printk("device_create error\n");
return -1;
}
else
{
printk("device_create ok\n");
}
return 0;
}
/*
* 硬件信息被移除了,或者驱动被卸载了,全部要释放,释放资源的操作就放在该函数中
*/
static int led_remove(struct platform_device * pdev)
{
printk("led_remove, remove led\n");
/* 注销虚拟地址 */
iounmap(gpbcon);
/* 注销类、以及类设备 /sys/class/led会被移除*/
device_destroy(led_cls, devid);
class_destroy(led_cls);
/* 删除设备,卸载注册的设备编号 */
cdev_del(&led_cdev);
unregister_chrdev_region(devid, 1);
return 0;
}
/*
* platform驱动
*/
static struct platform_driver led_driver = {
.probe = led_probe,
.remove = led_remove,
.driver = {
.name = "led_test", // 必须和platform设备的.name相同
}
};
/*
* platform驱动模块入口
*/
static int led_drv_init(void)
{
// platform驱动注册
int err = platform_driver_register(&led_driver);
if (err) {
printk("platform driver registered failed\n");
} else {
printk("platform driver registered successfully\n");
}
return err;
}
/*
* platform驱动模块出口
*/
static void __exit led_drv_exit(void)
{
printk("platform driver unregistered\n");
// platform驱动卸载
platform_driver_unregister(&led_driver);
}
module_init(led_drv_init);
module_exit(led_drv_exit);
MODULE_LICENSE("GPL");
在platform驱动里面我们实现了如下功能:
- 编写要注册的led驱动:platform_drive结构体;
- 编写file_opertions结构体,以及成员函数(.open、.write)、.probe函数;
- 当驱动和设备都insmod加载后,然后bus总线会匹配成功,就进入.probe函数;
- 在.probe函数中便使用platform_get_resource函数获取LED的地址和引脚,后动态分配字符设备编号,并注册字符设备和设备节点/dev/led;
- 如果驱动与设备已联系起来,当卸载驱动/设备时,就会调用.remove函数,在这里主要做一些清理工作,防止内存泄漏;
这里顺带介绍一下platform_get_resource函数:
struct resource * platform_get_resource(struct platform_device *dev, unsigned int type,unsigned int num);
获取设备的某个资源,获取成功,则返回一个resource资源结构体,函数参数如下:
-
*dev :指向某个platform device设备;
-
type::取的资源类型;
-
num::type资源下的第几个资源(从0开始);
5.4 Makefile
KERN_DIR :=/work/sambashare/linux-5.2.8
all:
make -C $(KERN_DIR) M=`pwd` modules
clean:
make -C $(KERN_DIR) M=`pwd` modules clean
rm -rf modules.order
obj-m += led_dev.o
obj-m += led_drv.o
六、platform总线设备驱动测试应用程序
在10.platform_led_dev下创建test文件夹,保存测试应用程序。
6.1 main.c
#include <sys/stat.h>
#include <fcntl.h>
#include <stdio.h>
void print_usage(char *file)
{
printf("Usage:\n");
printf("%s <dev> <on|off>\n",file);
printf("eg. \n");
printf("%s /dev/led on\n", file);
printf("%s /dev/led off\n", file);
}
int main(int argc,char **argv)
{
int fd;
int val;
char *filename;
if (argc != 3){
print_usage(argv[0]);
return 0;
}
filename = argv[1];
fd = open(filename,O_RDWR);
if(fd == -1){
printf("can't open %s!\n",filename);
return 0;
}
if (!strcmp("on", argv[2])){
// 亮灯
val = 1;
printf("%s on!\n",filename);
write(fd, &val, 4);
}else if (!strcmp("off", argv[2])){
// 灭灯
val = 0;
printf("%s off!\n",filename);
write(fd, &val, 4);
}else{
print_usage(argv[0]);
}
return 0;
}
6.2 Makefile
all:
arm-linux-gcc -march=armv4t -o main main.c
clean:
rm -rf *.o main
七、烧录开发板测试
platform设备驱动目录结构如下:
7.1 编译驱动
在ubuntu执行make命令编译驱动,并将驱动程序拷贝到nfs文件系统:
cd /work/sambashare/drivers/10.platform_led_dev
make
cp /work/sambashare/drivers/10.platform_led_dev/led_dev.ko /work/nfs_root/rootfs
cp /work/sambashare/drivers/10.platform_led_dev/led_drv.ko /work/nfs_root/rootfs
在开发板上安装驱动:
[root@zy:/]# insmod led_dev.ko
led_dev: loading out-of-tree module taints kernel.
platform device registered successfully
[root@zy:/]# insmod led_drv.ko
led_probe, found led
register_chrdev_region ok
class_create ok
device_create ok
platform driver registered successfully
查看设备节点文件:
[root@zy:/]# ls /dev/led -l
crw-rw---- 1 0 0 249, 0 Jan 1 00:00 /dev/led
查看platform总线:
[root@zy:/]# ls /sys/bus/platform -l
total 0
drwxr-xr-x 2 0 0 0 Jan 1 00:15 devices
drwxr-xr-x 30 0 0 0 Jan 1 00:00 drivers
-rw-r--r-- 1 0 0 4096 Jan 1 00:15 drivers_autoprobe
--w------- 1 0 0 4096 Jan 1 00:15 drivers_probe
--w------- 1 0 0 4096 Jan 1 00:15 uevent
在devices路径下可以看到我们注册的platform设备led_test:
[root@zy:/sys/bus/platform]# ls -l devices
total 0
lrwxrwxrwx 1 0 0 0 Jan 1 00:17 alarmtimer -> ../../../devices/platform/alarmtimer
lrwxrwxrwx 1 0 0 0 Jan 1 00:17 dm9000 -> ../../../devices/platform/dm9000
lrwxrwxrwx 1 0 0 0 Jan 1 00:18 led_test -> ../../../devices/platform/led_test
lrwxrwxrwx 1 0 0 0 Jan 1 00:17 s3c2410-lcd -> ../../../devices/platform/s3c2410-lcd
lrwxrwxrwx 1 0 0 0 Jan 1 00:17 s3c2410-ohci -> ../../../devices/platform/s3c2410-ohci
lrwxrwxrwx 1 0 0 0 Jan 1 00:17 s3c2410-wdt -> ../../../devices/platform/s3c2410-wdt
lrwxrwxrwx 1 0 0 0 Jan 1 00:17 s3c2440-i2c.0 -> ../../../devices/platform/s3c2440-i2c.0
lrwxrwxrwx 1 0 0 0 Jan 1 00:17 s3c2440-nand -> ../../../devices/platform/s3c2440-nand
lrwxrwxrwx 1 0 0 0 Jan 1 00:17 s3c2440-uart.0 -> ../../../devices/platform/s3c2440-uart.0
lrwxrwxrwx 1 0 0 0 Jan 1 00:17 s3c2440-uart.1 -> ../../../devices/platform/s3c2440-uart.1
lrwxrwxrwx 1 0 0 0 Jan 1 00:17 s3c2440-uart.2 -> ../../../devices/platform/s3c2440-uart.2
lrwxrwxrwx 1 0 0 0 Jan 1 00:17 s3c24xx-iis -> ../../../devices/platform/s3c24xx-iis
lrwxrwxrwx 1 0 0 0 Jan 1 00:17 s3c24xx_led.0 -> ../../../devices/platform/s3c24xx_led.0
lrwxrwxrwx 1 0 0 0 Jan 1 00:17 s3c24xx_led.1 -> ../../../devices/platform/s3c24xx_led.1
lrwxrwxrwx 1 0 0 0 Jan 1 00:17 s3c24xx_led.2 -> ../../../devices/platform/s3c24xx_led.2
lrwxrwxrwx 1 0 0 0 Jan 1 00:17 s3c24xx_led.3 -> ../../../devices/platform/s3c24xx_led.3
lrwxrwxrwx 1 0 0 0 Jan 1 00:17 serial8250 -> ../../../devices/platform/serial8250
lrwxrwxrwx 1 0 0 0 Jan 1 00:17 snd-soc-dummy -> ../../../devices/platform/snd-soc-dummy
在drivers路径下可以看到我们注册的platform驱动led_test:
[root@zy:/sys/bus/platform]# ls -l drivers
total 0
drwxr-xr-x 2 0 0 0 Jan 1 00:17 alarmtimer
drwxr-xr-x 2 0 0 0 Jan 1 00:17 dm9000
drwxr-xr-x 2 0 0 0 Jan 1 00:17 gpio-clk
drwxr-xr-x 2 0 0 0 Jan 1 00:22 led_test
drwxr-xr-x 2 0 0 0 Jan 1 00:17 of_fixed_clk
drwxr-xr-x 2 0 0 0 Jan 1 00:17 of_fixed_factor_clk
drwxr-xr-x 2 0 0 0 Jan 1 00:17 pata_platform
drwxr-xr-x 2 0 0 0 Jan 1 00:17 pwrseq_emmc
drwxr-xr-x 2 0 0 0 Jan 1 00:17 pwrseq_simple
drwxr-xr-x 2 0 0 0 Jan 1 00:17 s3c-adc
drwxr-xr-x 2 0 0 0 Jan 1 00:17 s3c-i2c
drwxr-xr-x 2 0 0 0 Jan 1 00:17 s3c-rtc
drwxr-xr-x 2 0 0 0 Jan 1 00:17 s3c-sdi
drwxr-xr-x 2 0 0 0 Jan 1 00:17 s3c2410-lcd
drwxr-xr-x 2 0 0 0 Jan 1 00:17 s3c2410-ohci
drwxr-xr-x 2 0 0 0 Jan 1 00:17 s3c2410-wdt
drwxr-xr-x 2 0 0 0 Jan 1 00:17 s3c2412-lcd
drwxr-xr-x 2 0 0 0 Jan 1 00:17 s3c24xx-dclk
drwxr-xr-x 2 0 0 0 Jan 1 00:17 s3c24xx-dma
drwxr-xr-x 2 0 0 0 Jan 1 00:17 s3c24xx-nand
drwxr-xr-x 2 0 0 0 Jan 1 00:17 samsung-pwm
drwxr-xr-x 2 0 0 0 Jan 1 00:17 samsung-uart
drwxr-xr-x 2 0 0 0 Jan 1 00:17 serial8250
drwxr-xr-x 2 0 0 0 Jan 1 00:17 simtec-i2c
drwxr-xr-x 2 0 0 0 Jan 1 00:17 sm501
drwxr-xr-x 2 0 0 0 Jan 1 00:17 sm501-fb
drwxr-xr-x 2 0 0 0 Jan 1 00:17 sm501-usb
drwxr-xr-x 2 0 0 0 Jan 1 00:17 snd-soc-dummy
drwxr-xr-x 2 0 0 0 Jan 1 00:17 soc-audio
7.2 编译测试应用程序
执行make命令编译测试应用程序,并将测试应用程序拷贝到nfs文件系统:
cd test
make
cp ./main /work/nfs_root/rootfs
运行应用程序:
./main /dev/led on
./main /dev/led off
可以看到LED1~LED4同时点亮和同时熄灭。
如果你想单独控制每一个LED,那需要为每个LED编写对应的驱动程序,这里就不演示了。
7.3 卸载LED驱动
通过用lsmod可以查看当前安装了哪些驱动:
[root@zy:/]# lsmod
led_drv 2643 0 - Live 0xbf004000 (O)
led_dev 1568 0 - Live 0xbf000000 (O)
卸载时直接运行:
[root@zy:/]# rmmod led_dev
platform device unregistered
led_remove, remove led
call led_release
[root@zy:/]# rmmod led_drv
platform driver unregistered
八、代码下载
Young / s3c2440_project[drivers]
参考文章
[2]一张图掌握 Linux platform 平台设备驱动框架
[4]14.linux-platform机制实现驱动层分离(详解)