LV10-01-LCD驱动-04-Linux下的LCD驱动

接下来了解一下Linux中自带的lcd驱动?若笔记中有错误或者不合适的地方,欢迎批评指正😃。

点击查看使用工具及版本
PC端开发环境 Windows Windows11
Ubuntu Ubuntu20.04.2的64位版本
VMware® Workstation 17 Pro 17.6.0 build-24238078
终端软件 MobaXterm(Professional Edition v23.0 Build 5042 (license))
Win32DiskImager Win32DiskImager v1.0
Linux开发板环境 Linux开发板 正点原子 i.MX6ULL Linux 阿尔法开发板
uboot NXP官方提供的uboot,使用的uboot版本为U-Boot 2019.04
linux内核 linux-4.19.71(NXP官方提供)
点击查看本文参考资料
分类 网址 说明
官方网站 https://www.arm.com/ ARM官方网站,在这里我们可以找到Cotex-Mx以及ARMVx的一些文档
https://www.nxp.com.cn/ NXP官方网站
https://www.nxpic.org.cn/NXP 官方社区
https://u-boot.readthedocs.io/en/latest/u-boot官网
https://www.kernel.org/linux内核官网
点击查看相关文件下载
分类 网址 说明
NXP https://github.com/nxp-imx NXP imx开发资源GitHub组织,里边会有u-boot和linux内核的仓库
nxp-imx/linux-imx/releases/tag/v4.19.71 NXP linux内核仓库tags中的v4.19.71
nxp-imx/uboot-imx/releases/tag/rel_imx_4.19.35_1.1.0 NXP u-boot仓库tags中的rel_imx_4.19.35_1.1.0
I.MX6ULL i.MX 6ULL Applications Processors for Industrial Products I.MX6ULL 芯片手册(datasheet,可以在线查看)
i.MX 6ULL Applications ProcessorReference Manual I.MX6ULL 参考手册(下载后才能查看,需要登录NXP官网)
Source Code https://elixir.bootlin.com/linux/latest/source linux kernel源码
kernel/git/stable/linux.git - Linux kernel stable tree linux kernel源码(官网,tag 4.19.71)
https://elixir.bootlin.com/u-boot/latest/source uboot源码

一、Framebuffer 设备

1. 裸机LCD驱动回顾

先来回顾一下裸机的时候 LCD 驱动是怎么编写的,裸机 LCD 驱动编写流程如下:

①、初始化 I.MX6U 的 eLCDIF 控制器,重点是 LCD 屏幕宽(width)、高(height)、 hspw、hbp、 hfp、 vspw、 vbp 和 vfp 等信息。

②、初始化 LCD 像素时钟。

③、设置 RGBLCD 显存。

④、应用程序直接通过操作显存来操作 LCD,实现在 LCD 上显示字符、图片等信息。

2. Framebuffer

2.1 fb简介

在 Linux 中应用程序最终也是通过操作 RGB LCD 的显存来实现在 LCD 上显示字符、图片等信息。在裸机中我们可以随意的分配显存,但是在 Linux 系统中内存的管理很严格,显存是需要申请的,不是想用就能用的。而且因为虚拟内存的存在,驱动程序设置的显存和应用程序访问的显存要是同一片物理内存。

为了解决上述问题, Framebuffer 诞生了, Framebuffer 翻译过来就是帧缓冲,简称 fb。它是一种机制,将系统中所有跟显示有关的硬件以及软件集合起来,虚拟出一个 fb 设备,当我们编写好 LCD 驱动以后会生成一个名为/dev/fbX(X=0~n)的设备,应用程序通过访问/dev/fbX 这个设备就可以访问 LCD。

image-20250411135815803

图中的/dev/fb0 就是 LCD 对应的设备文件, /dev/fb0 是个字符设备,因此肯定有file_operations 操作集。

2.2 fb子系统

前面我们知道Framebuffer子系统为用户空间操作显示设备提供了统一的接口,屏蔽了底层硬件之间的差异,用户只需要操作一块 内存缓冲区即可把需要的图像显示到LCD设备上。Framebuffer子系统主要分为两个部分,如下图所示:

1
  • 核心层: 主要实现字符设备的创建,为不同的显示设备提供文件通用处理接口;同时创建graphics设备类,占据主设备号29。
  • 硬件设备层: 主要提供显示设备的时序、显存、像素格式等硬件信息,实现显示设备的私有文件接口,并创建显示设备文件/dev/fbx(x=0~n)暴露给用户空间。 硬件设备层的代码需要驱动开发人员根据具体的显示设备提供给内核。

3. fb的file_operations

fb的file_operations操作集定义在fbmem.c - 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,
};

这个就和前面学习的字符设备是一样的。

4. fb_info

Linux 内核将所有的 Framebuffer 抽象为一个叫做 fb_info 的结构体, fb_info 结构体包含了 Framebuffer 设备的完整属性和操作集合,因此每一个 Framebuffer 设备都必须有一个 fb_info

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
struct fb_info {
atomic_t count;
//......
struct fb_var_screeninfo var; /* 当前可变参数 */
struct fb_fix_screeninfo fix; /* 当前固定参数 */
struct fb_monspecs monspecs; /* 当前显示器特性 */
struct work_struct queue; /* 帧缓冲事件队列 */
struct fb_pixmap pixmap; /* 图像硬件映射 */
struct fb_pixmap sprite; /* 光标硬件映射 */
struct fb_cmap cmap; /* 当前调色板 */
struct list_head modelist; /* 当前模式列表 */
struct fb_videomode *mode; /* 当前视频模式 */
//......
struct fb_ops *fbops; /* 帧缓冲操作函数集 */
struct device *device; /* This is the parent */
struct device *dev; /* 当前 fb 设备 */
int class_flag; /* 私有 sysfs 标志 */
//......
union {
char __iomem *screen_base; /* 虚拟内存基地址(屏幕显存) */
char *screen_buffer;
};
unsigned long screen_size; /* 虚拟内存大小(屏幕显存大小) */
void *pseudo_palette; /* 伪 16 位调色板 */
//......
};

fb_info 结构体的成员变量很多,我们重点关注 var、 fix、 fbops、 screen_base、 screen_size和 pseudo_palette。

二、LCD驱动分析

1. 设备树

我们先来看一下NXP 官方的设备树,官方的evk板已经添加了 LCD 设备节点,只是此节点的 LCD 屏幕信息是针对 NXP 官方 EVK 开发板所使用的 4.3 寸 480*272 编写的,alpha开发板上面的lcd引脚什么的和官方eck板一致,我们可以参考官方的板子先来学习一下。

1.1 lcdif: lcdif@21c8000

我们打开imx6ul.dtsi - arch/arm/boot/dts/imx6ul.dtsi,找到这个lcdif: lcdif@21c8000节点:

1
2
3
4
5
6
7
8
9
10
lcdif: lcdif@21c8000 {
compatible = "fsl,imx6ul-lcdif", "fsl,imx28-lcdif";
reg = <0x021c8000 0x4000>;
interrupts = <GIC_SPI 5 IRQ_TYPE_LEVEL_HIGH>;
clocks = <&clks IMX6UL_CLK_LCDIF_PIX>,
<&clks IMX6UL_CLK_LCDIF_APB>,
<&clks IMX6UL_CLK_DUMMY>;
clock-names = "pix", "axi", "disp_axi";
status = "disabled";
};

这个就是imx6ull中的lcd控制器的节点信息,可以看到地址是0x021c8000,我们之前裸机学习的时候知道,eLCDIF 控制器的起始地址就是这个:

image-20250411141629866

这里的 lcdif 节点信息是所有使用 I.MX6ULL 芯片的板子所共有的,并不是完整的 lcdif 节点信息。像屏幕参数这些需要根据不同的硬件平台去添加,比如向 imx6ull-alpha-emmc.dts 中的 lcdif 节点添加其他的属性信息。

1.2 pinctrl子系统信息

1.2.1 pinctrl_lcdif_dat: lcdifdatgrp

我们打开imx6ul-14x14-evk.dtsi - arch/arm/boot/dts/imx6ul-14x14-evk.dtsi找到以下内容:

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
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
>;
};

这里就是LCD用到的数据线所有的GPIO的配置。

1.2.2 pinctrl_lcdif_ctrl: lcdifctrlgrp

还有一些LCD的信号相关的引脚在这里:pinctrl_lcdif_ctrl: lcdifctrlgrp

1
2
3
4
5
6
7
8
9
10
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
>;
};

1.3 GPIO子系统信息

上面使用了pinctrl子系统来设置了用到的GPIO的复用功能,GPIO的电气属性(工作模式,驱动能力)这些,但像gpio输出高低电平这些,是还需要gpio子系统来进行配置,不过nxp官方的设备树中并没有相关的了。这里知道肯可能会有就是了。

1.4 屏幕参数节点&lcdif

上面已经找到了pinctrl和gpio子系统的信息,接下来就是找到应用的地方,我们找到&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>;
};
};
};

这里就是lcd控制器配置的地方,这里就会使用对应pinctrl子系统中的配置。

1.5 时钟配置

1.5.1 &lcdif的两个时钟属性

我们来看一下上面的时钟配置,前面找到&lcdif的时候,里面有两行:

1
2
3
4
5
&lcdif {
assigned-clocks = <&clks IMX6UL_CLK_LCDIF_PRE_SEL>;
assigned-clock-parents = <&clks IMX6UL_CLK_PLL5_VIDEO_DIV>;
//......
};

可以参考一下clock-bindings.txt ,这里大概就是在说当平台需要初始化默认的父时钟和时钟频率时,可通过设备树节点中的以下属性配置:

  • assigned-clocks:需配置的时钟列表(phandle + 时钟标识符)
  • assigned-clock-parents:指定父时钟列表(phandle + 时钟标识符对)
  • assigned-clock-rates:指定时钟频率列表(单位:Hz)

例如:

1
2
3
4
5
6
7
8
9
10
11
uart@a000 {
compatible = "fsl,imx-uart";
reg = <0xa000 0x1000>;
...
clocks = <&osc 0>, <&pll 1>;
clock-names = "baud", "register";

assigned-clocks = <&clkcon 0>, <&pll 2>;
assigned-clock-parents = <&pll 2>;
assigned-clock-rates = <0>, <460800>;
};

在这个例子中,<&pll 2>时钟被设置为时钟<&clkcon 0>的父时钟,<&pll 2>时钟被分配了一个460800 Hz的频率值。

对于这个lcdif节点来说,就是把<&clks IMX6UL_CLK_LCDIF_PRE_SEL>的父时钟设置为<&clks IMX6UL_CLK_PLL5_VIDEO_DIV>

1.5.2 &clk节点

我们来看一下前面的&clk节点,这个节点定义在imx6ul.dtsi

1
2
3
4
5
6
7
8
9
clks: ccm@20c4000 {
compatible = "fsl,imx6ul-ccm";
reg = <0x020c4000 0x4000>;
interrupts = <GIC_SPI 87 IRQ_TYPE_LEVEL_HIGH>,
<GIC_SPI 88 IRQ_TYPE_LEVEL_HIGH>;
#clock-cells = <1>;
clocks = <&ckil>, <&osc>, <&ipp_di0>, <&ipp_di1>;
clock-names = "ckil", "osc", "ipp_di0", "ipp_di1";
};

看这个reg属性,可以知道这个节点起始地址是0x020c4000,这里就是我们的CCM时钟控制器的起始地址:

image-20250411153108154

1.5.3 总结

这里详细的就不去深入了,知道这里是设置了一下时钟源就行了。

2. 驱动框架分析

前面我们知道lcdif: lcdif@21c8000节点的 compatible 属性值为“fsl,imx6ul-lcdif”和“fsl,imx28-lcdif” ,搜索就可以发现,驱动是这个文件:mxsfb.c - drivers/video/fbdev/mxsfb.c。点开看一眼就会发现,这个其实也是个平台设备驱动。

2.1 mxsfb_driver

既然是个平台设备驱动,那按惯例,肯定先看这个mxsfb_driver平台设备驱动结构体的定义:

1
2
3
4
5
6
7
8
9
10
static struct platform_driver mxsfb_driver = {
.probe = mxsfb_probe,
.remove = mxsfb_remove,
.shutdown = mxsfb_shutdown,
.id_table = mxsfb_devtype,
.driver = {
.name = DRIVER_NAME,
.of_match_table = mxsfb_dt_ids,
},
};

这里的设备树匹配表为:mxsfb_dt_ids

1
2
3
4
5
static const struct of_device_id mxsfb_dt_ids[] = {
{ .compatible = "fsl,imx23-lcdif", .data = &mxsfb_devtype[0], },
{ .compatible = "fsl,imx28-lcdif", .data = &mxsfb_devtype[1], },
{ /* sentinel */ }
};

所以这里就可以完成匹配啦。

2.2 mxsfb_driver.mxsfb_probe

匹配上之后,肯定就是调用函数mxsfb_driver.mxsfb_probe了,这个函数主要工作如下:

①、申请 fb_info

②、初始化 fb_info 结构体中的各个成员变量,时间参数信息都会存放在这个结构体中。

③、初始化 eLCDIF 控制器,这里会用到fb_info 中的一些参数,例如前面着重学习的时间参数。

④、使用 register_framebuffer() 函数向 Linux 内核注册初始化好的 fb_info

详细的可以去看驱动,这里就不深入分析了。

三、LCD驱动移植

上面的设备树其实是不完整的,里面少了一些参数,我们移植的时候加进去。

1. 设备树修改

1.1 LCD 屏幕 IO 配置

首先要检查一下设备树中 LCD 所使用的 IO 配置,这个其实 NXP 都已经给我们写好了,不需要修改,不过我们还是要看一下。