通过framebuffer,应用程序用mmap把显存映射到应用程序虚拟地址空间,将要显示的数据写入这个内存空间就可以在屏幕上显示出来。
驱动程序分配系统内存作为显存;实现file_operations结构中的接口,为应用程序服务;实现fb_ops结构中的接口,控制和操作LCD控制器;
驱动程序将显存的起始地址和长度传给LCD控制器的寄存器,LCD控制器会自动的将显存中的数据显示在LCD屏上。
LCD的接口类型大致有:SPI/I2C/MCU/CPU/RGB/MDDI/MIPI,SPI/I2C用于低速黑白屏,MCU/CPU/RGB为并口,在智能机之前的功能机上用的多,手机进入到大屏时代后,并口的传输速度跟不上,特别是面临高清播放的应用,能力不足,所以出现了MDDI和MIPI,MDDI为高通推出,MIPI为多家重量级厂商联合成立的组织,其推出了一系列移动标准,其中就包括MIPIDSI。
Mipi推出的时间不长,但推广速度很快,包括iphone4/MeizuM9都采用mipi接口(可能现在除了高通的手机,大部分都是)
Mipi接口有物理规范,因此我们看到的支持DSI的开发板和LCD都是mipi规范的排线。但是目前mipi接口的LCD基本上买不到,网上有一些自己做开发的,也是用的iphone的屏
开发板部分,PandaBoard在CPU这块就没有将mipi的信号引出来,没法用,samsungs5pc100开发板没有引出接口,samsungs5pv210开发板一般有mipi接口驱动部分,目前只能有Omap的代码可以参考
Mipi接口支持2中mode:videomode和commandmode
Video mode和rgb接口是类似的,framebufferdriver都用systemmemory,pixel数据存放在buffer中,mipihost按照指定的时序将数据通过dbi总线发送给lcd。因为mipi的信号线和数据线是复用的,时序的同步实际上也有datapackage完成,而不像并口那样有信号线上的电平完成。
在Video mode下,整个架构和rgb等并行接口并无太大的差别。而command mode需要LCDmodule将LCDRAM集成在其中,host以command的方式将显示数据发送给LCD,LCD从自身的LCDRAM中获取数据刷新。
相较而言,commandmode比较省电,当屏幕不更新的时候,mipi总线可以idle,LCD凭借自身的RAM进行刷新动作,而videomode需要不停的传输数据到LCD,即使画面无变化,但command mode可能在大屏高分辨率高清播放的场景下能力不足
1、u-boot中
我们所加的屏的驱动文件中通过init初始化函数对屏的LCD寄存器进行初始化。Sc8810_fb.c中的probe调用find_adapt_from_readid,这个函数中就是遍历我们所加屏列表文件的lcd_panel数组,并且用所加屏文件中的lcd_readid()函数读id,如果读成功则返回ID值,将这个保存到kernel中。
下面从U-boot在上电后被SPL从NAND中拷贝到SDRAM,然后执行board_init_f跳转到board_init_r开始。
u_boot/arch/arm/lib/Board.c中的board_init_r
void board_init_r (gd_t *id, ulong dest_addr)
{
//这里面的两个重要的函数
stdio_init(); /* get the devices list going.
对有关设备进行初始化,例如LCD,video, keyboard等*/
do_cboot(NULL,0,1,NULL);//进入启动函数
//…
}
依次走这两个函数,先看stdio_init()中的代码
在stdio_init()中我们对设备进行初始化,这个函数在u-boot/common/Stdio.c中
intstdio_init (void)
{
//…
#ifdefCONFIG_LCD
drv_lcd_init();//LCD的初始化
#endif
#ifdefined(CONFIG_VIDEO) || defined(CONFIG_CFB_CONSOLE)
drv_video_init();
#endif
#ifdefCONFIG_KEYBOARD
drv_keyboard_init();
#endif
//等等
return(0);
}
在drv_lcd_init()中这个函数中主要是初始化LCD,这个函数的实现在u-boot/common/Lcd.c中
intdrv_lcd_init (void)
{
structstdio_dev lcddev;
intrc = 0;
lcd_base= (void *)(gd->fb_base);
lcd_line_length= (panel_info.vl_col * NBITS (panel_info.vl_bpix)) / 8;
lcd_init(lcd_base); /* LCD初始化*/
/*Device initialization */
memset(&lcddev, 0, sizeof (lcddev));
strcpy(lcddev.name, “lcd”);
lcddev.ext = 0; /* No extensions */
lcddev.flags= DEV_FLAGS_OUTPUT; /* Output only */
lcddev.putc = lcd_putc; /* ‘putc’ function */
lcddev.puts = lcd_puts; /* ‘puts’ function */
rc =stdio_register (&lcddev);
return(rc == 0) ? 1 : rc;
}
lcd_init(lcd_base)函数的实现在u-boot/common/Lcd.c中
staticint lcd_init (void *lcdbase)
{
lcd_ctrl_init(lcdbase);
lcd_is_enabled= 1;
lcd_clear(NULL, 1, 1, NULL); /* dummy args */
lcd_enable();
console_col= 0;
#ifdefCONFIG_LCD_INFO_BELOW_LOGO
console_row= 7 + BMP_LOGO_HEIGHT / VIDEO_FONT_HEIGHT;
#else
console_row= 1; /* leave 1 blank line below logo */
#endif
return0;
}
lcd_ctrl_init(lcd_base)函数的实现在u-boot/drivers/video/Sc8810_fb.c中
voidlcd_ctrl_init(void *lcdbase)
{
sc8810fb_probe(lcdbase);
}
sc8810fb_probe(lcdbase)这个函数主要是我们写的驱动入口函数,实现在u-boot/drivers/video/Sc8810_fb.c中
staticint sc8810fb_probe(void * lcdbase)
{
//…
//initadc for lcd adc compare
ADC_Init();
lcd_adapt= find_adapt_from_readid(fb);
//如果find_adapt_from_readid()没匹配到,则返回-1,执行下面这段,默认匹配panel列表中的第
一个panel
if(lcd_adapt== -1) {
lcd_adapt= 0;
}
ret =mount_panel(fb, lcd_panel[lcd_adapt].panel);
if(ret) {
printk(“unsupportedpanel!!”);
return-EFAULT;
}
//…
return0;
}
其中匹配函数find_adapt_from_readid()的实现在kernel/drivers/video/Sc8810_fb.c中
staticint find_adapt_from_readid(struct sc8810fb_info *fb)
{
inti;
uint32_tid;
//遍历整个lcd_panel数组
for(i= 0;i<(sizeof(lcd_panel))/(sizeof(lcd_panel[0]));i++) {
//first,try mount
mount_panel(fb,lcd_panel[i].panel);
//hwinit to every panel
hw_init(fb);
//readid
//我们所加屏的代码中的读id函数,如果读到
if(fb->panel->ops->lcd_readid){
id= fb->panel->ops->lcd_readid(fb->panel);
//没有读到返回的是0,执行else,默认走lcd_panel数组中的第一个panel
}else {
id= lcd_readid_default(fb->panel);
}
//ifthe id is right?
if(id== lcd_panel[i].lcd_id) {
//将这个id保存,这个保存的id在后面kernel启动时会使用到,后面再讲
save_lcd_id_to_kernel(id);
returni;
}
}
return-1;
}
通过find_adapt_from_readid(),匹配到相应的panel,这就会匹配到相应panel的结构体,也就调用到了我们所加的相应的驱动代码。
到这里lcd在stdio_init()中的代码就大致走完了.下面解释do_cboot()中的相应的代码.
do_cboot()代码的实现在u-boot/property/Cmd_cboot.c中
intdo_cboot(cmd_tbl_t *cmdtp, int flag, int argc, char *const argv[])
{
//…
#ifdefCONFIG_AUTOBOOT
normal_mode();//正常开机
#endif
boot_pwr_check();
#ifndefCONFIG_SC8810
CHG_ShutDown();
if(charger_connected()){
mdelay(10);
CHG_TurnOn();
}else{
if(is_bat_low()){
printf(“shutdown again for low battery\n”);
power_down_devices();
while(1)
;
}
}
#else
CHG_Init();
if(is_bat_low()){
printf(“shutdown again for low battery\n”);
power_down_devices();
while(1)
;
}#endif
boot_pwr_check();
board_keypad_init();//初始化键盘
boot_pwr_check();
unsignedcheck_reboot_mode(void);
unsignedrst_mode= check_reboot_mode();
if(rst_mode== RECOVERY_MODE){ //
DBG(“func:%s line: %d\n”, func, LINE);
recovery_mode();
}
elseif(rst_mode == FASTBOOT_MODE){
DBG(“func:%s line: %d\n”, func, LINE);
fastboot_mode();
}elseif(rst_mode == NORMAL_MODE){
normal_mode();
}else if (rst_mode == ALARM_MODE) {
intflag = alarm_flag_check();
if(flag == 1)
alarm_mode();
elseif (flag == 2)
normal_mode();
else{
power_down_devices();
while(1)
;
}
}else if (rst_mode == SLEEP_MODE) {
sleep_mode();
}
#ifdefCONFIG_SC8810
// normal_mode();
#endif
DBG(“func:%s line: %d\n”, func, LINE);
intrecovery_init(void);
intret =0;
ret= recovery_init();
if(ret== 1){//检查是否是recovery模式
DBG(“func:%s line: %d\n”, func, LINE);
recovery_mode_without_update();
}elseif(ret == 2){
recovery_mode();
}
//findthe power up trigger
if(boot_pwr_check()>= get_pwr_key_cnt()){//如果按power键大于12次,则认为是长按
DBG("%s:power button press\n", FUNCTION);
//goon to check other keys
mdelay(50);
for(i=0;i<10;i++){
key_code= board_key_scan();//获取另外一个按键
if(key_code!= KEY_RESERVED){
key_mode= check_key_boot(key_code);//查找对应的按键码对应的开机模式
if(key_mode!= 0)
break;
}
}
switch(key_mode){
caseBOOT_FASTBOOT:
fastboot_mode(); //fastboot模式
break;
caseBOOT_RECOVERY:
recovery_mode(); //recovery模式
break;
caseBOOT_UPDATE:
update_mode();
break;
caseBOOT_CALIBRATE:
engtest_mode();
return;//back to normal boot
break;
caseBOOT_DLOADER:
dloader_mode();
break;
default:
break;
}
}
//…
return1;
}
其中主要的是正常开机的代码normal_mode(),函数的实现在u-boot/property/Normal_mode.c中
voidnormal_mode(void)
{
#ifdefCONFIG_SC8810
//MMU_Init(CONFIG_MMU_TABLE_ADDR);
vibrator_hw_init();//马达初始化
#endif
set_vibrator(1);//马达震动一下
#ifBOOT_NATIVE_LINUX
vlx_nand_boot(BOOT_PART,CONFIG_BOOTARGS, BACKLIGHT_ON);
#else
vlx_nand_boot(BOOT_PART,NULL, BACKLIGHT_ON);
#endif
}
正常启动的函数中的主要是调用vlx_nand_boot,
//ifnot to boot native linux, cmdline=NULL, kerne_pname=boot,backlight_set=on.
voidvlx_nand_boot(char * kernel_pname, char * cmdline, int backlight_set)
{
//…
//下面这一段是执行刚开机时显示的logo
#ifdefCONFIG_SPLASH_SCREEN
#defineSPLASH_PART “boot_logo”
//…
//下面这一段是将u-boot中lcd的readid中保存的id供后面kernel使用,fb0_id来表示
{
//addlcd id
externuint32_t load_lcd_id_to_kernel();
uint32_tlcd_id = load_lcd_id_to_kernel();
str_len= strlen(buf);
sprintf(&buf[str_len]," video=sprdfb:fb0_id=0x%x,fb1_id=0x%x", lcd_id, 0);
str_len= strlen(buf);
}
//…
creat_atags(VLX_TAG_ADDR, buf, NULL,0);//这个函数是将buf处理一下并且放到地址为VLX_TAG_ADDR处
//…
start_linux();//这个函数中调用theKernel(0,machine_type, VLX_TAG_ADDR);
跳到这 个内核地址
}
2.kernel
kernel层中从Sprdfb_main.c中开始看,从module_init(sprdfb_init);到sprdfb_init()函数中
staticint __init sprdfb_init(void)
{
char*option = NULL;
if(fb_get_options(“sprdfb”, &option))
return-ENODEV;
sprdfb_setup(option);
lcdc_hardware_init();
returnplatform_driver_register(&sprdfb_driver);
}
其中这个函数中的有一个函数sprdfb_setup(option),也在当前文件中定义的,这里面就找到u-boot中读的id了。
staticint __init sprdfb_setup(char *options)
{
//…
elseif (get_opt_int(this_opt, “fb0_id”,&panel_id[0]))//这里就通过fb0_id找到uboot中保存的id
//…
}
这个函数中的操作的数据最终在回调函数probe中dev->device_id= panel_id[pdev->id];就能找到这个里面的pdev的定义:
staticstruct platform_device sprd_fb_device = {
.name =“sprdfb”,
.id =0,
.dev.platform_data= &lcd_data,
};
这个device是通过platform_driver_register(&sprdfb_driver)进行平台驱动注册,匹配平台设备得到的id=0,所以找的是panel_id[0],就得到了dev->device_id=panel_id[0]。
sprdfb_setup(option);结束后,platform_driver_register(&sprdfb_driver)进行平台设备的注册,这个结构体中有个回调函数probe,这里面是我们些的驱动代码的入口。
staticint sprdfb_probe(struct platform_device *pdev)
{
//…
dev->device_id= panel_id[pdev->id];
ret =lcdc_early_init(platform_data, dev);
//…
}
传参数dev到lcdc_early_init(platform_data,dev)中,这个函数的实现在kernel\drivers\video\sc8810\Lcdc.c中。
首先通过find_adapt_from_uboot()查找panel,如果找到返回相应的索引号,否则返回-1,如果返回-1,则在lcdc_early_init()调用find_adapt_from_readid()查找整个panel。
intlcdc_early_init(struct sprd_lcd_platform_data* platform_data,structsprdfb_device *dev)
{
//…
lcd_adapt= find_adapt_from_uboot(dev->device_id, platform_data);
if(lcd_adapt == -1) {
dev->need_reinit= 1;
lcd_adapt= find_adapt_from_readid(dev,platform_data);
}
//…
}
进入到find_adapt_fromuboot中的第一个参数就是Uboot中保存的lcd_id,这样就走到这个函数中拿Uboot中保存的lcd_id比较。
platform_driver_register(&sprdfb_driver)进行平台设备驱动注册,这个里面有platform的匹配。首先在开机的时候kernel初始化会做一些板极的初始化工作,告诉内核驱动的存在,内核根据sprdfb_driver.name找到device, 然后把device的信息通过platform_device *pdev这个参数传递给driver下挂着的各个功能函数,从而使驱动完成使命。
这里用的sprdfb_driver的定义如下:
staticstruct platform_driver sprdfb_driver = {
.probe= sprdfb_probe,
#ifndefCONFIG_HAS_EARLYSUSPEND
.suspend= sprdfb_suspend,
.resume= sprdfb_resume,
#endif
.driver= {
.name= “sprdfb”,
.owner= THIS_MODULE,
},
};
我们看到driver中的name字段是sprdfb,自然我们要匹配这个的平台设备structplatform_device结构中的name字段要和structplatform_driver匹配
staticvoid __init openphone_init(void){
…
platform_add_devices(devices,ARRAY_SIZE(devices));
…
}
devices是一个定义的全局结构体。
kernel/arch/arm/mach-sc8810/Common.c中
externstruct sprd_lcd_platform_data lcd_data;
staticstruct platform_device sprd_fb_device = {
.name =“sprdfb”,
.id =0,
.dev.platform_data= &lcd_data,
};
平台设备也有了,平台设备注册进了内核之后,那自然会调用驱动中的probe函数。
每次注册一个platform driver时,内核会对注册在platform中的没有注册驱动的设备轮询,如果name匹配的话则传struct platform_driver的sprdfb_driver这个结构体,其中有个probe函数,被回调,这个函数中主要是填充结构体struct fb_info并使用register_framebuffer()函数在系统中进行注册,并且调用lcdc_early_init()初始化。在linux中,帧缓冲设备采用structfb_info结构来描述,当然首先要为这个结构申请内存空间,为structfb_info申请内存空间的函数就是这里的 framebuffer_alloc,这两个操作之前有个如下的申请空间的操作。
structfb_info* fb;
fb =framebuffer_alloc(sizeof(struct sprdfb_device), &pdev->dev);