BMP 图片解析与 LCD 显示
概述
本文说明 BMP 文件中的像素数据如何转换为 LCD 可显示的格式。
BMP 文件结构与像素布局
1. BMP 文件结构
文件头 → 位图信息头 → 可选调色板 → 像素数据- 文件头:包含文件类型、文件大小和像素数据起始位置
bfOffBits。 - 位图信息头:记录宽度、高度、位深和压缩方式等。
- 调色板:索引色 BMP 可能使用,保存颜色索引与实际颜色的对应关系。
- 像素数据:实际的图像内容。
常见 BMP 文件头为 14 字节,信息头常见为 40 字节,但不同 BMP 版本可能不同。不要固定假设像素数据总是从第 54 字节开始,应读取 bfOffBits。BMP 多字节数值通常采用小端序。
2. 行对齐与图像方向
每行 4 字节对齐
24 位未压缩 BMP 的一个像素通常占 3 字节,且像素数据每行会补齐到 4 字节的倍数:
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 位:
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 显示流程与代码示例
打开 BMP 和 /dev/fb0
↓
查询 Framebuffer 参数并映射显存
↓
解析 BMP 头,读取宽、高、位深、压缩方式和 bfOffBits
↓
逐行读取像素,处理行对齐和上下方向
↓
按屏幕格式转换颜色
↓
根据 line_length 计算显存地址并写入
↓
释放映射并关闭文件写入屏幕时,若目标为 RGB565:
size_t offset = (size_t)(screen_y + var.yoffset) * fix.line_length
+ (size_t)(screen_x + var.xoffset) * 2;
/* 检查 offset 未越过 fix.smem_len 后,再写入 rgb565。 */示例代码
#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_RGB | BGR888 转 RGB565 |
| 32 bpp BI_RGB | BGRX8888 转 RGB565,忽略第四字节 |
索引色、RLE、JPEG/PNG 压缩、任意颜色掩码和大端序主机不在此实现的支持范围。代码检查文件头、数据区偏移、行大小、截断输入与裁剪边界;Framebuffer 仅接受小端、线性 TRUECOLOR RGB565,且可见画面偏移为零。其他布局需先扩展实现,不能仅凭 16 bpp 判断支持。
待验证事项
参考资料与关联文章
前置知识:Framebuffer 基本原理与显示编程 中的
mmap()、line_length和像素格式。关联主题:RGB565、BMP 文件格式、C 文件 I/O。
