FPGAでILI9341搭載2.8インチLCDを制御（前編）

## はじめに

この記事では、**Tang Nano 9K** と **ILI9341搭載2.8インチLCD** を使って、SPI通信を用いてディスプレイとやり取りしながら、
- 図形の描画  
- 制限付きで画像の描画（135×135px / 3色）

に挑戦した内容をまとめています。

すべてのコードはこちらのリポジトリで公開しています。
→ [GitHub - fpga-spi-display](https://github.com/BruCandy/fpga-spi-display)

本プロジェクトでは、SPI通信や描画制御を **Verilog**で記述し、Tang Nano 9K へ **Gowin EDA** を使って実装しています。  
また、画像のリサイズやフォーマット変換には **Python** を使用しました。

> ※ 初めての記事投稿＆ほぼ独学で取り組んだ内容です。不備があればコメントで教えていただけると助かります！

Raspberry PiやArduinoを使って今回のLCDを操作する記事は多く見つかりましたが、FPGA（特にTang Nano 9K）を使った事例は海外を含めてもかなり限られており、日本語で書かれた記事となると、さらに情報が少ないと感じたため、今回記事としてまとめました。

制限付きではありますが、図形や画像の描画に成功した一例として、本記事がこれからFPGAやディスプレイ制御に取り組む方の参考になれば幸いです。

---

## このプロジェクトを始めたきっかけ

現在、情報系の学部3年生で、大学に入ってからパソコンに触れ始めました。  
プログラミングサークルに所属し、先輩方の影響でFPGAに興味を持つようになり、春休みから勉強をスタート。

もともと画像処理系のプロジェクトに取り組んでいたこともあり、  「FPGAで画像を表示してみたい！」という思いから始まりました。

パラレル通信のLCDを探しましたが、入手性の問題から断念。  
秋葉原で入手できる ILI9341（SPI通信対応）搭載の2.8インチLCD を使うことにしました。

---

## 必要なもの
- Tang Nano 9K（秋葉原で購入可）
- ILI9341搭載 2.8インチLCD（SPI通信対応）
- Gowin EDA（設定がすこし面倒）
- ユニバーサル基板や配線類（ブレッドボード、ジャンパワイヤでも可）

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3671248/62af2bc8-2934-445b-9435-0d73875a2ef3.jpeg" width="400">


---

## ハードウェア接続

Tang Nano 9K を マスター、LCDを スレーブ として接続しています。  
今回は表示のみ行うので、MISO（Master In Slave Out）は未接続です。
なお、クロックやリセットボタンのピン設定については、リポジトリ内にある `.cst` ファイルを参照してください。 

| Tang Nano 9K側ピン | LCD側ピン |
|------------------|------------------|
| 3V3              | VCC              |
| GND              | GND              |
| 84               | CS               |
| 83               | RESET            |
| 82               | DC/RS            |
| 81               | SDI (MOSI)       |
| 80               | SCK              |
| 3V3              | LED              |


接続後、以下のように画面に電源が入り、バックライトが点灯します。


<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3671248/cb1e3868-1715-4921-91b7-e501db120a03.jpeg" width="400">


---

## SPI通信について

SPIは以下の4本の信号線を用いたシリアル通信方式です。

- SCK：クロック
- CS：スレーブ選択（LOWで有効）
- MOSI：マスター → スレーブ
- MISO：スレーブ → マスター（今回未使用）

SPIはシリアル通信のため、データは1bitずつ送信されます。  
複数のスレーブを接続する場合、SCK / MOSI / MISO は共通で使い、CS だけを個別に制御することでスレーブを切り替えます。

今回の構成ではスレーブは LCD 1つのみのため、CS は常にその1つだけを対象に制御しています。  

また、今回のLCDは、マスター（Tang Nano 9K）からのコマンドやデータを受信して動作するだけで、応答を返す必要がないため、MISO（Master In Slave Out）は使用しません。
そのため、MOSI・SCK・CS の3本のみで通信が完結しています。

SCK からは常にクロックが流れ、MOSI 経由でコマンドや描画データを 1bitずつ送信しています。

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3671248/3410763a-e618-4bee-b27a-36eb05a9170a.png" width="800">

---

## 実装の流れ

まず、Verilogで1ビットずつデータを送信する SPIモジュールを作成します。  
このモジュールを用いて、コマンドやデータをタイミング通りに送信することにより、ディスプレイに図形や画像を描画できるようになります。

送信するコマンドやデータの種類、タイミングは、以下のドキュメントやコードを参考にしました。

- [ILI9341 データシート（Adafruit版）](https://cdn-shop.adafruit.com/datasheets/ILI9341.pdf)
- [rdagger/micropython-ili9341](https://github.com/rdagger/micropython-ili9341)  
   → `ili9341.py` 内にあるコマンドの種類が参考になります。


---

### ① 初期化処理

LCDを動作させるには、以下のコマンドとデータ（以下の表の値はすべて16進数表記）を順に送ります。

| 内容       | コマンド | データ     | 
|------------|----------|------------|
| SWRESET    | 01       | -          | 
| PWCTR1     | C0       | 00, 23     | 
| PWCTR2     | C1       | 00, 10     | 
| VMCTR1     | C5       | 3E, 28     | 
| VMCTR2     | C7       | 00, 86     | 
| MADCTL     | 36       | 00, 88     | 
| PIXFMT     | 3A       | 00, 55     | 
| SLPOUT     | 11       | -          | 
| DISPLAY_ON | 29       | -          | 



SWRESET、PIXFMT、SLPOUT、DISPLAY_ON は必須コマンドです。
他の設定はデフォルトでも動作すると思いますが、今回は上記の値で安定動作を確認しています。

初期化に成功すると、以下のように画面に反応があるはずです。

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3671248/d9da8c63-979c-4770-b4cc-39a9ba1740a0.jpeg" width="400">


---

### ② 画面クリア（全体を黒で塗りつぶし）

- 画面サイズ：240 × 320
- クリアには `SET_COLUMN`, `SET_PAGE`, `WRITE_RAM` の3ステップで行います。

| 内容         | コマンド |　説明|
|--------------|----------|------|
| SET_COLUMN   | 2A       | X方向の描画範囲の指定|
| SET_PAGE     | 2B       | Y方向の描画範囲の指定|
| WRITE_RAM    | 2C       | 色の指定|



#### 1. `SET_COLUMN`：X座標範囲の指定

- 描画する列（X方向の始点と終点）を指定します。  
- 今回はディスプレイ全体（0〜239）を指定するため、次の4バイトを送信します。
00 00 00 EF // Start X: 0, End X: 239
> 各座標は16bit幅なので、上位8bit・下位8bitの順で送ります。

#### 2. `SET_PAGE`：Y座標範囲の指定

- 描画する行（Y方向）を指定します。  
- 320行あるため、Y方向は 8行単位で処理します。
00 00 00 07 // Start Y: 0, End Y: 7
00 08 00 0F // Start Y: 8, End Y: 15
...
01 38 01 3F // Start Y: 312, End Y: 319
> SET_COLUMNと同じように各座標は16bit幅なので、上位8bit・下位8bitの順で送ります。
> 今回はカウンタを使って8行ずつ順番に処理します。

#### 3. `WRITE_RAM`：色データの送信

- 指定したX×Y範囲にピクセルデータ（色）を送信します。
- 今回はBGR888形式で黒を送信します（例：`0x00, 0x00, 0x00`）。

#### 制限
本来、初期化時に `PIXFMT` コマンドで BGR565 (16bit/pixel) を指定しているため、描画の際にBGR形式の16bitデータ（5bit-6bit-5bit）を送るべきです。

しかし、実際にBGR565で描画しようとしたところ、一部の領域が黒く塗りつぶされないという現象が発生しました。  
試しに `WRITE_RAM` コマンドで送信する色データの量を増やしたところ、黒塗りに成功したため、現在は BGR888 (24bit/pixel) での送信に切り替えています。

今回のクリア処理では単に黒色（`0x00`連続）を送るだけなので問題ありませんが、画像を表示する場合は送信する色のデータが複雑になるため同じようにはいきません。  
このようにBGR565で正しく表示できないことが本プロジェクトにおける画像表示の「制限」となっています。

<!-- TODO: 後でコードの変更
※ BGR565での送信がうまくいかなかったため、BGR888で送信しています。これが本プロジェクトの画像表示の「制限」につながっています。 
-->

#### クリア処理を実行
画面のクリアに成功すると、以下のように画面全体が黒く塗りつぶされます。

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3671248/bf8c9c3c-2437-4326-92ee-73a59d793d08.jpeg" width="400">

---

### ③ 正方形を描画

画面クリアとほぼ同じ要領で、白色の正方形を表示してみました。
送信するコマンドは画面クリアと同じで、データを以下のように変更します。
- `SET_COLUMN`: X = 70〜170
- `SET_PAGE`: Y = 110〜210（カウンタを使って、1行ずつ）
- `WRITE_RAM`: 白 = `0xFF, 0xFF, 0xFF`

実際に動かすと以下のように画面に白い正方形がしっかり表示されます。

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3671248/00b57d70-8991-44a6-a01b-db97ae0fb51d.jpeg" width="400">


---

## 次回予告

ここまでで初期化と単純な図形表示までは実装できました。 

次回は、さらに複雑な図形の描画や、制限付きで画像の描画に挑戦した内容を紹介します。

→ 後編は [こちら](https://qiita.com/BruCandy/items/c24610f12f4f1cba07da) です。

---

## 参考リンク
-  [後編の記事](https://qiita.com/BruCandy/items/c24610f12f4f1cba07da) 
- [ILI9341 データシート（Adafruit版）](https://cdn-shop.adafruit.com/datasheets/ILI9341.pdf)
- [github.com/rdagger/micropython-ili9341](https://github.com/rdagger/micropython-ili9341)
- [ラズベリーパイ Pico にILI9341(MSP2807)を接続する](https://kousaku-prog.com/ili9341msp2807)