接续上章内容继续,本章的内容为设备注册相关问题。register_chrdev(linux/fs.h),驱动向内核注册自己的file_operations。
static inline int register_chrdev(unsigned int major, const char *name,const struct file_operations *fops)
{return __register_chrdev(major, 0, 256, name, fops);
}
major为主设备号也就是硬件号,name为设备驱动名,file_operations为结构体。函数在头文件中定义的化,两个文件同时包括函数时,会在编译时爆出重复定义,而inline可以解决这个问题。
内核如何管理字符设备驱动,内核用数组来存储注册字符设备驱动[255],[major]设备号就是下标号。cat /proc/devices可以查看自己注册的设备号,设备号从1~254。
在module_init宏所注册的函数中去做注册驱动,在module_exit中去做注销。
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 *, 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 (*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 (*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);int (*setlease)(struct file *, long, struct file_lock **);
};
定义一个最简单的设备注册:
struct file_operations xxx_fops{.owner=THIS_MODULE//惯例.open=test_chrdev_open //自己修改名字.release=test_chrdev_release //自己修改名字
};static int test_chrdev_open(static inode *inode,struct file *file)
{//打开设备硬件操作的代码
}static int test_chrdev_release(static inode *inode,struct file *file)
{//关闭设备硬件操作的代码
}
内核做if(ret......)判断为err返回一般错误代码 。
注销:
其实自己去查没用到的设备号不是很靠谱,这时就要让系统去分配一个设备号。
注销时候用ret返回的号去进行注销(从后向前分配254开始的第一个空设备符)。没注销与卸载函数与操作,设备号就会一直被占用。
驱动设备文件/devices下一个文件就是一个硬件设备。设备文件的关键信息:设备号(主从设备号);主设备号,申请的那个数字——fileoperations管理(种类),次设备号,就是相同类型设备的第几个设备,设备号就=主设备号+次设备号,如果不去设备次设备号则默认为0。
用mknod创建设备文件mknod /dev/xxx c 主设备号 次设备号。
用ls -l去看设备文件可见设备文件号与属性。
在应用程序中,open()调用为file_operations中绑定的.open,close()为.release所绑定的函数。就驱动而言.write()的本质是把信息从应用层传内核驱动在写入硬件(应用层的虚拟空间与驱动不在同一个空间,所以使用copy_from_user用户to内核;copy_to_user内核to用户)实现传递。
test_chrdev_write()
{ret=copy_from_user(kbuf,ubuf,count);......
}
会将应用层的ubuf cp到驱动层的kbuf,这样就能操作硬件了。
test_chrdev_read()
{ret=copy_to_user(ubuf,kbuf,count);......
}
会将驱动层的kbuf cp到应用层的ubuf。通过这样的操作就能read(),write()进行读写了。
驱动操作的就是硬件。首先寄存器地址不同于裸机,裸机的寄存器地址是物理地址,而OS操作的地址是虚拟地址。寄存器物理地址是由cpu决定的,虚拟地址与映射表相关。而且在编程习惯时不同于裸机裸机是通过指针或解引用对寄存器进行直接读写,而OS方面的驱动是函数方法。
内核映射方法本质是mmu的开启,映射分有静态映射和动态映射。静态映射是指代码进行硬编写,写的死,内核一直时已经决定,开机时建立,关机及自毁。动态映射是根据需求,随时进行建立、使用、销毁。动态映射与静态映射可同时使用。
静态映射,虽然效率高但始终占用虚拟地址空间。动态映射效率一般,每次使用都需要去建立或销毁,但移植性强。
静态映射表,是一个头文件,不同的内核静态映射表的位置、名字可能不同,使用就限制了移植性。
静态映射表,主映射表 map-xxx.h定义模块基地址,不够用就自己追加。虚拟地址基地址表为map-base.h;GPIO相关主映射表regs_gpio.h;GPIO具体寄存器映射表gpio_bank.h。
主映射表有一个基地址,表中其他地址均为虚拟地址基地址+偏移量得到。
VA为虚拟地址,已得到虚拟映射;PA为物理地址,可能已映射,但可以再做动态映射。
#define request_mem_region(start,n,name) __request_region(&iomem_resource, (start), (n), (name), 0)
、
ret=ioremap(start,n);//真正去做映射,返回的ret为虚拟地址iounmap(虚拟地址);//撤销映射release_mem_region(物理起始地址,长度);//释放映射
一个物理地址可以被多个虚拟地址映射。一个寄存器位4位,也就是一个char,以申请8位可以*(p+1)使用第二位操作位。
注册设备号接口,有两个一个是老接口register_chrdev,一个是新接口register_chrdev_region注册设备号(指定);alloc_chrdev_region(分配设备号)+cdev注册。
struct cdev {struct kobject kobj;struct module *owner;const struct file_operations *ops;struct list_head list;dev_t dev;unsigned int count;
};void cdev_init(struct cdev *, const struct file_operations *);//cdev初始化struct cdev *cdev_alloc(void);//cdev结构体内存分配int cdev_add(struct cdev *, dev_t, unsigned);//cdev向内核注册void cdev_del(struct cdev *);//cdev注销
前面说过设备号=主设备号+次设备号。次设备号的作用是为了区分相同、相近设备被占用设备号不用去从新写个新的驱动。
设备号32位=16位主设备号+16位次设备号。
#define MAJOR(dev) ((unsigned int) ((dev) >> MINORBITS)) 主设备号
#define MINOR(dev) ((unsigned int) ((dev) & MINORMASK)) 次设备号
#define MKDEV(ma,mi) (((ma) << MINORBITS) | (mi)) 设备号
设备注册:
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);
}
注销
cdev_del(&test_dev);
unregister_chrdev_region(设备号,次设备号);
自动分配设备号:
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;
}
可以用MAJOR(my_dev),MINOR(my_dev)查询主次设备号。
PS:内核中一般驱动err,会使用goto结构倒影式处理方式进行处理。
static struc cdev *pcdev将内存分配在.data段。pcdev=cdev_alloc()分配内存空间位置在堆。cdev_del(pcdev)用于清楚dev内存。
cdev_init(pcdev,&test_fops);
等同于
pcdev->owner=THIS_MODULE;
pcdev->ops=&test_fops;
udev是应用层创建设备文件方法,在嵌入式中以busybox集成mdev实现创建设备文件的方法。内核与udev之间有一套信息传输机制(netlink)。应用层启动udev,内核使用相对应的接口class_create以及device_create。
注册完成字符设备后进行创建:
static struct class *test_class;
test_class=class_create(THIS_MODULE,"类名");
device_create(test_class,NULL,my_dev,"设备文件名");
test_class:在/sys/class下创建同名文件夹,再再文件夹下创建"设备文件名"同名文件夹(/dev/xxx那个名字),文件夹中包括相关驱动信息,可用cat查看。
注销设备
device_destroy(test_class,mydev);
class_destroy(test_class);
静态内存映射过程:
以结构体方式封装多个寄存器,实现一次性动态多个映射:
#define GPJ0_BASE 0x11111111
typedef struct GPJ0REG
{volatile unsigned int gpj0con;volatile unsigned int gpj0dat;
}gpj0_reg_t;gpj0_reg_t *pGPJ0REG;
request_mem_region(GPJ0_REGBASE,sizeof(gpj0_reg_t),"GPJ0REG");//声明
pGPJ0REG=ioremap(GPJ0_REGBASE,size(gpj0_reg_t));//映射//解映射
iounmap(pGPJ0REG);
release_mem_region(GPJ0_BASE,sizeo(gpj0_reg_t));
解引用访问寄存器:
#define rGPJ0CON *((volatile unsigned int *)GPJ0CON
这种方法去访问硬件在统一编址下可用,但是在(x86)下无法使用,移植性并不好。
内核提供了内存映射操作I/O空间的相应接口:writel(v,c)/readl(c)。v:值 c:地址。
#define writel(v,c) ({ __iowmb(); writel_relaxed(v,c); })
#define readl(c) ({ u32 __v = readl_relaxed(c); __iormb(); __v; })#define iowrite32(v, addr) writel((v), (addr))
#define ioread32(addr) readl(addr)
writeb为1字节/writew为2字节/writel为4字节。
static void __iomem *baseadr;//寄存器虚拟地址基地址requse_mem-region(GPJ0BASE,8,"GPJ0BASE");
baseaddr=ioremap(GPJ0BASE,8);
writel(0x......,baseaddr+偏移量);
iounmap(base_addr);
release_mem_region(base_addr,8);