news 2026/7/22 6:15:05

嵌入式Linux C应用编程——Framebuffer应用编程

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
嵌入式Linux C应用编程——Framebuffer应用编程

什么是 FrameBuffer
FrameBuffer(帧缓冲) 是 Linux 系统中的一种显示驱动接口。它将显示设备(如 LCD)进行抽象,屏蔽了不同显示设备硬件的实现差异,对应用层呈现为一块显示内存(显存),允许上层应用程序直接对显示缓冲区进行读写操作,而无需关心物理显存的位置等具体细节,这些都由 FrameBuffer 设备驱动来完成。

在 Linux 系统中,显示设备被称为 FrameBuffer 设备(帧缓冲设备),LCD 显示屏即属于 FrameBuffer 设备。FrameBuffer 设备对应的设备文件为 /dev/fbX(X 为数字,如 0、1、2、3 等),Linux 最多可支持 32 个 FrameBuffer 设备,分别为 /dev/fb0 到 /dev/fb31。开发板出厂系统中,/dev/fb0 设备节点便是 LCD 屏。

应用程序读写 /dev/fbX 就相当于读写显示设备的显示缓冲区(显存)。例如,若 LCD 分辨率为 800*480,每个像素点用 24 位(RGB888)表示,则显存大小为:

800
×
480
×
24
/
8

1152000
字节
执行以下命令可将 LCD 清屏(填充为黑色):

dd if=/dev/zero of=/dev/fb0 bs=1024 count=1125
该命令将 1125 × 1024 个字节的 0x00 数据全部写入 LCD 显存中。

LCD 显示原理
LCD 屏幕由按矩阵排列的像素点阵组成,每个像素由 R(红)、G(绿)、B(蓝)三个子像素构成。显示的原理大致如下:

显存(FrameBuffer):系统在内存中划出一块区域作为显存,每个像素的颜色数据按顺序存放在这块内存中。显存中第 (y × 宽度 + x) 个位置的数据,就对应屏幕上第 y 行第 x 列的像素。
LCD 控制器:硬件上有一个 LCD 控制器,它会以固定的刷新率(如 60Hz)不断地从显存中读取像素数据。
显示驱动:控制器将读出的数字信号转换为 LCD 面板需要的模拟电压,驱动每个子像素显示对应的亮度,组合起来就成了一帧完整的画面。
刷新过程:控制器从显存起始地址开始,逐行扫描读取每个像素的数据并输出到屏幕,扫完一帧后立即从头开始下一帧,周而复始。
所以,应用程序往显存中写入数据,LCD 控制器自动将其显示到屏幕上——这就是为什么读写 /dev/fbX 就能控制 LCD 显示。

为什么需要显存?
LCD 需要持续刷新:LCD 面板必须以固定频率(如 60Hz)不断刷新,否则画面会消失。这意味着显示数据的供给是连续且实时的。
CPU 无法独占:CPU 还要运行其他任务,不可能一直都在送数据给 LCD。
显存作为缓冲区:在内存中划出一块区域作为显存,CPU 只需要在需要更新画面时往里面写数据,其余时间可以去做别的事;而 LCD 控制器通过 DMA(直接存储器访问) 方式独立地从显存读取数据并驱动显示,完全不需要 CPU 干预。
LCD 应用编程介绍
操作 /dev/fbX 设备的一般步骤如下:

打开 /dev/fbX 设备文件。
使用 ioctl() 获取当前显示设备的参数信息(如分辨率、像素格式),据此计算显示缓冲区的大小。
通过存储映射 I/O(mmap()) 将屏幕的显示缓冲区映射到用户空间。
映射成功后,直接读写显存进行绘图或图片显示等操作。
完成显示后,调用 munmap() 取消映射,并调用 close() 关闭设备文件。
19.3.1 使用 ioctl() 获取屏幕参数信息
对于 FrameBuffer 设备,常用的 ioctl() 请求如下:

宏定义 功能 数据结构
FBIOGET_VSCREENINFO 获取可变参数信息 struct fb_var_screeninfo
FBIOPUT_VSCREENINFO 设置可变参数信息 struct fb_var_screeninfo
FBIOGET_FSCREENINFO 获取固定参数信息 struct fb_fix_screeninfo
这些宏定义及数据结构均定义在 <linux/fb.h> 头文件中。

#define FBIOGET_VSCREENINFO 0x4600
#define FBIOPUT_VSCREENINFO 0x4601
#define FBIOGET_FSCREENINFO 0x4602
struct fb_var_screeninfo 结构体
该结构体描述 FrameBuffer 设备的可变参数信息:

struct fb_var_screeninfo {
__u32 xres; /* 可视区域 X 分辨率(一行像素点数)/
__u32 yres; /
可视区域 Y 分辨率(一列像素点数)/
__u32 xres_virtual; /
虚拟区域 X 分辨率/
__u32 yres_virtual; /
虚拟区域 Y 分辨率/
__u32 xoffset; /
虚拟到可见屏幕的行偏移/
__u32 yoffset; /
虚拟到可见屏幕的列偏移/
__u32 bits_per_pixel; /
像素深度 bpp/
__u32 grayscale; /
=0 彩色, =1 灰度, >1 FOURCC 颜色 */

struct fb_bitfield red; /* 红色分量色域偏移 */ struct fb_bitfield green; /* 绿色分量色域偏移 */ struct fb_bitfield blue; /* 蓝色分量色域偏移 */ struct fb_bitfield transp; /* 透明度分量色域偏移 */ __u32 nonstd; /* =0 标准像素格式, ≠0 非标准 */ __u32 activate; __u32 height; /* 显示高度(mm) */ __u32 width; /* 显示宽度(mm) */ __u32 accel_flags; /* 时序参数 */ __u32 pixclock; /* pixel clock in ps */ __u32 left_margin; __u32 right_margin; __u32 upper_margin; __u32 lower_margin; __u32 hsync_len; __u32 vsync_len; __u32 sync; __u32 vmode; __u32 rotate; __u32 colorspace; __u32 reserved[4];

};
通过 xres、yres 获取屏幕分辨率,bits_per_pixel 获取像素深度,显存大小计算公式为:

screen_size

x
r
e
s
×
y
r
e
s
×
b
i
t
s
p
e
r
p
i
x
e
l
/
8
xres_virtual、yres_virtual 表示虚拟分辨率,通常与 xres、yres 一致,但也可以设置得比实际分辨率大。虚拟区域配合 xoffset、yoffset 可实现两个实用功能:

滚动(Panning):虚拟区域大于屏幕时,通过修改偏移量让屏幕显示虚拟区域的不同部分,实现平滑滚动而无需重新绘制。
双缓冲(Double Buffering):将 yres_virtual 设为 2 * yres,显存中就有上下两个页面。一页用于显示的同时往另一页绘图,然后切换偏移量,避免画面撕裂。
red、green、blue 三个 struct fb_bitfield 变量描述 RGB 颜色通道的位偏移和位长度,据此可判断 LCD 的像素格式(如 RGB888、RGB565、BGR888 等)。

struct fb_fix_screeninfo 结构体
该结构体描述 FrameBuffer 设备的固定参数信息(不可修改):

struct fb_fix_screeninfo {
char id[16]; /* 字符串标识符/
unsigned long smem_start; /
显存起始地址(物理地址)/
__u32 smem_len; /
显存长度/
__u32 type;
__u32 type_aux;
__u32 visual;
__u16 xpanstep;
__u16 ypanstep;
__u16 ywrapstep;
__u32 line_length; /
一行的字节数/
unsigned long mmio_start; /
MMIO 起始地址(物理地址)/
__u32 mmio_len; /
MMIO 长度 */
__u32 accel;
__u16 capabilities;
__u16 reserved[2];
};
line_length 表示屏幕一行像素点所占的字节数,通常可用 line_length * yres 计算显示缓冲区大小。

struct fb_var_screeninfo —— 前缀 var 代表 Variable(可变),描述分辨率、像素格式等可动态调整的参数,可通过 FBIOPUT_VSCREENINFO 修改(前提是驱动支持)。
struct fb_fix_screeninfo —— 前缀 fix 代表 Fixed(固定),描述显存地址、行长等硬件底层固定的参数,应用程序只能读取,不可修改。
struct fb_bitfield 结构体
struct fb_bitfield {
__u32 offset; /* 偏移量/
__u32 length; /
长度/
__u32 msb_right; /
!= 0 : 最高有效位在右侧 */
};
获取屏幕参数示例程序
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <sys/ioctl.h>
#include <linux/fb.h>

int main(int argc, char *argv[])
{
struct fb_fix_screeninfo fb_fix;
struct fb_var_screeninfo fb_var;
int fd;

/* 打开 framebuffer 设备 */ if (0 > (fd = open("/dev/fb0", O_WRONLY))) { perror("open error"); exit(-1); } /* 获取参数信息 */ ioctl(fd, FBIOGET_VSCREENINFO, &fb_var); ioctl(fd, FBIOGET_FSCREENINFO, &fb_fix); printf("分辨率: %d*%d\n" "像素深度 bpp: %d\n" "一行的字节数: %d\n" "像素格式: R<%d %d> G<%d %d> B<%d %d>\n", fb_var.xres, fb_var.yres, fb_var.bits_per_pixel, fb_fix.line_length, fb_var.red.offset, fb_var.red.length, fb_var.green.offset, fb_var.green.length, fb_var.blue.offset, fb_var.blue.length); /* 关闭设备文件退出程序 */ close(fd); exit(0);

}
注意:ALPHA/Mini I.MX6U 开发板出厂系统中,LCD 驱动程序将其实现为 RGB565 格式的显示设备。用户可通过修改设备树使其支持 RGB888,或通过 ioctl 修改。不建议通过 FBIOPUT_VSCREENINFO 随意修改可变参数,若驱动支持不够完善,可能引发问题。

使用 mmap() 将显示缓冲区映射到用户空间
为什么需要使用存储映射 I/O 而非普通 read()/write()?当数据量较大时,普通 I/O 方式效率较低。例如,一台 1920×1080 分辨率、ARGB8888 像素格式的显示器,刷一帧图像的数据量为:

1920
×
1080
×
32
/
8

8294400
字节(约 8MB)
且显示的图像往往是动态更新的,数据量庞大,使用普通 I/O 必然导致效率低下,因此采用 mmap() 存储映射 I/O 方式。

为什么普通 I/O 效率低?

每次调用 read()/write() 都经历以下过程:

系统调用:用户态 → 内核态切换
数据拷贝:将数据从用户缓冲区拷贝到内核缓冲区(一次额外的内存拷贝)
写入设备:驱动将内核缓冲区的数据写入显存
如果要刷新一帧 8MB 的数据,每刷一部分就要重复一次上述流程,系统调用次数和数据拷贝量都非常大。

而 mmap() 的优势在于:建立映射后,应用程序直接读写映射区的内存,相当于直接操作显存——无需系统调用、无需内核态切换、无需数据拷贝。这也是存储映射 I/O 的优势。

LCD 应用编程
本节实现 LCD 上的画点(打点)、画线、画矩形、填充等基本操作。

关键宏定义
#define argb8888_to_rgb565(color) ({
unsigned int temp = (color);
((temp & 0xF80000UL) >> 8) |
((temp & 0xFC00UL) >> 5) |
((temp & 0xF8UL) >> 3);
})
该宏将 ARGB8888 格式颜色转换为 RGB565 格式。因为开发板出厂系统的 LCD 是 RGB565 格式(每像素 16 位),而绘图函数传参用的是 ARGB8888(32 位整数,如 0xFF0000 表示红色),写入显存前必须压缩为 16 位 RGB565 值,否则直接赋值给 unsigned short 会发生截断导致颜色错误。

之所以不直接用 RGB565 传参,我猜测应该是主要有两点考虑:

可读性:0xFF0000 一眼就能看出是红色,而 RGB565 的红色 0xF800 不够直观。
灵活性:若后期将 LCD 切换为 RGB888 模式,只需修改转换宏,所有调用了 lcd_fill(…, 0xFF0000) 的代码无需改动。
主要函数
函数 功能 说明
lcd_draw_point(x, y, color) 打点 在 (x, y) 位置绘制颜色为 color 的像素点
lcd_draw_line(x, y, dir, length, color) 画线 从 (x, y) 开始画水平或垂直的线条,dir!=0 为水平,dir=0 为垂直
lcd_draw_rectangle(s_x, e_x, s_y, e_y, color) 画矩形 绘制指定矩形区域的四条边
lcd_fill(s_x, e_x, s_y, e_y, color) 填充 将指定矩形区域填充为指定颜色
示例代码(lcd_test.c)
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <sys/ioctl.h>
#include <sys/mman.h>
#include <linux/fb.h>

#define argb8888_to_rgb565(color) ({
unsigned int temp = (color);
((temp & 0xF80000UL) >> 8) |
((temp & 0xFC00UL) >> 5) |
((temp & 0xF8UL) >> 3);
})

static int width; // LCD X 分辨率
static int height; // LCD Y 分辨率
static unsigned short *screen_base = NULL; // 映射后的显存基地址

static void lcd_draw_point(unsigned int x, unsigned int y, unsigned int color)
{
unsigned short rgb565_color = argb8888_to_rgb565(color);

if (x >= width) x = width - 1; if (y >= height) y = height - 1; screen_base[y * width + x] = rgb565_color;

}

static void lcd_draw_line(unsigned int x, unsigned int y, int dir,
unsigned int length, unsigned int color)
{
unsigned short rgb565_color = argb8888_to_rgb565(color);
unsigned int end;
unsigned long temp;

if (x >= width) x = width - 1; if (y >= height) y = height - 1; temp = y * width + x; if (dir) { // 水平线 end = x + length - 1; if (end >= width) end = width - 1; for ( ; x <= end; x++, temp++) screen_base[temp] = rgb565_color; } else { // 垂直线 end = y + length - 1; if (end >= height) end = height - 1; for ( ; y <= end; y++, temp += width) screen_base[temp] = rgb565_color; }

}

static void lcd_draw_rectangle(unsigned int start_x, unsigned int end_x,
unsigned int start_y, unsigned int end_y,
unsigned int color)
{
int x_len = end_x - start_x + 1;
int y_len = end_y - start_y - 1;

lcd_draw_line(start_x, start_y, 1, x_len, color); // 上边 lcd_draw_line(start_x, end_y, 1, x_len, color); // 下边 lcd_draw_line(start_x, start_y + 1, 0, y_len, color); // 左边 lcd_draw_line(end_x, start_y + 1, 0, y_len, color); // 右边

}

static void lcd_fill(unsigned int start_x, unsigned int end_x,
unsigned int start_y, unsigned int end_y,
unsigned int color)
{
unsigned short rgb565_color = argb8888_to_rgb565(color);
unsigned long temp;
unsigned int x;

if (end_x >= width) end_x = width - 1; if (end_y >= height) end_y = height - 1; temp = start_y * width; for ( ; start_y <= end_y; start_y++, temp += width) { for (x = start_x; x <= end_x; x++) screen_base[temp + x] = rgb565_color; }

}

int main(int argc, char *argv[])
{
struct fb_fix_screeninfo fb_fix;
struct fb_var_screeninfo fb_var;
unsigned int screen_size;
int fd;

/* 打开 framebuffer 设备 */ if (0 > (fd = open("/dev/fb0", O_RDWR))) { perror("open error"); exit(EXIT_FAILURE); } /* 获取参数信息 */ ioctl(fd, FBIOGET_VSCREENINFO, &fb_var); ioctl(fd, FBIOGET_FSCREENINFO, &fb_fix); /* 计算显存大小:用 line_length * yres 而非 xres * yres * bpp / 8 * 因为 line_length 是驱动实际返回的一行字节数,可能包含对齐填充字节 * 用后者算出的值可能偏小,导致 mmap 映射区域不足 */ screen_size = fb_fix.line_length * fb_var.yres; width = fb_var.xres; height = fb_var.yres; /* mmap(起始地址, 长度, 读写权限, 映射方式, 文件描述符, 偏移量) PROT_WRITE:映射区可写 MAP_SHARED:共享映射。对映射区的修改会写回文件 (从用户空间映射区写到磁盘设备中)。 * 将显存映射到用户空间,返回映射区的首地址 */ screen_base = mmap(NULL, screen_size, PROT_WRITE, MAP_SHARED, fd, 0); if (MAP_FAILED == (void *)screen_base) { perror("mmap error"); close(fd); exit(EXIT_FAILURE); } /* 画正方形方块 */ int w = height * 0.25; lcd_fill(0, width-1, 0, height-1, 0x0); // 清屏(黑色) lcd_fill(0, w, 0, w, 0xFF0000); // 红色方块 lcd_fill(width-w, width-1, 0, w, 0xFF00); // 绿色方块 lcd_fill(0, w, height-w, height-1, 0xFF); // 蓝色方块 lcd_fill(width-w, width-1, height-w, height-1, 0xFFFF00); // 黄色方块 /* 画线: 十字交叉线 */ lcd_draw_line(0, height * 0.5, 1, width, 0xFFFFFF); // 白色水平线 lcd_draw_line(width * 0.5, 0, 0, height, 0xFFFFFF); // 白色垂直线 /* 画矩形 */ unsigned int s_x, s_y, e_x, e_y; s_x = 0.25 * width; s_y = w; e_x = width - s_x; e_y = height - s_y; for ( ; (s_x <= e_x) && (s_y <= e_y); s_x+=5, s_y+=5, e_x-=5, e_y-=5) lcd_draw_rectangle(s_x, e_x, s_y, e_y, 0xFFFFFF); /* 退出 */ munmap(screen_base, screen_size); close(fd); exit(EXIT_SUCCESS);

}
main() 函数执行流程
open() 打开 /dev/fb0 获取文件描述符 fd。
ioctl() 获取 LCD 的可变参数和固定参数信息,计算显存大小和分辨率。
mmap() 建立显存映射。
调用自定义函数在 LCD 上绘图(画方块、画线、画矩形)。
munmap() 取消映射,close() 关闭设备文件,退出程序。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/7/22 6:11:35

AI时代产品经理的技术可行性评估与跨团队协作

1. AI时代产品经理的角色进化2007年&#xff0c;乔布斯发布第一代iPhone时&#xff0c;产品经理还主要依靠市场调研和直觉决策。2023年&#xff0c;ChatGPT的爆发让产品开发进入全新时代。作为经历过这个转型期的从业者&#xff0c;我深刻感受到&#xff1a;AI正在重塑产品经理…

作者头像 李华
网站建设 2026/7/22 6:09:12

Unity3D集成Qwen3-32B大模型:构建智能对话机器人的架构与实战

1. 项目概述&#xff1a;当游戏引擎遇上大语言模型 最近在做一个挺有意思的项目&#xff0c;叫Clawdbot。简单来说&#xff0c;这是一个在Unity3D里跑起来的智能对话机器人。但它的“智能”内核&#xff0c;不是传统的规则脚本&#xff0c;而是直接集成了Qwen3-32B这个大语言模…

作者头像 李华
网站建设 2026/7/22 6:08:05

C++时间复杂度实战:从算法原理到工程优化与性能陷阱

1. 项目概述&#xff1a;为什么时间复杂度是C程序员的“内功心法”刚入行那会儿&#xff0c;我总觉得算法题做出来就行&#xff0c;直到有一次线上服务因为一个O(n)的查询在大流量下直接崩掉&#xff0c;才真正体会到时间复杂度&#xff08;Time Complexity&#xff09;不是书本…

作者头像 李华
网站建设 2026/7/22 6:07:02

C++实战:构建股票收益预测系统,集成学习与超参优化全解析

1. 项目概述&#xff1a;用C构建一个实战级股票收益预测系统在量化金融和算法交易领域&#xff0c;用机器学习模型预测股票收益是一个经典且充满挑战的课题。很多朋友可能习惯用Python的Scikit-learn或XGBoost快速搭建原型&#xff0c;但当我们追求极致的执行效率、低延迟的预测…

作者头像 李华
网站建设 2026/7/22 6:06:20

Excel文件损坏修复全攻略:从基础到高级方法

1. Excel文件崩溃的常见场景与原因分析每次遇到Excel文件突然崩溃打不开的情况&#xff0c;那种心跳漏拍的感觉我都记忆犹新。作为财务分析岗位出身的我&#xff0c;曾经因为一个包含全年预算数据的xlsx文件损坏&#xff0c;差点错过重要汇报。经过多年与Excel故障的"斗争…

作者头像 李华
网站建设 2026/7/22 6:04:29

比较好用的云手机有哪些 全价位机型综合测评指南

云手机长期使用的真实成本&#xff0c;不能只看页面标注的包月标价&#xff0c;各类隐藏增值收费、带宽限速、功能分层解锁&#xff0c;会让低价套餐的实际开销大幅上浮。当下多数产品采用“低价引流功能拆分收费”的运营模式&#xff0c;离线保活、ROOT权限、批量群控、高清画…

作者头像 李华