跳转到正文
知识笔记

BMP 图片解析与 LCD 显示 ​

概述 ​

本文说明 BMP 文件中的像素数据如何转换为 LCD 可显示的格式。

BMP 文件结构与像素布局 ​

1. BMP 文件结构 ​

text
文件头 → 位图信息头 → 可选调色板 → 像素数据
  • 文件头:包含文件类型、文件大小和像素数据起始位置 bfOffBits。
  • 位图信息头:记录宽度、高度、位深和压缩方式等。
  • 调色板:索引色 BMP 可能使用,保存颜色索引与实际颜色的对应关系。
  • 像素数据:实际的图像内容。

常见 BMP 文件头为 14 字节,信息头常见为 40 字节,但不同 BMP 版本可能不同。不要固定假设像素数据总是从第 54 字节开始,应读取 bfOffBits。BMP 多字节数值通常采用小端序。

2. 行对齐与图像方向 ​

每行 4 字节对齐

24 位未压缩 BMP 的一个像素通常占 3 字节,且像素数据每行会补齐到 4 字节的倍数:

c
row_bytes = (width * 3 + 3) & ~3;

例如一行有 3 个像素,像素本身占 9 字节,文件中该行会占 12 字节。BMP 的 row_bytes 与 LCD 的 fix.line_length 是两回事,不能混用。

图片上下方向

  • height > 0:通常自下向上存储,先读到的是图片最下面一行。
  • height < 0:自上向下存储,先读到的是图片最上面一行。

处理时要根据高度正负,将文件行映射到正确的屏幕行。

3. 颜色格式转换 ​

常见 24 位 BMP 像素顺序是 B、G、R,而 LCD 可能使用 RGB565。若目标 Framebuffer 已确认是 RGB565,可以将 8 位颜色通道压缩为 5/6/5 位:

c
uint8_t b = pixel[0];
uint8_t g = pixel[1];
uint8_t r = pixel[2];

uint16_t rgb565 = ((uint16_t)(r >> 3) << 11)
                | ((uint16_t)(g >> 2) << 5)
                | (uint16_t)(b >> 3);

16 位 BMP 不一定是 RGB565,24 位 BMP 也不一定都是未压缩格式;读取前要检查位深和压缩方式。

概括起来,BMP 是磁盘上的图片文件,Framebuffer 是显示系统使用的像素缓冲区。两者的格式可能不同,因此显示图片通常需要先解析文件头,再按行读取像素、处理方向与对齐,必要时转换颜色,最后写入显存。

Framebuffer 显示流程与代码示例 ​

text
打开 BMP 和 /dev/fb0
        ↓
查询 Framebuffer 参数并映射显存
        ↓
解析 BMP 头,读取宽、高、位深、压缩方式和 bfOffBits
        ↓
逐行读取像素,处理行对齐和上下方向
        ↓
按屏幕格式转换颜色
        ↓
根据 line_length 计算显存地址并写入
        ↓
释放映射并关闭文件

写入屏幕时,若目标为 RGB565:

c
size_t offset = (size_t)(screen_y + var.yoffset) * fix.line_length
              + (size_t)(screen_x + var.xoffset) * 2;
/* 检查 offset 未越过 fix.smem_len 后,再写入 rgb565。 */

示例代码 ​

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 <stdint.h>
#include <limits.h>
#include <sys/mman.h>

/* ================= 数据结构定义 ================= */

/**
 * @brief BMP 文件头结构体 (14 Bytes)
 * @note  使用 packed 属性取消编译器内存对齐,确保结构体布局与 BMP 文件二进制格式严格一致。
 *        该结构体依赖小端序主机环境,直接通过 read() 读取即可正确解析。
 */
typedef struct {
    unsigned char  type[2];       /* 文件标识符: 'B' + 'M' = 0x4D42 */
    unsigned int   size;          /* 文件总大小 (Bytes) */
    unsigned short reserved1;     /* 保留字段 */
    unsigned short reserved2;     /* 保留字段 */
    unsigned int   offset;        /* 文件头至像素数据区的偏移量 (Bytes) */
} __attribute__((packed)) bmp_file_header;

/**
 * @brief BMP 位图信息头结构体 (40 Bytes, BITMAPINFOHEADER)
 * @note  height 字段符号约定: 正值表示倒向存储(Bottom-Up),负值表示正向存储(Top-Down)。
 */
typedef struct {
    unsigned int   size;              /* 本结构体大小,标准值为 40 */
    int            width;             /* 图像宽度 (Pixels) */
    int            height;            /* 图像高度 (Pixels),符号决定存储方向 */
    unsigned short planes;            /* 位面数,固定为 1 */
    unsigned short bpp;               /* 位深度 (Bits Per Pixel): 16/24/32 */
    unsigned int   compression;       /* 压缩类型: 0 = BI_RGB (无压缩) */
    unsigned int   image_size;        /* 图像数据大小 (Bytes),未压缩时可置 0 */
    int            x_pels_per_meter;  /* 水平打印分辨率 */
    int            y_pels_per_meter;  /* 垂直打印分辨率 */
    unsigned int   clr_used;          /* 实际使用的调色板颜色数 */
    unsigned int   clr_important;     /* 重要颜色数,0 表示全部重要 */
} __attribute__((packed)) bmp_info_header;

/* ================= 辅助函数 ================= */

/**
 * @brief RGB888 至 RGB565 格式转换
 * @param r 红色分量 [0, 255]
 * @param g 绿色分量 [0, 255]
 * @param b 蓝色分量 [0, 255]
 * @return  16-bit RGB565 像素值 (R[15:11] G[10:5] B[4:0])
 */
static inline unsigned short rgb888_to_rgb565(unsigned char r,
                                              unsigned char g,
                                              unsigned char b)
{
    return ((r >> 3) << 11) | ((g >> 2) << 5) | (b >> 3);
}

/* ================= 核心功能函数 ================= */

/**
 * @brief  解析 BMP 文件并渲染至 Framebuffer
 * @param  path         BMP 文件路径
 * @param  fb_base      Framebuffer 映射基地址 (由调用方维护,函数内不修改该指针)
 * @param  screen_w     屏幕水平分辨率 (Pixels)
 * @param  screen_h     屏幕垂直分辨率 (Pixels)
 * @param  line_length  屏幕单行字节跨度 (Bytes, 即 fb_fix.line_length)
 * @return 0 成功, -1 失败
 *
 * @note   支持 BI_RGB 的 RGB555/BGR24/BGRX32,以及掩码明确的 16 位 BI_BITFIELDS;
 *         图片未覆盖区域保留调用方预先填充的背景色。
 */
static int show_bmp_image(const char *path, unsigned short *fb_base,
                          int screen_w, int screen_h, unsigned long line_length)
{
    bmp_file_header file_h;
    bmp_info_header info_h;
    uint32_t masks[3] = {0x7c00, 0x03e0, 0x001f};
    unsigned char *row = NULL;
    int fd = -1, result = -1;
    uint64_t row_bytes, required, rows;
    off_t file_size;
    int rgb565 = 0;

    if (!fb_base || screen_w <= 0 || screen_h <= 0 ||
        line_length < (uint64_t)screen_w * 2 || line_length % 2 != 0) {
        fprintf(stderr, "invalid framebuffer geometry\n");
        return -1;
    }
    fd = open(path, O_RDONLY);
    if (fd < 0) { perror("open bmp"); return -1; }
    if (read(fd, &file_h, sizeof(file_h)) != sizeof(file_h) ||
        read(fd, &info_h, sizeof(info_h)) != sizeof(info_h)) {
        fprintf(stderr, "truncated BMP header\n");
        goto out;
    }
    if (memcmp(file_h.type, "BM", 2) ||
        !(info_h.size == 40 || info_h.size == 52 || info_h.size == 56 ||
          info_h.size == 108 || info_h.size == 124) ||
        info_h.planes != 1 || info_h.width <= 0 ||
        info_h.height == 0 || info_h.height == INT_MIN ||
        !(info_h.bpp == 16 || info_h.bpp == 24 || info_h.bpp == 32)) {
        fprintf(stderr, "unsupported BMP header or bit depth\n");
        goto out;
    }
    if (info_h.compression != 0 &&
        !(info_h.compression == 3 && info_h.bpp == 16)) {
        fprintf(stderr, "unsupported BMP compression\n");
        goto out;
    }
    if (file_h.offset < (uint64_t)14 + info_h.size) {
        fprintf(stderr, "invalid pixel offset\n");
        goto out;
    }
    if (info_h.compression == 3) {
        /* BITMAPINFOHEADER 后附掩码;扩展 DIB 的掩码位于同一偏移。 */
        if (file_h.offset < 66 || lseek(fd, 54, SEEK_SET) < 0 ||
            read(fd, masks, sizeof(masks)) != sizeof(masks)) {
            fprintf(stderr, "missing color masks\n");
            goto out;
        }
        rgb565 = masks[0] == 0xf800 && masks[1] == 0x07e0 && masks[2] == 0x001f;
        if (!rgb565 && !(masks[0] == 0x7c00 && masks[1] == 0x03e0 && masks[2] == 0x001f)) {
            fprintf(stderr, "unsupported 16-bit color masks\n");
            goto out;
        }
    }
    /* BI_RGB 的 16 位数据按 RGB555 解析,不能默认当作 RGB565。 */
    row_bytes = (((uint64_t)info_h.width * info_h.bpp + 31) / 32) * 4;
    rows = info_h.height < 0 ? -(int64_t)info_h.height : info_h.height;
    if (rows > (UINT64_MAX - file_h.offset) / row_bytes) {
        fprintf(stderr, "BMP size overflow\n");
        goto out;
    }
    required = file_h.offset + row_bytes * rows;
    file_size = lseek(fd, 0, SEEK_END);
    if (file_size < 0 || required > (uint64_t)file_size || row_bytes > SIZE_MAX) {
        fprintf(stderr, "truncated BMP pixel data\n");
        goto out;
    }
    row = malloc((size_t)row_bytes);
    if (!row) { perror("malloc"); goto out; }
    int draw_w = info_h.width < screen_w ? info_h.width : screen_w;
    int draw_h = rows < (uint64_t)screen_h ? (int)rows : screen_h;
    for (int y = 0; y < draw_h; ++y) {
        uint64_t source_y = info_h.height < 0 ? (uint64_t)y : rows - 1 - y;
        uint64_t offset = file_h.offset + source_y * row_bytes;
        if (lseek(fd, (off_t)offset, SEEK_SET) < 0 ||
            read(fd, row, (size_t)row_bytes) != (ssize_t)row_bytes) {
            fprintf(stderr, "failed to read complete BMP row\n");
            goto out;
        }
        unsigned short *target = (unsigned short *)((unsigned char *)fb_base + (size_t)y * line_length);
        for (int x = 0; x < draw_w; ++x) {
            size_t index = (size_t)x * (info_h.bpp / 8);
            if (info_h.bpp == 16) {
                uint16_t pixel;
                memcpy(&pixel, row + index, sizeof(pixel));
                if (rgb565) target[x] = pixel;
                else {
                    unsigned int g = (pixel >> 5) & 31;
                    target[x] = ((pixel & 0x7c00) << 1) |
                                (((g << 1) | (g >> 4)) << 5) | (pixel & 31);
                }
            } else {
                target[x] = rgb888_to_rgb565(row[index + 2], row[index + 1], row[index]);
            }
        }
    }
    result = 0;
out:
    free(row);
    close(fd);
    return result;
}

/* ================= 主函数 ================= */

int main(int argc, char *argv[])
{
    struct fb_fix_screeninfo fb_fix;  /* 固定屏幕参数 (line_length, 物理地址等) */
    struct fb_var_screeninfo fb_var;  /* 可变屏幕参数 (xres, yres, bits_per_pixel 等) */
    int fd_fb;
    unsigned short *screen_base = NULL;
    unsigned long screen_size;

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

    /* 打开 Framebuffer 设备节点 */
    if ((fd_fb = open("/dev/fb0", O_RDWR)) < 0) {
        perror("open /dev/fb0");
        return -1;
    }

    /* 获取屏幕参数 */
    if (ioctl(fd_fb, FBIOGET_VSCREENINFO, &fb_var) < 0 ||
        ioctl(fd_fb, FBIOGET_FSCREENINFO, &fb_fix) < 0) {
        perror("ioctl");
        close(fd_fb);
        return -1;
    }

    /* 本示例仅支持 16-bit RGB565 格式 */
    if (fb_var.bits_per_pixel != 16 ||
        fb_var.red.offset != 11 || fb_var.red.length != 5 ||
        fb_var.green.offset != 5 || fb_var.green.length != 6 ||
        fb_var.blue.offset != 0 || fb_var.blue.length != 5 ||
        fb_var.red.msb_right || fb_var.green.msb_right || fb_var.blue.msb_right ||
        fb_fix.type != FB_TYPE_PACKED_PIXELS || fb_fix.visual != FB_VISUAL_TRUECOLOR ||
        fb_var.xoffset || fb_var.yoffset || !fb_var.xres || !fb_var.yres ||
        !fb_fix.line_length || !fb_fix.smem_len || fb_fix.line_length % 2 ||
        fb_fix.line_length < (uint64_t)fb_var.xres * 2 ||
        fb_var.yres > fb_fix.smem_len / fb_fix.line_length) {
        fprintf(stderr, "unsupported bpp=%d, requires 16 (RGB565)\n",
                fb_var.bits_per_pixel);
        close(fd_fb);
        return -1;
    }

    /* 计算 Framebuffer 映射区域大小 */
    screen_size = fb_fix.smem_len;

    /*
     * 将内核态显存映射至用户态地址空间。
     * PROT_WRITE: 可写; MAP_SHARED: 共享设备映射,显示刷新行为由驱动决定。
     */
    screen_base = mmap(NULL, screen_size, PROT_WRITE, MAP_SHARED, fd_fb, 0);
    if (screen_base == MAP_FAILED) {
        perror("mmap");
        close(fd_fb);
        return -1;
    }

    /* 清屏: 填充白色背景 (RGB565: 0xFFFF) */
    memset(screen_base, 0xFF, screen_size);

    /* 渲染 BMP 图像 */
    int render_result = show_bmp_image(argv[1], screen_base,
                   fb_var.xres, fb_var.yres, fb_fix.line_length);

    /* 资源释放 */
    munmap(screen_base, screen_size);
    close(fd_fb);

    return render_result == 0 ? EXIT_SUCCESS : EXIT_FAILURE;
}

验证范围: BMP 解析函数使用生成的小图片进行主机端编译和数据验证;实际 Framebuffer 显示仍需目标板测试。输入支持以下有限格式,不是通用 BMP 解码器:

输入处理方式
16 bpp BI_RGB按 RGB555 解读并转换为 RGB565
16 bpp BI_BITFIELDS仅接受 RGB555 或 RGB565 的明确掩码
24 bpp BI_RGBBGR888 转 RGB565
32 bpp BI_RGBBGRX8888 转 RGB565,忽略第四字节

索引色、RLE、JPEG/PNG 压缩、任意颜色掩码和大端序主机不在此实现的支持范围。代码检查文件头、数据区偏移、行大小、截断输入与裁剪边界;Framebuffer 仅接受小端、线性 TRUECOLOR RGB565,且可见画面偏移为零。其他布局需先扩展实现,不能仅凭 16 bpp 判断支持。

待验证事项 ​

参考资料与关联文章 ​