Linux驱动开发进阶(七)- DRM驱动程序计划
1、媒介[*]学习参考书籍以及本文涉及的示例程序:李山文的《Linux驱动开发进阶》
[*]本文属于个人学习后的总结,不太具备讲授功能。
2、DRAM(KMS、GEM)
2.1、KMS
KMS由frame buffer、plane、CRTC、encoder、connector、vblank、property组成
https://i-blog.csdnimg.cn/img_convert/97461f4847c7304ea72c202e1dea472c.png
https://i-blog.csdnimg.cn/img_convert/3d2bb77859088303920776041637deb3.png
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设备节点:
https://i-blog.csdnimg.cn/img_convert/776ca2b74361429c0c8fbbbca1e3cb59.png
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_openstatic 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,如下所示:
https://i-blog.csdnimg.cn/img_convert/2c3ad864fb3df66066e2aa1d3762c08b.png
https://i-blog.csdnimg.cn/img_convert/8f1ca9ee6a0fdf39a3065b6f300c35bb.png
此中min_width设置显示的最小宽度,别的三个也是同一个意思。funcs设置mode_config的操纵集合:
https://i-blog.csdnimg.cn/img_convert/298bdf48a4978ea01ff19f9425bb1ca3.png
此中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个字段:
https://i-blog.csdnimg.cn/img_convert/fb569d32b9432cd8534c785325ae5056.png
分别是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, ...)
https://i-blog.csdnimg.cn/img_convert/87d49cc548c6daf2dc6066ece49f0458.png
例如下面定义了一个图层操纵结构体,然后使用drm_universal_plane_init对其初始化:
https://i-blog.csdnimg.cn/img_convert/bdc41cd697c6fa4e96bbec3b275f787b.png
上面的plane_funcs为图层的操纵集合,假设此处我们没有物理图层,因此我们可以使用drm提供的软件图层来实现。上面的funcs函数集合实现了图层的基本操纵,但这些操纵都是通用的,即与硬件无关的,实际中很多显示设备的图层操纵都不一样,因此,DRM将这些不一样的操纵统一归纳为helper函数,这也就是funcs_helper函数集合的存在原因。例如我们现在的显示器是一块液晶屏,其驱动是ST7789,这块液晶屏使用SPI总线通讯,很显然没有硬件图层,因为我们必须实现helper函数来完成图层的图像添补操纵。例如下面这段代码:
https://i-blog.csdnimg.cn/img_convert/ee0ea86fe809841741b73e066a0bbd56.png
https://i-blog.csdnimg.cn/img_convert/5d3ee79294b78bb803291bc6cfe18bce.png
https://i-blog.csdnimg.cn/img_convert/14bc57ef8d97036230ea90c45cf7382f.png
https://i-blog.csdnimg.cn/img_convert/1c8d4fd0878a3585ea26c86a77da1a65.png
https://i-blog.csdnimg.cn/img_convert/1b3aced818c3547a32d41b64d4b8d5d3.png
上面我们定义了一个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);
https://i-blog.csdnimg.cn/img_convert/4334016d53167680da9e87eb186ce8c2.png
https://i-blog.csdnimg.cn/img_convert/184c8cd418bcf61917aeca7aab62d1e5.png
上面的代码是对于没有硬件CRTC而言的,直接将图像数据发送到显示器上,而对于有CRTC的SoC而言,我们就必要将数据传送给CRTC的输入接口中,实际上也是一样的,判断图像数据格式,然后将其搬运到CRTC中。对于有CRTC硬件环境下,其操纵会更加简单,我们只必要指定数据位置,然后设置寄存器,触发CRTC进行刷新图形即可。有些硬件甚至只必要在开始设置好寄存器后,无需进行软件操纵,硬件会主动完成将plane中的数据刷新到CRTC中。
https://i-blog.csdnimg.cn/img_convert/10d0941bb09344b30da70d119ac59a28.png
https://i-blog.csdnimg.cn/img_convert/88be3761187e71755ed0de3ba7936cdb.png
3.4.2、crtc初始化
drm中的crtc结构体如下:
https://i-blog.csdnimg.cn/img_convert/e058bc64cf6934525fdd19d185e23ce3.png
此中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, ...)
https://i-blog.csdnimg.cn/img_convert/4de5f99d4dc00a41e573b984768e6f57.png
如下图所示,我们定义一个crtc结构体,然后使用drm_crtc_init_with_planes对齐初始化:
https://i-blog.csdnimg.cn/img_convert/21b2a871aa618f0d0a3a11a773b50ebf.png
上面的代码中funcs结构体与硬件有关,此中enable_vblank为场消隐使能,disable_vblank为场消隐关闭,如果有CRTC硬件,则应该实现各自的功能,这里如果没有硬件,则不必要做消隐处置惩罚。我们再来看helper函数集合,如下,我们定义了一个helper函数集合:
https://i-blog.csdnimg.cn/img_convert/e08aaa26aa4e48bb6ae273c288060963.png
crtc_helper_funcs结构体中初始化了crtc_helper_mode_valid字段,该字段用来设置CRTC的显示模式,例如上面我们每次设置CRTC模式都直接将我们定义的mylcd_mode赋值给CRTC的mode,即调用drm_crtc_helper_mode_valid_fixed函数即可。
https://i-blog.csdnimg.cn/img_convert/972a7107ac37e3661b83e6f742e0c348.png
上面的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,代码如下:
https://i-blog.csdnimg.cn/img_convert/3a515a1d81a6f6f648e1c85fa07d39e2.png
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;
struct iosys_map data;
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;
/* Vendor & product info */
u8 mfg_id;
u8 prod_code;
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;
/* Detailing timings 1-4 */
struct detailed_timing detailed_timings;
/* Number of 128 byte ext. blocks */
u8 extensions;
/* Checksum */
u8 checksum;
} __attribute__((packed));
8.2、panel
panel并不是drm组件中的必要组件,而是为了方便开发者获取显示器信息。将显示器抽象成了panel,即面板。同时edid的信息交给panel来完成。信赖这个大家应该常见,在设备树中常出现,一样平常一个屏幕对应一个panel,里面主要有具体屏幕的时序。panel结构体如下:
https://i-blog.csdnimg.cn/img_convert/e340e0fd173ed83247b17ab627d21a87.png
此中funcs为panel的操纵集合,来完成获取显示器时序,关闭显示器,开启显示器等操纵。
8.3、bridge
比如现在有edp,hdmi,rgb等不同的显示设备。对于soc而言,其只有一个显示控制器,经过encoder的信号不能直接输出到connector,而是必要转换为符合具体显示器格式的信号,即引入了bridge。
bridge并不一定是桥接芯片的抽象,也可能是soc的一部分或者一个通道。
免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!更多信息从访问主页:qidao123.com:ToB企服之家,中国第一个企服评测及商务社交产业平台。
页:
[1]