拓冰建站拓冰建站
首页 / 资讯中心 / 正文

BMP转RGB565工具详解:从图片解析到单片机LCD显示的完整实践

简介这是一款专为单片机开发场景设计的BMP图像转RGB565格式转换工具面向嵌入式初学者、课程设计与毕业设计学生及资源受限平台的开发者解决在STM32、ESP32等MCU上高效加载和显示图像时面临的格式兼容与内存优化难题。压缩包仅7KB共含3个核心文件Python主程序实现BMP头解析、像素数据读取与RGB888→RGB565位域重映射、依赖清单明确所需Pillow等基础库及详细README文档含使用示例、参数说明与常见问题。已有40人学习下载工具无需编译环境开箱即用输出为C数组格式可直接嵌入单片机GUI或LCD驱动代码源码结构清晰、注释完整兼顾功能实用性与教学可读性显著降低图像资源适配门槛。 搞单片机显示的人十有八九都折腾过图片转数组这件事尤其是做屏幕菜单、开机logo、游戏精灵图的时候。BMP转RGB565工具这个玩意儿听起来很小实际却是嵌入式GUI开发里绕不开的刚需。我最早是拿网上那些在线转换网页顶着的后来发现批量转换、裁剪、透明色处理全都受限干脆自己花一晚上写了个小工具用到现在已经成了我所有屏幕项目的标配。这篇文章就完整拆一下这个工具的来龙去脉从BMP文件格式怎么读到RGB565数据怎么生成再到单片机工程里怎么接入把整个链路讲透附上能直接用的Python脚本和C代码片段新手照着抄老手可以拿来改。写这个工具之前我先弄明白一个核心问题到底是谁在转换什么BMP是Windows时代留下的无压缩位图格式图像信息完整解码简单RGB565是16位色深编码用5位红、6位绿、5位蓝来表示一个像素正好能被16位单片机一个字存下直接怼到LCD的GRAM里就能显示。所以所谓转换本质是两件事把BMP的像素数据抠出来把每个像素的BGR888颜色值压缩成RGB565格式然后按照单片机方便取用的方式组织成C语言数组。整个过程不涉及压缩算法也不需要解码库但有几个细节坑特别多比如行对齐、图像倒置、颜色通道顺序、数组大小端全都在后面文章里详细说。适合读这篇文章的人我觉得主要是三类一是用STM32、51、ESP32这类芯片做屏幕显示需要把图片素材转成C数组的二是想搞明白BMP文件结构、图像数据在内存里到底长什么样的同学三是想封装一套自己的图片转换流水线后续可以批量处理素材的开发者。不管你是刚接触单片机还是一直在跟屏较劲这篇文章都能给你一套直接可用的方案。1. 为什么要专门做一次颜色格式转换很多刚开始做屏幕上手的同学会有个疑问LCD驱动库里明明有画点函数我直接读BMP像素一个个画不就行了理论上是这样但实际做一次你就知道有多难受。BMP一张1024x600的图像素点接近61万个每个像素如果用24位真彩表示就是3个字节。单片机读SD卡里的BMP每读一个像素做三次FATFS读写再转换成LCD需要的颜色格式写完一整张图得几十秒卡顿到怀疑人生。所以业界的常规做法就是离线转换把颜色格式的变换、字节序的处理全部在电脑上完成单片机那边只负责把数组里的RGB565数据按顺序怼到显存或者GRAM里连运算都省了。这样做有两个明显的好处一是显示速度极快纯内存搬运SPI屏刷一张全屏图也就几百毫秒并口屏可以做到几十毫秒二是Flash里存储的就是LCD驱动最熟悉的格式省了中间层转换的代码量和CPU开销。RGB565这个格式能够流行起来核心原因是16位色深在视觉和存储之间取得了不错的平衡。64K色对人眼来说已经比较细腻渐变过渡没有明显的色阶断层单片机用16位一个字就能表示一个像素内存里天然对齐DMA搬运特别方便。相比之下RGB888需要3个字节在低端MCU上处理起来别扭RG555虽然也只有16位但少了绿色分量显示绿色植物和人物肤色时会有明显的色彩偏差。所以目前主流的中小尺寸LCD屏尤其是SPI接口和并口接口的屏绝大多数都支持RGB565直接刷显。还有一个容易被忽略的原因BMP文件本身是为PC显示设计的像素排列自底向上也有少数自顶向下颜色通道是BGR顺序行数据按照4字节对齐直接拿原始数据给LCD是不认的。所以转换工具实际上是在帮单片机做掉那些“脏活”——把图像数据重排列、重新编码最后交给单片机一份即插即用的数组。2. 工具的核心设计与关键代码拆解2.1 选型思路为什么用Python而不是C#或者在线工具市面上已有的转换工具大概分这么几类其一是在线网页转换器传一张图复制一段数组适合应急但功能简陋不支持批量数据量大时浏览器还会卡死其二是Image2Lcd这类上位机软件功能确实强支持多种输出格式、扫描方式、灰度阈值但界面有些老旧以及部分功能需要配合特定设备使用并不是所有单片机玩家都顺手其三就是自己写脚本。我选Python的理由很实在处理二进制文件方便struct模块直接读写文件头不需要自己整内存指针PIL虽然不是必须但处理缩放、裁剪、旋转这些预处理操作时候特别省事脚本本身是文本复制到任何一台电脑都能跑不需要GUI界面最关键的是可以通过命令行参数批量处理整个目录下的素材这在做嵌入式UI资源管理的时候价值非常大。用C#或者C写当然也可以但为了一个转换工具去开一个Visual Studio工程还要考虑不同系统的兼容性性价比不高。Python脚本在Windows、Linux、macOS上都跑得起来配合makefile或者批处理脚本就能搭成一套资源构建流水线以后素材换了重新生成数组改一行命令的事。2.2 解析BMP文件头搞清楚像素数据在哪BMP文件的结构其实很清晰分为BMP文件头BITMAPFILEHEADER14字节、位图信息头BITMAPINFOHEADER通常40字节、调色板可选24位及以上没有、像素数据区。我用struct.unpack逐个字段读出来关键信息包括bfOffBits从文件开头到像素数据的偏移量通常24位位图是54字节。biWidth和biHeight图像宽高注意biHeight可能是负数负值表示自顶向下的扫描顺序正值表示自底向上。biBitCount颜色位数24或者32是我们最关心的8位和4位进到这里直接报错或者走调色板逻辑。biCompression压缩类型BI_RGB0才是无压缩BI_RLE8这类压缩BMP在单片机场景下并不常见工具目前不支持。读BMP的时候有个大坑每一行的像素数据不是简单地width * bytesPerPixel而是要按4字节对齐。BMP规范要求每一行数据的字节数是4的倍数不够就在行尾填0。换算公式是rowSize ((width * bytesPerPixel 3) // 4) * 4网上很多简化代码不看这个字段直接按行宽读结果就是图片显示错位、出现斜条纹。像素数据读出来以后还要处理自底向上的问题。BMP文件里存储的像素顺序是从图像最后一行开始的也就是说读出来的第一个像素是图片左下角那个点如果我们直接用这个顺序生成数组LCD上显示出来的图必然是上下颠倒的。转换的时候必须从最后一行往前读。2.3 RGB565颜色转换从BGR888到16位的压缩过程24位BMP每个像素用3个字节顺序是B、G、R单片机LCD驱动通常需要的是16位RGB565即红色占高5位绿色占中间6位蓝色占低5位。转换公式非常简单uint16_t rgb565 ((r 0xF8) 8) | ((g 0xFC) 3) | (b 3);也就是红取高5位、绿取高6位、蓝取高5位拼成一个16位整数。为什么不是直接的(r 11) | (g 5) | b因为8位颜色值的低几位本来就是细节噪声直接移位可能导致高位溢出操作时候先做掩码再移位更稳妥。但是这里还有一个容易出错的地方到底应该让红色占16位整数的高字节还是低字节这个取决于单片机的字节序和LCD控制器的数据线顺序。我的做法是在工具里生成RGB565宏定义之后额外提供一个大小端选项转换时按选项决定每个像素拼出来的16位整数是先放低字节还是先放高字节。具体来说如果MCU是STM32这类小端处理器直接把uint16_t数组发送给LCD内存里低字节在前高字节在后。而很多SPI接口的LCD控制器发送数据时是高字节先发的也就是说数组里存的虽然是uint16_t但每个像素的低8位是高位数据。这个问题不解决屏幕上会看到每个像素的红蓝色块对调画面蒙上一层诡异的紫色或者绿色。工具里提供一个--swap参数默认不交换字节我实际用的工程都开这个选项。2.4 数组生成输出C文件而不是纯文本工具最后输出的不是一串逗号分隔的数字而是合法的C语言文件里面包含尺寸宏、数组声明、颜色位掩码注释甚至还能顺带生成一个图片描述结构体。这样做的原因是把生成的数据直接扔进工程里就能编译省去手写拷贝的环节。生成的C文件大概长这样#ifndef IMAGE_LOGO_H #define IMAGE_LOGO_H #define IMG_LOGO_WIDTH 128 #define IMG_LOGO_HEIGHT 64 static const uint16_t img_logo_pixels[] { 0x0000, 0x7C0F, 0xFFFF, // ... }; #endif数组我用static const限定这样在编译阶段就会被放进Flash区不占宝贵的RAM。同时加上#ifndef头文件保护方便直接include。工具还支持把同一目录下的多个BMP批量生成到一个文件里命名用文件名去掉后缀的驼峰格式这样管理图标素材非常方便。对大图比如全屏240x320的图片数组里会出现7万多个数字生成的C文件本身有几百KB。这种情况我建议用const uint8_t数组按字节存储或者干脆输出二进制bin文件通过烧录器直接放到外部Flash里文章后面的扩展部分专门讲这个。3. 实际操作流程从一张BMP到屏幕点亮3.1 准备BMP源文件注意尺寸和色深转换之前先准备好素材。我的经验是源图尽量用无压缩的24位BMPPNG不行因为PNG做了压缩直接读文件头拿不到像素JPG更别想有损压缩解码要调用库。如果手上只有PNG或者JPG先用Pillow转换一下在脚本里加两行from PIL import Image img Image.open(input.png).convert(RGB) img.save(input.bmp)关于尺寸建议源图尺寸和LCD分辨率完全一致或者至少是显示区域的正整数比。单片机屏幕不像PC窗口有缩放算法图片数组多大就显示多大超出屏幕的部分会被丢弃小于屏幕的部分显示出来有黑边。某些屏幕驱动里支持设置显示窗口SetWindow这时候可以只刷图片区域思路稍微不同。色深方面24位BMP是标准选择32位BMP多了一个Alpha通道虽然RGB565本身没有透明概念但后续可以配合透明色处理。8位索引色BMP虽然体积小但是调色板转换逻辑复杂性能收益在Flash充足的前提下不值得我自己通常不用。3.2 运行工具生成C数组把脚本保存为bmp2rgb565.py命令行传入参数跑一下python bmp2rgb565.py logo.bmp -o logo.c --width 128 --height 64 --swap工具内部的处理流程是这样读BMP文件头校验文件格式。用Pillow如果装了或者直接解析像素得到标准RGB元组序列。如果需要裁剪就按--x和--y参数切片如果需要缩放就先调用Pillow的resize。遍历每个像素转换成RGB565输出到C文件。脚本核心部分代码大概长这样def bmp_to_rgb565(data, width, height): pixels [] row_size ((width * 3 3) // 4) * 4 pixel_bytes data[:row_size * height] for y in range(height - 1, -1, -1): row pixel_bytes[y * row_size:(y 1) * row_size] for x in range(width): b row[x * 3] g row[x * 3 1] r row[x * 3 2] val ((r 0xF8) 8) | ((g 0xFC) 3) | (b 3) pixels.append(val) return pixels注意这个从上往下读的方式就完成了图像的上下翻转修正。如果你屏幕驱动里本身已经支持上下翻转也可以去掉这个逻辑具体根据硬件来。3.3 在单片机工程里接入生成的数组数组生成好之后放到工程项目中然后写一个显示屏的函数。这里以常见的SPI屏为例刷图的核心逻辑是这样的void LCD_ShowImage(uint16_t x, uint16_t y, uint16_t width, uint16_t height, const uint16_t *data) { LCD_SetWindow(x, y, x width - 1, y height - 1); for (uint32_t i 0; i (uint32_t)width * height; i) { LCD_WriteData16(data[i]); } }LCD_SetWindow的作用是告诉控制器接下来要写的像素区域很多屏显示花屏就是因为忘记设置窗口导致数据写到了错误的位置。LCD_WriteData16在SPI接口下就是先发高字节再发低字节和我们在工具里设置了--swap是配套的。实际项目里我通常会把图片的宽高和像素指针打包成一个结构体typedef struct { uint16_t width; uint16_t height; const uint16_t *pixels; } ImageDef;然后用这个结构体管理所有图片资源做菜单切换时只要切换结构体指针就行代码写起来特别清爽。这个习惯我是在做一个小型游戏机项目时养成的对于管理几十张图片素材尤其管用。3.4 实测内存占用与显示效果以128x64的单色屏升级成RGB565彩屏为例一张全屏图片的数据量是128 * 64 * 2 16KB。对于STM32F103这类Flash有64KB的芯片放4到5张全屏图就快满了所以图片资源管理是个很现实的问题。如果是240x320的全屏图一张就是150KB这时候Flash完全装不下就必须考虑外部存储方案。显示效果方面RGB565在显示渐变色照片时能看出轻微的色阶过渡感尤其是天空、云层这类大面积渐变区域放大仔细看有一圈一圈的纹路这是16位色深的物理限制和转换工具没关系。但如果画面颜色明显偏蓝或者偏绿那就不是色深问题而是转换时通道顺序错了需要回头检查BGR顺序。4. 常见问题与排查技巧实录4.1 图片上下颠倒现象是一张风景图显示出来天在下面、地在上面。原因很简单BMP存储顺序是自底向上直接按顺序输出就是倒的。解决办法是遍历像素时从最后一行开始往上读。如果你用的是Pillow的load()函数它已经把图像转成了正常的从上到下的顺序就不需要再做这个翻转但如果直接用struct解析原始像素这个问题一定会遇到。4.2 颜色通道顺序错误画面偏蓝或者偏紫颜色偏蓝是最容易发现的只要转换时把RGB当成BGR读就会出现。BMP文件里像素数据顺序是B、G、R如果你写代码时按R、G、B顺序读画面就会明显偏蓝。还有一些LCD屏要求的数据格式是BGR565而不是RGB565这时候画面上红色和蓝色完全互换脸是蓝的、天是红的一眼就能看出来。处理方法是工具里加一个--format rgb|bgr选项根据屏幕数据手册来选择默认输出RGB565。4.3 图片显示出现斜纹、错位像是被“撕裂”了这个问题十有八九是行对齐没处理。BMP每行数据按4字节对齐存储如果宽度不是4的倍数行尾会多出几个空字节。转换时如果按width * 3去跳行从第二行开始就会偏移导致图片从某一行开始错位。排查方法就是打印每一行的字节偏移检查rowSize是否等于width * 3。如果不等按公式计算对齐后的行宽。4.4 数组太大Flash空间不够怎么办前面提到大图数组轻松几百KB单片机的Flash根本放不下。常用方案有三种一是把图片压缩成RLE或者LZ4格式存在外部Flash显示时解码二是做图片分割只加载当前屏幕需要显示的部分三是使用调色板压缩色深把图像从RGB565降到RGB444甚至RGB332牺牲画质换空间。我自己在项目里常用的是第一种简单RLE压缩在图标类素材上能有50%以上的压缩率解码速度也够快。4.5 透明背景变成黑色块这个情况常见于ICO或者PNG转过来的素材要么提前把背景色改成纯黑要么在转换工具里指定一个透明色转换时遇到这个颜色就写一个特殊值比如0x0000。显示时用这个值做透明判断但RGB565里黑色也是0x0000所以透明色策略在低色深下效果有限最好是在配图和UI设计时就规避透明需求。5. 工具扩展方向从单张转换到资源构建流水线用顺手之后我对这个工具做了几个小改进分享给有同样需求的朋友。第一个改进是批量转换。脚本支持传一个目录自动遍历所有BMP输出一个assets.c和一个assets.h每个图片分别命名成img_文件名。这一个改动让素材更新的效率提升了一个量级以前是转一张、拷贝一段、改代码现在改完图跑一下脚本重新编译完事。第二个改进是自动生成图片索引表。assets.h里除了声明数组还生成一个ImageDef结构体数组const ImageDef g_assets[] { {128, 64, img_logo}, {32, 32, img_icon_battery}, // ... };这样代码里只通过枚举或者宏下标访问图片不需要关心具体的内存地址。第三个改进是集成到构建流程里。在Makefile里加一条规则图片文件变化时自动重新运行转换脚本assets.c: assets/*.bmp python scripts/bmp2rgb565.py assets/ -o assets.c这样整个工程就变成了“改图 - 编译 - 烧录”的自动化流程跟游戏开发里资源管线是一个思路。我也尝试过用C#写一个带界面的版本但最后发现脚本流水线才是最适合嵌入式开发的形态命令行虽然不漂亮但效率高、可集成、方便版本管理。最近我还在这套工具基础上扩展了JPEG解码配合使用——大尺寸背景图用JPEG存放在SD卡或者外部Flash运行时解码到内存再刷屏小图标用RGB565数组存在内部Flash。两种方案按素材类型灵活切换算是比较实用的一个架构后续有机会再单独写一篇。本文还有配套的精品资源点击获取
分享:

看完干货,该让你的企业上线了

免费需求沟通 · 48 小时内出具建站方案 · 河南本地可上门