Feb 10, 2026
5 min read
ESP32,
Rust,
C,
Embedded Systems,

Rust嵌入式开发:在st7789屏幕上显示图片

embedded-graphics 是 Rust 在嵌入式开发中显示屏图形显示标准,因此我们可以使用它来显示图片。

然而,embedded-graphics 并不支持常见的图片格式,比如 png、jpg 等。

通过文档得知,embedded-graphics 只支持 bmp、tga、qoi 这三种格式。

事实上没那么简单, st7789 屏幕并不支持 RGB888(也叫RGB24位) 色彩格式。通过查阅文档可知, 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
    
    Args:
        input_path: 输入的JPG图片路径
        output_path: 输出的TGA图片路径(可选,默认为输入文件名.tga)
        size: 目标尺寸,默认为(64, 64)
    
    Returns:
        输出文件的路径
    """
    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 length
    header[1] = 0    # Color map type
    header[2] = 2    # Image type: uncompressed true-color
    header[3] = 0    # Color map spec: first entry low
    header[4] = 0    # Color map spec: first entry high
    header[5] = 0    # Color map spec: length low
    header[6] = 0    # Color map spec: length high
    header[7] = 0    # Color map spec: depth
    header[8] = 0    # X origin low
    header[9] = 0    # X origin high
    header[10] = 0   # Y origin low
    header[11] = 0   # Y origin high
    header[12] = size[0] & 0xFF      # Width low
    header[13] = (size[0] >> 8) & 0xFF  # Width high
    header[14] = size[1] & 0xFF      # Height low
    header[15] = (size[1] >> 8) & 0xFF  # Height high
    header[16] = 16  # Bits per pixel
    header[17] = 0x20  # Image descriptor: bit 5=1 (origin at bottom-left)
    
    # 写入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格式)
    
    Args:
        input_path: 输入的JPG图片路径
        output_path: 输出的BMP图片路径(可选,默认为输入文件名.bmp)
        size: 目标尺寸,默认为64x64
    
    Returns:
        输出文件的路径
    """
    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是bottom-up格式,需要从底部开始存储行
        # 即第一行像素数据对应图像的最底部
        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 屏幕上:

logo

核心代码很少,其实大概就是如下所示:

    // 需要清空屏幕,否则会显示上一次的内容
    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个大坑

下面这两个坑都会造成类似下面的颜色显示错误:

IMG_20260212_214349.jpg

第一个坑: 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();

显示在屏幕上是白色的。比如我之前显示的温度湿度的屏幕界面:

IMG_20260209_225748.jpg

可以发现,我设置的是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();

最后效果效果如下:

IMG_20260212_214239.jpg

完整代码

#![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 屏幕显示图片的大小是有限的,不能超过屏幕的分辨率。