【Rust】ESP32とST7735を使ったディスプレイの描画処理

2024年7月6日
【Rust】ESP32とST7735を使ったディスプレイの描画処理

前回の記事で、RustとESP32を使ってLチカを試した。

今回はST7735のTFT液晶ディスプレイで画像の描画を行ってみる。

準備

以下のライブラリを追加する。

組み込み用の2Dグラフィックライブラリ

ST7735向けのデバイスドライバ

依存パッケージを追加

$ cargo add embedded-graphics st7735-lcd

Cargo.tomlのdependenciesに以下が追加された。

embedded-graphics = "0.8.1"
st7735-lcd = "0.10.0"

SPI通信について

ESP32とディスプレイはSPI(Serial Peripheral Interface)を通して通信を行う。

SPIはマスターデバイスと複数のスレーブデバイスで通信を行う。 通常、以下の4本の信号線SPIバスを含む。

  • SCLK: クロック用で、データの同期に使用する
  • MOSI (Master Out Slave In): マスターがスレーブに情報を送る
  • MISO (Master In Slave Out): スレーブがマスターに情報を返す
  • CS/SS (Chip Select/Slave Select) : スレーブを選択する

ESP32とST7735ディスプレイを接続する場合、マスターがESP32、スレーブがST7735となる。 基本的に、ESP32がデータを送信し、ST7735がデータを受信するため、今回の用途ではMISOは使わない。

ピン設定

今回は以下のピン設定で、ESP32とST7735ディスプレイを接続する。

ST7735 LCD ピンESP32 GPIO機能説明
LED3.3VバックライトLCDのバックライト制御(通常は3.3Vに接続)
SCKGPIO18SCLKSPIクロック信号
SDAGPIO23MOSIマスターからスレーブへのデータ送信
A0GPIO2DCデータ/コマンド選択
RESETGPIO4RSTディスプレイのリセット
CSGPIO15CSチップセレクト信号
GNDGNDグラウンド電源のグラウンド
VCC3.3V電源3.3Vの電源供給

今回、ディスプレイは常時点灯させるため、LEDピンを3.3V電源に直接接続した。

① ディスプレイにHelloWorldを描画

まずはじめに、ST7735ディスプレイ1枚に対して、"hello world"と表示するプログラムを作成する。

全体のコードは以下の通り。 エラーハンドリングなどは雑なので注意。

use embedded_graphics::mono_font::ascii::FONT_6X10;
use embedded_graphics::mono_font::MonoTextStyleBuilder;
use embedded_graphics::pixelcolor::Rgb565;
use embedded_graphics::prelude::*;
use embedded_graphics::text::{Baseline, Text};
use esp_idf_hal::delay::FreeRtos;
use esp_idf_hal::gpio::{AnyIOPin, PinDriver};
use esp_idf_hal::prelude::Peripherals;
use esp_idf_hal::prelude::*;
use esp_idf_hal::spi;

use st7735_lcd;

fn main() -> anyhow::Result<()> {
    esp_idf_sys::link_patches();
    esp_idf_svc::log::EspLogger::initialize_default();

    let peripherals = Peripherals::take()?;

    // ピンの設定
    let spi = peripherals.spi2;
    let sclk = peripherals.pins.gpio18;
    let sdo = peripherals.pins.gpio23;
    let cs = peripherals.pins.gpio15;
    let rst = PinDriver::output(peripherals.pins.gpio4)?;
    let dc = PinDriver::output(peripherals.pins.gpio2)?;

    // SPIドライバーの設定
    let driver_config = Default::default();
    let spi_config = spi::SpiConfig::new().baudrate(20.MHz().into());
    let spi =
        spi::SpiDeviceDriver::new_single(spi, sclk, sdo, None::<AnyIOPin>, Some(cs), &driver_config, &spi_config)?;

    // ディスプレイの設定
    let rgb = false;
    let inverted = false;
    let width = 128;
    let height = 128;

    let mut delay = FreeRtos;
    let mut display = st7735_lcd::ST7735::new(spi, dc, rst, rgb, inverted, width, height);

    // ディスプレイの初期化
    display.init(&mut delay).unwrap();
    display.clear(Rgb565::BLACK).unwrap();
    display.set_offset(0, 0);

    // テキストのスタイルを設定
    let text_style = MonoTextStyleBuilder::new()
        .font(&FONT_6X10)
        .text_color(Rgb565::WHITE)
        .build();

    // "hello world"テキストを描画
    Text::with_baseline(
        "hello world",
        Point::new(0, 0),
        text_style,
        Baseline::Top,
    ).draw(&mut display).unwrap();

    println!("The screen should display 'hello world'.");
    loop {
        FreeRtos::delay_ms(1000);
    }
}

以下、コードを解説する。

ピンの設定

let spi = peripherals.spi2;
let sclk = peripherals.pins.gpio18;
let sdo = peripherals.pins.gpio23;
let cs = peripherals.pins.gpio15;
let rst = PinDriver::output(peripherals.pins.gpio4)?;
let dc = PinDriver::output(peripherals.pins.gpio2)?;

ST7735 LCDディスプレイとの通信に必要なピンを設定。 上述の今回のピン設定に対応している。

SPIドライバーの設定

let driver_config = Default::default();
let spi_config = spi::SpiConfig::new().baudrate(20.MHz().into());
let spi = spi::SpiDeviceDriver::new_single(spi, sclk, sdo, None::<AnyIOPin>, Some(cs), &driver_config, &spi_config)?;

SPIドライバーを設定している。 通信速度を20MHzに設定し、ピン設定を指定してSPIデバイスドライバーを初期化する。 MISOピンは使用しないため、None::<AnyIOPin>を指定。

ディスプレイの初期化

let rgb = false;
let inverted = false;
let width = 128;
let height = 128;

let mut delay = FreeRtos;
let mut display = st7735_lcd::ST7735::new(spi, dc, rst, rgb, inverted, width, height);

display.init(&mut delay).unwrap();
display.clear(Rgb565::BLACK).unwrap();
display.set_offset(0, 0);

ST7735ディスプレイオブジェクトを作成し、初期化。 ディスプレイのサイズを128x128ピクセルに設定し、バックグラウンドを黒色にクリアする。

テキスト描画

let text_style = MonoTextStyleBuilder::new()
    .font(&FONT_6X10)
    .text_color(Rgb565::WHITE)
    .build();

Text::with_baseline(
    "hello world",
    Point::new(0, 0),
    text_style,
    Baseline::Top,
).draw(&mut display).unwrap();

テキストスタイルを設定し、"hello world"というテキストをディスプレイの左上隅に白色で描画。

テキスト描画の実行

以下の通り、ディスプレイに"hello world"と表示された。

ディスプレイにhello worldを表示

② ディスプレイに画像を描画

次に、ST7735ディスプレイに画像を描画する。

プロフィール画像

ひとまず上記の画像を表示するが、描画するにあたって、128x128のbitmap画像に変換する必要がある。 画像のリサイズと変換には、以下のPhotoscape Xを使用した。

サイズ変更した後、BMP形式で保存する。

プロフィール画像

プロジェクトにresourcesディレクトリを追加する。

project/
├── src/
│   └── main.rs
├── resources/
│   └── image.bmp
└── Cargo.toml

tinybmpを追加でインストールする。

$ cargo add tinybmp

以下のバージョンのtinybmpがCargo.tomlに追加された。

tinybmp = "0.6.0"

以下、bitmap画像を描画するコードを示す。

use embedded_graphics::image::Image;
use embedded_graphics::pixelcolor::Rgb565;
use embedded_graphics::prelude::*;
use esp_idf_hal::delay::FreeRtos;
use esp_idf_hal::gpio::{AnyIOPin, PinDriver};
use esp_idf_hal::prelude::Peripherals;
use esp_idf_hal::prelude::*;
use esp_idf_hal::spi;

use st7735_lcd;
use tinybmp::Bmp;

const BITMAP_DATA: &[u8] = include_bytes!("../resources/image.bmp");

fn main() -> anyhow::Result<()> {
    esp_idf_sys::link_patches();
    esp_idf_svc::log::EspLogger::initialize_default();

    let peripherals = Peripherals::take()?;

    // ピンの設定
    let spi = peripherals.spi2;
    let sclk = peripherals.pins.gpio18;
    let sdo = peripherals.pins.gpio23;
    let cs = peripherals.pins.gpio15;
    let rst = PinDriver::output(peripherals.pins.gpio4)?;
    let dc = PinDriver::output(peripherals.pins.gpio2)?;

    // SPIドライバーの設定
    let driver_config = Default::default();
    let spi_config = spi::SpiConfig::new().baudrate(20.MHz().into());
    let spi =
        spi::SpiDeviceDriver::new_single(spi, sclk, sdo, None::<AnyIOPin>, Some(cs), &driver_config, &spi_config)?;

    // ディスプレイの設定
    let rgb = false;
    let inverted = false;
    let width = 128;
    let height = 128;

    let mut delay = FreeRtos;
    let mut display = st7735_lcd::ST7735::new(spi, dc, rst, rgb, inverted, width, height);

    // ディスプレイの初期化
    display.init(&mut delay).unwrap();
    display.clear(Rgb565::BLACK).unwrap();
    display.set_offset(0, 0);

    // BMPデータの読み込みと描画
    let bmp = Bmp::from_slice(BITMAP_DATA).unwrap();
    let image = Image::new(&bmp, Point::zero());
    image.draw(&mut display).unwrap();

    println!("The screen should display the bitmap image.");
    loop {
        FreeRtos::delay_ms(1000);
    }
}

以下、bitmap画像を描画するために変更した箇所について解説する。

BMPデータの読み込み

const BITMAP_DATA: &[u8] = include_bytes!("../resources/image.bmp");

include_bytes!マクロを使用して、コンパイル時にBMP画像ファイルをバイト配列として含めている。 これにより、画像データがプログラム中に埋め込まれる。

BMPデータの解析と描画

// BMPデータの読み込みと描画
let bmp = Bmp::from_slice(BITMAP_DATA).unwrap();
let image = Image::new(&bmp, Point::zero());
image.draw(&mut display).unwrap();
  1. Bmp::from_slice(BITMAP_DATA)を使用して、バイナリデータからBMPオブジェクトを作成
  2. Image::new(&bmp, Point::zero())で、BMPデータからImageオブジェクトを作成
  3. image.draw(&mut display)で、作成したImageオブジェクトをディスプレイに描画

画像描画の実行

以下の通り、ディスプレイに画像が表示された。

ディスプレイにしろくま画像を表示

次の記事はこちら。