2016-11-20 18:19:56 viewsky11 阅读数 5921
  • linux设备驱动模型-linux驱动开发第5部分

    本课程是linux驱动开发的第5个课程,主要内容是linux的设备驱动模型,包括总线、类、设备、驱动等概念,重点通过platform平台总线的工作来演示设备驱动模型的工作方法,实践环节对上个课程的LED驱动进行平台总线式改造,终目标是让大家彻底掌握linux的总线式设备驱动模型。

    4821 人正在学习 去看看 朱有鹏

基础性总结

1, linux驱动一般分为3大类:

* 字符设备
* 块设备
* 网络设备

2, 开发环境构建:

* 交叉工具链构建
* NFS和tftp服务器安装

3, 驱动开发中设计到的硬件:

* 数字电路知识
* ARM硬件知识
* 熟练使用万用表和示波器
* 看懂芯片手册和原理图

4, linux内核源代码目录结构:

这里写图片描述
* arch/: arch子目录包括了所有和体系结构相关的核心代码。它的每一个子目录都代表一种支持的体系结构,例如i386就是关于intel cpu及与之相兼容体系结构的子目录。
* block/: 部分块设备驱动程序;
* crypto: 常用加密和散列算法(如AES、SHA等),还有一些压缩和CRC校验算法;
* documentation/: 文档目录,没有内核代码,只是一套有用的文档;
* drivers/: 放置系统所有的设备驱动程序;每种驱动程序又各占用一个子目录:如,/block 下为块设备驱动程序,比如ide(ide.c)。如果你希望查看所有可能包含文件系统的设备是如何初始化的,你可以看 drivers/block/genhd.c中的device_setup()。
* fs/: 所有的文件系统代码和各种类型的文件操作代码,它的每一个子目录支持一个文件系统, 例如fat和ext2;
* include/: include子目录包括编译核心所需要的大部分头文件。与平台无关的头文件在 include/linux子目录下,与 intel cpu相关的头文件在include/asm-i386子目录下,而include/scsi目录则是有关scsi设备的头文件目录;
* init/: 这个目录包含核心的初始化代码(注:不是系统的引导代码),包含两个文件main.c和Version.c,这是研究核心如何工作的好的起点之一;
* ipc/: 这个目录包含核心的进程间通讯的代码;
* kernel/: 主要的核心代码,此目录下的文件实现了大多数linux系统的内核函数,其中最重要的文件当属sched.c;同样,和体系结构相关的代码在arch/i386/kernel下;
* lib/: 放置核心的库代码;
* mm/:这个目录包括所有独立于 cpu 体系结构的内存管理代码,如页式存储管理内存的分配和释放等;而和体系结构相关的内存管理代码则位于arch/i386/mm/下;
* net/: 核心与网络相关的代码;
* scripts/: 描述文件,脚本,用于对核心的配置;
* security: 主要是一个SELinux的模块;
* sound: 常用音频设备的驱动程序等;
* usr: 实现了用于打包和压缩的cpio;

5, 内核的五个子系统:

* 进程调试(SCHED)
* 内存管理(MM)
* 虚拟文件系统(VFS)
* 网络接口(NET)
* 进程间通信(IPC)

6, linux内核的编译:

* 配置内核:make menuconfig,使用后会生成一个.confiig配置文件,记录哪些部分被编译入内核,哪些部分被编译成内核模块。
* 编译内核和模块的方法:make zImage
Make modules
* 执行完上述命令后,在arch/arm/boot/目录下得到压缩的内核映像zImage,在内核各对应目录得到选中的内核模块。

7, 在linux内核中增加程序

(直接编译进内核)要完成以下3项工作:
* 将编写的源代码拷入linux内核源代码相应目录
* 在目录的Kconifg文件中增加关于新源代码对应项目的编译配置选项
* 在目录的Makefile文件中增加对新源代码的编译条目

8, linux下C编程的特点:

内核下的Documentation/CodingStyle描述了linux内核对编码风格的要求。具体要求不一一列举,以下是要注意的:
* 代码中空格的应用
* 当前函数名:
GNU C预定义了两个标志符保存当前函数的名字,__FUNCTION__保存函数在源码中的名字,__PRETTY_FUNCTION__保存带语言特色的名字。
由于C99已经支持__func__宏,在linux编程中应该不要使用__FUNCTION__,应该使用__func__
*内建函数:不属于库函数的其他内建函数的命名通常以__builtin开始。

9,内核模块

内核模块主要由如下几部分组成:
(1) 模块加载函数
(2) 模块卸载函数
(3) 模块许可证声明(常用的有Dual BSD/GPL,GPL,等)
(4) 模块参数(可选)它指的是模块被加载的时候可以传递给它的值,它本身对应模块内部的全局变量。例如P88页中讲到的一个带模块参数的例子:
insmod book.ko book_name=”GOOD BOOK” num=5000
(5) 模块导出符号(可选)导出的符号可以被其他模块使用,在使用之前只需声明一下。
(6) 模块作者等声明信息(可选)
以下是一个典型的内核模块:

/*
 * A kernel module: book
 * This example is to introduce module params
 *
 * The initial developer of the original code is Baohua Song
 * <author@linuxdriver.cn>. All Rights Reserved.
 */

#include <linux/init.h>
#include <linux/module.h>

static char *book_name = “dissecting Linux Device Driver”;
static int num = 4000;

static int book_init(void)
{
        printk(KERN_INFO “ book name:%s\n”,book_name);
        printk(KERN_INFO “ book num:%d\n”,num);
        return 0;
}

static void book_exit(void)
{
        printk(KERN_INFO “ Book module exit\n “);
}

module_init(book_init);
module_exit(book_exit);
module_param(num, int, S_IRUGO);
module_param(book_name, charp, S_IRUGO);
MODULE_AUTHOR(“Song Baohua, author@linuxdriver.cn”);
MODULE_LICENSE(“Dual BSD/GPL”);
MODULE_DESCRIPTION(“A simple Module for testing module params”);
MODULE_VERSION(“V1.0”);

注意:标有__init的函数在链接的时候都放在.init.text段,在.initcall.init中还保存了一份函数指针,初始化的时候内核会通过这些函数指针调用__init函数,在初始化完成后释放init区段。
模块编译常用模版:

KVERS = $(shell uname -r)
# Kernel modules
obj-m += book.o
# Specify flags for the module compilation.
#EXTRA_CFLAGS=-g -O0
build: kernel_modules
kernel_modules:
        make -C /lib/modules/$(KVERS)/build M=$(CURDIR) modules

clean:
        make -C /lib/modules/$(KVERS)/build M=$(CURDIR) clean

注意要指明内核版本,并且内核版本要匹配——编译模块使用的内核版本要和模块欲加载到的那个内核版本要一致。
模块中经常使用的命令:

insmod,lsmod,rmmod

系统调用:

int open(const char *pathname,int flags,mode_t mode);

flag表示文件打开标志,如:O_RDONLY
mode表示文件访问权限,如:S_IRUSR(用户可读),S_IRWXG(组可以读、写、执行)

10,linux文件系统与设备驱动的关系

这里写图片描述
应用程序和VFS之间的接口是系统调用,而VFS与磁盘文件系统以及普通设备之间的接口是file_operation结构体成员函数。
两个重要的函数:
(1)struct file结构体定义在/linux/include/linux/fs.h(Linux 2.6.11内核)中定义。文件结构体代表一个打开的文件,系统中每个打开的文件在内核空间都有一个关联的struct file。它由内核在打开文件时创建,并传递给在文件上进行操作的任何函数。在文件的所有实例都关闭后,内核释放这个数据结构。在内核创建和驱动源码中,struct file的指针通常被命名为file或filp。
在驱动开发中,文件读/写模式mode、标志f_flags都是设备驱动关心的内容,而私有数据指针private_data在驱动中被广泛使用,大多被指向设备驱动自定义的用于描述设备的结构体。驱动程序中常用如下类似的代码来检测用户打开文件的读写方式:

if (file->f_mode & FMODE_WRITE) //用户要求可写
{
}
if (file->f_mode & FMODE_READ) //用户要求可读
{
}

下面的代码可用于判断以阻塞还是非阻塞方式打开设备文件:

if (file->f_flags & O_NONBLOCK) //非阻塞
pr_debug("open:non-blocking\n");
else //阻塞
pr_debug("open:blocking\n");

(2)struct inode结构体定义在linux/fs.h中

11,devfs、sysfs、udev三者的关系:

(1)devfs
linux下有专门的文件系统用来对设备进行管理,devfs和sysfs就是其中两种。在2.4内核4一直使用的是devfs,devfs挂载于/dev目录下,提供了一种类似于文件的方法来管理位于/dev目录下的所有设备,我们知道/dev目录下的每一个文件都对应的是一个设备,至于当前该设备存在与否先且不论,而且这些特殊文件是位于根文件系统上的,在制作文件系统的时候我们就已经建立了这些设备文件,因此通过操作这些特殊文件,可以实现与内核进行交互。但是devfs文件系统有一些缺点,例如:不确定的设备映射,有时一个设备映射的设备文件可能不同,例如我的U盘可能对应sda有可能对应sdb;没有足够的主/次设备号,当设备过多的时候,显然这会成为一个问题;/dev目录下文件太多而且不能表示当前系统上的实际设备;命名不够灵活,不能任意指定等等。
(2)sysfs
正因为上述这些问题的存在,在linux2.6内核以后,引入了一个新的文件系统sysfs,它挂载于/sys目录下,跟devfs一样它也是一个虚拟文件系统,也是用来对系统的设备进行管理的,它把实际连接到系统上的设备和总线组织成一个分级的文件,用户空间的程序同样可以利用这些信息以实现和内核的交互,该文件系统是当前系统上实际设备树的一个直观反应,它是通过kobject子系统来建立这个信息的,当一个kobject被创建的时候,对应的文件和目录也就被创建了,位于/sys下的相关目录下,既然每个设备在sysfs中都有唯一对应的目录,那么也就可以被用户空间读写了。用户空间的工具udev就是利用了sysfs提供的信息来实现所有devfs的功能的,但不同的是udev运行在用户空间中,而devfs却运行在内核空间,而且udev不存在devfs那些先天的缺陷。
(3)udev
udev是一种工具,它能够根据系统中的硬件设备的状况动态更新设备文件,包括设备文件的创建,删除等。设备文件通常放在/dev目录下,使用udev后,在/dev下面只包含系统中真实存在的设备。它于硬件平台无关的,位于用户空间,需要内核sysfs和tmpfs的支持,sysfs为udev提供设备入口和uevent通道,tmpfs为udev设备文件提供存放空间。

12,linux设备模型:

这里写图片描述

在linux内核中,分别使用bus_type,device_driver,device来描述总线、驱动和设备,这3个结构体定义于include/linux/device.h头文件中。驱动和设备正是通过bus_type中的match()函数来配对的。

13, 重要结构体解析

(1)cdev结构体
在linux2.6内核中,使用cdev结构体描述一个字符设备,定义如下:

struct cdev{
    struct kobject kobj;//内嵌的kobject对象
    struct module *owner;//所属模块
    struct file_operations *ops;//文件操作结构体
    struct list_head list;
    dev_t dev;//设备号,长度为32位,其中高12为主设备号,低20位为此设备号
    unsigned int count;
};

(2)file_operations结构体
结构体file_operations在头文件linux/fs.h中定义,用来存储驱动内核模块提供的对设备进行各种操作的函数的指针。这些函数实际会在应用程序进行linux的open(),write(),read(),close()等系统调用时最终被调用。该结构体的每个域都对应着驱动内核模块用来处理某个被请求的事务的函数地址。源代码(2.6.28.7)如下:

struct file_operations{
    struct module*owner;
    loff_t (*llseek)(struct file*,loff_t,int);
    ssize_t (*read)(struct file*,char__user*,size_t,loff_t*);
    ssize_t (*write)(struct file*,constchar__user*,size_t,loff_t*);
    ssize_t (*aio_read)(struct kiocb*,cons tstruct iovec*,unsigned long,loff_t);
    ssize_t (*aio_write)(struct kiocb*,const struct iovec*,unsigned long,loff_t);
    int (*readdir)(struct file*,void*,filldir_t);
    unsigned int (*poll)(struct file*,struct poll_table_struct*);
    int (*ioctl)(struc inode*,struct file*,unsigned int,unsigned long);
    long (*unlocked_ioctl)(struct file*,unsigned int,unsigned long);
    long (*compat_ioctl)(struct file*,unsigned int,unsigned long);
    int (*mmap)(struct file*,struct vm_area_struct*);
    int (*open)(struct inode*,struct file*);
    int (*flush)(struct file*,fl_owner_t id);
    int (*release)(struct inode*,struct file*);
    int (*fsync)(struct file*,struct dentry*,int datasync);
    int (*aio_fsync)(struct kiocb*,int datasync);
    in (*fasync)(int,struct file*,int);
    int (*lock)(struct file*,int,struct file_lock*);
    ssize_t (*sendpage)(struct file*,struct page*,int,size_t,loff_t*,int);
    unsigned long (*get_unmapped_area)(struct file*,unsigned long,unsigned long,unsigned long,unsigned long);
    in t(*check_flags)(int);
    int (*dir_notify)(structfile*filp,unsignedlongarg);
    int (*flock)(structfile*,int,structfile_lock*);
    ssize_t (*splice_write)(struct pipe_inode_info*,struct file*,loff_t*,size_t,unsig ned int);
    ssize_t (*splice_read)(struct file*,loff_t*,struct pipe_inode_info*,size_t,unsigned int);
    int(*setlease)(struct file*,long,struct file_lock**);
}; 

解析:

struct module*owner;
/*第一个file_operations成员根本不是一个操作;它是一个指向拥有这个结构的模块的指针.
这个成员用来在它的操作还在被使用时阻止模块被卸载.几乎所有时间中,它被简单初始化为
THIS_MODULE,一个在中定义的宏.这个宏比较复杂,在进行简单学习操作的时候,一般初始化为THIS_MODULE。*/
loff_t (*llseek)(struct file*filp,loff_tp,int orig);
/*(指针参数filp为进行读取信息的目标文件结构体指针;参数p为文件定位的目标偏移量;参数orig为对文件定位
的起始地址,这个值可以为文件开头(SEEK_SET,0,当前位置(SEEK_CUR,1),文件末尾(SEEK_END,2))
llseek方法用作改变文件中的当前读/写位置,并且新位置作为(正的)返回值.
loff_t参数是一个"longoffset",并且就算在32位平台上也至少64位宽.错误由一个负返回值指示.
如果这个函数指针是NULL,seek调用会以潜在地无法预知的方式修改file结构中的位置计数器(在"file结构"一节中描述).*/
ssize_t (*read)(struct file *filp,char__user *buffer,size_t size,loff_t *p);
/*(指针参数filp为进行读取信息的目标文件,指针参数buffer为对应放置信息的缓冲区(即用户空间内存地址),
参数size为要读取的信息长度,参数p为读的位置相对于文件开头的偏移,在读取信息后,这个指针一般都会移动,移动的值为要读取信息的长度值)
这个函数用来从设备中获取数据.在这个位置的一个空指针导致read系统调用以-EINVAL("Invalidargument")失败.
一个非负返回值代表了成功读取的字节数(返回值是一个"signedsize"类型,常常是目标平台本地的整数类型).*/
ssize_t (*aio_read)(struct kiocb*,char__user *buffer,size_t size,loff_t p);
/*可以看出,这个函数的第一、三个参数和本结构体中的read()函数的第一、三个参数是不同的,
异步读写的第三个参数直接传递值,而同步读写的第三个参数传递的是指针,因为AIO从来不需要改变文件的位置。
异步读写的第一个参数为指向kiocb结构体的指针,而同步读写的第一参数为指向file结构体的指针,每一个I/O请求都对应一个kiocb结构体);
初始化一个异步读--可能在函数返回前不结束的读操作.如果这个方法是NULL,所有的操作会由read代替进行(同步地).
(有关linux异步I/O,可以参考有关的资料,《linux设备驱动开发详解》中给出了详细的解答)*/
ssize_t (*write)(struct file*filp,const char__user *buffer,size_t count,loff_t *ppos);
/*(参数filp为目标文件结构体指针,buffer为要写入文件的信息缓冲区,count为要写入信息的长度,
ppos为当前的偏移位置,这个值通常是用来判断写文件是否越界)
发送数据给设备.如果NULL,-EINVAL返回给调用write系统调用的程序.如果非负,返回值代表成功写的字节数.
(注:这个操作和上面的对文件进行读的操作均为阻塞操作)*/
ssize_t (*aio_write)(struct kiocb*,const char__user *buffer,size_t count,loff_t *ppos);
/*初始化设备上的一个异步写.参数类型同aio_read()函数;*/
int (*readdir)(struct file*filp,void*,filldir_t);
/*对于设备文件这个成员应当为NULL;它用来读取目录,并且仅对文件系统有用.*/
unsigned int(*poll)(struct file*,struct poll_table_struct*);
/*(这是一个设备驱动中的轮询函数,第一个参数为file结构指针,第二个为轮询表指针)
这个函数返回设备资源的可获取状态,即POLLIN,POLLOUT,POLLPRI,POLLERR,POLLNVAL等宏的位“或”结果。
每个宏都表明设备的一种状态,如:POLLIN(定义为0x0001)意味着设备可以无阻塞的读,POLLOUT(定义为0x0004)意味着设备可以无阻塞的写。
(poll方法是3个系统调用的后端:poll,epoll,和select,都用作查询对一个或多个文件描述符的读或写是否会阻塞.
poll方法应当返回一个位掩码指示是否非阻塞的读或写是可能的,并且,可能地,提供给内核信息用来使调用进程睡眠直到I/O变为可能.
如果一个驱动的poll方法为NULL,设备假定为不阻塞地可读可写.
(这里通常将设备看作一个文件进行相关的操作,而轮询操作的取值直接关系到设备的响应情况,可以是阻塞操作结果,同时也可以是非阻塞操作结果)*/
int (*ioctl)(struct inode*inode,struct file*filp,unsigned int cmd,unsigned long arg);
/*(inode和filp指针是对应应用程序传递的文件描述符fd的值,和传递给open方法的相同参数.
cmd参数从用户那里不改变地传下来,并且可选的参数arg参数以一个unsignedlong的形式传递,不管它是否由用户给定为一个整数或一个指针.
如果调用程序不传递第3个参数,被驱动操作收到的arg值是无定义的.
因为类型检查在这个额外参数上被关闭,编译器不能警告你如果一个无效的参数被传递给ioctl,并且任何关联的错误将难以查找.)
ioctl系统调用提供了发出设备特定命令的方法(例如格式化软盘的一个磁道,这不是读也不是写).另外,几个ioctl命令被内核识别而不必引用fops表.
如果设备不提供ioctl方法,对于任何未事先定义的请求(-ENOTTY,"设备无这样的ioctl"),系统调用返回一个错误.*/
int(*mmap)(struct file*,struct vm_area_struct*);
/*mmap用来请求将设备内存映射到进程的地址空间.如果这个方法是NULL,mmap系统调用返回-ENODEV.
(如果想对这个函数有个彻底的了解,那么请看有关“进程地址空间”介绍的书籍)*/
int(*open)(struct inode *inode,struct file *filp);
/*(inode为文件节点,这个节点只有一个,无论用户打开多少个文件,都只是对应着一个inode结构;
但是filp就不同,只要打开一个文件,就对应着一个file结构体,file结构体通常用来追踪文件在运行时的状态信息)
尽管这常常是对设备文件进行的第一个操作,不要求驱动声明一个对应的方法.如果这个项是NULL,设备打开一直成功,但是你的驱动不会得到通知.
与open()函数对应的是release()函数。*/
int(*flush)(struct file*);
/*flush操作在进程关闭它的设备文件描述符的拷贝时调用;它应当执行(并且等待)设备的任何未完成的操作.
这个必须不要和用户查询请求的fsync操作混淆了.当前,flush在很少驱动中使用;
SCSI磁带驱动使用它,例如,为确保所有写的数据在设备关闭前写到磁带上.如果flush为NULL,内核简单地忽略用户应用程序的请求.*/
int(*release)(struct inode*,struct file*);
/*release()函数当最后一个打开设备的用户进程执行close()系统调用的时候,内核将调用驱动程序release()函数:
void release(struct inode inode,struct file *file),release函数的主要任务是清理未结束的输入输出操作,释放资源,用户自定义排他标志的复位等。
在文件结构被释放时引用这个操作.如同open,release可以为NULL.*/
int (*synch)(struct file*,struct dentry*,intdatasync);
//刷新待处理的数据,允许进程把所有的脏缓冲区刷新到磁盘。
int(*aio_fsync)(struct kiocb*,int);
/*这是fsync方法的异步版本.所谓的fsync方法是一个系统调用函数。系统调用fsync
把文件所指定的文件的所有脏缓冲区写到磁盘中(如果需要,还包括存有索引节点的缓冲区)。
相应的服务例程获得文件对象的地址,并随后调用fsync方法。通常这个方法以调用函数__writeback_single_inode()结束,
这个函数把与被选中的索引节点相关的脏页和索引节点本身都写回磁盘。*/
int(*fasync)(int,struct file*,int);
//这个函数是系统支持异步通知的设备驱动,下面是这个函数的模板:
static int***_fasync(intfd,structfile*filp,intmode)
{
struct***_dev*dev=filp->private_data;
returnfasync_helper(fd,filp,mode,&dev->async_queue);//第四个参数为fasync_struct结构体指针的指针。
//这个函数是用来处理FASYNC标志的函数。(FASYNC:表示兼容BSD的fcntl同步操作)当这个标志改变时,驱动程序中的fasync()函数将得到执行。
}
/*此操作用来通知设备它的FASYNC标志的改变.异步通知是一个高级的主题,在第6章中描述.
这个成员可以是NULL如果驱动不支持异步通知.*/
int (*lock)(struct file*,int,struct file_lock*);
//lock方法用来实现文件加锁;加锁对常规文件是必不可少的特性,但是设备驱动几乎从不实现它.
ssize_t (*readv)(structfile*,const struct iovec*,unsigned long,loff_t*);
ssize_t (*writev)(struct file*,const struct iovec*,unsigned long,loff_t*);
/*这些方法实现发散/汇聚读和写操作.应用程序偶尔需要做一个包含多个内存区的单个读或写操作;
这些系统调用允许它们这样做而不必对数据进行额外拷贝.如果这些函数指针为NULL,read和write方法被调用(可能多于一次).*/
ssize_t (*sendfile)(struct file*,loff_t*,size_t,read_actor_t,void*);
/*这个方法实现sendfile系统调用的读,使用最少的拷贝从一个文件描述符搬移数据到另一个.
例如,它被一个需要发送文件内容到一个网络连接的web服务器使用.设备驱动常常使sendfile为NULL.*/
ssize_t (*sendpage)(structfile*,structpage*,int,size_t,loff_t*,int);
/*sendpage是sendfile的另一半;它由内核调用来发送数据,一次一页,到对应的文件.设备驱动实际上不实现sendpage.*/
unsigned long(*get_unmapped_area)(struct file*,unsigned long,unsignedlong,unsigned long,unsigned long);
/*这个方法的目的是在进程的地址空间找一个合适的位置来映射在底层设备上的内存段中.
这个任务通常由内存管理代码进行;这个方法存在为了使驱动能强制特殊设备可能有的任何的对齐请求.大部分驱动可以置这个方法为NULL.[10]*/
int (*check_flags)(int)
//这个方法允许模块检查传递给fnctl(F_SETFL...)调用的标志.
int (*dir_notify)(struct file*,unsigned long);
//这个方法在应用程序使用fcntl来请求目录改变通知时调用.只对文件系统有用;驱动不需要实现dir_notify.

14, 字符设备驱动程序设计基础

主设备号和次设备号(二者一起为设备号):
一个字符设备或块设备都有一个主设备号和一个次设备号。主设备号用来标识与设备文件相连的驱动程序,用来反映设备类型。次设备号被驱动程序用来辨别操作的是哪个设备,用来区分同类型的设备。
linux内核中,设备号用dev_t来描述,2.6.28中定义如下:

typedef u_long dev_t;

在32位机中是4个字节,高12位表示主设备号,低12位表示次设备号。

可以使用下列宏从dev_t中获得主次设备号:也可以使用下列宏通过主次设备号生成dev_t:

MAJOR(dev_tdev);
MKDEV(intmajor,intminor);
MINOR(dev_tdev);

分配设备号(两种方法):
(1)静态申请:

int register_chrdev_region(dev_t from,unsigned count,const char *name);

(2)动态分配:

int alloc_chrdev_region(dev_t *dev,unsigned baseminor,unsigned count,const char *name);

注销设备号:

void unregister_chrdev_region(dev_t from,unsigned count);

创建设备文件:
利用cat/proc/devices查看申请到的设备名,设备号。
(1)使用mknod手工创建:mknod filename type major minor
(2)自动创建;
利用udev(mdev)来实现设备文件的自动创建,首先应保证支持udev(mdev),由busybox配置。在驱动初始化代码里调用class_create为该设备创建一个class,再为每个设备调用device_create创建对应的设备。

15, 字符设备驱动程序设计

设备注册:
字符设备的注册分为三个步骤:
(1)分配

cdev:struct cdev *cdev_alloc(void);

(2)初始化

cdev:void cdev_init(struct cdev *cdev,const struct file_operations *fops);

(3)添加

cdev:int cdev_add(struct cdev *p,dev_t dev,unsigned count)

设备操作的实现:
file_operations函数集的实现。

struct file_operations xxx_ops={
.owner=THIS_MODULE,
.llseek=xxx_llseek,
.read=xxx_read,
.write=xxx_write,
.ioctl=xxx_ioctl,
.open=xxx_open,
.release=xxx_release,
…
};

特别注意:驱动程序应用程序的数据交换:
驱动程序和应用程序的数据交换是非常重要的。file_operations中的read()和write()函数,就是用来在驱动程序和应用程序间交换数据的。通过数据交换,驱动程序和应用程序可以彼此了解对方的情况。但是驱动程序和应用程序属于不同的地址空间。驱动程序不能直接访问应用程序的地址空间;同样应用程序也不能直接访问驱动程序的地址空间,否则会破坏彼此空间中的数据,从而造成系统崩溃,或者数据损坏。安全的方法是使用内核提供的专用函数,完成数据在应用程序空间和驱动程序空间的交换。这些函数对用户程序传过来的指针进行了严格的检查和必要的转换,从而保证用户程序与驱动程序交换数据的安全性。这些函数有:

unsigned long copy_to_user(void__user *to,const void *from,unsigned long n);
unsigned long copy_from_user(void *to,constvoid __user *from,unsigned long n);
put_user(local,user);
get_user(local,user);

设备注销:

void cdev_del(struct cdev *p);

16,ioctl函数说明

ioctl是设备驱动程序中对设备的I/O通道进行管理的函数。所谓对I/O通道进行管理,就是对设备的一些特性进行控制,例如串口的传输波特率、马达的转速等等。它的调用个数如下:

int ioctl(int fd,ind cmd,…);

其中fd就是用户程序打开设备时使用open函数返回的文件标示符,cmd就是用户程序对设备的控制命令,后面的省略号是一些补充参数,有或没有是和cmd的意义相关的。
ioctl函数是文件结构中的一个属性分量,就是说如果你的驱动程序提供了对ioctl的支持,用户就可以在用户程序中使用ioctl函数控制设备的I/O通道。

命令的组织是有一些讲究的,因为我们一定要做到命令和设备是一一对应的,这样才不会将正确的命令发给错误的设备,或者是把错误的命令发给正确的设备,或者是把错误的命令发给错误的设备。
所以在Linux核心中是这样定义一个命令码的:

设备类型 序列号 方向 数据尺寸
8bit 8bit 2bit 13~14bit

这样一来,一个命令就变成了一个整数形式的命令码。但是命令码非常的不直观,所以LinuxKernel中提供了一些宏,这些宏可根据便于理解的字符串生成命令码,或者是从命令码得到一些用户可以理解的字符串以标明这个命令对应的设备类型、设备序列号、数据传送方向和数据传输尺寸。
点击(此处)折叠或打开

/*used to create numbers*/
#define _IO(type,nr)        _IOC(_IOC_NONE,(type),(nr),0)
#define _IOR(type,nr,size)    _IOC(_IOC_READ,(type),(nr),(_IOC_TYPECHECK(size)))
#define _IOW(type,nr,size)    _IOC(_IOC_WRITE,(type),(nr),(_IOC_TYPECHECK(size)))
#define _IOWR(type,nr,size)    _IOC(_IOC_READ|_IOC_WRITE,(type),(nr),(_IOC_TYPECHECK(size)))
#defin e_IOR_BAD(type,nr,size)    _IOC(_IOC_READ,(type),(nr),sizeof(size))
#define _IOW_BAD(type,nr,size)    _IOC(_IOC_WRITE,(type),(nr),sizeof(size))
#define _IOWR_BAD(type,nr,size)_IOC(_IOC_READ|_IOC_WRITE,(type),(nr),sizeof(size))

#define _IOC(dir,type,nr,size)\
    (((dir)<<_IOC_DIRSHIFT)|\
    ((type)<<_IOC_TYPESHIFT)|\
    ((nr)<<_IOC_NRSHIFT)|\
    ((size)<<_IOC_SIZESHIFT))

17,文件私有数据

大多数linux的驱动工程师都将文件私有数据private_data指向设备结构体,read等个函数通过调用private_data来访问设备结构体。这样做的目的是为了区分子设备,如果一个驱动有两个子设备(次设备号分别为0和1),那么使用private_data就很方便。
这里有一个函数要提出来:

container_of(ptr,type,member)//通过结构体成员的指针找到对应结构体的的指针

其定义如下:

/**
*container_of-castamemberofastructureouttothecontainingstructure
*@ptr:    thepointertothemember.
*@type:    thetypeofthecontainerstructthisisembeddedin.
*@member:    thenameofthememberwithinthestruct.
*
*/
#define container_of(ptr,type,member)({            \
    const typeof(((type*)0)->member)*__mptr=(ptr);    \
    (type*)((char*)__mptr-offsetof(type,member));})

18,字符设备驱动的结构

可以概括如下图:
这里写图片描述
字符设备是3大类设备(字符设备、块设备、网络设备)中较简单的一类设备,其驱动程序中完成的主要工作是初始化、添加和删除cdev结构体,申请和释放设备号,以及填充file_operation结构体中操作函数,并实现file_operations结构体中的read()write()ioctl()等重要函数。如图所示为cdev结构体、file_operations和用户空间调用驱动的关系。

19, 自旋锁与信号量

为了避免并发,防止竞争。内核提供了一组同步方法来提供对共享数据的保护。我们的重点不是介绍这些方法的详细用法,而是强调为什么使用这些方法和它们之间的差别。
Linux使用的同步机制可以说从2.0到2.6以来不断发展完善。从最初的原子操作,到后来的信号量,从大内核锁到今天的自旋锁。这些同步机制的发展伴随Linux从单处理器到对称多处理器的过度;伴随着从非抢占内核到抢占内核的过度。锁机制越来越有效,也越来越复杂。目前来说内核中原子操作多用来做计数使用,其它情况最常用的是两种锁以及它们的变种:一个是自旋锁,另一个是信号量。

自旋锁
自旋锁是专为防止多处理器并发而引入的一种锁,它在内核中大量应用于中断处理等部分(对于单处理器来说,防止中断处理中的并发可简单采用关闭中断的方式,不需要自旋锁)。
自旋锁最多只能被一个内核任务持有,如果一个内核任务试图请求一个已被争用(已经被持有)的自旋锁,那么这个任务就会一直进行忙循环——旋转——等待锁重新可用。要是锁未被争用,请求它的内核任务便能立刻得到它并且继续进行。自旋锁可以在任何时刻防止多于一个的内核任务同时进入临界区,因此这种锁可有效地避免多处理器上并发运行的内核任务竞争共享资源。

自旋锁的基本形式如下:
spin_lock(&mr_lock);
//临界区
spin_unlock(&mr_lock);

信号量
Linux中的信号量是一种睡眠锁。如果有一个任务试图获得一个已被持有的信号量时,信号量会将其推入等待队列,然后让其睡眠。这时处理器获得自由去执行其它代码。当持有信号量的进程将信号量释放后,在等待队列中的一个任务将被唤醒,从而便可以获得这个信号量。
信号量的睡眠特性,使得信号量适用于锁会被长时间持有的情况;只能在进程上下文中使用,因为中断上下文中是不能被调度的;另外当代码持有信号量时,不可以再持有自旋锁。
信号量基本使用形式为:

static DECLARE_MUTEX(mr_sem);//声明互斥信号量
if(down_interruptible(&mr_sem))
//可被中断的睡眠,当信号来到,睡眠的任务被唤醒
//临界区
up(&mr_sem);

信号量和自旋锁区别
从严格意义上说,信号量和自旋锁属于不同层次的互斥手段,前者的实现有赖于后者,在信号量本身的实现上,为了保证信号量结构存取的原子性,在多CPU中需要自旋锁来互斥。
信号量是进程级的。用于多个进程之间对资源的互斥,虽然也是在内核中,但是该内核执行路径是以进程的身份,代表进程来争夺进程。鉴于进程上下文切换的开销也很大,因此,只有当进程占用资源时间比较长时,用信号量才是较好的选择。
当所要保护的临界区访问时间比较短时,用自旋锁是非常方便的,因为它节省上下文切换的时间,但是CPU得不到自旋锁会在那里空转直到执行单元锁为止,所以要求锁不能在临界区里长时间停留,否则会降低系统的效率
由此,可以总结出自旋锁和信号量选用的3个原则:
1:当锁不能获取到时,使用信号量的开销就是进程上线文切换的时间Tc,使用自旋锁的开销就是等待自旋锁(由临界区执行的时间决定)Ts,如果Ts比较小时,应使用自旋锁比较好,如果Ts比较大,应使用信号量。
2:信号量所保护的临界区可包含可能引起阻塞的代码,而自旋锁绝对要避免用来保护包含这样的代码的临界区,因为阻塞意味着要进行进程间的切换,如果进程被切换出去后,另一个进程企图获取本自旋锁,死锁就会发生。
3:信号量存在于进程上下文,因此,如果被保护的共享资源需要在中断或软中断情况下使用,则在信号量和自旋锁之间只能选择自旋锁,当然,如果一定要是要那个信号量,则只能通过down_trylock()方式进行,不能获得就立即返回以避免阻塞
自旋锁VS信号量
需求建议的加锁方法
低开销加锁优先使用自旋锁
短期锁定优先使用自旋锁
长期加锁优先使用信号量
中断上下文中加锁使用自旋锁
持有锁是需要睡眠、调度使用信号量

20, 阻塞与非阻塞I/O

一个驱动当它无法立刻满足请求应当如何响应?一个对 read 的调用可能当没有数据时到来,而以后会期待更多的数据;或者一个进程可能试图写,但是你的设备没有准备好接受数据,因为你的输出缓冲满了。调用进程往往不关心这种问题,程序员只希望调用 read 或 write 并且使调用返回,在必要的工作已完成后,你的驱动应当(缺省地)阻塞进程,使它进入睡眠直到请求可继续。
阻塞操作是指在执行设备操作时若不能获得资源则挂起进程,直到满足可操作的条件后再进行操作。
一个典型的能同时处理阻塞与非阻塞的globalfifo读函数如下:

/*globalfifo读函数*/
static ssize_t globalfifo_read(struct file *filp, char __user *buf, size_t count,
    loff_t *ppos)
{
    int ret;
    struct globalfifo_dev *dev = filp->private_data;
    DECLARE_WAITQUEUE(wait, current);

    down(&dev->sem); /* 获得信号量 */
    add_wait_queue(&dev->r_wait, &wait); /* 进入读等待队列头 */

    /* 等待FIFO非空 */
    if (dev->current_len == 0) {
        if (filp->f_flags &O_NONBLOCK) {
            ret = - EAGAIN;
            goto out;
        }
        __set_current_state(TASK_INTERRUPTIBLE); /* 改变进程状态为睡眠 */
        up(&dev->sem);

        schedule(); /* 调度其他进程执行 */
        if (signal_pending(current)) {
            /* 如果是因为信号唤醒 */
            ret = - ERESTARTSYS;
            goto out2;
        }

        down(&dev->sem);
    }

    /* 拷贝到用户空间 */
    if (count > dev->current_len)
        count = dev->current_len;

    if (copy_to_user(buf, dev->mem, count)) {
        ret = - EFAULT;
        goto out;
    } else {
        memcpy(dev->mem, dev->mem + count, dev->current_len - count); /* fifo数据前移 */
        dev->current_len -= count; /* 有效数据长度减少 */
        printk(KERN_INFO "read %d bytes(s),current_len:%d\n", count, dev->current_len);

        wake_up_interruptible(&dev->w_wait); /* 唤醒写等待队列 */

        ret = count;
    }
out:
    up(&dev->sem); /* 释放信号量 */
out2:
    remove_wait_queue(&dev->w_wait, &wait); /* 从附属的等待队列头移除 */
    set_current_state(TASK_RUNNING);
    return ret;
}

21, poll方法

使用非阻塞I/O的应用程序通常会使用select()poll()系统调用查询是否可对设备进行无阻塞的访问。select()poll()系统调用最终会引发设备驱动中的poll()函数被执行。
这个方法由下列的原型:

unsigned int (*poll) (struct file *filp, poll_table *wait);

这个驱动方法被调用, 无论何时用户空间程序进行一个 poll, select, 或者 epoll 系统调用, 涉及一个和驱动相关的文件描述符. 这个设备方法负责这 2 步:

  1. 对可能引起设备文件状态变化的等待队列,调用poll_wait()函数,将对应的等待队列头添加到poll_table.
  2. 返回一个位掩码, 描述可能不必阻塞就立刻进行的操作.

poll_table结构, 给 poll 方法的第 2 个参数, 在内核中用来实现 poll, select, 和 epoll 调用; 它在 中声明, 这个文件必须被驱动源码包含. 驱动编写者不必要知道所有它内容并且必须作为一个不透明的对象使用它; 它被传递给驱动方法以便驱动可用每个能唤醒进程的等待队列来加载它, 并且可改变 poll 操作状态. 驱动增加一个等待队列到poll_table结构通过调用函数 poll_wait:

void poll_wait (struct file *, wait_queue_head_t *, poll_table *);

poll 方法的第 2 个任务是返回位掩码, 它描述哪个操作可马上被实现; 这也是直接的. 例如, 如果设备有数据可用, 一个读可能不必睡眠而完成; poll 方法应当指示这个时间状态. 几个标志(通过 定义)用来指示可能的操作:
POLLIN:如果设备可被不阻塞地读, 这个位必须设置.
POLLRDNORM:这个位必须设置, 如果”正常”数据可用来读. 一个可读的设备返回( POLLIN|POLLRDNORM ).
POLLOUT:这个位在返回值中设置, 如果设备可被写入而不阻塞.
……
poll的一个典型模板如下:

static unsigned int globalfifo_poll(struct file *filp, poll_table *wait)
{
    unsigned int mask = 0;
    struct globalfifo_dev *dev = filp->private_data; /*获得设备结构体指针*/

    down(&dev->sem);

    poll_wait(filp, &dev->r_wait, wait);
    poll_wait(filp, &dev->w_wait, wait);
    /*fifo非空*/
    if (dev->current_len != 0) {
        mask |= POLLIN | POLLRDNORM; /*标示数据可获得*/
    }
    /*fifo非满*/
    if (dev->current_len != GLOBALFIFO_SIZE) {
        mask |= POLLOUT | POLLWRNORM; /*标示数据可写入*/
    }

    up(&dev->sem);
    return mask;
}

应用程序如何去使用这个poll呢?一般用select()来实现,其原型为:

int select(int numfds, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, struct timeval *timeout);

其中,readfds, writefds, exceptfds,分别是被select()监视的读、写和异常处理的文件描述符集合。numfds是需要检查的号码最高的文件描述符加1。
以下是一个具体的例子:

/*======================================================================
    A test program in userspace 
    This example is to introduce the ways to use "select"
     and driver poll 

    The initial developer of the original code is Baohua Song
    <author@linuxdriver.cn>. All Rights Reserved.
======================================================================*/
#include <sys/types.h>
#include <sys/stat.h>
#include <stdio.h>
#include <fcntl.h>
#include <unistd.h>
#include <sys/time.h>

#define FIFO_CLEAR 0x1
#define BUFFER_LEN 20
main()
{
  int fd, num;
  char rd_ch[BUFFER_LEN];
  fd_set rfds,wfds;

  /*以非阻塞方式打开/dev/globalmem设备文件*/
  fd = open("/dev/globalfifo", O_RDONLY | O_NONBLOCK);
  if (fd != - 1)
  {
    /*FIFO清0*/
    if (ioctl(fd, FIFO_CLEAR, 0) < 0)
    {
      printf("ioctl command failed\n");
    }
    while (1)
    {
      FD_ZERO(&rfds);// 清除一个文件描述符集rfds
      FD_ZERO(&wfds); 
      FD_SET(fd, &rfds);// 将一个文件描述符fd,加入到文件描述符集rfds中
      FD_SET(fd, &wfds);

      select(fd + 1, &rfds, &wfds, NULL, NULL);
      /*数据可获得*/
      if (FD_ISSET(fd, &rfds)) //判断文件描述符fd是否被置位
      {
          printf("Poll monitor:can be read\n");
      }
      /*数据可写入*/
      if (FD_ISSET(fd, &wfds))
      {
          printf("Poll monitor:can be written\n");
      } 
    }
  }
  else
  {
    printf("Device open failure\n");
  }
}

其中:
FD_ZERO(fd_set *set); //清除一个文件描述符集set
FD_SET(int fd, fd_set *set); //将一个文件描述符fd,加入到文件描述符集set中
FD_CLEAR(int fd, fd_set *set); //将一个文件描述符fd,从文件描述符集set中清除
FD_ISSET(int fd, fd_set *set); //判断文件描述符fd是否被置位。

22,并发与竞态介绍

Linux设备驱动中必须解决一个问题是多个进程对共享资源的并发访问,并发的访问会导致竞态,在当今的Linux内核中,支持SMP与内核抢占的环境下,更是充满了并发与竞态。幸运的是,Linux 提供了多钟解决竞态问题的方式,这些方式适合不同的应用场景。例如:中断屏蔽、原子操作、自旋锁、信号量等等并发控制机制。
并发与竞态的概念
并发是指多个执行单元同时、并发被执行,而并发的执行单元对共享资源(硬件资源和软件上的全局变量、静态变量等)的访问则很容易导致竞态。
临界区概念是为解决竞态条件问题而产生的,一个临界区是一个不允许多路访问的受保护的代码,这段代码可以操纵共享数据或共享服务。临界区操纵坚持互斥锁原则(当一个线程处于临界区中,其他所有线程都不能进入临界区)。然而,临界区中需要解决的一个问题是死锁。

23, 中断屏蔽

在单CPU 范围内避免竞态的一种简单而省事的方法是进入临界区之前屏蔽系统的中断。CPU 一般都具有屏蔽中断和打开中断的功能,这个功能可以保证正在执行的内核执行路径不被中断处理程序所抢占,有效的防止了某些竞态条件的发送,总之,中断屏蔽将使得中断与进程之间的并发不再发生。
中断屏蔽的使用方法:

local_irq_disable() /屏蔽本地CPU 中断/ 
….. 
critical section /临界区受保护的数据/ 
….. 
local_irq_enable() /打开本地CPU 中断/ 

由于Linux 的异步I/O、进程调度等很多重要操作都依赖于中断,中断对内核的运行非常重要,在屏蔽中断期间的所有中断都无法得到处理,因此长时间屏蔽中断是非常危险的,有可能造成数据的丢失,甚至系统崩溃的后果。这就要求在屏蔽了中断后,当前的内核执行路径要尽快地执行完临界区代码。
local_irq_disable()不同的是,local_irq_save(flags)除了进行禁止中断的操作外,还保存当前CPU 的中断状态位信息;与local_irq_enable()不同的是,local_irq_restore(flags) 除了打开中断的操作外,还恢复了CPU 被打断前的中断状态位信息。

24, 原子操作

原子操作指的是在执行过程中不会被别的代码路径所中断的操作,Linux 内核提供了两类原子操作——位原子操作和整型原子操作。它们的共同点是在任何情况下都是原子的,内核代码可以安全地调用它们而不被打断。然而,位和整型变量原子操作都依赖于底层CPU 的原子操作来实现,因此这些函数的实现都与 CPU 架构密切相关。
1 整型原子操作
1)、设置原子变量的值

void atomic_set(atomic v,int i); /设置原子变量的值为 i */ 
atomic_t v = ATOMIC_INIT(0); /定义原子变量 v 并初始化为 0 / 

2)、获取原子变量的值

int atomic_read(atomic_t v) /返回原子变量 v 的当前值*/ 

3)、原子变量加/减

void atomic_add(int i,atomic_t v) /原子变量增加 i */ 
void atomic_sub(int i,atomic_t v) /原子变量减少 i */ 

4)、原子变量自增/自减

void atomic_inc(atomic_t v) /原子变量增加 1 */ 
void atomic_dec(atomic_t v) /原子变量减少 1 */ 

5)、操作并测试

int atomic_inc_and_test(atomic_t *v); 
int atomic_dec_and_test(atomic_t *v); 
int atomic_sub_and_test(int i, atomic_t *v); 

上述操作对原子变量执行自增、自减和减操作后测试其是否为 0 ,若为 0 返回true,否则返回false。注意:没有atomic_add_and_test(int i, atomic_t *v)
6)、操作并返回

int atomic_add_return(int i, atomic_t *v); 
int atomic_sub_return(int i, atomic_t *v); 
int atomic_inc_return(atomic_t *v); 
int atomic_dec_return(atomic_t *v); 

上述操作对原子变量进行加/减和自增/自减操作,并返回新的值。
2 位原子操作
1)、设置位

void set_bit(nr,void addr);/设置addr 指向的数据项的第 nr 位为1 */ 

2)、清除位

void clear_bit(nr,void addr)/设置addr 指向的数据项的第 nr 位为0 */ 

3)、取反位

void change_bit(nr,void addr); /对addr 指向的数据项的第 nr 位取反操作*/ 

4)、测试位

test_bit(nr,void addr);/返回addr 指向的数据项的第 nr位*/ 

5)、测试并操作位

int test_and_set_bit(nr, void *addr); 
int test_and_clear_bit(nr,void *addr); 
int test_amd_change_bit(nr,void *addr); 

25, 自旋锁

自旋锁(spin lock)是一种典型的对临界资源进行互斥访问的手段。为了获得一个自旋锁,在某CPU 上运行的代码需先执行一个原子操作,该操作测试并设置某个内存变量,由于它是原子操作,所以在该操作完成之前其他执行单元不能访问这个内存变量。如果测试结果表明锁已经空闲,则程序获得这个自旋锁并继续执行;如果测试结果表明锁仍被占用,则程序将在一个小的循环里面重复这个“测试并设置” 操作,即进行所谓的“自旋”。
理解自旋锁最简单的方法是把它当做一个变量看待,该变量把一个临界区标记为“我在这运行了,你们都稍等一会”,或者标记为“我当前不在运行,可以被使用”。
Linux中与自旋锁相关操作有:
1)、定义自旋锁

spinlock_t my_lock; 

2)、初始化自旋锁

spinlock_t my_lock = SPIN_LOCK_UNLOCKED; /静态初始化自旋锁/ 
void spin_lock_init(spinlock_t lock); /动态初始化自旋锁*/ 

3)、获取自旋锁

/若获得锁立刻返回真,否则自旋在那里直到该锁保持者释放/ 
void spin_lock(spinlock_t *lock); 
/若获得锁立刻返回真,否则立刻返回假,并不会自旋等待/ 
void spin_trylock(spinlock_t *lock) 

4)、释放自旋锁

void spin_unlock(spinlock_t *lock) 

自旋锁的一般用法:

spinlock_t lock; /定义一个自旋锁/ 
spin_lock_init(&lock); /动态初始化一个自旋锁/ 
…… 
spin_lock(&lock); /获取自旋锁,保护临界区/ 
……./临界区/ 
spin_unlock(&lock); /解锁/ 

自旋锁主要针对SMP 或单CPU 但内核可抢占的情况,对于单CPU 且内核不支持抢占的系统,自旋锁退化为空操作。尽管用了自旋锁可以保证临界区不受别的CPU和本地CPU内的抢占进程打扰,但是得到锁的代码路径在执行临界区的时候,还可能受到中断和底半部(BH)的影响,为了防止这种影响,就需要用到自旋锁的衍生。
获取自旋锁的衍生函数:

void spin_lock_irq(spinlock_t lock); /获取自旋锁之前禁止中断*/ 
void spin_lock_irqsave(spinlock_t lock, unsigned long flags);/获取自旋锁之前禁止中断,并且将先前的中断状态保存在flags 中*/ 
void spin_lock_bh(spinlock_t lock); /在获取锁之前禁止软中断,但不禁止硬件中断*/ 

释放自旋锁的衍生函数:

void spin_unlock_irq(spinlock_t *lock) 
void spin_unlock_irqrestore(spinlock_t *lock,unsigned long flags); 
void spin_unlock_bh(spinlock_t *lock); 

解锁的时候注意要一一对应去解锁。
自旋锁注意点:
(1)自旋锁实际上是忙等待,因此,只有占用锁的时间极短的情况下,使用自旋锁才是合理的。
(2)自旋锁可能导致系统死锁。
(3)自旋锁锁定期间不能调用可能引起调度的函数。如:copy_from_user()、copy_to_user()、kmalloc()、msleep()等函数。
(4)拥有自旋锁的代码是不能休眠的。

26, 读写自旋锁

它允许多个读进程并发执行,但是只允许一个写进程执行临界区代码,而且读写也是不能同时进行的。
1)、定义和初始化读写自旋锁

rwlock_t my_rwlock = RW_LOCK_UNLOCKED; /* 静态初始化 */ 
rwlock_t my_rwlock; 
rwlock_init(&my_rwlock); /* 动态初始化 */ 

2)、读锁定

void read_lock(rwlock_t *lock); 
void read_lock_irqsave(rwlock_t *lock, unsigned long flags); 
void read_lock_irq(rwlock_t *lock); 
void read_lock_bh(rwlock_t *lock); 

3)、读解锁

void read_unlock(rwlock_t *lock); 
void read_unlock_irqrestore(rwlock_t *lock, unsigned long flags); 
void read_unlock_irq(rwlock_t *lock); 
void read_unlock_bh(rwlock_t *lock); 

在对共享资源进行读取之前,应该先调用读锁定函数,完成之后调用读解锁函数。
4)、写锁定

void write_lock(rwlock_t *lock); 
void write_lock_irqsave(rwlock_t *lock, unsigned long flags); 
void write_lock_irq(rwlock_t *lock); 
void write_lock_bh(rwlock_t *lock); 
void write_trylock(rwlock_t *lock); 

5)、写解锁

void write_unlock(rwlock_t *lock); 
void write_unlock_irqrestore(rwlock_t *lock, unsigned long flags); 
void write_unlock_irq(rwlock_t *lock); 
void write_unlock_bh(rwlock_t *lock); 

在对共享资源进行写之前,应该先调用写锁定函数,完成之后应调用写解锁函数。

读写自旋锁的一般用法:

rwlock_t lock; /定义一个读写自旋锁 rwlock/ 
rwlock_init(&lock); /初始化/ 
read_lock(&lock); /读取前先获取锁/ 
…../临界区资源/ 
read_unlock(&lock); /读完后解锁/ 
write_lock_irqsave(&lock, flags); /写前先获取锁/ 
…../临界区资源/ 
write_unlock_irqrestore(&lock,flags); /写完后解锁/ 

27, 顺序锁(sequence lock)

顺序锁是对读写锁的一种优化,读执行单元在写执行单元对被顺序锁保护的资源进行写操作时仍然可以继续读,而不必等地写执行单元完成写操作,写执行单元也不必等待所有读执行单元完成读操作才进去写操作。但是,写执行单元与写执行单元依然是互斥的。并且,在读执行单元读操作期间,写执行单元已经发生了写操作,那么读执行单元必须进行重读操作,以便确保读取的数据是完整的,这种锁对于读写同时进行概率比较小的情况,性能是非常好的。
顺序锁有个限制,它必须要求被保护的共享资源不包含有指针,因为写执行单元可能使得指针失效,但读执行单元如果正要访问该指针,就会导致oops。
1)、初始化顺序锁

seqlock_t lock1 = SEQLOCK_UNLOCKED; /静态初始化/ 
seqlock lock2; /动态初始化/ 
seqlock_init(&lock2) 

2)、获取顺序锁

void write_seqlock(seqlock_t *s1); 
void write_seqlock_irqsave(seqlock_t *lock, unsigned long flags) 
void write_seqlock_irq(seqlock_t *lock); 
void write_seqlock_bh(seqlock_t *lock); 
int write_tryseqlock(seqlock_t *s1); 

3)、释放顺序锁

void write_sequnlock(seqlock_t *s1); 
void write_sequnlock_irqsave(seqlock_t *lock, unsigned long flags) 
void write_sequnlock_irq(seqlock_t *lock); 
void write_sequnlock_bh(seqlock_t *lock); 

写执行单元使用顺序锁的模式如下:

write_seqlock(&seqlock_a); 
/写操作代码/ 
…….. 
write_sequnlock(&seqlock_a); 

4)、读开始

unsigned read_seqbegin(const seqlock_t *s1); 
unsigned read_seqbegin_irqsave(seqlock_t *lock, unsigned long flags); 

5)、重读

int read_seqretry(const seqlock_t *s1, unsigned iv); 
int read_seqretry_irqrestore(seqlock_t *lock,unsigned int seq,unsigned long flags); 

读执行单元使用顺序锁的模式如下:

unsigned int seq; 
do{ 
seq = read_seqbegin(&seqlock_a); 
/读操作代码/ 
……. 
}while (read_seqretry(&seqlock_a, seq)); 

28, 信号量

信号量的使用
信号量(semaphore)是用于保护临界区的一种最常用的办法,它的使用方法与自旋锁是类似的,但是,与自旋锁不同的是,当获取不到信号量的时候,进程不会自旋而是进入睡眠的等待状态。
1)、定义信号量

struct semaphore sem; 

2)、初始化信号量

void sema_init(struct semaphore sem, int val); /初始化信号量的值为 val */ 

更常用的是下面这二个宏:

#define init_MUTEX(sem) sema_init(sem, 1) 
#define init_MUTEX_LOCKED(sem) sem_init(sem, 0) 

然而,下面这两个宏是定义并初始化信号量的“快捷方式”

DECLARE_MUTEX(name) /一个称为name信号量变量被初始化为 1 / 
DECLARE_MUTEX_LOCKED(name) /一个称为name信号量变量被初始化为 0 / 

3)、获得信号量

/该函数用于获取信号量,若获取不成功则进入不可中断的睡眠状态/ 
void down(struct semaphore *sem); 
/该函数用于获取信号量,若获取不成功则进入可中断的睡眠状态/ 
void down_interruptible(struct semaphore *sem); 
/该函数用于获取信号量,若获取不成功立刻返回 -EBUSY/ 
int down_trylock(struct sempahore *sem); 

4)、释放信号量

void up(struct semaphore sem); /释放信号量 sem ,并唤醒等待者*/ 

信号量的一般用法:

DECLARE_MUTEX(mount_sem); /定义一个信号量mount_sem,并初始化为 1 / 
down(&mount_sem); /* 获取信号量,保护临界区*/ 
….. 
critical section /临界区/ 
….. 
up(&mount_sem); /释放信号量/ 

29, 读写信号量

读写信号量可能引起进程阻塞,但是它允许多个读执行单元同时访问共享资源,但最多只能有一个写执行单元。
1)、定义和初始化读写信号量

struct rw_semaphore my_rws; /定义读写信号量/ 
void init_rwsem(struct rw_semaphore sem); /初始化读写信号量*/ 

2)、读信号量获取

void down_read(struct rw_semaphore *sem); 
int down_read_trylock(struct rw_semaphore *sem); 

3)、读信号量释放

void up_read(struct rw_semaphore *sem); 

4)、写信号量获取

void down_write(struct rw_semaphore *sem); 
int down_write_trylock(struct rw_semaphore *sem); 

5)、写信号量释放

void up_write(struct rw_semaphore *sem); 

30, completion

完成量(completion)用于一个执行单元等待另外一个执行单元执行完某事。
1)、定义完成量

struct completion my_completion; 

2)、初始化完成量

init_completion(&my_completion); 

3)、定义并初始化的“快捷方式”

DECLARE_COMPLETION(my_completion) 

4)、等待完成量

void wait_for_completion(struct completion c); /等待一个 completion 被唤醒*/ 

5)、唤醒完成量

void complete(struct completion c); /只唤醒一个等待执行单元*/ 
void complete(struct completion c); /唤醒全部等待执行单元*/ 

31, 自旋锁VS信号量

信号量是进程级的,用于多个进程之间对资源的互斥,虽然也是在内核中,但是该内核执行路径是以进程的身份,代表进程来争夺资源的。如果竞争失败,会发送进程上下文切换,当前进程进入睡眠状态,CPU 将运行其他进程。鉴于开销比较大,只有当进程资源时间较长时,选用信号量才是比较合适的选择。然而,当所要保护的临界区访问时间比较短时,用自旋锁是比较方便的。
总结:
解决并发与竞态的方法有(按本文顺序):
(1)中断屏蔽
(2)原子操作(包括位和整型原子)
(3)自旋锁
(4)读写自旋锁
(5)顺序锁(读写自旋锁的进化)
(6)信号量
(7)读写信号量
(8)完成量
其中,中断屏蔽很少单独被使用,原子操作只能针对整数进行,因此自旋锁和信号量应用最为广泛。自旋锁会导致死循环,锁定期间内不允许阻塞,因此要求锁定的临界区小;信号量允许临界区阻塞,可以适用于临界区大的情况。读写自旋锁和读写信号量分别是放宽了条件的自旋锁 信号量,它们允许多个执行单元对共享资源的并发读。

2016-10-31 00:04:31 changliang7731 阅读数 307
  • linux设备驱动模型-linux驱动开发第5部分

    本课程是linux驱动开发的第5个课程,主要内容是linux的设备驱动模型,包括总线、类、设备、驱动等概念,重点通过platform平台总线的工作来演示设备驱动模型的工作方法,实践环节对上个课程的LED驱动进行平台总线式改造,终目标是让大家彻底掌握linux的总线式设备驱动模型。

    4821 人正在学习 去看看 朱有鹏

平台总线和IIC,SPI,IIS都是总线类型,一般的,总线下,挂载对应的设备。但实际上,设备要正常运转,是需要驱动程序来未知提供驱动的。所以linux内核也把驱动挂载在对应的总线下。总线,驱动,设备三者缺一不可.
相应的,内核衍生出来的平台总线,那么便衍生出来了平台设备和凭条驱动。他们均有自己的专属函数来注册,注销。这个是成一套体系结构的.在以后的驱动开发中,很是常见与重要.

一)内核中的总线,设备,驱动
1)总线

struct bus_type {
    const char      *name;
    const char      *dev_name;
    struct device       *dev_root;
    struct bus_attribute    *bus_attrs;//总线属性
    struct device_attribute *dev_attrs;//设备属性
    struct driver_attribute *drv_attrs;//驱动属性

    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 (*suspend)(struct device *dev, pm_message_t state);
    int (*resume)(struct device *dev);

    const struct dev_pm_ops *pm;//电源管理

    struct iommu_ops *iommu_ops;

    struct subsys_private *p;//将bus device sysfs联系起来
    struct lock_class_key lock_key;
};

注册:bus_register(struct bus_type *bus);
注销:bus_unregister(struct bus_type *bus);

2)设备

struct device {
    struct device       *parent;

    struct device_private   *p;

    struct kobject kobj;
    const char      *init_name; /* initial name of the device */
    const struct device_type *type;

    struct mutex        mutex;  /* mutex to synchronize calls to
                     * its driver.
                     */

    struct bus_type *bus;       /* type of bus device is on  设备所属的总线*/
    struct device_driver *driver;   /* which driver has allocated this
                       device */
    void        *platform_data; /* Platform specific data, device
                       core doesn't touch it */
    struct dev_pm_info  power;
    struct dev_pm_domain    *pm_domain;

#ifdef CONFIG_PINCTRL
    struct dev_pin_info *pins;
#endif

#ifdef CONFIG_NUMA
    int     numa_node;  /* NUMA node this device is close to */
#endif
    u64     *dma_mask;  /* dma mask (if dma'able device) */
    u64     coherent_dma_mask;/* Like dma_mask, but for
                         alloc_coherent mappings as
                         not all hardware supports
                         64 bit addresses for consistent
                         allocations such descriptors. */

    struct device_dma_parameters *dma_parms;

    struct list_head    dma_pools;  /* dma pools (if dma'ble) */

    struct dma_coherent_mem *dma_mem; /* internal for coherent mem
                         override */
#ifdef CONFIG_CMA
    struct cma *cma_area;       /* contiguous memory area for dma
                       allocations */
#endif
    /* arch specific additions */
    struct dev_archdata archdata;

    struct device_node  *of_node; /* associated device tree node */
    struct acpi_dev_node    acpi_node; /* associated ACPI device node */

    dev_t           devt;   /* dev_t, creates the sysfs "dev" */
    u32         id; /* device instance */

    spinlock_t      devres_lock;
    struct list_head    devres_head;

    struct klist_node   knode_class;
    struct class        *class;
    const struct attribute_group **groups;  /* optional groups */

    void    (*release)(struct device *dev);
    struct iommu_group  *iommu_group;
};

设备注册:int device_register(struct device *dev)
设备注销:void device_unregister(struct device *dev)

3)设备驱动

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 */

    const struct of_device_id   *of_match_table;
    const struct acpi_device_id *acpi_match_table;

    int (*probe) (struct device *dev);// 设备match到驱动,则调用对应驱动的probe函数,驱动设备
    int (*remove) (struct device *dev);//当驱动或者设备被卸载,则调用对应驱动的remove函数
    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;

    struct driver_private *p;
};

驱动注册:int driver_register(struct device_driver *drv)
驱动注销:void driver_unregister(struct device_driver *drv)

二)平台设备总线
总线:

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

设备:

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;

    /* MFD cell pointer */
    struct mfd_cell *mfd_cell;

    /* arch specific additions */
    struct pdev_archdata    archdata;
};
平台设备的注册:platform_device_register(struct platform_device * pdev)
平台设备的注销:platform_device_unregister(struct platform_device * pdev)

驱动:

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;
};
平台驱动的注册:int platform_driver_register(struct platform_driver *drv)
平台驱动的注销:void platform_driver_unregister(struct platform_driver *drv)

平台设备匹配的流程:

先插入一个函数,match

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);

    /* 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);
}
//match 的方式:不同的内核,可能有所不同:我的内核版本:3.10.46

1.OF style match first
2.ACPI style match
3.match against the id table
4.driver name match

大概的匹配流程:
1)平台设备注册流程:

platform_device_register()
{
--->platform_device_add()
    {
        --->device_add()
        {
            --->device_add()
            {
                --->bus_probe_device()
                {

                    --->device_attach()
                    {
                        --->bus_for_each_drv(dev->bus, NULL, dev, __device_attach)
                        {
                            --->__device_attach()
                            {
                                --->driver_match_device()//platform_match()
                                //当match到驱动后,继续下面步骤:
                                --->driver_probe_device()
                                {
                                    --->really_probe()
                                    {
                                        --->drv->probe()//调用对应驱动的probe函数
                                    }

                                }

                            }
                        }
                    }


                }
            }
        }
    }

}

2)平台驱动注册流程:

platform_driver_register()
{
    --->driver_register()
    {
        --->bus_add_driver()
        {
            --->driver_attach()
            {
                --->bus_for_each_dev(drv->bus, NULL, drv, __driver_attach)
                {
                    --->__driver_attach()
                    {
                        --->driver_match_device()//platform_match()
                        //当match到对应的设备后,继续
                        --->driver_probe_device()
                        {
                            --->really_probe()
                            {
                                --->drv->probe()//调用当前注册驱动的probe函数
                            }
                        }
                    }
                }
            }
        }
    }
}

这里写图片描述

1.设备向对应平台bus注册设备,设备挂载到平台bus下面的dev list链表中.
2.驱动向对应平台bus注册驱动,驱动挂载到平台bus下面的driver list链表中.
3.设备和对应总线下的driver list链表中每个driver进行匹配(match).匹配成功则调用对应driver的probe函数,否则匹配失败,则设备无法被驱动,设备无法工作.

match函数已经在上面列出。它有自己的匹配规则,上一篇的实例是按照match name来进行匹配的.
当设备或者驱动被卸载时,驱动的remove函数都会被调用.

平台设备总线的意义:移植性强。通过我们是不需要写平台设备文件的,只需要写对应的驱动文件。而驱动文件一般移植时并不需要修改。只需要修改对应的platform_data 平台数据和resource 平台资源列表便可.
这样修改一个驱动或者移植一个驱动到另一个内核便变得很简单。

2014-07-31 20:24:42 mcuwlxlong 阅读数 1268
  • linux设备驱动模型-linux驱动开发第5部分

    本课程是linux驱动开发的第5个课程,主要内容是linux的设备驱动模型,包括总线、类、设备、驱动等概念,重点通过platform平台总线的工作来演示设备驱动模型的工作方法,实践环节对上个课程的LED驱动进行平台总线式改造,终目标是让大家彻底掌握linux的总线式设备驱动模型。

    4821 人正在学习 去看看 朱有鹏

本文摘自本人拙著 《嵌入式Linux驱动模板精讲与项目实践》

初步看起来Linux设备驱动开发涉及内容很多,而需要实现驱动的设备千差万别,其实做一段时间驱动之后回首看来主要就是以下几点:

1)对驱动进行分类,先归纳为哪个类型的驱动,归类正确再利用内核提供的子系统进行开发,往往会发现其实很多通用的事情内核已经帮我们做了,一个优秀的驱动工程师应该最大程度上利用内核的资源。内核已经实现的毕竟稳定性强,可移植性高。

2)找到内核的提供的子系统,接下来就是要制作该子系统对该类设备提供的表征,也就是描述该类驱动的结构体,然后定义这个结构体把必要的数据进行初始化,最后调用该内核子系统提供的接口函数提交给内核管理。这是大部分驱动程序开发的战略流程。

3)明确子系统已经做了什么,需要在自己驱动中实现哪些内容,通常做法是找一个接近的驱动程序进行修改,而不是一行一行代码进行编写。到内核中找接近的驱动例程是一个很好很快的方法。这些例程基本上提供接口如何使用,调用流程等等。

4)以上基本上都是与内核接口有关,驱动第二个涉及到的就是芯片手册,这个与做其他嵌入式软件一致,故从单片机软件开发或者其他操作系统软件开发转过来做Linux驱动开发的人员来说,这部分是一个强项。

5)驱动另外一个内容就是协议,各种嵌入式总线协议从简单的SPI到复杂的PCI或者USB等等。协议基本知识是需要掌握的,好在内核对各种常见协议都是以子系统的形式提供,在子系统中做了大部分共性的工作,大大降低了驱动开发的工作量。

    故学好驱动开发,一个重要的方面就是对内核的学习,熟悉内核的组织和思维方式,所以对其思想做个总结是非常有必要的。

2017-08-26 11:42:27 qq_23084801 阅读数 442
  • linux设备驱动模型-linux驱动开发第5部分

    本课程是linux驱动开发的第5个课程,主要内容是linux的设备驱动模型,包括总线、类、设备、驱动等概念,重点通过platform平台总线的工作来演示设备驱动模型的工作方法,实践环节对上个课程的LED驱动进行平台总线式改造,终目标是让大家彻底掌握linux的总线式设备驱动模型。

    4821 人正在学习 去看看 朱有鹏

1、软件系统分为:应用程序、库、操作系统(内核)、驱动程序,开发人员专注某一层,了解邻层的接口。如,应用程序调用库函数open,库根据open传入的参数执行swi指令引起CPU异常进入内核。内核的异常处理函数根据参数找到相应驱动程序。内核与驱动程序没有界限,因为驱动程序最终是要编进内核。驱动程序从不主动运行。在有MMU的系统中,应用程序处于用户空间,驱动程序处于内核空间。

2、Linux外设分为:字符设备(读写以字节方式进行)、块设备(数据读写以块方式,数据有格式)、网络接口(数据读写是大小不固定的块)。

3、Linux设备驱动程序开发步骤
(1)初始化驱动程序,如向内核注册这个驱动程序,这样应用程序传入文件名时,内核才能找到相应的驱动程序。
(2)设计要实现的操作函数,如open等。
(3)实现中断服务
(4)编译驱动程序到内核,如果动态编译为模块,则用insmod rmmod命令进行加载和卸载。加载:调用模块的初始化函数,向内核注册驱动程序。卸载:调用模块清除函数。
(5)测试

4、应用程序使用统一的接口函数(系统调用)调用硬件启动程序。字符设备驱动程序的函数集合在 include/linux/fs.h 的file_operations 结构中

//为驱动函数规定统一以文件操作的接口,如open、read ....
//当应用程序使用open函数打开某个设备,就会调用 file_operations 中的open函数。从这个角度,编写字符设备驱动程序就是为具体硬件的file_operations 结构编写需要的函数。
struct file_operations {
    struct module *owner;
    loff_t (*llseek) (struct file *, loff_t, int);
    ssize_t (*read) (struct file *, char __user *, size_t, loff_t *);
    ssize_t (*write) (struct file *, const char __user *, size_t, loff_t *);
    ssize_t (*aio_read) (struct kiocb *, const struct iovec *, unsigned long, loff_t);
    ssize_t (*aio_write) (struct kiocb *, const struct iovec *, unsigned long, loff_t);
    int (*readdir) (struct file *, void *, filldir_t);
    unsigned int (*poll) (struct file *, struct poll_table_struct *);
    int (*ioctl) (struct inode *, struct file *, unsigned int, unsigned long);
    long (*unlocked_ioctl) (struct file *, unsigned int, unsigned long);
    long (*compat_ioctl) (struct file *, unsigned int, unsigned long);
    int (*mmap) (struct file *, struct vm_area_struct *);
    int (*open) (struct inode *, struct file *);
    int (*flush) (struct file *, fl_owner_t id);
    int (*release) (struct inode *, struct file *);
    int (*fsync) (struct file *, struct dentry *, int datasync);
    int (*aio_fsync) (struct kiocb *, int datasync);
    int (*fasync) (int, struct file *, int);
    int (*lock) (struct file *, int, struct file_lock *);
    ssize_t (*sendfile) (struct file *, loff_t *, size_t, read_actor_t, void *);
    ssize_t (*sendpage) (struct file *, struct page *, int, size_t, loff_t *, int);
    unsigned long (*get_unmapped_area)(struct file *, unsigned long, unsigned long, unsigned long, unsigned long);
    int (*check_flags)(int);
    int (*dir_notify)(struct file *filp, unsigned long arg);
    int (*flock) (struct file *, int, struct file_lock *);
    ssize_t (*splice_write)(struct pipe_inode_info *, struct file *, loff_t *, size_t, unsigned int);
    ssize_t (*splice_read)(struct file *, loff_t *, struct pipe_inode_info *, size_t, unsigned int);
};

5、安装驱动程序时调用初始化函数,把驱动出现的file_operations 结构和主设备号向内核注册。内核为字符设备保存一个数组,主设备号为数组标号。加载即在对于标号填入对应设备file_operations 地址,卸载相反。

//对于字符设备使用如下函数进行注册
int register_chrdev(unsigned int major, consr char *name, struct file_operations *fops);

//之后当应用程序操作设备时,Linux系统就会根据设备文件类型、主设备号找到file_operations 

简单的驱动程序编写(不涉及中断、select机制、fasync异步通知机制)
(1)编写驱动初始化函数
(2)构造file_operations 中成员函数

6、LED驱动程序分析
(1)初始化,指定加载和卸载函数

//执行 insmod s3c24xx_leds 就会调用该函数
static int __init s3c24xx_leds_init(void)
{
    int ret;

    /* 注册字符设备驱动程序
     * 参数为主设备号、设备名字、file_operations结构;
     * 这样,主设备号就和具体的file_operations结构联系起来了,
     * 操作主设备为LED_MAJOR的设备文件时,就会调用s3c24xx_leds_fops中的相关成员函数
     * LED_MAJOR可以设为0,表示由内核自动分配主设备号
     */
    ret = register_chrdev(LED_MAJOR, DEVICE_NAME, &s3c24xx_leds_fops);
    if (ret < 0) {
      printk(DEVICE_NAME " can't register major number\n");
      return ret;
    }

    printk(DEVICE_NAME " initialized\n");
    return 0;
}

 // 执行”rmmod s3c24xx_leds.ko”命令时就会调用这个函数 
static void __exit s3c24xx_leds_exit(void)
{
    /* 卸载驱动程序 */
    unregister_chrdev(LED_MAJOR, DEVICE_NAME);
}

/* 这两行指定驱动程序的初始化函数和卸载函数 */
//要是不适用这2行就要把加载和卸载函数名改为init_module和cleanup_module
module_init(s3c24xx_leds_init);
module_exit(s3c24xx_leds_exit);

(2)file_operations 结构

static struct file_operations s3c24xx_leds_fops = 
{
    .owner  =   THIS_MODULE,    /* 这是一个宏,推向编译模块时自动创建的__this_module变量 */
    .open   =   s3c24xx_leds_open,     
    .ioctl  =   s3c24xx_leds_ioctl,
};

(3)应用程序对设备文件/dev/leds执行open和ioclt时会调用的函数

// 应用程序对设备文件/dev/leds执行open(...)时,就会调用s3c24xx_leds_open函数
 //open即设置LED引脚为输出
static int s3c24xx_leds_open(struct inode *inode, struct file *file)
{
    int i; 
    for (i = 0; i < 4; i++) 
    {
        // 设置GPIO引脚的功能:本驱动中LED所涉及的GPIO引脚设为输出功能,该函数在内核中实现
        s3c2410_gpio_cfgpin(led_table[i],led_cfg_table[i]);
    }
    return 0;
}

/* 应用程序对设备文件/dev/leds执行ioclt(...)时,
 * 就会调用s3c24xx_leds_ioctl函数
 */
 //根据命令,实现LED的开和关,open返回inode和file给ioctl
static int s3c24xx_leds_ioctl(struct inode *inode, struct file *file, unsigned int cmd, unsigned long arg)
{
    if (arg > 4) {
        return -EINVAL;
    }

    switch(cmd) {
    case IOCTL_LED_ON:
        // 设置指定引脚的输出电平为0,该函数在内核中实现
        s3c2410_gpio_setpin(led_table[arg], 0);
        return 0;

    case IOCTL_LED_OFF:
        // 设置指定引脚的输出电平为1,该函数在内核中实现
        s3c2410_gpio_setpin(led_table[arg], 1);
        return 0;

    default:
        return -EINVAL;
    }
}

7、驱动编译,编译在PC上,PC上有未编译的linux目录
cat /proc/devices 查看已有设备
(1)在drivers\char下加入文件s3c24xx_leds.c;
(2)在drivers\char\Makefile中加一行

obj-m += s3c24xx_leds.o

(3)在内核根目录下执行make modules生成drivers\chars\s3c24xx_leds.ko
(4)把s3c24xx_leds.ko(以网络传输等方式)放到开发板文件系统/lib/modules/2.6.22.6/下
(5)这是就可以使用insmod s3c24xx_leds 和rmmod s3c24xx_leds命令进行加载和卸载。

8、测试程序(应用程序)
(1)在PC上编译生成可执行文件,把它放到开发板文件系统/user/bin目录下
(2)在开发板文件系统中建立设备文件

mknod /dev/leds c 231 0

(3)用命令测试

led_test 1 on
led_test 2 off

应用程序的open和ioctl等系统调用,他们的参数和驱动程序中相应函数的参数不是一一对应的,经过了内核文件层的转换。

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/ioctl.h>

#define IOCTL_LED_ON    0
#define IOCTL_LED_OFF   1

void usage(char *exename)
{
    printf("Usage:\n");
    printf("    %s <led_no> <on/off>\n", exename);
    printf("    led_no = 1, 2, 3 or 4\n");
}

int main(int argc, char **argv)
{
    unsigned int led_no;
    int fd = -1;

    if (argc != 3)
        goto err;

    fd = open("/dev/leds", 0);  // 打开设备,根据/dev/leds提取设备类型、主设备号,根据这些可以找到对应file_operations 
    if (fd < 0) {
        printf("Can't open /dev/leds\n");
        return -1;
    }

    led_no = strtoul(argv[1], 0, 0) - 1;    // 操作哪个LED?
    if (led_no > 3)
        goto err;

    if (!strcmp(argv[2], "on")) {
        ioctl(fd, IOCTL_LED_ON, led_no);    // 点亮它
    } else if (!strcmp(argv[2], "off")) {
        ioctl(fd, IOCTL_LED_OFF, led_no);   // 熄灭它
    } else {
        goto err;
    }

    close(fd);
    return 0;

err:
    if (fd > 0) 
        close(fd);
    usage(argv[0]);
    return -1;
}

9、Makefile,把开发版根文件挂载到PC,PC直接编译无需下载

KERN_DIR = /work/linux-2.6//内核目录

all:    
    make -C $(KERN_DIR ) M = 'pwd' module
    // -C表示到KERN_DIR 目录下执行Makefile
boj-m += s3c24xx_led.o
2016-11-03 12:40:17 changliang7731 阅读数 336
  • linux设备驱动模型-linux驱动开发第5部分

    本课程是linux驱动开发的第5个课程,主要内容是linux的设备驱动模型,包括总线、类、设备、驱动等概念,重点通过platform平台总线的工作来演示设备驱动模型的工作方法,实践环节对上个课程的LED驱动进行平台总线式改造,终目标是让大家彻底掌握linux的总线式设备驱动模型。

    4821 人正在学习 去看看 朱有鹏

IIC协议介绍:
作为一种应用很广的串行总线协议,这个是需要我们很认真去了解的。绝大多数的驱动开发中均少不了IIC驱动.
我手上的板子有一个eeprom和touch screen都是IIC接口。现在我们先来研究IIC协议以及ac24cxx的spec研读.
为下一章的驱动编写做准备.

这里我们拿24cxx的datasheet来说明.

IIC协议:由一个SDA信号和一个SCL信号组成.主控mcu作为主设备,其他外围器件作为从设备.所有操作的开始均是由主设备发起。
IIC有start,stop,ACK信号.

这里写图片描述
数据传输时:在每一个scl为高 level时,要求对应的sda 数据稳定不变,当scl的level为低时,允许sda的电平进行变化.

这里写图片描述
正因为这种特性,通常我们这样讲:IIC的数据传输,波形上直观的表现为:data数据一定要包住clk.

start:SCL为高电平时,SDA产生一个下降沿
stop:当SCL处于高电平时,SDA产生一个上升沿
ACK:SCL为高电平时,SDA为低电平


24c02/24c04/24c08 spec研读:

首先:IIC设备均会有一个 dev 的addr.支持IIC的多个设备可以挂在同一条总线上,操作时以设备地址来区分.

这里写图片描述

这里写图片描述

这里写图片描述

24cxx device addr 的确定
24c02: 与A2,A1,A0的硬件连接相关
24c04 : 与A2,A1的硬件连接相关,A0未连接
24c08 : 与A2的硬件连接相关,A1,A0未连接
size:
24c02: 256 个字节, 寻址需要 8bit
24c04: 512 个字节,寻址需要 9bit
24c08: 1024个字节,寻址需要 10bit

通常,向一个地址写一个数据,需要先写dev 的地址,8bit,再写入数据存放位置的地址,此时24c02一个字节可以表示完全它本身的地址,但是24c04和24c08却不行,需要两个字节,再传数据

24cxx 写入方式:
1.字节写入,一次传送一个字节,写完结束!
2.连续写: 24c02一次可以连续写8个字节,24c04/24c08一次可以连续写16个字节
每写入一个字节,地址会自动+1,24c02的地址第三位会自动+,24c04/24c08的低四位会自动+。溢出后自动变0,恢复到起始写地址.

这里写图片描述

24cxx的读方式:
1.当前读:ic里面有一个conter会记录上次操作的地址,当再次进行不指定地址的读操作,直接返回记录地址上的数据
2.随机读:指定任意的地址,再发送读命令,返回对应地址的数据
3.顺序读:发生在当前读或者随机读方式下,一次读多个字节出来

Linux驱动开发准备

阅读数 290

Linux驱动开发必看

阅读数 1625

linux下spi驱动开发

阅读数 876

没有更多推荐了,返回首页