1、设备打开与关闭
(1)open方法
open 方法提供给驱动来做任何的初始化来准备后续的操作. 在大部分驱动中, open 应当进行下面的工作:
检查设备特定的错误(例如设备没准备好, 或者类似的硬件错误
如果它第一次打开, 初始化设备
如果需要, 更新 f-op 指针
分配并填充要放进filp->private_data的任何数据结构
int (*open)(struct inode *inode,struct file *filp);
(2)release方法
release方法是open的反向动作,有的方法的实现称为device_close, 而不是device_release。任一方式,设备方法应当进行下面的任务:
释放open分配在filp->private_data中的任何东西;
在最后的close关闭设备;
int xxx_release(struct inode *inode, struct file *filp)
{
…
return 0;
}
2、设备操作函数
(1)read 方法
read 的返回值由调用的应用程序解释:
如果这个值等于传递给read系统调用的count参数, 请求的字节数已经被传送。
如果是正数, 但是小于count, 只有部分数据被传送。这可能由于几个原因, 依赖于设备。常常, 应用程序重新试着读取。例如, 如果你使用fread函数来读取, 库函数重新发出系统调用直到请求的数据传送完成。
如果值为 0, 到达了文件末尾(没有读取数据)。
一个负值表示有一个错误。这个值指出了什么错误, 根据<linux/errno.h>。出错的典型返回值包括 -EINTR(被打断的系统调用) 或者 -EFAULT( 坏地址 )。
(2)write 方法
write, 象 read, 可以传送少于要求的数据, 根据返回值的下列规则:
如果值等于 count,要求的字节数已被传送.
如果正值,但是小于 count, 只有部分数据被传送。程序最可能重试写入剩下的数据。
如果值为 0,什么没有写。这个结果不是一个错误, 没有理由返回一个错误码。再一次, 标准库重试写调用。
一个负值表示发生一个错误;如同对于读,有效的错误值是定义于<linux/errno.h>中。
(3)设备控制操作
虽然在文件操作结构体"struct file_operations"中有很多对应的设备操作函数,但是有些命令是实在找不到对应的操作函数。这可以通过设备驱动程序中的函数ioctl()来完成。ioctl()的用法与具体设备密切关联,因此需要根据设备的实际情况进行具体分析。如下图所示。
int (*ioctl) (struct inode * node, struct file *filp, unsigned int cmd, unsigned long arg);
参数:
1)inode和file:ioctl的操作有可能是要修改文件的属性,或者访问硬件。要修改文件属性的话,就要用到这两个结构体了,所以这里传来了它们的指针。
2)cmd:命令参数,用来传递应用层传来的数值以控制驱动对应的操作,参数内容可以由开发者根据需要进行约定设置。
3) arg,可选参数,如果arg是一个整数,可以直接使用;如果是指针,我们必须确保这个用户地址是有效的,因此,使用之前需要进行正确检查。
3、设备中断处理
(1)中断
程序中断通常简称中断,是指CPU在正常运行程序的过程中,由于预选安排或发生了各种随机的内部或外部事件,使CPU中断正在运行的程序,而转到为相应的服务程序去处理,这个过程称为程序中断。如下图:
(2)轮询
轮询(Polling)I/O方式或程序控制I/O方式,是让CPU以一定的周期按次序查询每一个外设,看它是否有数据输入或输出的要求,若有,则进行相应的输入/输出服务;若无,或I/O处理完毕后,CPU就接着查询下一个外设。
所需硬件:外设接口提供状态端口、数据端口
软件机制:应用程序必须定时查询各个接口的状态端口,判断是否需要输入、输出数据,如果需要,则通过数据端口进行数据操作。
特点:CPU通过执行指令主动对外部设备进行查询,外部设备处于被动地位。
(3)ARM Linux中断注册、释放和中断处理函数
在响应一个中断时,内核会执行该中断号对应的一个函数,该函数就叫做该中断对应的中断处理函数。
在ARM处理器中,中断响应后,需要通过内核提供的相关接口调用中断服务程序来进行中断处理。在驱动编程中,我们只要通过接口告诉内核关联驱动处理函数,当来了指定中断时,内核决定该执行哪个中断处理函数。
1)注册中断处理函数
int request_irq(unsigned int irq, irq_handler_t handler, unsigned long irqflags, const char *devname, void *dev_id)
该函数将中断号irq与中断处理函数handler对应。其中参数irq用于指定要分配的中断号。
handler:中断处理函数指针,用于关联具体的中断处理函数,调用时直接填写终端处理函数名。
irqflags:中断处理标记,主要指定中断的触发方式,不同处理器外部中断触发方式不一样,有边沿触发(上升沿或下降沿)、电平触发(高电平或低电平)等。
devname:该字符串将显示在/proc/irq和/pro/interrupt中。
dev_id:设备ID 号。
2)释放中断处理函数
void free_irq(unsigned int irq, void *dev_id)
free_irq()函数主要用于释放终端资源,里面的参数和注册中断处理函数中的参数含义一样。
3)中断处理函数
static irqreturn_t intr_handler(int irq, void *dev_id)
irq,这是调用中断处理函数时传给它的中断号,对于新版本的内核,这个参数已经用处不大,一般只用于打印。
dev_id,这个参数与request_irq()的参数dev_id一致。
irqreturn_t是中断返回类型,中断处理函数的返回值主要有三个。
#define IRQ_NONE (0) //如果产生的中断并不会执行该中断处理函数时返回该值
#define IRQ_HANDLED (1) //中断处理函数正确调用会返回
#define IRQ_RETVAL(x) ((x) != 0) //指定返回的数值,如果非0,返回IRQ_HADLER
4、platform总线定义
Linux内核中常见的总线有I2C、PCI、串口总线、SPI、CAN总线、单总线等,所以有些设备和驱动就可以挂在这些总线上,然后通过总线上的match进行设备和驱动的匹配。但是有的设备并不属于这些常见总线,所以我们引入了一种虚拟总线,也就是platform总线的概念,对应的设备叫做platform设备,对应的驱动叫做platform驱动。当然引入platform的概念,可以做到与板子相关的代码和驱动的代码分离,使得驱动有更好的可扩展性和跨平台性。
platform总线相应的设备称为platform_device,而对应的驱动为platform_driver。Linux的platform_driver机制和传统的device_driver机制(即:通过driver_register函数进行注册)相比,一个十分明显的优势在于platform机制将设备本身的资源注册进内核,由内核统一管理,在驱动程序中用使用这些资源时,通过platform device提供的标准接口进行申请并使用。
采用platform来描述的资源有一个共同点:可以在CPU的总线上直接取址,平台设备会分到一个名称(用在驱动绑定中)以及一系列诸如地址和中断请求号(IRQ)之类的资源。
基于platform总线的驱动开发流程如下:
(1)定义初始化platform bus
(2)定义各种platform devices
(3)注册各种platform devices
(4)定义相关platform driver
(5)注册相关platform driver
(6)操作相关设备
platform总线在内核中用platform_bus_type结构表示,总线本身也是一个设备,Linux内核用platform_bus结构表示。
platform总线作为一个设备在系统启动时自动创建,具体函数调用流程为:start_kernel()->rest_init()->kernel_init()->do_basic_setup()->driver_init()-> platform_bus_init()。
5、platform总线的重要数据结构与函数
基于Platform总线的驱动开发流程如下:
(1)定义初始化platform bus
(2)定义各种platform devices
(3)注册各种platform devices
(4)定义相关platform driver
(5)注册相关platform driver
(6)操作相关设备
总线本身也是一个设备,platform总线在内核中用platform_bus_type结构表示,Linux内核用platform_bus结构表示。
platform总线作为一个设备在系统启动时自动创建,具体函数调用流程为:start_kernel()->rest_init()->kernel_init()->do_basic_setup()->driver_init()-> platform_bus_init()。
platform总线的重要数据结构与函数:
(1)两个结构体platform_device和platform_driver
platform_device的注册和注销使用以下函数:
int platform_device_register(struct platform_device *pdev) //同样的,需要判断返回值
void platform_device_unregister(struct platform_device *pdev)
注册后,同样会在/sys/device/目录下创建一个以name命名的目录,并且创建软连接到/sys/bus/platform/device下。
struct bus_type platform_bus_type = {
.name = "platform", //定义了总线名字为platform,总线注册后新建目录sys/bus/platform
.dev_attrs = platform_dev_attrs,
.match = platform_match, //指定配对函数
.uevent = platform_uevent,
.pm = PLATFORM_PM_OPS_PTR,
};
可以看到,总线中定义了成员名字和match函数,当有总线或者设备注册到platform总线时,内核自动调用match函数,判断设备和驱动的name是否一致。
(2)platform_driver结构体:(include\linux\platform_device.h)
两组接口函数(driver\base\platform.c)
int platform_driver_register(struct platform_driver *);
// 用来注册我们的设备驱动
void platform_driver_unregister(struct platform_driver *);
// 用来卸载我们的设备驱动
int platform_device_register(struct platform_device *);
// 用来注册我们的设备
void platform_device_unregister(struct platform_device *);
// 用来卸载我们的设备
(3)platform驱动注册
int platform_driver_register(struct platform_driver *drv)
void platform_driver_unregister(struct platform_driver *drv)
注册成功后内核会在/sys/bus/platform/driver/目录下创建一个名字为driver->name的目录。
要用注册一个platform驱动的步骤:
注册设备platform_device_register;
注册驱动platform_driver_register;
驱动注册中,需要实现的结构体是:platform_driver。需要注意的是:注册的时候platform_driver和platform_device中的name变量的名称必须是相同的。在驱动程序的初始化函数中,调用了platform_driver_register()注册platform_driver。这样在platform_driver_register()注册时,会将当前注册的platform_driver中的name变量的值和已注册的所有platform_device中的name变量的值进行比较,只有找到具有相同名称的 platform_device才能注册成功。
6、platform总线设备驱动实例
我们根据设备管理的分层与面向对象思想,将模拟的usb鼠标设备和驱动添加到platform总线上。
(1)platform USB鼠标设备数据结构与装卸函数
1)鼠标platform设备数据结构定义与初始化
鼠标platform设备数据结构定义与初始化
truct platform_device mouse_dev = {
.name = "plat_usb_mouse", //将以这个名字创建目录
.dev = {
.bus_id = "usb_mouse", //不会用这个名字创建目录了,这里不设置bus_id也行的。
.release = usb_dev_release,
},
2)USB鼠标设备装载函数
static int __init usb_device_init(void)
{
int ret;
ret = platform_device_register(&mouse_dev);
if(ret){
printk("device register failed!\n");
return ret;
}
printk("usb device init\n");
return 0;
}
static void __exit usb_device_exit(void)
{
platform_device_unregister(&mouse_dev);
printk("usb device bye!\n");
}
将之前usb结构体和注册函数更改为platform类型就可以了。(2)platform USB鼠标驱动数据结构与装卸函数
USB鼠标驱动也是一样需要定义初始化设备驱动结构体。
struct platform_driver mouse_drv = {
.probe = usb_driver_probe,
.remove = usb_driver_remove,
.driver = {
.name = "plat_usb_mouse", //在/sys/中的驱动目录名字
},
};
USB鼠标设备驱动加载函数:
static int __init usb_driver_init(void)
{
int ret;
/*驱动注册,注册成功后在/sys/platform/usb/driver目录下创建目录
* plat_usb_mouse*/
ret = platform_driver_register(&mouse_drv);
if(ret){
printk("driver register failed!\n");
return ret;
}
printk("usb driver init\n");
return 0;
}
USB鼠标设备驱动卸载函数:
static void __exit usb_driver_exit(void)
{
platform_driver_unregister(&mouse_drv);
printk("usb driver bye!\n");
}
设备和驱动都以”plat_usb_mouse”命名,这样的话match函数也就能配对成功。
7、设备树基本概念及作用
驱动的开发方法可以分为三种:传统方法、总线方法和设备树方法。在kernel 3.0以及之后的版本,都是采用设备树的方法实现驱动与设备之间的联系。将设备注册改为设备树实现,解决了总线方法中代码冗余多的问题。
设备树包含DTC(device tree compiler),DTS(device tree source和DTB(device tree blob)。
一种总线控制器可以连接多个设备。DTS文件的主要功能就是按照上图所示的结构来描述板子上的设备信息,DTS文件描述设备信息是有相应的语法规则要求的。
设备树的主要优势:对于同一SoC的不同主板,只需更换设备树文件.dtb即可实现不同主板的无差异支持,而无须更换内核文件。
设备树由一系列被命名的节点(Node)和属性(Property)组成,其中节点本身可包含子节点,而属性就是成对出现的名与值,在设备树中,可描述的信息包含:
1. CPU的数量和类型
2.内存基地址和大小
3.总线和桥
4.外设连接
5.中断控制器和中断使用情况
6.GPIO控制器和GPIO使用情况
7.时间控制器和时钟使用情况
设备树的描述硬件资源的数据结构通过bootloader将硬件资源传给内核,使得内核和硬件资源描述相对独立,也就是说DTS源文件经过DTC编译后的设备树*.dtb文件由Bootloader读入内存,之后由内核来解析。
设备树源文件扩展名为.dts,一般而言,一个*.dts文件对应一个ARM的machine。
*.dtsi文件,类似于C语言的头文件,在dts文件中需要进行include *.dtsi文件。
DTC为编译工具,它可以将.dts文件编译成.dtb文件。
Bootloader在引导内核时,会预先读取*.dtb到内存,进而由内核解析。
DTS语法:
标准属性:
(1)compatible属性
用于将设备和驱动绑定起来,格式:"manufacturer, model"
例如:
compatible = "fsl, imx6ul-evk-wm8960","fsl, imx-audio-wm8960"
其中“fsl”表示厂商是飞思卡尔,“imx6ul-evk-wm8960”和“imx-audio-wm8960”表示驱动模块名字。
一般驱动程序文件都会有一个OF匹配表,如果设备节点的compatible属性值和OF匹配表中的任何一个值相等,那么就表示设备可以使用这个驱动。
(2)model属性
model属性值也是一个字符串,一般model属性描述设备模块信息,比如名字什么的,比如:
model = "wm8960-audio";
(3)status属性
status属性看名字就知道是和设备状态有关的,status属性值也是字符串,字符串是设备的状态信息,可选的状态如下表所示。
(4)#address-cells和#size-cells属性
#address-cells属性值决定了子节点reg属性中地址信息所占用的字长(32位),#size-cells属性值决定了子节点reg属性中长度信息所占的字长。
(5)reg属性
reg属性的值一般是(address,length)对。reg属性一般用于描述设备地址空间资源信息,如某个外设的寄存器地址范围信息。
(6)ranges属性
ranges属性值可以为空或者按照(child-bus-address, parent-bus-address, length)格式编写的数字矩阵,ranges是一个地址映射/转换表,ranges属性每个项目由子地址、父地址和地址空间长度这三部分组成。
ranges = <0x0 0xE0000000 0x00100000>;
此属性值指定了一个1024KB(0x00100000)的地址范围,子地址空间的物理起始地址为0x0,父地址空间的物理起始地址为0xE0000000。
(7)name属性
name属性值为字符串,name属性用于记录节点名字,name属性已经被弃用,不推荐使用name属性,一些老的设备树文件可能会使用此属性。
(8)device_type属性
device_type属性值为字符串,此属性只能用于cpu节点或者memory节点。