我是
靠谱客的博主
默默云朵,最近开发中收集的这篇文章主要介绍
基于framebuffer(fb)的驱动分析 基于framebuffer的驱动分析,觉得挺不错的,现在分享给大家,希望可以做个参考。
概述
基于framebuffer的驱动分析
framebuffer帧缓冲(简称fb)是linux内核中用代码虚拟出的一个设备,是一个platform类型设备,设备文件位于/dev/fb*
- 在嵌入式系统中一般没有专门的显存,而仅仅是从RAM(SDRAM)空间中分配一段显示缓冲区
- framebuffer的作用是:向应用层提供一个统一标准接口的显示设备。不论最终输出是通过hdmi还是lcd控制器,可以认为所有的GUI都是向fb输出画面的
- 实际上是frambuffer就是linux内核驱动申请的一片内存空间,然后lcd内有一片sram,cpu内部有个lcd控制器,它有个单独的dma用来将frambuffer中的数据拷贝到lcd的sram中去,拷贝到lcd的sram中的数据就会显示在lcd上,具体数据的内容是由应用程序控制的。
- LCD驱动和framebuffer驱动没有必然的联系,它只是驱动LCD正常工作的,比如有信号传过来,那么LCD驱动负责把信号转成显示屏上的内容,至于什么内容,怎么显示,它根本不关心也不知道。
- 对于现代LCD,有一种“多屏叠加”的机制,即一个LCD设备可以有多个独立虚拟屏幕,以达到画面叠加的效果。所以fb与LCD不是一对一的关系,在常见的情况下,一个LCD对应了fb0~fb4。像QT这种GUI会默认把画面输出到fb0
1.画面输出原理
- 如何输出画面?应用程序通过往显存中写数据,LCD控制器将自动把显存中的数据映射到lcd屏幕。映射是全自动的,应用层负责往显存里写数据,而驱动要做的仅仅是配置LCD控制器、创建显存罢了
- 由于显存实际是处于内核态的物理内存,所以要把这块物理内存映射到用户态,所谓“映射”就可以理解为建立了一个“符号链接”,这样应用程序就可以直接操作这块物理内存了
- 关于LCD的硬件原理详见LCD简介
- 关于如何在应用层测试fb,详见framebuffer的使用与测试
2.framebuffer驱动结构
fb的结构和misc极为类似,由内核中的fb框架实现一部分,然后再由设备驱动本身实现一部分。设备驱动本身就是一个普通的platform总线驱动
虽然每家原厂写的fb设备驱动可能有些差异,但是基本的套路还是相同的
- 在内核fb框架中,所有的fb设备公用一个主设备号(都是29),它们之间以次设备号互相区分。所以在框架中使用register_chrdev注册了一个主设备号为29的设备,而在驱动中device_create创建设备文件主设备号都为29,次设备号不同
- 由上图可以看出,内核提供了fb框架,原厂提供了fb的platform设备和驱动;不论有多少LCD屏幕,用的都是这一套platform驱动,它们的操作方式都是固定的,唯一的区别就在platform_data里的硬件参数。而我们驱动工程师重点关注的就是该硬件参数
3.修改LCD的硬件参数(2.6版本内核)
当我们板子上的LCD需要更换时,驱动中也需要进行相应的修改
- 具体的思路,是去修改platform_device中的platform_data,LCD的硬件参数都在里面,但是怎么找到这个platform_data是一门学问,因为原厂写的代码是比较复杂的。。。。具体方法详见基于platform总线的驱动分析 的文末
- 不难找到设置platform_data的地方,如图
那么目前导入的platform_data是哪一个呢?根据menuconfig和xxxxdefconfig,分析可知是ek070tn93_fb_data
:
static struct s3c_platform_fb ek070tn93_fb_data __initdata = {
.hw_ver = 0x62,
.nr_wins = 5,
.default_win = CONFIG_FB_S3C_DEFAULT_WINDOW,
.swap = FB_SWAP_WORD | FB_SWAP_HWORD,
.lcd = &ek070tn93,
.cfg_gpio = ek070tn93_cfg_gpio,
.backlight_on = ek070tn93_backlight_on,
.backlight_onoff = ek070tn93_backlight_off,
.reset_lcd = ek070tn93_reset_lcd,
};
- 里面最关键的是
.lcd
这个成员,即结构体ek070tn93
,查看发现里面有时序、分辨率等各种参数,这样我们就可以随便修改参数了
static struct s3cfb_lcd ek070tn93 = {
.width = S5PV210_LCD_WIDTH,
.height = S5PV210_LCD_HEIGHT,
.bpp = 32,
.freq = 60,
.timing = {
.h_fp = 210,
.h_bp = 38,
.h_sw = 10,
.v_fp = 22,
.v_fpe = 1,
.v_bp = 18,
.v_bpe = 1,
.v_sw = 7,
},
.polarity = {
.rise_vclk = 0,
.inv_hsync = 1,
.inv_vsync = 1,
.inv_vden = 0,
},
};
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
4.修改LCD的硬件参数(3.0+版本内核)
对于新的内核,platformdata都包含在了dts中,所以需要在dts中修改LCD的硬件参数。有关设备树详见设备树详解
- 首先进入我们项目的dts以及包含的dtsi,寻找合适的地方安放我们新增的时序,一般某个dtsi里有一个叫display-timings的节点,里面会放时序。其实说实话时序放在哪里根本就无所谓,因为lcd/ldb节点是通过标号来访问具体的时序节点的,我们之所以选择放在display-timings里,仅仅是为了规范一点
display-timings {
lq4851lg03:lvds_1280x480_53M{
clock-frequency = <53172000>;
hactive = <1280>;
vactive = <480>;
hback-porch = <268>;
hfront-porch = <70>;
vback-porch = <10>;
vfront-porch = <10>;
hsync-len = <70>;
vsync-len = <25>;
pixelclk-active = <0>;
};
ak070tn93:ttl_1280x480_45M{
clock-frequency = <45000000>;
hactive = <1280>;
vactive = <480>;
hback-porch = <40>;
hfront-porch = <73>;
vback-porch = <20>;
vfront-porch = <23>;
hsync-len = <20>;
vsync-len = <10>;
};
xxxxx:xxxxx{
/*需要添加的参数*/
};
};
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 那么我们有两种方案,第一种是直接在默认的时序上修改,第二种添加一个时序,并在项目的dts中使用该时序。在此,我们选择第二种更优越的方法
- 首先在已有的时序后面添加一个时序,具体参数照抄datasheet即可,唯一要注意的是某几个参数的名字可能和datasheet上不同:hsync-len对应datasheet上的Horizontal pulse width;vsync-len 对应datasheet上的Vertical pulse width。那么上面代码中的
pixelclk-active = <0>;
意味着什么呢?这个元素代表时钟的极性,如果显示图片清晰度不足时,可以尝试加入该元素 - 打开我们项目的dts,然后可以做如下的修改
&mxcfb1 {
disp_dev = "lcd";
};
&ldb {
status = "disabled";
};
&lcd {
status = "okay";
native-mode = <&xxxx>;
};
- 可以看到有三项,mxcfb1、ldb、lcd。mxcfb1里面的disp_dev决定了图像通过什么方式输出,显然这里有两种方式,ldb和lcd,即lvds输出或ttl输出RGB信号。这里我们选择了lcd(即ttl输出RGB信号),那么ldb显然是要被”disabled”了,而lcd显然是”okay”,并且lcd的native-mode选择了我们刚刚添加的时序
- 有时,除了时序之外,显示的模式可能也需要设置,比如某个屏幕需要jeida的data-mapping模式,而我们项目中的dtsi中并未设置过:
ldb: ldb@020e0008 {
#address-cells = <1>;
#size-cells = <0>;
gpr = <&gpr>
status = "disabled"
lvds-channel@0 {
reg = <0>
status = "disabled"
}
- 那么在我们项目的dts中就要对其进行引用,并设置(类似于重写)
&ldb {
status = "okay";
lvds-channel@0 {
native-mode = <&SHARP_LQ123B5LW>;
fsl,data-mapping = "jeida";
status = "okay";
};
};
5.修改logo显示(2.6版本内核)
注意:本段关于logo显示的ne
当kernel启动,在probe函数运行时,一般会往fb中输出一个小企鹅logo(开发板厂商可能会改成其他的)。很多时候产品是不需要这个企鹅logo的,我们要学会去修改它
- 在probe函数中,我们可以发现调用了显示logo的相关代码,下面是三星写的,其他厂家应该也不会有很大的区别:
#if !defined(CONFIG_FRAMEBUFFER_CONSOLE) && defined(CONFIG_LOGO)
if (fb_prepare_logo( fbdev->fb[pdata->default_win], FB_ROTATE_UR)) {
printk("Start display and show logon");
fb_set_cmap(&fbdev->fb[pdata->default_win]->cmap, fbdev->fb[pdata->default_win]);
fb_show_logo(fbdev->fb[pdata->default_win], FB_ROTATE_UR);
}
#endif
- 不难发现如果要让logo消失,只需要让CONFIG_LOGO不被定义即可,即在menuconfig中配置,或者直接修改xxxdefconfig
- 那么如果我们想要添加另外的logo呢?linux的启动logo全部放在drivers/video/logo/下,而且都是以一种专门的格式存在的,称之为ppm格式。我们可以使用专门的工具把png格式图片转成ppm格式,ubuntu里也有这个工具,网上教程很多
- 假设添加完了,我们要选择该logo,怎么选择?很简单,这些logo也是由kconfig管理的,我们只要在drivers/video/logo/下的kconfig和makefile中照葫芦画瓢,添加我们的logo,然后就能在menuconfig中选择了!
- 有时,我们做了一个很小的logo,比屏幕像素小得多,它会被系统放到屏幕左上角。那么如何把它放到屏幕正中央呢?有两种思路,第一种方法是把logo的整个画面做的和屏幕分辨率一样大小,这样自然就对齐到中间了;第二种方法是修改显示logo函数的参数。首先找到probe函数中的
fb_show_logo
,在进去一层,找到fb_show_logo_line
,查看其定义,发现里面有两行:
image.dx = 0
image.dy = y
这便是logo图像的坐标偏移量,只需改成恰当的值即可让logo显示到中央了
/*********************************************************************************************/
b_fix_screeninfo 和 fb_var_screeninfo
fb_fix_screeninfo 和 fb_var_screeninfo 都和 frame buffer 有关。
结构体的成员变量
- struct fb_fix_screeninfo {
- char id[16];
- unsigned long smem_start;
-
- __u32 smem_len;
- __u32 type;
- __u32 type_aux;
- __u32 visual;
- __u16 xpanstep;
- __u16 ypanstep;
- __u16 ywrapstep;
- __u32 line_length;
- unsigned long mmio_start;
-
- __u32 mmio_len;
- __u32 accel;
-
- __u16 reserved[3];
- };
结构体的成员变量
- struct fb_var_screeninfo {
- __u32 xres;
- __u32 yres;
- __u32 xres_virtual;
- __u32 yres_virtual;
- __u32 xoffset;
- __u32 yoffset;
-
- __u32 bits_per_pixel;
- __u32 grayscale;
-
- struct fb_bitfield red;
- struct fb_bitfield green;
- struct fb_bitfield blue;
- struct fb_bitfield transp;
-
- __u32 nonstd;
-
- __u32 activate;
-
- __u32 height;
- __u32 width;
-
- __u32 accel_flags;
-
-
- __u32 pixclock;
- __u32 left_margin;
- __u32 right_margin;
- __u32 upper_margin;
- __u32 lower_margin;
- __u32 hsync_len;
- __u32 vsync_len;
- __u32 sync;
- __u32 vmode;
- __u32 rotate;
- __u32 reserved[5];
- };
fb_fix_screeninfo 的 line_length 成员,含义是一行的 size,以字节数表示,就是屏幕的宽度。
结 构fb_var_screeninfo定义了视频硬件一些可变的特性。这些特性在程序运行期间可以由应用程序动态改变
6.在LCD上划线
***************************************************************************************************************************************
- #include <stdlib.h>
- #include <unistd.h>
- #include <stdio.h>
- #include <fcntl.h>
- #include <linux/fb.h>
- #include <linux/kd.h>
- #include <sys/mman.h>
- #include <sys/ioctl.h>
- #include <sys/time.h>
- #include <string.h>
- #include <errno.h>
- struct fb_var_screeninfo vinfo;
- struct fb_fix_screeninfo finfo;
- char *frameBuffer = 0;
-
-
- void printFixedInfo ()
- {
- printf ("Fixed screen info:n"
- "tid: %sn"
- "tsmem_start:0x%lxn"
- "tsmem_len:%dn"
- "ttype:%dn"
- "ttype_aux:%dn"
- "tvisual:%dn"
- "txpanstep:%dn"
- "typanstep:%dn"
- "tywrapstep:%dn"
- "tline_length: %dn"
- "tmmio_start:0x%lxn"
- "tmmio_len:%dn"
- "taccel:%dn"
- "n",
- finfo.id, finfo.smem_start, finfo.smem_len, finfo.type,
- finfo.type_aux, finfo.visual, finfo.xpanstep, finfo.ypanstep,
- finfo.ywrapstep, finfo.line_length, finfo.mmio_start,
- finfo.mmio_len, finfo.accel);
- }
-
-
- void printVariableInfo ()
- {
- printf ("Variable screen info:n"
- "txres:%dn"
- "tyres:%dn"
- "txres_virtual:%dn"
- "tyres_virtual:%dn"
- "tyoffset:%dn"
- "txoffset:%dn"
- "tbits_per_pixel:%dn"
- "tgrayscale:%dn"
- "tred: offset:%2d, length: %2d, msb_right: %2dn"
- "tgreen: offset:%2d, length: %2d, msb_right: %2dn"
- "tblue: offset:%2d, length: %2d, msb_right: %2dn"
- "ttransp: offset:%2d, length: %2d, msb_right: %2dn"
- "tnonstd:%dn"
- "tactivate:%dn"
- "theight:%dn"
- "twidth:%dn"
- "taccel_flags:0x%xn"
- "tpixclock:%dn"
- "tleft_margin:%dn"
- "tright_margin: %dn"
- "tupper_margin:%dn"
- "tlower_margin:%dn"
- "thsync_len:%dn"
- "tvsync_len:%dn"
- "tsync:%dn"
- "tvmode:%dn"
- "n",
- vinfo.xres, vinfo.yres, vinfo.xres_virtual, vinfo.yres_virtual,
- vinfo.xoffset, vinfo.yoffset, vinfo.bits_per_pixel,
- vinfo.grayscale, vinfo.red.offset, vinfo.red.length,
- vinfo.red.msb_right,vinfo.green.offset, vinfo.green.length,
- vinfo.green.msb_right, vinfo.blue.offset, vinfo.blue.length,
- vinfo.blue.msb_right, vinfo.transp.offset, vinfo.transp.length,
- vinfo.transp.msb_right, vinfo.nonstd, vinfo.activate,
- vinfo.height, vinfo.width, vinfo.accel_flags, vinfo.pixclock,
- vinfo.left_margin, vinfo.right_margin, vinfo.upper_margin,
- vinfo.lower_margin, vinfo.hsync_len, vinfo.vsync_len,
- vinfo.sync, vinfo.vmode);
- }
下面才是我们的重点,这个代码是我自己参考别人画矩形的代码改过来的
-
- void drawline_rgb16 (int x0,int y0, int width,int height, int color,int flag0)
- {
- const int bytesPerPixel = 2;
- const int stride = finfo.line_length / bytesPerPixel;,一行有多少个点
- const int red = (color & 0xff0000) >> (16 + 3);
- const int green = (color & 0xff00) >> (8 + 2);
- const int blue = (color & 0xff) >> 3;
- const short color16 = blue | (green << 5) | (red << (5 +6));
- int flag=flag0;
-
-
- short *dest = (short *) (frameBuffer)+ (y0 + vinfo.yoffset) * stride + (x0 +vinfo.xoffset);
-
- int x=0,y=0;
- if(flag==0)
- {
- for (x = 0; x < width; ++x)
- {
- dest[x] = color16;
- }
- }
- else if(flag==1)
- {
- for(y=0;y<height;y++)
- {
- dest[x]=color16;
-
- dest +=stride;
- }
- }
- }
解释:我的屏的lcd分辨率是480*272,分辨率的意思是一行有480个点,一共有272行,其实屏蔽上都是一个个点组成的,在上面画线的意思并不是真正意思上的拿一支笔画线。打个比方来说你你把一行中80-180个点都改成红色(我们屏蔽不是黑色么),改完你就可以看见一条红线了,感觉就是画了一条红色的直线对不对?
而且“上色”是从左到右一个点一个点,一行一行“上色”的,屏幕的坐标系如下图所示:
- short *dest = (short *) (frameBuffer)+ (y0 + vinfo.yoffset) * stride + (x0 +vinfo.xoffset);
上面这一行代码的具体意思就是定位到(x0,y0)这个坐标,也就是我们要画的其实位置
可以下面这个代码画一个矩形。
-
- void drawRect_rgb16 (int x0, int y0, int width,int height, int color)
- {
- const int bytesPerPixel = 2;
- const int stride = finfo.line_length / bytesPerPixel;
- const int red = (color & 0xff0000) >> (16 + 3);
- const int green = (color & 0xff00) >> (8 + 2);
- const int blue = (color & 0xff) >> 3;
- const short color16 = blue | (green << 5) | (red << (5 +6));
-
- short *dest = (short *) (frameBuffer)+ (y0 + vinfo.yoffset) * stride + (x0 +vinfo.xoffset);
-
- int x, y;
- for (y = 0; y < height; ++y)
- {
- for (x = 0; x < width; ++x)
- {
- dest[x] = color16;
- }
- dest += stride;
- }
- }
下面是main函数:
- int main (int argc, char **argv)
- {
- const char *devfile = "/dev/fb0";
- long int screensize = 0;
- int fbFd = 0;
-
-
-
-
- fbFd = open (devfile, O_RDWR);
- if (fbFd == -1)
- {
- perror ("Error: cannot open framebuffer device");
- exit (1);
- }
-
-
- if (ioctl (fbFd, FBIOGET_FSCREENINFO, &finfo) == -1)
- {
- perror ("Error reading fixed information");
- exit (2);
- }
- printFixedInfo ();
-
- if (ioctl (fbFd, FBIOGET_VSCREENINFO, &vinfo) == -1)
- {
- perror ("Error reading variable information");
- exit (3);
- }
- printVariableInfo ();
-
-
- screensize = finfo.smem_len;
-
-
- frameBuffer =(char *) mmap (0, screensize, PROT_READ | PROT_WRITE, MAP_SHARED,fbFd, 0);
- if (frameBuffer == MAP_FAILED)
- {
- perror ("Error: Failed to map framebuffer device to memory");
- exit (4);
- }
-
-
-
- drawline_rgb16(50,80,260,0,0xffff0000,0);
-
- drawline_rgb16(160,10,0,180,0xff00ff00,1);
- sleep (2);
- printf (" Done.n");
-
- munmap (frameBuffer, screensize);
-
- close (fbFd);
- return 0;
- }
用一个流程图还说明一下mian函数吧
*****************************************************************************************************************************************************************************************
****************************************************************************************************************************************************************************************
这里最重要的就是mmap这个函数了
void *mmap(void *addr, size_t len, int prot, int flags, int fd, off_t offset);
addr指定文件应被映射到进程空间的起始地址,一般被指定一个空指针,此时选择起始地址的任务留给内核来完成。函数的返回值为最后文件映射到进程空间的地址,进程可直接操作起始地址为该值的有效地址。
len是映射到调用进程地址空间的字节数,它从被映射文件开头offset个字节开始算起。
prot参数指定共享内存的访问权限。可取如下几个值的或:PROT_READ(可读),PROT_WRITE(可写),PROT_EXEC(可执行),PROT_NONE(不可访问)。
flags由以下几个常值指定:MAP_SHARED, MAP_PRIVATE, MAP_FIXED。其中,MAP_SHARED,MAP_PRIVATE必选其一,而MAP_FIXED则不推荐使用。
如果指定为MAP_SHARED,则对映射的内存所做的修改同样影响到文件。如果是MAP_PRIVATE,则对映射的内存所做的修改仅对该进程可见,对文件没有影响。
offset参数一般设为0,表示从文件头开始映射。
mmap使得进程之间通过映射同一个普通文件实现共享内存。普通文件被映射到进程地址空间后,进程可以像访问普通内存一样对文件进行访问,不必再调用read(),write()等操作。
再来看看我们代码中的mmap的实例
frameBuffer =(char *) mmap (0, screensize, PROT_READ | PROT_WRITE, MAP_SHARED,fbFd, 0);
前面我们不是说了frambuffer就是linux内核驱动申请的一片内存空间,lcd驱动将frambuffer的地址通过mmap()将这片内存映射到应用程序空间,这样我们写入到fb的数据就写入到内核驱动里的frambuffer中去了,而lcd 的dma就将这些数据写入到lcd的sram中,从而显示在lcd上.
最后
以上就是默默云朵为你收集整理的基于framebuffer(fb)的驱动分析 基于framebuffer的驱动分析的全部内容,希望文章能够帮你解决基于framebuffer(fb)的驱动分析 基于framebuffer的驱动分析所遇到的程序开发问题。
如果觉得靠谱客网站的内容还不错,欢迎将靠谱客网站推荐给程序员好友。
本图文内容来源于网友提供,作为学习参考使用,或来自网络收集整理,版权属于原作者所有。
发表评论 取消回复