Linux驱动开发进阶(七)- DRM驱动程序设计

发布于:2025-04-11 ⋅ 阅读:(43) ⋅ 点赞:(0)

1、前言

  1. 学习参考书籍以及本文涉及的示例程序:李山文的《Linux驱动开发进阶》
  2. 本文属于个人学习后的总结,不太具备教学功能。

2、DRAM(KMS、GEM)

2.1、KMS

KMS由frame buffer、plane、CRTC、encoder、connector、vblank、property组成

2.2、GEM

3、DRM

3.1、驱动结构体

drm驱动结构体很大,里面都是一些操作函数。面对这么多函数,我们先不深究,继续看drm设备结构体。

struct drm_driver {
    ...
}

3.2、设备结构体

drm设备结构体里面不再是操作函数,而是一些版本号、标志位等等。

struct drm_device {
    ...
}

3.3、DRM驱动注册

drm驱动设备并没有完全符合总线设备驱动模型,drm驱动依赖的不再是总线,而是依赖一个父设备,所以注册drm驱动时,需要指定父设备。drm驱动和drm设备的绑定是显示的。

先分配一个drm设备结构体,第一个参数为drm驱动结构体,第二参数为父设备:

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

然后注册,第一个参数为设备地址,第二个参数为是否执行驱动的load函数,一般填0,即不执行:

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

对于drm设备的注销:

void drm_dev_unregister(struct drm_device *dev)		// 用于注销drm驱动
void drm_dev_put(struct drm_device *dev)			// 用于减少drm设备的引用计数

对于支持热插拔的drm驱动而言,应该使用如下函数:

drm_dev_unplug(struct drm_device *dev)

下面是一个简单的例子,说明如何注册drm驱动:

#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/init.h>
#include <linux/kobject.h>
#include <linux/sysfs.h>
#include <drm/drm_drv.h>
#include <drm/drm_file.h>
#include <drm/drm_ioctl.h>
#include <drm/drm_gem.h>

static struct drm_device *drm_dummy_dev;

static void dummy_release(struct device *dev)
{
	
}

static struct device dummy_dev = {
	.init_name	= "dummy",
	.release	= dummy_release,
};

static struct drm_driver drm_dummy_driver = {
	.name	= "drm-test",
	.desc	= "drm dummy test",
	.date	= "20250409",
	.major	= 1,
	.minor	= 0,
};

static int __init drm_test_init(void)
{

	int ret;

	ret = device_register(&dummy_dev);
	if(ret < 0)
	{
		printk(KERN_ERR "device register error!\n");
		return ret;
	}

	drm_dummy_dev = drm_dev_alloc(&drm_dummy_driver, &dummy_dev);
	ret = drm_dev_register(drm_dummy_dev, 0);
	
	return ret;
}

static void __exit drm_test_exit(void)
{

	drm_dev_unregister(drm_dummy_dev);
	drm_dev_put(drm_dummy_dev);
	device_unregister(&dummy_dev);
}

module_init(drm_test_init);
module_exit(drm_test_exit);

MODULE_LICENSE("GPL");
MODULE_AUTHOR("qq.com");
MODULE_VERSION("0.1");
MODULE_DESCRIPTION("drm dummy test");

编译成ko文件,加载后,会在/dev/dri/下出现cardX设备节点:

3.4、DRM模式设置

我们知道drm驱动包括KMS和GEM两个部分,其中KMS就是内核模式设置,也是最重要的部分。内核模式就是内核用来显示图像的一种方式,KMS由多个组件构成,包括frame buffer、plane、CRTC、encoder、connector、vblank、property。下面将通过代码来体会这些组件在drm驱动中的作用。

现在我们基于上一个示例程序,实现drm_driver的fops,这些open,release等操作都是由drm子系统实现的。其中drm_ioctl函数实现了应用程序对drm驱动的信息获取,例如版本信息、总线ID、驱动支持的特性等等。但目前这些操作和硬件无关,但应用程序需要根据这些信息来完成相关的设置。

static const struct file_operations drm_fops = {
	.owner 	= THIS_MODULE,
	.open	= drm_open,
	.release= drm_release,
	.unlocked_ioctl = drm_ioctl,
	.poll	= drm_poll,
	.read	= drm_read,
};

完整示例代码如下:

#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/init.h>
#include <linux/kobject.h>
#include <linux/sysfs.h>
#include <drm/drm_drv.h>
#include <drm/drm_file.h>
#include <drm/drm_ioctl.h>
#include <drm/drm_gem.h>

static struct drm_device *drm_dummy_dev;

static void dummy_release(struct device *dev)
{
	
}

static struct device dummy_dev = {
	.init_name	= "dummy",
	.release	= dummy_release,
};

static const struct file_operations drm_fops = {
	.owner 	= THIS_MODULE,
	.open	= drm_open,
	.release= drm_release,
	.unlocked_ioctl = drm_ioctl,
	.poll	= drm_poll,
	.read	= drm_read,
};

static struct drm_driver drm_dummy_driver = {
	.fops	= &drm_fops,
	.name	= "drm-test",
	.desc	= "drm dummy test",
	.date	= "20250409",
	.major	= 1,
	.minor	= 0,
};

static int __init drm_test_init(void)
{

	int ret;

	ret = device_register(&dummy_dev);
	if(ret < 0)
	{
		printk(KERN_ERR "device register error!\n");
		return ret;
	}

	drm_dummy_dev = drm_dev_alloc(&drm_dummy_driver, &dummy_dev);
	ret = drm_dev_register(drm_dummy_dev, 0);
	
	return ret;
}drm_open

static void __exit drm_test_exit(void)
{

	drm_dev_unregister(drm_dummy_dev);
	drm_dev_put(drm_dummy_dev);
	device_unregister(&dummy_dev);
}

module_init(drm_test_init);
module_exit(drm_test_exit);

MODULE_LICENSE("GPL");
MODULE_AUTHOR("qq.com");
MODULE_VERSION("0.1");
MODULE_DESCRIPTION("drm dummy test");

但我们目前还没有实现模式设置要做的事情。实现模式设置实际就是实现各个组件,这些组件包括frame buffer、plane、CRTC、encoder、connector、vblank、property。设置之前,先初始化设置模式结构体,即drm_device中的mode_config字段。

/**
 * 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 {
    ...
}

该结构体实在太膨大了。主要是一些关于drm驱动中用到的东西,例如各种锁、各种属性、各种模式设置的操作结构体。可以通过一个函数来初始化:

int drmm_mode_config_init(struct drm_device *dev)

调用该函数后,还需要手动初始化mode_config的width、height、funcs、async_page_flio,如下所示:

其中min_width设置显示的最小宽度,其它三个也是同一个意思。funcs设置mode_config的操作集合:

其中drm_gem_fb_create用来创建frame buffer,其他两个为额外的检查和提交函数。最后async_page_flip=false表示不支持异步plane。初始化完毕后,还需要初始化plane,crtc,encoder,connector组件。

3.4.1、plane初始化

plane表示图层,即每个显示设备至少有一个图层,每个图层都有自己的frame buffer,图层和frame buffer配合就可以实现图像的保存与处理,drm的plane结构体如下:

struct drm_plane {
    ...
}

drm_plane结构体也很膨大,我们看其中比较重要的5个字段:

分别是crtc指针、fb指针、funcs指针、funcs_helper指针,保存这四个指针是因为图层位于fb和crtc之间,硬件图层处理的数据来自frame buffer中的图像数据,而处理完毕后的数据需要传送给crtc进行信号编码。

初始化drm_plane结构体的函数如下:

int drm_universal_plane_init(struct drm_device *dev, struct drm_plane *plane,
     uint32_t possible_crtcs,
     const struct drm_plane_funcs *funcs,
     const uint32_t *formats, unsigned int format_count,
     const uint64_t *format_modifiers,
     enum drm_plane_type type,
     const char *name, ...)

例如下面定义了一个图层操作结构体,然后使用drm_universal_plane_init对其初始化:

上面的plane_funcs为图层的操作集合,假设此处我们没有物理图层,因此我们可以使用drm提供的软件图层来实现。上面的funcs函数集合实现了图层的基本操作,但这些操作都是通用的,即与硬件无关的,实际中很多显示设备的图层操作都不一样,因此,DRM将这些不一样的操作统一归纳为helper函数,这也就是funcs_helper函数集合的存在原因。例如我们现在的显示器是一块液晶屏,其驱动是ST7789,这块液晶屏使用SPI总线通信,很显然没有硬件图层,因为我们必须实现helper函数来完成图层的图像填充操作。例如下面这段代码:

上面我们定义了一个plane_helper_funcs结构体,其初始化了prepare_fb,该回调函数实现了一个简单的GEM函数,即用于为FB分配内存的接口,然后是初始化atomic_check刷图前的检测工作,返回0表示准备就绪,返回非0表示存在错误,这里我们实现了一个简单的drm_plane_atomic_check函数来对传入的图像参数进行检测,首先获取当前plane的参数,参数保存在state中,然后获取当前crtc的参数,参数也保存在state中,使用drm_atomic_helper_check_plane_state函数对这两个传入的参数进行验证,如果plane和crtc的参数符合,则返回0,否则返回非0。atomic_update回调函数用来初始化刷图函数,该函数用来将plane(图层)上的数据刷新到液晶屏中,由于作者这里使用的是带控制器的液晶屏,内部有控制器和显存,没有硬件CRTC,因此作者这里直接将plane中的数据刷新到LCD上。需要注意的是,刷图的过程中,需要区分pix的像素格式,这里我们支持两种像素格式,即DRM_FORMAT_XRGB8888和DRM_FORMAT_RGB565,对于大部分的视频文件而言,其像素为XRGB8888格式,因此要想支持视频播放,则必须支持DRM_FORMAT_XRGB8888格式,对于一般的虚拟终端而言,其像素格式大多为DRM_FORMAT_RGB565,因此这里也需要对其进行支持。最后,我们需要调用drm_plane_helper_add函数来初始化plane中helper_private字段,该函数如下:

drm_plane_helper_add(primary, &plane_helper_funcs);

上面的代码是对于没有硬件CRTC而言的,直接将图像数据发送到显示器上,而对于有CRTC的SoC而言,我们就需要将数据传送给CRTC的输入接口中,实际上也是一样的,判断图像数据格式,然后将其搬运到CRTC中。对于有CRTC硬件情况下,其操作会更加简单,我们只需要指定数据位置,然后设置寄存器,触发CRTC进行刷新图形即可。有些硬件甚至只需要在开始配置好寄存器后,无需进行软件操作,硬件会自动完成将plane中的数据刷新到CRTC中。

3.4.2、crtc初始化

drm中的crtc结构体如下:

其中primary用来保存drm的主plane结构体,cursor保存drm的光标plane结构体,funcs保存着crtc的操作集合,helper_private保存着crtc的helper操作集合。crtc的初始化,使用如下函数即可初始化:

int drm_crtc_init_with_planes(struct drm_device *dev, struct drm_crtc *crtc,
    struct drm_plane *primary,
    struct drm_plane *cursor,
    const struct drm_crtc_funcs *funcs,
    const char *name, ...)

如下图所示,我们定义一个crtc结构体,然后使用drm_crtc_init_with_planes对齐初始化:

上面的代码中funcs结构体与硬件有关,其中enable_vblank为场消隐使能,disable_vblank为场消隐关闭,如果有CRTC硬件,则应该实现各自的功能,这里如果没有硬件,则不需要做消隐处理。我们再来看helper函数集合,如下,我们定义了一个helper函数集合:

crtc_helper_funcs结构体中初始化了crtc_helper_mode_valid字段,该字段用来设置CRTC的显示模式,例如上面我们每次设置CRTC模式都直接将我们定义的mylcd_mode赋值给CRTC的mode,即调用drm_crtc_helper_mode_valid_fixed函数即可。

上面的mylcd_mode结构体定义了其模式为:屏幕宽度像素为240,高度像素为320,屏幕宽度尺寸为37mm,屏幕高度尺寸为49mm。
atomic_check用来完成CRTC参数的检测,和plane一样,首先获取CRTC的参数,参数保存在state中,然后调用相关的API来检测参数是否合法。atomic_enable用来开启显示前的工作,即使能CRTC刷图时序,atomic_disable用来关闭显示,即失能CRTC刷图时序。作者这里由于使用的是带有控制器的液晶屏,没有使用SoC中显示控制器,因此这里enable和disable回调函数对应着屏幕的初始化工作以及退出工作。

我们为crtc添加helper函数集合只需要使用如下函数即可:

static inline void drm_crtc_helper_add(crtc, &crtc_helper_funcs);

该函数会将crtc中的helper_funcs字段初始化为crtc_helper_funcs,代码如下:

3.4.3、encoder初始化

3.4.4、connect初始化

4、示例说明

不管何种drm驱动,核心都离不开plane、crtc、encoder、connector。

以一个分辨率为240*240,控制芯片为st7789v的液晶显示屏为例,讲解如何开发一个drm驱动。示例程序在:https://gitee.com/li-shan-asked/linux-advanced-development-code/tree/master/part7/drm-st7789-6.1.37

这里的st7789v就是一个显示控制芯片,可以类比为现代SoC里的显示控制模块。所以很多drm驱动都由原厂bsp工程师实现。

5、DRM Simple Display框架

DRM的KMS核心是四个结构体,即p1ane、crtc、encoder、connector。这四个结构体对应着四个显示组件,在软件层面是必须存在的,但硬件不一定存在。既然软件层面是一定存在的,那就可以将这四个结构体封蔽为一个类,这便是DRM的pipe类:

struct drm_simple_display_pipe {
	struct drm_crtc crtc;
	struct drm_plane plane;
	struct drm_encoder encoder;
	struct drm_connector *connector;

	const struct drm_simple_display_pipe_funcs *funcs;
};

将上面的示例程序使用drm simple display框架改进后,可以参考:https://gitee.com/li-shan-asked/linux-advanced-development-code/tree/master/part7/drm-st7789-5.16.17

6、DRM热插拔

在connector组件中实现热插拔检测。有两种检测方式,中断和POLL。中断的话,是申请了一个线程化中断,在顶半步的下半部的线程函数里实现热插拔检测和事件发生。POLL就是初始化了延时工作队列,每10s轮询热插拔引脚的状态。

实际的热插拔应该具实际情况来完善。比如在每次插上屏幕后都应该初始化屏幕。在用户态结合udev规则完成其它业务。

7、DRM中的plane update

以上面的示例程序中的plane_update为例:

static void drm_plane_atomic_update(struct drm_plane *plane,
					struct drm_atomic_state *state)
{
	int ret;
	int idx;
	struct drm_rect rect;
	struct drm_plane_state *old_pstate,*plane_state;
	struct iosys_map map[DRM_FORMAT_MAX_PLANES];
	struct iosys_map data[DRM_FORMAT_MAX_PLANES];
	struct drm_display *drm = container_of(plane, struct drm_display, primary);
	struct iosys_map dst_map = IOSYS_MAP_INIT_VADDR(drm->buffer);
	plane_state = drm_atomic_get_new_plane_state(state, plane);
	old_pstate = drm_atomic_get_old_plane_state(state, plane);
	drm_atomic_helper_damage_merged(old_pstate, plane_state, &rect);
	if (!drm_dev_enter(plane->dev, &idx))
		return;
	ret = drm_gem_fb_begin_cpu_access(plane_state->fb, DMA_FROM_DEVICE);
	if (ret)
		return;
	ret = drm_gem_fb_vmap(plane_state->fb, map, data);
	if (ret)
		return;
	if(plane_state->fb->format->format == DRM_FORMAT_XRGB8888) {
		drm_fb_xrgb8888_to_rgb565(&dst_map, NULL, data, plane_state->fb, &rect, 1);
	}
	else if(plane_state->fb->format->format == DRM_FORMAT_RGB565) {
		drm_fb_memcpy(&dst_map, NULL, data, plane_state->fb, &rect);
	}
	drm_gem_fb_vunmap(plane_state->fb, map);
	fb_set_win(drm, rect.x1, rect.y1, rect.x2 - 1, rect.y2 - 1);
	gpiod_set_value(drm->dc, 1);  //高电平,发送数据
	spi_write(drm->spi, drm->buffer, (rect.x2 - rect.x1) * (rect.y2 - rect.y1)*2);
	drm_dev_exit(idx);
}

上面的代码中,定义了两个drmplane_state,一个是老的plane,一个是新的plane。老的plane记录着图像渲染之前的图像,而新的plane记录着渲染的区域,我们将渲染的区域又称为damage区城。为了得到最终的图形,需要使用drm_atomic_helper_damage_merged函数来合并两个plane区域,需要变化的区域被记录到drm_rect中。因此我们在刷新图片的时候,并不需要将整个plane进行刷新,只需要刷新drnrect部分即可。

8、DRM相关结构

8.1、edid

扩展显示器识别数据。给驱动程序获取显示的硬件相关信息。结构体定义如下:

struct edid {
	u8 header[8];
	/* Vendor & product info */
	u8 mfg_id[2];
	u8 prod_code[2];
	u32 serial; /* FIXME: byte order */
	u8 mfg_week;
	u8 mfg_year;
	/* EDID version */
	u8 version;
	u8 revision;
	/* Display info: */
	u8 input;
	u8 width_cm;
	u8 height_cm;
	u8 gamma;
	u8 features;
	/* Color characteristics */
	u8 red_green_lo;
	u8 black_white_lo;
	u8 red_x;
	u8 red_y;
	u8 green_x;
	u8 green_y;
	u8 blue_x;
	u8 blue_y;
	u8 white_x;
	u8 white_y;
	/* Est. timings and mfg rsvd timings*/
	struct est_timings established_timings;
	/* Standard timings 1-8*/
	struct std_timing standard_timings[8];
	/* Detailing timings 1-4 */
	struct detailed_timing detailed_timings[4];
	/* Number of 128 byte ext. blocks */
	u8 extensions;
	/* Checksum */
	u8 checksum;
} __attribute__((packed));

8.2、panel

panel并不是drm组件中的必要组件,而是为了方便开发者获取显示器信息。将显示器抽象成了panel,即面板。同时edid的信息交给panel来完成。相信这个大家应该常见,在设备树中常出现,一般一个屏幕对应一个panel,里面主要有具体屏幕的时序。panel结构体如下:

其中funcs为panel的操作集合,来完成获取显示器时序,关闭显示器,开启显示器等操作。

8.3、bridge

比如现在有edp,hdmi,rgb等不同的显示设备。对于soc而言,其只有一个显示控制器,经过encoder的信号不能直接输出到connector,而是需要转换为符合具体显示器格式的信号,即引入了bridge。

bridge并不一定是桥接芯片的抽象,也可能是soc的一部分或者一个通道。