前言
本系列文章以分享笔者以前的学习和工作内容为主,旨在查缺补漏,如有错误和不足之处,请读者不吝指正。
本文基于Android 4.4代码分析sc7731 lcd的基本流程。
显示系统
这里暂时以7710芯片手册为例,多媒体显示系统包含 LCDC 和 DISPC,其中 LCDC 仅支持 MCU 接口类型,支持 2 路显示;而 DISPC 同时支持 DBI 和 DPI 接口类型。
- LCDC
支持最多6个图层的Alaha Blending,RGB888数据格式到RGB565/RGB666数据格式的Dithering等功能。
- LCM
LCM接收AHB总线或者LCDC数据,并按照MCU接口格式按照固定的时序传输到显示模组上。
MCU支持8080和6080两种传输格式,两者的区别只是在读写控制上。
- DISPC
这里可以看出2种路线,一种直接由LCDC DBI经由DISPC DBI输出;另一种直接由AXI Domain到DISPC DBI/DPI输出。所以如果使用MIPI接口显示模组,是不需要经过LCDC显示模块的,直接由DISPC模块控制。
uboot
lcd相关源码位于 drivers/video/sprdfb
,代码文件截图如下:
可以看到代码兼容了多款 Soc,Makefile 中也包含了多个平台的配置。
头文件 include/configs/sp7731gea.h
,里面定义了 CONFIG_SC8830
。
1 | #define CONFIG_SC8830 |
这里值得一提的是7731和8830的AP相同,只是Modem存在差异,前者支持WCMDA,后者支持TD_SCMDA。
1 | COBJS-$(CONFIG_SC8830)+= sprdfb_main.o sprdfb_panel.o sprdfb_dispc.o \ |
能够看出该平台的显示系统支持多种lcd接口,包括mcu、rgb、mipi、i2c、spi。
这里的mipi dsi有2个不同版本的IP核,参考头文件中的定义,这里使用的是dsi_1_21a中的代码。
下面以 lcd_nt35516_mipi.c
为例,分析 uboot lcd 的整个流程。
lcd 模组配置
drivers/video/sprdfb/lcd/Makefile
包含编译该屏驱动的配置1
COBJS-$(CONFIG_FB_LCD_NT35516_MIPI) += lcd_nt35516_mipi.o
drivers/video/sprdfb/sprdfb_panel.c
中包含该屏的配置1
2
3
4
5
6
7
8
9
10
11extern struct panel_spec lcd_nt35516_mipi_spec;
static struct panel_cfg panel_cfg[] = {
...
#ifdef CONFIG_FB_LCD_NT35516_MIPI
{
.lcd_id = 0x16,
.panel = &lcd_nt35516_mipi_spec,
},
#endif
...
}drivers/video/sprdfb/lcd/lcd_nt35516_mipi.c
中详细描述了该模组的配置1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42struct panel_spec lcd_nt35516_mipi_spec = {
//.cap = PANEL_CAP_NOT_TEAR_SYNC,
#ifdef CONFIG_FB_LOW_RES_SIMU
.display_width = 480,
.display_height= 854,
#endif
.width = 540,
.height = 960,
.fps = 60,
.type = LCD_MODE_DSI,
.direction = LCD_DIRECT_NORMAL,
.info = {
.mipi = &lcd_nt35516_mipi_info //(1) mipi lcd模组的常规配置
},
.ops = &lcd_nt35516_mipi_operations, //(2) mipi lcd的操作集合
};
static struct info_mipi lcd_nt35516_mipi_info = {
.work_mode = SPRDFB_MIPI_MODE_VIDEO,
.video_bus_width = 24, /*18,16*/
.lan_number = 3,
.phy_feq = 500*1000,
.h_sync_pol = SPRDFB_POLARITY_POS,
.v_sync_pol = SPRDFB_POLARITY_POS,
.de_pol = SPRDFB_POLARITY_POS,
.te_pol = SPRDFB_POLARITY_POS,
.color_mode_pol = SPRDFB_POLARITY_NEG,
.shut_down_pol = SPRDFB_POLARITY_NEG,
.timing = &lcd_nt35516_mipi_timing, //(3) mipi lcd的时序proch配置
.ops = NULL,
};
static struct panel_operations lcd_nt35516_mipi_operations = {
.panel_init = nt35516_mipi_init, //包含mipi屏大量初始化代码
.panel_readid = nt35516_readid, //mipi屏的读取id操作
};
static struct timing_rgb lcd_nt35516_mipi_timing = {
.hfp = 20, /* unit: pixel */
.hbp = 20,
.hsync = 20,//4,
.vfp = 10, /*unit: line*/
.vbp = 10,
.vsync = 6,
};
lcd 模组初始化流程
系统上电执行芯片 RomCode,完成 DDR 和外部存储器等常用外设的初始化;
之后读取外部存储器中的 uboot 并跳转执行,优先完成板级初始化 board_init_f
和 board_init_r
,其中 board_init_r
中调用了stdio_init
。代码流程图如下:
我们把重点聚焦在 sprdfb_probe 的实现上,下面剔除了部分无关代码。
1 | @ drivers/video/sprdfb/sprdfb_main.c |
- (1) 关闭pwm控制
- (2) 获取display controller的接口,定义在
sprdfb_dispc.c
1
2
3
4
5
6
7
8
9
10@ drivers/video/sprdfb/sprdfb_dispc.c
struct display_ctrl sprdfb_dispc_ctrl = {
.name = "dispc",
.early_init = sprdfb_dispc_early_init,
.init = sprdfb_dispc_init,
.uninit = sprdfb_dispc_uninit,
.refresh = sprdfb_dispc_refresh,
.update_clk = dispc_update_clock,
};
- (3) 调用
sprdfb_dispc_early_init
,配置相关的clk,使能dispc module
- (4)
sprdfb_panel_probe
会读取和保存 lcd 模组的 id,并对该模组完成初始化。 - (5)
sprdfb_dispc_init
会完成显示系统 dithering 和 osd layer 的设置。
需要特别注意 uboot lcd 初始化流程中会保存 lcd_base 和 lcd_id,通过命令行参数传递,以供 kernel 阶段继续使用。
lcd 刷新显示流程
看到这里读者可能会有疑问,uboot logo 是什么时候显示的呢?
下面开始分析lcd refresh流程,这里要从 board_init_r
中的do_cboot代码分析。
normal_mode
代码定义了多种启动方式:normal、recovery、fastboot、charge、watchdog等,这里直接看normal_boot的代码。
首先初始化马达硬件,完成开机震动,然后调用 vlx_nand_boot,这里的代码取决有是 nand 方案还是 emmc 方案,这里假设是 emmc 启动。1
2
3
4
5
6
7
8
9
10
11@ property/normal_mode.c
void normal_mode(void) {
vibrator_hw_init();
set_vibrator(1);
#if BOOT_NATIVE_LINUX
vlx_nand_boot(BOOT_PART, CONFIG_BOOTARGS, BACKLIGHT_ON);
#else
vlx_nand_boot(BOOT_PART, NULL, BACKLIGHT_ON);
#endif
}vlx_nand_boot
1
2
3
4
5
6
7
8
9
10@ property/normal_emc_mode.c
void vlx_nand_boot(char * kernel_pname, char * cmdline, int backlight_set) {
_boot_display_logo(dev, backlight_set); //(1)
set_vibrator(FALSE);
_boot_load_kernel_ramdisk_image(dev, kernel_pname, hdr);
_boot_secure_check();
sipc_addr_reset();
vlx_entry();
}
调用_boot_display_logo,读取logo分区图片内容,并调用lcd_display_logo,重点关注。
关闭马达;
从存储器读取kernel、ramdisk、dtb image,保存相关内存地址。
secure boot校验流程,根据产品定义,可能会对DSP、Modem、SIMLOCK等image进行校验。
sipc 内存区域清零,这部分由展讯自己实现,用于AP和Modem之间的通信交换,后面有文章会涉及。
跳转到内核启动
_boot_display_logo
从logo分区中读取图片信息,存放到缓存中,调用lcd_display_logo
刷新显示1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23LOCAL __inline void _boot_display_logo(block_dev_desc_t *dev, int backlight_set)
{
size_t size;
#if defined(CONFIG_LCD_720P) || defined(CONFIG_LCD_HD) //LiWei add CONFIG_LCD_HD
size = 1<<20;
#else
size = 1<<19;
#endif
uint8 * bmp_img = malloc(size);
if(!bmp_img){
debugf("%s: malloc for splash image failed!\n",__FUNCTION__);
return;
}
if(!_boot_partition_read(dev, L"logo", 0, size, bmp_img))
{
debugf("%s: read logo partition failed!\n",__FUNCTION__);
goto end;
}
lcd_display_logo(backlight_set,(ulong)bmp_img,size);
end:
free(bmp_img);
return;
}lcd_display_logo
因为是bmp格式图片,需要调用 lcd_display_bitmap 进行格式转换后才能在lcd上显示;1
2
3
4
5
6
7
8
9
10
11
12
13
14void lcd_display_logo(int backlight_set,ulong bmp_img,size_t size)
{
#define mdelay(t) ({unsigned long msec=(t); while (msec--) { udelay(1000);}})
if(backlight_set == BACKLIGHT_ON){
lcd_display_bitmap((ulong)bmp_img, 0, 0);
Dcache_CleanRegion((unsigned int)(lcd_base), size<<1);
lcd_display();
mdelay(50);
set_backlight(255);
} else {
Dcache_CleanRegion((unsigned int)(lcd_base), size<<1);
lcd_display();
}
}lcd_display
参考前面的内容,refresh函数指针等同于sprdfb_dispc_refresh
,定义在drivers/video/sprdfb_dispc.c
1
2
3
4
5
6
7
8
9
10@ drivers/video/sprdfb/sprdfb_main.c
void lcd_display(void)
{
real_refresh(&s_sprdfb_dev);
}
static int real_refresh(struct sprdfb_device *dev)
{
dev->ctrl->refresh(dev);
return 0;
}sprdfb_dispc_refresh
这里针对mipi lcd模组,可以简单理解为cmd mode和video mode不同方式刷新显示,这里不再详细分析操作寄存器的部分。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17@ drivers/video/sprdfb_dispc.c
static int32_t sprdfb_dispc_refresh (struct sprdfb_device *dev)
{
if(SPRDFB_PANEL_IF_DPI == dev->panel_if_type) { //cmd mode
dispc_set_bits((1<<5), DISPC_DPI_CTRL);
if(is_first_frame){
udelay(30);
dispc_clear_bits((1<<4), DISPC_DPI_CTRL);
dispc_set_bits((1 << 4), DISPC_CTRL);
is_first_frame = 0;
}
} else { //video mode
dispc_set_bits((1 << 4), DISPC_CTRL);
...
dispc_set_bits((1<<0), DISPC_INT_CLR);
}
}
至此,uboot lcd的初始化和刷新显示已经分析完成了。
kernel
lcd 相关代码如下图:
和 uboot 代码架构基本相似,下面以 lcd_nt35516_mipi.c
为例进行分析
lcd模组配置
驱动文件放置路径: drivers/video/sprdfb/lcd/lcd_nt35516_mipi.c
drivers/video/sprdfb/lcd/Makefile
包含编译该屏驱动的配置1
obj-$(CONFIG_FB_LCD_NT35516_MIPI) += lcd_nt35516_mipi.o=
drivers/video/sprdfb/Kconfig
1
2
3
4config FB_LCD_NT35516_MIPI
boolean "support NT35516 mipi panel"
depends on FB_SC8825 || FB_SCX35 || FB_SCX30G
default narch/arm/configs/sp7731gea-dt_defconfig
1
CONFIG_FB_LCD_NT35516_MIPI=y
drivers/video/sprdfb/lcd/lcd_nt35516_mipi.c
这个文件配置了该模组的基本信息和操作方法1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62static struct panel_operations lcd_nt35516_mipi_operations = {
.panel_init = nt35516_mipi_init,
.panel_readid = nt35516_readid,
.panel_enter_sleep = nt35516_enter_sleep,
.panel_esd_check = nt35516_check_esd,
};
static struct timing_rgb lcd_nt35516_mipi_timing = {
.hfp = 20, /* unit: pixel */
.hbp = 20,
.hsync = 20,//4,
.vfp = 10, /*unit: line*/
.vbp = 10,
.vsync = 6,
};
static struct info_mipi lcd_nt35516_mipi_info = {
.work_mode = SPRDFB_MIPI_MODE_VIDEO,
.video_bus_width = 24, /*18,16*/
.lan_number = 3,
.phy_feq = 500*1000,
.h_sync_pol = SPRDFB_POLARITY_POS,
.v_sync_pol = SPRDFB_POLARITY_POS,
.de_pol = SPRDFB_POLARITY_POS,
.te_pol = SPRDFB_POLARITY_POS,
.color_mode_pol = SPRDFB_POLARITY_NEG,
.shut_down_pol = SPRDFB_POLARITY_NEG,
.timing = &lcd_nt35516_mipi_timing,
.ops = NULL,
};
struct panel_spec lcd_nt35516_mipi_spec = {
#ifdef CONFIG_FB_LOW_RES_SIMU
.display_width = 480,
.display_height = 854,
#endif
.width = 540,
.height = 960,
.fps = 60,
.type = LCD_MODE_DSI,
.direction = LCD_DIRECT_NORMAL,
.is_clean_lcd = true,
.info = {
.mipi = &lcd_nt35516_mipi_info
},
.ops = &lcd_nt35516_mipi_operations,
};
struct panel_cfg lcd_nt35516_mipi = {
/* this panel can only be main lcd */
.dev_id = SPRDFB_MAINLCD_ID,
.lcd_id = 0x16,
.lcd_name = "lcd_nt35516_mipi",
.panel = &lcd_nt35516_mipi_spec,
};
static int __init lcd_nt35516_mipi_init(void)
{
return sprdfb_panel_register(&lcd_nt35516_mipi);
}
subsys_initcall(lcd_nt35516_mipi_init);
subsys_initcall 是一个内核中的一个宏,定义特殊的init段,在内核启动过程中会依次调用这些函数,基本和module_init类似,但优先级更高。
sprdfb_panel_register 定义在 drivers/video/sprdfb/sprdfb_panel.c
,会把该模组对应的 panel_cfg 添加到维护的全局链表panel_list_main
中。
lcd 模组初始化流程
arch/arm/configs/sp7731gea-native_defconfig
1
CONFIG_MACH_SP7731GEA=y
arch/arm/boot/dts/Makefile
1
2
3
4
5arch/arm/boot/dts/sprd-scx35_sp7731gea.dts
226:dtb-$(CONFIG_MACH_SP7731GEA) += sprd-scx35_sp7731gea.dtb
227:dtb-$(CONFIG_MACH_SP7731GEA_LM) += sprd-scx35_sp7731gea_lm.dtb
228:dtb-$(CONFIG_MACH_SP7731GEA_FWVGA) += sprd-scx35_sp7731gea_fwvga.dtb
229:dtb-$(CONFIG_MACH_SP7731GEAOPENPHONE) += sprd-scx35_sp7731geaopenphone.dtbsprd-scx35_sp7731gea.dts
其中lcd controller配置如下:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28aliases {
serial0 = &uart0;
serial1 = &uart1;
serial2 = &uart2;
serial3 = &uart3;
i2c0 = &i2c0;
i2c1 = &i2c1;
i2c2 = &i2c2;
i2c3 = &i2c3;
lcd0 = &fb0;
spi0 = &spi0;
spi1 = &spi1;
spi2 = &spi2;
hwspinlock0 = &hwspinlock0;
hwspinlock1 = &hwspinlock1;
};
fb0: fb@20800000 {
compatible = "sprd,sprdfb";
reg = <0xf5122000 0x1000>,<0xf5146000 0x1000>;
interrupts = <0 46 0x0>,<0 48 0x0>, <0 49 0x0>;
clock-names = "dispc_clk_parent", "dispc_dbi_clk_parent", "dispc_dpi_clk_parent", "dispc_emc_clk_parent", "dispc_clk", "dispc_dbi_clk", "dispc_dpi_clk", "dispc_emc_clk", "fb_spi_clock", "fb_spi_clock_parent";
clocks = <&clk_256m>, <&clk_256m>, <&clk_384m>, <&clk_aon_apb>, <&clk_dispc0>, <&clk_dispc0_dbi>, <&clk_dispc0_dpi>, <&clk_disp_emc>, <&clk_spi2>, <&ext_26m>;
clock-src = <256000000 256000000 384000000>;
dpi_clk_div = <7>;
sprd,fb_use_reservemem;
sprd,fb_mem = <0x9F311000 0x5EF000>;
};drivers/video/sprdfb/sprdfb_main.c
与之相匹配的lcd控制器驱动位于drivers/video/sprdfb/sprdfb_main.c
,当设备节点device_node被解析为platform_device注册到系统时,会和这个驱动进行匹配。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25#ifdef CONFIG_OF
static const struct of_device_id sprdfb_dt_ids[] = {
{ .compatible = "sprd,sprdfb", },
{}
};
#endif
static struct platform_driver sprdfb_driver = {
.probe = sprdfb_probe,
#ifndef CONFIG_HAS_EARLYSUSPEND
.suspend = sprdfb_suspend,
.resume = sprdfb_resume,
#endif
.remove = sprdfb_remove,
.driver = {
.name = "sprd_fb",
.owner = THIS_MODULE,
#ifdef CONFIG_OF
.of_match_table = of_match_ptr(sprdfb_dt_ids),
#endif
},
};
static int __init sprdfb_init(void)
{
return platform_driver_register(&sprdfb_driver);
}
驱动加载流程大致如下图:
sprdfb_probe 函数的定义如下,简单分析下代码流程
1 | static int sprdfb_probe(struct platform_device *pdev) |
(1) 为lcd控制器分配 framebuffer 结构体 fb_info;
(2) 读取设备树中lcd id,从前面设备树的信息lcd0来看,id = 0;
(3)
sprdfb_dispc_ctrl
保存为Soc显示控制器的操作接口;(4) 获取设备树中配置的寄存器地址信息。
(5)
sprdfb_panel_get
会调用adapt_panel_from_uboot
遍历模组链表panel_list_main
中 panel_cfg,将lcd_id 和 uboot中读取的模组芯片id进行对比,如果匹配则返回对应的panel_cfg;如果没有匹配,会再后续的流程中调用sprdfb_panel_probe
重新加载模组驱动。这期间会设置 DISPC 的dsi硬件配置。1
2
3
4
5
6
7static struct panel_spec *adapt_panel_from_readid(struct sprdfb_device *dev)
{
struct panel_cfg *cfg;
struct list_head *panel_list;
int id;
}(6)
sprdfb_dispc_logo_proc
按照panel_cfg的尺寸申请缓存区,并从uboot lcd_base中拷贝logo数据;1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59void sprdfb_dispc_logo_proc(struct sprdfb_device *dev)
{
uint32_t kernel_fb_size = 0;
uint32_t logo_src_v = 0;
uint32_t logo_dst_v = 0;//use the second frame buffer ,virtual
uint32_t logo_dst_p = 0;//use the second frame buffer ,physical
uint32_t logo_size = 0;// should be rgb565
if(dev == NULL) {
printk("sprdfb: %s[%d]: dev == NULL, return without process logo!!\n",__func__,__LINE__);
return;
}
if(lcd_base_from_uboot == 0) {
printk("sprdfb: %s[%d]: lcd_base_from_uboot == 0, return without process logo!!\n",__func__,__LINE__);
return;
}
if(SPRDFB_PANEL_IF_DPI != dev->panel_if_type)
return;
logo_size = dev->panel->width * dev->panel->height * 2;// should be rgb565
dev->logo_buffer_size = logo_size;
dev->logo_buffer_addr_v = __get_free_pages(GFP_ATOMIC | __GFP_ZERO , get_order(logo_size));
logo_dst_v = dev->logo_buffer_addr_v;
logo_dst_p = __pa(dev->logo_buffer_addr_v);
logo_src_v = (uint32_t)ioremap(lcd_base_from_uboot, logo_size);
memcpy(logo_dst_v, logo_src_v, logo_size);
dma_sync_single_for_device(dev, logo_dst_p, logo_size, DMA_TO_DEVICE);
iounmap(logo_src_v);
dispc_write(logo_dst_p, DISPC_OSD_BASE_ADDR);
sprdfb_dispc_refresh_logo(dev);
}
static int32_t sprdfb_dispc_refresh_logo (struct sprdfb_device *dev)
{
dispc_clear_bits(0x1f, DISPC_INT_EN);//disable all interrupt
dispc_set_bits(0x1f, DISPC_INT_CLR);// clear all interruption
dispc_set_bits(BIT(5), DISPC_DPI_CTRL);//update
//wait for update- done interruption
for(i=0; i<500; i++) {
if(!(dispc_read(DISPC_INT_RAW) & (0x10))){
udelay(1000);
} else {
break;
}
}
if(i >= 500) {
printk("sprdfb: [%s] wait dispc done int time out!! (0x%x)\n", __func__, dispc_read(DISPC_INT_RAW));
}
dispc_set_bits((1<<5), DISPC_INT_CLR);
return 0;
}(7) sprdfb_dispc_early_init 恢复使能 DISPC 模组;
(8) setup_fb_mem 解析设备树“sprd,fb_mem”配置 fb_info screen_base等参数
(9) setup_fb_info 进一步配置 fb_info,设置固定参数和可变参数;
(10) register_framebuffer 向内核注册帧缓冲区;
(11) sprdfb_dispc_init 设置 DISPC 的dithering 和 osd layer blending功能。
总结
针对lcd驱动调试的一些总结:
- 背光不亮
通常直接排查硬件,检查背光电压,pwm波形输出等
- 白屏显示
1.优先检查电压,一般液晶模组在完成初始化后,VGH/VGL经过charge pump,基本能够达到正负15v左右。
2.读取模组id,优先检查硬件接口是否有误,然后排查软件接口的设置,可通过示波器辅助快速定位。
3.检查是否初始化代码有误,存在模组代码不匹配的情况。
- 显示异常
1.优先排查硬件,曾经有遇到过BB的VDD输出2.8V,DVDD输出1.8v,但是模组内部将VDD/DVDD相连,直接导致初始化升压不够,显示异常。
2.检查分辨率、像素、时序配置和proch设定是否存在问题。
3.画面撕裂问题,一般需要开启TE,BB会在每帧数据开始前发送信号同步。
4.画面翻转问题,一般修改模组初始化代码,修改显示刷新方向解决。