以下内容源于朱有鹏嵌入式课程的学习与整理,如有侵权请告知删除。
一、注册字符设备驱动的老接口
在《字符设备驱动基础》里,注册字符设备驱动使用的函数是register_chrdev()函数。
该函数的介绍,见博客字符设备驱动基础3——使用register_chrdev()函数注册字符设备。
值得一提的是,该函数同时完成设备号的分配,以及驱动的注册。
二、注册字符设备驱动的新接口
1、概述
(1)注册设备驱动的新方法
注册设备驱动的新方法,将以前的register_chrdev()函数完成的工作拆分为两个步骤。
主步骤一:注册设备号
主要涉及register_chrdev_region()函数 或 alloc_chrdev_region()函数。
这两个函数的功能都是分配设备号,根据情况选其一即可。
register_chrdev_region()函数,其传参表示想索要某个设备号。
alloc_chrdev_region()函数,是让内核自动分配设备号。
主步骤二:注册设备驱动
主要涉及cdev_add()函数。
(2)其他涉及的函数
cdev_init函数:初始化,主要将cdev和fops关联起来。
cdev_alloc函数:分配空间
cdev_del函数:注销设备驱动
unregister_chrdev_region:注销设备号
(3)涉及的新结构体
struct cdev结构体。
(4)注册、注销字符设备驱动的流程
注册字符设备驱动的流程
先使用register_chrdev_region()函数申请设备号,然后使用cdev_alloc函数来给struct cdev结构体分配空间,接着使用cdev_init函数来初始化,最后使用cdev_add()函数注册设备驱动。
注销字符设备驱动的流程
先使用cdev_del()函数注销设备驱动,再使用unregister_chrdev_region()函数注销设备号。
2、新接口的介绍
(1)struct cdev结构体
该结构体定义在文件\include\linux\cdev.h文件中,内容如下:
struct cdev {struct kobject kobj;struct module *owner;const struct file_operations *ops; //文件操作函数集struct list_head list;dev_t dev; //设备号unsigned int count; };
其中,dev表示设备号(含主次设备号),它属于dev_t类型(其实就是init类型)。
设备号由主设备号和次设备号组成,在\include\linux\kdev_t.h文件中有这样的宏定义:
#define MAJOR(dev) ((dev)>>8) //从设备号中提取主设备号 #define MINOR(dev) ((dev) & 0xff) //从设备号中提取次设备号 #define MKDEV(ma,mi) ((ma)<<8 | (mi)) //由主次设备号得到设备号
(2)register_chrdev_region()函数
此函数定义在/fs/char_dev.c文件中,用于向内核注册(即申请)一组字符设备的设备编号,这就是函数名字带有“region”(区域、范围)的原因。
这一组字符设备的主设备号相同而次设备号不同,所以使用参数提供这组字符设备的统一的名字(name)、这组字符设备的数量(即次设备号的数目,count)、这组字符设备的设备编号的起始编号(from)。
函数的内容如下:
/*** register_chrdev_region() - register a range of device numbers* @from: the first in the desired range of device numbers; must include* the major number.* @count: the number of consecutive device numbers required* @name: the name of the device or driver.** Return value is zero on success, a negative error code on failure.*/ int register_chrdev_region(dev_t from, unsigned count, const char *name) {struct char_device_struct *cd;dev_t to = from + count;dev_t n, next;for (n = from; n < to; n = next) {next = MKDEV(MAJOR(n)+1, 0);if (next > to)next = to;cd = __register_chrdev_region(MAJOR(n), MINOR(n),next - n, name);if (IS_ERR(cd))goto fail;}return 0; fail:to = n;for (n = from; n < to; n = next) {next = MKDEV(MAJOR(n)+1, 0);kfree(__unregister_chrdev_region(MAJOR(n), MINOR(n), next - n));}return PTR_ERR(cd); }
参数含义
(1)from
要分配的设备编号范围的初始值(初始,所以才叫from,次设备号常设为0)。
(2)count
连续编号范围(或者说次设备的数目)。
(3)name
设备名称 (通过/proc/devices可以查看到)。
假如主设备号是 200,次设备号有0、1、2、3,则 from 为MKDEV(200,0)(MKDEV用来合成设备号,0表示起始次设备号),count为4(表示有四个次设备号)。
结构体struct char_device_struct的定义
static struct char_device_struct {struct char_device_struct *next;unsigned int major; //主设备的编号unsigned int baseminor; //次设备的起始编号int minorct; //次设备的数目char name[64]; //设备(或者说驱动)的名字struct cdev *cdev; //指向 字符设备驱动程序描述符 的指针 } *chrdevs[CHRDEV_MAJOR_HASH_SIZE]; //#define CHRDEV_MAJOR_HASH_SIZE 255
由该结构体的定义可知,内核中一共有255个主设备号。同一主设备号下的不同设备,它们的次设备号是按起始次设备号递增排序的。这些同主设备号而不同次设备号的设备,统一用一个struct char_device_struct结构体来描述。
总结:使用register_chrdev_region()函数申请设备号时,要事先明确申请哪个主次设备号,这要通过cat /proc/devices来查看哪些主设备号没有被使用。有没有自动分配主次设备号的函数呢?有的,那就是下面的alloc_chrdev_region函数。
(3)alloc_chrdev_region()函数
此函数位于/fs/char_dev.c文件中,用于让内核自动分配设备号。
函数的内容如下:
/*** alloc_chrdev_region() - register a range of char device numbers* @dev: output parameter for first assigned number* @baseminor: first of the requested range of minor numbers* @count: the number of minor numbers required* @name: the name of the associated device or driver** Allocates a range of char device numbers. The major number will be* chosen dynamically, and returned (along with the first minor number)* in @dev. Returns zero or a negative error code.*/ int alloc_chrdev_region(dev_t *dev, unsigned baseminor, unsigned count,const char *name) {struct char_device_struct *cd;cd = __register_chrdev_region(0, baseminor, count, name);if (IS_ERR(cd))return PTR_ERR(cd);*dev = MKDEV(cd->major, cd->baseminor);return 0; }
如果设备号成功分配,则把分配的设备号放入第一个参数dev中(它是输出型参数)。
(4)cdev_init()函数
此函数位于/fs/char_dev.c文件中,函数的内容如下:
/*** cdev_init() - initialize a cdev structure* @cdev: the structure to initialize* @fops: the file_operations for this device** Initializes @cdev, remembering @fops, making it ready to add to the* system with cdev_add().*/ void cdev_init(struct cdev *cdev, const struct file_operations *fops) {memset(cdev, 0, sizeof *cdev);INIT_LIST_HEAD(&cdev->list);kobject_init(&cdev->kobj, &ktype_cdev_default);cdev->ops = fops; }
函数主要作用,是将cdev(设备的体现)与fops(驱动的体现)关联起来,即把文件操作结构体指针赋值给cdev的ops。
可以不使用此函数,直接用下面的代码。
//cdev_init(pcdev, &test_fops);pcdev->owner = THIS_MODULE; pcdev->ops = &test_fops;
(5)cdev_add()函数
此函数位于/fs/char_dev.c文件中,内容如下:
/*** cdev_add() - add a char device to the system* @p: the cdev structure for the device* @dev: the first device number for which this device is responsible* @count: the number of consecutive minor numbers corresponding to this* device** cdev_add() adds the device represented by @p to the system, making it* live immediately. A negative error code is returned on failure.*/ int cdev_add(struct cdev *p, dev_t dev, unsigned count) {p->dev = dev;p->count = count;return kobj_map(cdev_map, dev, count, NULL, exact_match, exact_lock, p); }
这个函数的作用是注册设备驱动。
这个函数主要是填充struct cdev结构体变量的成员dev和count。
注意前面在cdev_init函数中,已经填充了struct cdev结构体变量的成员ops。
(6)cdev_alloc()函数
此函数位于/fs/char_dev.c文件中,用于申请(存放struct cdev结构体变量的)空间。
此函数的内容如下:
/*** cdev_alloc() - allocate a cdev structure** Allocates and returns a cdev structure, or NULL on failure.*/ struct cdev *cdev_alloc(void) {struct cdev *p = kzalloc(sizeof(struct cdev), GFP_KERNEL);if (p) {INIT_LIST_HEAD(&p->list);kobject_init(&p->kobj, &ktype_cdev_dynamic);}return p; }
其实也可以直接定义一个struct cdev结构体变量,不过这个变量分配在数据段,整个程序运行期间都存在,不够灵活。
static struct cdev test_cdev;
而采用cdev_alloc()函数来申请(存放struct cdev结构体变量的)空间,这个空间是在堆上的。我们可以在数据段上定义一个struct cdev结构体指针,用来指向cdev_alloc()函数申请的空间,这个指针只占4个字节。
//static struct cdev test_cdev; //这个变量分配在数据段,整个程序运行期间都存在,不够灵活static struct cdev *pcdev; //只占4个字节,之后cdev_alloc给它实例化,分配在堆上,可以按需分配。由cdev_del释放 pcdev = cdev_alloc(); // 给pcdev分配内存,指针实例化
三、代码示例
1、注册字符设备驱动的代码
// 使用新的cdev接口来注册字符设备驱动,需要2步// 第1步:注册主次设备号mydev = MKDEV(MYMAJOR, 0);//MYMAJOR在这里是200,这里合成了要申请的设备号,即200,0retval = register_chrdev_region(mydev, MYCNT, MYNAME);//这里申请刚才合成的设备号if (retval) {printk(KERN_ERR "Unable to register minors for %s\n", MYNAME);return -EINVAL;}printk(KERN_INFO "register_chrdev_region success\n");// 第2步:注册字符设备驱动cdev_init(&test_cdev, &test_fops);//这步其实可以用其他代码来代替retval = cdev_add(&test_cdev, mydev, MYCNT);//注册字符设备驱动if (retval) {printk(KERN_ERR "Unable to cdev_add\n");return -EINVAL;}printk(KERN_INFO "cdev_add success\n");
2、注销字符设备驱动的代码
// 使用新的接口来注销字符设备驱动,注销分2步:// 第一步真正注销字符设备驱动用cdev_delcdev_del(&test_cdev);// 第二步去注销申请的主次设备号unregister_chrdev_region(mydev, MYCNT);
3、完整的程序
(1)驱动源代码
#include <linux/module.h> // module_init module_exit
#include <linux/init.h> // __init __exit
#include <linux/fs.h>
#include <asm/uaccess.h>
#include <mach/regs-gpio.h>
#include <mach/gpio-bank.h> // arch/arm/mach-s5pv210/include/mach/gpio-bank.h
#include <linux/string.h>
#include <linux/io.h>
#include <linux/ioport.h>
#include <linux/cdev.h>#define MYMAJOR 200 //注意通过cat /proc/devices查看这个200主设备号是否被占用
#define MYCNT 1 //表示次设备号只有一个
#define MYNAME "testchar" //表示设备或者说驱动(混在一起的)的名字#define GPJ0CON S5PV210_GPJ0CON
#define GPJ0DAT S5PV210_GPJ0DAT#define rGPJ0CON *((volatile unsigned int *)GPJ0CON)
#define rGPJ0DAT *((volatile unsigned int *)GPJ0DAT)#define GPJ0CON_PA 0xe0200240
#define GPJ0DAT_PA 0xe0200244unsigned int *pGPJ0CON;
unsigned int *pGPJ0DAT;int mymajor;
static dev_t mydev;
static struct cdev test_cdev;
char kbuf[100]; // 内核空间的bufstatic int test_chrdev_open(struct inode *inode, struct file *file)
{printk(KERN_INFO "test_chrdev_open\n");rGPJ0CON = 0x11111111;rGPJ0DAT = ((0<<3) | (0<<4) | (0<<5)); // 亮return 0;
}static int test_chrdev_release(struct inode *inode, struct file *file)
{printk(KERN_INFO "test_chrdev_release\n");rGPJ0DAT = ((1<<3) | (1<<4) | (1<<5));return 0;
}ssize_t test_chrdev_read(struct file *file, char __user *ubuf, size_t count, \loff_t *ppos)
{int ret = -1;printk(KERN_INFO "test_chrdev_read\n");ret = copy_to_user(ubuf, kbuf, count);if (ret){printk(KERN_ERR "copy_to_user fail\n");return -EINVAL;}printk(KERN_INFO "copy_to_user success..\n");return 0;
}// 写函数的本质就是将应用层传递过来的数据先复制到内核中,
// 然后将之以正确的方式写入硬件完成操作。
static ssize_t test_chrdev_write(struct file *file, const char __user *ubuf,size_t count, loff_t *ppos)
{int ret = -1;printk(KERN_INFO "test_chrdev_write\n");// 使用该函数将应用层传过来的ubuf中的内容拷贝到驱动空间中的一个buf中//memcpy(kbuf, ubuf); // 不行,因为2个不在一个地址空间中memset(kbuf, 0, sizeof(kbuf));ret = copy_from_user(kbuf, ubuf, count);if (ret){printk(KERN_ERR "copy_from_user fail\n");return -EINVAL;}printk(KERN_INFO "copy_from_user success..\n");if (kbuf[0] == '1'){rGPJ0DAT = ((0<<3) | (0<<4) | (0<<5));}else if (kbuf[0] == '0'){rGPJ0DAT = ((1<<3) | (1<<4) | (1<<5));}return 0;
}// 自定义一个file_operations结构体变量,并且去填充
static const struct file_operations test_fops = {.owner = THIS_MODULE, // 惯例,直接写即可.open = test_chrdev_open, // 将来应用open打开这个设备时实际调用的.release = test_chrdev_release, // 就是这个.open对应的函数.write = test_chrdev_write,.read = test_chrdev_read,
};// 模块安装函数
static int __init chrdev_init(void)
{ int retval;printk(KERN_INFO "chrdev_init helloworld init\n");/*// 在module_init宏调用的函数中去注册字符设备驱动// major传0进去表示要让内核帮我们自动分配一个合适的空白的没被使用的主设备号// 内核如果成功分配就会返回分配的主设备好;如果分配失败会返回负数mymajor = register_chrdev(0, MYNAME, &test_fops);if (mymajor < 0){printk(KERN_ERR "register_chrdev fail\n");return -EINVAL;}printk(KERN_INFO "register_chrdev success... mymajor = %d.\n", mymajor);
*/ // 使用新的cdev接口来注册字符设备驱动// 新的接口注册字符设备驱动需要2步// 第1步:注册/分配主次设备号mydev = MKDEV(MYMAJOR, 0);retval = register_chrdev_region(mydev, MYCNT, MYNAME);if (retval) {printk(KERN_ERR "Unable to register minors for %s\n", MYNAME);return -EINVAL;}printk(KERN_INFO "register_chrdev_region success\n");// 第2步:注册字符设备驱动cdev_init(&test_cdev, &test_fops);retval = cdev_add(&test_cdev, mydev, MYCNT);if (retval) {printk(KERN_ERR "Unable to cdev_add\n");return -EINVAL;}printk(KERN_INFO "cdev_add success\n");// 使用动态映射的方式来操作寄存器if (!request_mem_region(GPJ0CON_PA, 4, "GPJ0CON"))return -EINVAL;if (!request_mem_region(GPJ0DAT_PA, 4, "GPJ0CON"))return -EINVAL;pGPJ0CON = ioremap(GPJ0CON_PA, 4);pGPJ0DAT = ioremap(GPJ0DAT_PA, 4);*pGPJ0CON = 0x11111111;*pGPJ0DAT = ((0<<3) | (0<<4) | (0<<5)); // 亮return 0;
}// 模块卸载函数
static void __exit chrdev_exit(void)
{printk(KERN_INFO "chrdev_exit helloworld exit\n");*pGPJ0DAT = ((1<<3) | (1<<4) | (1<<5)); // 解除映射iounmap(pGPJ0CON);iounmap(pGPJ0DAT);release_mem_region(GPJ0CON_PA, 4);release_mem_region(GPJ0DAT_PA, 4);/* // 在module_exit宏调用的函数中去注销字符设备驱动unregister_chrdev(mymajor, MYNAME);
*/ // 使用新的接口来注销字符设备驱动// 注销分2步:// 第一步真正注销字符设备驱动用cdev_delcdev_del(&test_cdev);// 第二步去注销申请的主次设备号unregister_chrdev_region(mydev, MYCNT);
}module_init(chrdev_init);
module_exit(chrdev_exit);// MODULE_xxx这种宏作用是用来添加模块描述信息
MODULE_LICENSE("GPL"); // 描述模块的许可证
MODULE_AUTHOR("aston"); // 描述模块的作者
MODULE_DESCRIPTION("module test"); // 描述模块的介绍信息
MODULE_ALIAS("alias xxx"); // 描述模块的别名信息
(2)应用层代码
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <string.h>#define FILE "/dev/test"
char buf[100];int main(void)
{int fd = -1;int i = 0;fd = open(FILE, O_RDWR);if (fd < 0){printf("open %s error.\n", FILE);return -1;}printf("open %s success..\n", FILE);while (1){memset(buf, 0 , sizeof(buf));printf("请输入 on 或者 off 或者 flash 或者 quit :\n");scanf("%s", buf);if (!strcmp(buf, "on")){write(fd, "1", 1);}else if (!strcmp(buf, "off")){write(fd, "0", 1);}else if (!strcmp(buf, "flash")){for (i=0; i<3; i++){write(fd, "1", 1);sleep(1);write(fd, "0", 1);sleep(1);}} else if (!strcmp(buf, "quit")){break;}elsebreak;}close(fd); return 0;
}