0%

Linux驱动学习日记(27) LCD 驱动

Linux 下的 LCD 驱动简析

Framebuffer 设备

  在裸机中,LCD 驱动编写流程如下:
  1、初始化 IMX6U 的 eLCDIF 控制器,重点是 LCD 屏幕的宽高、hspw、hbf、hfp、vspw、vbp 和 vfp 等信息。
  2、初始化 LCD 像素时钟。
  3、设置 RGBLCD 显存。
  4、应用程序直接通过操作显存来操作 LCD,实现在 LCD 上显示字符、图片等信息。
  在 Linux 中应用程序最终也是通过操作 LCD 的显存来实现在 LCD 显示的。在裸机中我们可以随意的分配内存,但是在 Linux 中,内存的管理很严格,显存需要申请,不是想用就能用的。而且因为虚拟内存的存在,驱动程序设置的显存和应用程序访问的显存还是同一片物理内存。
  为了解决上述问题,Framebuffer 诞生了,这个词翻译过来就是帧缓冲,简称 fb。fb 是一种机制,将系统中所有跟显示有关的硬件以及软件全部集合起来,虚拟出一个 fb 设备,当我们编写好 LCD 驱动后会生成一个名为 /dev/fbX 的设备,应用程序通过访问这个设备就可以访问 LCD。NXP 官方的 Linux 内核默认开启了 LCD 驱动,因此我们可以看到 /dev/fb0 这样一个设备:

图 1 fb设备存在

  /dev/fb0 就是 LCD 对应的设备文件,这也是个字符设备,所以肯定也有 file_operations 操作集,fb 的 file_operations 定义在 drivers/video/fbdev/core/fbmem.c 文件中,如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
static const struct file_operations fb_fops = {
.owner = THIS_MODULE,
.read = fb_read,
.write = fb_write,
.unlocked_ioctl = fb_ioctl,
#ifdef CONFIG_COMPAT
.compat_ioctl = fb_compat_ioctl,
#endif
.mmap = fb_mmap,
.open = fb_open,
.release = fb_release,
#if defined(HAVE_ARCH_FB_UNMAPPED_AREA) || \
(defined(CONFIG_FB_PROVIDE_GET_FB_UNMAPPED_AREA) && \
!defined(CONFIG_MMU))
.get_unmapped_area = get_fb_unmapped_area,
#endif
#ifdef CONFIG_FB_DEFERRED_IO
.fsync = fb_deferred_io_fsync,
#endif
.llseek = default_llseek,
};

DRM 驱动

  在现代 Linux 版本中,DRM 是 Linux 内核中最新的显示子系统框架,其作用为统一管理 GPU、LCD、HDMI、显示控制器等等,其在内核中的位置为 drivers/gpu/drm,它解决了旧 fb 系统只能单屏、没有图层、不能热插拔、没有 GPU 管理等问题。

  DRM 不再只管理 framebuffer,而是管理整个显示管线。其流程如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
应用程序


Framebuffer


Plane(图层)


CRTC(扫描控制器)


Encoder


Connector


显示设备(LCD/HDMI)

  DRM 的五个核心对象如下:
  1、Framebuffer:就是原来的图像缓冲区,应用程序写入像素。
  2、Plane:显示图层,例如 背景图层、UI 图层、视频图层 等等。很多 SoC 有多个 plane
  3、CRTC:显示控制器,负责扫描 framebuffer、生成时序、输出像素等等。
  4、Encoder:它负责像素信号的转换,比如从 RGB 到 HDMI。
  5、Connector:表示物理接口,例如 HDMI、LCDS、DSI、RGBLCD 等等。
  DRM probe 函数流程如下:

1
2
3
4
5
6
7
8
9
10
probe()


创建 drm_device


初始化 KMS


注册 DRM

  其中 KMS 表示内核设置显示模式,包括了分辨率、刷新率、颜色格式等等。

修改设备树

  正如前面所说,6ULL 的 eLCDIF 接口驱动程序已经写好了,因此 LCD 驱动部分我们不需要去修改,我们只需要根据 LCD 所使用的 LCD 参数来修改设备树。重点注意三个地方
  ①、LCD 所使用的 IO 配置。
  ②、LCD 屏幕节点修改,修改相应的属性值,切换到我们所使用的 LCD 屏幕参数。
  ③、LCD 背光节点修心修改,要根据实际所使用的背光 IO 来修改相应的设备节点信息。

LCD 屏幕 IO 配置

  首先检查设备树中 LCD 所使用的 IO 配置,这个其实 NXP 已经写好,我们无需修改,内容如下:

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
pinctrl_lcdif_dat: lcdifdatgrp {
fsl,pins = <
MX6UL_PAD_LCD_DATA00__LCDIF_DATA00 0x79
MX6UL_PAD_LCD_DATA01__LCDIF_DATA01 0x79
MX6UL_PAD_LCD_DATA02__LCDIF_DATA02 0x79
MX6UL_PAD_LCD_DATA03__LCDIF_DATA03 0x79
MX6UL_PAD_LCD_DATA04__LCDIF_DATA04 0x79
MX6UL_PAD_LCD_DATA05__LCDIF_DATA05 0x79
MX6UL_PAD_LCD_DATA06__LCDIF_DATA06 0x79
MX6UL_PAD_LCD_DATA07__LCDIF_DATA07 0x79
MX6UL_PAD_LCD_DATA08__LCDIF_DATA08 0x79
MX6UL_PAD_LCD_DATA09__LCDIF_DATA09 0x79
MX6UL_PAD_LCD_DATA10__LCDIF_DATA10 0x79
MX6UL_PAD_LCD_DATA11__LCDIF_DATA11 0x79
MX6UL_PAD_LCD_DATA12__LCDIF_DATA12 0x79
MX6UL_PAD_LCD_DATA13__LCDIF_DATA13 0x79
MX6UL_PAD_LCD_DATA14__LCDIF_DATA14 0x79
MX6UL_PAD_LCD_DATA15__LCDIF_DATA15 0x79
MX6UL_PAD_LCD_DATA16__LCDIF_DATA16 0x79
MX6UL_PAD_LCD_DATA17__LCDIF_DATA17 0x79
MX6UL_PAD_LCD_DATA18__LCDIF_DATA18 0x79
MX6UL_PAD_LCD_DATA19__LCDIF_DATA19 0x79
MX6UL_PAD_LCD_DATA20__LCDIF_DATA20 0x79
MX6UL_PAD_LCD_DATA21__LCDIF_DATA21 0x79
MX6UL_PAD_LCD_DATA22__LCDIF_DATA22 0x79
MX6UL_PAD_LCD_DATA23__LCDIF_DATA23 0x79
>;
};

pinctrl_lcdif_ctrl: lcdifctrlgrp {
fsl,pins = <
MX6UL_PAD_LCD_CLK__LCDIF_CLK 0x79
MX6UL_PAD_LCD_ENABLE__LCDIF_ENABLE 0x79
MX6UL_PAD_LCD_HSYNC__LCDIF_HSYNC 0x79
MX6UL_PAD_LCD_VSYNC__LCDIF_VSYNC 0x79
/* used for lcd reset */
MX6UL_PAD_SNVS_TAMPER9__GPIO5_IO09 0x79
>;
};

LCD 屏幕参数节点信息修改

  在 imx6ull-hcw-emmc.dtsi 文件中找到 lcdif 节点,内容如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
&lcdif {
assigned-clocks = <&clks IMX6UL_CLK_LCDIF_PRE_SEL>;
assigned-clock-parents = <&clks IMX6UL_CLK_PLL5_VIDEO_DIV>;
pinctrl-names = "default";
pinctrl-0 = <&pinctrl_lcdif_dat
&pinctrl_lcdif_ctrl>;
status = "okay";

port {
display_out: endpoint {
remote-endpoint = <&panel_in>;
};
};
};

  这段代码中,port{} 这一段是 DRM / OF graph 连接方式的核心,它不再像老的 fbdev 一样直接在 lcdif 节点内部写 display-timings,它将 lcdif 的输出口,通过 endpoint 连到 panel 的输入口。
  找到 panel 节点,如下所示:

1
2
3
4
5
6
7
8
9
10
panel {
compatible = "innolux,at043tn24";
backlight = <&backlight_display>;

port {
panel_in: endpoint {
remote-endpoint = <&display_out>;
};
};
};

  但是我们并没有找到对应的硬件参数啊,在哪里呢?可以看到这个节点的 compatible 被设置为了 “innolux,at043tn24”,在 panel-simple.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
static const struct drm_display_mode innolux_at043tn24_mode = {
.clock = 9000,
.hdisplay = 480,
.hsync_start = 480 + 2,
.hsync_end = 480 + 2 + 41,
.htotal = 480 + 2 + 41 + 2,
.vdisplay = 272,
.vsync_start = 272 + 2,
.vsync_end = 272 + 2 + 10,
.vtotal = 272 + 2 + 10 + 2,
.flags = DRM_MODE_FLAG_NHSYNC | DRM_MODE_FLAG_NVSYNC,
};

static const struct panel_desc innolux_at043tn24 = {
.modes = &innolux_at043tn24_mode,
.num_modes = 1,
.bpc = 8,
.size = {
.width = 95,
.height = 54,
},
.bus_format = MEDIA_BUS_FMT_RGB888_1X24,
.connector_type = DRM_MODE_CONNECTOR_DPI,
.bus_flags = DRM_BUS_FLAG_DE_HIGH | DRM_BUS_FLAG_PIXDATA_DRIVE_POSEDGE,
};

  然而我们现在需要适配自己的 RGB 屏幕,最直接的做法就是改成从设备树读取,我们将 panel 节点改成如下所示:

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
panel {
compatible = "panel-dpi";
backlight = <&backlight_display>;
status = "okay";

port {
panel_in: endpoint {
remote-endpoint = <&display_out>;
};
};

panel-timing {
clock-frequency = <33000000>;
hactive = <800>;
vactive = <480>;
hfront-porch = <8>;
hback-porch = <4>;
hsync-len = <41>;
vfront-porch = <4>;
vback-porch = <2>;
vsync-len = <10>;

hsync-active = <0>;
vsync-active = <0>;
de-active = <1>;
pixelclk-active = <0>;
};
};

  编译设备树,替换并重启内核,可以看到 LCD 屏幕启动成功!

图 2 fb参数