1.什么叫linux设备驱动模型?

1.1 驱动模型有四个概念

类class

总线bus

设备device

驱动driver

1.2 kobject和对象生命周期

kobject:所有驱动抽象出来的基类

驱动中存在机制使得驱动不需要的时候可以自己释放掉自己

1.3 sysfs

将驱动中一些硬件信息以文件的形式展现出来,在用户空间就可以操控或者读取硬件

1.4 udev

为了实现内核空间与用户空间的通信,用户空间可以迅速知道内核空间发生的事件。

2. 为什么需要设备驱动模型?

早期内核(2.4)根本就没有驱动模型,但也能用。

后期内核(2.6)正式引入设备驱动模型,因为设备越来越多,而且有功耗的要设备驱动模型负责

我们需要统一实现和维护一些特性,诸如:电源管理、热插拔、对象生命周期、用户空间和驱动空间的交互等基础设施求

设备驱动模型目的是简化驱动程序编写,但是客观上设备驱动模型本身设计和实现很复杂。

3. 驱动开发的两个重点

1.驱动本身干活代码的编写,重点在硬件,有哪些寄存器需要修改和读取

2.驱动什么时候要被安装和调用的问题,重点在设备驱动模型。

4. 设备驱动的底层架构

4.1 kobject

该结构体为所有设备类驱动的对象的基类。位于linux/kobject.h

提供的服务:对象引用计数、维护对象链表、对象上锁、对用户空间的表示

list_head:内核链表,平行驱动的连接

kobject * parent:上下级驱动的连接

krf:引用计数,帮助维护对象的生命周期,归零释放。

kset:对象上锁

sd:对用户空间的表示

4.2 kobj_type

每个kobject都包绑定了一个kobj_type,提供/sys/目录下的操作

release:释放方法

sys_ops:操作方法

attribute:提供应用接口

4.3 kset

将kobject组织成一个目录关系 

5. 总线式设备

usb、pcie、i2c

内核根本不管底层的usb设备,他负责usb总线和pcie总线之类的。

由usb总线驱动负责usb设备的安装卸载,驱动管理

5.1 总线bus_type

总线类结构体模版,依旧变量+函数指针

bus_attribute:总线本身自己的属性。

device_attribute:进入device文件夹后里面的属性

driver_attribute:进入driver文件夹后里面的属性

match:设备匹配驱动函数

dev_pm_ops:电源管理集合

5.2 设备device

struct device:硬件设备在内核驱动框架中的抽象类,一般会被包含,比如struct usb_devices

5.3 驱动device_driver

device_driver:设备驱动结构体。

名字(name),驱动与设备的匹配就是依靠名字。

探测函数(probe),用以检测一个设备是否可以被该驱动管理。

5.4 类class、class_devices

struct class 和struct class_devices,用以提供一些功能。对设备进行一个分类管理

设备被总线、类、多重管理。

6. platform平台总线

这个东西不像usb这种有真实的物理总线,他是虚拟的。这类设备没有专用的外设接口,使用cpu的地址总线连接的,所以我们虚拟出来一个platform平台总线,对应soc内部集成的各种内部外设

工作体系:

=========================================================================

cpu与外界的通信原理大致可以分两类:

1.地址总线接口,32位接口,0-(2^32)-1个地址,也就是4G。寄存器、内存、网卡。你直接访问地址就行。

2.专用类接口,硬盘,sd卡,usb

=========================================================================

6.1 设备结构体与驱动结构体

platform_device和platform_driver

dev:通用的设备属性

resource:用到的资源,比如中断号和gpio,是个指针,就是说有可能是数组(首地址)

u32 num_resource:用到的资源的个数

platform_device_id:支持的设备id表,放了兼容的设备。

archdata:自留地,存放专用数据,提供一种扩展性。

全是函数指针。

probe:驱动探索函数,其实干的设备初始化的工作,返回正常那就是能用

remove:卸载一个设备,sys目录下已经没有了

shutdown:关闭一个设备

suspend:挂起一个设备

resume:恢复设备,唤醒设备

device_driver:驱动的通用特性

platform_device_id:驱动支持的设备id表

6.2 注册函数

platform_device_register和platform_driver_register

6.3 平台总线的工作原理

1.内核系统启动时在bus系统中注册platform(不用我们干)

2.内核移植需要提供了platform_device(没人干我们就得干)一般在kernel/arch/arm/mach-xxx中

触摸屏ts设备:

lcd背光设备:

注册:

3.写驱动的提供platform_driver

背光的驱动和注册:

4.platform的match函数发现driver和device匹配后,调用driver的probe函数来完成驱动的初始化和安装。然后设备开始工作。

6.4 怎么写驱动

写一个设备结构体,写一个驱动结构体,注册设备、注册驱动

6.5 代码分析

platform系统注册

入口函数:在bus系统中注册platform总线系统

error = device_register(&platform_bus);

在设备文件夹中注册一个平台总线目录

error =  bus_register(&platform_bus_type);

在bus系统中注册platform总线系统

platform_bus_type:

struct bus_type platform_bus_type = {
	.name		= "platform",
	.dev_attrs	= platform_dev_attrs,
	.match		= platform_match,
	.uevent		= platform_uevent,
	.pm		= &platform_dev_pm_ops,
};

每种总线都会有自己的匹配(match)方法。但一般都是看名字匹配的。

ok不分析了,直接写代码

7. 平台总线实现

无敌麻烦,兄弟!!

设备(修改内核):

1.结构体头文件

自己写一个用来描述设备非通用信息的结构体,由于device结构体中的platform成员是void*类型,该指针可以指向任意数据类型。选好路径放好。

#ifndef __ASM_ARCH_LEDSGPIO_H
#define __ASM_ARCH_LEDSGPIO_H "leds-gpio.h"

#define S5PV210_LEDF_ACTLOW	    (1<<0)		/* LED is on when GPIO low */
#define S5PV210_LEDF_TRISTATE	(1<<1)		/* tristate to turn off */

struct s5pv210_led_platdata {
	unsigned int		 gpio;
	unsigned int		 flags;

	char			*name;
	char			*def_trigger;
};

#endif /* __ASM_ARCH_LEDSGPIO_H */

2.定义设备描述

在mach-x210中有对platform设备的数据信息。我们需要将自己的platform设备写在这里。先添加设备,注意设备名一定要与驱动的名字相同。其dev成员的platform_data成员连接到下一步添加的非通用信息。

static struct platform_device x210_led1 = {
	.name   = "lxy210_led",
	.id     = 0,
	.dev = {
		.platform_data = &(x210_led1_platdata),
	},
};

static struct platform_device x210_led2 = {
	.name   = "lxy210_led",
	.id     = 1,
	.dev = {
		.platform_data = &(x210_led2_platdata),
	},
};

static struct platform_device x210_led3 = {
	.name   = "lxy210_led",
	.id     = 2,
	.dev = {
		.platform_data = &(x210_led3_platdata),
	},
};

3.填充设备自留数据/属性

这些都是设备的非通用信息。

static struct s5pv210_led_platdata x210_led1_platdata = {
	.name = "led1",
	.gpio = S5PV210_GPJ0(3),
	.flags = S5PV210_LEDF_ACTLOW | S5PV210_LEDF_TRISTATE,
	.def_trigger = "",
};

static struct s5pv210_led_platdata x210_led2_platdata = {
	.name = "led2",
	.gpio = S5PV210_GPJ0(4),
	.flags = S5PV210_LEDF_ACTLOW | S5PV210_LEDF_TRISTATE,
	.def_trigger = "",
};

static struct s5pv210_led_platdata x210_led3_platdata = {
	.name = "led3",
	.gpio = S5PV210_GPJ0(5),
	.flags = S5PV210_LEDF_ACTLOW | S5PV210_LEDF_TRISTATE,
	.def_trigger = "",
};

4.添加进初始化表

这个指针数组包含了内核初始化时初始化的platform设备,我们将下面自己的设备添加到这个数组中。完成设备的添加。

	&x210_led1,
	&x210_led2,
	&x210_led3,

5.头文件

别忘了把自己写的头文件包含进来。

#include <mach/leds-gpio.h>

驱动(修改外部代码):

1. 修改驱动初始化、退出函数

static int lxy210_led_init(void)
{
	return platform_driver_register(&lxy210_led_driver);
}
static void lxy210_led_exit(void)
{
	platform_driver_unregister(&lxy210_led_driver);
}

2. 定义驱动结构体

这个驱动需要支撑3个led设备

static struct platform_driver lxy210_led_driver = {
	.probe		= lxy210_led_probe,
	.remove		= lxy210_led_remove,
	.driver		= {
		.name		= "lxy210_led",
		.owner		= THIS_MODULE,
	},
};

3. 创建led结构体

考虑到一个驱动需要带3个led,考虑给lxy_led_set和gpio_request函数的输入输出。

我们需要新定义一个结构体包含cdev和platdata(自留数据)。

to_gpio函数用以在设置led电平的时候获取gpio号,卸载驱动时获取gpio号

pdev_to_gpio函数用以注销led_class时获取cdev

struct s5pv210_gpio_led{
	struct led_classdev cdev;	// 包含一个led_classdev结构体
	struct s5pv210_led_platdata *platdata;	// 指向平台数据的指针
};

static inline struct s5pv210_gpio_led *pdev_to_gpio(struct platform_device *dev)
{
	return platform_get_drvdata(dev);
}

static inline struct s5pv210_gpio_led *to_gpio(struct led_classdev *led_cdev)
{
	return container_of(led_cdev, struct s5pv210_gpio_led, cdev);
}

4. 填充驱动初始化函数

static int lxy210_led_probe(struct platform_device *dev)
{
	struct s5pv210_led_platdata *platdata = dev->dev.platform_data;
	printk(KERN_INFO "Probing LED device: %s\n", platdata->name);

	struct s5pv210_gpio_led *led_gpio;
	led_gpio = kzalloc(sizeof(struct s5pv210_gpio_led), GFP_KERNEL);
	if (!led_gpio) {
		printk(KERN_ERR "Failed to allocate memory for LED GPIO structure\n");
		return -ENOMEM;
	}

	platform_set_drvdata(dev, led_gpio);	// 将led_gpio结构体指针存储在平台设备的驱动数据中

	if (gpio_request(platdata->gpio, platdata->name) < 0) {
		printk(KERN_ERR "Failed to request GPIO for %s\n", platdata->name);
		return -EBUSY;
	}
	else{
		printk(KERN_INFO "Successfully requested GPIO for %s\n", platdata->name);
	}

	gpio_direction_output(platdata->gpio, 1);	// 将GPIO设置为输出,并初始值为0(LED打开)

	led_gpio->cdev.name = platdata->name;	// 设置LED设备的名称
	led_gpio->cdev.brightness = LED_OFF;	// 设置LED设备的初始亮度为关闭
	led_gpio->cdev.brightness_set = lxy_led_set;	// 设置LED亮度设置函数
	led_gpio->platdata = platdata;	// 存储平台数据指针
	if (led_classdev_register(&dev->dev, &led_gpio->cdev) < 0) {	// 注册LED设备
		printk(KERN_ERR "Failed to register LED device\n");
		gpio_free(platdata->gpio);	// 释放GPIO资源
		kfree(led_gpio);	// 释放分配的内存
		return -EINVAL;
	}
	else{
		printk(KERN_INFO "Successfully registered LED device\n");
	}
	return 0;
}

5. 填充卸载函数

记得要free内存

static int lxy210_led_remove(struct platform_device *dev)
{
	struct s5pv210_gpio_led *led_gpio = pdev_to_gpio(dev);
	struct s5pv210_led_platdata *platdata = led_gpio->platdata;
	gpio_free(platdata->gpio);	// 释放GPIO资源
	kfree(led_gpio);	// 释放分配的内存
	led_classdev_unregister(&led_gpio->cdev);	// 注销LED设备
	printk(KERN_INFO "Removed LED device: %s\n", platdata->name);
	return 0;
}

6.头文件

#include <linux/platform_device.h>	// platform_driver_register  platform_driver_unregister
#include <mach/leds-gpio.h>		// s5pv210_led_platdata结构体定义
#include <linux/slab.h>			// kzalloc  kfree

效果(点灯工程师石锤):

设备:

[root@LXY210 lxy210_led.0]# ls /sys/bus/platform/devices/
lxy210_led.0      
lxy210_led.1      
lxy210_led.2 

驱动:

[root@LXY210 lxy210_led.0]# ls /sys/bus/platform/drivers/lxy210_led/
lxy210_led.0    lxy210_led.1     lxy210_led.2  

设备文件:

[root@LXY210 lxy210_led.0]# ls /sys/class/leds/
led1    led2    led3    mmc0::  mmc1::  mmc2::  mmc3::

至此,linux平台总线设备驱动学习完美完成

Logo

AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。

更多推荐