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

文章目录

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设备结构体。

c 复制代码
struct drm_driver {
    ...
}

3.2、设备结构体

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

c 复制代码
struct drm_device {
    ...
}

3.3、DRM驱动注册

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

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

c 复制代码
struct drm_device *drm_dev_alloc(struct drm_driver *driver,
				 struct device *parent)

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

c 复制代码
int drm_dev_register(struct drm_device *dev, unsigned long flags)

对于drm设备的注销:

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

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

c 复制代码
drm_dev_unplug(struct drm_device *dev)

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

c 复制代码
#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、驱动支持的特性等等。但目前这些操作和硬件无关,但应用程序需要根据这些信息来完成相关的设置。

c 复制代码
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,
};

完整示例代码如下:

c 复制代码
#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字段。

c 复制代码
/**
 * 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驱动中用到的东西,例如各种锁、各种属性、各种模式设置的操作结构体。可以通过一个函数来初始化:

c 复制代码
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结构体如下:

c 复制代码
struct drm_plane {
    ...
}

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

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

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

c 复制代码
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字段,该函数如下:

c 复制代码
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的初始化,使用如下函数即可初始化:

c 复制代码
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函数集合只需要使用如下函数即可:

c 复制代码
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类:

c 复制代码
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为例:

c 复制代码
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

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

c 复制代码
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的一部分或者一个通道。

相关推荐
A小辣椒1 天前
TShark:Wireshark CLI 功能
linux
A小辣椒1 天前
TShark:基础知识
linux
AlfredZhao1 天前
OCI 明明分配了 200G 系统盘,为什么 df 只看到 30G?
linux·oci
AlfredZhao2 天前
vi 删除指定范围的行,不用再反复按 dd
linux·vi
用户9718356334662 天前
银河麒麟 KY10 申威(SW64) 安装 nginx-1.16.1-2.p01.ky10.sw_64.rpm 详细步骤
linux
猪脚踏浪2 天前
linux 拷贝文件或目录到指定的位置
linux
摇滚侠3 天前
Linux CentOS7 rpm 安装 MySQL 5.7
linux·运维·mysql
bush43 天前
嵌入式linux学习记录十四、术语
linux·嵌入式
载数而行5203 天前
Linux 11 动态监控指令top
linux
不会C语言的男孩3 天前
Linux 系统编程 · 第 8 章:进程基础
linux·c语言