声明:本文以韦东山老师的视频为模本进行编写,开发板为s3c2440,LCD为A043-24-TT-11,此LCD为480*272 的4.3寸屏幕。与老师所讲的略有不同。同时本文为复习视频所学的内容,如有巧合,敬请谅解;
要写LCD驱动就要先从内核中找到支持LCD的软件相关的部分,也就是fbmem.c文件。fbmem.c作为LCD的软件部分为其提供了代码不变的部分,即在入口函数中分配好了主设备号:29,file_operations结构体和register_chrdev函数,详细代码为:
register_chrdev(FB_MAJOR,"fb",&fb_fops)
。而fbmem.c会根据不同的设备通过register_fb数组找到不同的设置代码进行调用。
我们所要编写的将是硬件相关的部分,就是将设备的fb_info结构体设置好然后放到register_fb中,让fbmem.c调用,这部分与硬件相关,相对变化较大。而在编写代码之前需要先了解fbmem.c做了什么工作,而我们自己编写的驱动又该做什么样的工作。首先我们分析入口函数,通过上面的代码我们知道了fbmem.c已经为我们将软件的框架搭好,而假设当我们使用应用程序打开这个设备时,我们将调用file_operations中的open函数.open = fb_open:
int fbidx = iminor(inode); //获得次设备号
struct fb_info *info; //分配一个fb_info结构体
info = registered_fb[fbidx]; //根据次设备号从registered_fb中找到相应的fb_info结构体
if (info->fbops->fb_open) //如果已经在info中定义了fb_open
res = info->fbops->fb_open(info,1); //调用info中的fb_open
通过上面的分析我们可以看出,我们是通过register_fb来获得fb_info结构体的,而register_fb数组又是由什么决定那?我们通过从fbmem.c 中查找发现,在register_framebuffer函数中为register_fb赋值:
for (i = 0 ; i < FB_MAX; i++) //找到一个没有占用的次设备
if (!registered_fb[i])
break;
fb_info->dev = device_create(fb_class, fb_info->device,
MKDEV(FB_MAJOR, i), "fb%d", i); //使用vdev机制自动生成设备
registered_fb[i] = fb_info; //将这个fb_info结构体根据获得次设备号放入register_fb中
有上面的分析知道我们要写一个与我们的LCD硬件相关的fb_info结构体,并通过register_framebuffer函数将这个fb_info结构体通过次设备号放入register_fb中,而fbmem.c 就可以通过次设备号调用register_fb中的fb_info,进而驱动这个硬件。所以我们所要写的LCD驱动可以分为以下四步:
- 分配一个fb_info结构体
- 设置这个结构体
- 做硬件相关的操作
- 通过register_framebuffer函数注册fb_info
有了上面的步骤,我们按着上面的步骤写程序:而一些细节的部分我会在程序中说明:
下面我们写第一步:
既然要分配一个fb_info结构体,我们就应该先定义这个结构体:
static struct fb_info *s3c_lcd;
然后是为其分配:
s3c_lcd =framebuffer_alloc(0,NULL);
需要说明:
/**
* framebuffer_alloc 函数就是创造一个新的frame buffer info结构体
* @size: 是设备私有数据,可以是0
* @dev: 指向fb的设备,这里可以为 NULL
* Returns: 返回一个新的fb_info结构体或者NULL(如果出错).
*/
struct fb_info *framebuffer_alloc(size_t size, struct device *dev)
下面就该进入第二步,设置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 */
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 */
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_info的固定参数:struct fb_fix_screeninfo fix;
- 设置fb_info的可变参数:struct fb_var_screeninfo var;
- 设置fb_info操作函数:struct fb_ops *fbops;
- fb_info的其他设置:char __iomem *screen_base;
unsigned long screen_size;
void *pseudo_palette;
下面我们先设置fb_info的固定参数(固定参数多为硬件相关而不会变化的,如屏幕的尺寸,显存物理地址,和屏幕类型等):
strcpy(s3c_lcd->fix.id,"mylcd");
//s3c_lcd->fix.smem_start //LCD显存的物理地址,在3.3中设置
s3c_lcd->fix.smem_len = 480*272*16/8; //显存的长度
s3c_lcd->fix.type = FB_TYPE_PACKED_PIXELS;
s3c_lcd->fix.visual = FB_VISUAL_TRUECOLOR; //TFT屏为真彩 {MOD}
s3c_lcd->fix.line_length = 480*16/8; //一行的长度(单位:type)
再设置fb_info的可变参数(而可变的参数是可以根据不同情况而进行不同设置的,如:x,y方向虚拟分辨率,多少字节每像素,以及RGB所占有的比例等):
s3c_lcd->var.xres = 480; //x方向真实的分辨率
s3c_lcd->var.yres = 272; //y方向真实的分辨率
s3c_lcd->var.xres_virtual = 480; //x方向虚拟的分辨率
s3c_lcd->var.yres_virtual = 272; //y方向虚拟的分辨率
s3c_lcd->var.xoffset = 0; //x方向真实分辨率与虚拟分辨率的差值
s3c_lcd->var.yoffset = 0; //y方向真实分辨率与虚拟分辨率的差值
s3c_lcd->var.bits_per_pixel = 16; //设置16个字节每像素
/*RGB:565*/
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;
之后设置操作函数:
s3c_lcd->fbops = &s3c_lcdfb_ops;
static struct fb_ops s3c_lcdfb_ops = {
.owner = THIS_MODULE,
.fb_setcolreg = s3c_lcdfb_setcolreg, /* 调 {MOD}板 */
.fb_fillrect = cfb_fillrect,
.fb_copyarea = cfb_copyarea,
.fb_imageblit = cfb_imageblit,
};
最后就是对fb_info的其他设置:
//s3c_lcd->screen_base //显存的虚拟地址
s3c_lcd->screen_size = 480*272*2; //屏幕的尺寸
s3c_lcd->pseudo_palette = pseudo_palette; //调 {MOD}板
上面对fb_info设置完后就该到第三步:硬件相关的设置 ,而硬件首先要配置的就是用于LCD的GPIO接口,GPIO的图为:
而相应的代码为:
gpbcon = ioremap(0x56000010, 8);
gpbdat = gpbcon + 1;
gpccon = ioremap(0x56000020, 4);
gpdcon = ioremap(0x56000030, 4);
gpgcon = ioremap(0x56000060, 4);
/* 设置背光灯 */
*gpbcon &= ~(3); //清零
*gpbcon |= (1); //设置输出模式
*gpbdat &= ~(1); //设置低电平
/* 设置RGB数据接口 */
*gpccon = 0xaaaaaaaa;
*gpdcon = 0xaaaaaaaa;
/* 设置PWREN */
*gpgcon &= ~(3<<4); //清零
*gpgcon |= (3<<4); //设置LCD模式
下面将是对LCD控制器的设置,以使其可以支持相应的LCD,在这之前我们先构造一个lcd_reg的结构体用于存放LCD控制器的各种寄存器:
/* s3c2440 lcd registers */
struct lcd_reg{
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 TCONSEL;
};
由于我开发板上的LCD与视频中的LCD不是同一种类型的,所以这部分代码我做了相应的改动所以从lcdcon1到lcdcon4,我要另做说明:
/*
*LCDCON1
*bit[17:8] CLKVAL :TFT: VCLK = HCLK / [(CLKVAL+1) * 2] ( CLKVAL> 0 )
* LCD手册:VCLK =9MHZ ,而HCLK =100MHZ
* 所以 CLKVAL=5
*bit[6:5] PNRMODE :0b11 = TFT LCD panel
*bit[4:1] BPPMODE :0b1100 = 16 bpp for TFT
*bit [0] ENVID :0 = Disable the video output and the LCD control signal.
*/
lcd_regs->lcdcon1 = (5<<8) | (3<<5) | (12<<1) | (0<<0);
而垂直方向的时间参数发生了变化:
所以代码为:
/*
*LCDCON2 :垂直方向时间参数
*bit[31:24] VBPD : VSYNC 后再过多长时间才能发出第一个数据
* =1
*bit[23:14] LINEVAL : 多少行
* =271
*bit [13:6] VFPD : 最后一行数据后再过多久发VSYNC信号
* =1
*bit [5:0] VSPW : VSYNC脉冲宽度
* =9
*/
lcd_regs->lcdcon2 = (1<<24) | (271<<14) | (1<<6) | (9<<0);
水平方向的时间参数:
所以代码为:
/*
*水平方向时间参数
*LCDCON3:
*bit[25:19] HBPD : HSYNC 后再过多长时间才能发出第一个数据
* =2
*bit[18:8] HOZVAL : 多少列
* =479
*bit[7:0] HFPD : 一行中发出最后一个像素数据后再过多久发HSYNC信号
* =2
*
*LCDCON4 :
*bit[7:0] HSPW : HSYNC脉冲宽度
* =40
*/
lcd_regs->lcdcon3 = (2<<19) | (479<<8) | (2<<0);
lcd_regs->lcdcon4 = (40<<0);
信号的极性并没有发生变化,所以代码为:
/*
*信号极性
*LCDCON5:
*
*bit[11] FRM565 : 16bpp输出形式
* 1 = 5:6:5 Format
*bit[10] INVVCLK : 表示是上升沿读取数据还是下降沿读取数据
* 0 = 下降沿读取数据
*bit[9] INVVLINE : 水平方向同步信号是否反转
* 1 = 反转
*bit[8] INVVFRAME: 垂直方向同步信号是否反转
* 1 = 反转
*bit[7] INVVD : 数据脉冲是否反转
* 0 = Normal(不反转)
*bit[6] INVVDEN : 数据使能位是否反转
* 0 = normal
*bit[5] INVPWREN : PWREN(LCD电源)位是否反转
* 0 = normal
*bit[3] PWREN : PWREN(LCD电源)位是否使能
* 0 = Disable PWREN signal(不使能)
*bit[1] BSWP :字节转换位
* 0 = Swap Disable
*bit[0] HWSWP : 半字转换控制位
* 1 = Swap Enable(转换)
*/
lcd_regs->lcdcon5 = (1<<11) | (1<<9) | (1<<8) | (1<<0);
上面的工作做完我们的对LCD的设置就基本完成了,下面就是对显存的设置了,
/*分配显存:
//s3c_lcd->fix.smem_start
//s3c_lcd->fix.smem_len = 480*272*16/8;
//s3c_lcd->screen_base
*/
s3c_lcd->screen_base = dma_alloc_writecombine(NULL,272*480*2,&(s3c_lcd->fix.smem_start),GFP_KERNEL);
/*
*把地址告诉LCD控制器:
*LCDSADDR1:
*bit[29:21] LCDBANK :A[30:22] of the bank location for the video buffer in the system memory
*bit[20:0] LCDBASEU :A[21:1] of the start address of the LCD frame buffer
*
*LCDSADDR2:
*bit[20:0] LCDBASEL :A[21:1] of the end address of the LCD frame buffer
* LCDBASEL = ((the frame end address) >>1) + 1
* = LCDBASEU + (PAGEWIDTH+OFFSIZE) x (LINEVAL+1)
*
*LCDSADDR3:
*bit[10:0] PAGEWIDTH : Virtual screen page width (the number of half words).
*
*/
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;
而在这里我需要讲一下dma_alloc_writecombine函数:
void *
dma_alloc_writecombine(struct device *dev, size_t size, dma_addr_t *handle, gfp_t gfp)
此函数为分配显存的函数,而且这一显存的物理地址为连续的地址, 其中第一个参数dev为设备,第二个参数size表示要分配的地址的大小,第三个参数handle为物理地址,而第四个参数为标记。而此函数的返回值为分配内存的虚拟地址。
上面的工作做完后,我们就基本完成入口函数的程序,只差最后一步将fb_info结构体注册了:
register_framebuffer(s3c_lcd);
然后我们完善程序,如调 {MOD}板:
static u32 pseudo_palette[16];
s3c_lcd->pseudo_palette = pseudo_palette;
//在操作函数中
.fb_setcolreg = s3c_lcdfb_setcolreg,
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;
}
u32 *pseudo_palette = info->pseudo_palette;
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;
}
/* from pxafb.c */
static inline unsigned int chan_to_field(unsigned int chan, struct fb_bitfield *bf)
{
chan &= 0xffff;
chan >>= 16 - bf->length;
return chan << bf->offset;
}
出口函数:
unregister_framebuffer(s3c_lcd);
lcd_regs->lcdcon1 &= ~1; //关LCD控制器
lcd_regs->lcdcon5 &= ~(1<<3); //关LCD本身,给LCD断电
*gpbdat &= ~1; //关背光灯
dma_free_writecombine(NULL,s3c_lcd->fix.smem_len,s3c_lcd->screen_base,s3c_lcd->fix.smem_start);
iounmap(gpbcon);
iounmap(gpccon);
iounmap(gpdcon);
iounmap(gpgcon);
iounmap(lcd_regs);
framebuffer_release(s3c_lcd);
写完出口函数,我们的LCD驱动程序就写完了。
下面就是测试了:测试的步骤为:
- 进入内核目录:make menuconfig ,去掉原来的LCD驱动程序
- make uImage :生成没有LCD驱动的内核
- cp arch/arm/boot/uImage /work/nfs_root/uImage_nolcds
- make modules :得到cfd_fillrect,cfb_copyarea,cfb_imageblit函数对应的模块
- 使用新的uImage_nolcds 启动:nfs 30000000 192.168.1.111:/work/nfs_root/uImage_nolcds bootm 30000000
- 编译自己写的LCD驱动程序,并将其考到根文件系统
- 在开发板上使用安装驱动:insmod cfbcopyarea.ko insmod cfdfillrect.ko insmod cfbimageblit.ko insmod lcd.ko
- echo hollo >/dev/tty1 ,可以在LCD上可以看到hello
- cat lcd.ko >/dev/fb0 ,花屏
- 修改/etc/inittab,加一行tty1::askfirst:-/bin/sh
- 使用新的uImage_nolcds 重新启动:nfs 30000000 192.168.1.111:/work/nfs_root/uImage_nolcds bootm 30000000
- insmod input.ko
- 可以按键s2,s3,s4,屏幕会显示ls,和ls命令后的内容
我的文章中可能有些概念或道理讲的不详细,下面是我看到的几篇我认为比较好的文章我在这里转载:
linux LCD驱动(二)–FrameBuffer:这一篇对framebuffer做了比较详尽的描写,可以让你有跟深入的认识。
嵌入式Linux之我行——S3C2440上LCD驱动(FrameBuffer)实例开发讲解:这一篇确实像其所说的是个实例,可以让你对LCD驱动的整个过程有一个全面的了解。
嵌入式Linux驱动笔记(三)------LCD驱动程序:这篇文章中使用的是480*272 的TFT屏,并附加了全部的驱动程序。
linux驱动LCD 驱动程序代码编写:这篇文章中对LCD驱动的测试有很详尽的描写
10-S3C2440驱动学习(四)嵌入式linux-LCD驱动程序:这篇文章发现的比较晚,发现这篇文章是最接近视频的,他的很多地方都使用视频的截图加以说明,使得更容易回顾老师所讲的内容。