学习目的:
- 实现LCD驱动程序编写
前面通过对linux内核中LCD的驱动框架进行了分析,弄清楚了内核中LCD的驱动框架,通过分析知道内核中已经在fbmem.c文件中注册了LCD这一类设备的字符设备驱动,向上实现了上层通用的访问接口,向下给驱动设计者预留了fb_info注册接口。现在基于我们的开发板平台(JZ2240),编写适配于自己硬件的驱动程序
从前面分析可以知道,编写LCD驱动需要驱动的编写者去完成以下内容(可参考内核中s3c2410fb.c):
1) 分配一个fb_info结构体
2) 根据使用LCD硬件特征,设置fb_info结构体的一些参数
3) 配置硬件相关操作
4) 注册fb_info结构体
下面我们就开始按照这些步骤,完成LCD驱动程序的编写
1、fb_info结构体介绍
fb_info结构体是LCD驱动程序中最核心的数据结构,应用程序通过fbmem.c文件中注册的设备访问入口,找到打开设备文件的fb_info结构体,最终访问到硬件,fb_info结构体声明如下:
struct fb_info { int node; int flags; struct fb_var_screeninfo var; /* Current var */ struct fb_fix_screeninfo fix; /* Current fix */ struct fb_monspecs monspecs; /* Current Monitor specs */ struct work_struct queue; /* Framebuffer event queue */ struct fb_pixmap pixmap; /* Image hardware mapper */ struct fb_pixmap sprite; /* Cursor hardware mapper */ struct fb_cmap cmap; /* Current cmap */ struct list_head modelist; /* mode list */ struct fb_videomode *mode; /* current mode */ #ifdef CONFIG_FB_BACKLIGHT /* assigned backlight device */ /* set before framebuffer registration, remove after unregister */ struct backlight_device *bl_dev; /* Backlight level curve */ struct mutex bl_curve_mutex; u8 bl_curve[FB_BACKLIGHT_LEVELS]; #endif #ifdef CONFIG_FB_DEFERRED_IO struct delayed_work deferred_work; struct fb_deferred_io *fbdefio; #endif struct fb_ops *fbops; struct device *device; /* This is the parent */ struct device *dev; /* This is this fb device */ int class_flag; /* private sysfs flags */ #ifdef CONFIG_FB_TILEBLITTING struct fb_tile_ops *tileops; /* Tile Blitting */ #endif char __iomem *screen_base; /* Virtual address */ unsigned long screen_size; /* Amount of ioremapped VRAM or 0 */ void *pseudo_palette; /* Fake palette of 16 colors */ #define FBINFO_STATE_RUNNING 0 #define FBINFO_STATE_SUSPENDED 1 u32 state; /* Hardware state i.e suspend */ void *fbcon_par; /* fbcon use-only private area */ /* From here on everything is device dependent */ void *par; };
可以看到fb_info的结构体比较复杂,它的肯定是为了抽象出所有LCD硬件而设计的,但我们不需要去了解每一个成员变量的功能和使用方法。在这里面有三个比较重要的结构体,分别是fb_var_screeninfo、fb_fix_screeninfo、fb_ops,这三个结构体也是在每个LCD驱动程序中都必需设置的
1)fb_var_screeninfo结构体记录了用户可以修改的显示参数,比如屏幕的分辨率、屏幕位域
2)fb_fix_screeninfo结构体记录了用户不可修改的参数,比如屏幕的显存的位置、长度
3)fb_ops结构体记录了操作底层硬件函数的地址,如果fb_ops中定义了read函数,应用程序的read函数最终调用到fb_ops中read函数指针指向的函数,通过file->fops->read中找到打开设备fb_info,调用fb_info->fbops->read
2、驱动使用函数介绍
struct fb_info *framebuffer_alloc(size_t size, struct device *dev);------------------------------>① void framebuffer_release(struct fb_info *info);
/*================================================================================================*/ int register_framebuffer(struct fb_info *fb_info);----------------------------------------------->② int unregister_framebuffer(struct fb_info *fb_info);
/*================================================================================================*/
void *dma_alloc_writecombine(struct device *dev, size_t size, dma_addr_t *handle, gfp_t gfp);---->③
size:分配内存的大小
handler:申请到内存的物理地址
gfp:分配出来内存的参数,常用标志如下:
GFP_ATOMIC 用来从中断处理和进程上下文之外的其他代码中分配内存. 从不睡眠.
GFP_KERNEL 内核内存的正常分配. 可能睡眠.
GFP_USER 用来为用户空间页来分配内存; 它可能睡眠.
返回申请到内存的虚拟地址
void dma_free_writecombine(struct device *dev, size_t size, void *cpu_addr, dma_addr_t handle);
cpu_addr:虚拟地址,
handle:物理地址
① 动态分配一个fb_info结构体,size代表额外分配的内存供驱动使用,可以用fb_info中的fbcon_par指针指向这块内存,如果不使用设置为0,dev指针,指向fb的device,可以设置为NULL
② 向内核注册和注销fb_info结构体
③ 分配和注销连续地址的内存,一般LCD控制器使用的显存地址是连续的,分配显存时需要使用这些函数
3、驱动程序编写
3.1 入口函数
入口函数里面完成了LCD驱动中的大部分工作,分配了fb_info结构体、分配了连续地址内存作为LCD控制器显存,设置了fb_info结构体的一些信息,配置硬件,注册fb_info结构体
int lcd_drv_init(void) { lcd_fb_info = framebuffer_alloc(0, NULL); /* set fb_info variable part */ lcd_fb_info->var.xres = 480; //x方向分辨率 lcd_fb_info->var.yres = 272; //y方向分别率 lcd_fb_info->var.xres_virtual = 480; //x方向虚拟分辨率 lcd_fb_info->var.yres_virtual = 272; //y方向虚拟分辨率 lcd_fb_info->var.bits_per_pixel = 16; //每像素位数 /* color format RGB565 */ lcd_fb_info->var.red.length = 5; //lcd颜色格式 lcd_fb_info->var.red.offset = 11; lcd_fb_info->var.green.length = 6; lcd_fb_info->var.green.offset = 5; lcd_fb_info->var.blue.length = 5; lcd_fb_info->var.blue.offset = 0; lcd_fb_info->var.activate = FB_ACTIVATE_NOW; /* set fb_info fixed part */ strcpy(lcd_fb_info->fix.id, "tft-lcd"); lcd_fb_info->fix.smem_len = 480*272*16/8; //显存大小 lcd_fb_info->fix.type = FB_TYPE_PACKED_PIXELS; lcd_fb_info->fix.visual = FB_VISUAL_TRUECOLOR; lcd_fb_info->fix.line_length = 480*16/8; //显示一行需要字节数 /* set fb_info fbops pointer*/ lcd_fb_info->fbops = &lcd_fb_ops; lcd_fb_info->pseudo_palette = pseudo_palette; //调色板颜色 lcd_fb_info->screen_size = 480*272*16/8; /* set lcd connect gpio pin register */ gpbcon = ioremap(0x56000010, 8); gpbdat = gpbcon+1; gpccon = ioremap(0x56000020, 4); gpdcon = ioremap(0x56000030, 4); gpgcon = ioremap(0x56000060, 4); *gpccon = 0xaaaaaaaa; /* GPIO管脚用于VD[7:0],LCDVF[2:0],VM,VFRAME,VLINE,VCLK,LEND */ *gpdcon = 0xaaaaaaaa; /* GPIO管脚用于VD[23:8] */ *gpbcon &= ~(3); /* GPB0设置为输出引脚 */ *gpbcon |= 1; *gpbdat &= ~1; /* 输出低电平 */ *gpgcon |= (3<<8); /* GPG4用作LCD_PWREN */ /* set s3c2440 lcd control register */ lcd_regs = ioremap(0x4D000000, sizeof(struct lcd_regs)); /* bit[17:8]: VCLK = HCLK / [(CLKVAL+1) x 2], LCD手册P14 * 10MHz(100ns) = 100MHz / [(CLKVAL+1) x 2] * CLKVAL = 4 * bit[6:5]: 0b11, TFT LCD * bit[4:1]: 0b1100, 16 bpp for TFT * bit[0] : 0 = Disable the video output and the LCD control signal. */ lcd_regs->lcdcon1 = (4<<8) | (3<<5) | (0x0c<<1); /* 垂直方向的时间参数 * bit[31:24]: VBPD, VSYNC之后再过多长时间才能发出第1行数据 * * bit[23:14]: 多少行, * bit[13:6] : VFPD, 发出最后一行数据之后,再过多长时间才发出VSYNC * * bit[5:0] : VSPW, VSYNC信号的脉冲宽度 */ lcd_regs->lcdcon2 = (1<<24) | (271<<14) | (1<<6) | (9<<0); /* 水平方向的时间参数 * bit[25:19]: HBPD, VSYNC之后再过多长时间才能发出第1行数据 * HBPD=16 * bit[18:8]: 多少列, * bit[7:0] : HFPD, 发出最后一行里最后一个象素数据之后,再过多长时间才发出HSYNC * */ lcd_regs->lcdcon3 = (1<<19) | (479<<8) | (1<<0); /* 水平方向的同步信号 * bit[7:0] : HSPW, HSYNC信号的脉冲宽度 */ lcd_regs->lcdcon4 = 40; /* 信号的极性 * bit[11]: 1=565 format * bit[10]: 0 = The video data is fetched at VCLK falling edge * bit[9] : 1 = HSYNC信号要反转,即低电平有效 * bit[8] : 1 = VSYNC信号要反转,即低电平有效 * bit[6] : 0 = VDEN不用反转 * bit[3] : 0 = PWREN输出0 * bit[1] : 0 = BSWP * bit[0] : 1 = HWSWP 2440手册P413 */ lcd_regs->lcdcon5 = (1<<11) | (0<<10) | (1<<9) | (1<<8) | (1<<0); /* 3.3 分配显存(framebuffer), 并把地址告诉LCD控制器 */ lcd_fb_info->screen_base = dma_alloc_writecombine(NULL, lcd_fb_info->fix.smem_len, &lcd_fb_info->fix.smem_start, GFP_KERNEL); lcd_regs->lcdsaddr1 = (lcd_fb_info->fix.smem_start >> 1) & ~(3<<30); lcd_regs->lcdsaddr2 = ((lcd_fb_info->fix.smem_start + lcd_fb_info->fix.smem_len) >> 1) & 0x1fffff; lcd_regs->lcdsaddr3 = (480*16/16); /* 一行的长度(单位: 2字节) */ /* 启动LCD */ lcd_regs->lcdcon1 |= (1<<0); /* 使能LCD本身 */ lcd_regs->lcdcon5 |= (1<<3); *gpbdat |= 1; /* 输出高电平, 使能背光 */ register_framebuffer(lcd_fb_info); return 0; }
配置硬件部分分以下几个步骤
1)寄存器物理地址到虚拟地址映射
2)配置相关GPIO引脚
3)根据LCD芯片手册时序图,设置LCD控制器各寄存器值
4)使能LCD控制器、开启背光
入口函数在设置好fb_info结构体,完成硬件配置工作后,调用register_framebuffer向内核注册fb_info信息
3.2 fb_ops结构体实现
static struct fb_ops lcd_fb_ops = { .owner = THIS_MODULE, .fb_setcolreg = s3c_lcdfb_setcolreg,------------------>① .fb_fillrect = cfb_fillrect,-------------------------->② .fb_copyarea = cfb_copyarea,-------------------------->③ .fb_imageblit = cfb_imageblit,------------------------>④ };
fb_ops中实现了4个函数指针,cfb_fillrect、cfb_copuarea、cfb_imageblit函数由内核实现的函数,对/dev/fbn设备文件进行访问时,会调用到这些函数
① 调用s3c_lcdfb_setcolreg函数,来设置调色板fb_info->pseudo_palette
② 填充矩形
③ 拷贝显存数据
④ 图片显示
3.3 出口函数
void lcd_drv_exit(void) { unregister_framebuffer(lcd_fb_info); lcd_regs->lcdcon1 &= ~(1<<0); /* 关闭LCD本身 */ *gpbdat &= ~1; /* 关闭背光 */ dma_free_writecombine(NULL, lcd_fb_info->fix.smem_len, lcd_fb_info->screen_base, lcd_fb_info->fix.smem_start); framebuffer_release(lcd_fb_info); iounmap(gpbcon); iounmap(gpccon); iounmap(gpdcon); iounmap(gpgcon); iounmap(lcd_regs); }
驱动卸载时,注销注册的fb_info结构体,关闭LCD,释放申请的显存,释放动态分配的fb_info结构体,取消寄存器物理地址到虚拟地址映射
4、驱动程序测试
内核中如果已经将内核自带的lcd驱动编译在内,需要重新make menuconfig将lcd自带的驱动编译成模块
Device Drivers ---> Graphics support ---> <*> Support for frame buffer devices ---> <M> S3C2410 LCD framebuffer support
执行make uImage生成uImage文件
执行make modules,因为内核自带的LCD驱动没有编进内核,LCD驱动相关其他代码肯定也没编译进内核,而我们编写驱动的fb_ops里的成员使用了内核中关于LCD驱动的fb_fillrect(), fb_copyarea(),fb_imageblit()函数,我们需要将其编译成.ko文件在加载lcd驱动之前加载
使用insmod加载编译好的驱动程序
insmod cfbcopyarea.ko
insmod cfbfillrect.ko
insmod cfbimgblt.ko
insmod lcd_drv.ko
使用echo hello > /dev/tty1命令进行测试
如果驱动正常,内核中Console display driver support / <*> Framebuffer Console support 被编译在内,lcd上将显示hello字符串
完整驱动代码
#include <linux/module.h> #include <linux/kernel.h> #include <linux/errno.h> #include <linux/string.h> #include <linux/mm.h> #include <linux/slab.h> #include <linux/delay.h> #include <linux/fb.h> #include <linux/init.h> #include <linux/dma-mapping.h> #include <linux/interrupt.h> #include <linux/workqueue.h> #include <linux/wait.h> #include <linux/platform_device.h> #include <linux/clk.h> #include <asm/io.h> #include <asm/uaccess.h> #include <asm/div64.h> #include <asm/mach/map.h> //nclude <asm/arch/regs-lcd.h> //nclude <asm/arch/regs-gpio.h> //nclude <asm/arch/fb.h> static int s3c_lcdfb_setcolreg(unsigned int regno, unsigned int red, unsigned int green, unsigned int blue, unsigned int transp, struct fb_info *info); struct lcd_regs { unsigned long lcdcon1; unsigned long lcdcon2; unsigned long lcdcon3; unsigned long lcdcon4; unsigned long lcdcon5; unsigned long lcdsaddr1; unsigned long lcdsaddr2; unsigned long lcdsaddr3; unsigned long redlut; unsigned long greenlut; unsigned long bluelut; unsigned long reserved[9]; unsigned long dithmode; unsigned long tpal; unsigned long lcdintpnd; unsigned long lcdsrcpnd; unsigned long lcdintmsk; unsigned long lpcsel; }; static struct fb_info *lcd_fb_info; static struct fb_ops lcd_fb_ops = { .owner = THIS_MODULE, .fb_setcolreg = s3c_lcdfb_setcolreg, .fb_fillrect = cfb_fillrect, .fb_copyarea = cfb_copyarea, .fb_imageblit = cfb_imageblit, }; static volatile unsigned long *gpbcon; static volatile unsigned long *gpbdat; static volatile unsigned long *gpccon; static volatile unsigned long *gpdcon; static volatile unsigned long *gpgcon; static volatile struct lcd_regs* lcd_regs; static u32 pseudo_palette[16]; static inline unsigned int chan_to_field(unsigned int chan, struct fb_bitfield *bf) { chan &= 0xffff; chan >>= 16 - bf->length; return chan << bf->offset; } static int s3c_lcdfb_setcolreg(unsigned int regno, unsigned int red, unsigned int green, unsigned int blue, unsigned int transp, struct fb_info *info) { unsigned int val; if (regno > 16) return 1; /* 用red,green,blue三原色构造出val */ val = chan_to_field(red, &info->var.red); val |= chan_to_field(green, &info->var.green); val |= chan_to_field(blue, &info->var.blue); //((u32 *)(info->pseudo_palette))[regno] = val; pseudo_palette[regno] = val; return 0; } int lcd_drv_init(void) { lcd_fb_info = framebuffer_alloc(0, NULL); /* set fb_info variable part */ lcd_fb_info->var.xres = 480; lcd_fb_info->var.yres = 272; lcd_fb_info->var.xres_virtual = 480; lcd_fb_info->var.yres_virtual = 272; lcd_fb_info->var.bits_per_pixel = 16; /* color format RGB565 */ lcd_fb_info->var.red.length = 5; lcd_fb_info->var.red.offset = 11; lcd_fb_info->var.green.length = 6; lcd_fb_info->var.green.offset = 5; lcd_fb_info->var.blue.length = 5; lcd_fb_info->var.blue.offset = 0; lcd_fb_info->var.activate = FB_ACTIVATE_NOW; /* set fb_info fixed part */ strcpy(lcd_fb_info->fix.id, "tft-lcd"); lcd_fb_info->fix.smem_len = 480*272*16/8; lcd_fb_info->fix.type = FB_TYPE_PACKED_PIXELS; lcd_fb_info->fix.visual = FB_VISUAL_TRUECOLOR; lcd_fb_info->fix.line_length = 480*16/8; /* set fb_info fbops pointer*/ lcd_fb_info->fbops = &lcd_fb_ops; lcd_fb_info->pseudo_palette = pseudo_palette; lcd_fb_info->screen_size = 480*272*16/8; /* set lcd connect gpio pin register */ gpbcon = ioremap(0x56000010, 8); gpbdat = gpbcon+1; gpccon = ioremap(0x56000020, 4); gpdcon = ioremap(0x56000030, 4); gpgcon = ioremap(0x56000060, 4); *gpccon = 0xaaaaaaaa; /* GPIO管脚用于VD[7:0],LCDVF[2:0],VM,VFRAME,VLINE,VCLK,LEND */ *gpdcon = 0xaaaaaaaa; /* GPIO管脚用于VD[23:8] */ *gpbcon &= ~(3); /* GPB0设置为输出引脚 */ *gpbcon |= 1; *gpbdat &= ~1; /* 输出低电平 */ *gpgcon |= (3<<8); /* GPG4用作LCD_PWREN */ /* set s3c2440 lcd control register */ lcd_regs = ioremap(0x4D000000, sizeof(struct lcd_regs)); /* bit[17:8]: VCLK = HCLK / [(CLKVAL+1) x 2], LCD手册P14 * 10MHz(100ns) = 100MHz / [(CLKVAL+1) x 2] * CLKVAL = 4 * bit[6:5]: 0b11, TFT LCD * bit[4:1]: 0b1100, 16 bpp for TFT * bit[0] : 0 = Disable the video output and the LCD control signal. */ lcd_regs->lcdcon1 = (4<<8) | (3<<5) | (0x0c<<1); /* 垂直方向的时间参数 * bit[31:24]: VBPD, VSYNC之后再过多长时间才能发出第1行数据 * LCD手册 T0-T2-T1=4 * VBPD=3 * bit[23:14]: 多少行, 320, 所以LINEVAL=320-1=319 * bit[13:6] : VFPD, 发出最后一行数据之后,再过多长时间才发出VSYNC * LCD手册T2-T5=322-320=2, 所以VFPD=2-1=1 * bit[5:0] : VSPW, VSYNC信号的脉冲宽度, LCD手册T1=1, 所以VSPW=1-1=0 */ lcd_regs->lcdcon2 = (1<<24) | (271<<14) | (1<<6) | (9<<0); /* 水平方向的时间参数 * bit[25:19]: HBPD, VSYNC之后再过多长时间才能发出第1行数据 * LCD手册 T6-T7-T8=17 * HBPD=16 * bit[18:8]: 多少列, 240, 所以HOZVAL=240-1=239 * bit[7:0] : HFPD, 发出最后一行里最后一个象素数据之后,再过多长时间才发出HSYNC * LCD手册T8-T11=251-240=11, 所以HFPD=11-1=10 */ lcd_regs->lcdcon3 = (1<<19) | (479<<8) | (1<<0); /* 水平方向的同步信号 * bit[7:0] : HSPW, HSYNC信号的脉冲宽度, LCD手册T7=5, 所以HSPW=5-1=4 */ lcd_regs->lcdcon4 = 40; /* 信号的极性 * bit[11]: 1=565 format * bit[10]: 0 = The video data is fetched at VCLK falling edge * bit[9] : 1 = HSYNC信号要反转,即低电平有效 * bit[8] : 1 = VSYNC信号要反转,即低电平有效 * bit[6] : 0 = VDEN不用反转 * bit[3] : 0 = PWREN输出0 * bit[1] : 0 = BSWP * bit[0] : 1 = HWSWP 2440手册P413 */ lcd_regs->lcdcon5 = (1<<11) | (0<<10) | (1<<9) | (1<<8) | (1<<0); /* 3.3 分配显存(framebuffer), 并把地址告诉LCD控制器 */ lcd_fb_info->screen_base = dma_alloc_writecombine(NULL, lcd_fb_info->fix.smem_len, &lcd_fb_info->fix.smem_start, GFP_KERNEL); lcd_regs->lcdsaddr1 = (lcd_fb_info->fix.smem_start >> 1) & ~(3<<30); lcd_regs->lcdsaddr2 = ((lcd_fb_info->fix.smem_start + lcd_fb_info->fix.smem_len) >> 1) & 0x1fffff; lcd_regs->lcdsaddr3 = (480*16/16); /* 一行的长度(单位: 2字节) */ /* 启动LCD */ lcd_regs->lcdcon1 |= (1<<0); /* 使能LCD本身 */ lcd_regs->lcdcon5 |= (1<<3); *gpbdat |= 1; /* 输出高电平, 使能背光 */ register_framebuffer(lcd_fb_info); return 0; } void lcd_drv_exit(void) { unregister_framebuffer(lcd_fb_info); lcd_regs->lcdcon1 &= ~(1<<0); /* 关闭LCD本身 */ *gpbdat &= ~1; /* 关闭背光 */ dma_free_writecombine(NULL, lcd_fb_info->fix.smem_len, lcd_fb_info->screen_base, lcd_fb_info->fix.smem_start); framebuffer_release(lcd_fb_info); iounmap(gpbcon); iounmap(gpccon); iounmap(gpdcon); iounmap(gpgcon); iounmap(lcd_regs); } module_init(lcd_drv_init); module_exit(lcd_drv_exit); MODULE_LICENSE("GPL");