跳转到正文
知识笔记
📖 学习中

BMP 图片的 LCD 显示 ​

想弄清的问题 ​

BMP 文件里的像素数据怎样转换成 LCD 能显示的格式?

关键概念 ​

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 是显示系统使用的像素缓冲区。两者的格式可能不同,因此显示图片通常需要先解析文件头,再按行读取像素、处理方向与对齐,必要时转换颜色,最后写入显存。

用法与最小示例 ​

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 <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   支持 16/24/32 bpp 无压缩 BMP;超出屏幕范围的区域自动裁剪;
 *         图片未覆盖区域保留调用方预先填充的背景色。
 */
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;
    int fd = -1;
    int j, k;

    /* 打开文件并读取头部 */
    if ((fd = open(path, O_RDONLY)) < 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)) {
        perror("read bmp header");
        close(fd);
        return -1;
    }

    /* 校验文件签名 */
    if (memcmp(file_h.type, "BM", 2) != 0) {
        fprintf(stderr, "invalid BMP signature\n");
        close(fd);
        return -1;
    }

    /* 暂不支持压缩格式 */
    if (info_h.compression != 0) {
        fprintf(stderr, "compressed BMP not supported (compression=%u)\n",
                info_h.compression);
        close(fd);
        return -1;
    }

    printf("BMP: %dx%d, bpp=%d, offset=%u\n",
           info_h.width, info_h.height, info_h.bpp, file_h.offset);

    /*
     * 计算 BMP 文件内单行字节数。
     * BMP 规范要求每行按 4 字节对齐,不足部分以 0x00 填充。
     * 对齐公式: row_bytes = ((width * bpp + 31) / 32) * 4
     */
    unsigned int bmp_line_bytes = ((info_h.width * info_h.bpp + 31) / 32) * 4;
    unsigned char *bmp_buf = malloc(bmp_line_bytes);
    if (!bmp_buf) {
        close(fd);
        return -1;
    }

    /* 解析图像尺寸与存储方向 */
    int img_w = info_h.width;
    int img_h = info_h.height < 0 ? -info_h.height : info_h.height;
    int is_top_down = info_h.height < 0;  /* height < 0 表示 Top-Down 存储 */

    /* 裁剪至屏幕可视区域 */
    int draw_w = img_w < screen_w ? img_w : screen_w;
    int draw_h = img_h < screen_h ? img_h : screen_h;

    /* 逐行读取并写入显存 */
    for (j = 0; j < draw_h; j++) {
        /* 根据存储方向确定当前读取的文件行索引 */
        int bmp_row_idx = is_top_down ? j : (img_h - 1 - j);

        /* 定位至目标行起始位置并读取 */
        if (lseek(fd, file_h.offset + bmp_row_idx * bmp_line_bytes, SEEK_SET) < 0 ||
            read(fd, bmp_buf, bmp_line_bytes) <= 0) {
            break;
        }

        /*
         * 计算当前行在 Framebuffer 中的起始地址。
         * fb_base 类型为 unsigned short* (2 Bytes),故行偏移需以 line_length/2 为单位。
         * 此处从屏幕左上角 (0,0) 开始绘制。
         */
        unsigned short *fb_line = fb_base + j * (line_length / 2);

        for (k = 0; k < draw_w; k++) {
            unsigned short pixel565 = 0;
            int src_idx = k * (info_h.bpp / 8);

            if (info_h.bpp == 16) {
                /* 16-bit BMP: 直接读取 (默认按 RGB565 处理,部分文件可能为 RGB555) */
                pixel565 = *(unsigned short *)(bmp_buf + src_idx);
            } else if (info_h.bpp == 24 || info_h.bpp == 32) {
                /* 24/32-bit BMP: 存储顺序为 BGR,需转换为 RGB565 */
                unsigned char b = bmp_buf[src_idx];
                unsigned char g = bmp_buf[src_idx + 1];
                unsigned char r = bmp_buf[src_idx + 2];
                pixel565 = rgb888_to_rgb565(r, g, b);
            }

            fb_line[k] = pixel565;
        }
    }

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

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

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) {
        fprintf(stderr, "unsupported bpp=%d, requires 16 (RGB565)\n",
                fb_var.bits_per_pixel);
        close(fd_fb);
        return -1;
    }

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

    /*
     * 将内核态显存映射至用户态地址空间。
     * 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 图像 */
    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 0;
}

当前示例的适用范围是 16 位(RGB565)、未压缩 BMP → RGB565 Framebuffer:像素数据被逐行直接拷进显存,不做颜色转换;若输入是 24 位 BMP,需要先按上文把 BGR888 压缩成 RGB565。PNG、JPEG、索引色 BMP、压缩 BMP 或其他屏幕像素格式需要额外处理。图片大于屏幕时也要进行边界裁剪。

待弄清的问题 ​

参考与关联 ​