入局:应用程序是如何操控LCD显示器的?
我们知道应用程序的调用接口,无非open/read/write...然后通过驱动程序最终作用到硬件设备上。以字符设备为例,对于驱动的开发者,实现了应用程序调用的驱动层中与之相匹配的drv_open/drv_read/drv_write函数,为应用层序提供了操作实际硬件设备的通道。那么,对于LCD驱动程序又是如何?先来了解下两个非常重要的概念。
一、LCD控制器
LCD控制器的功能是控制驱动信号,进而驱动LCD。用户只需要通过读写一系列的寄存器,完成配置和显示驱动。在驱动LCD设计的过程中首要的是配置LCD控制器,而在配置LCD控制器中最重要的一步则是帧缓冲区(Frame Buffer)的指定。用户所要显示的内容皆是从缓冲区中读出,从而显示到屏幕上的。帧缓冲区的大小由屏幕的分辨率和显示色彩数决定。驱动帧缓冲的实现是整个驱动开发过程的重点。
帧缓冲区是出现在Linux 2.2.xx及以后版本内核当中的一种驱动程序接口,这种接口将显示设备抽象为帧缓冲区设备区。帧缓冲区为图像硬件设备提供了一种抽象化处理,它代表了一些视频硬件设备,允许应用软件通过定义明确的界面来访问图像硬件设备。这样软件无须了解任何涉及硬件底层驱动的东西(如硬件寄存器)。它允许上层应用程序在图形模式下直接对显示缓冲区进行读写和I/O控制等操作。通过专门的设备节点可对该设备进行访问,如/dev/fb*。用户可以将它看成是显示内存的一个映像,将其映射到进程地址空间之后,就可以进行读写操作,而读写操作可以反映到LCD。
二、帧缓冲
帧缓冲(Frame Buffer)是Linux为显示设备提供的一个接口,把显存抽象后的一种设备,允许上层应用程序在图形模式下直接对显示缓冲区进行读写操作。用户不必关心物理显存的位置、换页机制等等具体细节,这些都是由Frame Buffer设备驱动来完成的。帧缓冲设备属于字符设备。
Linux系统Frame Buffer本质上只是提供了对图形设备的硬件抽象,在开发者看来,Frame Buffer是一块显示缓存,向显示缓存中写入特定格式的数据就意味着向屏幕输出内容。
由于有了frambuffer的抽象,使得应用程序通过定义好的接口就可以访问硬件。所以应用程序不需要考虑底层的(寄存器级)的操作。应用程序对设备文件的访问一般在/dev目录,如 /dev/fb*。
三、内核中的Frame Buffer
内核中的frambuffer在:drivers/video/fbmem.c(fb: frame buffer)
1.我们进入fbmem.c,从入口函数开始分析:
static int __init fbmem_init(void)
{
create_proc_read_entry("fb", 0, NULL, fbmem_read_proc, NULL);
if (register_chrdev(FB_MAJOR,"fb",&fb_fops)) //(1)创建字符设备
printk("unable to get major %d for fb devs\n", FB_MAJOR);
fb_class = class_create(THIS_MODULE, "graphics"); //(2)创建类
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;
}
(1) 创建字符设备"fb", FB_MAJOR=29,主设备号为29。
(2)创建类,但并没有创建设备节点,因为需要注册了LCD驱动后,才会有设备节点;
2.应用层对驱动的操作、数据的读取
2.1 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])) //(1) 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;
}
(1) registered_fb[fbidx] 这个数组也是fb_info结构体,其中fbidx等于次设备号id,显然这个数组就是保存我们各个lcd驱动的信息;
2.2 fb_read函数如下:
static ssize_t fb_read(struct file *file, char __user *buf, size_t count, loff_t *ppos)
{
unsigned long p = *ppos;
struct inode *inode = file->f_path.dentry->d_inode;
int fbidx = iminor(inode); //获取次设备号
struct fb_info *info = registered_fb[fbidx]; //获取次设备号的lcd驱动的信息
u32 *buffer, *dst;
u32 __iomem *src;
int c, i, cnt = 0, err = 0;
unsigned long total_size;
... ...
if (info->fbops->fb_read)
return info->fbops->fb_read(info, buf, count, ppos);
total_size = info->screen_size; //获取屏幕长度
... ...
buffer = kmalloc((count > PAGE_SIZE) ? PAGE_SIZE : count,GFP_KERNEL); //分配缓冲区
if (!buffer)
return -ENOMEM;
src = (u32 __iomem *) (info->screen_base + p); //获取显存物理基地址
if (info->fbops->fb_sync)
info->fbops->fb_sync(info);
while (count) {
c = (count > PAGE_SIZE) ? PAGE_SIZE : count; //获取页地址
dst = buffer;
/*因为src是32位,一个src等于4个字节,所以页地址c >> 2*/
for (i = c >> 2; i--; )
*dst++ = fb_readl(src++); //读取显存每个像素点数据,放到dst地址上
if (c & 3) {
u8 *dst8 = (u8 *) dst;
u8 __iomem *src8 = (u8 __iomem *) src;
for (i = c & 3; i--;)
*dst8++ = fb_readb(src8++);
src = (u32 __iomem *) src8;
}
if (copy_to_user(buf, buffer, c)) { //上传数据,长度等于页地址大小
err = -EFAULT;
break;
}
*ppos += c;
buf += c;
cnt += c;
count -= c;
}
kfree(buffer);
return (err) ? err : cnt;
}
从.open和.read函数中可以发现,都依赖于fb_info帧缓冲信息结构体,它从registered_fb[fbidx]数组中得到,这个数组保存我们各个lcd驱动的信息。由此可见,fbmem.c提供的都是些抽象出来的东西,最终都得依赖registered_fb这个数组。
3. registered_fb的注册
int register_framebuffer(struct fb_info *fb_info)
{
... ...
for (i = 0 ; i < FB_MAX; i++) //查找空的数组
if (!registered_fb[i])
break;
fb_info->node = i;
... ...
/*创建设备节点,名称为fdi,主设备号为29,次设备号为i */
fb_info->dev = device_create(fb_class, fb_info->device,MKDEV(FB_MAJOR, i), "fb%d", i);
... ...
registered_fb[i] = fb_info;
... ...
}
这个register_framebuffer()除了注册fb_info,还创建了设备节点。
4. xxxfb.c
以s3c2410fb.c为例,分析驱动的实现。
1.我们进入drivers/video/s3c2410fb.c,从入口函数开始分析:
int __devinit s3c2410fb_init(void)
{
return platform_driver_register(&s3c2410fb_driver);
}
既然是总线设备驱动模型,那我们关心的是它的probe函数。
2. 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;
//根据平台设备,获得某些硬件相关的信息;如:
//获取LCD设备信息(长宽、类型等)
mach_info = pdev->dev.platform_data;
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); //1.分配一个fb_info结构体
if (!fbinfo) {
return -ENOMEM;
}
/*2.设置fb_info*/
info = fbinfo->par;
info->fb = fbinfo;
info->dev = &pdev->dev;
... ...
/*3.硬件相关的操作,设置中断,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); //4.注册一个fb_info结构体
if (ret < 0) {
printk(KERN_ERR "Failed to register framebuffer device: %d\n", ret);
goto free_video_memory;
}
... ...
return ret;
}
看到这里驱动的写法也大致清晰:
- 分配一个fb_info结构体: framebuffer_alloc();
- 设置fb_info
- 硬件相关的操作(设置中断,LCD时钟频率,显存地址, 配置引脚... ...)
- 注册fb_info: register_framebuffer()
附:
LCD的显示过程与时序:
1.显示从屏幕左上角第一行的第一个点开始,一个点一个点地在LCD上显示,点与点之间的时间间隔为VCLK(像素时钟信号);当显示到屏幕的最右边就结束这一行(Line),这一行的显示对应时序图上的HSYNC(水平同步信号)
2. 接下来显示指针又回到屏幕的左边从第二行开始显示,显示指针针在从第一行的右边回到第二行的左边是需要一定的时间的,我们称之为行切换。
3. 以此类推,显示指针就这样一行一行的显示至矩形的右下角才把一幅图像(帧:frame)显示完成,这一帧的显示时间在时序图上表示为VSYNC(垂直同步信号)。
参考:
https://sites.google.com/a/hongdy.org/www/linux/kernel/lcddriver