LCD操作原理
在Linux系统中通过Framebuffer(简称为fb,又叫显存)驱动程序来控制LCD.
Frame是帧的意思,buffer是缓冲的意思,这意味着Framebuffer就是一块内存,里面保存着一帧图像。Framebuffer中保存着一帧图像的每一个像素颜色值,假设LCD的分辨率是1024x768,每一个像素的颜色用32位来表示,那么Framebuffer的大小就是:1024x768x32/8=3145728字节。
简单介绍LCD的操作原理:
① 驱动程序设置好LCD控制器:
根据LCD的参数设置LCD控制器的时序、信号极性;
根据LCD分辨率、BPP分配Framebuffer。
② APP使用ioctl获得LCD分辨率、BPP
③ APP通过mmap映射Framebuffer,在Framebuffer中写入数据
假设需要设置LCD中坐标(x,y)处像素的颜色,首要要找到这个像素对应的内存,然后根据它的BPP值设置颜色。假设fb_base是APP执行mmap后得到的Framebuffer地址,如下图所示:
可以用以下公式算出(x,y)坐标处像素对应的Framebuffer地址:
(x,y)像素起始地址=fb_base+(xres*bpp/8)*y + x*bpp/8
最后一个要解决的问题就是像素的颜色怎么表示?它是用RGB三原色(红、绿、蓝)来表示的,在不同的BPP格式中,用不同的位来分别表示R、G、B,如下图所示:
对于32BPP,一般只设置其中的低24位,高8位表示透明度,一般的LCD都不支持。
对于24BPP,硬件上为了方便处理,在Framebuffer中也是用32位来表示,效果跟32BPP是一样的。
对于16BPP,常用的是RGB565;很少的场合会用到RGB555,这可以通过ioctl读取驱动程序中的RGB位偏移来确定使用哪一种格式。
上面指的是一个像素在framebuffer中的存放格式!比如一个rgb888像素 在framebuffer中是使用4字节来存放 但是其高8位是无效位。
问题来了:
1.Frambuffer在哪里?
可能是芯片的内存 也 可能是和lcd屏幕集成在一起 这得看这款LCD的接口模式
2.谁把Frambuffer中的数据发给LCD?
LCD控制器会周而复始的把Framebuffer中的RGB数据发送给LCD
LCD的接口模式
1.8080接口模式
这种接口将LCD控制器 显存 LCD屏幕 集成在为一个芯片。所以这种接口的模块 我们重点是需要阅读数据手册,因为每个厂家的芯片内部实现可能不一样。
这种接口就和我们在学习32的时候使用到OLED屏幕一样.
2.TFT-RGB接口模式
这种接口将LCD控制器集成在ARM芯片内部,这种接口的好处就是可以使用内存来自己分配Frambuffer的大小,可以支持更高分辨率的屏幕而不用担心显存.
所以这种接口的重点就在于分配Frambuffer和初始化LCD控制器。
搞清楚上图中接口的作用!
假设上图是一个LCD屏幕,屏幕中一个一个密密麻麻的黑点称之为像素点,每一行有若干个点,试想下有一个电子枪,电子枪位于某一个像素点的背后,然后向这个像素发射红,绿,蓝三种原色,这三种颜色不同比例的组合成任意一种颜色。电子枪在像素点的背后,一边移动一边发出各种颜色的光,电子枪从左往右移动,到右边边缘之后就跳到下一行的行首,继续从左往右移动,如此往复,一直移动到屏幕右下角的像素点,最后就跳回原点。
** 问题1:电子枪如何移动?**
答: 有一条像素时钟信号线(DCLK),连接屏幕,每来一个像素时钟信号(DCLK),电子枪就移动一个像素。
** 问题2:电子枪打出的颜色该如何确定?**
答:有三组红,绿,蓝信号线(RGB),连接屏幕,由这三组信号线(RGB)确定颜色
** 问题3:电子枪移动到LCD屏幕右边边缘时,如何得知需要跳到下一行的行首?**
答:有一条水平同步信号线(HSYNC),连接屏幕,当接收到水平同步信号(HSYNC),电子枪就跳到下一行的最左边
** 问题4:电子枪如何得知需要跳到原点?**
答:有一条垂直同步信号线(VSYNC),连接屏幕,当接收到垂直同步信号线(VSYNC),电子枪就由屏幕右下脚跳到左上角(原点)
问题5:电子枪如何得知三组信号线(RGB)确定的颜色就是它是需要的呢?
答:有一条RGB数据使能信号线(DE),连接屏幕,当接收到数据使能信号线(DE),电子枪就知道这时由这三组信号线(RGB)确定的颜色是有效的,可以发射到该像素点。
1.DCLK(像素时钟信号)
> 简单说就是:每来一个像素时钟信号(DCLK),就移动一个像素。所以DCLK的单位可以认为是像素!
像素时钟信号都有以下两个方面的作用:
(1)指挥RGB信号按顺序传输。像素时钟信号就像指挥员指挥队伍时发出的口令“一、二,一、二……”,数字RGB信号在像素时钟信号的作用下,按照一定的顺序,由驱动板传输到LCD面板中,使各电路按照一定的节拍协调地工作。
(2)确保数据传输的正确性。无论是驱动板电路,还是LCD面板电路,在读取数字RGB信号时,都是在像素时钟的作用与控制下进行的,各电路只有在像素时钟的下降沿(或上升沿)到来时才对数字RGB数据进行读取,以确保读取数据的正确性。图所示为像素时钟与数字RGB信号之间的对应关系示意图(1024×768液晶面板)。
2.RGB三组线
(R[0:7] ,G[0:7],B[0:7] 也就是data线)
三组信号线组成,分别代表R(红色),G(绿色),B(蓝色),这三组信号中的每一组都会有8根信号,三组共同组成24根线来控制颜色数据。
> **LCD的RGB三组引脚上,数据是直接来自Framebuffer的吗?** 对于使用真彩色的LCD控制器,RGB引脚上的数据确实是来自Framebuffer;
>
> 但是对于使用调色板的LCD控制器,Framebuffer中的数据只是用来取出调色板中的颜色,调色板中的数据会被放到RGB引脚上去。
3.HSYNC(水平同步信号)
简单说就是当写完一行像素就回到行首
4.VSYNC(垂直同步信号)
简单说就是当移动到右下角的像素时就移回到左上角像素
5.DE(data enable)
数据使能线。
fb_info
我们需要知道对于每一个硬件设备(LCD)都需要一个 fb_info结构体来描述
它包含了这个硬件设备的基本信息以及操作函数 也就是说不同的fb_info对应不同的硬件设备
fb_info结构体:
重点关注 var 、fix和 fbops 这三个参数。
- var这个结构体保存:分辨率、BPP(多少位表示一个像素)、红绿蓝的位置等等信息
- fix这个结构体保存 :显存的起始地址、显存的长度等等信息
- fbops这个结构体保存:这个Framebuffer对应的操作函数
重点注意的参数:
var (可变参数)中:
fix(固定参数中):
从smem_len 和 xres 和 yres 就可以知道驱动程序支持的最大framebuffer数量,我们可以根据这个最大framebuffer数量在应用程序 使用ioctl来设置yres_virtual 也就是启用多少个framebuffer!!!!!
内核是如何来管理 不同的硬件设备对应的fb_info结构体的呢?
通过register_fb这个指针数组 每个成员都是一个指向fb_info结构体的指针
我们在给每一个硬件设备编写驱动程序的时候 都会注册一个 fb_info结构体 来描述这个硬件设备的信息
使用register_farmebuffer()来注册fb_info
- register_framebuffer ( ):
int
register_framebuffer(struct fb_info *fb_info)
{
int ret;
mutex_lock(®istration_lock);
ret = do_register_framebuffer(fb_info);
mutex_unlock(®istration_lock);
return ret;
}
static int do_register_framebuffer(struct fb_info *fb_info)
{
int i, ret;
struct fb_event event;
struct fb_videomode mode;
if (fb_check_foreignness(fb_info))
return -ENOSYS;
ret = do_remove_conflicting_framebuffers(fb_info->apertures,
fb_info->fix.id,
fb_is_primary_device(fb_info));
if (ret)
return ret;
if (num_registered_fb == FB_MAX)
return -ENXIO;
num_registered_fb++;
for (i = 0 ; i < FB_MAX; i++)
if (!registered_fb[i])
break;
fb_info->node = i;
atomic_set(&fb_info->count, 1);
mutex_init(&fb_info->lock);
mutex_init(&fb_info->mm_lock);
fb_info->dev = device_create(fb_class, fb_info->device,
MKDEV(FB_MAJOR, i), NULL, "fb%d", i);
if (IS_ERR(fb_info->dev)) {
/* Not fatal */
printk(KERN_WARNING "Unable to create device for framebuffer %d; errno = %ld\n", i, PTR_ERR(fb_info->dev));
fb_info->dev = NULL;
} else
fb_init_device(fb_info);
if (fb_info->pixmap.addr == NULL) {
fb_info->pixmap.addr = kmalloc(FBPIXMAPSIZE, GFP_KERNEL);
if (fb_info->pixmap.addr) {
fb_info->pixmap.size = FBPIXMAPSIZE;
fb_info->pixmap.buf_align = 1;
fb_info->pixmap.scan_align = 1;
fb_info->pixmap.access_align = 32;
fb_info->pixmap.flags = FB_PIXMAP_DEFAULT;
}
}
fb_info->pixmap.offset = 0;
if (!fb_info->pixmap.blit_x)
fb_info->pixmap.blit_x = ~(u32)0;
if (!fb_info->pixmap.blit_y)
fb_info->pixmap.blit_y = ~(u32)0;
if (!fb_info->modelist.prev || !fb_info->modelist.next)
INIT_LIST_HEAD(&fb_info->modelist);
if (fb_info->skip_vt_switch)
pm_vt_switch_required(fb_info->dev, false);
else
pm_vt_switch_required(fb_info->dev, true);
fb_var_to_videomode(&mode, &fb_info->var);
fb_add_videomode(&mode, &fb_info->modelist);
registered_fb[i] = fb_info;
event.info = fb_info;
if (!lockless_register_fb)
console_lock();
if (!lock_fb_info(fb_info)) {
if (!lockless_register_fb)
console_unlock();
return -ENODEV;
}
fb_notifier_call_chain(FB_EVENT_FB_REGISTERED, &event);
unlock_fb_info(fb_info);
if (!lockless_register_fb)
console_unlock();
return 0;
}
在do_regitser_framebuffer()这个函数中我们可以注意到两句代码:
for (i = 0 ; i < FB_MAX; i++)
if (!registered_fb[i])
break;
以及
registered_fb[i] = fb_info;
从这两段代码可以看出:
我们调用register_framebuffer(my_fb_info)其实就是把我们要注册的fb_info结构体放入全局数组 registered_fb :
从代码也可以看出 它是一个个看数组成员是否已经使用 如果没有被使用就使用,而不是使用对应的数组下标为次设备号的成员,我猜这是因为 我们的硬件设备对应的次设备号在 注册的时候会按顺序注册 然后再调用register_framebuffer() 这样子一般来说就会将我们注册的fb_info 放入到数组下标为次设备号的成员中了,所以在后面会判断一些file->private_data 和 registered_fb[minor]是否相同
也就是说内核通过 registered_fb数组来管理 每个硬件设备的 fb_info结构体,每次注册fb_info结构体 都会将其放入到 registered_fb数组中去。
一般来说硬件设备的设备节点的次设备号 就对应 其fb_info在 registered_fb数组的哪一个下标成员中
LCD驱动程序基本框架
Framebuffer驱动程序框架
分为上下两层:
- fbmem.c:承上启下
- 实现、注册cdev结构体 和 file_operations结构体
- 把APP的read、write等调用向下转发到具体的硬件驱动程序xxx_fb.c,以使用具体的xxx_fb_read、xxx_fb_write函数
- xxx_fb.c:硬件相关的驱动程序
- 实现、注册fb_info结构体
- 实现硬件操作
因为每个lcd屏幕硬件架构都不一样 ,fbmem.c文件不可能适用于全部的lcd屏幕 具体的lcd需要对应具体的驱动文件 也就是说fbmem.c就是起一个中转的作用 让我们应用程序访问到具体的LCD屏幕对应的 xxx_fb.c 驱动程序
面试如果问 LCD驱动程序的框架 就从这开始说 上下两层 然后展开了讲!
1.fbmem.c代码分析!
我们先来看看fbmem.c的代码(最好去看fbmem.c的源码!)
首先先看入口函数:
****我们可以看出在入口函数调用了:
register_chrdev(FB_MAJOR, "fb", &fb_fops)
也就是说 将主设备号 29 下的所有次设备号范围 内的设备节点 都使用一个cdev结构体来描述。(也就是说所有硬件设备(lcd)的主设备号都为29,和我们之前在学习输入子系统时一样所有输入设备的主设备号都为13 输入设备的设备节点都在/dev/input/下面) 这个cdev结构体对应的file_operations结构体是:
fb_ops
所以 我们应用程序 访问主设备号29下的设备节点 也就是fbx节点 比如fb0 、fb1 通过其文件描述符
调用read、write接口 就会调用到 fb_ops 这个file_operations 中的fb_read、fb_write 。
我们以应用程序调用open、ioctl、read为例子,就会调用到fbmem.c中的fb_fops中的 fb_open、fb_read函数、fb_ioctl函数为例子:
重点看下面这部分!
首先
在do_regitser_framebuffer()这个函数中我们可以注意到两句代码:
for (i = 0 ; i < FB_MAX; i++)
if (!registered_fb[i])
break;
以及
registered_fb[i] = fb_info;
从这两段代码可以看出:
我们调用register_framebuffer(my_fb_info)其实就是把我们要注册的fb_info结构体放入全局数组 registered_fb :
从代码也可以看出 它是一个个看数组成员是否已经使用 如果没有被使用就使用,而不是使用对应的数组下标为次设备号的成员,我猜这是因为 我们的硬件设备对应的次设备号在 注册的时候会按顺序注册 然后再调用register_framebuffer() 这样子一般来说就会将我们注册的fb_info 放入到数组下标为次设备号的成员中了
1.fb_open:
应用程序调用open 打开设备节点 就会调用到fbmem.c中的 fb_open函数 然后返回一个文件描述符:
fb_open(struct inode *inode, struct file *file)
__acquires(&info->lock)
__releases(&info->lock)
{
int fbidx = iminor(inode);
struct fb_info *info;
int res = 0;
info = get_fb_info(fbidx);
if (!info) {
request_module("fb%d", fbidx);
info = get_fb_info(fbidx);/* 获取fb_info */
if (!info)
return -ENODEV;
}
if (IS_ERR(info))
return PTR_ERR(info);
mutex_lock(&info->lock);
if (!try_module_get(info->fbops->owner)) {
res = -ENODEV;
goto out;
}
file->private_data = info;
if (info->fbops->fb_open) {
res = info->fbops->fb_open(info,1);
if (res)
module_put(info->fbops->owner);
}
#ifdef CONFIG_FB_DEFERRED_IO
if (info->fbdefio)
fb_deferred_io_open(info, inode, file);
#endif
out:
mutex_unlock(&info->lock);
if (res)
put_fb_info(info);
return res;
}