Linux显示设备驱动开发 Drm驱动&&Makefile

学习Linux驱动:Makefile、drm驱动框架、lcd驱动的编译、加载流程

在Linux显示设备驱动开发时,通常关注FBDEV(Framebuffer Device), DRM/KMS子系统

Drm驱动框架学习:

参考教程:

https://doc.embedfire.com/linux/rk356x/driver/zh/latest/linux_driver/framework_drm.html

RK平台的DRM架构目录是/kernel/drivers/gpu/drm/rockchip/。通过drm_drv.c文件中的drm_bind()创建和注册DRM设备。

1. 概念:

在FrameBuffer Device驱动框架下,我们能够快速开发出可供简单使用的显示驱动。 但是随着芯片显示外设的性能逐渐增强、3D渲染及GPU的引入,FrameBuffer框架看起来似乎就有些落伍了, 最直接的体现,就是在传统的框架下,对于许多芯片显示外设的新特性如: 显示覆盖(菜单层级)、GPU加速、硬件光标等功能并不能得到很好得支持, 并且FrameBuffer框架将底层的显存通过用户空间/dev/fb接口,暴露给了用户空间, 这很容易导致不同的应用程序在操作显存时,产生访问冲突,而且这种方式看起来似乎不是那么安全。

在这背景下,就需要一个现代的图形显示框架来解决这些问题,那么DRM(Direct Rendering Manager,直接图形管理器)诞生。

DRM全称是Direct Rendering Manager,管理进行显示输出的buffer分配、帧缓冲。libdrm库提供了一系列友好的控制封装, 使用户可以方便的进行显示的控制, modetest是libdrm源码自带的调试工具, 可以对drm进行一些基础的调试,但并不是只能通过libdrm库来控制drm, 用户可以直接操作drm的ioctl或者是使用framebuffer的 接口实现显示操作。

2. DRM框架

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

  • 用户空间(libdrm driver):
    • Libdrm(DRM框架在用户空间的Lib)
  • 内核空间(DRM driver):
    • KMS(Kernel Mode Setting,内核显示模式设置)
    • GEM(Graphic Execution Manager,图形执行管理器)

2.1. Libdrm

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

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

下载libdrm 压缩包

地址:Index of /libdrm

2.2. KMS(Kernel Mode Setting)

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

2.2.1. DRM FrameBuffer

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

2.2.2. Planes

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

2.2.3. CRTC

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

2.2.4. Encoder

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

2.2.5. Connector

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

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

2.3. GEM(generic DRM memory-management)

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

GEM框架提供的功能包括:

  • 内存分配和释放
  • 命令执行
  • 执行命令时的管理

3. 设备节点

DRM 框架创建设备节点或者GPU设备文件,如下所示:

  • /dev/dri/cardX, X为0-15的数值,默认使用的是/dev/dri/card0,这个文件同时支持绘图和显示,例如实现显示器显示帧缓冲功能,用户可以使用open操作并调用KMS接口完成,这里不详细介绍。
  • /dev/dri/renderDX,X可以为128,129等的数值,这个文件只支持绘图功能,用户可以open操作并调用EGL+GBM做OpenGL绘制,这里不详细介绍。

节点常见操作:

  1. 查看当前支持的可用显示分辨率列表:

cat /sys/class/drm/card0-HDMI-A-1/modes

  1. 查看当前分辨率:

cat /sys/class/drm/card0-HDMI-A-1/mode

  1. 打开hdmiout显示:

echo on > /sys/class/drm/card0-HDMI-A-1/status

  1. 关闭hdmiout显示:

echo off > /sys/class/drm/card0-HDMI-A-1/status

  1. 查看hdmiout显示状态:(打开是connected,关闭是disconnected)

cat /sys/class/drm/card0-HDMI-A-1/status

  1. 重新检测HDMI连接:

echo detect > /sys/class/drm/card0-HDMI-A-1/status

在设置HDMI分辨率的时候,如果设置为自适应模式(Auto)的时候,就去设置status为detect,如果设置HDMI为固定分辨率的时候(如1080p)就设置HDMI的状态为on,即让它一直为输出HDMI信号的状态

  1. 查看 hdmi 的edid数据或者将DP/HDMI接口的EDID数据转化为十六进制原始数据保存为.bin文件

Linux下:

cat /sys/class/drm/card0-DP-1/edid > /data/edid.bin

cat /sys/class/drm/card0-HDMI-A-1/edid > /data/edid.bin

EDID:全称是Extended Display Identification Data (外部显示设备标识数据);VGA、DVI的EDID由主块128字节组成,HDMI的EDID增加扩展块(128字节),扩展块的内容主要是和音频属性相关的,DVI和VGA没有音频,HDMI自带音频,扩展块数据规范按照CEA-861x标准定义,未来可能增加到512或256的整数倍。

  1. 查看驱动名字

cat /sys/kernel/debug/dri/0/name

  1. 查看屏幕信息(当前的输出分辨率和帧率)

cat /sys/kernel/debug/dri/0/summary

4. panel-simple.c 驱动

要求:熟悉初始化,获取dts数据,gpio控制,休眠唤醒等流程

4.1. 初始化

相关代码

复制代码
static int __init panel_simple_init(void)
{
	int err;

	err = platform_driver_register(&panel_simple_platform_driver);
	if (err < 0)
		return err;

	if (IS_ENABLED(CONFIG_DRM_MIPI_DSI)) {
		err = mipi_dsi_driver_register(&panel_simple_dsi_driver);
		if (err < 0)
			return err;
	}

	return 0;
}
module_init(panel_simple_init);

函数主要实现的操作:

注册平台驱动和MIPI DSI驱动

4.1.1. 调用platform_driver_register注册平台驱动

传入参数为panel_simple_platform_driver结构体

结构体具体实现

复制代码
static struct platform_driver panel_simple_platform_driver = {
	.driver = {
		.name = "panel-simple",
		.of_match_table = platform_of_match,
	},
	.probe = panel_simple_platform_probe,
	.remove = panel_simple_platform_remove,
	.shutdown = panel_simple_platform_shutdown,
};

结构体成员:

driver:驱动名和适配平台

platform_of_match内容:

推测是做好适配的平台

panel_simple_platform_probe : LCD 面板驱动的平台设备探测函数

函数实现

复制代码
static int panel_simple_platform_probe(struct platform_device *pdev)
{
	struct device *dev = &pdev->dev;
	const struct of_device_id *id;
	const struct panel_desc *desc;
	struct panel_desc *d;
	int err;

	id = of_match_node(platform_of_match, pdev->dev.of_node);
	if (!id)
		return -ENODEV;

	if (!id->data) {
		d = devm_kzalloc(dev, sizeof(*d), GFP_KERNEL);
		if (!d)
			return -ENOMEM;

		err = panel_simple_of_get_desc_data(dev, d);
		if (err) {
			dev_err(dev, "failed to get desc data: %d\n", err);
			return err;
		}
	}

	desc = id->data ? id->data : d;

	return panel_simple_probe(&pdev->dev, desc);
}
  1. 变量声明
  • pdev:平台设备对象,表示与驱动匹配的硬件设备。
  • dev:指向平台设备关联的 device 结构体,用于后续资源管理。
  • id:用于存储设备树匹配结果。
  • desc:指向面板描述符(panel_desc)的指针,包含面板的时序、分辨率等参数。
  • d:临时面板描述符指针,用于动态分配场景。
  • err:错误码
  1. 设备树匹配

    复制代码
     id = of_match_node(platform_of_match, pdev->dev.of_node);
     if (!id)
         return -ENODEV;
  • of_match_node:通过设备树节点(pdev->dev.of_node)与驱动定义的匹配表 platform_of_match 进行匹配。
  • 若未匹配到设备树节点(id == NULL),返回错误码 -ENODEV(设备不存在)。
  1. 动态分配面板描述符

    复制代码
     if (!id->data) {
         d = devm_kzalloc(dev, sizeof(*d), GFP_KERNEL);
         if (!d)
             return -ENOMEM;
    
         err = panel_simple_of_get_desc_data(dev, d);
         if (err) {
             dev_err(dev, "failed to get desc data: %d\n", err);
             return err;
         }
     }

当id->data为空,说明设备树节点未直接关联静态面板描述符,需要动态解析。

devm_kzalloc:使用设备资源管理(devres)分配内存,自动释放内存避免泄漏。

panel_simple_of_get_desc_data:从设备树节点中解析面板参数(如时序、分辨率、GPIO 配置等),填充到 d 中。

  1. 确定最终面板描述符

    复制代码
    desc = id->data ? id->data : d;

逻辑

  • 如果 id->data 非空(静态描述符已定义),直接使用它。
  • 否则使用动态分配的 d(从设备树解析得到)。
  1. 核心初始化函数
  • panel_simple_probe:核心初始化函数,通常完成以下操作:
    1. 注册 drm_panel 结构体,定义面板操作(如 enabledisable)。
    2. 解析 desc 中的时序参数,配置显示控制器(CRTC)。
    3. 初始化背光、电源管理 GPIO。
    4. 将面板注册到 DRM 框架,供用户空间(如 modetest)使用。
4.1.2. 调用panel_simple_dsi_probe注册MIPI DSI驱动

传入参数为panel_simple_dsi_driver结构体

结构体具体实现

复制代码
 static struct mipi_dsi_driver panel_simple_dsi_driver = {
	.driver = {
		.name = "panel-simple-dsi",
		.of_match_table = dsi_of_match,
	},
	.probe = panel_simple_dsi_probe,
	.remove = panel_simple_dsi_remove,
	.shutdown = panel_simple_dsi_shutdown,
};   

结构体成员:

driver:驱动名和适配平台

dsi_of_match内容:

panel_simple_dsi_probe: LCD 面板驱动的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;
	struct panel_desc_dsi *d;
	const struct of_device_id *id;
	int err;

	id = of_match_node(dsi_of_match, dsi->dev.of_node);
	if (!id)
		return -ENODEV;

	if (!id->data) {
		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: %d\n", err);
			return err;
		}
	}

	desc = id->data ? id->data : d;

	err = panel_simple_probe(&dsi->dev, &desc->desc);
	if (err < 0)
		return err;

	panel = dev_get_drvdata(dev);
	panel->dsi = dsi;

	if (!panel->base.backlight) {
		struct backlight_properties props;

		memset(&props, 0, sizeof(props));
		props.type = BACKLIGHT_RAW;
		props.brightness = 255;
		props.max_brightness = 255;

		panel->base.backlight =
			devm_backlight_device_register(dev, "dcs-backlight",
						       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: %d\n",
				err);
			return err;
		}
	}

	dsi->mode_flags = desc->flags;
	dsi->format = desc->format;
	dsi->lanes = desc->lanes;

	err = mipi_dsi_attach(dsi);
	if (err) {
		struct panel_simple *panel = dev_get_drvdata(&dsi->dev);

		drm_panel_remove(&panel->base);
	}

	return err;
}

4.2. 获取dts数据

TODO

4.3. gpio控制

4.3.1. GPIO资源获取

相关代码

复制代码
	panel->no_hpd = of_property_read_bool(dev->of_node, "no-hpd");
	if (!panel->no_hpd) {
		err = panel_simple_get_hpd_gpio(dev, panel, true);
		if (err)
			return err;
	}

	panel->supply = devm_regulator_get(dev, "power");
	if (IS_ERR(panel->supply)) {
		err = PTR_ERR(panel->supply);
		dev_err(dev, "failed to get power regulator: %d\n", err);
		return err;
	}

	panel->enable_gpio = devm_gpiod_get_optional(dev, "enable", GPIOD_ASIS);
	if (IS_ERR(panel->enable_gpio)) {
		err = PTR_ERR(panel->enable_gpio);
		if (err != -EPROBE_DEFER)
			dev_err(dev, "failed to get enable GPIO: %d\n", err);
		return err;
	}

	panel->reset_gpio = devm_gpiod_get_optional(dev, "reset", GPIOD_ASIS);
	if (IS_ERR(panel->reset_gpio)) {
		err = PTR_ERR(panel->reset_gpio);
		if (err != -EPROBE_DEFER)
			dev_err(dev, "failed to get reset GPIO: %d\n", err);
		return err;
	}
  1. 判断设备树中是否有"no-hpd"(不支持热插拔)属性,如果没有,即支持热插拔,则获取热插拔的gpio;
  2. 获取名为"power"的电源管理器;
  3. 获取gpio(使能和复位)状态 devm_gpiod_get_optional

4.4. 休眠唤醒

4.4.1. 休眠
  1. 禁用面板

相关代码:

复制代码
static int panel_simple_disable(struct drm_panel *panel)
{
	struct panel_simple *p = to_panel_simple(panel);

	if (!p->enabled)
		return 0;

	if (p->desc->delay.disable)
		msleep(p->desc->delay.disable);

	p->enabled = false;

	return 0;
}

a. 通过to_panel_simple函数将drm_panel类型的结构体转换为panel_simple类型

b. 判断面板是否已关闭,若已关闭则直接返回

c. 判断是否有延时需求,根据延时字段进行ms延时

d. 更新enabled属性

  1. 解除准备

相关代码:

复制代码
static int panel_simple_unprepare(struct drm_panel *panel)
{
    struct panel_simple *p = to_panel_simple(panel);

    if (!p->prepared)
        return 0;

    if (p->desc->exit_seq)
        if (p->dsi)
            panel_simple_xfer_dsi_cmd_seq(p, p->desc->exit_seq);

#ifndef CONFIG_PRODUCT_RK3588_210B_52
    gpiod_direction_output(p->reset_gpio, 1);
#endif
    gpiod_direction_output(p->enable_gpio, 0);

    panel_simple_regulator_disable(p);

    if (p->desc->delay.unprepare)
        msleep(p->desc->delay.unprepare);

    p->prepared = false;

    return 0;
}

a. 通过to_panel_simple函数将drm_panel类型的结构体转换为panel_simple类型

b. 如果设备已解除准备,则直接返回

c. 如果面板有定义退出序列(exit_seq),且面板使用dsi接口,则调用panel_simple_xfer_dsi_cmd_seq

d. 控制复位/使能gpio,禁用电源管理器

e. 判断是否有延时需求,根据延时字段执行ms延时

f. 更新prepared属性

4.4.2. 唤醒
  1. 使能面板

相关代码:

复制代码
static int panel_simple_enable(struct drm_panel *panel)
{
	struct panel_simple *p = to_panel_simple(panel);

	if (p->enabled)
		return 0;

	if (p->desc->delay.enable)
		msleep(p->desc->delay.enable);

	p->enabled = true;

	return 0;
}

a. 通过to_panel_simple函数将drm_panel类型的结构体转换为panel_simple类型

b. 判断面板是否已开启,若已开启则直接返回

c. 判断是否有延时需求,根据延时字段进行ms延时

d. 更新enabled属性

  1. 保持准备

相关代码:

复制代码
static int panel_simple_prepare(struct drm_panel *panel)
{
	struct panel_simple *p = to_panel_simple(panel);
	unsigned int delay;
	int err;
	int hpd_asserted;

	if (p->prepared)
		return 0;

	err = panel_simple_regulator_enable(p);
	if (err < 0) {
		dev_err(panel->dev, "failed to enable supply: %d\n", err);
		return err;
	}

	gpiod_direction_output(p->enable_gpio, 1);

	delay = p->desc->delay.prepare;
	if (p->no_hpd)
		delay += p->desc->delay.hpd_absent_delay;
	if (delay)
		msleep(delay);

	if (p->hpd_gpio) {
		if (IS_ERR(p->hpd_gpio)) {
			err = panel_simple_get_hpd_gpio(panel->dev, p, false);
			if (err)
				return err;
		}

		err = readx_poll_timeout(gpiod_get_value_cansleep, p->hpd_gpio,
					 hpd_asserted, hpd_asserted,
					 1000, 2000000);
		if (hpd_asserted < 0)
			err = hpd_asserted;

		if (err) {
			dev_err(panel->dev,
				"error waiting for hpd GPIO: %d\n", err);
			return err;
		}
	}

	gpiod_direction_output(p->reset_gpio, 1);

	if (p->desc->delay.reset)
		msleep(p->desc->delay.reset);

	gpiod_direction_output(p->reset_gpio, 0);

	if (p->desc->delay.init)
		msleep(p->desc->delay.init);

	if (p->desc->init_seq)
		if (p->dsi)
			panel_simple_xfer_dsi_cmd_seq(p, p->desc->init_seq);

	p->prepared = true;

	return 0;
}

a. 通过to_panel_simple函数将drm_panel类型的结构体转换为panel_simple类型

b. 判断面板是否保持准备,若已准备则直接返回

c. 使能电源管理器,拉高使能gpio

d. 根据是否支持热插拔,调节延时时间

e. 判断是否支持热插拔,是则轮询获取gpio的值直到信号被断言

f. 控制重启gpio,并根据reset和init字段进行延时

g.修改prepared字段

Makefile

参考教程:

https://blog.csdn.net/weixin_47257473/article/details/131675814

1. 介绍

make是一个命令 makefile/Makefile是一个文件.

  1. 什么是Makefile脚本?

Makefile脚本集合了程序的编译指令的文件,make是一个命令工具,当执行make命令时,它会自动读取Makefile中的编译指令并执行,会自动完成整个项目的自动化编译工作.

2.为什么需要Makefile脚本:

项目中如果有很多.c文件,它们的编译指令会有很多,需要的编译时间比较长,依赖关系非常复杂。

当项目中的.h文件被修改时,对应的.c文件需要重新编译,但是我们无法人为的分辨出哪些文件需要重新编译,只能全部重新编译一下,这项操作非常耗时。此时Makefile便发挥了用场.

所以make/Makefile又叫自动化的构建项目.

2. 简单实现

Makefile主要由两部分组成:a.依赖关系,b.依赖方法

我们举个例子:

你在学校,然后给你爸打电话,电话通了之后你说: "爸,我是你儿子",然后就直接挂了.

这样你就表明了依赖关系,我们是父子关系,但是后面什么都没有说了,你爸爸以为你遇到了什么危险.

这个时候你又打过去了说:"孩子没钱了,打点生活费吧",此时这个便才是依赖方法.

所以我们Makefile要达成一个目的,必须要满足这两个条件.

我们举个实际代码的例子来理解:

我们直接

复制代码
vim Makefile

此时便会创建一个Makefile文件并且打开.

假设此时有一个test.c源文件,我们想编译成mytest可以执行文件.

依赖关系:

复制代码
mytest:test.c

其中mytest称作目标文件,test.c称作依赖文件.

依赖方法:

必须第依赖关系的下一行,以Tab键为空开始写.

编译源文件,需要用到gcc编译器:

复制代码
         gcc test.c -o mytest 

这样一个简单的Makefile文件便写好了.

复制代码
mytest:test.c
    gcc test.c -o mytest

从vim中退出来,编写一个简单的test.c文件:

复制代码
#include <stdio.h>
int main()
{
    printf("hello,linux!");
    return 0;
}

此时目录下会有两个我们刚才创建的文件:Makefile和test.c

我们直接执行make命令:

我们发现执行命令之后,有了我们想要的mytest可执行文件,然后再./mytest执行这个文件,发现程序成功运行了.

3. .PHONY

那如果我们想清理某个文件,该怎么写呢?既然是清理文件那还需要依赖什么文件删除吗?

这个时候需要用一个东西叫**.PHONY伪目标.**

因为我们clean目的是清除某些文件,而删除操作又不会需要依赖文件,所以会创建一个伪目标,相当于依赖这个伪目标,然后执行依赖方法.

格式如下:

1.先在前面写一个.PHONY:clean

2.第二行紧接着输入clean:

3.Tab键开头,然后输入依赖方法 rm -rf mytest

所以输入进去应该是这样:

复制代码
.PHONY:clean
clean:
    rm -rf mytest

然后我们使用一下它。退出vim,执行make clean

我们发现mytest文件被删除了.

还有一个作用就是总是被执行.

什么叫总是被执行呢?先来看如果我们一直make会发生什么呢?

可以发现这里的意思是说mytest已经是最新了.但是我就是想让它每次都执行,这个时候你在前面加上.PHONY即可:

复制代码
.PHONY:mytest
mytest:test.c
    gcc test.c -o mytest

然后退出,便可以使make每次都被执行了.

4. Makefile编译多个文件

首先创建一个test.h文件,用于声明:

复制代码
#include "stdio.h"

extern void show();

再将原先test.c文件里内容做修改如下:

复制代码
#include "test.h"
void show()
{
    printf("hello.,linux!\r\n");
}

再创建一个main.c用于执行

复制代码
#include "test.h"

int main()
{
    show();
    return 0;
}

在编写Makefile时,我们习惯于把依赖文件写成.o的形式.而.o又需要.c来编译。所以Makefile会自动向下寻找这些编译语句,我们只要写上编译的语句即可.

复制代码
mytest:main.o test.o
    gcc -o mytest main.o test.o
test.o:test.c
    gcc -o test.o -c test.c
main.o:main.c
    gcc -o main.o -c main.c

.PHONY:clean
clean:
    rm -rf mytest

执行make语句.发现已经编译成功且正常输出.

相关推荐
小马过河R2 小时前
浅谈AI辅助编码从氛围编程Vibe Coding到基于spec规范驱动开发
人工智能·驱动开发·ai编程
huangyuchi.2 小时前
【Linux 网络】理解并应用应用层协议:HTTP(附简单HTTP服务器C++代码)
linux·服务器·网络·网络协议·http·c/c++
电子小子洋酱2 小时前
Linux驱动开发学习笔记(更新中)
linux·笔记·单片机
行思理2 小时前
Linux多PHP如何切换系统默认PHP版本
linux·运维·php
AI视觉网奇2 小时前
图生3d 人脸 算法笔记 2025
笔记·3d
charlie1145141912 小时前
现代C++工程实践:简单的IniParser4——实现ini_parser
开发语言·c++·笔记·学习·工程
jimy12 小时前
ps aux|grep pid 和 ps -p pid 的区别
java·linux·开发语言
weixin_437546332 小时前
注释文件夹下脚本的Debug
java·linux·算法
zfj3213 小时前
容器 的 cpu request limit 与 linux cgroups 的关系
linux·运维·服务器·kubernetes·cgroup