STM32MP157驱动开发——Linux LCD驱动(上)
- 0.前言
- 一、LCD 和 LTDC 简介
- 1.LCD 简介
- 1)分辨率
- 2)像素格式
- 3)LCD 屏幕接口
- 4)LCD 时间参数
- 5)RGB LCD 屏幕时序
- 6)像素时钟
- 7)显存
- 2.LTDC 接口
- 二、DRM 驱动框架
- 1.DRM 简介
- 2.ST 官方的 DRM 驱动框架介绍
- drm_device 结构体
- drm_driver 结构体
- 3.RGB 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 列,如下图所示:
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 个字节的内存,这四个字节每个位的分配如下图所示:
一个像素点是 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这个屏幕进行驱动开发。(猜测都是通用接口,应该差别不大)
原理图:
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,那么其扫描如下图所示:
一帧图像也是由一行一行组成的。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 屏幕时序
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 的帧显示时序如下图所示:
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屏幕的参数如下表所示:
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接口功能框架:
从图中可以看出 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 模块,这两模块也分为好几个小模块。
图形执行管理器(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 通过以下结构来与显示器对接:
蓝色框表示 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 2048static 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 = &ire_am_480272h3tmqw_t01h,
3137 }, {
3138 .compatible = "ampire,am800480r3tmqwa1h",
3139 .data = &ire_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 = &ire_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 结构体。