前言
Linux的源码中本身已经抽象出了LCD驱动的公共部分代码——drivers/video/fbmem.c,对于驱动开发人员来讲,只需要理解这部分的代码并会调用其提供的接口即可。驱动开发人员需要做的就是针对具体的SOC和LCD,设置对应的LCD参数和寄存器值即可。
至于fbmem.c的流程已经有很多文章介绍过了,我这里就不具体介绍了,可以参考一下这篇文章:Linux Framebuffer驱动剖析之二—驱动框架、接口实现和使用。下面我就具体介绍一下怎么针对具体的SOC和LCD进行编程。
正文
这里直接给出一个LCD驱动程序编写的流程吧:
1. 分配 一个fb_info结构体:framebuffer_alloc
2. 设置
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 <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>
#include <asm/arch/regs-lcd.h>
#include <asm/arch/regs-gpio.h>
#include <asm/arch/fb.h>
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 = HWSWP 2440芯片手册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;
}
函数的参数中有红蓝绿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的设置
S3C2440芯片手册中的时序图:
下面是LCD手册展示的时序图:
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:
下面是S3C2440芯片手册中的水平方向参数的时序图:
LCD手册中的水平参数的时序图:
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结构体注册进系统。