嵌入式系统与单片机|技术阅读
登录|注册

您现在的位置是:嵌入式系统与单片机 > 技术阅读 > Linux DRM 框架与实例分析

Linux DRM 框架与实例分析

1、DRM 框架

Linux 图像子系统涉及 GUI、3D application、DRM/KMS、hardware 等:

在 Linux display 驱动开发时,通常关注 FBDEV(Framebuffer Device),DRM/KMS 子系统。在 FrameBuffer Device 驱动框架下,我们能够快速开发出可供简单使用的显示驱动。

但是随着芯片显示外设的性能逐渐增强、3D 渲染及 GPU 的引入,FrameBuffer 框架就落伍了,显示覆盖 (菜单层级)、GPU 加速、硬件光标等功能并不能得到很好得支持,并且 FrameBuffer 将底层的显存通过 /dev/fb 暴露给用户空间,容易导致不同的应用程序在操作显存时产生访问冲突,不安全。因此,需要一个现代的图形显示框架来解决这些问题,DRM(Direct Rendering Manager,直接图形管理器) 诞生。

DRM 将现代显示领域中会涉及的一些操作进行分层并使这些模块独立,如果上层应用想操作显存、显示效果、GPU,都必须在一些框架的约束下进行。

我们可以从用户空间、内核空间的两个角度去了解 DRM 框架:

用户空间 (libdrm driver):

  • Libdrm(DRM 框架在用户空间的 Lib)

内核空间 (DRM driver):

  • KMS(Kernel Mode Setting,内核显示模式设置)
  • GEM(Graphic Execution Manager,图形执行管理器)

通常用 DRM/KMS 来指代整个 DRM subsystem,但是 KMS 和 DRM driver 只是整个 DRM subsystem 的其中 2 个部分。

Libdrm

DRM 框架在用户空间提供的 Libdrm,对底层接口进行封装,主要是对各种 IOCTL 接口进行封装,向上层提供通用的 API 接口,用户或应用程序在用户空间调用 libdrm 提供的库函数,即可访问到显示的资源,并对显示资源进行管理和使用。

这样通过 libdrm 对显示资源进行统一访问,libdrm 将命令传递到内核最终由 DRM 驱动接管各应用的请求并处理,可以有效避免访问冲突。

KMS(Kernel Mode Setting)

KMS 属于 DRM 框架下的一个大模块,主要负责两个功能:显示参数设置及显示画面控制。这两个基本功能可以说是显示驱动必须基本的能力,在 DRM 框架下,为了将这两部分适配得符合现代显示设备逻辑,又分出了几部分子模块配合框架。

1、DRM FrameBuffer

DRM FrameBuffer 是一个软件抽象,硬件无关的基本元素,描述了图层显示内容的信息 (width, height, pixel_format,pitch 等)。

2、Planes

平面,图层的意思。基本的显示控制单位,每个图像拥有一个 Planes,Planes 的属性控制着图像的显示区域、图像翻转、色彩混合方式等,最终图像经过 Planes 并通过 CRTC 组件,得到多个图像的混合显示或单独显示的等功能。

3、CRTC

CRTC:Cathode Ray Tube Controller,负责把要显示图像,转化为底层硬件层面上的具体时序要求,还负责着帧切换、电源控制、色彩调整等,可以连接多个 Encoder ,实现复制屏幕功能。

4、Encoder

编码器,转换输出器,负责电源管理、显然输出需要不同的信号转换器,将内存的像素转换成显示器需要的信号。

5、Connector

连接器,负责硬件设备的接入,比如 HDMI,VGA 等,可以获取到设备 EDID , DPMS 连接状态等等。

上述的这些组件,最终完成了一个完整的 DRM 显示控制过程,如下图所示:

上面 CRTC、Planes、Encoder、Connector 这些组件是对硬件的抽象,即使没有实际的硬件与之对应,在软件驱动中也需要实现这些,否则 DRM 子系统无法正常运行。

GEM(generic DRM memory-management)

GEM 负责对 DRM 使用的内存 (如显存) 进行管理, 是一个软件抽象。

GEM 框架提供的功能包括:

  • 内存分配和释放
  • 命令执行
  • 执行命令时的管理
  • 2、RK 平台 DRM 实现

    显示功能的驱动一般由芯片厂商 rockchip 来负责实现,完成一个 DRM-Host,主机驱动代码一般位于 drivers/gpu/drm/xxx/ 目录下,这里 xxx 代指芯片厂商。

    在驱动中 rockchip 的显示驱动使用 component 框架,显示驱动为 master,显示驱动下的设备称为 component。

    在 display_subsystem 设备节点中的 ports 节点就是关联的 component,实际指向 vopl_out 和 vopb_out 节点,(VOP 是各种输出图像的接口),在该节点下有多个 port,可以同时输出多个视频信号。

    rk3399.dtsi

    display_subsystem: display-subsystem {
     status = "okay";
     compatible = "rockchip,display-subsystem";
     ports = <&vopl_out>, <&vopb_out>;
     clocks = <&cru PLL_VPLL>, <&cru PLL_CPLL>;
     clock-names = "hdmi-tmds-pll""default-vop-pll";
     devfreq = <&dmc>;
     logo-memory-region = <&drm_logo>;
     secure-memory-region = <&secure_memory>;
     
     route {
      route_dsi: route-dsi {
       status = "disabled";
       logo,uboot = "logo.bmp";
       logo,kernel = "logo_kernel.bmp";
       logo,mode = "center";
       charge_logo,mode = "center";
       connect = <&vopb_out_dsi>;
      };
        ......
     };
    };
     vopl: vop@ff8f0000 {
      compatible = "rockchip,rk3399-vop-lit";
      reg = <0x0 0xff8f0000 0x0 0x600>,
       <0x0 0xff8f1c00 0x0 0x200>,
       <0x0 0xff8f2000 0x0 0x400>;
      reg-names = "regs""cabc_lut""gamma_lut";
      interrupts = <GIC_SPI 119 IRQ_TYPE_LEVEL_HIGH 0>;
      clocks = <&cru ACLK_VOP1>, <&cru DCLK_VOP1>, <&cru HCLK_VOP1>, <&cru DCLK_VOP1_DIV>;
      clock-names = "aclk_vop""dclk_vop""hclk_vop""dclk_source";
      iommus = <&vopl_mmu>;
      power-domains = <&power RK3399_PD_VOPL>;
      resets = <&cru SRST_A_VOP1>, <&cru SRST_H_VOP1>, <&cru SRST_D_VOP1>;
      reset-names = "axi""ahb""dclk";
      status = "disabled";

      vopl_out: port {
       #address-cells = <1>;
       #size-cells = <0>;

       vopl_out_dsi: endpoint@0 {
        reg = <0>;
        remote-endpoint = <&dsi_in_vopl>;
       };

       vopl_out_edp: endpoint@1 {
        reg = <1>;
        remote-endpoint = <&edp_in_vopl>;
       };

       vopl_out_hdmi: endpoint@2 {
        reg = <2>;
        remote-endpoint = <&hdmi_in_vopl>;
       };

       vopl_out_dp: endpoint@3 {
        reg = <3>;
        remote-endpoint = <&dp_in_vopl>;
       };

       vopl_out_dsi1: endpoint@4 {
        reg = <4>;
        remote-endpoint = <&dsi1_in_vopl>;
       };
      };
     };
        
     vopb: vop@ff900000 {
      compatible = "rockchip,rk3399-vop-big";
      reg = <0x0 0xff900000 0x0 0x600>,
       <0x0 0xff901c00 0x0 0x200>,
       <0x0 0xff902000 0x0 0x1000>;
      reg-names = "regs""cabc_lut""gamma_lut";
      interrupts = <GIC_SPI 118 IRQ_TYPE_LEVEL_HIGH 0>;
      clocks = <&cru ACLK_VOP0>, <&cru DCLK_VOP0>, <&cru HCLK_VOP0>, <&cru DCLK_VOP0_DIV>;
      clock-names = "aclk_vop""dclk_vop""hclk_vop""dclk_source";
      resets = <&cru SRST_A_VOP0>, <&cru SRST_H_VOP0>, <&cru SRST_D_VOP0>;
      reset-names = "axi""ahb""dclk";
      power-domains = <&power RK3399_PD_VOPB>;
      iommus = <&vopb_mmu>;
      status = "disabled";

      vopb_out: port {
       #address-cells = <1>;
       #size-cells = <0>;

       vopb_out_edp: endpoint@0 {
        reg = <0>;
        remote-endpoint = <&edp_in_vopb>;
       };

       vopb_out_dsi: endpoint@1 {
        reg = <1>;
        remote-endpoint = <&dsi_in_vopb>;
       };

       vopb_out_hdmi: endpoint@2 {
        reg = <2>;
        remote-endpoint = <&hdmi_in_vopb>;
       };

       vopb_out_dp: endpoint@3 {
        reg = <3>;
        remote-endpoint = <&dp_in_vopb>;
       };

       vopb_out_dsi1: endpoint@4 {
        reg = <4>;
        remote-endpoint = <&dsi1_in_vopb>;
       };
      };
     };

    平台驱动 rockchip-drm 匹配到设备树,会到设备树 dts 查找 ports 节点和 iommus 节点,使用 component_master_add_with_match 函数注册自己到 component 框架中,设置了 rockchip_drm_ops,其 component 可以通过 component_add 函数增加,master 匹配上所有 component 后,会调用 master 的 bind 回调函数,最后通过 drm_dev_register() 函数注册到 DRM core

    平台驱动源码如下 drivers/gpu/drm/rockchip/rockchip_drm_drv.c

    static const struct component_master_ops rockchip_drm_ops = {
     .bind = rockchip_drm_bind,
     .unbind = rockchip_drm_unbind,
    };

    static int rockchip_drm_platform_probe(struct platform_device *pdev)
    {
     struct device *dev = &pdev->dev;
     struct component_match *match = NULL;
     struct device_node *np = dev->of_node;
     struct device_node *port;
     int i;

     DRM_INFO("Rockchip DRM driver version: %s\n", DRIVER_VERSION);
     if (!np)
      return -ENODEV;

     for (i = 0;; i++) {
      struct device_node *iommu;
      port = of_parse_phandle(np, "ports", i);
      if (!port)
       break;

      if (!of_device_is_available(port->parent)) {
       of_node_put(port);
       continue;
      }

      iommu = of_parse_phandle(port->parent, "iommus"0);
      if (!iommu || !of_device_is_available(iommu->parent)) {
       dev_dbg(dev, "no iommu attached for %s, using non-iommu buffers\n",
        port->parent->full_name);
        is_support_iommu = false;
      }
      component_match_add(dev, &match, compare_of, port->parent);
      of_node_put(port);
     }
    ......
     for (i = 0;; i++) {
      port = of_parse_phandle(np, "ports", i);
      if (!port)
       break;

      if (!of_device_is_available(port->parent)) {
       of_node_put(port);
       continue;
      }

      rockchip_add_endpoints(dev, &match, port);
      of_node_put(port);
     }

     port = of_parse_phandle(np, "backlight"0);
     if (port && of_device_is_available(port)) {
      component_match_add(dev, &match, compare_of, port);
      of_node_put(port);
     }

     return component_master_add_with_match(dev, &rockchip_drm_ops, match);
    }
    static const struct of_device_id rockchip_drm_dt_ids[] = {
     { .compatible = "rockchip,display-subsystem", },
     { /* sentinel */ },
    };
    MODULE_DEVICE_TABLE(of, rockchip_drm_dt_ids);

    static struct platform_driver rockchip_drm_platform_driver = {
     .probe = rockchip_drm_platform_probe,
     .remove = rockchip_drm_platform_remove,
     .shutdown = rockchip_drm_platform_shutdown,
     .driver = {
      .name = "rockchip-drm",
      .of_match_table = rockchip_drm_dt_ids,
      .pm = &rockchip_drm_pm_ops,
     },
    };

    3、RK3399 实例

    博主手里的是 MIPI DSI 屏幕,设备树配置如下:

     dsi@ff960000 {
      compatible = "rockchip,rk3399-mipi-dsi";
      reg = <0x0 0xff960000 0x0 0x8000>;
      interrupts = <GIC_SPI 45 IRQ_TYPE_LEVEL_HIGH 0>;
      clocks = <0x8 0xa2 0x8 0x170 0x8 0xa3>;
      clock-names = "ref""pclk""phy_cfg";
      power-domains = <&power RK3399_PD_VIO>;
      resets = <&cru SRST_P_MIPI_DSI0>;
      reset-names = "apb";
      rockchip,grf = <&grf>;
      status = "okay";
      #address-cells = <0x1>;
      #size-cells = <0x0>;
      phandle = <0x137>;

      ports {
       port {
        #address-cells = <1>;
        #size-cells = <0>;

        endpoint@0 {
         reg = <0x0>;
         remote-endpoint = <&vopb_out_dsi>;
         status = "okay";
         phandle = <0xab>;
        };

        endpoint@1 {
         reg = <0x1>;
         remote-endpoint = <&vopl_out_dsi>;
         status = "disabled";
         phandle = <0xa3>;
        };
       };
      };

      panel@0 {
       status = "okay";
       compatible = "simple-panel-dsi";
       reg = <0x0>;
       backlight = <&backlight>;
       dsi,flags = <0xa03>;
       dsi,format = <MIPI_DSI_FMT_RGB888>;
       dsi,lanes = <4>;
       dsi,channel = <0>;
       enable-delay-ms = <35>;
       prepare-delay-ms = <6>;
       unprepare-delay-ms = <0>;
       disable-delay-ms = <20>;
       size,width = <120>;
       size,height = <170>;
       panel-init-sequence = [29 ...... 29];
       panel-exit-sequence = <0x5050128 0x5780110>;
       phandle = <0x138>;

       power_ctr {
        rockchip,debug = <0>;
        power_enable = <1>;
        phandle = <0x139>;

        lcd-rst {
         gpios = <&gpio4 RK_PD6 GPIO_ACTIVE_HIGH>;
         pinctrl-names = "default";
         pinctrl-0 = <&lcd_panel_reset>;
         rockchip,delay = <6>;
         phandle = <0x13a>;
        };
       };

       display-timings {
        native-mode = <&timing0>;
        phandle = <0x13b>;

        timing0 {
         clock-frequency = <0x3938700>;
         hactive = <0x320>;
         vactive = <0x500>;
         hsync-len = <0x14>;
         hback-porch = <0x14>;
         hfront-porch = <0x14>;
         vsync-len = <0x4>;
         vback-porch = <0x4>;
         vfront-porch = <0xa>;
         hsync-active = <0x0>;
         vsync-active = <0x0>;
         de-active = <0x0>;
         pixelclk-active = <0x0>;
         phandle = <0xba>;
        };
       };
      };
     };

    对应的 DSI 驱动是 drivers/gpu/drm/rockchip/dw-mipi-dsi.c

    对应的 panel 驱动是 drivers/gpu/drm/panel/panel-simple.c

    static const struct of_device_id dw_mipi_dsi_dt_ids[] = {
     { .compatible = "rockchip,rk3399-mipi-dsi", .data = &rk3399_socdata, },
     { /* sentinel */ }
    };
    MODULE_DEVICE_TABLE(of, dw_mipi_dsi_dt_ids);

    static struct platform_driver dw_mipi_dsi_driver = {
     .probe  = dw_mipi_dsi_probe,
     .remove  = dw_mipi_dsi_remove,
     .driver  = {
      .of_match_table = dw_mipi_dsi_dt_ids,
      .pm = &dw_mipi_dsi_pm_ops,
      .name = DRIVER_NAME,
     },
    };
    module_platform_driver(dw_mipi_dsi_driver);
    static int dw_mipi_dsi_probe(struct platform_device *pdev)
    {
     struct device *dev = &pdev->dev;
     struct dw_mipi_dsi *dsi;
     struct device_node *np = dev->of_node;
     struct resource *res;
     void __iomem *regs;
     int ret;
     int dsi_id;

     dsi = devm_kzalloc(dev, sizeof(*dsi), GFP_KERNEL);
     if (!dsi)
      return -ENOMEM;

     dsi_id = of_alias_get_id(np, "dsi");
     if (dsi_id < 0)
      dsi_id = 0;

     dsi->id = dsi_id;
     dsi->dev = dev;
     dsi->pdata = of_device_get_match_data(dev);
     platform_set_drvdata(pdev, dsi);

     ret = dw_mipi_dsi_parse_dt(dsi);

     res = platform_get_resource(pdev, IORESOURCE_MEM, 0);
     regs = devm_ioremap_resource(dev, res);
     dsi->irq = platform_get_irq(pdev, 0);
     dsi->pclk = devm_clk_get(dev, "pclk");
     dsi->regmap = devm_regmap_init_mmio(dev, regs,&dw_mipi_dsi_regmap_config);

     if (dsi->pdata->soc_type == RK3126) {
      dsi->h2p_clk = devm_clk_get(dev, "h2p");
      }
     }

     dsi->grf = syscon_regmap_lookup_by_phandle(np, "rockchip,grf");
     dsi->rst = devm_reset_control_get(dev, "apb");

     ret = mipi_dphy_attach(dsi);

     ret = devm_request_irq(dev, dsi->irq, dw_mipi_dsi_irq_handler,IRQF_SHARED, dev_name(dev), dsi);

     dsi->dsi_host.ops = &dw_mipi_dsi_host_ops;
     dsi->dsi_host.dev = dev;

     ret = mipi_dsi_host_register(&dsi->dsi_host);

     ret = component_add(dev, &dw_mipi_dsi_ops);
     if (ret)
      mipi_dsi_host_unregister(&dsi->dsi_host);

     return ret;
    }

    这里会注册 mipi dsi 中断,中断处理函数是 dw_mipi_dsi_irq_handler

    4、debug

    1、连接 ADB,安装测试工具 apt install libdrm-tests,执行 modetest,会打印 Encoders、Connectors、CRTCs、Planes 的详细信息。

    2、DRM driver 会在 /dev/dri 下创建 3 个设备节点:card0、controlD64、renderD128,libdrm 可以打开 card0 在用户空间操作。

    3、DRM 的应用编程有两种接口:legacy 接口和 atomic 接口,目前一般用 atomic 接口。

    5、后记

    虽然 DRM 功能符合现代显示设备的需求,但是仍有众多的老设备及软件需要 Framebuffer 的支持。所以在 DRM 框架下,有部分代码用于实现在 DRM 框架下,去模拟 FB 设备。

    在 rockchip 提供的显示驱动代码中,也有模拟 FB 设备的相关代码,参见 drivers/gpu/drm/rockchip/rockchip_drm_fb.c 文件,最终效果就是设备目录下,出现熟悉的身影 /dev/fb0