1. 基本概念
LCD是Liquid Crystal Display的简称,也就是经常所说的液晶显示器。LCD能够支持彩色图像的显示和视频的播放,是一种非常重要的输出设备。如果我们的系统要用GUI(图形界面接口),比如minigui,MicroWindows。这时LCD设备驱动程序就应该编写成frambuffer接口,而不是编写成仅仅操作底层的LCD控制器接口。
framebuffer是Linux系统为显示设备提供的一个接口,它将显示缓冲区抽象,屏蔽图像硬件的底层差异,允许上层应用程序在图形模式下直接对显示缓冲区进行操作。framebuffer又叫帧缓冲,是Linux为操作显示设备提供的一个用户接口。用户应用程序可以通过framebuffer透明地访问不同类型的显示设备。从这个方面来说,framebuffer是硬件设备显示缓冲区的抽象。Linux抽象出framebuffer这个帧缓冲区可以供用户应用程序直接读写,通过更改framebuffer中的内容,就可以立刻显示在LCD显示屏上。
framebuffer是一个标准的字符设备,主设备号是29,次设备号根据缓冲区的数目而定。framebuffer对应/dev/fb%d设备文件。根据显卡的多少,设备文件可能是/dev/fb0、/dev/fb1等。缓冲区设备也是一种普通的内存设备,可以直接对其进行读写。
对用户程序而言,它和/dev下面的其他设备没有什么区别,用户可以把frameBuffer看成一块内存,既可以写,又可以读。显示器将根据内存数据显示对应的图像界面。这一切都由LCD控制器和响应的驱动程序来完成。
2. 分析内核
2.1 驱动框架
LCD驱动也是一个字符设备驱动,那么内核中是如何实现的呢?框架如下图:
接下来我们便通过上图结构深入分析内核实现LCD驱动的过程。
2.2 fbmem.c(drivers/video中)
fbmem.c是frambuffer驱动的核心,他向上给应用程序提供了系统调用接口,向下对特定的硬件提供底层的驱动接口。底层驱动可以通过接口向内核注册自己。fbmem.c提供了frambuffer驱动的所有接口代码,从而避免了。
2.2.1 入口函数fbmem_init()
首先我们定位到内核(linux-2.6.22.6)的fbmem.c的入口函数fbmem_init(),代码如下:
fbmem_init(void)
{
create_proc_read_entry("fb", 0, NULL, fbmem_read_proc, NULL);
if (register_chrdev(FB_MAJOR,"fb",&fb_fops)) //创建字符设备
printk("unable to get major %d for fb devs\n", FB_MAJOR);
fb_class = class_create(THIS_MODULE, "graphics"); //创建类
if (IS_ERR(fb_class)) {
printk(KERN_WARNING "Unable to create fb class; errno = %ld\n", PTR_ERR(fb_class));
fb_class = NULL;
}
return 0;
}
在fbmem_init()中创建字符设备"fb", 注册file_oprations结构体fb_fops,主设备FB_MAJOR为29。启动内核后在开发板上执行“cat /proc/devices”如下:
可以看到,确实是创建了主设备号为29的"fb"设备,而这里还没有创建设备节点,后面会提到,内核将该工作放到注册lcd驱动的接口函数里了。
2.2.2 fb_open()
继续将代码定位到注册的file_operations结构体里面的fb_open()函数,部分代码如下:
static int fb_open(struct inode *inode, struct file *file)
{
int fbidx = iminor(inode); //获取设备节点的次设备号
struct fb_info *info; //定义fb_info结构体
int res = 0;
...
if (!(info = registered_fb[fbidx])) //info= registered_fb[fbidx],获取此设备号的lcd驱动信息
try_to_load(fbidx);
...
if (info->fbops->fb_open) {
res = info->fbops->fb_open(info,1); //调用registered_fb[fbidx]->fbops->fb_open
if (res)
module_put(info->fbops->owner);
}
return res;
}
fb_open()函数间接调用registered_fb[fbidx]->fbops->fb_open(),内核搜索registered_fb发现(include/linux/fb.h中):
extern struct fb_info *registered_fb[FB_MAX]; //#define FB_MAX 32
可知registered_fb是一个struct fb_info结构体类型全局数组,搜索内核发现在register_framebuffer()函数中被赋值。
2.2.3 register_framebuffer()
register_framebuffer()函数部分代码如下:
int register_framebuffer(struct fb_info *fb_info)
{
int i;
...
for (i = 0 ; i < FB_MAX; i++) //查找空的registered_fb数组项
if (!registered_fb[i])
break;
fb_info->node = i;
fb_info->dev = device_create(fb_class, fb_info->device, MKDEV(FB_MAJOR, i), "fb%d", i); //创建设备
...
registered_fb[i] = fb_info; //填充新的registered_fb数组项
...
return 0;
}
register_framebuffer()函数首先从registered_fb数组中查找空的数组项,然后填充fb_info结构体,赋给这个空的数组项中,在这里还创建了设备节点(前面创建字符设备未完成的工作)。从这里我们可以看出,register_framebuffer()函数通过注册各种各样的fb_info,来让内核支持多种LCD设备,并且以/dev/fb*的形式命名。
2.2.4 fb_mmap()
framebuffer的显示缓冲区位于Linux的内核态地址空间。而在Linux中,每个应用程序都有自己的虚拟地址空间,在应用程序中是不能直接访问物理缓冲区的。为此,Linux在文件操作file_operations结构中提供了mmap()函数,可将文件的内容映射到用户空间。对应帧缓冲设备,则可以通过映射操作,将屏幕缓冲区的物理地址映射到用户空间的一段虚拟地址中,之后用户就可以通过读写这段虚拟地址访问屏幕缓冲区,在屏幕上绘图。
2.2.5 总结
通过引入fb_info的形式,将硬件相关的部分与fs文件设备操作分离开,增加了内核代码的稳定性。我们只需调用register_framebuffer()函数注册一个新的fb_info结构体,即可向内核新增一个LCD驱动设备。
2.3 s3c2410fb.c(drivers/video中)
接下来我们再来分析内核如何构建fb_info结构体。
2.3.1 入口函数s3c2410fb_init()
分析驱动,首先从入口函数入手,s3c2410fb_init()代码如下:
int __devinit s3c2410fb_init(void)
{
return platform_driver_register(&s3c2410fb_driver);
}
该函数注册一个platform_driver,从上一节十一、Linux驱动之platform总线设备驱动可知,当内核有成员.name名称相同的platform_device时,会调用到platform_driver里的成员.probe,在这里就是s3c2410fb_probe()函数。
2.3.2 s3c2410fb_probe()
s3c2410fb_probe()部分代码如下:
static int __init s3c2410fb_probe(struct platform_device *pdev)
{
struct s3c2410fb_info *info;
struct fb_info *fbinfo;
struct s3c2410fb_hw *mregs;
int ret;
int irq;
int i;
u32 lcdcon1;
mach_info = pdev->dev.platform_data; //获取LCD设备信息(长宽、类型等)
if (mach_info == NULL) {
dev_err(&pdev->dev,"no platform data for lcd, cannot attach\n");
return -EINVAL;
}
mregs = &mach_info->regs;
irq = platform_get_irq(pdev, 0); //得到中断号
if (irq < 0) {
dev_err(&pdev->dev, "no irq for device\n");
return -ENOENT;
}
fbinfo = framebuffer_alloc(sizeof(struct s3c2410fb_info), &pdev->dev); //分配一个fb_info结构体
if (!fbinfo) {
return -ENOMEM;
}
/*设置fb_info*/
info = fbinfo->par;
info->fb = fbinfo;
info->dev = &pdev->dev;
... ...
/*硬件相关的操作,设置中断,LCD时钟频率,显存地址, 配置引脚... ...*/
ret = request_irq(irq, s3c2410fb_irq, IRQF_DISABLED, pdev->name, info); //设置中断
info->clk = clk_get(NULL, "lcd"); //获取时钟
clk_enable(info->clk); //使能时钟
ret = s3c2410fb_map_video_memory(info); //显存地址
ret = s3c2410fb_init_registers(info); //设置寄存器,配置引脚
... ...
ret = register_framebuffer(fbinfo); //注册一个fb_info结构体
if (ret < 0) {
printk(KERN_ERR "Failed to register framebuffer device: %d\n", ret);
goto free_video_memory;
}
...
return ret;
}
该函数主要工作内容如下:
(1) 分配一个fb_info结构体
(2) 设置fb_info
(3) 与LCD硬件相关的操作
(4) 注册fb_info结构体
接下来仿造s3c2410fb.c编写LCD驱动程序。
3. 编写代码
3.1 代码框架
3.1.1 在LCD驱动的入口函数中
1. 分配一个fb_info结构体
2. 设置fb_info
2.1 设置固定的参数fb_info-> fix
2.2 设置可变的参数fb_info-> var
2.3 设置操作函数fb_info-> fbops
2.4 设置fb_info 其它的成员
3. 设置硬件相关的操作
3.1 配置LCD引脚
3.2 根据LCD手册设置LCD控制器
3.3 分配显存(framebuffer),把地址告诉LCD控制器和fb_info
4. 开启LCD,并注册fb_info: register_framebuffer()
4.1 直接在init函数中开启LCD(后面讲到电源管理,再来优化)
4.1.1 控制LCDCON5允许PWREN信号,
4.1.2 然后控制LCDCON1输出PWREN信号,
4.1.3 输出GPB0高电平来开背光,
4.2 注册fb_info
3.1.2 在LCD驱动的出口函数中
1. 卸载内核中的fb_info
2. 控制LCDCON1关闭PWREN信号,关背光,iounmap注销地址
3. 释放DMA缓存地址dma_free_writecombine()
4. 释放注册的fb_info
3.2 编写代码
驱动程序lcd.c代码如下:
#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 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_ops s3c_lcdfb_ops = {
.owner = THIS_MODULE,
.fb_setcolreg = s3c_lcdfb_setcolreg,
.fb_fillrect = cfb_fillrect, //填充矩形
.fb_copyarea = cfb_copyarea, //复制数据
.fb_imageblit = cfb_imageblit, //绘画图形
};
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;
static volatile struct lcd_regs* lcd_regs;
static u32 pseudo_palette[16];
/* 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;
}
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;
}
static int lcd_init(void)
{
/* 1. 分配一个fb_info */
s3c_lcd = framebuffer_alloc(0, NULL);
/* 2. 设置 */
/* 2.1 设置固定的参数 */
strcpy(s3c_lcd->fix.id, "mylcd");
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为真彩色,所以要设置成这个
s3c_lcd->fix.line_length = 480*2; //每行的长度,以字节为单位
/* 2.2 设置可变的参数 */
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.bits_per_pixel = 16; //每个象素使用多少位
/* RGB:565 */
s3c_lcd->var.red.offset = 11; //红色偏移值为11
s3c_lcd->var.red.length = 5; //红色位长为5
s3c_lcd->var.green.offset = 5; //绿色偏移值为5
s3c_lcd->var.green.length = 6; //绿色位长为6
s3c_lcd->var.blue.offset = 0; //蓝色偏移值为0
s3c_lcd->var.blue.length = 5; //蓝色位长为5
s3c_lcd->var.activate = FB_ACTIVATE_NOW; //使设置的值立即生效
/* 2.3 设置操作函数 */
s3c_lcd->fbops = &s3c_lcdfb_ops;
/* 2.4 其他的设置 */
s3c_lcd->pseudo_palette = pseudo_palette; //存放调色板所调颜色的数组
//s3c_lcd->screen_base = ; //显存的虚拟地址,这个在后面设置
s3c_lcd->screen_size = 480*272*16/8; //显存的大小
/* 3. 硬件相关的操作 */
/* 3.1 配置GPIO用于LCD */
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 */
/* 3.2 根据LCD手册设置LCD控制器, 比如VCLK的频率等 */
lcd_regs = ioremap(0x4D000000, sizeof(struct lcd_regs));
lcd_regs->lcdcon1 = (4<<8) | (3<<5) | (0x0c<<1);
lcd_regs->lcdcon2 = (1<<24) | (271<<14) | (1<<6) | (9);//垂直方向的时间参数
lcd_regs->lcdcon3 = (1<<19) | (479<<8) | (1); //水平方向的时间参数
lcd_regs->lcdcon4 = 40; //水平方向的同步信号
lcd_regs->lcdcon5 = (1<<11) | (0<<10) | (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->lcdcon1 |= (1<<0); /* 使能LCD控制器 */
lcd_regs->lcdcon5 |= (1<<3); /* 使能LCD本身 */
*gpbdat |= 1; /* 输出高电平, 使能背光 */
/* 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(gpbcon);
iounmap(gpccon);
iounmap(gpdcon);
iounmap(gpgcon);
framebuffer_release(s3c_lcd);
}
module_init(lcd_init);
module_exit(lcd_exit);
MODULE_LICENSE("GPL");
Makefile代码如下:
KERN_DIR = /work/system/linux-2.6.22.6 //内核目录
all:
make -C $(KERN_DIR) M=`pwd` modules
clean:
make -C $(KERN_DIR) M=`pwd` modules clean
rm -rf modules.order
obj-m += lcd.o
4. 测试
内核:linux-2.6.22.6
编译器:arm-linux-gcc-3.4.5
环境:ubuntu9.10
4.1 配置内核
1. 重新配置内核,将内核自带的lcd驱动配置为模块。在linux-2.6.22.6内核目录下执行:
make menuconfig
2. 配置步骤如下:
Device Drivers --->
Graphics support --->
<M> S3C2410 LCD framebuffer support
3. 编译内核与模块
make uImage
make modules
4.2 重烧内核
1. 将linux-2.6.22.6/arch/arm/boot下的uImage烧写到开发板,网络文件系统启动。
2. 编译lcd.c文件,拷贝cfbcopyarea.ko、cfbfillrect.ko、cfbimgblt.ko到网络文件系统。
make
cp linux-2.6.22.6/drivers/video/cfb*.ko /work/nfs_root/first_fs
3. 装载驱动
insmod cfbcopyarea.ko
insmod cfbfillrect.ko
insmod cfbimgblt.ko
insmod lcd.ko
4.3 测试
1. 开发板上执行:
echo hello > /dev/tty1 (此时开发板lcd上有“hello”显示出来)
cat lcd.ko > /dev/fb0 (此时开发板lcd花屏)
vi /etc/inittab
# /etc/inittab
::sysinit:/etc/init.d/rcS
s3c2410_serial0::askfirst:-/bin/sh //添加这行代码,将输出信息输出到lcd上
tty1::askfirst:-/bin/sh
::ctrlaltdel:/sbin/reboot
::shutdown:/bin/umount -a -r
2. 安装十、Linux驱动之输入子系统使用里的驱动
Insmod buttons.ko (此时按下开发板上的按键值就能输出到lcd上了)
5. 相关知识点
分配DMA缓存函数函数原型如下:
void *dma_alloc_writecombine(struct device *dev, size_t size, dma_addr_t *handle, gfp_t gfp); //分配DMA缓存区给显存
参数:
dev:指针,填0表示这个申请的缓冲区里没有内容。
size:分配的地址大小(字节单位)。
handle:申请到的物理起始地址。
gfp:分配出来的内存参数。
该函数分配一段DMA缓存区,分配出来的内存会禁止cache缓存(因为DMA传输不需要CPU),它和 dma_alloc_coherent ()函数相似,不过 dma_alloc_coherent ()函数是分配出来的内存会禁止cache缓存以及禁止写入缓冲区。
对应函数:
dma_free_writecombine(dev,size,cpu_addr,handle); //释放缓存