Skip to content

LV065-Linux下的LCD驱动

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

一、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

c
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

c
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 节点:

c
			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 找到以下内容:

c
	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

c
	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

c
&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 的时候,里面有两行:

c
&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)

例如:

c
    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

c
			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 平台设备驱动结构体的定义:

c
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

c
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 都已经给我们写好了,不需要修改,不过我们还是要看一下。