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

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 字节800 \times 480 \times 24 / 8 = 1152000 \text{ 字节} 800×480×24/8=1152000 字节

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

bash 复制代码
dd if=/dev/zero of=/dev/fb0 bs=1024 count=1125

该命令将 1125 × 1024 个字节的 0x00 数据全部写入 LCD 显存中。

LCD 显示原理

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

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

所以,应用程序往显存中写入数据,LCD 控制器自动将其显示到屏幕上------这就是为什么读写 /dev/fbX 就能控制 LCD 显示。

为什么需要显存?

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

LCD 应用编程介绍

操作 /dev/fbX 设备的一般步骤如下:

  1. 打开 /dev/fbX 设备文件。
  2. 使用 ioctl() 获取当前显示设备的参数信息(如分辨率、像素格式),据此计算显示缓冲区的大小。
  3. 通过存储映射 I/O(mmap() 将屏幕的显示缓冲区映射到用户空间。
  4. 映射成功后,直接读写显存进行绘图或图片显示等操作。
  5. 完成显示后,调用 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> 头文件中。

c 复制代码
#define FBIOGET_VSCREENINFO 0x4600
#define FBIOPUT_VSCREENINFO 0x4601
#define FBIOGET_FSCREENINFO 0x4602
struct fb_var_screeninfo 结构体

该结构体描述 FrameBuffer 设备的可变参数信息:

c 复制代码
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];
};

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

screen_size=xres×yres×bits_per_pixel/8\text{screen\_size} = xres \times yres \times bits\_per\_pixel / 8 screen_size=xres×yres×bits_per_pixel/8

xres_virtualyres_virtual 表示虚拟分辨率 ,通常与 xresyres 一致,但也可以设置得比实际分辨率大。虚拟区域配合 xoffsetyoffset 可实现两个实用功能:

  • 滚动(Panning):虚拟区域大于屏幕时,通过修改偏移量让屏幕显示虚拟区域的不同部分,实现平滑滚动而无需重新绘制。
  • 双缓冲(Double Buffering) :将 yres_virtual 设为 2 * yres,显存中就有上下两个页面。一页用于显示的同时往另一页绘图,然后切换偏移量,避免画面撕裂。

redgreenblue 三个 struct fb_bitfield 变量描述 RGB 颜色通道的位偏移和位长度,据此可判断 LCD 的像素格式(如 RGB888、RGB565、BGR888 等)。

struct fb_fix_screeninfo 结构体

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

c 复制代码
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 结构体
c 复制代码
struct fb_bitfield {
    __u32 offset;       /* 偏移量 */
    __u32 length;       /* 长度 */
    __u32 msb_right;    /* != 0 : 最高有效位在右侧 */
};
获取屏幕参数示例程序
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 <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)1920 \times 1080 \times 32 / 8 = 8294400 \text{ 字节(约 8MB)} 1920×1080×32/8=8294400 字节(约 8MB)

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

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

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

  1. 系统调用:用户态 → 内核态切换
  2. 数据拷贝 :将数据从用户缓冲区 拷贝到内核缓冲区(一次额外的内存拷贝)
  3. 写入设备:驱动将内核缓冲区的数据写入显存

如果要刷新一帧 8MB 的数据,每刷一部分就要重复一次上述流程,系统调用次数和数据拷贝量都非常大。

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

LCD 应用编程

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

关键宏定义

c 复制代码
#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 传参,我猜测应该是主要有两点考虑:

  1. 可读性0xFF0000 一眼就能看出是红色,而 RGB565 的红色 0xF800 不够直观。
  2. 灵活性 :若后期将 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

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() 函数执行流程

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

显示 BMP 图片

BMP 图像介绍

BMP(Bitmap)是 Windows 操作系统中的标准图像文件格式,文件后缀名为 .bmp。它采用位映射存储格式,图像数据没有进行任何压缩,因此文件占用空间大但无失真、解析简单。

BMP 图像深度可选:1bit、4bit、8bit、16bit、24bit、32bit。典型的 BMP 文件由以下四部分组成:

数据段 大小 说明
BMP 文件头 14 字节 文件格式、大小、位图数据偏移量
位图信息头 通常 40 或 56 字节 尺寸、位深度、压缩方式、颜色索引
调色板 由颜色索引数决定 可选,索引到颜色的映射表
位图数据 由图像尺寸决定 图像原始数据

真彩色图像(16位、24位、32位)不需要调色板,位图信息头后面紧跟位图数据。

BMP 文件头结构
c 复制代码
typedef struct tagBITMAPFILEHEADER {
    UINT16 bfType;        /* 文件类型,"BM" 表示 Windows 位图 */
    DWORD  bfSize;        /* 文件大小(字节) */
    UINT16 bfReserved1;   /* 保留,必须为 0 */
    UINT16 bfReserved2;   /* 保留,必须为 0 */
    DWORD  bfOffBits;     /* 从文件起始到位图数据的字节偏移量 */
} BITMAPFILEHEADER;
位图信息头结构
c 复制代码
typedef struct tagBITMAPINFOHEADER {
    DWORD  biSize;            /* 位图信息头大小 */
    LONG   biWidth;           /* 图像宽度(像素) */
    LONG   biHeight;          /* 图像高度(像素),正数为倒向位图,负数为正向位图 */
    WORD   biPlanes;          /* 色彩平面数,总为 1 */
    WORD   biBitCount;        /* 像素深度(1/4/8/16/24/32) */
    DWORD  biCompression;     /* 压缩类型:0=RGB, 3=Bit-Fields */
    DWORD  biSizeImage;       /* 图像数据大小 */
    LONG   biXPelsPerMeter;   /* 水平分辨率(像素/米) */
    LONG   biYPelsPerMeter;   /* 垂直分辨率(像素/米) */
    DWORD  biClrUsed;         /* 实际使用的颜色索引数 */
    DWORD  biClrImportant;    /* 重要颜色索引数 */
} BITMAPINFOHEADER;
正向位图与倒向位图
  • 正向位图biHeight 为负数):图像数据按左上角到右下角排列,水平从左到右,垂直从上到下。
  • 倒向位图biHeight 为正数):图像数据按左下角到右上角排列,水平从左到右,垂直从下到上。大多数 BMP 图像为倒向位图。
RGB 与 Bit-Fields 编码

对于 16bpp 及以上的位图,不使用调色板,有两种编码格式:

  • RGB 编码:R、G、B 三种颜色信息容量均分(如 24bpp 各占 8 位)。
  • Bit-Fields(BF)编码 :利用位域掩码确定 RGB 三分量的信息容量。当压缩方式为 BF(biCompression=3)时,位图信息头大小为 56 字节,多出的 16 字节为 R、G、B、A 四个分量的位域掩码。

A 分量Alpha(透明度) 通道,取值范围 0~255,0 表示完全透明,255 表示完全不透明。在 BMP 中通常无实际意义,但 ARGB8888 格式会占 8 位。

位域掩码的作用:指出 R、G、B(或 A)各分量在一个像素值中分别占多少位、位于什么位置。通过掩码即可从像素值中提取或组合出各颜色分量,从而动态判断像素格式。

对于 RGB565 格式,R、G、B 的位域掩码分别为 0xF8000x07E00x001F

  • R 通道:高 5 位(bit 15~11)
  • G 通道:中间 6 位(bit 10~5)
  • B 通道:低 5 位(bit 4~0)

在 LCD 上显示 BMP 图像

显示 BMP 图像的核心思路:解析 BMP 文件头和位图信息头,读取位图数据,然后写入 LCD 显存。

示例代码

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 <string.h>
#include <linux/fb.h>
#include <sys/mman.h>

/**** BMP 文件头数据结构 ****/
typedef struct {
    unsigned char type[2];
    unsigned int size;
    unsigned short reserved1;
    unsigned short reserved2;
    unsigned int offset;
} __attribute__ ((packed)) bmp_file_header;

/**** 位图信息头数据结构 ****/
typedef struct {
    unsigned int size;
    int width;
    int height;
    unsigned short planes;
    unsigned short bpp;           /* bits per pixel,每个像素占多少位 */
    unsigned int compression;
    unsigned int image_size;
    int x_pels_per_meter;
    int y_pels_per_meter;
    unsigned int clr_used;
    unsigned int clr_omportant;
} __attribute__ ((packed)) bmp_info_header;

static int width;
static int height;
static unsigned short *screen_base = NULL;
static unsigned long line_length;

static int show_bmp_image(const char *path)
{
    bmp_file_header file_h;
    bmp_info_header info_h;
    unsigned short *line_buf = NULL;
    unsigned long line_bytes;
    unsigned int min_h, min_bytes;
    int fd = -1;
    int j;

    /* 打开文件 */
    if (0 > (fd = open(path, O_RDONLY))) {
        perror("open error");
        return -1;
    }

    /* 读取 BMP 文件头 */
    if (sizeof(bmp_file_header) !=
        read(fd, &file_h, sizeof(bmp_file_header))) {
        perror("read error");
        close(fd);
        return -1;
    }

    if (0 != memcmp(file_h.type, "BM", 2)) {
        fprintf(stderr, "it's not a BMP file\n");
        close(fd);
        return -1;
    }

    /* 读取位图信息头 */
    if (sizeof(bmp_info_header) !=
        read(fd, &info_h, sizeof(bmp_info_header))) {
        perror("read error");
        close(fd);
        return -1;
    }

    printf("文件大小: %d\n"
           "位图数据的偏移量: %d\n"
           "位图信息头大小: %d\n"
           "图像分辨率: %d*%d\n"
           "像素深度: %d\n", file_h.size, file_h.offset,
           info_h.size, info_h.width, info_h.height,
           info_h.bpp);

    /* 将文件读写位置移动到图像数据开始处 */
    if (-1 == lseek(fd, file_h.offset, SEEK_SET)) {
        perror("lseek error");
        close(fd);
        return -1;
    }

    /* 申请一个 buf、暂存 bmp 图像的一行数据 */
    line_bytes = info_h.width * info_h.bpp / 8;  // bpp ÷ 8 将位(bit)转为字节(Byte)
    line_buf = malloc(line_bytes);
    if (NULL == line_buf) {
        fprintf(stderr, "malloc error\n");
        close(fd);
        return -1;
    }

    if (line_length > line_bytes)           // line_length: LCD一行字节数
        min_bytes = line_bytes;             // line_bytes: BMP一行字节数
    else                                    // 取较小值防止memcpy越界
        min_bytes = line_length;

    /* 读取图像数据显示到 LCD */
    if (0 < info_h.height) {        // 倒向位图
        if (info_h.height > height) {
            min_h = height;
            lseek(fd, (info_h.height - height) * line_bytes, SEEK_CUR);
            screen_base += width * (height - 1);
        } else {
            min_h = info_h.height;
            screen_base += width * (info_h.height - 1);
        }

        for (j = min_h; j > 0; screen_base -= width, j--) {
            read(fd, line_buf, line_bytes);
            memcpy(screen_base, line_buf, min_bytes);
        }
    } else {                        // 正向位图
        int temp = 0 - info_h.height;
        if (temp > height)
            min_h = height;
        else
            min_h = temp;

        for (j = 0; j < min_h; j++, screen_base += width) {
            read(fd, line_buf, line_bytes);
            memcpy(screen_base, line_buf, min_bytes);
        }
    }

    close(fd);
    free(line_buf);
    return 0;
}

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

    if (2 != argc) {
        fprintf(stderr, "usage: %s <bmp_file>\n", argv[0]);
        exit(-1);
    }

    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);

    screen_size = fb_fix.line_length * fb_var.yres;
    line_length = fb_fix.line_length;
    width = fb_var.xres;
    height = fb_var.yres;

    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);
    }

    memset(screen_base, 0xFF, screen_size);
    show_bmp_image(argv[1]);

    munmap(screen_base, screen_size);
    close(fd);
    exit(EXIT_SUCCESS);
}

显示 BMP 的主要流程:

  1. 打开 BMP 文件,读取文件头和位图信息头。
  2. 校验文件类型是否为 "BM"
  3. 通过 lseek() 将文件读写位置移至图像数据起始处(file_h.offset)。
  4. 根据 info_h.height 判断正向/倒向位图,逐行读取图像数据并 memcpy 到显存映射区。
  5. 关闭文件,释放资源。

注意 :示例代码默认传入的 BMP 图像为 RGB565 格式。如需兼容 RGB888 等其他格式,需根据 bmp_info_header 中的 compression 和位域掩码进行判断处理。

在开发板上测试

  1. 使用交叉编译工具编译示例代码。
  2. 将编译得到的可执行文件及 BMP 图像文件拷贝到开发板 Linux 系统的用户家目录下。
  3. 退出出厂系统的 Qt GUI 应用程序。(查看QT进程,然后关闭QT进程)
  4. 执行测试程序,LCD 屏上将显示指定的 BMP 图像。
相关推荐
今天也想躺ping12 小时前
linux系统移植pjsua库实现sip通话功能
linux·sip·交叉编译
hkj880813 小时前
Linux 总线-设备-驱动(Bus-Device-Driver)完整协作原理
linux·运维·microsoft
早点睡啊Y13 小时前
深入学 LangChain 官方文档(一):总览、安装与快速开始
linux·服务器·langchain
一直在努力学习的菜鸟13 小时前
Rocky8.5编译安装PHP8.4
linux·运维
国产化创客14 小时前
Kindle完整越狱改造:从闲置泡面盖到Linux开发与智能家居终端
linux·运维·物联网·嵌入式·智能家居·智能硬件
qetfw14 小时前
CentOS 7 搭建 MariaDB 数据库服务
linux·数据库·centos·mariadb
皮卡狮15 小时前
进程的地址空间
linux
Darkwanderor15 小时前
动、静态库相关内容的详细介绍
linux·c语言·开发语言·c++
2401_8582861117 小时前
OS79.【Linux】POSIX信号量
linux·运维·服务器