嵌入式 Linux 实战:基于 framebuffer 封装 LCD 显示模块

一、项目背景

在嵌入式 Linux 开发中,LCD 显示是非常基础的人机交互功能。很多开发板都会提供显示屏,应用程序可以通过 Linux 提供的 framebuffer 设备接口访问 LCD 显存,从而实现颜色显示、图形绘制、图片显示等功能。

本次练习基于粤嵌 GEC6818 开发板,通过操作 /dev/fb0 设备,实现 LCD 显示模块的封装。项目主要完成 LCD 初始化、屏幕清屏、指定坐标画点、绘制矩形以及资源释放等功能。

本项目的目的不是单纯让屏幕显示颜色,而是将 LCD 显示功能封装成一个可复用模块,为后续实现 BMP 图片显示、触摸屏控制、菜单界面和简单 GUI 项目打基础。

二、开发环境

项目内容

  • 开发板:粤嵌GEC6818开发板
  • 操作系统:嵌入式 Linux
  • 显示设备:/dev/fb0
  • 编程语言:C
  • 编译工具:gcc
  • 构建工具:Makefile
  • 代码托管:Gitee
    项目仓库地址:

https://gitee.com/res-wangwei/embedded-linux-lcd-framebuffer

三、framebuffer 基本原理

framebuffer 是 Linux 系统提供的一种显示设备接口。应用程序可以通过 /dev/fb0 访问 LCD 显示设备。

在本项目中,LCD 显示的基本流程如下:

open 打开 /dev/fb0
        ↓
ioctl 获取屏幕参数
        ↓
计算 framebuffer 映射大小
        ↓
mmap 映射显存
        ↓
通过指针写入像素颜色
        ↓
munmap 解除映射
        ↓
close 关闭设备

其中,ioctl 用于获取 LCD 的分辨率和像素位数,mmap 用于将 LCD 显存映射到用户空间。映射成功后,程序可以像操作普通内存一样修改屏幕显示内容。

四、项目目录结构

本项目采用模块化结构,而不是把所有代码都写在一个 main.c 中。

lcd_module_project/
├── include/
│   └── lcd.h
├── src/
│   ├── lcd.c
│   └── main.c
├── Makefile
├── README.md
└── .gitignore

各文件作用如下:

文件	作用
main.c	程序入口,负责调用 LCD 模块接口
lcd.h	LCD 模块头文件,声明对外接口
lcd.c	LCD 模块实现文件,完成 framebuffer 操作
Makefile	管理项目编译
README.md	项目说明文档

这种结构更接近实际公司项目中的代码组织方式,便于后续维护和功能扩展。

五、LCD 模块接口设计

本项目在 lcd.h 中设计了以下接口:

int lcd_init(const char *fb_path);
void lcd_clear(unsigned int color);
void lcd_draw_point(int x, int y, unsigned int color);
void lcd_draw_rect(int x, int y, int w, int h, unsigned int color);
void lcd_uninit(void);

各接口功能如下:

函数	功能
lcd_init()	初始化 framebuffer 设备
lcd_clear()	使用指定颜色清屏
lcd_draw_point()	在指定坐标画一个像素点
lcd_draw_rect()	在指定区域绘制矩形
lcd_uninit()	释放 framebuffer 相关资源

这样设计后,main.c 不需要关心底层如何打开 /dev/fb0、如何 mmap 映射显存,只需要调用这些封装好的接口即可。

六、LCD 初始化实现

LCD 初始化主要完成四件事:

1. 打开 framebuffer 设备
2. 获取屏幕参数
3. 计算显存大小
4. mmap 映射显存

核心代码如下:

int lcd_init(const char *fb_path)
{
    struct fb_var_screeninfo vinfo;

    lcd_fd = open(fb_path, O_RDWR);
    if (lcd_fd < 0)
    {
        perror("open framebuffer error");
        return -1;
    }

    if (ioctl(lcd_fd, FBIOGET_VSCREENINFO, &vinfo) < 0)
    {
        perror("ioctl FBIOGET_VSCREENINFO failed");
        close(lcd_fd);
        lcd_fd = -1;
        return -1;
    }

    screen_width = vinfo.xres;
    screen_height = vinfo.yres;
    bits_per_pixel = vinfo.bits_per_pixel;

    screensize = screen_width * screen_height * bits_per_pixel / 8;

    fb_mem = mmap(NULL, screensize, PROT_READ | PROT_WRITE, MAP_SHARED, lcd_fd, 0);
    if (fb_mem == MAP_FAILED)
    {
        perror("mmap failed");
        close(lcd_fd);
        lcd_fd = -1;
        fb_mem = NULL;
        return -1;
    }

    printf("LCD init success: %d x %d, %d bpp\n",
           screen_width, screen_height, bits_per_pixel);

    return 0;
}

这里需要注意,mmap() 失败时返回的是 MAP_FAILED,不是 NULL。因此判断映射是否失败时应使用:

if (fb_mem == MAP_FAILED)

七、清屏功能实现

清屏的本质是:遍历整个屏幕,把每一个像素都设置为同一种颜色。

void lcd_clear(unsigned int color)
{
    if (fb_mem == NULL)
    {
        return;
    }

    for (int i = 0; i < screen_width * screen_height; i++)
    {
        fb_mem[i] = color;
    }
}

本开发板 LCD 为 32 位色,每个像素占 4 字节,因此使用 unsigned int * 指针操作 framebuffer,每次写入一个像素。

常见颜色值如下:

0x00FF0000  // 红色
0x0000FF00  // 绿色
0x000000FF  // 蓝色
0x00FFFFFF  // 白色
0x00000000  // 黑色

八画点功能实现

画点函数用于在指定坐标显示一个像素点。

void lcd_draw_point(int x, int y, unsigned int color)
{
    if (fb_mem == NULL)
    {
        return;
    }

    if (x < 0 || x >= screen_width || y < 0 || y >= screen_height)
    {
        return;
    }

    fb_mem[y * screen_width + x] = color;
}

LCD 屏幕是二维坐标,但 framebuffer 在内存中是一维连续空间。因此坐标 (x, y) 对应的一维数组下标为:

y * screen_width + x

例如屏幕宽度为 800,则坐标 (400, 240) 对应的位置为:

240 * 800 + 400

这里还需要进行边界判断,防止坐标超出屏幕范围,造成越界访问。

九、画矩形功能实现

矩形本质上是由大量像素点组成的,因此可以通过重复调用 lcd_draw_point() 实现。

void lcd_draw_rect(int x, int y, int w, int h, unsigned int color)
{
    if (fb_mem == NULL)
    {
        return;
    }

    if (w <= 0 || h <= 0)
    {
        return;
    }

    for (int j = y; j < y + h; j++)
    {
        for (int i = x; i < x + w; i++)
        {
            lcd_draw_point(i, j, color);
        }
    }
}

外层循环控制纵坐标,也就是一行一行向下绘制;内层循环控制横坐标,也就是每一行从左到右绘制。

例如:

lcd_draw_rect(100, 100, 200, 100, 0x0000FF00);

表示从坐标 (100, 100) 开始,绘制一个宽 200、高 100 的绿色矩形。

十、资源释放

程序退出前需要释放资源,主要包括:

1. 解除 mmap 映射
2. 关闭 framebuffer 文件描述符

代码如下:

void lcd_uninit(void)
{
    if (fb_mem != NULL)
    {
        munmap(fb_mem, screensize);
        fb_mem = NULL;
    }

    if (lcd_fd >= 0)
    {
        close(lcd_fd);
        lcd_fd = -1;
    }
}

释放资源时需要先判断资源是否有效,避免重复释放或关闭无效文件描述符。

十一、主函数调用流程

main.c 中只负责调用 LCD 模块接口:

int main(int argc, char const *argv[])
{
    if (lcd_init("/dev/fb0") < 0)
    {
        return -1;
    }

    lcd_clear(0x00FF0000);

    lcd_draw_rect(100, 100, 200, 100, 0x0000FF00);
    lcd_draw_rect(350, 150, 120, 200, 0x000000FF);
    lcd_draw_point(400, 240, 0x00FFFFFF);

    sleep(5);

    lcd_clear(0x00000000);

    lcd_uninit();

    return 0;
}

这样 main.c 逻辑非常清晰:
初始化 LCD
清屏
画矩形
画点
延时
清屏
释放资源

十二、运行效果

程序运行后,LCD 屏幕显示红色背景,并绘制绿色矩形和蓝色矩形。

图片插入位置:

在这里插入你拍的开发板运行照片:

图 1 LCD 显示红色背景、绿色矩形和蓝色矩形

可以配文字:

从运行结果可以看到,程序成功完成了 LCD 清屏和矩形绘制功能,说明 framebuffer 映射和像素写入功能正常。

十三、遇到的问题与解决方法

  1. mmap() 失败判断

一开始容易误以为 mmap() 失败返回 NULL,实际上 mmap() 失败返回 MAP_FAILED。

正确写法:

if (fb_mem == MAP_FAILED)
{
perror("mmap failed");
return -1;
}
2. 坐标边界问题

在画点函数中,需要判断坐标是否合法:

if (x < 0 || x >= screen_width || y < 0 || y >= screen_height)
{
return;
}

如果屏幕宽度是 800,则合法横坐标是 0 ~ 799,因此判断条件应使用 x >= screen_width。

  1. 矩形循环逻辑

矩形绘制需要双层循环:

for (int j = y; j < y + h; j++)
{
for (int i = x; i < x + w; i++)
{
lcd_draw_point(i, j, color);
}
}

外层循环控制行,内层循环控制列。矩形可以理解为一行一行画出来的多个像素点。

  1. Git 上传问题

项目完成后,使用 Git 将代码上传到 Gitee。第一次上传时需要初始化仓库、提交代码并关联远程仓库:

git init
git add .
git commit -m "完成 LCD framebuffer 显示模块封装"
git remote add origin 仓库地址
git branch -M main
git push -u origin main

后续修改只需要:

git add .
git commit -m "提交说明"
git push

十四、面试知识点总结

  1. framebuffer 是什么?

framebuffer 是 Linux 提供的一种显示设备接口,通常对应 /dev/fb0。应用程序可以通过该设备访问 LCD 显存,从而控制屏幕显示内容。

  1. 为什么要使用 mmap()?

mmap() 可以将 framebuffer 显存映射到用户空间,程序可以像操作普通内存一样修改屏幕像素,适合大量像素数据的直接访问。

  1. ioctl() 在本项目中有什么作用?

本项目中使用 ioctl() 的 FBIOGET_VSCREENINFO 命令获取 LCD 的屏幕参数,例如宽度、高度和像素位数。

  1. 为什么要进行边界判断?

如果绘制点或矩形时坐标超出屏幕范围,可能会导致 framebuffer 越界访问,因此需要进行坐标合法性检查。

  1. lcd.h、lcd.c、main.c 分别负责什么?

lcd.h 负责声明 LCD 模块对外接口;lcd.c 负责实现 LCD 底层操作;main.c 负责调用 LCD 模块接口并组织程序流程。

十五、总结

本次项目完成了基于 Linux framebuffer 的 LCD 显示模块封装,掌握了 open、ioctl、mmap、munmap、close 等系统调用在嵌入式 Linux 显示开发中的基本用法。

通过本项目,我理解了 framebuffer 的基本工作流程,也初步掌握了模块化编程方法。相比将所有代码写在 main.c 中,将 LCD 操作封装为独立模块可以提高代码复用性和可维护性。

本项目为后续实现 BMP 图片显示、触摸屏交互、串口控制显示以及简单 GUI 界面打下了基础。

posted @ 2026-05-21 22:08  wwwicjh  阅读(74)  评论(0)    收藏  举报