20. DRM应用编程–legacy接口

该接口是过时了的DRM编程的应用开发接口, 但是该接口可以增加我们对于DRM的理解, 所以我先以 legacy接口 作为指导, 给大家了解一下DRM编程的具体流程以及DRM的实验, 下一章节再给大家讲述主流的atomic接口。

本小节将以四个实验带你进入DRM应用编程。

参考 《DRM(Direct Rendering Manager)》

本章的示例代码目录为:base_linux/screen/drm/drm-legacy

20.1. 最简单DRM(drm-single)

以下使用伪代码的方式,简单介绍如何编写一个最简单的DRM应用程序。

伪代码:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
    int main(int argc, char **argv)
    {
            //打开drm设备
            open("/dev/dri/card0");

            //获取drm的信息
            drmModeGetResources(...);

            //获取显示模式连接器的信息
            drmModeGetConnector(...);

            //创建dumb缓冲区
            drmIoctl(DRM_IOCTL_MODE_CREATE_DUMB);

            //将缓冲区绑定到FB对象
            drmModeAddFB(...);

            //将缓冲区映射到用户空间,并缓冲区的指针
            drmIoctl(DRM_IOCTL_MODE_MAP_DUMB);
            mmap(...);

            //缓冲区与特定的显示器连接器、显示模式等进行关联
            drmModeSetCrtc(crtc_id, fb_id, connector_id, mode);
    }

详细参考代码如下:

base_linux/screen/drm/drm-legacy/drm-single.c
  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
#define _GNU_SOURCE
#include <errno.h>
#include <fcntl.h>
#include <stdbool.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/mman.h>
#include <time.h>
#include <unistd.h>
#include <xf86drm.h>
#include <xf86drmMode.h>

struct drm_device {
	uint32_t width;			//显示器的宽的像素点数量
	uint32_t height;		//显示器的高的像素点数量
	uint32_t pitch;			//每行占据的字节数
	uint32_t handle;		//drm_mode_create_dumb的返回句柄
	uint32_t size;			//显示器占据的总字节数
	uint32_t *vaddr;		//mmap的首地址
	uint32_t fb_id;			//创建的framebuffer的id号
	struct drm_mode_create_dumb create ;	//创建的dumb
    struct drm_mode_map_dumb map;			//内存映射结构体
};

drmModeConnector *conn;	    //connetor相关的结构体
drmModeRes *res;		    //资源
uint32_t conn_id;           //connetor的id号
uint32_t crtc_id;           //crtc的id号
int fd;					    //文件描述符

#define RED 0XFF0000
#define GREEN 0X00FF00
#define BLUE 0X0000FF

struct drm_device buf;

static int drm_create_fb(struct drm_device *bo)
{
	/* create a dumb-buffer, the pixel format is XRGB888 */
	bo->create.width = bo->width;
	bo->create.height = bo->height;
	bo->create.bpp = 32;

	/* handle, pitch, size will be returned */
	drmIoctl(fd, DRM_IOCTL_MODE_CREATE_DUMB, &bo->create);

	/* bind the dumb-buffer to an FB object */
	bo->pitch = bo->create.pitch;
	bo->size = bo->create.size;
	bo->handle = bo->create.handle;
	drmModeAddFB(fd, bo->width, bo->height, 24, 32, bo->pitch,
			   bo->handle, &bo->fb_id);
	
	//每行占用字节数,共占用字节数,MAP_DUMB的句柄
	printf("pitch = %d ,size = %d, handle = %d \n",bo->pitch,bo->size,bo->handle);

	/* map the dumb-buffer to userspace */
	bo->map.handle = bo->create.handle;
	drmIoctl(fd, DRM_IOCTL_MODE_MAP_DUMB, &bo->map);

	bo->vaddr = mmap(0, bo->create.size, PROT_READ | PROT_WRITE,
			MAP_SHARED, fd, bo->map.offset);

	/* initialize the dumb-buffer with white-color */
	memset(bo->vaddr, 0xff,bo->size);

	return 0;
}

static void drm_destroy_fb(struct drm_device *bo)
{
	struct drm_mode_destroy_dumb destroy = {};
	drmModeRmFB(fd, bo->fb_id);
	munmap(bo->vaddr, bo->size);
	destroy.handle = bo->handle;
	drmIoctl(fd, DRM_IOCTL_MODE_DESTROY_DUMB, &destroy);
}

int drm_init()
{
	//打开drm设备,设备会随设备树的更改而改变,多个设备时,请留一下每个屏幕设备对应的drm设备
	fd = open("/dev/dri/card0", O_RDWR | O_CLOEXEC);
    if(fd < 0){
        printf("wrong\n");
        return 0;
    }

	//获取drm的信息
	res = drmModeGetResources(fd);
	crtc_id = res->crtcs[0];
	conn_id = res->connectors[0];
	//打印CRTCS,以及conneter的id
	printf("crtc = %d , conneter = %d\n",crtc_id,conn_id);

	conn = drmModeGetConnector(fd, conn_id);
	buf.width = conn->modes[0].hdisplay;
	buf.height = conn->modes[0].vdisplay;

	//打印屏幕分辨率
	printf("width = %d , height = %d\n",buf.width,buf.height);

	//创建framebuffer层
	drm_create_fb(&buf);

	//设置CRTCS
	drmModeSetCrtc(fd, crtc_id, buf.fb_id,
			0, 0, &conn_id, 1, &conn->modes[0]);

	return 0;
}

void drm_exit()
{
	drm_destroy_fb(&buf);
	drmModeFreeConnector(conn);
	drmModeFreeResources(res);
	close(fd);
}

int main(int argc, char **argv)
{
	int i;
	drm_init();
	sleep(2);
	//清屏设置颜色
	for(i=0;i<buf.width*buf.height;i++)
		buf.vaddr[i] = 0x123456;

	sleep(2);
	drm_exit();

	exit(0);
}

在main函数中,执行以下操作:

  • 第125行,调用drm_init函数进行初始化。

  • 第128-129行,使用循环将帧缓冲区的所有像素设置为0x123456,即改变屏幕颜色。

  • 第132行,调用drm_exit函数进行清理和退出。

在drm_init函数中,执行以下操作:

  • 第84行,打开DRM设备文件(/dev/dri/card0)。

  • 第91行,调用drmModeGetResources函数获取DRM设备的信息,包括CRTC(Cathode Ray Tube Controller)和连接器(Connector)的ID。

  • 第97行,调用drmModeGetConnector函数获取连接器的详细信息,包括屏幕的分辨率。

  • 第105行,调用drm_create_fb函数创建帧缓冲区。

  • 第108行,调用drmModeSetCrtc函数将帧缓冲区与CRTC关联。

在drm_create_fb函数中,执行以下操作:

  • 第41-44行,设置帧缓冲区的宽度、高度和像素格式。

  • 第47行,调用drmIoctl函数并传入DRM_IOCTL_MODE_CREATE_DUMB命令,创建一个dumb缓冲区。该函数会返回缓冲区的句柄、偏移量、行字节数等信息。

  • 第53行,使用drmModeAddFB函数将dumb缓冲区绑定到一个帧缓冲对象(Frame Buffer Object,FBO)上,并获取帧缓冲区的ID。

  • 第61行,调用drmIoctl函数并传入DRM_IOCTL_MODE_MAP_DUMB命令,将dumb缓冲区映射到用户空间。通过mmap函数获取缓冲区的指针。

  • 第67行,使用memset函数将帧缓冲区初始化为白色。

20.1.1. 详细代码分析

总体而言,四步就可以初始化drm的最小显示程序:

  1. 打开设备。

  2. 获取crtc_id,connector_id,以及它们的结构体信息。

  3. 创建Framebuffer。

  4. 设置CRTC。

初始化
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
int drm_init()
{
	//打开drm设备,设备会随设备树的更改而改变,多个设备时,请留一下每个屏幕设备对应的drm设备
	fd = open("/dev/dri/card0", O_RDWR | O_CLOEXEC);
    if(fd < 0){
        printf("wrong\n");
        return 0;
    }

	//获取drm的信息
	res = drmModeGetResources(fd);
	crtc_id = res->crtcs[0];
	conn_id = res->connectors[0];
	//打印CRTCS,以及conneter的id
	printf("crtc = %d , conneter = %d\n",crtc_id,conn_id);

	conn = drmModeGetConnector(fd, conn_id);
	buf.width = conn->modes[0].hdisplay;
	buf.height = conn->modes[0].vdisplay;

	//打印屏幕分辨率
	printf("width = %d , height = %d\n",buf.width,buf.height);

	//创建framebuffer层
	drm_create_fb(&buf);

	//设置CRTCS
	drmModeSetCrtc(fd, crtc_id, buf.fb_id,
			0, 0, &conn_id, 1, &conn->modes[0]);

	return 0;
}
  • 第6-11行,打开设备,drm设备的位置在/dev/dri/cardx,如果被占用无法打开会报错。

除了上述的打开方式还可以使用专用的drm设备打开函数,我们使用的是rockchip的芯片并且使用官方的驱动,所以使用”rockchip”。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
fd = drmOpen("rockchip", NULL);
if (fd < 0) {
   printf("failed to open rockchip drm\n");
   return fd;
}

//这是modetest程序里的设备匹配表
static const char * const modules[] = {
   "i915" , "amdgpu" , "radeon" , "nouveau" ,
   "vmwgfx" , "omapdrm" , "exynos" , "tilcdc",
   "msm" , "sti" , "tegra" , "imx-drm",
   "rockchip" , "atmel-hlcdc" , "fsl-dcu-drm",
   "vc4" , "virtio_gpu" , "mediatek" , "meson",
   "pl111" , "stm" , "sun4i-drm",
};
  • 第10-15行,获取drm的资源,获取并打印CRTC和connector的id号。

res是一个指向drmModeRes结构体的指针,drmModeRes结构体原型如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
//drmModeRes结构体原型
typedef struct _drmModeRes {

     int count_fbs;          //framebuffer的数量
     uint32_t *fbs;

     int count_crtcs;        //crtcs的数量
     uint32_t *crtcs;

     int count_connectors;   //connectors的数量
     uint32_t *connectors;

     int count_encoders;     //encodersr的数量
     uint32_t *encoders;

     uint32_t min_width, max_width;      //最小宽度和最大宽度
     uint32_t min_height, max_height;    //最小高度和最大高度

} drmModeRes, *drmModeResPtr;
  • 第17-22行,通过connectors的id获取connector的资源, 将屏幕的宽度和高度记录在结构体中,并将其打印。

res是一个指向drmModeConnector结构体的指针,drmModeConnector结构体原型如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
//conn结构体原型
typedef struct _drmModeConnector {
        uint32_t connector_id;              //自身的id
        uint32_t encoder_id;                //相连接的encoder_id
        uint32_t connector_type;
        uint32_t connector_type_id;
        drmModeConnection connection;       //connector的连接信息枚举
        uint32_t mmWidth, mmHeight;         /**< HxW in millimeters */
        drmModeSubPixel subpixel;           //子像素枚举

        int count_modes;                    //模式数量
        drmModeModeInfoPtr modes;           //存放分辨率,时序,时钟等信息的指针

        int count_props;                    //atomic模式使用的
        uint32_t *props; /**< List of property ids */
        uint64_t *prop_values; /**< List of property values */

        int count_encoders;                 //encoder的数量
        uint32_t *encoders;/**< List of encoder ids */
} drmModeConnector, *drmModeConnectorPtr;

//上文的modes指针
typedef struct _drmModeModeInfo {
    uint32_t clock;     //时钟信息,这里是mipi屏的时钟,单位KHz
    //hdisplay 宽分辨率,vdisplay 高分辨率,其他则为屏幕的timing
    uint16_t hdisplay, hsync_start, hsync_end, htotal, hskew;
    uint16_t vdisplay, vsync_start, vsync_end, vtotal, vscan;

    uint32_t vrefresh;  //屏幕刷新率

    uint32_t flags;
    uint32_t type;
    char name[DRM_DISPLAY_MODE_LEN];    //显示模式名字
} drmModeModeInfo, *drmModeModeInfoPtr;

//如果hdmi支持多种模式,就会有多个modes与之对应
  • 第24-26行:创建framebuffer,并mmap到用户内存上,后面详细讲解

  • 第27-30行:设置CRTCS,在这一步我们就可以在屏幕上看到东西了

framebuffer与crtc的关系图

未找到图片

drmModeSetCrtc函数原型:

1
2
3
int drmModeSetCrtc(int fd, uint32_t crtcId, uint32_t bufferId,
                uint32_t x, uint32_t y, uint32_t *connectors, int count,
                drmModeModeInfoPtr mode);
  1. fd:文件描述符。

  2. crtcId:要配置的crtc-id号。

  3. bufferId:要配置的framebuffer-id号。

  4. x:x轴偏移量,设置偏移量就可以显示framebuffer的其他区域。

  5. y:y轴偏移量,设置偏移量就可以显示framebuffer的其他区域。

  6. connectors:要连接的connectors-id号。

  7. count:connector_count。

  8. mode:想要使用的模式。

初始化就完成了,之后就可以通过framebuffer操作屏幕了。

创建framebuffer部分说明如下:

创建framebuffer
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
struct drm_device {

	struct drm_mode_create_dumb create ;	//创建的dumb
    struct drm_mode_map_dumb map;			//内存映射结构体

};

static int drm_create_fb(struct drm_device *bo)
{
	/* create a dumb-buffer, the pixel format is XRGB888 */
	bo->create.width = bo->width;
	bo->create.height = bo->height;
	bo->create.bpp = 32;

	/* handle, pitch, size will be returned */
	drmIoctl(fd, DRM_IOCTL_MODE_CREATE_DUMB, &bo->create);

	/* bind the dumb-buffer to an FB object */
	bo->pitch = bo->create.pitch;
	bo->size = bo->create.size;
	bo->handle = bo->create.handle;
	drmModeAddFB(fd, bo->width, bo->height, 24, 32, bo->pitch,
			   bo->handle, &bo->fb_id);
	
	//每行占用字节数,共占用字节数,MAP_DUMB的句柄
	printf("pitch = %d ,size = %d, handle = %d \n",bo->pitch,bo->size,bo->handle);

	/* map the dumb-buffer to userspace */
	bo->map.handle = bo->create.handle;
	drmIoctl(fd, DRM_IOCTL_MODE_MAP_DUMB, &bo->map);

	bo->vaddr = mmap(0, bo->create.size, PROT_READ | PROT_WRITE,
			MAP_SHARED, fd, bo->map.offset);

	/* initialize the dumb-buffer with white-color */
	memset(bo->vaddr, 0xff,bo->size);

	return 0;
}

  • 第3行, drm_mode_create_dumb 结构体用于构建framebuffer的属性。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
//结构体原型
struct drm_mode_create_dumb {
        __u32 height;        //高占用的像素点
        __u32 width;         //宽占用的像素点
        __u32 bpp;           //每个像素的位数
        __u32 flags;
   //下面的参数,创建之后会返回
        /* handle, pitch, size will be returned */
        __u32 handle;        //用于mmap
        __u32 pitch;         //每行字节数
        __u64 size;          //总字节数
};
  • 第4行:drm_mode_map_dumb 用于构建mmap内存区域。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
/* set up for mmap of a dumb scanout buffer */
struct drm_mode_map_dumb {
   /** Handle for the object being mapped. */
   __u32 handle;
   __u32 pad;
   /**
   * Fake offset to use for subsequent mmap call
   *
   * This is a fixed-size type for 32/64 compatibility.
   */
   __u64 offset;
};
  • 第10-14行,构建framebuffer的大小属性, 本次实验把framebuffer的大小构建成和显示区域一样的大小, 可以更改数值以构建更大的framebuffer属性。

  • 第15-16行,提交创建的属性,如果通过就会返回drm_mode_create_dumb的其他属性。

  • 第18-21行,将返回的属性值传入到全局变量 drm_device 中。

  • 第22-23行,创建Framebuffer。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
//函数原型及衍生
/* Creates a new framebuffer with an buffer object as its scanout buffer.*/
extern int drmModeAddFB(int fd, uint32_t width, uint32_t height, uint8_t depth,
                     uint8_t bpp, uint32_t pitch, uint32_t bo_handle,uint32_t *buf_id);
/* ...with a specific pixel format */
extern int drmModeAddFB2(int fd, uint32_t width, uint32_t height,
                      uint32_t pixel_format, const uint32_t bo_handles[4],
                      const uint32_t pitches[4], const uint32_t offsets[4],
                      uint32_t *buf_id, uint32_t flags);
extern drmModeFBPtr drmModeGetFB(int fd, uint32_t bufferId);

这里,我们主要讲述 drmModeAddFB 函数,其他函数可以自行阅读源码进行理解。

drmModeAddFB 函数分析:

  1. fd:文件描述符。

  2. width:framebuffer宽的像素点数量。

  3. height:framebuffer高的像素点数量。

  4. depth:framebuffer每个像素的实际位数,三个字节-RGB888。

  5. bpp:每个像素的占用的位数,四个字节-XRGB8888。

  6. pitch:每行占用的字节数:720x4。

  7. bo_handle:上文中创建dumb的返回值。

  8. buf_id:framebuffer的id号,以指针的形式传入,函数成功后,返回数值。

drmModeAddFB2 函数:创建特殊格式的framebuffer,例如YUV,C8格式。

drmModeGetFB 函数:获取framebuffer的资源。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
//结构体原型
typedef struct _drmModeFB {
   uint32_t fb_id;
   uint32_t width, height;
   uint32_t pitch;
   uint32_t bpp;
   uint32_t depth;
   /* driver specific handle */
   uint32_t handle;
} drmModeFB, *drmModeFBPtr;
  • 第28-34行:设置mmap,映射framebuffer的内存区域到用户空间里。

  • 第36行:将framebuffer区域全部变成白色。

经过这几步,就成功创建framebuffer并映射到用户空间中,供用户使用。

  1. 构建framebuffer区域的属性,创建dumb。

  2. 根据属性创建framebuffer。

  3. 根据句柄,映射framebuffer到用户空间。

注销drm
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
void drm_exit()
{
	drm_destroy_fb(&buf);
	drmModeFreeConnector(conn);
	drmModeFreeResources(res);
	close(fd);
}

static void drm_destroy_fb(struct drm_device *bo)
{
	struct drm_mode_destroy_dumb destroy = {};
	drmModeRmFB(fd, bo->fb_id);
	munmap(bo->vaddr, bo->size);
	destroy.handle = bo->handle;
	drmIoctl(fd, DRM_IOCTL_MODE_DESTROY_DUMB, &destroy);
}

注销drm的函数比较简单,只需以下几步:

  1. 删除framebuffer,取消内存的映射,摧毁dumb。

  2. 释放connector资源。

  3. 释放resource资源。

  4. 关闭文件描述符。

20.1.2. 编译

进入lubancat_rk_code_storage/base_linux/screen/drm/drm-legacy/drm-single目录。

1
2
 #编译
 make

方法2:

1
2
#编译
gcc -o drm-single drm-single.c `pkg-config --cflags libdrm` `pkg-config --libs libdrm`

20.1.3. 运行

1
2
#运行
./drm-single

现象如下:

屏幕先变白后变蓝,然后程序退出后屏幕变为黑屏。而在终端中可以看到程序输出了屏幕的crtc的id以及conneter的id、分辨率、每行的字节数以及总字节数。

20.2. 双缓冲DRM(drm-double.c)

双缓冲的原理是改变CRTC的扫描内存的位置。

20.2.1. 单framebuffer双缓冲

  • 创建一个两倍于显示区域的framebuffer,通过改变偏移量进行帧的切换,扫完一帧后就切换另一帧显示。

未找到图片
代码位置
1
base_linux/screen/drm/drm-legacy/drm-double-one-fb/drm-double-one-fb.c

20.2.2. 编译

方法1:

1
2
 #编译
 make

方法2:

1
2
#编译
gcc -o drm-double-one-fb drm-double-one-fb.c `pkg-config --cflags libdrm` `pkg-config --libs libdrm`

20.2.3. 运行

1
    ./drm-double-one-fb

现象:

  1. 屏幕变红

  2. 按下按键

  3. 屏幕变蓝

  4. 按下按键

  5. 屏幕变红

  6. 按下按键

  7. 退出程序

由于大致操作与最小程序差不多,这里我只讲与最小程序之间的差异。

创建framebuffer
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
static int drm_create_fb(struct drm_device *bo)
{
	/* create a dumb-buffer, the pixel format is XRGB888 */
	bo->create.width = bo->width;
	bo->create.height = bo->height*2;
	bo->create.bpp = 32;

	/* handle, pitch, size will be returned */
	drmIoctl(fd, DRM_IOCTL_MODE_CREATE_DUMB, &bo->create);

	/* bind the dumb-buffer to an FB object */
	bo->pitch = bo->create.pitch;
	bo->size = bo->create.size;
	bo->handle = bo->create.handle;
	drmModeAddFB(fd, bo->width, bo->height*2, 24, 32, bo->pitch,
			   bo->handle, &bo->fb_id);
	
	//每行占用字节数,共占用字节数,MAP_DUMB的句柄
	printf("pitch = %d ,size = %d, handle = %d \n",bo->pitch,bo->size,bo->handle);

	/* map the dumb-buffer to userspace */
	bo->map.handle = bo->create.handle;
	drmIoctl(fd, DRM_IOCTL_MODE_MAP_DUMB, &bo->map);

	bo->vaddr = mmap(0, bo->create.size, PROT_READ | PROT_WRITE,
			MAP_SHARED, fd, bo->map.offset);

	/* initialize the dumb-buffer with white-color */
	memset(bo->vaddr, 0x00,bo->size);

	return 0;
}

与原操作不同的是

  • 第5行,我把dumb的高度扩大一倍。

  • 第15行,在创建framebuffer的时候,我把高度扩展一倍。

  • 这时,得到的framebuffer是上一个实验的framebuffer大小的两倍。

main函数
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
int main(int argc, char **argv)
{
	int i;
	int size;
	drm_init();
	size = buf.width*buf.height;
	//buffer上层布满红色
	for(i=0;i<size;i++)
		buf.vaddr[i] = RED;
	//buffer下层布满蓝色
	for(i=size;i<size*2;i++)
		buf.vaddr[i] = BLUE;

	//输入字符
	getchar();
	//切换buffer下层
	drmModeSetCrtc(fd, crtc_id, buf.fb_id,
			0, 1080, &conn_id, 1, &conn->modes[0]);
	//注意根据实际修改1080这个参数,为当前测试屏幕height,如果是1024*600的屏幕,就将1080修改为600.	

	//输入字符
	getchar();
	//切换buffer上层
	drmModeSetCrtc(fd, crtc_id, buf.fb_id,
			0, 0, &conn_id, 1, &conn->modes[0]);
	//输入字符
	getchar();

	drm_exit();

  • 切换的操作使用drmModeSetCrtc()函数, 该函数可以使用偏移量将framebuffer向下移动一个显示区域的大小 这样,屏幕就会显示出不一样的内容。

20.2.4. 多framebuffer双缓冲

  • 创建两个framebuffer,通过切换framebuffer来进行双缓冲的显示。

未找到图片
代码位置
1
base_linux/screen/drm/drm-legacy/drm-double-muti-fb/drm-double-muti-fb.c

20.2.5. 编译

方法1:

1
2
 #编译
 make

方法2:

1
2
#编译
gcc -o drm-double-muti-fb drm-double-muti-fb.c `pkg-config --cflags libdrm` `pkg-config --libs libdrm`

20.2.6. 运行

1
    ./drm-double-muti-fb

现象:

  1. 屏幕变红

  2. 按下按键

  3. 屏幕变蓝

  4. 按下按键

  5. 屏幕变红

  6. 按下按键

  7. 退出程序

由于大致操作与最小程序差不多,这里我只讲与最小程序之间的差异。

drm初始化
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
struct drm_device buf[2];

int drm_init()
{
	//打开drm设备,设备会随设备树的更改而改变,多个设备时,请留一下每个屏幕设备对应的drm设备
	fd = open("/dev/dri/card0", O_RDWR | O_CLOEXEC);
    if(fd < 0){
        printf("wrong\n");
        return 0;
    }

	//获取drm的信息
	res = drmModeGetResources(fd);
	crtc_id = res->crtcs[0];
	conn_id = res->connectors[0];
	//打印CRTCS,以及conneter的id
	printf("crtc = %d , conneter = %d\n",crtc_id,conn_id);

	conn = drmModeGetConnector(fd, conn_id);
	buf[0].width = conn->modes[0].hdisplay;
	buf[0].height = conn->modes[0].vdisplay;

	buf[1].width = conn->modes[0].hdisplay;
	buf[1].height = conn->modes[0].vdisplay;

	//打印屏幕分辨率
	printf("width = %d , height = %d\n",buf[0].width,buf[0].height);

	//创建framebuffer层
	drm_create_fb(&buf[0]);
	drm_create_fb(&buf[1]);

	//设置CRTCS
	drmModeSetCrtc(fd, crtc_id, buf[0].fb_id,
			0, 0, &conn_id, 1, &conn->modes[0]);

	return 0;
}
  • 第1行,定义里两个结构体,他们分别存放framebuffer信息

  • 第20-24行,分别获取分辨率

  • 第29-31,分别设置framebuffer,并将他们的内存映射到用户空间内

  • 第33-36行,将屏幕设置成buf[0]的数据;

main函数
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
int main(int argc, char **argv)
{
	int i;
	int size0,size1;
	drm_init();
	size0 = buf[0].width*buf[0].height;
	size1 = buf[1].width*buf[1].height;
	//buffer上层布满红色
	for(i=0;i<size0;i++)
		buf[0].vaddr[i] = RED;
	//buffer下层布满蓝色
	for(i=0;i<size1;i++)
		buf[1].vaddr[i] = BLUE;
	
	drmModeSetCrtc(fd, crtc_id, buf[0].fb_id,
		0, 0, &conn_id, 1, &conn->modes[0]);

	//输入字符
	getchar();
	//切换buffer下层
	drmModeSetCrtc(fd, crtc_id, buf[1].fb_id,
			0, 0, &conn_id, 1, &conn->modes[0]);
	//输入字符
	getchar();
	//切换buffer上层
	drmModeSetCrtc(fd, crtc_id, buf[0].fb_id,
			0, 0, &conn_id, 1, &conn->modes[0]);
	//输入字符
	getchar();

	drm_exit();

	exit(0);
}
  • 可以看到与单framebuffer相比,格式几乎一模一样, 最大的区别是传入的fb_id以及没有偏移量的产生, 即操作传入的fb_id就可以操作屏幕

20.2.7. 小总结

这两个方法各有各的好处, 单framebuffer简单,写起来更快捷,适合于图像简单的场景, 多framebuffer复杂,但是扩展性更强,在多framebuffer的情况下, 可以单独扩展每个framebuffer的大小,适合图像比较复杂的场景。

20.3. 页翻转

drmModePageFlip()的功能和drmModeSetCrtc()一样是用于更新显示内容的, 但是它和drmModeSetCrtc()最大的区别在于, drmModePageFlip()只会等到VSYNC到来后才会真正执行framebuffer切换动作, 而drmModeSetCrtc()则会立即执行framebuffer切换动作。 drmModeSetCrtc()对于某些硬件来说,很容易造成撕裂(tear effect)问题, 而drmModePageFlip()则不会造成这种问题。

由于drmModePageFlip()本身是基于VSYNC事件机制的,因此底层DRM驱动必须支持VBLANK事件。

未找到图片

20.3.1. 页翻转实验

代码位置
1
base_linux/screen/drm/drm-legacy/drm-page-flip/drm-page-flip.c

20.3.2. 编译

方法1:

1
2
 #编译
 make

方法2:

1
2
#编译
gcc -o drm-page-flip drm-page-flip.c `pkg-config --cflags libdrm` `pkg-config --libs libdrm`

20.3.3. 运行

1
    ./drm-page-flip

现象:

  1. 屏幕变红

  2. 按下按键

  3. 屏幕变蓝

  4. 按下按键

  5. 屏幕变红

  6. 按下按键

  7. 退出程序

20.3.4. 程序分析:

main函数
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51

static void drm_page_flip_handler(int fd, uint32_t frame,
				    uint32_t sec, uint32_t usec,
				    void *data)
{
	static int i = 0;
	uint32_t crtc_id = *(uint32_t *)data;

	if(i==0)
		i=1;
	else
		i=0;
  
	drmModePageFlip(fd, crtc_id, buf[i].fb_id,
			DRM_MODE_PAGE_FLIP_EVENT, data);
}

int main(int argc, char **argv)
{
	int i;
	int size0,size1;
	ev.version = DRM_EVENT_CONTEXT_VERSION;
	ev.page_flip_handler = drm_page_flip_handler;

	drm_init();
	size0 = buf[0].width*buf[0].height;
	size1 = buf[1].width*buf[1].height;
	//buffer1布满红色
	for(i=0;i<size0;i++)
		buf[0].vaddr[i] = RED;
	//buffer2布满蓝色
	for(i=0;i<size1;i++)
		buf[1].vaddr[i] = BLUE;
	
	drmModePageFlip(fd, crtc_id, buf[0].fb_id,
			DRM_MODE_PAGE_FLIP_EVENT, &crtc_id);
	//输入字符
	getchar();
	//切换buffer2
	drmHandleEvent(fd, &ev);
	//输入字符
	getchar();
	//切换buffer1
	drmHandleEvent(fd, &ev);
	//输入字符
	getchar();

	drm_exit();

	exit(0);
}

该实验与多framebuffer双缓冲的结构差不多,现在我来讲述实验中不一样的地方:

  • 首先需要先定义 drmEventContext,然后设置 ev.versionev.drm_page_flip_handler

  • 当运行 drmHandleEvent(fd, &ev); 时,就会出触发 drm_page_flip_handler() 函数,进行页翻转。

drmModePageFlip函数原型:

1
2
//函数原型
int drmModePageFlip ( int fd, uint32_t crtc_id, uint32_t fb_id, uint32_t flags, void * user_data )
  1. fd:打开的DRM设备的文件描述符。

  2. crtc_id:CRTC要修改framebuffer的CRTC ID。

  3. fb_id:Framebuffer要显示的Framebuffer ID。

  4. flags:影响操作的标志。支持的值是: DRM_MODE_PAGE_FLIP_ASYNC :立即翻转,而不是vblank, DRM_MODE_PAGE_FLIP_EVENT :发送翻页事件

  5. user_data:如果请求vblank事件,则页面翻转处理程序使用的数据。

20.4. drm-planes

Planes有个非常强的特性:支持多个plane叠加,图层可以自由的剪裁,拉伸以及合成。

未找到图片

LubanCat2系列支持双屏异显, 而LubanCat-Zero和LubanCat-1系列仅支持单个屏幕显示, 它们的HDMI和mipi接口的CRTC都具有三个图层, 一个PRIMARY图层,两个OVERLAY图层, 如果想要设置鼠标图层,可以按照下列操作:

未找到图片

注意

LubanCatQ1系列仅支持HDMI显示

20.4.1. drm-planes实验

本实验基于最简单DRM(drm-single)进行编写,对其进行部分修改而成。

代码位置
1
base_linux/screen/drm/drm-legacy/drm-planes/drm-planes.c

20.4.2. 编译

方法1:

1
2
 #编译
 make

方法2:

1
2
#编译
gcc -o drm-planes drm-planes.c `pkg-config --cflags libdrm` `pkg-config --libs libdrm`

20.4.3. 运行

1
    ./drm-planes

实验现象:

  • ./drm-planes

  • 按下按键

  • 屏幕显示红绿蓝三条横

未找到图片 未找到图片

20.4.4. 程序分析:

新增初始化代码
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
drmModePlaneRes *plane_res;//图层资源
uint32_t plane_id[3];   //图层id数组

	drmSetClientCap(fd, DRM_CLIENT_CAP_UNIVERSAL_PLANES, 1);
	plane_res = drmModeGetPlaneResources(fd);
	printf("count_planes = %d\n",plane_res-> count_planes);
	for(i=0;i<3;i++){
		plane_id[i] = plane_res->planes[i];
		printf("planes[%d]= %d\n",i,plane_id[i]);
	}

drmModePlaneRes 结构体原型:

1
2
3
4
typedef struct _drmModePlaneRes {
     uint32_t count_planes;  //planes的数量
     uint32_t *planes;       //指向planes-id数组的结构体指针
} drmModePlaneRes, *drmModePlaneResPtr;
  • drmSetClientCap(fd, DRM_CLIENT_CAP_UNIVERSAL_PLANES, 1); 设置这个的话,可以让DRM的核心驱动程序(DRM CORE)暴露所有的planes到用户空间里,即开了这个我们才可以操作planes。

  • 第5-10行:获取planes的资源,并将所有的图层id储存到数组中,打印出来。

main函数
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34

int main(int argc, char **argv)
{
	int i;
	int j = 0;
	drm_init();

	//显示三色
	for(j=0;j<3;j++){
		for(i =j*buf.width*buf.height/3;i< (j+1)*buf.width*buf.height/3;i++)
			buf.vaddr[i] = color_table[j];
	}

	getchar();
	//将framebuffer上2/3的区域放到图层一上,
	//此时屏幕改变,将的framebuffer区域拉伸到整个屏幕中
	drmModeSetPlane(fd, plane_id[0], crtc_id, buf.fb_id, 0,
			0, 0, buf.width, buf.height,
			0 << 16, 0 << 16, buf.width << 16, buf.height/3*2 << 16);

	getchar();
	//将framebuffer区域缩放一倍放到图层二上,把图层二的位置放到屏幕的下方
	//叠加在图层一上,可以看到图层二覆盖了图层一的部分区域
	drmModeSetPlane(fd, plane_id[1], crtc_id, buf.fb_id, 0,
			buf.width/4, buf.height/2, buf.width/2, buf.height/2,
			0 << 16, 0 << 16, buf.width << 16, buf.height << 16);

	getchar();

	drm_exit();
	
	exit(0);
}
	
  • drmModeSetPlane(); 函数原型:

1
2
3
4
5
6
extern int drmModeSetPlane(int fd, uint32_t plane_id, uint32_t crtc_id,
                        uint32_t fb_id, uint32_t flags,
                        int32_t crtc_x, int32_t crtc_y,
                        uint32_t crtc_w, uint32_t crtc_h,
                        uint32_t src_x, uint32_t src_y,
                        uint32_t src_w, uint32_t src_h);

里面的参数可以对照下图:

未找到图片

上图就是把帽子通过上面的函数,把帽子叠加到图像中去

ret = drmModeSetPlane( fd, plane_id, crtc_id, fb_id, flags,crtc_x, crtc_y,crtc_w, crtc_h, src_x << 16, src_y <<16, src_w<<16, src_h<<16);

上述函数可以对照上图的移动。

  • 第17-20行:将framebuffer(0,0)开始截取上面三分之二的矩形拉伸到整个屏幕中

  • 第22-26行:将整个framebuffer缩小一倍放到图层二的下半部分的中间,然后显示图层二(显示会覆盖图层一)

  • 当 SRC 与 CRTC 的 X/Y 不相等时,则实现了平移的效果;

  • 当 SRC 与 CRTC 的 W/H 不相等时,则实现了缩放和拉伸的效果;

  • 当 SRC 与 FrameBuffer 的 W/H 不相等时,则实现了裁剪的效果

20.5. legacy接口函数

在前面我们使用过的legacy接口有下列:

1
2
drmModeSetCrtc();
drmModeSetPlane();
  • 两个函数具有一定的重合性,部分功能两个都可以同时起作用。

  • 在drm驱动激活时,framebuffer会与PRIMARY Planes绑定在一起, drmModeSetCrtc()可以直接通过操作framebuffer, 而不用通过设置Planes达到显示的效果。

  • drmModeSetPlane()则是更正规的操作Planes的函数。

  • 在双缓冲设计,我们同样可以像drmModeSetCrtc()操作那样去使用drmModeSetPlane()来构建双缓冲, 图像大小一致设置偏移量或者切换buffer。

20.6. 总结

DRM应用编程–legacy接口的框架基于最简单DRM(drm-single)的实验, 其他的操作也是在最简单DRM(drm-single)的实验的框架上进行修改, 步骤:

  1. 打开设备(两种方法)。

  2. 创建page-flip-handle (page-flip中使用)。

  3. 获取plane资源 (planes中使用)。

  4. 获取drm的资源,包括crtcs和connecter位号。

  5. 获取connecter的基本信息,高和宽的像素点。

  6. 创建framebuffer,创建mmap区, 双缓冲drm需要额外配置framebuffer

  7. 设置CRTCS 或 page-flip

  8. 设置plane资源 (planes中使用)。

  9. 操作屏幕。

20.7. 双屏异显

rk3568的双屏异显和多屏异显时Planes的设置会有些奇怪。

开启三个屏幕时的分配:

  1. VP0–》 HDMI 可插拔 -》 二

  2. VP1–》 MIPI 主屏幕 -》 三

  3. VP2–》 DP转VGA,可插拔 -》 一

当我们打开了双屏异显时,这里以同时打开HDMI和MIPI屏为例:

  • 通过设置 drminit() 中的 crtc_id = res->crtcs[0]; 该函数为获取HDMI的CRTC

  • 通过设置 drminit() 中的 crtc_id = res->crtcs[1]; 该函数为获取MIPI屏幕的CRTC

planes的设置需要多几步:

1
2
3
4
5
//获取所有的planes资源
     plane_res = drmModeGetPlaneResources(fd);
     for(i=0;i<plane_res->count_planes;i++){
             plane_id[i] = plane_res->planes[i];
     }

planes的绑定信息,需要我们使用函数来获取:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
//通过plane_id获取drmModePlanePtr的资源
extern drmModePlanePtr drmModeGetPlane(int fd, uint32_t plane_id);

typedef struct _drmModePlane {
        uint32_t count_formats;       //格式数量
        uint32_t *formats;            //格式指针
        uint32_t plane_id;

        uint32_t crtc_id;             //绑定的crtc_id
        uint32_t fb_id;               //绑定的crtc_id

        uint32_t crtc_x, crtc_y;
        uint32_t x, y;

        uint32_t possible_crtcs;      //可能绑定的crtc_id
        uint32_t gamma_size;
} drmModePlane, *drmModePlanePtr;
  • 在RK系列中由于图层是可变的,除了PRIMARY_PLANE可以从crtc_id中得到

  • 其他图层需要possible_crtcs中获得,possible_crtcs =1 –》 HDMI , possible_crtcs =1 –》 MIPI

因此在操作图层时需要知道哪个CRTC对应哪些Planes。

这里直接提供答案:

  1. HDMI -》 plane_id[0],plane_id[2],plane_id[5]

  2. MIPI -》 plane_id[1],plane_id[3],plane_id[4]

我们设置图层时既可以依据上方来进行。