一、DRM与Framebuffer
Linux 中的 DRM(Direct Rendering Manager) 驱动和 Framebuffer (fbdev) 驱动是两种不同的图形驱动方式,它们之间有一些区别。
Framebuffer驱动:
- 直接控制显卡的帧缓冲区,提供基本的显卡输出功能;
- 使用一些内核数据结构和API来管理图形界面,并提供一组接口与用户空间的应用程序进行通信;
- 相对简单,适合于嵌入式系统或者不需要高性能图形的应用场景。
DRM驱动:
- 提供一种分离的图形驱动架构,将硬件驱动程序、内核模块和用户空间驱动程序进行分离;
- 支持多个应用程序同时访问显卡,并提供了更丰富的图形功能,例如硬件加速和3D加速;
- 提供了一些内核接口,可以让用户空间应用程序与驱动程序进行交互;
- 支持多显示器和多GPU的配置。
总之,一句话,DRM是Linux目前主流的图形显示框架,相比FB架构,DRM更能适应当前日益更新的显示硬件。尽管FB退出历史舞台,在DRM 中也并未将其遗弃,而是集合到DRM中,供部分嵌入式设备使用。
二、DRM驱动
linux驱动系列学习之DRM(十)_linux drm_紫川宁520的博客-CSDN博客
1、概述
DRM从模块上划分,可以简单分为3部分:libdrm、KMS、GEM。
libdrm(DRM框架在用户空间的Lib)
libdrm是一个用户空间的DRM库,提供了DRM驱动的用户空间接口。用户或应用程序在用户空间调用libdrm提供的库函数, 即可访问到显示的资源,并对显示资源进行管理和使用。
KMS(内核显示模式设置)
KMS属于DRM框架下的一个大模块,主要负责两个功能:显示参数及显示控制。 这两个基本功能可以说是显示驱动必须具备的能力,在DRM框架下, 为了将这两部分适配得符合现代显示设备逻辑,又分出了几部分子模块配合框架(CRTC,ENCODER,CONNECTOR,PLANE,FB,VBLANK,property)。
更新画面:显示buffer的切换,多图层的合成方式,以及每个图层的显示位置。
设置显示参数:包括分辨率、刷新率、电源状态(休眠唤醒)等。
GMS(图形执行管理器)
它提供了一种抽象的显存管理方式(主要负责显示buffer的分配和释放),使用户空间应用程序可以更方便地管理显存,而不需要了解底层硬件的细节。(DUMB、PRIME、fence)
基本元素
DRM框架涉及到的元素很多,大致如下:
KMS:CRTC,ENCODER,CONNECTOR,PLANE,FB,VBLANK,property
GEM:DUMB、PRIME、fence
学习DRM驱动其实就是学习上面各个元素的实现及用法,如果你能掌握这些知识点,那么在编写DRM驱动的时候就能游刃有余。
Linux DRM(二)基本概念和特性_Younix脏羊的博客-CSDN博客
元素 | 说明 |
---|---|
CRTC | 电子束控制器,用于控制显卡输出信号,将帧缓存中的图像数据按照一定的方式输出到显示器上,并控制显示器的显示模式、分辨率、刷新率等参数。在DRM里有多个显存,可以通过操作CRTC来控制要显示的那个显存。 |
ENCODER | 编码器,用于将 CRTC 输出的图像信号转换成一定格式的数字信号,通常用于连接显示器或 TV 等设备。如 HDMI、DisplayPort、DVI 等。每个 CRTC 可以有一个或多个 Encoder。 |
CONNECTOR | 通常是用于将 Encoder 输出的信号传递给显示器,并与显示器建立连接。每个 Encoder 可以有一个或多个 Connector。 |
PLANE | 硬件图层,负责获取显存,再输出到CRTC里,可以看作是一个显示器的图层,每个 crtc 至少要有一个 plane。 通常会有多个 plane,每个 plane 可以分别设置自己的属性,从而实现多个图像内容的叠加。例如,在一个视频播放应用中,可以使用一个 plane 显示视频内容,另一个 plane 显示控制面板或字幕等内容,从而实现多个图像内容的同时显示。 |
Framebuffer | 帧缓存,用于存储屏幕上的每个像素点的颜色信息。只用于描述显存信息(如 format、pitch、size 等),不负责显存的分配释放 |
VBLANK | 软件和硬件的同步机制,RGB时序中的垂直消影区,软件通常使用硬件VSYNC来实现 |
property | 原子操作的基础,任何你想设置的参数,都可以做成property,供用户空间使用,是DRM驱动中最灵活、最方便的Mode setting机制 |
DUMB | 是一个dumb缓冲区,负责一些简单的buffer显示,可以通过CPU直接渲染 |
PRIME | 连续、非连续物理内存都支持,基于DMA-BUF机制,可以实现buffer共享,多用于大内存复杂场景 |
fence | buffer同步机制,基于内核dma_fence机制实现,用于防止显示内容出现异步问题 |
以上内容来自:参考内容1&2。
2、DRM内部Objects
上图蓝色部分则是对物理硬件的抽象,黄色部分则是对软件的抽象。虚线以上的为drm_mode_object,虚线以下为 drm_gem_object。
上面这些objects的关系如下图所示:
通过上图可以看到,plane 是连接 framebuffer 和 crtc 的纽带,而 encoder 则是连接 crtc 和 connector 的纽带。与物理 buffer 直接打交道的是 gem 而不是 framebuffer。
个人理解:
buffer是硬件存储设备, 由gem分配和释放,framebuffer用于描述分配的显存的信息(如 format、pitch、size 等),而plane用于描述图层信息,描述的是framebuffer中哪些点处于同一个图层,多个plane隶属于同一个crtc,crtc控制显卡输出图像信号,encoder将 crtc 输出的图像信号转换成一定格式的数字信号,如 HDMI、DisplayPort、DVI 等, connector用于将 Encoder 输出的信号传递给显示器,并与显示器建立连接。
需要注意的是,上图蓝色部分即使没有实际的硬件与之对应,在软件驱动中也需要实现这些 objects,否则 DRM 子系统无法正常运行。
3、drm_panel
Encoder 驱动程序负责将图形数据转换为 LCD 显示器所需的视频信号,而 Connector 驱动程序则负责将这些信号发送到正确的显示设备上。LCD 驱动程序需要和Encoder、Connector 这两个驱动程序进行交互,以完成图形输出的控制。
耦合的产生:
(1)connector 的主要作用就是获取显示参数,所以会在 LCD 驱动中去构造 connector object。但是 connector 初始化时需要 attach 上一个 encoder object,而这个 encoder object 往往是在另一个硬件驱动中生成的,为了访问该 encoder object,势必会产生一部分耦合的代码。
(2)encoder 除了扮演信号转换的角色,还担任着通知显示设备休眠唤醒的角色。因此,当 encoder 通知 LCD 驱动执行相应的 enable/disable 操作时,就一定会调用 LCD 驱动导出的全局函数,这也必然会产生一部分的耦合代码。
Encoder 驱动与 LCD 驱动之间的耦合会造成以下问题:
可维护性下降:耦合代码过多,使得维护代码变得困难。
可移植性下降:由于硬件平台的不同,encoder 驱动的实现可能不同,这就导致了 LCD 驱动对 encoder 驱动的依赖程度变高,降低了代码的可移植性。
系统可扩展性下降:由于 encoder 驱动与 LCD 驱动的紧耦合关系,当系统需要支持新的显示设备时,需要修改大量的代码,增加了系统扩展性的难度。
为了解决该耦合的问题,DRM 子系统为开发人员提供了 drm_panel 结构体,该结构体封装了 connector & encoder 对 LCD 访问的常用接口。
于是,原来的 Encoder 驱动和 LCD 驱动之间的耦合,就转变成了上图中 Encoder 驱动与 drm_panel、drm_panel 与 LCD 驱动之间的“耦合”,从而实现了 Encoder 驱动与 LCD 驱动之间的解耦合。
drm_panel 不属于 objects 的范畴,它只是一堆回调函数的集合。但它的存在降低了 LCD 驱动与 encoder 驱动之间的耦合度。
三、DRM 驱动常用结构体
🍚 drm_mode_object
对于plane、crtc、encoder、connector 几个对象,DRM框架将其称作“对象”,有一个公共基类struct drm_mode_object,这几个对象都由此基类扩展而来(该类作为crtc等结构体的成员)。
如 struct crtc 结构体中存在基类成员 struct drm_mode_object:
/**
* struct drm_crtc - central CRTC control structure
*
* Each CRTC may have one or more connectors associated with it. This structure
* allows the CRTC to be controlled.
*/
struct drm_crtc {
/*挂载到&drm_mode_config.crtc_list*/
struct list_head head;
/** crtc名称 */
char *name;
/** kms mode对象 */
struct drm_mode_object base;// 基类
/*primary层*/
struct drm_plane *primary;
/*鼠标层*/
struct drm_plane *cursor;
/*序号*/
unsigned index;
...
};
事实上,这个基类扩展出来的子类并不是只有上面提到的几种,本文只考虑以上四种。以下为结构体 drm_mode_object 的定义:
struct drm_mode_object {
uint32_t id;
uint32_t type;
struct drm_object_properties *properties;
struct kref refcount;
void (*free_cb)(struct kref *kref);
};
struct drm_mode_object 中 id 和 type 分别为这个对象在KMS子系统中的ID和类型(即上面提到的plane、CRTC等)。drm_mode_object 从的定义中即可发现其实现了两个比较重要的功能:
kref 引用计数及生命周期管理:
指 drm_mode_object 对象在内核中的生命周期的管理。每个 drm_mode_object 对象都有一个引用计数,当一个对象被创建时,它的引用计数被初始化为1。每当一个新的引用指向该对象时,它的引用计数就会增加1,每当一个引用被释放时,它的引用计数就会减少1。当对象的引用计数降为0时,内核会自动释放该对象。这种方式确保了内核中不会存在不再使用的对象,从而避免了内存泄漏。
drm_object_properties 属性管理:
属性管理是指在DRM驱动中,每个对象都可以拥有一组属性(例如分辨率、刷新率等),并且可以动态地增加、删除或修改属性。这些属性可以被用户空间的应用程序或者其他驱动程序获取或者设置。
🍚 Framebuffer
注意framebuffer没有对应的回调函数:
// include/drm/drm_framebuffer.h
struct drm_framebuffer {
struct drm_device *dev;
struct list_head head;
struct drm_mode_object base;
const struct drm_format_info *format; // drm格式信息
const struct drm_framebuffer_funcs *funcs;
unsigned int pitches[4]; // Line stride per buffer
unsigned int offsets[4]; // Offset from buffer start to the actual pixel data in bytes, per buffer.
uint64_t modifier; // Data layout modifier
unsigned int width;
unsigned int height;
int flags;
int hot_x;
int hot_y;
struct list_head filp_head;
struct drm_gem_object *obj[4];
};
struct drm_framebuffer 主要元素的展示如下图所示(来自brezillon-drm-kms),内存缓冲区组织,采取FOURCC格式代码:
🍚 Plane
drm_plane结构体详见:LXR linux/include/drm/drm_plane.h
struct drm_plane_funcs 结构体成员:
1、 update_plane:为给定的CRTC和framebuffer启用并配置平面
2、 disable_plane:关闭plane
3、 destroy:清除plane所有的资源
4、 reset:复位软硬件状态为关闭
5、 set_property:更新plane的property
6、 atomic_duplicate_state:复制当前的plane状态并返回
7、 atomic_destroy_state:销毁当前的plane状态
8、 atomic_set/get_property:原子操作设置/获得property
9、 format_mod_supported:该可选挂钩用于DRM,以确定给定的格式/修饰符组合是否对平面有效
struct drm_plane_helper_funcs结构体成员:
1、 prepare_fb:该钩子通过固定其后备存储器或将其重新定位到连续的VRAM块,为扫描准备帧缓冲区。其他可能的准备工作包括冲洗缓存。
2、 cleanup_fb:清除在prepare_fb分配的给framebuffer和plane的资源
3、 atomic_check:检查该挂钩中的plane特定约束
4、 atomic_update:更新plane状态
5、 atomic_disable:关闭
6、 atomic_async_check:异步检查
7、 atomic_async_update:异步更新状态
🍚 Crtc
drm_crtc结构体详见:LXR linux/include/drm/drm_crtc.h
struct drm_crtc_funcs结构体成员:
1、 reset;复位,设置为关闭状态
2、 destroy:清除CRTC资源
3、 cursor_set:更新鼠标图像
4、 cursor_move:更新光标位置
5、 gamma_set:gamma设置
6、 set_config:修改配置
7、 page_flip:修改帧缓冲页,避免垂直消影期间用新的缓冲区替代时产生撕裂
8、 set_property:设置property
9、 atomic_duplicate_state:复制当前的状态
10、 atomic_destroy_state:销毁复制的状态
11、 atomic_get_property:获取atomic_get_property
12、 set_crc_source:更改CRC的来源
13、 get_vblank_counter 获取CRTC的vblank计数器
14、 disable_vblank:关闭消影
struct drm_crtc_helper_funcs结构体成员
1、 dpms:控制CRTC电源功率
2、 commit:提交新的模式
3、 mode_valid:用于检查特定模式在此crtc中是否有效
4、 mode_fixup:用于验证模式
5、 mode_set:设置模式
6、 mode_set_nofb:用于更新CRTC的显示模式,而不更改主平面配置的任何内容
7、 mode_set_base:设置新的帧缓冲区和扫描位置
8、 atomic_flush:完成CRTC上多个平面的更新
9、 atomic_enable/disable:打开/关闭
🍚 Encoder
drm_encoder结构体详见:LXR linux/include/drm/drm_encoder.h
static const struct drm_encoder_funcs ltdc_encoder_funcs = {
.destroy = drm_encoder_cleanup,
};
static const struct drm_encoder_helper_funcs ltdc_encoder_helper_funcs = {
.disable = ltdc_encoder_disable,
.enable = ltdc_encoder_enable,
.mode_set = ltdc_encoder_mode_set,
};
🍚 helper 函数与使用
很多文章对 helper 函数和 drm_xxx_helper_funcs 函数产生了混淆!!!以下是我的理解,有错误评论区指出。
- drm_xxx_funcs 和 drm_xxx_helper_funcs 结构体是用于表示某个组件的基本操作和辅助操作的结构体;
- 而 helper 函数是 DRM 框架提供的通用函数,用于实现 DRM 驱动程序中常见的操作。
在 Linux DRM 驱动中,helper 函数指的是由 DRM 框架提供的一组辅助函数,用于简化驱动程序的实现。这些函数包括了多种类型的操作,如模式设置、原子操作、缓冲区管理等。
常见的helper函数:
在 drm_xxx_funcs 可以自定义回调函数(完全不使用helper函数),或者使用DRM驱动提供的helper函数直接填充(使用helper函数),如果直接使用helper函数填充不足以满足要求,还可以使用 drm_xxx_helper_funcs 中的回调函数进行补充、修改、覆盖!
终于理解了!如果你觉着有用,点个赞鼓励一下吧!
🍚 drm_xxx_funcs 和 drm_xxx_helper_funcs 区别
在DRM子系统中,当需要执行特定组件的操作时,就会调用对应的回调函数,这些回调函数会根据其实现的功能,对特定组件进行相应的操作。比如当系统检测到连接状态发生变化时,会调用drm_connector_funcs中的detect函数,该函数会根据当前连接状态,更新drm_connector的状态,并将状态通知给上层应用程序和显示管道。
drm_xxx_funcs 和 drm_xxx_helper_funcs 都是用来定义回调函数的结构体,它们之间的区别在于它们包含的函数指针的类型和含义不同。
它们的区别在于,drm_xxx_funcs 中包含一些必要的、基本的由驱动程序实现的函数指针,而 drm_xxx_helper_funcs 中包含一些可选的、辅助的由驱动程序选择性实现的辅助函数指针。
总结(来自参考文献,并进行修改):
drm_xxx_funcs 是 drm ioctl 操作的最终入口,但是对于大多数 SoC 厂商来说,它们的 drm_xxx_funcs 操作流程基本相同(你抄我,我抄你),只是在寄存器配置上存在差异,因此开发者们将那些 common 的操作流程抽象成了 helper 库函数,而将那些厂商差异化的代码放到了 drm_xxx_helper_funcs 中去,用于补充和客制化 helper 库函数。
架构优势:
这样双层的实现即能保证开发者有足够高的自由度(完全不用helper函数),也能简化开发者的开发(使用helper函数),同时提供给开发者hook(修改或者覆盖)特定helper函数的能力。
四、DRM驱动框架(VKMS)
1、VKMS 简介
VKMS 是 “Virtual Kernel Mode Setting” 的缩写,它于2018年7月5日被合入到 linux-4.19 主线版本中,并存放在 drivers/gpu/drm/vkms 目录下。之所以称它为 Virtual KMS,是因为该驱动不需要真实的硬件,它完全是一个软件虚拟的“显示”设备,甚至连显示都算不上,因为当它运行时,你看不到任何显示内容。它唯一能提供的,就是一个由高精度 timer 模拟的 VSYNC 中断信号!
该驱动存在的目的,主要是为了 DRM 框架自测试,以及方便那些无头显示器设备的调试应用。虽然我们看不到 VKMS 的显示效果,但是在驱动流程上,它实现了 modesetting 该有的基本操作(Modesetting是指在显示器上渲染图像的过程中,调整显示器的分辨率、刷新率、像素格式等参数,以适应不同的应用场景和显示设备。)。因其逻辑简单,代码量少,拿来做学习案例讲解再好不过。
随着内核版本的不断升级,添加到 VKMS 的功能也越来越多,截止到内核版本 kernel 5.7-rc2,该 VKMS 驱动已经集成了如下功能:
- Atomic Modeset
- VBlank
- Dumb Buffer
- Cursor & Primary Plane
- Framebuffer CRC 校验
- Plane Composition
- GEM Prime Import
下面我们将一步一步学习如何从零开始撰写一个VKMS驱动。
2、最简单的VKMS驱动
#include <drm/drmP.h>
static struct drm_device drm;// 用于抽象一个完整的DRM设备
static struct drm_driver vkms_driver = {
.name = "vkms",
.desc = "Virtual Kernel Mode Setting",
.date = "20180514",
.major = 1,
.minor = 0,
};
static int __init vkms_init(void)
{
drm_dev_init(&drm, &vkms_driver, NULL);
drm_dev_register(&drm, 0);
return 0;
}
module_init(vkms_init);
我们将该驱动以 build-in 方式编译进内核,然后启动内核,如果你在 kernel log 中仔细查找,会发现有如下 drm log, 这些信息正是从上面的 name、date、major、minor 字段中获取的。
[drm] Initialized vkms 1.0.0 20180514 for virtual device on minor 0
除此之外,DRM 框架还为我们做了下面这些事情:
创建设备节点:/dev/dri/card0
创建 sysfs 节点:/sys/class/drm/card0
创建 debugfs 节点:/sys/kernel/debug/dri/0
当然,简单是以牺牲功能为代价的。该驱动目前什么事情也做不了,你唯一能做的就是查看该驱动的名字:
$ cat /sys/kernel/debug/dri/0/name
vkms unique=vkms
3、VKMS驱动添加 fops 操作接口
#include <drm/drmP.h>
static struct drm_device drm;// 用于抽象一个完整的DRM设备
static const struct file_operations vkms_fops = {
.owner = THIS_MODULE,
.open = drm_open,
.release = drm_release,
.unlocked_ioctl = drm_ioctl,
.poll = drm_poll,
.read = drm_read,
};
static struct drm_driver vkms_driver = {
.fops = &vkms_fops,
.name = "vkms",
.desc = "Virtual Kernel Mode Setting",
.date = "20180514",
.major = 1,
.minor = 0,
};
static int __init vkms_init(void)
{
drm_dev_init(&drm, &vkms_driver, NULL);
drm_dev_register(&drm, 0);
return 0;
}
module_init(vkms_init);
有了 fops,我们就可以对 card0 进行 open / read 操作了。更重要的是,我们现在可以进行一些简单的 ioctl 操作了(DRM相关只读的操作,且不需要调用DRM相关回调函数的操作)!但是,到目前为止,凡是和 modesetting 相关的操作,我们还是操作不了。
4、添加 drm mode objects
#include <drm/drmP.h>
#include <drm/drm_encoder.h>
static struct drm_device drm;// 用于抽象一个完整的DRM设备
static struct drm_plane primary;
static struct drm_crtc crtc;
static struct drm_encoder encoder;
static struct drm_connector connector;
static const struct drm_plane_funcs vkms_plane_funcs;
static const struct drm_crtc_funcs vkms_crtc_funcs;
static const struct drm_encoder_funcs vkms_encoder_funcs;
static const struct drm_connector_funcs vkms_connector_funcs;
static const u32 vkms_formats[] = {
DRM_FORMAT_XRGB8888,
};
static void vkms_modeset_init(void)
{
drm_mode_config_init(&drm);// 初始化KMS框架(初始化一些全局结构体),本质上是初始化drm_device中mode_config结构体
// 初始化drm_device中包含的drm_connector,drm_crtc等对象
drm_universal_plane_init(&drm, &primary, 0, &vkms_plane_funcs,
vkms_formats, ARRAY_SIZE(vkms_formats),
NULL, DRM_PLANE_TYPE_PRIMARY, NULL);
drm_crtc_init_with_planes(&drm, &crtc, &primary, NULL, &vkms_crtc_funcs, NULL);
drm_encoder_init(&drm, &encoder, &vkms_encoder_funcs, DRM_MODE_ENCODER_VIRTUAL, NULL);
drm_connector_init(&drm, &connector, &vkms_connector_funcs, DRM_MODE_CONNECTOR_VIRTUAL);
}
static const struct file_operations vkms_fops = {
.owner = THIS_MODULE,
.open = drm_open,
.release = drm_release,
.unlocked_ioctl = drm_ioctl,
.poll = drm_poll,
.read = drm_read,
};
static struct drm_driver vkms_driver = {
.driver_features = DRIVER_MODESET, // 添加上 DRIVER_MODESET 标志位,告诉 DRM Core 当前驱动支持 modesetting 操作
.fops = &vkms_fops,
.name = "vkms",
.desc = "Virtual Kernel Mode Setting",
.date = "20180514",
.major = 1,
.minor = 0,
};
static int __init vkms_init(void)
{
drm_dev_init(&drm, &vkms_driver, NULL);
vkms_modeset_init();
drm_dev_register(&drm, 0);
return 0;
}
module_init(vkms_init);
由于上面4个 objects 在创建时,它们的 callback funcs 没有赋初值,所以真正的 modeset 操作目前还无法正常执行,不过我们至少可以使用一些只读的 modeset IOCTL 了(DRM objects 相关只读的操作,且不需要调用DRM相关回调函数的操作)。
5、添加 FB 和 GEM 支持
#include <drm/drmP.h>
#include <drm/drm_encoder.h>
#include <drm/drm_fb_cma_helper.h>
#include <drm/drm_gem_cma_helper.h>
static struct drm_device drm;// 用于抽象一个完整的DRM设备
static struct drm_plane primary;
static struct drm_crtc crtc;
static struct drm_encoder encoder;
static struct drm_connector connector;
static const struct drm_plane_funcs vkms_plane_funcs;
static const struct drm_crtc_funcs vkms_crtc_funcs;
static const struct drm_encoder_funcs vkms_encoder_funcs;
static const struct drm_connector_funcs vkms_connector_funcs;
/* add here */
// 定义了一组函数指针,用于管理驱动程序中的显示模式配置。
// 这些函数指针包括添加和删除连接器、CRTC和编解码器,以及更新显示模式等功能。
// 这些函数在驱动程序中被调用以进行显示模式的管理和配置。
static const struct drm_mode_config_funcs vkms_mode_funcs = {
.fb_create = drm_fb_cma_create, // 根据给定的帧缓冲参数,创建一个新的帧缓冲对象,并返回其句柄
};
static const u32 vkms_formats[] = {
DRM_FORMAT_XRGB8888,
};
static void vkms_modeset_init(void)
{
drm_mode_config_init(&drm);// 初始化KMS框架,本质上是初始化drm_device中的mode_config结构体
// 填充mode_config中int min_width, min_height; int max_width, max_height的值,这些值是framebuffer的大小限制
drm.mode_config.max_width = 8192;
drm.mode_config.max_height = 8192;
/* add here */
// 设置mode_config->funcs指针,本质是一组由驱动实现的回调函数,涵盖KMS中一些相当基本的操作
drm.mode_config.funcs = &vkms_mode_funcs;
// 初始化drm_device中包含的drm_connector,drm_crtc等对象
drm_universal_plane_init(&drm, &primary, 0, &vkms_plane_funcs,
vkms_formats, ARRAY_SIZE(vkms_formats),
NULL, DRM_PLANE_TYPE_PRIMARY, NULL);
drm_crtc_init_with_planes(&drm, &crtc, &primary, NULL, &vkms_crtc_funcs, NULL);
drm_encoder_init(&drm, &encoder, &vkms_encoder_funcs, DRM_MODE_ENCODER_VIRTUAL, NULL);
drm_connector_init(&drm, &connector, &vkms_connector_funcs, DRM_MODE_CONNECTOR_VIRTUAL);
}
// 文件操作回调函数
static const struct file_operations vkms_fops = {
.owner = THIS_MODULE,
.open = drm_open,
.release = drm_release,
.unlocked_ioctl = drm_ioctl,
.poll = drm_poll,
.read = drm_read,
/* add here */
.mmap = drm_gem_cma_mmap, // DRM的GEM对象映射到用户空间,以便用户空间可以使用它
};
static struct drm_driver vkms_driver = {
.driver_features = DRIVER_MODESET | DRIVER_GEM, // 添加DRIVER_GEM 标志位,告诉 DRM Core 该驱动支持 GEM 操作
.fops = &vkms_fops,
/* add here */
.dumb_create = drm_gem_cma_dumb_create, //
.gem_vm_ops = &drm_gem_cma_vm_ops,
.gem_free_object_unlocked = drm_gem_cma_free_object,
.name = "vkms",
.desc = "Virtual Kernel Mode Setting",
.date = "20180514",
.major = 1,
.minor = 0,
};
static int __init vkms_init(void)
{
drm_dev_init(&drm, &vkms_driver, NULL);
vkms_modeset_init();
drm_dev_register(&drm, 0);
return 0;
}
module_init(vkms_init);
现在,我们可以使用 IOCTL 来进行一些标准的 GEM 和 FB 操作了!
驱动解析1 :
我们知道drm_device用于抽象一个完整的DRM设备,而其中与Mode Setting相关的部分则由drm_mode_config 进行管理。drm_mode_config 的主要功能之一是提供对显示器模式的管理和配置。这包括添加、删除、修改和查询显示器模式的能力。此外,drm_mode_config还提供了与模式相关的配置选项,例如色彩空间、刷新率、分辨率等等。
在 drm_device 中存在 mode_config 这个 drm_mode_config 结构体:
/**
* struct drm_device - DRM device structure
*
* This structure represent a complete card that
* may contain multiple heads.
*/
struct drm_device {
/** @if_version: Highest interface version set */
int if_version;
/** @ref: Object ref-count */
struct kref ref;
...
/** @mode_config: Current mode config */
struct drm_mode_config mode_config;
...
};
在 mode_config 这个 drm_mode_config 结构体中存在一个类型为 drm_mode_config_func 的回调函数 func。drm_mode_config_func是一个函数指针结构体,用于驱动程序向内核注册显示器模式配置的回调函数。这些函数指针包括添加和删除连接器、CRTC和编解码器,以及更新显示模式等功能。当内核需要对显示器模式进行配置或管理时,它将调用这些回调函数以执行相应操作。
drm_mode_config_funcs结构体中的 fb_create 作用:
fb_create 函数的作用是根据给定的帧缓冲参数,创建一个新的帧缓冲对象(并不是分配内存,只是创建帧缓冲对象,因为 framebuffer 不涉及内存的分配与释放),并返回其句柄。
/**
* struct drm_mode_config - Mode configuration control structure
* @min_width: minimum fb pixel width on this device
* @min_height: minimum fb pixel height on this device
* @max_width: maximum fb pixel width on this device
* @max_height: maximum fb pixel height on this device
* @funcs: core driver provided mode setting functions
* @fb_base: base address of the framebuffer
* @poll_enabled: track polling support for this device
* @poll_running: track polling status for this device
* @delayed_event: track delayed poll uevent deliver for this device
* @output_poll_work: delayed work for polling in process context
* @preferred_depth: preferred RBG pixel depth, used by fb helpers
* @prefer_shadow: hint to userspace to prefer shadow-fb rendering
* @cursor_width: hint to userspace for max cursor width
* @cursor_height: hint to userspace for max cursor height
* @helper_private: mid-layer private data
*
* Core mode resource tracking structure. All CRTC, encoders, and connectors
* enumerated by the driver are added here, as are global properties. Some
* global restrictions are also here, e.g. dimension restrictions.
*/
struct drm_mode_config {
...
int min_width, min_height;
int max_width, max_height;
const struct drm_mode_config_funcs *funcs;
resource_size_t fb_base;
...
};
驱动解析2 :
dumb_create: struct drm_driver 结构体中的一个函数指针,它指向的函数用于在 DRM 子系统中创建 dumb buffer,也就是一个简单的、未映射的内存区域,通常用于测试或临时存储。
dumb_create:分配 dumb buffer 的回调接口,主要完成三件事:
(1)创建 gem object
(2)创建 gem handle
(3)分配物理 buffer (也可以等到后面再分配)
本例中直接使用 CMA helper 函数实现,该函数内部会分配最终的物理 buffer。
GEM分配的内存区域通过映射之后交给Frame buffer驱动程序使用。Frame buffer驱动程序不直接分配内存,而是通过访问GEM对象来获取内存区域的物理地址等信息,以便正确地管理和利用帧缓冲区中的显示数据。
在将GEM映射到帧缓冲区时,需要经过以下两个步骤:
- 将GEM对象中的内存区域映射到内核空间的虚拟地址空间中。
- 将内核空间中的GEM内存区域映射到显示设备的显存中。
驱动解析3 :
mmp: 将GEM内存映射到用户控件。创建 dumb buffer 的目的就是要拿去给 CPU 画图,因此没有 mmap 的 dumb buffer 是没有灵魂的,所以必须实现。通常使用 drm_gem_mmap() 来实现。在 kernel 驱动中,实现 mmap 系统调用离不开两个关键步骤:(1)内存分配 (2) 建立映射关系。这刚好也对应了 DRM 中的 dumb_create 和 mmap 操作。
驱动解析4 :
gem_vm_ops:该函数指针中包含的各种操作函数,例如 fault, open, close, access 等,都是用于实现 GEM 内存管理机制的各种功能。
- mmap 函数是将设备内存映射到进程的用户空间,让应用程序可以直接访问设备内存。
- gem_vm_ops 函数指针则是在用户空间程序请求显存时被调用(mmp),用于控制显存的访问权限、分配显存、映射显存到用户空间等操作,从而确保显存的安全性和高效性。
这个过程涉及到分配一块虚拟内存、映射物理内存、建立页表等操作。 因此,在实现 DRM 驱动时,这两个函数通常需要一起实现,以支持对设备内存的映射操作。
驱动解析5 :
gem_free_object_unlocked :该指针指向一个函数,用于释放GEM对象的资源。具体来说,当一个GEM对象不再被使用时,该函数将被调用来释放该对象的内存、锁定的页面等资源。这个函数指针可以在结构体中 struct drm_driver 定义,作为驱动的一部分,因此在驱动代码中可以使用它来释放GEM对象的资源。
6、实现 callback funcs 并添加 Legacy Modeset 支持
Legacy Modeset 是指在 Linux DRM 框架中使用传统的 KMS 模式设置,也称为“旧模式设置”。
在 Legacy Modeset 中,驱动程序需要执行多个步骤来更改图形模式,例如分配帧缓冲,设置分辨率和刷新率等。这些步骤是分开执行的,如果其中的任何一步出现错误,整个操作都可能失败。可能会导致画面闪烁、撕裂或丢失帧等问题,而且驱动程序必须花费额外的时间来撤消之前的更改。
因此,Legacy Modeset 的主要缺点是不具备原子性,无法保证更改的一致性和可靠性。这也是 Atom Modeset 出现的主要原因。Atom Modeset 可以将多个操作封装成一个事务并原子地提交给硬件,从而保证所有更改都是一致和可靠的。
#include <drm/drm_crtc_helper.h>
#include <drm/drm_plane_helper.h>
#include <drm/drm_fb_cma_helper.h>
#include <drm/drm_gem_cma_helper.h>
static struct drm_device drm;
static struct drm_plane primary;
static struct drm_crtc crtc;
static struct drm_encoder encoder;
static struct drm_connector connector;
static void vkms_crtc_dpms(struct drm_crtc *crtc, int mode)
{
}
static int vkms_crtc_mode_set(struct drm_crtc *crtc,
struct drm_display_mode *mode,
struct drm_display_mode *adjusted_mode,
int x, int y, struct drm_framebuffer *old_fb)
{
return 0;
}
static void vkms_crtc_prepare(struct drm_crtc *crtc)
{
}
static void vkms_crtc_commit(struct drm_crtc *crtc)
{
}
static int vkms_crtc_page_flip(struct drm_crtc *crtc,
struct drm_framebuffer *fb,
struct drm_pending_vblank_event *event,
uint32_t page_flip_flags,
struct drm_modeset_acquire_ctx *ctx)
{
unsigned long flags;
crtc->primary->fb = fb;
if (event) {
spin_lock_irqsave(&crtc->dev->event_lock, flags);
drm_crtc_send_vblank_event(crtc, event);
spin_unlock_irqrestore(&crtc->dev->event_lock, flags);
}
return 0;
}
static const struct drm_crtc_helper_funcs vkms_crtc_helper_funcs = {
.dpms = vkms_crtc_dpms,
.mode_set = vkms_crtc_mode_set,
.prepare = vkms_crtc_prepare,
.commit = vkms_crtc_commit,
};
static const struct drm_crtc_funcs vkms_crtc_funcs = {
.set_config = drm_crtc_helper_set_config,
.page_flip = vkms_crtc_page_flip,
.destroy = drm_crtc_cleanup,
};
static const struct drm_plane_funcs vkms_plane_funcs = {
.update_plane = drm_primary_helper_update,
.disable_plane = drm_primary_helper_disable,
.destroy = drm_plane_cleanup,
};
static int vkms_connector_get_modes(struct drm_connector *connector)
{
int count;
count = drm_add_modes_noedid(connector, 8192, 8192);
drm_set_preferred_mode(connector, 1024, 768);
return count;
}
static struct drm_encoder *vkms_connector_best_encoder(struct drm_connector *connector)
{
return &encoder;
}
static const struct drm_connector_helper_funcs vkms_conn_helper_funcs = {
.get_modes = vkms_connector_get_modes,
.best_encoder = vkms_connector_best_encoder,
};
static const struct drm_connector_funcs vkms_connector_funcs = {
.dpms = drm_helper_connector_dpms,
.fill_modes = drm_helper_probe_single_connector_modes,
.destroy = drm_connector_cleanup,
};
static const struct drm_encoder_funcs vkms_encoder_funcs = {
.destroy = drm_encoder_cleanup,
};
static const struct drm_mode_config_funcs vkms_mode_funcs = {
.fb_create = drm_fb_cma_create,
};
static const u32 vkms_formats[] = {
DRM_FORMAT_XRGB8888,
};
static void vkms_modeset_init(void)
{
drm_mode_config_init(&drm);
drm.mode_config.max_width = 8192;
drm.mode_config.max_height = 8192;
drm.mode_config.funcs = &vkms_mode_funcs;
drm_universal_plane_init(&drm, &primary, 0, &vkms_plane_funcs,
vkms_formats, ARRAY_SIZE(vkms_formats),
NULL, DRM_PLANE_TYPE_PRIMARY, NULL);
drm_crtc_init_with_planes(&drm, &crtc, &primary, NULL, &vkms_crtc_funcs, NULL);
drm_crtc_helper_add(&crtc, &vkms_crtc_helper_funcs);
drm_encoder_init(&drm, &encoder, &vkms_encoder_funcs, DRM_MODE_ENCODER_VIRTUAL, NULL);
drm_connector_init(&drm, &connector, &vkms_connector_funcs, DRM_MODE_CONNECTOR_VIRTUAL);
drm_connector_helper_add(&connector, &vkms_conn_helper_funcs);
drm_mode_connector_attach_encoder(&connector, &encoder);
}
static const struct file_operations vkms_fops = {
.owner = THIS_MODULE,
.open = drm_open,
.release = drm_release,
.unlocked_ioctl = drm_ioctl,
.poll = drm_poll,
.read = drm_read,
.mmap = drm_gem_cma_mmap,
};
static struct drm_driver vkms_driver = {
.driver_features = DRIVER_MODESET | DRIVER_GEM,
.fops = &vkms_fops,
.dumb_create = drm_gem_cma_dumb_create,
.gem_vm_ops = &drm_gem_cma_vm_ops,
.gem_free_object_unlocked = drm_gem_cma_free_object,
.name = "vkms",
.desc = "Virtual Kernel Mode Setting",
.date = "20180514",
.major = 1,
.minor = 0,
};
static int __init vkms_init(void)
{
drm_dev_init(&drm, &vkms_driver, NULL);
vkms_modeset_init();
drm_dev_register(&drm, 0);
return 0;
}
module_init(vkms_init);
- xxx_funcs 必须有,xxx_helper_funcs 可以没有。
- drm_xxx_init() 必须有,drm_xxx_helper_add() 可以没有。
- 只有当 xxx_funcs 采用 DRM 标准的 helper 函数实现时,才有可能 需要定义 xxx_helper_funcs 接口。
- xxx_funcs.destroy() 接口必须实现。
有了各种 funcs 和 helper funcs,我们现在终于可以执行真正的 modeset 操作了,不过目前只支持 legacy modeset。
7、将 Legacy code 转换为 Atomic 版本
DRM驱动的legacy模式是一种传统的方法,它使用一个内核模块来管理DRM资源,支持在内核空间和用户空间之间进行资源管理。这种模式比较简单,但性能不太好,而且容易出现错误。
Atomic模式是一种更新的模式,它使用多个内核模块来管理DRM资源,并且支持在内核空间和用户空间之间进行原子操作,从而提供更好的性能和可靠性。Atomic模式比legacy模式更复杂,但性能更好,而且更加可靠。
#include <drm/drm_atomic_helper.h>
#include <drm/drm_crtc_helper.h>
#include <drm/drm_fb_cma_helper.h>
#include <drm/drm_gem_cma_helper.h>
#include <linux/hrtimer.h>
static struct drm_device drm;
static struct drm_plane primary;
static struct drm_crtc crtc;
static struct drm_encoder encoder;
static struct drm_connector connector;
static struct hrtimer vblank_hrtimer;
static enum hrtimer_restart vkms_vblank_simulate(struct hrtimer *timer)
{
drm_crtc_handle_vblank(&crtc);
hrtimer_forward_now(&vblank_hrtimer, 16666667);
return HRTIMER_RESTART;
}
static void vkms_crtc_atomic_enable(struct drm_crtc *crtc,
struct drm_crtc_state *old_state)
{
hrtimer_init(&vblank_hrtimer, CLOCK_MONOTONIC, HRTIMER_MODE_REL);
vblank_hrtimer.function = &vkms_vblank_simulate;
hrtimer_start(&vblank_hrtimer, 16666667, HRTIMER_MODE_REL);
}
static void vkms_crtc_atomic_disable(struct drm_crtc *crtc,
struct drm_crtc_state *old_state)
{
hrtimer_cancel(&vblank_hrtimer);
}
static void vkms_crtc_atomic_flush(struct drm_crtc *crtc,
struct drm_crtc_state *old_crtc_state)
{
unsigned long flags;
if (crtc->state->event) {
spin_lock_irqsave(&crtc->dev->event_lock, flags);
drm_crtc_send_vblank_event(crtc, crtc->state->event);
spin_unlock_irqrestore(&crtc->dev->event_lock, flags);
crtc->state->event = NULL;
}
}
static const struct drm_crtc_helper_funcs vkms_crtc_helper_funcs = {
.atomic_enable = vkms_crtc_atomic_enable,
.atomic_disable = vkms_crtc_atomic_disable,
.atomic_flush = vkms_crtc_atomic_flush,
};
static const struct drm_crtc_funcs vkms_crtc_funcs = {
.set_config = drm_atomic_helper_set_config,
.page_flip = drm_atomic_helper_page_flip,
.destroy = drm_crtc_cleanup,
.reset = drm_atomic_helper_crtc_reset,
.atomic_duplicate_state = drm_atomic_helper_crtc_duplicate_state,
.atomic_destroy_state = drm_atomic_helper_crtc_destroy_state,
};
static void vkms_plane_atomic_update(struct drm_plane *plane,
struct drm_plane_state *old_state)
{
}
static const struct drm_plane_helper_funcs vkms_plane_helper_funcs = {
.atomic_update = vkms_plane_atomic_update,
};
static const struct drm_plane_funcs vkms_plane_funcs = {
.update_plane = drm_atomic_helper_update_plane,
.disable_plane = drm_atomic_helper_disable_plane,
.destroy = drm_plane_cleanup,
.reset = drm_atomic_helper_plane_reset,
.atomic_duplicate_state = drm_atomic_helper_plane_duplicate_state,
.atomic_destroy_state = drm_atomic_helper_plane_destroy_state,
};
static int vkms_conn_get_modes(struct drm_connector *connector)
{
int count;
count = drm_add_modes_noedid(connector, 8192, 8192);
drm_set_preferred_mode(connector, 1024, 768);
return count;
}
static const struct drm_connector_helper_funcs vkms_conn_helper_funcs = {
.get_modes = vkms_conn_get_modes,
};
static const struct drm_connector_funcs vkms_connector_funcs = {
.fill_modes = drm_helper_probe_single_connector_modes,
.destroy = drm_connector_cleanup,
.reset = drm_atomic_helper_connector_reset, // reset函数通常被用于在DRM模式切换或重置时,重置plane对象的状态。
.atomic_duplicate_state = drm_atomic_helper_connector_duplicate_state,
.atomic_destroy_state = drm_atomic_helper_connector_destroy_state,
};
// 在DRM中,encoder对象的状态更新和禁用等操作都是通过对应的CRTC对象的状态进行修改来实现的,因此在encoder_funcs结构体中确实不需要实现.atomic_duplicate_state成员。encoder对象的状态更新和禁用等操作都是由CRTC对象的状态更新和禁用等操作来完成的。
static const struct drm_encoder_funcs vkms_encoder_funcs = {
.destroy = drm_encoder_cleanup, // DRM框架中的一个helper函数,用于清理encoder对象的资源。
};
static const struct drm_mode_config_funcs vkms_mode_funcs = {
.fb_create = drm_fb_cma_create,
.atomic_check = drm_atomic_helper_check, // 遍历原子状态中的各个平面状态信息,并进行一些检查,以确保新的状态是合法的,并且可以被应用到显卡硬件上。
.atomic_commit = drm_atomic_helper_commit, // 根据原子状态中的各个平面状态信息,计算出新的显卡状态,并将其应用到显卡硬件上,从而实现显示输出
};
static const u32 vkms_formats[] = {
DRM_FORMAT_XRGB8888,
};
static void vkms_modeset_init(void)
{
drm_mode_config_init(&drm);
drm.mode_config.max_width = 8192;
drm.mode_config.max_height = 8192;
drm.mode_config.funcs = &vkms_mode_funcs;
drm_universal_plane_init(&drm, &primary, 0, &vkms_plane_funcs,
vkms_formats, ARRAY_SIZE(vkms_formats),
NULL, DRM_PLANE_TYPE_PRIMARY, NULL);
drm_plane_helper_add(&primary, &vkms_plane_helper_funcs);
drm_crtc_init_with_planes(&drm, &crtc, &primary, NULL, &vkms_crtc_funcs, NULL);
drm_crtc_helper_add(&crtc, &vkms_crtc_helper_funcs);
drm_encoder_init(&drm, &encoder, &vkms_encoder_funcs, DRM_MODE_ENCODER_VIRTUAL, NULL);
drm_connector_init(&drm, &connector, &vkms_connector_funcs, DRM_MODE_CONNECTOR_VIRTUAL);
drm_connector_helper_add(&connector, &vkms_conn_helper_funcs);
drm_mode_connector_attach_encoder(&connector, &encoder);
drm_mode_config_reset(&drm);
}
static const struct file_operations vkms_fops = {
.owner = THIS_MODULE,
.open = drm_open,
.release = drm_release,
.unlocked_ioctl = drm_ioctl,
.poll = drm_poll,
.read = drm_read,
.mmap = drm_gem_cma_mmap,
};
static struct drm_driver vkms_driver = {
.driver_features = DRIVER_MODESET | DRIVER_GEM | DRIVER_ATOMIC, // 给 driver_features 添加上 DRIVER_ATOMIC 标志位,告诉 DRM Core 该驱动支持 Atomic 操作。
.fops = &vkms_fops,
.dumb_create = drm_gem_cma_dumb_create,
.gem_vm_ops = &drm_gem_cma_vm_ops,
.gem_free_object_unlocked = drm_gem_cma_free_object,
.name = "vkms",
.desc = "Virtual Kernel Mode Setting",
.date = "20180514",
.major = 1,
.minor = 0,
};
static int __init vkms_init(void)
{
drm_dev_init(&drm, &vkms_driver, NULL);
vkms_modeset_init();
drm_vblank_init(&drm, 1);
drm.irq_enabled = true;
drm_dev_register(&drm, 0);
return 0;
}
module_init(vkms_init);
驱动分析1:
atomic_duplicate_state:创建一个新的原子状态,并将原子状态中的所有对象(包括CRTC、connector、encoder等等)进行复制。
atomic_destroy_state:释放原子状态占用的所有资源,包括CRTC、connector、encoder等等。具体来说,该函数会遍历原子状态对象中的所有对象,并依次释放它们占用的资源。另外,该函数还会释放原子状态对象本身。
驱动分析2:
drm_mode_config_funcs.atomic_commit() 接口是 atomic 操作的主要入口函数,必须实现。这里直接使用 drm_atomic_helper_commit() 函数实现。
drm_atomic_helper_commit() 函数的作用是帮助应用程序提交一组关联的对象的更新请求,并保证它们以原子方式生效,从而保证图形显示的稳定性和一致性。
在DRM中,图形显示涉及多个对象,包括CRTC(控制器)、encoder(编码器)和connector(连接器)等。这些对象之间存在复杂的依赖关系,更新请求必须以原子方式提交,以确保它们同时生效,从而避免出现不一致的图形显示。
驱动分析3:
在 plane/crtc/encoder/connector objects 初始化完成之后,一定要调用 drm_mode_config_reset() 来动态创建各个 pipeline 的软件状态(即 drm_xxx_state)。
驱动分析4:
与 Legacy 相比,Atomic 的 xxx_funcs 必须 实现如下接口:
reset()
atomic_duplicate_state()
atomic_destroy_state()
它们主要用于维护 drm_xxx_state 数据结构,不能省略!
驱动分析5:
drm_plane_helper_funcs.atomic_update() 必须实现!(不太理解)
这个函数用于在DRM原子模式下更新plane对象的状态。如果一个plane对象没有实现.atomic_update函数,那么在使用DRM原子模式时,就无法正确地更新该plane对象的状态,从而导致显示异常或者错误。
驱动分析6:
Atomic 操作依赖 VSYNC 中断(即 VBLANK 事件),因此需要使用 hrtimer 来提供软件中断信号。在驱动初始化时调用 drm_vblank_init(),在 VSYNC 中断处理函数中调用 drm_handle_vblank()。
五、总结
要实现一个 DRM KMS 驱动,通常需要实现如下代码:
fops、drm_driver
dumb_create、fb_create、atomic_commit
drm_xxx_funcs、drm_xxx_helper_funcs
drm_xxx_init()、drm_xxx_helper_add()
drm_dev_init()、drm_dev_register()
但这都只是表象,核心仍然是上面介绍的7个 objects,一切都围绕着这几个 objects 展开:
- 为了创建 crtc/plane/encoder/connector objects,需要调用 drm_xxx_init()。
- 为了创建 framebuffer object,需要实现 fb_create() callback。
- 为了创建 gem object,需要实现 dumb_create() callback。
- 为了创建 property objects,需要调用 drm_mode_config_init()。
- 为了让这些 objects 动起来,需要实现各种 funcs 和 helper funcs。
- 为了支持 atomic 操作,需要实现 atomic_commit() callback。
六、panel-simple.c文件分析
panel_simple.c只是DRM驱动中用于与RGB屏幕交互的一种手段(相当于connector),它仅适用于简单的设备和场景。
在panel_simple.c中的DRM驱动生成的/dev/dri/card0设备节点同时也会自动生成一个/dev/fb0设备节点,用于向后兼容传统的Framebuffer接口。
但需要注意的是,这个Framebuffer接口只能使用一些基本的功能,如显存映射和控制台输出等,而不能使用DRM提供的高级功能。
对于全志的DRM架构如下图所示:
七、参考内容
2.DRM(Direct Rendering Manager)学习简介_何小龙csdn_何小龙的博客-CSDN博客
3.DRM (Direct Rendering Manager) 的基本概念 - 代码天地
4.DRM几个重要的结构体及panel开发_drm panel_紫川宁520的博客-CSDN博客