Linux设备驱动之Framebuffer分析

在Linux内核中,Framebuffer(帖缓冲)驱动是显示驱动的标准,Framebuffer将显示设备抽象为帖缓冲区,用户通过内存映射到进程地址空间之后,就可以直接进行读写操作,且写操作可以立即在屏幕上进行显示,在Linux内核/linux/drivers/video/下有相关的显示驱动与接口,其中Frmaebuffer驱动接口为fbmem.c,此文件提供了LCD驱动的通用文件操作接口,如read 、write、 ioctl等应用程序可能应用到的文件接口,特定平台的LCD驱动程序可以实现自己的文件操作接口,也可以直接应用fbmem.c中所提供的文件操作接口,在一般情况下,通常应用fbmem.c所提供的文件操作接口,因为内核所提供的文件操作接口几乎能满足我们的需求,如果有特别要求,可以选择性的重新实现相关的文件操作接口。至于是怎么进行连接的,我将在后面进行分析。再进行分析fbmem.c之前,先来看看几个重要的结构体。所有Freambuffer所用到的结构体都定义在/include/linux/fb.h中,有兴趣的读者可以去看看源码,这样也许会有更大的收获,特别是ioclt所需要应用的宏定义,如果不知道这些宏定义的用途,那么就不知道怎么在应用程序中应用ioctl函数来控制LCD以实现相关的功能。跳过相关的宏定义(对于相关宏定义,请自行查看内核源码,在此不进行说明)在第136行中定义了struct fb_fix_screeninfo结构体,这个结构体用于描述显卡自身的属性,包含识别符、缓存地址、显示类型等,但是注意的一点是,当LCD系统正常运行后,不能修改此结构体的值。再一个与显卡相关的结构体是struct fb_var_screeninfo结构体,此结构用于描述显卡的一般特性,比如实际分辨率,虚拟分辨率,实际分辨率与虚拟分辨率之间的位移等,可以说,这是一个非重要的结构体,它决定了将进行驱动的LCD的尺寸等特性。在进行LCD显示时,通常需要进行相关的颜色设置,struct fb_cmap用于描述设备无关的颜色映射信息。可以通过FBIOGETCMAP 和 FBIOPUTCMAP对应的ioctl 操作设定或获取颜色映射信息。还有一些比如光标、图片等信息有关的结构体不进行说明,内核中对每个结构体中的相关域都做了详细的注释,相信我亲爱的读者能够看懂。Famebuffer驱动有自己特有的文件操作接口fb_ops,该结构体的定义位于fb.h中的第543行,在应用fbmem.c驱动接口来书写自的LCD驱动时,可以不必完全实现所有的域,但是,有下面的几个域是必须要进行实现的。第一:void (*fb_fillrect) (struct fb_info *info, const struct fb_fillrect *rect);该函数是应用显卡的加速功能进行添充一个矩形,如果显卡不支持硬件加还功能,可以应用常规操作来代替,但是一定要在自己特定平台的LCD驱动程序实现这个函数,其实,这个域只是一个函数指针域,也就是不能让fb_fillrect这个批针为NULL,就行了。第二个是:void (*fb_copyarea) (struct fb_info *info, const struct fb_copyarea *region);用于进行区域的复制。第三个是:void (*fb_imageblit) (struct fb_info *info, const struct fb_image *image);用于在屏幕上画一幅图片。第四个是:int (*fb_cursor) (struct fb_info *info, struct fb_cursor *cursor);对于这四个函数,如果不想实现的话,可以应用内核给我们提供的函数,让这四个函数指针分别指向这些函数,这四个函数为:cfbcopyarea.c中的cfb_copyarea;cfbfillrect.c中的cfb_fillrect,softcursor.c中的soft_cursor, cfbimgbit.c中的cfb_imagedbit,请查看相关的源码文件。所以,准确来说,这四个函数也不是一定要自己实现,只是必须不要使这四个函数指针为NULL即可。最后一个最重要的结构体是struct fb_info,这个结构体用于描述当前显卡的状态,fb_info只能在内核中可见,struct fb_ops *fbops这个域指向上述的结构体,用来进行Framebuffer独有的文件操作。现代显卡不仅支持单通道显示,也支持多通道显示,每个显示法必须拥有一个自己的独立的数据区,所以,也就是说,不同显示方法可以共享显卡,所以fb_info就是用于区分不同显示方法的结构,每一个显示方法必须拥用自己独立的fb_info,如果支持多显,则必须定义fb_info数组或者动态内存分配多个fb_info结构体变量。好了,是时候分析fbmem.c的时候了,在此文件的最开始就定了一个fb_info数组,用于支持不同的显卡或者显示方法,如下:struct fb_info *registered_fb[FB_MAX];共中FB_MAX为32,在fb.h可见其宏定义,在这里我们找几个重要的函数进行一下说明就行了,其它函数请读者自己进行分析。在此不都进行分析。char* fb_get_buffer_offset(struct fb_info *info, struct fb_pixmap *buf, u32 size)此函数应用获取buffer offset,在这个函数中,有一点最为重要的就是I/O同步显示,代码如下所示:if (buf->flags & FB_PIXMAP_IO) {if (info->fbops->fb_sync && (buf->flags & FB_PIXMAP_SYNC))info->fbops->fb_sync(info);return addr;}还有一段代码也就用于支持同步的if (info->fbops->fb_sync && (buf->flags & FB_PIXMAP_SYNC))info->fbops->fb_sync(info);也许细心的读者发现有这个一句info->fbops->fb_sync(info)这是fb_ops结构体中的一个函数指针域,我在上面的分析中说过,这个域可以为空,因为在这个函数中,在操作这个域时,都先进行判断此域是否为空,其实,fb_ops中那些域可以为NULL,那么不可以为NULL,看一下fb_mem.c中的源码就可以知道。在int fb_show_logo(struct fb_info *info)函数中,需要应用fb_ops结构体中的fb_imageblit域来在屏幕上画一幅图象,且是必须的,所以,fb_ops结构中fb_imageblit域不能为NULL,如果为NULL,那么这个函数就没有办法完成功能,也就是一个无用的函数,在函数的引用为fbmem.c中的420行,引用过程如下:for (x = 0; x < num_online_cpus() * (fb_logo.logo->width + 8) &&\ x <= info->var.xres-fb_logo.logo->width; x += (fb_logo.logo->width + 8)) {image.dx = x;info->fbops->fb_imageblit(info, &image);}当系统调用read、write、open、release等函数时,首先调用fbmem.c中实现的文件接口函数,再进行判断fb_ops中的相关域是否为NULL,如果不为NULL,则在适当的地方调用fb_ops中相应的函数来完成相关的功能。但是,关于在应用程序中调用ioctl会得到什么结果,需要什么参数,请查看fb_ioctl,进此不进行说明,因为这个函数太长也太复杂,不是一两句能分析清楚的。不过在此说一下fb_read与fb_write函数中在进行用户空间与内核空间进行数据交换时,有一个小小的技巧,这个技巧在我们编程过程中是比较有用的,特别在源地址不能直接进行交换数据的情况下。首先我们不能确定我们的数据都为4个字节的整数倍,所以,我们必须分开来处理,是4个字节的整数倍的部分,我们一次移动4个字节,这对于ARM这样的32位处理器来说是最好的,因为这样寻址最快,余下的部分就只能一个字节一字节的移动了,很多人也许会想到应用求模来求出不足4字节的部分,这对于ARM这样的处理器来说,求模与除法有点难度,因为硬件不直接支特除法,所以,我们只能应用移位的方法来处理,请认真分析源码,特别if (c & 3)(用于判断是否为4的整数倍),for (i = c & 3; i–;)这两句对我们用于处理缓冲区的时候很有用,比如,我们有一个能容纳8个unsigned int型的环形缓冲区,在缓冲下标到达8时,我们需要从0开始,很多人也许会用到下面的语句:if (index == 8)index = 0;其实,这对代码效率的优化有一定的影响,当然也不太大,但是,在一个工程中,大的影响都是从小的影响开始的,所以,我们最好应用这样一句语句来代替它,这样一样的能完成任务,而完成得更好:index &= 0x7;能看得出怎么做到的吧,如果在7的范围内,index的值不会做任何的改变,但是一但是8,也就是0x8,写出其二进制一看就知道了,0x8的二进制为(1000),而0x7的二进制为(0111),所以进行按位与当然又回到了0,但是,应用这个技巧是有一个条件的,那就是缓冲区的的大小必须为2的次方,才能正确应用这个技巧,好了,对于这个技巧不再进行说明了,但是,请注意一点,每一个小小的优化对整个工程都是一个很大的优化。对于对代码优化感兴趣的读者,可以参考一下《ARM嵌入式系统开发-软件设计与优化》这本书,这是我的老师给我推荐的。fb_read进行数据处理的部分代码如下所示:while (count) {c= (count > PAGE_SIZE) ? PAGE_SIZE : count;dst = buffer;for (i = c >> 2; i–; )*dst++ = fb_readl(src++);if (c & 3) {u8 *dst8 = (u8 *) dst;u8 __iomem *src8 = (u8 __iomem *) src;for (i = c & 3; i–;)*dst8++ = fb_readb(src8++);src = (u32 __iomem *) src8;}if (copy_to_user(buf, buffer, c)) {err = -EFAULT;break;}*ppos += c;buf += c;cnt += c;count -= c;}很经典的一小段代码,请慢慢的消化,看Linux内核源码的其中一个最大的收获就是学到很多经典的算法,这些算法在实际编程中很有用,有时候对某一个工程来言也许起到关键性的因素。另一个,也是LCD驱动中最重要的功能函数之一就是fb_mmap函数,看过ARM中LCD控制器的读者都知道,在一般显卡中都有显存的存在,但是对于嵌入式系统而言,几乎没有谁会在CPU上集成显卡也是不可能的事,所以,一般应用内存映射为LCD显存地址,应用DMA或者其它方式把此缓冲区的内容在不经过CPU的情况下进行显示,因为图片、视频等数据非常大,如果应用CPU直接处理,那么CPU的工作会非常之重同时显示也许不尽人意,同样,在Linux内核与应用程序之间,为了减少内核把应用程序中的数据先复制到内核空间的时间(请注意用户空间与内核空间的不同),也需要进行内存的映射,目的是为了使用户空间与内核空间都可以直接应用此内存,将Linxu内核空间与用户空间内存和应用内存为LCD显存结合起来,就可以知道这个函数的用途有多大,首先,它完成用户空间与内核空间可以直接使用内一块内存的功能,在此同时,将此内存映射为LCD显存,应用程序可以直接读写该内存,在LCD屏幕上立即得到显示。比如将一张照片写入该内存,那么在LCD上立即显示此照片,本人在最后将给大家一个例子,也就是一个显示照片的例子。关于这个函数是怎么实现的,请查看源码;应用程序是怎么应用mmap函数的,请查看相关的资料。在此不进行详细说明了。在这个文件中还提供了两个函数给我们进行注册与注销我们特定平台的LCD驱动程序。int register_framebuffer(struct fb_info *fb_info),这个函数应用完成特定平台LCD驱动的注册;int unregister_framebuffer(struct fb_info *fb_info)这个函数用于注销特定平台的LCD驱动。关于此文件的其实函数请读者自行查看源码并进行分析,在此不进行相关的说明。在video目录下,还有一个skeletonfb.c文件,该文件是一个框架,主要是说明怎样应用fb_mem.c所提供的register_framebuffer与unregister_framebuffer这两个函数来实现特定平台LCD驱动的开发过程,有详细的说明,如果能把这个文件看懂,那么在这个文件的基础上进行添充相关的实现,就可以开发出一个特定平台的LCD驱动程序,在此目录下,还有很多其它的LCD驱动程序,比如sa1100fb.c,这是一个StrongARM 1100 LCD的驱动程序;还有一个vfb.c,这一个虚拟LCD驱动程序,其实,Linux内核中的很多源码是很有用的,我们可以直接应用或者修改之而用之,问题在于你必须明确知道你的需要与内核源码中的相关驱动的硬件特性与你所需要的差别,修改这些差别也就为你所用,但是,这也就是困难之处,不像其它程序,如果C不够好,同时对整个内核驱动构架不了解(或者某一子系统不了解),看懂内核源码很困难,想修改为之所用更加困难。对于应用S3C2440或者S3C2410等的朋友,可以直接应用内核提供的s3c2410fb.c这个文件,这是一个对于S3C2440和S3C2410都实用的LCD驱动,如果已经编译进内核中,一般目录为/dev/fb/0,当然也有可能不是这个,请查看相关的资料,也就是说,我们在应用程序中可以直接应用open函数打开此设备文件,这样就可以直接操作LCD显示了。s3c2410fb.c是作者参考skeletonfb.c, sa1100fb.c等相关驱动书写的,还是一句话,看懂,修改并用之,这也许是Linux设备驱动开发做得最多的事,也许也是Linux在嵌入式受喜欢的一个原因为,正如上面所说的,看懂,很难。关于s3c2410fb.c的实现过程,大体框架与skeletonfb.c差不多,请读者自己查看skeletonfb.c与S3C2410或者S3C2440手册中LCD控制器一节。在s3c2410fb.c中需要说明的是static void s3c2410fb_set_lcdaddr函数,这个函数真正的完成了把用户空间与内核空间共的内存映射为LCD显存,所以在此做一下详细说明,在S3C2440手册LCD控制器一节中,可以看到LCSADDR1寄存器,此寄存器主要是用于存储LCD显存的起始地址,不过我们只用到1到30,所以,我们需要把内存的起始地址右移一位。关于LCDSADDR1的相关说明如下:LCDBANK[29:21]:These bits indicate A[30:22] of the bank location for the video buffer in the system memory. LCDBANK value cannot be changed even when moving the view port. LCD frame buffer should be within aligned 4MB region, which ensures that LCDBANK value will not be changed when moving the view port. So, care should be taken to use the malloc()function.LCDBASEU [20:0]:For dual-scan LCD : These bits indicate A[21:1] of the start address of the upper address counter, which is for the upper frame memory of dual scan LCD or the frame memory of single scan LCD.For single-scan LCD : These bits indicate A[21:1] of the start address of the LCD frame buffer.所以在此函数中把内存地址右移了一位,源码为:saddr1= fbi->fb.fix.smem_start >> 1;对于LCDSADDR2寄存器的处理要相当的烦琐一些,我们还是从CPU手册开始,在CPU手册中,LCDSADDR2的描述如下:LCDBASEL [20:0]:For dual-scan LCD: These bits indicate A[21:1] of the start address of the lower address counter, which is used for the lower frame memoryof dual scan LCD.For single scan LCD: These bits indicate A[21:1] of the end address of the LCD frame buffer.LCDBASEL = ((the frame end address) >>1) + 1= LCDBASEU +(PAGEWIDTH+OFFSIZE) x (LINEVAL+1)查看源码可知,我们应用的是单扫描方式,所以,A[21:1]应该为LCD 显存的结束地址,此函数中处理如下:saddr2= fbi->fb.fix.smem_start;/* 起始地址 */saddr2 += (var->xres * var->yres * var->bits_per_pixel)/8; /* 缓冲区大小 */saddr2>>= 1; /*执行((the frame end address) >>1)*/上面的代码请注意为什么没有执行加1操作。LCDSADDR3的处理相对来说较为简单多了,这个寄存器只处理相关的OFFSIZE各PAGEWIDTH,我们还是从CPU手册着手吧。其描述如下:OFFSIZE [21:11] Virtual screen offset size (the number of half words).This value defines the difference between the address of the last half word displayed on the previous LCD line and the address of the first half word to be displayed in the new LCD line.PAGEWIDTH [10:0] Virtual screen page width (the number of half words).This value defines the width of the view port in the frame.所以,函数中只是调用相关的宏来进行处理,源码如下:saddr3 =S3C2410_OFFSIZE(0) | S3C2410_PAGEWIDTH(var->xres);把这三个寄存器的内所需的值求出以后,最后写入到相关的寄存器就可以了,写入的源码如下:writel(saddr1, S3C2410_LCDSADDR1);writel(saddr2, S3C2410_LCDSADDR2);writel(saddr3, S3C2410_LCDSADDR3);到此,这个函数分析完了,在此也将就说明一下,看内核相关的源码时,请查看相关的芯片手册,如果对芯片的功能不了解,主要是芯片所需的时序,相关寄存器的值的规定,功能等不了解。你看懂的只是一串字符串,不知道其含义与驱动的功能。对于显卡的处理,也就是颜色的处理方式问题上,因为s3c2410fb所用的颜色模式为16BPP模式,我们可以查看S3C2440或者S3C2440 CPU手册,其对16BPP颜色模的描述如下:16 BPP color mode16 bits (5 bits of red, 6 bits of green, 5 bits of blue) of video data correspond to 1 pixel. But, stn controller will use only 12 bit color data. It means that only upper 4bit each color data will be used as pixel data (R[15:12], G[10:7], B[4:1]). The following table shows color data format in words:所以,对应的var结构体变量相应的域也需要进与此相对应,在s3c2410fb_check_var中做了如下的处理:if (var->bits_per_pixel == 16) {var->red.offset= 11;var->green.offset= 5;var->blue.offset= 0;var->red.length= 5;var->green.length= 6;var->blue.length= 5;var->transp.length= 0;} else {var->red.length= 8;var->red.offset= 0;var->green.length= 0;var->green.offset= 8;var->blue.length= 8;var->blue.offset= 0;var->transp.length= 0;}关于以红、绿、兰这三种颜色为基色,它们在1 word中的表示顺序与规则请自行查看相关的资料。关于其它函数请参照相关的芯片手册自行分析。在s3c2410fb.c中,我个人认为也就上述两点有一定的难度,因为它们不那么直观。需要相应的转化才能看懂。关于应用程序,必须定义相关的结构体变量来实现ioctl的应用,如果有需要的话,但是,必须定义fb_var_screeninfo与fb_fix_screeninfo结构体变量,因为我们需要获取驱动的相关信息,比如显示区域的大小,像素等重要信息,这在进行内存映射时必须用到的,因为如果不知道像素,显示区域的大小,我们不能准确的映射内存,那么对此内映的读写将没有太多的意义。本人在写应用测试程序时,能正确显示图片,但是在加入ASCII与中文字库后,想进行简单的显示ASCII与中文的操作,可是,没有成功,不知道是什么原因,裸机的也不行了,就是能正常显示图片,不能显示字符,以前做过的裸机LCD驱动实验现在只能看图片了,这同时也打乱了我的计划,因为本人想设计一个基于Linux操作系统的时钟系统(应用相关的GUI在LCD上画一个时钟表,并显示星期,同时也要求显示实时温度),做为嵌入式系统课程的课程设计。也许是LCD有什么问题吧,正在寻求答案中,如果那位朋友也见过这样的情况,并且给解决了,请告诉一下本人,本人急需知道问题所在。好了,我该停笔了,花了整整一个晚上的时间,但是我很乐意写,因为我习惯记录学习每一个知识点的理解,设备驱动文档,时间长了,可以做为复习资料。同时,也可以提高个人文档书写能力。特别是在嵌入式领域,驱动设计者很多时候不写相关的应用程序,这就得要求说明白驱动给应用程序提供的接口,比如应用ioctl需要宏定义,功能;读、写需要传入多大的内存地址,需要什么特别的数据结构、读出什么值,这些值需不需要进行进一步的变换,如果需要变换,应该怎么变换。比如,一个DS18B20的驱动,以其读温度值为例说明。由于用户空与内核空间只能传送char型数据,所以,一个16位的温度值,需要分开为两个值读取,这就要求说明,在读取的这两个值中,那一个是低8位,那一个是高8位,否则别人怎么进行数据的组合。说了很多,还是觉到没有说明白,这也许是本人的水平有限,不过,上面所说的都是本人现在水平所能及的了,如果有什么错误,请给予指正,让本人也有学习改正的机会,Linux操作系本是开源的。同时,若想研究控制台相关驱动的读者,有兴趣我们一起学习,Framebuffer后也许会学习之。在video目录下有一个子目录,console目录,此目录下都是一些关于控制台的相关驱动,特别是,在此目录下有大量的字库,当然没有中文的,关于中文字库在Mini2440开发板提供的UCOS操作系统实验中有一个16 * 16的中文字库,网上也有很多其它类型的中文字库,不过关键是要有使用说明,若有需要本人所做实验源码和与本人讨论问题的朋友,欢迎与我联系,本人很想找几个朋友一起学习,因为我们学校我不知道还有谁在学习我学习的这个方向,我所认识的范围内也就只有我一个人。一个人学习真的很没有味道,同时进步也很慢,因为有什么问题总是自己想,自己找资料,有时候别人一句话就可以解决的问题,我一个人也许需要几个小时或者几天的时间来解决。得意时应善待他人,因为你失意时会需要他们

Linux设备驱动之Framebuffer分析

相关文章:

你感兴趣的文章:

标签云: