2. 实现Rockchip CRTC驱动

任务说明

本案例为Rockchip平台(以RK3568为例)实现一个最小可用的CRTC驱动,挂载到DRM/KMS框架中。核心工作包括:

  1. 实现CRTC硬件初始化(时钟、复位、VOP寄存器映射);
  2. 实现crtc_mode_set回调,完成时序配置与使能输出;
  3. 实现crtc_page_flip回调,在VBlank中断中切换扫描缓冲区地址。

驱动不涉及具体HDMI/DSI等外部桥接芯片,仅完成CRTC本身与内存扫描输出的最小闭环,便于在开发板上直接验证modetest输出。

环境与依赖

  • 内核版本:Linux 5.10(Rockchip BSP分支)
  • 硬件:RK3568 EVB(或RK3399,需对应修改寄存器基址)
  • 依赖头文件:drm_crtc_helper.h、drm_plane_helper.h、drm_gem_dma_helper.h、rockchip_drm_drv.h(BSP自带)
  • 设备树:需在vop节点中提供reg、interrupts、clocks、resets属性

关键完整代码

以下为最小CRTC驱动实现,文件可直接编译为内核模块(需与BSP的rockchipdrm主驱动链接):

#include <linux/module.h>
#include <linux/platform_device.h>
#include <linux/of.h>
#include <linux/clk.h>
#include <linux/reset.h>
#include <linux/delay.h>
#include <drm/drm_crtc_helper.h>
#include <drm/drm_plane_helper.h>
#include <drm/drm_gem_dma_helper.h>
#include <drm/drm_vblank.h>
#include <drm/drm_fourcc.h>
#include "rockchip_drm_drv.h"

#define VOP_REG_OFFSET 0x0
#define WIN0_CTRL0     0x50
#define WIN0_DSP_ADDR  0x60
#define SYS_CTRL       0x04

struct rk_crtc {
    struct drm_crtc base;
    void __iomem *regs;
    struct clk *aclk;
    struct clk *dclk;
    struct reset_control *rst;
    int irq;
};

static inline struct rk_crtc *to_rk_crtc(struct drm_crtc *crtc)
{
    return container_of(crtc, struct rk_crtc, base);
}

static void rk_crtc_enable_vblank(struct drm_crtc *crtc)
{
    struct rk_crtc *rk = to_rk_crtc(crtc);
    u32 val = readl(rk->regs + SYS_CTRL);
    val |= BIT(1); /* 使能帧中断 */
    writel(val, rk->regs + SYS_CTRL);
}

static void rk_crtc_disable_vblank(struct drm_crtc *crtc)
{
    struct rk_crtc *rk = to_rk_crtc(crtc);
    u32 val = readl(rk->regs + SYS_CTRL);
    val &= ~BIT(1);
    writel(val, rk->regs + SYS_CTRL);
}

static int rk_crtc_mode_set(struct drm_crtc *crtc,
                struct drm_display_mode *mode,
                struct drm_display_mode *adjusted,
                int x, int y,
                struct drm_framebuffer *old_fb)
{
    struct rk_crtc *rk = to_rk_crtc(crtc);
    u32 htotal = mode->htotal;
    u32 vtotal = mode->vtotal;
    u32 hsync = mode->hsync_end - mode->hsync_start;
    u32 vsync = mode->vsync_end - mode->vsync_start;

    /* 关闭显示通路 */
    writel(0, rk->regs + SYS_CTRL);

    /* 配置时序寄存器(简化:仅写总长与同步宽度) */
    writel((htotal << 16) | mode->hdisplay, rk->regs + 0x10);
    writel((vtotal << 16) | mode->vdisplay, rk->regs + 0x14);
    writel((hsync << 16) | (mode->hsync_start - mode->hdisplay),
           rk->regs + 0x18);
    writel((vsync << 16) | (mode->vsync_start - mode->vdisplay),
           rk->regs + 0x1c);

    /* 使能VOP输出 */
    writel(0x1, rk->regs + SYS_CTRL);
    return 0;
}

static int rk_crtc_page_flip(struct drm_crtc *crtc,
                 struct drm_framebuffer *fb,
                 struct drm_pending_vblank_event *event,
                 uint32_t flags)
{
    struct rk_crtc *rk = to_rk_crtc(crtc);
    struct drm_gem_dma_object *gem;

    if (!fb || !fb->obj[0])
        return -EINVAL;

    gem = drm_fb_dma_get_gem_obj(fb, 0);
    if (!gem)
        return -EINVAL;

    /* 将新帧地址写入WIN0扫描地址寄存器 */
    writel((u32)gem->dma_addr, rk->regs + WIN0_DSP_ADDR);

    /* 等待VBlank后提交事件 */
    if (event) {
        struct drm_crtc_state *state = crtc->state;
        unsigned long flags_irq;

        spin_lock_irqsave(&crtc->dev->event_lock, flags_irq);
        if (state && state->event) {
            drm_crtc_send_vblank_event(crtc, event);
        } else {
            drm_crtc_vblank_get(crtc);
            drm_crtc_arm_vblank_event(crtc, event);
        }
        spin_unlock_irqrestore(&crtc->dev->event_lock, flags_irq);
    }
    return 0;
}

static irqreturn_t rk_crtc_irq_handler(int irq, void *data)
{
    struct drm_device *drm = data;
    struct rk_crtc *rk = NULL;
    u32 status;

    /* 实际项目中需遍历crtc列表找到对应实例 */
    rk = container_of(drm->mode_config.crtc_list.next,
              struct rk_crtc, base.head);
    status = readl(rk->regs + 0x40);
    if (status & BIT(0)) {
        writel(status, rk->regs + 0x40); /* 清中断 */
        drm_crtc_handle_vblank(&rk->base);
        return IRQ_HANDLED;
    }
    return IRQ_NONE;
}

static const struct drm_crtc_funcs rk_crtc_funcs = {
    .reset = drm_atomic_helper_crtc_reset,
    .destroy = drm_crtc_cleanup,
    .set_config = drm_atomic_helper_set_config,
    .page_flip = rk_crtc_page_flip,
    .atomic_duplicate_state = drm_atomic_helper_crtc_duplicate_state,
    .atomic_destroy_state = drm_atomic_helper_crtc_destroy_state,
    .enable_vblank = rk_crtc_enable_vblank,
    .disable_vblank = rk_crtc_disable_vblank,
};

static const struct drm_crtc_helper_funcs rk_crtc_helper_funcs = {
    .mode_set = rk_crtc_mode_set,
};

static int rk_crtc_bind(struct device *dev, struct drm_device *drm)
{
    struct rk_crtc *rk;
    struct resource *res;
    struct platform_device *pdev = to_platform_device(dev);
    int ret;

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

    res = platform_get_resource(pdev, IORESOURCE_MEM, 0);
    rk->regs = devm_ioremap_resource(dev, res);
    if (IS_ERR(rk->regs))
        return PTR_ERR(rk->regs);

    rk->aclk = devm_clk_get(dev, "aclk");
    if (IS_ERR(rk->aclk))
        return PTR_ERR(rk->aclk);

    rk->dclk = devm_clk_get(dev, "dclk");
    if (IS_ERR(rk->dclk))
        return PTR_ERR(rk->dclk);

    rk->rst = devm_reset_control_get(dev, "vop");
    if (IS_ERR(rk->rst))
        return PTR_ERR(rk->rst);

    rk->irq = platform_get_irq(pdev, 0);
    if (rk->irq < 0)
        return rk->irq;

    /* 硬件初始化 */
    reset_control_assert(rk->rst);
    udelay(10);
    reset_control_deassert(rk->rst);
    clk_prepare_enable(rk->aclk);
    clk_prepare_enable(rk->dclk);

    ret = devm_request_irq(dev, rk->irq, rk_crtc_irq_handler,
                   0, "rk-crtc", drm);
    if (ret)
        goto err_disable_clk;

    drm_crtc_init_with_planes(drm, &rk->base, NULL, NULL,
                  &rk_crtc_funcs, NULL);
    drm_crtc_helper_add(&rk->base, &rk_crtc_helper_funcs);
    drm_crtc_vblank_reset(&rk->base);
    platform_set_drvdata(pdev, rk);
    return 0;

err_disable_clk:
    clk_disable_unprepare(rk->dclk);
    clk_disable_unprepare(rk->aclk);
    return ret;
}

static void rk_crtc_unbind(struct device *dev, struct drm_device *drm)
{
    struct rk_crtc *rk = platform_get_drvdata(to_platform_device(dev));
    clk_disable_unprepare(rk->dclk);
    clk_disable_unprepare(rk->aclk);
}

static const struct component_ops rk_crtc_ops = {
    .bind = rk_crtc_bind,
    .unbind = rk_crtc_unbind,
};

static int rk_crtc_probe(struct platform_device *pdev)
{
    return component_add(&pdev->dev, &rk_crtc_ops);
}

static int rk_crtc_remove(struct platform_device *pdev)
{
    component_del(&pdev->dev, &rk_crtc_ops);
    return 0;
}

static const struct of_device_id rk_crtc_dt_ids[] = {
    { .compatible = "rockchip,rk3568-vop", },
    { /* sentinel */ }
};
MODULE_DEVICE_TABLE(of, rk_crtc_dt_ids);

static struct platform_driver rk_crtc_driver = {
    .probe = rk_crtc_probe,
    .remove = rk_crtc_remove,
    .driver = {
        .name = "rk3568-crtc",
        .of_match_table = rk_crtc_dt_ids,
    },
};
module_platform_driver(rk_crtc_driver);

MODULE_LICENSE("GPL");
MODULE_DESCRIPTION("Rockchip Minimal CRTC Driver");

编译与集成

  1. 将上述代码保存为drivers/gpu/drm/rockchip/rk_crtc_min.c,并在rockchip_drm_drv.c的component_list中追加该驱动(或通过module_platform_driver独立加载,需确保rockchipdrm主驱动已注册)。
  2. 设备树中确保VOP节点匹配rockchip,rk3568-vop,且已提供aclk、dclk时钟与vop复位信号。
  3. 编译内核或模块:
make ARCH=arm64 CROSS_COMPILE=aarch64-linux-gnu- drivers/gpu/drm/rockchip/

验证方法

  1. 启动内核后确认驱动加载:dmesg | grep rk-crtc,应无错误。
  2. 使用modetest -M rockchip查看CRTC是否注册成功,应能看到一个CRTC及关联的plane。
  3. 执行modetest -M rockchip -s 0:1920x1080触发模式设置,观察屏幕是否有输出(需连接HDMI或DSI面板)。
  4. 通过modetest -M rockchip -v验证VBlank中断是否产生,计数器应持续递增。

通过条件:modetest能列出CRTC;执行模式设置命令无错误返回;VBlank计数递增;若连接显示器,画面正常显示无撕裂。