嵌入式 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 中的 b、g、r 每个都是 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 显示函数,并做一个小验证:修改 x0、y0 参数,让同一张 BMP 图片从 LCD 的不同位置显示,从而进一步理解 lcd_draw_point(x0 + x, y0 + y, color) 的作用。

浙公网安备 33010602011771号