前言
在MTK平台点亮一块屏不是难事,因为MTK的LCM框架很完善,我们的屏驱动需要的工作不是很多。不过要想成为一个优秀的工程师,不能仅仅满足于此,至少要了解整个框架流程。
MTK平台的lcm流程分为Lk和kernel两个阶段,这篇文章我们先来分析lk阶段的流程。
正文
lk阶段起来后,display相关的初始化主要在platform_init()函数中完成。
vendor\mediatek\proprietary\bootable\bootloader\lk\platform\mt8167\platform.c
void disp_thread_routine()
{
#ifdef LK_PROFILING
unsigned int time_disp_init;
time_disp_init = get_timer(0);
#endif
/* initialize the frame buffet information */
g_fb_size = mt_disp_get_vram_size(); --------------------- (1)
/* framebuffer的起始地址 */
g_fb_base = mblock_reserve(&g_boot_arg->mblock_info, g_fb_size, 0x10000, 0x100000000, RANKMAX);
dprintf(CRITICAL, "FB base = 0x%x, FB size = %d\n", g_fb_base, g_fb_size);
#ifdef VDEC_LDVT_RESERVED_MEMORY_SIZE
g_vdec_base = mblock_reserve(&g_boot_arg->mblock_info, VDEC_LDVT_RESERVED_MEMORY_SIZE, 0x200000, 0x100000000, RANKMAX);
g_vdec_base = ALIGN_TO(g_vdec_base,0x200000);
dprintf(CRITICAL, "VDEC base = 0x%x, VDEC size = %d\n", g_vdec_base, 0x10000000);
#endif
mt_disp_init((void *)g_fb_base); -------------------- (2)
/* show black picture fisrtly in case of backlight is on before nothing is drawed*/
mt_disp_fill_rect(0, 0, CFG_DISPLAY_WIDTH, CFG_DISPLAY_HEIGHT, 0x0); -------------------- (3)
mt_disp_update(0, 0, CFG_DISPLAY_WIDTH, CFG_DISPLAY_HEIGHT); //将framebuffer的数据更新到LCM
#ifdef LK_PROFILING
dprintf(INFO, "[PROFILE] ------- disp init takes %d ms -------- \n", (int)get_timer(time_disp_init));
#endif
drv_video_init();
EVENT
event_signal(&disp_ready_event, true);
}
void platform_init(void)
{
...
THREAD CREATE
thread_resume(thread_create("disp", &disp_thread_routine, NULL, DEFAULT_PRIORITY, DEFAULT_STACK_SIZE));
...
}
(1)
首先是获取framebuffer的大小:
vendor\mediatek\proprietary\bootable\bootloader\lk\platform\mt8167\primary_display.c
UINT32 DISP_GetFBRamSize(void)
{
return ALIGN_TO(DISP_GetScreenWidth(), MTK_FB_ALIGNMENT) * DISP_GetScreenHeight() * ((DISP_GetScreenBpp() + 7) >> 3) * DISP_GetPages();
}
unsigned int DISP_GetVRamSize(void)
{
static UINT32 vramSize = 0;
if (0 == vramSize) {
vramSize = DISP_GetFBRamSize();
vramSize += DAL_GetLayerSize();
#if 1
if (g_is_64bit_kernel) {
vramSize = ALIGN_TO(vramSize, 0x400000); // both start addr and size needs 2MB align, so size must 4MB align
} else {
vramSize = ALIGN_TO(vramSize, 0x100000); // both start addr and size needs 2MB align, so size must 4MB align
}
#else
vramSize = ALIGN_TO(vramSize, 0x10000); // just align to 64KB is OK
#endif
DISPMSG("^^ DISP_GetVRamSize: %u bytes\n", vramSize);
}
return vramSize;
}
先调用DISP_GetFBRamSize()去计算整个buffer的大小,这就涉及到screen的width、height和bpp的值了。我们先看一下怎么获取width值:
vendor\mediatek\proprietary\bootable\bootloader\lk\platform\mt8167\primary_display.c
int primary_display_get_width(void)
{
if (pgc->plcm == NULL) {
pgc->plcm = disp_lcm_probe(NULL, LCM_INTERFACE_NOTDEFINED);
DISPMSG("lcm handle is null, after probe:0x%08x\n",pgc->plcm);
if (pgc->plcm == NULL)
return 0;
}
if (pgc->plcm->params) {
return pgc->plcm->params->width;
} else {
DISPERR("lcm_params is null!\n");
return 0;
}
}
UINT32 DISP_GetScreenWidth(void)
{
return primary_display_get_width();
}
由于第一次注册,所以pgc->plcm是NULL,就先调用disp_lcm_probe()注册我们的lcm模块:
vendor\mediatek\proprietary\bootable\bootloader\lk\platform\mt8167\disp_lcm.c
int _lcm_count(void)
{
return lcm_count;
}
disp_lcm_handle *disp_lcm_probe(char *plcm_name, LCM_INTERFACE_ID lcm_id)
{
DISPFUNC();
int ret = 0;
bool isLCMFound = false;
bool isLCMConnected = false;
bool isDriverIDFound = false;
LCM_DRIVER *lcm_drv = NULL;
LCM_PARAMS *lcm_param = NULL;
disp_lcm_handle *plcm = NULL;
if (_lcm_count() == 0) {
DISPERR("no lcm driver defined in linux kernel driver\n");
return NULL;
} else if (_lcm_count() == 1) { ------------------------ (1.1)
lcm_drv = lcm_driver_list[0];
isLCMFound = true;
} else {
...
}
if (isLCMFound == false) {
DISPERR("FATAL ERROR!!!No LCM Driver defined\n");
return NULL;
}
plcm = &_disp_lcm_driver[0];
lcm_param = &_disp_lcm_params;
if (plcm && lcm_param) {
plcm->params = lcm_param;
plcm->drv = lcm_drv; //指向我们自定义的LCM_DRIVER
} else {
DISPERR("FATAL ERROR!!!kzalloc plcm and plcm->params failed\n");
goto FAIL;
}
#if defined(MTK_LCM_DEVICE_TREE_SUPPORT)
load_lcm_resources_from_DT(plcm->drv);
#endif
plcm->drv->get_params(plcm->params); -------------------------- (1.2)
plcm->lcm_if_id = plcm->params->lcm_if; //get_params中没有设置
/* below code is for lcm driver forward compatible */
if (plcm->params->type == LCM_TYPE_DSI && plcm->params->lcm_if == LCM_INTERFACE_NOTDEFINED)
plcm->lcm_if_id = LCM_INTERFACE_DSI0; //get_params函数中可以看到设置为了DSI类型
if (plcm->params->type == LCM_TYPE_DPI && plcm->params->lcm_if == LCM_INTERFACE_NOTDEFINED)
plcm->lcm_if_id = LCM_INTERFACE_DPI0;
if (plcm->params->type == LCM_TYPE_DBI && plcm->params->lcm_if == LCM_INTERFACE_NOTDEFINED)
plcm->lcm_if_id = LCM_INTERFACE_DBI0;
#if 1
if (LCM_TYPE_DSI == plcm->params->type) {
disp_path_handle handle = NULL;
int ret = 0;
char buffer = 0;
handle = _display_interface_path_init(plcm); //暂时不分析
#if 0
...
#endif
_display_interface_path_deinit(handle);
}
plcm->is_connected = isLCMConnected;
#endif
_dump_lcm_info(plcm);
return plcm;
FAIL:
return NULL;
}
(1.1)
首先就是获取你注册的lcm驱动的数量,正常都是只注册一个,也就是你移植的屏驱动:
vendor\mediatek\proprietary\bootable\bootloader\lk\dev\lcm\mt65xx_lcm_list.c
extern LCM_DRIVER hn116wx1_100_dsi_edp_A15_A17_lcm_drv;
LCM_DRIVER *lcm_driver_list[] = {
#if defined(HN116WX1_100_DSI_EDP_A15_A17)
&hn116wx1_100_dsi_edp_A15_A17_lcm_drv,
#endif
...
}
unsigned int lcm_count = sizeof(lcm_driver_list) / sizeof(LCM_DRIVER *);
在调试一款新的屏,MTK平台是有特定的套路的,比如这里就要我们自己的屏驱动LCM_DRIVER填充到lcm_driver_list列表中。而HN116WX1_100_DSI_EDP_A15_A17是在device/mediatek/mt8167/ProjectConfig.mk中的CUSTOM_LK_LCM和CUSTOM_UBOOT_LCM定义的。现在看一下我们实现的LCM_DRIVER hn116wx1_100_dsi_edp_A15_A17_lcm_drv。
首先我们需要在vendor/mediatek/proprietary/bootable/bootloader/lk/dev/lcm/目录新建我们驱动的目录,比如我的代码目录HN116WX1_100_DSI_EDP_A15_A17,这个要和CUSTOM_LK_LCM定义的一致,否则编译不到我们的驱动。
vendor\mediatek\proprietary\bootable\bootloader\lk\dev\lcm\HN116WX1_100_DSI_EDP_A15_A17\HN116WX1_100_DSI_EDP_A15_A17.c
LCM_DRIVER hn116wx1_100_dsi_edp_A15_A17_lcm_drv = {
.name = "hn116wx1_100_dsi_edp_A15_A17",
.set_util_funcs = lcm_set_util_funcs,
.get_params = lcm_get_params,
.init = lcm_init,
.init_power = lcm_init_power,
.suspend = lcm_suspend,
.resume = lcm_resume,
//.esd_check = IT6151_ESD_Check,
//.esd_recover = IT6151_ESD_Recover,
};
我们需要填充LCM_DRIVER结构体的成员,主要包括初始化、参数获取、睡眠和唤醒等函数。函数的具体作用我们后面再讲。
(1.2)
这一步就是调用我们屏驱动的.get_params函数,并赋值给变量_disp_lcm_params
vendor\mediatek\proprietary\bootable\bootloader\lk\dev\lcm\HN116WX1_100_DSI_EDP_A15_A17\HN116WX1_100_DSI_EDP_A15_A17.c
static void lcm_get_params(LCM_PARAMS *params)
{
memset(params, 0, sizeof(LCM_PARAMS));
params->type = LCM_TYPE_DSI;
params->width = FRAME_WIDTH;
params->height = FRAME_HEIGHT;
#if (LCM_DSI_CMD_MODE)
params->dsi.mode = CMD_MODE;
#else
params->dsi.mode = SYNC_PULSE_VDO_MODE;//SYNC_PULSE_VDO_MODE; //BURST_VDO_MODE; //SYNC_EVENT_VDO_MODE;
#endif
params->dsi.LANE_NUM = LCM_FOUR_LANE;
params->dsi.data_format.format = LCM_DSI_FORMAT_RGB888;//LCM_DSI_FORMAT_RGB888;
params->dsi.PS = LCM_PACKED_PS_24BIT_RGB888; //LCM_PACKED_PS_24BIT_RGB888;//LCM_LOOSELY_PS_18BIT_RGB666;//LCM_PACKED_PS_18BIT_RGB666;
params->dsi.vertical_sync_active = 6; //4; //6; //(12-4-4); //1;
params->dsi.vertical_backporch = 3;//8; //6; //4; //6; //10;
params->dsi.vertical_frontporch = 3;//8;//6; //4//6; //10;
params->dsi.vertical_active_line = FRAME_HEIGHT;
params->dsi.horizontal_sync_active = 32;//4; //40; //44; //(120-40-40); //1;
params->dsi.horizontal_backporch = 65;//60;//43;//44; //40; //44; //57;
params->dsi.horizontal_frontporch = 65;//60;//45;//108;//44; //40; //44; //32;
params->dsi.horizontal_active_pixel = FRAME_WIDTH;
params->dsi.PLL_CLOCK = 440;//180;//216;//240;//LCM_DSI_6589_PLL_CLOCK_NULL; //LCM_DSI_6589_PLL_CLOCK_396_5;
params->dsi.cont_clock = 1;
params->dsi.ssc_disable = 1;
params->dsi.edp_panel = 1;
}
这里的参数都非常重要,设置不对就会影响屏幕点亮,我们分别了解一下各参数的意义。
(a)LCM_TYPE_DSI
MIPI标准中有DBI、DPI和DSI三种display接口,我们平台的显示屏是DSI接口的
(b)FRAME_WIDTH和FRAME_HEIGHT
显示屏的分辨率参数,比如我们平台的是1920*1080
#define FRAME_WIDTH (1920) //(800)
#define FRAME_HEIGHT (1080) //(1280)
(c)SYNC_PULSE_VDO_MODE
首先介绍一下,DSI的两种基本的操作模式:命令模式(Command Mode)和视频模式(Video Mode)。顾名思义,命令模式主要是用来进行命令操作的,如应用处理器向显示设备(如LCD)发送相关命令,显示设备返回对应的相关数据。显然,命令模式需要双向的数据通道,即需要Data Lane支持Bidirectional模式。视频模式则是用来传输用于显示的视频或图像数据的,且视频信息只能在HS模式下进行传输。
因此,我们这里的dsi.mode设置为视频模式,而视频模式又分三种子模式:
-
Non-Busrt Mode with Sync Pluses
-
Non-Burst Mode with Sync Events
-
Burst Mode
下图是这三种模式的时序图:
Non-Busrt Mode with Sync Pluses模式主要是用于发送DPI(Display Pixel Interface)类型的数据,可以精确地匹配DPI的像素传输速率,以及时序事件的宽度(Widths of timing events),如同步脉冲。因此,Non-Busrt Mode with Sync Pluses模式的每一个Sync Start都必须都要有一个唯一的Sync End与之对应,比如VS都对应一个VSE,HSS都对应一个HSE。
Non-Busrt Mode with Sync Events模式与Non-Busrt Mode with Sync Pluses模式类似,可以认为前者是后者的简化版本。因为Non-Busrt Mode with Sync Events模式不需要精确控制时序事件的宽度,所以Non-Busrt Mode with Sync Events只有Sync Start,而没有Sync End,所以只有VS和和HSS,没有对应的VSE和HSE。
Burst Mode相比于Non-Burst Mode with Sync Events模式,可以更快完成一帧(或一行)图像像素的传输,因此可以节省更多的时间以进入LP模式,进而降低系统的功耗。但是,需要注意的是,Burst Mode需要使用的是经过时序压缩的数据格式(Time-compressed burst format),所以上图中Burst Mode的HACT部分会比Non-Burst Mode with Sync Events的短。
再回到我们自己的平台,一般情况我们的panel IC的datasheet会注明支持哪种模式:
从上面的datasheet可以看出来,我这个panel IC同时支持3种video mode,这里我们通过宏SYNC_PULSE_VDO_MODE,将video mode设置为Non-Busrt Mode with Sync Pluses。
(d)LCM_DSI_FORMAT_RGB888和LCM_PACKED_PS_24BIT_RGB888
这个主要看panel IC支持什么格式,设置对应的格式就好。
(e)“vertical_"和"horizontal_”
params->dsi.vertical_sync_active = 6; //4; //6; //(12-4-4); //1;
params->dsi.vertical_backporch = 3;//8; //6; //4; //6; //10;
params->dsi.vertical_frontporch = 3;//8;//6; //4//6; //10;
params->dsi.vertical_active_line = FRAME_HEIGHT;
params->dsi.horizontal_sync_active = 32;//4; //40; //44; //(120-40-40); //1;
params->dsi.horizontal_backporch = 65;//60;//43;//44; //40; //44; //57;
params->dsi.horizontal_frontporch = 65;//60;//45;//108;//44; //40; //44; //32;
params->dsi.horizontal_active_pixel = FRAME_WIDTH;
这几个参数非常重要,不过我们可以很容易的向panel厂家获得。
(f)dsi.PLL_CLOCK
这个频率可以根据(e)提到的前后肩参数算出来,可以参考这篇文章:
disp_lcm_probe()函数走完后,我们也就能获得screen的width值。我们再回到DISP_GetFBRamSize()函数:
UINT32 DISP_GetFBRamSize(void)
{
return ALIGN_TO(DISP_GetScreenWidth(), MTK_FB_ALIGNMENT) * DISP_GetScreenHeight() * ((DISP_GetScreenBpp() + 7) >> 3) * DISP_GetPages();
}
同理,DISP_GetScreenHeight()和获取width值一样,都是在lcm_get_params()里面设置的。DISP_GetScreenBpp()设置了一个固定的BPP值为32,DISP_GetPages()的含义不是很清楚,不过从注释来看是设置了需要内存两倍大小的framebuffer?
static UINT32 disp_fb_bpp = 32; ///ARGB8888
static UINT32 disp_fb_pages = 3; ///double buffer
UINT32 DISP_GetScreenBpp(void)
{
return disp_fb_bpp;
}
UINT32 DISP_GetPages(void)
{
return disp_fb_pages; // Double Buffers
}
(2)
vendor\mediatek\proprietary\bootable\bootloader\lk\platform\mt8167\mt_disp_drv.c
void mt_disp_init(void *lcdbase)
{
...
primary_display_init(NULL);
...
}
mt_disp_init()顾名思义就是初始化显示相关的东西
vendor\mediatek\proprietary\bootable\bootloader\lk\platform\mt8167\primary_display.c
int primary_display_init(char *lcm_name)
{
...
ret = disp_lcm_init(pgc->plcm);
...
}
disp_lcm_init()函数就是调用我们自己LCM驱动LCM_DRIVER中定义的函数
vendor\mediatek\proprietary\bootable\bootloader\lk\platform\mt8167\disp_lcm.c
int disp_lcm_init(disp_lcm_handle *plcm)
{
DISPFUNC();
LCM_DRIVER *lcm_drv = NULL;
bool isLCMConnected = false;
if (_is_lcm_inited(plcm)) {
lcm_drv = plcm->drv;
if (lcm_drv->init_power) {
lcm_drv->init_power();
}
if (lcm_drv->init) {
if (!disp_lcm_is_inited(plcm)) {
lcm_drv->init();
}
} else {
DISPERR("FATAL ERROR, lcm_drv->init is null\n");
return -1;
}
...
return 0;
} else {
DISPERR("plcm is null\n");
return -1;
}
}
再回忆一下我们的LCM驱动:
LCM_DRIVER hn116wx1_100_dsi_edp_A15_A17_lcm_drv = {
.name = "hn116wx1_100_dsi_edp_A15_A17",
.set_util_funcs = lcm_set_util_funcs,
.get_params = lcm_get_params,
.init = lcm_init,
.init_power = lcm_init_power,
.suspend = lcm_suspend,
.resume = lcm_resume,
//.esd_check = IT6151_ESD_Check,
//.esd_recover = IT6151_ESD_Recover,
};
所以lcm_drv->init_power()其实就是调用的lcm_init_power,不过我们这个函数为空,没有实际动作。然后就是lcm_drv->init()初始化LCM,一般就是给LCM下发init code:
static struct LCM_setting_table lcm_initialization_setting[] = {
{0xB0, 1, {0x01}}, //含义:命令, 参数个数, 参数
...
{0xE2, 1, {0x07}},
{0x11, 0, {} }, //DCS_Short_Write_NP(0x11) 不带参数的命令
{REGFLAG_DELAY, 120, {} }, //Delay(120),延时120微秒
{0x29, 0, {} }, //DCS_Short_Write_NP(0x29)
{REGFLAG_DELAY, 20, {} },
{REGFLAG_END_OF_TABLE, 0x00, {} } //表示结束,都需要添加
};
static void init_lcm_registers(void)
{
push_table(lcm_initialization_setting,
sizeof(lcm_initialization_setting) / sizeof(struct LCM_setting_table), 1);
}
static void lcm_init_lcm(void)
{
#ifdef BUILD_LK
cmdline_append("lcm_name=hx82790d");
init_lcm_registers();
#endif
}
上面的初始化代码是MTK平台很常见的,而struct LCM_setting_table lcm_initialization_setting就是我们需要下发给LCM的初始化参数,这个都是屏厂提供(我大概注释了一下屏参的格式含义)。
(3)
初始化完LCM后,在Lk阶段就可以显示logo了。
vendor\mediatek\proprietary\bootable\bootloader\lk\platform\mt8167\mt_logo.c
/*
* Fill rectangle region for with black or other color
*
*/
void mt_disp_fill_rect(UINT32 left, UINT32 top,
UINT32 right, UINT32 bottom,
UINT32 color)
{
dprintf(0, "[lk logo: %s %d]\n",__FUNCTION__,__LINE__);
init_fb_screen();
RECT_REGION_T rect = {left, top, right, bottom};
fill_rect_with_color(mt_get_fb_addr(), rect, color, phical_screen);
}
从注释来看也知道,这是在初始化阶段先给framebuffer填全0数据,使屏幕显示黑色。另外我们也关注一下init_fb_screen()里面的初始化动作:
void init_fb_screen()
{
dprintf(0, "[lk logo: %s %d]\n",__FUNCTION__,__LINE__);
...
if(0 == strncmp(MTK_LCM_PHYSICAL_ROTATION, "270", 3))
{
phical_screen.rotation = 270;
} else if(0 == strncmp(MTK_LCM_PHYSICAL_ROTATION, "90", 2)){
phical_screen.rotation = 90;
} else if(0 == strncmp(MTK_LCM_PHYSICAL_ROTATION, "180", 3) && (phical_screen.need180Adjust == 1)){
phical_screen.rotation = 180;
} else {
phical_screen.rotation = 0;
}
...
}
我们重点关注一下MTK_LCM_PHYSICAL_ROTATION,这是定义屏幕画面旋转角度的,比如我们平板是竖屏横用可能就需要旋转90度,可以在device/mediatek/mt8167/ProjectConfig.mk中定义MTK_LCM_PHYSICAL_ROTATION的值。
通过漫长的初始化之后,我们准备开始显示logo画面,还是在platform_init()阶段
vendor\mediatek\proprietary\bootable\bootloader\lk\platform\mt8167\mt_logo.c
/*
* Show first boot logo when phone boot up
*
*/
void mt_disp_show_boot_logo(void)
{
dprintf(0, "[lk logo: %s %d]\n",__FUNCTION__,__LINE__);
mt_logo_get_custom_if();
if(logo_cust_if->show_boot_logo)
{
logo_cust_if->show_boot_logo();
}
else
{
///show_logo(0);
init_fb_screen();
fill_animation_logo(BOOT_LOGO_INDEX, mt_get_fb_addr(), (void *)mt_get_tempfb_addr(), logo_addr, phical_screen);
mt_disp_update(0, 0, CFG_DISPLAY_WIDTH, CFG_DISPLAY_HEIGHT);
}
return;
}
init_fb_screen()再次初始化屏幕,然后就是调用fill_animation_logo()填充framebuffer,然后再调用mt_disp_update()更新到LCM上。
结语
至此,我们就暂时分析到这里,目的主要是为了分析我们自己移植的LCM驱动在display框架中的作用。