一. 概念
贴一张图(这张图可能和其他地方的不一样,我作了修改,后文会解释):
DRM架构包含如上图所示的几个部分,显示一张图像,就是分别获取上面的各个节点信息,进行配置,将将fb -> planes -> crtc -> encoder - > connector连接起来 ,最终实现把FB的内容推送到显示器显示的目的。
1.1 FrameBuffer帧缓存
FB是图像实际存放的一段内存区域,是我们想要显示的原始图像所在空间,一般存放的是RGBX或者YUV的图像数据。这些数据可能是虚拟地址空间或者DMA、CMA等内存区域,和显示模组没有硬件关系。
1.2 Plane图层
显示的基本单元,每个图层可能关联一个FB的数据。图层是显示模组硬件,每个图层叠加顺序、支持的透明度等一般是硬件决定的,如果超过图层数量限制就需要软件来额外处理。
注:
很多示例图上是把FB和Planes分开的输入到CRTC的,但我理解,所有要显示的FB都必须要依赖到一个图层才能显示,除非CRTC本身有一个背景层,这个背景层不能叫plane,其它在此基础上做透明叠加等操作的额外图层才能叫Plane???而现在通用的显示控制器一般是可以自由设定图层顺序和透明度的,每个plane可能是等效的,可以分别配置z-order来随意叠加,所以我是把所有FB直接指向了planes。如有错误请指正。
1.3 CRTC 扫描(CRT)控制器
把一个或者多个图层的数据作处理,形成视觉上的一张图像。比如有两个图层,一个是播放文件的视频图层,另一个是UI操作图层,将其叠加为一个新的图,送到下一级(Encoder)作显示。使用CRT猜测是沿用旧时的称呼。
1.4 Encoder 编码器
将CRTC生成的最终画面转化为不同的显示信号。
1.5 Connector连接器
将信号传送到显示器。
2. API简述
drm用户态接口是依赖于libdrm,因此需要先编译安装该库文件。使用libdrm来显示的主要api如下。
2.1 获取显示资源drmModeGetResources
drmModeRes *res;
int drm_fd;
drm_fd = open("/dev/dri/card0", O_RDWR | O_CLOEXEC);
res = drmModeGetResources(fd);
drmModeRes 包含了上述各个模组的统计信息和指针:
typedef struct _drmModeRes {
int count_fbs;
uint32_t *fbs;
int count_crtcs;
uint32_t *crtcs;
int count_connectors;
uint32_t *connectors;
int count_encoders;
uint32_t *encoders;
uint32_t min_width, max_width;
uint32_t min_height, max_height;
} drmModeRes, *drmModeResPtr;
因此我们通过该接口,可以获取到当前系统的显示资源。
2.2 获取连接器drmModeGetConnector
drmModeConnector *connector;
connector = drmModeGetConnector(drm_fd, res->connectors[0]);
当然一个系统可能具有多个连接器(比如笔记本电脑一般同时支持LCD、HDMI两个屏幕),我们一般是循环遍历drmModeRes的所有connector,找到我们想要显示的设备。
接下来看看连接器有哪些东东:
typedef struct _drmModeConnector {
uint32_t connector_id;
uint32_t encoder_id; /**< Encoder currently connected to */
uint32_t connector_type;
uint32_t connector_type_id;
drmModeConnection connection;
uint32_t mmWidth, mmHeight; /**< HxW in millimeters */
drmModeSubPixel subpixel;
int count_modes;
drmModeModeInfoPtr modes;
int count_props;
uint32_t *props; /**< List of property ids */
uint64_t *prop_values; /**< List of property values */
int count_encoders;
uint32_t *encoders; /**< List of encoder ids */
} drmModeConnector, *drmModeConnectorPtr;
drmModeConnection 是连接状态,比如HDMI设备的插拔检测。
另外连接器有两个重要的信息,一个是对应的编码器Encoder,另一个是显示模式drmModeMode。
count_encoders和 *encoder是其对应的编码器数目和指针。
drmModeModeInfoPtr 是显示模式,包含分辨率(hdisplay,vdisplay),同步,和帧率(vrefresh)等信息:
typedef struct _drmModeModeInfo {
uint32_t clock;
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;
我们需要根据分辨率这些信息来获取支持的图像buf大小,以创建FB。
2.3 drmModeGetEncoder从connector获取对应的Encoder
drmModeEncoder *encoder;
encoder = drmModeGetEncoder(drm_fd, conn->encoders[i]);
不同的connector对应的Encoder可能存在差异(HDMI的编码和LCD肯定不一样),因此我们需要从connector中
drmModeEncoder 信息如下,
typedef struct _drmModeEncoder {
uint32_t encoder_id;
uint32_t encoder_type;
uint32_t crtc_id;
uint32_t possible_crtcs;
uint32_t possible_clones;
} drmModeEncoder, *drmModeEncoderPtr;
2.4 获取CRTC
上述,取得drmModeEncoder 时,drmModeEncoder包含了possible_crtcs,这是当前Encoder可用的CRTC掩码值,所以需要根据这个值拿到drmModeRes中crtcs数组的index,以取得对应的CRTC。
for (j = 0; j < res->count_crtcs; j++) {
if (encoder->possible_crtcs & (1 << j)) {
crtc_id = res->crtcs[j];
if (crtc_id > 0) {
my_drm->crtc_id = crtc_id;
drmModeFreeEncoder(encoder);
return 0;
}
}
crtc_id = -1;
}
2.5 创建FB
现在,我们拿到了Connector、Encoder、CRTC,就可以创建FB了。为啥没有遇到Plane呢?因为每个CRTC会有默认的plane,简单的单图层显示不需要设置plane接口。
为了方便,我们定义一个结构体:
struct my_drm_buffer {
void *fb_base;
uint32_t width;
uint32_t height;
uint32_t stride;
uint32_t size;
uint32_t handle;
uint32_t buf_id;
unsigned long paddr;
};
创建FB,我们需要用到上述的各个节点,以及drmModeModeInfo对应的关键信息。
不过在此之前,我们先要用DRM_IOCTL_MODE_CREATE_DUMB创建一个dumb buffer,笨蛋buffer。
2.5.1 创建dumb buffer
int drm_create_fb(int fd, int index, struct my_drm_buffer *buf)
{
struct drm_mode_create_dumb creq;
int ret;
memset(&creq, 0, sizeof(creq));
creq.width = buf->width;
creq.height = buf->height;
creq.bpp = 32;
ret = drmIoctl(fd, DRM_IOCTL_MODE_CREATE_DUMB, &creq);
if (ret < 0) {
ALOGE("cannot create dumb buffer[%d]", index);
return ret;
}
dumb buffer如下,后三个参数值是ioctl之后返回的。其中handle是后续操作dumb buffer需要的句柄。
struct drm_mode_create_dumb {
__u32 height;
__u32 width;
__u32 bpp;
__u32 flags;
/* handle, pitch, size will be returned */
__u32 handle;
__u32 pitch;
__u64 size;
};
接下来添加FB。
2.5.2 添加FB
buf->stride = creq.pitch;
buf->size = creq.size;
buf->handle = creq.handle;
ret = drmModeAddFB(fd, buf->width, buf->height, creq.bpp, creq.bpp,
buf->stride, buf->handle, &buf->buf_id);
我们将handle等参数传入,会得到一个buf_id。这个buf_id就是后续我们操作FB的句柄。
2.5.3 映射buffer
映射buffer包含两个步骤,映射dumb buf,映射FB。完成之后我们就可以通过buf->fb_base来访问这个buf了。接下来我们把FB默认的画面清为白色0xFF。
struct drm_mode_map_dumb mreq;
memset(&mreq, 0, sizeof(mreq));
mreq.handle = buf->handle;
ret = drmIoctl(fd, DRM_IOCTL_MODE_MAP_DUMB, &mreq);
if (ret) {
ALOGE("Map buffer[%d] dump ioctl fail", index);
}
buf->fb_base = mmap(0, buf->size, PROT_READ | PROT_WRITE, MAP_SHARED,
fd, mreq.offset);
if (buf->fb_base == MAP_FAILED) {
ALOGE("Cannot mmap dumb buffer[%d]", index);
}
memset(buf->fb_base, 255, buf->size);
2.5.4 设置模式drmModeSetCrtc
drmModeSetCrtc将我们上述获取的信息设置到底层,
drmModeSetCrtc(fd, my_drm->crtc_id, buf->buf_id,
0, 0, pipe->con_ids, pipe->num_cons,
pipe->mode);
该接口的主要功能是设置drm_mode_crtc的参数,函数内部填充该结构体,并通过ioctl传递到驱动。
struct drm_mode_crtc {
__u64 set_connectors_ptr;
__u32 count_connectors;
__u32 crtc_id; /**< Id */
__u32 fb_id; /**< Id of framebuffer */
__u32 x; /**< x Position on the framebuffer */
__u32 y; /**< y Position on the framebuffer */
__u32 gamma_size;
__u32 mode_valid;
struct drm_mode_modeinfo mode;
};
其中第4第5个参数是显示的坐标偏移,其他参数暂未用到,后面再说。
最后,我们可以直接操作FB的buf,将数据送显了。
问题
我们申请了一个buffer,并将其送显。当我们在刷新的时候,显示端就会同步显示刷新的过程。
因此,通常我们需要申请两个buffer,一个是送显的buffer,另一个是我们填充数据的buffer。同一个buffer操作互斥,填充buffer0的时候,送显的是buffer1,等待buffer0填充结束后再送显buffer0,然后填充buffer1。如此反复交替使用。
本文主要对libdrm的接口和数据作梳理,详细请查阅libdrm-2.4.110 estsmodetestmodetest.c代码。
参考:
modeset.c简化描述
最简单的DRM应用程序