前言 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 |
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的设置
文章图片
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结构体注册进系统。
推荐阅读
- stm32|基于STM32和freeRTOS智能门锁设计方案
- RTC驱动程序
- linux驱动|基于老罗的freg案例,使用NDK工具调用驱动流程详细分析
- file_operation
- Android之LCD屏驱动
- 嵌入式基础|AM437x——RTC驱动