文章节选自:
http://ybmmwjl.blog.163.com/blog/static/65638781201082073454413/
1.1 整体架构及原理分析
![驱动开发之旅(二) - Johnson Shepherd - 萩ㄖの傳奇 驱动开发之旅(二) - Johnson Shepherd - 萩ㄖの傳奇](http://img.ph.126.net/zvMnPGBPB01qSjlw_HIvmQ==/1506454075355985236.png)
file_operations是字符设备驱动的核心。
事实上,内核驱动的开发属于内核应用级别的开发,而非内核开发。因为在开发的过程中,我们仅仅需要掌握驱动开发的API,去应用他们,至于内核,也仅仅是系统调用。
内核模块由加载/写在函数、功能函数以及一系列声明组成,它可以被传入参数,也可以到处符号供其他模块使用。
由于Linux设备驱动以内核模块的形式而存在 , 在具体的设备驱动开发中,将驱动编译为模块也有很强的工程意义,因为如果将正在开发中的驱动直接编译入内核,而开发过程中会不断修改代码则需要不断第编译 内核并重新启动Linux,但是如果编译为模块,则值需要rmmod并insmod即可,开发效率大为提高。因此,驱动模块也继承了普通的模块编程的一些 要求:
- 模块加载函数(必须) module_init()
- 模块卸载函数(必须) module_exit()
- 模块许可声明(必须) 最常见的是:MODULE_LICENSE("Dual BSD/GPL")采用BSD/GPL双LICENSE
- 模块参数(可选) module_param()
- 模块导出符号(可选) EXPORT_SYMBOL
- 模块作者等信息(可选)MODULE_AUTHOR(), MODULE_DESCRIPTION(), MODULE_VERSION()……
1.2 传统的字符驱动开发
在Linux系统中,传统字符驱动由如下几个部分组成:
1. 字符设备驱动模块加载/卸载函数
在字符设备驱动模块加载函数中应该实现设备号的申请和cdev的注册,而在卸载函数中应该实现设备号的释放和cdev的注销。
通常习惯将设备定义为一个设备相关的结构体,其包含该设备所涉及的cdev、私有数据及信号量等信息。
拿globalmem程序举例:
struct globalmem_dev {
struct cdev cdev;
unsigned char mem[GLOBALMEM_SIZE];
};
紧接着是加载与卸载函数,入xx_init, xx_exit等,下面是globalmem程序中的部分代码:
static int __init globalmem_init(void)
{
dev_t devno = MKDEV(globalmem_major, 0);
int retval = 0;
……
return retval;
}
static void __exit globalmem_exit(void)
{
dev_t devno;
……
}
通普通模块编程一样,字符驱动在最后也需要这两个函数:
module_init(globalmem_init);
module_exit(globalmem_exit);
2. 设备驱动的file_operations 结构体中成员函数
file_operations中的成员函数是用户空间堆Linux进行系统调用最终的落实这。大多数字符设备驱动会实现read、write和ioctl函数,常见的字符设备驱动的这3个函数的形式如下列代码所示:
ssize_t xxx-read(struct file *filp, char __user *buf, size_t count, loff_t *f_pos)
{
...
copy_to_user(buf, ..., ...);
...
}
ssize_t xxx_write(struct file *filp, const char __user *buf, size_t count, loff_t *f_pos)
{
...
copy_from_user(..., buf, ...);
...
}
int xxx_ioctl(struct inode *inode, struct file *filp, unsigned int cmd, unsigned long arg)
{
...
switch (cmd)
{
case xxx_cmd1:
...
break;
case xxx_cmd2:
...
break;
case xxx_cmd3:
...
break;
default:
/*不支持的命令*/
return - ENOTTY;
}
return 0;
}
// GNU C的写法,为file_operations结构体中的每一个指针函数赋值
static struct file_operations tp_led_fops = {
.owner = THIS_MODULE,
.write = tp_led_write,
.ioctl
= tp_led_ioctl,
.open = tp_led_open,
.release = tp_led_release,
};
接下来就是测试程序了。
1.3 驱动模型
通过引入platform概念,在驱动注册和管理上带来了新的变化。
使用platform_device来描述设备 ,
通过platform_driver描述设备驱动 , 注册和注销设备或者驱动都有一系列接口函数。但并不是任何设备都可以抽象称为platform_device。platform_device是在系统中 以独立实体出现的设备,包括传统的基于端口的设备、主机到外设的总线以及大部分片内集成的控制器等。这些设备的一个共同点是CPU都可以通过总线直接对他 们进行访问。在极少数情况下,一个platform_device可能会经过一小段其他的总线,但是它的寄存器依然可以被CPU直接访问。
1)定义platform_device来描述设备
用于描述platform_device的结构体是堆传统设备device的封装,内容如下:
struct platform_device{
const char
*name;
u32
id;
struct device
num_resources;
struct resource *resource;
};
name是设备名称,用于与platform_driver绑定,resource用于描述设备的资源地址、IRQ等,主要是添加堆用的resource到链表中,以便对设备占用资源进行统一管理。该结构体中最重要的两个属性就是他们了。
2)注册平台设备
int platform_device_register(struct platform_device *pdev);
int platform_add_devices(struct platform_device **pdevs, int ndev);
通过platform_device_alloc函数可以动态分配一个设备,然后进行初始化和注册:
struct platform_device *platform_device_alloc(const char *name, int id);
通过platform_device_register_simple函数可以一部实现分配和注册设备操作:
struct platform_device *platfrom_device_register_simple(const char *name, int id, struct resource *res, unsigned int nres);
3)定义platfrom_driver来描述驱动
platform_deiver 是device_driver的封装,提供了俄驱动的probe和remove方法,也提供了与电源管理相关的shutdown和supend等方法,实 现了一些关于热插拔、电源管理的函数。platform_driver还封装了device_driver。device_driver是描述Linux 设备驱动的基本数据结构。
4)注册/卸载platform_driver
通常可以利用
platform_driver_register 函数堆platform_driver进行注册,如果已经知道某设备不能热插拔,则可以在驱动初始化阶段调用
platform_driver_probe 以减少驱动运行时堆内存的消耗。
写在驱动的接口是platfrorm_driver_unregister。
总结:基于platform编写驱动的流程很简单:定义platform_device->注册platform_device->定义platform_driver->注册platform_driver。
在platform_device与plat_driver中均没有对file_operations的初始化过程。
1.4 miscdevice
Linux2.6 的驱动程序分成了很多子系统,不同子类都在传统的字符设备驱动上进行了更高层次的封装,使用起来更加方便,但是必须了解这些子系统的架构。另外一有一些字 符设备,由于自己的特殊性,无法归类到已经有的某个子类,Linux系统则将他们归到misc类,称为miscdevice,这类设备拥有共同的主设备号 10。misc类设备在嵌入式系统中很常见。
miscdevice也有单独的注册函数
misc_register 与
misc_deregister 。
struct miscdevice {
int
minor;
const char
*name;
const struct file_operations *fops;
struct list_head
list;
struct device
*parent;
struct device
*this_device;
};
注意在miscdevice中封装了file_operations,而platform_device中却没有。
有些设备可以通过传统的字符设备驱动来编写,但是通过miscdevice设备驱动来编写可能会更加简单。
总 结:传统驱动开发与驱动模型是两种套路。在驱动模型中,描述device有两种方式:platform_device与miscdevice。描述驱动的 有platform_driver,在platform_driver中又封装了device_driver。device_driver是描述 Linux设备驱动的基本数据结构。下图展示了这些驱动以及设备中的
常用成员 :
![驱动开发之旅(二) - Johnson Shepherd - 萩ㄖの傳奇 驱动开发之旅(二) - Johnson Shepherd - 萩ㄖの傳奇](http://img.ph.126.net/GADvz6gbWFF_p1qmjx1-_A==/2368611929020513055.jpg)
可以看到,device中基本上都是封装一些数据成员来描述设备的特性,而driver则除了封装一个驱动之外还封装了对该驱动的一些操作。
1.5 程序要点
1. GPIO
GPIO_P3_OUTP_SET(GPIO_IOBASE)
GPIO_IOBASE在源代码为:#define GPIO_BASE 0x40028000 表示GPIO的基地址的物理地址。
该宏的定义位于../linux-2.6.27.8-smartarm3250/arch/arm/mach-lpc32xx/include/mach/platform.h中。
对于GPIO,在源代码为:
#define GPIO_P3_OUTP_SET(x)
(x + 0x004) 置位,表示输出高电平
#define GPIO_P3_OUTP_CLR(x)
(x + 0x008) 清位,表示输出低电平
这两个宏的原理是根据GPIO的基地址计算set和clr操作的地址偏移量,
调用宏_raw_writel(_BIT(7), GPIO_P3_OUTP_CLR(GPIO_IOBASE)); 表示GPIO_07输出低电平,若使用SET则输出高电平。
2. 结构体
2.1 file_operations
对于简单的LED驱动,该结构体在填充时候比较简单。
首先open与release是必须的 ,其次就是ioctl与write,可以使用write实现,也可以使用ioctl来实现。
static struct file_operations tp_led_fops = {
.owner = THIS_MODULE,
.write = tp_led_write,
.ioctl
= tp_led_ioctl,
.open = tp_led_open,
.release = tp_led_release,
};
owner:是指向拥有这个结构的模块的指针,这个成员用来在他的操作还在被使用时阻止模块被卸载,通常数尺化为THIS——MODULE。
write: size_t(*write)(struct file *, const char __user *, size_t, loff_t *); 发送数据给设备。如果NULL, -EINVAL返回给调用write系统调用的程序。如果非负,返回值嗲比爱成功写入的字节数。
read:ssize_t(*read) (struct file *, char __user *, size_t, loff_t *); 用来从设备中获取数据。在这个位置的一个空指针导致read系统调用以-EINVAL(“Invalid argument”)失败。一个非负返回值代表了成功读取的字节数(
返回值是一个signed size类型,常常是目标平台本地的整数类型 )
.ioctl:int (*ioctl)(struct inode *, struct file *, unsigned int, unsigned long); 该系统调用提供了发出设备特定命令的方法(例如格式化软盘的一个刺刀,这不是读取也不是写入)。另外,几个ioctl命令被内核识别而不必引用fops 表。如果设备不提供ioctl方法,对于任何未事先定义的请求(-ENOTTY,“设备无这样的ioctl”),系统调用返回一个错误。
open:int(*open)(struct inode*, struct file*); 尽管这常常是对设备文件进行的第一个操作,不要求驱动声明一个对应的方法。如果这个选项是NULL,设备打开一直成功,但是你的驱动不会得到通知。
release:int (*release)(struct inode *, struct file *); 在文件结构被释放的时候引用这个操作。如同open,release可以为NULL。
2.2 miscdevice
虽然按照传统的字符驱动也可以编写,但是使用miscdevice来编写更加简单。
static struct miscdevice tp_led_miscdev =
{
.minor
= MISC_DYNAMIC_MINOR,
.name
= DEV_NAME,
.fops
= &tp_led_fops,
};
.minor表示动态分配LED设备的次设备号。由于miscdevice设备共用一个主设备号10,所以归类为misc的设备只有一个属于自己的次设备号。(如果希望为某个设备单独分配主设备号,则不恩你个归类为misc设备,不能按照misc类设备编写驱动程序)
.name是设备名称,使用自定义的DEVICE_NAME为其赋值
.fops是file_operations的地址。
3. 函数
try_module_get与module_put取代2.4内核中的模块使用技术管理宏;模块的使用技术不必由自身管理(更多参考
这里 )
如果模块已经插入内核,则递增该模块引用计数,如果该模块还没有插入内核,则返回0表示出错。
而module_put则是减少模块的使用计数。
这两个函数刚好一个用在open中,一个用在release中。
4. 流程
1)定义四个数据结构,file_operations,miscdevice,
platform_device , device_driver。其中,miscdevice封装了file_operations,注意并没有使用
platform_driver 来封装device_driver,而知直接使用了device_driver。
2)填充这四个结构体,注意其中
platform_device 并不需要填充,只需要在初始化函数中为其动态开辟空间即可。而device_driver需要填充其中的probe与remove函数。
3)注册miscdevice, platform_device, device_driver这三个结构体,它们注册/卸载的函数以及位置分别如下:
名称 | 注册/卸载函数 | 位置 |
miscdevice | int misc_register(struct miscdevice *misc) int misc_deregister(struct miscdevice *misc) | 在加载驱动时注册: driver封装的probe; 在卸载驱动时注销: driver封装的remove |
platform_device | struct platform_device *platform_device_register_simple( const char *name, int id, struct resource *res, unsigned int nres) | 在模块初始化函数中注册xx_init; 在模块卸载时注销 xx_exit |
device_driver | int driver_register(struct device_driver*) int driver_unregister(struct device_driver*) | 同platform_device |
然后就是测试程序了。