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 或其他屏幕像素格式需要额外处理。图片大于屏幕时也要进行边界裁剪。
待弄清的问题
参考与关联
- 前置知识:Framebuffer 基本原理与显示编程 中的
mmap()、line_length和像素格式。 - 关联主题:RGB565、BMP 文件格式、C 文件 I/O。
