RaspberryPi Pico + ILI9341 (3.2インチ) LCD で Hello, World

# はじめに

本書は、以下の記事（M5StampS3 + ILI9341 で Hello, World を表示したもの）を RaspberryPi Pico で試作してみたものです。

https://qiita.com/suzukiplan/items/b5882449fdbf571b4e80

余談ですが、私は自作の同人ハードを作ろうと思っていて、当初開発環境が整備されている ESP32（ESP32-WROOM-32 や M5StampS3 など） を SoC として採用しようと思って検証を進めていたのですが、ESP32 は無線通信機能（WiFi、Bluetooth）が標準実装されている点が __非常に厄介（主に北米向けの展開が難しくなりそう）__ なので、最初から無線機能が付いていない RaspberryPi Pico (無印) を試してみることにしました。

> _無線機能が付いていない ESP32 が出て欲しいところです。（IoT でも WiFi や Bluetooth が不要なシーンは多くあると思うので）_

なお、様々な紆余曲折があり、以下3種類の有名所のディスプレイドライバを使ったそれぞれのサンプルコード（PlatformIO 対応）を示しています。

|[Adafruit ILI9341](#adafruit-ili9341-で-hello-world)|[LovyanGFX](#lovyangfx-で-hello-world)|[TFT_eSPI](#tft_espi-で-hello-world)|
|:-:|:-:|:-:|
|![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2161417/adbdb5bc-6209-bcb3-2fbb-f7cc7c3629cc.png)|![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2161417/70a24c8e-e405-8b33-72ea-d3de387fac52.png)|![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2161417/17af0164-ab06-a54d-d5b0-da07d6926ee8.png)|

# 必要なもの

|品目|単価（税込）|必要数量|入手先|
|:-|-:|-:|:-|
|[RaspberryPi Pico](https://www.marutsu.co.jp/pc/i/2194960/)|¥770|1個|マルツオンライン|
|[ILI9341 "3.2 (Module with Touch)](https://ja.aliexpress.com/item/1005003005413104.html)|約¥1,500|1個|Aliexpress|
|[ピンヘッダ（40pins）](https://www.marutsu.co.jp/pc/i/10041/)|¥107|1個|マルツオンライン|
|[ブレッドボード](https://www.marutsu.co.jp/pc/i/14660/)|¥449|1個|マルツオンライン|
|[ジャンパー線（♂♀）](https://www.marutsu.co.jp/pc/i/69682/)|¥328|2組 (20本)|マルツオンライン|
|[ハンダ一式](https://qiita.com/suzukiplan/items/b5882449fdbf571b4e80#3-%E3%83%9B%E3%83%BC%E3%83%A0%E3%82%BB%E3%83%B3%E3%82%BF%E3%83%BC%E3%81%A7%E8%AA%BF%E9%81%94%E5%88%86%E3%82%AB%E3%82%A4%E3%83%B3%E3%82%BA)|¥3,776|1セット|ホームセンター|

**合計: ¥7,258** _（新規ですべて揃える場合）_

RaspberryPi Pico は標準のものだとピンヘッダがついていないので、ピンヘッダとハンダ一式が必要ですが、ハンダ付けされているものを購入すればピンヘッダとハンダ一式は無くてもとりあえず大丈夫です。

> _私は自分でハンダ付けしました（そっちの方が安いし、ハンダ付けの練習を兼ねて）_

# ビルド環境構築 (PlatformIO Core)

まず、RaspberryPi Pico のプログラムを C/C++ で記述する手段には次の 2 通りがあります。

- Pico SDK を用いる（cmake）
- Arduino Framework を用いる（ArduinoIDE や PlatformIO）

前者の Pico SDK を用いる場合の手順については、以下の解説記事が参考になります。

https://qiita.com/n0bisuke/items/f4ff96bd74932c8def77

ただし、LCD のディスプレイドライバが Arduino Framework を前提にしているので、本書では Pico SDK ではなく Arduino Framework を用いる場合のビルド環境構築手順を示します。

> _そうでなくても、開発環境として Pico SDK よりも Arduino Framework の方が整備されている印象なので、Arduino Framework を用いた方が生産性が高くなりそうです。（完全互換とは言い難いですが、M5Stack や M5Stamp と一定のコード共通化を図ることもできそう）_

なお、RaspberryPi Pico の開発には RaspberryPi の OS を用いるのが一般的（かどうかは不明ですが公式が推奨している）らしいのですが、私は Pico を除くラズパイを持っていないので、開発には普通の PC を用いる前提で記述します。

_PC は macOS を使う前提で記しますが、おそらく Linux や Windows でも概ね同じ手順で使えるものと思われます。_

## (1) Install PlatformIO Core

以下の手順で PlatformIO Core をインストールします。

https://platformio.org/install/cli

PlatformIO Core は PlatformIO のコアプログラム（コマンドライン用）です。

https://github.com/platformio/platformio-core

> _Node.js でいうところの npm のようなものという理解で大丈夫だと思われます。_

一般的に PlatformIO といえば Visual Studio Code を前提とした利用手順で解説されているものが多いですが、私は Node.js などのようにコマンドラインでビルドすることに慣れているので、Core のみ利用する派です。_（IDE は IDEA でも VSC でも各々好きなものを使えば OK というスタンス）_

macOS の場合 HomeBrew でサクッとインストールできます。

```bash
brew install platformio
```

PlatformIO Core をインストールできたら `pio init` で作業用のプロジェクトを作成とリポジトリの初期化をします。

```bash
mkdir /path/to/workdir
cd /path/to/workdir
pio init
git init
git add -A
git commit -m "initial commit (empty project)"
```

## (2) platformio.ini

RaspberryPi Pico 向けの platformio.ini の記述内容については以下で解説されています。

https://docs.platformio.org/en/stable/boards/raspberrypi/pico.html

正規品の RaspberryPi Pico を使う場合、ミニマムでは以下の記述で大丈夫です。

```platformio.ini
[env:pico]
platform = raspberrypi
board = pico
framework = arduino
```

## (3) src/app.cpp (Lチカ)

ビルド → デプロイ → 動作テストの一連の流れを確認するため、本体LEDを点滅させるいわゆる「Lチカ」と呼ばれるプログラムを src/app.cpp に書いてみます。

```src/app.cpp
#include <Arduino.h>

void setup() {
  pinMode(25, OUTPUT);
}

void loop() {
  digitalWrite(25, HIGH);
  delay(1000);
  digitalWrite(25, LOW);
  delay(1000);
}
```

少し解説すると、RaspberryPi Pico (正規品) の本体 LED は GPIO 25 番に割り当てられているので、GPIO 25 の `pinMode` を `OUTPUT` (出力) にして、`digitalWrite` で `HIGH` を書き込むことで点灯、`Low` を書き込むことで消灯します。

## (4) ビルド & ファームウェア更新（アップロード）

RaspberryPi Pico と PC を USB ケーブルで接続してから以下のコマンドを実行すれば、ビルドとファームウェアの更新が実行されます。

```
pio run -t upload
```

> ファームウェアの更新がうまく行かない場合、RaspberryPi Pico 本体の BOOTSEL ボタンを押しながら USB ケーブルへ接続すれば良いようです。

RaspberryPi Pico 本体の LED が 1 秒間隔で点灯と消灯を繰り返す形で動作すれば、ビルド & ファームウェアの更新は成功です。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2161417/0f94bd68-a66c-3236-9740-392777e390e2.png)

# Pico と LCD の配線（ピンアサイン）

以下の記事の「タッチあり」の配線を参考にしました。（※若干、変更点があります）

http://tamanegi.digick.jp/computer-embedded/module/ili9341/

> 参考にした配線図面を上記サイトから参酌:
> ![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2161417/50bab8a2-ba4f-ad00-420f-fb8038349863.png)

:::note info
上記の記事では LED (バックライト) を 3V3 に接続してますが、3V3 は後で DAC を接続する時に使うので、上図で空いている適当な GPIO (GPIO27) に接続するように変更しました。
:::

以下、配線表です。（JP線の色は少し変えてます）

|RaspberryPi Pico|ILI9341 LCD|下図JP線の色|
|:-|:-|:-:|
|`3.3V`|`VCC`|赤 `#FF0000` |
|
`3.3V`
 `GPIO27`|`LED`|灰 `#AAAAAA` |
|`GND`|`GND`|黒 `#000000` |
|`GPIO17 (SPI0 CS)`|`CS`|橙 `#FF7000` |
|`GPIO22`|`RESET`|茶 `#907000` |
|`GPIO28`|`DC`|青 `#0000FF` |
|`GPIO19 (SPI0 TX)`|`SDI (MOSI)`|黄 `#FFFF00` |
|`GPIO18 (SPI0 SCK)`|`SCK`|白 `#FFFFFF` |
|`GPIO20`|`T_CS`|橙 `#FF7000`|
|`GPIO16 (SPI0 MISO)`|`T_DO` (MISO)|緑 `#008000` |
|`GPIO18 (SPI0 SCK)`|`T_CLK`|白 `#FFFFFF` |
|`GPIO19 (SPI0 MOSI)`|`T_DIN` (MOSI)|黄 `#FFFF00` |

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2161417/c6b18278-a02b-1043-253e-05ed8d0333e3.png)

# Adafruit ILI9341 で Hello, World

## (1) platformio.ini 編集

`lib_deps` に `adafruit/Adafruit GFX Library` と `adafruit/Adafruit ILI9341` を追加します。

```platformio.ini
[env:pico]
platform = raspberrypi
board = pico
framework = arduino
lib_deps = adafruit/Adafruit GFX Library, adafruit/Adafruit ILI9341
```

## (2) src/app.cpp 編集

src/app.cpp の内容を[以前の記事（M5StampS3版)](https://qiita.com/suzukiplan/items/b5882449fdbf571b4e80#3-lcd%E5%88%B6%E5%BE%A1%E3%83%97%E3%83%AD%E3%82%B0%E3%83%A9%E3%83%A0%E4%BD%9C%E6%88%90)相当の内容にします。

:::note info
最初の `#define` の GPIO 番号のみ書き換えています。
:::

```src/app.cpp
#include <Adafruit_ILI9341.h>

#define PIN_CS 17
#define PIN_DC 28
#define PIN_RST 22
#define PIN_LED 27
#define PIN_MOSI 19
#define PIN_SCLK 18

static Adafruit_ILI9341 lcd(PIN_CS, PIN_DC, PIN_MOSI, PIN_SCLK, PIN_RST);

void setup()
{
    pinMode(PIN_LED, OUTPUT);
    digitalWrite(PIN_LED, HIGH);
    lcd.begin();
    lcd.startWrite();
    lcd.setRotation(2);
    lcd.writeFillRect(0, 0, lcd.width(), lcd.height(), 0x0007);
    const char* hello = "Hello, World!";
    int16_t x, y;
    uint16_t w, h;
    lcd.setTextSize(3);
    lcd.setTextColor(0xFFFF, 0x0000);
    lcd.getTextBounds(hello, 0, 0, &x, &y, &w, &h);
    lcd.setCursor(lcd.width() / 2 - w / 2, lcd.height() / 2 - h / 2);
    lcd.print(hello);
    lcd.endWrite();
}

void loop() { }
```

## (3) 実行結果

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2161417/adbdb5bc-6209-bcb3-2fbb-f7cc7c3629cc.png)

無事表示できました。

# LovyanGFX で Hello, World

Adafruit ILI9341 だと性能面で実用的ではないため LovyanGFX に置き換えます。

## (1) platformio.ini 編集

```platformio.ini
[env:pico]
platform = https://github.com/maxgerhardt/platform-raspberrypi.git
board = pico
board_build.core = earlephilhower
framework = arduino
lib_deps = lovyan03/LovyanGFX
```

## (2) src/app.cpp 編集

```src/app.cpp
#include <LovyanGFX.hpp>

class ILI9341 : public lgfx::LGFX_Device
{
  private:
    const int pinLed = 27;
    const int pinCs = 17;
    const int pinReset = 22;
    const int pinDc = 28;
    const int pinMosi = 19;
    const int pinSck = 18;
    const int pinMiso = -1;
    const int pinBusy = -1;

    lgfx::Panel_ILI9341 panel;
    lgfx::Bus_SPI bus;

    void initBusConfig()
    {
        auto cfg = this->bus.config();
        cfg.spi_host = 0;
        cfg.spi_mode = 0;
        cfg.freq_write = 40000000;
        cfg.freq_read = 16000000;
        cfg.pin_sclk = this->pinSck;
        cfg.pin_mosi = this->pinMosi;
        cfg.pin_miso = this->pinMiso;
        cfg.pin_dc = this->pinDc;
        this->bus.config(cfg);
    }

    void initPanelConfig()
    {
        auto cfg = this->panel.config();
        cfg.pin_cs = this->pinCs;
        cfg.pin_rst = this->pinReset;
        cfg.pin_busy = -1;
        cfg.panel_width = 240;
        cfg.panel_height = 320;
        cfg.offset_x = 0;
        cfg.offset_y = 0;
        cfg.offset_rotation = 2;
        cfg.dummy_read_pixel = 8;
        cfg.dummy_read_bits = 1;
        cfg.readable = false;
        cfg.invert = false;
        cfg.rgb_order = false;
        cfg.dlen_16bit = false;
        cfg.bus_shared = true;
        this->panel.config(cfg);
    }

  public:
    ILI9341(void)
    {
        this->initBusConfig();
        this->initPanelConfig();
        this->panel.setBus(&this->bus);
        setPanel(&this->panel);
        pinMode(this->pinLed, OUTPUT);
    }

    void led(bool on)
    {
        digitalWrite(this->pinLed, on ? HIGH : LOW);
    }
};

static ILI9341 gfx;

void setup()
{
    // 初期化中は本体LEDを点灯
    pinMode(25, OUTPUT);
    digitalWrite(25, HIGH);

    // ディスプレイを初期化
    gfx.init();
    gfx.led(true);
    gfx.startWrite();
    gfx.setRotation(2);
    gfx.writeFillRect(0, 0, gfx.width(), gfx.height(), 0x003F);
    gfx.setTextSize(2);
    gfx.setTextColor(0xFFFF, 0x0000);
    gfx.setCursor(0, 0);
    gfx.println("Hello, World!");
    gfx.println("Using LovyanGFX.");
    gfx.endWrite();
}

void loop()
{
    delay(1000);
    digitalWrite(25, LOW);
    delay(1000);
    digitalWrite(25, HIGH);
}
```

## (3) 実行結果

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2161417/70a24c8e-e405-8b33-72ea-d3de387fac52.png)

## 教訓

当初、何故か LovyanGFX だと正常に動作せず、迷いに迷って lovyan03/LovyanGFX で issue を切ってしまいました。

https://github.com/lovyan03/LovyanGFX/issues/437

ですが、よくよく調べてみると RP2040 版の LovyanGFX では内部的に `void init(void)` というメソッドを使っていて、自前の派生クラス `ILI9341` でうっかり `init` をオーバーライドしてしまったことで初期化処理が走らなかったという初歩的なバグがあることに気づきました :sweat_smile: 

# TFT_eSPI で Hello, World

LovyanGFX は高性能で素晴らしいです。

ただし、私が作ろうとしている同人ハードウェアの入力インタフェースはタッチパネルなので、タッチパネルに対応する必要があります。

しかしながら、RaspberryPi Pico (RP2040) 版 LovyanGFX は、現時点（2023.08.14時点）では残念ながらタッチパネルをサポートしていないとのことでした。

> [LovyanGFX の README.md](https://github.com/lovyan03/LovyanGFX/blob/master/README.md) より抜粋
> ![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2161417/62a53b39-63b4-5bed-ebb5-7b8021cf3495.png)

タッチパネル対応する必要がある場合、LovyanGFX ではなく TFT_eSPI を使う必要がありそうです。

https://github.com/Bodmer/TFT_eSPI/

## (1) platfromio.ini

TFT_eSPI は、使用するディスプレイドライバの種類やピンの割当などを [User_Setup.h](https://github.com/Bodmer/TFT_eSPI/blob/master/User_Setup.h) で定義するというかなり微妙なインタフェース仕様になってますが、コンパイルオプションで `-DUSER_SETUP_LOADED=1` を定義することで User_Setup.h の include をスキップできる実装になっていたので、それを指定しつつ、必要なすべてのリテラルをコンパイルオプション（`-D`）で指定するのが恐らくベターな使い方かと思われます。_（というか、PlatformIOの場合はそうせざるを得ない筈）_

```platformio.ini
[env:pico]
platform = https://github.com/maxgerhardt/platform-raspberrypi.git
board = pico
board_build.core = earlephilhower
board_build.mcu = rp2040
board_build.f_cpu = 133000000L
framework = arduino
lib_deps = bodmer/TFT_eSPI
build_flags =
    -DUSER_SETUP_LOADED=1
    -DILI9341_DRIVER=1
    -DTFT_BL=27
    -DTFT_BACKLIGHT_ON=HIGH
    -DTFT_CS=17
    -DTFT_RST=22
    -DTFT_DC=28
    -DTFT_MOSI=19
    -DTFT_SCLK=18
    -DTFT_MISO=16
    -DTFT_BL=27
    -DTOUCH_CS=20
    -DSPI_FREQUENCY=40000000
    -DSPI_READ_FREQUENCY=20000000
    -DSPI_TOUCH_FREQUENCY=2500000
    -DLOAD_GLCD=1
    -DLOAD_FONT2=1
    -DLOAD_FONT4=1
    -DLOAD_FONT6=1
    -DLOAD_FONT7=1
    -DLOAD_FONT8=1
    -DLOAD_GFXFF=1
    -DSMOOTH_FONT=1
```

## (2) src/app.cpp 編集

```src/app.cpp
#include <SPI.h>
#include <TFT_eSPI.h>

static TFT_eSPI gfx(240, 320);

void setup()
{
    // 初期化中は本体LEDを点灯
    pinMode(25, OUTPUT);
    digitalWrite(25, HIGH);

    pinMode(TFT_BL, OUTPUT);
    digitalWrite(TFT_BL, HIGH);

    // ディスプレイを初期化
    gfx.init();
    gfx.startWrite();
    gfx.setRotation(2);
    gfx.fillScreen(0x003F);
    gfx.setCursor(0, 0, 2);
    gfx.setTextColor(0xFFFF);
    gfx.setTextSize(2);
    gfx.println("Hello, World!");
    gfx.println("Using TFT_eSPI");
    gfx.endWrite();
}

void loop()
{
    delay(1000);
    digitalWrite(25, LOW);
    delay(1000);
    digitalWrite(25, HIGH);
}
```

## (3) 実行結果

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2161417/17af0164-ab06-a54d-d5b0-da07d6926ee8.png)