驱动之路#04:LCD 驱动程序分析(基于RK3576)

描述

  题图:河北太行山脉,山头密密麻麻都被太阳能板覆盖了。

欢迎关注,每周更新!☞

本合集分享的是,我当初学习Linux驱动的来时路——《《驱动之路》开篇:自序&前言》。

正文

在实际工作中,虽然并不需要我们从零编写 LCD 驱动程序,但必须掌握 LCD 驱动框架 —— 这是分析现有驱动、定位问题,以及在必要时修改驱动以适配硬件或需求的必修课。

分析之前,首先需要掌握字符设备驱动的基本框架,看前面文章《驱动之路#01:Hello World!》。LCD 驱动本质上也是字符设备驱动,理解了字符设备驱动的框架,有助于分析 LCD 驱动程序。

下面我们就运用前面掌握的字符设备驱动基本框架,以 RK3576 平台的 panel-simple.c 为例,浅浅分析 LCD 驱动。

 

驱动位置:kernel-6.1/drivers/gpu/drm/panel/panel-simple.c

 

如何阅读驱动程序,准确说是如何阅读字符设备驱动程序?阅读一个字符设备驱动程序,应该从它的入口函数开始看。

看到 panel-simple.c 的入口函数*register() 注册了 3 个驱动,说明该驱动程序同时兼容DPI、DSI和SPI接口连接的 panel。

lcd

拿 panel_simple_dsi_driver 举例分析,继续往下看。当 driver 与 device(即 dts) 的 compatible 属性匹配就会调用 panel_simple_dsi_probe 函数。

lcd

只有我们弄清楚 panel_simple_dsi_probe  函数的具体操作就基本了解了  panel_simple_dsi_driver 驱动,这就是分析字符设备驱动程序的大致流程。

接下来,分析 panel_simple_dsi_probe 函数,直接看代码注释。

 

/* * MIPI-DSI 面板探测函数 * 负责初始化DSI接口的面板设备 */static int panel_simple_dsi_probe(struct mipi_dsi_device *dsi){    /* 局部变量定义 */    struct panel_simple *panel;      // 通用面板数据结构    struct device *dev = &dsi->dev;  // 获取设备指针,简化后续访问    const struct panel_desc_dsi *desc; // DSI面板描述符(预定义或动态)    struct panel_desc_dsi *d;        // 动态分配的描述符指针    const struct of_device_id *id;   // 设备树匹配结果    int err;                         // 错误码    /* 1. 设备树匹配 - 检查设备是否在驱动支持列表中 */    id = of_match_node(dsi_of_match, dsi->dev.of_node);    if (!id)        return -ENODEV;  // 设备不在支持列表中,返回设备不存在错误    /* 2. 面板描述符处理 */    if (!id->data) {        /* 情况A: 设备树中定义了自定义面板,没有预定义描述符 */        // 动态分配DSI面板描述符内存        d = devm_kzalloc(dev, sizeof(*d), GFP_KERNEL);        if (!d)            return -ENOMEM;  // 内存分配失败        /* 从设备树节点解析面板参数 */        err = panel_simple_dsi_of_get_desc_data(dev, d);        if (err) {            dev_err(dev, "failed to get desc data: %dn", err);            return err;  // 参数解析失败        }    }    /* 注意: 如果 id->data 不为空,表示使用预定义的描述符,跳过动态分配 */    /* 3. 选择最终使用的描述符 */    desc = id->data ? id->data : d;  // 优先使用预定义,否则使用动态解析的描述符    /* 4. 调用通用面板探测函数进行基础初始化 */    err = panel_simple_probe(&dsi->dev, &desc->desc);    if (err < 0)        return err;  // 通用面板初始化失败    /* 5. 获取面板数据结构并关联DSI设备 */    panel = dev_get_drvdata(dev);  // 从设备私有数据中获取panel_simple指针    panel->dsi = dsi;              // 将DSI设备指针保存到面板结构中,建立双向关联    /* 6. 背光设备处理 */    if (!panel->base.backlight) {        /* 如果面板没有背光设备,注册一个DCS背光设备 */        struct backlight_properties props;  // 背光属性结构        /* 初始化背光属性 */        memset(&props, 0, sizeof(props));        props.type = BACKLIGHT_RAW;       // 背光类型:原始模式        props.brightness = 255;           // 初始亮度:最大值        props.max_brightness = 255;       // 最大亮度:255级        /* 注册背光设备 */        panel->base.backlight =                devm_backlight_device_register(dev, dev_name(dev),                                               dev, panel, &dcs_bl_ops,                                               &props);        /* 错误处理 */        if (IS_ERR(panel->base.backlight)) {            err = PTR_ERR(panel->base.backlight);            dev_err(dev, "failed to register dcs backlight: %dn", err);            return err;  // 背光设备注册失败        }    }    /* 注意: 如果面板已有背光设备(如通过设备树引用),则跳过此步骤 */    /* 7. 配置DSI主机控制器参数 */    dsi->mode_flags = desc->flags;   // 设置DSI工作模式标志    dsi->format = desc->format;      // 设置像素数据格式    dsi->lanes = desc->lanes;        // 设置数据通道数量    /* 8. 将面板附加到DSI主机 */    err = mipi_dsi_attach(dsi);    if (err) {        /* 附件失败时的清理工作 */        struct panel_simple *panel = mipi_dsi_get_drvdata(dsi);        drm_panel_remove(&panel->base);  // 从DRM子系统移除面板    }    /* 9. 返回执行结果 */    return err;  // 成功返回0,失败返回错误码}

 

我们在设备树中定义的屏参以及mipi 屏幕相关初始化代码。

lcd

最终是通过函数 panel_simple_dsi_of_get_desc_data()和 panel_simple_of_get_desc_data()获取到的。

lcd

至此,mipi-dsi 屏幕驱动分析完毕!能分析阅读整体驱动框架和函数调用流程就行,不需要花大量时间和精力去解读每行代码解读,AI时代没多大意义。

最后附上两个比较重要的函数,供阅读参考。

 

附录:static int panel_simple_dsi_of_get_desc_data(struct device *dev,                     struct panel_desc_dsi *desc){    struct device_node *np = dev->of_node;    u32 val;    int err;    // 1. 首先解析通用面板参数    err = panel_simple_of_get_desc_data(dev, &desc->desc);    if (err)        return err;    // 2. 设置连接器类型为DSI    desc->desc.connector_type = DRM_MODE_CONNECTOR_DSI;    // 3. 从dts解析DSI特有参数    if (!of_property_read_u32(np, "dsi,flags", &val))        desc->flags = val;    if (!of_property_read_u32(np, "dsi,format", &val))        desc->format = val;    if (!of_property_read_u32(np, "dsi,lanes", &val))        desc->lanes = val;    return 0;}函数 panel_simple_dsi_of_get_desc_data( )中调用 panel_simple_dsi_of_get_desc_data( )获取面板 desc_data。static int panel_simple_of_get_desc_data(struct device *dev,                     struct panel_desc *desc){    struct device_node *np = dev->of_node;    u32 bus_flags;    const void *data;    int len;    int err;    // 1. 解析显示时序    if (of_child_node_is_present(np, "display-timings")) {        struct drm_display_mode *mode;        mode = devm_kzalloc(dev, sizeof(*mode), GFP_KERNEL);        if (!mode)            return -ENOMEM;        // 从设备树解析DRM显示模式        if (!of_get_drm_display_mode(np, mode, &bus_flags,                         OF_USE_NATIVE_MODE)) {            desc->modes = mode;            desc->num_modes = 1;            desc->bus_flags = bus_flags;        }    } else if (of_child_node_is_present(np, "panel-timing")) {        struct display_timing *timing;        struct videomode vm;        timing = devm_kzalloc(dev, sizeof(*timing), GFP_KERNEL);        if (!timing)            return -ENOMEM;        // 从设备树解析显示时序        if (!of_get_display_timing(np, "panel-timing", timing)) {            desc->timings = timing;            desc->num_timings = 1;            // 转换时序标志为总线标志            bus_flags = 0;            vm.flags = timing->flags;            drm_bus_flags_from_videomode(&vm, &bus_flags);            desc->bus_flags = bus_flags;        }    }    // 2. 解析基本面板属性    if (desc->num_modes || desc->num_timings) {        of_property_read_u32(np, "bpc", &desc->bpc);           // 每颜色位数        of_property_read_u32(np, "connector-type", &desc->connector_type);        of_property_read_u32(np, "bus-format", &desc->bus_format); // 总线格式        of_property_read_u32(np, "width-mm", &desc->size.width);   // 物理宽度        of_property_read_u32(np, "height-mm", &desc->size.height); // 物理高度    }    // 3. 解析时序延迟参数    of_property_read_u32(np, "prepare-delay-ms", &desc->delay.prepare);    of_property_read_u32(np, "enable-delay-ms", &desc->delay.enable);    of_property_read_u32(np, "disable-delay-ms", &desc->delay.disable);    of_property_read_u32(np, "unprepare-delay-ms", &desc->delay.unprepare);    of_property_read_u32(np, "reset-delay-ms", &desc->delay.reset);    of_property_read_u32(np, "init-delay-ms", &desc->delay.init);    // 4. 解析初始化序列    data = of_get_property(np, "panel-init-sequence", &len);    if (data) {        desc->init_seq = devm_kzalloc(dev, sizeof(*desc->init_seq), GFP_KERNEL);        if (!desc->init_seq)            return -ENOMEM;        // 解析初始化命令序列        err = panel_simple_parse_cmd_seq(dev, data, len, desc->init_seq);        if (err) {            dev_err(dev, "failed to parse init sequencen");            return err;        }    }    // 5. 解析退出序列    data = of_get_property(np, "panel-exit-sequence", &len);    if (data) {        desc->exit_seq = devm_kzalloc(dev, sizeof(*desc->exit_seq), GFP_KERNEL);        if (!desc->exit_seq)            return -ENOMEM;        // 解析退出命令序列        err = panel_simple_parse_cmd_seq(dev, data, len, desc->exit_seq);        if (err) {            dev_err(dev, "failed to parse exit sequencen");            return err;        }    }    return 0;}

 

(完)

审核编辑 黄宇

打开APP阅读更多精彩内容
声明:本文内容及配图由入驻作者撰写或者入驻合作网站授权转载。文章观点仅代表作者本人,不代表电子发烧友网立场。文章及其配图仅供工程师学习之用,如有内容侵权或者其他违规问题,请联系本站处理。 举报投诉

全部0条评论

快来发表一下你的评论吧 !

×
20
完善资料,
赚取积分