STM32MP157驱动开发——Linux LCD驱动(上)

这篇具有很好参考价值的文章主要介绍了STM32MP157驱动开发——Linux LCD驱动(上)。希望对大家有所帮助。如果存在错误或未考虑完全的地方,请大家不吝赐教,您也可以点击"举报违法"按钮提交疑问。


0.前言

  LCD 是很常用的一个外设,通过 LCD 可以显示图片、界面UI等,提高人机交互的效率。STM32MP1 提供了一个 LTDC 接口用于连接 RGB 接口的液晶屏。本节就来学习如何使用这个接口。

一、LCD 和 LTDC 简介

1.LCD 简介

  LCD 全称是 Liquid Crystal Display,也就是液晶显示器,是现在最常用到的显示器。网上对于 LCD 的原理解释如下:

LCD 的构造是在两片平行的玻璃基板当中放置液晶盒,下基板玻璃上设置 TFT(薄膜晶体管),上基板玻璃上设置彩色滤光片,通过 TFT 上的信号与电压改变来控制液晶分子的转动方向,从而达到控制每个像素点偏振光出射与否而达到显示目的。

现在要在 STM32MP1 开发板上使用 LCD,不需要去研究 LCD 的具体实现原理,只需要从使用的角度去关注 LCD 的几个重要点:

1)分辨率

  提起 LCD 显示器,我们都会听到 720P、1080P、2K 或 4K 这样的字眼,这个就是 LCD 显示器分辨率。LCD 显示器都是由一个个的像素点组成,像素点就类似一个灯(在 OLED 显示器中,像素点就是一个小灯),这个小灯是 RGB 灯,也就是由 R(红色)、 G(绿色)和 B(蓝色)这三种颜色组成的,而 RGB 就是光的三原色。1080P 的意思就是 LCD 屏幕上的像素数量是 19201080 个,也就是这个屏幕一列 1080 个像素点,一共 1920 列,如下图所示:
ips_dev_panel_t参数设置,STM32MP157,驱动开发,stm32,linux
X 轴就是 LCD 显示器的横轴,Y 轴就是显示器的竖轴。图中的小方块就是像素点,一共有 1920
1080=2073600 个像素点。左上角的 A 点是第一个像素点,右下角的 C 点就是最后一个像素点。2K 就是 25601440 个像素点,4K 是 38402160 个像素点。很明显,在 LCD 尺寸不变的情况下,分辨率越高越清晰。同样的,分辨率不变的情况下,LCD 尺寸越小越清晰。比如常用的 24 寸显示器基本都是 1080P 的,而
现在使用的 5 寸的手机基本也是 1080P 的,但是手机显示细腻程度就要比 24 寸的显示器要好很多!
由此可见,LCD 显示器的分辨率是一个很重要的参数,但是并不是分辨率越高的 LCD 就越好。衡量一款 LCD 的好坏,分辨率只是其中的一个参数,还有色彩还原程度、色彩偏离、亮度、可视角度、屏幕刷新率等其他参数。

2)像素格式

  一个像素点就相当于一个 RGB 小灯,通过控制 R、G、B 这三种颜色的亮度就可以显示出各种各样的色彩。那该如何控制 R、G、B 这三种颜色的显示亮度呢?一般一个像素点的 R、G、B 这三部分分别使用 8bit 的数据,那么一个像素点就是 8bit*3=24bit,也就是说一个像素点 3 个字节,这种像素格式称为 RGB888。如果再加入 8bit 的 Alpha(透明)通道的话一个像素点就是 32bit,也就是 4 个字节,这种像素格式称为 ARGB8888。如果学习过 STM32 的话应该还听过 RGB565 这种像素格式,在本小节中使用 ARGB8888 这种像素格式,一个像素占用 4 个字节的内存,这四个字节每个位的分配如下图所示:
ips_dev_panel_t参数设置,STM32MP157,驱动开发,stm32,linux
  一个像素点是 4 个字节,其中 bit31 ~ bit24 是 Alpha 通道,bit23 ~ bit16 是 RED 通道,bit15 ~ bit14 是 GREEN 通道,bit7 ~ bit0 是 BLUE 通道。所以红色对应的值就是 0X00FF0000,蓝色对应的值就是 0X000000FF,绿色对应的值为 0X0000FF00。通过调节 R、G、B的比例可以产生其它的颜色,比如0X00FFFF00就是黄色,0X00000000就是黑色,0X00FFFFFF 就是白色。例如电脑的画图工具也可以通过这种方式设置颜色。

3)LCD 屏幕接口

  LCD 屏幕或者说显示器有很多种接口,比如在显示器上常见的 VGA、HDMI、DP 等等,但是 STM32MP1 开发板不支持这些接口。STM32MP1 支持 RGB 接口的 LCD,RGBLCD 接口的信号线如下表所示:

信号线 描述
R[7:0] 8 根红色数据线
G[7:0] 8 根绿色数据线
B[7:0] 8 根蓝色数据线
DE 数据使能线
VSYNC 垂直同步信号线
HSYNC 水平同步信号线
PCLK 像素时钟信号

R[7:0]、G[7:0] 和 B[7:0] 这 24 根是数据线,DE、VSYNC、HSYNC 和 PCLK 这四根是控制信号线。RGB LCD 一般有两种驱动模式:DE 模式和 HV 模式,这两个模式的区别是 DE 模式需要用到 DE 信号线,而 HV 模式不需要用到 DE 信号线,在 DE 模式下是可以不需要 HSYNC 信号线的,即使不接 HSYNC 信号线 LCD 也可以正常工作。

正点原子一共有四款 RGB LCD 屏幕,两款 4.3 寸和两款 7 寸屏,每一款有两种分辨率。笔者在买开发板时没有购买 LCD 屏,所以按照教程中,对 7 寸,1024*600这个屏幕进行驱动开发。(猜测都是通用接口,应该差别不大)
原理图:
ips_dev_panel_t参数设置,STM32MP157,驱动开发,stm32,linux
J1 就是对外接口,是一个 40PIN 的 FPC 座(0.5mm 间距),通过 FPC 线,可以连接到 STM32MP1 开发板上。该接口十分完善,采用 RGB888 格式,并支持 DE&HV 模式,还支持触摸屏和背光控制。右侧的几个电阻,并不是都焊接的,用户可以根据自己实际需要而选择
是否焊接(正点原子出厂屏幕不能做修改!)。默认情况,R1和R6焊接,设置 LCD_LR 和 LCD_UD,控制 LCD 的扫描方向,是从左到右,从上到下(横屏看)。而 LCD_R7/G7/B7 则用来设置 LCD 的 ID,由于 RGB LCD 没有读写寄存器,也就没有所谓的 ID,这里通过在模块上面,控制R7/G7/B7 的上/下拉,来自定义 LCD 模块的 ID,帮助 SOC 判断当前 LCD 面板的分辨率和相关参数,以提高程序兼容性。这几个位的设置关系如下表所示:

M2 LCD_B7 M1 LCD_G7 M0 LCD_R7 LCD ID 说明
0 0 0 4342 ATK-4342 RGBLCD 模块,分辨率:480*272
0 0 1 7084 ATK-7084 RGBLCD 模块,分辨率:800*480
0 1 0 7016 ATK-7016 RGBLCD 模块,分辨率:1024*600
1 0 0 4384 ATK-4384 RGBLCD 模块,分辨率:800*480
X X X NC 暂时未用到

ATK-7016 模块,就设置 M2:M0=010 即可。这样在程序里面读取 LCD_R7/G7/B7,得到 M0:M2 的值,从而判断 RGBLCD 模块的型号,并执行不同的配置,即可实现不同 LCD 模块的兼容。

4)LCD 时间参数

  如果将 LCD 显示一帧图像的过程想象成绘画,那么在显示的过程中就是用一根“笔”在不同的像素点画上不同的颜色。这根笔按照从左至右、从上到下的顺序扫描每个像素点,并且在像素画上对应的颜色,当画到最后一个像素点的时候一幅图像就绘制好了。假如一个 LCD 的分辨率为 1024*600,那么其扫描如下图所示:
ips_dev_panel_t参数设置,STM32MP157,驱动开发,stm32,linux
  一帧图像也是由一行一行组成的。HSYNC 是水平同步信号,也叫做行同步信号,当产生此信号就表示开始显示新的一行了,所以此信号都是在图的最左边。VSYNC 信号是垂直同步信号,也叫做帧同步信号,当产生此信号就表示开始显示新的一帧图像了,所以此信号在图的左上角。
  在图中还有一圈“黑边”,真正有效的显示区域是中间的白色部分。这一圈“黑边”要从显示器的“祖先” CRT 显示器开始说起,CRT 显示器就是以前很常见的那种大屁股显示器,CRT 显示器屁股后面是个电子枪,这个电子枪就是上面说的“画笔”,电子枪打出的电子撞击到屏幕上的荧光物质使其发光。只要控制电子枪从左到右扫完一行(也就是扫描一行),然后从上到下扫描完所有行,这样一帧图像就显示出来了。也就是说,显示一帧图像电子枪是按照‘Z’形在运动,当扫描速度很快的时候看起来就是一幅完成的画面了。当显示完一行以后会发出 HSYNC 信号,此时电子枪就会关闭,然后迅速的移动到屏幕的左边,当 HSYNC 信号结束以后就可以显示新的一行数据了,电子枪就会重新打开。在 HSYNC 信号结束到电子枪重新打开之间会插入一段延时,这段延时就是图中的 HBP。当显示完一行以后就会关闭电子枪等待 HSYNC 信号产生,关闭电子枪到 HSYNC 信号产生之间会插入一段延时,这段延时就是图中的 HFP 信号。同理,当显示完一帧图像以后电子枪也会关闭,然后等到 VSYNC 信号产生,期间也会加入一段延时,这段延时就是图中的 VFP。VSYNC 信号产生,电子枪移动到左上角,当 VSYNC 信号结束以后电子枪重新打开,中间也会加入一段延时,这段延时就是图 中的 VBP。
  HBP、HFP、VBP 和 VFP 就是导致图中黑边的原因,RGB LCD 屏幕内部有一个 IC,发送一行或者一帧数据给 IC,IC 是需要反应时间的。通过这段反应时间可以让 IC 识别到一行数据扫描完了,要换行了,或者一帧图像扫描完了,要开始下一帧图像显示了。因此,在 LCD 屏幕中继续存在 HBP、HFP、VPB 和 VFP 这四个参数的主要目的是为了锁定有效的像素数据。这四个时间是 LCD 重要的时间参数,后面编写 LCD 驱动的时候要用到,至于这四个时间参数具体值是多少,需要去查看所使用的 LCD 数据手册。

5)RGB LCD 屏幕时序

ips_dev_panel_t参数设置,STM32MP157,驱动开发,stm32,linux
HSYNC:行同步信号,当此信号有效就表示开始显示新的一行数据,查阅所使用的LCD 数据手册可以知道此信号是低电平有效还是高电平有效,假设此时是低电平有效。
HSPW:有些地方也叫做 thp,是 HSYNC 信号宽度,也就是 HSYNC 信号持续时间。HSYNC信号不是一个脉冲,而是需要持续一段时间才是有效的,单位为 CLK。
HBP:有些地方叫做 thb,术语叫做行同步信号后肩,单位是 CLK。
HOZVAL:有些地方叫做 thd,显示一行数据所需的时间,假如屏幕分辨率为 1024*600,那么 HOZVAL 就是 1024,单位为 CLK。
HFP:有些地方叫做 thf,术语叫做行同步信号前肩,单位是 CLK。
当 HSYNC 信号发出以后,需要等待 HSPW+HBP 个 CLK 时间才会接收到真正有效的像素数据。当显示完一行数据以后需要等待 HFP 个 CLK 时间才能发出下一个 HSYNC 信号,所以显示一行所需要的时间就是:HSPW + HBP + HOZVAL + HFP。

一帧图像就是由很多个行组成的, RGB LCD 的帧显示时序如下图所示:
ips_dev_panel_t参数设置,STM32MP157,驱动开发,stm32,linux
VSYNC:帧同步信号,当此信号有效的话就表示开始显示新的一帧数据,查阅所使用的 LCD 数据手册可以知道此信号是低电平有效还是高电平有效,假设此时是低电平有效。
VSPW:些地方也叫做 tvp,是 VSYNC 信号宽度,也就是 VSYNC 信号持续时间,单位为 1 行的时间。
VBP:有些地方叫做 tvb,术语叫做帧同步信号后肩,单位为 1 行的时间。
LINE:有些地方叫做 tvd,显示一帧有效数据所需的时间,假如屏幕分辨率为 1024*600,那么 LINE 就是 600 行的时间。
VFP:有些地方叫做 tvf,术语叫做帧同步信号前肩,单位为 1 行的时间。

显示一帧所需要的时间就是: VSPW+VBP+LINE+VFP 个行时间
最终的计算公式:T = (VSPW+VBP+LINE+VFP) * (HSPW + HBP + HOZVAL + HFP)
因此在配置一款 RGB LCD 的时候需要知道这几个参数:HOZVAL(屏幕有效宽度)、LINE(屏幕有效高度)、HBP、HSPW、HFP、VSPW、VBP 和 VFP。ALIENTEK 三款 RGB LCD屏幕的参数如下表所示:
ips_dev_panel_t参数设置,STM32MP157,驱动开发,stm32,linux
ips_dev_panel_t参数设置,STM32MP157,驱动开发,stm32,linux

6)像素时钟

像素时钟就是 RGB LCD 的时钟信号,以 ATK7016 这款屏幕为例,显示一帧图像所需要的时钟数就是:
= (VSPW+VBP+LINE+VFP) * (HSPW + HBP + HOZVAL + HFP)
= (3 + 20 + 600 + 12) * (20 + 140 + 1024 + 160)
= 635 * 1344
= 853440
显示一帧图像需要853440个时钟数,那么显示60帧就是: 853440 * 60 = 51206400 ≈ 51.2M,所以像素时钟就是 51.2MHz。

7)显存

如果采用 ARGB8888 格式,一个像素需要 4 个字节的内存来存放像素数据,那么 1024600 分辨率就需要 1024600*4 = 2457600B ≈ 2.4MB 内存。但是 RGB LCD 内部是没有内存的,所以就需要在开发板上的 DDR3 中分出一段内存作为 RGB LCD 屏幕的显存,如果要在屏幕上显示图像直接操作这部分显存即可。

2.LTDC 接口

  LTDC 是 STM32MP1 自带的液晶屏幕接口,用于连接 RGB LCD 接口的屏幕,LTDC 接口特性如下:

① 24 位 RGB 并行像素输出,每像素 8 位(RGB888)
② 2 个带有专用 FIFO 的显示层(FIFO 深度 64x64 位)
③ 查色表(CLUT),每个图层最高 256 种颜色(256x24)位
④ 可针对不同显示面板编程时序
⑤ 每层有多达 8 个输入颜色格式可供选择,分别为:ARGB8888、RGB888、RGB565、ARGB1555、ARGB4444、L8、AL44、AL88

LTDC接口功能框架:
ips_dev_panel_t参数设置,STM32MP157,驱动开发,stm32,linux
从图中可以看出 LTDC 的信号可以分为两类:4 个控制信号(LCD_CLK 像素时钟、LCD_HSYNC 水平同步、LCD_VSYNC 垂直同步、LCD_DE 数据有效)和 3 个 RGB 数据信号(8bit x 3)。

二、DRM 驱动框架

1.DRM 简介

  在 Linux 系统中,主流的显示框架有两种:DRM(Direct Rendering Module)框架和 FB(FrameBuffer)框架。FB 框架不能处理基于 3D 加速 GPU 显卡,而 DRM 可以统一管理 GPU显示,所以 DRM 相对于 FB 更能适应新的显示硬件。比如 DRM 支持多层合成、支持 VSYNC、支持 DMA-BUF、支持 fence 机制等等。
  下图就是一个 DRM 驱动框架,包括两部分:DRM core 和 DRM driver。DRM core 提供了一个基本的 DRM 框架,DRM driver 就可以注册进 DRM 框架,同时为用户空间提供一组 ioctl。libdrm 对底层接口(DRM driver 提供的 ioctl)进行封装,向上层提供统一的 API 接口。DRM driver 包含了 GEM 模块和 KMS 模块,这两模块也分为好几个小模块。
ips_dev_panel_t参数设置,STM32MP157,驱动开发,stm32,linux
图形执行管理器(GEM):全称 Graphics Execution Manager,这是一个内存管理器,主要负责内存的分配和释放,可以调用 GPU。
DUMB:这是一个 dumb 缓冲区,主要负责一些简单的 buffer 显示,可以通过 CPU 直接渲染 dumb,GPU 不会使用 dumb。
内核显示模式设置(KMS):全称 Kernel Mode Setting,主要负责显示的控制,包括屏幕分辨率、屏幕刷新率和颜色深度等等。
CRTC:就是指显示控制器,在 DRM 里有多个显存,就可以通过操作 CRTC 来控制要显示那个显存。
Encoder:负责从 CRTC 里输出的 timing 时序转换成外部设备所需要的信号的模块,同时也负责控制 LCD 的显示。
Connector:连接物理显示设备的连接器,比如 DSI、 HDMI 等等。
Plane:负责获取显存,再输出到 CRTC 里,说明 CRTC 必须要有一个 Plane。
帧缓冲(FB):能够显示图层的 buffer。

GEM 和 KMS 通过以下结构来与显示器对接:
ips_dev_panel_t参数设置,STM32MP157,驱动开发,stm32,linux
蓝色框表示 KMS 里的模块。plane 是连接 crtc 和 framebuffer 的纽带;encoder 是连接 crtc 和 connector 的纽带。GEM 是负责和物理的 buffer 打交道。plane 把获取到显存输出到 crtc 里,crtc 通过 connector 接口输出到显示器。

2.ST 官方的 DRM 驱动框架介绍

  在 Linux 系统中,DRM 驱动的核心主要就是一个 drm_driver 结构体,驱动程序要初始化drm_driver 结构体,然后调用 drm_dev_init 函数,将其注册到 DRM core。
   在设备树文件 stm32mp151.dtsi 中,有一个 ltdc 节点:

ltdc: display-controller@5a001000 {
	compatible = "st,stm32-ltdc";
	reg = <0x5a001000 0x400>;
	interrupts = <GIC_SPI 88 IRQ_TYPE_LEVEL_HIGH>,
				 <GIC_SPI 89 IRQ_TYPE_LEVEL_HIGH>;
	clocks = <&rcc LTDC_PX>;
	clock-names = "lcd";
	resets = <&rcc LTDC_R>;
	status = "disabled";
};

   这个文件中的 ltdc 节点信息是所有使用 STM32MP1 芯片的板子所共有的,且不是完整的 ltdc 节点信息。其中 ltdc 节点的 compatible 属性值为“st,stm32-ltdc”,在 Linux源码中搜索这个字符串就可以找到 STM32MP1 的 DRM 驱动文件,这个文件为 “drivers/gpu/drm/stm/drv.c”文件:

static const struct of_device_id drv_dt_ids[] = {
	{.compatible = "st,stm32-ltdc"},
	{},
};
MODULE_DEVICE_TABLE(of, drv_dt_ids);

static struct platform_driver stm_drm_platform_driver = {
		.probe = stm_drm_platform_probe,
		.remove = stm_drm_platform_remove,
		.driver = {
			.name = "stm32-display",
			.of_match_table = drv_dt_ids,
			.pm = &drv_pm_ops,
		},
};

   可以看出,这是一个标准的 platform 驱动,当驱动和设备匹配以后 stm_drm_platform_probe 函数就会执行。和其他设备驱动一样,DRM 也分为 DRM 设备和 DRM 驱动,drm_device 结构体为 DRM 设备,drm_driver 为 DRM 驱动。

drm_device 结构体

drm_device 结构体定义在 include/drm/drm_device.h 文件里,部分内容如下:

struct drm_device {
	struct list_head legacy_dev_list;
	int if_version;
	struct kref ref;
......
	u32 max_vblank_count;
	struct list_head vblank_event_list;
	spinlock_t event_lock;
	struct drm_agp_head *agp;
	struct pci_dev *pdev;
	unsigned int num_crtcs;
	struct drm_mode_config mode_config;
	struct mutex object_name_lock;
	struct idr object_name_idr;
	struct drm_vma_offset_manager *vma_offset_manager;
	struct drm_vram_mm *vram_mm;
	enum switch_power_state switch_power_state;
	struct drm_fb_helper *fb_helper;
};

   在编写 DRM 驱动时需要自行申请 drm_device 内存并且使用初始化,可以直接通过 drm_dev_alloc 函数来完成,此函数会先调用 kzalloc 为 drm_device 分配内存,然后调用 drm_dev_init 初始化 drm_device。
原型

struct drm_device *drm_dev_alloc(struct drm_driver *driver, struct device *parent)

参数
driver:drm_driver 结构体指针,也就是 DRM 设备对应的 DRM 驱动
parent:父设备
返回值
返回分配成功的新 DRM 设备
ERR_PTR:drm_device 申请失败

drm_device 分配成功以后还需要使用 drm_dev_register 函数向内核注册:
原型

int drm_dev_register(struct drm_device *dev, unsigned long flags)

参数
dev:需要注册到内核的 drm_device
flags:传递给驱动 .load 函数的标志
返回值
0:成功
负数:失败

drm_driver 结构体

   Linux 内核为 DRM 驱动提供一个叫做 drm_driver 的结构体,drm_driver 结构体包含了 DRM驱动的完整属性和操作集合,因此每一个 DRM 驱动都必须有一个 drm_driver。drm_driver 结构体定义在 include/drm/drm_drv.h 文件里:

struct drm_driver {
	int (*load) (struct drm_device *, unsigned long flags);
	int (*open) (struct drm_device *, struct drm_file *);
......

	int (*dumb_create)(struct drm_file *file_priv,
					   struct drm_device *dev,
					   struct drm_mode_create_dumb *args);
	int (*dumb_map_offset)(struct drm_file *file_priv,
						   struct drm_device *dev, uint32_t handle,
						   uint64_t *offset);
	int (*dumb_destroy)(struct drm_file *file_priv,
						struct drm_device *dev,
						uint32_t handle);
	const struct vm_operations_struct *gem_vm_ops;
	
	int major; /* 驱动主设备号 */
	int minor; /* 驱动次设备号 */
	int patchlevel; /* 驱动补丁等级 */
	char *name; /* 驱动名字 */
	char *desc; /* 驱动描述 */
	char *date; /* 驱动日期 */
	u32 driver_features; /* 驱动特性 */
	const struct drm_ioctl_desc *ioctls;
	int num_ioctls;
	const struct file_operations *fops;
	
	struct list_head legacy_dev_list;
	int (*firstopen) (struct drm_device *);
	void (*preclose) (struct drm_device *, struct drm_file *file_priv);
	int (*dma_ioctl) (struct drm_device *dev, void *data,
					  struct drm_file *file_priv);
	int (*dma_quiescent) (struct drm_device *);
	int (*context_dtor) (struct drm_device *dev, int context);
	int dev_priv_size;
};

成员变量比较多,重点是driver_features、fops 和 dumb_create。
①dumb_create 是一个回调函数, 用于创建 gem 对象,并分配物理 buffer。
②driver_features 用来描述驱动特性,枚举类型 drm_driver_feature 定义了可以选择的驱动特性:

DRIVER_GEM:驱动使用 GEM 内存管理,此特性必须选中!
DRIVER_MODESET:驱动支持模式设置接口(KMS)。
DRIVER_RENDER:驱动支持专用渲染节点。
DRIVER_ATOMIC:驱动提供完整的原子操作,以供用户空间 API 函数操作。
DRIVER_SYNCOBJ:驱动支持 SYNCOBJ, 用于命令提交的显式同步。
DRIVER_SYNCOBJ_TIMELINE:驱动支持 SYNCOBJ 时间线。
DRIVER_USE_AGP:驱动程序使用 AGP 接口, DRM 核心将管理 AGP 资源。
DRIVER_LEGACY:表明这是一个使用影子附着的旧驱动程序,不使用。
DRIVER_PCI_DMA:驱动支持 PCI DMA。
DRIVER_SG:驱动可以提供 scatter/gather DMA 功能
DRIVER_HAVE_DMA:驱动支持 DMA。
DRIVER_HAVE_IRQ:驱动支持 IRQ,旧驱动使用。
DRIVER_KMS_LEGACY_CONTEXT:仅供 nouveau 使用!

③fops 就是一个简单的字符设备接口结构体

当设备和驱动匹配成功以后 stm_drm_platform_probe 函数就会执行, 函数内容如下:

static int stm_drm_platform_probe(struct platform_device *pdev)
{
	struct device *dev = &pdev->dev;
	struct drm_device *ddev;
	int ret;
	
	DRM_DEBUG("%s\n", __func__);
	
	dma_set_coherent_mask(dev, DMA_BIT_MASK(32));
	
	ddev = drm_dev_alloc(&drv_driver, dev);
	if (IS_ERR(ddev))
		return PTR_ERR(ddev);
	
	ret = drv_load(ddev);
	if (ret)
		goto err_put;
	
	ret = drm_dev_register(ddev, 0);
	if (ret)
		goto err_put;
	
	drm_fbdev_generic_setup(ddev, 16);
	
	return 0;
	
err_put:
	drm_dev_put(ddev);
	return ret;
}

①drm_dev_alloc 函数,此函数主要完成以下功能:

a.给 drm_device 分配内存。
b.通过 drm_dev_init 函数初始化 drm_device3

drm_dev_alloc 会通过调用 drm_dev_init 函数将 drm_driver 和 drm_device 联系起来,drm_device 结构体里面有个 drvier 指针成员变量,此成员变量指向 DRM 设备对应的 DRM 驱动的。因此,drm_dev_init 函数会通过将 drm_device 下的 driver 成员变量指向 drm_driver 来实现两者相连。
②drv_load 这个函数就是初始化 KMS
③注册 drm_device 对象进 DRM core

drv_load 函数:

#define STM_MAX_FB_WIDTH 2048
#define STM_MAX_FB_HEIGHT 2048

static const struct drm_mode_config_funcs drv_mode_config_funcs = {
	.fb_create = drm_gem_fb_create,
	.atomic_check = drm_atomic_helper_check,
	.atomic_commit = drm_atomic_helper_commit,
};
......
static int drv_load(struct drm_device *ddev)
{
	struct platform_device *pdev = to_platform_device(ddev->dev);
	struct ltdc_device *ldev;
	int ret;

	DRM_DEBUG("%s\n", __func__);
	
	ldev = devm_kzalloc(ddev->dev, sizeof(*ldev), GFP_KERNEL);
	if (!ldev)
		return -ENOMEM;

	ddev->dev_private = (void *)ldev;

	drm_mode_config_init(ddev);

	/*
	* set max width and height as default value.
	* this value would be used to check framebuffer size limitation
	* at drm_mode_addfb().
	*/
	ddev->mode_config.min_width = 0;
	ddev->mode_config.min_height = 0;
	ddev->mode_config.max_width = STM_MAX_FB_WIDTH;
	ddev->mode_config.max_height = STM_MAX_FB_HEIGHT;
	ddev->mode_config.funcs = &drv_mode_config_funcs;
	
	ret = ltdc_load(ddev);
	if (ret)
		goto err;
	
	drm_mode_config_reset(ddev);
	drm_kms_helper_poll_init(ddev);
	
	platform_set_drvdata(pdev, ddev);
	
	return 0;
err:
	drm_mode_config_cleanup(ddev);
	return ret;
}

①前两行设置DRM 驱动 X 轴(宽度)最大支持 2048 个像素,设置 DRM 驱动 Y 轴(宽度)最大支持 2048 个像素,可以看出驱动里面设置的最大分辨率支持2048 * 2048。但是根据STM32MP157 手册所描述,最大支持1366 * 768分辨率的屏幕。
② mode_config.funcs 设置 framebuffer 的回调函数结构体。
③ ltdc_load 引入 drm_panel 结构体,此结构体作用是提供一堆控制回调函数,比如屏幕参数回调函数,背光控制函数等等。ltdc_load函数是负责初始化ltdc接口(同时connector 和 encoder 一起初始化)。在 connector 初始化时,就会调用 drm_panel 结构体里的获取屏幕参数函数(所以只需要提供一个屏的驱动就能正常显示了)。通常 encoder 和 connector 是放在同一个驱动初始化的,目的是为了方便驱动程序设计。

要完成整个 DRM 驱动的正常初始化,前面的 GEM 和 KMS 这些模块已经由 ST 官方提供,开发人员只需提供一个 drm_panel 对象即可。打开“include/drm/drm_panel.h”文件:

struct drm_panel {
	struct drm_device *drm; /* drm_device 对象 */
	struct drm_connector *connector; /* connector 对象 */
	struct device *dev; /* 设备节点 dev */
	const struct drm_panel_funcs *funcs; /* 回调函数的结构体 */
	struct list_head list;
};

这里就是 drm_panel 的结构体定义,开发者按照要求实现一个 drm_panel 对象后,传入驱动框架即可完成驱动开发。

3.RGB LCD 驱动分析(屏的驱动)

  drm_panel 结构体是基类,panel_simple 在 drm_panel 基础上增加了一些成员变量,相当于继承类。LCD 驱动文件为 drivers/gpu/drm/panel/panelsimple.c,其中有如下内容:

99  struct panel_simple {
100 	struct drm_panel base;
101 	bool prepared;
102 	bool enabled;
103 	bool no_hpd;
104
105 	const struct panel_desc *desc;
106
107 	struct backlight_device *backlight;
108 	struct regulator *supply;
109 	struct i2c_adapter *ddc;
110
111 	struct gpio_desc *enable_gpio;
112
113 	struct drm_display_mode override_mode;
114 };

panel_simple 结构体用来管理 RGB LCD 设备。
第 100行,base成员变量,为 drm_panel 结构体类型。可以看出 panel_simple 就是在 drm_panel 的基础上发展而来的,在 DRM 驱动注册的时候就会回调 base->funcs。
第 105 行,desc 属性就是 RGB 屏参数结构体。
第 107 行,屏的背光结构体。

与设备树匹配

LCD 驱动的 platform_driver:

3491 static struct platform_driver panel_simple_platform_driver = {
3492 	.driver = {
3493 		.name = "panel-simple",
3494 		.of_match_table = platform_of_match,
3495 },
3496 	.probe = panel_simple_platform_probe,
3497 	.remove = panel_simple_platform_remove,
3498 	.shutdown = panel_simple_platform_shutdown,
3499 };

这是一个标准的 platform 驱动框架, 第 3494 行就是匹配表。platform_of_match 内容如下所示(有省略):

3133 static const struct of_device_id platform_of_match[] = {
3134 	{
3135 		.compatible = "ampire,am-480272h3tmqw-t01h",
3136 		.data = &ampire_am_480272h3tmqw_t01h,
3137 	}, {
3138 		.compatible = "ampire,am800480r3tmqwa1h",
3139 		.data = &ampire_am800480r3tmqwa1h,
3140 	}, {}
3141 };

platform_of_match 里面有大量的匹配项,分别针对不同的屏幕,比如:第 3135 行就是一个匹配项,compatible 内容为“ampire,am-480272h3tmqw-t01h”。
第 3136 行,在 platform 框架里有个 data 成员变量,这个是一个 void 类型的指针,这里指向 ampire_am_480272h3tmqw_t01h,内容如下所示:

515 static const struct drm_display_mode ampire_am_480272h3tmqw_t01h_mode = {
516 	.clock = 9000, /* LCD 像素时钟,单位 KHz */
517 	.hdisplay = 480, /* LCD X 轴像素个数 */
518 	.hsync_start = 480 + 2, /* LCD X 轴+hbp 的像素个数 */
519 	.hsync_end = 480 + 2 + 41, /* LCD X 轴+hbp+hspw 的像素个数 */
520 	.htotal = 480 + 2 + 41 + 2, /* LCD X 轴+hbp+hspw+hfp 的像素个数 */
521 	.vdisplay = 272, /* LCD Y 轴像素个数 */
522 	.vsync_start = 272 + 2, /* LCD Y 轴+vbp 的像素个数 */
523 	.vsync_end = 272 + 2 + 10, /* LCD Y 轴+vbp+vspw 的像素个数 */
524 	.vtotal = 272 + 2 + 10 + 2, /* LCD Y 轴+vbp+vspw+vfp 的像素个数 */
525 	.vrefresh = 60, /* LCD 的刷新频率为 60HZ */
526 	.flags = DRM_MODE_FLAG_PHSYNC | DRM_MODE_FLAG_PVSYNC,
527 };
528
529 static const struct panel_desc ampire_am_480272h3tmqw_t01h = {
530 	.modes = &ampire_am_480272h3tmqw_t01h_mode,
531 	.num_modes = 1,
532 	.bpc = 8,
533 	.size = {
534 		.width = 105,
535 		.height = 67,
536 	},
537 	.bus_format = MEDIA_BUS_FMT_RGB888_1X24,
538 };

第 516~525 行,drm_display_mode 结构体就是用来设置屏幕参数。
第 529 行,定义一个 panel_desc 结构体对象。
第 530 行,modes 变量设置为 ampire_am_480272h3tmqw_t01h_mode。
第 531 行,设置 modes 的数量。
第 532 行,设置屏幕为 8bit。
第 533~536 行,设置屏幕实际显示区域的物理宽度,单位为毫米,此屏幕尺寸为 105mm x 67mm。
第 537 行,bus_format 属性设置总线模式, include/uapi/linux/media-bus-format.h 里面定义了所有可选的总线类型:

1  #define MEDIA_BUS_FMT_FIXED 0x0001
2
3  /* RGB - next is 0x101d */
4  #define MEDIA_BUS_FMT_RGB444_1X12 0x1016
5  #define MEDIA_BUS_FMT_RGB444_2X8_PADHI_BE 0x1001
6  #define MEDIA_BUS_FMT_RGB444_2X8_PADHI_LE 0x1002
7  #define MEDIA_BUS_FMT_RGB555_2X8_PADHI_BE 0x1003
8  #define MEDIA_BUS_FMT_RGB555_2X8_PADHI_LE 0x1004
9  #define MEDIA_BUS_FMT_RGB565_1X16 0x1017
10 #define MEDIA_BUS_FMT_BGR565_2X8_BE 0x1005
11 #define MEDIA_BUS_FMT_BGR565_2X8_LE 0x1006
12 #define MEDIA_BUS_FMT_RGB565_2X8_BE 0x1007
13 #define MEDIA_BUS_FMT_RGB565_2X8_LE 0x1008
14 #define MEDIA_BUS_FMT_RGB666_1X18 0x1009
15 #define MEDIA_BUS_FMT_RBG888_1X24 0x100e
16 #define MEDIA_BUS_FMT_RGB666_1X24_CPADHI 0x1015
17 #define MEDIA_BUS_FMT_RGB666_1X7X3_SPWG 0x1010
18 #define MEDIA_BUS_FMT_BGR888_1X24 0x1013
19 #define MEDIA_BUS_FMT_BGR888_3X8 0x101b
20 #define MEDIA_BUS_FMT_GBR888_1X24 0x1014
21 #define MEDIA_BUS_FMT_RGB888_1X24 0x100a
22 #define MEDIA_BUS_FMT_RGB888_2X12_BE 0x100b
23 #define MEDIA_BUS_FMT_RGB888_2X12_LE 0x100c
24 #define MEDIA_BUS_FMT_RGB888_3X8 0x101c
25 #define MEDIA_BUS_FMT_RGB888_1X7X4_SPWG 0x1011
26 #define MEDIA_BUS_FMT_RGB888_1X7X4_JEIDA 0x1012
27 #define MEDIA_BUS_FMT_ARGB8888_1X32 0x100d
......
121 /* HSV - next is 0x6002 */
122 #define MEDIA_BUS_FMT_AHSV8888_1X32 0x6001

可以看出,Linux 内核支持很多种不同的总线格式,比如 RGB、YUV、Bayer 等。以 MEDIA_BUS_FMT_RGB888_1X24 为例,这个总线格式的含义如下:
①从“RGB888”可以看出,这是一个 RGB888 格式的。
②后面的“1X24”表示一个像素点使用 24bit,如果是 2X8 就表示一个像素点使用 2 个 8bit 表示。
③有的右边还会有“BE”或“LE”,BE 表示最高位先传输,LE 表示最低位先传输。假设设备树里有个设备节点的 compatible 属性为“ampire,am-480272h3tmqw-t01h”,那么就会和驱动匹配成功,然后运行 panel_simple_platform_probe 函数,此函数的内容如下所示:

3470 static int panel_simple_platform_probe(struct platform_device *pdev)
3471 {
3472 const struct of_device_id *id;
3473
3474 id = of_match_node(platform_of_match, pdev->dev.of_node);
3475 if (!id)
3476 return -ENODEV;
3477
3478 return panel_simple_probe(&pdev->dev, id->data);
3479 }

第 3474 行,使用 of_match_node 函数查找匹配的设备 ID。
第 3478 行,当得到匹配的设备 ID(of_device_id)以后就可以通过提取 data 成员变量得到屏幕参数信息,比如此处 id->data 就是 ampire_am_480272h3tmqw_t01h。最后调用 panel_simple_probe 函数将其注册到内核,panel_simple_probe 函数也定义在 drivers/gpu/drm/panel/panel-simple.c 文件中,函数内容如下所示:

414 static int panel_simple_probe(struct device *dev, const struct panel_desc *desc)
415 {
416 	struct device_node *backlight, *ddc;
417 	struct panel_simple *panel;
418 	struct display_timing dt;
419 	int err;
420
421 	panel = devm_kzalloc(dev, sizeof(*panel), GFP_KERNEL);
422 	if (!panel)
423 		return -ENOMEM;
424
425 	panel->enabled = false;
426 	panel->prepared = false;
427 	panel->desc = desc;
428
......
444 	backlight = of_parse_phandle(dev->of_node, "backlight", 0);
445 	if (backlight) {
446 		panel->backlight = of_find_backlight_by_node(backlight);
447 		of_node_put(backlight);
448
449 		if (!panel->backlight)
450 			return -EPROBE_DEFER;
451 	}
452
......
467 	drm_panel_init(&panel->base);
468 	panel->base.dev = dev;
469 	panel->base.funcs = &panel_simple_funcs;
470
471 	err = drm_panel_add(&panel->base);
472 	if (err < 0)
473 		goto free_ddc;
474
475 	dev_set_drvdata(dev, panel);
476
477 	return 0;
478
479 free_ddc:
480 	if (panel->ddc)
481 		put_device(&panel->ddc->dev);
482 free_backlight:
483 	if (panel->backlight)
484 		put_device(&panel->backlight->dev);
485 	return err;
486 }

第 427 行,设置屏幕参数。
第 444 行,从设备树里获取背光节点,所以我们的设备树要提供“backlight”属性。
第 467 行,用 drm_panel_init 函数初始化屏幕。
第 469 行,设置 panel_simple 的回调函数,为 DRM 驱动注册的时候提供屏的参数。
第 471 行,把屏幕注册到内核。

三、总结

LCD 屏的驱动分析结束,总结一下添加自己的屏要做哪些操作:
①在根节点下提供一个 LCD 设备树,包含背光的节点和引用 ltdc 节点。
②在 panel-simple.c 文件里的 platform_of_match 结构体里添加一组设备 ID,此设备 ID 对应所使用的屏幕,重点是屏幕参数 panel_desc 结构体。文章来源地址https://www.toymoban.com/news/detail-719193.html

到了这里,关于STM32MP157驱动开发——Linux LCD驱动(上)的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处: 如若内容造成侵权/违法违规/事实不符,请点击违法举报进行投诉反馈,一经查实,立即删除!

领支付宝红包 赞助服务器费用

相关文章

  • STM32MP157驱动开发——LED驱动(原始架构)

    LED 的驱动方式,常见的有四种。 ① 使用引脚输出 3.3V 点亮 LED,输出 0V 熄灭 LED。 ② 使用引脚拉低到 0V 点亮 LED,输出 3.3V 熄灭 LED。 ③ 使用引脚输出 1.2V 点亮 LED,输出 0V 熄灭 LED。 ④ 使用引脚输出 0V 点亮 LED,输出 1.2V 熄灭 LED。 有的芯片为了省电等原因,其引脚驱动能力

    2024年02月03日
    浏览(38)
  • STM32MP157驱动开发——按键驱动(休眠与唤醒)

    当应用程序必须等待某个事件发生,比如必须等待按键被按下时,可以使用“休眠-唤醒”机制: ① APP 调用 read 等函数试图读取数据,比如读取按键; ② APP 进入内核态,也就是调用驱动中的对应函数,发现有数据则复制到用户空间并马上返回; ③ 如果 APP 在内核态,也就

    2024年02月16日
    浏览(41)
  • STM32MP157驱动开发——按键驱动(POLL 机制)

    使用休眠-唤醒的方式等待某个事件发生时,有一个缺点:等待的时间可能很久。我们可以加上一个超时时间,这时就可以使用 poll 机制。 ① APP 不知道驱动程序中是否有数据,可以先调用 poll 函数查询一下,poll 函数可以传入超时时间; ② APP 进入内核态, 调用到驱动程序的

    2024年02月15日
    浏览(37)
  • STM32MP157驱动开发——USB设备驱动

    参考文章:【正点原子】I.MX6U嵌入式Linux驱动开发——Linux USB驱动   由于 USB 协议太过庞大和复杂,所以本节只对 STM32MP157 自带的 USB 驱动进行使能和测试。详细的 USB 接口和协议的介绍,可以参考原子哥的资料《USB2.0 协议中文版.pdf》和《USB3.0 协议中文版.pdf》。   USB 全

    2023年04月14日
    浏览(53)
  • STM32MP157驱动开发——按键驱动(线程化处理)

    工作队列是在内核的线程的上下文中执行的 工作队列中有多个 work,前一个 work 没处理完会影响后面的 work。解决方法有如下2种: 比如自己创建一个内核线程,不跟别的 work 在一块。例如存储设备比如 SD/TF采用的就是单独一个线程。 使用线程化的中断处理。中断的处理仍然

    2024年02月16日
    浏览(43)
  • STM32MP157驱动开发——按键驱动(定时器)

    定时器涉及函数参考内核源码:includelinuxtimer.h 给定时器的各个参数赋值: 设置定时器 :主要是初始化 timer_list 结构体,设置其中的函数、参数。 a) 向内核添加定时器。timer-expires 表示超时时间。 b) 当超时时间到达,内核就会调用这个函数:timer-function(timer-data)。 修改定时

    2024年02月15日
    浏览(53)
  • 驱动开发,stm32mp157a开发板的led灯控制实验

            编写LED灯的驱动,在应用程序中编写控制LED灯亮灭的代码逻辑实现LED灯功能的控制; LED1-PE10 LED1亮灭: RCC寄存器[4]-1 0X50000A28 GPIOE_MODER[21:20]-01 (输出) 0X50006000 GPIOE_ODR[10]-1(输出高电平) 0(输出低电平)0X50006014 LED2-PF10 LED2亮灭: RCC寄存器[5]-1 0X50000A28 GPIOE_MODER[21:20]

    2024年02月09日
    浏览(44)
  • STM32 Linux开发板丨STM32MP157开发板资料手册+实战教程+视频教程

    iTOP-STM32MP157开发板是基于意法半导体STARM双Cortex-A7核加单Cortex-M4核的一款多核异构处理器。Cortex-A7内核提供对开源操作系统Linux的支持,借助Linux系统庞大而丰富的软件组件处理复杂应用。M4内核上运行对于实时性要求严格的应用。 开发板既有A7核,又有M4核,从学习者角度来看

    2024年02月12日
    浏览(39)
  • STM32MP157开发板Linux+Qt项目实战:智慧家庭

    stm32mp157开发板FS-MP1A是华清远见自主研发的一款高品质、高性价比的Linux+单片机二合一的嵌入式教学级开发板。开发板搭载ST的STM32MP157高性能微处理器,集成2个Cortex-A7核和1个Cortex-M4 核,A7核上可以跑Linux操作系统,M4核上可以跑FreeRTOS、RT-Thread等实时操作系统。开发板搭配仿真

    2024年02月09日
    浏览(51)
  • LED驱动(原始架构)——STM32MP157

    LED 的驱动方式,常见的有四种。 ① 使用引脚输出 3.3V 点亮 LED,输出 0V 熄灭 LED。 ② 使用引脚拉低到 0V 点亮 LED,输出 3.3V 熄灭 LED。 ③ 使用引脚输出 1.2V 点亮 LED,输出 0V 熄灭 LED。 ④ 使用引脚输出 0V 点亮 LED,输出 1.2V 熄灭 LED。 有的芯片为了省电等原因,其引脚驱动能力

    2024年02月16日
    浏览(43)

觉得文章有用就打赏一下文章作者

支付宝扫一扫打赏

博客赞助

微信扫一扫打赏

请作者喝杯咖啡吧~博客赞助

支付宝扫一扫领取红包,优惠每天领

二维码1

领取红包

二维码2

领红包