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 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参数