分类: 嵌入式
2011-10-24 13:18:54
编写一个完整的字符设备驱动. 我们开发一个字符驱动是因为这一类适合大部分简单硬件设备.从一个真实设备驱动提取的代码片段: scull( Simple Character Utility for Loading Localities). scull 是一个字符驱动, 操作一块内存区域好像它是一个设备.
1.Major and Minor Numbers2.Allocating and Freeing Device Numbers主设备号标识设备对应的驱动程序,现代Linux内核允许多个驱动程序共享主设备号,但大多数设备仍然按照“一个主设备号对应一个驱动程序”的原则组织
次设备号由内核使用,用于正确确定设备文件所指的设备。次设备号用来指向驱动程序所实现的设备
内核用dev_t类型(在
中定义)用来保存设备编号——包括主设备号和次设备号,dev_t是一个32位的数,其中12位用来表示主设备号,其余20位用来表示次设备号
(dev_t)-->主设备号、次设备号
MAJOR(dev_t dev)
MINOR(dev_t dev)主设备号、次设备号-->(dev_t)
MKDEV(int major,int minor)
3.Some Important Data Structures建立一个字符设备之前,驱动程序首先要做的事情就是获得一个或多个设备编号。其这主要函数在
中声明:
int register_chrdev_region(dev_t first, unsigned int count,char *name); //指定设备编号
int alloc_chrdev_region(dev_t *dev, unsigned int firstminor,unsigned int count, char *name); //动态生成设备编号
void unregister_chrdev_region(dev_t first, unsigned int count); //释放设备编号
建议采用动态分配设备编号,一旦动态分配了设备号,可以从/proc/devices中读取到
Here's the code we use in scull 's source to get a major number:
if (scull_major) { dev = MKDEV(scull_major, scull_minor); //make major number into dev_t result = register_chrdev_region(dev, scull_nr_devs, "scull"); } else { result = alloc_chrdev_region(&dev, scull_minor, scull_nr_devs, "scull"); scull_major = MAJOR(dev); } if (result < 0) { printk(KERN_WARNING "scull: can't get major %d\n", scull_major); return result; }
4.Char Device RegistrationMost of the fundamental driver operations involve three important kernel data structures, called file_operations, file, and inode.
到现在, 我们已经保留了一些设备编号给我们使用, 但是我们还没有连接任何我们设备操作到这些编号上. file_operation 结构是一个字符驱动如何建立这个连接. 这个结构, 定义在
, 是一个函数指针的集合. 每个打开文件(内部用一个 file 结构来代表, 稍后我们会查看)与它自身的函数集合相关连( 通过包含一个称为 f_op 的成员, 它指向一个 file_operations 结构). 这些操作大部分负责实现系统调用, 因此, 命名为 open, read, 等等. 我们可以认为文件是一个"对象"并且其上的函数操作称为它的"方法", 使用面向对象编程的术语来表示一个对象声明的用来操作对象的动作. 这是我们在 Linux 内核中看到的第一个面向对象编程的现象, 后续章中我们会看到更多. inode结构
内核用inode结构在内部表示文件,inode结构中包含了大量有关文件的信息,作为常规,只有下面两个字段对编写驱动程序代码有用:
dev_t i_rdev 对表示设备文件的inode结构,该字段包含了真正的设备编号
struct cdev *i_cdev 表示字符设备的内核的内部结构。当inode指向一个字符设备文件时,该字段包含了指向struct cdev结构的指针
内核开发者增加了两个新的宏,可用来从一个inode中获得主设备号和次设备号
unsigned int iminor(struct inode *inode);
unsigned int imajor(struct inode *inode);
内核在内部使用类型 struct cdev 的结构来代表字符设备. 在内核调用你的设备操作前, 你编写分配并注册一个或几个这些结构. 为此, 你的代码应当包含
, 这个结构和它的关联帮助函数定义在这里. 如果你想在运行时获得一个独立的 cdev 结构, 你可以为此使用这样的代码:
struct cdev *my_cdev = cdev_alloc(); my_cdev->ops = &my_fops;初始化struct cdevvoid cdev_init(struct cdev *cdev, struct file_operations *fops);初始化cdev.ownercdev.owner = THHIS_MODULE;
通知内核struct cdev的信息(动完全准备好处理设备上的操作再调用该函数)
int cdev_add(struct cdev *dev, dev_t num, unsigned int count);从系统去除一个字符设备void cdev_del(struct cdev *dev);
4.1Device Registration in scullstruct scull_dev { struct scull_qset *data; /* Pointer to first quantum set */ int quantum; /* the current quantum size */ int qset; /* the current array size */ unsigned long size; /* amount of data stored here */ unsigned int access_key; /* used by sculluid and scullpriv */ struct semaphore sem; /* mutual exclusion semaphore */ struct cdev cdev; /* Char device structure */ };我们关注于 cdev, 我们的设备与内核接口的 struct cdev. 这个结构必须初始化并且如上所述添加到系统中; 处理这个任务的 scull 代码是:
static void scull_setup_cdev(struct scull_dev *dev, int index) { int err, devno = MKDEV(scull_major, scull_minor + index); cdev_init(&dev->cdev, &scull_fops); dev->cdev.owner = THIS_MODULE; dev->cdev.ops = &scull_fops; err = cdev_add (&dev->cdev, devno, 1); /* Fail gracefully if need be */ if (err) printk(KERN_NOTICE "Error %d adding scull%d", err, index); }
5.open and release注册一个字符设备的经典方法是使用:
int register_chrdev(unsigned int major, const char *name, struct file_operations *fops);这里, major 是感兴趣的主编号, name 是驱动的名子(出现在 /proc/devices), fops 是缺省的 file_operations 结构. 一个对 register_chrdev 的调用为给定的主编号注册 0 - 255 的次编号, 并且为每一个建立一个缺省的 cdev 结构. 使用这个接口的驱动必须准备好处理对所有 256 个次编号的 open 调用( 不管它们是否对应真实设备 ), 它们不能使用大于 255 的主或次编号.
如果你使用 register_chrdev, 从系统中去除你的设备的正确的函数是:
int unregister_chrdev(unsigned int major, const char *name);major 和 name 必须和传递给 register_chrdev 的相同, 否则调用会失败.
openThe open method is provided for a driver to do any initialization in preparation for later operations. In most drivers, open should perform the following tasks:
Check for device-specific errors (such as device-not-ready or similar hardware problems)
Initialize the device if it is being opened for the first time
Update the f_op pointer, if necessary
Allocate and fill any data structure to be put in filp->private_data
The first order of business, however, is usually to identify which device is being opened. Remember that the prototype for the open method is:
int (*open)(struct inode *inode, struct file *filp);The inode argument has the information we need in the form of its i_cdev field, which contains the cdev structure we set up before. The only problem is that we do not normally want the cdev structure itself, we want the scull_dev structure that contains that cdev structure. The C language lets programmers play all sorts of tricks to make that kind of conversion; programming such tricks is error prone, however, and leads to code that is difficult for others to read and understand. Fortunately, in this case, the kernel hackers have done the tricky stuff for us, in the form of the container_of macro, defined in
: 大意:inode参数在其i_cdev字段包含了我们先前设置的cdev结构信息,但我们只需要得到包含cdev结构的scull_dev结构信息,可以通过container_of来实现
container_of(pointer, container_type, container_field);This macro takes a pointer to a field of type container_field, within a structure of type container_type, and returns a pointer to the containing structure. In scull_open, this macro is used to find the appropriate device structure:
struct scull_dev *dev; /* device information */ dev = container_of(inode->i_cdev, struct scull_dev, cdev); filp->private_data = dev; /* for other methods */
Once it has found the scull_dev structure, scull stores a pointer to it in the private_data field of the file structure for easier access in the future.
The (slightly simplified) code for scull_open is:
int scull_open(struct inode *inode, struct file *filp) { struct scull_dev *dev; /* device information */ dev = container_of(inode->i_cdev, struct scull_dev, cdev); filp->private_data = dev; /* for other methods */ /* now trim to 0 the length of the device if open was write-only */ if ( (filp->f_flags & O_ACCMODE) = = O_WRONLY) { scull_trim(dev); /* ignore errors */ } return 0; /* success */ }
release6. scull's Memory Usagethe device method should perform the following tasks:
Deallocate anything that open allocated in filp->private_data
Shut down the device on last close
The scull driver introduces two core functions used to manage memory in the Linux kernel. These functions, defined in
, are:void *kmalloc(size_t size, int flags); void kfree(void *ptr); The layout of a scull device
struct scull_qset { void **data; struct scull_qset *next; };The next code fragment shows in practice how struct scull_dev and struct scull_qset are used to hold data.
int scull_trim(struct scull_dev *dev) { struct scull_qset *next, *dptr; int qset = dev->qset; /* "dev" is not-null */ int i; for (dptr = dev->data; dptr; dptr = next) { /* all the list items */ if (dptr->data) { for (i = 0; i < qset; i++) kfree(dptr->data[i]); kfree(dptr->data); dptr->data = NULL; } next = dptr->next; kfree(dptr); } dev->size = 0; dev->quantum = scull_quantum; dev->qset = scull_qset; dev->data = NULL; return 0; }7.read and write
The read and write methods both perform a similar task, that is, copying data from and to application code. Therefore, their prototypes are pretty similar, and it's worth introducing them at the same time:ssize_t read(struct file *filp, char _ _user *buff, size_t count, loff_t *offp); ssize_t write(struct file *filp, const char _ _user *buff, size_t count, loff_t *offp);
The code for read and write in scull needs to copy a whole segment of data to or from the user address space. This capability is offered by the following kernel functions, which copy an arbitrary array of bytes and sit at the heart of most read and write implementations:unsigned long copy_to_user(void _ _user *to, const void *from, unsigned long count); unsigned long copy_from_user(void *to, const void _ _user *from, unsigned long count);上述两个函数会检查用户空间指针是否有效,返回要拷贝的数据数量,若不返回0,就是返回一个-EFAULT 给用户
The arguments to read