43. Linux LCD 驱动开发详解
在嵌入式Linux系统中,LCD驱动的实现主要围绕“打点”与“读点”操作展开。通过直接操控显存中的像素数据,可以实现对屏幕内容的精确控制。为了构建更加丰富的用户界面,通常会在此基础上引入GUI图形库,从而支持更复杂的UI设计。
本次目标是实现一个基本的Linux内核驱动,使其能够在LCD屏幕上完成像素级的绘制功能。以i.MX6ULL平台为例,其官方EVK开发板已自带了屏幕驱动支持,设备节点表现为:
/dev/fb0
该节点是Framebuffer子系统向上层应用程序提供的标准接口文件。应用层可通过对该设备节点的读写操作,间接控制LCD的显示内容。
Framebuffer机制简介
Framebuffer(帧缓冲)是Linux内核中用于抽象LCD显示设备的一种核心机制。它将底层的LCD控制器和显存资源封装成统一的设备文件形式(如/dev/fbX,其中X为序号),使得应用程序无需关心硬件细节即可进行图形输出。
对于RGB接口的LCD屏幕,应用程序通过访问/dev/fbX对应的显存区域,即可实现字符、图像等内容的显示。这种机制屏蔽了不同硬件之间的差异,提升了系统的可移植性。
因此,我们的主要任务就是编写并配置好对应/dev/fbX的设备驱动程序,确保其能正确初始化LCD控制器,并提供可用的显存映射。
硬件支持与参数适配
大多数情况下,LCD控制器的底层驱动已经由芯片厂商或屏幕供应商完成。例如i.MX6ULL内置的LCDIF模块,其控制器驱动已在内核中实现。开发者通常只需根据所使用的具体屏幕型号,调整相应的时序参数和分辨率设置即可,无需从零编写驱动代码。
在设备树文件 imx6ull.dtsi 中,可以看到如下定义:
lcdif: lcdif@021c8000 {
compatible = "fsl,imx6ul-lcdif", "fsl,imx28-lcdif";
reg = <0x021c8000 0x4000>;
interrupts = <GIC_SPI 5 IRQ_TYPE_LEVEL_HIGH>;
clocks = <&clks IMX6UL_CLK_LCDIF_PIX>,
<&clks IMX6UL_CLK_LCDIF_APB>,
<&clks IMX6UL_CLK_DUMMY>;
clock-names = "pix", "axi", "disp_axi";
status = "disabled";
};
上述代码描述了LCD控制器的寄存器地址、中断、时钟等资源配置,其中compatible字段标明了驱动匹配的关键标识:
- "fsl,imx6ul-lcdif"
- "fsl,imx28-lcdif"
驱动源码定位与结构分析
通过全局搜索compatible字符串,可以定位到对应的驱动源文件:
drivers/video/fbdev/mxsfb.c
该文件实现了针对i.MX系列芯片的Framebuffer驱动,采用标准的platform驱动框架。其关键结构如下:
static const struct of_device_id mxsfb_dt_ids[] = {
{ .compatible = "fsl,imx23-lcdif", .data = &mxsfb_devtype[0], },
{ .compatible = "fsl,imx28-lcdif", .data = &mxsfb_devtype[1], },
{ /* sentinel */ }
};
该数组用于设备树匹配,确保驱动能够正确绑定到对应的硬件节点。
platform驱动注册部分如下:
static struct platform_driver mxsfb_driver = {
.probe = mxsfb_probe,
.remove = mxsfb_remove,
.shutdown = mxsfb_shutdown,
.id_table = mxsfb_devtype,
.driver = {
.name = DRIVER_NAME,
.of_match_table = mxsfb_dt_ids,
.pm = &mxsfb_pm_ops,
},
};
module_platform_driver(mxsfb_driver);
这是一个典型的platform驱动模型,使用module_platform_driver()宏完成自动注册。当设备树中存在匹配的节点时,内核会调用.probe函数进行硬件初始化。
Framebuffer核心结构体 fb_info
在Linux内核中,每一个Framebuffer设备都由一个struct fb_info结构体来表示,它是整个LCD驱动管理的核心数据结构。其定义位于头文件:
include/linux/fb.h
该结构体包含多个重要成员,用于描述当前显示状态和硬件特性:
struct fb_info {
atomic_t count;
int node;
int flags;
struct mutex lock; /* 保护open/release/ioctl等操作 */
struct mutex mm_lock; /* 保护mmap及显存相关字段 */
struct fb_var_screeninfo var; /* 可变参数:分辨率、位深、刷新率等 */
struct fb_fix_screeninfo fix; /* 固定参数:显存起始地址、长度等 */
struct fb_monspecs monspecs; /* 显示器规格信息 */
struct work_struct queue; /* 帧缓冲事件队列 */
struct fb_pixmap pixmap; /* 图像硬件映射器 */
...
};
其中:
var字段保存动态可变的显示参数,如当前分辨率、颜色深度、像素格式等;fix字段记录不可更改的硬件属性,如显存物理地址、总大小等。
这些信息共同构成了用户空间操作LCD的基础,也是驱动实现过程中必须正确填充的关键内容。
struct fb_info {
struct fb_pixmap sprite; /* Cursor hardware mapper */
struct fb_cmap cmap; /* 当前颜色映射表 */
struct list_head modelist; /* 显示模式链表 */
struct fb_videomode *mode; /* 当前活动的显示模式 */
#ifdef CONFIG_FB_BACKLIGHT
struct backlight_device *bl_dev; /* 关联的背光控制设备 */
// 注册帧缓冲前设置,注销后移除
struct mutex bl_curve_mutex;
u8 bl_curve[FB_BACKLIGHT_LEVELS]; /* 背光亮度调节曲线 */
#endif
#ifdef CONFIG_FB_DEFERRED_IO
struct delayed_work deferred_work;
struct fb_deferred_io *fbdefio; /* 延迟IO机制相关结构 */
#endif
struct fb_ops *fbops; /* 帧缓冲操作函数集 */
struct device *device; /* 父设备指针 */
struct device *dev; /* 当前fb设备的设备结构 */
int class_flag; /* sysfs中使用的私有标志位 */
#ifdef CONFIG_FB_TILEBLITTING
struct fb_tile_ops *tileops; /* 用于图块渲染的操作接口 */
#endif
char __iomem *screen_base; /* 显存虚拟内存映射地址 */
unsigned long screen_size; /* 映射的显存大小,若无则为0 */
void *pseudo_palette; /* 16色伪调色板(用于某些不支持真彩色的场景) */
#define FBINFO_STATE_RUNNING 0
#define FBINFO_STATE_SUSPENDED 1
u32 state; /* 硬件运行状态,例如是否挂起 */
void *fbcon_par; /* 仅供fbcon使用的私有数据区 */
/* 以下字段均为设备特定部分 */
void *par; /* 指向底层硬件驱动私有数据的指针 */
/* 注意:使用 aperture 的 base/size,而非 smem_start/size,
因为 smem_start 可能只是 aperture 内部分配的一段内存,
并不一定覆盖整个物理窗口区域 */
struct apertures_struct {
unsigned int count;
struct aperture {
resource_size_t base; /* aperture 物理基地址 */
resource_size_t size; /* aperture 区域大小 */
} ranges[0];
} *apertures;
bool skip_vt_switch; /* 挂起/恢复时跳过虚拟终端切换 */
};
结构体初始化说明
在实际使用中,需对 fb_info 结构体进行正确初始化。该结构体代表一个帧缓冲设备实例,其核心操作集合由 fb_ops 提供,定义了如绘图、刷新、模式设置等底层操作。
注册接口函数
系统通过如下函数将初始化完成的帧缓冲信息注册到内核:
int register_framebuffer(struct fb_info *fb_info)
- 参数说明:
fb_info:指向已填充好的struct fb_info实例,表示待注册的帧缓冲设备。
- 返回值:
- 返回 0 表示注册成功;
- 返回负数表示失败,常见如 -EINVAL(参数无效)、-ENODEV(无可用设备号)等。
注册成功后的用户空间体现
当调用 register_framebuffer 成功后,内核会触发一系列事件,最终在用户空间生成对应的设备节点。
/dev/fb0
内核事件通知机制
设备注册完成后,内核通过设备模型子系统发送设备添加事件。
uevent
具体流程如下:
内核发出
kobject_uevent
类型的uevent事件,携带关键属性信息,包括但不限于:
- 设备类别标识
- 主设备号与次设备号
- 设备路径和名称(如 framebuffer.0)
add
这些信息以环境变量形式传递给用户态管理程序。
MAJOR=29
MINOR=0
DEVNAME=fb0
udev
规则匹配与设备节点创建
用户空间的设备管理守护进程(如 udev 或嵌入式系统中的 mdev)监听内核发出的uevent事件。
udev
当接收到新的设备事件时,守护进程开始工作:
uevent
- 解析事件内容,识别出这是一个帧缓冲设备;
- 根据预设规则(通常配置于
- 文件中)进行处理;
- 确认帧缓冲设备的主设备号为
- (Linux系统约定值);
- 依据不同的次设备号(0、1、2…),在 /dev 目录下创建相应的字符设备文件。
/lib/udev/rules.d/
29
例如,为第一个帧缓冲设备创建节点的命令等效于:
mknod /dev/fb0 c 29 0
其中:
c
表示创建的是字符设备类型。
在嵌入式 Linux 系统中,帧缓冲(framebuffer)设备通过主设备号和次设备号进行管理。其中,
29 表示主设备号,次设备号则用于标识同一类型下的不同实例。
帧缓冲设备的注销操作由以下函数完成:
int unregister_framebuffer(struct fb_info *fb_info);
结构体 fb_info 包含多个成员变量,在实际开发中需重点关注以下几个字段:var、fix、fbops、screen_base、screen_size 以及 pseudo_palette。这些成员分别用于描述可变参数、固定参数、操作函数集、显存映射地址、屏幕内存大小和伪调色板配置。
在 MXS 架构的 LCDIF 驱动中,mxsfb_probe 函数承担了初始化工作的核心任务,其主要流程包括:
- 分配并申请
fb_info结构体实例; - 对
fb_info中的关键成员进行初始化设置; - 配置 eLCDIF 控制器的相关寄存器以适配硬件时序;
- 调用
register_framebuffer函数将初始化完成的fb_info注册到内核中,使其成为可用的图形设备。
为了使屏幕正常工作,必须修改设备树以启用对应的显示驱动。系统通过匹配设备节点中的 compatible 字段来识别设备。在 imx6ull.dtsi 文件中,存在如下定义:
lcdif: lcdif@021c8000 {
compatible = "fsl,imx6ul-lcdif", "fsl,imx28-lcdif";
reg = <0x021c8000 0x4000>;
interrupts = <GIC_SPI 5 IRQ_TYPE_LEVEL_HIGH>;
clocks = <&clks IMX6UL_CLK_LCDIF_PIX>,
<&clks IMX6UL_CLK_LCDIF_APB>,
<&clks IMX6UL_CLK_DUMMY>;
clock-names = "pix", "axi", "disp_axi";
status = "disabled";
};
在具体板级文件 imx6ull-alientek.dts 中,对该节点进行了进一步配置与激活:
&lcdif {
pinctrl-names = "default";
pinctrl-0 = <&pinctrl_lcdif_dat
&pinctrl_lcdif_ctrl>;
display = <&display0>;
status = "okay";
display0: display {
bits-per-pixel = <16>;
bus-width = <24>;
display-timings {
native-mode = <&timing0>;
timing0: timing0 {
clock-frequency = <35500000>;
hactive = <800>;
vactive = <480>;
hfront-porch = <210>;
hback-porch = <46>;
hsync-len = <20>;
vback-porch = <23>;
vfront-porch = <22>;
vsync-len = <3>;
hsync-active = <0>;
vsync-active = <0>;
de-active = <1>;
pixelclk-active = <1>;
};
};
};
};
上述设备树配置可支持分辨率为 480×800 的显示屏,并能在启动后显示 Linux 企鹅 Logo。若需调整开机图像显示选项,可通过内核配置菜单进入:
Device Drivers --->
Graphics support --->
Bootup logo (LOGO [=y])
在此处可根据需要启用或更改启动Logo的显示行为。
继续查看设备树中的引脚控制部分:
pinctrl-0 = <&pinctrl_lcdif_dat
&pinctrl_lcdif_ctrl>;
其中,数据线引脚组定义如下:
pinctrl_lcdif_dat: lcdifdatgrp {
fsl,pins = <
MX6UL_PAD_LCD_DATA00__LCDIF_DATA00 0x79
MX6UL_PAD_LCD_DATA01__LCDIF_DATA01 0x79
MX6UL_PAD_LCD_DATA02__LCDIF_DATA02 0x79
MX6UL_PAD_LCD_DATA03__LCDIF_DATA03 0x79
MX6UL_PAD_LCD_DATA04__LCDIF_DATA04 0x79
...
>;
};
控制信号引脚组定义如下:
pinctrl_lcdif_ctrl: lcdifctrlgrp {
fsl,pins = <
MX6UL_PAD_LCD_CLK__LCDIF_CLK 0x79
MX6UL_PAD_LCD_ENABLE__LCDIF_ENABLE 0x79
MX6UL_PAD_LCD_HSYNC__LCDIF_HSYNC 0x79
MX6UL_PAD_LCD_VSYNC__LCDIF_VSYNC 0x79
>;
};
将相关参数与LCD屏幕驱动进行匹配时,需注意电气特性的调整。例如,将原值 0x79 修改为 0x49,主要是考虑到 0x79 对应的驱动强度过高,可能对网络部分的驱动电路造成干扰。
关于 display0 节点的具体定义和含义,可参考设备树绑定文档:Documentation/devicetree/bindings/fb,其中详细说明了各个参数的作用。我们需要根据所使用开发板的实际屏幕规格,修改这些参数以确保显示正常工作。
display0: display {
bits-per-pixel = <32>;
bus-width = <24>;
display-timings {
native-mode = <&timing0>;
timing0: timing0 {
clock-frequency = <51200000>;
hactive = <1024>;
vactive = <600>;
hfront-porch = <160>;
hback-porch = <140>;
hsync-len = <20>;
vback-porch = <20>;
vfront-porch = <1>;
vsync-len = <3>;
// 根据屏幕数据手册中的时序图设置四根控制线的有效电平
hsync-active = <0>;
vsync-active = <0>;
de-active = <1>;
pixelclk-active = <1>;
};
};
};
完成上述修改后,重新编译并重启开发板,即可看到启动 logo 成功显示。
1. 操作显示驱动
通过以下命令测试字符输出到屏幕:
echo hello world > /dev/tty1
执行后可在屏幕上观察到“hello world”字符串被正确打印。
若希望将Linux系统的默认输出重定向至LCD屏幕,可修改 /etc/inittab 文件:
# etc/inittab ::sysinit:/etc/init.d/rcS console::askfirst:-/bin/sh tty1::askfirst:-/bin/sh ::restart:/sbin/init ::ctrlaltdel:/sbin/reboot ::shutdown:/bin/umount -a -r ::shutdown:/sbin/swapoff -a
接着修改U-Boot环境变量中的 bootargs 参数:
原设置:
setenv bootargs 'console=ttymxc0,115200 rw root=/dev/nfs nfsroot=192.168.3.15:/home/alientek/Desktop/nfs/rootfs ip=192.168.3.55:192.168.3.15:192.168.3.1:255.255.255.0::eth0:off'
修改为:
setenv bootargs 'console=tty1 console=ttymxc0,115200 root=/dev/nfs rw nfsroot=192.168.1.250:/home/zuozhongkai/linux/nfs/rootfs ip=192.168.1.251:192.168.1.250:192.168.1.1:255.255.255.0::eth0:off'
关键更改是添加 console=tty1,使系统同时在串口和LCD终端上输出信息。
2. 背光控制
背光通常由PWM信号调节亮度。在调试阶段,也可直接连接背光引脚供电强制点亮。
进入背光设备目录:
cd /sys/devices/platform/backlight/backlight/backlight
查看可用接口:
actual_brightness device subsystem bl_power max_brightness type brightness power uevent
通过写入数值调整亮度:
echo 5 > brightnessecho 1 > brightnessecho 7 > brightness
对应的设备树配置如下:
backlight {
compatible = "pwm-backlight";
pwms = <&pwm1 0 5000000>;
brightness-levels = <0 4 8 16 32 64 128 255>;
default-brightness-level = <7>;
status = "okay";
};
如需禁用屏幕自动休眠功能,可修改内核源码文件:drivers/tty/vt/vt.c
将:
static int blankinterval = 10*60;
改为:
static int blankinterval = 0;
保存后重新编译内核镜像(zImage),并加载到开发板运行,即可实现屏幕常亮。
在Linux系统中,帧缓冲设备(framebuffer)注册后通常会在/dev目录下生成对应的设备节点,例如/dev/fb0。通过该接口,应用程序可以直接操作显示内存,实现对屏幕的底层控制。若需进一步了解文字显示、图像绘制或UI界面开发,可参考关键词:linux fb编程、mmap、QT。
以下是一个简单的示例程序,展示了如何通过系统调用访问帧缓冲设备并进行像素级绘图:
#include <sys/mman.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <unistd.h>
#include <linux/fb.h>
#include <fcntl.h>
#include <stdio.h>
#include <string.h>
#include <sys/ioctl.h>
static int fd_fb; // 帧缓冲设备文件描述符
static struct fb_var_screeninfo var; /* 当前屏幕可变参数 */
static int screen_size; // 屏幕缓冲区总大小(字节)
static unsigned char *fb_base; // 映射后的帧缓冲内存起始地址
static unsigned int line_width; // 每行像素占用的字节数
static unsigned int pixel_width; // 单个像素所占字节数
该程序的核心功能之一是lcd_put_pixel函数,用于在指定坐标位置绘制一个带有颜色的像素点,并支持多种像素格式(如8位、16位、32位色深):
void lcd_put_pixel(int x, int y, unsigned int color)
{
unsigned char *pen_8 = fb_base + y * line_width + x * pixel_width;
unsigned short *pen_16;
unsigned int *pen_32;
unsigned int red, green, blue;
pen_16 = (unsigned short *)pen_8;
pen_32 = (unsigned int *)pen_8;
switch (var.bits_per_pixel)
{
case 8:
{
*pen_8 = color;
break;
}
case 16:
{
/* 使用RGB565格式 */
red = (color >> 16) & 0xff;
green = (color >> 8) & 0xff;
blue = (color >> 0) & 0xff;
color = ((red >> 3) << 11) | ((green >> 2) << 5) | (blue >> 3);
*pen_16 = color;
break;
}
case 32:
{
*pen_32 = color;
break;
}
default:
{
printf("不支持 %dbpp\n", var.bits_per_pixel);
break;
}
}
}

主函数中首先打开/dev/fb0设备文件,获取其读写权限:
int main(int argc, char **argv)
{
int i;
fd_fb = open("/dev/fb0", O_RDWR);
if (fd_fb < 0)
{
printf("无法打开 /dev/fb0\n");
return -1;
}
接着通过ioctl系统调用获取当前屏幕的可变信息,包括分辨率(xres、yres)和像素位深(bits_per_pixel)等,存储于var结构体中:
if (ioctl(fd_fb, FBIOGET_VSCREENINFO, &var))
{
printf("无法获取屏幕信息\n");
return -1;
}
// 根据获取到的信息计算相关参数
line_width = var.xres * var.bits_per_pixel / 8;
pixel_width = var.bits_per_pixel / 8;
后续可通过mmap将帧缓冲区映射至用户空间,从而直接操作显存完成图形绘制。整个流程为Linux平台下实现无GUI依赖的图形输出提供了基础支持。
// 计算屏幕所需的内存大小:根据分辨率和像素位数计算帧缓冲区总字节数
screen_size = var.xres * var.yres * var.bits_per_pixel / 8;
// 将帧缓冲设备的物理内存映射至进程地址空间,实现直接内存访问
// 使用 mmap 映射后,可通过指针 fb_base 直接读写显存,避免频繁系统调用
fb_base = (unsigned char *)mmap(NULL, screen_size, PROT_READ | PROT_WRITE, MAP_SHARED, fd_fb, 0);
if (fb_base == (unsigned char *)-1)
{
printf("can't mmap\n");
return -1;
}
/* 清空屏幕内容:将整个帧缓冲区填充为白色 */
// 利用 memset 函数将映射内存全部置为 0xff,对应白色显示
memset(fb_base, 0xff, screen_size);
/* 在屏幕中央绘制 100 个红色像素点 */
// 通过循环调用 lcd_put_pixel,在水平方向上画出一条红色线段
for (i = 0; i < 100; i++)
lcd_put_pixel(var.xres/2+i, var.yres/2, 0xFF0000);
// 程序结束前释放相关资源
munmap(fb_base, screen_size); // 解除内存映射,回收虚拟内存区域
close(fd_fb); // 关闭帧缓冲设备文件描述符
return 0;


雷达卡


京公网安备 11010802022788号







