设备号:主设备号 + 次设备号 也可以叫主次设备号
新接口注册字符设备驱动,其实就是填充这个struct cdev类型的结构体,主要填充的内容就是
file_operations这个结构体变量,让cdev中的ops成员的值为file_operations结构体变量的值。让owner成员等于THIS_MODULE在用cdev_add函数将这个结构体和设备号,注册到内核中
1、注册字符设备驱动新接口1
(1)老接口:register_chrdev
老的接口用这个函数去注册设备驱动。可以看前面的基础笔记
(2)新的接口:register_chrdev_region/alloc_chrdev_region +
cdev
@1:register_chrdev_region和函数是用来向内核注册一个设备号的,向内核登记了一个号码,我们指定一个设备号,向内核去注册。
@2:alloc_chrdev_region函数是让内核帮我们自动分配一个设备号,我们不指定一个设备号,让内核自动的帮我们分配一个设备号。
@3:设备号有了,但是我们驱动还是没有注册的,因为我们的file_operations结构还没有注册到内核中。
@4:cdev是一个结构体,里面的成员来共同帮助我们注册驱动到内核中,表达字符设备的,将这个struct
cdev结构体进行填充,主要填充的内容就是
file_operations这个结构体变量,让cdev中的ops成员的值为file_operations结构体变量的值。让owner成员等于THIS_MODULE,之后这个结构体会被cdev_add函数
注册到内核中,也就是内核的管理驱动的链表中,cdev_add时还有设备号,和次设备号个数等信息。
(3)cdev,结构体,在内核中,可以通过搜索cdev.h找到
@1:struct cdev {
struct kobject kobj;
struct module *owner;//填充时,值要为 THIS_MODULE,表示模块
const struct file_operations
*ops;//这个file_operations结构体,注册驱动的关键,要填充成这个结构体变量
struct list_head list;
dev_t dev;//设备号,主设备号+次设备号
unsigned int count;//次设备好个数
};
@2:这个cdev结构体,可以用很多函数来操作他。
如:
cdev_alloc:让内核为这个结构体分配内存的
cdev_init:将struct
cdev结构体变量和file_operations结构体进行绑定的
cdev_add:向内核里面添加一个驱动,注册驱动的
cdev_del:从内核中注销掉一个驱动。注销驱动
2、设备号
(1)dev_t类型
(2)MKDEV、MAJOR、MINOR三个宏
@1:MKDEV宏。是用来将主设备号和次设备号,转换成一个主次设备号的。(设备号)
@2:MAJOR宏。从设备号里面提取出来主设备号的。
@3:MINOR宏。从设备号中提取出来次设备号的。
3、编程实践
(1)使用register_chrdev_region函数和cdev_init函数和cdev_add函数,进行字符设备驱动的注册。什么意思上面都有讲。
(2)使用新的接口去注册字符设备驱动,需要两步
@1:第一步。注册或者让内核为我们分配一个主次设备号。使用register_chrdev_region去注册我们指定的设备号,或者使用alloc_chrdev_region让内核为我们
分配一个设备号。
int register_chrdev_region(dev_t from, unsigned count,
const char *name);
--第1个参数是:主次设备号,起始的主次设备号,用MKDEV宏来将主设备号和次设备号变成主次设备号
--第2个参数:注册几个次设备号,以第一个参数设备号中的次设备号为起始。
比如:我们要注册一个主设备号为200,次设备号为0、1、2、3。所以第一个参数我们就可以用MKDEV(200,
0)宏,来得到一个由主设备号和次设备号转换成的
设备号,主设备号是200,次设备号是0。第二个参数count我们就可以传递为4,就表示要注册200和0,200和1,一直到200和4,的设备号。
--第3个参数:是要注册的这个驱动的名字,我们自己定
--返回值:为1,表示注册主次设备号有错,说明内核中的这个主次设备号不可用
@2:第二步。注册字符设备驱动。cdev_init,和cdev_add函数
注册设备驱动,其实就是填充这个struct
cdev类型的结构体,在用cdev_add函数将这个结构体和设备号,注册到内核中。
cdev_init(struct cdev *cdev, const struct file_operations
*fops)
先调用这个cdev_init函数,将我们定义的一全局的struct
cdev变量和file_operations结构体进行绑定,就是进行结构体的填充
int cdev_add(struct cdev *p, dev_t dev, unsigned
count),就是将填充好的结构体,和设备号,次设备号数量通过cdev_add注册到内核中的链表中。
在调用这个函数
--第一个参数。我们定的那个struct cdev全局变量
--第二个参数。设备号,我们一般会定义一个dev_t类型的变量,让这个变量来接收MKDEV宏将主设备号和次设备号转换后的设备号的。第二个参数我们传这个变量
--第三个参数。就是要注册的次设备号的数量
--返回值:返回值如果是1,表示注册这个驱动失败
(3)使用新的接口去注销驱动
@1:第一步:用cdev_del函数注销掉设备驱动
void cdev_del(struct cdev *p),直接传我们定义的这个struct
cdev全局变量就行,因为这个struct
cdev的全局变量,在cdev_init时已经和file_operations
结构体绑定起来了。
@2:第二步:注销register_chrdev_region申请的主次设备号
void unregister_chrdev_region(dev_t from, unsigned
count)。
用这个函数,来进行注销申请的主次设备号。
--第一个参数。我们用MKDEV宏传主设备号和次设备号得到的那个设备号。
--第二个参数。次设备号是几个。我们申请了几个,就传几个。
4、使用alloc_chrdev_region函数,来让内核自动给我们分配设备号
(1)使用alloc_chrdev_region函数,让内核为我们自动分配一个设备号
int alloc_chrdev_region(dev_t *dev, unsigned baseminor,
unsigned count, const char *name)
@1:这个函数的第一个参数,是输出型参数,获得一个分配到的设备号。可以用MAJOR宏和MINOR宏,将主设备号和次设备号,提取打印出来,看是自动分配的是
多少,方便我们在mknod创建设备文件时用到主设备号和次设备号。 mknod /dev/xxx c 主设备号
次设备号
@2:第二个参数:次设备号的基准,从第几个次设备号开始分配。
@3:第三个参数:次设备号的个数。
@4:第四个参数:驱动的名字。
@5;返回值:小于0,则错误,自动分配设备号错误。否则分配得到的设备号就被第一个参数带出来。
5、中途出错的倒影式处理方法。
(1)在一个函数中有很多操作,没一个操作都有可能会出现错误,如果第一个操作没有出错,第二操作出错了,我们不应该直接返回,而是应该将第一个操作用到
的系统资源释放掉后,在返回。以此类推,如果第二个操作没有出错,但是第三个操作出错了,我们也不应该直接返回,而是应该将第二个操作用到的系统资源和
第一个操作用到的系统资源释放后,在返回。这个就是中途出错的倒影式处理方法。1234的顺序,用到了系统资源,那么对应的每一步的出错处理,就是4321的。
6、使用cdev_alloc。
(1)这个函数的作用是,给定义出来的struct
cdev类型的指针分配堆内存,cdev_alloc内部其实就是调用了内核中的kmalloc来进行给struct_cdev来分配堆内存的
,内存大小就是这个struct cdev的大小。
例:如果我们定义了一个全局的 static struct cdev *pcdev; 我们就可以用 pcdev =
cdev_alloc();来给这个pcdev分配堆内存空间。cdev_del(pcdev)时会释放
掉这个申请到的堆内存,cdev_del函数内部是能知道你的struct
cdev定义的对象是用的堆内存还是栈内存还是数据段内存的。这个函数cdev_del调用时,会先去
看你有没有使用堆内存,如果有用到的话,会先去释放掉你用的堆内存,然后在注销掉你这个设备驱动。防止内存泄漏。注意如果struct
cdev要用堆内存一定
要用内核提供的这个cdev_alloc去分配堆内存,因为内部会做记录,这样在cdev_del注销掉这个驱动的时候,才会去释放掉那段堆内存。
(2)如果你用cdev_alloc给struct
cdev分配堆内存空间了,因为你用的cdev_alloc,所以你后面可以不要用cdev_init函数来进行struct
cdev和fops的绑定。
可以直接pcdev->ops = fops;
来进行绑定,cdev_init函数中的其他的操作在cdev_alloc中都有做了。
补充:我们struct cdev定义出来的变量中,owner成员变量,值要为THIS_MODULE;
cdev_init是将struct cdev成员中的ops填充成fops。
pcdev->owner = THIS_MODULE;
//这一步,即使你用的是cdev_init也要填充。因为cdev_init中没有做。
pcdev->ops = fops;
7、字符设备驱动注册代码分析。
主次设备号的申请或注册的
__register_chrdev_region
(1)老接口
register_chrdev我们写驱动的人用的函数
__register_chrdev参数分别是:主设备号、起始次设备号、次设备号上限、驱动名字、file_operations结构体
__register_chrdev_region注册设备驱动主次设备号的,参数分别是:主设备号、起始次设备号、次设备号上限、驱动名字
cdev_alloc为struct cdev *cdev 分配堆内存
cdev->owner = fops->owner;
填充结构体
cdev->ops = fops; 填充结构体
cdev_add注册驱动,参数分别是:struct cdev结构体的指针、设备号、几个次设备号
(2)新接口
register_chrdev_region参数分别是:主次设备号、次设备号个数、驱动名字
__register_chrdev_region注册设备驱动主次设备号的,参数分别是:主设备号、起始次设备号、次设备号上限、驱动名字
alloc_chrdev_region
自动分配设备号的,参数分别是:会输出设备号的参数、次设备号的起始数、次设备号的个数、驱动名字
cd = __register_chrdev_region(0, baseminor, count,
name);cd这个结构体中,会得到主设备号,起始设备号
struct char_device_struct *cd,
*dev = MKDEV(cd->major,
cd->baseminor);输出型参数赋值成MKDEV转换得到的设备号
8、自动创建字符设备驱动设备文件,不用mknod去手动创建,(驱动安装上后,自动创建设备文件,驱动卸载后,自动删除设备文件)
(1)解决方案:udev,嵌入式中用的是mdev,busybox中集成的
@1:udev就是一个应用程序,在busybox,是busybox中的一个命令,对应的嵌入式版本的busybox中的命令就是mdev,一个应用程序。mknod也是一个应用程序
(2)内核驱动和应用层udev之间有一套信息传输机制(netlink协议),当内核驱动中安装了一个驱动的时候,应用层的udev就会知道安装好了一个新的驱动模块,
udev会知道安装的这个驱动模块的文件,主次设备号等信息,并且根据这些信息就会去创建一个设备文件。大致就是这样的
(3)在安装驱动之前,一定要先启用应用层的udev。在我们制作的根文件系统中的/etc/init.d/rcS这个文件中,我们已经添加了启动mdev的语句,所以系统启动起
来后,busybox中的mdev这个应用程序已经是启动的了。其次内核驱动中一定要使用专用的接口,才能达到驱动安装后,使得mdev知道相应的信息,去创建设备文件
(4)驱动注册和驱动注销时信息会发送到应用层busybox中的mdev中,由mdev在应用层进行设备文件的创建和删除
(5)内核驱动设备类相关的函数。内核驱动中和busybox中的mdev通信的接口,就是class_create函数和device_create函数。内核驱动中的这个两个设备类,就是
可以通过调用这两个接口,给应用层busybox中的mdev应用程序发信息,让mdev创建设备文件。
驱动安装时让应用层mdev创建设备驱动文件的操作
@1:class_create:参数分别是:THIS_MODULE、要创建的类名。返回值是一个struct
class类型的全局结构体变量。
@2:device_create:参数分别是:class_create返回的struct
class类型的全局结构体变量、NULL、设备号、NULL、和将来mdev创建出来的设备文件的名字(就
是将来会在/dev目录下创建的设备文件的名字)。
(6)驱动卸载的时候,内核驱动对应的和busybox中的mdev通信的删除设备文件的两个设备类,device_destroy和class_destroy
驱动卸载时让应用层mdev删除设备驱动文件的操作
@1:class_destroy:参数分别是:class_create返回的那个struct
class全局结构体变量、和设备号
@2:class_destroy:一个参数,参数是class_create返回的那个struct
class全局结构体变量
9、设备类相关代码分析
(1)sys虚拟文件系统,内核在运行的时候,内核中的一些数据结构信息会在这个sys文件系统中体现出来,我们可以读这个文件。
(2)我们自己写的驱动中,用class_create来创建了一个why_class设备类,创建好后,就会在根文件系统的/sys/class中看到我们创建的这个设备类,我们device_create创建了一驱动的设备test111,这个驱动的设备就会在根文件系统的/sys/class/why_class/中看到,我们可以进入到这个test111驱动设备中,可以看到
有很多文件,这些文件对应的就是内核中一些数据结构,像dev文件,就是记录了主次设备号的数据结构,我们可以读取这个文件来知道,主次设备号,也可以
通过uevent来知道这个驱动的主次设备号和名字,应用层的busybox中的mdev应用程序就可以通过读取这个sys虚拟文件系统中的,我们在class_create和device_create时创建出来的设备驱动,就可以知道安装的这个驱动的相关设备号和名字等信息,进而创建出来设备文件
(3)内核中发生的一些改变,我们可以通过这个sys虚拟文件系统来知道,所以这个sys就是应用层和内核层的一扇窗户,让应用层可以通过去读这个文件夹下的相关
信息来知道一些东西,并作出一些动作,想驱动安装好后,应用层就是通过读取这个sys文件系统下多出来的那个驱动设备文件的信息,也就是多出来的那个数据
结构,的信息。来相应的创建出来设备号和驱动文件的名字对应的设备驱动文件
(4)
class_create
__class_create
__class_register
kset_register
kobject_uevent
add_class_attrs
class_create_file
sysfs_create_file
(5)
device_create
device_create_vargs
10、静态映射表建立过程
(1)映射表的具体物理地址和虚拟地址的值,就是那些物理地址到虚拟地址转换映射的宏,在上一个笔记中记录了
(2)映射表建立函数。这个函数用来由映射表(就是(1)中说的那些宏)来建立Linux内核的页表映射关系,页表在uboot移植过程中有所涉及了,不清楚可以博客搜(就是由虚拟地址可以查出来物理地址的表,就是让MMU控制器来使用,将这个页表和MMU对应起来以后,我们就可以给MMU一个虚拟地址,MMU就会帮我们查这个表,找到对应的物理地址)。
这个映射表建立函数是在。arch/arm/mach-s5pv210/mach-smdkc110.c中的smdkc110_map_io函数
smdkc110_map_io
s5p_init_io
iotable_init函数中的第一个参数s5p_iodesc,这个东西是一个结构体数组,这个数组中的每一个元素就是一个结构体,就是一个映射关系。
所以内核移植时,真正给定的页表是在arch/arm/plat-s5p/cpu.c中的s5p_iodesc结构体数组。他就是真正的映射表
static struct map_desc s5p_iodesc[] __initdata = {
{
.virtual= (unsigned long)S5P_VA_CHIPID,//对应的就是虚拟地址
.pfn= __phys_to_pfn(S5P_PA_CHIPID),//对应的就是物理地址
.length= SZ_4K,//对应的就是映射的长度
.type= MT_DEVICE,//这个描述映射类型的,说明是设备的映射类型
}, {
.virtual= (unsigned long)S3C_VA_SYS,
.pfn= __phys_to_pfn(S5P_PA_SYSCON),
.length= SZ_64K,
.type= MT_DEVICE,
}, {
.virtual= (unsigned long)S3C_VA_UART,
.pfn= __phys_to_pfn(S3C_PA_UART),
.length= SZ_4K,
.type= MT_DEVICE,
}, {
.virtual= (unsigned long)VA_VIC0,
.pfn= __phys_to_pfn(S5P_PA_VIC0),
.length= SZ_16K,
.type= MT_DEVICE,
}, {
.virtual= (unsigned long)VA_VIC1,
.pfn= __phys_to_pfn(S5P_PA_VIC1),
.length= SZ_16K,
.type= MT_DEVICE,
}, {
.virtual= (unsigned long)S3C_VA_TIMER,
.pfn= __phys_to_pfn(S5P_PA_TIMER),
.length= SZ_16K,
.type= MT_DEVICE,
}, {
.virtual= (unsigned long)S5P_VA_GPIO,
.pfn= __phys_to_pfn(S5P_PA_GPIO),
.length= SZ_4K,
.type= MT_DEVICE,
},
};
如果这个结构体中对应的我们想要的物理地址和虚拟地址的映射没有的话,我们可以自己在这个结构体中进行添加
iotable_init函数的第一个参数是这个结构体,这个函数的作用的就是将这个结构体数组所表示的映射表,转换成了MMU能够识别的页表映射关系,建立MMU识别的
页表映射关系,这样当我们开机的时候,MMU就可以直接帮我们使用的虚拟地址对应上物理地址了
(3)开机时调用映射表建立函数
内核在启动的时候,什么怎么调用到了smdkc110_map_io这个函数,从而在调用了iotable_init函数去建立真正的MMU所用的映射表呢?
内核启动第一阶段汇编结束后,进入了C阶段。
start_kernel
setup_arch
paging_init
devicemaps_init中的
if (mdesc->map_io)
mdesc->map_io();这句代码会调用smdkc110_map_io这个函数,重而调用了映射表建立函数iotable_init,将MMU能识别的映射表建立起来
map_io这成员是在arch/arm/mach-s5pv210/mach-smdkc110.c中的最后面,用一个宏来定义了这个类型的结构体,将成员实例化得
MACHINE_START(SMDKV210, "SMDKV210")
#endif
.phys_io= S3C_PA_UART & 0xfff00000,
.io_pg_offst= (((u32)S3C_VA_UART)
>> 18) & 0xfffc,
.boot_params= S5P_PA_SDRAM + 0x100,
//.fixup= smdkv210_fixup,
.init_irq= s5pv210_init_irq,
.map_io= smdkc110_map_io,//这个
.init_machine= smdkc110_machine_init,
.timer= &s5p_systimer,
11、动态映射结构体的方式操作寄存器
12、内核提供的专用读写寄存器的接口
(1)writel和readl
@1:writel(v, c)//向c内存地址开始的四个字节内存地址中写入v内容,写寄存器
@2:readl(c)//读取c内存地址开始的四个字节内存中的内存,读寄存器
(2)iowrite32和ioread32
@1:iowrite32(v, p) //向p内存地址开始的四个字节内存中写入v。写寄存器
@2:ioread32(p)//读取p内存地址开始的四个字节内存中的内容,读寄存器