本文共 35941 字,大约阅读时间需要 119 分钟。
一、开发环境
二、背景知识
|
|
|
|
1. 帧缓冲设备驱动在Linux子系统中的结构如下:
我们从上面这幅图看,帧缓冲设备在Linux中也可以看做是一个完整的子系统,大体由fbmem.c和xxxfb.c组成。向上给应用程序提供完善的设备文件操作接口(即对FrameBuffer设备进行read、write、ioctl等操作),接口在Linux提供的fbmem.c文件中实现;向下提供了硬件操作的接口,只是这些接口Linux并没有提供实现,因为这要根据具体的LCD控制器硬件进行设置,所以这就是我们要做的事情了(即xxxfb.c部分的实现)。2. 帧缓冲相关的重要数据结构: 从帧缓冲设备驱动程序结构看,该驱动主要跟fb_info结构体有关,该结构体记录了帧缓冲设备的全部信息,包括设备的设置参数、状态以及对底层硬件操作的函数指针。在Linux中,每一个帧缓冲设备都必须对应一个fb_info,fb_info在/linux/fb.h中的定义如下:(只列出重要的一些)
|
其中,比较重要的成员有struct fb_var_screeninfo var、struct fb_fix_screeninfo fix和struct fb_ops *fbops,他们也都是结构体。下面我们一个一个的来看。
fb_var_screeninfo结构体主要记录用户可以修改的控制器的参数,比如屏幕的分辨率和每个像素的比特数等,该结构体定义如下:
|
而fb_fix_screeninfo结构体又主要记录用户不可以修改的控制器的参数,比如屏幕缓冲区的物理地址和长度等,该结构体的定义如下:
|
fb_ops结构体是对底层硬件操作的函数指针,该结构体中定义了对硬件的操作有:(这里只列出了常用的操作)
|
3. 帧缓冲设备作为平台设备: 在S3C2440中,LCD控制器被集成在芯片的内部作为一个相对独立的单元,所以Linux把它看做是一个平台设备,故在内核代码/arch/arm/plat-s3c24xx/devs.c中定义有LCD相关的平台设备及资源,代码如下:
|
|
注意:可能有很多朋友不知道上面红色部分的参数是做什么的,其值又是怎么设置的?其实它是跟你的开发板LCD控制器密切相关的,看了下面两幅图相信就大概知道他们是干什么用的:
上面第一幅图是开发板原理图的LCD控制器部分,第二幅图是S3c2440数据手册中IO端口C和IO端口D控制器部分。原理图中使用了GPC8-15和GPD0-15来用做LCD控制器VD0-VD23的数据端口,又分别使用GPC0、GPC1端口用做LCD控制器的LEND和VCLK信号,对于GPC2-7则是用做STN屏或者三星专业TFT屏的相关信号。然而,S3C2440的各个IO口并不是单一的功能,都是复用端口,要使用他们首先要对他们进行配置。所以上面红色部分的参数就是把GPC和GPD的部分端口配置成LCD控制功能模式。
从以上讲述的内容来看,要使LCD控制器支持其他的LCD屏,重要的是根据LCD的数据手册修改以上这些参数的值。下面,我们再看一下在驱动中是如果引用到s3c2410fb_mach_info结构体的(注意上面讲的是在内核中如何使用的)。在mach-smdk2440.c中有:
|
s3c24xx_fb_set_platdata定义在plat-s3c24xx/devs.c中:
|
这里再讲一个小知识:不知大家有没有留意,在平台设备驱动中,platform_data可以保存各自平台设备实例的数据,但这些数据的类型都是不同的,为什么都可以保存?这就要看看platform_data的定义,定义在/linux/device.h中,void *platform_data是一个void类型的指针,在Linux中void可保存任何数据类型。
四、帧缓冲(FrameBuffer)设备驱动实例代码:
|
for (i = 0; i < mach_info->num_displays; i++) /*fb缓存的长度*/ { /*计算FrameBuffer缓存的最大大小,这里右移3位(即除以8)是因为色位模式BPP是以位为单位*/ unsigned long smem_len = (mach_info->displays[i].xres * mach_info->displays[i].yres * mach_info->displays[i].bpp) >> 3; if(fbinfo->fix.smem_len < smem_len) { fbinfo->fix.smem_len = smem_len; } } /*初始化LCD控制器之前要延迟一段时间*/ msleep(1); /*初始化完fb_info后,开始对LCD各寄存器进行初始化,其定义在后面讲到*/ my2440fb_init_registers(fbinfo); /*初始化完寄存器后,开始检查fb_info中的可变参数,其定义在后面讲到*/ my2440fb_check_var(fbinfo); /*申请帧缓冲设备fb_info的显示缓冲区空间,其定义在后面讲到*/ ret = my2440fb_map_video_memory(fbinfo); if (ret) { dev_err(&pdev->dev, "failed to allocate video RAM: %d/n", ret); ret = -ENOMEM; goto err_nofb; } /*最后,注册这个帧缓冲设备fb_info到系统中, register_framebuffer定义在fb.h中在fbmem.c中实现*/ ret = register_framebuffer(fbinfo); if (ret < 0) { dev_err(&pdev->dev, "failed to register framebuffer device: %d/n", ret); goto err_video_nomem; } /*对设备文件系统的支持(对设备文件系统的理解请参阅:嵌入式Linux之我行——设备文件系统剖析与使用) 创建frambuffer设备文件,device_create_file定义在linux/device.h中*/ ret = device_create_file(&pdev->dev, &dev_attr_debug); if (ret) { dev_err(&pdev->dev, "failed to add debug attribute/n"); } return 0;/*以下是上面错误处理的跳转点*/err_nomem: release_resource(fbvar->lcd_mem); kfree(fbvar->lcd_mem);err_nomap: iounmap(fbvar->lcd_base);err_noclk: clk_disable(fbvar->lcd_clock); clk_put(fbvar->lcd_clock);err_noirq: free_irq(fbvar->lcd_irq_no, fbvar);err_nofb: platform_set_drvdata(pdev, NULL); framebuffer_release(fbinfo);err_video_nomem: my2440fb_unmap_video_memory(fbinfo); return ret;}/*LCD中断服务程序*/static irqreturn_t lcd_fb_irq(int irq, void *dev_id){ struct my2440fb_var *fbvar = dev_id; void __iomem *lcd_irq_base; unsigned long lcdirq; /*LCD中断挂起寄存器基地址*/ lcd_irq_base = fbvar->lcd_base + S3C2410_LCDINTBASE; /*读取LCD中断挂起寄存器的值*/ lcdirq = readl(lcd_irq_base + S3C24XX_LCDINTPND); /*判断是否为中断挂起状态*/ if(lcdirq & S3C2410_LCDINT_FRSYNC) { /*填充调色板*/ if (fbvar->palette_ready) { my2440fb_write_palette(fbvar); } /*设置帧已插入中断请求*/ writel(S3C2410_LCDINT_FRSYNC, lcd_irq_base + S3C24XX_LCDINTPND); writel(S3C2410_LCDINT_FRSYNC, lcd_irq_base + S3C24XX_LCDSRCPND); } return IRQ_HANDLED;}/*填充调色板*/static void my2440fb_write_palette(struct my2440fb_var *fbvar){ unsigned int i; void __iomem *regs = fbvar->lcd_base; fbvar->palette_ready = 0; for (i = 0; i < 256; i++) { unsigned long ent = fbvar->palette_buffer[i]; if (ent == PALETTE_BUFF_CLEAR) { continue; } writel(ent, regs + S3C2410_TFTPAL(i)); if (readw(regs + S3C2410_TFTPAL(i)) == ent) { fbvar->palette_buffer[i] = PALETTE_BUFF_CLEAR; } else { fbvar->palette_ready = 1; } }}/*LCD各寄存器进行初始化*/static int my2440fb_init_registers(struct fb_info *fbinfo){ unsigned long flags; void __iomem *tpal; void __iomem *lpcsel; /*从lcd_fb_probe探测函数设置的私有变量结构体中再获得LCD相关信息的数据*/ struct my2440fb_var *fbvar = fbinfo->par; struct s3c2410fb_mach_info *mach_info = fbvar->dev->platform_data; /*获得临时调色板寄存器基地址,S3C2410_TPAL宏定义在mach-s3c2410/include/mach/regs-lcd.h中。 注意对于lpcsel这是一个针对三星TFT屏的一个专用寄存器,如果用的不是三星的TFT屏应该不用管它。*/ tpal = fbvar->lcd_base + S3C2410_TPAL; lpcsel = fbvar->lcd_base + S3C2410_LPCSEL; /*在修改下面寄存器值之前先屏蔽中断,将中断状态保存到flags中*/ local_irq_save(flags); /*这里就是在上一篇章中讲到的把IO端口C和D配置成LCD模式*/ modify_gpio(S3C2410_GPCUP, mach_info->gpcup, mach_info->gpcup_mask); modify_gpio(S3C2410_GPCCON, mach_info->gpccon, mach_info->gpccon_mask); modify_gpio(S3C2410_GPDUP, mach_info->gpdup, mach_info->gpdup_mask); modify_gpio(S3C2410_GPDCON, mach_info->gpdcon, mach_info->gpdcon_mask); /*恢复被屏蔽的中断*/ local_irq_restore(flags); writel(0x00, tpal);/*临时调色板寄存器使能禁止*/ writel(mach_info->lpcsel, lpcsel);/*在上一篇中讲到过,它是三星TFT屏的一个寄存器,这里可以不管*/ return 0;}/*该函数实现修改GPIO端口的值,注意第三个参数mask的作用是将要设置的寄存器值先清零*/static inline void modify_gpio(void __iomem *reg, unsigned long set, unsigned long mask){ unsigned long tmp; tmp = readl(reg) & ~mask; writel(tmp | set, reg);}/*检查fb_info中的可变参数*/static int my2440fb_check_var(struct fb_info *fbinfo){ unsigned i; /*从lcd_fb_probe探测函数设置的平台数据中再获得LCD相关信息的数据*/ struct fb_var_screeninfo *var = &fbinfo->var;/*fb_info中的可变参数*/ struct my2440fb_var *fbvar = fbinfo->par;/*在lcd_fb_probe探测函数中设置的私有结构体数据*/ struct s3c2410fb_mach_info *mach_info = fbvar->dev->platform_data;/*LCD的配置结构体数据,这个配置结构体的赋值在上一篇章的"3. 帧缓冲设备作为平台设备"中*/ struct s3c2410fb_display *display = NULL; struct s3c2410fb_display *default_display = mach_info->displays + mach_info->default_display; int type = default_display->type;/*LCD的类型,看上一篇章的"3. 帧缓冲设备作为平台设备"中的type赋值是TFT类型*/ /*验证X/Y解析度*/ if (var->yres == default_display->yres && var->xres == default_display->xres && var->bits_per_pixel == default_display->bpp) { display = default_display; } else { for (i = 0; i < mach_info->num_displays; i++) { if (type == mach_info->displays[i].type && var->yres == mach_info->displays[i].yres && var->xres == mach_info->displays[i].xres && var->bits_per_pixel == mach_info->displays[i].bpp) { display = mach_info->displays + i; break; } } } if (!display) { return -EINVAL; } /*配置LCD配置寄存器1中的5-6位(配置成TFT类型)和配置LCD配置寄存器5*/ fbvar->regs.lcdcon1 = display->type; fbvar->regs.lcdcon5 = display->lcdcon5; /* 设置屏幕的虚拟解析像素和高度宽度 */ var->xres_virtual = display->xres; var->yres_virtual = display->yres; var->height = display->height; var->width = display->width; /* 设置时钟像素,行、帧切换值,水平同步、垂直同步长度值 */ var->pixclock = display->pixclock; var->left_margin = display->left_margin; var->right_margin = display->right_margin; var->upper_margin = display->upper_margin; var->lower_margin = display->lower_margin; var->vsync_len = display->vsync_len; var->hsync_len = display->hsync_len; /*设置透明度*/ var->transp.offset = 0; var->transp.length = 0; /*根据色位模式(BPP)来设置可变参数中R、G、B的颜色位域。对于这些参数值的设置请参考CPU数据 手册中"显示缓冲区与显示点对应关系图",例如在上一篇章中我就画出了8BPP和16BPP时的对应关系图*/ switch (var->bits_per_pixel) { case 1: case 2: case 4: var->red.offset = 0; var->red.length = var->bits_per_pixel; var->green = var->red; var->blue = var->red; break; case 8:/* 8 bpp 332 */ if (display->type != S3C2410_LCDCON1_TFT) { var->red.length = 3; var->red.offset = 5; var->green.length = 3; var->green.offset = 2; var->blue.length = 2; var->blue.offset = 0; }else{ var->red.offset = 0; var->red.length = 8; var->green = var->red; var->blue = var->red; } break; case 12:/* 12 bpp 444 */ var->red.length = 4; var->red.offset = 8; var->green.length = 4; var->green.offset = 4; var->blue.length = 4; var->blue.offset = 0; break; case 16:/* 16 bpp */ if (display->lcdcon5 & S3C2410_LCDCON5_FRM565) { /* 565 format */ var->red.offset = 11; var->green.offset = 5; var->blue.offset = 0; var->red.length = 5; var->green.length = 6; var->blue.length = 5; } else { /* 5551 format */ var->red.offset = 11; var->green.offset = 6; var->blue.offset = 1; var->red.length = 5; var->green.length = 5; var->blue.length = 5; } break; case 32:/* 24 bpp 888 and 8 dummy */ var->red.length = 8; var->red.offset = 16; var->green.length = 8; var->green.offset = 8; var->blue.length = 8; var->blue.offset = 0; break; } return 0;}/*申请帧缓冲设备fb_info的显示缓冲区空间*/static int __init my2440fb_map_video_memory(struct fb_info *fbinfo){ dma_addr_t map_dma;/*用于保存DMA缓冲区总线地址*/ struct my2440fb_var *fbvar = fbinfo->par;/*获得在lcd_fb_probe探测函数中设置的私有结构体数据*/ unsigned map_size = PAGE_ALIGN(fbinfo->fix.smem_len);/*获得FrameBuffer缓存的大小, PAGE_ALIGN定义在mm.h中*/ /*将分配的一个写合并DMA缓存区设置为LCD屏幕的虚拟地址(对于DMA请参考DMA相关知识) dma_alloc_writecombine定义在arch/arm/mm/dma-mapping.c中*/ fbinfo->screen_base = dma_alloc_writecombine(fbvar->dev, map_size, &map_dma, GFP_KERNEL); if (fbinfo->screen_base) { /*设置这片DMA缓存区的内容为空*/ memset(fbinfo->screen_base, 0x00, map_size); /*将DMA缓冲区总线地址设成fb_info不可变参数中framebuffer缓存的开始位置*/ fbinfo->fix.smem_start = map_dma; } return fbinfo->screen_base ? 0 : -ENOMEM;}/*释放帧缓冲设备fb_info的显示缓冲区空间*/static inline void my2440fb_unmap_video_memory(struct fb_info *fbinfo){ struct my2440fb_var *fbvar = fbinfo->par; unsigned map_size = PAGE_ALIGN(fbinfo->fix.smem_len); /*跟申请DMA的地方想对应*/ dma_free_writecombine(fbvar->dev, map_size, fbinfo->screen_base, fbinfo->fix.smem_start);}/*LCD FrameBuffer设备移除的实现,注意这里使用一个__devexit宏,和lcd_fb_probe接口函数相对应。 在Linux内核中,使用了大量不同的宏来标记具有不同作用的函数和数据结构,这些宏在include/linux/init.h 头文件中定义,编译器通过这些宏可以把代码优化放到合适的内存位置,以减少内存占用和提高内核效率。 __devinit、__devexit就是这些宏之一,在probe()和remove()函数中应该使用__devinit和__devexit宏。 又当remove()函数使用了__devexit宏时,则在驱动结构体中一定要使用__devexit_p宏来引用remove(), 所以在第①步中就用__devexit_p来引用lcd_fb_remove接口函数。*/static int __devexit lcd_fb_remove(struct platform_device *pdev){ struct fb_info *fbinfo = platform_get_drvdata(pdev); struct my2440fb_var *fbvar = fbinfo->par; /*从系统中注销帧缓冲设备*/ unregister_framebuffer(fbinfo); /*停止LCD控制器的工作*/ my2440fb_lcd_enable(fbvar, 0); /*延迟一段时间,因为停止LCD控制器需要一点时间 */ msleep(1); /*释放帧缓冲设备fb_info的显示缓冲区空间*/ my2440fb_unmap_video_memory(fbinfo); /*将LCD平台数据清空和释放fb_info空间资源*/ platform_set_drvdata(pdev, NULL); framebuffer_release(fbinfo); /*释放中断资源*/ free_irq(fbvar->lcd_irq_no, fbvar); /*释放时钟资源*/ if (fbvar->lcd_clock) { clk_disable(fbvar->lcd_clock); clk_put(fbvar->lcd_clock); fbvar->lcd_clock = NULL; } /*释放LCD IO空间映射的虚拟内存空间*/ iounmap(fbvar->lcd_base); /*释放申请的LCD IO端口所占用的IO空间*/ release_resource(fbvar->lcd_mem); kfree(fbvar->lcd_mem); return 0;}/*停止LCD控制器的工作*/static void my2440fb_lcd_enable(struct my2440fb_var *fbvar, int enable){ unsigned long flags; /*在修改下面寄存器值之前先屏蔽中断,将中断状态保存到flags中*/ local_irq_save(flags); if (enable) { fbvar->regs.lcdcon1 |= S3C2410_LCDCON1_ENVID; } else { fbvar->regs.lcdcon1 &= ~S3C2410_LCDCON1_ENVID; } writel(fbvar->regs.lcdcon1, fbvar->lcd_base + S3C2410_LCDCON1); /*恢复被屏蔽的中断*/ local_irq_restore(flags);}/*对LCD FrameBuffer平台设备驱动电源管理的支持,CONFIG_PM这个宏定义在内核中*/#ifdef CONFIG_PM/*当配置内核时选上电源管理,则平台设备的驱动就支持挂起和恢复功能*/static int lcd_fb_suspend(struct platform_device *pdev, pm_message_t state){ /*挂起LCD设备,注意这里挂起LCD时并没有保存LCD控制器的各种状态,所以在恢复后LCD不会继续显示挂起前的内容 若要继续显示挂起前的内容,则要在这里保存LCD控制器的各种状态,这里就不讲这个了,以后讲到电源管理再讲*/ struct fb_info *fbinfo = platform_get_drvdata(pdev); struct my2440fb_var *fbvar = fbinfo->par; /*停止LCD控制器的工作*/ my2440fb_lcd_enable(fbvar, 0); msleep(1); /*停止时钟*/ clk_disable(fbvar->lcd_clock); return 0;}static int lcd_fb_resume(struct platform_device *pdev){ /*恢复挂起的LCD设备*/ struct fb_info *fbinfo = platform_get_drvdata(pdev); struct my2440fb_var *fbvar = fbinfo->par; /*开启时钟*/ clk_enable(fbvar->lcd_clock); /*初始化LCD控制器之前要延迟一段时间*/ msleep(1); /*恢复时重新初始化LCD各寄存器*/ my2440fb_init_registers(fbinfo); /*重新激活fb_info中所有的参数配置,该函数定义在第③步中再讲*/ my2440fb_activate_var(fbinfo); /*正与挂起时讲到的那样,因为没保存挂起时LCD控制器的各种状态, 所以恢复后就让LCD显示空白,该函数定义也在第③步中再讲*/ my2440fb_blank(FB_BLANK_UNBLANK, fbinfo); return 0;}#else/*如果配置内核时没选上电源管理,则平台设备的驱动就不支持挂起和恢复功能,这两个函数也就无需实现了*/#define lcd_fb_suspend NULL#define lcd_fb_resume NULL#endif fbinfo->flags = FBINFO_FLAG_DEFAULT; fbinfo->pseudo_palette = &fbvar->pseudo_pal;
|
|