1.前言
在前面的文章《Linux图形驱动架构演进》中,我们从全局角度梳理了一下Linux的图形驱动的发展过程以及技术栈。接下来我们就从最古老和最简单的FrameBuffer/fbdev开始入手,进行一些绘图测试,亲自感受一下直接操作FrameBuffer/fbdev驱动进行绘图的过程,讲再多原理不如实操一遍。
2.FrameBuffer/fbdev绘图测试
2.1 环境准备
用VMWare装一个Ubuntu-18.04.6或者Ubuntu-20.04.6的虚拟机,或者参考文章 《【开箱即用】VMWare-Ubuntu系列虚拟机下载》下载直接可用的虚拟机。接下来我们在Ubuntu-18.04.6中进行绘图实验。
2.2 枚举fbdev设备
在Linux中一切皆文件,文件不一定是磁盘上的真实文件,文件只是一套标准接口而已。它底层可以是任何对象,只要它实现了文件的操作接口,并且已暴露到了文件系统中,那么它就是一个文件。
进入Ubuntu环境,执行以下命令,枚举 /dev下的设备文件:
ls -l /dev
输出如下:
可以看到只有一个FrameBuffer设备fb0,可能是因为只有一个显示屏幕,所以只有一个FrameBuffer设备fb0。
2.3 向fb0写入像素数据!
现在向fb0写入像素数据,就可以在屏幕上显示图形了,因为写入的数据会被刷新或者映射到显示器显示的相关内存中,随着显示器的刷新,很快画面就会从内存中刷新到屏幕显示。
代码如下:
main.c
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <fcntl.h>
#include <sys/ioctl.h>
#include <linux/fb.h>
#include <time.h>
// framebuffer结构体
typedef struct framebuffer_info {
int fd;
struct fb_var_screeninfo vinfo;
struct fb_fix_screeninfo finfo;
long int screensize;
char *buffer; // 内存缓冲区
} framebuffer_t;
// 初始化framebuffer
int init_framebuffer(framebuffer_t *fb) {
// 打开framebuffer设备
fb->fd = open("/dev/fb0", O_RDWR);
if (fb->fd == -1) {
perror("无法打开framebuffer设备");
return -1;
}
// 获取可变屏幕信息
if (ioctl(fb->fd, FBIOGET_VSCREENINFO, &fb->vinfo) == -1) {
perror("无法获取可变屏幕信息");
close(fb->fd);
return -1;
}
// 获取固定屏幕信息
if (ioctl(fb->fd, FBIOGET_FSCREENINFO, &fb->finfo) == -1) {
perror("无法获取固定屏幕信息");
close(fb->fd);
return -1;
}
// 计算屏幕大小
fb->screensize = fb->vinfo.xres * fb->vinfo.yres * fb->vinfo.bits_per_pixel / 8;
// 分配内存缓冲区
fb->buffer = malloc(fb->screensize);
if (fb->buffer == NULL) {
perror("分配内存缓冲区失败");
close(fb->fd);
return -1;
}
// 初始化缓冲区为黑色
memset(fb->buffer, 0, fb->screensize);
printf("Framebuffer初始化成功:\n");
printf(" 分辨率: %dx%d\n", fb->vinfo.xres, fb->vinfo.yres);
printf(" 颜色深度: %d位\n", fb->vinfo.bits_per_pixel);
printf(" 屏幕大小: %ld字节\n", fb->screensize);
printf(" 行长度: %d字节\n", fb->finfo.line_length);
printf(" 颜色格式: 红偏移=%d, 绿偏移=%d, 蓝偏移=%d\n",
fb->vinfo.red.offset, fb->vinfo.green.offset, fb->vinfo.blue.offset);
printf(" 颜色长度: 红长度=%d, 绿长度=%d, 蓝长度=%d\n",
fb->vinfo.red.length, fb->vinfo.green.length, fb->vinfo.blue.length);
return 0;
}
// 带颜色的像素设置函数(写入内存缓冲区)
void set_pixel_color(framebuffer_t *fb, int x, int y, unsigned int color) {
if (x >= 0 && x < fb->vinfo.xres && y >= 0 && y < fb->vinfo.yres) {
long int location = (x + fb->vinfo.xoffset) * (fb->vinfo.bits_per_pixel / 8) +
(y + fb->vinfo.yoffset) * fb->finfo.line_length;
if (location >= 0 && location < fb->screensize - 4) {
if (fb->vinfo.bits_per_pixel == 32) {
// 尝试不同的颜色格式
unsigned int pixel_color;
// 根据颜色偏移构建像素值
if (fb->vinfo.red.offset == 16 && fb->vinfo.green.offset == 8 && fb->vinfo.blue.offset == 0) {
// RGB格式
pixel_color = color;
} else if (fb->vinfo.red.offset == 0 && fb->vinfo.green.offset == 8 && fb->vinfo.blue.offset == 16) {
// BGR格式 - 交换红蓝分量
pixel_color = ((color & 0xFF) << 16) | (color & 0xFF00) | ((color & 0xFF0000) >> 16);
} else {
// 默认尝试RGB格式
pixel_color = color;
}
// 直接写入内存缓冲区
*((unsigned int*)(fb->buffer + location)) = pixel_color;
} else if (fb->vinfo.bits_per_pixel == 16) {
// 16位RGB565格式
unsigned short pixel_color = ((color & 0xF80000) >> 8) | // 红色
((color & 0x00FC00) >> 5) | // 绿色
((color & 0x0000F8) >> 3); // 蓝色
// 直接写入内存缓冲区
*((unsigned short*)(fb->buffer + location)) = pixel_color;
}
}
}
}
// 简单的像素设置函数(红色)
void set_pixel_simple(framebuffer_t *fb, int x, int y) {
set_pixel_color(fb, x, y, 0xFF0000); // 红色
}
// 清屏(清空内存缓冲区)
void clear_screen_simple(framebuffer_t *fb) {
memset(fb->buffer, 0, fb->screensize);
}
// 批量写入内存缓冲区到framebuffer
void flush_buffer(framebuffer_t *fb) {
if (lseek(fb->fd, 0, SEEK_SET) == -1) {
perror("定位到文件开头失败");
return;
}
if (write(fb->fd, fb->buffer, fb->screensize) != fb->screensize) {
perror("批量写入framebuffer失败");
}
}
// 绘制简单的正方形
void draw_square_simple(framebuffer_t *fb, int center_x, int center_y, int size) {
int x, y;
int half_size = size / 2;
for (y = center_y - half_size; y < center_y + half_size; y++) {
for (x = center_x - half_size; x < center_x + half_size; x++) {
set_pixel_simple(fb, x, y);
}
}
}
// 清理framebuffer
void cleanup_framebuffer(framebuffer_t *fb) {
if (fb->buffer != NULL) {
free(fb->buffer);
}
if (fb->fd != -1) {
close(fb->fd);
}
}
int main() {
framebuffer_t fb;
printf("Linux Framebuffer Demo - 绘制四个彩色矩形\n");
printf("========================================\n");
// 初始化framebuffer
if (init_framebuffer(&fb) != 0) {
printf("Framebuffer初始化失败!\n");
return -1;
}
printf("开始清屏...\n");
clear_screen_simple(&fb);
printf("清屏完成\n");
printf("开始绘制四个彩色矩形...\n");
printf("按Ctrl+C退出\n");
// 四个矩形的颜色:红、蓝、绿、黄(顺时针)
unsigned int colors[] = {
0xFF0000, // 红色
0x0000FF, // 蓝色
0x00FF00, // 绿色
0xFFFF00 // 黄色
};
// 计算四个矩形的位置(不重叠,从左上角开始排列)
unsigned int rect_width = fb.vinfo.xres / 2; // 矩形宽度(屏幕一半)
unsigned int rect_height = fb.vinfo.yres / 2; // 矩形高度(屏幕一半)
// 矩形左上角位置(不重叠排列)
unsigned int rect_positions[4][2] = {
{0, 0}, // 红色:左上角
{rect_width, 0}, // 蓝色:右上角
{0, rect_height}, // 绿色:左下角
{rect_width, rect_height} // 黄色:右下角
};
printf("绘制四个不重叠的矩形:红(左上)、蓝(右上)、绿(左下)、黄(右下)\n");
printf("屏幕分辨率: %dx%d, 行长度: %d字节\n", fb.vinfo.xres, fb.vinfo.yres, fb.finfo.line_length);
printf("矩形大小: %dx%d\n", rect_width, rect_height);
printf("注意:行长度=%d字节,每行有%d字节填充\n", fb.finfo.line_length, fb.finfo.line_length - fb.vinfo.xres * 4);
printf("矩形位置:\n");
printf(" 红色(左上): (%d,%d) 到 (%d,%d)\n", 0, 0, rect_width-1, rect_height-1);
printf(" 蓝色(右上): (%d,%d) 到 (%d,%d)\n", rect_width, 0, rect_width*2-1, rect_height-1);
printf(" 绿色(左下): (%d,%d) 到 (%d,%d)\n", 0, rect_height, rect_width-1, rect_height*2-1);
printf(" 黄色(右下): (%d,%d) 到 (%d,%d)\n", rect_width, rect_height, rect_width*2-1, rect_height*2-1);
printf("开始循环绘制四个矩形...\n");
printf("按Ctrl+C退出\n");
int frame_count = 0;
while (1) {
// 清屏
clear_screen_simple(&fb);
// 绘制四个矩形
for (int rect = 0; rect < 4; rect++) {
unsigned int start_x = rect_positions[rect][0];
unsigned int start_y = rect_positions[rect][1];
unsigned int color = colors[rect];
// 绘制矩形(从左上角开始,严格边界检查)
for (unsigned int y = start_y; y < start_y + rect_height && y < fb.vinfo.yres; y++) {
for (unsigned int x = start_x; x < start_x + rect_width && x < fb.vinfo.xres; x++) {
set_pixel_color(&fb, x, y, color);
}
}
}
// 批量写入内存缓冲区到framebuffer
flush_buffer(&fb);
// 每50帧输出一次状态
frame_count++;
if (frame_count % 50 == 0) {
printf("帧 %d: 四个矩形绘制完成\n", frame_count);
}
// 延迟
usleep(40000);
}
// 清理资源
cleanup_framebuffer(&fb);
printf("程序结束\n");
return 0;
}
流程示意图:
使用管理员权限运行,按下Ctrl + Alt + F6进入字符模式后,可以看到如下效果:
按Ctrl + Alt + F1可以切换回Ubuntu桌面。
在字符模式下,是使用FrameBuffer进行图形渲染的。原本显示的是一些系统提示符:
在我们向FrameBuffer连续写入图形像素数据后,原有的像素数据被覆盖了,变成了四个彩色矩形。
2.4 使用mmap实现高效写入
代码如下:
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <fcntl.h>
#include <sys/ioctl.h>
#include <sys/mman.h>
#include <linux/fb.h>
#include <time.h>
// framebuffer结构体
typedef struct framebuffer_info {
int fd;
char *fbp;
struct fb_var_screeninfo vinfo;
struct fb_fix_screeninfo finfo;
long int screensize;
} framebuffer_t;
// 初始化framebuffer
int init_framebuffer(framebuffer_t *fb) {
// 打开framebuffer设备
fb->fd = open("/dev/fb0", O_RDWR);
if (fb->fd == -1) {
perror("无法打开framebuffer设备");
return -1;
}
// 获取可变屏幕信息
if (ioctl(fb->fd, FBIOGET_VSCREENINFO, &fb->vinfo) == -1) {
perror("无法获取可变屏幕信息");
close(fb->fd);
return -1;
}
// 获取固定屏幕信息
if (ioctl(fb->fd, FBIOGET_FSCREENINFO, &fb->finfo) == -1) {
perror("无法获取固定屏幕信息");
close(fb->fd);
return -1;
}
// 计算屏幕大小
fb->screensize = fb->vinfo.xres * fb->vinfo.yres * fb->vinfo.bits_per_pixel / 8;
// 映射framebuffer到内存
fb->fbp = (char*)mmap(0, fb->screensize, PROT_READ | PROT_WRITE, MAP_SHARED, fb->fd, 0);
if (fb->fbp == MAP_FAILED) {
perror("无法映射framebuffer");
close(fb->fd);
return -1;
}
printf("Framebuffer初始化成功:\n");
printf(" 分辨率: %dx%d\n", fb->vinfo.xres, fb->vinfo.yres);
printf(" 颜色深度: %d位\n", fb->vinfo.bits_per_pixel);
printf(" 屏幕大小: %ld字节\n", fb->screensize);
printf(" 行长度: %d字节\n", fb->finfo.line_length);
printf(" 颜色格式: 红偏移=%d, 绿偏移=%d, 蓝偏移=%d\n",
fb->vinfo.red.offset, fb->vinfo.green.offset, fb->vinfo.blue.offset);
printf(" 颜色长度: 红长度=%d, 绿长度=%d, 蓝长度=%d\n",
fb->vinfo.red.length, fb->vinfo.green.length, fb->vinfo.blue.length);
return 0;
}
// 带颜色的像素设置函数
void set_pixel_color(framebuffer_t *fb, int x, int y, unsigned int color) {
if (x >= 0 && x < fb->vinfo.xres && y >= 0 && y < fb->vinfo.yres) {
long int location = (x + fb->vinfo.xoffset) * (fb->vinfo.bits_per_pixel / 8) +
(y + fb->vinfo.yoffset) * fb->finfo.line_length;
if (location >= 0 && location < fb->screensize - 4) {
if (fb->vinfo.bits_per_pixel == 32) {
// 尝试不同的颜色格式
unsigned int pixel_color;
// 根据颜色偏移构建像素值
if (fb->vinfo.red.offset == 16 && fb->vinfo.green.offset == 8 && fb->vinfo.blue.offset == 0) {
// RGB格式
pixel_color = color;
} else if (fb->vinfo.red.offset == 0 && fb->vinfo.green.offset == 8 && fb->vinfo.blue.offset == 16) {
// BGR格式 - 交换红蓝分量
pixel_color = ((color & 0xFF) << 16) | (color & 0xFF00) | ((color & 0xFF0000) >> 16);
} else {
// 默认尝试RGB格式
pixel_color = color;
}
*((unsigned int*)(fb->fbp + location)) = pixel_color;
} else if (fb->vinfo.bits_per_pixel == 16) {
// 16位RGB565格式
unsigned short pixel_color = ((color & 0xF80000) >> 8) | // 红色
((color & 0x00FC00) >> 5) | // 绿色
((color & 0x0000F8) >> 3); // 蓝色
*((unsigned short*)(fb->fbp + location)) = pixel_color;
}
}
}
}
// 简单的像素设置函数(红色)
void set_pixel_simple(framebuffer_t *fb, int x, int y) {
set_pixel_color(fb, x, y, 0xFF0000); // 红色
}
// 清屏
void clear_screen_simple(framebuffer_t *fb) {
memset(fb->fbp, 0, fb->screensize);
}
// 绘制简单的正方形
void draw_square_simple(framebuffer_t *fb, int center_x, int center_y, int size) {
int x, y;
int half_size = size / 2;
for (y = center_y - half_size; y < center_y + half_size; y++) {
for (x = center_x - half_size; x < center_x + half_size; x++) {
set_pixel_simple(fb, x, y);
}
}
}
// 清理framebuffer
void cleanup_framebuffer(framebuffer_t *fb) {
if (fb->fbp != MAP_FAILED) {
munmap(fb->fbp, fb->screensize);
}
if (fb->fd != -1) {
close(fb->fd);
}
}
int main() {
framebuffer_t fb;
printf("Linux Framebuffer Demo - 绘制四个彩色矩形\n");
printf("========================================\n");
// 初始化framebuffer
if (init_framebuffer(&fb) != 0) {
printf("Framebuffer初始化失败!\n");
return -1;
}
printf("开始清屏...\n");
clear_screen_simple(&fb);
printf("清屏完成\n");
printf("开始绘制四个彩色矩形...\n");
printf("按Ctrl+C退出\n");
// 四个矩形的颜色:红、蓝、绿、黄(顺时针)
unsigned int colors[] = {
0xFF0000, // 红色
0x0000FF, // 蓝色
0x00FF00, // 绿色
0xFFFF00 // 黄色
};
// 计算四个矩形的位置(不重叠,从左上角开始排列)
unsigned int rect_width = fb.vinfo.xres / 2; // 矩形宽度(屏幕一半)
unsigned int rect_height = fb.vinfo.yres / 2; // 矩形高度(屏幕一半)
// 矩形左上角位置(不重叠排列)
unsigned int rect_positions[4][2] = {
{0, 0}, // 红色:左上角
{rect_width, 0}, // 蓝色:右上角
{0, rect_height}, // 绿色:左下角
{rect_width, rect_height} // 黄色:右下角
};
printf("绘制四个不重叠的矩形:红(左上)、蓝(右上)、绿(左下)、黄(右下)\n");
printf("屏幕分辨率: %dx%d, 行长度: %d字节\n", fb.vinfo.xres, fb.vinfo.yres, fb.finfo.line_length);
printf("矩形大小: %dx%d\n", rect_width, rect_height);
printf("注意:行长度=%d字节,每行有%d字节填充\n", fb.finfo.line_length, fb.finfo.line_length - fb.vinfo.xres * 4);
printf("矩形位置:\n");
printf(" 红色(左上): (%d,%d) 到 (%d,%d)\n", 0, 0, rect_width-1, rect_height-1);
printf(" 蓝色(右上): (%d,%d) 到 (%d,%d)\n", rect_width, 0, rect_width*2-1, rect_height-1);
printf(" 绿色(左下): (%d,%d) 到 (%d,%d)\n", 0, rect_height, rect_width-1, rect_height*2-1);
printf(" 黄色(右下): (%d,%d) 到 (%d,%d)\n", rect_width, rect_height, rect_width*2-1, rect_height*2-1);
printf("开始循环绘制四个矩形...\n");
printf("按Ctrl+C退出\n");
int frame_count = 0;
while (1) {
// 清屏
clear_screen_simple(&fb);
// 绘制四个矩形
for (int rect = 0; rect < 4; rect++) {
unsigned int start_x = rect_positions[rect][0];
unsigned int start_y = rect_positions[rect][1];
unsigned int color = colors[rect];
// 绘制矩形(从左上角开始,严格边界检查)
for (unsigned int y = start_y; y < start_y + rect_height && y < fb.vinfo.yres; y++) {
for (unsigned int x = start_x; x < start_x + rect_width && x < fb.vinfo.xres; x++) {
set_pixel_color(&fb, x, y, color);
}
}
}
// 每50帧输出一次状态
frame_count++;
if (frame_count % 50 == 0) {
printf("帧 %d: 四个矩形绘制完成\n", frame_count);
}
// 延迟
usleep(40000);
}
// 清理资源
cleanup_framebuffer(&fb);
printf("程序结束\n");
return 0;
}
代码逻辑基本不变,只是采用了mmap将fb0文件映射到了用户空间,我们的代码可以直接读写对应的内存,非常方便,也减少了系统调用,提高了写入效率,运行效果如下:
代码已上传gitee,项目地址:
-
https://gitee.com/pivotfuture/linux_framebuffer_draw_test
2.5 存在的问题
我们看到渲染结果中下面的两个矩形因为被裁剪,出现了显示不全的问题。这可能是因为字符模式下存在一些特殊的设置导致的,大家可以搜索相关资料或通过进一步实验去寻找原因。
3.结语
本次我们进行了FrameBuffer绘图实验。动手实验能够获得更深入的经验,印象能够更加深刻,否则只能浮于表面。后面我们将继续通过实验探索Linux图形驱动架构的相关技术。