分类: LINUX
2018-09-15 14:42:26
原文地址:LCD框架介绍 作者:wylhistory
LCD架构介绍
.
Revision History
Date Issue Description Author
<04/15/2010> <0.1> LCD架构分析 Wylhistory
目录
1. 摘要 3
2. 介绍 3
3. 思路 3
4. 初始化 4
4.1 设备和驱动注册 4
4.2 PROBE函数的执行 6
5. 用户使用流程 8
5.1 上层软件使用流程 8
5.2 驱动处理逻辑 9
6. 总结 12
7. 未讨论 12
1. 摘要
分析一下在pxa310平台,LCD驱动的架构;
2. 介绍
每次换LCD的屏都会经历一个痛苦的过程,所以想理一下它的架构,以便在以后的集成过程中,能有一个简单的指导;
硬件平台:pxa310
软件平台:android 2.1
内核:2.6.29
3. 思路
对于驱动的集成,我的一个原始的思路是这样的,驱动包含两方面的内容,
A) 硬件管理;
B) 提供对上接口,也就是提供给应用层接口;
从硬件管理的角度来说,任何一个模块想要工作,比如wifi,蓝牙, LCD等都必须要和我们的CPU相连,以实现通讯,所以要让这些模块工作,从上面一句话里面可以得出的结论就是这样:
(模块正常工作)==(模块本身工作正常)+(模块控制器工作正常)+(接口工作正常);
也就是说,想让一个驱动工作,就必须要考虑这三方面的内容,对应于我们要讨论的LCD的驱动,也就意味着:
A) LCD屏本身需要正确的初始化,以进入正常工作的状态(这个目前的lcd屏都不提供反馈信号);
B) LCD controller本身要正确的初始化,以进入正常的工作状态(这个可以通过寄存器的打印验证);
C) 接口需要正确配置,以实现LCD controller和LCD屏之间的通讯,接口通常都包含命令接口和数据接口,我们的命令接口是SPI的(通过gpio口模拟SPI协议),所以需要对SPI接口做相应的初始化,数据接口一般分为RGB接口和CPU接口,rgb一般是16根线,有时候需要扩展以适应屏的pin脚,而cpu通过是8根,我们这边也需要扩展,比如高位扩展比如我们这边传递5位的red,那么我们的m4——m0位接到对方的p7——p3,而对方的p0——p2就接到我们的m4——m2(这个可以通过示波器验证);
而从对上的接口来说,各个驱动通常都有统一的接口,比如:
Open,close,ioctl,write,read,mmap,当然不同的驱动有可能只选择某些来实现,对于LCD驱动来说,最重要的当然是Open,ioctl,以及mmap,我们后面可以看看它的代码逻辑;
下面会从初始化和上层使用角度来具体分析:
4. 初始化
4.1 设备和驱动注册
图4.1设备和驱动的注册
有的驱动设备的注册和驱动的注册是放在不同的地方的,不过LCD的驱动是放在同一个函数里面的,就在pxafb.c里面的pxafb_init里面;
按照惯例,是先注册设备,再注册驱动,我们可以看到,设备的注册最后到了函数pxa_register_device里面,看看它的代码:
void pxa_register_device(struct platform_device *dev, void *data)
{
int ret;
dev->dev.platform_data = data;
ret = platform_device_register(dev);
if (ret)
dev_err(&dev->dev, "unable to register device: %d\n", ret);
}
这里最重要的就是上面蓝色这句话,也就是保存了传入的这个info参数到platform_data里面,后面可以看到,很多操作都是需要这个数据结构的参与的,甚至可以说,这个数据结构对于LCD的驱动来说是至关重要的,那么不看看它,就不够人情了,如下:
struct pxafb_mach_info lin2008_lcdpar = {
.modes = lin2008_modes,
.num_modes = 1,
.lccr0 = LCCR0_Act ,
.lccr3 = LCCR3_STALL | LCCR3_OutEnH,
};
这里先看看这个lccr0的值,注释如下:
#define LCCR0_Act (LCCR0_PAS*1) /* Active display (TFT) */
简单说也就是定义这个屏的类型;
Lccr3的值定义如下:
#define LCCR3_STALL (1 << 28) /* Stall pixel clock if underrun occurs */
#define LCCR3_OutEnH (LCCR3_OEP*0) /* Output Enable active High */
翻译一下就是说,如果发生没有数据传输的情况,那么就停止像素时钟;
第二行,使能输出,并且是高有效;
这里我最关心的还是lin2008_modes,我们看看它的内容:
static struct pxafb_mode_info lin2008_modes[] = {
[0] = {
// .pixclock = 68296, // consider for 812*492*36 frame
// .pixclock = 48344, // consider for 812*492*60 frame
.pixclock = 40977, // consider for 504*807*60
// .pixclock = 68296, // consider for 504*807*36
.xres = 480,
.yres = 800,
.bpp = 16,
.hsync_len = 7,
.left_margin = 8,
.right_margin = 16,
.vsync_len = 1,
.upper_margin = 4,
.lower_margin = 3,
.sync = 0,
},
};
这里面的信息都很重要,一个个来说:
Pixclock:这里是指时钟频率的倒数,否则怎么算都不对的,而且这里没有计算行同步和帧同步,40997== (1<<12)/(480+8+16)×(800+4+3)×60;也就是说这个值等于x的分辨率加上前廊+后廊以后与y的分辨率加上上廊加下廊后的值的乘积;
OK,现在我们回到先前设备注册的地方,也就是函数pxa_register_device里面的dev成员,如下:
struct platform_device pxa_device_fb = {
.name = "pxa2xx-fb",
.id = -1,
.dev = {
.dma_mask = &fb_dma_mask,
.coherent_dma_mask = 0xffffffff,
},
.num_resources = ARRAY_SIZE(pxafb_resources),
.resource = pxafb_resources,
};
这里面唯一需要关注的就是这个资源,如下:
static struct resource pxafb_resources[] = {
[0] = {
.start = 0x44000000,
.end = 0x4400ffff,
.flags = IORESOURCE_MEM,
},
[1] = {
.start = IRQ_LCD,
.end = IRQ_LCD,
.flags = IORESOURCE_IRQ,
},
};
也就是说与LCD controller相关的寄存器的地址范围就在0x44000000——0x4400ffff之间;
既然设备以及注册了,我们再看看驱动的注册,
platform_driver_register(&pxafb_driver),这里最重要的还是pxafb_driver,下面看看它的值:
static struct platform_driver pxafb_driver = {
.probe = pxafb_probe,
#ifdef CONFIG_PM
.suspend = pxafb_suspend,
.resume = pxafb_resume,
#endif
.driver = {
.name = "pxa2xx-fb",
},
};
4.2 Probe函数的执行
根据platform设备的配备原则,只要名字相同,就可以结婚了,正所谓“金风玉露一相逢,便调用probe函数”
于是这里的probe函数也就是pxafb_probe开始被调用了,这个函数挺长的,总结一下,至少干了以下几件事:
1, 调用lcdchip_init对LCD屏本身进行初始化;
2, 取出inf = dev->dev.platform_data,这是先前我们在设备注册的时候赋值的,它是原始数据的来源,后面的很多操作很它有关;
3, 一堆的参数检查,不讨论;
4, 构建framebuffer信息,对于pxa310的架构,有三个层,所以不同的层拥有不同的framebuffer信息,但是这些信息的初始都来自于先前的platform_data,可以把这一步的动作理解为我前面总结的是为了提供对上接口,而做的准备,这里会对我们在应用层看到的var 信息和fix信息做填充;需要单独说明的是这两句代码:fbi->fb.fbops = &pxafb_ops;……; INIT_WORK(&fbi->task, pxafb_task);前一个设置函数操作集,后面一句话初始化一个work pxafb_task后面会用到;
5, 通过pxafb_map_video_memory函数分配framebuffer空间,空间的长度为xres*yres*bpp/2(如果是android系统还会再乘以2,因为是双缓冲)+PAGESIZE,这个pagesize,通常是用来存放DMA描述符的,如果要调整上层看到的framebuffer的大小,这里需要修改,当然返回给上层的smem_len也必须要修改;
6, 设置中断处理函数,通常也就用在调试的时候;
7, 通过pxafb_set_par设置DMA描述符的地址,以及描述符里面的内容,如果有某个相关的寄存器有改变,那么就通过pxafb_schedule_work(fbi, C_REENABLE)来启动在第4步里面设置的work也就是pxafb_task来运行,注意不是立即运行;
8, 通过register_framebuffer(&fbi->fb)来注册这个framebuffer信息,本质上就是让registered_fb【index】指向先前我们分配的这个framebuffer信息,以便在后面使用的时候可以取出来;
9, 调用set_ctrlr_state(fbi, C_ENABLE)再通过pxafb_enable_controller 来enable controller,这里会把先前在第7步里面赋好的值放入具体的寄存器;
好现在先看看第4步里面的pxafb_ops:
static struct fb_ops pxafb_ops = {
.owner = THIS_MODULE,
.fb_check_var = pxafb_check_var,
.fb_set_par = pxafb_set_par,
.fb_setcolreg = pxafb_setcolreg,
.fb_fillrect = cfb_fillrect,
.fb_copyarea = cfb_copyarea,
.fb_imageblit = cfb_imageblit,
.fb_blank = pxafb_blank,
.fb_mmap = pxafb_mmap,
.fb_pan_display = pxafb_pan_display,
#ifdef CONFIG_FB_PXA_MINILCD
.fb_ioctl = pxafb_minilcd_ioctl,
#else
.fb_ioctl = pxafb_ioctl,
#endif
};
再看看这个pxafb_task:
static void pxafb_task(struct work_struct *work)
{
struct pxafb_info *fbi =
container_of(work, struct pxafb_info, task);
u_int state = xchg(&fbi->task_state, -1);
set_ctrlr_state(fbi, state);
}
这里面先取得fbi->task_state放到state里面,估计是为了实现互斥的访问,所以搞了一个xchg操作;然后通过set_ctrlr_state来实现真正的LCD controller的寄存器的操作;
总结下来就是说,参数的原始设置在static struct pxafb_mode_info lin2008_modes里面,必须要正确填写,估计不同的屏这个地方都需要修改,然后就是controller的enable,这个是需要到set_ctrlr_state的时候,才会被传递到寄存器的,那么有就是说,在这之前,必须要保证相关的值已经被正确的赋给了framebuffer info,需要注意的就是那个描述符的内容的填写,结构如下:
struct pxafb_dma_descriptor {
unsigned int fdadr;
unsigned int fsadr;
unsigned int fidr;
unsigned int ldcmd;
};
fdadr:下一个描述符的地址,注意这个是DMA的地址,在我们的平台上就是物理地址,通常可以让它指向这个结构本身的地址,这样就可以形成一个环,从而实现一直刷新;
fsadr:这个是数据的地址,也就是说应用层在这个地方开始写的数据,对于android平台,这个地址有可能是framebuffer的起始地址,也有可能是起始地址+xres*yres*bpp/2;
fidr,这个值没怎么使用到,我们都填的是0;
ldcm,也就是这个数据的长度,如果要刷一屏的话,对于800X480的分辨率16位色,那么就是800*480*2;
5. 用户使用流程
5.1 上层软件使用流程
代码如下:
static int fb_post(struct framebuffer_device_t* dev, buffer_handle_t buffer)
{
if (private_handle_t::validate(buffer) < 0)
return -EINVAL;
fb_context_t* ctx = (fb_context_t*)dev;
private_handle_t const* hnd = reinterpret_cast
private_module_t* m = reinterpret_cast
dev->common.module);
if (m->currentBuffer) {
m->base.unlock(&m->base, m->currentBuffer);
m->currentBuffer = 0;
}
if (hnd->flags & private_handle_t::PRIV_FLAGS_FRAMEBUFFER) {
m->base.lock(&m->base, buffer,
private_module_t::PRIV_USAGE_LOCKED_FOR_POST,
0, 0, m->info.xres, m->info.yres, NULL);
const size_t offset = hnd->base - m->framebuffer->base;
m->info.activate = FB_ACTIVATE_VBL;
m->info.yoffset = offset / m->finfo.line_length;
if (ioctl(m->framebuffer->fd, FBIOPUT_VSCREENINFO, &m->info) == -1) {
LOGE("FBIOPUT_VSCREENINFO failed");
m->base.unlock(&m->base, buffer);
return -errno;
}
m->currentBuffer = buffer;
} else {
// If we can't do the page_flip, just copy the buffer to the front
// FIXME: use copybit HAL instead of memcpy
void* fb_vaddr;
void* buffer_vaddr;
m->base.lock(&m->base, m->framebuffer,
GRALLOC_USAGE_SW_WRITE_RARELY,
0, 0, m->info.xres, m->info.yres,
&fb_vaddr);
m->base.lock(&m->base, buffer,
GRALLOC_USAGE_SW_READ_RARELY,
0, 0, m->info.xres, m->info.yres,
&buffer_vaddr);
memcpy(fb_vaddr, buffer_vaddr, m->finfo.line_length * m->info.yres);
m->base.unlock(&m->base, buffer);
m->base.unlock(&m->base, m->framebuffer);
}
return 0;
}
这个函数会在surfaceflinger层的postframebuffer到opengl层到nativewindow层的queueBuffer里面会调用fb->post(fb, handle)触发这个函数的执行;
从驱动的角度来说,它最关心的还是FBIOPUT_VSCREENINFO这个ioctl,下面看看驱动对应的处理逻辑;
5.2 驱动处理逻辑
如下图所示:
上层的ioctl会触发这个fb_ioctl的执行,在里面首先从registerd_fb[fbidx]取出先前我们注册的framebuffer信息(对于fb0来说就是在probe函数里面通过register_framebuffer来实现注册的);然后通过fb_set_var把这个信息传递下去,var代表了上层传下来的数据信息,这个函数也是相当的长,不过我还是贴出来了,去掉了一写错误处理:
int
fb_set_var(struct fb_info *info, struct fb_var_screeninfo *var)
{
int flags = info->flags;
int ret = 0;
……………………….
…………………….....
if ((var->activate & FB_ACTIVATE_FORCE) ||
memcmp(&info->var, var, sizeof(struct fb_var_screeninfo))) {
u32 activate = var->activate;
if (!info->fbops->fb_check_var) {
*var = info->var;
goto done;
}
ret = info->fbops->fb_check_var(var, info);
if (ret)
goto done;
if ((var->activate & FB_ACTIVATE_MASK) == FB_ACTIVATE_NOW) {
struct fb_videomode mode;
if (info->fbops->fb_get_caps) {
ret = fb_check_caps(info, var, activate);
if (ret)
goto done;
}
info->var = *var;
if (info->fbops->fb_set_par)
info->fbops->fb_set_par(info);
fb_pan_display(info, &info->var);
fb_set_cmap(&info->cmap, info);
fb_var_to_videomode(&mode, &info->var);
if (info->modelist.prev && info->modelist.next &&
!list_empty(&info->modelist))
ret = fb_add_videomode(&mode, &info->modelist);
if (!ret && (flags & FBINFO_MISC_USEREVENT)) {
struct fb_event event;
int evnt = (activate & FB_ACTIVATE_ALL) ?
FB_EVENT_MODE_CHANGE_ALL :
FB_EVENT_MODE_CHANGE;
info->flags &= ~FBINFO_MISC_USEREVENT;
event.info = info;
event.data = &mode;
fb_notifier_call_chain(evnt, &event);
}
}
}
done:
return ret;
}
A) 先通过pxafb_check_var检查传入的参数的有效性;
B) 调用info->fbops->fb_set_par(info)也就是pxafb_set_par计算新的DMA传输相关的参数,通过调用pxafb_activate_var来把新的包括行同步,帧同步,前廊后廊,上廊下廊等也包括DMA描述符内容的更新保存在fbi->reg*成员里面,然后触发pxafb_task的调度,注意这时候还没有被执行;
C) fb_pan_display-> info->fbops->fb_pan_display(var, info),最后就调用到了pxafb_pan_display,这个函数最主要的作用就是pxafb_schedule_work(fbi, C_CHANGE_DMA_BASE);注意这个状态因为是后设置的,也就是说前面在fb_set_par的时候设置的状态将会被这个状态覆盖,那么最后在执行pxafb_task的时候取得的状态将会是这个C_CHANGE_DMA_BASE的状态;
等这个pxafb_task被调度执行的时候,将会走到C_CHANGE_DMA_BASE的分支,如下:
static void pxafb_task(struct work_struct *work)
{
struct pxafb_info *fbi =
container_of(work, struct pxafb_info, task);
u_int state = xchg(&fbi->task_state, -1);
set_ctrlr_state(fbi, state);
}
void set_ctrlr_state(struct pxafb_info *fbi, u_int state)
{
……
case C_CHANGE_DMA_BASE:
fbi->dmadesc_fbhigh_cpu->fsadr = fbi->screen_dma +
(fbi->fb.var.xres*fbi->fb.var.yoffset*\
fbi->fb.var.bits_per_pixel/8);
break;
……
}
可以看到基本上就是修改fsadr的地址,它的值,我们前面有看到就是实际的数据的起始地址,通过这样的方式,那么下一次DMA数据传递的时候,数据将会从这个地址取得,从而实现了android平台下的双缓冲机制;
6. 总结
A)对于分辨率的修改,也就是说,如果屏本身支持这个新的分辨率,那么只需要对 static struct pxafb_mode_info lin2008_modes这个结构体修改就可以了,它这里面的xres,yres是后面许多数据结构里面的值的根源,如果需要在只支持800X480的屏上模拟320X480的话,还需要修改前廊后廊以扩展到相应的pixelclock就应该可以了;
B)真正对于controller的设置都是在set_ctrlr_state里面做的,而这些值的设置都是在pxafb_activate_var里面做的,也就是说,如果要欺骗controller也就是说为了实现实际的值和上层看到的值的差异性,我想可以在这里做hack;
C)对于屏不亮的问题,我想首先要考虑的是电压问题,如果电压正常,那么就考虑另外四根线,CLOCK,hsync,vsync,以及de线,如果这个都正常,再看看数据线是否正常,这些都可以通过示波器看出来,当然正确的对屏本身的初始化是前提,而屏无法提供反馈信号,这也是调试LCD驱动最痛苦的地方,因为你无法判断初始化是否已经正确;
D)可以通过打开中断来判断,每帧数据的发送情况,当然,里面的打印信息不是完全有效,但是可以提供一定的参考,而且如果它在一直打印,至少可以说明,数据在不断的刷新;
E)显示结果和应该的结果如果有差异,可以通过抓出framebuffer的内容转化层图片来比较;当然也可以通过cat一幅图片到framebuffer来判断驱动是否已经工作,如果开始的时候cat工作,过了一定时间cat又不工作,说明在这个过程中某些硬件的配置已经被改变了,这个需要看看是不是别的模块和你的设置有冲突;
7. 未讨论
1, controller寄存器的详细设置没有分析,这个我觉得需要对照spec详细看;
2, lcd屏本身的初始化没有仔细看,这个我觉得和屏本身有关;
3, 其它;
备注:
作者:wylhistory
联系方式: wylhistory@gmail.com