ESP32-S3で動かす秋月300円液晶

秋月300円液晶」(もしくはただ単に「300円液晶」)と呼ばれる液晶があります。

令和最新版

まさに秋月最大の謎。2006年販売開始、しかも2023年現在になっても未だに在庫されているという不思議な商品です。
カラーグラフィックTFT液晶モジュール+インバータセット: ディスプレイ・表示器 秋月電子通商-電子部品・ネット通販
その特徴は、

  • わざわざ「メーカー問い合わせ不可」と表記されていること
  • 少なくとも発売当時としてはかなり鮮やかなフルカラー液晶であること
  • 独特な縦横比(400*96px)
  • 独特な駆動電圧
  • 独特な駆動方式
  • 今となっては逆に珍しいCCFLバックライト

そして300円という値段です(特徴が多すぎる)。正式な型番は「LTA042B010F」。

5つ買っても1500円

ちなみに、実は現在は液晶モジュールそのものの価格は200円に値下げされており、バックライト用インバータとのセットが300円で販売されています。
販売開始直後の様子を詳しくは知らないのですが、今でもブログや個人サイトやニコニコ動画で駆動に挑戦した記録が残っており(駆動させること自体が挑戦だったわけです)、その頃のブームの様子をうかがい知ることができます。当時の記事をいくつか見ると、AVR、PIC、H8、そしてCPLDFPGAなど様々なデバイスで駆動ができていたことがわかります。ですが今は2023年です。当時は存在しなかったデバイスで挑戦してみましょう。
ESP32-S3です。

いつ見ても すごい形の 基板かな

先人の記録

さて、まずはネット上に大量に残されている記録を読み漁ることから始めます。


中でも、このあたりの資料を特に参考にします。もちろんここ以外にも様々な方が情報を残してくださっています。先人に感謝!

コネクタ

当時の記録を見ていると、まず液晶のコネクタからどうやって信号を取り出すかといったところから苦労している様子です。
300円液晶のコネクタの物理的仕様は、36ピン0.5mmピッチのFFC(フレキシブルフラットケーブル)コネクタというもの。FFCの入手が面倒だったころは、このコネクタごと取っ払ってしまい、基板に直接線をはんだ付けしたりしていたようですね。
が!実は秋月から便利で安価な40ピン0.5mmFFCコネクタ→DIPの変換基板が売られています。
フレキコネクタDIP化基板(0.5mmピッチ40P・コネクタ実装済): 組立キット(モジュール) 秋月電子通商-電子部品・ネット通販
これと液晶を接続するためのFFCそのものも必要となりますが、AliExpressやAitendoで安定して(?)購入できる時代になりましたのでそのあたりも問題ないと思います。
なお、液晶側のコネクタは36ピンなので、36ピンのケーブルを用意して40ピンコネクタ側にはうまいこと場所を調整しながら接続するとか、40ピンのケーブルを用意して片方を4ピン分切り取るとか、いい感じに工夫して接続します。

こういうこと

電源

この液晶を駆動するにあたって、以下の電源が必要となります。

  • ロジック電源(5V)
  • 電極印加用正負電源(±9~15V?)
  • バックライトインバータ用電源(12V~?)

さらにESP32を駆動する場合、そのために3.3Vも。
電極印加用の正負電源を外から与えてやる必要がある、というのが原始的な感じがあっていいですね。
さらにこのインバータ(K-G00-500-A11)自体も一癖ありまして、商品ページにはこの液晶パネルのバックライト専用設計品ではないと明記されています(セット販売されているのに!w)。このインバータの定格入力電圧は12Vとされているのですが、実はこの液晶のCCFLには12Vでは電圧不足のようで明るさにムラがでてしまい、およそ13.5Vを超えたあたりからやっと均一な明るさが出てくるようになります。
まあFFCのコネクタも複数の電圧の電源も正直言って面倒この上ないのですが、ここまで用意ができてやっと300円液晶駆動のスタートラインなわけです。

テストパターン

ESP32でガッツリ駆動させる前に、まずは内蔵のテストパターンを出せるかどうかを試します。
以下のように信号を入力します。

1    TEST      HIGH(5V)
2    NCLK      とりあえず1MHzとか
3    GND 
4    HSYNC     LOW
5    VSYNC     LOW
6    不明      LOW
7    GND 
8    B5        LOW
:   :         :
13   B0        LOW
14   GND
15   G5        LOW
:   :         :
20   G0        LOW
21   DVDD      5V
22   R5        LOW
:   :         :
27   R0        LOW
28   AVDD      LOW
29   VCPP_ADJ  2~3V
30   GND 
31   GVDD      5V
32   GND 
33   VSS       -9~-15V
34   GND 
35   VGON      9~15V
36   GND 


各電源端子およびコントラスト調節端子(29番)には適した電圧を、TEST端子(1番)にはHIGHを、クロック端子(2番)には適当なクロックを、それ以外のロジックはLOWに落とします。コントラスト調節端子は2~3Vがベストですが、実は0Vでも5Vでも一応映るので面倒くさいなら電源端子直結でもいいです。
クロック周波数はとりあえず当初の実験では1MHzとしましたが0.1MHzでも10MHzでもいいと思います。

バックライトの輝度ムラの件はサラッと書いてますが実は結構悩んだ

これで内蔵テストパターンが映ります!!
ちなみに、バックライトの明るさにムラがあるせいでまるでグラデーションがかかったような感じになり右側が全体的に暗くなっていますが、この時はインバータ基板に12V電源でテストしていたためです。先述の通りもうちょっと高い電圧にすればバックライトは均一な明るさになります。

テストパターンの表示ができたので、今度はいよいよ本番、ESP32-S3からの画像の入力もやっていきましょう。

ゴリっ

上記のリンクの通りすでに駆動方法は完全に解析されつくしているので、それをコードとして書けばいいだけの話ではあります。しかしながら、さすがに現代的なESP32とはいえフルカラーグラフィックの処理は全部GPIOでソフトウェア処理できるほど軽いものではありません。
やはりDMAやタイマーを組み合わせてうまいことデータを転送しなければ……と思い、どんな内蔵ペリフェラルが使えるかと(ESP32シリーズのひとつで比較的高機能な)ESP32-S3のテクニカルリファレンスマニュアルをボーっと読んでいたのですが、その時以下のような記述を発見しました。

アッ!!って声出た

Hardware Reference - ESP32-S3 - — ESP-IDF Programming Guide latest documentation
ここでいうLCD(RGB Format)、300円液晶の駆動方式そのものなのでは……!?

どうもこの300円液晶の駆動方式、これ特有の特殊なものではなく、「パラレルRGB」(もしくは単に「RGB」)などと呼ばれる一般的なもののようです。
parallel RGB - Google 検索
確かに検索するとそれっぽい説明がいくつも出てきます。
LCD - ESP32-S3 - — ESP-IDF Programming Guide v4.4.6 documentation
これが公式のAPIリファレンス。バージョンが最新ではないv4.4.xですが、これは執筆時Arduino IDEからダウンロードされるバージョンに従っています。
しかし、これまでの300円液晶の解析記事ではパラレルRGBなんて表記は一回も見てないような。多分このあたりの話、「趣味でやってる人は知らない」「仕事でやっている人は知ってて当たり前」みたいな感じなんですかね……

まあとにかく仕組みが分かってしまえばあとは本当にコードを書くだけです。

#include "img.hpp"

#include <esp_lcd_panel_ops.h>
#include <esp_lcd_panel_rgb.h>

void setup()
{
    pinMode(6, OUTPUT);
    digitalWrite(6, HIGH);
    esp_lcd_panel_handle_t panel_handle = NULL;
    esp_lcd_rgb_panel_config_t panel_config =
    {
        .clk_src = LCD_CLK_SRC_XTAL, // 40MHzらしい
        .timings =
        {
            .pclk_hz = 5000000,
            .h_res = 400,
            .v_res = 96,
            .hsync_pulse_width = 1,
            .hsync_back_porch = 107,
            .hsync_front_porch = 1,
            .vsync_pulse_width = 1,
            .vsync_back_porch = 15,
            .vsync_front_porch = 1,
            .flags = 
            {
                .hsync_idle_low = 1,
                .vsync_idle_low = 1,
                .pclk_active_neg = 1,
            },
        },
        .data_width = 16, // RGB565なので
        .hsync_gpio_num = 1, // GPIOピン番号は各自の環境に合わせてください。以下同様
        .vsync_gpio_num = 2,
        .de_gpio_num = -1, // 未使用は-1
        .pclk_gpio_num = 4,
        .data_gpio_nums = {38, 39, 40, 41, 42,  3, 46,  9,
                           14, 21, 47, 15, 16, 17, 18,  8}, 
        //                 B1  B2  B3  B4  B5  G0  G1  G2
        //                 G3  G4  G5  R1  R2  R3  R4  R5
        .disp_gpio_num = -1,
    };

    ESP_ERROR_CHECK(esp_lcd_new_rgb_panel(&panel_config, &panel_handle));
    ESP_ERROR_CHECK(esp_lcd_panel_reset(panel_handle));
    ESP_ERROR_CHECK(esp_lcd_panel_init(panel_handle));
    esp_lcd_panel_draw_bitmap(panel_handle, 10, 10, 10 + imgWidth, 10 + imgHeight, img);
}

void loop()
{
}

ゴリゴリっと書きます。え、これだけでいいの?って感じです。
コードの上半分の配列は画像データです。1つ1つの要素の2バイト(16bit)のデータはRGB565で表現された色のデータで、液晶の1ピクセルの画素の色に対応するわけです。コメント内にURLありますが改めて以下にデータの変換に使用したツールのリンクを貼ります。
画像データImageData化
で、これをESP32-S3に書き込むと

すべてが報われた瞬間

映ります!!

LovyanGFXを使う

正直こんな感じに画像1枚出せただけでもう感動ものなの(だってあの300円液晶を動かしてるんですよ!?)ですが、さすがに画像データを用意するのが面倒です。何かライブラリが使えないかと思うわけですね。ESP32シリーズの高速なグラフィックライブラリと言えば、そうLovyanGFXです。
GitHub - lovyan03/LovyanGFX: SPI LCD graphics library for ESP32 (ESP-IDF/ArduinoESP32) / ESP8266 (ArduinoESP8266) / SAMD51(Seeed ArduinoSAMD51)
実はこのHello Worldの表示に成功したのが2022年10月31日なのですが、そのたった2か月後、2022年12月24日にLovyanGFX 0.5.0がリリースされます。
ここのリリースノートを見ると……

これって恋?

なんとLovyanGFXでパラレルRGB液晶がサポートされるようになるのです!すごいタイミングです。
そうと分かれば、もう1回ゴリっと書きます。

#pragma once

#define LGFX_USE_V1

#include <LovyanGFX.hpp>

#include <lgfx/v1/platforms/esp32s3/Panel_RGB.hpp>
#include <lgfx/v1/platforms/esp32s3/Bus_RGB.hpp>

class LGFX : public lgfx::LGFX_Device
{
    lgfx::Panel_RGB _panel_instance;
    lgfx::Bus_RGB _bus_instance;
    lgfx::Light_PWM _light_instance;

public:
    LGFX(void)
    {
        {
            auto cfg = _panel_instance.config();

            cfg.memory_width  = 400;
            cfg.memory_height = 96;
            cfg.panel_width  = 400;
            cfg.panel_height = 96;
            
            _panel_instance.config(cfg);
        }

        {
            auto cfg = _bus_instance.config();

            cfg.panel = &_panel_instance;
            cfg.pin_d0  = GPIO_NUM_38; // B1
            cfg.pin_d1  = GPIO_NUM_39; // B2
            cfg.pin_d2  = GPIO_NUM_40; // B3
            cfg.pin_d3  = GPIO_NUM_41; // B4
            cfg.pin_d4  = GPIO_NUM_42; // B5
            cfg.pin_d5  = GPIO_NUM_3;  // G0
            cfg.pin_d6  = GPIO_NUM_46; // G1
            cfg.pin_d7  = GPIO_NUM_9;  // G2
            cfg.pin_d8  = GPIO_NUM_14; // G3
            cfg.pin_d9  = GPIO_NUM_21; // G4
            cfg.pin_d10 = GPIO_NUM_47; // G5
            cfg.pin_d11 = GPIO_NUM_15; // R1
            cfg.pin_d12 = GPIO_NUM_16; // R2
            cfg.pin_d13 = GPIO_NUM_17; // R3
            cfg.pin_d14 = GPIO_NUM_18; // R4
            cfg.pin_d15 = GPIO_NUM_8;  // R5

            cfg.pin_henable = GPIO_NUM_45; // de_gpio_numのこと -1だと再起動ループになってしまうのでボード上で使っていない45を指定した
            cfg.pin_vsync   = GPIO_NUM_2;
            cfg.pin_hsync   = GPIO_NUM_1;
            cfg.pin_pclk    = GPIO_NUM_4;
            cfg.freq_write  = 5000000;

            cfg.hsync_polarity    = 0;
            cfg.hsync_front_porch = 1;
            cfg.hsync_pulse_width = 1;
            cfg.hsync_back_porch  = 107;
            cfg.vsync_polarity    = 0;
            cfg.vsync_front_porch = 1;
            cfg.vsync_pulse_width = 1;
            cfg.vsync_back_porch  = 15;
            cfg.pclk_idle_high    = 1;

            _bus_instance.config(cfg);
            _panel_instance.setBus(&_bus_instance);
        }

        {
            auto cfg = _light_instance.config();

            cfg.pin_bl = 6;
            cfg.invert = false;
            cfg.freq   = 200;
            cfg.pwm_channel = 7;

            _light_instance.config(cfg);
            _panel_instance.setLight(&_light_instance);
        }
        setPanel(&_panel_instance);
    }
};

独自のクラスを作って「LGFX_ESP32S3_LTA042B010F.hpp」で保存。

#include "LGFX_ESP32S3_LTA042B010F.hpp"

static LGFX lcd;

void setup()
{
    lcd.init();
    lcd.setBrightness(255);
    lcd.setTextSize(2, 2);
    int color_temp;
    for (int i = 0; i < 2; i++) {
        for (int j = 0; j < 3; j++) {
            color_temp = 255;
            for(int k = 0; k < 8; k++) {
                switch (j) {
                case 0:
                    lcd.setTextColor(lcd.color565(color_temp, 0, 0), i * 0xFFFF);
                    break;
                case 1:
                    lcd.setTextColor(lcd.color565(0, color_temp, 0), i * 0xFFFF);
                    break;
                case 2:
                    lcd.setTextColor(lcd.color565(0, 0, color_temp), i * 0xFFFF);
                    break;
                default:
                    break;
                }
                color_temp -= 32;
                lcd.print("A");
            }
        }
        lcd.println();
    }
}

void loop(void)
{
}

これでバッチリ映ります。

ここまで来たのもひとえに皆様のおかげです

いやLovyanGFXすごくないですか!? ついにあの300円液晶が手軽なグラフィックライブラリで動かせるようになった瞬間です。

はい

というわけで、ここだけは覚えて帰ってください。

  • 秋月300円液晶はパラレルRGBなどと呼ばれるインターフェースで駆動されている
  • LovyanGFX 0.5.0以降では、ESP32-S3の内蔵ペリフェラルからパラレルRGBの信号を出力できるようになった

当時を詳しく知らないのであくまで憶測ですが、開発環境や部品調達の容易さなどの違いで、当時のブームの時と比較しても300円液晶の駆動はずいぶんと楽になっていると思います。
こんな感じで比較的容易に触れるようになっている300円液晶で、今こそ皆さんも遊んでみてはいかがでしょうか?

(基板の話、電源の話、クロック供給の話に続く、かも......)