基于 framebuffer 显示 24 位 BMP 图片
一、项目背景
在前面的练习中,我已经基于 Linux framebuffer 封装了 LCD 显示模块,实现了 LCD 初始化、清屏、画点、画矩形等基础功能。
本次练习在已有 LCD 模块的基础上,继续实现 BMP 图片显示功能。程序通过文件 I/O 读取 BMP 图片文件,解析 BMP 文件头,获取图片宽度、高度、位深和像素数据偏移量,然后读取像素数据,并将 BMP 的 BGR 像素格式转换成 LCD 使用的 RGB 颜色格式,最终通过 lcd_draw_point() 将图片显示到开发板 LCD 屏幕上。
本项目主要用于巩固以下知识点:
- Linux 文件 I/O
- BMP 文件格式
- 小端数据解析
- framebuffer 显示
- BGR 转 RGB
- 动态内存申请与释放
- 模块化编程
二、开发环境
| 项目 | 内容 |
|---|---|
| 开发板 | 粤嵌 GEC6818 开发板 |
| 系统 | 嵌入式 Linux |
| 显示设备 | /dev/fb0 |
| 编程语言 | C |
| 编译方式 | Makefile |
| 图片格式 | 24 位 BMP |
| 代码托管 | Gitee |
项目仓库地址:
https://gitee.com/res-wangwei/embedded-linux-lcd-framebuffer
三、项目目录结构
本项目在之前 LCD 模块的基础上继续扩展,目录结构如下:
lcd_module_project/
├── include/
│ └── lcd.h
├── src/
│ ├── lcd.c
│ └── main.c
├── Makefile
├── README.md
└── .gitignore
其中:
| 文件 | 作用 |
|---|---|
lcd.h |
声明 LCD 显示模块接口 |
lcd.c |
实现 LCD 初始化、画点、画矩形、BMP 显示等功能 |
main.c |
程序入口,调用 LCD 模块接口 |
Makefile |
管理项目编译 |
.gitignore |
忽略编译生成文件和测试图片 |
四、BMP 文件格式简介
BMP 是一种位图文件格式,图片中的像素数据通常按照一定格式直接存储在文件中。相比 JPG、PNG 等压缩图片格式,BMP 文件结构更简单,适合嵌入式 Linux 初学阶段练习图片显示。
一个普通 BMP 文件大致由以下部分组成:
BMP 文件头
BMP 信息头
可能存在的调色板或额外信息
像素数据
在本次项目中,主要处理 24 位未压缩 BMP 图片。24 位 BMP 中,一个像素占 3 个字节,存储顺序通常是:
B G R
而 LCD 显示时常用的颜色格式是:
0x00RRGGBB
因此显示 BMP 图片时,需要将 BMP 文件中的 BGR 顺序转换为 LCD 使用的 RGB 颜色值。
五、BMP 文件头解析
BMP 文件头中保存了图片的关键信息,例如宽度、高度、位深和像素数据偏移量。
常用字段位置如下:
| 偏移位置 | 字节数 | 含义 |
|---|---|---|
| 10 | 4 字节 | 像素数据偏移量 |
| 18 | 4 字节 | 图片宽度 |
| 22 | 4 字节 | 图片高度 |
| 28 | 2 字节 | 图片位深 |
由于 BMP 文件中多字节数据采用小端格式存储,因此不能简单地只读取一个字节。为了提高代码可读性和可移植性,我封装了两个小端读取函数。
/**
* @brief 从缓冲区中读取 16 位小端数据
*
* @param buf 指向数据起始位置的指针
*
* @return 解析得到的 16 位无符号整数
*/
static unsigned short read_le16(const unsigned char *buf)
{
return (unsigned short)(buf[0] | (buf[1] << 8));
}
/**
* @brief 从缓冲区中读取 32 位小端数据
*
* @param buf 指向数据起始位置的指针
*
* @return 解析得到的 32 位无符号整数
*/
static unsigned int read_le32(const unsigned char *buf)
{
return (unsigned int)(buf[0] |
(buf[1] << 8) |
(buf[2] << 16) |
(buf[3] << 24));
}
这样解析 BMP 头部时,就可以写成:
unsigned int bmp_width = read_le32(&bmp_header[18]);
unsigned int bmp_height = read_le32(&bmp_header[22]);
unsigned short bmp_bit_count = read_le16(&bmp_header[28]);
unsigned int data_offset = read_le32(&bmp_header[10]);
相比直接使用强制类型转换:
int bmp_width = *(int *)&bmp_header[18];
使用 read_le16() 和 read_le32() 更清晰,也可以避免内存对齐和字节序相关的问题,更符合工程代码习惯。
六、读取 BMP 文件头
BMP 显示函数首先以只读方式打开图片文件,然后读取 BMP 文件头信息。
int lcd_show_bmp(const char *bmp_path, int x0, int y0)
{
int bmp_fd;
unsigned char bmp_header[54];
bmp_fd = open(bmp_path, O_RDONLY);
if (bmp_fd < 0)
{
perror("open bmp failed");
return -1;
}
printf("open bmp success:%s\n", bmp_path);
if (read(bmp_fd, bmp_header, 54) != 54)
{
perror("read bmp header failed");
close(bmp_fd);
return -1;
}
unsigned int bmp_width = read_le32(&bmp_header[18]);
unsigned int bmp_height = read_le32(&bmp_header[22]);
unsigned short bmp_bit_count = read_le16(&bmp_header[28]);
unsigned int data_offset = read_le32(&bmp_header[10]);
printf("bmp_width = %u, bmp_height = %u, bmp_bit_count = %u\n",
bmp_width, bmp_height, bmp_bit_count);
}
这里需要注意,当前版本只支持 24 位 BMP 图片,因此需要判断位深:
if (bmp_bit_count != 24)
{
printf("only support 24-bit bmp\n");
close(bmp_fd);
return -1;
}
七、计算 BMP 每行字节数
24 位 BMP 中,一个像素占 3 个字节,因此一行原始字节数为:
bmp_width * 3
但 BMP 文件格式要求每一行像素数据按 4 字节对齐。如果一行字节数不是 4 的倍数,需要在行尾补齐。
因此实际每行字节数计算方式为:
int line_bytes = (bmp_width * 3 + 3) / 4 * 4;
这个公式的作用是将 bmp_width * 3 向上补齐到 4 的倍数。
例如:
宽度为 17 时:
17 × 3 = 51 字节
51 不是 4 的倍数,需要补齐到 52 字节
整张图片的像素数据大小为:
unsigned int bmp_data_size = line_bytes * bmp_height;
八、读取 BMP 像素数据
得到像素数据偏移量和数据大小后,就可以申请内存,并读取 BMP 像素数据。
unsigned char *bmp_data = calloc(1, bmp_data_size);
if (bmp_data == NULL)
{
printf("calloc bmp_data failed\n");
close(bmp_fd);
return -1;
}
if (lseek(bmp_fd, data_offset, SEEK_SET) < 0)
{
perror("lseek bmp data failed");
free(bmp_data);
close(bmp_fd);
return -1;
}
ssize_t read_size = read(bmp_fd, bmp_data, bmp_data_size);
if (read_size != (ssize_t)bmp_data_size)
{
printf("read bmp data failed: read_size = %ld, expected = %u\n",
read_size, bmp_data_size);
free(bmp_data);
close(bmp_fd);
return -1;
}
printf("read bmp data success\n");
这里曾经出现过一个错误:一开始只申请了 256 字节内存:
unsigned char *bmp_data = calloc(1, 256);
但是实际图片数据大小是 100 多万字节,导致读取数据时发生内存越界,最终程序在 free() 时崩溃。正确做法是根据 bmp_data_size 申请内存:
unsigned char *bmp_data = calloc(1, bmp_data_size);
九、BMP 像素显示到 LCD
读取到 BMP 像素数据后,需要遍历每一个像素点,并将 BGR 转换为 RGB。
BMP 图片通常是从下往上存储像素数据的,因此显示时需要处理上下倒置问题:
int bmp_y = bmp_height - 1 - y;
显示核心代码如下:
for (int y = 0; y < (int)bmp_height; y++)
{
for (int x = 0; x < (int)bmp_width; x++)
{
int bmp_y = bmp_height - 1 - y;
int index = bmp_y * line_bytes + x * 3;
unsigned char b = bmp_data[index + 0];
unsigned char g = bmp_data[index + 1];
unsigned char r = bmp_data[index + 2];
unsigned int color = (r << 16) | (g << 8) | b;
lcd_draw_point(x0 + x, y0 + y, color);
}
}
其中:
unsigned int color = (r << 16) | (g << 8) | b;
表示将 BMP 中的 BGR 格式转换为 LCD 使用的 RGB 颜色格式。
最后释放内存并关闭文件:
free(bmp_data);
bmp_data = NULL;
close(bmp_fd);
return 0;
十、主函数调用
在 main.c 中,先初始化 LCD,然后清屏,再显示 BMP 图片。
#include "lcd.h"
#include <unistd.h>
int main(int argc, char const *argv[])
{
if (lcd_init("/dev/fb0") < 0)
{
return -1;
}
lcd_clear(0x00000000);
if (lcd_show_bmp("test.bmp", 0, 0) < 0)
{
lcd_uninit();
return -1;
}
sleep(5);
lcd_clear(0x00000000);
lcd_uninit();
return 0;
}
显示图片前调用 lcd_clear() 是为了避免图片没有覆盖到的区域残留之前的背景内容。
十一、运行结果
开发板运行程序后,终端输出如下:
LCD init success:800 x 480 ,32 bpp
open bmp success:test.bmp
bmp_width = 528, bmp_height = 651,bmp_bit_count = 24
bmp line_bytes = 1584
bmp data_offset = 138
bmp bmp_data_size = 1031184
read bmp data success
LCD 屏幕可以正确显示 BMP 图片。

图 1 开发板 LCD 正确显示 BMP 图片
十二、遇到的问题与解决方法
1. BMP 图片打不开
问题现象:
open bmp failed: No such file or directory
原因是程序运行在开发板上,但代码中使用了 Ubuntu 虚拟机中的图片路径。
错误写法:
lcd_show_bmp("/home/www/lcd_module_project/test.bmp", 0, 0);
解决方法是使用开发板当前运行目录下的相对路径:
lcd_show_bmp("test.bmp", 0, 0);
同时需要将 lcd_demo 和 test.bmp 一起拷贝到开发板的 /workspace 目录。
2. 申请内存过小导致程序崩溃
问题现象:
free(): invalid next size
Aborted
原因是只申请了 256 字节内存,却读取了整张 BMP 图片的像素数据,导致内存越界。
错误写法:
unsigned char *bmp_data = calloc(1, 256);
正确写法:
unsigned char *bmp_data = calloc(1, bmp_data_size);
3. 图片显示后背景残留
如果 BMP 图片没有覆盖整个 LCD 屏幕,未覆盖区域会保留之前的显示内容。
解决方法是在显示图片前先清屏:
lcd_clear(0x00000000);
lcd_show_bmp("test.bmp", 0, 0);
4. BMP 行字节对齐问题
24 位 BMP 每个像素占 3 字节,但 BMP 每行数据必须按 4 字节对齐,因此每行字节数不能简单使用 bmp_width * 3,而应该使用:
int line_bytes = (bmp_width * 3 + 3) / 4 * 4;
十三、今日复盘
本次练习完成了基于 framebuffer 的 BMP 图片显示功能。相比之前的清屏、画点、画矩形,本次练习增加了文件格式解析和像素数据处理,难度明显提高。
本次主要掌握了以下内容:
1. BMP 文件头的读取与解析
2. 小端格式数据的读取方法
3. 24 位 BMP 的 BGR 像素顺序
4. BMP 每行 4 字节对齐规则
5. 使用 calloc 根据实际数据大小申请内存
6. 通过 lcd_draw_point() 将图片像素显示到 LCD 屏幕
十四、总结
通过本次练习,我在已有 LCD framebuffer 模块的基础上,实现了 24 位 BMP 图片显示功能。该功能将 Linux 文件 I/O、BMP 文件格式解析、动态内存管理和 framebuffer 显示结合起来,是嵌入式 Linux 图形显示开发中的一个重要基础。
后续可以在该项目基础上继续扩展触摸屏控制,实现点击切换图片、电子相册等功能。

浙公网安备 33010602011771号