LCD模块对于现代人的生活而言,就跟柴米油盐一样,成了生活中必不可少的一部分。因此,对于驱动工程师而言,LCD模块也是必须掌握的一个模块。下面,本文将对该模块进行简单的梳理。
说起LCD驱动必然离不开framebuffer驱动框架,这是一个大工程。如果从头开始分析,很容易半途而废。本文将直接从mtk平台添加的驱动程序开始讲述,并由此向下追述。
一、移植LCD驱动
首先,在mtk平台下移植一个LCD驱动程序,以项目中使用的nt52005为例,需要做下述工作:
1、LK阶段:
1.1、添加nt52005的驱动程序及Makefile文件,路径如下:
vendor\mediatek\proprietary\bootable\bootloader\lk\dev\lcm\
1.2、在[project].mk文件中添加LCM配置信息,文件路径如下: vendor\mediatek\proprietary\bootable\bootloader\lk\project[project].mk
1.3、将LCM结构体及名字添加到LCM list文件中:
vendor\mediatek\proprietary\bootable\bootloader\lk\dev\lcm\mt65xx_lcm_list.c
2、kernel阶段:
2.1、添加nt52005的驱动程序及Makefile文件,路径如下:
[kernel]\driver\misc\mediatek\lcm\
2.2、将LCM结构体及名字添加到LCM list文件中:
[kernel]\driver\misc\mediatek\lcm\mt65xx_lcm_list.h及65xx_lcm_list.c文件中;
2.3、在项目配置文件中修改LCM_WIDTH、LCM_HEIGHT、及CUSTOM_KERNEL_LCM
这样,我们的驱动代码移植工作基本就完成了。
二、LCD驱动代码分析
下面我们从项目中使用的nt52005驱动程序入手进行代码的分析(kernel阶段),代码如下:
#ifndef BUILD_LK
#include <linux/string.h>
#endif
#include "lcm_drv.h"
#ifdef BUILD_LK
#include <platform/upmu_common.h>
#include <platform/mt_gpio.h>
#include <platform/mt_pmic.h>
#include <platform/upmu_hw.h>
#include <string.h>
#elif defined(BUILD_UBOOT)
#include <asm/arch/mt_gpio.h>
#else
#include <mt-plat/mt_gpio.h>
#include <mt-plat/upmu_common.h>
#include <mach/gpio_const.h>
#include <mach/upmu_hw.h>
#include <linux/delay.h>
#endif
// ---------------------------------------------------------------------------
// Local Constants
// ---------------------------------------------------------------------------
#define FRAME_WIDTH (800)
#define FRAME_HEIGHT (1280)
#define LCM_RST GPIO146
//#define LCM_BKL GPIO69
// ---------------------------------------------------------------------------
// Local Variables
// ---------------------------------------------------------------------------
static LCM_UTIL_FUNCS lcm_util;
#define SET_RESET_PIN(v) (lcm_util.set_reset_pin((v)))
#define UDELAY(n) (lcm_util.udelay(n))
#define MDELAY(n) (lcm_util.mdelay(n))
// ---------------------------------------------------------------------------
// Local Functions
// ---------------------------------------------------------------------------
#define dsi_set_cmdq_V2(cmd, count, ppara, force_update) (lcm_util.dsi_set_cmdq_V2(cmd, count, ppara, force_update))
#define dsi_set_cmdq(pdata, queue_size, force_update) (lcm_util.dsi_set_cmdq(pdata, queue_size, force_update))
#define wrtie_cmd(cmd) (lcm_util.dsi_write_cmd(cmd))
#define write_regs(addr, pdata, byte_nums) (lcm_util.dsi_write_regs(addr, pdata, byte_nums))
#define read_reg(cmd) (lcm_util.dsi_dcs_read_lcm_reg(cmd))
#define read_reg_v2(cmd, buffer, buffer_size) (lcm_util.dsi_dcs_read_lcm_reg_v2(cmd, buffer, buffer_size))
#define dsi_lcm_set_gpio_out(pin, out) (lcm_util.set_gpio_out(pin, out))
#define dsi_lcm_set_gpio_mode(pin, mode) (lcm_util.set_gpio_mode(pin, mode))
#define dsi_lcm_set_gpio_dir(pin, dir) (lcm_util.set_gpio_dir(pin, dir))
#define dsi_lcm_set_gpio_pull_enable(pin, en) (lcm_util.set_gpio_pull_enable(pin, en))
#define LCM_DSI_CMD_MODE (0)
static void init_lcm_registers(void)
{
unsigned int data_array[16];
#if 1
data_array[0] = 0x00011500; //software reset
dsi_set_cmdq(data_array, 1, 1);
mdelay(20);
#endif
}
// ---------------------------------------------------------------------------
// LCM Driver Implementations
// ---------------------------------------------------------------------------
static void lcm_set_util_funcs(const LCM_UTIL_FUNCS * util)
{
memcpy(&lcm_util, util, sizeof(LCM_UTIL_FUNCS));
}
static void lcm_get_params(LCM_PARAMS * params)
{
memset(params, 0, sizeof(LCM_PARAMS));
params->type = LCM_TYPE_DSI; // 可选择:LCM_TYPE_DBI, LCM_TYPE_DPI, LCM_TYPE_DSI
params->width = FRAME_WIDTH; // FRAME_WIDTH=800;
params->height = FRAME_HEIGHT; // FRAME_HEIGHT=1080
#if (LCM_DSI_CMD_MODE)
params->dsi.mode = CMD_MODE;
#else
params->dsi.mode = BURST_VDO_MODE;
#endif
// DSI
/* Command mode setting */
//1 Three lane or Four lane
params->dsi.LANE_NUM = LCM_FOUR_LANE;
//The following defined the fomat for data coming from LCD engine.
params->dsi.data_format.color_order = LCM_COLOR_ORDER_RGB;
params->dsi.data_format.trans_seq = LCM_DSI_TRANS_SEQ_MSB_FIRST;
params->dsi.data_format.padding = LCM_DSI_PADDING_ON_LSB;
params->dsi.data_format.format = LCM_DSI_FORMAT_RGB888;
params->dsi.PS = LCM_PACKED_PS_24BIT_RGB888;
params->dsi.vertical_sync_active = 1;
params->dsi.vertical_backporch = 10;
params->dsi.vertical_frontporch = 10;
params->dsi.vertical_active_line = FRAME_HEIGHT;
params->dsi.horizontal_sync_active = 1;
params->dsi.horizontal_backporch = 57;
params->dsi.horizontal_frontporch = 32;
params->dsi.horizontal_active_pixel = FRAME_WIDTH;
//params->dsi.pll_select=1; //0: MIPI_PLL; 1: LVDS_PLL
// Bit rate calculation
//1 Every lane speed
params->dsi.PLL_CLOCK = 312;
}
static void lcm_init(void)
{
pmic_set_register_value(PMIC_RG_VCN33_EN_WIFI,1);
mt_set_gpio_mode(LCM_RST, GPIO_MODE_00);
mt_set_gpio_dir(LCM_RST, GPIO_DIR_OUT);
mt_set_gpio_out(LCM_RST, GPIO_OUT_ONE);
mdelay(10);
mt_set_gpio_out(LCM_RST, GPIO_OUT_ZERO);
mdelay(10);
mt_set_gpio_out(LCM_RST, GPIO_OUT_ONE);
mdelay(20);
init_lcm_registers(); //reset,软复位
mdelay(20);
}
static void lcm_suspend(void)
{
unsigned int data_array[16];
pmic_set_register_value(PMIC_RG_VCN33_EN_WIFI,0);
data_array[0] = 0x00280500; // Display Off
dsi_set_cmdq(data_array, 1, 1);
data_array[0] = 0x00100500; // Sleep In
dsi_set_cmdq(data_array, 1, 1);
mdelay(120);
mt_set_gpio_mode(LCM_RST, GPIO_MODE_00);
mt_set_gpio_dir(LCM_RST, GPIO_DIR_OUT);
mt_set_gpio_out(LCM_RST, GPIO_OUT_ZERO);
}
static void lcm_resume(void)
{
pmic_set_register_value(PMIC_RG_VCN33_EN_WIFI,1);
lcm_init();
}
#if (LCM_DSI_CMD_MODE)
static void lcm_update(unsigned int x, unsigned int y,
unsigned int width, unsigned int height)
{
unsigned int x0 = x;
unsigned int y0 = y;
unsigned int x1 = x0 + width - 1;
unsigned int y1 = y0 + height - 1;
unsigned char x0_MSB = ((x0 >> 8) & 0xFF);
unsigned char x0_LSB = (x0 & 0xFF);
unsigned char x1_MSB = ((x1 >> 8) & 0xFF);
unsigned char x1_LSB = (x1 & 0xFF);
unsigned char y0_MSB = ((y0 >> 8) & 0xFF);
unsigned char y0_LSB = (y0 & 0xFF);
unsigned char y1_MSB = ((y1 >> 8) & 0xFF);
unsigned char y1_LSB = (y1 & 0xFF);
unsigned int data_array[16];
data_array[0] = 0x00053902;
data_array[1] =
(x1_MSB << 24) | (x0_LSB << 16) | (x0_MSB << 8) | 0x2a;
data_array[2] = (x1_LSB);
dsi_set_cmdq(data_array, 3, 1);
data_array[0] = 0x00053902;
data_array[1] =
(y1_MSB << 24) | (y0_LSB << 16) | (y0_MSB << 8) | 0x2b;
data_array[2] = (y1_LSB);
dsi_set_cmdq(data_array, 3, 1);
data_array[0] = 0x002c3909;
dsi_set_cmdq(data_array, 1, 0);
}
#endif
LCM_DRIVER nt52005_dsi_lcm_drv = {
.name = "nt52005_dsi",
.set_util_funcs = lcm_set_util_funcs,
.get_params = lcm_get_params,
.init = lcm_init,
.suspend = lcm_suspend,
.resume = lcm_resume,
#if (LCM_DSI_CMD_MODE)
.update = lcm_update,
#endif
};
以上就是整个LCD的驱动程序了。说到底,整个程序其实就是在实现一个主结构体 LCM_DRIVER nt52005_dsi_lcm_drv。这里我们需要思考一下几个问题:
1、这个驱动是怎么被调用的,为什么通过它就能实现对LCD的控制。
我们知道,在驱动移植的时候,我们需要将结构体nt52005_dsi_lcm_drv的信息添加到添加到LCM_DRIVER 数组lcm_driver_list中,该数组定义如下:
LCM_DRIVER *lcm_driver_list[] = {
#if defined(MTK_LCM_DEVICE_TREE_SUPPORT)
&lcm_common_drv,
#else
...
#endif
我们的驱动主结构体添加在else中,所以首先我们必须注释掉MTK_LCM_DEVICE_TREE_SUPPORT这个宏定义,来确保我们的驱动结构体是有效的。
接下来我们顺着这个数组入手查找结构体在哪里被调用。可以发现,在disp_lcm.c程序的disp_lcm_probe函数中,调用了这个数组,disp_lcm_probe及相关如下:
unsigned int lcm_count = sizeof(lcm_driver_list) / sizeof(LCM_DRIVER *); //在mt65xx_lcm_list.c中定义
int _lcm_count(void)
{
return lcm_count;
}
...
disp_lcm_handle *disp_lcm_probe(char *plcm_name, LCM_INTERFACE_ID lcm_id)
{
int lcmindex = 0;
bool isLCMFound = false;
bool isLCMInited = false;
LCM_DRIVER *lcm_drv = NULL;
LCM_PARAMS *lcm_param = NULL;
disp_lcm_handle *plcm = NULL;
DISPPRINT("%s\n", __func__);
DISPCHECK("plcm_name=%s\n", plcm_name);
if (_lcm_count() == 0) {
DISPERR("no lcm driver defined in linux kernel driver\n");
return NULL;
} else if (_lcm_count() == 1) {
if (plcm_name == NULL) {
lcm_drv = lcm_driver_list[0];
isLCMFound = true;
isLCMInited = false;
} else {
lcm_drv = lcm_driver_list[0];
#if defined(MTK_LCM_DEVICE_TREE_SUPPORT)
lcm_drv->name = lcm_name_list[0];
#endif
if (strcmp(lcm_drv->name, plcm_name)) {
DISPERR
("FATAL ERROR!!!LCM Driver defined in kernel is different with LK\n");
return NULL;
}
isLCMInited = true;
isLCMFound = true;
}
lcmindex = 0;
} else {
if (plcm_name == NULL) {
/* TODO: we need to detect all the lcm driver */
} else {
int i = 0;
for (i = 0; i < _lcm_count(); i++) {
lcm_drv = lcm_driver_list[i];
#if defined(MTK_LCM_DEVICE_TREE_SUPPORT)
lcm_drv->name = lcm_name_list[i];
#endif
if (!strcmp(lcm_drv->name, plcm_name)) {
isLCMFound = true;
isLCMInited = true;
lcmindex = i;
break;
}
}
DISPERR("FATAL ERROR: can't found lcm driver:%s in linux kernel driver\n",
plcm_name);
}
/* TODO: */
}
if (isLCMFound == false) {
DISPERR("FATAL ERROR!!!No LCM Driver defined\n");
return NULL;
}
plcm = kzalloc(sizeof(uint8_t *) * sizeof(disp_lcm_handle), GFP_KERNEL);
lcm_param = kzalloc(sizeof(uint8_t *) * sizeof(LCM_PARAMS), GFP_KERNEL);
if (plcm && lcm_param) {
plcm->params = lcm_param;
plcm->drv = lcm_drv;
plcm->is_inited = isLCMInited;
plcm->index = lcmindex;
} 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);
plcm->lcm_if_id = plcm->params->lcm_if;
/* 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;
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 ((lcm_id == LCM_INTERFACE_NOTDEFINED) || lcm_id == plcm->lcm_if_id) {
plcm->lcm_original_width = plcm->params->width;
plcm->lcm_original_height = plcm->params->height;
_dump_lcm_info(plcm);
return plcm;
}
DISPERR("the specific LCM Interface [%d] didn't define any lcm driver\n", lcm_id);
goto FAIL;
}
FAIL:
kfree(plcm);
kfree(lcm_param);
return NULL;
}
可以看到,disp_lcm_probe函数首先从lcm_driver_list和lcm_name_list中找到对应的LCM_DRIVER 结构体,也即找到我们的驱动程序。这也就是为什么我们在移植的时候为什么要添加到这个数组中的原因。
接着,分配了disp_lcm_handle结构体空间,并将找到的驱动结构体保存找这个结构体中。这个结构体空间最终会被返回给调用的程序。顺带提一下,在这里我们调用了驱动程序中的get_params来进行参数的设置,这个函数后面我们会进行分析。
继续disp_lcm_probe函数,我们发现,在我们进行查找lcm driver的时候,都是将我们的lcm_driver_list中驱动程序的name与plcm_name进行比较得到的,而这个plcm_name的值究竟是什么呢,这个关系到我们是否能找到我们添加的驱动程序。
这里我们向上追溯发现disp_lcm_probe函数是在primary_diaplay.c文件中primary_display_init中被调用的,参数也是primary_display_init的第一个参数。继续向上追溯,找到mtkfb.c中mtkfb_probe函数,其调用如下:
primary_display_init(mtkfb_find_lcm_driver(), lcd_fps);
所以可见,我们拿来查找lcm驱动的名字就是mtkfb_find_lcm_driver()函数的返回值。我们来看一下,这个函数如何确定我们的驱动信息的。该函数实现如下:
char *mtkfb_find_lcm_driver(void)
{
#ifdef CONFIG_OF
if (1 == _parse_tag_videolfb()) {
pr_debug("[mtkfb] not found LCM driver, return NULL\n");
return NULL;
}
#else
{
char *p, *q;
p = strstr(saved_command_line, "lcm=");
/* we can't find lcm string in the command line, the uboot should be old version */
if (p == NULL)
return NULL;
p += 6;
if ((p - saved_command_line) > strlen(saved_command_line + 1))
return NULL;
pr_debug("%s, %s\n", __func__, p);
q = p;
while (*q != ' ' && *q != '\0')
q++;
memset((void *)mtkfb_lcm_name, 0, sizeof(mtkfb_lcm_name));
strncpy((char *)mtkfb_lcm_name, (const char *)p, (int)(q - p));
mtkfb_lcm_name[q - p + 1] = '\0';
}
#endif
/* printk("%s, %s\n", __func__, mtkfb_lcm_name); */
return mtkfb_lcm_name;
}
这里可以看出,如果内核定义了CONFIG_OF(即使用设备树),则从设备树中获取mtkfb_lcm_name,否则就是从bootloader传递过来的参数中获取。
到这里,我们的驱动程序被查找的过程基本了解了。
2、这个结构体中都有哪些成员,主要的作用或者功能;
3、对驱动程序中重要的函数分析
下面我们开始逐一解决上述三个问题:
首先看第二个问题,LCM_DRIVER结构体的定义在lcm_drv.h中,具体如下:
typedef struct {
/*lcm的名字,需要添加到mt65xx_lcm_list.c中lcm_name_list数组中,以及defconfig文件中*/
const char *name;
/*设置一些实用的函数 */
void (*set_util_funcs)(const LCM_UTIL_FUNCS *util);
/*对一些参数定义,例如屏的分辨率,屏的接口类型等等*/
void (*get_params)(LCM_PARAMS *params);
void (*init)(void);
void (*suspend)(void);
void (*resume)(void);
/* for power-on sequence refinement */
void (*init_power)(void);
void (*suspend_power)(void);
void (*resume_power)(void);
void (*update)(unsigned int x, unsigned int y, unsigned int width, unsigned int height);
unsigned int (*compare_id)(void);
#if defined(MTK_LCM_DEVICE_TREE_SUPPORT)
void (*parse_dts)(const LCM_DTS *DTS, unsigned char force_update);
#endif
/* /CABC backlight related function */
void (*set_backlight)(unsigned int level);
void (*set_backlight_cmdq)(void *handle, unsigned int level);
void (*set_pwm)(unsigned int divider);
unsigned int (*get_pwm)(unsigned int divider);
void (*set_backlight_mode)(unsigned int mode);
/* / */
int (*adjust_fps)(void *cmdq, int fps, LCM_PARAMS *params);
/* ///ESD_RECOVERY// */
unsigned int (*esd_check)(void);
unsigned int (*esd_recover)(void);
unsigned int (*check_status)(void);
unsigned int (*ata_check)(unsigned char *buffer);
void (*read_fb)(unsigned char *buffer);
int (*ioctl)(LCM_DRV_IOCTL_CMD cmd, unsigned int data);
/* /// */
void (*enter_idle)(void);
void (*exit_idle)(void);
void (*change_fps)(unsigned int mode);
/* //switch mode */
void *(*switch_mode)(int mode);
void (*set_cmd)(void *handle, int *mode, unsigned int cmd_num);
void (*set_lcm_cmd)(void *handle, unsigned int *lcm_cmd, unsigned int *lcm_count, unsigned int *lcm_value);
/* /PWM/ */
void (*set_pwm_for_mix)(int enable);
} LCM_DRIVER;
这个结构体中,我们需要重点关注一下name、init、suspend、resume、set_util_funcs以及get_params这几个成员。
一、LCD的接口类型
目前手机上的彩色LCD的连接方式一般采用下述方式:
● DSI接口(MIPI协议族)
● MCU/CPU接口
● TTL/RGB接口
● VSYNC接口
为了引出本文要阐释的内容,本节中将介绍MCU模式和RGB模式,下节再重点介绍DSI模式。
1、MCU接口
MCU-LCD接口因早期在单片机领域以及中低端手机中大量使用而得名,其标准术语是Intel提出的Interface 80,因此在很多文档中使用 I80 来指MCU-LCD屏。
根据时序不同,MCU-LCD接口可以分为8080和6800两种模式。数据位传输有8位,9位,16位,18位,24位。连线分为:CS/,RS(寄存器选择),RD/,WR/以及数据线。
MCU-LCD接口具有以下优点:控制简单方便,无需时钟和同步信号。缺点是:要耗费GRAM,所以难以做到大屏(3.8以上)。
对于MCU接口的LCM,其内部的LCD驱动器芯片内带有GRAM。驱动器的主要功能是对主机发过的数据/命令,变换成每个象素的RGB数据,并在屏上显示出来。这个过程不需要点、行、帧时钟。
MCU接口框架如下图:
M6800模式
M6800模式支持可选择的总线宽度8/9/16/18-bit(默认为8位),其实际设计思想是与I80的思想是一样的,主要区别就是该模式的总线控制读写信号组合在一个引脚上(/WR),而增加了一个锁存信号(E)数据位传输有8位,9位,16位和18位。
I8080模式
I80模式连线分为:CS/,RS(寄存器选择),RD/,WR/,再就是数据线了。优点是:控制简单方便,无需时钟和同步信号。缺点是:要耗费GRAM,所以难以做到大屏(QVGA以上)。
MCU接口标准名称是I80,管脚的控制脚有5个:
CS 片选信号
RS (置1为写数据,置0为写命令)
/WR (为0表示写数据) 数据命令区分信号
/RD (为0表示读数据)
RESET 复位LCD( 用固定命令系列 0 1 0来复位)
2、RGB接口
(未完待续)