全部版块 我的主页
论坛 数据科学与人工智能 IT基础 Linux操作系统
75 0
2025-12-01

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
  1. 解析事件内容,识别出这是一个帧缓冲设备;
  2. 根据预设规则(通常配置于
  3. /lib/udev/rules.d/
  4. 文件中)进行处理;
  5. 确认帧缓冲设备的主设备号为
  6. 29
  7. (Linux系统约定值);
  8. 依据不同的次设备号(0、1、2…),在 /dev 目录下创建相应的字符设备文件。

例如,为第一个帧缓冲设备创建节点的命令等效于:

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 > brightness
echo 1 > brightness
echo 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;
二维码

扫码加我 拉你入群

请注明:姓名-公司-职位

以便审核进群资格,未注明则拒绝

栏目导航
热门文章
推荐文章

说点什么

分享

扫码加好友,拉您进群
各岗位、行业、专业交流群