Skip to content

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

kingzcheung
Published date:
Edit this post

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 屏幕上显示:

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

Previous
Rust嵌入式开发:如何解决st7789屏幕在重绘时闪烁的问题
Next
Methods for Displaying Chinese Characters in Embedded Rust