一个字符设备或块设备都有一个主设备号和一个次设备号。主设备号用来标识与设备文件相连的驱动程序,用来反 映设备类型。次设备号被驱动程序用来辨别操作的是哪个设备,用来区分同类型的设备。
linux内核中,设备号用dev_t来描述,2.6.28中定义如下:
typedef u_long dev_t;
在32位机中是4个字节,高12位表示主设备号,低12位表示次设备号。
可以使用下列宏从dev_t中获得主次设备号:
也可以使用下列宏通过主次设备号生成dev_t:
MAJOR(dev_t dev);
MKDEV(int major,int minor);
MINOR(dev_t dev);
//宏定义:
#define MINORBITS 20
#define MINORMASK ((1U << MINORBITS) - 1) //1U后面的U表示1是unsigned int (如果是在VS编译器下的话,就是32位),把1U左移(<<,这是位运算符号,按位左移)20位就相当于1 * 2^20,然后-1,也就是mask替代了(2^20-1)。
//得到的结果就是高12位全为0,低20位全为1
#define MAJOR(dev) ((unsigned int) ((dev) >> MINORBITS))
#define MINOR(dev) ((unsigned int) ((dev) & MINORMASK))
#define MKDEV(ma,mi) (((ma) << MINORBITS) | (mi))
2.分配设备号(两种方法)
(1静态申请
int register_chrdev_region(dev_t from,unsigned count,const char *name);
(2)动态分配
int alloc_chardev_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
创建对应的设备。
3、字符设备驱动程序重要的数据结构:
(1)struct
file:
代表一个打开的文件描述符,系统中每一个打开的文件在内核中都有一个关联的struct file。它由内核在open时创建,并传递给在文件上操作的任何函数,直到最后关闭。当文件的所有实例都关闭之后,内核释放这个数据结构。
//重要成员
const struct file_operations *f_op; //该操作是定义文件关联操作的。内核在执行open时对这个指针赋值。
off_t
f_pos; //该文件读写位置。
void
*private_data;
//该成员是系统调用时保存状态信息非常有用的资源。
2)struct inode:
用来记录文件的物理信息。它和代表打开的file结构是不同的。一个文件可以对应多个file结构,但只有一个inode结构。inode一般作为file_operations结构中函数的参数传递过来。
inode译成中文就是索引节点。每个存储设备或存储设备的分区(存储设备是硬盘、软盘、U盘 ... ...
)被格式化为文件系统后,应该有两部份,一部份是inode,另一部份是Block,Block是用来存储数据用的。而inode呢,就是用来存储这些数
据的信息,这些信息包括文件大小、属主、归属的用户组、读写权限等。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);
(3)struct file_operations
struct file_operation ***_ops={
.owner=THIS_MODULE,
.llseek=***_llseek,
.read=***_read,
.write=***_write,
.ioctl=***_ioctl,
.open=***_open,
.release=***_release,
.....
}; //strude file_operations
是一个函数指针的集合
struct module *owner;
字符设备驱动程序注意事项
1)open()调用可能由于几个原因而失败。
设备驱动程序是内核的一部分,它完成以下的功能
1、对设备初始化和释放;
(1)字符设备cdev结构体初始化:
***********不是每个字符设备驱动都需要,cdev是为了构建设备模型,便于设备文件的管理所产生的。如果你的字符设备比较简单或者你不需要构建设备模型,是可以不需要cdev.内核中每个字符设备都对应一个 cdev 结构的变量,下面是它的定义: linux-2.6.22/include/linux/cdev.h
file_operation结构是虚拟层上的东西,这样使得驱动程序可以操作设备。*******************
struct cdev {
struct kobject kobj; // 每个 cdev 都是一个 kobject
struct module *owner; // 指向实现驱动的模块
const struct file_operations *ops; // 操纵这个字符设备文件的方法
struct list_head list; // 与 cdev 对应的字符设备文件的 inode->i_devices 的链表头
dev_t dev; // 起始设备编号
unsigned int count; // 设备范围号大小
};
静态内存定义初始化:
struct cdev
my_cdev;
cdev_init(&my_cdev,
&fops);
my_cdev.owner = THIS_MODULE;
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;
}
struct cdev *my_cdev = cdev_alloc();
my_cdev->ops = &fops;
my_cdev->owner = THIS_MODULE;
{
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;
}
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);
}
关 于 kobj_map() 函数就不展开了,我只是大致讲一下它的原理。内核中所有都字符设备都会记录在一个 kobj_map 结构的 cdev_map 变量中。这个结构的变量中包含一个散列表用来快速存取所有的对象。kobj_map() 函数就是用来把字符设备编号和 cdev 结构变量一起保存到 cdev_map 这个散列表里。当后续要打开一个字符设备文件时,通过调用 kobj_lookup() 函数,根据设备编号就可以找到 cdev 结构变量,从而取出其中的 ops 字段。
当一个字符设备驱动不再需要的时候(比如模块卸载),就可以用 cdev_del() 函数来释放 cdev 占用的内存。
void cdev_del(struct cdev *p)
{
cdev_unmap(p->dev, p->count);
kobject_put(&p->kobj);
}
其中 cdev_unmap() 调用 kobj_unmap() 来释放 cdev_map 散列表中的对象。kobject_put() 释放 cdev 结构本身。
下面这个是必须要有的:
设备初始化
{
major=register_chrdev(0,"first_drv",&first_drv_fops); //注册,告诉内核。
firstdrv_class=class_create(THIS_MODULE,"firstdrv"); //创建一个类
if(IS_ERR(firstdrv_class))
return PTR_ERR(firstdrv_class);
firstdrv_class_dev=class_device_create(firstdrv_class,NULL,MKDEV(major,0),NULL,"xyz");//xyz为设备名字
gpfcon=(volatile unsigned long *)ioremap(0x56000050,16);
gpfdat=gpfcon+1;
if(unlikely(IS_ERR(firstdrv_class_dev)))
return PTR_ERR(firstdev_class_drv);
return 0;
}
设备卸载
static vodi first_drv_exit(void)
{
unregister_chrdev(major,"first_drv");//卸载
class_device_unregister(firstdrv_class_dev);
class_destroy(firstdrv_class);
iounremap();
}
2、把数据从内核传送到硬件(copy_to_user)和从硬件读取数据(copy_form_user);
结构体file_operations在头文件 linux/fs.h中定义,用来存储驱动内核模块提供的对设备进行各种操作的函数的指针。该结构体的每个域都对应着驱动内核模块用来处理某个被请求的 事务的函数的地址。
fileoprations结构体上定义的函数进行具体的数据操作。
.owner=THIS_MODULE;
.write=second_drv_write;
.open=second_drv_open;
.read=second_drv_read;
}
int major;
{
gpfcon &= ~((0x3<<(4*2))|(0x3<<(5*2))|(0x3<<(6*2))); //GPF4,5,6为输出
gpfcon |= ((0x1<<(4*2))|(0x1<<(5*2))|(0x1<<(6*2))); //
return 0;
}
static ssize_t first_drv_read(struct inode *inode,struct file *file)
{
return 0;
}
static ssize_t first_drv_write(struct file *file,const char _user *buf,size_t count,loff_t *ppos)
{
int val;
copy_from_user(&val,buf,count);
if(val==1)
{
gpfdata|=((1<<4)|(1<<5)|(1<<6)); //点灯
}
else
{
gpfdata &=~ ((1<<4)|(1<<5)|(1<<6)); //灭灯
}
return 0;
}
测试程序:
int main()
{
int fd;
int val=1;
fd=open(("/dev/xyz",O_RDWR);
if(fd<0)
{
printf("cant‘t open");
}
if(argc!=2)
{
printf("Usage:\n");
printf("%s<on|off>\n",argv[0]);//<>表示参数不可以省略,|表示或者的意思/
}
if(strcmp(argc[1],on)==0) //如果第二个参数等于on的话
{
val=1;
}
else
{
val=0;
}
write(fd,&val,4);
return 0;
}
写驱动程序的时候要:1.写框架。2.看原理图 3.看手册 3.写代码(在驱动程序中,不能直接使用物理地址,要ioremap,使用虚拟地址)
主设备号作用是找到fileoperations。次设备号是留给我们自己用的。
int minor=MINOR(inode->i_rdev);次设备号代表的意思有我们自己决定。
3、读取应用程序传送给设备文件的数据和回送应用程序请求的数据;
1)应用程序调用一系列函数库,通过对文件的操作完成一系列功能:
应用程序以文件形式访问各种硬件设备(linux特有的抽象方式,把所有的硬件访问抽象为对文件的读写、设置)
函数库:
部分函数无需内核的支持,由库函数内部通过代码实现,直接完成功能
部分函数涉及到硬件操作或内核的支持,由内核完成对应功能,我们称其为系统调用
2)内核处理系统调用,根据设备文件类型、主设备号、从设备号(后面会讲解),调用设备驱动程序;
3)设备驱动直接与硬件通信;
4、检测和处理设备出现的错误。