学习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 压缩包
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绘制,这里不详细介绍。
节点常见操作:
- 查看当前支持的可用显示分辨率列表:
cat /sys/class/drm/card0-HDMI-A-1/modes
- 查看当前分辨率:
cat /sys/class/drm/card0-HDMI-A-1/mode
- 打开hdmiout显示:
echo on > /sys/class/drm/card0-HDMI-A-1/status
- 关闭hdmiout显示:
echo off > /sys/class/drm/card0-HDMI-A-1/status
- 查看hdmiout显示状态:(打开是connected,关闭是disconnected)
cat /sys/class/drm/card0-HDMI-A-1/status
- 重新检测HDMI连接:
echo detect > /sys/class/drm/card0-HDMI-A-1/status
在设置HDMI分辨率的时候,如果设置为自适应模式(Auto)的时候,就去设置status为detect,如果设置HDMI为固定分辨率的时候(如1080p)就设置HDMI的状态为on,即让它一直为输出HDMI信号的状态
- 查看 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的整数倍。
- 查看驱动名字
cat /sys/kernel/debug/dri/0/name
- 查看屏幕信息(当前的输出分辨率和帧率)
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);
}
- 变量声明
- pdev:平台设备对象,表示与驱动匹配的硬件设备。
- dev:指向平台设备关联的
device结构体,用于后续资源管理。 - id:用于存储设备树匹配结果。
- desc:指向面板描述符(
panel_desc)的指针,包含面板的时序、分辨率等参数。 - d:临时面板描述符指针,用于动态分配场景。
- err:错误码
-
设备树匹配
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(设备不存在)。
-
动态分配面板描述符
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 中。
-
确定最终面板描述符
desc = id->data ? id->data : d;
逻辑:
- 如果
id->data非空(静态描述符已定义),直接使用它。 - 否则使用动态分配的
d(从设备树解析得到)。
- 核心初始化函数
- panel_simple_probe:核心初始化函数,通常完成以下操作:
-
- 注册
drm_panel结构体,定义面板操作(如enable、disable)。 - 解析
desc中的时序参数,配置显示控制器(CRTC)。 - 初始化背光、电源管理 GPIO。
- 将面板注册到 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;
}
- 判断设备树中是否有"no-hpd"(不支持热插拔)属性,如果没有,即支持热插拔,则获取热插拔的gpio;
- 获取名为"power"的电源管理器;
- 获取gpio(使能和复位)状态 devm_gpiod_get_optional
4.4. 休眠唤醒
4.4.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属性
- 解除准备
相关代码:
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. 唤醒
- 使能面板
相关代码:
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属性
- 保持准备
相关代码:
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是一个文件.
- 什么是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语句.发现已经编译成功且正常输出.