1)实验平台:正点原子STM32MP157开发板
2)购买链接:https://item.taobao.com/item.htm?&id=629270721801
3)全套实验源码+手册+视频下载地址:http://www.openedv.com/thread-318813-1-1.html
4)正点原子官方B站:https://space.bilibili.com/394620890
5)正点原子STM32MP157技术交流群:691905614
在这里插入图片描述

第三十九章 LCD驱动实验

LCD是很常用的一个外,通过LCD可以显示绚丽的图片、界面等,提交人机交互的效率。STM32MP1提供了一个LTDC接口用于连接RGB接口的液晶屏。本章我们就来学校一下如何在Linux下驱动LCD屏。

39.1 LCD和LTDC简介
39.1.1 LCD简介
LCD全称是Liquid Crystal Display,也就是液晶显示器,是现在最常用到的显示器。手机、电脑、各种人机交互设备等基本都用到了LCD,最常见就是手机和电脑显示器了。由于笔者不是LCD从业人员,对于LCD的具体原理不了解,网上对于LCD的原理解释如下:
LCD 的构造是在两片平行的玻璃基板当中放置液晶盒,下基板玻璃上设置TFT(薄膜晶体管),上基板玻璃上设置彩色滤光片,通过TFT上的信号与电压改变来控制液晶分子的转动方向,从而达到控制每个像素点偏振光出射与否而达到显示目的。
我们现在要在STM32MP1开发板上使用LCD,所以不需要去研究LCD的具体实现原理,我们只需要从使用的角度去关注LCD的几个重要点:
1、分辨率
提起LCD显示器,我们都会听到720P、1080P、2K或4K这样的字眼,这个就是LCD显示器分辨率。LCD显示器都是由一个个的像素点组成,像素点就类似一个灯(在OLED显示器中,像素点就是一个小灯),这个小灯是RGB灯,也就是由R(红色)、G(绿色)和B(蓝色)这三种颜色组成的,而RGB就是光的三原色。1080P的意思就是LCD屏幕上的像素数量是1920*1080个,也就是这个屏幕一列1080个像素点,一共1920列,如图39.1.1.1所示:
C

图39.1.1.1 LCD像素点排布
图39.1.1.1就是1080P 显示器的像素示意图,X轴就是LCD显示器的横轴,Y轴就是显示器的竖轴。图中的小方块就是像素点,一共有19201080=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个字节的内存,这四个字节每个位的分配如图39.1.1.2所示:
在这里插入图片描述

图39.1.1.2 ARGB8888数据格式
在图39.1.1.2中,一个像素点是4个字节,其中bit31bit24是Alpha通道,bit23bit16是RED通道,bit15bit14是GREEN通道,bit7bit0是BLUE通道。所以红色对应的值就是0X00FF0000,蓝色对应的值就是0X000000FF,绿色对应的值为0X0000FF00。通过调节R、G、B的比例可以产生其它的颜色,比如0X00FFFF00就是黄色,0X00000000就是黑色,0X00FFFFFF就是白色。大家可以打开电脑的“画图”工具,在里面使用调色板即可获取到想要的颜色对应的数值,如图39.1.1.3所示:
在这里插入图片描述

图39.1.1.3 颜色选取
3、LCD屏幕接口
LCD屏幕或者说显示器有很多种接口,比如在显示器上常见的VGA、HDMI、DP等等,但是STM32MP1开发板不支持这些接口。STM32MP1支持RGB接口的LCD,RGBLCD接口的信号线如表39.1.1.1所示:
在这里插入图片描述

表39.1.1.1 RGB数据线
表39.1.1.1就是RGBLCD的信号线,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也可以正常工作。
ALIENTEK一共有四款RGB LCD屏幕,型号分别为:ATK-4342(4.3寸,480272)、ATK-4842(4.3寸,800480)、ATK-7084(7寸,800480)和ATK-7016(7寸,1024600),本教程就以ATK-7016这款屏幕为例讲解,ATK-7016的屏幕接口原理图如图39.1.1.4所示:
在这里插入图片描述

图39.1.1.4 RGB LCD液晶屏屏幕接口
图中J1就是对外接口,是一个40PIN的FPC座(0.5mm间距),通过FPC线,可以连接到STM32MP1开发板上面。该接口十分完善,采用RGB888格式,并支持DE&HV模式,还支持触摸屏和背光控制。右侧的几个电阻,并不是都焊接的,用户可以根据自己时间需要而选择是否焊接(正点原子出厂屏幕不能做修改!)。默认情况,R1和R6焊接,设置LCD_LR和LCD_UD,控制LCD的扫描方向,是从左到右,从上到下(横屏看)。而LCD_R7/G7/B7则用来设置LCD的ID,由于RGBLCD没有读写寄存器,也就没有所谓的ID,这里我们通过在模块上面,控制R7/G7/B7的上/下拉,来自定义LCD模块的ID,帮助SOC判断当前LCD面板的分辨率和相关参数,以提高程序兼容性。这几个位的设置关系如表39.1.1.2所示:
在这里插入图片描述

表39.1.1.2 ALIENTEK RGBLCD模块ID对应关系
ATK-7016模块,就设置M2:M0=010即可。这样,我们在程序里面,读取LCD_R7/G7/B7,得到M0:M2的值,从而判断RGBLCD模块的型号,并执行不同的配置,即可实现不同LCD模块的兼容。
4、LCD时间参数
如果将LCD显示一帧图像的过程想象成绘画,那么在显示的过程中就是用一根“笔”在不同的像素点画上不同的颜色。这根笔按照从左至右、从上到下的顺序扫描每个像素点,并且在像素画上对应的颜色,当画到最后一个像素点的时候一幅图像就绘制好了。假如一个LCD的分辨率为1024*600,那么其扫描如图39.1.1.5所示:
在这里插入图片描述

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

图39.1.1.6 行显示时序
图39.1.1.6就是RGB LCD的行显示时序,我们来分析一下其中重要几个的参数:
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的帧显示时序如图39.1.1.7所示:
在这里插入图片描述

图39.1.1.7 帧显示时序图
图39.1.1.7就是RGB LCD的帧显示时序,我们来分析一下其中重要的几个参数:
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屏幕的参数如表39.1.1.3所示:
在这里插入图片描述

表39.1.1.3 RGB LCD屏幕时间参数
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屏幕的显存,我们如果要在屏幕上显示什么图像的话直接操作这部分显存即可。
39.1.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。
图39.1.2.1为LTDC功能框架图:

在这里插入图片描述

图39.1.2.1 LTDC功能框架图
从图可以看出LTDC的信号可以分为两类:4个控制信号(LCD_CLK像素时钟、LCD_HSYNC水平同步、LCD_VSYNC垂直同步、LCD_DE数据有效)和3个RGB数据信号(8bit x 3)。
39.2 DRM驱动框架
39.2.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。1ibdrm对底层接口(DRM driver提供的ioctl)进行封装,向上层提供统一的API接口。DRM driver包含了GEM模块和KMS模块,这两模块也分为好几个小模块。
在这里插入图片描述

图39.1.2.1 DRM驱动框架
先介绍图39.1.2.1中的部分名词意思和作用:
图形执行管理器(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。
接着我们看下DRM driver里的两大模块GEM和KMS是如何连接显示器的,如下图所示:
在这里插入图片描述

图39.1.2.2 DRM driver模块关系图
在图39.1.2.2里,蓝色框表示KMS里的模块。plane是连接crtc和framebuffer的纽带,而encoder是连接crtc和connector的纽带。GEM是负责和物理的buffer打交道不是framebuffer。plane把获取到显存输出到crtc里,crtc通过connector接口输出到显示器。
39.2.2 ST官方的DRM驱动框架介绍
在Linux系统中,DRM驱动的核心主要就一个drm_driver结构体,驱动程序要初始化drm_driver结构体,然后调用drm_dev_init函数,将其注册到DRM core。
我们简单分析ST官方编写的在Linux下的 DRM驱动,打开stm32mp151.dtsi,然后找到ltdc节点内容,如下所示:

示例代码39.2.2.1 ltdc节点
1652    ltdc: display-controller@5a001000 {
1653        compatible = "st,stm32-ltdc";
1654        reg = <0x5a001000 0x400>;
1655        interrupts = <GIC_SPI 88 IRQ_TYPE_LEVEL_HIGH>,
1656                 <GIC_SPI 89 IRQ_TYPE_LEVEL_HIGH>;
1657        clocks = <&rcc LTDC_PX>;
1658        clock-names = "lcd";
1659        resets = <&rcc LTDC_R>;
1660        status = "disabled";
1661    };
示例代码39.2.2.1中的ltdc节点信息是所有使用STM32MP1芯片的板子所共有的,并不是完整的ltdc节点信息。可以看出ltdc节点的compatible属性值为“st,stm32-ltdc”,因此在Linux源码中搜索这个字符串就可以找到STM32MP1的DRM驱动文件,这个文件为“drivers/gpu/drm/stm/drv.c”文件,打开此文件找的如下内容所示:
示例代码39.2.2.2 platform下的DRM驱动
236 static const struct of_device_id drv_dt_ids[] = {
237     { .compatible = "st,stm32-ltdc"},
238     { /* end node */ },
239 };
240 MODULE_DEVICE_TABLE(of, drv_dt_ids);
241
242 static struct platform_driver stm_drm_platform_driver = {
243     .probe = stm_drm_platform_probe,
244     .remove = stm_drm_platform_remove,
245     .driver = {
246         .name = "stm32-display",
247         .of_match_table = drv_dt_ids,
248         .pm = &drv_pm_ops,
249     },
250 };

从示例代码39.2.2.2可以看出,这是一个标准的platform驱动,当驱动和设备匹配以后stm_drm_platform_probe函数就会执行。
和其他设备驱动一样,DRM也分为DRM设备和DRM驱动,drm_device结构体为DRM设备,drm_driver为DRM驱动,我们依次来看一下这两个结构体。
1、drm_device结构体
drm_device结构体定义在include/drm/drm_device.h文件里,内容如下(有省略):

示例代码39.2.2.3 drm_device结构体
1  struct drm_device {
2   	struct list_head legacy_dev_list;
3   	int if_version;
4   	struct kref ref;
......
30  	u32 max_vblank_count;
31  	struct list_head vblank_event_list;
32  	spinlock_t event_lock;
33  	struct drm_agp_head *agp;
34  	struct pci_dev *pdev;
35  	unsigned int num_crtcs;
36  	struct drm_mode_config mode_config;
37  	struct mutex object_name_lock;
38  	struct idr object_name_idr;
39  	struct drm_vma_offset_manager *vma_offset_manager;
40  	struct drm_vram_mm *vram_mm;
41  	enum switch_power_state switch_power_state;
42  	struct drm_fb_helper *fb_helper;
43 };
我们在编写DRM驱动的时候需要自行申请drm_device内存并且使用初始化,这个可以直接通过drm_dev_alloc函数来完成,函数原型如下:

struct drm_device *drm_dev_alloc(struct drm_driver *driver, struct device *parent)
此函数会先调用kzalloc为drm_device分配内存,然后调用drm_dev_init初始化drm_device,函数参数和返回值含义如下:
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,成功;负数,失败。
2、drm_driver结构体
Linux内核为DRM驱动提供一个叫做drm_driver的结构体,drm_driver结构体包含了DRM驱动的完整属性和操作集合,因此每一个DRM驱动都必须有一个drm_driver。drm_driver结构体定义在include/drm/drm_drv.h文件里,内容如下(有省略):

示例代码39.2.2.4 drm_driver结构体
1  struct drm_driver {
2   int (*load) (struct drm_device *, unsigned long flags);
3   int (*open) (struct drm_device *, struct drm_file *);
......
56 
57  int (*dumb_create)(struct drm_file *file_priv,
58             struct drm_device *dev,
59             struct drm_mode_create_dumb *args);
60  int (*dumb_map_offset)(struct drm_file *file_priv,
61                 struct drm_device *dev, uint32_t handle,
62                 uint64_t *offset);
63  int (*dumb_destroy)(struct drm_file *file_priv,
64              struct drm_device *dev,
65              uint32_t handle);
66  const struct vm_operations_struct *gem_vm_ops;
67 
68  int major;     	 	/* 驱动主设备号	*/
69  int minor;      		/* 驱动次设备号 	*/
70  int patchlevel; 		/* 驱动补丁等级 	*/
71  char *name;     		/* 驱动名字 		*/
72  char *desc;     		/* 驱动描述 		*/
73  char *date;     		/* 驱动日期 		*/
74  u32 driver_features;	/* 驱动特性	 	*/
75  const struct drm_ioctl_desc *ioctls;
76  int num_ioctls;
77  const struct file_operations *fops;
78 
79  struct list_head legacy_dev_list;
80  int (*firstopen) (struct drm_device *);
81  void (*preclose) (struct drm_device *, struct drm_file *file_priv);
82  int (*dma_ioctl) (struct drm_device *dev, void *data, 
struct drm_file *file_priv);
83  int (*dma_quiescent) (struct drm_device *);
84  int (*context_dtor) (struct drm_device *dev, int context);
85  int dev_priv_size;
86 };
drm_driver结构体的成员变量很多,我们重点关注driver_features、fops和dumb_create。
第57行,dumb_create是一个回调函数,用于创建gem对象,并分配物理buffer。
第74行,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使用!
第77行,fops这个相信大家都很熟悉了吧,就是一个简单的字符设备接口结构体。
前面我们说了,STM32MP1的LTDC是个platform驱动,当设备和驱动匹配成功以后stm_drm_platform_probe函数就会执行,函数内容如下:

示例代码39.2.2.5 stm_drm_platform_probe函数
191 static int stm_drm_platform_probe(struct platform_device *pdev)
192 {
193     struct device *dev = &pdev->dev;
194     struct drm_device *ddev;
195     int ret;
196
197     DRM_DEBUG("%s\n", __func__);
198
199     dma_set_coherent_mask(dev, DMA_BIT_MASK(32));
200
201     ddev = drm_dev_alloc(&drv_driver, dev);
202     if (IS_ERR(ddev))
203         return PTR_ERR(ddev);
204
205     ret = drv_load(ddev);
206     if (ret)
207         goto err_put;
208
209     ret = drm_dev_register(ddev, 0);
210     if (ret)
211         goto err_put;
212
213     drm_fbdev_generic_setup(ddev, 16);
214
215     return 0;
216
217 err_put:
218     drm_dev_put(ddev);
219
220     return ret;
221 }
第201行,调用drm_dev_alloc函数,此函数主要完成三个功能:
①、给drm_device分配内存。
②、通过drm_dev_init函数初始化drm_device。
drm_dev_alloc会通过调用drm_dev_init函数将drm_driver和drm_device联系起来,drm_device结构体里面有个drvier指针成员变量,此成员变量指向DRM设备对应的DRM驱动的。因此,drm_dev_init函数会通过将drm_device下的driver成员变量指向drm_driver来实现两者相连。
第205 行,drv_load这个函数就是初始化KMS,放到后面分析。
第209 行,注册drm_device对象进DRM core。
接下来看下,drv_load函数,内容如下:
示例代码39.2.2.6 drv_load函数
29  #define STM_MAX_FB_WIDTH    2048
30  #define STM_MAX_FB_HEIGHT   2048
31 
32  static const struct drm_mode_config_funcs drv_mode_config_funcs = {
33      .fb_create = drm_gem_fb_create,
34      .atomic_check = drm_atomic_helper_check,
35      .atomic_commit = drm_atomic_helper_commit,
36  };
......
79  static int drv_load(struct drm_device *ddev)
80  {
81      struct platform_device *pdev = to_platform_device(ddev->dev);
82      struct ltdc_device *ldev;
83      int ret;
84 
85      DRM_DEBUG("%s\n", __func__);
86 
87      ldev = devm_kzalloc(ddev->dev, sizeof(*ldev), GFP_KERNEL);
88      if (!ldev)
89          return -ENOMEM;
90 
91      ddev->dev_private = (void *)ldev;
92 
93      drm_mode_config_init(ddev);
94 
95      /*
96       * set max width and height as default value.
97       * this value would be used to check framebuffer size limitation
98       * at drm_mode_addfb().
99       */
100     ddev->mode_config.min_width = 0;
101     ddev->mode_config.min_height = 0;
102     ddev->mode_config.max_width = STM_MAX_FB_WIDTH;
103     ddev->mode_config.max_height = STM_MAX_FB_HEIGHT;
104     ddev->mode_config.funcs = &drv_mode_config_funcs;
105
106     ret = ltdc_load(ddev);
107     if (ret)
108         goto err;
109
110     drm_mode_config_reset(ddev);
111     drm_kms_helper_poll_init(ddev);
112
113     platform_set_drvdata(pdev, ddev);
114
115     return 0;
116 err:
117     drm_mode_config_cleanup(ddev);
118     return ret;
119 }

第102行,设置DRM驱动X轴(宽度)最大支持2048个像素。
第103行,设置DRM驱动Y轴(宽度)最大支持2048个像素,结合上一行,可以看出驱动里面设置的最大分辨率支持20482048。但是根据STM32MP157手册所描述,最大支持1366768分辨率的屏幕。
第104行,设置framebuffer的回调函数结构体。
第106行,这里我们要引入drm_panel结构体,此结构体作用是提供一堆控制回调函数。比如屏幕参数回调函数,背光控制函数等等。ltdc_load函数是负责初始化ltdc接口(同时connector和encoder一起初始化)。在connector初始化的时候,就会调用drm_panel结构体里的获取屏幕参数函数(所以我们只需要提供一个屏的驱动就能正常显示了)。通常encoder和connector是放在同一个驱动初始化的,目的是为了方便驱动程序设计。
重点来了!要完成整个DRM驱动的正常初始化,前面的GEM和KMS这些模块ST官方已经提供给我们了,只需提供一个drm_panel对象就行了。打开“include/drm/drm_panel.h”文件,找到如下内容所示:

示例代码39.2.2.7 drm_panel结构体
1   struct drm_panel {
2       struct drm_device *drm; 					/* drm_device 对象 	*/ 
3       struct drm_connector *connector; 		/* connector 对象 		*/ 
4       struct device *dev; 						/* 设备节点 dev 		*/
5       const struct drm_panel_funcs *funcs; 	/* 回调函数的结构体 		*/ 
6       struct list_head list;  
7   };

这个结构体是比较简单的相信各位能够看懂。主要是第5行,funcs对象成员。在这里我们的DRM驱动分析基本完成了。
39.3 RGB LCD驱动分析(屏的驱动)
上一小节说了,drm_panel就是DRM驱动的核心结构体,我们需要实现此结构体。我们先来看一下panel_simple结构体。这里用到了面向对象的思维,drm_panel结构体是基类,panel_simple在drm_panel基础上增加了一些成员变量,相当于继承类。
我们使用的是RGB LCD屏,所以我们的LCD驱动文件为drivers/gpu/drm/panel/panel-simple.c,打文件找到如下内容所示:

示例代码39.3.1 panel_simple结构体
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行,屏的背光结构体。
接着我们看下如何跟设备树匹配的,找到如下所示内容:
示例代码39.3.2 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    };
示例代码39.3.1中,这是一个标准的platform驱动框架,第3494行就是匹配表。platform_of_match内容如下所示(有省略):
示例代码39.3.3 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        }
3142    };
可以看出,platform_of_match里面有大量的匹配项,分别针对不同的屏幕,比如:

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

示例代码39.3.4 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×67mm。
第537行, bus_format属性设置总线模式,include/uapi/linux/media-bus-format.h里面定义了所有可选的总线类型:

示例代码39.3.5 总线配置项
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
......
120 
121 /* HSV - next is    0x6002 */
122 #define MEDIA_BUS_FMT_AHSV8888_1X32     		0x6001
可以看出,Linux内核支持很多种不同的总线格式,比如RGB、YUV、Bayer等。我们以MEDIA_BUS_FMT_RGB888_1X24为例,看看这个总线格式的含义:
①、从“RGB88”可以看出,这是一个RGB888格式的。
②、后面的“1X24”表示一个像素点使用24bit,如果是2X8就表示一个像素点使用2个8bit表示。
③、有的右面也会有“BE”或“LE”,BE表示最高位先传输,LE表示最低位先传输。

假设设备树里有个设备节点的compatible 属性为“ampire,am-480272h3tmqw-t01h”,那么就会和驱动匹配成功,然后运行panel_simple_platform_probe函数,此函数的内容如下所示:

示例代码39.3.6 LCD驱动的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文件中,函数内容如下所示:
示例代码39.3.7 panel_simple_probe函数
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
486     return err;
487 }
第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结构体。
39.4 硬件原理图分析
在STM32MP1开发板里,有一个RGB LCD接口,其原理如下所示:
在这里插入图片描述

图39.4.1 RGB LCD接口硬件原理图
图39.4.1左边对应的是LCD接口引脚图,右边为STM32MP1开发板RGB LCD接口的原理图。
39.5 LCD驱动程序编写
39.5.1 修改设备树
首先就是修改设备树,重点要注意三个地方:
①、LCD所使用的IO配置,由于STM32MP1的IO支持复用,所以大家实际所使用的LCD引脚可能不一样。因此首先要根据实际硬件设计,修改LCD所有使用的IO配置。
②、LDTC接口节点修改,修改相应的属性值,告诉内核要指定那个输出接口,比如输出到RGB LCD屏、MIPI屏等。本实验用到是正点原子ATK7016屏,也就是RGB LCD屏。
③、输出接口节点的编写,比如本实验用到的RGB LCD屏需要在根节点下添加RGB LCD节点。
④、LCD背光节点信息修改,要根据实际所使用的背光IO来修改相应的设备节点信息。接下来我们依次来看一下上面这三个节点如何去修改。
1、LCD屏幕使用的IO配置
首先要检查一下设备树中LCD所使用的IO配置,这个其实ST都已经给我们写好了,需要修改,不过我们还是要看一下。打开arch/arm/boot/dts/stm32mp15-pinctrl.dtsi文件,在pinctrl节点中找到如下内容:

示例代码39.5.1.1 ltdc的pinmux节点
1   ltdc_pins_b: ltdc-b-0 {
2       pins {
3           pinmux = <STM32_PINMUX('I', 14, AF14)>, 	/* LCD_CLK 	*/
4                <STM32_PINMUX('I', 12, AF14)>, 	/* LCD_HSYNC 	*/
5                <STM32_PINMUX('I', 13, AF14)>, 	/* LCD_VSYNC 	*/
6                <STM32_PINMUX('K',  7, AF14)>, 	/* LCD_DE 		*/
7                <STM32_PINMUX('I', 15, AF14)>, 	/* LCD_R0 		*/
8                <STM32_PINMUX('J',  0, AF14)>, 	/* LCD_R1 		*/
9                <STM32_PINMUX('J',  1, AF14)>, 	/* LCD_R2 		*/
10               <STM32_PINMUX('J',  2, AF14)>, 	/* LCD_R3 		*/
11               <STM32_PINMUX('J',  3, AF14)>, 	/* LCD_R4 		*/
12               <STM32_PINMUX('J',  4, AF14)>,	/* LCD_R5 		*/
13               <STM32_PINMUX('J',  5, AF14)>, 	/* LCD_R6 		*/
14               <STM32_PINMUX('J',  6, AF14)>, 	/* LCD_R7 		*/
15               <STM32_PINMUX('J',  7, AF14)>, 	/* LCD_G0 		*/
16               <STM32_PINMUX('J',  8, AF14)>, 	/* LCD_G1 		*/
17               <STM32_PINMUX('J',  9, AF14)>, 	/* LCD_G2 		*/
18               <STM32_PINMUX('J', 10, AF14)>,	/* LCD_G3 		*/
19               <STM32_PINMUX('J', 11, AF14)>,	/* LCD_G4 		*/
20               <STM32_PINMUX('K',  0, AF14)>, 	/* LCD_G5 		*/
21               <STM32_PINMUX('K',  1, AF14)>, 	/* LCD_G6 		*/
22               <STM32_PINMUX('K',  2, AF14)>, 	/* LCD_G7 		*/
23               <STM32_PINMUX('J', 12, AF14)>, /* LCD_B0 		*/
24               <STM32_PINMUX('J', 13, AF14)>, /* LCD_B1 		*/
25               <STM32_PINMUX('J', 14, AF14)>, /* LCD_B2 		*/
26               <STM32_PINMUX('J', 15, AF14)>, /* LCD_B3 		*/
27               <STM32_PINMUX('K',  3, AF14)>, 	/* LCD_B4 		*/
28               <STM32_PINMUX('K',  4, AF14)>, 	/* LCD_B5 		*/
29               <STM32_PINMUX('K',  5, AF14)>, 	/* LCD_B6 		*/
30               <STM32_PINMUX('K',  6, AF14)>; 	/* LCD_B7 		*/
31          bias-disable;
32          drive-push-pull;
33          slew-rate = <1>;
34      };
35  };
36
37  ltdc_pins_sleep_b: ltdc-b-1 {
38      pins {
39          pinmux = <STM32_PINMUX('I', 14, ANALOG)>, /* LCD_CLK */
40               <STM32_PINMUX('I', 12, ANALOG)>, 	/* LCD_HSYNC */
41               <STM32_PINMUX('I', 13, ANALOG)>, 	/* LCD_VSYNC */
42               <STM32_PINMUX('K',  7, ANALOG)>, 	/* LCD_DE 	*/
43               <STM32_PINMUX('I', 15, ANALOG)>, 	/* LCD_R0 	*/
44               <STM32_PINMUX('J',  0, ANALOG)>, 	/* LCD_R1 	*/
45               <STM32_PINMUX('J',  1, ANALOG)>, 	/* LCD_R2 	*/
46               <STM32_PINMUX('J',  2, ANALOG)>, 	/* LCD_R3 	*/
47               <STM32_PINMUX('J',  3, ANALOG)>, 	/* LCD_R4 	*/
48               <STM32_PINMUX('J',  4, ANALOG)>, 	/* LCD_R5 	*/
49               <STM32_PINMUX('J',  5, ANALOG)>, 	/* LCD_R6 	*/
50               <STM32_PINMUX('J',  6, ANALOG)>, 	/* LCD_R7 	*/
51               <STM32_PINMUX('J',  7, ANALOG)>, 	/* LCD_G0 	*/
52               <STM32_PINMUX('J',  8, ANALOG)>, 	/* LCD_G1 	*/
53               <STM32_PINMUX('J',  9, ANALOG)>, 	/* LCD_G2 	*/
54               <STM32_PINMUX('J', 10, ANALOG)>, 	/* LCD_G3 	*/
55               <STM32_PINMUX('J', 11, ANALOG)>, 	/* LCD_G4 	*/
56               <STM32_PINMUX('K',  0, ANALOG)>, 	/* LCD_G5 	*/
57               <STM32_PINMUX('K',  1, ANALOG)>, 	/* LCD_G6 	*/
58               <STM32_PINMUX('K',  2, ANALOG)>, 	/* LCD_G7 	*/
59               <STM32_PINMUX('J', 12, ANALOG)>, 	/* LCD_B0 	*/
60               <STM32_PINMUX('J', 13, ANALOG)>, 	/* LCD_B1 	*/
61               <STM32_PINMUX('J', 14, ANALOG)>, 	/* LCD_B2 	*/
62               <STM32_PINMUX('J', 15, ANALOG)>, 	/* LCD_B3 	*/
63               <STM32_PINMUX('K',  3, ANALOG)>, 	/* LCD_B4 	*/
64               <STM32_PINMUX('K',  4, ANALOG)>, 	/* LCD_B5 	*/
65               <STM32_PINMUX('K',  5, ANALOG)>, 	/* LCD_B6 	*/
66               <STM32_PINMUX('K',  6, ANALOG)>; 	/* LCD_B7 	*/
67      };
68  };
第1~35行,子节点ltdc_pins_b,为RGB LCD的24根数据线配置项和4根控制线配置项。
第37~67行,子节点ltdc_pins_sleep_b,同样也是RGB LCD的24根数据线配置项和4根控制线配置项。
可以看出,这里有两个pinmux分别为:ltdc_pins_b和ltdc_pins_sleep_b,其中ltdc_pins_b是在默认模式下的RGB LCD pinmux配置,ltdc_pins_sleep_b是在sleep模式下RGB LCD的pinmux配置。如果我们想要LCD屏进入睡眠模式就切换为sleep模式。
正点原子STM32MP1开发板RGB LCD屏幕所使用的引脚和ST官方开发板一致,因此示例代码39.5.1不需要做任何修改。如果你所使用的开发板其LCD引脚和正点原子STM32MP1开发板不一致,一定要根据自己的实际硬件修改示例代码39.5.1.1。

2、LDTC接口节点修改
LTDC节点在stm32mp151.dtsi里已经写好一部分了,我们只需要告诉LTDC节点输出到RGB LCD 屏里就行。在stm32mp157d-atk.dts文件,添加如下内容所示:

示例代码39.5.5.2 ltdc节点
1   &ltdc {
2       pinctrl-names = "default", "sleep";
3       pinctrl-0 = <&ltdc_pins_b>;
4       pinctrl-1 = <&ltdc_pins_sleep_b>;
5       status = "okay";
6 
7       port {
8           #address-cells = <1>;
9           #size-cells = <0>;
10
11          ltdc_ep0_out: endpoint@0 {
12              reg = <0>;
13              remote-endpoint = <&rgb_panel_in>;
14          };
15      };
16  };
第2~4行,给LTDC设置了两个pinmux模式,pinctrl-0为default模式,pinctrl-1为sleep模式,系统默认使用default模式。
第8~9行,设置port下子节点的reg属性的地址信息描述。
第11~14行,在port下添加了一个子节点为ltdc_ep0_out。在第12行里,reg属性值为0。在第13行里,remote-endpoint属性是用来告诉ltdc节点输出到那里,我们是用RGB LCD屏做实验,所以输出到rgb_panel_in接口。

3、输出接口的编写
我们还需要添加一个LCD设备树节点,在stm32mp157d-atk.dts文件的根节点“/”下添加如下所示内容:

示例代码39.5.5.3 LCD屏的设备节点
 1    panel_rgb: panel-rgb {
 2        compatible = "alientek,lcd-rgb";
 3        backlight = <&backlight>;
 4        status = "okay";
 5 
 6        port {
 7            rgb_panel_in: endpoint {
 8                remote-endpoint = <&ltdc_ep0_out>;
 9            };
 10       };
 11   };
第2行,设置compatible属性值为“alientek,lcd-rgb”,所以我们稍后要在panel-simple.c文件里的platform_of_match数组增加一个of_device_id结构体,此结构体的compatible成员属性值为“alientek,lcd-rgb”。
第3行,设置backlight属性值为“&backlight”,此属性值为引用背光节点,稍后给大家讲解如何编写。
第6~9行,告诉LCD驱动,要从LTDC节点里获取显示数据。第8行就是引用ltdc节点。

39.5.2 在panel-simple.c文件里面添加屏幕参数
接着我们就要在panel-simple.c文件里面添加屏幕参数,打开此文件,找到platform_of_match数组,添加如下内容:
示例代码39.5.2.1 屏的匹配属性
1 .compatible = “alientek,lcd-rgb”,
2 .data = &alientek_desc,
添加完成以后如图39.5.2.1所示:
在这里插入图片描述

图39.5.2.1 新添加的LCD屏幕参数
在图39.5.2.1里的alientek_desc保存参数,我们还需要继续在panel-simple.c里面实现alientek_desc,添加如下所示代码:

示例代码39.5.2.2 alientek_desc结构体
1   static const struct drm_display_mode ATK7016_mode = {
2   	.clock = 51200,            	/* LCD像素时钟,单位KHz	*/
3      	.hdisplay = 1024,           	/* LCD X轴像素个数 		*/
4      	.hsync_start = 1024 + 140, 	/* LCD X轴+hbp的像素个数 	*/
5      	.hsync_end = 1024 + 140 + 20, 	/* LCD X轴+hbp+hspw的像素个数*/
6     	.htotal = 1024 + 140 + 20 + 160,/* LCD X轴+hbp+hspw+hfp*/
7     	.vdisplay = 600,             	/* LCD Y轴像素个数 */
8      	.vsync_start = 600 + 20,   	/* LCD Y轴+vbp的像素个数 	*/
9      	.vsync_end = 600 + 20 + 3, 	/* LCD Y轴+vbp+vspw的像	*/
10    	.vtotal = 600 + 20 + 3 + 12,/* LCD Y轴+vbp+vspw+vfp 	*/
11    	.vrefresh = 60,               	/* LCD的刷新频率为60HZ 	*/
12  };
13
14  static const struct panel_desc alientek_desc = {
15    	.modes = &ATK7084_mode,
16     	.num_modes = 1,
17     	.bus_format = MEDIA_BUS_FMT_RGB888_1X24,
18  };

在示例代码39.5.2.2就是ATK7016屏的参数,并且设置为RGB888模式。如果使用的其他屏幕,请按照屏幕手册对应的时序参数设置。
第2~11行,ATK7016 屏幕时序参数,根据自己所使用的屏幕修改即可。
39.5.3 LCD屏幕背光节点信息
正点原子的LCD接口背光控制IO连接到了STM32MP1的PD13引脚上,我们需要将PD13复用为TIM4_CH2,然后配置TIM4的CH2输出PWM信号,然后通过此PWM信号来控制LCD屏幕背光的亮度,接着我们来看一下如何在设备树中添加背光节点信息。
首先是PD13这个pinmux的配置,在stm32mp15-pinctrl.dtsi中找到如下内容:

示例代码39.5.3.1 PD13的pinmux配置
1   pwm4_pins_b: pwm4-1 {
2       pins {
3           pinmux = <STM32_PINMUX('D', 13, AF2)>; /* TIM4_CH2 */
4           bias-pull-down;
5           drive-push-pull;
6           slew-rate = <0>;
7       };
8   };
9 
10  pwm4_sleep_pins_b: pwm4-sleep-1 {
11      pins {
12          pinmux = <STM32_PINMUX('D', 13, ANALOG)>; /* TIM4_CH2 */
13      };
14  };

示例代码39.5.3.1默认设置了PD13引脚的两种pinmux配置,从第3行可以看出,设置PD13复用为TIM4_CH2,TIM4_CH2这里的意思为TIM4里的第二个PWM通道,并且设置电气属性为内部下拉和推挽输出。这是因为ST官方开发板就使用了PD13作为LCD的背光控制引脚,正点原子STM32MP1开发板也使用PD13作为LCD背光控制引脚,所以不需要修改。如果你使用其他引脚作为LCD的背光控制引脚,那么就需要进行修改。
LCD背光要使用到timers4,因此也要设置timers4节点,在stm32mp151.dtsi文件中找到如下内容:

示例代码39.5.3.2 timers4节点
1   timers4: timer@40002000 {
2       #address-cells = <1>;
3       #size-cells = <0>;
4       compatible = "st,stm32-timers";
5       reg = <0x40002000 0x400>;
6       clocks = <&rcc TIM4_K>;
7       clock-names = "int";
8       dmas = <&dmamux1 29 0x400 0x80000001>,
9              <&dmamux1 30 0x400 0x80000001>,
10             <&dmamux1 31 0x400 0x80000001>,
11             <&dmamux1 32 0x400 0x80000001>;
12      dma-names = "ch1", "ch2", "ch3", "ch4";
13      status = "disabled";
14      
15      pwm {
16          compatible = "st,stm32-pwm";
17          #pwm-cells = <3>;
18          status = "disabled";
19      };
20
21      timer@3 {
22          compatible = "st,stm32h7-timer-trigger";
23          reg = <3>;
24          status = "disabled";
25      };
26
27      counter {
28          compatible = "st,stm32-timer-counter";
29          status = "disabled";
30      };
31  };
timers4节点信息大家不要修改,如果要修改timers4节点内容的话请在stm32mp157d-atk.dts文件中修改。第15~19行timers4的pwm子节点,此节点下的compatible属性值为“st,stm32-pwm”,在整个Linux源码文件中搜索此值即可找到stm32mp1的pwm驱动文件。此驱动文件为drivers/pwm/pwm-stm32.c,这里我们就不详细的去分析这个文件了。

继续在stm32mp157d-atk.dts文件中向timers4追加内容,如下所示:

示例代码39.5.3.3 向tim4节点追加内容
1   &timers4 {
2       status = "okay";
3       /* spare dmas for other usage */
4       /delete-property/dmas;
5       /delete-property/dma-names;
6       pwm4: pwm {
7           pinctrl-0 = <&pwm4_pins_b>;
8           pinctrl-1 = <&pwm4_sleep_pins_b>;
9           pinctrl-names = "default", "sleep";
10          #pwm-cells = <2>;
11          status = "okay";
12      };
13  };
第2行,把status设置为okay。
第4~5行,设置此节点不用dma。
第6行,pwm4是我们为pwm设置的一个别名。
第7~9行,设置PWM所使用的IO配置。
第10行,此参数是用来规定pwms属性的参数。比如:#pwm-cells =<2>,表示pwms属性有2个参数,如下所示:

pwms= <&pwm4 1 5000000>
其中pwm4表示使用PWM4,后面两个是参数,其中1表示使用PWM4的通道2(通道从1开始);5000000表示为5KHz。
如果背光用的其他pwm通道,比如pwm2,那么就需要仿照示例代码39.5.3.3,向timers2节点追加相应的内容。比如:如果我们要设置TIM2_CH4,那么timers2里的pwm节点下的pinmux配置就是TIM2_CH4。
到这里,PWM和相关的IO已经准备好了,但是Linux系统怎么知道TIM4_CH2就是控制LCD背光的呢?因此我们还需要一个节点来将LCD背光和TIM4_CH2连接起来。这个节点就是backlight,backlight节点描述可以参考Documentation/devicetree/bindings/leds/backlight/pwm-backlight.txt这个文档,此文档详细讲解了backlight节点该如何去创建,这里大概总结一下:
①、节点名称要为“backlight”。
②、节点的compatible属性值要为“pwm-backlight”,因此可以通过在Linux内核中搜索“pwm-backlight”来查找PWM背光控制驱动程序,这个驱动程序文件为drivers/video/backlight/pwm_bl.c,感兴趣的可以去看一下这个驱动程序。
③、pwms属性用于描述背光所使用的PWM的通道以及PWM频率,比如本章我们要使用的pwm4的第二个通道,pwm频率设置为5KHz。
④、brightness-levels属性描述亮度级别,范围为0~255,0表示PWM占空比为0%,也就是亮度最低,255表示100%占空比,也就是亮度最高。至于设置几级亮度,大家可以自行填写此属性。
⑤、default-brightness-level属性为默认亮度级别。
根据上述5点设置backlight节点,我们在根节点创建一个backlight节点,在stm32mp157d-atk.dts文件中新建内容如下:

示例代码39.5.3.4 backlight节点
1    backlight: backlight {
2        compatible = "pwm-backlight";
3        pwms = <&pwm4 1 5000000>;
4        brightness-levels = <0 4 8 16 32 64 128 255>;
5        power-supply = <&v3v3>;
6        default-brightness-level = <7>;
7        status = "okay";
8    };

第 3 行,设置背光使用 pwm4的第二个通道,PWM 频率为 5KHz。
第 4 行,设置背 8 级背光(0~7),分别为 0、4、8、16、32、64、128、255,对应占空比为0%、1.57%、3.13%、6.27%、12.55%、25.1%、50.19%、100%,如果嫌少的话可以自行添加一些其他的背光等级值。
第 5 行,设置默认背光等级为 7,也就是 100%的亮度。
注意:背光的驱动代码有点bug,如果设置0级屏不会灭屏,打开pwm_bl.c文件,找到123行,如图39.5.3.1所示:
在这里插入图片描述

图39.5.3.1 修改背光驱动bug
将图39.5.3.1中123行的‘>’改为 ‘>=’即可。
关于背光的设备树节点信息就讲到这里,整个的 LCD 设备树节点内容我们就讲完了,按照这些节点内容配置自己的开发板即可。
39.6 运行测试
39.6.1 LCD屏幕的DRM基本测试
1、编译新的内核和设备树
输入如下命令重新编译Linux内核和设备树:
make uImage dtbs LOADADDR=0xC2000040 -j16
编译完成以后一会要使用新的设备树和内核启动Linux内核。
2、配置内核
ST官方的默认配置已经使能了DRM驱动,还是要告诉各位如何配置内核,打开 Linux
内核图形化配置界面,按下路径找到对应的配置项:
Device Drivers
Graphics support
[] Direct Rendering Manager (XFree86 4.1.0 and higher DRI support) //选中
[
] DRM Support for STMicroelectronics SoC Series //选中
Display Panels
[] support for simple panels //选中
Backlight & LCD device support
[
] Generic PWM based Backlight Driver //选中
用新的uImage镜像和stm32mp157d-atk.dtb设备树用来启动内核,如果设置正确那么在文件系统/sys/class/drm/路径下,有如图39.6.1.1所示内容:
在这里插入图片描述

图39.6.1.1 屏配置成功的结果
如果没有出现图39.6.1.1的内容,就要检查一下设备树和panel-simple.c文件是否设置正确。因为Linux一切皆文件,所以DRM驱动肯定会提供一个接口给用户使用,接口为“/dev/dri/card0”。可以通过此接口来设置LCD的显示。
2、文件系统使能libdrm库
在前面39.2小节里,没有libdrm库是不能调用drm驱动的,所以我们要在文件系统使能libdrm库,跳转到buildroot-2020.02.6的目录下,打开buildroot的图形化配置界面,根据如下配置去使能libdrm库。
Location:
-> Target packages
-> Libraries
-> Graphics
->[]libdrm //选中
->[
]Install test programs //选中
如图39.6.1.2所示:
在这里插入图片描述

图39.6.1.2配置libdrm库
图39.6.1.2中的Install test programs配置会生成modetest命令,此命令是用来测试DRM驱动。保存并重新编译buildroot的文件系统,然后直接将新得到的根文件系统解压到开发板正在使用的根文件系统中,命令如下:
sudo tar -axvf rootfs.tar
3、测试
重新启动开发板,我们使用modetest命令进行测试,输入如下命令可以查看modetest命令的使用方法:
modetest --help
帮助信息如图39.6.1.3所示:
在这里插入图片描述

图39.6.1.3 modetest帮助信息

先输入如下命令查看一下设备信息:
modetest -M stm
-M:指定模块,这里我们查看“stm”这个设备。
输入命令以后就会打印出stm这个设备的详细信息,比较长,图39.6.1.3是一些重要信息:
在这里插入图片描述

图39.6.1.3 stm设备信息
从图39.6.1.3可以看出,Connectors的id为32,CRTC的id为35,这两个ID很重要,一会测试要用到,大家要根据自己的实际情况填写。
输入如下命令测试DRM驱动:
modetest -M stm -s 32@35:1024x600
命令参数介绍如下:
-M:指定stm模块。
-s:32表示connectors的ID,35表示CRTC的ID ,1024x600表示显示的模式。
显示结果如图39.6.1.4所示:
在这里插入图片描述

图39.6.1.4 DRM显示测试
39.6.2 LCD屏幕的FB基本测试
在39.2小节里我们说了,KMS包含了FB框架。DRM驱动默认为CRTC用来控制,CRTC是可以模仿FB框架,实现使用FB接口。示例代码39.2.2.5中的第213行就是负责初始化基于CRTC的FB接口。我们只需在Linux内核图形化配置界面里配置以下选项。
1、使能DRM驱动的FB
配置路径如下:

Device Drivers
-> Graphics support
-> Direct Rendering Manager (XFree86 4.1.0 and higher DRI support)
-> [*]Enable legacy fbdev support for your modesetting driver //选中
如图39.6.2.1所示:
在这里插入图片描述

图39.6.2.1 使能DRM的FB支持
2、使能PL110
-> Device Drivers
-> Graphics support
-> Frame buffer Devices
-> Support for frame buffer devices
-> <*>ARM PrimeCell PL110 support //选中,支持/dev/fb0

配置如图39.6.2.2所示:

在这里插入图片描述

图39.6.2.2 使能PL100
3、使能Linux logo显示
Linux内核启动的时候可以选择显示小企鹅logo。打开Linux内核图形化配置界面,按下路径找到对应的配置项:
-> Device Drivers
-> Graphics support
-> [] Bootup logo //选中
-> [
] Standard black and white Linux logo (NEW) //选中
-> [] Standard 16-color Linux logo (NEW) //选中
-> [
] Standard 224-color Linux logo (NEW) //选中
如图39.6.2.3所示:
在这里插入图片描述

图39.6.2.3 logo配置项
图39.6.2.3中这三个选项分别对应黑白、16位、24位色彩格式的logo,我们把这三个都选中,都编译进Linux内核里面。设置好以后保存退出,重新编译Linux内核,编译完成以后使用新编译出来uImage镜像启动系统,在LCD屏幕左上角出现两个彩色的小企鹅logo(企鹅的个数和CPU的个数相同),屏幕背景色为黑色,如图39.6.2.3所示:
在这里插入图片描述

图39.6.2.3 Linux启动logo显示
39.6.3 设置LCD作为终端控制台
LCD作为终端控制台前提条件要实现FB接口。我们一直使用MobaXterm作为Linux开发板终端,开发板通过串口和MobaXterm进行通信。现在我们已经驱动起来LCD并且提供了FB接口,所以可以设置LCD作为终端,也就是开发板使用自己的显示设备作为自己的终端,接上键盘就可以直接在开发板上敲命令了,将LCD设置为终端控制台的方法如下:
1、设置uboot中的bootargs
重启开发板,进入Linux命令行,重新设置bootargs参数的console内容,命令如下所示:
setenv bootargs ‘console=tty1 console=ttySTM0,115200 root=/dev/nfs nfsroot=192.168.1.21:/home/liangwencong/linux/nfs/rootfs,proto=tcp rw ip=192.168.1.246:192.168.1.21:192.168.1.1:255.255.255.0::eth0:off’
注意红色字体部分设置console,这里我们设置了两遍console,第一次设置console=tty1,也就是设置LCD屏幕为控制台,第二遍又设置console=ttymxc0,115200,也就是设置串口也作为控制台。相当于我们打开了两个console,一个是LCD,一个是串口,大家重启开发板就会发现LCD和串口都会显示Linux启动log信息。但是此时我们还不能使用LCD作为终端进行交互,因为我们的设置还未完成。
2、修改/etc/inittab文件
打开开发板根文件系统中的/etc/inittab文件,在里面加入下面这一行:
tty1::askfirst:-/bin/sh
添加完成以后的/etc/inittab文件内容如图39.6.3.1所示:
在这里插入图片描述

图 39.6.3.1 修改后的/etc/inittab 文件
修改完成以后保存/etc/inittab 并退出,然后重启开发板,重启以后开发板 LCD 屏幕最后一行会显示下面一行语句:Please press Enter to activate this console.上述提示语句说的是:按下回车键使能当前终端。至此,我们就拥有了两套终端,一个是基于串口的 MobaXterm,一个就是我们开发板的 LCD屏幕,但是为了方便调试,我们以后还是以 MobaXterm 为主。我们可以通过下面这一行命令向LCD 屏幕输出“hello linux!”
echo hello linux! > /dev/tty1
屏幕显示如图39.6.3.2所示:
在这里插入图片描述

图39.6.3.2 LCD终端显示
39.6.4 LCD背光调节
39.5小节已经讲过了,背光设备树节点设置了8个等级的背光调节,可以设置为0~7,我们可以通过设置背光等级来实现LCD背光亮度的调节,进入如下目录:
/sys/devices/platform/backlight/backlight/backlight
此目录下的文件如图39.6.4.1所示:
在这里插入图片描述

图39.6.4.1 目录下的文件和子目录
图39.6.4.1中的brightness表示当前亮度等级,max_bgigntness表示最大亮度等级。当前这两个文件内容如图39.6.4.2所示:
在这里插入图片描述

图39.6.4.2 brightness和max_brightness文件内容
从图39.6.4.2可以看出,当前屏幕亮度等级为7,根据前面的分析可以,这个是100%亮度。屏幕最大亮度等级为7。如果我们要修改屏幕亮度,只需要向brightness写入需要设置的屏幕亮度等级即可。比如设置屏幕亮度等级为6,那么可以使用如下命令:
echo 6 > brightness
输入上述命令以后就会发现屏幕亮度变暗了,如果设置brightness为0的话就会关闭LCD背光,屏幕就会熄灭。

Logo

更多推荐