embedded-graphics 是 Rust 嵌入式开发中显示图形标准的库,因此我们可以用它来显示图片。
但是,embedded-graphics 不支持常见的图片格式,如 png、jpg 等。
从文档中我们了解到,embedded-graphics 只支持 bmp、tga 和 qoi 格式。
实际上,这并没有那么简单。ST7789 屏幕不支持 RGB888(也被称为 24 位 RGB)颜色格式。经过文档研究,我们知道 ST7789 屏幕只支持 RGB565(16 位)颜色格式。
所以解决方案很简单:我们只需要将图片从 24 位转换为 16 位颜色空间,并转换为 bmp、tga 或 qoi 格式即可在屏幕上显示。
转换图片
你可以使用 Python 脚本来将任何图片格式转换为上述格式。
例如,这里是我实现的将 jpg 转换为 tga 的脚本:
def convert_jpg_to_tga_rgb565(input_path: str, output_path: Optional[str] = None, size: Tuple[int, int] = (64, 64)) -> str:
"""
调整 JPG 图片大小并转换为 RGB565 格式 TGA
参数:
input_path: 输入 JPG 图片路径
output_path: 输出 TGA 图片路径(可选,默认使用输入文件名.tga)
size: 目标大小,默认为 (64, 64)
返回:
输出文件路径
"""
input_path_obj = Path(input_path)
# 确定输出路径
if output_path is None:
output_path_obj = input_path_obj.with_suffix('.tga')
else:
output_path_obj = Path(output_path)
output_path_str = str(output_path_obj)
# 打开图片
with Image.open(input_path_obj) as img:
# 转换为 RGB 模式
if img.mode != 'RGB':
img = img.convert('RGB')
# 调整到指定尺寸
img_resized = img.resize(size, Image.Resampling.LANCZOS)
# RGB565 布局:bit15-11=红色(5 位),bit10-5=绿色(6 位),bit4-0=蓝色(5 位)
pixel_data = bytearray()
for r, g, b in img_resized.getdata():
r5 = (r >> 3) & 0x1F # 红色:8 位到 5 位
g6 = (g >> 2) & 0x3F # 绿色:8 位到 6 位
b5 = (b >> 3) & 0x1F # 蓝色:8 位到 5 位
# 组合为 16 位 RGB565:高到低 = RRRRR GGGGGG BBBBB
pixel565 = (r5 << 11) | (g6 << 5) | b5
pixel_data.append(pixel565 & 0xFF)
pixel_data.append((pixel565 >> 8) & 0xFF)
header = bytearray(18)
header[0] = 0 # ID 长度
header[1] = 0 # 颜色映射类型
header[2] = 2 # 图片类型:未压缩真彩色
header[3] = 0 # 颜色映射规范:第一个条目低位
header[4] = 0 # 颜色映射规范:第一个条目高位
header[5] = 0 # 颜色映射规范:长度低位
header[6] = 0 # 颜色映射规范:长度高位
header[7] = 0 # 颜色映射规范:深度
header[8] = 0 # X 起始点低位
header[9] = 0 # X 起始点高位
header[10] = 0 # Y 起始点低位
header[11] = 0 # Y 起始点高位
header[12] = size[0] & 0xFF # 宽度低位
header[13] = (size[0] >> 8) & 0xFF # 宽度高位
header[14] = size[1] & 0xFF # 高度低位
header[15] = (size[1] >> 8) & 0xFF # 高度高位
header[16] = 16 # 每像素位数
header[17] = 0x20 # 图片描述符:bit 5=1(原点在左下角)
# 写入 TGA 文件
with open(output_path_str, 'wb') as f:
f.write(header)
f.write(pixel_data)
return output_path_str
注意,你需要小心是否转换为 RGB565( RRRRR GGGGGG BBBBB) 或 BGR565( BBBBB GGGGGG RRRRR)。两者都支持,这非常重要 - 稍后会用到。
顺序主要由以下代码控制:
# BGR565
pixel565 = (b5 << 11) | (g6 << 5) | r5
# RGB565
pixel565 = (r5 << 11) | (g6 << 5) | b5
jpg 到 bmp (rgb565) 转换大致如下:
def convert_jpg_to_bmp_rgb565(input_path: str, output_path: Optional[str] = None, size: Tuple[int, int] = (128,128)) -> str:
"""
将 JPG 图片转换为 16 位 RGB565 格式 BMP(BITFIELDS 格式)
参数:
input_path: 输入 JPG 图片路径
output_path: 输出 BMP 图片路径(可选,默认使用输入文件名.bmp)
size: 目标大小,默认为 64x64
返回:
输出文件路径
"""
input_path_obj = Path(input_path)
if not input_path_obj.exists():
raise FileNotFoundError(f"未找到输入文件:{input_path}")
# 确定输出路径
if output_path is None:
output_path_obj = input_path_obj.with_suffix('.bmp')
else:
output_path_obj = Path(output_path)
output_path_str = str(output_path_obj)
# 打开图片
with Image.open(input_path_obj) as img:
# 转换为 RGB 模式
if img.mode != 'RGB':
img = img.convert('RGB')
# 调整到指定尺寸
img_resized = img.resize(size, Image.Resampling.LANCZOS)
# 获取所有像素数据
all_pixels = list(img_resized.getdata())
width, height = size
# BMP 是从底部向上格式,需要从底部存储行
# 即像素数据的第一行对应图片的底部
pixel_data = bytearray()
for y in range(height - 1, -1, -1): # 从底部开始
for x in range(width):
r, g, b = all_pixels[y * width + x]
# RGB565:R:5 位,G:6 位,B:5 位
r5 = (r >> 3) & 0x1F
g6 = (g >> 2) & 0x3F
b5 = (b >> 3) & 0x1F
# 组合为 16 位:RRRRR GGGGGG BBBBB
pixel565 = (r5 << 11) | (g6 << 5) | b5
# 小端序存储
pixel_data.append(pixel565 & 0xFF)
pixel_data.append((pixel565 >> 8) & 0xFF)
# 创建 BITFIELDS 格式 BMP 头部(70 字节)
header = create_bitfields_bmp_header(size[0], size[1], len(pixel_data))
# 写入 BMP 文件
with open(output_path_str, 'wb') as f:
f.write(header)
f.write(pixel_data)
return output_path_str
驱动图片显示
TGA 和 BMP 图片分别使用这两个库:
tinytga = "0.5.0"
tinybmp = "0.7.0"
这里我将使用以下图片作为示例在 ST7789 屏幕上显示:

核心代码很少,大致如下:
// 需要清除屏幕,否则会显示之前的画面
display.clear(Rgb565::BLACK).unwrap();
let data = include_bytes!("../../jing.tga");
let img: Tga<Rgb565> = Tga::from_slice(data).unwrap();
// 如果使用 bmp 图片,使用以下代码
// let data = include_bytes!("../../jing.bmp");
// let img: Bmp<Rgb565> = Bmp::from_slice(data).unwrap();
let image = Image::new(&img, Point::zero());
image.draw(&mut display.color_converted()).unwrap();
loop {
delay.delay_millis(500);
}
2 个主要坑
以下两个坑都可能导致颜色显示错误,如下所示:

第一个坑:Rgb565 还是 Bgr565?
在转换时,你必须注意顺序。如果你转换为 Rgb,那么初始化时使用 Tga<Rgb565>。如果是 Bgr,那么使用 Tga<Bgr565>。
另外,在初始化屏幕时,需要设置:
let mut display = Builder::new(ST7789, di)
.reset_pin(rst)
.color_order(mipidsi::options::ColorOrder::Rgb) //<======== 这里设置为 Rgb 或 Bgr
.init(&mut delay)
.unwrap();
第二个坑:屏幕颜色反转
你可能像我一样,在走出第一个坑后又掉进第二个坑。我不知道这是我的初始化问题还是 mipidsi 驱动的问题,但默认情况下如果你什么都不做,只是设置这个:
display.clear(Rgb565::BLACK).unwrap();
它在屏幕上显示为白色。例如,我之前的温湿度屏幕界面:

你可以看到我设置了 Rgb565::BLACK,但屏幕显示白色。这个问题在 C 语言中没有发生。
解决方案是在初始化屏幕时添加以下参数来反转屏幕颜色:
let mut display = Builder::new(ST7789, di)
.reset_pin(rst)
.color_order(mipidsi::options::ColorOrder::Rgb)
// 反转屏幕颜色
.invert_colors(mipidsi::options::ColorInversion::Inverted)
.init(&mut delay)
.unwrap();
最终结果如下:

完整代码
#![no_std]
#![no_main]
#![deny(
clippy::mem_forget,
reason = "mem::forget is generally not safe to do with esp_hal types, especially those \
holding buffers for the duration of a data transfer."
)]
#![deny(clippy::large_stack_frames)]
use embedded_graphics::prelude::RgbColor;
use embedded_graphics::{
Drawable,
image::Image,
pixelcolor::{Rgb565, Bgr565},
prelude::{DrawTarget, DrawTargetExt, OriginDimensions, Point, Size},
};
use embedded_hal_bus::spi::ExclusiveDevice;
use esp_alloc as _;
use esp_hal::main;
use esp_hal::{clock::CpuClock, delay::Delay, gpio, spi::master::Config, time::Rate};
use mipidsi::{Builder, interface::SpiInterface, models::ST7789, options::Orientation};
use tinybmp::Bmp;
use tinytga::Tga;
#[panic_handler]
fn panic(_: &core::panic::PanicInfo) -> ! {
loop {}
}
extern crate alloc;
esp_bootloader_esp_idf::esp_app_desc!();
#[allow(
clippy::large_stack_frames,
reason = "it's not unusual to allocate larger buffers etc. in main"
)]
#[main]
fn main() -> ! {
// static mut APP_CORE_STACK: Stack<8192> = Stack::new();
let mut delay = Delay::new();
let config = esp_hal::Config::default().with_cpu_clock(CpuClock::max());
let peripherals = esp_hal::init(config);
// LCD 显示初始化
let dc = gpio::Output::new(peripherals.GPIO15, gpio::Level::Low, Default::default());
let mut rst = gpio::Output::new(peripherals.GPIO7, gpio::Level::Low, Default::default());
rst.set_high();
let cs = gpio::Output::new(peripherals.GPIO16, gpio::Level::High, Default::default());
let spi = esp_hal::spi::master::Spi::new(
peripherals.SPI2,
Config::default().with_frequency(Rate::from_mhz(30)),
)
.unwrap()
.with_sck(peripherals.GPIO5)
.with_mosi(peripherals.GPIO6);
let spi_device = ExclusiveDevice::new_no_delay(spi, cs).unwrap();
let mut buffer = [0_u8; 512];
let di = SpiInterface::new(spi_device, dc, &mut buffer);
let mut display = Builder::new(ST7789, di)
.reset_pin(rst)
.color_order(mipidsi::options::ColorOrder::Rgb)
.invert_colors(mipidsi::options::ColorInversion::Inverted)
.init(&mut delay)
.unwrap();
// 需要清除屏幕,否则会显示之前的画面
display.clear(Rgb565::BLACK).unwrap();
let data = include_bytes!("../../jing.tga");
let img: Tga<Rgb565> = Tga::from_slice(data).unwrap();
// let data = include_bytes!("../../jing.bmp");
// let img: Bmp<Bgr565> = Bmp::from_slice(data).unwrap();
let image = Image::new(&img, Point::new(64, 64));
image.draw(&mut display.color_converted()).unwrap();
loop {
delay.delay_millis(500);
}
}
最后
另外,在 ST7789 屏幕上显示的图片大小是有限制的,不能超过屏幕分辨率。