ESP32でRustを使ってLCD(ST7735)を制御する

· 7min

自作の組み込み機器の表示での文字などの表示は、7セグLEDを使うことが多いが、7セグLEDは結構ピン数が多いので面倒。ずっと気になっていたLCDを試してみた。

今回のLCDはこちら。

1.8インチTFT液晶ディスプレイモジュール(ST7735)

結線はSPIなので、少なくて済む。SPIのCLOCK, DATA, CSあと、ST7735はなぜかデータかコマンドかを知らせる制御線がSPIとは別に必要という不思議な造りになっているので、4線で接続する。

ST7735の制御について検索すると、だいたいはArduinoを使ったもので、しかもライブラリを使っておしまいという感じで、今回はESP32相手なのであまり参考にならない。ゆるプロで、ローレベルのST7735に送るコマンドが解説されていたので参考にさせてもらう。

今回もRustを使う。セットアップはLチカで書いた通り。ただ、ESP8684はユーザー用のSPIが1つしかないので、今回はノーマルなESP32を使った(電子ボリュームもSPIで制御なので2つ欲しい。まぁ両方同じSPIにぶら下げてCSで切り替えればいいのだけど、LCDのデータ転送はかなり速いので、あまり色々ぶら下げると信号がなまるかなと)。普通に以下を実行するだけで良く、ESP8684の時のようなクロックを26MHzに変更する設定などは不要。

cargo generate --git https://github.com/esp-rs/esp-idf-template cargo

ST7735は、1バイトのコマンドをまず送り、その後にコマンドによって決まる長さのデータ(パラメータと呼ぶ)を送る。例によって、こういう部分はST7735用のクレートに分けてみた。

作ったmain.rsはこんな感じ。

use esp_idf_svc::hal;
use hal::delay::FreeRtos;
use hal::gpio::{AnyInputPin, PinDriver};
use hal::peripherals::Peripherals;
use hal::spi::*;
use hal::units::*;
use st7735_rs::command::{Caset, Colmod, Command, Dispon, Ramwr, Raset, Slpout};
use st7735_rs::color_format::{ColorFormat, Pixel, Pixel12};
use embedded_hal::blocking::spi::WriteIter;

struct CmdSender<'a> {
    dat_cmd: PinDriver<'a, hal::gpio::Gpio12, hal::gpio::Output>,
    spi: SpiDeviceDriver<'a, &'a SpiDriver<'a>>,
}

impl<'a> CmdSender<'a> {
    fn new(
        dat_cmd: PinDriver<'a, hal::gpio::Gpio12, hal::gpio::Output>,
        spi: SpiDeviceDriver<'a, &'a SpiDriver<'a>>,
    ) -> Self {
        Self { dat_cmd, spi }
    }

    fn send(&mut self, mut cmd: impl Command) -> anyhow::Result<()> {
        self.dat_cmd.set_low()?;
        self.spi.write(&[cmd.cmd_byte()])?;
        self.dat_cmd.set_high()?;
        self.spi.write_iter(cmd.parm_bytes())?;
        FreeRtos::delay_ms(cmd.post_delay().as_millis() as u32);
        Ok(())
    }
}

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

    let peripherals = Peripherals::take()?;
    let spi = peripherals.spi2;
    let sclk = peripherals.pins.gpio14;
    let sdo = peripherals.pins.gpio13;
    let cs = peripherals.pins.gpio15;
    let driver = SpiDriver::new::<SPI2>(
        spi, sclk, sdo,
        Option::<AnyInputPin>::None,
        &SpiDriverConfig::new(),
    )?;

    let cfg = config::Config::new().baudrate(15.MHz().into());
    let spi_drv: SpiDeviceDriver<'_, &SpiDriver<'_>> = SpiDeviceDriver::new(&driver, Some(cs), &cfg)?;
    let dat_cmd: PinDriver<'_, hal::gpio::Gpio12, hal::gpio::Output> = PinDriver::output(peripherals.pins.gpio12)?;
    
    let mut cmd_sender = CmdSender::new(dat_cmd, spi_drv);
    cmd_sender.send(Slpout)?;
    cmd_sender.send(Colmod::new(ColorFormat::Bit12))?;
    cmd_sender.send(Dispon)?;
    let x_range = 0..10;
    cmd_sender.send(Caset::new(x_range.clone()))?;
    let y_range = 0..10;
    cmd_sender.send(Raset::new(y_range.clone()))?;
    cmd_sender.send(Ramwr::fill_rect(x_range.clone(), y_range.clone(), Pixel::<Pixel12>::BLUE))?;

    loop {
        FreeRtos::delay_ms(100);
    }
}

Cargo.tomlはdependenciesに以下を指定。

[dependencies]
log = "0.4"
esp-idf-svc = "0.51"
anyhow = "1"
st7735-rs = "0"
embedded-hal = "0.2"

クレートを分けたので、main.rsではコマンドを送っているだけ。なお、ESP32のSPIはフラッシュメモリーの読み込みなどに使われるものが1つ、ユーザーが自由に使えるものが2つ(それぞれSPI2, SPI3。またはHSPI, VSPIと呼ばれる)。今回はSPI2を使用。

ESP32のデータシートで、以下のように記載があるので注意。SPIの出力信号に入力専用のGPIOピンを割り当てても動作しない。

Note that the I/O GPIO pins are 0-19, 21-23, 25-27, 32-39, while the output GPIOs are 0-19, 21-23, 25-27,
32-33. GPIO pins 34-39 are input-only.

SPIのクロックは仕様書によると、66nsとあったので、逆算して15MHzにした。もっと速くても動く模様(30Mhzに設定しても動いた)。

  • Slpout: 液晶をスリープから復帰させる
  • Colmod: 液晶の表示方向、色数の設定(12bitだと4096色。昔のマイコンみたい)
  • Dispon: 表示をonにする
  • Caset, Raset: 液晶の書き換えはX方向の範囲とY方向の範囲をこれらのコマンドで設定してから、ピクセルを後続のRamwrで送る
  • Ramwr: 書き換えるピクセルを送信

表示

砂嵐になっているのは、電源on直後はメモリーにゴミが入っているため。左下に10x10の青い正方形が描画されていることが分かる。描画速度はどのくらいか計算してみる。この液晶は128x160ドットで、12bitモードでは2ドットで3バイトのデータが必要なので、画面全体の書き換えに必要なのは、

128 x 160 / 2 * 3 = 30720バイト = 245760ビット

SPIのクロックが15MHzだから、

245760 / 15 / 1000 / 1000 = 0.0164秒

1 / 0.0164 = 61FPS

理想値だけど、十分な速度だね。18ビットカラーのモードだと26万色表示でき、この時は1ドットで3バイト必要なので、30FPSということになる。ちょっと調べた感じだとダブルバッファリングとかは無いみたい。常に書き換えをした時にチラついたりしないかとかは未検証。まぁ自分はそんな使い方はしなさそうなので、とりあえずこれでOKにする。

あと液晶の端子にRESETがあったけど、今は3.3Vにプルアップしている。今のところ特に問題ないけど電源on直後に不安定ならパワーオンリセット回路を入れるなり、ESP32のGPIOにつないで、ソフトウェアでリセットした方が良いだろう。あと、LEDという端子はバックライトだった。とりあえず100Ωを入れて3.3Vにつないである。信号線は5Vトレラントではないので注意。まぁESP32なら問題無し。