嵌入式 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 映射和像素写入功能正常。
十三、遇到的问题与解决方法
- 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。
- 矩形循环逻辑
矩形绘制需要双层循环:
for (int j = y; j < y + h; j++)
{
for (int i = x; i < x + w; i++)
{
lcd_draw_point(i, j, color);
}
}
外层循环控制行,内层循环控制列。矩形可以理解为一行一行画出来的多个像素点。
- 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
十四、面试知识点总结
- framebuffer 是什么?
framebuffer 是 Linux 提供的一种显示设备接口,通常对应 /dev/fb0。应用程序可以通过该设备访问 LCD 显存,从而控制屏幕显示内容。
- 为什么要使用 mmap()?
mmap() 可以将 framebuffer 显存映射到用户空间,程序可以像操作普通内存一样修改屏幕像素,适合大量像素数据的直接访问。
- ioctl() 在本项目中有什么作用?
本项目中使用 ioctl() 的 FBIOGET_VSCREENINFO 命令获取 LCD 的屏幕参数,例如宽度、高度和像素位数。
- 为什么要进行边界判断?
如果绘制点或矩形时坐标超出屏幕范围,可能会导致 framebuffer 越界访问,因此需要进行坐标合法性检查。
- 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 界面打下了基础。

浙公网安备 33010602011771号