Linux FrameBuffer绘图实验

撬动未来的支点 24天前 153

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

输出如下:

枚举fbdev设备
枚举fbdev设备

可以看到只有一个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] = {
        {00},                                    // 红色:左上角
        {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"00, 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;
}

流程示意图:

绘制流程1
绘制流程1

使用管理员权限运行,按下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] = {
        {00},                                    // 红色:左上角
        {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"00, 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文件映射到了用户空间,我们的代码可以直接读写对应的内存,非常方便,也减少了系统调用,提高了写入效率,运行效果如下:

使用mmap写入运行效果
使用mmap写入运行效果

代码已上传gitee,项目地址:

  • https://gitee.com/pivotfuture/linux_framebuffer_draw_test

2.5 存在的问题

我们看到渲染结果中下面的两个矩形因为被裁剪,出现了显示不全的问题。这可能是因为字符模式下存在一些特殊的设置导致的,大家可以搜索相关资料或通过进一步实验去寻找原因。

3.结语

本次我们进行了FrameBuffer绘图实验。动手实验能够获得更深入的经验,印象能够更加深刻,否则只能浮于表面。后面我们将继续通过实验探索Linux图形驱动架构的相关技术。

最新回复 (0)
返回
发新帖