基于 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 图片。

663a413483dd0cdffe8903362fa99da8

图 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_demotest.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 图形显示开发中的一个重要基础。

后续可以在该项目基础上继续扩展触摸屏控制,实现点击切换图片、电子相册等功能。

posted @ 2026-05-30 11:18  wwwicjh  阅读(45)  评论(0)    收藏  举报