跳转至

1. drm kms

KMS全称是Kernel Mode Setting,这里的mode是指显示控制器的mode,详见下面对drm_mode的分析。与KMS相对应的是User Mode Setting,早期Unix的Xorg几乎完整实现了一套图形栈,此时Mode Setting这项功能主要是由用户态的DDX(Device Depedent Driver)实现的。UMS由于存在各种各样的问题,已经被放弃,目前主流驱动已经在多年以前完成了KMS接口的迁移,并将Mode Setting相关的实现从用户态移动到了内核态。本文着重分析内核KMS相关功能的框架实现。

事实上,显示控制器的设计从最初(CRT显示器时代)到现在(LCD显示器时代)并没有根本性的变化。KMS将整个显示控制器的显示pipeline抽象成以下几个部分:

  • plane
  • crtc
  • encoder
  • connector

其中每一个部分的含义可以参考内核文档,这里不赘述,这里只分析其在内核框架中是如何实现的。

对象管理

对于这几个对象,DRM框架将其称作“对象”,有一个公共的基类struct 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);
};

其中id和type分别为这个对象在KMS子系统中的ID和类型(即上面提到的几种)。注意所有的drm_mode_object的id共用一个namespace,保存在drm_device->mode_config.object_idr中。因此,框架提供了drm_mode_object_find函数用于查找对应id的对象。当前DRM框架中存在如下的对象类型:

#define DRM_MODE_OBJECT_CRTC 0xcccccccc
#define DRM_MODE_OBJECT_CONNECTOR 0xc0c0c0c0
#define DRM_MODE_OBJECT_ENCODER 0xe0e0e0e0
#define DRM_MODE_OBJECT_MODE 0xdededede
#define DRM_MODE_OBJECT_PROPERTY 0xb0b0b0b0
#define DRM_MODE_OBJECT_FB 0xfbfbfbfb
#define DRM_MODE_OBJECT_BLOB 0xbbbbbbbb
#define DRM_MODE_OBJECT_PLANE 0xeeeeeeee
#define DRM_MODE_OBJECT_ANY 0

drm_mode_object的定义中即可发现其实现了两个比较重要的功能:

  • 引用计数及生命周期管理
  • 属性管理

属性在DRM中由struct drm_property表示,其本质是一个DRM_MODE_OBJECT_PROPERTY类型的drm_mode_object。一个drm_mode_object的所有属性保存在其内部的drm_object_properties中,其实现如下:

struct drm_object_properties {
  int count;
  struct drm_property *properties[DRM_OBJECT_MAX_PROPERTY];
  uint64_t values[DRM_OBJECT_MAX_PROPERTY];
};
可以看到每一个对象最多可以有24个属性。这里注意一个实现细节,drm_property表示一个属性对象,描述属性的类型(如整形,range,浮点数等)、名称和取值范围(约束)。drm_object_properties中的properties保存属性的类型,而values保存对应类型的值。这是因为同一类型的对象基本上都共有特定名称和类型的属性,独立的属性对象使得我们不需要为在每一个对象中都保存同样的属性名称和类型。对象的属性可以通过drm_object_property_*函数操作。

helper架构

helper架构是我起的名,知道是指什么东西就好。DRM子系统的API比较难抽象,简单来说就是硬件各有各的不同,很多情况下,驱动可以使用一个共同的实现,而在其它情况下,驱动需要提供自己的实现。因此,DRM驱动核心的接口使用了helper架构,其基本思想是通过一组回调函数抽象特定组件的操作,比如drm_connector_funcs,同时又使用另外一组helper函数给出了原先那组回调函数的通用实现,让开发最者实现这组helper函数抽象出的回调函数即可。

这样双层的实现即能保证开发者有足够高的自由度(完全不用helper函数),也能简化开发者的开发(使用helper函数),同时提供给开发者hook特定helper函数的能力。下面以drm_connector为例说明helper架构的实现与使用方式。

正常情况下,创建drm_connector对象时需要提供struct drm_connector_funcs回调函数组,而使用helper函数时,可以直接用helper函数填充对应回调函数:

static const struct drm_connector_funcs vc4_hdmi_connector_funcs = {
        .detect = vc4_hdmi_connector_detect,
        .fill_modes = drm_helper_probe_single_connector_modes,
        .destroy = vc4_hdmi_connector_destroy,
        .reset = drm_atomic_helper_connector_reset,
        .atomic_duplicate_state = drm_atomic_helper_connector_duplicate_state,
        .atomic_destroy_state = drm_atomic_helper_connector_destroy_state,
};
事实上helper函数并不万能,只是抽象出了大多数驱动程序应该共享的行为,而特定于硬件的部分,则需要以回调函数的形式提供给helper函数,这个回调函数组由struct drm_connector_helper_funcs提供。在创建drm_connector时,需要通过drm_connector_helper_add函数注册。函数将对应的回调函数对象的地址保存在了drm_connector中的helper_private指针中,如下:
static inline void drm_connector_helper_add(struct drm_connector *connector,
                                            const struct drm_connector_helper_funcs *funcs)
{
        connector->helper_private = funcs;
}
这一套实现位于include/drm/drm_modeset_helper_vtables.h中,其他的DRM对象都有类似的实现,可以详细阅读drm_connector_helper_funcs的注释,理解其中对应的回调函数的用途。在实现DRM驱动时,helper架构会频繁用到,合理掌握helper函数可以极大简化开发,提升驱动程序的兼容性。

驱动入口

我们知道drm_device用于抽象一个完整的DRM设备,而其中与Mode Setting相关的部分则由drm_mode_config进行管理。为了让一个drm_device支持KMS相关的API,DRM框架要求驱动:

  • 注册drm_driver时,driver_features标志位中需要存在DRIVER_MODESET
  • 在probe函数中调用drm_mode_config_init函数初始化KMS框架,本质上是初始化drm_device中的mode_config结构体
  • 填充mode_config中int min_width, min_height; int max_width, max_height的值,这些值是framebuffer的大小限制
  • 设置mode_config->funcs指针,本质上是一组由驱动实现的回调函数,涵盖KMS中一些相当基本的操作
  • 最后初始化drm_device中包含的drm_connectordrm_crtc等对象

我们知道注册一个支持KMS的DRM设备时,会在/dev/drm/下创建一个card%d文件,用户态可以通过打开该文件,并对文件描述符做相应的操作实现相应的功能。该文件描述符对应的文件操作回调函数(filesystem_operations)位于drm_driver中,并由驱动程序填充。典型如下:

static const struct file_operations vkms_driver_fops = {
        .owner          = THIS_MODULE,
        .open           = drm_open,
        .mmap           = drm_gem_mmap,
        .unlocked_ioctl = drm_ioctl,
        .compat_ioctl   = drm_compat_ioctl,
        .poll           = drm_poll,
        .read           = drm_read,
        .llseek         = no_llseek,
        .release        = drm_release,
};
基本都为DRM框架预先提供好的helper函数,可以根据驱动需要灵活改变。

CRTC

Framebuffer

内核文档 framebuffer应该是唯一一个与硬件无关的抽象了。驱动程序需要提供自己的framebuffer实现,其主要入口就是前面提到的drm_mode_config_funcs->fb_create回调函数。驱动程序通过扩展drm_framebuffer结构体可以向framebuffer中加入自己私有的字段。

struct virtio_gpu_framebuffer {
        struct drm_framebuffer base;
        struct virtio_gpu_fence *fence;
};
创建framebuffer时,需要通过drm_framebuffer_init函数将framebuffer初始化,并导出到用户空间。fb_create函数接受一个drm_mode_fb_cmd2类型的参数:
struct drm_mode_fb_cmd2 {
        __u32 fb_id;
        __u32 width;
        __u32 height;
        __u32 pixel_format; /* fourcc code from drm_fourcc.h */
        __u32 flags; /* see above flags */
        __u32 handles[4];
        __u32 pitches[4]; /* pitch for each plane */
        __u32 offsets[4]; /* offset of each plane */
        __u64 modifier[4]; /* ie, tiling, compress */
};
其中最重要的就是handle,handle是Buffer Object的指针,该Buffer Object就是被创建framebuffer的存储后端。

TODO framebuffer releated operation

Plane

内核文档

plane由drm_plane表示,其本质是对显示控制器中scanout硬件的抽象。简单来说,给定一个plane,可以让其与一个framebuffer关联表示进行scanout的数据,同时控制控制scanout时进行的额外操作,比如colorspace的改变,旋转、拉伸等操作。drm_plane是与硬件强相关的,显示控制器支持的plane是固定的,其支持的功能也是由硬件决定的。

对于drm_plane的分析,我们从其结构体定义入手。首先可以看到,一个plane必须要与一个drm_deivce关联,且一个drm_device中支持的所有plane都被保存在一个链表中。drm_plane中存有一个mask,用以表示该drm_plane可以绑定的CRTC。同时drm_plane中也保存了一个format_types数组,表示该plane支持的framebuffer格式。

所有的drm_plane必为三种类型之一:

  • Primary - 主plane,一般控制整个显示器的输出。CRTC必须要有一个这样的plane。
  • Curosr - 表示鼠标光标,可选。
  • Overlay - 叠加plane,可以在主plane上叠加一层输出,可选。

来回顾一点历史:内核向用户态导出的接口实际上不包含Primary Plane,对应plane的接口只能操作Cursor PlaneOverlay Plane,后期提供了一个Universial Plane特性,使得用户态API可以直接操作Primary Plane。在明白这个历史遗留问题后,对drm_plane的实现就好理解了。