IT评测·应用市场-qidao123.com技术社区

标题: Linux驱动开发进阶(七)- DRM驱动程序计划 [打印本页]

作者: 不到断气不罢休    时间: 2025-4-11 18:18
标题: Linux驱动开发进阶(七)- DRM驱动程序计划
1、媒介

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设备结构体。
  1. struct drm_driver {
  2.     ...
  3. }
复制代码
3.2、设备结构体

drm设备结构体里面不再是操纵函数,而是一些版本号、标志位等等。
  1. struct drm_device {
  2.     ...
  3. }
复制代码
3.3、DRM驱动注册

drm驱动设备并没有完全符合总线设备驱动模子,drm驱动依赖的不再是总线,而是依赖一个父设备,所以注册drm驱动时,必要指定父设备。drm驱动和drm设备的绑定是显示的。
先分配一个drm设备结构体,第一个参数为drm驱动结构体,第二参数为父设备:
  1. struct drm_device *drm_dev_alloc(struct drm_driver *driver,
  2.                                  struct device *parent)
复制代码
然后注册,第一个参数为设备地址,第二个参数为是否实行驱动的load函数,一样平常填0,即不实行:
  1. int drm_dev_register(struct drm_device *dev, unsigned long flags)
复制代码
对于drm设备的注销:
  1. void drm_dev_unregister(struct drm_device *dev)                // 用于注销drm驱动
  2. void drm_dev_put(struct drm_device *dev)                        // 用于减少drm设备的引用计数
复制代码
对于支持热插拔的drm驱动而言,应该使用如下函数:
  1. drm_dev_unplug(struct drm_device *dev)
复制代码
下面是一个简单的例子,说明怎样注册drm驱动:
  1. #include <linux/module.h>
  2. #include <linux/kernel.h>
  3. #include <linux/init.h>
  4. #include <linux/kobject.h>
  5. #include <linux/sysfs.h>
  6. #include <drm/drm_drv.h>
  7. #include <drm/drm_file.h>
  8. #include <drm/drm_ioctl.h>
  9. #include <drm/drm_gem.h>
  10. static struct drm_device *drm_dummy_dev;
  11. static void dummy_release(struct device *dev)
  12. {
  13.        
  14. }
  15. static struct device dummy_dev = {
  16.         .init_name        = "dummy",
  17.         .release        = dummy_release,
  18. };
  19. static struct drm_driver drm_dummy_driver = {
  20.         .name        = "drm-test",
  21.         .desc        = "drm dummy test",
  22.         .date        = "20250409",
  23.         .major        = 1,
  24.         .minor        = 0,
  25. };
  26. static int __init drm_test_init(void)
  27. {
  28.         int ret;
  29.         ret = device_register(&dummy_dev);
  30.         if(ret < 0)
  31.         {
  32.                 printk(KERN_ERR "device register error!\n");
  33.                 return ret;
  34.         }
  35.         drm_dummy_dev = drm_dev_alloc(&drm_dummy_driver, &dummy_dev);
  36.         ret = drm_dev_register(drm_dummy_dev, 0);
  37.        
  38.         return ret;
  39. }
  40. static void __exit drm_test_exit(void)
  41. {
  42.         drm_dev_unregister(drm_dummy_dev);
  43.         drm_dev_put(drm_dummy_dev);
  44.         device_unregister(&dummy_dev);
  45. }
  46. module_init(drm_test_init);
  47. module_exit(drm_test_exit);
  48. MODULE_LICENSE("GPL");
  49. MODULE_AUTHOR("qq.com");
  50. MODULE_VERSION("0.1");
  51. 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、驱动支持的特性等等。但现在这些操纵和硬件无关,但应用程序必要根据这些信息来完成相关的设置。
  1. static const struct file_operations drm_fops = {
  2.         .owner         = THIS_MODULE,
  3.         .open        = drm_open,
  4.         .release= drm_release,
  5.         .unlocked_ioctl = drm_ioctl,
  6.         .poll        = drm_poll,
  7.         .read        = drm_read,
  8. };
复制代码
完整示例代码如下:
  1. #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 = {
  2.         .owner         = THIS_MODULE,
  3.         .open        = drm_open,
  4.         .release= drm_release,
  5.         .unlocked_ioctl = drm_ioctl,
  6.         .poll        = drm_poll,
  7.         .read        = drm_read,
  8. };
  9. 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字段。
  1. /**
  2. * struct drm_mode_config - Mode configuration control structure
  3. * @min_width: minimum fb pixel width on this device
  4. * @min_height: minimum fb pixel height on this device
  5. * @max_width: maximum fb pixel width on this device
  6. * @max_height: maximum fb pixel height on this device
  7. * @funcs: core driver provided mode setting functions
  8. * @fb_base: base address of the framebuffer
  9. * @poll_enabled: track polling support for this device
  10. * @poll_running: track polling status for this device
  11. * @delayed_event: track delayed poll uevent deliver for this device
  12. * @output_poll_work: delayed work for polling in process context
  13. * @preferred_depth: preferred RBG pixel depth, used by fb helpers
  14. * @prefer_shadow: hint to userspace to prefer shadow-fb rendering
  15. * @cursor_width: hint to userspace for max cursor width
  16. * @cursor_height: hint to userspace for max cursor height
  17. * @helper_private: mid-layer private data
  18. *
  19. * Core mode resource tracking structure.  All CRTC, encoders, and connectors
  20. * enumerated by the driver are added here, as are global properties.  Some
  21. * global restrictions are also here, e.g. dimension restrictions.
  22. */
  23. struct drm_mode_config {
  24.     ...
  25. }
复制代码
该结构体着实太膨大了。主要是一些关于drm驱动中用到的东西,例如各种锁、各种属性、各种模式设置的操纵结构体。可以通过一个函数来初始化:
  1. 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结构体如下:
  1. struct drm_plane {
  2.     ...
  3. }
复制代码
drm_plane结构体也很膨大,我们看此中比较紧张的5个字段:

分别是crtc指针、fb指针、funcs指针、funcs_helper指针,保存这四个指针是因为图层位于fb和crtc之间,硬件图层处置惩罚的数据来自frame buffer中的图像数据,而处置惩罚完毕后的数据必要传送给crtc进行信号编码。
初始化drm_plane结构体的函数如下:
  1. int drm_universal_plane_init(struct drm_device *dev, struct drm_plane *plane,
  2.      uint32_t possible_crtcs,
  3.      const struct drm_plane_funcs *funcs,
  4.      const uint32_t *formats, unsigned int format_count,
  5.      const uint64_t *format_modifiers,
  6.      enum drm_plane_type type,
  7.      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字段,该函数如下:
  1. 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的初始化,使用如下函数即可初始化:
  1. int drm_crtc_init_with_planes(struct drm_device *dev, struct drm_crtc *crtc,
  2.     struct drm_plane *primary,
  3.     struct drm_plane *cursor,
  4.     const struct drm_crtc_funcs *funcs,
  5.     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函数集合只必要使用如下函数即可:
  1. 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类:
  1. struct drm_simple_display_pipe {
  2.         struct drm_crtc crtc;
  3.         struct drm_plane plane;
  4.         struct drm_encoder encoder;
  5.         struct drm_connector *connector;
  6.         const struct drm_simple_display_pipe_funcs *funcs;
  7. };
复制代码
将上面的示例程序使用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为例:
  1. static void drm_plane_atomic_update(struct drm_plane *plane,
  2.                                         struct drm_atomic_state *state)
  3. {
  4.         int ret;
  5.         int idx;
  6.         struct drm_rect rect;
  7.         struct drm_plane_state *old_pstate,*plane_state;
  8.         struct iosys_map map[DRM_FORMAT_MAX_PLANES];
  9.         struct iosys_map data[DRM_FORMAT_MAX_PLANES];
  10.         struct drm_display *drm = container_of(plane, struct drm_display, primary);
  11.         struct iosys_map dst_map = IOSYS_MAP_INIT_VADDR(drm->buffer);
  12.         plane_state = drm_atomic_get_new_plane_state(state, plane);
  13.         old_pstate = drm_atomic_get_old_plane_state(state, plane);
  14.         drm_atomic_helper_damage_merged(old_pstate, plane_state, &rect);
  15.         if (!drm_dev_enter(plane->dev, &idx))
  16.                 return;
  17.         ret = drm_gem_fb_begin_cpu_access(plane_state->fb, DMA_FROM_DEVICE);
  18.         if (ret)
  19.                 return;
  20.         ret = drm_gem_fb_vmap(plane_state->fb, map, data);
  21.         if (ret)
  22.                 return;
  23.         if(plane_state->fb->format->format == DRM_FORMAT_XRGB8888) {
  24.                 drm_fb_xrgb8888_to_rgb565(&dst_map, NULL, data, plane_state->fb, &rect, 1);
  25.         }
  26.         else if(plane_state->fb->format->format == DRM_FORMAT_RGB565) {
  27.                 drm_fb_memcpy(&dst_map, NULL, data, plane_state->fb, &rect);
  28.         }
  29.         drm_gem_fb_vunmap(plane_state->fb, map);
  30.         fb_set_win(drm, rect.x1, rect.y1, rect.x2 - 1, rect.y2 - 1);
  31.         gpiod_set_value(drm->dc, 1);  //高电平,发送数据
  32.         spi_write(drm->spi, drm->buffer, (rect.x2 - rect.x1) * (rect.y2 - rect.y1)*2);
  33.         drm_dev_exit(idx);
  34. }
复制代码
上面的代码中,定义了两个drmplane_state,一个是老的plane,一个是新的plane。老的plane纪录着图像渲染之前的图像,而新的plane纪录着渲染的地域,我们将渲染的地域又称为damage区城。为了得到终极的图形,必要使用drm_atomic_helper_damage_merged函数来合并两个plane地域,必要变化的地域被纪录到drm_rect中。因此我们在刷新图片的时间,并不必要将整个plane进行刷新,只必要刷新drnrect部分即可。
8、DRM相关结构

8.1、edid

扩展显示器识别数据。给驱动程序获取显示的硬件相关信息。结构体定义如下:
  1. struct edid {
  2.         u8 header[8];
  3.         /* Vendor & product info */
  4.         u8 mfg_id[2];
  5.         u8 prod_code[2];
  6.         u32 serial; /* FIXME: byte order */
  7.         u8 mfg_week;
  8.         u8 mfg_year;
  9.         /* EDID version */
  10.         u8 version;
  11.         u8 revision;
  12.         /* Display info: */
  13.         u8 input;
  14.         u8 width_cm;
  15.         u8 height_cm;
  16.         u8 gamma;
  17.         u8 features;
  18.         /* Color characteristics */
  19.         u8 red_green_lo;
  20.         u8 black_white_lo;
  21.         u8 red_x;
  22.         u8 red_y;
  23.         u8 green_x;
  24.         u8 green_y;
  25.         u8 blue_x;
  26.         u8 blue_y;
  27.         u8 white_x;
  28.         u8 white_y;
  29.         /* Est. timings and mfg rsvd timings*/
  30.         struct est_timings established_timings;
  31.         /* Standard timings 1-8*/
  32.         struct std_timing standard_timings[8];
  33.         /* Detailing timings 1-4 */
  34.         struct detailed_timing detailed_timings[4];
  35.         /* Number of 128 byte ext. blocks */
  36.         u8 extensions;
  37.         /* Checksum */
  38.         u8 checksum;
  39. } __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的一部分或者一个通道。

免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!更多信息从访问主页:qidao123.com:ToB企服之家,中国第一个企服评测及商务社交产业平台。




欢迎光临 IT评测·应用市场-qidao123.com技术社区 (https://dis.qidao123.com/) Powered by Discuz! X3.4