展讯8810中LCD 在uboot和Kernel中的基本流程

    技术2024-06-30  77

    LCD显示的基本原理

    通过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可能在大屏高分辨率高清播放的场景下能力不足

    展讯SC8810LCD点亮的基本架构

    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);

    Processed: 0.036, SQL: 9