驱动程序|Linux的LCD驱动

前言 Linux的源码中本身已经抽象出了LCD驱动的公共部分代码——drivers/video/fbmem.c,对于驱动开发人员来讲,只需要理解这部分的代码并会调用其提供的接口即可。驱动开发人员需要做的就是针对具体的SOC和LCD,设置对应的LCD参数和寄存器值即可。
至于fbmem.c的流程已经有很多文章介绍过了,我这里就不具体介绍了,可以参考一下这篇文章:Linux Framebuffer驱动剖析之二—驱动框架、接口实现和使用。下面我就具体介绍一下怎么针对具体的SOC和LCD进行编程。
正文 这里直接给出一个LCD驱动程序编写的流程吧:

1. 分配 一个fb_info结构体:framebuffer_alloc2. 设置 2.1 设置固定的参数 2.2 设置可变的参数 2.3 设置操作函数:和我们自己的fb_ops联系起来 2.4 其他设置:比如设置调色板3. 硬件相关的设置 3.1 配置GPIO用于LCD 3.2 根据LCD手册设置LCD控制器,比如VCLK的频率等 3.3 分配显存(framebuffer),并把地址告诉LCD控制器4. 注册:register_framebuffer()

根据上面的步骤,下面我们直接给出代码再来解释:
#include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include static struct fb_info *s3c_lcd; 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; 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; }; struct lcd_regs *lcd_regs; static u32 pseudo_palette[16]; static inline unsigned int chan_to_field(unsigned int chan, const struct fb_bitfield *bf) { chan &= 0xffff; chan >>= 16 - bf->length; /*对应到s3c_lcd->var.red.length*/ return chan << bf->offset; /*对应到s3c_lcd->var.red.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; val= chan_to_field(red, &info->var.red); val |= chan_to_field(green, &info->var.green); val |= chan_to_field(blue, &info->var.blue); pseudo_palette[regno] = val; return 0; }static struct fb_ops s3c_fb_ops = { .owner= THIS_MODULE, .fb_setcolreg = s3c_lcdfb_setcolreg, .fb_fillrect = cfb_fillrect, .fb_copyarea = cfb_copyarea, .fb_imageblit = cfb_imageblit, }; static void lcd_init(void) { /*1. 分配 一个fb_info结构体*/ s3c_lcd = framebuffer_alloc(0, NULL); if (!s3c_lcd) return -ENOMEM; printk("framebuffer alloc success\n"); /*2. 设置*/ /*2.1 设置固定的参数*/ strcpy(s3c_lcd->fix.id, "mylcd"); s3c_lcd->fix.smem_len = 480 * 272 * 16 / 8; /*RGB565*/ s3c_lcd->fix.type= FB_TYPE_PACKED_PIXELS; s3c_lcd->fix.visual= FB_VISUAL_TRUECOLOR; s3c_lcd->fix.line_length = 480 * 2; /*一开始写成了320,造成了Segmentation fault*/ /*2.2 设置可变的参数*/ s3c_lcd->var.xres = 480; s3c_lcd->var.yres = 272; s3c_lcd->var.xres_virtual = 480; s3c_lcd->var.yres_virtual = 272; s3c_lcd->var.bits_per_pixel = 16; /*RGB565*/ s3c_lcd->var.red.offset= 11; s3c_lcd->var.red.length= 5; s3c_lcd->var.green.offset = 5; s3c_lcd->var.green.length = 6; s3c_lcd->var.blue.offset= 0; s3c_lcd->var.blue.length= 5; s3c_lcd->var.activate = FB_ACTIVATE_NOW; /*2.3 设置操作函数*/ s3c_lcd->fbops = &s3c_fb_ops; /*2.4 其他设置*/ s3c_lcd->pseudo_palette = pseudo_palette; //s3c_lcd->screen_base = ; /*显存的虚拟地址*/ s3c_lcd->screen_size = 480 * 272 * 16 / 8; printk("start ot ioremap\n"); /*3. 硬件相关的设置*/ /*3.1 配置GPIO用于LCD*/ gpbcon = ioremap(0x56000010, 8); gpbdat = gpbcon + 1; gpccon = ioremap(0x56000020, 4); gpdcon = ioremap(0x56000030, 4); gpgcon = ioremap(0x56000060, 4); *gpdcon = 0xaaaaaaaa; /*设置为output : 01 = Output*/ *gpccon = 0xaaaaaaaa; *gpbcon &= ~(3<<0); *gpbcon |= 1; /*output*/ *gpbdat &= ~1; /*输出低电平,关闭背光灯*/ *gpgcon &= ~(3<<8); *gpgcon |= (3<<8); //GPG4: 11 = LCD_PWRDN /*3.2 根据LCD手册设置LCD控制器,比如VCLK的频率等*/ lcd_regs = ioremap(0X4D000000, sizeof(struct lcd_regs)); printk("after ioremap the register\n"); /* bit[17:8] :VCLK = HCLK / [(CLKVAL+1) x 2] *(10MHz)100ns= 100MHz / [(CLKVAL+1) x 2] *CLKVAL = 4 * bit[6:5]: 11 = TFT LCD panel * bit[4:1]: 1100 = 16 bpp for TFT * bit[0]: 0 = Disable the video output and the LCD control signal */ lcd_regs->lcdcon1 = (4<<8) | (3<<5) | (0xc<<1); /*垂直方向的时间参数 *bit[31:24] : VBPD, VSYNC之后过多长时间才能发出第一行数据 *bit[23:14] : LINEVAL, 多少行数据 *bit[13:6] : VFPD, 发出最后一行数据后,过多久发出VSYNC *bit[5:0] : VSPW, VSYNC的脉冲宽度 */ lcd_regs->lcdcon2 = (1<<24) | (271<<14) | (1<<6) | (9); /*水平方向的时间参数 *bit[25:19] : HBPD, HSYNC之后过多长时间才能发出第一个像素数据 *bit[18:8] : HOZVAL, 多少列数据 *bit[7:0] : HFPD, 发出最后一个像素后,过多久发出HSYNC *LCDCON4的bit[7:0] : HSPW, HSYNC的脉冲宽度 */ lcd_regs->lcdcon3 = (1<<19) | (479<<8) | (1); lcd_regs->lcdcon4 = (40); /*信号的极性 *bit[11] : 1 = 5:6:5 format *bit[10] : 0 = 低电平有效 *bit[9]: 1 = HSYNC信号要反转,即低电平有效 *bit[8]: 1 = VSYNC信号要反转,即低电平有效 *bit[3]: 0 = PWREN输出0 *bit[1]: 0 = BSWP *bit[0]: 1 = HWSWP2440芯片手册P413 */ lcd_regs->lcdcon5 = (1<<11) | (1<<9) | (1<<8) | (1<<0); /*3.3 分配显存(framebuffer),并把地址告诉LCD控制器*/ s3c_lcd->screen_base = dma_alloc_writecombine(NULL, s3c_lcd->fix.smem_len, &s3c_lcd->fix.smem_start, GFP_KERNEL); lcd_regs->lcdsaddr1 = (s3c_lcd->fix.smem_start >> 1) & ~(3<<30); lcd_regs->lcdsaddr2 = ((s3c_lcd->fix.smem_start + s3c_lcd->fix.smem_len)>>1) & (0x1fffff); lcd_regs->lcdsaddr3 = 480 * 16 / 16; /* 一行的长度(单位是:2字节)*/ //s3c_lcd->fix.smem_start = xxx; /* 启动LCD */ lcd_regs->lcdcon5 |= (1<<3); /*使能LCD本身*/ lcd_regs->lcdcon1 |= (1<<0); /*使能LCD控制器*/ *gpbdat |= 1; /* 输出高电平,使能背光 */ printk("after set the lcdconx\n"); /*4. 注册*/ register_framebuffer(s3c_lcd); return 0; }static void lcd_exit(void) { unregister_framebuffer(s3c_lcd); lcd_regs->lcdcon1 &= ~(1<<0); /*关掉LCD本身*/ *gpbdat &= ~1; /* 关掉背光 */ dma_free_writecombine(NULL , s3c_lcd->fix.smem_len, s3c_lcd->screen_base, s3c_lcd->fix.smem_start); iounmap(lcd_regs); iounmap(gpccon); iounmap(gpdcon); iounmap(gpgcon); iounmap(gpbcon); framebuffer_release(s3c_lcd); }module_init(lcd_init); module_exit(lcd_exit); MODULE_LICENSE("GPL");

1、分配 一个fb_info结构体
直接就调用framebuffer_alloc()函数来分配一个struct fb_info结构体就好了,没什么好讲的
2、设置LCD的的固定和可变参数
这两个结构体的参数,根据注释和已有的LCD驱动代码,我们是可以一个一个进行填充的。这里重点讲一下s3c_lcd->fix.smem_len(framebuffer大小),根据我们的4.3寸的LCD手册,像素是480*272的,且像素的格式是RGB565,,也就是16bit,所以framebuffer的大小就应该是480*272*16/8(单位:byte)。同理,s3c_lcd->fix.line_length代表每列的长度,单位是byte,因为像素为480*272,每个像素的大小为2byte,所以每列长度为480*2(单位:byte)。
固定参数结构体: struct fb_fix_screeninfo { char id[16]; /* identification string eg "TT Builtin" */ unsigned long smem_start; /* Start of frame buffer mem */ /* (physical address) */ __u32 smem_len; /* Length of frame buffer mem */ __u32 type; /* see FB_TYPE_**/ __u32 type_aux; /* Interleave for interleaved Planes */ __u32 visual; /* see FB_VISUAL_**/ __u16 xpanstep; /* zero if no hardware panning*/ __u16 ypanstep; /* zero if no hardware panning*/ __u16 ywrapstep; /* zero if no hardware ywrap*/ __u32 line_length; /* length of a line in bytes*/ unsigned long mmio_start; /* Start of Memory Mapped I/O*/ /* (physical address) */ __u32 mmio_len; /* Length of Memory Mapped I/O*/ __u32 accel; /* Indicate to driver which */ /*specific chip/card we have */ __u16 reserved[3]; /* Reserved for future compatibility */ };

s3c_lcd->var.bits_per_pixel顾名思义就是每个像素有多少bit,我们这里是RGB565,也就是16 bit。假如每个像素格式如下:
R R R R R G G G G G G B B B B B
很明显,R的offset为从最低位(右起)偏移11位,占5bit,所以可变参数的设置就为:
s3c_lcd->var.red.offset= 11; s3c_lcd->var.red.length= 5;

同理,G和B的设置也就很容易了。
可变参数结构体: struct fb_var_screeninfo { __u32 xres; /* visible resolution*/ __u32 yres; __u32 xres_virtual; /* virtual resolution*/ __u32 yres_virtual; __u32 xoffset; /* offset from virtual to visible */ __u32 yoffset; /* resolution*/ __u32 bits_per_pixel; /* guess what*/ __u32 grayscale; /* != 0 Graylevels instead of colors */ struct fb_bitfield red; /* bitfield in fb mem if true color, */ struct fb_bitfield green; /* else only length is significant */ struct fb_bitfield blue; struct fb_bitfield transp; /* transparency*/ __u32 nonstd; /* != 0 Non standard pixel format */ __u32 activate; /* see FB_ACTIVATE_**/ __u32 height; /* height of picture in mm*/ __u32 width; /* width of picture in mm*/ __u32 accel_flags; /* (OBSOLETE) see fb_info.flags */ /* Timing: All values in pixclocks, except pixclock (of course) */ __u32 pixclock; /* pixel clock in ps (pico seconds) */ __u32 left_margin; /* time from sync to picture */ __u32 right_margin; /* time from picture to sync */ __u32 upper_margin; /* time from sync to picture */ __u32 lower_margin; __u32 hsync_len; /* length of horizontal sync */ __u32 vsync_len; /* length of vertical sync */ __u32 sync; /* see FB_SYNC_**/ __u32 vmode; /* see FB_VMODE_**/ __u32 rotate; /* angle we rotate counter clockwise */ __u32 reserved[5]; /* Reserved for future compatibility */ };

3、操作函数的设置
我是直接参考现有的LCD驱动:drivers/video/s3c2410fb.c。唯一需要我们自己改动的就是调色板的函数,后面再说
4、 假的调色板
为什么这里要说是假的调色板呢?因为我这里的LCD驱动程序实际上并没用到调色板这个东西,但是为了兼容,也模仿程序加了一个。至于什么是调色板,可以参考一下我以前的文章:S3C2440芯片的LCD控制器,里面有一小节降到了调色板的概念。
看一下设置调色板的函数:
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; val= chan_to_field(red, &info->var.red); val |= chan_to_field(green, &info->var.green); val |= chan_to_field(blue, &info->var.blue); pseudo_palette[regno] = val; return 0; }

【驱动程序|Linux的LCD驱动】函数的参数中有红蓝绿3种颜色,然后经过chan_to_field()函数的“调色”后,赋值给val,然后就放到“调色板“pseudo_palette中。我们再看一下chan_to_field函数中是怎么”调色“的。
static inline unsigned int chan_to_field(unsigned int chan, const struct fb_bitfield *bf) { chan &= 0xffff; chan >>= 16 - bf->length; /*对应到s3c_lcd->var.red.length*/ return chan << bf->offset; /*对应到s3c_lcd->var.red.offset*/ }

其实也很简单,一个像素点的格式是RGB565,在设置fb_info结构体的可变参数时,我们曾经设置过RGB 3色的长度和偏移,这里就是从chan(表示R、G或者B的一种)得到一个像素点中R或者G或者B的值。最后将RGB拼凑成val放到pseudo_palette中。
5、配置GPIO用于LCD
每个板子LCD相关的GPIO都不一样,所以设置肯定各不相同,不过道理是一样的,根据硬件原理图,将LCD所用到的GPIO都ioremap一下,然后就根据芯片手册赋相关的值,使其成为输出还是输入引脚。
6、配置LCD控制器
一般来说,开发板是通过LCD控制器来控制LCD扫描的频率、垂直方向和水平方向的各种时间参数,所以我们还需要根据芯片手册来设置LCD控制器的各个寄存器的值,达到我们的要求。由于LCD控制器有很多寄存器,这里我就不一一列举了,下面示范一下怎么通过读芯片手册,来设置LCD控制器的寄存器。(下面的设置是根据3.5寸的LCD屏幕手册来设置的,上面的代码参数设置是4.3寸的屏幕,所以数值会有差异)
下面看一下寄存器LCDCON2(垂直方向参数)的各个bit的设置驱动程序|Linux的LCD驱动
文章图片

S3C2440芯片手册中的时序图:
驱动程序|Linux的LCD驱动
文章图片


下面是LCD手册展示的时序图:
驱动程序|Linux的LCD驱动
文章图片

LCD参考手册中的时序的具体参考值如下:
驱动程序|Linux的LCD驱动
文章图片

所以结合LCD参考手册中的时序图和具体的时序值,我们可以反推出LCDCON2中各个bit的值:
VBPD + 1 = T0 - T2 - T0 = 327 - 322 - 1 = 4,所以 VBPD = 3
LINEVAL + 1 = T5 = 320,所以LINEVAL = 319
VFPD + 1 = T2 - T5 = 2,所以VFPD = 1
VSPW + 1 = T1 = 1,所以VSPW = 0
******************************************************************************************************************
下面看一下设置水平方向参数的寄存器LCDCON3和LCDCON4:
驱动程序|Linux的LCD驱动
文章图片

驱动程序|Linux的LCD驱动
文章图片

下面是S3C2440芯片手册中的水平方向参数的时序图:
驱动程序|Linux的LCD驱动
文章图片

LCD手册中的水平参数的时序图:
驱动程序|Linux的LCD驱动
文章图片

LCD手册中水平方向参数的具体值:
驱动程序|Linux的LCD驱动
文章图片

所以结合LCD手册,我们可以得到LCDCON3寄存器各个bit的值:
HBPD + 1 = T6 - T8 - T7 = 273 - 251 - 5 = 17,所以HBPD = 16
HOZVAL + 1 = T11 = 240,所以HOZVAL = 239
HFPD + 1 = T8 - T11 = 251 - 240 = 11,所以HFPD = 10
HSPW + 1 = T7 = 5.所以HSPW = 4
7、分配显存
一般来说,配置高点的电子设备都有单独的显存来做显示的工作,不过我的开发板配置较低,需要从内存中直接划分一块出来做显存用。Linux已经为我们提供了一些接口函数,比如这里我们可以直接调用dma_alloc_writecombine()来分配一块由我们指定大小的内存,如果分配成功就会返回物理地址和虚拟地址的首地址。这个函数的作用可以参考一下这篇文章:、、dma_alloc_writecombine 和mmap函数。然后就是通过设置LCDADDRx寄存器,将显存的起始地址和大小告诉LCD控制器。
8、注册framebuffer
其实在Linux的整个LCD框架下编程,我们需要做的工作,也就是设置一些硬件相关的参数,这些工作都做完后,就用register_framebuffer()函数直接将设置好的fb_info结构体注册进系统。

    推荐阅读