嵌入式 Linux 学习记录:从能跑到能复写 BMP 显示函数

一、为什么要重新复写 lcd_show_bmp()

之前我已经实现过基于 framebuffer 的 BMP 图片显示功能,程序能够在开发板 LCD 上正常显示 24 位 BMP 图片。

但是第一次实现时,更多是按照已有代码一步一步敲出来,虽然功能能够跑通,但对函数内部的整体流程、关键变量和错误处理还不够熟练。

因此今天没有继续增加新功能,而是重新复写 lcd_show_bmp() 函数,目标是把这个函数从“能跑”进一步变成“能理解、能拆分、能自己组织代码”。

二、lcd_show_bmp() 的四个模块

今天复写时,我没有一上来直接写完整代码,而是先把 lcd_show_bmp() 拆成四个模块。

1. 打开 BMP 文件并读取文件头

这一部分主要完成:

open 打开 BMP 文件
read 读取 54 字节 BMP 文件头

核心代码如下:

unsigned char bmp_header[54];

int bmp_fd = open(bmp_path, O_RDONLY);
if (bmp_fd < 0)
{
    perror("open bmp failed");
    return -1;
}

if (read(bmp_fd, bmp_header, 54) != 54)
{
    perror("read bmp header failed");
    close(bmp_fd);
    return -1;
}

这里需要注意,bmp_header 应该使用 unsigned char 类型,因为 BMP 文件头是按字节读取的。如果写成 unsigned int bmp_header[54],数组大小和含义都会发生变化。

2. 解析 BMP 信息并读取像素数据

这一部分主要解析 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]);

其中:

bmp_width:图片宽度
bmp_height:图片高度
bmp_bit_count:图片位深
data_offset:像素数据开始的位置

当前版本只支持 24 位 BMP,所以需要判断:

if (bmp_bit_count != 24)
{
    printf("only support 24-bit bmp\n");
    close(bmp_fd);
    return -1;
}

接着计算每行实际字节数和整张图片像素数据大小:

unsigned int line_bytes = (bmp_width * 3 + 3) / 4 * 4;
unsigned int bmp_data_size = line_bytes * bmp_height;

line_bytes 不能简单写成 bmp_width * 3,因为 BMP 文件每一行像素数据需要按 4 字节对齐。

然后申请内存并读取像素数据:

unsigned char *bmp_data = calloc(1, bmp_data_size);
if (bmp_data == NULL)
{
    perror("calloc memory failed");
    close(bmp_fd);
    return -1;
}

if (lseek(bmp_fd, data_offset, SEEK_SET) < 0)
{
    perror("lseek data_offset 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;
}

这部分让我进一步理解了资源管理:

open 成功后,后续失败要 close
calloc 成功后,后续失败要 free

3. 遍历像素并显示到 LCD

读取到 BMP 像素数据后,需要双层循环遍历图片的每一个像素。

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);
    }
}

其中:

bmp_y:解决 BMP 图片上下倒置问题
index:找到当前像素在 bmp_data 中的位置
b/g/r:取出 BMP 当前像素的 B、G、R 分量
color:转换成 LCD 使用的 0x00RRGGBB 格式
lcd_draw_point():将当前像素显示到 LCD

今天重点理解了这一句:

int index = bmp_y * line_bytes + x * 3;

它的含义是:

bmp_y * line_bytes:跳过前 bmp_y 行,来到当前行开头
x * 3:来到当前行第 x 个像素

4. 释放内存并关闭 BMP 文件

函数最后释放申请的内存,并关闭 BMP 文件:

free(bmp_data);
close(bmp_fd);

return 0;

这和前面的资源申请对应:

calloc → free
open → close

三、今天复写时出现的问题

今天复写过程中主要出现了以下问题:

1. bmp_header 类型写错

一开始把 BMP 文件头写成:

unsigned int bmp_header[54];

后来改成:

unsigned char bmp_header[54];

因为 BMP 文件头需要按字节读取和解析。

2. index 计算容易写错

曾经把下标计算写成:

int index = bmp_y * bmp_height + x * 3;

这是错误的。bmp_height 是图片高度,不是一行占多少字节。

正确写法是:

int index = bmp_y * line_bytes + x * 3;

3. color 类型不能写成 unsigned char

BMP 中的 bgr 每个都是 1 字节,可以使用 unsigned char

但是组合后的 LCD 颜色值是:

0x00RRGGBB

需要使用 unsigned int 保存。

正确写法:

unsigned int color = (r << 16) | (g << 8) | b;

4. perror 和 printf 要区分使用

系统调用失败时可以使用 perror(),例如:

open()
read()
lseek()

但如果是自己判断出的逻辑问题,例如“不支持 24 位以外的 BMP”,更适合使用:

printf("only support 24-bit bmp\n");

四、今天真正理解的内容

通过今天复写,我进一步理解了:

1. lcd_show_bmp() 不是一整坨代码,而是可以拆成 4 个模块
2. 文件操作基本套路是 open → read/lseek/read → close
3. 内存操作基本套路是 calloc → 使用 → free
4. data_offset 表示从文件哪里开始读像素数据
5. bmp_data_size 表示要读取多少像素数据
6. line_bytes 表示 BMP 文件中一行实际占多少字节
7. bmp_y 是为了解决 BMP 上下倒置
8. index 是为了解决二维图片坐标到一维内存下标的转换
9. BGR 转 RGB 是因为 BMP 存储顺序和 LCD 颜色格式不同

五、总结

今天没有继续增加新功能,而是重新复写了 lcd_show_bmp() 函数。虽然写代码时还不够流畅,但已经能够把 BMP 图片显示流程拆分成几个模块,并能够定位一些常见错误。

这次练习让我认识到,嵌入式 Linux 学习不能只追求“代码能跑”,更重要的是理解每一段代码在整个流程中的作用。

下一步计划是继续巩固 BMP 显示函数,并做一个小验证:修改 x0y0 参数,让同一张 BMP 图片从 LCD 的不同位置显示,从而进一步理解 lcd_draw_point(x0 + x, y0 + y, color) 的作用。

posted @ 2026-06-03 17:24  wwwicjh  阅读(9)  评论(0)    收藏  举报